Repository: karaoke-dev/karaoke Branch: master Commit: 38296c690f56 Files: 1273 Total size: 3.3 MB Directory structure: gitextract_yy2emyho/ ├── .config/ │ └── dotnet-tools.json ├── .editorconfig ├── .git-blame-ignore-revs ├── .gitattributes ├── .github/ │ ├── CODEOWNERS │ ├── labeler.yml │ └── workflows/ │ ├── ci.yml │ ├── crowdin.yml │ ├── dotnet-format.yml │ ├── labeler.yml │ └── release.yml ├── .gitignore ├── .globalconfig ├── CONTRIBUTING.md ├── CodeAnalysis/ │ ├── BannedSymbols.txt │ └── osu.ruleset ├── Directory.Build.props ├── LICENSE ├── README.md ├── appveyor.yml ├── cake.config ├── osu.Game.Rulesets.Karaoke/ │ ├── Beatmaps/ │ │ ├── ElementId.cs │ │ ├── Formats/ │ │ │ ├── KaraokeJsonBeatmapDecoder.cs │ │ │ ├── KaraokeJsonBeatmapEncoder.cs │ │ │ ├── KaraokeLegacyBeatmapDecoder.cs │ │ │ └── KaraokeLegacyBeatmapEncoder.cs │ │ ├── IHasPrimaryKey.cs │ │ ├── IKaraokeBeatmapResourcesProvider.cs │ │ ├── KaraokeBeatmap.cs │ │ ├── KaraokeBeatmapConverter.cs │ │ ├── KaraokeBeatmapExtension.cs │ │ ├── KaraokeBeatmapProcessor.cs │ │ ├── KaraokeBeatmapResourcesProvider.cs │ │ ├── Metadatas/ │ │ │ ├── NoteInfo.cs │ │ │ ├── Page.cs │ │ │ ├── PageInfo.cs │ │ │ ├── Singer.cs │ │ │ ├── SingerInfo.cs │ │ │ ├── SingerState.cs │ │ │ └── Types/ │ │ │ └── ISinger.cs │ │ └── Utils/ │ │ └── SingerUtils.cs │ ├── Bindables/ │ │ ├── BindableCultureInfo.cs │ │ └── BindableFontUsage.cs │ ├── Configuration/ │ │ ├── KaraokeRulesetConfigManager.cs │ │ ├── KaraokeRulesetEditCheckerConfigManager.cs │ │ ├── KaraokeRulesetEditConfigManager.cs │ │ ├── KaraokeRulesetEditGeneratorConfigManager.cs │ │ ├── KaraokeRulesetLyricEditorConfigManager.cs │ │ └── KaraokeSessionStatics.cs │ ├── Difficulty/ │ │ ├── KaraokeDifficultyAttributes.cs │ │ ├── KaraokeDifficultyCalculator.cs │ │ ├── KaraokePerformanceAttributes.cs │ │ ├── KaraokePerformanceCalculator.cs │ │ ├── Preprocessing/ │ │ │ └── KaraokeDifficultyHitObject.cs │ │ └── Skills/ │ │ └── Strain.cs │ ├── Edit/ │ │ ├── Blueprints/ │ │ │ ├── KaraokeSelectionBlueprint.cs │ │ │ ├── Lyrics/ │ │ │ │ └── LyricSelectionBlueprint.cs │ │ │ └── Notes/ │ │ │ ├── Components/ │ │ │ │ └── EditBodyPiece.cs │ │ │ └── NoteSelectionBlueprint.cs │ │ ├── ChangeHandlers/ │ │ │ ├── BeatmapListPropertyChangeHandler.cs │ │ │ ├── BeatmapPropertyChangeHandler.cs │ │ │ ├── Beatmaps/ │ │ │ │ ├── BeatmapPagesChangeHandler.cs │ │ │ │ ├── BeatmapSingersChangeHandler.cs │ │ │ │ ├── BeatmapTranslationsChangeHandler.cs │ │ │ │ ├── IBeatmapPagesChangeHandler.cs │ │ │ │ ├── IBeatmapSingersChangeHandler.cs │ │ │ │ └── IBeatmapTranslationsChangeHandler.cs │ │ │ ├── ChangeForbiddenException.cs │ │ │ ├── HitObjectChangeHandler.cs │ │ │ ├── HitObjectPropertyChangeHandler.cs │ │ │ ├── HitObjectsChangeHandler.cs │ │ │ ├── IAutoGenerateChangeHandler.cs │ │ │ ├── IHitObjectPropertyChangeHandler.cs │ │ │ ├── IImportBeatmapChangeHandler.cs │ │ │ ├── ILockChangeHandler.cs │ │ │ ├── ImportBeatmapChangeHandler.cs │ │ │ ├── LockChangeHandler.cs │ │ │ ├── Lyrics/ │ │ │ │ ├── ILyricLanguageChangeHandler.cs │ │ │ │ ├── ILyricListPropertyChangeHandler.cs │ │ │ │ ├── ILyricPropertyAutoGenerateChangeHandler.cs │ │ │ │ ├── ILyricPropertyChangeHandler.cs │ │ │ │ ├── ILyricReferenceChangeHandler.cs │ │ │ │ ├── ILyricRubyTagsChangeHandler.cs │ │ │ │ ├── ILyricSingerChangeHandler.cs │ │ │ │ ├── ILyricTextChangeHandler.cs │ │ │ │ ├── ILyricTimeTagsChangeHandler.cs │ │ │ │ ├── ILyricTranslationChangeHandler.cs │ │ │ │ ├── ILyricsChangeHandler.cs │ │ │ │ ├── LyricLanguageChangeHandler.cs │ │ │ │ ├── LyricPropertyAutoGenerateChangeHandler.cs │ │ │ │ ├── LyricPropertyChangeHandler.cs │ │ │ │ ├── LyricReferenceChangeHandler.cs │ │ │ │ ├── LyricRubyTagsChangeHandler.cs │ │ │ │ ├── LyricSingerChangeHandler.cs │ │ │ │ ├── LyricTextChangeHandler.cs │ │ │ │ ├── LyricTimeTagsChangeHandler.cs │ │ │ │ ├── LyricTranslationChangeHandler.cs │ │ │ │ └── LyricsChangeHandler.cs │ │ │ ├── Notes/ │ │ │ │ ├── INotePropertyChangeHandler.cs │ │ │ │ ├── INotesChangeHandler.cs │ │ │ │ ├── NotePropertyChangeHandler.cs │ │ │ │ └── NotesChangeHandler.cs │ │ │ └── Stages/ │ │ │ ├── ClassicStageChangeHandler.cs │ │ │ ├── IClassicStageChangeHandler.cs │ │ │ ├── IStageElementCategoryChangeHandler.cs │ │ │ ├── IStagesChangeHandler.cs │ │ │ ├── StageElementCategoryChangeHandler.cs │ │ │ ├── StagePropertyChangeHandler.cs │ │ │ └── StagesChangeHandler.cs │ │ ├── Checks/ │ │ │ ├── CheckBeatmapAvailableTranslations.cs │ │ │ ├── CheckBeatmapNoteInfo.cs │ │ │ ├── CheckBeatmapPageInfo.cs │ │ │ ├── CheckBeatmapProperty.cs │ │ │ ├── CheckClassicStageInfo.cs │ │ │ ├── CheckHitObjectProperty.cs │ │ │ ├── CheckHitObjectReferenceProperty.cs │ │ │ ├── CheckLyricLanguage.cs │ │ │ ├── CheckLyricReferenceLyric.cs │ │ │ ├── CheckLyricRubyTag.cs │ │ │ ├── CheckLyricSinger.cs │ │ │ ├── CheckLyricText.cs │ │ │ ├── CheckLyricTimeTag.cs │ │ │ ├── CheckLyricTranslations.cs │ │ │ ├── CheckNoteReferenceLyric.cs │ │ │ ├── CheckNoteText.cs │ │ │ ├── CheckNoteTime.cs │ │ │ ├── CheckStageInfo.cs │ │ │ └── Issues/ │ │ │ ├── BeatmapClassicLyricTimingPointIssue.cs │ │ │ ├── BeatmapPageIssue.cs │ │ │ ├── LyricIssue.cs │ │ │ ├── LyricRubyTagIssue.cs │ │ │ ├── LyricTimeTagIssue.cs │ │ │ └── NoteIssue.cs │ │ ├── Components/ │ │ │ ├── ContextMenu/ │ │ │ │ ├── LyricLockContextMenu.cs │ │ │ │ └── SingerContextMenu.cs │ │ │ ├── Cursor/ │ │ │ │ └── TimeTagTooltip.cs │ │ │ ├── Menus/ │ │ │ │ ├── KaraokeEditorMenu.cs │ │ │ │ └── KaraokeSkinEditorMenu.cs │ │ │ └── Sprites/ │ │ │ ├── DrawableTextIndex.cs │ │ │ └── DrawableTimeTag.cs │ │ ├── Debugging/ │ │ │ └── DebugBeatmapManager.cs │ │ ├── DrawableKaraokeEditorRuleset.cs │ │ ├── EditorNotePlayfield.cs │ │ ├── Export/ │ │ │ └── ExportLyricManager.cs │ │ ├── Generator/ │ │ │ ├── Beatmaps/ │ │ │ │ ├── BeatmapPropertyDetector.cs │ │ │ │ ├── BeatmapPropertyGenerator.cs │ │ │ │ └── Pages/ │ │ │ │ ├── PageGenerator.cs │ │ │ │ └── PageGeneratorConfig.cs │ │ │ ├── ConfigCategoryAttribute.cs │ │ │ ├── ConfigSourceAttribute.cs │ │ │ ├── DetectorNotSupportedException.cs │ │ │ ├── GeneratorConfig.cs │ │ │ ├── GeneratorConfigExtension.cs │ │ │ ├── GeneratorNotSupportedException.cs │ │ │ ├── GeneratorSelector.cs │ │ │ ├── Lyrics/ │ │ │ │ ├── Language/ │ │ │ │ │ ├── LanguageDetector.cs │ │ │ │ │ └── LanguageDetectorConfig.cs │ │ │ │ ├── LyricGeneratorSelector.cs │ │ │ │ ├── LyricPropertyDetector.cs │ │ │ │ ├── LyricPropertyGenerator.cs │ │ │ │ ├── Notes/ │ │ │ │ │ ├── NoteGenerator.cs │ │ │ │ │ └── NoteGeneratorConfig.cs │ │ │ │ ├── ReferenceLyric/ │ │ │ │ │ ├── ReferenceLyricDetector.cs │ │ │ │ │ └── ReferenceLyricDetectorConfig.cs │ │ │ │ ├── Romanisation/ │ │ │ │ │ ├── Ja/ │ │ │ │ │ │ ├── JaRomanisationGenerator.cs │ │ │ │ │ │ └── JaRomanisationGeneratorConfig.cs │ │ │ │ │ ├── RomanisationGenerateResult.cs │ │ │ │ │ ├── RomanisationGenerator.cs │ │ │ │ │ ├── RomanisationGeneratorConfig.cs │ │ │ │ │ └── RomanisationGeneratorSelector.cs │ │ │ │ ├── RubyTags/ │ │ │ │ │ ├── Ja/ │ │ │ │ │ │ ├── JaRubyTagGenerator.cs │ │ │ │ │ │ └── JaRubyTagGeneratorConfig.cs │ │ │ │ │ ├── RubyTagGenerator.cs │ │ │ │ │ ├── RubyTagGeneratorConfig.cs │ │ │ │ │ └── RubyTagGeneratorSelector.cs │ │ │ │ └── TimeTags/ │ │ │ │ ├── Ja/ │ │ │ │ │ ├── JaTimeTagGenerator.cs │ │ │ │ │ └── JaTimeTagGeneratorConfig.cs │ │ │ │ ├── TimeTagGenerator.cs │ │ │ │ ├── TimeTagGeneratorConfig.cs │ │ │ │ ├── TimeTagGeneratorSelector.cs │ │ │ │ └── Zh/ │ │ │ │ ├── ZhTimeTagGenerator.cs │ │ │ │ └── ZhTimeTagGeneratorConfig.cs │ │ │ ├── PropertyDetector.cs │ │ │ ├── PropertyGenerator.cs │ │ │ └── Stages/ │ │ │ ├── Classic/ │ │ │ │ ├── ClassicLyricLayoutCategoryGenerator.cs │ │ │ │ ├── ClassicLyricLayoutCategoryGeneratorConfig.cs │ │ │ │ ├── ClassicLyricTimingInfoGenerator.cs │ │ │ │ ├── ClassicLyricTimingInfoGeneratorConfig.cs │ │ │ │ ├── ClassicStageInfoGenerator.cs │ │ │ │ └── ClassicStageInfoGeneratorConfig.cs │ │ │ ├── Preview/ │ │ │ │ ├── PreviewStageInfoGenerator.cs │ │ │ │ └── PreviewStageInfoGeneratorConfig.cs │ │ │ ├── StageInfoGenerator.cs │ │ │ ├── StageInfoGeneratorConfig.cs │ │ │ ├── StageInfoGeneratorSelector.cs │ │ │ └── StageInfoPropertyGenerator.cs │ │ ├── KaraokeBeatmapVerifier.cs │ │ ├── KaraokeBlueprintContainer.cs │ │ ├── KaraokeEditorPlayfield.cs │ │ ├── KaraokeHitObjectComposer.cs │ │ ├── KaraokeSelectionHandler.cs │ │ ├── Setup/ │ │ │ ├── Components/ │ │ │ │ ├── FormLanguageList.cs │ │ │ │ └── FormSingerList.cs │ │ │ ├── KaraokeNoteSection.cs │ │ │ ├── KaraokeSingerSection.cs │ │ │ └── KaraokeTranslationSection.cs │ │ └── Utils/ │ │ ├── EditorBeatmapUtils.cs │ │ ├── HitObjectWritableUtils.cs │ │ ├── LockStateUtils.cs │ │ ├── ValueChangedEventUtils.cs │ │ └── ZoomableScrollContainerUtils.cs │ ├── Extensions/ │ │ ├── EnumerableExtensions.cs │ │ ├── OsuGameExtensions.cs │ │ ├── RegexExtensions.cs │ │ ├── ScrollContainerExtensions.cs │ │ ├── TrickyCompositeDrawableExtension.cs │ │ └── TypeExtensions.cs │ ├── Flags/ │ │ └── FlagState.cs │ ├── Graphics/ │ │ ├── Containers/ │ │ │ ├── OrderRearrangeableListContainer.cs │ │ │ └── RearrangeableTextFlowListContainer.cs │ │ ├── Cursor/ │ │ │ ├── BackgroundToolTip.cs │ │ │ ├── LyricToolTip.cs │ │ │ └── SingerToolTip.cs │ │ ├── Drawables/ │ │ │ ├── DrawableCircleSingerAvatar.cs │ │ │ ├── DrawableSingerAvatar.cs │ │ │ └── SingerDisplay.cs │ │ ├── KaraokeIcon.cs │ │ ├── Shapes/ │ │ │ ├── CornerBackground.cs │ │ │ └── RightTriangle.cs │ │ ├── Sprites/ │ │ │ ├── DisplayLyricProcessor.cs │ │ │ ├── DrawableKaraokeSpriteText.cs │ │ │ ├── DrawableLyricSpriteText.cs │ │ │ ├── LyricDisplayProperty.cs │ │ │ ├── LyricDisplayType.cs │ │ │ ├── LyricStyle.cs │ │ │ └── Processor/ │ │ │ ├── BaseDisplayProcessor.cs │ │ │ ├── LyricFirstDisplayProcessor.cs │ │ │ └── RomanisedSyllableFirstDisplayProcessor.cs │ │ ├── UserInterface/ │ │ │ ├── BindableBoolMenuItem.cs │ │ │ └── BindableEnumMenuItem.cs │ │ └── UserInterfaceV2/ │ │ ├── FontSelector.cs │ │ ├── LabelledColourSelector.cs │ │ ├── LabelledHueSelector.cs │ │ ├── LabelledImageSelector.cs │ │ ├── LabelledRealTimeSliderBar.cs │ │ ├── LanguageSelector.cs │ │ └── LanguageSelectorPopover.cs │ ├── IO/ │ │ ├── Archives/ │ │ │ └── CachedFontArchiveReader.cs │ │ ├── Serialization/ │ │ │ ├── Converters/ │ │ │ │ ├── ColourConverter.cs │ │ │ │ ├── CultureInfoConverter.cs │ │ │ │ ├── DictionaryConverter.cs │ │ │ │ ├── ElementIdConverter.cs │ │ │ │ ├── FontUsageConverter.cs │ │ │ │ ├── GenericTypeConverter.cs │ │ │ │ ├── KaraokeSkinElementConverter.cs │ │ │ │ ├── LyricConverter.cs │ │ │ │ ├── ReferenceLyricPropertyConfigConverter.cs │ │ │ │ ├── RubyTagConverter.cs │ │ │ │ ├── RubyTagsConverter.cs │ │ │ │ ├── ShaderConverter.cs │ │ │ │ ├── SortableJsonConverter.cs │ │ │ │ ├── StageInfoConverter.cs │ │ │ │ ├── TimeTagConverter.cs │ │ │ │ ├── TimeTagsConverter.cs │ │ │ │ ├── ToneConverter.cs │ │ │ │ └── TranslationConverter.cs │ │ │ ├── KaraokeJsonSerializableExtensions.cs │ │ │ ├── SkinJsonSerializableExtensions.cs │ │ │ └── WritablePropertiesOnlyResolver.cs │ │ └── Stores/ │ │ ├── FntGlyphStore.cs │ │ ├── KaraokeLocalFontStore.cs │ │ └── TtfGlyphStore.cs │ ├── Integration/ │ │ └── Formats/ │ │ ├── IDecoder.cs │ │ ├── IEncoder.cs │ │ ├── KarDecoder.cs │ │ ├── KarEncoder.cs │ │ ├── LrcDecoder.cs │ │ ├── LrcEncoder.cs │ │ ├── LrcParserUtils.cs │ │ ├── LyricTextDecoder.cs │ │ └── LyricTextEncoder.cs │ ├── Judgements/ │ │ ├── KaraokeJudgement.cs │ │ ├── KaraokeJudgementResult.cs │ │ ├── KaraokeLyricJudgement.cs │ │ └── KaraokeNoteJudgement.cs │ ├── KaraokeControlInputManager.cs │ ├── KaraokeEditInputManager.cs │ ├── KaraokeInputManager.cs │ ├── KaraokeRuleset.cs │ ├── KaraokeSkinComponentLookup.cs │ ├── KaraokeSkinComponents.cs │ ├── Localisation/ │ │ ├── ChangelogStrings.cs │ │ ├── CommonStrings.cs │ │ └── KaraokeSettingsSubsectionStrings.cs │ ├── Mods/ │ │ ├── IApplicableToMicrophone.cs │ │ ├── IApplicableToSettingHUDOverlay.cs │ │ ├── IApplicableToStage.cs │ │ ├── IApplicableToStageElement.cs │ │ ├── IApplicableToStageHitObjectCommand.cs │ │ ├── IApplicableToStageInfo.cs │ │ ├── IApplicableToStagePlayfieldCommand.cs │ │ ├── KaraokeModAutoplay.cs │ │ ├── KaraokeModAutoplayBySinger.cs │ │ ├── KaraokeModClassicStage.cs │ │ ├── KaraokeModDisableNote.cs │ │ ├── KaraokeModFlashlight.cs │ │ ├── KaraokeModHiddenNote.cs │ │ ├── KaraokeModLyricConfiguration.cs │ │ ├── KaraokeModNoFail.cs │ │ ├── KaraokeModPerfect.cs │ │ ├── KaraokeModPractice.cs │ │ ├── KaraokeModPreviewStage.cs │ │ ├── KaraokeModSnow.cs │ │ ├── KaraokeModSuddenDeath.cs │ │ ├── KaraokeModTranslation.cs │ │ ├── KaraokeModWindowsUpdate.cs │ │ ├── LanguageSettingsControl.cs │ │ └── ModStage.cs │ ├── Objects/ │ │ ├── BarLine.cs │ │ ├── Drawables/ │ │ │ ├── DrawableBarLine.cs │ │ │ ├── DrawableKaraokeHitObject.cs │ │ │ ├── DrawableKaraokeScrollingHitObject.cs │ │ │ ├── DrawableLyric.cs │ │ │ └── DrawableNote.cs │ │ ├── KaraokeHitObject.cs │ │ ├── LegacyProperties.cs │ │ ├── Lyric.cs │ │ ├── Lyric_Binding.cs │ │ ├── Lyric_Working.cs │ │ ├── Note.cs │ │ ├── Note_Binding.cs │ │ ├── Note_Working.cs │ │ ├── Properties/ │ │ │ ├── IReferenceLyricPropertyConfig.cs │ │ │ ├── ReferenceLyricConfig.cs │ │ │ └── SyncLyricConfig.cs │ │ ├── RubyTag.cs │ │ ├── TimeTag.cs │ │ ├── Title.cs │ │ ├── TitlePart.cs │ │ ├── Tone.cs │ │ ├── Types/ │ │ │ ├── IHasLock.cs │ │ │ ├── IHasOrder.cs │ │ │ ├── IHasPage.cs │ │ │ ├── IHasSingers.cs │ │ │ ├── IHasText.cs │ │ │ └── IHasWorkingProperty.cs │ │ ├── Utils/ │ │ │ ├── LyricUtils.cs │ │ │ ├── LyricsUtils.cs │ │ │ ├── NoteUtils.cs │ │ │ ├── NotesUtils.cs │ │ │ ├── OrderUtils.cs │ │ │ ├── RubyTagUtils.cs │ │ │ ├── RubyTagsUtils.cs │ │ │ ├── TimeTagUtils.cs │ │ │ └── TimeTagsUtils.cs │ │ └── Workings/ │ │ ├── HitObjectWorkingPropertyValidator.cs │ │ ├── InvalidWorkingPropertyAssignException.cs │ │ ├── LyricWorkingProperty.cs │ │ ├── LyricWorkingPropertyValidator.cs │ │ ├── NoteWorkingProperty.cs │ │ └── NoteWorkingPropertyValidator.cs │ ├── Online/ │ │ └── API/ │ │ └── Requests/ │ │ ├── ChangelogRequestUtils.cs │ │ ├── GetChangelogBuildRequest.cs │ │ ├── GetChangelogRequest.cs │ │ ├── GithubAPIRequest.cs │ │ └── Responses/ │ │ ├── APIChangelogBuild.cs │ │ └── APIChangelogIndex.cs │ ├── Overlays/ │ │ ├── Changelog/ │ │ │ ├── ChangeLogMarkdownContainer.cs │ │ │ ├── ChangelogBadgeInfo.cs │ │ │ ├── ChangelogBuild.cs │ │ │ ├── ChangelogContent.cs │ │ │ ├── ChangelogHeader.cs │ │ │ ├── ChangelogListing.cs │ │ │ ├── ChangelogPullRequestInfo.cs │ │ │ ├── ChangelogSingleBuild.cs │ │ │ └── Sidebar/ │ │ │ ├── ChangelogSection.cs │ │ │ ├── ChangelogSidebar.cs │ │ │ └── YearsPanel.cs │ │ ├── Dialog/ │ │ │ └── OkPopupDialog.cs │ │ └── KaraokeChangelogOverlay.cs │ ├── Replays/ │ │ ├── KaraokeAutoGenerator.cs │ │ ├── KaraokeAutoGeneratorBySinger.cs │ │ ├── KaraokeFramedReplayInputHandler.cs │ │ └── KaraokeReplayFrame.cs │ ├── Resources/ │ │ └── Skin/ │ │ └── Default/ │ │ ├── default.json │ │ ├── lyric-font-infos.json │ │ └── note-styles.json │ ├── Scoring/ │ │ ├── KaraokeHitWindows.cs │ │ ├── KaraokeLyricHitWindows.cs │ │ ├── KaraokeNoteHitWindows.cs │ │ └── KaraokeScoreProcessor.cs │ ├── Screens/ │ │ ├── Edit/ │ │ │ ├── AutoGenerateSection.cs │ │ │ ├── AutoGenerateSubsection.cs │ │ │ ├── Beatmaps/ │ │ │ │ ├── BeatmapEditorRoundedScreen.cs │ │ │ │ ├── BeatmapEditorScreen.cs │ │ │ │ ├── Components/ │ │ │ │ │ ├── Menus/ │ │ │ │ │ │ ├── AutoFocusToEditLyricMenu.cs │ │ │ │ │ │ ├── GeneratorConfigMenu.cs │ │ │ │ │ │ ├── ImportLyricMenu.cs │ │ │ │ │ │ ├── LockStateMenu.cs │ │ │ │ │ │ ├── LyricEditorModeMenu.cs │ │ │ │ │ │ ├── LyricEditorPreferLayoutMenu.cs │ │ │ │ │ │ └── LyricEditorTextSizeMenu.cs │ │ │ │ │ └── UserInterfaceV2/ │ │ │ │ │ └── LyricSelector.cs │ │ │ │ ├── ILyricsProvider.cs │ │ │ │ ├── KaraokeBeatmapEditor.cs │ │ │ │ ├── KaraokeBeatmapEditorScreenMode.cs │ │ │ │ ├── Lyrics/ │ │ │ │ │ ├── BindableBlueprintContainer.cs │ │ │ │ │ ├── CaretPosition/ │ │ │ │ │ │ ├── Algorithms/ │ │ │ │ │ │ │ ├── CaretPositionAlgorithm.cs │ │ │ │ │ │ │ ├── CharGapCaretPositionAlgorithm.cs │ │ │ │ │ │ │ ├── CharIndexCaretPositionAlgorithm.cs │ │ │ │ │ │ │ ├── ClickingCaretPositionAlgorithm.cs │ │ │ │ │ │ │ ├── CreateRemoveTimeTagCaretPositionAlgorithm.cs │ │ │ │ │ │ │ ├── CreateRubyTagCaretPositionAlgorithm.cs │ │ │ │ │ │ │ ├── CuttingCaretPositionAlgorithm.cs │ │ │ │ │ │ │ ├── ICaretPositionAlgorithm.cs │ │ │ │ │ │ │ ├── IIndexCaretPositionAlgorithm.cs │ │ │ │ │ │ │ ├── IndexCaretPositionAlgorithm.cs │ │ │ │ │ │ │ ├── NavigateCaretPositionAlgorithm.cs │ │ │ │ │ │ │ ├── RecordingTimeTagCaretMoveMode.cs │ │ │ │ │ │ │ ├── RecordingTimeTagCaretPositionAlgorithm.cs │ │ │ │ │ │ │ └── TypingCaretPositionAlgorithm.cs │ │ │ │ │ │ ├── ClickingCaretPosition.cs │ │ │ │ │ │ ├── CreateRemoveTimeTagCaretPosition.cs │ │ │ │ │ │ ├── CreateRubyTagCaretPosition.cs │ │ │ │ │ │ ├── CuttingCaretPosition.cs │ │ │ │ │ │ ├── ICaretPosition.cs │ │ │ │ │ │ ├── ICharGapCaretPosition.cs │ │ │ │ │ │ ├── ICharIndexCaretPosition.cs │ │ │ │ │ │ ├── IIndexCaretPosition.cs │ │ │ │ │ │ ├── NavigateCaretPosition.cs │ │ │ │ │ │ ├── RecordingTimeTagCaretPosition.cs │ │ │ │ │ │ └── TypingCaretPosition.cs │ │ │ │ │ ├── ClipboardToast.cs │ │ │ │ │ ├── Content/ │ │ │ │ │ │ ├── ApplySelectingArea.cs │ │ │ │ │ │ ├── CircleCheckbox.cs │ │ │ │ │ │ ├── Components/ │ │ │ │ │ │ │ ├── Badges/ │ │ │ │ │ │ │ │ ├── Badge.cs │ │ │ │ │ │ │ │ ├── LanguageBadge.cs │ │ │ │ │ │ │ │ ├── ReferenceLyricBadge.cs │ │ │ │ │ │ │ │ ├── SingerBadge.cs │ │ │ │ │ │ │ │ └── TimeTagBadge.cs │ │ │ │ │ │ │ ├── FixedInfo/ │ │ │ │ │ │ │ │ ├── InvalidInfo.cs │ │ │ │ │ │ │ │ ├── LockInfo.cs │ │ │ │ │ │ │ │ └── OrderInfo.cs │ │ │ │ │ │ │ └── Lyrics/ │ │ │ │ │ │ │ ├── BlueprintLayer.cs │ │ │ │ │ │ │ ├── Blueprints/ │ │ │ │ │ │ │ │ ├── LyricPropertyBlueprintContainer.cs │ │ │ │ │ │ │ │ ├── RubyBlueprintContainer.cs │ │ │ │ │ │ │ │ ├── RubyTagSelectionBlueprint.cs │ │ │ │ │ │ │ │ ├── TimeTagBlueprintContainer.cs │ │ │ │ │ │ │ │ └── TimeTagSelectionBlueprint.cs │ │ │ │ │ │ │ ├── CaretLayer.cs │ │ │ │ │ │ │ ├── Carets/ │ │ │ │ │ │ │ │ ├── DrawableCaret.cs │ │ │ │ │ │ │ │ ├── DrawableCaretState.cs │ │ │ │ │ │ │ │ ├── DrawableCreateRemoveTimeTagCaret.cs │ │ │ │ │ │ │ │ ├── DrawableCreateRubyTagCaret.cs │ │ │ │ │ │ │ │ ├── DrawableCuttingCaret.cs │ │ │ │ │ │ │ │ ├── DrawableRangeCaret.cs │ │ │ │ │ │ │ │ ├── DrawableRecordingTimeTagCaret.cs │ │ │ │ │ │ │ │ ├── DrawableTypingCaret.cs │ │ │ │ │ │ │ │ └── ICanAcceptRangeIndex.cs │ │ │ │ │ │ │ ├── EditLyricLayer.cs │ │ │ │ │ │ │ ├── GridLayer.cs │ │ │ │ │ │ │ ├── IInteractableLyricState.cs │ │ │ │ │ │ │ ├── IPreviewLyricPositionProvider.cs │ │ │ │ │ │ │ ├── InteractLyricLayer.cs │ │ │ │ │ │ │ ├── InteractableLyric.cs │ │ │ │ │ │ │ ├── Layer.cs │ │ │ │ │ │ │ ├── LayerLoader.cs │ │ │ │ │ │ │ ├── LyricLayer.cs │ │ │ │ │ │ │ ├── PreviewKaraokeSpriteText.cs │ │ │ │ │ │ │ ├── TimeTagLayer.cs │ │ │ │ │ │ │ └── UIEventLayer.cs │ │ │ │ │ │ ├── Compose/ │ │ │ │ │ │ │ ├── BottomEditor/ │ │ │ │ │ │ │ │ ├── AdjustTimeTagBottomEditor.cs │ │ │ │ │ │ │ │ ├── AdjustTimeTags/ │ │ │ │ │ │ │ │ │ ├── AdjustTimeTagBlueprintContainer.cs │ │ │ │ │ │ │ │ │ ├── AdjustTimeTagScrollContainer.cs │ │ │ │ │ │ │ │ │ ├── AdjustTimeTagSelectionBlueprint.cs │ │ │ │ │ │ │ │ │ ├── CurrentTimeMarker.cs │ │ │ │ │ │ │ │ │ └── TimeTagOrderedSelectionContainer.cs │ │ │ │ │ │ │ │ ├── BaseBottomEditor.cs │ │ │ │ │ │ │ │ ├── NoteBottomEditor.cs │ │ │ │ │ │ │ │ ├── Notes/ │ │ │ │ │ │ │ │ │ ├── NoteEditPopover.cs │ │ │ │ │ │ │ │ │ ├── NoteEditor.cs │ │ │ │ │ │ │ │ │ ├── NoteEditorBlueprintContainer.cs │ │ │ │ │ │ │ │ │ └── NoteEditorSelectionBlueprint.cs │ │ │ │ │ │ │ │ ├── RecordingTimeTagBottomEditor.cs │ │ │ │ │ │ │ │ ├── RecordingTimeTags/ │ │ │ │ │ │ │ │ │ ├── CentreMarker.cs │ │ │ │ │ │ │ │ │ ├── RecordingTimeTagPart.cs │ │ │ │ │ │ │ │ │ ├── RecordingTimeTagScrollContainer.cs │ │ │ │ │ │ │ │ │ └── TimeTagsVisualisation.cs │ │ │ │ │ │ │ │ └── TimeTagScrollContainer.cs │ │ │ │ │ │ │ ├── ComposeContent.cs │ │ │ │ │ │ │ ├── CreateNewLyricDetailRow.cs │ │ │ │ │ │ │ ├── DetailLyricList.cs │ │ │ │ │ │ │ ├── DetailRow.cs │ │ │ │ │ │ │ ├── EditLyricDetailRow.cs │ │ │ │ │ │ │ ├── LyricComposer.cs │ │ │ │ │ │ │ ├── LyricEditor.cs │ │ │ │ │ │ │ ├── Panel.cs │ │ │ │ │ │ │ ├── PanelDirection.cs │ │ │ │ │ │ │ ├── Panels/ │ │ │ │ │ │ │ │ ├── InvalidPanel.cs │ │ │ │ │ │ │ │ ├── IssueSection.cs │ │ │ │ │ │ │ │ ├── PanelSection.cs │ │ │ │ │ │ │ │ └── PropertyPanel.cs │ │ │ │ │ │ │ ├── SpecialActionToolbar.cs │ │ │ │ │ │ │ └── Toolbar/ │ │ │ │ │ │ │ ├── Carets/ │ │ │ │ │ │ │ │ ├── MoveToCaretPositionButton.cs │ │ │ │ │ │ │ │ ├── MoveToFirstIndexButton.cs │ │ │ │ │ │ │ │ ├── MoveToLastIndexButton.cs │ │ │ │ │ │ │ │ ├── MoveToNextIndexButton.cs │ │ │ │ │ │ │ │ ├── MoveToNextLyricButton.cs │ │ │ │ │ │ │ │ ├── MoveToPreviousIndexButton.cs │ │ │ │ │ │ │ │ └── MoveToPreviousLyricButton.cs │ │ │ │ │ │ │ ├── Panels/ │ │ │ │ │ │ │ │ ├── ToggleInvalidInfoPanelButton.cs │ │ │ │ │ │ │ │ └── TogglePropertyPanelButton.cs │ │ │ │ │ │ │ ├── Playback/ │ │ │ │ │ │ │ │ └── PlaybackSwitchButton.cs │ │ │ │ │ │ │ ├── Separator.cs │ │ │ │ │ │ │ ├── ToolbarButton.cs │ │ │ │ │ │ │ ├── ToolbarEditActionButton.cs │ │ │ │ │ │ │ ├── ToolbarToggleButton.cs │ │ │ │ │ │ │ └── View/ │ │ │ │ │ │ │ └── AdjustFontSizeButton.cs │ │ │ │ │ │ ├── ContentWrapper.cs │ │ │ │ │ │ ├── List/ │ │ │ │ │ │ │ ├── CreateNewLyricPreviewRow.cs │ │ │ │ │ │ │ ├── EditLyricPreviewRow.cs │ │ │ │ │ │ │ ├── InfoControl.cs │ │ │ │ │ │ │ ├── ListContent.cs │ │ │ │ │ │ │ ├── PreviewLyricList.cs │ │ │ │ │ │ │ └── PreviewRow.cs │ │ │ │ │ │ ├── LyricList.cs │ │ │ │ │ │ ├── MainContent.cs │ │ │ │ │ │ └── Row.cs │ │ │ │ │ ├── DeleteLyricDialog.cs │ │ │ │ │ ├── IIssueNavigator.cs │ │ │ │ │ ├── ILyricEditorClipboard.cs │ │ │ │ │ ├── ILyricEditorState.cs │ │ │ │ │ ├── ILyricEditorVerifier.cs │ │ │ │ │ ├── IssueNavigator.cs │ │ │ │ │ ├── LyricEditor.cs │ │ │ │ │ ├── LyricEditorClipboard.cs │ │ │ │ │ ├── LyricEditorColourProvider.cs │ │ │ │ │ ├── LyricEditorIssueTable.cs │ │ │ │ │ ├── LyricEditorLayout.cs │ │ │ │ │ ├── LyricEditorMode.cs │ │ │ │ │ ├── LyricEditorScreen.cs │ │ │ │ │ ├── LyricEditorSkin.cs │ │ │ │ │ ├── LyricEditorVerifier.cs │ │ │ │ │ ├── OsuColourExtensions.cs │ │ │ │ │ ├── Settings/ │ │ │ │ │ │ ├── Components/ │ │ │ │ │ │ │ ├── BlockSectionWrapper.cs │ │ │ │ │ │ │ └── Markdown/ │ │ │ │ │ │ │ ├── LyricEditorDescriptionTextFlowContainer.cs │ │ │ │ │ │ │ ├── SwitchModeDescriptionAction.cs │ │ │ │ │ │ │ └── SwitchMoteText.cs │ │ │ │ │ │ ├── LabelledObjectFieldSwitchButton.cs │ │ │ │ │ │ ├── LabelledObjectFieldTextBox.cs │ │ │ │ │ │ ├── Language/ │ │ │ │ │ │ │ ├── AssignLanguageSubsection.cs │ │ │ │ │ │ │ ├── LanguageAutoGenerateSubsection.cs │ │ │ │ │ │ │ ├── LanguageEditModeSpecialAction.cs │ │ │ │ │ │ │ ├── LanguageIssueSection.cs │ │ │ │ │ │ │ ├── LanguageSettingsHeader.cs │ │ │ │ │ │ │ └── LanguageSwitchSpecialActionSection.cs │ │ │ │ │ │ ├── LanguageSettings.cs │ │ │ │ │ │ ├── LyricEditorAutoGenerateSubsection.cs │ │ │ │ │ │ ├── LyricEditorIssueSection.cs │ │ │ │ │ │ ├── LyricEditorSettings.cs │ │ │ │ │ │ ├── LyricEditorSettingsHeader.cs │ │ │ │ │ │ ├── LyricPropertiesSection.cs │ │ │ │ │ │ ├── LyricPropertySection.cs │ │ │ │ │ │ ├── NoteSettings.cs │ │ │ │ │ │ ├── Notes/ │ │ │ │ │ │ │ ├── NoteAutoGenerateSubsection.cs │ │ │ │ │ │ │ ├── NoteClearSubsection.cs │ │ │ │ │ │ │ ├── NoteConfigSection.cs │ │ │ │ │ │ │ ├── NoteEditModeSpecialAction.cs │ │ │ │ │ │ │ ├── NoteEditPropertyMode.cs │ │ │ │ │ │ │ ├── NoteEditPropertyModeSection.cs │ │ │ │ │ │ │ ├── NoteEditPropertySection.cs │ │ │ │ │ │ │ ├── NoteIssueSection.cs │ │ │ │ │ │ │ ├── NoteSettingsHeader.cs │ │ │ │ │ │ │ └── NoteSwitchSpecialActionSection.cs │ │ │ │ │ │ ├── Reference/ │ │ │ │ │ │ │ ├── LabelledReferenceLyricSelector.cs │ │ │ │ │ │ │ ├── ReferenceLyricAutoGenerateSection.cs │ │ │ │ │ │ │ ├── ReferenceLyricConfigSection.cs │ │ │ │ │ │ │ ├── ReferenceLyricIssueSection.cs │ │ │ │ │ │ │ ├── ReferenceLyricSection.cs │ │ │ │ │ │ │ └── ReferenceLyricSettingsHeader.cs │ │ │ │ │ │ ├── ReferenceSettings.cs │ │ │ │ │ │ ├── Romanisation/ │ │ │ │ │ │ │ ├── Components/ │ │ │ │ │ │ │ │ └── LabelledRomanisedTextBox.cs │ │ │ │ │ │ │ ├── RomanisationAutoGenerateSection.cs │ │ │ │ │ │ │ ├── RomanisationEditSection.cs │ │ │ │ │ │ │ ├── RomanisationIssueSection.cs │ │ │ │ │ │ │ └── RomanisationSettingsHeader.cs │ │ │ │ │ │ ├── RomanisationSettings.cs │ │ │ │ │ │ ├── Ruby/ │ │ │ │ │ │ │ ├── Components/ │ │ │ │ │ │ │ │ └── LabelledRubyTagTextBox.cs │ │ │ │ │ │ │ ├── RubyTagAutoGenerateSection.cs │ │ │ │ │ │ │ ├── RubyTagConfigToolSection.cs │ │ │ │ │ │ │ ├── RubyTagEditModeSubsection.cs │ │ │ │ │ │ │ ├── RubyTagEditSection.cs │ │ │ │ │ │ │ ├── RubyTagIssueSection.cs │ │ │ │ │ │ │ └── RubyTagSettingsHeader.cs │ │ │ │ │ │ ├── RubyTagSettings.cs │ │ │ │ │ │ ├── SelectLyricButton.cs │ │ │ │ │ │ ├── SettingsDirection.cs │ │ │ │ │ │ ├── SingerSettings.cs │ │ │ │ │ │ ├── Singers/ │ │ │ │ │ │ │ └── SingerEditSection.cs │ │ │ │ │ │ ├── SpecialActionSection.cs │ │ │ │ │ │ ├── SwitchSubsection.cs │ │ │ │ │ │ ├── Text/ │ │ │ │ │ │ │ ├── TextDeleteSubsection.cs │ │ │ │ │ │ │ ├── TextEditModeSpecialAction.cs │ │ │ │ │ │ │ ├── TextIssueSection.cs │ │ │ │ │ │ │ ├── TextSettingsHeader.cs │ │ │ │ │ │ │ └── TextSwitchSpecialActionSection.cs │ │ │ │ │ │ ├── TextSettings.cs │ │ │ │ │ │ ├── TimeTagSettings.cs │ │ │ │ │ │ └── TimeTags/ │ │ │ │ │ │ ├── Components/ │ │ │ │ │ │ │ └── LabelledOpacityAdjustment.cs │ │ │ │ │ │ ├── CreateTimeTagActionSection.cs │ │ │ │ │ │ ├── CreateTimeTagTypeSubsection.cs │ │ │ │ │ │ ├── RecordingTapControl.cs │ │ │ │ │ │ ├── TapButton.cs │ │ │ │ │ │ ├── TimeTagAdjustConfigSection.cs │ │ │ │ │ │ ├── TimeTagAutoGenerateSection.cs │ │ │ │ │ │ ├── TimeTagIssueSection.cs │ │ │ │ │ │ ├── TimeTagRecordingConfigSection.cs │ │ │ │ │ │ ├── TimeTagRecordingToolSection.cs │ │ │ │ │ │ └── TimeTagSettingsHeader.cs │ │ │ │ │ └── States/ │ │ │ │ │ ├── ILyricCaretState.cs │ │ │ │ │ ├── ILyricSelectionState.cs │ │ │ │ │ ├── LyricCaretState.cs │ │ │ │ │ ├── LyricEditorSelectingAction.cs │ │ │ │ │ ├── LyricSelectionState.cs │ │ │ │ │ ├── Modes/ │ │ │ │ │ │ ├── CreateTimeTagType.cs │ │ │ │ │ │ ├── EditLanguageModeState.cs │ │ │ │ │ │ ├── EditNoteModeState.cs │ │ │ │ │ │ ├── EditReferenceLyricModeState.cs │ │ │ │ │ │ ├── EditRomanisationModeState.cs │ │ │ │ │ │ ├── EditRubyModeState.cs │ │ │ │ │ │ ├── EditTextModeState.cs │ │ │ │ │ │ ├── EditTimeTagModeState.cs │ │ │ │ │ │ ├── IEditLanguageModeState.cs │ │ │ │ │ │ ├── IEditNoteModeState.cs │ │ │ │ │ │ ├── IEditReferenceLyricModeState.cs │ │ │ │ │ │ ├── IEditRomanisationModeState.cs │ │ │ │ │ │ ├── IEditRubyModeState.cs │ │ │ │ │ │ ├── IEditTextModeState.cs │ │ │ │ │ │ ├── IEditTimeTagModeState.cs │ │ │ │ │ │ ├── IHasBlueprintSelection.cs │ │ │ │ │ │ ├── IHasEditStep.cs │ │ │ │ │ │ ├── IHasSpecialAction.cs │ │ │ │ │ │ ├── LanguageEditStep.cs │ │ │ │ │ │ ├── ModeStateWithBlueprintContainer.cs │ │ │ │ │ │ ├── NoteEditStep.cs │ │ │ │ │ │ ├── ReferenceLyricEditStep.cs │ │ │ │ │ │ ├── RomanisationTagEditStep.cs │ │ │ │ │ │ ├── RubyTagEditMode.cs │ │ │ │ │ │ ├── RubyTagEditStep.cs │ │ │ │ │ │ ├── TextEditStep.cs │ │ │ │ │ │ └── TimeTagEditStep.cs │ │ │ │ │ ├── MovingCaretAction.cs │ │ │ │ │ └── RangeCaretPosition.cs │ │ │ │ ├── LyricsProvider.cs │ │ │ │ ├── Pages/ │ │ │ │ │ ├── Components/ │ │ │ │ │ │ └── Timeline/ │ │ │ │ │ │ ├── LyricBlueprintContainer.cs │ │ │ │ │ │ ├── PageBlueprintContainer.cs │ │ │ │ │ │ ├── PageSelectionBlueprint.cs │ │ │ │ │ │ ├── PagesTimeLine.cs │ │ │ │ │ │ └── PreviewLyricSelectionBlueprint.cs │ │ │ │ │ ├── IPageEditorVerifier.cs │ │ │ │ │ ├── IPageStateProvider.cs │ │ │ │ │ ├── PageEditor.cs │ │ │ │ │ ├── PageEditorEditMode.cs │ │ │ │ │ ├── PageEditorVerifier.cs │ │ │ │ │ ├── PageScreen.cs │ │ │ │ │ └── Settings/ │ │ │ │ │ ├── ConfirmReGeneratePageDialog.cs │ │ │ │ │ ├── PageAutoGenerateSection.cs │ │ │ │ │ ├── PageEditorIssueSection.cs │ │ │ │ │ ├── PageEditorSettingsHeader.cs │ │ │ │ │ ├── PageSettings.cs │ │ │ │ │ └── PagesSection.cs │ │ │ │ ├── Singers/ │ │ │ │ │ ├── DeleteSingerDialog.cs │ │ │ │ │ ├── Detail/ │ │ │ │ │ │ ├── AvatarSection.cs │ │ │ │ │ │ ├── EditSingerSection.cs │ │ │ │ │ │ ├── MetadataSection.cs │ │ │ │ │ │ └── SingerEditPopover.cs │ │ │ │ │ ├── ISingerScreenScrollingInfoProvider.cs │ │ │ │ │ ├── Rows/ │ │ │ │ │ │ ├── Components/ │ │ │ │ │ │ │ ├── SingerAvatar.cs │ │ │ │ │ │ │ └── Timeline/ │ │ │ │ │ │ │ ├── LyricTimelineSelectionBlueprint.cs │ │ │ │ │ │ │ ├── SingerLyricEditorBlueprintContainer.cs │ │ │ │ │ │ │ └── SingerLyricTimeline.cs │ │ │ │ │ │ ├── CreateNewLyricPlacementRow.cs │ │ │ │ │ │ ├── DefaultLyricPlacementRow.cs │ │ │ │ │ │ ├── LyricPlacementRow.cs │ │ │ │ │ │ └── SingerLyricPlacementRow.cs │ │ │ │ │ ├── SingerEditSection.cs │ │ │ │ │ ├── SingerRearrangeableList.cs │ │ │ │ │ ├── SingerRearrangeableListItem.cs │ │ │ │ │ └── SingerScreen.cs │ │ │ │ └── Translations/ │ │ │ │ ├── Components/ │ │ │ │ │ ├── CreateNewTranslationButton.cs │ │ │ │ │ ├── LanguageDropdown.cs │ │ │ │ │ ├── LyricTranslationTextBox.cs │ │ │ │ │ ├── PreviewLyricSpriteText.cs │ │ │ │ │ └── RemoveTranslationButton.cs │ │ │ │ ├── DeleteLanguagePopupDialog.cs │ │ │ │ ├── ITranslationInfoProvider.cs │ │ │ │ ├── TranslationEditSection.cs │ │ │ │ └── TranslationScreen.cs │ │ │ ├── BottomBar.cs │ │ │ ├── Components/ │ │ │ │ ├── Containers/ │ │ │ │ │ └── BindableScrollContainer.cs │ │ │ │ ├── Issues/ │ │ │ │ │ ├── IssueIcon.cs │ │ │ │ │ └── IssuesToolTip.cs │ │ │ │ ├── Markdown/ │ │ │ │ │ ├── DescriptionFormat.cs │ │ │ │ │ ├── DescriptionTextFlowContainer.cs │ │ │ │ │ ├── IDescriptionAction.cs │ │ │ │ │ ├── InputKeyDescriptionAction.cs │ │ │ │ │ └── InputKeyText.cs │ │ │ │ ├── Menus/ │ │ │ │ │ └── GenericScreenSelectionTabControl.cs │ │ │ │ ├── Timeline/ │ │ │ │ │ ├── EditableLyricTimelineSelectionBlueprint.cs │ │ │ │ │ ├── EditableTimeline.cs │ │ │ │ │ ├── EditableTimelineBlueprintContainer.cs │ │ │ │ │ └── EditableTimelineSelectionBlueprint.cs │ │ │ │ └── UserInterface/ │ │ │ │ ├── DeleteIconButton.cs │ │ │ │ └── LanguagesSelector.cs │ │ │ ├── EditorSection.cs │ │ │ ├── EditorSectionButton.cs │ │ │ ├── EditorSettings.cs │ │ │ ├── EditorSettingsHeader.cs │ │ │ ├── EditorTable.cs │ │ │ ├── EditorVerifier.cs │ │ │ ├── GeneratorConfigPopover.cs │ │ │ ├── GenericEditor.cs │ │ │ ├── GenericEditorScreen.cs │ │ │ ├── IEditorVerifier.cs │ │ │ ├── ISectionItemsEditorProvider.cs │ │ │ ├── Import/ │ │ │ │ └── Lyrics/ │ │ │ │ ├── AssignLanguage/ │ │ │ │ │ ├── AssignLanguageNavigation.cs │ │ │ │ │ ├── AssignLanguageStepScreen.cs │ │ │ │ │ └── UseLanguageDetectorPopupDialog.cs │ │ │ │ ├── DragFile/ │ │ │ │ │ ├── Components/ │ │ │ │ │ │ └── DrawableDragFile.cs │ │ │ │ │ ├── DragFileStepScreen.cs │ │ │ │ │ └── ImportLyricDialog.cs │ │ │ │ ├── EditLyric/ │ │ │ │ │ ├── EditLyricNavigation.cs │ │ │ │ │ └── EditLyricStepScreen.cs │ │ │ │ ├── GenerateRuby/ │ │ │ │ │ ├── GenerateRubyNavigation.cs │ │ │ │ │ ├── GenerateRubyStepScreen.cs │ │ │ │ │ └── UseAutoGenerateRubyPopupDialog.cs │ │ │ │ ├── GenerateTimeTag/ │ │ │ │ │ ├── AlreadyContainTimeTagPopupDialog.cs │ │ │ │ │ ├── GenerateTimeTagNavigation.cs │ │ │ │ │ ├── GenerateTimeTagStepScreen.cs │ │ │ │ │ ├── UseAutoGenerateRomanisationPopupDialog.cs │ │ │ │ │ └── UseAutoGenerateTimeTagPopupDialog.cs │ │ │ │ ├── IHasTopNavigation.cs │ │ │ │ ├── IImportStateResolver.cs │ │ │ │ ├── ILyricImporterStepScreen.cs │ │ │ │ ├── ImportLyricHeader.cs │ │ │ │ ├── ImportLyricManager.cs │ │ │ │ ├── ImportLyricOverlay.cs │ │ │ │ ├── LyricImporter.cs │ │ │ │ ├── LyricImporterStep.cs │ │ │ │ ├── LyricImporterStepScreen.cs │ │ │ │ ├── LyricImporterStepScreenWithLyricEditor.cs │ │ │ │ ├── LyricImporterSubScreenStack.cs │ │ │ │ ├── NotImportStepScreenException.cs │ │ │ │ ├── RollBackPopupDialog.cs │ │ │ │ ├── RollBackResetPopupDialog.cs │ │ │ │ ├── Success/ │ │ │ │ │ └── SuccessStepScreen.cs │ │ │ │ └── TopNavigation.cs │ │ │ ├── IssueSection.cs │ │ │ ├── IssueTable.cs │ │ │ ├── SectionItemsEditor.cs │ │ │ ├── SectionTimingInfoItemsEditor.cs │ │ │ ├── Stages/ │ │ │ │ └── Classic/ │ │ │ │ ├── ClassicStageEditor.cs │ │ │ │ ├── ClassicStageEditorScreenMode.cs │ │ │ │ ├── ClassicStageScreen.cs │ │ │ │ ├── Config/ │ │ │ │ │ └── ConfigScreen.cs │ │ │ │ └── Stage/ │ │ │ │ ├── IStageEditorStateProvider.cs │ │ │ │ ├── IStageEditorVerifier.cs │ │ │ │ ├── Settings/ │ │ │ │ │ ├── StageEditorIssueSection.cs │ │ │ │ │ ├── StageEditorSettingsHeader.cs │ │ │ │ │ ├── StageSettings.cs │ │ │ │ │ └── TimingPointsSection.cs │ │ │ │ ├── StageCategoryScreenStack.cs │ │ │ │ ├── StageEditor.cs │ │ │ │ ├── StageEditorEditCategory.cs │ │ │ │ ├── StageEditorEditMode.cs │ │ │ │ ├── StageEditorVerifier.cs │ │ │ │ └── StageScreen.cs │ │ │ ├── WorkspaceScreen.cs │ │ │ └── WorkspaceScreenStack.cs │ │ ├── Section.cs │ │ ├── Settings/ │ │ │ ├── Header.cs │ │ │ ├── KaraokeSettings.cs │ │ │ ├── KaraokeSettingsColourProvider.cs │ │ │ ├── KaraokeSettingsPanel.cs │ │ │ ├── KaraokeSettingsSection.cs │ │ │ ├── KaraokeSettingsSubsection.cs │ │ │ ├── KaraokeVersionManager.cs │ │ │ ├── Previews/ │ │ │ │ ├── DefaultPreview.cs │ │ │ │ ├── Gameplay/ │ │ │ │ │ ├── LyricPreview.cs │ │ │ │ │ ├── NotePlayfieldPreview.cs │ │ │ │ │ └── ShowCursorPreview.cs │ │ │ │ ├── Graphics/ │ │ │ │ │ └── ManageFontPreview.cs │ │ │ │ ├── Input/ │ │ │ │ │ ├── MicrophoneDevicePreview.cs │ │ │ │ │ └── MicrophoneSoundVisualizer.cs │ │ │ │ ├── SettingsSubsectionPreview.cs │ │ │ │ └── UnderConstructionPreview.cs │ │ │ ├── Sections/ │ │ │ │ ├── ConfigSection.cs │ │ │ │ ├── Gameplay/ │ │ │ │ │ ├── GeneralSettings.cs │ │ │ │ │ ├── NoteSettings.cs │ │ │ │ │ └── ScoringSettings.cs │ │ │ │ ├── Graphics/ │ │ │ │ │ ├── LyricFontSettings.cs │ │ │ │ │ ├── ManageFontSettings.cs │ │ │ │ │ ├── NoteFontSettings.cs │ │ │ │ │ └── TransparentSettings.cs │ │ │ │ ├── Input/ │ │ │ │ │ └── MicrophoneSettings.cs │ │ │ │ ├── ScoringSection.cs │ │ │ │ └── StyleSection.cs │ │ │ ├── SettingsFont.cs │ │ │ └── SettingsMicrophoneDeviceDropdown.cs │ │ └── Skin/ │ │ ├── Config/ │ │ │ ├── ConfigScreen.cs │ │ │ ├── IntervalSection.cs │ │ │ ├── LyricConfigSection.cs │ │ │ ├── LyricFontInfoManager.cs │ │ │ ├── PositionSection.cs │ │ │ └── RubyAndRomanisationSection.cs │ │ ├── KaraokeSkinEditor.cs │ │ ├── KaraokeSkinEditorScreen.cs │ │ ├── KaraokeSkinEditorScreenMode.cs │ │ └── Style/ │ │ ├── LyricColorSection.cs │ │ ├── LyricFontSection.cs │ │ ├── LyricShadowSection.cs │ │ ├── LyricStylePreview.cs │ │ ├── NoteColorSection.cs │ │ ├── NoteFontSection.cs │ │ ├── NoteStylePreview.cs │ │ ├── StyleScreen.cs │ │ └── StyleSection.cs │ ├── Skinning/ │ │ ├── Argon/ │ │ │ └── KaraokeArgonSkinTransformer.cs │ │ ├── Default/ │ │ │ ├── DefaultBodyPiece.cs │ │ │ └── KaraokeDefaultSkinTransformer.cs │ │ ├── Elements/ │ │ │ ├── ElementType.cs │ │ │ ├── IKaraokeSkinElement.cs │ │ │ ├── InvalidDrawableTypeException.cs │ │ │ ├── LayoutGroup.cs │ │ │ ├── LyricFontInfo.cs │ │ │ └── NoteStyle.cs │ │ ├── Fonts/ │ │ │ ├── BitmapFontCompressor.cs │ │ │ ├── BitmapFontImageGenerator.cs │ │ │ ├── FontInfo.cs │ │ │ └── FontManager.cs │ │ ├── InternalSkinStorageResourceProvider.cs │ │ ├── KaraokeBeatmapSkin.cs │ │ ├── KaraokeIndexLookup.cs │ │ ├── KaraokeSkin.cs │ │ ├── KaraokeSkinConfigurationLookup.cs │ │ ├── KaraokeSkinLookup.cs │ │ ├── Legacy/ │ │ │ ├── KaraokeClassicSkinTransformer.cs │ │ │ ├── KaraokeLegacySkinTransformer.cs │ │ │ ├── LegacyColumnBackground.cs │ │ │ ├── LegacyHitExplosion.cs │ │ │ ├── LegacyJudgementLine.cs │ │ │ ├── LegacyKaraokeColumnElement.cs │ │ │ ├── LegacyKaraokeElement.cs │ │ │ ├── LegacyNotePiece.cs │ │ │ └── LegacyStageBackground.cs │ │ ├── Tools/ │ │ │ └── SkinConverterTool.cs │ │ └── Triangles/ │ │ └── KaraokeTrianglesSkinTransformer.cs │ ├── Stages/ │ │ ├── Commands/ │ │ │ ├── IStageCommand.cs │ │ │ ├── Lyrics/ │ │ │ │ └── LyricStyleCommand.cs │ │ │ ├── StageAlphaCommand.cs │ │ │ ├── StageAnchorCommand.cs │ │ │ ├── StageCommand.cs │ │ │ ├── StageHeightCommand.cs │ │ │ ├── StageOriginCommand.cs │ │ │ ├── StagePaddingCommand.cs │ │ │ ├── StageScaleCommand.cs │ │ │ ├── StageWidthCommand.cs │ │ │ ├── StageXCommand.cs │ │ │ └── StageYCommand.cs │ │ ├── Drawables/ │ │ │ ├── DrawableStage.cs │ │ │ ├── DrawableStageBeatmapCoverInfo.cs │ │ │ ├── IStageElementRunner.cs │ │ │ ├── IStageHitObjectRunner.cs │ │ │ ├── IStagePlayfieldRunner.cs │ │ │ ├── StageElementRunner.cs │ │ │ ├── StageHitObjectRunner.cs │ │ │ ├── StagePlayfieldRunner.cs │ │ │ └── StageRunner.cs │ │ ├── HitObjectCommandProvider.cs │ │ ├── IHitObjectCommandProvider.cs │ │ ├── IPlayfieldCommandProvider.cs │ │ ├── IStageElement.cs │ │ ├── IStageElementProvider.cs │ │ ├── IStageElementWithDuration.cs │ │ ├── Infos/ │ │ │ ├── Classic/ │ │ │ │ ├── ClassicLyricCommandProvider.cs │ │ │ │ ├── ClassicLyricLayout.cs │ │ │ │ ├── ClassicLyricLayoutAlignment.cs │ │ │ │ ├── ClassicLyricLayoutCategory.cs │ │ │ │ ├── ClassicLyricTimingInfo.cs │ │ │ │ ├── ClassicLyricTimingPoint.cs │ │ │ │ ├── ClassicPlayfieldCommandProvider.cs │ │ │ │ ├── ClassicStageDefinition.cs │ │ │ │ ├── ClassicStageInfo.cs │ │ │ │ ├── ClassicStyle.cs │ │ │ │ └── ClassicStyleCategory.cs │ │ │ ├── Preview/ │ │ │ │ ├── PreviewElementProvider.cs │ │ │ │ ├── PreviewLyricCommandProvider.cs │ │ │ │ ├── PreviewLyricLayout.cs │ │ │ │ ├── PreviewLyricLayoutCategory.cs │ │ │ │ ├── PreviewPlayfieldCommandProvider.cs │ │ │ │ ├── PreviewStageDefinition.cs │ │ │ │ ├── PreviewStageInfo.cs │ │ │ │ ├── PreviewStageTimingCalculator.cs │ │ │ │ ├── PreviewStyle.cs │ │ │ │ └── PreviewStyleCategory.cs │ │ │ ├── StageDefinition.cs │ │ │ ├── StageElement.cs │ │ │ ├── StageElementCategory.cs │ │ │ ├── StageInfo.cs │ │ │ └── Types/ │ │ │ └── IHasCalculatedProperty.cs │ │ ├── PlayfieldCommandProvider.cs │ │ ├── StageBeatmapCoverInfo.cs │ │ ├── StageElementProvider.cs │ │ └── StageSprite.cs │ ├── Statistics/ │ │ ├── BeatmapMetadataGraph.cs │ │ ├── NotScorableGraph.cs │ │ └── ScoringResultGraph.cs │ ├── Timing/ │ │ └── StopClock.cs │ ├── UI/ │ │ ├── Components/ │ │ │ ├── CenterLine.cs │ │ │ ├── DefaultColumnBackground.cs │ │ │ ├── DefaultJudgementLine.cs │ │ │ ├── RealTimeScoringVisualization.cs │ │ │ ├── ReplayScoringVisualization.cs │ │ │ ├── ScoringMarker.cs │ │ │ ├── ScoringStatus.cs │ │ │ └── VoiceVisualization.cs │ │ ├── DefaultHitExplosion.cs │ │ ├── DrawableKaraokeRuleset.cs │ │ ├── DrawableNoteJudgement.cs │ │ ├── HUD/ │ │ │ ├── BindableNumberExtension.cs │ │ │ ├── GeneralSettingOverlay.cs │ │ │ ├── ISettingHUDOverlay.cs │ │ │ ├── PracticeOverlay.cs │ │ │ ├── SettingButton.cs │ │ │ ├── SettingButtonsDisplay.cs │ │ │ ├── SettingOverlay.cs │ │ │ └── SettingOverlayContainer.cs │ │ ├── KaraokePlayfield.cs │ │ ├── KaraokePlayfieldAdjustmentContainer.cs │ │ ├── KaraokeReplayRecorder.cs │ │ ├── KaraokeScrollingDirection.cs │ │ ├── KaraokeSettingsSubsection.cs │ │ ├── LyricPlayfield.cs │ │ ├── NotePlayfield.cs │ │ ├── PlayerSettings/ │ │ │ ├── ClickablePlayerSliderBar.cs │ │ │ ├── ILyricNavigator.cs │ │ │ ├── LyricsPreview.cs │ │ │ ├── PitchSettings.cs │ │ │ ├── PlaybackSettings.cs │ │ │ ├── PlayerDropdown.cs │ │ │ └── PracticeSettings.cs │ │ ├── Position/ │ │ │ ├── INotePositionInfo.cs │ │ │ ├── NotePositionCalculator.cs │ │ │ └── NotePositionInfo.cs │ │ └── Scrolling/ │ │ └── ScrollingNotePlayfield.cs │ ├── Utils/ │ │ ├── ActivatorUtils.cs │ │ ├── AssemblyUtils.cs │ │ ├── BindablesUtils.cs │ │ ├── CharUtils.cs │ │ ├── ComparableUtils.cs │ │ ├── CultureInfoUtils.cs │ │ ├── EnumUtils.cs │ │ ├── FontUsageUtils.cs │ │ ├── FontUtils.cs │ │ ├── JpStringUtils.cs │ │ ├── RectangleFUtils.cs │ │ ├── StackTraceUtils.cs │ │ ├── TextIndexUtils.cs │ │ ├── TypeUtils.cs │ │ └── VersionUtils.cs │ └── osu.Game.Rulesets.Karaoke.csproj ├── osu.Game.Rulesets.Karaoke.Architectures/ │ ├── BaseTest.cs │ ├── Edit/ │ │ └── Checks/ │ │ ├── TestCheck.cs │ │ └── TestCheckTest.cs │ ├── Extensions.cs │ ├── MethodUtils.cs │ ├── Project.cs │ ├── TestClass.cs │ ├── TestTestClass.cs │ └── osu.Game.Rulesets.Karaoke.Architectures.csproj ├── osu.Game.Rulesets.Karaoke.Tests/ │ ├── .vscode/ │ │ ├── launch.json │ │ └── tasks.json │ ├── Asserts/ │ │ ├── ObjectAssert.cs │ │ ├── RubyTagAssert.cs │ │ └── TimeTagAssert.cs │ ├── Beatmaps/ │ │ ├── ElementIdTest.cs │ │ ├── Formats/ │ │ │ ├── KaraokeLegacyBeatmapDecoderTest.cs │ │ │ └── KaraokeLegacyBeatmapEncoderTest.cs │ │ ├── KaraokeBeatmapConversionTest.cs │ │ ├── Metadatas/ │ │ │ ├── PageInfoTest.cs │ │ │ └── SingerInfoTest.cs │ │ ├── TestKaraokeBeatmap.cs │ │ └── Utils/ │ │ └── SingerUtilsTest.cs │ ├── Bindables/ │ │ ├── BindableCultureInfoTest.cs │ │ └── BindableFontUsageTest.cs │ ├── Difficulty/ │ │ ├── DifficultyCalculatorTest.cs │ │ └── KaraokeDifficultyCalculatorTest.cs │ ├── Editor/ │ │ ├── ChangeHandlers/ │ │ │ ├── BaseChangeHandlerTest.cs │ │ │ ├── BaseHitObjectChangeHandlerTest.cs │ │ │ ├── BaseHitObjectPropertyChangeHandlerTest.cs │ │ │ ├── Beatmaps/ │ │ │ │ ├── BeatmapPagesChangeHandlerTest.cs │ │ │ │ ├── BeatmapSingersChangeHandlerTest.cs │ │ │ │ └── BeatmapTranslationsChangeHandlerTest.cs │ │ │ ├── ImportBeatmapChangeHandlerTest.cs │ │ │ ├── LockChangeHandlerTest.cs │ │ │ ├── Lyrics/ │ │ │ │ ├── LyricLanguageChangeHandlerTest.cs │ │ │ │ ├── LyricPropertyAutoGenerateChangeHandlerTest.cs │ │ │ │ ├── LyricPropertyChangeHandlerTest.cs │ │ │ │ ├── LyricReferenceChangeHandlerTest.cs │ │ │ │ ├── LyricRubyTagsChangeHandlerTest.cs │ │ │ │ ├── LyricSingerChangeHandlerTest.cs │ │ │ │ ├── LyricTextChangeHandlerTest.cs │ │ │ │ ├── LyricTimeTagsChangeHandlerTest.cs │ │ │ │ ├── LyricTranslationChangeHandlerTest.cs │ │ │ │ └── LyricsChangeHandlerTest.cs │ │ │ ├── Notes/ │ │ │ │ ├── NotePropertyChangeHandlerTest.cs │ │ │ │ └── NotesChangeHandlerTest.cs │ │ │ └── Stages/ │ │ │ ├── BaseStageInfoChangeHandlerTest.cs │ │ │ ├── ClassicStageChangeHandlerTest.cs │ │ │ ├── StageElementCategoryChangeHandlerTest.cs │ │ │ └── StagesChangeHandlerTest.cs │ │ ├── Checks/ │ │ │ ├── BaseCheckTest.cs │ │ │ ├── BeatmapPropertyCheckTest.cs │ │ │ ├── CheckBeatmapAvailableTranslationsTest.cs │ │ │ ├── CheckBeatmapNoteInfoTest.cs │ │ │ ├── CheckBeatmapPageInfoTest.cs │ │ │ ├── CheckClassicStageInfoTest.cs │ │ │ ├── CheckLyricLanguageTest.cs │ │ │ ├── CheckLyricReferenceLyricTest.cs │ │ │ ├── CheckLyricRubyTagTest.cs │ │ │ ├── CheckLyricSingerTest.cs │ │ │ ├── CheckLyricTextTest.cs │ │ │ ├── CheckLyricTimeTagTest.cs │ │ │ ├── CheckLyricTranslationsTest.cs │ │ │ ├── CheckNoteReferenceLyricTest.cs │ │ │ ├── CheckNoteTextTest.cs │ │ │ ├── CheckNoteTimeTest.cs │ │ │ ├── CheckStageInfoTest.cs │ │ │ └── HitObjectCheckTest.cs │ │ ├── Generator/ │ │ │ ├── BaseGeneratorSelectorTest.cs │ │ │ ├── BasePropertyDetectorTest.cs │ │ │ ├── BasePropertyGeneratorTest.cs │ │ │ ├── Beatmaps/ │ │ │ │ ├── BaseBeatmapDetectorTest.cs │ │ │ │ ├── BaseBeatmapGeneratorTest.cs │ │ │ │ └── Pages/ │ │ │ │ └── PageGeneratorTest.cs │ │ │ ├── GeneratorConfigExtensionTest.cs │ │ │ ├── GeneratorConfigHelper.cs │ │ │ ├── Lyrics/ │ │ │ │ ├── BaseLyricDetectorTest.cs │ │ │ │ ├── BaseLyricGeneratorSelectorTest.cs │ │ │ │ ├── BaseLyricGeneratorTest.cs │ │ │ │ ├── Language/ │ │ │ │ │ └── LanguageDetectorTest.cs │ │ │ │ ├── Notes/ │ │ │ │ │ └── NoteGeneratorTest.cs │ │ │ │ ├── ReferenceLyric/ │ │ │ │ │ └── ReferenceLyricDetectorTest.cs │ │ │ │ ├── Romanisation/ │ │ │ │ │ ├── BaseRomanisationGeneratorTest.cs │ │ │ │ │ ├── Ja/ │ │ │ │ │ │ └── JaRomanisationGeneratorTest.cs │ │ │ │ │ ├── RomanisationGenerateResultHelper.cs │ │ │ │ │ └── RomanisationGeneratorSelectorTest.cs │ │ │ │ ├── RubyTags/ │ │ │ │ │ ├── BaseRubyTagGeneratorTest.cs │ │ │ │ │ ├── Ja/ │ │ │ │ │ │ └── JaRubyTagGeneratorTest.cs │ │ │ │ │ └── RubyTagGeneratorSelectorTest.cs │ │ │ │ └── TimeTags/ │ │ │ │ ├── BaseTimeTagGeneratorTest.cs │ │ │ │ ├── Ja/ │ │ │ │ │ └── JaTimeTagGeneratorTest.cs │ │ │ │ ├── TimeTagGeneratorSelectorTest.cs │ │ │ │ └── Zh/ │ │ │ │ └── ZhTimeTagGeneratorTest.cs │ │ │ └── Stages/ │ │ │ ├── BaseStageElementCategoryGeneratorTest.cs │ │ │ ├── BaseStageInfoGeneratorTest.cs │ │ │ ├── BaseStageInfoPropertyGeneratorTest.cs │ │ │ ├── Classic/ │ │ │ │ ├── ClassicLyricLayoutCategoryGeneratorTest.cs │ │ │ │ ├── ClassicLyricTimingInfoGeneratorTest.cs │ │ │ │ └── ClassicStageInfoGeneratorTest.cs │ │ │ ├── Preview/ │ │ │ │ └── PreviewStageInfoGeneratorTest.cs │ │ │ └── StageInfoGeneratorSelectorTest.cs │ │ ├── TestSceneEditor.cs │ │ ├── TestSceneSetupScreen.cs │ │ ├── TestSceneTimeTagTooltip.cs │ │ └── Utils/ │ │ ├── HitObjectWritableUtilsTest.cs │ │ ├── LockStateUtilsTest.cs │ │ └── ValueChangedEventUtilsTest.cs │ ├── Extensions/ │ │ ├── EnumerableExtensionsTest.cs │ │ ├── PlayerTestSceneExtensions.cs │ │ └── PrimaryKeyObjectExtension.cs │ ├── Flags/ │ │ └── FlagStateTest.cs │ ├── Graphics/ │ │ ├── Sprites/ │ │ │ ├── DisplayLyricProcessorTest.cs │ │ │ └── Processor/ │ │ │ ├── DisplayProcessorTestScene.cs │ │ │ ├── TestSceneLyricFirstDisplayProcessor.cs │ │ │ └── TestSceneRomanisedSyllableFirstDisplayProcessor.cs │ │ ├── TestSceneFontSelector.cs │ │ ├── TestSceneLanguageSelector.cs │ │ ├── TestSceneLyricTooltip.cs │ │ ├── TestSceneRightTriangle.cs │ │ └── TestSceneSingerToolTip.cs │ ├── Helper/ │ │ ├── TestCaseCheckHelper.cs │ │ ├── TestCaseElementIdHelper.cs │ │ ├── TestCaseNoteHelper.cs │ │ ├── TestCaseTagHelper.cs │ │ └── TestCaseToneHelper.cs │ ├── IO/ │ │ ├── Serialization/ │ │ │ ├── Converters/ │ │ │ │ ├── BaseSingleConverterTest.cs │ │ │ │ ├── ColourConverterTest.cs │ │ │ │ ├── CultureInfoConverterTest.cs │ │ │ │ ├── ElementIdConverterTest.cs │ │ │ │ ├── FontUsageConverterTest.cs │ │ │ │ ├── KaraokeSkinElementConverterTest.cs │ │ │ │ ├── LyricConverterTest.cs │ │ │ │ ├── ReferenceLyricPropertyConfigConverterTest.cs │ │ │ │ ├── RubyTagConverterTest.cs │ │ │ │ ├── RubyTagsConverterTest.cs │ │ │ │ ├── ShaderConverterTest.cs │ │ │ │ ├── StageInfoConverterTest.cs │ │ │ │ ├── TimeTagConverterTest.cs │ │ │ │ ├── TimeTagsConverterTest.cs │ │ │ │ ├── ToneConverterTest.cs │ │ │ │ └── TranslationConverterTest.cs │ │ │ ├── KaraokeJsonSerializableExtensionsTest.cs │ │ │ └── SkinJsonSerializableExtensionsTest.cs │ │ └── Stores/ │ │ ├── BaseGlyphStoreTest.cs │ │ └── TtfGlyphStoreTest.cs │ ├── Integration/ │ │ └── Formats/ │ │ ├── KarDecoderTest.cs │ │ ├── KarEncoderTest.cs │ │ ├── KarFileTest.cs │ │ ├── LrcDecoderTest.cs │ │ ├── LrcEncoderTest.cs │ │ ├── LrcParserUtilsTest.cs │ │ ├── LyricTextDecoderTest.cs │ │ └── LyricTextEncoderTest.cs │ ├── KaraokeTestBrowser.cs │ ├── Mods/ │ │ ├── KaraokeModStageTestScene.cs │ │ ├── KaraokeModTestScene.cs │ │ ├── ModsTest.cs │ │ ├── TestSceneKaraokeModAutoplay.cs │ │ ├── TestSceneKaraokeModAutoplayBySinger.cs │ │ ├── TestSceneKaraokeModClassicStage.cs │ │ ├── TestSceneKaraokeModDisableNote.cs │ │ ├── TestSceneKaraokeModFlashlight.cs │ │ ├── TestSceneKaraokeModFun.cs │ │ ├── TestSceneKaraokeModLyricConfiguration.cs │ │ ├── TestSceneKaraokeModPerfect.cs │ │ ├── TestSceneKaraokeModPractice.cs │ │ ├── TestSceneKaraokeModPreviewStage.cs │ │ ├── TestSceneKaraokeModSuddenDeath.cs │ │ └── TestSceneKaraokeModTranslation.cs │ ├── Objects/ │ │ ├── LyricTest.cs │ │ ├── NoteTest.cs │ │ ├── RubyTagTest.cs │ │ ├── TimeTagTest.cs │ │ ├── ToneCalculationTest.cs │ │ ├── Utils/ │ │ │ ├── LyricUtilsTest.cs │ │ │ ├── LyricsUtilsTest.cs │ │ │ ├── NoteUtilsTest.cs │ │ │ ├── NotesUtilsTest.cs │ │ │ ├── OrderUtilsTest.cs │ │ │ ├── RubyTagUtilsTest.cs │ │ │ ├── RubyTagsUtilsTest.cs │ │ │ ├── TimeTagUtilsTest.cs │ │ │ └── TimeTagsUtilsTest.cs │ │ └── Workings/ │ │ ├── HitObjectWorkingPropertyValidatorTest.cs │ │ ├── LyricWorkingPropertyValidatorTest.cs │ │ └── NoteWorkingPropertyValidatorTest.cs │ ├── Overlays/ │ │ ├── Changelog/ │ │ │ ├── ChangelogPullRequestInfoTest.cs │ │ │ ├── TestSceneKaraokeChangeLogMarkdownContainer.cs │ │ │ └── TestSceneKaraokeChangeLogOverlay.cs │ │ └── TestSceneOverlayColourProvider.cs │ ├── Ranking/ │ │ ├── TestKaraokeScoreInfo.cs │ │ ├── TestSceneBeatmapMetadataGraph.cs │ │ ├── TestSceneHitEventTimingDistributionGraph.cs │ │ ├── TestSceneNotScorableGraph.cs │ │ ├── TestSceneScoringResultGraph.cs │ │ └── TestSceneStatisticsPanel.cs │ ├── Replays/ │ │ ├── TestSceneAutoGeneration.cs │ │ └── TestSceneAutoGenerationBySinger.cs │ ├── Resources/ │ │ ├── TestResources.cs │ │ ├── Testing/ │ │ │ ├── Beatmaps/ │ │ │ │ ├── karaoke-file-samples-expected-conversion.json │ │ │ │ ├── karaoke-file-samples-without-note.osu │ │ │ │ ├── karaoke-file-samples.osu │ │ │ │ ├── karaoke-note-samples.osu │ │ │ │ └── karaoke-translation-samples.osu │ │ │ ├── Fonts/ │ │ │ │ └── Fnt/ │ │ │ │ └── OpenSans/ │ │ │ │ └── LICENSE.txt │ │ │ ├── Kar/ │ │ │ │ ├── default.kar │ │ │ │ └── light.kar │ │ │ └── Track/ │ │ │ └── demo.json │ │ └── special-skin/ │ │ ├── default.json │ │ ├── lyric-font-infos.json │ │ └── note-styles.json │ ├── Screens/ │ │ ├── Edit/ │ │ │ ├── Beatmap/ │ │ │ │ ├── BeatmapEditorScreenTestScene.cs │ │ │ │ ├── Components/ │ │ │ │ │ └── TestSceneLyricSelector.cs │ │ │ │ ├── Lyrics/ │ │ │ │ │ ├── CaretPosition/ │ │ │ │ │ │ ├── Algorithms/ │ │ │ │ │ │ │ ├── BaseCaretPositionAlgorithmTest.cs │ │ │ │ │ │ │ ├── BaseCharIndexCaretPositionAlgorithmTest.cs │ │ │ │ │ │ │ ├── BaseIndexCaretPositionAlgorithmTest.cs │ │ │ │ │ │ │ ├── ClickingCaretPositionAlgorithmTest.cs │ │ │ │ │ │ │ ├── CreateRemoveTimeTagCaretPositionAlgorithmTest.cs │ │ │ │ │ │ │ ├── CreateRubyTagCaretPositionAlgorithmTest.cs │ │ │ │ │ │ │ ├── CuttingCaretPositionAlgorithmTest.cs │ │ │ │ │ │ │ ├── NavigateCaretPositionAlgorithmTest.cs │ │ │ │ │ │ │ ├── RecordingTimeTagCaretPositionAlgorithmTest.cs │ │ │ │ │ │ │ └── TypingCaretPositionAlgorithmTest.cs │ │ │ │ │ │ └── IndexCaretPositionTest.cs │ │ │ │ │ ├── Content/ │ │ │ │ │ │ ├── SingleLyricEditorTest.cs │ │ │ │ │ │ ├── TestSceneInteractableLyric.cs │ │ │ │ │ │ └── TestScenePreviewKaraokeSpriteText.cs │ │ │ │ │ ├── LyricEditorTest.cs │ │ │ │ │ ├── LyricEditorVerifierTest.cs │ │ │ │ │ ├── Settings/ │ │ │ │ │ │ └── TestSceneLyricEditorDescriptionTextFlowContainer.cs │ │ │ │ │ ├── States/ │ │ │ │ │ │ ├── BaseLyricCaretStateTest.cs │ │ │ │ │ │ ├── LyricCaretStateActionTest.cs │ │ │ │ │ │ ├── LyricCaretStateMoveCaretTest.cs │ │ │ │ │ │ └── LyricCaretStateSwitchModeTest.cs │ │ │ │ │ └── TestSceneLyricEditorColourProvider.cs │ │ │ │ ├── TestSceneEditorMenuBar.cs │ │ │ │ ├── TestSceneKaraokeBeatmapEditor.cs │ │ │ │ ├── TestSceneLyricEditorScreen.cs │ │ │ │ ├── TestScenePageScreen.cs │ │ │ │ ├── TestSceneSingerScreen.cs │ │ │ │ └── TestSceneTranslationsScreen.cs │ │ │ ├── Components/ │ │ │ │ ├── Issues/ │ │ │ │ │ ├── TestSceneIssueIcon.cs │ │ │ │ │ └── TestSceneIssuesToolTip.cs │ │ │ │ └── Markdown/ │ │ │ │ └── TestSceneDescriptionTextFlowContainer.cs │ │ │ ├── GenericEditorScreenTestScene.cs │ │ │ ├── GenericEditorTestScene.cs │ │ │ ├── Import/ │ │ │ │ └── TestSceneLyricImporter.cs │ │ │ └── Stages/ │ │ │ └── Classic/ │ │ │ ├── ClassicStageScreenTestScene.cs │ │ │ ├── TestSceneClassicStageEditor.cs │ │ │ ├── TestSceneConfigScreen.cs │ │ │ └── TestSceneStageScreen.cs │ │ ├── ScreenTestScene.cs │ │ ├── Settings/ │ │ │ ├── Previews/ │ │ │ │ └── TestSceneMicrophoneSoundVisualizer.cs │ │ │ └── TestSceneKaraokeSettings.cs │ │ ├── Skin/ │ │ │ ├── KaraokeSkinEditorScreenTestScene.cs │ │ │ ├── TestSceneConfigScreen.cs │ │ │ ├── TestSceneKaraokeSkinEditor.cs │ │ │ └── TestSceneStyleScreen.cs │ │ ├── TestManageFontPreview.cs │ │ └── TestSceneGeneratorConfigPopover.cs │ ├── Skinning/ │ │ ├── Fonts/ │ │ │ ├── BitmapFontCompressorTest.cs │ │ │ └── BitmapFontImageGeneratorTest.cs │ │ ├── KaraokeBeatmapSkinDecodingTest.cs │ │ ├── KaraokeHitObjectTestScene.cs │ │ ├── KaraokeSkinDecodingTest.cs │ │ ├── KaraokeSkinnableColumnTestScene.cs │ │ ├── KaraokeSkinnableTestScene.cs │ │ ├── NotePlayfieldTestContainer.cs │ │ ├── TestSceneColumnBackground.cs │ │ ├── TestSceneDrawableJudgement.cs │ │ ├── TestSceneHitExplosion.cs │ │ ├── TestSceneLyric.cs │ │ ├── TestSceneNote.cs │ │ └── TestSceneNotePlayfield.cs │ ├── Stages/ │ │ ├── Drawables/ │ │ │ └── TestSceneDrawableStageBeatmapCoverInfo.cs │ │ └── Infos/ │ │ ├── Classic/ │ │ │ └── ClassicLyricTimingInfoTest.cs │ │ ├── Preview/ │ │ │ └── PreviewStageTimingCalculatorTest.cs │ │ └── StageElementCategoryTest.cs │ ├── TestSceneOsuGame.cs │ ├── UI/ │ │ ├── Position/ │ │ │ └── NotePositionCalculatorTest.cs │ │ ├── TestSceneControlLayer.cs │ │ ├── TestSceneKaraokePlayer.cs │ │ ├── TestSceneNotePlayfield.cs │ │ ├── TestSceneRulesetIcon.cs │ │ └── TestSceneScoringStatus.cs │ ├── Utils/ │ │ ├── BindablesUtilsTest.cs │ │ ├── CharUtilsTest.cs │ │ ├── ComparableUtilsTest.cs │ │ ├── CultureInfoUtilsTest.cs │ │ ├── EnumUtilsTest.cs │ │ ├── FontUsageUtilsTest.cs │ │ ├── FontUtilsTest.cs │ │ ├── JpStringUtilsTest.cs │ │ ├── RectangleFUtilsTest.cs │ │ ├── TextIndexUtilsTest.cs │ │ ├── TypeUtilsTest.cs │ │ └── VersionUtilsTest.cs │ ├── VisualTestRunner.cs │ └── osu.Game.Rulesets.Karaoke.Tests.csproj ├── osu.Game.Rulesets.Karaoke.sln ├── osu.Game.Rulesets.Karaoke.sln.DotSettings └── osu.licenseheader ================================================ FILE CONTENTS ================================================ ================================================ FILE: .config/dotnet-tools.json ================================================ { "version": 1, "isRoot": true, "tools": { "ppy.localisationanalyser.tools": { "version": "2024.802.0", "commands": [ "localisation" ] }, "jetbrains.resharper.globaltools": { "version": "2025.1.3", "commands": [ "jb" ] }, "nvika": { "version": "4.0.0", "commands": [ "nvika" ] }, "codefilesanity": { "version": "0.0.37", "commands": [ "CodeFileSanity" ] } } } ================================================ FILE: .editorconfig ================================================ # EditorConfig is awesome: http://editorconfig.org root = true [*.{csproj,props,targets}] charset = utf-8-bom end_of_line = crlf insert_final_newline = true indent_style = space indent_size = 2 trim_trailing_whitespace = true [g_*.cs] generated_code = true [*.cs] end_of_line = crlf insert_final_newline = true indent_style = space indent_size = 4 trim_trailing_whitespace = true #license header file_header_template = Copyright (c) andy840119 . Licensed under the GPL Licence.\nSee the LICENCE file in the repository root for full licence text. #Roslyn naming styles #PascalCase for public and protected members dotnet_naming_style.pascalcase.capitalization = pascal_case dotnet_naming_symbols.public_members.applicable_accessibilities = public,internal,protected,protected_internal,private_protected dotnet_naming_symbols.public_members.applicable_kinds = property,method,field,event dotnet_naming_rule.public_members_pascalcase.severity = error dotnet_naming_rule.public_members_pascalcase.symbols = public_members dotnet_naming_rule.public_members_pascalcase.style = pascalcase #camelCase for private members dotnet_naming_style.camelcase.capitalization = camel_case dotnet_naming_symbols.private_members.applicable_accessibilities = private dotnet_naming_symbols.private_members.applicable_kinds = property,method,field,event dotnet_naming_rule.private_members_camelcase.severity = warning dotnet_naming_rule.private_members_camelcase.symbols = private_members dotnet_naming_rule.private_members_camelcase.style = camelcase dotnet_naming_symbols.local_function.applicable_kinds = local_function dotnet_naming_rule.local_function_camelcase.severity = warning dotnet_naming_rule.local_function_camelcase.symbols = local_function dotnet_naming_rule.local_function_camelcase.style = camelcase #all_lower for private and local constants/static readonlys dotnet_naming_style.all_lower.capitalization = all_lower dotnet_naming_style.all_lower.word_separator = _ dotnet_naming_symbols.private_constants.applicable_accessibilities = private dotnet_naming_symbols.private_constants.required_modifiers = const dotnet_naming_symbols.private_constants.applicable_kinds = field dotnet_naming_rule.private_const_all_lower.severity = warning dotnet_naming_rule.private_const_all_lower.symbols = private_constants dotnet_naming_rule.private_const_all_lower.style = all_lower dotnet_naming_symbols.private_static_readonly.applicable_accessibilities = private dotnet_naming_symbols.private_static_readonly.required_modifiers = static,readonly dotnet_naming_symbols.private_static_readonly.applicable_kinds = field dotnet_naming_rule.private_static_readonly_all_lower.severity = warning dotnet_naming_rule.private_static_readonly_all_lower.symbols = private_static_readonly dotnet_naming_rule.private_static_readonly_all_lower.style = all_lower dotnet_naming_symbols.local_constants.applicable_kinds = local dotnet_naming_symbols.local_constants.required_modifiers = const dotnet_naming_rule.local_const_all_lower.severity = warning dotnet_naming_rule.local_const_all_lower.symbols = local_constants dotnet_naming_rule.local_const_all_lower.style = all_lower #ALL_UPPER for non private constants/static readonlys dotnet_naming_style.all_upper.capitalization = all_upper dotnet_naming_style.all_upper.word_separator = _ dotnet_naming_symbols.public_constants.applicable_accessibilities = public,internal,protected,protected_internal,private_protected dotnet_naming_symbols.public_constants.required_modifiers = const dotnet_naming_symbols.public_constants.applicable_kinds = field dotnet_naming_rule.public_const_all_upper.severity = warning dotnet_naming_rule.public_const_all_upper.symbols = public_constants dotnet_naming_rule.public_const_all_upper.style = all_upper dotnet_naming_symbols.public_static_readonly.applicable_accessibilities = public,internal,protected,protected_internal,private_protected dotnet_naming_symbols.public_static_readonly.required_modifiers = static,readonly dotnet_naming_symbols.public_static_readonly.applicable_kinds = field dotnet_naming_rule.public_static_readonly_all_upper.severity = warning dotnet_naming_rule.public_static_readonly_all_upper.symbols = public_static_readonly dotnet_naming_rule.public_static_readonly_all_upper.style = all_upper #Roslyn formating options #Formatting - indentation options csharp_indent_case_contents = true csharp_indent_case_contents_when_block = false csharp_indent_labels = one_less_than_current csharp_indent_switch_labels = true #Formatting - new line options csharp_new_line_before_catch = true csharp_new_line_before_else = true csharp_new_line_before_finally = true csharp_new_line_before_open_brace = all #csharp_new_line_before_members_in_anonymous_types = true #csharp_new_line_before_members_in_object_initializers = true # Currently no effect in VS/dotnet format (16.4), and makes Rider confusing csharp_new_line_between_query_expression_clauses = true #Formatting - organize using options dotnet_sort_system_directives_first = true #Formatting - spacing options csharp_space_after_cast = false csharp_space_after_colon_in_inheritance_clause = true csharp_space_after_keywords_in_control_flow_statements = true csharp_space_before_colon_in_inheritance_clause = true csharp_space_between_method_call_empty_parameter_list_parentheses = false csharp_space_between_method_call_name_and_opening_parenthesis = false csharp_space_between_method_call_parameter_list_parentheses = false csharp_space_between_method_declaration_empty_parameter_list_parentheses = false csharp_space_between_method_declaration_parameter_list_parentheses = false #Formatting - wrapping options csharp_preserve_single_line_blocks = true csharp_preserve_single_line_statements = true #Roslyn language styles #Style - this. qualification dotnet_style_qualification_for_field = false:warning dotnet_style_qualification_for_property = false:warning dotnet_style_qualification_for_method = false:warning dotnet_style_qualification_for_event = false:warning #Style - type names dotnet_style_predefined_type_for_locals_parameters_members = true:warning dotnet_style_predefined_type_for_member_access = true:warning csharp_style_var_when_type_is_apparent = true:none csharp_style_var_for_built_in_types = false:warning csharp_style_var_elsewhere = true:silent #Style - modifiers dotnet_style_require_accessibility_modifiers = for_non_interface_members:warning csharp_preferred_modifier_order = public,private,protected,internal,new,abstract,virtual,sealed,override,static,readonly,extern,unsafe,volatile,async:warning #Style - parentheses # Skipped because roslyn cannot separate +-*/ with << >> #Style - expression bodies csharp_style_expression_bodied_accessors = true:warning csharp_style_expression_bodied_constructors = false:none csharp_style_expression_bodied_indexers = true:warning csharp_style_expression_bodied_methods = false:silent csharp_style_expression_bodied_operators = true:warning csharp_style_expression_bodied_properties = true:warning csharp_style_expression_bodied_local_functions = true:silent #Style - expression preferences dotnet_style_object_initializer = true:warning dotnet_style_collection_initializer = true:warning dotnet_style_prefer_inferred_anonymous_type_member_names = true:warning dotnet_style_prefer_auto_properties = true:warning dotnet_style_prefer_conditional_expression_over_assignment = true:silent dotnet_style_prefer_conditional_expression_over_return = true:silent dotnet_style_prefer_compound_assignment = true:warning #Style - null/type checks dotnet_style_coalesce_expression = true:warning dotnet_style_null_propagation = true:warning csharp_style_pattern_matching_over_is_with_cast_check = true:warning csharp_style_pattern_matching_over_as_with_null_check = true:warning csharp_style_throw_expression = true:silent csharp_style_conditional_delegate_call = true:warning #Style - unused dotnet_style_readonly_field = true:silent dotnet_code_quality_unused_parameters = non_public:silent csharp_style_unused_value_expression_statement_preference = discard_variable:silent csharp_style_unused_value_assignment_preference = discard_variable:warning #Style - variable declaration csharp_style_inlined_variable_declaration = true:warning csharp_style_deconstructed_variable_declaration = false:silent #Style - other C# 7.x features dotnet_style_prefer_inferred_tuple_names = true:warning csharp_prefer_simple_default_expression = true:warning csharp_style_pattern_local_over_anonymous_function = true:warning dotnet_style_prefer_is_null_check_over_reference_equality_method = true:silent #Style - C# 8 features csharp_prefer_static_local_function = true:warning csharp_prefer_simple_using_statement = true:silent csharp_style_prefer_index_operator = false:silent csharp_style_prefer_range_operator = false:silent csharp_style_prefer_switch_expression = false:none #Style - C# 10 features csharp_style_namespace_declarations = file_scoped:suggestion #Style - C# 12 features csharp_style_prefer_primary_constructors = false [*.{yaml,yml}] insert_final_newline = true indent_style = space indent_size = 2 trim_trailing_whitespace = true dotnet_diagnostic.OLOC001.words_in_name = 5 dotnet_diagnostic.OLOC001.license_header = // Copyright (c) andy840119 . Licensed under the GPL Licence.\n// See the LICENCE file in the repository root for full licence text. ================================================ FILE: .git-blame-ignore-revs ================================================ # Partial everything c3d76bde9f3d78b420ab6a18b8191c00a503b2be # Make test project to the file-scope namespace 3546a366f24d23dee1188758df0f8c6744dd1001 # Make main project to the file-scope namespace 5597711949c30a9699c4ab3957c0d98e6ff2212c # Apply the trailing comma in the whole solution. 70fa847a40aa6a6c8ea3f1b277e4c2a23c8484a4 ================================================ FILE: .gitattributes ================================================ # Autodetect text files and ensure that we normalise their # line endings to lf internally. When checked out they may # use different line endings. * text=auto # Check out with crlf (Windows) line endings *.sln text eol=crlf *.csproj text eol=crlf *.cs text diff=csharp eol=crlf *.resx text eol=crlf *.vsixmanifest text eol=crlf packages.config text eol=crlf App.config text eol=crlf *.bat text eol=crlf *.cmd text eol=crlf *.snippet text eol=crlf *.manifest text eol=crlf *.licenseheader text eol=crlf # Check out with lf (UNIX) line endings *.sh text eol=lf .gitignore text eol=lf .gitattributes text eol=lf *.md text eol=lf .travis.yml text eol=lf ================================================ FILE: .github/CODEOWNERS ================================================ # License related /CodeAnalysis/ @andy840119 /LICENSE @andy840119 /README.md @andy840119 /osu.licenseheader @andy840119 # CI config /.github/ @andy840119 /Directory.Build.props @andy840119 /appveyor.yml @andy840119 /cake.config @andy840119 # Resource related /assets/ @andy840119 /osu.Game.Rulesets.Karaoke/Resources/ @andy840119 /osu.Game.Rulesets.Karaoke.Tests/Resources/ @andy840119 # Editor or git config /.config/dotnet-tools.json @andy840119 /.editorconfig @andy840119 /.gitattributes @andy840119 /.gitignore @andy840119 /osu.Game.Rulesets.Karaoke.sln @andy840119 /osu.Game.Rulesets.Karaoke.sln.DotSettings @andy840119 ================================================ FILE: .github/labeler.yml ================================================ localization: - osu.Game.Rulesets.Karaoke/Localisation/* ================================================ FILE: .github/workflows/ci.yml ================================================ name: .NET Core on: push: branches: [ master ] pull_request: branches: [ master ] jobs: build: name: Build and Test runs-on: ubuntu-latest steps: - name: Checkout repository uses: actions/checkout@v2 - name: Install dependencies run: dotnet restore - name: Build with .NET run: dotnet build --no-restore --configuration Release - name: Unit Tests run: dotnet test --no-build --no-restore --configuration Release ================================================ FILE: .github/workflows/crowdin.yml ================================================ name: Crowdin Action on: push: branches: [ master ] paths: - 'osu.Game.Rulesets.Karaoke/Localisation/**' jobs: generate-localization-file: name: Generate the localization file runs-on: ubuntu-latest steps: - name: Checkout repository uses: actions/checkout@v2 - name: Install dependencies run: dotnet restore - name: Install the localization tools. run: dotnet tool restore - name: Generate the localization file run: dotnet localisation to-resx ./osu.Game.Rulesets.Karaoke/osu.Game.Rulesets.Karaoke.csproj --output ./crowdin - name: Upload the localization into crowdin uses: crowdin/github-action@1.4.9 with: # upload the source to the target path of the https://github.com/karaoke-dev/karaoke-resources # see the document in the https://support.crowdin.com/configuration-file/?q=dest upload_sources: true upload_sources_args: '--dest master/osu.Game.Rulesets.Karaoke.Resources/Localisation/%file_name%.%file_extension%' source: crowdin/*.resx # there's no translation can be uploaded in this repo, but we still need to give it a value. translation: crowdin/%file_name%.%locale%.%file_extension% # This is a numeric id, not to be confused with Crowdin API v1 "project identifier" string. # See "API v2" on https://crowdin.com/project//settings#api project_id: ${{ secrets.CROWDIN_PROJECT_ID }} # A personal access token, not to be confused with Crowdin API v1 "API key". # See https://crowdin.com/settings#api-key to generate a token. token: ${{ secrets.CROWDIN_PERSONAL_TOKEN }} env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }} CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }} - name: Send notification after upload success or failed. uses: mshick/add-pr-comment@v2 with: if: always() message: | 🚀🚀🚀 New localization string has been successfully uploaded to the crowdin 🚀🚀🚀 Go to https://crowdin.com/project/karaoke-dev to fill the translation. message-failure: | 💥💥💥 New localization string uploaded failed to the crowdin. 💥💥💥 You can ignore this message or contact to the developer if you want to translate it now. ================================================ FILE: .github/workflows/dotnet-format.yml ================================================ name: Format check on pull request on: pull_request jobs: dotnet-format: runs-on: ubuntu-latest steps: - name: Checkout repo uses: actions/checkout@v4 - name: Install .NET 8.0.x uses: actions/setup-dotnet@v4 with: dotnet-version: "8.0.x" - name: Restore Tools run: dotnet tool restore - name: Restore Packages run: dotnet restore osu.Game.Rulesets.Karaoke.sln - name: Restore inspectcode cache uses: actions/cache@v4 with: path: ${{ github.workspace }}/inspectcode key: inspectcode-${{ hashFiles('.config/dotnet-tools.json', '.github/workflows/ci.yml', 'osu*.sln', '.editorconfig', '.globalconfig', 'CodeAnalysis/*', '**/*.csproj', '**/*.props') }} - name: Dotnet code style # The EnforceCodeStyleInBuild might cause false positive errors, disabling. # run: dotnet build -c Debug -warnaserror osu.Game.Rulesets.Karaoke.sln -p:EnforceCodeStyleInBuild=true run: dotnet build -c Debug -warnaserror osu.Game.Rulesets.Karaoke.sln - name: CodeFileSanity run: | exit_code=0 while read -r line; do if [[ ! -z "$line" ]]; then echo "::error::$line" exit_code=1 fi done <<< $(dotnet codefilesanity) exit $exit_code - name: InspectCode # Still use XML output since vika's poor support for new formats run: dotnet jb inspectcode $(pwd)/osu.Game.Rulesets.Karaoke.sln --no-build -f="xml" --output="inspectcodereport.xml" --caches-home="inspectcode" --verbosity=WARN - name: NVika run: dotnet nvika parsereport "${{github.workspace}}/inspectcodereport.xml" --treatwarningsaserrors ================================================ FILE: .github/workflows/labeler.yml ================================================ name: Pull Request Labeler on: pull_request_target: paths: # we only add use this action for add the localization label for now, so run this action if localization changed. - 'osu.Game.Rulesets.Karaoke/Localisation/**' jobs: triage: permissions: contents: read pull-requests: write runs-on: ubuntu-latest steps: - uses: actions/labeler@v4 with: repo-token: "${{ secrets.GITHUB_TOKEN }}" ================================================ FILE: .github/workflows/release.yml ================================================ name: Tagged Release on: push: tags: ['*'] jobs: build: name: Build and Create Release runs-on: ubuntu-latest steps: - name: Checkout repository uses: actions/checkout@v2 - name: Setup .NET 8.0.x uses: actions/setup-dotnet@v3 with: dotnet-version: "8.0.x" - name: Fetch all tags run: git fetch --depth=1 origin +refs/tags/*:refs/tags/* - name: Get current tag run: echo "CURRENT_TAG=$(git describe --abbrev=0 --tags $(git rev-list --tags --max-count=1))" >> $GITHUB_ENV - name: Install dependencies run: dotnet restore - name: Build run: dotnet build osu.Game.Rulesets.Karaoke --configuration Release -p:version=${{env.CURRENT_TAG}} --no-restore - name: Create Release id: create_release uses: actions/create-release@latest env: GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }} with: tag_name: ${{ github.ref }} release_name: ${{ github.ref }} - name: Zip the dlls run: | cd osu.Game.Rulesets.Karaoke/bin/Release/net8.0/DLLs zip -r ../osu.Game.Rulesets.Karaoke.zip ./* - name: Upload Release Asset uses: softprops/action-gh-release@v1 with: token: ${{ secrets.RELEASE_TOKEN }} files: | osu.Game.Rulesets.Karaoke/bin/Release/net8.0/osu.Game.Rulesets.Karaoke.zip draft: true body: | Thank you for showing interest in this ruleset. This is a tagged release (${{ env.CURRENT_TAG }}). - name: Generate changelog run: | sudo npm install github-release-notes -g gren release -T ${{secrets.RELEASE_TOKEN}} --tags=${{env.CURRENT_TAG}} --override ================================================ FILE: .gitignore ================================================ ## Ignore Visual Studio temporary files, build results, and ## files generated by popular Visual Studio add-ons. # User-specific files *.suo *.user *.userosscache *.sln.docstates # User-specific files (MonoDevelop/Xamarin Studio) *.userprefs # Build results [Dd]ebug/ [Dd]ebugPublic/ [Rr]elease/ [Rr]eleases/ bld/ [Bb]in/ [Oo]bj/ [Ll]og/ # Visual Studio 2015 cache/options directory .vs/ # Uncomment if you have tasks that create the project's static files in wwwroot #wwwroot/ # MSTest test Results [Tt]est[Rr]esult*/ [Bb]uild[Ll]og.* # NUNIT *.VisualState.xml TestResult.xml # Build Results of an ATL Project [Dd]ebugPS/ [Rr]eleasePS/ dlldata.c # DNX project.lock.json project.fragment.lock.json artifacts/ *_i.c *_p.c *_i.h *.ilk *.meta *.obj *.pch *.pdb *.pgc *.pgd *.rsp *.sbr *.tlb *.tli *.tlh *.tmp *.tmp_proj *.log *.vspscc *.vssscc .builds *.pidb *.svclog *.scc # Chutzpah Test files _Chutzpah* # Visual C++ cache files ipch/ *.aps *.ncb *.opendb *.opensdf *.sdf *.cachefile *.VC.db *.VC.VC.opendb # Visual Studio profiler *.psess *.vsp *.vspx *.sap # TFS 2012 Local Workspace $tf/ # Guidance Automation Toolkit *.gpState # ReSharper is a .NET coding add-in _ReSharper*/ *.[Rr]e[Ss]harper *.DotSettings.user # JustCode is a .NET coding add-in .JustCode # TeamCity is a build add-in _TeamCity* # DotCover is a Code Coverage Tool *.dotCover # NCrunch _NCrunch_* .*crunch*.local.xml nCrunchTemp_* # MightyMoose *.mm.* AutoTest.Net/ # Web workbench (sass) .sass-cache/ # Installshield output folder [Ee]xpress/ # DocProject is a documentation generator add-in DocProject/buildhelp/ DocProject/Help/*.HxT DocProject/Help/*.HxC DocProject/Help/*.hhc DocProject/Help/*.hhk DocProject/Help/*.hhp DocProject/Help/Html2 DocProject/Help/html # Click-Once directory publish/ # Publish Web Output *.[Pp]ublish.xml *.azurePubxml # TODO: Comment the next line if you want to checkin your web deploy settings # but database connection strings (with potential passwords) will be unencrypted *.pubxml *.publishproj # Microsoft Azure Web App publish settings. Comment the next line if you want to # checkin your Azure Web App publish settings, but sensitive information contained # in these scripts will be unencrypted PublishScripts/ # NuGet Packages *.nupkg # The packages folder can be ignored because of Package Restore **/packages/* # except build/, which is used as an MSBuild target. !**/packages/build/ # Uncomment if necessary however generally it will be regenerated when needed #!**/packages/repositories.config # NuGet v3's project.json files produces more ignoreable files *.nuget.props *.nuget.targets # Microsoft Azure Build Output csx/ *.build.csdef # Microsoft Azure Emulator ecf/ rcf/ # Windows Store app package directories and files AppPackages/ BundleArtifacts/ Package.StoreAssociation.xml _pkginfo.txt # Visual Studio cache files # files ending in .cache can be ignored *.[Cc]ache # but keep track of directories ending in .cache !*.[Cc]ache/ # Others ClientBin/ ~$* *~ *.dbmdl *.dbproj.schemaview *.pfx *.publishsettings node_modules/ orleans.codegen.cs Resource.designer.cs # Since there are multiple workflows, uncomment next line to ignore bower_components # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) #bower_components/ # RIA/Silverlight projects Generated_Code/ # Backup & report files from converting an old project file # to a newer Visual Studio version. Backup files are not needed, # because we have git ;-) _UpgradeReport_Files/ Backup*/ UpgradeLog*.XML UpgradeLog*.htm # SQL Server files *.mdf *.ldf # Business Intelligence projects *.rdl.data *.bim.layout *.bim_*.settings # Microsoft Fakes FakesAssemblies/ # GhostDoc plugin setting file *.GhostDoc.xml # Node.js Tools for Visual Studio .ntvs_analysis.dat # Visual Studio 6 build log *.plg # Visual Studio 6 workspace options file *.opt # Visual Studio LightSwitch build output **/*.HTMLClient/GeneratedArtifacts **/*.DesktopClient/GeneratedArtifacts **/*.DesktopClient/ModelManifest.xml **/*.Server/GeneratedArtifacts **/*.Server/ModelManifest.xml _Pvt_Extensions # Paket dependency manager .paket/paket.exe paket-files/ # FAKE - F# Make .fake/ # JetBrains Rider .idea/ *.sln.iml # Python Tools for Visual Studio (PTVS) __pycache__/ *.pyc # Cake # /tools/** /build/tools/** /build/temp/** # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 # User-specific stuff .idea/**/workspace.xml .idea/**/tasks.xml .idea/**/usage.statistics.xml .idea/**/dictionaries .idea/**/shelf # Generated files .idea/**/contentModel.xml # Sensitive or high-churn files .idea/**/dataSources/ .idea/**/dataSources.ids .idea/**/dataSources.local.xml .idea/**/sqlDataSources.xml .idea/**/dynamic.xml .idea/**/uiDesigner.xml .idea/**/dbnavigator.xml # Gradle .idea/**/gradle.xml .idea/**/libraries # Gradle and Maven with auto-import # When using Gradle or Maven with auto-import, you should exclude module files, # since they will be recreated, and may cause churn. Uncomment if using # auto-import. .idea/modules.xml .idea/*.iml .idea/modules *.iml *.ipr # CMake cmake-build-*/ # Mongo Explorer plugin .idea/**/mongoSettings.xml # File-based project format *.iws # IntelliJ out/ # mpeltonen/sbt-idea plugin .idea_modules/ # JIRA plugin atlassian-ide-plugin.xml # Cursive Clojure plugin .idea/replstate.xml # Crashlytics plugin (for Android Studio and IntelliJ) com_crashlytics_export_strings.xml crashlytics.properties crashlytics-build.properties fabric.properties # Editor-based Rest Client .idea/httpRequests # Android studio 3.1+ serialized cache file .idea/caches/build_file_checksums.ser # fastlane fastlane/report.xml # inspectcode inspectcodereport.xml inspectcode # BenchmarkDotNet /BenchmarkDotNet.Artifacts *.GeneratedMSBuildEditorConfig.editorconfig ================================================ FILE: .globalconfig ================================================ is_global = true # .NET Code Style # IDE styles reference: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ # IDE0001: Simplify names dotnet_diagnostic.IDE0001.severity = warning # IDE0002: Simplify member access dotnet_diagnostic.IDE0002.severity = warning # IDE0003: Remove qualification dotnet_diagnostic.IDE0003.severity = warning # IDE0004: Remove unnecessary cast dotnet_diagnostic.IDE0004.severity = warning # IDE0005: Remove unnecessary imports dotnet_diagnostic.IDE0005.severity = warning # IDE0034: Simplify default literal dotnet_diagnostic.IDE0034.severity = warning # IDE0036: Sort modifiers dotnet_diagnostic.IDE0036.severity = warning # IDE0040: Add accessibility modifier dotnet_diagnostic.IDE0040.severity = warning # IDE0049: Use keyword for type name dotnet_diagnostic.IDE0040.severity = warning # IDE0055: Fix formatting dotnet_diagnostic.IDE0055.severity = warning # IDE0051: Private method is unused dotnet_diagnostic.IDE0051.severity = silent # IDE0052: Private member is unused dotnet_diagnostic.IDE0052.severity = silent # IDE0073: File header dotnet_diagnostic.IDE0073.severity = warning # IDE0130: Namespace mismatch with folder dotnet_diagnostic.IDE0130.severity = warning # IDE1006: Naming style dotnet_diagnostic.IDE1006.severity = warning #Disable operator overloads requiring alternate named methods dotnet_diagnostic.CA2225.severity = none # Banned APIs dotnet_diagnostic.RS0030.severity = error ================================================ FILE: CONTRIBUTING.md ================================================ # Contributing Guidelines Thank you for showing interest in the development of karaoke ruleset. We aim to provide a good collaborating environment for everyone involved, and as such have decided to list some of the most important things to keep in mind in the process. The guidelines below have been chosen based on past experience. ## Table of contents 1. [Reporting bugs](#reporting-bugs) 2. [Providing general feedback](#providing-general-feedback) 3. [Issue or discussion?](#issue-or-discussion) 4. [Submitting pull requests](#submitting-pull-requests) 5. [Resources](#resources) ## Reporting bugs A **bug** is a situation in which there is something clearly *and objectively* wrong with the ruleset. Examples of applicable bug reports are: - The ruleset crashes to desktop when I start a beatmap - Cannot load karoake beatmap that edited before. - The ruleset slows down a lot when I play this specific map. - Text effect looks weird if using directX renderer. To track bug reports, we primarily use GitHub **issues**. When opening an issue, please keep in mind the following: - Before opening the issue, please search for any similar existing issues using the text search bar and the issue labels. This includes both open and closed issues (we may have already fixed something, but the fix hasn't yet been released). - When opening the issue, please fill out as much of the issue template as you can. In particular, please make sure to include logs and screenshots as much as possible. The instructions on how to find the log files are included in the issue template. - We may ask you for follow-up information to reproduce or debug the problem. Please look out for this and provide follow-up info if we request it. If we cannot reproduce the issue, it is deemed low priority, or it is deemed to be specific to your setup in some way, the issue may be downgraded to a discussion. This will be done by a maintainer for you. ## Providing general feedback If you wish to: - Provide *subjective* feedback on the ruleset (about how the UI looks, about how the default skin works, about ruleset mechanics, about how the PP and scoring systems work, etc.), - Suggest a new feature to be added to the ruleset. - Report a non-specific problem with the ruleset that you think may be connected to your hardware or operating system specifically. then it is generally best to start with a **discussion** first. Discussions are a good avenue to group subjective feedback on a single topic, or gauge interest in a particular feature request. When opening a discussion, please keep in mind the following: - Use the search function to see if your idea has been proposed before, or if there is already a thread about a particular issue you wish to raise. - If proposing a feature, please try to explain the feature in as much detail as possible. - If you're reporting a non-specific problem, please provide applicable logs, screenshots, or video that illustrate the issue. If a discussion gathers enough traction, then it may be converted into an issue. This will be done by a maintainer for you. ## Issue or discussion? We realise that the line between an issue and a discussion may be fuzzy, so while we ask you to use your best judgement based on the description above, please don't think about it too hard either. Feedback in a slightly wrong place is better than no feedback at all. When in doubt, it's probably best to start with a discussion first. We will escalate to issues as needed. ## Submitting pull requests The [issue tracker](https://github.com/karaoke-dev/karaoke/issues) should provide plenty of issues to start with. We also have a [`Good for contributor`](https://github.com/karaoke-dev/karaoke/issues?q=is%3Aissue+is%3Aopen+label%3A"Good+for+contributor") label. In the case of simple issues, a direct PR is okay. However, if you decide to work on an existing issue which doesn't seem trivial, **please ask us first**. This way we can try to estimate if it is a good fit for you and provide the correct direction on how to address it. In addition, note that while we do not rule out external contributors from working on roadmapped issues, we will generally prefer to handle them ourselves unless they're not very time sensitive. If you'd like to propose a subjective change to one of the visual aspects of the ruleset, or there is a bigger task you'd like to work on, but there is no corresponding issue or discussion thread yet for it, **please open a discussion or issue first** to avoid wasted effort. Aside from the above, below is a brief checklist of things to watch out when you're preparing your code changes: - Make sure you're comfortable with the principles of object-oriented programming, the syntax of `C\#` and your `development environment`. - Make sure you are familiar with [git](https://git-scm.com/) and [the pull request workflow](https://help.github.com/en/github/collaborating-with-issues-and-pull-requests/proposing-changes-to-your-work-with-pull-requests). - Discuss with us first if you're planning to work on a `bigger change`, or `it's UI/UX related`. - Please do not make code changes via the GitHub web interface. - Please add tests for your changes. We expect most new features and bugfixes to have test coverage, unless the effort of adding them is prohibitive. The visual testing methodology we use is described in more detail [here](https://github.com/ppy/osu-framework/wiki/Development-and-Testing). After you're done with your changes and you wish to open the PR, please observe the following recommendations: - Please submit the pull request from a [topic branch](https://git-scm.com/book/en/v2/Git-Branching-Branching-Workflows#_topic_branch) (not `master`), and keep the *Allow edits from maintainers* check box selected, so that we can push fixes to your PR if necessary. - Please write the commit messages with useful information in mind. We recommend reading [this article](https://chris.beams.io/posts/git-commit/) for some good tips. - Unlike osu! project, `force-push` is allowed in here. Use it if you need to. - Rebase the PR branch to the `master` if PR is too old or has conflicts. We are highly committed to quality when it comes to the karaoke ruleset project. This means that contributions from less experienced community members can take multiple rounds of review to get to a mergeable state. If you're uncertain about some part of the codebase or some inner workings of the karaoke ruleset, please reach out either by leaving a comment in the relevant issue, discussion, or PR thread, or by posting a message in the [development Discord server](https://discord.gg/ga2xZXk). We will try to help you as much as we can. ## Resources - [`ppy/osu`](https://github.com/ppy/osu): The game that karaoke ruleset is running on. - [`ppy/osu` wiki](https://github.com/ppy/osu/wiki): Contains articles about various technical aspects of the game - [`ppy/osu-framework`](https://github.com/ppy/osu-framework): The game framework that karaoke ruleset is running on. - [`ppy/osu-framework` wiki](https://github.com/ppy/osu-framework/wiki): Contains introductory information about osu!framework, the bespoke 2D game framework we use for the game. . ================================================ FILE: CodeAnalysis/BannedSymbols.txt ================================================ M:System.Object.Equals(System.Object,System.Object)~System.Boolean;Don't use object.Equals. Use IEquatable or EqualityComparer.Default instead. M:System.Object.Equals(System.Object)~System.Boolean;Don't use object.Equals. Use IEquatable or EqualityComparer.Default instead. M:System.ValueType.Equals(System.Object)~System.Boolean;Don't use object.Equals(Fallbacks to ValueType). Use IEquatable or EqualityComparer.Default instead. M:System.Nullable`1.Equals(System.Object)~System.Boolean;Use == instead. T:System.IComparable;Don't use non-generic IComparable. Use generic version instead. T:SixLabors.ImageSharp.IDeepCloneable`1;Use osu.Game.Utils.IDeepCloneable instead. M:osu.Framework.Graphics.Sprites.SpriteText.#ctor;Use OsuSpriteText. M:osu.Framework.Bindables.IBindableList`1.GetBoundCopy();Fails on iOS. Use manual ctor + BindTo instead. (see https://github.com/mono/mono/issues/19900) T:Microsoft.EntityFrameworkCore.Internal.EnumerableExtensions;Don't use internal extension methods. T:Microsoft.EntityFrameworkCore.Internal.TypeExtensions;Don't use internal extension methods. T:NuGet.Packaging.CollectionExtensions;Don't use internal extension methods. M:Realms.IRealmCollection`1.SubscribeForNotifications`1(Realms.NotificationCallbackDelegate{``0});Use osu.Game.Database.RealmObjectExtensions.QueryAsyncWithNotifications(IRealmCollection,NotificationCallbackDelegate) instead. M:System.Guid.#ctor;Probably meaning to use Guid.NewGuid() instead. If actually wanting empty, use Guid.Empty. M:Realms.CollectionExtensions.SubscribeForNotifications`1(System.Linq.IQueryable{``0},Realms.NotificationCallbackDelegate{``0});Use osu.Game.Database.RealmObjectExtensions.QueryAsyncWithNotifications(IQueryable,NotificationCallbackDelegate) instead. M:Realms.CollectionExtensions.SubscribeForNotifications`1(System.Collections.Generic.IList{``0},Realms.NotificationCallbackDelegate{``0});Use osu.Game.Database.RealmObjectExtensions.QueryAsyncWithNotifications(IList,NotificationCallbackDelegate) instead. M:System.Threading.Tasks.Task.Wait();Don't use Task.Wait. Use Task.WaitSafely() to ensure we avoid deadlocks. P:System.Threading.Tasks.Task`1.Result;Don't use Task.Result. Use Task.GetResultSafely() to ensure we avoid deadlocks. ================================================ FILE: CodeAnalysis/osu.ruleset ================================================  ================================================ FILE: Directory.Build.props ================================================ 12.0 true enable osu.licenseheader $(MSBuildThisFileDirectory)CodeAnalysis\osu.ruleset true $(NoWarn);CS1591 ================================================ FILE: LICENSE ================================================ GNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The GNU General Public License is a free, copyleft license for software and other kinds of works. The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. We, the Free Software Foundation, use the GNU General Public License for most of our software; it applies also to any other work released this way by its authors. You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things. To protect your rights, we need to prevent others from denying you these rights or asking you to surrender the rights. Therefore, you have certain responsibilities if you distribute copies of the software, or if you modify it: responsibilities to respect the freedom of others. For example, if you distribute copies of such a program, whether gratis or for a fee, you must pass on to the recipients the same freedoms that you received. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. Developers that use the GNU GPL protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License giving you legal permission to copy, distribute and/or modify it. For the developers' and authors' protection, the GPL clearly explains that there is no warranty for this free software. For both users' and authors' sake, the GPL requires that modified versions be marked as changed, so that their problems will not be attributed erroneously to authors of previous versions. Some devices are designed to deny users access to install or run modified versions of the software inside them, although the manufacturer can do so. This is fundamentally incompatible with the aim of protecting users' freedom to change the software. The systematic pattern of such abuse occurs in the area of products for individuals to use, which is precisely where it is most unacceptable. Therefore, we have designed this version of the GPL to prohibit the practice for those products. If such problems arise substantially in other domains, we stand ready to extend this provision to those domains in future versions of the GPL, as needed to protect the freedom of users. Finally, every program is threatened constantly by software patents. States should not allow patents to restrict development and use of software on general-purpose computers, but in those that do, we wish to avoid the special danger that patents applied to a free program could make it effectively proprietary. To prevent this, the GPL assures that patents cannot be used to render the program non-free. The precise terms and conditions for copying, distribution and modification follow. TERMS AND CONDITIONS 0. Definitions. "This License" refers to version 3 of the GNU General Public License. "Copyright" also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. "The Program" refers to any copyrightable work licensed under this License. Each licensee is addressed as "you". "Licensees" and "recipients" may be individuals or organizations. To "modify" a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a "modified version" of the earlier work or a work "based on" the earlier work. A "covered work" means either the unmodified Program or a work based on the Program. To "propagate" a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well. To "convey" a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying. An interactive user interface displays "Appropriate Legal Notices" to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion. 1. Source Code. The "source code" for a work means the preferred form of the work for making modifications to it. "Object code" means any non-source form of a work. A "Standard Interface" means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language. The "System Libraries" of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A "Major Component", in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it. The "Corresponding Source" for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work. The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source. The Corresponding Source for a work in source code form is that same work. 2. Basic Permissions. All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law. You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you. Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary. 3. Protecting Users' Legal Rights From Anti-Circumvention Law. No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures. When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures. 4. Conveying Verbatim Copies. You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program. You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee. 5. Conveying Modified Source Versions. You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions: a) The work must carry prominent notices stating that you modified it, and giving a relevant date. b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to "keep intact all notices". c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it. d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so. A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an "aggregate" if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate. 6. Conveying Non-Source Forms. You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways: a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange. b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge. c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b. d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements. e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d. A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work. A "User Product" is either (1) a "consumer product", which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, "normally used" refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product. "Installation Information" for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made. If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM). The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network. Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying. 7. Additional Terms. "Additional permissions" are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions. When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission. Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms: a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or d) Limiting the use for publicity purposes of names of licensors or authors of the material; or e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors. All other non-permissive additional terms are considered "further restrictions" within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying. If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms. Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way. 8. Termination. You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11). However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation. Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice. Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10. 9. Acceptance Not Required for Having Copies. You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so. 10. Automatic Licensing of Downstream Recipients. Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License. An "entity transaction" is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts. You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it. 11. Patents. A "contributor" is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's "contributor version". A contributor's "essential patent claims" are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, "control" includes the right to grant patent sublicenses in a manner consistent with the requirements of this License. Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version. In the following three paragraphs, a "patent license" is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To "grant" such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party. If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. "Knowingly relying" means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid. If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it. A patent license is "discriminatory" if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007. Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law. 12. No Surrender of Others' Freedom. If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program. 13. Use with the GNU Affero General Public License. Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU Affero General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the special requirements of the GNU Affero General Public License, section 13, concerning interaction through a network will apply to the combination as such. 14. Revised Versions of this License. The Free Software Foundation may publish revised and/or new versions of the GNU General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU General Public License, you may choose any version ever published by the Free Software Foundation. If the Program specifies that a proxy can decide which future versions of the GNU General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program. Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version. 15. Disclaimer of Warranty. THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 16. Limitation of Liability. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 17. Interpretation of Sections 15 and 16. If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . Also add information on how to contact you by electronic and paper mail. If the program does terminal interaction, make it output a short notice like this when it starts in an interactive mode: Copyright (C) This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, your program's commands might be different; for a GUI interface, you would use an "about box". You should also get your employer (if you work as a programmer) or school, if any, to sign a "copyright disclaimer" for the program, if necessary. For more information on this, and how to apply and follow the GNU GPL, see . The GNU General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. But first, please read . ================================================ FILE: README.md ================================================ ![](assets/logo.png) # karaoke --dev [![CodeFactor](https://www.codefactor.io/repository/github/karaoke-dev/karaoke/badge)](https://www.codefactor.io/repository/github/karaoke-dev/karaoke) [![Build status](https://ci.appveyor.com/api/projects/status/07ytm0sei6l5oy08?svg=true)](https://ci.appveyor.com/project/andy840119/karaoke) [![Crowdin](https://badges.crowdin.net/karaoke-dev/localized.svg)](https://crowdin.com/project/karaoke-dev) [![Waifu](https://img.shields.io/badge/月子我婆-passed-ff69b4.svg)](https://github.com/karaoke-dev/karaoke) [![GitHub last commit](https://img.shields.io/github/last-commit/karaoke-dev/karaoke)](https://github.com/karaoke-dev/karaoke/releases) [![Tagged Release](https://github.com/karaoke-dev/karaoke/workflows/Tagged%20Release/badge.svg)](https://github.com/karaoke-dev/karaoke/releases) [![dev chat](https://discordapp.com/api/guilds/299006062323826688/widget.png?style=shield)](https://discord.gg/ga2xZXk) [![Total lines](https://tokei.rs/b1/github/karaoke-dev/karaoke)](https://ghloc.vercel.app/karaoke-dev/karaoke?branch=master) [![Dashboard](https://img.shields.io/badge/Dashboard-stonks!-informational)](https://www.repotrends.com/karaoke-dev/karaoke) [![Star History Chart](https://img.shields.io/github/stars/karaoke-dev/karaoke?style=flat&label=Stars&color=yellow&cacheSeconds=86000)](https://seladb.github.io/StarTrack-js/#/preload?r=karaoke-dev,karaoke) [![airpods pro](https://img.shields.io/badge/Andy's%20airpods%20pro-missing-red.svg)](https://github.com/karaoke-dev/karaoke/issues/1514) [![airpods 2](https://img.shields.io/badge/Andy's%20airpods%202-missing-red)](https://github.com/karaoke-dev/karaoke/issues/1513) The source code of the `karaoke` ruleset, running on [osu!lazer](https://github.com/ppy/osu). ## Status This project is still not very stable, `so we recommend looking around this project to find new features instead of actually using it`. Also: - This project doesn't have much of a [demo](https://github.com/karaoke-dev/sample-beatmap) currently available. And most demos are Japanese only. - This project is not very stable, especially in the editor. - Beatmap does not support save feature. Reccommend using this ruleset until [support batch import song](https://github.com/karaoke-dev/karaoke/issues/2144). If you run into any problems, you can shoot us an email (support@karaoke.dev) or send me a message on [Discord](https://discord.gg/ga2xZXk). I will typically reply faster on Discord. And feel free to report any bugs if found. ## How to run this project See [this tutorial](https://karaoke-dev.github.io/how-to-install/) to get the ruleset from the existing build. Or you can compile it yourself: `release build` then copy `Packed/osu.Game.Rulesets.Karaoke.dll` into your [ruleset folder](https://github.com/LumpBloom7/sentakki/wiki/Ruleset-installation-guide) ## License This repo is covered under the [GPL V3](LICENSE) license. If you plan on using this repo for commercial purposes, please contact us at (support@karaoke.dev) to get permission first. Using this repo to create, use or using the beatmap format to distribution any `PIRATED`, `unauthorized` karaoke songs/beatmaps is absolutely forbidden. This repo is trying to make song author(or people has copyright) ability to distribution their songs with karaoke format without any restriction, not for copycat to make thing with copyright issue. ## Thanks to - [osu!](https://github.com/ppy/osu) and it's [framework](https://github.com/ppy/osu-framework) for karaoke! - [RhythmKaTTE](http://juna-idler.blogspot.com/2016/05/rhythmkatte-version-01.html), [RhythmicaLyrics](http://suwa.pupu.jp/RhythmicaLyrics.html) and [Aegisub](https://github.com/Aegisub/Aegisub), an open-source software used to create lyrics with time tags. Parts of the lyrics editor in this ruleset were inspired by them. - [ニコカラメーカー](http://shinta0806be.ldblog.jp/tag/%E3%83%8B%E3%82%B3%E3%82%AB%E3%83%A9%E3%83%A1%E3%83%BC%E3%82%AB%E3%83%BC), a software to convert `.lrc` files into karaoke video with beautiful text effects. - [JetBrains](https://www.jetbrains.com/?from=osu-karaoke), for contributing a free [Rider](https://www.jetbrains.com/rider/) license used to developing. - [Appveyor](https://www.appveyor.com/), [CodeFactor](https://www.codefactor.io/) and [Github action](https://github.com/features/actions) for providing free `CI`/`CD` service. - [Figma](https://www.figma.com/), for quick creation of assets like logos or icon. - [Miro](https://miro.com/). Used for flow-charts and deciding how to structure some parts. ================================================ FILE: appveyor.yml ================================================ clone_depth: 1 version: '{branch}-{build}' image: Visual Studio 2022 branches: only: - master dotnet_csproj: patch: true file: 'osu.Game.Rulesets.Karaoke\osu.Game.Rulesets.Karaoke.csproj' # Use wildcard when it's able to exclude Xamarin projects version: '0.0.{build}' cache: - '%LOCALAPPDATA%\NuGet\v3-cache -> appveyor.yml' before_build: - ps: dotnet --info # Useful when version mismatch between CI and local - ps: nuget restore -verbosity quiet # Only nuget.exe knows both new (.NET Core) and old (Xamarin) projects build: project: osu.Game.Rulesets.Karaoke.sln parallel: true verbosity: minimal after_build: - ps: dotnet tool restore test: assemblies: except: - '**\*Android*' - '**\*iOS*' - 'build\**\*' ================================================ FILE: cake.config ================================================ [Nuget] Source=https://api.nuget.org/v3/index.json UseInProcessClient=true LoadDependencies=true ================================================ FILE: osu.Game.Rulesets.Karaoke/Beatmaps/ElementId.cs ================================================ // Copyright (c) andy840119 . Licensed under the GPL Licence. // See the LICENCE file in the repository root for full licence text. using System; using System.Linq; namespace osu.Game.Rulesets.Karaoke.Beatmaps; /// /// As unique identifier for the elements in the /// Like how works. /// public readonly struct ElementId : IComparable, IComparable, IEquatable { public static ElementId Empty => default; private const int length = 7; private readonly string? id; public ElementId(string id) { if (string.IsNullOrEmpty(id)) { throw new ArgumentException("id should not be empty", nameof(id)); } if (id.Length != length) { throw new ArgumentException($"id length must be {length}.", nameof(id)); } if (!checkFormat(id)) { throw new ArgumentException("id format is not correct", nameof(id)); } this.id = id; } // char should be 0~9 and a~f private static bool checkFormat(string id) => id.Where(c => c is < '0' or > '9').All(c => c >= 'a' && c <= 'f'); public static ElementId NewElementId() { // take 7 digits string str = Guid.NewGuid().ToString("N"); string id = str[..length]; return new ElementId(id); } public int CompareTo(ElementId other) { return string.Compare(id, other.id, StringComparison.Ordinal); } public int CompareTo(object? obj) { if (obj == null) { return 1; } if (obj is not ElementId elementId) { throw new ArgumentException("Compared object should be the same type.", nameof(obj)); } return CompareTo(elementId); } public bool Equals(ElementId other) { return id == other.id; } public override bool Equals(object? obj) { return obj is ElementId other && Equals(other); } public static bool operator ==(ElementId id1, ElementId id2) => id1.Equals(id2); public static bool operator !=(ElementId id1, ElementId id2) => !id1.Equals(id2); public override int GetHashCode() { return (id ?? string.Empty).GetHashCode(); } public override string ToString() { return id ?? string.Empty; } } ================================================ FILE: osu.Game.Rulesets.Karaoke/Beatmaps/Formats/KaraokeJsonBeatmapDecoder.cs ================================================ // Copyright (c) andy840119 . Licensed under the GPL Licence. // See the LICENCE file in the repository root for full licence text. using System; using System.Collections.Generic; using System.Linq; using Newtonsoft.Json; using Newtonsoft.Json.Serialization; using osu.Game.Beatmaps; using osu.Game.Beatmaps.Formats; using osu.Game.IO; using osu.Game.IO.Serialization; using osu.Game.Rulesets.Karaoke.IO.Serialization; namespace osu.Game.Rulesets.Karaoke.Beatmaps.Formats; public class KaraokeJsonBeatmapDecoder : JsonBeatmapDecoder { public new static void Register() { AddDecoder("// karaoke json file format v", m => new KaraokeJsonBeatmapDecoder()); // use this weird way to let all the fall-back beatmap(include karaoke beatmap) become karaoke beatmap. SetFallbackDecoder(() => new KaraokeJsonBeatmapDecoder()); } protected override void ParseStreamInto(LineBufferedReader stream, Beatmap output) { var globalSetting = KaraokeJsonSerializableExtensions.CreateGlobalSettings(); globalSetting.ContractResolver = new KaraokeBeatmapContractResolver(); // create id if object is by reference. globalSetting.PreserveReferencesHandling = PreserveReferencesHandling.Objects; // should not let json decoder to read this line. if (stream.PeekLine()?.Contains("// karaoke json file format v") ?? false) { stream.ReadLine(); } // equal to stream.ReadToEnd().DeserializeInto(output); in the base class. JsonConvert.PopulateObject(stream.ReadToEnd(), output, globalSetting); } private class KaraokeBeatmapContractResolver : SnakeCaseKeyContractResolver { protected override IList CreateProperties(Type type, MemberSerialization memberSerialization) { var props = base.CreateProperties(type, memberSerialization); return type == typeof(BeatmapInfo) ? props.Where(p => p.PropertyName != "ruleset_id").ToList() : props; } } } ================================================ FILE: osu.Game.Rulesets.Karaoke/Beatmaps/Formats/KaraokeJsonBeatmapEncoder.cs ================================================ // Copyright (c) andy840119 . Licensed under the GPL Licence. // See the LICENCE file in the repository root for full licence text. using Newtonsoft.Json; using osu.Game.Beatmaps; using osu.Game.Rulesets.Karaoke.IO.Serialization; namespace osu.Game.Rulesets.Karaoke.Beatmaps.Formats; public class KaraokeJsonBeatmapEncoder { public string Encode(Beatmap output) { var globalSetting = KaraokeJsonSerializableExtensions.CreateGlobalSettings(); string json = JsonConvert.SerializeObject(output, globalSetting); return "// karaoke json file format v1" + '\n' + json; } } ================================================ FILE: osu.Game.Rulesets.Karaoke/Beatmaps/Formats/KaraokeLegacyBeatmapDecoder.cs ================================================ // Copyright (c) andy840119 . Licensed under the GPL Licence. // See the LICENCE file in the repository root for full licence text. using System; using System.Collections.Generic; using System.Globalization; using System.Linq; using osu.Game.Beatmaps; using osu.Game.Beatmaps.Formats; using osu.Game.Rulesets.Karaoke.Edit.Generator.Lyrics.Notes; using osu.Game.Rulesets.Karaoke.Integration.Formats; using osu.Game.Rulesets.Karaoke.Objects; using osu.Game.Rulesets.Objects; namespace osu.Game.Rulesets.Karaoke.Beatmaps.Formats; public class KaraokeLegacyBeatmapDecoder : LegacyBeatmapDecoder { public new const int LATEST_VERSION = 1; public new static void Register() { AddDecoder("karaoke file format v", m => new KaraokeLegacyBeatmapDecoder(Parsing.ParseInt(m.Split('v').Last()))); // use this weird way to let all the fall-back beatmap(include karaoke beatmap) become karaoke beatmap. SetFallbackDecoder(() => new KaraokeLegacyBeatmapDecoder()); } public KaraokeLegacyBeatmapDecoder(int version = LATEST_VERSION) : base(version) { } private readonly IList karFormatLines = new List(); private readonly IList noteLines = new List(); private readonly IList translations = new List(); protected override void ParseLine(Beatmap beatmap, Section section, string line) { if (section != Section.HitObjects) { // should not let base.ParseLine read the line like "Mode: 111" if (line.StartsWith("Mode", StringComparison.Ordinal)) { beatmap.BeatmapInfo.Ruleset = new KaraokeRuleset().RulesetInfo; return; } base.ParseLine(beatmap, section, line); return; } if (line.ToLower().StartsWith("@ruby", StringComparison.Ordinal)) { // kar format queue karFormatLines.Add(line); } else if (line.ToLower().StartsWith("@note", StringComparison.Ordinal)) { // add tone line queue noteLines.Add(line); } else if (line.ToLower().StartsWith("@tr", StringComparison.Ordinal)) { // add translation queue translations.Add(line); } else if (line.StartsWith('@')) { // Remove @ in time tag and add into kar queue karFormatLines.Add(line[1..]); } else if (line.ToLower() == "end") { string content = string.Join("\n", karFormatLines); // Create decoder var decoder = new KarDecoder(); var lyrics = decoder.Decode(content); // Apply hitobjects beatmap.HitObjects = lyrics.OfType().ToList(); processNotes(beatmap, noteLines); processTranslations(beatmap, translations); } } private void processNotes(Beatmap beatmap, IList lines) { var noteGenerator = new NoteGenerator(new NoteGeneratorConfig()); // Remove all karaoke note beatmap.HitObjects.RemoveAll(x => x is Note); var lyrics = beatmap.HitObjects.OfType().ToList(); for (int l = 0; l < lyrics.Count; l++) { var lyric = lyrics[l]; string? line = lines.ElementAtOrDefault(l)?.Split('=').Last(); // Create default note if not exist if (string.IsNullOrEmpty(line)) { beatmap.HitObjects.AddRange(noteGenerator.Generate(lyric)); continue; } string[] notes = line.Split(','); var defaultNotes = noteGenerator.Generate(lyric).ToList(); int minNoteNumber = Math.Min(notes.Length, defaultNotes.Count); // Process each note for (int i = 0; i < minNoteNumber; i++) { string note = notes[i]; var defaultNote = defaultNotes[i]; // Support multi note in one time tag, format like ([1;0.5;か]|1#|...) if (!note.StartsWith('(') || !note.EndsWith(')')) { // Process and add note applyNote(defaultNote, note); beatmap.HitObjects.Add(defaultNote); } else { float startPercentage = 0; string[] rubyNotes = note.Replace("(", string.Empty).Replace(")", string.Empty).Split('|'); for (int j = 0; j < rubyNotes.Length; j++) { string rubyNote = rubyNotes[j]; string tone; float percentage = (float)Math.Round((float)1 / rubyNotes.Length, 2, MidpointRounding.AwayFromZero); string? ruby = defaultNote.RubyText?.ElementAtOrDefault(j).ToString(); // Format like [1;0.5;か] if (note.StartsWith('[') && note.EndsWith(']')) { string[] rubyNoteProperty = note.Replace("[", string.Empty).Replace("]", string.Empty).Split(';'); // Copy tome property tone = rubyNoteProperty[0]; // Copy percentage property if (rubyNoteProperty.Length >= 2) float.TryParse(rubyNoteProperty[1], out percentage); // Copy text property if (rubyNoteProperty.Length >= 3) ruby = rubyNoteProperty[2]; } else { tone = rubyNote; } // Split note and apply them var splitDefaultNote = SliceNote(defaultNote, startPercentage, percentage); startPercentage += percentage; if (!string.IsNullOrEmpty(ruby)) splitDefaultNote.Text = ruby; // Process and add note applyNote(splitDefaultNote, tone); beatmap.HitObjects.Add(splitDefaultNote); } } } } static void applyNote(Note note, string noteStr, string? ruby = null, double? duration = null) { if (noteStr == "-") note.Display = false; else { note.Display = true; note.Tone = convertTone(noteStr); } if (!string.IsNullOrEmpty(ruby)) note.Text = ruby; if (duration != null) note.Duration = duration.Value; //Support format : 1 1. 1.5 1+ 1# static Tone convertTone(string tone) { bool half = false; if (tone.Contains('.') || tone.Contains('#')) { half = true; // only get digit part tone = tone.Split('.').FirstOrDefault()?.Split('#').FirstOrDefault() ?? string.Empty; } if (!int.TryParse(tone, out int scale)) throw new InvalidCastException($"{tone} does not support in {nameof(KaraokeLegacyBeatmapDecoder)}"); return new Tone { Scale = scale, Half = half, }; } } } private void processTranslations(Beatmap beatmap, IEnumerable translationLines) { var availableTranslations = new List(); var lyrics = beatmap.HitObjects.OfType().ToList(); var translations = translationLines.Select(translation => new { key = translation.Split('=').FirstOrDefault()?.Split('[').LastOrDefault()?.Split(']').FirstOrDefault(), value = translation.Split('=').LastOrDefault() ?? string.Empty, }).GroupBy(x => x.key, y => y.value).ToList(); foreach (var translation in translations) { // get culture and translation string? languageCode = translation.Key; if (string.IsNullOrEmpty(languageCode)) continue; var cultureInfo = new CultureInfo(languageCode); var values = translation.ToList(); int size = Math.Min(lyrics.Count, translation.Count()); for (int j = 0; j < size; j++) { lyrics[j].Translations.Add(cultureInfo, values[j]); } availableTranslations.Add(cultureInfo); } var dictionary = new LegacyProperties { AvailableTranslationLanguages = availableTranslations, }; beatmap.HitObjects.Add(dictionary); } internal static Note SliceNote(Note note, double startPercentage, double durationPercentage) { if (startPercentage < 0 || startPercentage + durationPercentage > 1) throw new ArgumentOutOfRangeException($"{nameof(Note)} cannot assign split range of start from {startPercentage} and duration {durationPercentage}"); double durationFromStartTime = note.Duration * startPercentage; double secondNoteDuration = note.Duration * (1 - startPercentage - durationPercentage); // todo: there's no need to create the new note. var newNote = note.DeepClone(); newNote.StartTimeOffset = note.StartTimeOffset + durationFromStartTime; newNote.EndTimeOffset = note.EndTimeOffset - secondNoteDuration; return newNote; } } ================================================ FILE: osu.Game.Rulesets.Karaoke/Beatmaps/Formats/KaraokeLegacyBeatmapEncoder.cs ================================================ // Copyright (c) andy840119 . Licensed under the GPL Licence. // See the LICENCE file in the repository root for full licence text. using System; using System.Collections.Generic; using System.Linq; using osu.Game.Beatmaps; using osu.Game.Rulesets.Karaoke.Integration.Formats; using osu.Game.Rulesets.Karaoke.Objects; namespace osu.Game.Rulesets.Karaoke.Beatmaps.Formats; public class KaraokeLegacyBeatmapEncoder { public string Encode(Beatmap output) { var encoder = new KarEncoder(); var results = new List { encoder.Encode(output), string.Join("\n", encodeNotes(output)), string.Join("\n", encodeTranslations(output)), }; return string.Join("\n\n", results.Where(x => !string.IsNullOrEmpty(x))); } private IEnumerable encodeNotes(Beatmap output) { var notes = output.HitObjects.OfType().ToList(); var lyrics = output.HitObjects.OfType().ToList(); return notes.GroupBy(x => x.ReferenceLyric).Select(g => { var lyric = g.Key; if (lyric == null) throw new ArgumentNullException(); // Get note group var noteGroup = g.ToList().GroupBy(n => n.ReferenceTimeTagIndex); // Convert to group format string noteGroupStr = string.Join(",", noteGroup.Select(x => { if (x.Count() == 1) return convertNote(x.First()); return "(" + string.Join("|", x.Select(convertNote)) + ")"; })); return $"note{lyrics.IndexOf(lyric) + 1}={noteGroupStr}"; }).ToList(); // Convert single note static string convertNote(Note note) { return !note.Display ? "-" : convertTone(note.Tone); // Convert tone to string static string convertTone(Tone tone) => tone.Scale + (tone.Half ? "#" : string.Empty); } } private IEnumerable encodeTranslations(Beatmap output) { if (!output.AnyTranslation()) yield break; var lyrics = output.HitObjects.OfType().ToList(); var availableTranslations = output.AvailableTranslationLanguages(); foreach (var translation in availableTranslations) { foreach (var lyric in lyrics) { string translationString = lyric.Translations.TryGetValue(translation, out string? value) ? value : string.Empty; yield return $"@tr[{translation.Name}]={translationString}"; } } } } ================================================ FILE: osu.Game.Rulesets.Karaoke/Beatmaps/IHasPrimaryKey.cs ================================================ // Copyright (c) andy840119 . Licensed under the GPL Licence. // See the LICENCE file in the repository root for full licence text. using System; namespace osu.Game.Rulesets.Karaoke.Beatmaps; public interface IHasPrimaryKey { ElementId ID { get; } } public static class PrimaryKeyObjectExtension { public static TObject ChangeId(this TObject obj, ElementId id) where TObject : IHasPrimaryKey { // get id from the obj and override the id. var propertyInfo = obj.GetType().GetProperty(nameof(IHasPrimaryKey.ID)); if (propertyInfo == null) throw new InvalidOperationException(); propertyInfo.SetValue(obj, id); return obj; } } ================================================ FILE: osu.Game.Rulesets.Karaoke/Beatmaps/IKaraokeBeatmapResourcesProvider.cs ================================================ // Copyright (c) andy840119 . Licensed under the GPL Licence. // See the LICENCE file in the repository root for full licence text. using osu.Framework.Graphics.Textures; using osu.Game.Rulesets.Karaoke.Beatmaps.Metadatas.Types; namespace osu.Game.Rulesets.Karaoke.Beatmaps; public interface IKaraokeBeatmapResourcesProvider { Texture? GetSingerAvatar(ISinger singer); } ================================================ FILE: osu.Game.Rulesets.Karaoke/Beatmaps/KaraokeBeatmap.cs ================================================ // Copyright (c) andy840119 . Licensed under the GPL Licence. // See the LICENCE file in the repository root for full licence text. using System.Collections.Generic; using System.Globalization; using System.Linq; using osu.Framework.Graphics.Sprites; using osu.Game.Beatmaps; using osu.Game.Rulesets.Karaoke.Beatmaps.Metadatas; using osu.Game.Rulesets.Karaoke.Objects; namespace osu.Game.Rulesets.Karaoke.Beatmaps; public class KaraokeBeatmap : Beatmap { public IList AvailableTranslationLanguages { get; set; } = new List(); public SingerInfo SingerInfo { get; set; } = new(); public PageInfo PageInfo { get; set; } = new(); public NoteInfo NoteInfo { get; set; } = new(); public bool Scorable { get; set; } public override IEnumerable GetStatistics() { int singers = SingerInfo.GetAllSingers().Count(); int lyrics = HitObjects.Count(s => s is Lyric); var defaultStatistic = new List { new() { Name = "Singer", Content = singers.ToString(), CreateIcon = () => new SpriteIcon { Icon = FontAwesome.Solid.User }, }, new() { Name = "Lyric", Content = lyrics.ToString(), CreateIcon = () => new SpriteIcon { Icon = FontAwesome.Solid.AlignLeft }, }, }; if (Scorable) { int notes = HitObjects.Count(s => s is Note { Display: true }); defaultStatistic.Add(new BeatmapStatistic { Name = "Note", Content = notes.ToString(), CreateIcon = () => new SpriteIcon { Icon = FontAwesome.Solid.Music }, }); } else { defaultStatistic.Add(new BeatmapStatistic { Name = "This beatmap is not scorable.", Content = "This beatmap is not scorable.", CreateIcon = () => new SpriteIcon { Icon = FontAwesome.Solid.Times }, }); } return defaultStatistic.ToArray(); } } ================================================ FILE: osu.Game.Rulesets.Karaoke/Beatmaps/KaraokeBeatmapConverter.cs ================================================ // Copyright (c) andy840119 . Licensed under the GPL Licence. // See the LICENCE file in the repository root for full licence text. using System; using System.Collections.Generic; using System.Linq; using System.Threading; using osu.Game.Beatmaps; using osu.Game.Rulesets.Karaoke.Objects; using osu.Game.Rulesets.Objects; namespace osu.Game.Rulesets.Karaoke.Beatmaps; public class KaraokeBeatmapConverter : BeatmapConverter { public KaraokeBeatmapConverter(IBeatmap beatmap, Ruleset ruleset) : base(beatmap, ruleset) { } public override bool CanConvert() => Beatmap.HitObjects.All(h => h is KaraokeHitObject); protected override Beatmap ConvertBeatmap(IBeatmap original, CancellationToken cancellationToken) { var beatmap = base.ConvertBeatmap(original, cancellationToken); // Apply property created from legacy decoder var propertyDictionary = beatmap.HitObjects.OfType().FirstOrDefault(); if (propertyDictionary == null) return beatmap; if (beatmap is not KaraokeBeatmap karaokeBeatmap) throw new InvalidCastException(nameof(beatmap)); karaokeBeatmap.AvailableTranslationLanguages = propertyDictionary.AvailableTranslationLanguages; beatmap.HitObjects.Remove(propertyDictionary); return beatmap; } protected override IEnumerable ConvertHitObject(HitObject original, IBeatmap beatmap, CancellationToken cancellationToken) => throw new NotImplementedException(); protected override Beatmap CreateBeatmap() => new KaraokeBeatmap(); } ================================================ FILE: osu.Game.Rulesets.Karaoke/Beatmaps/KaraokeBeatmapExtension.cs ================================================ // Copyright (c) andy840119 . Licensed under the GPL Licence. // See the LICENCE file in the repository root for full licence text. using System.Collections.Generic; using System.Globalization; using System.Linq; using osu.Game.Beatmaps; namespace osu.Game.Rulesets.Karaoke.Beatmaps; public static class KaraokeBeatmapExtension { public static bool IsScorable(this IBeatmap beatmap) { // we should throw invalidate exception here but it will cause test case failed. // because beatmap in the working beatmap in test case not always be karaoke beatmap class. return beatmap is KaraokeBeatmap karaokeBeatmap && karaokeBeatmap.Scorable; } public static IList AvailableTranslationLanguages(this IBeatmap beatmap) => (beatmap as KaraokeBeatmap)?.AvailableTranslationLanguages ?? new List(); public static bool AnyTranslation(this IBeatmap beatmap) => beatmap.AvailableTranslationLanguages().Any(); public static float PitchToScale(this IBeatmap beatmap, float pitch) { return pitch / 20 - 7; } } ================================================ FILE: osu.Game.Rulesets.Karaoke/Beatmaps/KaraokeBeatmapProcessor.cs ================================================ // Copyright (c) andy840119 . Licensed under the GPL Licence. // See the LICENCE file in the repository root for full licence text. using System; using System.Linq; using osu.Game.Beatmaps; using osu.Game.Rulesets.Karaoke.Objects.Types; using osu.Game.Screens.Edit; namespace osu.Game.Rulesets.Karaoke.Beatmaps; public class KaraokeBeatmapProcessor : BeatmapProcessor { public KaraokeBeatmapProcessor(IBeatmap beatmap) : base(beatmap) { } public override void PreProcess() { var karaokeBeatmap = getKaraokeBeatmap(Beatmap); base.PreProcess(); applyInvalidProperty(karaokeBeatmap); return; static KaraokeBeatmap getKaraokeBeatmap(IBeatmap beatmap) => beatmap switch { // goes to there while parsing the beatmap. KaraokeBeatmap karaokeBeatmap => karaokeBeatmap, // goes to there while editing the beatmap. EditorBeatmap editorBeatmap => getKaraokeBeatmap(editorBeatmap.PlayableBeatmap), _ => throw new InvalidCastException($"The beatmap is not a {nameof(KaraokeBeatmap)}"), }; } private void applyInvalidProperty(KaraokeBeatmap beatmap) { // should convert to array here because validate the working property might change the start-time and the end time. // which will cause got the wrong item in the array. foreach (var hitObject in beatmap.HitObjects.OfType().ToArray()) { hitObject.ValidateWorkingProperty(beatmap); } } } ================================================ FILE: osu.Game.Rulesets.Karaoke/Beatmaps/KaraokeBeatmapResourcesProvider.cs ================================================ // Copyright (c) andy840119 . Licensed under the GPL Licence. // See the LICENCE file in the repository root for full licence text. using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Textures; using osu.Game.Beatmaps; using osu.Game.Rulesets.Karaoke.Beatmaps.Metadatas.Types; namespace osu.Game.Rulesets.Karaoke.Beatmaps; public partial class KaraokeBeatmapResourcesProvider : Component, IKaraokeBeatmapResourcesProvider { [Resolved] private BeatmapManager beatmapManager { get; set; } = null!; [Resolved] private IBindable working { get; set; } = null!; public Texture? GetSingerAvatar(ISinger singer) { return null; } /* public Texture? GetSingerAvatar(ISinger singer) { var provider = getBeatmapResourceProvider(); if (provider == null) return null; if (singer is not Singer s) return null; var beatmapSetInfo = working.Value.BeatmapSetInfo; if (beatmapSetInfo == null) return null; string? path = beatmapSetInfo.GetPathForFile($"assets/singers/{s.AvatarFile}"); return provider.LargeTextureStore.Get(path); } private IBeatmapResourceProvider? getBeatmapResourceProvider() { // todo : use better way to get the resource provider. var prop = typeof(BeatmapManager).GetField("workingBeatmapCache", BindingFlags.Instance | BindingFlags.NonPublic); if (prop == null) throw new ArgumentNullException(); return prop.GetValue(beatmapManager) as WorkingBeatmapCache; } */ } ================================================ FILE: osu.Game.Rulesets.Karaoke/Beatmaps/Metadatas/NoteInfo.cs ================================================ // Copyright (c) andy840119 . Licensed under the GPL Licence. // See the LICENCE file in the repository root for full licence text. using osu.Game.Rulesets.Karaoke.Objects; namespace osu.Game.Rulesets.Karaoke.Beatmaps.Metadatas; public class NoteInfo { public int Columns { get; set; } = 9; public Tone MaxTone => new() { Scale = Columns / 2, }; public Tone MinTone => -MaxTone; } ================================================ FILE: osu.Game.Rulesets.Karaoke/Beatmaps/Metadatas/Page.cs ================================================ // Copyright (c) andy840119 . Licensed under the GPL Licence. // See the LICENCE file in the repository root for full licence text. using System; using Newtonsoft.Json; using osu.Framework.Bindables; using osu.Game.Utils; namespace osu.Game.Rulesets.Karaoke.Beatmaps.Metadatas; public class Page : IDeepCloneable, IComparable { [JsonIgnore] public readonly Bindable TimeBindable = new(); public double Time { get => TimeBindable.Value; set => TimeBindable.Value = value; } public Page DeepClone() { return new Page { Time = Time, }; } public int CompareTo(Page? other) => Time.CompareTo(other?.Time); public override int GetHashCode() => Time.GetHashCode(); } ================================================ FILE: osu.Game.Rulesets.Karaoke/Beatmaps/Metadatas/PageInfo.cs ================================================ // Copyright (c) andy840119 . Licensed under the GPL Licence. // See the LICENCE file in the repository root for full licence text. using System; using System.Collections.Generic; using System.Collections.Specialized; using System.Diagnostics; using System.Linq; using Newtonsoft.Json; using osu.Framework.Bindables; using osu.Game.Utils; namespace osu.Game.Rulesets.Karaoke.Beatmaps.Metadatas; public class PageInfo : IDeepCloneable { [JsonIgnore] public IBindable PagesVersion => pagesVersion; private readonly Bindable pagesVersion = new(); public BindableList Pages = new(); [JsonIgnore] public List SortedPages { get; private set; } = new(); public PageInfo() { Pages.CollectionChanged += (_, args) => { switch (args.Action) { case NotifyCollectionChangedAction.Add: Debug.Assert(args.NewItems != null); foreach (var c in args.NewItems.Cast()) c.TimeBindable.ValueChanged += timeValueChanged; break; case NotifyCollectionChangedAction.Reset: case NotifyCollectionChangedAction.Remove: Debug.Assert(args.OldItems != null); foreach (var c in args.OldItems.Cast()) c.TimeBindable.ValueChanged -= timeValueChanged; break; } onPageChanged(); void timeValueChanged(ValueChangedEvent e) => onPageChanged(); }; void onPageChanged() { SortedPages = Pages.OrderBy(x => x.Time).ToList(); pagesVersion.Value++; } } public Page? GetPageAt(double time) { if (SortedPages.Count < 2) return null; var page = SortedPages.LastOrDefault(x => x.Time <= time); // should not be able to get the page if time exceed the last page. var lastPage = SortedPages.LastOrDefault(); if (page == lastPage && page?.Time < time) return null; return page; } public int? GetPageIndexAt(double time) { var page = GetPageAt(time); if (page == null) return null; return SortedPages.IndexOf(page); } public int? GetPageOrder(Page page) { int index = SortedPages.IndexOf(page); return index == -1 ? null : index + 1; } public PageInfo DeepClone() { var controlPointInfo = Activator.CreateInstance(); return controlPointInfo; } } ================================================ FILE: osu.Game.Rulesets.Karaoke/Beatmaps/Metadatas/Singer.cs ================================================ // Copyright (c) andy840119 . Licensed under the GPL Licence. // See the LICENCE file in the repository root for full licence text. using Newtonsoft.Json; using osu.Framework.Bindables; using osu.Game.Rulesets.Karaoke.Beatmaps.Metadatas.Types; namespace osu.Game.Rulesets.Karaoke.Beatmaps.Metadatas; public class Singer : ISinger { [JsonProperty] public ElementId ID { get; private set; } = ElementId.NewElementId(); [JsonIgnore] public readonly Bindable OrderBindable = new(); /// /// Order /// public int Order { get => OrderBindable.Value; set => OrderBindable.Value = value; } [JsonIgnore] public readonly Bindable AvatarFileBindable = new(); public string AvatarFile { get => AvatarFileBindable.Value; set => AvatarFileBindable.Value = value; } [JsonIgnore] public Bindable HueBindable = new BindableFloat { MinValue = 0, MaxValue = 1, }; public float Hue { get => HueBindable.Value; set => HueBindable.Value = value; } [JsonIgnore] public readonly Bindable NameBindable = new(); public string Name { get => NameBindable.Value; set => NameBindable.Value = value; } [JsonIgnore] public readonly Bindable RomanisationBindable = new(); public string Romanisation { get => RomanisationBindable.Value; set => RomanisationBindable.Value = value; } [JsonIgnore] public readonly Bindable EnglishNameBindable = new(); public string EnglishName { get => EnglishNameBindable.Value; set => EnglishNameBindable.Value = value; } [JsonIgnore] public readonly Bindable DescriptionBindable = new(); public string Description { get => DescriptionBindable.Value; set => DescriptionBindable.Value = value; } } ================================================ FILE: osu.Game.Rulesets.Karaoke/Beatmaps/Metadatas/SingerInfo.cs ================================================ // Copyright (c) andy840119 . Licensed under the GPL Licence. // See the LICENCE file in the repository root for full licence text. using System; using System.Collections.Generic; using System.Linq; using osu.Framework.Bindables; using osu.Game.Rulesets.Karaoke.Beatmaps.Metadatas.Types; namespace osu.Game.Rulesets.Karaoke.Beatmaps.Metadatas; public class SingerInfo { public bool SupportSingerState { get; set; } // todo: should make the property as readonly. public BindableList Singers { get; set; } = new(); // todo: should make the property as readonly. public BindableList SingerState { get; set; } = new(); public IEnumerable GetAllSingers() => Singers.OrderBy(x => x.Order); public IEnumerable GetAllAvailableSingerStates(Singer singer) => SingerState.Where(x => x.MainSingerId == singer.ID).OrderBy(x => x.Order); public IDictionary GetSingerByIds(ElementId[] singerIds) { var matchedMainSingers = GetAllSingers().Where(x => singerIds.Contains(x.ID)); return matchedMainSingers.ToDictionary(k => k, v => { var matchedSingerStates = GetAllAvailableSingerStates(v); return matchedSingerStates.Where(x => singerIds.Contains(x.ID)).ToArray(); }); } public IDictionary GetSingerMap() { var matchedMainSingers = GetAllSingers(); return matchedMainSingers.ToDictionary(k => k, v => GetAllAvailableSingerStates(v).ToArray()); } public Singer AddSinger(Action? action = null) { var singer = new Singer(); action?.Invoke(singer); Singers.Add(singer); return singer; } public SingerState AddSingerState(Singer singer, Action? action = null) { if (!Singers.Contains(singer)) throw new InvalidOperationException("Main singer must in the singer info."); var mainSingerId = singer.ID; var singerState = new SingerState(mainSingerId); action?.Invoke(singerState); SingerState.Add(singerState); return singerState; } public bool RemoveSinger(ISinger singer) { switch (singer) { case Singer mainSinger: { var singerStates = GetAllAvailableSingerStates(mainSinger); foreach (var singerState in singerStates) { RemoveSinger(singerState); } return Singers.Remove(mainSinger); } case SingerState singerState: return SingerState.Remove(singerState); default: throw new InvalidCastException(); } } public bool HasSinger(ISinger singer) { return singer switch { Singer mainSinger => Singers.Contains(mainSinger), SingerState singerState => SingerState.Contains(singerState), _ => throw new InvalidCastException(), }; } } ================================================ FILE: osu.Game.Rulesets.Karaoke/Beatmaps/Metadatas/SingerState.cs ================================================ // Copyright (c) andy840119 . Licensed under the GPL Licence. // See the LICENCE file in the repository root for full licence text. using Newtonsoft.Json; using osu.Framework.Bindables; using osu.Game.Rulesets.Karaoke.Beatmaps.Metadatas.Types; namespace osu.Game.Rulesets.Karaoke.Beatmaps.Metadatas; public class SingerState : ISinger { public SingerState() { } public SingerState(ElementId mainSingerId) { MainSingerId = mainSingerId; } [JsonProperty] public ElementId ID { get; private set; } = ElementId.NewElementId(); [JsonProperty] public ElementId MainSingerId { get; private set; } [JsonIgnore] public readonly Bindable OrderBindable = new(); /// /// Order /// public int Order { get => OrderBindable.Value; set => OrderBindable.Value = value; } [JsonIgnore] public Bindable HueBindable = new BindableFloat { MinValue = 0, MaxValue = 1, }; public float Hue { get => HueBindable.Value; set => HueBindable.Value = value; } [JsonIgnore] public readonly Bindable DescriptionBindable = new(); public string Description { get => DescriptionBindable.Value; set => DescriptionBindable.Value = value; } } ================================================ FILE: osu.Game.Rulesets.Karaoke/Beatmaps/Metadatas/Types/ISinger.cs ================================================ // Copyright (c) andy840119 . Licensed under the GPL Licence. // See the LICENCE file in the repository root for full licence text. using osu.Game.Rulesets.Karaoke.Objects.Types; namespace osu.Game.Rulesets.Karaoke.Beatmaps.Metadatas.Types; public interface ISinger : IHasOrder, IHasPrimaryKey { float Hue { get; } } ================================================ FILE: osu.Game.Rulesets.Karaoke/Beatmaps/Utils/SingerUtils.cs ================================================ // Copyright (c) andy840119 . Licensed under the GPL Licence. // See the LICENCE file in the repository root for full licence text. using System; using System.Collections.Generic; using System.Linq; using osu.Framework.Graphics; using osu.Game.Rulesets.Karaoke.Beatmaps.Metadatas.Types; using osuTK.Graphics; namespace osu.Game.Rulesets.Karaoke.Beatmaps.Utils; public static class SingerUtils { public static int GetShiftingStyleIndex(IEnumerable singerIds) => singerIds.Sum(x => (int)Math.Pow(2, x - 1)); public static int[] GetSingersIndex(int styleIndex) { if (styleIndex < 1) return Array.Empty(); string binary = Convert.ToString(styleIndex, 2); return binary.Select((v, i) => new { value = v, singer = binary.Length - i }) .Where(x => x.value == '1').Select(x => x.singer).OrderBy(x => x).ToArray(); } public static Color4 GetContentColour(ISinger singer) => Colour4.FromHSL(singer.Hue, 0.4f, 0.6f); public static Color4 GetBackgroundColour(ISinger singer) => Colour4.FromHSL(singer.Hue, 0.1f, 0.4f); } ================================================ FILE: osu.Game.Rulesets.Karaoke/Bindables/BindableCultureInfo.cs ================================================ // Copyright (c) andy840119 . Licensed under the GPL Licence. // See the LICENCE file in the repository root for full licence text. using System; using System.Globalization; using osu.Framework.Bindables; using osu.Framework.Logging; using osu.Game.Rulesets.Karaoke.Utils; namespace osu.Game.Rulesets.Karaoke.Bindables; public class BindableCultureInfo : Bindable { public BindableCultureInfo(CultureInfo? value = default) : base(value) { } public override void Parse(object? input, IFormatProvider provider) { if (input == null) { Value = null; return; } try { switch (input) { case string str: Value = CultureInfoUtils.CreateLoadCultureInfoByCode(str); break; case int lcid: Value = CultureInfoUtils.CreateLoadCultureInfoById(lcid); break; case CultureInfo cultureInfo: Value = cultureInfo; break; default: base.Parse(input, provider); break; } } catch (Exception ex) { Value = null; // It might have issue that the culture info is not available in the system. // Log it instead of throw exception. Logger.Error(ex, $"Failed to parse {input} into {typeof(CultureInfo)}"); } } protected override Bindable CreateInstance() => new BindableCultureInfo(); public override string ToString(string? format, IFormatProvider? formatProvider) => Value != null ? CultureInfoUtils.GetSaveCultureInfoCode(Value) : string.Empty; } ================================================ FILE: osu.Game.Rulesets.Karaoke/Bindables/BindableFontUsage.cs ================================================ // Copyright (c) andy840119 . Licensed under the GPL Licence. // See the LICENCE file in the repository root for full licence text. using System; using System.Linq; using System.Text.RegularExpressions; using osu.Framework.Bindables; using osu.Framework.Graphics.Sprites; using osu.Game.Rulesets.Karaoke.Extensions; namespace osu.Game.Rulesets.Karaoke.Bindables; public class BindableFontUsage : RangeConstrainedBindable { private const int default_min_font_size = 1; private const int default_max_font_size = 72; protected override FontUsage DefaultMinValue => Default.With(size: default_min_font_size); protected override FontUsage DefaultMaxValue => Default.With(size: default_max_font_size); public BindableFontUsage(FontUsage value = default) : base(value) { } public float MinFontSize { get => MinValue.Size; set => MinValue = MinValue.With(size: value); } public float MaxFontSize { get => MaxValue.Size; set => MaxValue = MaxValue.With(size: value); } // IDK why not being called in here while saving. public override string ToString(string? format, IFormatProvider? formatProvider) => $"family={Value.Family} weight={Value.Weight} size={Value.Size} italics={Value.Italics} fixedWidth={Value.FixedWidth}"; public override void Parse(object? input, IFormatProvider provider) { if (input is not string str || string.IsNullOrEmpty(str)) { Value = default; return; } // because FontUsage.ToString() will have "," symbol. str = str.Replace(",", string.Empty); var regex = new Regex(@"\b(?font|family|weight|size|italics|fixedWidth)(?[=]+)(?("".*"")|(\S*))", RegexOptions.Compiled | RegexOptions.IgnoreCase); var dictionary = regex.Matches(str).ToDictionary(k => k.GetGroupValue("key"), v => v.GetGroupValue("value")); if (dictionary.TryGetValue("Font", out string? font)) { string? family = font.Contains('-') ? font.Split('-').FirstOrDefault() : font; string? weight = font.Contains('-') ? font.Split('-').LastOrDefault() : string.Empty; float size = float.Parse(dictionary["Size"]); bool italics = dictionary["Italics"].ToLower() == "true"; bool fixedWidth = dictionary["FixedWidth"].ToLower() == "true"; Value = new FontUsage(family, size, weight, italics, fixedWidth); } else { string family = dictionary["family"]; string weight = dictionary["weight"]; float size = float.Parse(dictionary["size"]); bool italics = dictionary["italics"].ToLower() == "true"; bool fixedWidth = dictionary["fixedWidth"].ToLower() == "true"; Value = new FontUsage(family, size, weight, italics, fixedWidth); } } protected override Bindable CreateInstance() => new BindableFontUsage(); protected sealed override FontUsage ClampValue(FontUsage value, FontUsage minValue, FontUsage maxValue) { return value.With(size: Math.Clamp(value.Size, minValue.Size, maxValue.Size)); } protected sealed override bool IsValidRange(FontUsage min, FontUsage max) => min.Size <= max.Size; } ================================================ FILE: osu.Game.Rulesets.Karaoke/Configuration/KaraokeRulesetConfigManager.cs ================================================ // Copyright (c) andy840119 . Licensed under the GPL Licence. // See the LICENCE file in the repository root for full licence text. using System; using osu.Framework.Bindables; using osu.Framework.Configuration.Tracking; using osu.Framework.Graphics.Sprites; using osu.Game.Configuration; using osu.Game.Rulesets.Configuration; using osu.Game.Rulesets.Karaoke.Bindables; using osu.Game.Rulesets.Karaoke.UI; using osu.Game.Rulesets.Karaoke.Utils; namespace osu.Game.Rulesets.Karaoke.Configuration; public class KaraokeRulesetConfigManager : RulesetConfigManager { public KaraokeRulesetConfigManager(SettingsStore? settings, RulesetInfo ruleset, int? variant = null) : base(settings, ruleset, variant) { } protected override void InitialiseDefaults() { base.InitialiseDefaults(); // Visual SetDefault(KaraokeRulesetSetting.ScrollTime, 5000.0, 1000.0, 10000.0, 100.0); SetDefault(KaraokeRulesetSetting.ScrollDirection, KaraokeScrollingDirection.Left); SetDefault(KaraokeRulesetSetting.DisplayNoteRubyText, false); SetDefault(KaraokeRulesetSetting.ShowCursor, true); SetDefault(KaraokeRulesetSetting.NoteAlpha, 1, 0.2, 1, 0.01); SetDefault(KaraokeRulesetSetting.LyricAlpha, 1, 0.2, 1, 0.01); // Pitch SetDefault(KaraokeRulesetSetting.OverridePitchAtGameplay, false); SetDefault(KaraokeRulesetSetting.Pitch, 0, -10, 10); SetDefault(KaraokeRulesetSetting.OverrideVocalPitchAtGameplay, false); SetDefault(KaraokeRulesetSetting.VocalPitch, 0, -10, 10); SetDefault(KaraokeRulesetSetting.OverrideScoringPitchAtGameplay, false); SetDefault(KaraokeRulesetSetting.ScoringPitch, 0, -10, 10); // Playback SetDefault(KaraokeRulesetSetting.OverridePlaybackSpeedAtGameplay, false); SetDefault(KaraokeRulesetSetting.PlaybackSpeed, 0, -10, 10); // Device SetDefault(KaraokeRulesetSetting.MicrophoneDevice, string.Empty); // Font SetDefault(KaraokeRulesetSetting.MainFont, new FontUsage("Torus", 48, "Bold"), 48f, 48f); SetDefault(KaraokeRulesetSetting.RubyFont, new FontUsage("Torus", 20, "Bold"), 8f, 48f); SetDefault(KaraokeRulesetSetting.RubyMargin, 5, 0, 20); SetDefault(KaraokeRulesetSetting.RomanisationFont, new FontUsage("Torus", 20, "Bold"), 8f, 48f); SetDefault(KaraokeRulesetSetting.RomanisationMargin, 0, 0, 20); SetDefault(KaraokeRulesetSetting.ForceUseDefaultFont, false); SetDefault(KaraokeRulesetSetting.TranslationFont, new FontUsage("Torus", 18, "Bold"), 10f, 48f); SetDefault(KaraokeRulesetSetting.ForceUseDefaultTranslationFont, false); SetDefault(KaraokeRulesetSetting.NoteFont, new FontUsage("Torus", 12, "Bold"), 10f, 32f); SetDefault(KaraokeRulesetSetting.ForceUseDefaultNoteFont, false); } protected override void AddBindable(KaraokeRulesetSetting lookup, Bindable bindable) { switch (lookup) { case KaraokeRulesetSetting.MainFont: case KaraokeRulesetSetting.RubyFont: case KaraokeRulesetSetting.RomanisationFont: case KaraokeRulesetSetting.TranslationFont: case KaraokeRulesetSetting.NoteFont: base.AddBindable(lookup, new BindableFontUsage(TypeUtils.ChangeType(bindable.Value))); break; default: base.AddBindable(lookup, bindable); break; } } protected BindableFontUsage SetDefault(KaraokeRulesetSetting setting, FontUsage fontUsage, float? minFontSize = null, float? maxFontSize = null) { base.SetDefault(setting, fontUsage); // Should not use base.setDefault's value because it will return Bindable, not BindableFontUsage var bindable = GetOriginalBindable(setting); if (bindable is not BindableFontUsage bindableFontUsage) throw new InvalidCastException(nameof(bindable)); // Assign size restriction in here. if (minFontSize.HasValue) bindableFontUsage.MinFontSize = minFontSize.Value; if (maxFontSize.HasValue) bindableFontUsage.MaxFontSize = maxFontSize.Value; return bindableFontUsage; } public override TrackedSettings CreateTrackedSettings() => new() { new TrackedSetting(KaraokeRulesetSetting.ScrollTime, v => new SettingDescription(v, "Scroll Time", $"{v}ms")), new TrackedSetting(KaraokeRulesetSetting.DisplayNoteRubyText, b => new SettingDescription(b, "Toggle display", b ? "Show" : "Hide")), new TrackedSetting(KaraokeRulesetSetting.ShowCursor, b => new SettingDescription(b, "Cursor display", b ? "Show" : "Hide")), new TrackedSetting(KaraokeRulesetSetting.MicrophoneDevice, d => new SettingDescription(d, "Change to the new microphone device", d)), }; } public enum KaraokeRulesetSetting { // Visual ScrollTime, ScrollDirection, DisplayNoteRubyText, ShowCursor, NoteAlpha, LyricAlpha, // Pitch OverridePitchAtGameplay, Pitch, OverrideVocalPitchAtGameplay, VocalPitch, OverrideScoringPitchAtGameplay, ScoringPitch, // Playback OverridePlaybackSpeedAtGameplay, PlaybackSpeed, // Device MicrophoneDevice, // Font MainFont, RubyFont, RubyMargin, RomanisationFont, RomanisationMargin, ForceUseDefaultFont, TranslationFont, ForceUseDefaultTranslationFont, NoteFont, ForceUseDefaultNoteFont, } ================================================ FILE: osu.Game.Rulesets.Karaoke/Configuration/KaraokeRulesetEditCheckerConfigManager.cs ================================================ // Copyright (c) andy840119 . Licensed under the GPL Licence. // See the LICENCE file in the repository root for full licence text. using osu.Game.Configuration; using osu.Game.Rulesets.Karaoke.Objects.Utils; namespace osu.Game.Rulesets.Karaoke.Configuration; public class KaraokeRulesetEditCheckerConfigManager : InMemoryConfigManager { protected override void InitialiseDefaults() { base.InitialiseDefaults(); // Lyric SetDefault(KaraokeRulesetEditCheckerSetting.LyricRubyPositionSorting, RubyTagsUtils.Sorting.Asc); SetDefault(KaraokeRulesetEditCheckerSetting.LyricTimeTagTimeSelfCheck, SelfCheck.BasedOnStart); SetDefault(KaraokeRulesetEditCheckerSetting.LyricTimeTagTimeGroupCheck, GroupCheck.Asc); } } public enum KaraokeRulesetEditCheckerSetting { LyricRubyPositionSorting, LyricTimeTagTimeSelfCheck, LyricTimeTagTimeGroupCheck, } ================================================ FILE: osu.Game.Rulesets.Karaoke/Configuration/KaraokeRulesetEditConfigManager.cs ================================================ // Copyright (c) andy840119 . Licensed under the GPL Licence. // See the LICENCE file in the repository root for full licence text. using osu.Game.Configuration; namespace osu.Game.Rulesets.Karaoke.Configuration; public class KaraokeRulesetEditConfigManager : InMemoryConfigManager; public enum KaraokeRulesetEditSetting; ================================================ FILE: osu.Game.Rulesets.Karaoke/Configuration/KaraokeRulesetEditGeneratorConfigManager.cs ================================================ // Copyright (c) andy840119 . Licensed under the GPL Licence. // See the LICENCE file in the repository root for full licence text. using System; using System.Collections.Generic; using osu.Framework.Bindables; using osu.Game.Configuration; using osu.Game.Rulesets.Karaoke.Edit.Generator; using osu.Game.Rulesets.Karaoke.Edit.Generator.Beatmaps.Pages; using osu.Game.Rulesets.Karaoke.Edit.Generator.Lyrics.Language; using osu.Game.Rulesets.Karaoke.Edit.Generator.Lyrics.Notes; using osu.Game.Rulesets.Karaoke.Edit.Generator.Lyrics.ReferenceLyric; using osu.Game.Rulesets.Karaoke.Edit.Generator.Lyrics.Romanisation.Ja; using osu.Game.Rulesets.Karaoke.Edit.Generator.Lyrics.RubyTags.Ja; using osu.Game.Rulesets.Karaoke.Edit.Generator.Lyrics.TimeTags.Ja; using osu.Game.Rulesets.Karaoke.Edit.Generator.Lyrics.TimeTags.Zh; using osu.Game.Rulesets.Karaoke.Edit.Generator.Stages.Classic; using osu.Game.Rulesets.Karaoke.Edit.Generator.Stages.Preview; namespace osu.Game.Rulesets.Karaoke.Configuration; public class KaraokeRulesetEditGeneratorConfigManager : InMemoryConfigManager { protected override void InitialiseDefaults() { base.InitialiseDefaults(); // Beatmap page SetDefault(); // Classic stage. SetDefault(); SetDefault(); SetDefault(); // Preview stage. SetDefault(); // Language detection SetDefault(); // Language detection SetDefault(); // Ruby generator SetDefault(); // Time tag generator SetDefault(); SetDefault(); // Romanisation generator SetDefault(); // Note generator SetDefault(); } protected void SetDefault() where T : GeneratorConfig, new() { var defaultValue = CreateDefaultConfig(); var setting = GetSettingByType(); SetDefault(setting, defaultValue); } protected static T CreateDefaultConfig() where T : GeneratorConfig, new() => new(); protected static KaraokeRulesetEditGeneratorSetting GetSettingByType() => typeof(TValue) switch { Type t when t == typeof(PageGeneratorConfig) => KaraokeRulesetEditGeneratorSetting.BeatmapPageGeneratorConfig, Type t when t == typeof(ClassicLyricLayoutCategoryGeneratorConfig) => KaraokeRulesetEditGeneratorSetting.ClassicLyricLayoutCategoryGeneratorConfig, Type t when t == typeof(ClassicLyricTimingInfoGeneratorConfig) => KaraokeRulesetEditGeneratorSetting.ClassicLyricTimingInfoGeneratorConfig, Type t when t == typeof(ClassicStageInfoGeneratorConfig) => KaraokeRulesetEditGeneratorSetting.ClassicStageInfoGeneratorConfig, Type t when t == typeof(PreviewStageInfoGeneratorConfig) => KaraokeRulesetEditGeneratorSetting.PreviewStageInfoGeneratorConfig, Type t when t == typeof(ReferenceLyricDetectorConfig) => KaraokeRulesetEditGeneratorSetting.ReferenceLyricDetectorConfig, Type t when t == typeof(LanguageDetectorConfig) => KaraokeRulesetEditGeneratorSetting.LanguageDetectorConfig, Type t when t == typeof(JaRubyTagGeneratorConfig) => KaraokeRulesetEditGeneratorSetting.JaRubyTagGeneratorConfig, Type t when t == typeof(JaTimeTagGeneratorConfig) => KaraokeRulesetEditGeneratorSetting.JaTimeTagGeneratorConfig, Type t when t == typeof(ZhTimeTagGeneratorConfig) => KaraokeRulesetEditGeneratorSetting.ZhTimeTagGeneratorConfig, Type t when t == typeof(JaRomanisationGeneratorConfig) => KaraokeRulesetEditGeneratorSetting.JaRomanisationGeneratorConfig, Type t when t == typeof(NoteGeneratorConfig) => KaraokeRulesetEditGeneratorSetting.NoteGeneratorConfig, _ => throw new NotSupportedException(), }; public TValue Get() where TValue : GeneratorConfig, new() { var lookup = GetSettingByType(); return Get(lookup); } public GeneratorConfig GetGeneratorConfig(KaraokeRulesetEditGeneratorSetting lookup) { if (!ConfigStore.TryGetValue(lookup, out IBindable? obj)) throw new KeyNotFoundException(); var prop = obj.GetType().GetProperty("Value"); if (prop?.GetValue(obj) is not GeneratorConfig generatorConfig) throw new InvalidCastException(); return generatorConfig; } } public enum KaraokeRulesetEditGeneratorSetting { // Beatmap BeatmapPageGeneratorConfig, // Classic stage. ClassicLyricLayoutCategoryGeneratorConfig, ClassicLyricTimingInfoGeneratorConfig, ClassicStageInfoGeneratorConfig, // Preview stage. PreviewStageInfoGeneratorConfig, // Reference lyric detection. ReferenceLyricDetectorConfig, // Language detection LanguageDetectorConfig, // Ruby generator JaRubyTagGeneratorConfig, // Time tag generator JaTimeTagGeneratorConfig, ZhTimeTagGeneratorConfig, // Romanisation generator. JaRomanisationGeneratorConfig, // Note generator NoteGeneratorConfig, } ================================================ FILE: osu.Game.Rulesets.Karaoke/Configuration/KaraokeRulesetLyricEditorConfigManager.cs ================================================ // Copyright (c) andy840119 . Licensed under the GPL Licence. // See the LICENCE file in the repository root for full licence text. using osu.Framework.Bindables; using osu.Game.Configuration; using osu.Game.Rulesets.Karaoke.Objects.Types; using osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics; using osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.CaretPosition.Algorithms; using osu.Game.Rulesets.Karaoke.Utils; namespace osu.Game.Rulesets.Karaoke.Configuration; public class KaraokeRulesetLyricEditorConfigManager : InMemoryConfigManager { protected override void InitialiseDefaults() { base.InitialiseDefaults(); // General SetDefault(KaraokeRulesetLyricEditorSetting.LyricEditorPreferLayout, LyricEditorLayout.List); SetDefault(KaraokeRulesetLyricEditorSetting.LyricEditorFontSize, FontUtils.DEFAULT_FONT_SIZE); SetDefault(KaraokeRulesetLyricEditorSetting.AutoFocusToEditLyric, true); SetDefault(KaraokeRulesetLyricEditorSetting.AutoFocusToEditLyricSkipRows, 1, 0, 4); SetDefault(KaraokeRulesetLyricEditorSetting.ClickToLockLyricState, LockState.Partial); // Composer SetDefault(KaraokeRulesetLyricEditorSetting.ShowPropertyPanelInComposer, true); SetDefault(KaraokeRulesetLyricEditorSetting.ShowInvalidInfoInComposer, true); SetDefault(KaraokeRulesetLyricEditorSetting.FontSizeInComposer, FontUtils.DEFAULT_FONT_SIZE_IN_COMPOSER); // Recording SetDefault(KaraokeRulesetLyricEditorSetting.RecordingTimeTagMovingCaretMode, RecordingTimeTagCaretMoveMode.None); SetDefault(KaraokeRulesetLyricEditorSetting.RecordingAutoMoveToNextTimeTag, true); SetDefault(KaraokeRulesetLyricEditorSetting.RecordingChangeTimeWhileMovingTheCaret, true); SetDefault(KaraokeRulesetLyricEditorSetting.RecordingTimeTagShowWaveform, true); SetDefault(KaraokeRulesetLyricEditorSetting.RecordingTimeTagWaveformOpacity, 0.5f, 0, 1, 0.01f); SetDefault(KaraokeRulesetLyricEditorSetting.RecordingTimeTagShowTick, true); SetDefault(KaraokeRulesetLyricEditorSetting.RecordingTimeTagTickOpacity, 0.5f, 0, 1, 0.01f); // Adjust SetDefault(KaraokeRulesetLyricEditorSetting.AdjustTimeTagShowWaveform, true); SetDefault(KaraokeRulesetLyricEditorSetting.AdjustTimeTagWaveformOpacity, 0.5f, 0, 1, 0.01f); SetDefault(KaraokeRulesetLyricEditorSetting.AdjustTimeTagShowTick, true); SetDefault(KaraokeRulesetLyricEditorSetting.AdjustTimeTagTickOpacity, 0.5f, 0, 1, 0.01f); } /// /// Binds a local bindable with a configuration-backed bindable. /// public void BindWith(KaraokeRulesetLyricEditorSetting lookup, IBindable bindable) => bindable.BindTo(GetOriginalBindable(lookup)); } public enum KaraokeRulesetLyricEditorSetting { // General LyricEditorPreferLayout, LyricEditorFontSize, AutoFocusToEditLyric, AutoFocusToEditLyricSkipRows, ClickToLockLyricState, // Composer ShowPropertyPanelInComposer, ShowInvalidInfoInComposer, FontSizeInComposer, // Recording RecordingTimeTagMovingCaretMode, RecordingAutoMoveToNextTimeTag, RecordingChangeTimeWhileMovingTheCaret, RecordingTimeTagShowWaveform, RecordingTimeTagWaveformOpacity, RecordingTimeTagShowTick, RecordingTimeTagTickOpacity, // Adjust AdjustTimeTagShowWaveform, AdjustTimeTagWaveformOpacity, AdjustTimeTagShowTick, AdjustTimeTagTickOpacity, } ================================================ FILE: osu.Game.Rulesets.Karaoke/Configuration/KaraokeSessionStatics.cs ================================================ // Copyright (c) andy840119 . Licensed under the GPL Licence. // See the LICENCE file in the repository root for full licence text. using System; using osu.Game.Beatmaps; using osu.Game.Configuration; using osu.Game.Rulesets.Karaoke.Objects; using osu.Game.Rulesets.Karaoke.UI.Components; namespace osu.Game.Rulesets.Karaoke.Configuration; public class KaraokeSessionStatics : InMemoryConfigManager { private readonly KaraokeRulesetConfigManager rulesetConfigManager; public KaraokeSessionStatics(KaraokeRulesetConfigManager config, IBeatmap? beatmap) { rulesetConfigManager = config; // Pitch bool overridePitch = getValue(KaraokeRulesetSetting.OverridePitchAtGameplay); int pitchValue = getValue(KaraokeRulesetSetting.Pitch); SetDefault(KaraokeRulesetSession.Pitch, overridePitch ? pitchValue : 0, -10, 10); bool overrideVocalPitch = getValue(KaraokeRulesetSetting.OverrideVocalPitchAtGameplay); int vocalPitchValue = getValue(KaraokeRulesetSetting.VocalPitch); SetDefault(KaraokeRulesetSession.VocalPitch, overrideVocalPitch ? vocalPitchValue : 0, -10, 10); bool overrideScoringPitch = getValue(KaraokeRulesetSetting.OverrideScoringPitchAtGameplay); int scoringPitchValue = getValue(KaraokeRulesetSetting.ScoringPitch); SetDefault(KaraokeRulesetSession.ScoringPitch, overrideScoringPitch ? scoringPitchValue : 0, -8, 8); // Playback bool overridePlaybackSpeed = getValue(KaraokeRulesetSetting.OverridePlaybackSpeedAtGameplay); int playbackSpeedValue = getValue(KaraokeRulesetSetting.PlaybackSpeed); SetDefault(KaraokeRulesetSession.PlaybackSpeed, overridePlaybackSpeed ? playbackSpeedValue : 0, -10, 10); // Practice SetDefault(KaraokeRulesetSession.SingingLyrics, Array.Empty()); // Scoring status SetDefault(KaraokeRulesetSession.ScoringStatus, ScoringStatusMode.NotInitialized); } private T getValue(KaraokeRulesetSetting setting) => rulesetConfigManager.Get(setting); } public enum KaraokeRulesetSession { // Pitch Pitch, VocalPitch, ScoringPitch, // Playback PlaybackSpeed, // Practice SingingLyrics, // Scoring status ScoringStatus, } ================================================ FILE: osu.Game.Rulesets.Karaoke/Difficulty/KaraokeDifficultyAttributes.cs ================================================ // Copyright (c) andy840119 . Licensed under the GPL Licence. // See the LICENCE file in the repository root for full licence text. using System.Collections.Generic; using Newtonsoft.Json; using osu.Game.Beatmaps; using osu.Game.Rulesets.Difficulty; namespace osu.Game.Rulesets.Karaoke.Difficulty; public class KaraokeDifficultyAttributes : DifficultyAttributes { /// /// The hit window for a GREAT hit inclusive of rate-adjusting mods (DT/HT/etc). /// /// /// Rate-adjusting mods do not affect the hit window at all in osu-stable. /// [JsonProperty("great_hit_window")] public double GreatHitWindow { get; set; } /// /// The score multiplier applied via score-reducing mods. /// [JsonProperty("score_multiplier")] public double ScoreMultiplier { get; set; } public override IEnumerable<(int attributeId, object value)> ToDatabaseAttributes() { foreach (var v in base.ToDatabaseAttributes()) yield return v; yield return (ATTRIB_ID_DIFFICULTY, StarRating); } public override void FromDatabaseAttributes(IReadOnlyDictionary values, IBeatmapOnlineInfo onlineInfo) { base.FromDatabaseAttributes(values, onlineInfo); StarRating = values[ATTRIB_ID_DIFFICULTY]; } } ================================================ FILE: osu.Game.Rulesets.Karaoke/Difficulty/KaraokeDifficultyCalculator.cs ================================================ // Copyright (c) andy840119 . Licensed under the GPL Licence. // See the LICENCE file in the repository root for full licence text. using System; using System.Collections.Generic; using System.Linq; using osu.Game.Beatmaps; using osu.Game.Extensions; using osu.Game.Rulesets.Difficulty; using osu.Game.Rulesets.Difficulty.Preprocessing; using osu.Game.Rulesets.Difficulty.Skills; using osu.Game.Rulesets.Karaoke.Beatmaps; using osu.Game.Rulesets.Karaoke.Difficulty.Preprocessing; using osu.Game.Rulesets.Karaoke.Difficulty.Skills; using osu.Game.Rulesets.Karaoke.Mods; using osu.Game.Rulesets.Karaoke.Objects; using osu.Game.Rulesets.Mods; namespace osu.Game.Rulesets.Karaoke.Difficulty; public class KaraokeDifficultyCalculator : DifficultyCalculator { private const double star_scaling_factor = 0.018; private readonly bool isForCurrentRuleset; private readonly double originalOverallDifficulty; public KaraokeDifficultyCalculator(IRulesetInfo ruleset, IWorkingBeatmap beatmap) : base(ruleset, beatmap) { isForCurrentRuleset = beatmap.BeatmapInfo.Ruleset.MatchesOnlineID(ruleset); originalOverallDifficulty = beatmap.BeatmapInfo.Difficulty.OverallDifficulty; } protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills, double clockRate) { if (beatmap.HitObjects.Count == 0) return new KaraokeDifficultyAttributes { Mods = mods }; return new KaraokeDifficultyAttributes { StarRating = skills[0].DifficultyValue() * star_scaling_factor, Mods = mods, // Todo: This int cast is temporary to achieve 1:1 results with osu!stable, and should be removed in the future GreatHitWindow = (int)Math.Ceiling(getHitWindow300(mods) / clockRate), MaxCombo = beatmap.HitObjects.Sum(h => h is Note ? 2 : 1), }; } protected override IEnumerable CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate) { var sortedObjects = beatmap.HitObjects.OfType().ToArray(); // todo : might have a sort. // LegacySortHelper.Sort(sortedObjects, Comparer.Create((a, b) => (int)Math.Round(a.StartTime) - (int)Math.Round(b.StartTime))); var objects = new List(); for (int i = 1; i < sortedObjects.Length; i++) objects.Add(new KaraokeDifficultyHitObject(sortedObjects[i], sortedObjects[i - 1], clockRate, objects, objects.Count)); return objects; } // Sorting is done in CreateDifficultyHitObjects, since the full list of hitobjects is required. protected override IEnumerable SortObjects(IEnumerable input) => input; protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods, double clockRate) => new Skill[] { new Strain(mods, ((KaraokeBeatmap)beatmap).NoteInfo), }; protected override Mod[] DifficultyAdjustmentMods => new Mod[] { new KaraokeModDisableNote(), new KaraokeModHiddenNote(), }; private int getHitWindow300(Mod[] mods) { if (!isForCurrentRuleset) return applyModAdjustments(Math.Round(originalOverallDifficulty) > 4 ? 34 : 47, mods); double od = Math.Min(10.0, Math.Max(0, 10.0 - originalOverallDifficulty)); return applyModAdjustments(34 + 3 * od, mods); static int applyModAdjustments(double value, Mod[] mods) { if (mods.Any(m => m is KaraokeModDisableNote)) value /= 1.4; else if (mods.Any(m => m is KaraokeModHiddenNote)) value *= 1.4; return (int)value; } } } ================================================ FILE: osu.Game.Rulesets.Karaoke/Difficulty/KaraokePerformanceAttributes.cs ================================================ // Copyright (c) andy840119 . Licensed under the GPL Licence. // See the LICENCE file in the repository root for full licence text. using System.Collections.Generic; using Newtonsoft.Json; using osu.Game.Rulesets.Difficulty; namespace osu.Game.Rulesets.Karaoke.Difficulty; public class KaraokePerformanceAttributes : PerformanceAttributes { [JsonProperty("difficulty")] public double Difficulty { get; set; } [JsonProperty("accuracy")] public double Accuracy { get; set; } [JsonProperty("scaled_score")] public double ScaledScore { get; set; } public override IEnumerable GetAttributesForDisplay() { foreach (var attribute in base.GetAttributesForDisplay()) yield return attribute; yield return new PerformanceDisplayAttribute(nameof(Difficulty), "Difficulty", Difficulty); yield return new PerformanceDisplayAttribute(nameof(Accuracy), "Accuracy", Accuracy); } } ================================================ FILE: osu.Game.Rulesets.Karaoke/Difficulty/KaraokePerformanceCalculator.cs ================================================ // Copyright (c) andy840119 . Licensed under the GPL Licence. // See the LICENCE file in the repository root for full licence text. using System; using System.Collections.Generic; using System.Linq; using osu.Game.Rulesets.Difficulty; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Scoring; using osu.Game.Scoring; namespace osu.Game.Rulesets.Karaoke.Difficulty; public class KaraokePerformanceCalculator : PerformanceCalculator { // Score after being scaled by non-difficulty-increasing mods private double scaledScore; private int countPerfect; private int countGreat; private int countGood; private int countOk; private int countMeh; private int countMiss; public KaraokePerformanceCalculator() : base(new KaraokeRuleset()) { } protected override PerformanceAttributes CreatePerformanceAttributes(ScoreInfo score, DifficultyAttributes attributes) { var karaokeAttributes = (KaraokeDifficultyAttributes)attributes; scaledScore = score.TotalScore; countPerfect = score.Statistics.GetValueOrDefault(HitResult.Perfect); countGreat = score.Statistics.GetValueOrDefault(HitResult.Great); countGood = score.Statistics.GetValueOrDefault(HitResult.Good); countOk = score.Statistics.GetValueOrDefault(HitResult.Ok); countMeh = score.Statistics.GetValueOrDefault(HitResult.Meh); countMiss = score.Statistics.GetValueOrDefault(HitResult.Miss); if (karaokeAttributes.ScoreMultiplier > 0) { // Scale score up, so it's comparable to other keymods scaledScore *= 1.0 / karaokeAttributes.ScoreMultiplier; } // Arbitrary initial value for scaling pp in order to standardize distributions across game modes. // The specific number has no intrinsic meaning and can be adjusted as needed. double multiplier = 0.8; if (score.Mods.Any(m => m is ModNoFail)) multiplier *= 0.9; if (score.Mods.Any(m => m is ModEasy)) multiplier *= 0.5; double difficultyValue = computeDifficultyValue(karaokeAttributes); double accValue = computeAccuracyValue(difficultyValue, karaokeAttributes); double totalValue = Math.Pow( Math.Pow(difficultyValue, 1.1) + Math.Pow(accValue, 1.1), 1.0 / 1.1 ) * multiplier; return new KaraokePerformanceAttributes { Difficulty = difficultyValue, Accuracy = accValue, ScaledScore = scaledScore, Total = totalValue, }; } private double computeDifficultyValue(KaraokeDifficultyAttributes attributes) { double difficultyValue = Math.Pow(5 * Math.Max(1, attributes.StarRating / 0.2) - 4.0, 2.2) / 135.0; difficultyValue *= 1.0 + 0.1 * Math.Min(1.0, totalHits / 1500.0); switch (scaledScore) { case <= 500000: difficultyValue = 0; break; case <= 600000: difficultyValue *= (scaledScore - 500000) / 100000 * 0.3; break; case <= 700000: difficultyValue *= 0.3 + (scaledScore - 600000) / 100000 * 0.25; break; case <= 800000: difficultyValue *= 0.55 + (scaledScore - 700000) / 100000 * 0.20; break; case <= 900000: difficultyValue *= 0.75 + (scaledScore - 800000) / 100000 * 0.15; break; default: difficultyValue *= 0.90 + (scaledScore - 900000) / 100000 * 0.1; break; } return difficultyValue; } private double computeAccuracyValue(double difficultyValue, KaraokeDifficultyAttributes attributes) { if (attributes.GreatHitWindow <= 0) return 0; // Lots of arbitrary values from testing. // Considering to use derivation from perfect accuracy in a probabilistic manner - assume normal distribution double accuracyValue = Math.Max(0.0, 0.2 - (attributes.GreatHitWindow - 34) * 0.006667) * difficultyValue * Math.Pow(Math.Max(0.0, scaledScore - 960000) / 40000, 1.1); return accuracyValue; } private double totalHits => countPerfect + countOk + countGreat + countGood + countMeh + countMiss; } ================================================ FILE: osu.Game.Rulesets.Karaoke/Difficulty/Preprocessing/KaraokeDifficultyHitObject.cs ================================================ // Copyright (c) andy840119 . Licensed under the GPL Licence. // See the LICENCE file in the repository root for full licence text. using System.Collections.Generic; using osu.Game.Rulesets.Difficulty.Preprocessing; using osu.Game.Rulesets.Karaoke.Objects; using osu.Game.Rulesets.Objects; namespace osu.Game.Rulesets.Karaoke.Difficulty.Preprocessing; public class KaraokeDifficultyHitObject : DifficultyHitObject { public new Note BaseObject => (Note)base.BaseObject; public KaraokeDifficultyHitObject(HitObject hitObject, HitObject lastObject, double clockRate, List objects, int index) : base(hitObject, lastObject, clockRate, objects, index) { } } ================================================ FILE: osu.Game.Rulesets.Karaoke/Difficulty/Skills/Strain.cs ================================================ // Copyright (c) andy840119 . Licensed under the GPL Licence. // See the LICENCE file in the repository root for full licence text. using System; using osu.Framework.Utils; using osu.Game.Rulesets.Difficulty.Preprocessing; using osu.Game.Rulesets.Difficulty.Skills; using osu.Game.Rulesets.Karaoke.Beatmaps.Metadatas; using osu.Game.Rulesets.Karaoke.Difficulty.Preprocessing; using osu.Game.Rulesets.Karaoke.Objects; using osu.Game.Rulesets.Mods; namespace osu.Game.Rulesets.Karaoke.Difficulty.Skills; public class Strain : StrainDecaySkill { private const double individual_decay_base = 0.125; private const double overall_decay_base = 0.30; protected override double SkillMultiplier => 1; protected override double StrainDecayBase => 1; private readonly double[] holdEndTimes; private readonly double[] individualStrains; private double individualStrain; private double overallStrain; public Strain(Mod[] mods, NoteInfo noteInfo) : base(mods) { int totalColumns = noteInfo.Columns; holdEndTimes = new double[totalColumns * 2 - 1]; individualStrains = new double[totalColumns * 2 - 1]; overallStrain = 1; } protected override double StrainValueOf(DifficultyHitObject current) { var maniaCurrent = (KaraokeDifficultyHitObject)current; double endTime = maniaCurrent.EndTime; int column = getColumnIndex(maniaCurrent.BaseObject.Tone); double holdFactor = 1.0; // Factor to all additional strains in case something else is held double holdAddition = 0; // Addition to the current note in case it's a hold and has to be released awkwardly // Fill up the holdEndTimes array for (int i = 0; i < holdEndTimes.Length; ++i) { // If there is at least one other overlapping end or note, then we get an addition, buuuuuut... if (Precision.DefinitelyBigger(holdEndTimes[i], maniaCurrent.StartTime, 1) && Precision.DefinitelyBigger(endTime, holdEndTimes[i], 1)) holdAddition = 1.0; // ... this addition only is valid if there is _no_ other note with the same ending. Releasing multiple notes at the same time is just as easy as releasing 1 if (Precision.AlmostEquals(endTime, holdEndTimes[i], 1)) holdAddition = 0; // We give a slight bonus to everything if something is held meanwhile if (Precision.DefinitelyBigger(holdEndTimes[i], endTime, 1)) holdFactor = 1.25; // Decay individual strains individualStrains[i] = applyDecay(individualStrains[i], current.DeltaTime, individual_decay_base); } holdEndTimes[column] = endTime; // Increase individual strain in own column individualStrains[column] += 2.0 * holdFactor; individualStrain = individualStrains[column]; overallStrain = applyDecay(overallStrain, current.DeltaTime, overall_decay_base) + (1 + holdAddition) * holdFactor; return individualStrain + overallStrain - CurrentStrain; // todo : implementation. static int getColumnIndex(Tone tone) => 0; } protected override double CalculateInitialStrain(double offset, DifficultyHitObject current) => applyDecay(individualStrain, offset - current.Previous(0).StartTime, individual_decay_base) + applyDecay(overallStrain, offset - current.Previous(0).StartTime, overall_decay_base); private double applyDecay(double value, double deltaTime, double decayBase) => value * Math.Pow(decayBase, deltaTime / 1000); } ================================================ FILE: osu.Game.Rulesets.Karaoke/Edit/Blueprints/KaraokeSelectionBlueprint.cs ================================================ // Copyright (c) andy840119 . Licensed under the GPL Licence. // See the LICENCE file in the repository root for full licence text. using osu.Framework.Graphics; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Karaoke.Objects; namespace osu.Game.Rulesets.Karaoke.Edit.Blueprints; public partial class KaraokeSelectionBlueprint : HitObjectSelectionBlueprint where T : KaraokeHitObject { protected KaraokeSelectionBlueprint(T hitObject) : base(hitObject) { RelativeSizeAxes = Axes.None; } } ================================================ FILE: osu.Game.Rulesets.Karaoke/Edit/Blueprints/Lyrics/LyricSelectionBlueprint.cs ================================================ // Copyright (c) andy840119 . Licensed under the GPL Licence. // See the LICENCE file in the repository root for full licence text. using osu.Game.Rulesets.Karaoke.Objects; namespace osu.Game.Rulesets.Karaoke.Edit.Blueprints.Lyrics; public partial class LyricSelectionBlueprint : KaraokeSelectionBlueprint { public LyricSelectionBlueprint(Lyric lyric) : base(lyric) { } } ================================================ FILE: osu.Game.Rulesets.Karaoke/Edit/Blueprints/Notes/Components/EditBodyPiece.cs ================================================ // Copyright (c) andy840119 . Licensed under the GPL Licence. // See the LICENCE file in the repository root for full licence text. using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Game.Graphics; using osu.Game.Rulesets.Karaoke.Skinning.Default; namespace osu.Game.Rulesets.Karaoke.Edit.Blueprints.Notes.Components; public partial class EditBodyPiece : Container { [BackgroundDependencyLoader] private void load(OsuColour colours) { Masking = true; BorderColour = colours.Yellow; BorderThickness = 2; CornerRadius = DefaultBodyPiece.CORNER_RADIUS; Child = new Box { RelativeSizeAxes = Axes.Both, AlwaysPresent = true, Alpha = 0, }; } } ================================================ FILE: osu.Game.Rulesets.Karaoke/Edit/Blueprints/Notes/NoteSelectionBlueprint.cs ================================================ // Copyright (c) andy840119 . Licensed under the GPL Licence. // See the LICENCE file in the repository root for full licence text. using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.UserInterface; using osu.Framework.Input.Events; using osu.Game.Graphics.UserInterface; using osu.Game.Rulesets.Karaoke.Edit.Blueprints.Notes.Components; using osu.Game.Rulesets.Karaoke.Edit.ChangeHandlers.Notes; using osu.Game.Rulesets.Karaoke.Objects; using osu.Game.Rulesets.Karaoke.UI; using osu.Game.Rulesets.Karaoke.UI.Position; using osu.Game.Rulesets.UI; using osu.Game.Rulesets.UI.Scrolling; using osu.Game.Screens.Edit; namespace osu.Game.Rulesets.Karaoke.Edit.Blueprints.Notes; public partial class NoteSelectionBlueprint : KaraokeSelectionBlueprint { [Resolved] private INotesChangeHandler notesChangeHandler { get; set; } = null!; [Resolved] private INotePropertyChangeHandler notePropertyChangeHandler { get; set; } = null!; [Resolved] private Playfield playfield { get; set; } = null!; [Resolved] private IScrollingInfo scrollingInfo { get; set; } = null!; [Resolved] private INotePositionInfo notePositionInfo { get; set; } = null!; [Resolved] private EditorBeatmap beatmap { get; set; } = null!; protected ScrollingHitObjectContainer HitObjectContainer => ((KaraokePlayfield)playfield).NotePlayfield.HitObjectContainer; public NoteSelectionBlueprint(Note note) : base(note) { AddInternal(new EditBodyPiece { RelativeSizeAxes = Axes.Both, }); } protected override void Update() { base.Update(); var anchor = scrollingInfo.Direction.Value == ScrollingDirection.Left ? Anchor.CentreLeft : Anchor.CentreRight; Anchor = Origin = anchor; Position = Parent.ToLocalSpace(HitObjectContainer.ScreenSpacePositionAtTime(HitObject.StartTime)) - AnchorPosition; Y += notePositionInfo.Calculator.YPositionAt(HitObject.Tone); Width = HitObjectContainer.LengthAtTime(HitObject.StartTime, HitObject.EndTime); Height = notePositionInfo.Calculator.ColumnHeight; } public override MenuItem[] ContextMenuItems => new MenuItem[] { new OsuMenuItem(HitObject.Display ? "Hide" : "Show", HitObject.Display ? MenuItemType.Destructive : MenuItemType.Standard, () => notePropertyChangeHandler.ChangeDisplayState(!HitObject.Display)), new OsuMenuItem("Split", MenuItemType.Destructive, () => notesChangeHandler.Split()), }; protected override bool OnClick(ClickEvent e) { // should only select current note before open the popover because note change handler will change property in all selected notes. beatmap.SelectedHitObjects.Clear(); beatmap.SelectedHitObjects.Add(HitObject); return base.OnClick(e); } } ================================================ FILE: osu.Game.Rulesets.Karaoke/Edit/ChangeHandlers/BeatmapListPropertyChangeHandler.cs ================================================ // Copyright (c) andy840119 . Licensed under the GPL Licence. // See the LICENCE file in the repository root for full licence text. using System; using System.Collections.Generic; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Game.Rulesets.Karaoke.Beatmaps; using osu.Game.Rulesets.Karaoke.Edit.Utils; using osu.Game.Rulesets.Karaoke.Objects; using osu.Game.Screens.Edit; namespace osu.Game.Rulesets.Karaoke.Edit.ChangeHandlers; // todo: not a good design because eventually karaoke beatmap will not have the the field with list type. // it should be wrap into class (e.g. localizationInfo) with list of translation inside. // so guess this class will be removed eventually. public abstract partial class BeatmapListPropertyChangeHandler : Component { [Resolved] private EditorBeatmap beatmap { get; set; } = null!; private KaraokeBeatmap karaokeBeatmap => EditorBeatmapUtils.GetPlayableBeatmap(beatmap); protected IEnumerable Lyrics => karaokeBeatmap.HitObjects.OfType(); // todo: should be interface. protected BindableList Items = new(); [BackgroundDependencyLoader] private void load() { Items.AddRange(GetItemsFromBeatmap(karaokeBeatmap)); // todo: find a better way to handle only beatmap property changed. beatmap.TransactionEnded += syncItemsFromBeatmap; syncItemsFromBeatmap(); void syncItemsFromBeatmap() { var items = GetItemsFromBeatmap(karaokeBeatmap); if (Items.SequenceEqual(items)) return; Items.AddRange(items.Except(Items)); Items.RemoveAll(x => !items.Contains(x)); } } protected void PerformObjectChanged(TItem item, Action? action) { // should call change from editor beatmap because there's only way to trigger transaction ended. beatmap.BeginChange(); action?.Invoke(item); beatmap.EndChange(); } protected abstract IList GetItemsFromBeatmap(KaraokeBeatmap beatmap); public void Add(TItem item) { var items = GetItemsFromBeatmap(karaokeBeatmap); if (items.Contains(item)) throw new InvalidOperationException(nameof(item)); PerformObjectChanged(item, i => { items.Add(i); OnItemAdded(i); }); } public void Remove(TItem item) { var items = GetItemsFromBeatmap(karaokeBeatmap); if (!items.Contains(item)) throw new InvalidOperationException($"{nameof(item)} is not in the list"); PerformObjectChanged(item, i => { items.Remove(i); OnItemRemoved(i); }); } protected abstract void OnItemAdded(TItem item); protected abstract void OnItemRemoved(TItem item); } ================================================ FILE: osu.Game.Rulesets.Karaoke/Edit/ChangeHandlers/BeatmapPropertyChangeHandler.cs ================================================ // Copyright (c) andy840119 . Licensed under the GPL Licence. // See the LICENCE file in the repository root for full licence text. using System; using System.Collections.Generic; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Caching; using osu.Framework.Graphics; using osu.Game.Rulesets.Karaoke.Beatmaps; using osu.Game.Rulesets.Karaoke.Edit.Utils; using osu.Game.Rulesets.Karaoke.Objects; using osu.Game.Rulesets.Karaoke.Objects.Types; using osu.Game.Rulesets.Objects; using osu.Game.Screens.Edit; namespace osu.Game.Rulesets.Karaoke.Edit.ChangeHandlers; public partial class BeatmapPropertyChangeHandler : Component { private readonly Cached changingCache = new(); [Resolved] private EditorBeatmap beatmap { get; set; } = null!; protected KaraokeBeatmap KaraokeBeatmap => EditorBeatmapUtils.GetPlayableBeatmap(beatmap); protected IEnumerable Lyrics => KaraokeBeatmap.HitObjects.OfType(); protected BeatmapPropertyChangeHandler() { changingCache.Validate(); } protected void PerformBeatmapChanged(Action action) { try { beatmap.BeginChange(); action(KaraokeBeatmap); beatmap.EndChange(); } catch { // We should make sure that editor beatmap will end the change if still changing. // will goes to here if have exception in the change handler. if (beatmap.TransactionActive) beatmap.EndChange(); throw; } } protected void PerformOnSelection(Action action) where T : HitObject { if (!changingCache.IsValid) throw new NotSupportedException("Cannot trigger the change while applying another change."); if (beatmap.SelectedHitObjects.Count == 0) throw new NotSupportedException($"Should contain at least one selected {nameof(T)}"); changingCache.Invalidate(); try { // should trigger the UpdateState() in the editor beatmap only if there's no active state. beatmap.PerformOnSelection(h => { if (h is T tHitObject) action(tHitObject); }); } catch { // We should make sure that editor beatmap will end the change if still changing. // will goes to here if have exception in the change handler. if (beatmap.TransactionActive) beatmap.EndChange(); throw; } finally { changingCache.Validate(); } } // todo: before having better solution to handle the undo/redo with better performance, we should use this to method to force invalidate all hit-object's working property. protected void InvalidateAllHitObjectWorkingProperty(TWorkingProperty property) where TWorkingProperty : struct, Enum { foreach (var hitObject in KaraokeBeatmap.HitObjects.OfType>()) { hitObject.InvalidateWorkingProperty(property); } beatmap.UpdateAllHitObjects(); } } ================================================ FILE: osu.Game.Rulesets.Karaoke/Edit/ChangeHandlers/Beatmaps/BeatmapPagesChangeHandler.cs ================================================ // Copyright (c) andy840119 . Licensed under the GPL Licence. // See the LICENCE file in the repository root for full licence text. using System; using System.Collections.Generic; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Localisation; using osu.Game.Rulesets.Karaoke.Beatmaps.Metadatas; using osu.Game.Rulesets.Karaoke.Configuration; using osu.Game.Rulesets.Karaoke.Edit.Generator.Beatmaps.Pages; using osu.Game.Rulesets.Karaoke.Objects.Workings; namespace osu.Game.Rulesets.Karaoke.Edit.ChangeHandlers.Beatmaps; public partial class BeatmapPagesChangeHandler : BeatmapPropertyChangeHandler, IBeatmapPagesChangeHandler { #region Auto-Generate [Resolved] private KaraokeRulesetEditGeneratorConfigManager generatorConfigManager { get; set; } = null!; public bool CanGenerate() { var config = getGeneratorConfig(); var generator = new PageGenerator(config); return generator.CanGenerate(KaraokeBeatmap); } public LocalisableString? GetGeneratorNotSupportedMessage() { var config = getGeneratorConfig(); var generator = new PageGenerator(config); return generator.GetInvalidMessage(KaraokeBeatmap); } public void AutoGenerate() { var config = getGeneratorConfig(); var generator = new PageGenerator(config); var pages = generator.Generate(KaraokeBeatmap); performPageInfoChanged(pageInfo => { if (config.ClearExistPages.Value) pageInfo.Pages.Clear(); pageInfo.Pages.AddRange(pages); }); } private PageGeneratorConfig getGeneratorConfig() => generatorConfigManager.Get(); #endregion public void Add(Page page) { performPageInfoChanged(pageInfo => { if (checkPageExist(pageInfo, page)) throw new InvalidOperationException($"Should not add duplicated {nameof(page)} into the {nameof(pageInfo)}."); pageInfo.Pages.Add(page); }); } public void Remove(Page page) { performPageInfoChanged(pageInfo => { if (!checkPageExist(pageInfo, page)) throw new InvalidOperationException($"{nameof(page)} does ont in the {nameof(pageInfo)}."); pageInfo.Pages.Remove(page); }); } public void RemoveRange(IEnumerable pages) { performPageInfoChanged(pageInfo => { foreach (var page in pages.ToArray()) { if (!checkPageExist(pageInfo, page)) throw new InvalidOperationException($"{nameof(page)} does ont in the {nameof(pageInfo)}."); pageInfo.Pages.Remove(page); } }); } public void ShiftingPageTime(IEnumerable pages, double offset) { performPageInfoChanged(pageInfo => { foreach (var page in pages) { if (!checkPageExist(pageInfo, page)) throw new InvalidOperationException($"{nameof(page)} does ont in the {nameof(pageInfo)}."); page.Time += offset; } }); } private static bool checkPageExist(PageInfo pageInfo, Page page) { return pageInfo.Pages.Contains(page); } private void performPageInfoChanged(Action action) { PerformBeatmapChanged(beatmap => { action(beatmap.PageInfo); InvalidateAllHitObjectWorkingProperty(LyricWorkingProperty.Page); InvalidateAllHitObjectWorkingProperty(NoteWorkingProperty.Page); }); } } ================================================ FILE: osu.Game.Rulesets.Karaoke/Edit/ChangeHandlers/Beatmaps/BeatmapSingersChangeHandler.cs ================================================ // Copyright (c) andy840119 . Licensed under the GPL Licence. // See the LICENCE file in the repository root for full licence text. using System; using System.IO; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions.IEnumerableExtensions; using osu.Game.Beatmaps; using osu.Game.Rulesets.Karaoke.Beatmaps.Metadatas; using osu.Game.Rulesets.Karaoke.Beatmaps.Metadatas.Types; using osu.Game.Rulesets.Karaoke.Objects.Utils; namespace osu.Game.Rulesets.Karaoke.Edit.ChangeHandlers.Beatmaps; public partial class BeatmapSingersChangeHandler : BeatmapPropertyChangeHandler, IBeatmapSingersChangeHandler { [Resolved] private BeatmapManager? beatmapManager { get; set; } [Resolved] private IBindable? working { get; set; } private SingerInfo singerInfo => KaraokeBeatmap.SingerInfo; public BindableList Singers => singerInfo.Singers; public void ChangeOrder(ISinger singer, int newIndex) { performSingerChanged(singer, s => { int oldOrder = s.Order; int newOrder = newIndex + 1; // order is start from 1 OrderUtils.ChangeOrder(Singers.ToArray(), oldOrder, newOrder, (switchSinger, oldOrder, newOrder) => { // todo : not really sure should call update? }); }); } public bool ChangeSingerAvatar(Singer singer, FileInfo fileInfo) { if (beatmapManager == null || working == null) return false; if (!fileInfo.Exists) throw new FileNotFoundException(); // note: follow the same logic in the ResourcesSection.ChangeBackgroundImage var set = working.Value.BeatmapSetInfo; // todo: we might re-format the new file name, like give it a hash name for prevent duplicated file name with other singer. string newFileName = fileInfo.Name; using (var stream = fileInfo.OpenRead()) { // in the future we probably want to check if this is being used elsewhere (other difficulties?) var oldFile = set.Files.FirstOrDefault(f => f.Filename == singer.AvatarFile); if (oldFile != null) beatmapManager.DeleteFile(set, oldFile); beatmapManager.AddFile(set, stream, $"assets/singers/{newFileName}"); } performSingerChanged(singer, s => { // Write-back the file name. s.AvatarFile = newFileName; }); return true; } public Singer Add() { var newSinger = singerInfo.AddSinger(s => { s.Order = getMaxSingerOrder() + 1; s.Name = "New singer"; }); return newSinger; int getMaxSingerOrder() => OrderUtils.GetMaxOrderNumber(singerInfo.GetAllSingers()); } public void Remove(Singer singer) { singerInfo.RemoveSinger(singer); // Should re-sort the order OrderUtils.ShiftingOrder(singerInfo.GetAllSingers().Where(x => x.Order > singer.Order), -1); // should clear removed singer ids in singer editor. Lyrics.ForEach(x => { x.SingerIds.Remove(singer.ID); }); } private void performSingerInfoChanged(Action action) { PerformBeatmapChanged(beatmap => { action(beatmap.SingerInfo); }); } private void performSingerChanged(TSinger singer, Action action) where TSinger : ISinger { performSingerInfoChanged(singerInfo => { if (!singerInfo.HasSinger(singer)) throw new InvalidOperationException("Singer should be in the beatmap"); action(singer); }); } } ================================================ FILE: osu.Game.Rulesets.Karaoke/Edit/ChangeHandlers/Beatmaps/BeatmapTranslationsChangeHandler.cs ================================================ // Copyright (c) andy840119 . Licensed under the GPL Licence. // See the LICENCE file in the repository root for full licence text. using System.Collections.Generic; using System.Globalization; using System.Linq; using osu.Framework.Bindables; using osu.Game.Rulesets.Karaoke.Beatmaps; namespace osu.Game.Rulesets.Karaoke.Edit.ChangeHandlers.Beatmaps; public partial class BeatmapTranslationsChangeHandler : BeatmapListPropertyChangeHandler, IBeatmapTranslationsChangeHandler { public IBindableList Languages => Items; protected override IList GetItemsFromBeatmap(KaraokeBeatmap beatmap) => beatmap.AvailableTranslationLanguages; protected override void OnItemAdded(CultureInfo item) { // there's no need to do anything. } protected override void OnItemRemoved(CultureInfo item) { // Delete from lyric also. foreach (var lyric in Lyrics.Where(lyric => lyric.Translations.ContainsKey(item))) { lyric.Translations.Remove(item); } } public bool IsLanguageContainsTranslation(CultureInfo cultureInfo) => Lyrics.Any(x => x.Translations.ContainsKey(cultureInfo)); } ================================================ FILE: osu.Game.Rulesets.Karaoke/Edit/ChangeHandlers/Beatmaps/IBeatmapPagesChangeHandler.cs ================================================ // Copyright (c) andy840119 . Licensed under the GPL Licence. // See the LICENCE file in the repository root for full licence text. using System.Collections.Generic; using osu.Framework.Localisation; using osu.Game.Rulesets.Karaoke.Beatmaps.Metadatas; namespace osu.Game.Rulesets.Karaoke.Edit.ChangeHandlers.Beatmaps; public interface IBeatmapPagesChangeHandler : IAutoGenerateChangeHandler { LocalisableString? GetGeneratorNotSupportedMessage(); void Add(Page page); void Remove(Page page); void RemoveRange(IEnumerable pages); void ShiftingPageTime(IEnumerable pages, double offset); } ================================================ FILE: osu.Game.Rulesets.Karaoke/Edit/ChangeHandlers/Beatmaps/IBeatmapSingersChangeHandler.cs ================================================ // Copyright (c) andy840119 . Licensed under the GPL Licence. // See the LICENCE file in the repository root for full licence text. using System.IO; using osu.Framework.Bindables; using osu.Game.Rulesets.Karaoke.Beatmaps.Metadatas; using osu.Game.Rulesets.Karaoke.Beatmaps.Metadatas.Types; namespace osu.Game.Rulesets.Karaoke.Edit.ChangeHandlers.Beatmaps; public interface IBeatmapSingersChangeHandler { // todo: should use IBindableList eventually, but cannot do that because it's bind to selection item. BindableList Singers { get; } void ChangeOrder(ISinger singer, int newIndex); bool ChangeSingerAvatar(Singer singer, FileInfo fileInfo); Singer Add(); void Remove(Singer singer); } ================================================ FILE: osu.Game.Rulesets.Karaoke/Edit/ChangeHandlers/Beatmaps/IBeatmapTranslationsChangeHandler.cs ================================================ // Copyright (c) andy840119 . Licensed under the GPL Licence. // See the LICENCE file in the repository root for full licence text. using System.Globalization; using osu.Framework.Bindables; namespace osu.Game.Rulesets.Karaoke.Edit.ChangeHandlers.Beatmaps; public interface IBeatmapTranslationsChangeHandler { IBindableList Languages { get; } void Add(CultureInfo culture); void Remove(CultureInfo culture); bool IsLanguageContainsTranslation(CultureInfo cultureInfo); } ================================================ FILE: osu.Game.Rulesets.Karaoke/Edit/ChangeHandlers/ChangeForbiddenException.cs ================================================ // Copyright (c) andy840119 . Licensed under the GPL Licence. // See the LICENCE file in the repository root for full licence text. using System; namespace osu.Game.Rulesets.Karaoke.Edit.ChangeHandlers; public class ChangeForbiddenException : InvalidOperationException { public ChangeForbiddenException(string message) : base(message) { } } ================================================ FILE: osu.Game.Rulesets.Karaoke/Edit/ChangeHandlers/HitObjectChangeHandler.cs ================================================ // Copyright (c) andy840119 . Licensed under the GPL Licence. // See the LICENCE file in the repository root for full licence text. using System; using System.Collections.Generic; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Caching; using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Graphics; using osu.Game.Rulesets.Karaoke.Edit.Utils; using osu.Game.Rulesets.Karaoke.Objects; using osu.Game.Rulesets.Objects; using osu.Game.Screens.Edit; namespace osu.Game.Rulesets.Karaoke.Edit.ChangeHandlers; public abstract partial class HitObjectChangeHandler : Component where THitObject : HitObject { private readonly Cached changingCache = new(); private bool triggerBeatmapSave = true; [Resolved] private EditorBeatmap beatmap { get; set; } = null!; protected IEnumerable HitObjects => beatmap.HitObjects.OfType(); protected HitObjectChangeHandler() { changingCache.Validate(); } protected void CheckExactlySelectedOneHitObject() { if (beatmap.SelectedHitObjects.OfType().Count() != 1) throw new InvalidOperationException($"Should be exactly one {nameof(THitObject)} being selected."); } // Can remove this method after as TransactionalCommitComponent injection for all rulesets(means customized ruleset is able to save/load beatmap). // we can use changeHandler.TransactionActive to check if there's any active transaction. // e.g. : changeHandler is TransactionalCommitComponent transactionalCommitComponent && !transactionalCommitComponent.TransactionActive protected void NotTriggerSaveStateOnThisChange() { triggerBeatmapSave = false; } protected virtual void PerformOnSelection(Action action) => PerformOnSelection(action); protected void PerformOnSelection(Action action) where T : HitObject { if (!changingCache.IsValid) throw new NotSupportedException("Cannot trigger the change while applying another change."); if (beatmap.SelectedHitObjects.Count == 0) throw new NotSupportedException($"Should contain at least one selected {nameof(THitObject)}"); changingCache.Invalidate(); try { // todo: follow-up the discussion in the https://github.com/karaoke-dev/karaoke/pull/1669 after support the change handler for customized ruleset. if (triggerBeatmapSave) { // should trigger the UpdateState() in the editor beatmap only if there's no active state. beatmap.PerformOnSelection(h => { if (h is T tHitObject) action(tHitObject); }); } else { // Just update the object property if already in the changing state. // e.g. dragging. beatmap.SelectedHitObjects.ForEach(h => { if (h is T tHitObject) action(tHitObject); }); } } catch { // We should make sure that editor beatmap will end the change if still changing. // will goes to here if have exception in the change handler. if (beatmap.TransactionActive) beatmap.EndChange(); throw; } finally { changingCache.Validate(); triggerBeatmapSave = true; } } protected void AddRange(IEnumerable hitObjects) where T : HitObject => hitObjects.ForEach(Add); protected virtual void Add(T hitObject) where T : HitObject { bool containsInBeatmap = HitObjects.Any(x => x == hitObject); if (containsInBeatmap) throw new InvalidOperationException("Seems this hit object is already in the beatmap."); if (isCreateObjectLocked(hitObject)) throw new AddOrRemoveForbiddenException(); beatmap.Add(hitObject); } protected virtual void Insert(int index, T hitObject) where T : HitObject { bool containsInBeatmap = HitObjects.Any(x => x == hitObject); if (containsInBeatmap) throw new InvalidOperationException("Seems this hit object is already in the beatmap."); if (isCreateObjectLocked(hitObject)) throw new AddOrRemoveForbiddenException(); beatmap.Insert(index, hitObject); } protected void RemoveRange(IEnumerable hitObjects) where T : HitObject => hitObjects.ForEach(Remove); protected void Remove(T hitObject) where T : HitObject { if (isRemoveObjectLocked(hitObject)) throw new AddOrRemoveForbiddenException(); beatmap.Remove(hitObject); } private bool isCreateObjectLocked(T hitObject) { return hitObject switch { Lyric => false, Note note => note.ReferenceLyric != null && HitObjectWritableUtils.IsCreateOrRemoveNoteLocked(note.ReferenceLyric), _ => throw new InvalidCastException(), }; } private bool isRemoveObjectLocked(T hitObject) { switch (hitObject) { case Lyric lyric: bool hasReferenceLyric = EditorBeatmapUtils.GetAllReferenceLyrics(beatmap, lyric).Any(); return hasReferenceLyric || HitObjectWritableUtils.IsRemoveLyricLocked(lyric); case Note note: return note.ReferenceLyric != null && HitObjectWritableUtils.IsCreateOrRemoveNoteLocked(note.ReferenceLyric); default: throw new InvalidCastException(); } } protected void TriggerHitObjectUpdate(T hitObject) where T : HitObject { beatmap.Update(hitObject); } public class AddOrRemoveForbiddenException : Exception { public AddOrRemoveForbiddenException() : base("Should not add or remove the hit-object.") { } } } ================================================ FILE: osu.Game.Rulesets.Karaoke/Edit/ChangeHandlers/HitObjectPropertyChangeHandler.cs ================================================ // Copyright (c) andy840119 . Licensed under the GPL Licence. // See the LICENCE file in the repository root for full licence text. using System; using System.Linq; using osu.Framework.Allocation; using osu.Game.Rulesets.Objects; using osu.Game.Screens.Edit; namespace osu.Game.Rulesets.Karaoke.Edit.ChangeHandlers; public abstract partial class HitObjectPropertyChangeHandler : HitObjectChangeHandler, IHitObjectPropertyChangeHandler where THitObject : HitObject { [Resolved] private EditorBeatmap beatmap { get; set; } = null!; protected sealed override void PerformOnSelection(Action action) { // note: should not check lyric in the perform on selection because it will let change handler in lazer broken. if (beatmap.SelectedHitObjects.OfType().Any(IsWritePropertyLocked)) throw new ChangeForbiddenException("This property might be locked or it's a reference property."); base.PerformOnSelection(action); } protected abstract bool IsWritePropertyLocked(THitObject hitObject); public virtual bool IsSelectionsLocked() => beatmap.SelectedHitObjects.OfType().Any(IsWritePropertyLocked); } ================================================ FILE: osu.Game.Rulesets.Karaoke/Edit/ChangeHandlers/HitObjectsChangeHandler.cs ================================================ // Copyright (c) andy840119 . Licensed under the GPL Licence. // See the LICENCE file in the repository root for full licence text. using osu.Game.Rulesets.Objects; namespace osu.Game.Rulesets.Karaoke.Edit.ChangeHandlers; public abstract partial class HitObjectsChangeHandler : HitObjectChangeHandler where THitObject : HitObject; ================================================ FILE: osu.Game.Rulesets.Karaoke/Edit/ChangeHandlers/IAutoGenerateChangeHandler.cs ================================================ // Copyright (c) andy840119 . Licensed under the GPL Licence. // See the LICENCE file in the repository root for full licence text. using System; namespace osu.Game.Rulesets.Karaoke.Edit.ChangeHandlers; /// /// This interface is defined checking able to generate or detect the property, and make the change for the property. /// /// public interface IEnumAutoGenerateChangeHandler where TEnum : Enum { bool CanGenerate(TEnum type); void AutoGenerate(TEnum type); } /// /// This interface is defined checking able to generate or detect the property, and make the change for the property. /// /// public interface IAutoGenerateChangeHandler { bool CanGenerate() where T : TType; void AutoGenerate() where T : TType; } /// /// This interface is defined checking able to generate or detect the property, and make the change for the property. /// public interface IAutoGenerateChangeHandler { bool CanGenerate(); void AutoGenerate(); } ================================================ FILE: osu.Game.Rulesets.Karaoke/Edit/ChangeHandlers/IHitObjectPropertyChangeHandler.cs ================================================ // Copyright (c) andy840119 . Licensed under the GPL Licence. // See the LICENCE file in the repository root for full licence text. namespace osu.Game.Rulesets.Karaoke.Edit.ChangeHandlers; public interface IHitObjectPropertyChangeHandler { bool IsSelectionsLocked(); } ================================================ FILE: osu.Game.Rulesets.Karaoke/Edit/ChangeHandlers/IImportBeatmapChangeHandler.cs ================================================ // Copyright (c) andy840119 . Licensed under the GPL Licence. // See the LICENCE file in the repository root for full licence text. using osu.Game.Beatmaps; namespace osu.Game.Rulesets.Karaoke.Edit.ChangeHandlers; public interface IImportBeatmapChangeHandler { void Import(IBeatmap newBeatmap); } ================================================ FILE: osu.Game.Rulesets.Karaoke/Edit/ChangeHandlers/ILockChangeHandler.cs ================================================ // Copyright (c) andy840119 . Licensed under the GPL Licence. // See the LICENCE file in the repository root for full licence text. using osu.Game.Rulesets.Karaoke.Objects.Types; namespace osu.Game.Rulesets.Karaoke.Edit.ChangeHandlers; public interface ILockChangeHandler : IHitObjectPropertyChangeHandler { void Lock(LockState lockState); void Unlock(); } ================================================ FILE: osu.Game.Rulesets.Karaoke/Edit/ChangeHandlers/ImportBeatmapChangeHandler.cs ================================================ // Copyright (c) andy840119 . Licensed under the GPL Licence. // See the LICENCE file in the repository root for full licence text. using System.Linq; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Game.Beatmaps; using osu.Game.Rulesets.Karaoke.Objects; using osu.Game.Screens.Edit; namespace osu.Game.Rulesets.Karaoke.Edit.ChangeHandlers; public partial class ImportBeatmapChangeHandler : Component, IImportBeatmapChangeHandler { [Resolved] private EditorBeatmap beatmap { get; set; } = null!; public void Import(IBeatmap newBeatmap) { beatmap.BeginChange(); beatmap.Clear(); var lyrics = newBeatmap.HitObjects.OfType().ToArray(); beatmap.AddRange(lyrics); beatmap.EndChange(); } } ================================================ FILE: osu.Game.Rulesets.Karaoke/Edit/ChangeHandlers/LockChangeHandler.cs ================================================ // Copyright (c) andy840119 . Licensed under the GPL Licence. // See the LICENCE file in the repository root for full licence text. using System; using osu.Game.Rulesets.Karaoke.Edit.Utils; using osu.Game.Rulesets.Karaoke.Objects; using osu.Game.Rulesets.Karaoke.Objects.Types; namespace osu.Game.Rulesets.Karaoke.Edit.ChangeHandlers; public partial class LockChangeHandler : HitObjectPropertyChangeHandler, ILockChangeHandler { public void Lock(LockState lockState) { PerformOnSelection(h => { if (h is IHasLock hasLock) hasLock.Lock = lockState; }); } public void Unlock() { Lock(LockState.None); } protected sealed override bool IsWritePropertyLocked(KaraokeHitObject hitObject) { return hitObject switch { Lyric lyric => HitObjectWritableUtils.IsWriteLyricPropertyLocked(lyric, nameof(Lyric.Lock)), Note note => HitObjectWritableUtils.IsWriteNotePropertyLocked(note, nameof(Lyric.Lock)), _ => throw new NotSupportedException(), }; } } ================================================ FILE: osu.Game.Rulesets.Karaoke/Edit/ChangeHandlers/Lyrics/ILyricLanguageChangeHandler.cs ================================================ // Copyright (c) andy840119 . Licensed under the GPL Licence. // See the LICENCE file in the repository root for full licence text. using System.Globalization; namespace osu.Game.Rulesets.Karaoke.Edit.ChangeHandlers.Lyrics; public interface ILyricLanguageChangeHandler : ILyricPropertyChangeHandler { void SetLanguage(CultureInfo? language); } ================================================ FILE: osu.Game.Rulesets.Karaoke/Edit/ChangeHandlers/Lyrics/ILyricListPropertyChangeHandler.cs ================================================ // Copyright (c) andy840119 . Licensed under the GPL Licence. // See the LICENCE file in the repository root for full licence text. using System.Collections.Generic; namespace osu.Game.Rulesets.Karaoke.Edit.ChangeHandlers.Lyrics; public interface ILyricListPropertyChangeHandler : ILyricPropertyChangeHandler { void Add(TItem item); void AddRange(IEnumerable items); void Remove(TItem item); void RemoveRange(IEnumerable items); } ================================================ FILE: osu.Game.Rulesets.Karaoke/Edit/ChangeHandlers/Lyrics/ILyricPropertyAutoGenerateChangeHandler.cs ================================================ // Copyright (c) andy840119 . Licensed under the GPL Licence. // See the LICENCE file in the repository root for full licence text. using System.Collections.Generic; using osu.Framework.Localisation; using osu.Game.Rulesets.Karaoke.Objects; namespace osu.Game.Rulesets.Karaoke.Edit.ChangeHandlers.Lyrics; public interface ILyricPropertyAutoGenerateChangeHandler : ILyricPropertyChangeHandler, IEnumAutoGenerateChangeHandler { IDictionary GetGeneratorNotSupportedLyrics(AutoGenerateType type); } public enum AutoGenerateType { DetectReferenceLyric, DetectLanguage, AutoGenerateRubyTags, AutoGenerateTimeTags, AutoGenerateRomanisation, AutoGenerateNotes, } ================================================ FILE: osu.Game.Rulesets.Karaoke/Edit/ChangeHandlers/Lyrics/ILyricPropertyChangeHandler.cs ================================================ // Copyright (c) andy840119 . Licensed under the GPL Licence. // See the LICENCE file in the repository root for full licence text. namespace osu.Game.Rulesets.Karaoke.Edit.ChangeHandlers.Lyrics; public interface ILyricPropertyChangeHandler : IHitObjectPropertyChangeHandler; ================================================ FILE: osu.Game.Rulesets.Karaoke/Edit/ChangeHandlers/Lyrics/ILyricReferenceChangeHandler.cs ================================================ // Copyright (c) andy840119 . Licensed under the GPL Licence. // See the LICENCE file in the repository root for full licence text. using System; using osu.Game.Rulesets.Karaoke.Objects; using osu.Game.Rulesets.Karaoke.Objects.Properties; namespace osu.Game.Rulesets.Karaoke.Edit.ChangeHandlers.Lyrics; public interface ILyricReferenceChangeHandler : ILyricPropertyChangeHandler { void UpdateReferenceLyric(Lyric? referenceLyric); void SwitchToReferenceLyricConfig(); void SwitchToSyncLyricConfig(); void AdjustLyricConfig(Action action) where TConfig : IReferenceLyricPropertyConfig; } ================================================ FILE: osu.Game.Rulesets.Karaoke/Edit/ChangeHandlers/Lyrics/ILyricRubyTagsChangeHandler.cs ================================================ // Copyright (c) andy840119 . Licensed under the GPL Licence. // See the LICENCE file in the repository root for full licence text. using System.Collections.Generic; using osu.Game.Rulesets.Karaoke.Objects; namespace osu.Game.Rulesets.Karaoke.Edit.ChangeHandlers.Lyrics; public interface ILyricRubyTagsChangeHandler : ILyricListPropertyChangeHandler { void SetIndex(RubyTag rubyTag, int? startIndex, int? endIndex); void ShiftingIndex(IEnumerable rubyTags, int offset); void SetText(RubyTag rubyTag, string text); } ================================================ FILE: osu.Game.Rulesets.Karaoke/Edit/ChangeHandlers/Lyrics/ILyricSingerChangeHandler.cs ================================================ // Copyright (c) andy840119 . Licensed under the GPL Licence. // See the LICENCE file in the repository root for full licence text. using osu.Game.Rulesets.Karaoke.Beatmaps.Metadatas.Types; namespace osu.Game.Rulesets.Karaoke.Edit.ChangeHandlers.Lyrics; public interface ILyricSingerChangeHandler : ILyricListPropertyChangeHandler { void Clear(); } ================================================ FILE: osu.Game.Rulesets.Karaoke/Edit/ChangeHandlers/Lyrics/ILyricTextChangeHandler.cs ================================================ // Copyright (c) andy840119 . Licensed under the GPL Licence. // See the LICENCE file in the repository root for full licence text. namespace osu.Game.Rulesets.Karaoke.Edit.ChangeHandlers.Lyrics; public interface ILyricTextChangeHandler : ILyricPropertyChangeHandler { void InsertText(int charGap, string text); void DeleteLyricText(int charGap, int count = 1); } ================================================ FILE: osu.Game.Rulesets.Karaoke/Edit/ChangeHandlers/Lyrics/ILyricTimeTagsChangeHandler.cs ================================================ // Copyright (c) andy840119 . Licensed under the GPL Licence. // See the LICENCE file in the repository root for full licence text. using System.Collections.Generic; using osu.Framework.Graphics.Sprites; using osu.Game.Rulesets.Karaoke.Objects; namespace osu.Game.Rulesets.Karaoke.Edit.ChangeHandlers.Lyrics; public interface ILyricTimeTagsChangeHandler : ILyricListPropertyChangeHandler { void SetTimeTagTime(TimeTag timeTag, double time); void SetTimeTagFirstSyllable(TimeTag timeTag, bool firstSyllable); void SetTimeTagRomanisedSyllable(TimeTag timeTag, string? romanisedSyllable); void ShiftingTimeTagTime(IEnumerable timeTags, double offset); void ClearTimeTagTime(TimeTag timeTag); void ClearAllTimeTagTime(); void AddByPosition(TextIndex index); void RemoveByPosition(TextIndex index); TimeTag Shifting(TimeTag timeTag, ShiftingDirection direction, ShiftingType type); } public enum ShiftingDirection { Left, Right, } public enum ShiftingType { State, Index, } ================================================ FILE: osu.Game.Rulesets.Karaoke/Edit/ChangeHandlers/Lyrics/ILyricTranslationChangeHandler.cs ================================================ // Copyright (c) andy840119 . Licensed under the GPL Licence. // See the LICENCE file in the repository root for full licence text. using System.Globalization; namespace osu.Game.Rulesets.Karaoke.Edit.ChangeHandlers.Lyrics; public interface ILyricTranslationChangeHandler : ILyricPropertyChangeHandler { void UpdateTranslation(CultureInfo cultureInfo, string translation); } ================================================ FILE: osu.Game.Rulesets.Karaoke/Edit/ChangeHandlers/Lyrics/ILyricsChangeHandler.cs ================================================ // Copyright (c) andy840119 . Licensed under the GPL Licence. // See the LICENCE file in the repository root for full licence text. using System.Collections.Generic; using osu.Game.Rulesets.Karaoke.Objects; namespace osu.Game.Rulesets.Karaoke.Edit.ChangeHandlers.Lyrics; public interface ILyricsChangeHandler { void Split(int index); void Combine(); void CreateAtPosition(); void CreateAtLast(); void AddBelowToSelection(Lyric newLyric); void AddRangeBelowToSelection(IEnumerable newLyrics); void Remove(); void ChangeOrder(int newOrder); } ================================================ FILE: osu.Game.Rulesets.Karaoke/Edit/ChangeHandlers/Lyrics/LyricLanguageChangeHandler.cs ================================================ // Copyright (c) andy840119 . Licensed under the GPL Licence. // See the LICENCE file in the repository root for full licence text. using System.Collections.Generic; using System.Globalization; using osu.Game.Rulesets.Karaoke.Edit.Utils; using osu.Game.Rulesets.Karaoke.Objects; namespace osu.Game.Rulesets.Karaoke.Edit.ChangeHandlers.Lyrics; public partial class LyricLanguageChangeHandler : LyricPropertyChangeHandler, ILyricLanguageChangeHandler { public void SetLanguage(CultureInfo? language) { PerformOnSelection(lyric => { if (EqualityComparer.Default.Equals(language, lyric.Language)) return; lyric.Language = language; }); } protected override bool IsWritePropertyLocked(Lyric lyric) => HitObjectWritableUtils.IsWriteLyricPropertyLocked(lyric, nameof(Lyric.Language)); } ================================================ FILE: osu.Game.Rulesets.Karaoke/Edit/ChangeHandlers/Lyrics/LyricPropertyAutoGenerateChangeHandler.cs ================================================ // Copyright (c) andy840119 . Licensed under the GPL Licence. // See the LICENCE file in the repository root for full licence text. using System; using System.Collections.Generic; using System.Globalization; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Localisation; using osu.Game.Rulesets.Karaoke.Configuration; using osu.Game.Rulesets.Karaoke.Edit.Generator; using osu.Game.Rulesets.Karaoke.Edit.Generator.Lyrics; using osu.Game.Rulesets.Karaoke.Edit.Generator.Lyrics.Language; using osu.Game.Rulesets.Karaoke.Edit.Generator.Lyrics.Notes; using osu.Game.Rulesets.Karaoke.Edit.Generator.Lyrics.ReferenceLyric; using osu.Game.Rulesets.Karaoke.Edit.Generator.Lyrics.Romanisation; using osu.Game.Rulesets.Karaoke.Edit.Generator.Lyrics.RubyTags; using osu.Game.Rulesets.Karaoke.Edit.Generator.Lyrics.TimeTags; using osu.Game.Rulesets.Karaoke.Edit.Utils; using osu.Game.Rulesets.Karaoke.Objects; using osu.Game.Rulesets.Karaoke.Objects.Properties; using osu.Game.Screens.Edit; namespace osu.Game.Rulesets.Karaoke.Edit.ChangeHandlers.Lyrics; public partial class LyricPropertyAutoGenerateChangeHandler : LyricPropertyChangeHandler, ILyricPropertyAutoGenerateChangeHandler { // should change this flag if wants to change property in the lyrics. // Not a good to waite a global property for that but there's no better choice. private AutoGenerateType? currentAutoGenerateType; [Resolved] private EditorBeatmap beatmap { get; set; } = null!; public bool CanGenerate(AutoGenerateType type) { currentAutoGenerateType = type; switch (type) { case AutoGenerateType.DetectReferenceLyric: var referenceLyricDetector = getDetector(HitObjects); return canDetect(referenceLyricDetector); case AutoGenerateType.DetectLanguage: var languageDetector = getDetector(); return canDetect(languageDetector); case AutoGenerateType.AutoGenerateRubyTags: var rubyGenerator = getSelector(); return canGenerate(rubyGenerator); case AutoGenerateType.AutoGenerateTimeTags: var timeTagGenerator = getSelector(); return canGenerate(timeTagGenerator); case AutoGenerateType.AutoGenerateRomanisation: var romanisationGenerator = getSelector, RomanisationGeneratorConfig>(); return canGenerate(romanisationGenerator); case AutoGenerateType.AutoGenerateNotes: var noteGenerator = getGenerator(); return canGenerate(noteGenerator); default: throw new ArgumentOutOfRangeException(nameof(type)); } bool canDetect(PropertyDetector detector) => HitObjects.Where(x => !IsWritePropertyLocked(x)).Any(detector.CanDetect); bool canGenerate(PropertyGenerator generator) => HitObjects.Where(x => !IsWritePropertyLocked(x)).Any(generator.CanGenerate); } public IDictionary GetGeneratorNotSupportedLyrics(AutoGenerateType type) { currentAutoGenerateType = type; switch (type) { case AutoGenerateType.DetectReferenceLyric: var referenceLyricDetector = getDetector(HitObjects); return getInvalidMessageFromDetector(referenceLyricDetector); case AutoGenerateType.DetectLanguage: var languageDetector = getDetector(); return getInvalidMessageFromDetector(languageDetector); case AutoGenerateType.AutoGenerateRubyTags: var rubyGenerator = getSelector(); return getInvalidMessageFromGenerator(rubyGenerator); case AutoGenerateType.AutoGenerateTimeTags: var timeTagGenerator = getSelector(); return getInvalidMessageFromGenerator(timeTagGenerator); case AutoGenerateType.AutoGenerateRomanisation: var romanisationGenerator = getSelector, RomanisationGeneratorConfig>(); return getInvalidMessageFromGenerator(romanisationGenerator); case AutoGenerateType.AutoGenerateNotes: var noteGenerator = getGenerator(); return getInvalidMessageFromGenerator(noteGenerator); default: throw new ArgumentOutOfRangeException(nameof(type)); } IDictionary getInvalidMessageFromDetector(PropertyDetector detector) => HitObjects.Select(x => new KeyValuePair(x, detector.GetInvalidMessage(x) ?? getReferenceLyricInvalidMessage(x))) .Where(x => x.Value != null) .ToDictionary(k => k.Key, v => v.Value!.Value); IDictionary getInvalidMessageFromGenerator(PropertyGenerator generator) => HitObjects.Select(x => new KeyValuePair(x, generator.GetInvalidMessage(x) ?? getReferenceLyricInvalidMessage(x))) .Where(x => x.Value != null) .ToDictionary(k => k.Key, v => v.Value!.Value); LocalisableString? getReferenceLyricInvalidMessage(Lyric lyric) { bool locked = IsWritePropertyLocked(lyric); return locked ? "Cannot modify property because has reference lyric." : default(LocalisableString?); } } public void AutoGenerate(AutoGenerateType type) { currentAutoGenerateType = type; switch (type) { case AutoGenerateType.DetectReferenceLyric: var referenceLyricDetector = getDetector(HitObjects); PerformOnSelection(lyric => { var referencedLyric = referenceLyricDetector.Detect(lyric); lyric.ReferenceLyricId = referencedLyric?.ID; // technically this property should be assigned by beatmap processor, but should be OK to assign here for testing purpose. lyric.ReferenceLyric = referencedLyric; if (lyric.ReferenceLyricId != null && lyric.ReferenceLyricConfig is not SyncLyricConfig) lyric.ReferenceLyricConfig = new SyncLyricConfig(); }); break; case AutoGenerateType.DetectLanguage: var languageDetector = getDetector(); PerformOnSelection(lyric => { var detectedLanguage = languageDetector.Detect(lyric); lyric.Language = detectedLanguage; }); break; case AutoGenerateType.AutoGenerateRubyTags: var rubyGenerator = getSelector(); PerformOnSelection(lyric => { lyric.RubyTags = rubyGenerator.Generate(lyric); }); break; case AutoGenerateType.AutoGenerateTimeTags: var timeTagGenerator = getSelector(); PerformOnSelection(lyric => { lyric.TimeTags = timeTagGenerator.Generate(lyric); }); break; case AutoGenerateType.AutoGenerateRomanisation: var romanisationGenerator = getSelector, RomanisationGeneratorConfig>(); PerformOnSelection(lyric => { var results = romanisationGenerator.Generate(lyric); foreach (var (key, value) in results) { var matchedTimeTag = lyric.TimeTags.Single(x => x == key); matchedTimeTag.FirstSyllable = value.FirstSyllable; matchedTimeTag.RomanisedSyllable = value.RomanisedSyllable; } }); break; case AutoGenerateType.AutoGenerateNotes: var noteGenerator = getGenerator(); PerformOnSelection(lyric => { // clear exist notes if from those var matchedNotes = EditorBeatmapUtils.GetNotesByLyric(beatmap, lyric); RemoveRange(matchedNotes); var notes = noteGenerator.Generate(lyric); AddRange(notes); }); break; default: throw new ArgumentOutOfRangeException(nameof(type)); } } public override bool IsSelectionsLocked() => throw new InvalidOperationException("Auto-generator does not support this check method."); protected override bool IsWritePropertyLocked(Lyric lyric) => currentAutoGenerateType switch { AutoGenerateType.DetectReferenceLyric => HitObjectWritableUtils.IsWriteLyricPropertyLocked(lyric, nameof(Lyric.ReferenceLyric), nameof(Lyric.ReferenceLyricConfig)), AutoGenerateType.DetectLanguage => HitObjectWritableUtils.IsWriteLyricPropertyLocked(lyric, nameof(Lyric.Language)), AutoGenerateType.AutoGenerateRubyTags => HitObjectWritableUtils.IsWriteLyricPropertyLocked(lyric, nameof(Lyric.RubyTags)), AutoGenerateType.AutoGenerateTimeTags => HitObjectWritableUtils.IsWriteLyricPropertyLocked(lyric, nameof(Lyric.TimeTags)), AutoGenerateType.AutoGenerateRomanisation => HitObjectWritableUtils.IsWriteLyricPropertyLocked(lyric, nameof(Lyric.TimeTags)), AutoGenerateType.AutoGenerateNotes => HitObjectWritableUtils.IsCreateOrRemoveNoteLocked(lyric), _ => throw new ArgumentOutOfRangeException(), }; #region Utililty [Resolved] private KaraokeRulesetEditGeneratorConfigManager? generatorConfigManager { get; set; } private LyricPropertyDetector getDetector() where TConfig : GeneratorConfig, new() { var config = getGeneratorConfig(); return createInstance>(config); } private LyricPropertyDetector getDetector(IEnumerable lyrics) where TConfig : GeneratorConfig, new() { var config = getGeneratorConfig(); return createInstance>(lyrics, config); } private LyricPropertyGenerator getGenerator() where TConfig : GeneratorConfig, new() { var config = getGeneratorConfig(); return createInstance>(config); } private LyricGeneratorSelector getSelector() where TBaseConfig : GeneratorConfig { return createInstance>(generatorConfigManager); } private static TType createInstance(params object?[]? args) { var generatedType = getChildType(typeof(TType)); var instance = (TType?)Activator.CreateInstance(generatedType, args); if (instance == null) throw new InvalidOperationException(); return instance; static Type getChildType(Type type) { // should get the assembly that the has the class GeneratorConfig. var assembly = typeof(GeneratorConfig).Assembly; return assembly.GetTypes() .Single(x => type.IsAssignableFrom(x) && !x.IsInterface && !x.IsAbstract); } } private TConfig getGeneratorConfig() where TConfig : GeneratorConfig, new() { if (generatorConfigManager == null) throw new InvalidOperationException(); return generatorConfigManager.Get(); } #endregion } ================================================ FILE: osu.Game.Rulesets.Karaoke/Edit/ChangeHandlers/Lyrics/LyricPropertyChangeHandler.cs ================================================ // Copyright (c) andy840119 . Licensed under the GPL Licence. // See the LICENCE file in the repository root for full licence text. using osu.Game.Rulesets.Karaoke.Objects; namespace osu.Game.Rulesets.Karaoke.Edit.ChangeHandlers.Lyrics; public abstract partial class LyricPropertyChangeHandler : HitObjectPropertyChangeHandler, ILyricPropertyChangeHandler; ================================================ FILE: osu.Game.Rulesets.Karaoke/Edit/ChangeHandlers/Lyrics/LyricReferenceChangeHandler.cs ================================================ // Copyright (c) andy840119 . Licensed under the GPL Licence. // See the LICENCE file in the repository root for full licence text. using System; using System.Linq; using osu.Game.Rulesets.Karaoke.Edit.Utils; using osu.Game.Rulesets.Karaoke.Objects; using osu.Game.Rulesets.Karaoke.Objects.Properties; namespace osu.Game.Rulesets.Karaoke.Edit.ChangeHandlers.Lyrics; public partial class LyricReferenceChangeHandler : LyricPropertyChangeHandler, ILyricReferenceChangeHandler { public void UpdateReferenceLyric(Lyric? referenceLyric) { if (referenceLyric != null && !HitObjects.Contains(referenceLyric)) throw new InvalidOperationException($"{nameof(referenceLyric)} should in the beatmap."); PerformOnSelection(lyric => { if (referenceLyric == lyric) throw new InvalidOperationException($"{nameof(referenceLyric)} should not be the same instance as {nameof(lyric)}"); if (referenceLyric?.ReferenceLyric != null) throw new InvalidOperationException($"{nameof(referenceLyric)} should not contains another reference lyric."); lyric.ReferenceLyricId = referenceLyric?.ID; if (lyric.ReferenceLyricId == null) { lyric.ReferenceLyricConfig = null; } else { // should make sure that config will be created if have reference lyric. // todo: not really sure should use sync config if lyric text are similar. lyric.ReferenceLyricConfig ??= new ReferenceLyricConfig(); } TriggerHitObjectUpdate(lyric); }); } public void SwitchToReferenceLyricConfig() { PerformOnSelection(lyric => { if (lyric.ReferenceLyric == null) throw new InvalidOperationException($"{nameof(lyric)} must have reference lyric."); lyric.ReferenceLyricConfig = new ReferenceLyricConfig(); }); } public void SwitchToSyncLyricConfig() { PerformOnSelection(lyric => { if (lyric.ReferenceLyric == null) throw new InvalidOperationException($"{nameof(lyric)} must have reference lyric."); lyric.ReferenceLyricConfig = new SyncLyricConfig(); }); } public void AdjustLyricConfig(Action action) where TConfig : IReferenceLyricPropertyConfig { PerformOnSelection(lyric => { if (lyric.ReferenceLyricConfig is not TConfig config) throw new InvalidOperationException($"{nameof(config)} must be the type of ${typeof(TConfig)}."); action(config); }); } protected override bool IsWritePropertyLocked(Lyric lyric) => HitObjectWritableUtils.IsWriteLyricPropertyLocked(lyric, nameof(Lyric.ReferenceLyric), nameof(Lyric.ReferenceLyricConfig)); } ================================================ FILE: osu.Game.Rulesets.Karaoke/Edit/ChangeHandlers/Lyrics/LyricRubyTagsChangeHandler.cs ================================================ // Copyright (c) andy840119 . Licensed under the GPL Licence. // See the LICENCE file in the repository root for full licence text. using System; using System.Collections.Generic; using System.Linq; using osu.Game.Rulesets.Karaoke.Edit.Utils; using osu.Game.Rulesets.Karaoke.Objects; using osu.Game.Rulesets.Karaoke.Objects.Utils; namespace osu.Game.Rulesets.Karaoke.Edit.ChangeHandlers.Lyrics; public partial class LyricRubyTagsChangeHandler : LyricPropertyChangeHandler, ILyricRubyTagsChangeHandler { public void Add(RubyTag item) { CheckExactlySelectedOneHitObject(); PerformOnSelection(lyric => { bool containsInLyric = this.containsInLyric(lyric, item); if (containsInLyric) throw new InvalidOperationException($"{nameof(item)} already in the lyric"); addToLyric(lyric, item); }); } public void AddRange(IEnumerable items) { CheckExactlySelectedOneHitObject(); PerformOnSelection(lyric => { // should convert to array because enumerable might change while deleting. foreach (var rubyTag in items.ToArray()) { bool containsInLyric = this.containsInLyric(lyric, rubyTag); if (containsInLyric) throw new InvalidOperationException($"{nameof(rubyTag)} already in the lyric"); addToLyric(lyric, rubyTag); } }); } public void Remove(RubyTag item) { CheckExactlySelectedOneHitObject(); PerformOnSelection(lyric => { bool containsInLyric = this.containsInLyric(lyric, item); if (!containsInLyric) throw new InvalidOperationException($"{nameof(item)} is not in the lyric"); removeFromLyric(lyric, item); }); } public void RemoveRange(IEnumerable items) { CheckExactlySelectedOneHitObject(); PerformOnSelection(lyric => { // should convert to array because enumerable might change while deleting. foreach (var rubyTag in items.ToArray()) { bool containsInLyric = this.containsInLyric(lyric, rubyTag); if (!containsInLyric) throw new InvalidOperationException($"{nameof(rubyTag)} is not in the lyric"); removeFromLyric(lyric, rubyTag); } }); } public void SetIndex(RubyTag rubyTag, int? startIndex, int? endIndex) { CheckExactlySelectedOneHitObject(); // note: it's ok not sort the text tag by index. PerformOnSelection(lyric => { bool containsInLyric = this.containsInLyric(lyric, rubyTag); if (!containsInLyric) throw new InvalidOperationException($"{nameof(rubyTag)} is not in the lyric"); if (startIndex != null) rubyTag.StartIndex = startIndex.Value; if (endIndex != null) rubyTag.EndIndex = endIndex.Value; // after change the index, should check if index is valid. if (RubyTagUtils.OutOfRange(rubyTag, lyric.Text)) throw new InvalidOperationException($"{nameof(startIndex)} or {nameof(endIndex)} is not valid"); }); } public void ShiftingIndex(IEnumerable rubyTags, int offset) { CheckExactlySelectedOneHitObject(); // note: it's ok not sort the text tag by index. PerformOnSelection(lyric => { foreach (var rubyTag in rubyTags) { bool containsInLyric = this.containsInLyric(lyric, rubyTag); if (!containsInLyric) throw new InvalidOperationException($"{nameof(rubyTag)} is not in the lyric"); (int startIndex, int endIndex) = RubyTagUtils.GetShiftingIndex(rubyTag, lyric.Text, offset); rubyTag.StartIndex = startIndex; rubyTag.EndIndex = endIndex; } }); } public void SetText(RubyTag rubyTag, string text) { CheckExactlySelectedOneHitObject(); PerformOnSelection(lyric => { bool containsInLyric = this.containsInLyric(lyric, rubyTag); if (!containsInLyric) throw new InvalidOperationException($"{nameof(rubyTag)} is not in the lyric"); rubyTag.Text = text; }); } private bool containsInLyric(Lyric lyric, RubyTag rubyTag) => lyric.RubyTags.Contains(rubyTag); private void addToLyric(Lyric lyric, RubyTag rubyTag) => lyric.RubyTags.Add(rubyTag); private void removeFromLyric(Lyric lyric, RubyTag rubyTag) => lyric.RubyTags.Remove(rubyTag); protected override bool IsWritePropertyLocked(Lyric lyric) => HitObjectWritableUtils.IsWriteLyricPropertyLocked(lyric, nameof(Lyric.RubyTags)); } ================================================ FILE: osu.Game.Rulesets.Karaoke/Edit/ChangeHandlers/Lyrics/LyricSingerChangeHandler.cs ================================================ // Copyright (c) andy840119 . Licensed under the GPL Licence. // See the LICENCE file in the repository root for full licence text. using System.Collections.Generic; using System.Linq; using osu.Game.Rulesets.Karaoke.Beatmaps.Metadatas.Types; using osu.Game.Rulesets.Karaoke.Edit.Utils; using osu.Game.Rulesets.Karaoke.Objects; namespace osu.Game.Rulesets.Karaoke.Edit.ChangeHandlers.Lyrics; public partial class LyricSingerChangeHandler : LyricPropertyChangeHandler, ILyricSingerChangeHandler { public void Add(ISinger singer) { PerformOnSelection(lyric => { lyric.SingerIds.Add(singer.ID); TriggerHitObjectUpdate(lyric); }); } public void AddRange(IEnumerable singers) { PerformOnSelection(lyric => { // should convert to array because enumerable might change while deleting. foreach (var singer in singers.ToArray()) { lyric.SingerIds.Add(singer.ID); } TriggerHitObjectUpdate(lyric); }); } public void Remove(ISinger singer) { PerformOnSelection(lyric => { lyric.SingerIds.Remove(singer.ID); TriggerHitObjectUpdate(lyric); }); } public void RemoveRange(IEnumerable singers) { PerformOnSelection(lyric => { // should convert to array because enumerable might change while deleting. foreach (var singer in singers.ToArray()) { lyric.SingerIds.Remove(singer.ID); } TriggerHitObjectUpdate(lyric); }); } public void Clear() { PerformOnSelection(lyric => { lyric.SingerIds.Clear(); TriggerHitObjectUpdate(lyric); }); } protected override bool IsWritePropertyLocked(Lyric lyric) => HitObjectWritableUtils.IsWriteLyricPropertyLocked(lyric, nameof(Lyric.SingerIds)); } ================================================ FILE: osu.Game.Rulesets.Karaoke/Edit/ChangeHandlers/Lyrics/LyricTextChangeHandler.cs ================================================ // Copyright (c) andy840119 . Licensed under the GPL Licence. // See the LICENCE file in the repository root for full licence text. using System.Linq; using osu.Game.Rulesets.Karaoke.Edit.Utils; using osu.Game.Rulesets.Karaoke.Objects; using osu.Game.Rulesets.Karaoke.Objects.Utils; namespace osu.Game.Rulesets.Karaoke.Edit.ChangeHandlers.Lyrics; public partial class LyricTextChangeHandler : LyricPropertyChangeHandler, ILyricTextChangeHandler { public void InsertText(int charGap, string text) { CheckExactlySelectedOneHitObject(); PerformOnSelection(lyric => { LyricUtils.AddText(lyric, charGap, text); }); } public void DeleteLyricText(int charGap, int count = 1) { CheckExactlySelectedOneHitObject(); PerformOnSelection(lyric => { LyricUtils.RemoveText(lyric, charGap - count, count); if (!string.IsNullOrEmpty(lyric.Text)) return; if (HitObjectWritableUtils.IsRemoveLyricLocked(lyric)) return; OrderUtils.ShiftingOrder(HitObjects.Where(x => x.Order > lyric.Order), -1); Remove(lyric); }); } protected override bool IsWritePropertyLocked(Lyric lyric) => HitObjectWritableUtils.IsWriteLyricPropertyLocked(lyric, nameof(Lyric.Text), nameof(Lyric.RubyTags), nameof(Lyric.TimeTags)); } ================================================ FILE: osu.Game.Rulesets.Karaoke/Edit/ChangeHandlers/Lyrics/LyricTimeTagsChangeHandler.cs ================================================ // Copyright (c) andy840119 . Licensed under the GPL Licence. // See the LICENCE file in the repository root for full licence text. using System; using System.Collections.Generic; using System.Linq; using osu.Framework.Graphics.Sprites; using osu.Game.Rulesets.Karaoke.Edit.Utils; using osu.Game.Rulesets.Karaoke.Objects; using osu.Game.Rulesets.Karaoke.Utils; namespace osu.Game.Rulesets.Karaoke.Edit.ChangeHandlers.Lyrics; public partial class LyricTimeTagsChangeHandler : LyricPropertyChangeHandler, ILyricTimeTagsChangeHandler { public void SetTimeTagTime(TimeTag timeTag, double time) { CheckExactlySelectedOneHitObject(); PerformOnSelection(lyric => { bool containsInLyric = lyric.TimeTags.Contains(timeTag); if (!containsInLyric) throw new InvalidOperationException($"{nameof(timeTag)} is not in the lyric"); timeTag.Time = time; }); } public void SetTimeTagFirstSyllable(TimeTag timeTag, bool firstSyllable) { CheckExactlySelectedOneHitObject(); PerformOnSelection(lyric => { bool containsInLyric = lyric.TimeTags.Contains(timeTag); if (!containsInLyric) throw new InvalidOperationException($"{nameof(timeTag)} is not in the lyric"); timeTag.FirstSyllable = firstSyllable; }); } public void SetTimeTagRomanisedSyllable(TimeTag timeTag, string? romanisedSyllable) { CheckExactlySelectedOneHitObject(); PerformOnSelection(lyric => { bool containsInLyric = lyric.TimeTags.Contains(timeTag); if (!containsInLyric) throw new InvalidOperationException($"{nameof(timeTag)} is not in the lyric"); timeTag.RomanisedSyllable = romanisedSyllable; if (!string.IsNullOrWhiteSpace(romanisedSyllable)) return; timeTag.RomanisedSyllable = string.Empty; timeTag.FirstSyllable = false; }); } public void ShiftingTimeTagTime(IEnumerable timeTags, double offset) { CheckExactlySelectedOneHitObject(); NotTriggerSaveStateOnThisChange(); PerformOnSelection(lyric => { foreach (var timeTag in timeTags) { bool containsInLyric = lyric.TimeTags.Contains(timeTag); if (!containsInLyric) throw new InvalidOperationException($"{nameof(timeTag)} is not in the lyric"); timeTag.Time += offset; } }); } public void ClearTimeTagTime(TimeTag timeTag) { CheckExactlySelectedOneHitObject(); PerformOnSelection(lyric => { bool containsInLyric = lyric.TimeTags.Contains(timeTag); if (!containsInLyric) throw new InvalidOperationException($"{nameof(timeTag)} is not in the lyric"); timeTag.Time = null; }); } public void ClearAllTimeTagTime() { PerformOnSelection(lyric => { foreach (var timeTag in lyric.TimeTags) { timeTag.Time = null; } }); } public void Add(TimeTag timeTag) { CheckExactlySelectedOneHitObject(); PerformOnSelection(lyric => { bool containsInLyric = lyric.TimeTags.Contains(timeTag); if (containsInLyric) throw new InvalidOperationException($"{nameof(timeTag)} already in the lyric"); insertTimeTag(lyric, timeTag, InsertDirection.End); }); } public void AddRange(IEnumerable timeTags) { CheckExactlySelectedOneHitObject(); PerformOnSelection(lyric => { // should convert to array because enumerable might change while deleting. foreach (var timeTag in timeTags.ToArray()) { bool containsInLyric = lyric.TimeTags.Contains(timeTag); if (containsInLyric) throw new InvalidOperationException($"{nameof(timeTag)} already in the lyric"); insertTimeTag(lyric, timeTag, InsertDirection.End); } }); } public void Remove(TimeTag timeTag) { CheckExactlySelectedOneHitObject(); PerformOnSelection(lyric => { // delete time tag from list lyric.TimeTags.Remove(timeTag); }); } public void RemoveRange(IEnumerable timeTags) { CheckExactlySelectedOneHitObject(); PerformOnSelection(lyric => { // should convert to array because enumerable might change while deleting. foreach (var timeTag in timeTags.ToArray()) { bool containsInLyric = lyric.TimeTags.Remove(timeTag); if (!containsInLyric) throw new InvalidOperationException($"{nameof(timeTag)} is not in the lyric"); } }); } public void AddByPosition(TextIndex index) { CheckExactlySelectedOneHitObject(); PerformOnSelection(lyric => { insertTimeTag(lyric, new TimeTag(index), InsertDirection.End); }); } public void RemoveByPosition(TextIndex index) { CheckExactlySelectedOneHitObject(); PerformOnSelection(lyric => { var matchedTimeTags = lyric.TimeTags.Where(x => x.Index == index).ToList(); if (!matchedTimeTags.Any()) return; var removedTimeTag = matchedTimeTags.MinBy(x => x.Time ?? double.MinValue); if (removedTimeTag != null) lyric.TimeTags.Remove(removedTimeTag); }); } public TimeTag Shifting(TimeTag timeTag, ShiftingDirection direction, ShiftingType type) { CheckExactlySelectedOneHitObject(); TimeTag newTimeTag = null!; PerformOnSelection(lyric => { bool containsInLyric = lyric.TimeTags.Contains(timeTag); if (!containsInLyric) throw new InvalidOperationException($"{nameof(timeTag)} is not in the lyric"); // remove the time-tag first. lyric.TimeTags.Remove(timeTag); // then, create a new one and insert into the list. var newIndex = calculateNewIndex(lyric, timeTag.Index, direction, type); double? newTime = timeTag.Time; newTimeTag = new TimeTag(newIndex, newTime); switch (direction) { case ShiftingDirection.Left: insertTimeTag(lyric, newTimeTag, InsertDirection.End); break; case ShiftingDirection.Right: insertTimeTag(lyric, newTimeTag, InsertDirection.Start); break; default: throw new InvalidOperationException(); } }); return newTimeTag; static TextIndex calculateNewIndex(Lyric lyric, TextIndex originIndex, ShiftingDirection direction, ShiftingType type) { var newIndex = getNewIndex(originIndex, direction, type); if (TextIndexUtils.OutOfRange(newIndex, lyric.Text)) throw new ArgumentOutOfRangeException(); return newIndex; static TextIndex getNewIndex(TextIndex originIndex, ShiftingDirection direction, ShiftingType type) => type switch { ShiftingType.Index => TextIndexUtils.ShiftingIndex(originIndex, direction == ShiftingDirection.Left ? -1 : 1), ShiftingType.State => direction == ShiftingDirection.Left ? TextIndexUtils.GetPreviousIndex(originIndex) : TextIndexUtils.GetNextIndex(originIndex), _ => throw new InvalidOperationException(), }; } } private void insertTimeTag(Lyric lyric, TimeTag timeTag, InsertDirection direction) { var timeTags = lyric.TimeTags; // just add if there's no time-tag if (lyric.TimeTags.Count == 0) { timeTags.Add(timeTag); return; } if (timeTags.All(x => x.Index < timeTag.Index)) { timeTags.Add(timeTag); } else if (timeTags.All(x => x.Index > timeTag.Index)) { timeTags.Insert(0, timeTag); } else { switch (direction) { case InsertDirection.Start: { var nextTimeTag = timeTags.FirstOrDefault(x => x.Index >= timeTag.Index) ?? timeTags.Last(); int index = timeTags.IndexOf(nextTimeTag); timeTags.Insert(index, timeTag); break; } case InsertDirection.End: { var previousTimeTag = timeTags.Reverse().FirstOrDefault(x => x.Index <= timeTag.Index) ?? timeTags.First(); int index = timeTags.IndexOf(previousTimeTag) + 1; timeTags.Insert(index, timeTag); break; } default: throw new InvalidOperationException(); } } } protected override bool IsWritePropertyLocked(Lyric lyric) => HitObjectWritableUtils.IsWriteLyricPropertyLocked(lyric, nameof(Lyric.TimeTags)); /// /// Insert direction if contains the time-tag with the same index. /// private enum InsertDirection { Start, End, } } ================================================ FILE: osu.Game.Rulesets.Karaoke/Edit/ChangeHandlers/Lyrics/LyricTranslationChangeHandler.cs ================================================ // Copyright (c) andy840119 . Licensed under the GPL Licence. // See the LICENCE file in the repository root for full licence text. using System.Collections.Generic; using System.Globalization; using osu.Game.Rulesets.Karaoke.Edit.Utils; using osu.Game.Rulesets.Karaoke.Objects; namespace osu.Game.Rulesets.Karaoke.Edit.ChangeHandlers.Lyrics; public partial class LyricTranslationChangeHandler : LyricPropertyChangeHandler, ILyricTranslationChangeHandler { public void UpdateTranslation(CultureInfo cultureInfo, string translation) { CheckExactlySelectedOneHitObject(); PerformOnSelection(lyric => { // should not save translation if is null or empty or whitespace if (string.IsNullOrWhiteSpace(translation)) { if (lyric.Translations.ContainsKey(cultureInfo)) lyric.Translations.Remove(cultureInfo); } else { if (!lyric.Translations.TryAdd(cultureInfo, translation)) lyric.Translations[cultureInfo] = translation; } }); } protected override bool IsWritePropertyLocked(Lyric lyric) => HitObjectWritableUtils.IsWriteLyricPropertyLocked(lyric, nameof(Lyric.Translations)); } ================================================ FILE: osu.Game.Rulesets.Karaoke/Edit/ChangeHandlers/Lyrics/LyricsChangeHandler.cs ================================================ // Copyright (c) andy840119 . Licensed under the GPL Licence. // See the LICENCE file in the repository root for full licence text. using System; using System.Collections.Generic; using System.Linq; using osu.Framework.Extensions.IEnumerableExtensions; using osu.Game.Rulesets.Karaoke.Objects; using osu.Game.Rulesets.Karaoke.Objects.Utils; namespace osu.Game.Rulesets.Karaoke.Edit.ChangeHandlers.Lyrics; public partial class LyricsChangeHandler : HitObjectsChangeHandler, ILyricsChangeHandler { public void Split(int index) { CheckExactlySelectedOneHitObject(); PerformOnSelection(lyric => { // Shifting order that order is larger than current lyric. int lyricOrder = lyric.Order; OrderUtils.ShiftingOrder(HitObjects.Where(x => x.Order > lyricOrder), 1); // Split lyric var (firstLyric, secondLyric) = LyricsUtils.SplitLyric(lyric, index); firstLyric.Order = lyric.Order; secondLyric.Order = lyric.Order + 1; // Add those tho lyric and remove old one. Add(secondLyric); Add(firstLyric); Remove(lyric); }); } public void Combine() { CheckExactlySelectedOneHitObject(); PerformOnSelection(lyric => { var previousLyric = HitObjects.GetPrevious(lyric); if (previousLyric == null) throw new ArgumentNullException(nameof(previousLyric)); // Shifting order that order is larger than current lyric. int lyricOrder = previousLyric.Order; OrderUtils.ShiftingOrder(HitObjects.Where(x => x.Order > lyricOrder), -1); var newLyric = LyricsUtils.CombineLyric(previousLyric, lyric); newLyric.Order = lyricOrder; // Add created lyric and remove old two. Add(newLyric); Remove(previousLyric); Remove(lyric); }); } public void CreateAtPosition() { AddBelowToSelection(new Lyric { Text = "New lyric", }); } public void CreateAtLast() { int order = OrderUtils.GetMaxOrderNumber(HitObjects.ToArray()); // Add new lyric to target order. Add(new Lyric { Text = "New lyric", Order = order + 1, }); } public void AddBelowToSelection(Lyric newLyric) { AddRangeBelowToSelection(new[] { newLyric }); } public void AddRangeBelowToSelection(IEnumerable newLyrics) { CheckExactlySelectedOneHitObject(); PerformOnSelection(lyric => { int order = lyric.Order; // Shifting order that order is larger than current lyric. OrderUtils.ShiftingOrder(HitObjects.Where(x => x.Order > order), newLyrics.Count()); foreach (var newLyric in newLyrics) { newLyric.Order = ++order; Add(newLyric); } }); } public void Remove() { PerformOnSelection(lyric => { // Shifting order that order is larger than current lyric. OrderUtils.ShiftingOrder(HitObjects.Where(x => x.Order > lyric.Order), -1); Remove(lyric); }); } public void ChangeOrder(int newOrder) { PerformOnSelection(lyric => { int oldOrder = lyric.Order; OrderUtils.ChangeOrder(HitObjects.ToArray(), oldOrder, newOrder + 1, (switchSinger, oldOrder, newOrder) => { // todo : not really sure should call update? }); }); } protected override void Add(T hitObject) { if (hitObject is Lyric lyric) { int index = getInsertIndex(lyric.Order); Insert(index, lyric); } else { base.Add(hitObject); } } protected override void Insert(int index, T hitObject) { if (hitObject is Lyric lyric) { base.Insert(index, lyric); } else { base.Add(hitObject); } } private int getInsertIndex(int order) => HitObjects.ToList().FindIndex(x => x.Order == order - 1) + 1; } ================================================ FILE: osu.Game.Rulesets.Karaoke/Edit/ChangeHandlers/Notes/INotePropertyChangeHandler.cs ================================================ // Copyright (c) andy840119 . Licensed under the GPL Licence. // See the LICENCE file in the repository root for full licence text. using osu.Game.Rulesets.Karaoke.Objects; namespace osu.Game.Rulesets.Karaoke.Edit.ChangeHandlers.Notes; public interface INotePropertyChangeHandler : IHitObjectPropertyChangeHandler { void ChangeText(string text); void ChangeRubyText(string? ruby); void ChangeDisplayState(bool display); void OffsetTone(Tone offset); } ================================================ FILE: osu.Game.Rulesets.Karaoke/Edit/ChangeHandlers/Notes/INotesChangeHandler.cs ================================================ // Copyright (c) andy840119 . Licensed under the GPL Licence. // See the LICENCE file in the repository root for full licence text. namespace osu.Game.Rulesets.Karaoke.Edit.ChangeHandlers.Notes; public interface INotesChangeHandler { void Split(float percentage = 0.5f); void Combine(); void Clear(); } ================================================ FILE: osu.Game.Rulesets.Karaoke/Edit/ChangeHandlers/Notes/NotePropertyChangeHandler.cs ================================================ // Copyright (c) andy840119 . Licensed under the GPL Licence. // See the LICENCE file in the repository root for full licence text. using System; using osu.Framework.Allocation; using osu.Game.Rulesets.Karaoke.Edit.Utils; using osu.Game.Rulesets.Karaoke.Objects; using osu.Game.Screens.Edit; namespace osu.Game.Rulesets.Karaoke.Edit.ChangeHandlers.Notes; public partial class NotePropertyChangeHandler : HitObjectPropertyChangeHandler, INotePropertyChangeHandler { [Resolved] private EditorBeatmap beatmap { get; set; } = null!; public void ChangeText(string text) { CheckExactlySelectedOneHitObject(); PerformOnSelection(note => { note.Text = text; }); } public void ChangeRubyText(string? ruby) { CheckExactlySelectedOneHitObject(); PerformOnSelection(note => { // Should change ruby text as null if remove all words. note.RubyText = string.IsNullOrEmpty(ruby) ? null : ruby; }); } public void ChangeDisplayState(bool display) { PerformOnSelection(note => { note.Display = display; // Move to center if note is not display if (!note.Display) note.Tone = new Tone(); }); } public void OffsetTone(Tone offset) { if (offset == default(Tone)) throw new InvalidOperationException("Offset number should not be zero."); var noteInfo = EditorBeatmapUtils.GetPlayableBeatmap(beatmap).NoteInfo; PerformOnSelection(note => { if (note.Tone >= noteInfo.MaxTone && offset > 0) return; if (note.Tone <= noteInfo.MinTone && offset < 0) return; note.Tone += offset; //Change all note to visible note.Display = true; }); } protected sealed override bool IsWritePropertyLocked(Note note) => HitObjectWritableUtils.IsWriteNotePropertyLocked(note, nameof(Note.Text), nameof(Note.RubyText), nameof(Note.Display)); } ================================================ FILE: osu.Game.Rulesets.Karaoke/Edit/ChangeHandlers/Notes/NotesChangeHandler.cs ================================================ // Copyright (c) andy840119 . Licensed under the GPL Licence. // See the LICENCE file in the repository root for full licence text. using System; using System.Linq; using osu.Framework.Allocation; using osu.Game.Rulesets.Karaoke.Objects; using osu.Game.Rulesets.Karaoke.Objects.Utils; using osu.Game.Screens.Edit; namespace osu.Game.Rulesets.Karaoke.Edit.ChangeHandlers.Notes; public partial class NotesChangeHandler : HitObjectsChangeHandler, INotesChangeHandler { [Resolved] private EditorBeatmap beatmap { get; set; } = null!; public void Split(float percentage = 0.5f) { CheckExactlySelectedOneHitObject(); PerformOnSelection(note => { var (firstNote, secondNote) = NotesUtils.SplitNote(note); Add(firstNote); Add(secondNote); Remove(note); }); } public void Combine() { PerformOnSelection(lyric => { var notes = beatmap.SelectedHitObjects.OfType().Where(n => n.ReferenceLyric == lyric).ToList(); if (notes.Count < 2) throw new InvalidOperationException($"Should have select at lest two {nameof(notes)}."); var combinedNote = NotesUtils.CombineNote(notes[0], notes[1]); for (int i = 2; i < notes.Count; i++) { combinedNote = NotesUtils.CombineNote(notes[i - 1], notes[i]); } RemoveRange(notes); Add(combinedNote); }); } public void Clear() { PerformOnSelection(lyric => { var notes = beatmap.HitObjects.OfType().Where(n => n.ReferenceLyric == lyric).ToList(); RemoveRange(notes); }); } } ================================================ FILE: osu.Game.Rulesets.Karaoke/Edit/ChangeHandlers/Stages/ClassicStageChangeHandler.cs ================================================ // Copyright (c) andy840119 . Licensed under the GPL Licence. // See the LICENCE file in the repository root for full licence text. using System; using System.Collections.Generic; using System.Linq; using osu.Framework.Allocation; using osu.Game.Rulesets.Karaoke.Objects; using osu.Game.Rulesets.Karaoke.Stages.Infos.Classic; using osu.Game.Screens.Edit; namespace osu.Game.Rulesets.Karaoke.Edit.ChangeHandlers.Stages; public partial class ClassicStageChangeHandler : StagePropertyChangeHandler, IClassicStageChangeHandler { [Resolved] private EditorBeatmap beatmap { get; set; } = null!; #region Layout definition public void EditLayoutDefinition(Action action) { performStageInfoChanged(x => { action(x.StageDefinition); }); } #endregion #region Timing info public void AddTimingPoint(Action? action = null) { performTimingInfoChanged(timingInfo => { timingInfo.AddTimingPoint(action); }); } public void RemoveTimingPoint(ClassicLyricTimingPoint timePoint) { performTimingInfoChanged(timingInfo => { timingInfo.RemoveTimingPoint(timePoint); }); } public void RemoveRangeOfTimingPoints(IEnumerable timePoints) { performTimingInfoChanged(timingInfo => { foreach (var timePoint in timePoints) { timingInfo.RemoveTimingPoint(timePoint); } }); } public void ShiftingTimingPoints(IEnumerable timePoints, double offset) { performTimingInfoChanged(timingInfo => { foreach (var timePoint in timePoints) { timePoint.Time += offset; } }); } public void AddLyricIntoTimingPoint(ClassicLyricTimingPoint timePoint) { performTimingInfoChanged(timingInfo => { var selectedLyric = beatmap.SelectedHitObjects.OfType(); foreach (var lyric in selectedLyric) { timingInfo.AddToMapping(timePoint, lyric); } }); } public void RemoveLyricFromTimingPoint(ClassicLyricTimingPoint timePoint) { performTimingInfoChanged(timingInfo => { var selectedLyric = beatmap.SelectedHitObjects.OfType(); foreach (var lyric in selectedLyric) { timingInfo.RemoveFromMapping(timePoint, lyric); } }); } private static bool checkTimingPointExist(ClassicLyricTimingInfo timingInfo, ClassicLyricTimingPoint timingPoint) { return timingInfo.Timings.Contains(timingPoint); } #endregion private void performStageInfoChanged(Action action) { throw new NotImplementedException(); } private void performTimingInfoChanged(Action action) { throw new NotImplementedException(); } } ================================================ FILE: osu.Game.Rulesets.Karaoke/Edit/ChangeHandlers/Stages/IClassicStageChangeHandler.cs ================================================ // Copyright (c) andy840119 . Licensed under the GPL Licence. // See the LICENCE file in the repository root for full licence text. using System; using System.Collections.Generic; using osu.Game.Rulesets.Karaoke.Stages.Infos.Classic; namespace osu.Game.Rulesets.Karaoke.Edit.ChangeHandlers.Stages; public interface IClassicStageChangeHandler { #region Layout definition void EditLayoutDefinition(Action action); #endregion #region Timing info void AddTimingPoint(Action? action = null); void RemoveTimingPoint(ClassicLyricTimingPoint timePoint); void RemoveRangeOfTimingPoints(IEnumerable timePoints); void ShiftingTimingPoints(IEnumerable timePoints, double offset); void AddLyricIntoTimingPoint(ClassicLyricTimingPoint timePoint); void RemoveLyricFromTimingPoint(ClassicLyricTimingPoint timePoint); #endregion } ================================================ FILE: osu.Game.Rulesets.Karaoke/Edit/ChangeHandlers/Stages/IStageElementCategoryChangeHandler.cs ================================================ // Copyright (c) andy840119 . Licensed under the GPL Licence. // See the LICENCE file in the repository root for full licence text. using System; using osu.Game.Rulesets.Karaoke.Beatmaps; using osu.Game.Rulesets.Karaoke.Stages.Infos; namespace osu.Game.Rulesets.Karaoke.Edit.ChangeHandlers.Stages; public interface IStageElementCategoryChangeHandler where TStageElement : StageElement { void AddElement(Action? action = null); void EditElement(ElementId? id, Action action); void RemoveElement(TStageElement element); void AddToMapping(TStageElement element); void RemoveFromMapping(); void ClearUnusedMapping(); } ================================================ FILE: osu.Game.Rulesets.Karaoke/Edit/ChangeHandlers/Stages/IStagesChangeHandler.cs ================================================ // Copyright (c) andy840119 . Licensed under the GPL Licence. // See the LICENCE file in the repository root for full licence text. using osu.Framework.Localisation; using osu.Game.Rulesets.Karaoke.Stages.Infos; namespace osu.Game.Rulesets.Karaoke.Edit.ChangeHandlers.Stages; public interface IStagesChangeHandler : IAutoGenerateChangeHandler { LocalisableString? GetGeneratorNotSupportedMessage() where TStageInfo : StageInfo; void Remove() where TStageInfo : StageInfo; } ================================================ FILE: osu.Game.Rulesets.Karaoke/Edit/ChangeHandlers/Stages/StageElementCategoryChangeHandler.cs ================================================ // Copyright (c) andy840119 . Licensed under the GPL Licence. // See the LICENCE file in the repository root for full licence text. using System; using System.Collections.Generic; using System.Linq; using osu.Game.Rulesets.Karaoke.Beatmaps; using osu.Game.Rulesets.Karaoke.Objects; using osu.Game.Rulesets.Karaoke.Stages.Infos; namespace osu.Game.Rulesets.Karaoke.Edit.ChangeHandlers.Stages; public partial class StageElementCategoryChangeHandler : StagePropertyChangeHandler, IStageElementCategoryChangeHandler where TStageElement : StageElement, IComparable, new() where THitObject : KaraokeHitObject, IHasPrimaryKey { private readonly Func, StageElementCategory> stageCategoryAction; public StageElementCategoryChangeHandler(Func, StageElementCategory> stageCategoryAction) { this.stageCategoryAction = stageCategoryAction; } public void AddElement(Action? action = null) { performStageInfoChanged(s => { s.AddElement(action); }); } public void EditElement(ElementId? id, Action action) { performStageInfoChanged(s => { s.EditElement(id, action); }); } public void RemoveElement(TStageElement element) { performStageInfoChanged(s => { s.RemoveElement(element); }); } public void AddToMapping(TStageElement element) { PerformOnSelection(hitObject => { performStageInfoChanged(s => { s.AddToMapping(element, hitObject); }); }); } public void RemoveFromMapping() { PerformOnSelection(hitObject => { performStageInfoChanged(s => { s.RemoveHitObjectFromMapping(hitObject); }); }); } public void ClearUnusedMapping() { performStageInfoChanged(s => { s.ClearUnusedMapping(id => Lyrics.Any(x => x.ID == id)); }); } private void performStageInfoChanged(Action> stageAction) { throw new NotImplementedException(); } } ================================================ FILE: osu.Game.Rulesets.Karaoke/Edit/ChangeHandlers/Stages/StagePropertyChangeHandler.cs ================================================ // Copyright (c) andy840119 . Licensed under the GPL Licence. // See the LICENCE file in the repository root for full licence text. using System; using System.Collections.Generic; using osu.Framework.Graphics; using osu.Game.Rulesets.Karaoke.Objects; using osu.Game.Rulesets.Objects; namespace osu.Game.Rulesets.Karaoke.Edit.ChangeHandlers.Stages; /// /// Note: will start to implement this class after the stage info is able to edit. /// public partial class StagePropertyChangeHandler : Component { protected IEnumerable Lyrics => throw new NotImplementedException(); protected void PerformOnSelection(Action action) where T : HitObject { throw new NotImplementedException(); } } ================================================ FILE: osu.Game.Rulesets.Karaoke/Edit/ChangeHandlers/Stages/StagesChangeHandler.cs ================================================ // Copyright (c) andy840119 . Licensed under the GPL Licence. // See the LICENCE file in the repository root for full licence text. using System; using System.Collections.Generic; using osu.Framework.Allocation; using osu.Framework.Localisation; using osu.Game.Rulesets.Karaoke.Beatmaps; using osu.Game.Rulesets.Karaoke.Configuration; using osu.Game.Rulesets.Karaoke.Edit.Generator.Stages; using osu.Game.Rulesets.Karaoke.Edit.Utils; using osu.Game.Rulesets.Karaoke.Stages.Infos; using osu.Game.Screens.Edit; namespace osu.Game.Rulesets.Karaoke.Edit.ChangeHandlers.Stages; public partial class StagesChangeHandler : StagePropertyChangeHandler, IStagesChangeHandler { [Resolved] private EditorBeatmap beatmap { get; set; } = null!; private KaraokeBeatmap karaokeBeatmap => EditorBeatmapUtils.GetPlayableBeatmap(beatmap); [Resolved] private KaraokeRulesetEditGeneratorConfigManager generatorConfigManager { get; set; } = null!; bool IAutoGenerateChangeHandler.CanGenerate() => CanGenerate(); public bool CanGenerate() where TStageInfo : StageInfo { return GetGeneratorNotSupportedMessage() == null; } public LocalisableString? GetGeneratorNotSupportedMessage() where TStageInfo : StageInfo { var stage = getStageInfo(); if (stage != null) return $"{nameof(TStageInfo)} already exist in the beatmap."; var generator = new StageInfoGeneratorSelector(generatorConfigManager); return generator.GetInvalidMessage(karaokeBeatmap); } void IAutoGenerateChangeHandler.AutoGenerate() => AutoGenerate(); public void AutoGenerate() where TStageInfo : StageInfo { var stage = getStageInfo(); if (stage != null) throw new InvalidOperationException($"{nameof(TStageInfo)} already exist in the beatmap."); var generator = new StageInfoGeneratorSelector(generatorConfigManager); var stageInfo = generator.Generate(karaokeBeatmap); getStageInfos().Add(stageInfo); } public void Remove() where TStageInfo : StageInfo { var stage = getStageInfo(); if (stage == null) throw new InvalidOperationException($"There's no {nameof(TStageInfo)} in the beatmap."); getStageInfos().Remove(stage); // todo: maybe should update the current stage. } private IList getStageInfos() => throw new NotImplementedException(); private TStageInfo? getStageInfo() where TStageInfo : StageInfo => throw new NotImplementedException(); } ================================================ FILE: osu.Game.Rulesets.Karaoke/Edit/Checks/CheckBeatmapAvailableTranslations.cs ================================================ // Copyright (c) andy840119 . Licensed under the GPL Licence. // See the LICENCE file in the repository root for full licence text. using System.Collections.Generic; using System.Globalization; using System.Linq; using osu.Game.Rulesets.Edit.Checks.Components; using osu.Game.Rulesets.Karaoke.Beatmaps; using osu.Game.Rulesets.Karaoke.Objects; using osu.Game.Rulesets.Objects; namespace osu.Game.Rulesets.Karaoke.Edit.Checks; public class CheckBeatmapAvailableTranslations : CheckBeatmapProperty, Lyric> { protected override string Description => "Beatmap with invalid localization info."; public override IEnumerable PossibleTemplates => new IssueTemplate[] { new IssueTemplateMissingTranslation(this), new IssueTemplateMissingPartialTranslation(this), new IssueTemplateTranslationNotInListedLanguage(this), }; protected override IList GetPropertyFromBeatmap(KaraokeBeatmap karaokeBeatmap) => karaokeBeatmap.AvailableTranslationLanguages; protected override IEnumerable CheckProperty(IList property) { // todo: maybe check duplicated languages? yield break; } protected override IEnumerable CheckHitObjects(IList property, IReadOnlyList hitObject) { if (hitObject.Count == 0) yield break; // check if some translations is missing or empty. foreach (var language in property) { var missingTranslationLyrics = hitObject.Where(x => !x.Translations.ContainsKey(language) || string.IsNullOrWhiteSpace(x.Translations[language])).ToArray(); if (missingTranslationLyrics.Length == hitObject.Count) { yield return new IssueTemplateMissingTranslation(this).Create(missingTranslationLyrics, language); } else if (missingTranslationLyrics.Any()) { yield return new IssueTemplateMissingPartialTranslation(this).Create(missingTranslationLyrics, language); } } // should check is lyric contains translation that is not listed in beatmap. // if got this issue, then it's a bug. var allTranslationLanguageInLyric = hitObject.SelectMany(x => x.Translations.Keys).Distinct(); var languageNotListInBeatmap = allTranslationLanguageInLyric.Except(property); foreach (var language in languageNotListInBeatmap) { var notContainsTranslationLyrics = hitObject.Where(x => !x.Translations.ContainsKey(language)); yield return new IssueTemplateTranslationNotInListedLanguage(this).Create(notContainsTranslationLyrics, language); } } public class IssueTemplateMissingTranslation : IssueTemplate { public IssueTemplateMissingTranslation(ICheck check) : base(check, IssueType.Problem, "There are no lyric translations for this language.") { } public Issue Create(IEnumerable hitObjects, CultureInfo cultureInfo) => new(hitObjects, this, cultureInfo); } public class IssueTemplateMissingPartialTranslation : IssueTemplate { public IssueTemplateMissingPartialTranslation(ICheck check) : base(check, IssueType.Problem, "Some lyrics in this language are missing translations.") { } public Issue Create(IEnumerable hitObjects, CultureInfo cultureInfo) => new(hitObjects, this, cultureInfo); } public class IssueTemplateTranslationNotInListedLanguage : IssueTemplate { public IssueTemplateTranslationNotInListedLanguage(ICheck check) : base(check, IssueType.Problem, "Seems some translation language is not listed in the beatmap, please contact developer to fix that bug.") { } public Issue Create(IEnumerable hitObjects, CultureInfo cultureInfo) => new(hitObjects, this, cultureInfo); } } ================================================ FILE: osu.Game.Rulesets.Karaoke/Edit/Checks/CheckBeatmapNoteInfo.cs ================================================ // Copyright (c) andy840119 . Licensed under the GPL Licence. // See the LICENCE file in the repository root for full licence text. using System.Collections.Generic; using osu.Game.Rulesets.Edit.Checks.Components; using osu.Game.Rulesets.Karaoke.Beatmaps; using osu.Game.Rulesets.Karaoke.Beatmaps.Metadatas; using osu.Game.Rulesets.Karaoke.Edit.Checks.Issues; using osu.Game.Rulesets.Karaoke.Objects; namespace osu.Game.Rulesets.Karaoke.Edit.Checks; public class CheckBeatmapNoteInfo : CheckBeatmapProperty { public const int MIN_COLUMNS = 9; public const int MAX_COLUMNS = 12; protected override string Description => "Check invalid note info and the note that out of range."; public override IEnumerable PossibleTemplates => new IssueTemplate[] { new IssueTemplateColumnNotEnough(this), new IssueTemplateColumnExceed(this), new IssueTemplateNoteToneTooLow(this), new IssueTemplateNoteToneTooHigh(this), }; protected override NoteInfo GetPropertyFromBeatmap(KaraokeBeatmap karaokeBeatmap) => karaokeBeatmap.NoteInfo; protected override IEnumerable CheckProperty(NoteInfo property) { switch (property.Columns) { case < MIN_COLUMNS: yield return new IssueTemplateColumnNotEnough(this).Create(); break; case > MAX_COLUMNS: yield return new IssueTemplateColumnExceed(this).Create(); break; } } protected override IEnumerable CheckHitObject(NoteInfo property, Note hitObject) { if (hitObject.Tone < property.MinTone) yield return new IssueTemplateNoteToneTooLow(this).Create(hitObject); if (hitObject.Tone > property.MaxTone) yield return new IssueTemplateNoteToneTooHigh(this).Create(hitObject); } public class IssueTemplateColumnNotEnough : IssueTemplate { public IssueTemplateColumnNotEnough(ICheck check) : base(check, IssueType.Warning, "Column is not enough.") { } public Issue Create() => new(this); } public class IssueTemplateColumnExceed : IssueTemplate { public IssueTemplateColumnExceed(ICheck check) : base(check, IssueType.Warning, "Column exceed.") { } public Issue Create() => new(this); } public class IssueTemplateNoteToneTooLow : IssueTemplate { public IssueTemplateNoteToneTooLow(ICheck check) : base(check, IssueType.Warning, "Note's tone is too low.") { } public Issue Create(Note note) => new NoteIssue(note, this); } public class IssueTemplateNoteToneTooHigh : IssueTemplate { public IssueTemplateNoteToneTooHigh(ICheck check) : base(check, IssueType.Warning, "Note's tone is too high.") { } public Issue Create(Note note) => new NoteIssue(note, this); } } ================================================ FILE: osu.Game.Rulesets.Karaoke/Edit/Checks/CheckBeatmapPageInfo.cs ================================================ // Copyright (c) andy840119 . Licensed under the GPL Licence. // See the LICENCE file in the repository root for full licence text. using System.Collections.Generic; using System.Linq; using osu.Game.Rulesets.Edit.Checks.Components; using osu.Game.Rulesets.Karaoke.Beatmaps; using osu.Game.Rulesets.Karaoke.Beatmaps.Metadatas; using osu.Game.Rulesets.Karaoke.Edit.Checks.Issues; using osu.Game.Rulesets.Karaoke.Objects; namespace osu.Game.Rulesets.Karaoke.Edit.Checks; public class CheckBeatmapPageInfo : CheckBeatmapProperty { public const double MIN_INTERVAL = 3000; public const double MAX_INTERVAL = 10000; protected override string Description => "Check invalid page in the beatmap"; public override IEnumerable PossibleTemplates => new IssueTemplate[] { new IssueTemplateLessThanTwoPages(this), new IssueTemplatePageIntervalTooShort(this), new IssueTemplatePageIntervalTooLong(this), new IssueTemplatePageIntervalShouldHaveAtLeastOneLyric(this), new IssueTemplateLyricNotWrapIntoTime(this), }; protected override PageInfo GetPropertyFromBeatmap(KaraokeBeatmap karaokeBeatmap) => karaokeBeatmap.PageInfo; protected override IEnumerable CheckProperty(PageInfo property) { var pages = property.Pages; if (pages.Count < 2) { yield return new IssueTemplateLessThanTwoPages(this).Create(); yield break; } for (int i = 1; i < pages.Count; i++) { var previous = pages[i - 1]; var current = pages[i]; double previousTime = previous.Time; double currentTime = current.Time; if (currentTime - previousTime < MIN_INTERVAL) yield return new IssueTemplatePageIntervalTooShort(this).Create(previous, current); if (currentTime - previousTime > MAX_INTERVAL) yield return new IssueTemplatePageIntervalTooLong(this).Create(previous, current); } } protected override IEnumerable CheckHitObjects(PageInfo property, IReadOnlyList hitObject) { var pages = property.Pages; if (pages.Count < 2) yield break; var availablePagesInObject = hitObject.ToDictionary(k => k, v => v.TimeValid ? property.GetPageAt(v.StartTime) : null); var missingHitObjectPages = pages.Where(page => !availablePagesInObject.ContainsValue(page)).ToArray(); for (int i = 1; i < missingHitObjectPages.Length; i++) { var previous = missingHitObjectPages[i - 1]; var current = missingHitObjectPages[i]; yield return new IssueTemplatePageIntervalShouldHaveAtLeastOneLyric(this).Create(previous, current); } foreach (var lyric in availablePagesInObject.Where(x => x.Value == null).Select(x => x.Key)) { yield return new IssueTemplateLyricNotWrapIntoTime(this).Create(lyric); } } public class IssueTemplateLessThanTwoPages : IssueTemplate { public IssueTemplateLessThanTwoPages(ICheck check) : base(check, IssueType.Warning, "Should have at least two pages.") { } public Issue Create() => new(this); } public class IssueTemplatePageIntervalTooShort : IssueTemplate { public IssueTemplatePageIntervalTooShort(ICheck check) : base(check, IssueType.Warning, "Interval between two pages are too short.") { } public Issue Create(Page startPage, Page endPage) => new BeatmapPageIssue(startPage, endPage, this); } public class IssueTemplatePageIntervalTooLong : IssueTemplate { public IssueTemplatePageIntervalTooLong(ICheck check) : base(check, IssueType.Warning, "Interval between two pages are too long.") { } public Issue Create(Page startPage, Page endPage) => new BeatmapPageIssue(startPage, endPage, this); } public class IssueTemplatePageIntervalShouldHaveAtLeastOneLyric : IssueTemplate { public IssueTemplatePageIntervalShouldHaveAtLeastOneLyric(ICheck check) : base(check, IssueType.Negligible, "Should have at least one lyric between two pages.") { } public Issue Create(Page startPage, Page endPage) => new BeatmapPageIssue(startPage, endPage, this); } public class IssueTemplateLyricNotWrapIntoTime : IssueTemplate { public IssueTemplateLyricNotWrapIntoTime(ICheck check) : base(check, IssueType.Negligible, "Lyric is not wrap by the page.") { } public Issue Create(Lyric lyric) => new LyricIssue(lyric, this); } } ================================================ FILE: osu.Game.Rulesets.Karaoke/Edit/Checks/CheckBeatmapProperty.cs ================================================ // Copyright (c) andy840119 . Licensed under the GPL Licence. // See the LICENCE file in the repository root for full licence text. using System; using System.Collections.Generic; using System.Linq; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Edit.Checks.Components; using osu.Game.Rulesets.Karaoke.Beatmaps; using osu.Game.Rulesets.Karaoke.Edit.Utils; using osu.Game.Rulesets.Karaoke.Objects; using osu.Game.Screens.Edit; namespace osu.Game.Rulesets.Karaoke.Edit.Checks; public abstract class CheckBeatmapProperty : ICheck where THitObject : KaraokeHitObject { public CheckMetadata Metadata => new(CheckCategory.Metadata, Description); protected abstract string Description { get; } public abstract IEnumerable PossibleTemplates { get; } public IEnumerable Run(BeatmapVerifierContext context) { var beatmap = getBeatmap(context); var property = GetPropertyFromBeatmap(beatmap); if (property == null) return Array.Empty(); var issues = CheckProperty(property); var hitObjects = context.CurrentDifficulty.Playable.HitObjects; var hitObjectIssues = hitObjects.OfType().SelectMany(x => CheckHitObject(property, x)); var hitObjectsIssues = CheckHitObjects(property, hitObjects.OfType().ToList()); return issues.Concat(hitObjectIssues).Concat(hitObjectsIssues); } protected abstract TProperty? GetPropertyFromBeatmap(KaraokeBeatmap karaokeBeatmap); protected virtual IEnumerable CheckProperty(TProperty property) { yield break; } protected virtual IEnumerable CheckHitObject(TProperty property, THitObject hitObject) { yield break; } protected virtual IEnumerable CheckHitObjects(TProperty property, IReadOnlyList hitObject) { yield break; } private static KaraokeBeatmap getBeatmap(BeatmapVerifierContext context) { // follow the usage in the IssueList in osu.Game if (context.CurrentDifficulty.Playable is EditorBeatmap editorBeatmap) return EditorBeatmapUtils.GetPlayableBeatmap(editorBeatmap); throw new InvalidOperationException(); } } ================================================ FILE: osu.Game.Rulesets.Karaoke/Edit/Checks/CheckClassicStageInfo.cs ================================================ // Copyright (c) andy840119 . Licensed under the GPL Licence. // See the LICENCE file in the repository root for full licence text. using System; using System.Collections.Generic; using System.Linq; using osu.Game.Rulesets.Edit.Checks.Components; using osu.Game.Rulesets.Karaoke.Edit.Checks.Issues; using osu.Game.Rulesets.Karaoke.Objects; using osu.Game.Rulesets.Karaoke.Stages.Infos.Classic; namespace osu.Game.Rulesets.Karaoke.Edit.Checks; public class CheckClassicStageInfo : CheckStageInfo { public const float MIN_ROW_HEIGHT = 30; public const float MAX_ROW_HEIGHT = 200; public const int MIN_LINE_SIZE = 0; public const int MAX_LINE_SIZE = 4; public const double MIN_TIMING_INTERVAL = 3000; public const double MAX_TIMING_INTERVAL = 10000; protected override string Description => "Check invalid info in the classic stage info."; public override IEnumerable CustomTemplates => new IssueTemplate[] { new IssueTemplateInvalidRowHeight(this), new IssueTemplateLessThanTwoTimingPoints(this), new IssueTemplateTimingIntervalTooShort(this), new IssueTemplateTimingIntervalTooLong(this), new IssueTemplateTimingInfoHitObjectNotExist(this), new IssueTemplateTimingInfoMappingHasNoTiming(this), new IssueTemplateTimingInfoTimingNotExist(this), new IssueTemplateTimingInfoLyricNotHaveTwoTiming(this), new IssueTemplateLyricLayoutInvalidLineNumber(this), }; public CheckClassicStageInfo() { RegisterCategory(x => x.StyleCategory, 0); RegisterCategory(x => x.LyricLayoutCategory, 2); } public override IEnumerable CheckStageInfoWithHitObjects(ClassicStageInfo stageInfo, IReadOnlyList hitObjects) { var issues = new List(); issues.AddRange(checkLyricLayoutDefinition(stageInfo.StageDefinition)); issues.AddRange(checkLyricTimingInfo(stageInfo.LyricTimingInfo, hitObjects.OfType().ToArray())); return issues; } private IEnumerable checkLyricLayoutDefinition(ClassicStageDefinition layoutDefinition) { if (layoutDefinition.LineHeight is < MIN_ROW_HEIGHT or > MAX_ROW_HEIGHT) yield return new IssueTemplateInvalidRowHeight(this).Create(); } private IEnumerable checkLyricTimingInfo(ClassicLyricTimingInfo timingInfo, IReadOnlyList hitObjects) { var timings = timingInfo.Timings; var mappings = timingInfo.Mappings; if (timings.Count < 2) { yield return new IssueTemplateLessThanTwoTimingPoints(this).Create(); yield break; } // check timing interval. for (int i = 1; i < timings.Count; i++) { var previous = timings[i - 1]; var current = timings[i]; double previousTime = previous.Time; double currentTime = current.Time; if (currentTime - previousTime < MIN_TIMING_INTERVAL) yield return new IssueTemplateTimingIntervalTooShort(this).Create(previous, current); if (currentTime - previousTime > MAX_TIMING_INTERVAL) yield return new IssueTemplateTimingIntervalTooLong(this).Create(previous, current); } // check have non-matched ids. foreach (var mapping in mappings) { // mapping lyric should be exist. if (hitObjects.All(x => x.ID != mapping.Key)) yield return new IssueTemplateTimingInfoHitObjectNotExist(this).Create(); // mapping timing should be exist. if (mapping.Value.Length == 0) yield return new IssueTemplateTimingInfoMappingHasNoTiming(this).Create(); // mapping timing should be exist. if (mapping.Value.Length != 0 && timings.All(x => !mapping.Value.Contains(x.ID))) yield return new IssueTemplateTimingInfoTimingNotExist(this).Create(); } // check mapping roles. foreach (var hitObject in hitObjects) { int timingAmounts = timingInfo.GetLyricTimingPoints(hitObject).Count(); // should have exactly 2 matched timing point in the lyric. if (timingAmounts != 0 && timingAmounts != 2) yield return new IssueTemplateTimingInfoLyricNotHaveTwoTiming(this).Create(); } } protected override IEnumerable CheckElement(TStageElement element) { switch (element) { case ClassicLyricLayout classicLyricLayout: if (classicLyricLayout.Line is < MIN_LINE_SIZE or > MAX_LINE_SIZE) yield return new IssueTemplateLyricLayoutInvalidLineNumber(this).Create(); break; case ClassicStyle: // todo: might need to check if skin resource is exist? break; default: throw new InvalidOperationException("Unknown stage element type."); } } public class IssueTemplateInvalidRowHeight : IssueTemplate { public IssueTemplateInvalidRowHeight(ICheck check) : base(check, IssueType.Warning, $"Row height should be in the range of {MIN_ROW_HEIGHT} and {MAX_ROW_HEIGHT}.") { } public Issue Create() => new(this); } #region timing info public class IssueTemplateLessThanTwoTimingPoints : IssueTemplate { public IssueTemplateLessThanTwoTimingPoints(ICheck check) : base(check, IssueType.Warning, "Should have at least two timing points.") { } public Issue Create() => new(this); } public class IssueTemplateTimingIntervalTooShort : IssueTemplate { public IssueTemplateTimingIntervalTooShort(ICheck check) : base(check, IssueType.Warning, "Interval between two timing points are too short.") { } public Issue Create(ClassicLyricTimingPoint startTimingPoint, ClassicLyricTimingPoint endTimingPoint) => new BeatmapClassicLyricTimingPointIssue(startTimingPoint, endTimingPoint, this); } public class IssueTemplateTimingIntervalTooLong : IssueTemplate { public IssueTemplateTimingIntervalTooLong(ICheck check) : base(check, IssueType.Warning, "Interval between two timing points are too long.") { } public Issue Create(ClassicLyricTimingPoint startTimingPoint, ClassicLyricTimingPoint endTimingPoint) => new BeatmapClassicLyricTimingPointIssue(startTimingPoint, endTimingPoint, this); } public class IssueTemplateTimingInfoHitObjectNotExist : IssueTemplate { public IssueTemplateTimingInfoHitObjectNotExist(ICheck check) : base(check, IssueType.Warning, "Maybe caused by hit-object has been deleted. Don't worry, go to the stage editor and will be easy to fix them.") { } public Issue Create() => new(this); } public class IssueTemplateTimingInfoMappingHasNoTiming : IssueTemplate { public IssueTemplateTimingInfoMappingHasNoTiming(ICheck check) : base(check, IssueType.Error, "Mapping should have the timing in the value. Should be the internal error.") { } public Issue Create() => new(this); } public class IssueTemplateTimingInfoTimingNotExist : IssueTemplate { public IssueTemplateTimingInfoTimingNotExist(ICheck check) : base(check, IssueType.Error, "It's caused by stage element has been deleted, but still remain the mapping data.") { } public Issue Create() => new(this); } public class IssueTemplateTimingInfoLyricNotHaveTwoTiming : IssueTemplate { public IssueTemplateTimingInfoLyricNotHaveTwoTiming(ICheck check) : base(check, IssueType.Warning, "Lyric should have exactly two timing. One is for start time and another one is for end time.") { } public Issue Create() => new(this); } #endregion #region element public class IssueTemplateLyricLayoutInvalidLineNumber : IssueTemplate { public IssueTemplateLyricLayoutInvalidLineNumber(ICheck check) : base(check, IssueType.Warning, $"Line number should be in the range of {MIN_LINE_SIZE} and {MAX_LINE_SIZE}.") { } public Issue Create() => new(this); } #endregion } ================================================ FILE: osu.Game.Rulesets.Karaoke/Edit/Checks/CheckHitObjectProperty.cs ================================================ // Copyright (c) andy840119 . Licensed under the GPL Licence. // See the LICENCE file in the repository root for full licence text. using System.Collections.Generic; using System.Linq; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Edit.Checks.Components; using osu.Game.Rulesets.Karaoke.Objects; namespace osu.Game.Rulesets.Karaoke.Edit.Checks; public abstract class CheckHitObjectProperty : ICheck where THitObject : KaraokeHitObject { public CheckMetadata Metadata => new(CheckCategory.HitObjects, Description); protected abstract string Description { get; } public abstract IEnumerable PossibleTemplates { get; } public virtual IEnumerable Run(BeatmapVerifierContext context) { var hitObjects = context.CurrentDifficulty.Playable.HitObjects.OfType(); return hitObjects.Select(Check).SelectMany(x => x); } protected abstract IEnumerable Check(THitObject hitObject); } ================================================ FILE: osu.Game.Rulesets.Karaoke/Edit/Checks/CheckHitObjectReferenceProperty.cs ================================================ // Copyright (c) andy840119 . Licensed under the GPL Licence. // See the LICENCE file in the repository root for full licence text. using System.Collections.Generic; using System.Linq; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Edit.Checks.Components; using osu.Game.Rulesets.Karaoke.Objects; namespace osu.Game.Rulesets.Karaoke.Edit.Checks; public abstract class CheckHitObjectReferenceProperty : CheckHitObjectProperty where THitObject : KaraokeHitObject where TReferencedHitObject : KaraokeHitObject { public override IEnumerable Run(BeatmapVerifierContext context) { var hitObjects = context.CurrentDifficulty.Playable.HitObjects.OfType(); var allAvailableReferencedHitObjects = context.CurrentDifficulty.Playable.HitObjects.OfType().ToArray(); var issues = base.Run(context); var referenceIssues = hitObjects.Select(x => CheckReferenceProperty(x, allAvailableReferencedHitObjects)).SelectMany(x => x); return issues.Concat(referenceIssues); } protected sealed override IEnumerable Check(THitObject hitObject) { yield break; } protected abstract IEnumerable CheckReferenceProperty(THitObject hitObject, IEnumerable allAvailableReferencedHitObjects); } ================================================ FILE: osu.Game.Rulesets.Karaoke/Edit/Checks/CheckLyricLanguage.cs ================================================ // Copyright (c) andy840119 . Licensed under the GPL Licence. // See the LICENCE file in the repository root for full licence text. using System.Collections.Generic; using osu.Game.Rulesets.Edit.Checks.Components; using osu.Game.Rulesets.Karaoke.Edit.Checks.Issues; using osu.Game.Rulesets.Karaoke.Objects; namespace osu.Game.Rulesets.Karaoke.Edit.Checks; public class CheckLyricLanguage : CheckHitObjectProperty { protected override string Description => "Lyric with invalid language."; public override IEnumerable PossibleTemplates => new IssueTemplate[] { new IssueTemplateNotFill(this), }; protected override IEnumerable Check(Lyric lyric) { if (lyric.Language == null) yield return new IssueTemplateNotFill(this).Create(lyric); } public class IssueTemplateNotFill : IssueTemplate { public IssueTemplateNotFill(ICheck check) : base(check, IssueType.Problem, "Lyric must have assign language.") { } public Issue Create(Lyric lyric) => new LyricIssue(lyric, this); } } ================================================ FILE: osu.Game.Rulesets.Karaoke/Edit/Checks/CheckLyricReferenceLyric.cs ================================================ // Copyright (c) andy840119 . Licensed under the GPL Licence. // See the LICENCE file in the repository root for full licence text. using System.Collections.Generic; using System.Linq; using osu.Game.Rulesets.Edit.Checks.Components; using osu.Game.Rulesets.Karaoke.Edit.Checks.Issues; using osu.Game.Rulesets.Karaoke.Objects; namespace osu.Game.Rulesets.Karaoke.Edit.Checks; public class CheckLyricReferenceLyric : CheckHitObjectReferenceProperty { protected override string Description => "Lyric with invalid reference lyric."; public override IEnumerable PossibleTemplates => new IssueTemplate[] { new IssueTemplateSelfReference(this), new IssueTemplateInvalidReferenceLyric(this), new IssueTemplateNullReferenceLyricConfig(this), new IssueTemplateHasReferenceLyricConfigWhenNoReferenceLyric(this), }; protected override IEnumerable CheckReferenceProperty(Lyric lyric, IEnumerable allAvailableReferencedHitObjects) { if (lyric.ReferenceLyric == lyric) yield return new IssueTemplateSelfReference(this).Create(lyric); if (lyric.ReferenceLyric != null && !allAvailableReferencedHitObjects.Contains(lyric.ReferenceLyric)) yield return new IssueTemplateInvalidReferenceLyric(this).Create(lyric); if (lyric.ReferenceLyric != null && lyric.ReferenceLyricConfig == null) yield return new IssueTemplateNullReferenceLyricConfig(this).Create(lyric); if (lyric.ReferenceLyric == null && lyric.ReferenceLyricConfig != null) yield return new IssueTemplateHasReferenceLyricConfigWhenNoReferenceLyric(this).Create(lyric); } public class IssueTemplateSelfReference : IssueTemplate { public IssueTemplateSelfReference(ICheck check) : base(check, IssueType.Error, "Lyric should not reference to itself.") { } public Issue Create(Lyric lyric) => new LyricIssue(lyric, this); } public class IssueTemplateInvalidReferenceLyric : IssueTemplate { public IssueTemplateInvalidReferenceLyric(ICheck check) : base(check, IssueType.Error, "Reference lyric does not exist in the beatmap.") { } public Issue Create(Lyric lyric) => new LyricIssue(lyric, this); } public class IssueTemplateNullReferenceLyricConfig : IssueTemplate { public IssueTemplateNullReferenceLyricConfig(ICheck check) : base(check, IssueType.Error, "Must have config if reference to another lyric.") { } public Issue Create(Lyric lyric) => new LyricIssue(lyric, this); } public class IssueTemplateHasReferenceLyricConfigWhenNoReferenceLyric : IssueTemplate { public IssueTemplateHasReferenceLyricConfigWhenNoReferenceLyric(ICheck check) : base(check, IssueType.Error, "Should not have the reference lyric config if reference to another lyric.") { } public Issue Create(Lyric lyric) => new LyricIssue(lyric, this); } } ================================================ FILE: osu.Game.Rulesets.Karaoke/Edit/Checks/CheckLyricRubyTag.cs ================================================ // Copyright (c) andy840119 . Licensed under the GPL Licence. // See the LICENCE file in the repository root for full licence text. using System.Collections.Generic; using osu.Game.Rulesets.Edit.Checks.Components; using osu.Game.Rulesets.Karaoke.Edit.Checks.Issues; using osu.Game.Rulesets.Karaoke.Objects; using osu.Game.Rulesets.Karaoke.Objects.Utils; namespace osu.Game.Rulesets.Karaoke.Edit.Checks; public class CheckLyricRubyTag : CheckHitObjectProperty { protected override string Description => "Lyric with invalid ruby tag."; public override IEnumerable PossibleTemplates => new IssueTemplate[] { new IssueTemplateOutOfRange(this), new IssueTemplateOverlapping(this), new IssueTemplateEmptyText(this), }; protected override IEnumerable Check(Lyric lyric) { string text = lyric.Text; var rubyTags = lyric.RubyTags; const RubyTagsUtils.Sorting sorting = RubyTagsUtils.Sorting.Asc; var outOfRangeTags = RubyTagsUtils.FindOutOfRange(rubyTags, text); var overlappingTags = RubyTagsUtils.FindOverlapping(rubyTags, sorting); var emptyTags = RubyTagsUtils.FindEmptyText(rubyTags); foreach (var rubyTag in outOfRangeTags) { yield return new IssueTemplateOutOfRange(this).Create(lyric, rubyTag); } foreach (var rubyTag in overlappingTags) { yield return new IssueTemplateOverlapping(this).Create(lyric, rubyTag); } foreach (var rubyTag in emptyTags) { yield return new IssueTemplateEmptyText(this).Create(lyric, rubyTag); } } public abstract class IssueTemplateLyricRuby : IssueTemplate { protected IssueTemplateLyricRuby(ICheck check, IssueType type, string unformattedMessage) : base(check, type, unformattedMessage) { } public Issue Create(Lyric lyric, RubyTag rubyTag) => new LyricRubyTagIssue(lyric, this, rubyTag, rubyTag); } public class IssueTemplateOutOfRange : IssueTemplateLyricRuby { public IssueTemplateOutOfRange(ICheck check) : base(check, IssueType.Error, "Ruby tag index is out of range.") { } } public class IssueTemplateOverlapping : IssueTemplateLyricRuby { public IssueTemplateOverlapping(ICheck check) : base(check, IssueType.Problem, "Ruby tag index is overlapping to another ruby tag.") { } } public class IssueTemplateEmptyText : IssueTemplateLyricRuby { public IssueTemplateEmptyText(ICheck check) : base(check, IssueType.Problem, "Ruby tag's text should not be empty or white-space only.") { } } } ================================================ FILE: osu.Game.Rulesets.Karaoke/Edit/Checks/CheckLyricSinger.cs ================================================ // Copyright (c) andy840119 . Licensed under the GPL Licence. // See the LICENCE file in the repository root for full licence text. using System.Collections.Generic; using System.Linq; using osu.Game.Rulesets.Edit.Checks.Components; using osu.Game.Rulesets.Karaoke.Edit.Checks.Issues; using osu.Game.Rulesets.Karaoke.Objects; namespace osu.Game.Rulesets.Karaoke.Edit.Checks; public class CheckLyricSinger : CheckHitObjectProperty { protected override string Description => "Lyric with invalid singer."; public override IEnumerable PossibleTemplates => new IssueTemplate[] { new IssueTemplateNoSinger(this), }; protected override IEnumerable Check(Lyric lyric) { if (!lyric.SingerIds.Any()) yield return new IssueTemplateNoSinger(this).Create(lyric); } public class IssueTemplateNoSinger : IssueTemplate { public IssueTemplateNoSinger(ICheck check) : base(check, IssueType.Problem, "Lyric must have at least one singer.") { } public Issue Create(Lyric lyric) => new LyricIssue(lyric, this); } } ================================================ FILE: osu.Game.Rulesets.Karaoke/Edit/Checks/CheckLyricText.cs ================================================ // Copyright (c) andy840119 . Licensed under the GPL Licence. // See the LICENCE file in the repository root for full licence text. using System.Collections.Generic; using osu.Game.Rulesets.Edit.Checks.Components; using osu.Game.Rulesets.Karaoke.Edit.Checks.Issues; using osu.Game.Rulesets.Karaoke.Objects; namespace osu.Game.Rulesets.Karaoke.Edit.Checks; public class CheckLyricText : CheckHitObjectProperty { protected override string Description => "Lyric with invalid text."; public override IEnumerable PossibleTemplates => new IssueTemplate[] { new IssueTemplateEmptyText(this), }; protected override IEnumerable Check(Lyric lyric) { if (string.IsNullOrWhiteSpace(lyric.Text)) yield return new IssueTemplateEmptyText(this).Create(lyric); } public class IssueTemplateEmptyText : IssueTemplate { public IssueTemplateEmptyText(ICheck check) : base(check, IssueType.Problem, "Lyric's text should not be empty or white-space only.") { } public Issue Create(Lyric lyric) => new LyricIssue(lyric, this); } } ================================================ FILE: osu.Game.Rulesets.Karaoke/Edit/Checks/CheckLyricTimeTag.cs ================================================ // Copyright (c) andy840119 . Licensed under the GPL Licence. // See the LICENCE file in the repository root for full licence text. using System; using System.Collections.Generic; using System.Linq; using osu.Framework.Graphics.Sprites; using osu.Game.Rulesets.Edit.Checks.Components; using osu.Game.Rulesets.Karaoke.Edit.Checks.Issues; using osu.Game.Rulesets.Karaoke.Objects; using osu.Game.Rulesets.Karaoke.Objects.Utils; using osu.Game.Rulesets.Karaoke.Utils; namespace osu.Game.Rulesets.Karaoke.Edit.Checks; public class CheckLyricTimeTag : CheckHitObjectProperty { protected override string Description => "Lyric with invalid time-tag."; public override IEnumerable PossibleTemplates => new IssueTemplate[] { new IssueTemplateEmpty(this), new IssueTemplateMissingStart(this), new IssueTemplateMissingEnd(this), new IssueTemplateOutOfRange(this), new IssueTemplateOverlapping(this), new IssueTemplateEmptyTime(this), new IssueTemplateInvalidRomanisedSyllable(this), new IssueTemplateShouldFillRomanisedSyllable(this), new IssueTemplateShouldNotFillRomanisedSyllable(this), new IssueTemplateShouldNotMarkFirstSyllable(this), }; protected override IEnumerable Check(Lyric lyric) { var issues = new List(); issues.AddRange(CheckTimeTag(lyric)); issues.AddRange(CheckTimeTagRomanisedSyllable(lyric)); return issues; } protected IEnumerable CheckTimeTag(Lyric lyric) { if (!lyric.TimeTags.Any()) { yield return new IssueTemplateEmpty(this).Create(lyric); yield break; } if (!TimeTagsUtils.HasStartTimeTagInLyric(lyric.TimeTags, lyric.Text)) yield return new IssueTemplateMissingStart(this).Create(lyric); if (!TimeTagsUtils.HasEndTimeTagInLyric(lyric.TimeTags, lyric.Text)) yield return new IssueTemplateMissingEnd(this).Create(lyric); // todo: maybe config? const GroupCheck group_check = GroupCheck.Asc; const SelfCheck self_check = SelfCheck.BasedOnStart; var outOfRangeTags = TimeTagsUtils.FindOutOfRange(lyric.TimeTags, lyric.Text); var overlappingTimeTags = TimeTagsUtils.FindOverlapping(lyric.TimeTags, group_check, self_check).ToArray(); var noTimeTimeTags = TimeTagsUtils.FindNoneTime(lyric.TimeTags); foreach (var timeTag in outOfRangeTags) { yield return new IssueTemplateOutOfRange(this).Create(lyric, timeTag); } foreach (var timeTag in overlappingTimeTags) { yield return new IssueTemplateOverlapping(this).Create(lyric, timeTag); } foreach (var timeTag in noTimeTimeTags) { yield return new IssueTemplateEmptyTime(this).Create(lyric, timeTag); } } protected IEnumerable CheckTimeTagRomanisedSyllable(Lyric lyric) { if (!lyric.TimeTags.Any()) { yield break; } foreach (var timeTag in lyric.TimeTags) { bool firstSyllable = timeTag.FirstSyllable; string? romanisedSyllable = timeTag.RomanisedSyllable; switch (timeTag.Index.State) { case TextIndex.IndexState.Start: // if input the romanised syllable, should be valid. if (romanisedSyllable != null && !isRomanisedSyllableValid(romanisedSyllable)) yield return new IssueTemplateInvalidRomanisedSyllable(this).Create(lyric, timeTag); // if is first romanised syllable, should not be null. if (firstSyllable && romanisedSyllable == null) yield return new IssueTemplateShouldFillRomanisedSyllable(this).Create(lyric, timeTag); break; case TextIndex.IndexState.End: if (romanisedSyllable != null) yield return new IssueTemplateShouldNotFillRomanisedSyllable(this).Create(lyric, timeTag); if (firstSyllable) yield return new IssueTemplateShouldNotMarkFirstSyllable(this).Create(lyric, timeTag); break; default: throw new ArgumentOutOfRangeException(); } } yield break; static bool isRomanisedSyllableValid(string text) { // should not be white-space only. if (string.IsNullOrWhiteSpace(text)) return false; // should be all latin text or white-space return text.All(c => CharUtils.IsLatin(c) || CharUtils.IsSpacing(c)); } } public class IssueTemplateEmpty : IssueTemplate { public IssueTemplateEmpty(ICheck check) : base(check, IssueType.Problem, "This lyric has no time-tag.") { } public Issue Create(Lyric lyric) => new LyricIssue(lyric, this); } public class IssueTemplateMissingStart : IssueTemplate { public IssueTemplateMissingStart(ICheck check) : base(check, IssueType.Problem, "Missing first time-tag in the lyric.") { } public Issue Create(Lyric lyric) => new LyricIssue(lyric, this); } public class IssueTemplateMissingEnd : IssueTemplate { public IssueTemplateMissingEnd(ICheck check) : base(check, IssueType.Problem, "Missing last time-tag in the lyric.") { } public Issue Create(Lyric lyric) => new LyricIssue(lyric, this); } public abstract class IssueTemplateLyricTimeTag : IssueTemplate { protected IssueTemplateLyricTimeTag(ICheck check, IssueType type, string unformattedMessage) : base(check, type, unformattedMessage) { } public Issue Create(Lyric lyric, TimeTag timeTag) => new LyricTimeTagIssue(lyric, this, timeTag, timeTag); } public class IssueTemplateOutOfRange : IssueTemplateLyricTimeTag { public IssueTemplateOutOfRange(ICheck check) : base(check, IssueType.Problem, "Time-tag index is out of range.") { } } public class IssueTemplateOverlapping : IssueTemplateLyricTimeTag { public IssueTemplateOverlapping(ICheck check) : base(check, IssueType.Problem, "Time-tag index is overlapping to another time-tag.") { } } public class IssueTemplateEmptyTime : IssueTemplateLyricTimeTag { public IssueTemplateEmptyTime(ICheck check) : base(check, IssueType.Problem, "Time-tag has no time.") { } } public class IssueTemplateInvalidRomanisedSyllable : IssueTemplateLyricTimeTag { public IssueTemplateInvalidRomanisedSyllable(ICheck check) : base(check, IssueType.Problem, "Romanised syllable should not be empty or white-space only.") { } } public class IssueTemplateShouldFillRomanisedSyllable : IssueTemplateLyricTimeTag { public IssueTemplateShouldFillRomanisedSyllable(ICheck check) : base(check, IssueType.Problem, "Romanised syllable should not be empty or white-space if in the first time-tag.") { } } public class IssueTemplateShouldNotFillRomanisedSyllable : IssueTemplateLyricTimeTag { public IssueTemplateShouldNotFillRomanisedSyllable(ICheck check) : base(check, IssueType.Error, "Should not have empty romanised syllable if time-tag is end.") { } } public class IssueTemplateShouldNotMarkFirstSyllable : IssueTemplateLyricTimeTag { public IssueTemplateShouldNotMarkFirstSyllable(ICheck check) : base(check, IssueType.Error, "Should not have empty romanised syllable if time-tag is end.") { } } } ================================================ FILE: osu.Game.Rulesets.Karaoke/Edit/Checks/CheckLyricTranslations.cs ================================================ // Copyright (c) andy840119 . Licensed under the GPL Licence. // See the LICENCE file in the repository root for full licence text. using System.Collections.Generic; using System.Globalization; using osu.Game.Rulesets.Edit.Checks.Components; using osu.Game.Rulesets.Karaoke.Edit.Checks.Issues; using osu.Game.Rulesets.Karaoke.Objects; namespace osu.Game.Rulesets.Karaoke.Edit.Checks; public class CheckLyricTranslations : CheckHitObjectProperty { protected override string Description => "Lyric with invalid translations."; public override IEnumerable PossibleTemplates => new IssueTemplate[] { new IssueTemplateEmptyText(this), }; protected override IEnumerable Check(Lyric lyric) { var translations = lyric.Translations; foreach ((var language, string translation) in translations) { if (string.IsNullOrWhiteSpace(translation)) yield return new IssueTemplateEmptyText(this).Create(lyric, language); } } public class IssueTemplateEmptyText : IssueTemplate { public IssueTemplateEmptyText(ICheck check) : base(check, IssueType.Problem, "Translation in the lyric should not by empty or white-space only.") { } public Issue Create(Lyric lyric, CultureInfo language) => new LyricIssue(lyric, this, language); } } ================================================ FILE: osu.Game.Rulesets.Karaoke/Edit/Checks/CheckNoteReferenceLyric.cs ================================================ // Copyright (c) andy840119 . Licensed under the GPL Licence. // See the LICENCE file in the repository root for full licence text. using System.Collections.Generic; using System.Linq; using osu.Game.Rulesets.Edit.Checks.Components; using osu.Game.Rulesets.Karaoke.Edit.Checks.Issues; using osu.Game.Rulesets.Karaoke.Objects; namespace osu.Game.Rulesets.Karaoke.Edit.Checks; public class CheckNoteReferenceLyric : CheckHitObjectReferenceProperty { protected override string Description => "Note with invalid reference lyric."; public override IEnumerable PossibleTemplates => new IssueTemplate[] { new IssueTemplateNullReferenceLyric(this), new IssueTemplateInvalidReferenceLyric(this), new IssueTemplateMissingReferenceTimeTag(this), new IssueTemplateMissingStartReferenceTimeTag(this), new IssueTemplateStartReferenceTimeTagMissingTime(this), new IssueTemplateMissingEndReferenceTimeTag(this), new IssueTemplateEndReferenceTimeTagMissingTime(this), }; protected override IEnumerable CheckReferenceProperty(Note note, IEnumerable allAvailableReferencedHitObjects) { if (note.ReferenceLyric == null) { yield return new IssueTemplateNullReferenceLyric(this).Create(note); yield break; } if (note.ReferenceLyric != null && !allAvailableReferencedHitObjects.Contains(note.ReferenceLyric)) yield return new IssueTemplateInvalidReferenceLyric(this).Create(note); var startTimeTag = note.StartReferenceTimeTag; var endTimeTag = note.EndReferenceTimeTag; if (startTimeTag == null && endTimeTag == null) { yield return new IssueTemplateMissingReferenceTimeTag(this).Create(note); yield break; } if (startTimeTag == null) yield return new IssueTemplateMissingStartReferenceTimeTag(this).Create(note); if (startTimeTag != null && startTimeTag.Time == null) yield return new IssueTemplateStartReferenceTimeTagMissingTime(this).Create(note); if (endTimeTag == null) yield return new IssueTemplateMissingEndReferenceTimeTag(this).Create(note); if (endTimeTag != null && endTimeTag.Time == null) yield return new IssueTemplateEndReferenceTimeTagMissingTime(this).Create(note); } public class IssueTemplateNullReferenceLyric : IssueTemplate { public IssueTemplateNullReferenceLyric(ICheck check) : base(check, IssueType.Error, "Note must have its parent lyric.") { } public Issue Create(Note note) => new NoteIssue(note, this); } public class IssueTemplateInvalidReferenceLyric : IssueTemplate { public IssueTemplateInvalidReferenceLyric(ICheck check) : base(check, IssueType.Error, "Note's reference lyric must in the beatmap.") { } public Issue Create(Note note) => new NoteIssue(note, this); } public class IssueTemplateMissingReferenceTimeTag : IssueTemplate { public IssueTemplateMissingReferenceTimeTag(ICheck check) : base(check, IssueType.Problem, "Note's reference time-tag is missing.") { } public Issue Create(Note note) => new NoteIssue(note, this); } public class IssueTemplateMissingStartReferenceTimeTag : IssueTemplate { public IssueTemplateMissingStartReferenceTimeTag(ICheck check) : base(check, IssueType.Problem, "Note's start reference time-tag is missing.") { } public Issue Create(Note note) => new NoteIssue(note, this); } public class IssueTemplateStartReferenceTimeTagMissingTime : IssueTemplate { public IssueTemplateStartReferenceTimeTagMissingTime(ICheck check) : base(check, IssueType.Problem, "Note's start reference time-tag is found but missing time.") { } public Issue Create(Note note) => new NoteIssue(note, this); } public class IssueTemplateMissingEndReferenceTimeTag : IssueTemplate { public IssueTemplateMissingEndReferenceTimeTag(ICheck check) : base(check, IssueType.Problem, "Note's end reference time-tag is missing.") { } public Issue Create(Note note) => new NoteIssue(note, this); } public class IssueTemplateEndReferenceTimeTagMissingTime : IssueTemplate { public IssueTemplateEndReferenceTimeTagMissingTime(ICheck check) : base(check, IssueType.Problem, "Note's end reference time-tag is found but missing time.") { } public Issue Create(Note note) => new NoteIssue(note, this); } } ================================================ FILE: osu.Game.Rulesets.Karaoke/Edit/Checks/CheckNoteText.cs ================================================ // Copyright (c) andy840119 . Licensed under the GPL Licence. // See the LICENCE file in the repository root for full licence text. using System.Collections.Generic; using osu.Game.Rulesets.Edit.Checks.Components; using osu.Game.Rulesets.Karaoke.Edit.Checks.Issues; using osu.Game.Rulesets.Karaoke.Objects; namespace osu.Game.Rulesets.Karaoke.Edit.Checks; public class CheckNoteText : CheckHitObjectProperty { protected override string Description => "Note with invalid text."; public override IEnumerable PossibleTemplates => new IssueTemplate[] { new IssueTemplateEmptyText(this), new IssueTemplateEmptyRubyText(this), }; protected override IEnumerable Check(Note note) { if (string.IsNullOrWhiteSpace(note.Text)) yield return new IssueTemplateEmptyText(this).Create(note); if (note.RubyText != null && string.IsNullOrWhiteSpace(note.RubyText)) yield return new IssueTemplateEmptyRubyText(this).Create(note); } public class IssueTemplateEmptyText : IssueTemplate { public IssueTemplateEmptyText(ICheck check) : base(check, IssueType.Problem, "Note must have text.") { } public Issue Create(Note note) => new NoteIssue(note, this); } public class IssueTemplateEmptyRubyText : IssueTemplate { public IssueTemplateEmptyRubyText(ICheck check) : base(check, IssueType.Error, "Note's ruby text should be null or has the value.") { } public Issue Create(Note note) => new NoteIssue(note, this); } } ================================================ FILE: osu.Game.Rulesets.Karaoke/Edit/Checks/CheckNoteTime.cs ================================================ // Copyright (c) andy840119 . Licensed under the GPL Licence. // See the LICENCE file in the repository root for full licence text. using System.Collections.Generic; using osu.Game.Rulesets.Edit.Checks.Components; using osu.Game.Rulesets.Karaoke.Edit.Checks.Issues; using osu.Game.Rulesets.Karaoke.Objects; namespace osu.Game.Rulesets.Karaoke.Edit.Checks; public class CheckNoteTime : CheckHitObjectProperty { public const double MIN_DURATION = 100; public const double MAX_DURATION = 10000; protected override string Description => "Note with invalid timing in the note."; public override IEnumerable PossibleTemplates => new IssueTemplate[] { new IssueTemplateInvalidReferenceTimeTagTime(this), new IssueTemplateDurationTooShort(this), new IssueTemplateDurationTooLong(this), }; protected override IEnumerable Check(Note note) { // because lyric's start and end time is assigned by the reference lyric, so should skip the check if note does not contains the reference lyric. var referenceLyric = note.ReferenceLyric; if (referenceLyric == null) yield break; // should make sure that reference time-tag has time. // if contains no time, will reported in the CheckNoteReferenceLyric. double? startTime = note.StartReferenceTimeTag?.Time; double? endTime = note.EndReferenceTimeTag?.Time; if (startTime == null || endTime == null) yield break; // should have alert if reference time-tag's time is invalid. if (endTime.Value < startTime.Value) { yield return new IssueTemplateInvalidReferenceTimeTagTime(this).Create(note); yield break; } // note's duration should be in the range. switch (note.Duration) { case < MIN_DURATION: yield return new IssueTemplateDurationTooShort(this).Create(note); break; case > MAX_DURATION: yield return new IssueTemplateDurationTooLong(this).Create(note); break; } // todo: check for offset time's range. } public class IssueTemplateInvalidReferenceTimeTagTime : IssueTemplate { public IssueTemplateInvalidReferenceTimeTagTime(ICheck check) : base(check, IssueType.Problem, "Note must have text.") { } public Issue Create(Note note) => new NoteIssue(note, this); } public class IssueTemplateDurationTooShort : IssueTemplate { public IssueTemplateDurationTooShort(ICheck check) : base(check, IssueType.Problem, "Note's duration too short.") { } public Issue Create(Note note) => new NoteIssue(note, this); } public class IssueTemplateDurationTooLong : IssueTemplate { public IssueTemplateDurationTooLong(ICheck check) : base(check, IssueType.Problem, "Note's duration too long.") { } public Issue Create(Note note) => new NoteIssue(note, this); } } ================================================ FILE: osu.Game.Rulesets.Karaoke/Edit/Checks/CheckStageInfo.cs ================================================ // Copyright (c) andy840119 . Licensed under the GPL Licence. // See the LICENCE file in the repository root for full licence text. using System; using System.Collections.Generic; using System.Linq; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Edit.Checks.Components; using osu.Game.Rulesets.Karaoke.Beatmaps; using osu.Game.Rulesets.Karaoke.Objects; using osu.Game.Rulesets.Karaoke.Stages.Infos; namespace osu.Game.Rulesets.Karaoke.Edit.Checks; public abstract class CheckStageInfo : ICheck where TStageInfo : StageInfo { public CheckMetadata Metadata => new(CheckCategory.Files, Description); protected abstract string Description { get; } public IEnumerable PossibleTemplates => baseTemplates.Concat(CustomTemplates); private IEnumerable baseTemplates => new IssueTemplate[] { new IssueTemplateNoElement(this), new IssueTemplateMappingHitObjectNotExist(this), new IssueTemplateMappingItemNotExist(this), }; public abstract IEnumerable CustomTemplates { get; } private readonly IList, IEnumerable>> stageInfoCategoryActions = new List, IEnumerable>>(); public void RegisterCategory(Func> categoryAction, int minimumRequiredElements) where TStageElement : StageElement, new() where THitObject : KaraokeHitObject, IHasPrimaryKey { stageInfoCategoryActions.Add((info, hitObjects) => { var category = categoryAction(info); return checkElementCategory(category, hitObjects.OfType().ToList(), minimumRequiredElements); }); } private IEnumerable checkElementCategory(StageElementCategory category, IReadOnlyList hitObjects, int minimumRequiredElements) where TStageElement : StageElement, new() where THitObject : KaraokeHitObject, IHasPrimaryKey { // check mapping. var issues = checkMappings(category, hitObjects).ToList(); // check element amount. if (category.AvailableElements.Count < minimumRequiredElements) issues.Add(new IssueTemplateNoElement(this).Create(minimumRequiredElements)); // check elements. issues.AddRange(CheckElement(category.DefaultElement)); foreach (var element in category.AvailableElements) { issues.AddRange(CheckElement(element)); } return issues; } private IEnumerable checkMappings(StageElementCategory category, IReadOnlyList hitObjects) where TStageElement : StageElement, new() where THitObject : KaraokeHitObject, IHasPrimaryKey { var elements = category.AvailableElements; var mappings = category.Mappings; foreach (var mapping in mappings) { if (hitObjects.All(x => x.ID != mapping.Key)) yield return new IssueTemplateMappingHitObjectNotExist(this).Create(); if (elements.All(x => x.ID != mapping.Value)) yield return new IssueTemplateMappingItemNotExist(this).Create(); } } public IEnumerable Run(BeatmapVerifierContext context) { var property = getStageInfo(context); if (property == null) return Array.Empty(); var hitObjects = context.CurrentDifficulty.Playable.HitObjects.OfType().ToList(); var issues = CheckStageInfoWithHitObjects(property, hitObjects).ToList(); foreach (var stageInfoCategoryAction in stageInfoCategoryActions) { issues.AddRange(stageInfoCategoryAction(property, hitObjects)); } return issues; // todo: get stage info from context. static TStageInfo? getStageInfo(BeatmapVerifierContext context) => throw new NotImplementedException(); } public abstract IEnumerable CheckStageInfoWithHitObjects(TStageInfo stageInfo, IReadOnlyList hitObjects); protected abstract IEnumerable CheckElement(TStageElement element) where TStageElement : StageElement; public class IssueTemplateNoElement : IssueTemplate { public IssueTemplateNoElement(ICheck check) : base(check, IssueType.Warning, "Should have at least {0} elements in the stage.") { } public Issue Create(int minimumRequiredElements) => new(this, minimumRequiredElements); } public class IssueTemplateMappingHitObjectNotExist : IssueTemplate { public IssueTemplateMappingHitObjectNotExist(ICheck check) : base(check, IssueType.Warning, "Maybe caused by hit-object has been deleted. Don't worry, go to the stage editor and will be easy to fix them.") { } public Issue Create() => new(this); } public class IssueTemplateMappingItemNotExist : IssueTemplate { public IssueTemplateMappingItemNotExist(ICheck check) : base(check, IssueType.Error, "It's caused by stage element has been deleted, but still remain the mapping data.") { } public Issue Create() => new(this); } } ================================================ FILE: osu.Game.Rulesets.Karaoke/Edit/Checks/Issues/BeatmapClassicLyricTimingPointIssue.cs ================================================ // Copyright (c) andy840119 . Licensed under the GPL Licence. // See the LICENCE file in the repository root for full licence text. using System; using osu.Game.Rulesets.Edit.Checks.Components; using osu.Game.Rulesets.Karaoke.Stages.Infos.Classic; namespace osu.Game.Rulesets.Karaoke.Edit.Checks.Issues; public class BeatmapClassicLyricTimingPointIssue : Issue { public ClassicLyricTimingPoint StartTimingPoint; public ClassicLyricTimingPoint EndTimingPoint; public BeatmapClassicLyricTimingPointIssue(ClassicLyricTimingPoint startTimingPoint, ClassicLyricTimingPoint endTimingPoint, IssueTemplate template, params object[] args) : base(template, args) { StartTimingPoint = startTimingPoint; EndTimingPoint = endTimingPoint; Time = Math.Min(StartTimingPoint.Time, EndTimingPoint.Time); } } ================================================ FILE: osu.Game.Rulesets.Karaoke/Edit/Checks/Issues/BeatmapPageIssue.cs ================================================ // Copyright (c) andy840119 . Licensed under the GPL Licence. // See the LICENCE file in the repository root for full licence text. using System; using osu.Game.Rulesets.Edit.Checks.Components; using osu.Game.Rulesets.Karaoke.Beatmaps.Metadatas; namespace osu.Game.Rulesets.Karaoke.Edit.Checks.Issues; public class BeatmapPageIssue : Issue { public Page StartPage; public Page EndPage; public BeatmapPageIssue(Page startPage, Page endPage, IssueTemplate template, params object[] args) : base(template, args) { StartPage = startPage; EndPage = endPage; Time = Math.Min(StartPage.Time, EndPage.Time); } } ================================================ FILE: osu.Game.Rulesets.Karaoke/Edit/Checks/Issues/LyricIssue.cs ================================================ // Copyright (c) andy840119 . Licensed under the GPL Licence. // See the LICENCE file in the repository root for full licence text. using osu.Game.Rulesets.Edit.Checks.Components; using osu.Game.Rulesets.Karaoke.Objects; namespace osu.Game.Rulesets.Karaoke.Edit.Checks.Issues; public class LyricIssue : Issue { public Lyric Lyric; public LyricIssue(Lyric lyric, IssueTemplate template, params object[] args) : base(lyric, template, args) { Lyric = lyric; Time = Lyric.TimeValid ? Lyric.StartTime : null; } } ================================================ FILE: osu.Game.Rulesets.Karaoke/Edit/Checks/Issues/LyricRubyTagIssue.cs ================================================ // Copyright (c) andy840119 . Licensed under the GPL Licence. // See the LICENCE file in the repository root for full licence text. using osu.Game.Rulesets.Edit.Checks.Components; using osu.Game.Rulesets.Karaoke.Objects; namespace osu.Game.Rulesets.Karaoke.Edit.Checks.Issues; public class LyricRubyTagIssue : LyricIssue { public readonly RubyTag RubyTag; public LyricRubyTagIssue(Lyric lyric, IssueTemplate template, RubyTag rubyTag, params object[] args) : base(lyric, template, args) { RubyTag = rubyTag; } } ================================================ FILE: osu.Game.Rulesets.Karaoke/Edit/Checks/Issues/LyricTimeTagIssue.cs ================================================ // Copyright (c) andy840119 . Licensed under the GPL Licence. // See the LICENCE file in the repository root for full licence text. using osu.Game.Rulesets.Edit.Checks.Components; using osu.Game.Rulesets.Karaoke.Objects; namespace osu.Game.Rulesets.Karaoke.Edit.Checks.Issues; public class LyricTimeTagIssue : LyricIssue { public readonly TimeTag TimeTag; public LyricTimeTagIssue(Lyric lyric, IssueTemplate template, TimeTag timeTag, params object[] args) : base(lyric, template, args) { TimeTag = timeTag; if (lyric.TimeValid) { Time = TimeTag.Time ?? Lyric.StartTime; } } } ================================================ FILE: osu.Game.Rulesets.Karaoke/Edit/Checks/Issues/NoteIssue.cs ================================================ // Copyright (c) andy840119 . Licensed under the GPL Licence. // See the LICENCE file in the repository root for full licence text. using osu.Game.Rulesets.Edit.Checks.Components; using osu.Game.Rulesets.Karaoke.Objects; namespace osu.Game.Rulesets.Karaoke.Edit.Checks.Issues; public class NoteIssue : Issue { public Note Note; public NoteIssue(Note note, IssueTemplate template, params object[] args) : base(note, template, args) { Note = note; } } ================================================ FILE: osu.Game.Rulesets.Karaoke/Edit/Components/ContextMenu/LyricLockContextMenu.cs ================================================ // Copyright (c) andy840119 . Licensed under the GPL Licence. // See the LICENCE file in the repository root for full licence text. using System; using System.Collections.Generic; using System.Linq; using osu.Game.Graphics.UserInterface; using osu.Game.Rulesets.Karaoke.Edit.ChangeHandlers; using osu.Game.Rulesets.Karaoke.Objects; using osu.Game.Rulesets.Karaoke.Objects.Types; namespace osu.Game.Rulesets.Karaoke.Edit.Components.ContextMenu; public class LyricLockContextMenu : OsuMenuItem { public LyricLockContextMenu(ILockChangeHandler lockChangeHandler, Lyric lyric, string name) : this(lockChangeHandler, new List { lyric }, name) { } public LyricLockContextMenu(ILockChangeHandler lockChangeHandler, List lyrics, string name) : base(name) { Items = Enum.GetValues().Select(l => new OsuMenuItem(l.ToString(), anyLyricInLockState(l) ? MenuItemType.Highlighted : MenuItemType.Standard, () => { // todo: how to make lyric as selected? lockChangeHandler.Lock(l); })).ToList(); bool anyLyricInLockState(LockState lockState) => lyrics.Any(lyric => lyric.Lock == lockState); } } ================================================ FILE: osu.Game.Rulesets.Karaoke/Edit/Components/ContextMenu/SingerContextMenu.cs ================================================ // Copyright (c) andy840119 . Licensed under the GPL Licence. // See the LICENCE file in the repository root for full licence text. using System; using System.Linq; using osu.Game.Graphics.UserInterface; using osu.Game.Rulesets.Karaoke.Beatmaps.Metadatas; using osu.Game.Rulesets.Karaoke.Edit.ChangeHandlers.Lyrics; using osu.Game.Rulesets.Karaoke.Edit.Utils; using osu.Game.Rulesets.Karaoke.Objects; using osu.Game.Rulesets.Karaoke.Objects.Utils; using osu.Game.Screens.Edit; namespace osu.Game.Rulesets.Karaoke.Edit.Components.ContextMenu; public class SingerContextMenu : OsuMenuItem { public SingerContextMenu(EditorBeatmap beatmap, ILyricSingerChangeHandler lyricSingerChangeHandler, string name, Action? postProcess = null) : base(name) { var lyrics = beatmap.SelectedHitObjects.OfType().ToArray(); // todo: should be able to support the sub-singer. var karaokeBeatmap = EditorBeatmapUtils.GetPlayableBeatmap(beatmap); var singers = karaokeBeatmap.SingerInfo.GetAllSingers(); Items = singers.Select(singer => new OsuMenuItem(singer.Name, anySingerInLyric(singer) ? MenuItemType.Highlighted : MenuItemType.Standard, () => { // if only one lyric if (allSingerInLyric(singer)) { lyricSingerChangeHandler.Remove(singer); } else { lyricSingerChangeHandler.Add(singer); } postProcess?.Invoke(); })).ToList(); bool anySingerInLyric(Singer singer) => lyrics.Any(lyric => LyricUtils.ContainsSinger(lyric, singer)); bool allSingerInLyric(Singer singer) => lyrics.All(lyric => LyricUtils.ContainsSinger(lyric, singer)); } } ================================================ FILE: osu.Game.Rulesets.Karaoke/Edit/Components/Cursor/TimeTagTooltip.cs ================================================ // Copyright (c) andy840119 . Licensed under the GPL Licence. // See the LICENCE file in the repository root for full licence text. using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Rulesets.Karaoke.Graphics.Cursor; using osu.Game.Rulesets.Karaoke.Objects; using osu.Game.Rulesets.Karaoke.Objects.Utils; using osu.Game.Rulesets.Karaoke.Utils; using osuTK; namespace osu.Game.Rulesets.Karaoke.Edit.Components.Cursor; public partial class TimeTagTooltip : BackgroundToolTip { private const int time_display_height = 25; private Box background = null!; private readonly OsuSpriteText trackTimer; private readonly OsuSpriteText index; private readonly OsuSpriteText indexState; protected override float ContentPadding => 5; public TimeTagTooltip() { Child = new GridContainer { AutoSizeAxes = Axes.Both, RowDimensions = new[] { new Dimension(GridSizeMode.Absolute, time_display_height), new Dimension(GridSizeMode.Absolute, BORDER), new Dimension(GridSizeMode.AutoSize), }, ColumnDimensions = new[] { new Dimension(GridSizeMode.AutoSize), }, Content = new[] { new Drawable[] { trackTimer = new OsuSpriteText { Font = OsuFont.GetFont(size: 21, fixedWidth: true), }, }, null, new Drawable[] { new FillFlowContainer { AutoSizeAxes = Axes.Both, Spacing = new Vector2(10), Children = new[] { index = new OsuSpriteText { Font = OsuFont.GetFont(size: 12), }, indexState = new OsuSpriteText { Font = OsuFont.GetFont(size: 12), }, }, }, }, }, }; } protected override Drawable SetBackground() { return background = new Box { RelativeSizeAxes = Axes.X, Height = time_display_height + BORDER, }; } private TimeTag? lastTimeTag; public override void SetContent(TimeTag timeTag) { if (timeTag == lastTimeTag) return; lastTimeTag = timeTag; trackTimer.Text = TimeTagUtils.FormattedString(timeTag); index.Text = $"Position: {timeTag.Index.Index}"; indexState.Text = TextIndexUtils.GetValueByState(timeTag.Index, "start", "end"); } [BackgroundDependencyLoader] private void load(OsuColour colours) { background.Colour = colours.Gray2; indexState.Colour = colours.Red; } } ================================================ FILE: osu.Game.Rulesets.Karaoke/Edit/Components/Menus/KaraokeEditorMenu.cs ================================================ // Copyright (c) andy840119 . Licensed under the GPL Licence. // See the LICENCE file in the repository root for full licence text. using osu.Framework.Graphics.UserInterface; using osu.Framework.Screens; using osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps; namespace osu.Game.Rulesets.Karaoke.Edit.Components.Menus; public class KaraokeEditorMenu : MenuItem { public KaraokeEditorMenu(IScreen screen, string text) : base(text, () => openKaraokeEditor(screen)) { } private static void openKaraokeEditor(IScreen screen) { screen.Push(new KaraokeBeatmapEditor()); } } ================================================ FILE: osu.Game.Rulesets.Karaoke/Edit/Components/Menus/KaraokeSkinEditorMenu.cs ================================================ // Copyright (c) andy840119 . Licensed under the GPL Licence. // See the LICENCE file in the repository root for full licence text. using osu.Framework.Graphics.UserInterface; using osu.Framework.Screens; using osu.Game.Rulesets.Karaoke.Screens.Skin; using osu.Game.Skinning; namespace osu.Game.Rulesets.Karaoke.Edit.Components.Menus; public class KaraokeSkinEditorMenu : MenuItem { public KaraokeSkinEditorMenu(IScreen screen, ISkin skin, string text) : base(text, () => openKaraokeSkin(screen, skin)) { } private static void openKaraokeSkin(IScreen screen, ISkin skin) { screen.Push(new KaraokeSkinEditor(skin)); } } ================================================ FILE: osu.Game.Rulesets.Karaoke/Edit/Components/Sprites/DrawableTextIndex.cs ================================================ // Copyright (c) andy840119 . Licensed under the GPL Licence. // See the LICENCE file in the repository root for full licence text. using osu.Framework.Graphics.Sprites; using osu.Game.Rulesets.Karaoke.Graphics.Shapes; using osu.Game.Rulesets.Karaoke.Utils; namespace osu.Game.Rulesets.Karaoke.Edit.Components.Sprites; public partial class DrawableTextIndex : RightTriangle { private TextIndex.IndexState state; public TextIndex.IndexState State { get => state; set { state = value; RightAngleDirection = TextIndexUtils.GetValueByState(state, TriangleRightAngleDirection.BottomLeft, TriangleRightAngleDirection.BottomRight); } } } ================================================ FILE: osu.Game.Rulesets.Karaoke/Edit/Components/Sprites/DrawableTimeTag.cs ================================================ // Copyright (c) andy840119 . Licensed under the GPL Licence. // See the LICENCE file in the repository root for full licence text. using System; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Sprites; using osu.Game.Graphics; using osu.Game.Rulesets.Karaoke.Edit.Components.Cursor; using osu.Game.Rulesets.Karaoke.Objects; using osu.Game.Rulesets.Karaoke.Utils; using osuTK.Graphics; namespace osu.Game.Rulesets.Karaoke.Edit.Components.Sprites; public sealed partial class DrawableTimeTag : CompositeDrawable, IHasCustomTooltip { private readonly IBindable bindableTime = new Bindable(); private readonly DrawableTextIndex drawableTextIndex; public Func? TimeTagColourFunc; public DrawableTimeTag() { InternalChild = drawableTextIndex = new DrawableTextIndex { RelativeSizeAxes = Axes.Both, }; } [BackgroundDependencyLoader] private void load(OsuColour colours) { bindableTime.BindValueChanged(x => { if (timeTag == null) return; drawableTextIndex.Colour = TimeTagColourFunc?.Invoke(timeTag, colours) ?? GetDefaultTimeTagColour(colours, timeTag); }, true); } private TimeTag? timeTag; public TimeTag? TimeTag { get => timeTag; set { if (timeTag == value) return; bindableTime.UnbindBindings(); timeTag = value; Alpha = timeTag == null ? 0 : 1; if (timeTag == null) return; bindableTime.BindTo(timeTag.TimeBindable); drawableTextIndex.State = timeTag.Index.State; } } public TimeTag TooltipContent => timeTag ?? new TimeTag(new TextIndex()); public ITooltip GetCustomTooltip() => new TimeTagTooltip(); public static Color4 GetDefaultTimeTagColour(OsuColour colours, TimeTag timeTag) { bool hasTime = timeTag.Time.HasValue; if (!hasTime) return colours.Gray7; return TextIndexUtils.GetValueByState(timeTag.Index, colours.Yellow, colours.YellowDarker); } } ================================================ FILE: osu.Game.Rulesets.Karaoke/Edit/Debugging/DebugBeatmapManager.cs ================================================ // Copyright (c) andy840119 . Licensed under the GPL Licence. // See the LICENCE file in the repository root for full licence text. using System; using System.Diagnostics; using System.IO; using System.Linq; using System.Reflection; using System.Text; using System.Threading; using osu.Framework.Allocation; using osu.Framework.Extensions; using osu.Framework.Graphics; using osu.Framework.Platform; using osu.Game.Beatmaps; using osu.Game.Database; using osu.Game.Extensions; using osu.Game.Overlays.Notifications; using osu.Game.Rulesets.Karaoke.Beatmaps.Formats; using osu.Game.Rulesets.Karaoke.Edit.Utils; using osu.Game.Screens.Edit; using osu.Game.Utils; using SharpCompress.Archives.Zip; using SharpCompress.Common; using SharpCompress.Writers.Zip; namespace osu.Game.Rulesets.Karaoke.Edit.Debugging; /// /// Save or export the beatmap for debug. /// Beatmap will be json format and might not be the final version. /// public partial class DebugBeatmapManager : Component { [Resolved] private Storage storage { get; set; } = null!; [Resolved] private EditorBeatmap beatmap { get; set; } = null!; [Resolved] private BeatmapManager beatmapManager { get; set; } = null!; /// /// Force save the beatmap with json format. /// Modified from /// public void OverrideTheBeatmapWithJsonFormat() { var karaokeBeatmap = EditorBeatmapUtils.GetPlayableBeatmap(beatmap); save(beatmap.BeatmapInfo, karaokeBeatmap); } /// /// Save the beatmap with json format to new difficulty. /// Modified from /// public void SaveToNewDifficulty() { var referenceWorkingBeatmap = beatmap; var targetBeatmapSet = beatmap.BeatmapInfo.BeatmapSet; if (targetBeatmapSet == null) { return; } // start modifiey var newBeatmap = EditorBeatmapUtils.GetPlayableBeatmap(beatmap); BeatmapInfo newBeatmapInfo; newBeatmap.BeatmapInfo = newBeatmapInfo = referenceWorkingBeatmap.BeatmapInfo.Clone(); // assign a new ID to the clone. newBeatmapInfo.ID = Guid.NewGuid(); // add "(copy)" suffix to difficulty name, and additionally ensure that it doesn't conflict with any other potentially pre-existing copies. newBeatmapInfo.DifficultyName = NamingUtils.GetNextBestName( targetBeatmapSet.Beatmaps.Select(b => b.DifficultyName), $"{newBeatmapInfo.DifficultyName} (copy)"); // clear the hash, as that's what is used to match .osu files with their corresponding realm beatmaps. newBeatmapInfo.Hash = string.Empty; // clear online properties. newBeatmapInfo.ResetOnlineInfo(); addDifficultyToSet(targetBeatmapSet, newBeatmap); return; void addDifficultyToSet(BeatmapSetInfo targetBeatmapSet, IBeatmap newBeatmap) { // populate circular beatmap set info <-> beatmap info references manually. // several places like `Save()` or `GetWorkingBeatmap()` // rely on them being freely traversable in both directions for correct operation. targetBeatmapSet.Beatmaps.Add(newBeatmap.BeatmapInfo); newBeatmap.BeatmapInfo.BeatmapSet = targetBeatmapSet; save(newBeatmap.BeatmapInfo, newBeatmap); } } /// /// Copied from /// /// /// /// private void save(BeatmapInfo beatmapInfo, IBeatmap beatmapContent) { // get realm from beatmapManager using reflection if (beatmapManager.GetType().GetProperty("Realm", BindingFlags.NonPublic | BindingFlags.Instance)?.GetValue(beatmapManager) is not RealmAccess realm) { throw new InvalidOperationException(); } var setInfo = beatmapInfo.BeatmapSet; Debug.Assert(setInfo != null); // Difficulty settings must be copied first due to the clone in `Beatmap<>.BeatmapInfo_Set`. // This should hopefully be temporary, assuming said clone is eventually removed. // Warning: The directionality here is important. Changes have to be copied *from* beatmapContent (which comes from editor and is being saved) // *to* the beatmapInfo (which is a database model and needs to receive values without the taiko slider velocity multiplier for correct operation). // CopyTo() will undo such adjustments, while CopyFrom() will not. beatmapContent.Difficulty.CopyTo(beatmapInfo.Difficulty); // All changes to metadata are made in the provided beatmapInfo, so this should be copied to the `IBeatmap` before encoding. beatmapContent.BeatmapInfo = beatmapInfo; // Since now this is a locally-modified beatmap, we also set all relevant flags to indicate this. // Importantly, the `ResetOnlineInfo()` call must happen before encoding, as online ID is encoded into the `.osu` file, // which influences the beatmap checksums. beatmapInfo.LastLocalUpdate = DateTimeOffset.Now; beatmapInfo.Status = BeatmapOnlineStatus.LocallyModified; beatmapInfo.ResetOnlineInfo(); realm.Write(r => { using var stream = new MemoryStream(); using (var sw = new StreamWriter(stream, Encoding.UTF8, 1024, true)) { sw.WriteLine(generateJsonBeatmap(beatmapContent)); } stream.Seek(0, SeekOrigin.Begin); // AddFile generally handles updating/replacing files, but this is a case where the filename may have also changed so let's delete for simplicity. var existingFileInfo = beatmapInfo.Path != null ? setInfo.GetFile(beatmapInfo.Path) : null; string targetFilename = createBeatmapFilenameFromMetadata(beatmapInfo); // ensure that two difficulties from the set don't point at the same beatmap file. if (setInfo.Beatmaps.Any(b => b.ID != beatmapInfo.ID && string.Equals(b.Path, targetFilename, StringComparison.OrdinalIgnoreCase))) throw new InvalidOperationException($"{setInfo.GetDisplayString()} already has a difficulty with the name of '{beatmapInfo.DifficultyName}'."); if (existingFileInfo != null) beatmapManager.DeleteFile(setInfo, existingFileInfo); string oldMd5Hash = beatmapInfo.MD5Hash; beatmapInfo.MD5Hash = stream.ComputeMD5Hash(); beatmapInfo.Hash = stream.ComputeSHA2Hash(); beatmapManager.AddFile(setInfo, stream, createBeatmapFilenameFromMetadata(beatmapInfo)); // beatmapManager.updateHashAndMarkDirty(setInfo); var method = typeof(BeatmapManager).GetMethod("updateHashAndMarkDirty", BindingFlags.Instance | BindingFlags.NonPublic); method?.Invoke(beatmapManager, new object?[] { setInfo }); var liveBeatmapSet = r.Find(setInfo.ID)!; setInfo.CopyChangesToRealm(liveBeatmapSet); liveBeatmapSet.Beatmaps.Single(b => b.ID == beatmapInfo.ID) .UpdateLocalScores(r); }); Debug.Assert(beatmapInfo.BeatmapSet != null); static string createBeatmapFilenameFromMetadata(BeatmapInfo beatmapInfo) { var metadata = beatmapInfo.Metadata; return $"{metadata.Artist} - {metadata.Title} ({metadata.Author.Username}) [{beatmapInfo.DifficultyName}].osu".GetValidFilename(); } } /// /// Export the json beatmap only. /// public void ExportToJson() { // note : this is for develop testing purpose. // will be removed eventually string beatmapName = string.IsNullOrEmpty(beatmap.Name) ? "[NoName]" : beatmap.Name; var exportStorage = storage.GetStorageForDirectory("json"); string filename = $"{beatmapName}.json"; using (var outputStream = exportStorage.GetStream(filename, FileAccess.Write, FileMode.Create)) using (var sw = new StreamWriter(outputStream)) { sw.WriteLine(generateJsonBeatmap(beatmap)); } exportStorage.PresentFileExternally(filename); } /// /// Export the whole beatmap with: /// 1. json format beatmap. /// 2. other resource like audio, background. /// public void ExportToJsonBeatmap() { // note : this is for develop testing purpose. // will be removed eventually string beatmapName = string.IsNullOrEmpty(beatmap.Name) ? "[NoName]" : beatmap.Name; var exportStorage = storage.GetStorageForDirectory("exports"); string filename = $"{beatmapName}.osz"; using (var outputStream = exportStorage.GetStream(filename, FileAccess.Write, FileMode.Create)) { string beatmapText = generateJsonBeatmap(beatmap); new KaraokeLegacyBeatmapExporter(storage, filename, beatmapText).ExportToStream(beatmap.BeatmapInfo.BeatmapSet!, outputStream, null); } exportStorage.PresentFileExternally(filename); } private static string generateJsonBeatmap(IBeatmap beatmap) { var encoder = new KaraokeJsonBeatmapEncoder(); var encodeBeatmap = new Beatmap { Difficulty = beatmap.Difficulty.Clone(), BeatmapInfo = beatmap.BeatmapInfo.Clone(), ControlPointInfo = beatmap.ControlPointInfo.DeepClone(), Breaks = beatmap.Breaks, HitObjects = beatmap.HitObjects.ToList(), }; encodeBeatmap.BeatmapInfo.BeatmapSet = new BeatmapSetInfo(); encodeBeatmap.BeatmapInfo.Metadata = new BeatmapMetadata { Title = "json beatmap", AudioFile = beatmap.Metadata.AudioFile, BackgroundFile = beatmap.Metadata.BackgroundFile, }; return encoder.Encode(encodeBeatmap); } private class KaraokeLegacyBeatmapExporter : LegacyBeatmapExporter { private readonly string filename; private readonly string content; public KaraokeLegacyBeatmapExporter(Storage storage, string filename, string content) : base(storage) { this.filename = filename; this.content = content; } public override void ExportToStream(BeatmapSetInfo model, Stream outputStream, ProgressNotification? notification, CancellationToken cancellationToken = new()) { // base.ExportModelTo(model, outputStream); using var zipArchive = ZipArchive.CreateArchive(); foreach (INamedFileUsage file in model.Files) { // do not export other osu beatmap. if (file.Filename.EndsWith(".osu", StringComparison.Ordinal)) continue; zipArchive.AddEntry(file.Filename, UserFileStorage.GetStream(file.File.GetStoragePath()), true); } // add the json file. using var jsonBeatmapStream = getJsonBeatmapStream(); zipArchive.AddEntry(filename, jsonBeatmapStream, true); zipArchive.SaveTo(outputStream, new ZipWriterOptions(CompressionType.Deflate)); } private Stream getJsonBeatmapStream() { var memoryStream = new MemoryStream(); var sw = new StreamWriter(memoryStream); sw.WriteLine(content); sw.Flush(); memoryStream.Position = 0; return memoryStream; } } } ================================================ FILE: osu.Game.Rulesets.Karaoke/Edit/DrawableKaraokeEditorRuleset.cs ================================================ // Copyright (c) andy840119 . Licensed under the GPL Licence. // See the LICENCE file in the repository root for full licence text. using System.Collections.Generic; using osu.Game.Beatmaps; using osu.Game.Rulesets.Karaoke.UI; using osu.Game.Rulesets.Karaoke.Utils; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.UI; using osu.Game.Rulesets.UI.Scrolling; namespace osu.Game.Rulesets.Karaoke.Edit; public partial class DrawableKaraokeEditorRuleset : DrawableKaraokeRuleset { public new IScrollingInfo ScrollingInfo => base.ScrollingInfo; protected override bool DisplayNotePlayfield => true; public DrawableKaraokeEditorRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList? mods) : base(ruleset, beatmap, mods) { } protected override Playfield CreatePlayfield() => new KaraokeEditorPlayfield(); public override PlayfieldAdjustmentContainer CreatePlayfieldAdjustmentContainer() { bool isCalledByComposer = StackTraceUtils.IsStackTraceContains("DrawableEditorRulesetWrapper"); if (isCalledByComposer) return new PlayfieldAdjustmentContainer(); return base.CreatePlayfieldAdjustmentContainer(); } } ================================================ FILE: osu.Game.Rulesets.Karaoke/Edit/EditorNotePlayfield.cs ================================================ // Copyright (c) andy840119 . Licensed under the GPL Licence. // See the LICENCE file in the repository root for full licence text. using System; using System.Collections.Generic; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Shapes; using osu.Game.Graphics; using osu.Game.Rulesets.Karaoke.UI.Components; using osu.Game.Rulesets.Karaoke.UI.Scrolling; using osuTK.Graphics; namespace osu.Game.Rulesets.Karaoke.Edit; public partial class EditorNotePlayfield : ScrollingNotePlayfield { private readonly SingerVoiceVisualization singerVoiceVisualization; public EditorNotePlayfield(int columns) : base(columns) { BackgroundLayer.AddRange(new Drawable[] { new Box { Depth = 1, Name = "Background", RelativeSizeAxes = Axes.Both, Colour = Color4.Black, Alpha = 0.5f, }, }); HitObjectArea.Add(singerVoiceVisualization = new SingerVoiceVisualization { Name = "Scoring Visualization", RelativeSizeAxes = Axes.Both, Alpha = 0.6f, }); } [BackgroundDependencyLoader] private void load() { // todo : load data from scoring manager. } public partial class SingerVoiceVisualization : VoiceVisualization> { protected override double GetTime(KeyValuePair frame) => frame.Key; protected override float GetPosition(KeyValuePair frame) => frame.Value ?? 0; private bool createNew = true; private double minAvailableTime; public void Add(KeyValuePair point) { // Start time should be largest and cannot be removed. double startTime = point.Key; if (startTime <= minAvailableTime) throw new ArgumentOutOfRangeException($"{nameof(startTime)} out of range."); minAvailableTime = startTime; if (!point.Value.HasValue) { // Next replay frame will create new path createNew = true; return; } if (createNew) { createNew = false; CreateNew(point); } else { Append(point); } } [BackgroundDependencyLoader] private void load(OsuColour colours) { Colour = colours.GrayF; } } } ================================================ FILE: osu.Game.Rulesets.Karaoke/Edit/Export/ExportLyricManager.cs ================================================ // Copyright (c) andy840119 . Licensed under the GPL Licence. // See the LICENCE file in the repository root for full licence text. using System.IO; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Platform; using osu.Game.Beatmaps; using osu.Game.Rulesets.Karaoke.Integration.Formats; using osu.Game.Screens.Edit; namespace osu.Game.Rulesets.Karaoke.Edit.Export; public partial class ExportLyricManager : Component { [Resolved] private Storage storage { get; set; } = null!; [Resolved] private EditorBeatmap beatmap { get; set; } = null!; public void ExportToKar() { var exportStorage = storage.GetStorageForDirectory("kar"); string filename = $"{beatmap.Name}.kar"; using (var outputStream = exportStorage.GetStream(filename, FileAccess.Write, FileMode.Create)) using (var sw = new StreamWriter(outputStream)) { var encoder = new KarEncoder(); sw.WriteLine(encoder.Encode(new Beatmap { HitObjects = beatmap.HitObjects.ToList(), })); } exportStorage.PresentFileExternally(filename); } public void ExportToText() { var exportStorage = storage.GetStorageForDirectory("text"); string filename = $"{beatmap.Name}.txt"; using (var outputStream = exportStorage.GetStream(filename, FileAccess.Write, FileMode.Create)) using (var sw = new StreamWriter(outputStream)) { var encoder = new LyricTextEncoder(); sw.WriteLine(encoder.Encode(new Beatmap { HitObjects = beatmap.HitObjects.ToList(), })); } exportStorage.PresentFileExternally(filename); } } ================================================ FILE: osu.Game.Rulesets.Karaoke/Edit/Generator/Beatmaps/BeatmapPropertyDetector.cs ================================================ // Copyright (c) andy840119 . Licensed under the GPL Licence. // See the LICENCE file in the repository root for full licence text. using osu.Game.Rulesets.Karaoke.Beatmaps; namespace osu.Game.Rulesets.Karaoke.Edit.Generator.Beatmaps; /// /// Base interface of the detector. /// /// /// public abstract class BeatmapPropertyDetector : PropertyDetector where TConfig : GeneratorConfig, new() { protected BeatmapPropertyDetector(TConfig config) : base(config) { } } ================================================ FILE: osu.Game.Rulesets.Karaoke/Edit/Generator/Beatmaps/BeatmapPropertyGenerator.cs ================================================ // Copyright (c) andy840119 . Licensed under the GPL Licence. // See the LICENCE file in the repository root for full licence text. using osu.Game.Rulesets.Karaoke.Beatmaps; namespace osu.Game.Rulesets.Karaoke.Edit.Generator.Beatmaps; /// /// Base interface of the auto-generator. /// /// /// public abstract class BeatmapPropertyGenerator : PropertyGenerator where TConfig : GeneratorConfig, new() { protected BeatmapPropertyGenerator(TConfig config) : base(config) { } } ================================================ FILE: osu.Game.Rulesets.Karaoke/Edit/Generator/Beatmaps/Pages/PageGenerator.cs ================================================ // Copyright (c) andy840119 . Licensed under the GPL Licence. // See the LICENCE file in the repository root for full licence text. using System; using System.Collections.Generic; using System.Linq; using osu.Framework.Localisation; using osu.Game.Beatmaps; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Karaoke.Beatmaps; using osu.Game.Rulesets.Karaoke.Beatmaps.Metadatas; using osu.Game.Rulesets.Karaoke.Edit.Checks; using osu.Game.Rulesets.Karaoke.Objects; using osu.Game.Rulesets.Karaoke.Utils; using osu.Game.Tests.Beatmaps; namespace osu.Game.Rulesets.Karaoke.Edit.Generator.Beatmaps.Pages; public class PageGenerator : BeatmapPropertyGenerator { public PageGenerator(PageGeneratorConfig config) : base(config) { } protected override LocalisableString? GetInvalidMessageFromItem(KaraokeBeatmap item) { var lyrics = item.HitObjects.OfType().ToArray(); if (lyrics.Length < 1) return "There's not lyric in the beatmap."; var timeTagChecker = new CheckLyricTimeTag(); var invalidIssues = timeTagChecker.Run(getContext(item)); if (invalidIssues.Any()) return "Should not have any time-tag related issues"; return null; } private static BeatmapVerifierContext getContext(IBeatmap beatmap) => new(beatmap, new TestWorkingBeatmap(beatmap)); protected override Page[] GenerateFromItem(KaraokeBeatmap item) { if (Config.MinTime.Value < CheckBeatmapPageInfo.MIN_INTERVAL || Config.MaxTime.Value > CheckBeatmapPageInfo.MAX_INTERVAL) throw new InvalidOperationException("Interval time should be validate."); var existPages = Config.ClearExistPages.Value ? Array.Empty() : item.PageInfo.SortedPages.ToArray(); var lyricTimingInfos = item.HitObjects.OfType() .Where(x => x.TimeValid) .Select(x => new LyricTimingInfo { StartTime = x.StartTime, EndTime = x.EndTime, }) .OrderBy(x => x).ToList(); if (lyricTimingInfos.Count == 0) return existPages; return calculatePageByLyrics(lyricTimingInfos, existPages).ToArray(); } private IEnumerable calculatePageByLyrics(IReadOnlyList lyricTimingInfos, IReadOnlyList existPages) { double currentTime; // create first page with it's start time. yield return createReturnPage(existPages.FirstOrDefault()?.Time ?? lyricTimingInfos.FirstOrDefault().StartTime); for (int i = 0; i < lyricTimingInfos.Count; i++) { bool lsLast = i == lyricTimingInfos.Count - 1; var currentLyricTimingInfo = lyricTimingInfos[i]; LyricTimingInfo? nextLyricTimingInfo = lsLast ? null : lyricTimingInfos[i + 1]; bool getAverageTimeWithNextLyric = nextLyricTimingInfo != null && nextLyricTimingInfo.Value.StartTime > currentLyricTimingInfo.EndTime; double expectedEndTime = getAverageTimeWithNextLyric ? (currentLyricTimingInfo.EndTime + nextLyricTimingInfo!.Value.StartTime) / 2 : currentLyricTimingInfo.EndTime; while (currentTime < expectedEndTime) { if (expectedEndTime - currentTime < Config.MinTime.Value && getAverageTimeWithNextLyric) { break; } if (expectedEndTime - currentTime > Config.MaxTime.Value) { yield return createReturnPage(currentTime + Config.MaxTime.Value); } else { yield return createReturnPage(expectedEndTime); } } } Page createReturnPage(double time) { currentTime = time; return new Page { Time = time }; } } private readonly struct LyricTimingInfo : IComparable { public double StartTime { get; init; } public double EndTime { get; init; } public int CompareTo(LyricTimingInfo other) { return ComparableUtils.CompareByProperty(this, other, t => t.StartTime, t => t.EndTime); } } } ================================================ FILE: osu.Game.Rulesets.Karaoke/Edit/Generator/Beatmaps/Pages/PageGeneratorConfig.cs ================================================ // Copyright (c) andy840119 . Licensed under the GPL Licence. // See the LICENCE file in the repository root for full licence text. using osu.Framework.Bindables; using osu.Game.Rulesets.Karaoke.Edit.Checks; namespace osu.Game.Rulesets.Karaoke.Edit.Generator.Beatmaps.Pages; public class PageGeneratorConfig : GeneratorConfig { [ConfigSource("Min time", "Min interval between pages.")] public Bindable MinTime { get; } = new BindableDouble(CheckBeatmapPageInfo.MIN_INTERVAL) { MinValue = CheckBeatmapPageInfo.MIN_INTERVAL, MaxValue = CheckBeatmapPageInfo.MAX_INTERVAL, }; [ConfigSource("Max time", "Max interval between pages.")] public Bindable MaxTime { get; } = new BindableDouble(CheckBeatmapPageInfo.MAX_INTERVAL) { MinValue = CheckBeatmapPageInfo.MIN_INTERVAL, MaxValue = CheckBeatmapPageInfo.MAX_INTERVAL, }; [ConfigSource("Clear the exist page.", "Clear the exist page after generated.")] public Bindable ClearExistPages { get; } = new BindableBool(); } ================================================ FILE: osu.Game.Rulesets.Karaoke/Edit/Generator/ConfigCategoryAttribute.cs ================================================ // Copyright (c) andy840119 . Licensed under the GPL Licence. // See the LICENCE file in the repository root for full licence text. using System; using osu.Framework.Localisation; namespace osu.Game.Rulesets.Karaoke.Edit.Generator; public class ConfigCategoryAttribute : Attribute, IEquatable { public LocalisableString Category { get; } public ConfigCategoryAttribute(string category) { Category = category; } public bool Equals(ConfigCategoryAttribute? other) { return Category == other?.Category; } public override bool Equals(object? obj) { return obj switch { ConfigCategoryAttribute category => Equals(category), _ => false, }; } public override int GetHashCode() { return HashCode.Combine(base.GetHashCode(), Category); } } ================================================ FILE: osu.Game.Rulesets.Karaoke/Edit/Generator/ConfigSourceAttribute.cs ================================================ // Copyright (c) andy840119 . Licensed under the GPL Licence. // See the LICENCE file in the repository root for full licence text. using System; using osu.Game.Configuration; namespace osu.Game.Rulesets.Karaoke.Edit.Generator; public class ConfigSourceAttribute : SettingSourceAttribute { public ConfigSourceAttribute(Type declaringType, string label, string? description = null) : base(declaringType, label, description) { } public ConfigSourceAttribute(string? label, string? description = null) : base(label, description) { } public ConfigSourceAttribute(Type declaringType, string label, string description, int orderPosition) : base(declaringType, label, description, orderPosition) { } public ConfigSourceAttribute(string label, string description, int orderPosition) : base(label, description, orderPosition) { } } ================================================ FILE: osu.Game.Rulesets.Karaoke/Edit/Generator/DetectorNotSupportedException.cs ================================================ // Copyright (c) andy840119 . Licensed under the GPL Licence. // See the LICENCE file in the repository root for full licence text. using System; namespace osu.Game.Rulesets.Karaoke.Edit.Generator; public class DetectorNotSupportedException : NotSupportedException { public DetectorNotSupportedException() : base("Cannot generate the property due to have some invalid fields, please make sure that run the CanDetect() first.") { } } ================================================ FILE: osu.Game.Rulesets.Karaoke/Edit/Generator/GeneratorConfig.cs ================================================ // Copyright (c) andy840119 . Licensed under the GPL Licence. // See the LICENCE file in the repository root for full licence text. namespace osu.Game.Rulesets.Karaoke.Edit.Generator; public abstract class GeneratorConfig; ================================================ FILE: osu.Game.Rulesets.Karaoke/Edit/Generator/GeneratorConfigExtension.cs ================================================ // Copyright (c) andy840119 . Licensed under the GPL Licence. // See the LICENCE file in the repository root for full licence text. using System.Collections.Generic; using System.Linq; using System.Reflection; namespace osu.Game.Rulesets.Karaoke.Edit.Generator; public static class GeneratorConfigExtension { public static IDictionary GetOrderedConfigsSourceDictionary(this GeneratorConfig config, ConfigCategoryAttribute defaultCategory) { return GetOrderedConfigsSourceProperties(config) .GroupBy(attr => attr.Item2) .ToDictionary( group => group.Key ?? defaultCategory, group => group.Select(x => (x.Item1, x.Item3)).ToArray() ); } public static ICollection<(ConfigSourceAttribute, ConfigCategoryAttribute?, PropertyInfo)> GetOrderedConfigsSourceProperties(this GeneratorConfig config) => GetConfigSourceProperties(config) .OrderBy(attr => attr.Item1) .ToArray(); public static IEnumerable<(ConfigSourceAttribute, ConfigCategoryAttribute?, PropertyInfo)> GetConfigSourceProperties(this GeneratorConfig config) { var type = config.GetType(); foreach (var property in type.GetProperties(BindingFlags.GetProperty | BindingFlags.Public | BindingFlags.Instance)) { var configSourceAttribute = property.GetCustomAttribute(true); var configCategoryAttribute = property.GetCustomAttribute(true); if (configSourceAttribute == null) continue; yield return (configSourceAttribute, configCategoryAttribute, property); } } } ================================================ FILE: osu.Game.Rulesets.Karaoke/Edit/Generator/GeneratorNotSupportedException.cs ================================================ // Copyright (c) andy840119 . Licensed under the GPL Licence. // See the LICENCE file in the repository root for full licence text. using System; namespace osu.Game.Rulesets.Karaoke.Edit.Generator; public class GeneratorNotSupportedException : NotSupportedException { public GeneratorNotSupportedException() : base("Cannot generate the property due to have some invalid fields, please make sure that run the CanGenerate() first.") { } } ================================================ FILE: osu.Game.Rulesets.Karaoke/Edit/Generator/GeneratorSelector.cs ================================================ // Copyright (c) andy840119 . Licensed under the GPL Licence. // See the LICENCE file in the repository root for full licence text. using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using osu.Game.Rulesets.Karaoke.Configuration; using osu.Game.Rulesets.Karaoke.Utils; namespace osu.Game.Rulesets.Karaoke.Edit.Generator; /// /// Able to select generator by property. /// /// The item that want to generate the property. /// The property in the item that will be generated. /// The config. public abstract class GeneratorSelector : PropertyGenerator where TBaseConfig : GeneratorConfig { private Dictionary, Lazy>> generators { get; } = new(); private readonly KaraokeRulesetEditGeneratorConfigManager generatorConfigManager; protected GeneratorSelector(KaraokeRulesetEditGeneratorConfigManager generatorConfigManager) { this.generatorConfigManager = generatorConfigManager; } protected void RegisterGenerator(Func selector) where TGenerator : PropertyGenerator where TConfig : TBaseConfig, new() { generators.Add(selector, new Lazy>(() => { var config = generatorConfigManager.Get(); return ActivatorUtils.CreateInstance(config); })); } protected bool TryGetGenerator(TItem item, [MaybeNullWhen(false)] out PropertyGenerator generator) { foreach (var (func, propertyGenerator) in generators) { if (!func(item)) continue; generator = propertyGenerator.Value; return true; } generator = null; return false; } } ================================================ FILE: osu.Game.Rulesets.Karaoke/Edit/Generator/Lyrics/Language/LanguageDetector.cs ================================================ // Copyright (c) andy840119 . Licensed under the GPL Licence. // See the LICENCE file in the repository root for full licence text. using System; using System.Globalization; using System.Linq; using osu.Framework.Localisation; using osu.Game.Rulesets.Karaoke.Objects; namespace osu.Game.Rulesets.Karaoke.Edit.Generator.Lyrics.Language; public class LanguageDetector : LyricPropertyDetector { private readonly LanguageDetection.LanguageDetector detector = new(); public LanguageDetector(LanguageDetectorConfig config) : base(config) { var targetLanguages = config.AcceptLanguages.Value ?? Array.Empty(); if (targetLanguages.Any()) { detector.AddLanguages(targetLanguages.Select(x => x.Name).ToArray()); } else { detector.AddAllLanguages(); } } protected override LocalisableString? GetInvalidMessageFromItem(Lyric item) { if (string.IsNullOrWhiteSpace(item.Text)) return "Lyric should not be empty."; return null; } protected override CultureInfo? DetectFromItem(Lyric item) { var result = detector.DetectAll(item.Text); string? languageCode = result.FirstOrDefault()?.Language; return languageCode == null ? null : new CultureInfo(languageCode); } } ================================================ FILE: osu.Game.Rulesets.Karaoke/Edit/Generator/Lyrics/Language/LanguageDetectorConfig.cs ================================================ // Copyright (c) andy840119 . Licensed under the GPL Licence. // See the LICENCE file in the repository root for full licence text. using System; using System.Globalization; using osu.Framework.Bindables; namespace osu.Game.Rulesets.Karaoke.Edit.Generator.Lyrics.Language; public class LanguageDetectorConfig : GeneratorConfig { [ConfigSource("Accept languages", "All accepted languages.")] public Bindable AcceptLanguages { get; } = new(Array.Empty()); } ================================================ FILE: osu.Game.Rulesets.Karaoke/Edit/Generator/Lyrics/LyricGeneratorSelector.cs ================================================ // Copyright (c) andy840119 . Licensed under the GPL Licence. // See the LICENCE file in the repository root for full licence text. using System.Collections.Generic; using System.Globalization; using osu.Framework.Localisation; using osu.Game.Rulesets.Karaoke.Configuration; using osu.Game.Rulesets.Karaoke.Objects; namespace osu.Game.Rulesets.Karaoke.Edit.Generator.Lyrics; public abstract class LyricGeneratorSelector : GeneratorSelector where TBaseConfig : GeneratorConfig { protected LyricGeneratorSelector(KaraokeRulesetEditGeneratorConfigManager generatorConfigManager) : base(generatorConfigManager) { } protected void RegisterGenerator(CultureInfo cultureInfo) where TGenerator : PropertyGenerator where TConfig : TBaseConfig, new() { RegisterGenerator(x => EqualityComparer.Default.Equals(x.Language, cultureInfo)); } protected override LocalisableString? GetInvalidMessageFromItem(Lyric item) { if (item.Language == null) return "Oops, language is missing."; if (string.IsNullOrWhiteSpace(item.Text)) return "Should have the text in the lyric"; if (!TryGetGenerator(item, out var generator)) return "Sorry, the language of lyric is not supported yet."; return generator.GetInvalidMessage(item); } protected override TProperty GenerateFromItem(Lyric item) { if (item.Language == null) throw new GeneratorNotSupportedException(); if (!TryGetGenerator(item, out var generator)) throw new GeneratorNotSupportedException(); return generator.Generate(item); } } ================================================ FILE: osu.Game.Rulesets.Karaoke/Edit/Generator/Lyrics/LyricPropertyDetector.cs ================================================ // Copyright (c) andy840119 . Licensed under the GPL Licence. // See the LICENCE file in the repository root for full licence text. using osu.Game.Rulesets.Karaoke.Objects; namespace osu.Game.Rulesets.Karaoke.Edit.Generator.Lyrics; /// /// Base interface of the detector. /// /// /// public abstract class LyricPropertyDetector : PropertyDetector where TConfig : GeneratorConfig, new() { protected LyricPropertyDetector(TConfig config) : base(config) { } } ================================================ FILE: osu.Game.Rulesets.Karaoke/Edit/Generator/Lyrics/LyricPropertyGenerator.cs ================================================ // Copyright (c) andy840119 . Licensed under the GPL Licence. // See the LICENCE file in the repository root for full licence text. using osu.Game.Rulesets.Karaoke.Objects; namespace osu.Game.Rulesets.Karaoke.Edit.Generator.Lyrics; /// /// Base interface of the auto-generator. /// /// /// public abstract class LyricPropertyGenerator : PropertyGenerator where TConfig : GeneratorConfig, new() { protected LyricPropertyGenerator(TConfig config) : base(config) { } } ================================================ FILE: osu.Game.Rulesets.Karaoke/Edit/Generator/Lyrics/Notes/NoteGenerator.cs ================================================ // Copyright (c) andy840119 . Licensed under the GPL Licence. // See the LICENCE file in the repository root for full licence text. using System.Collections.Generic; using System.Linq; using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Localisation; using osu.Game.Rulesets.Karaoke.Extensions; using osu.Game.Rulesets.Karaoke.Objects; using osu.Game.Rulesets.Karaoke.Objects.Utils; using osu.Game.Rulesets.Karaoke.Utils; namespace osu.Game.Rulesets.Karaoke.Edit.Generator.Lyrics.Notes; public class NoteGenerator : LyricPropertyGenerator { public NoteGenerator(NoteGeneratorConfig config) : base(config) { } protected override LocalisableString? GetInvalidMessageFromItem(Lyric item) { var timeTags = item.TimeTags; if (item.TimeTags.Count < 2) return "Sorry, lyric must have at least two time-tags."; if (timeTags.Any(x => x.Time == null)) return "All time-tag should have the time."; return null; } protected override Note[] GenerateFromItem(Lyric item) { var timeTags = TimeTagsUtils.ToTimeBasedDictionary(item.TimeTags); var notes = new List(); foreach (var timeTag in timeTags) { // should not continue if if (timeTags.LastOrDefault().Key == timeTag.Key) break; (double _, var textIndex) = timeTag; (double _, var nextTextIndex) = timeTags.GetNext(timeTag); int gapIndex = TextIndexUtils.ToGapIndex(textIndex); int nextGapIndex = TextIndexUtils.ToGapIndex(nextTextIndex); // prevent reverse time-tag to generate the note. if (gapIndex >= nextGapIndex) continue; int timeTagIndex = timeTags.IndexOf(timeTag); string text = item.Text[gapIndex..nextGapIndex]; string? ruby = item.RubyTags?.Where(x => x.StartIndex == gapIndex && x.EndIndex == nextGapIndex - 1).FirstOrDefault()?.Text; if (!string.IsNullOrEmpty(text)) { notes.Add(new Note { Text = text, RubyText = ruby, ReferenceLyricId = item.ID, // technically this property should be assigned by beatmap processor, but should be OK to assign here for testing purpose. ReferenceLyric = item, ReferenceTimeTagIndex = timeTagIndex, }); } } return notes.ToArray(); } } ================================================ FILE: osu.Game.Rulesets.Karaoke/Edit/Generator/Lyrics/Notes/NoteGeneratorConfig.cs ================================================ // Copyright (c) andy840119 . Licensed under the GPL Licence. // See the LICENCE file in the repository root for full licence text. namespace osu.Game.Rulesets.Karaoke.Edit.Generator.Lyrics.Notes; public class NoteGeneratorConfig : GeneratorConfig; ================================================ FILE: osu.Game.Rulesets.Karaoke/Edit/Generator/Lyrics/ReferenceLyric/ReferenceLyricDetector.cs ================================================ // Copyright (c) andy840119 . Licensed under the GPL Licence. // See the LICENCE file in the repository root for full licence text. using System; using System.Collections.Generic; using System.Linq; using osu.Framework.Localisation; using osu.Game.Rulesets.Karaoke.Objects; using osu.Game.Rulesets.Karaoke.Utils; namespace osu.Game.Rulesets.Karaoke.Edit.Generator.Lyrics.ReferenceLyric; public class ReferenceLyricDetector : LyricPropertyDetector { private readonly Lyric[] lyrics; public ReferenceLyricDetector(IEnumerable lyrics, ReferenceLyricDetectorConfig config) : base(config) { this.lyrics = lyrics.ToArray(); } protected override LocalisableString? GetInvalidMessageFromItem(Lyric item) { var referencedLyric = getReferenceLyric(item); if (referencedLyric == null) return "There's no matched lyric."; return null; } protected override Lyric? DetectFromItem(Lyric item) { var referencedLyric = getReferenceLyric(item); if (referencedLyric == null) return null; // prevent first lyric(referenced lyric) reference by other lyric. if (referencedLyric.Order > item.Order) return null; return referencedLyric; } private Lyric? getReferenceLyric(Lyric lyric) { if (!lyrics.Contains(lyric)) throw new InvalidOperationException(); return lyrics.Except(new[] { lyric }).OrderBy(x => x.Order).FirstOrDefault(x => canBeReferenced(lyric, x)); } private bool canBeReferenced(Lyric lyric, Lyric referencedLyric) { string lyricText = lyric.Text; string referencedLyricText = referencedLyric.Text; if (lyricText == referencedLyricText) return true; if (!Config.IgnorePrefixAndPostfixSymbol.Value) return false; // check if contains intersect part between two lyrics. if (!lyricText.Contains(referencedLyricText) && !referencedLyricText.Contains(lyricText)) return false; // check if except part are all symbols. var except1 = lyricText.Except(referencedLyricText); var except2 = referencedLyricText.Except(lyricText); return allCharsEmptyOrSymbol(except1) && allCharsEmptyOrSymbol(except2); static bool allCharsEmptyOrSymbol(IEnumerable chars) => chars.All(x => CharUtils.IsSpacing(x) || CharUtils.IsAsciiSymbol(x)); } } ================================================ FILE: osu.Game.Rulesets.Karaoke/Edit/Generator/Lyrics/ReferenceLyric/ReferenceLyricDetectorConfig.cs ================================================ // Copyright (c) andy840119 . Licensed under the GPL Licence. // See the LICENCE file in the repository root for full licence text. using osu.Framework.Bindables; namespace osu.Game.Rulesets.Karaoke.Edit.Generator.Lyrics.ReferenceLyric; public class ReferenceLyricDetectorConfig : GeneratorConfig { [ConfigSource("Ruby as Katakana", "Ruby as Katakana.")] public Bindable IgnorePrefixAndPostfixSymbol { get; } = new BindableBool(true); } ================================================ FILE: osu.Game.Rulesets.Karaoke/Edit/Generator/Lyrics/Romanisation/Ja/JaRomanisationGenerator.cs ================================================ // Copyright (c) andy840119 . Licensed under the GPL Licence. // See the LICENCE file in the repository root for full licence text. using System; using System.Collections.Generic; using System.IO; using System.Linq; using Lucene.Net.Analysis; using Lucene.Net.Analysis.Ja; using Lucene.Net.Analysis.TokenAttributes; using osu.Framework.Graphics.Sprites; using osu.Game.Rulesets.Karaoke.Extensions; using osu.Game.Rulesets.Karaoke.Objects; using osu.Game.Rulesets.Karaoke.Utils; namespace osu.Game.Rulesets.Karaoke.Edit.Generator.Lyrics.Romanisation.Ja; public class JaRomanisationGenerator : RomanisationGenerator { private readonly Analyzer analyzer; public JaRomanisationGenerator(JaRomanisationGeneratorConfig config) : base(config) { analyzer = Analyzer.NewAnonymous((fieldName, reader) => { Tokenizer tokenizer = new JapaneseTokenizer(reader, null, true, JapaneseTokenizerMode.SEARCH); return new TokenStreamComponents(tokenizer, new JapaneseReadingFormFilter(tokenizer, false)); }); } protected override IReadOnlyDictionary GenerateFromItem(Lyric item) { // Tokenize the text string text = item.Text; var tokenStream = analyzer.GetTokenStream("dummy", new StringReader(text)); // get the processing tags. var parameters = generateParameters(text, tokenStream, Config).ToArray(); // then, trying to mapping them with the time-tags. return Convert(item.TimeTags, parameters); } private static IEnumerable generateParameters(string text, TokenStream tokenStream, JaRomanisationGeneratorConfig config) { // Reset the stream and convert all result tokenStream.Reset(); while (true) { // Read next token tokenStream.ClearAttributes(); tokenStream.IncrementToken(); // Get result and offset var charTermAttribute = tokenStream.GetAttribute(); var offsetAttribute = tokenStream.GetAttribute(); // Get parsed result, result is Katakana. string katakana = charTermAttribute.ToString(); if (string.IsNullOrEmpty(katakana)) break; string parentText = text[offsetAttribute.StartOffset..offsetAttribute.EndOffset]; bool fromKanji = JpStringUtils.ToKatakana(katakana) != JpStringUtils.ToKatakana(parentText); // Convert to romanised syllable. string romanisedSyllable = JpStringUtils.ToRomaji(katakana); if (config.Uppercase.Value) romanisedSyllable = romanisedSyllable.ToUpper(); // Make tag yield return new RomanisationGeneratorParameter { FromKanji = fromKanji, StartIndex = offsetAttribute.StartOffset, EndIndex = offsetAttribute.EndOffset - 1, RomanisedSyllable = romanisedSyllable, }; } // Dispose tokenStream.End(); tokenStream.Dispose(); } internal static IReadOnlyDictionary Convert(IList timeTags, IList parameters) { var group = createGroup(timeTags, parameters); return group.ToDictionary(k => k.Key, x => { bool isFirst = timeTags.IndexOf(x.Key) == 0; // todo: use better to mark the first syllable. string romanisedSyllable = string.Join(" ", x.Value.Select(r => r.RomanisedSyllable)); return new RomanisationGenerateResult { FirstSyllable = isFirst, RomanisedSyllable = romanisedSyllable, }; }); static IReadOnlyDictionary> createGroup(IList timeTags, IList parameters) { var dictionary = timeTags.ToDictionary(x => x, v => new List()); int processedIndex = 0; foreach (var (timeTag, list) in dictionary) { while (processedIndex < parameters.Count && isTimeTagInRange(timeTags, timeTag, parameters[processedIndex])) { list.Add(parameters[processedIndex]); processedIndex++; } } if (processedIndex < parameters.Count - 1) throw new InvalidOperationException("Still have romanisations that haven't process"); return dictionary; } static bool isTimeTagInRange(IEnumerable timeTags, TimeTag currentTimeTag, RomanisationGeneratorParameter parameter) { if (currentTimeTag.Index.State == TextIndex.IndexState.End) return false; int romanisationIndex = parameter.StartIndex; var nextTimeTag = timeTags.GetNextMatch(currentTimeTag, x => x.Index > currentTimeTag.Index && x.Index.State == TextIndex.IndexState.Start); if (nextTimeTag == null) return romanisationIndex >= currentTimeTag.Index.Index; return romanisationIndex >= currentTimeTag.Index.Index && romanisationIndex < nextTimeTag.Index.Index; } } internal class RomanisationGeneratorParameter { public bool FromKanji { get; set; } public int StartIndex { get; set; } public int EndIndex { get; set; } public string RomanisedSyllable { get; set; } = string.Empty; } } ================================================ FILE: osu.Game.Rulesets.Karaoke/Edit/Generator/Lyrics/Romanisation/Ja/JaRomanisationGeneratorConfig.cs ================================================ // Copyright (c) andy840119 . Licensed under the GPL Licence. // See the LICENCE file in the repository root for full licence text. using osu.Framework.Bindables; namespace osu.Game.Rulesets.Karaoke.Edit.Generator.Lyrics.Romanisation.Ja; public class JaRomanisationGeneratorConfig : RomanisationGeneratorConfig { [ConfigSource("Uppercase", "Export romanisation with uppercase.")] public Bindable Uppercase { get; } = new BindableBool(); } ================================================ FILE: osu.Game.Rulesets.Karaoke/Edit/Generator/Lyrics/Romanisation/RomanisationGenerateResult.cs ================================================ // Copyright (c) andy840119 . Licensed under the GPL Licence. // See the LICENCE file in the repository root for full licence text. namespace osu.Game.Rulesets.Karaoke.Edit.Generator.Lyrics.Romanisation; public struct RomanisationGenerateResult { public bool FirstSyllable { get; set; } public string? RomanisedSyllable { get; set; } } ================================================ FILE: osu.Game.Rulesets.Karaoke/Edit/Generator/Lyrics/Romanisation/RomanisationGenerator.cs ================================================ // Copyright (c) andy840119 . Licensed under the GPL Licence. // See the LICENCE file in the repository root for full licence text. using System.Collections.Generic; using System.Linq; using osu.Framework.Graphics.Sprites; using osu.Framework.Localisation; using osu.Game.Rulesets.Karaoke.Objects; namespace osu.Game.Rulesets.Karaoke.Edit.Generator.Lyrics.Romanisation; public abstract class RomanisationGenerator : LyricPropertyGenerator, TConfig> where TConfig : RomanisationGeneratorConfig, new() { protected RomanisationGenerator(TConfig config) : base(config) { } protected override LocalisableString? GetInvalidMessageFromItem(Lyric item) { if (string.IsNullOrWhiteSpace(item.Text)) return "Lyric should not be empty."; if (item.TimeTags.FirstOrDefault()?.Index != new TextIndex()) return "Should have at least one index and that index should at the start of the lyric."; return null; } } ================================================ FILE: osu.Game.Rulesets.Karaoke/Edit/Generator/Lyrics/Romanisation/RomanisationGeneratorConfig.cs ================================================ // Copyright (c) andy840119 . Licensed under the GPL Licence. // See the LICENCE file in the repository root for full licence text. namespace osu.Game.Rulesets.Karaoke.Edit.Generator.Lyrics.Romanisation; public abstract class RomanisationGeneratorConfig : GeneratorConfig; ================================================ FILE: osu.Game.Rulesets.Karaoke/Edit/Generator/Lyrics/Romanisation/RomanisationGeneratorSelector.cs ================================================ // Copyright (c) andy840119 . Licensed under the GPL Licence. // See the LICENCE file in the repository root for full licence text. using System.Collections.Generic; using System.Globalization; using osu.Game.Rulesets.Karaoke.Configuration; using osu.Game.Rulesets.Karaoke.Edit.Generator.Lyrics.Romanisation.Ja; using osu.Game.Rulesets.Karaoke.Objects; namespace osu.Game.Rulesets.Karaoke.Edit.Generator.Lyrics.Romanisation; public class RomanisationGeneratorSelector : LyricGeneratorSelector, RomanisationGeneratorConfig> { public RomanisationGeneratorSelector(KaraokeRulesetEditGeneratorConfigManager generatorConfigManager) : base(generatorConfigManager) { RegisterGenerator(new CultureInfo(17)); RegisterGenerator(new CultureInfo(1041)); } } ================================================ FILE: osu.Game.Rulesets.Karaoke/Edit/Generator/Lyrics/RubyTags/Ja/JaRubyTagGenerator.cs ================================================ // Copyright (c) andy840119 . Licensed under the GPL Licence. // See the LICENCE file in the repository root for full licence text. using System.Collections.Generic; using System.IO; using System.Linq; using Lucene.Net.Analysis; using Lucene.Net.Analysis.Ja; using Lucene.Net.Analysis.TokenAttributes; using osu.Game.Rulesets.Karaoke.Objects; using osu.Game.Rulesets.Karaoke.Utils; namespace osu.Game.Rulesets.Karaoke.Edit.Generator.Lyrics.RubyTags.Ja; public class JaRubyTagGenerator : RubyTagGenerator { private readonly Analyzer analyzer; public JaRubyTagGenerator(JaRubyTagGeneratorConfig config) : base(config) { analyzer = Analyzer.NewAnonymous((fieldName, reader) => { Tokenizer tokenizer = new JapaneseTokenizer(reader, null, true, JapaneseTokenizerMode.SEARCH); return new TokenStreamComponents(tokenizer, new JapaneseReadingFormFilter(tokenizer, false)); }); } protected override RubyTag[] GenerateFromItem(Lyric item) { // Tokenize the text string text = item.Text; var tokenStream = analyzer.GetTokenStream("dummy", new StringReader(text)); return getProcessingRubyTags(text, tokenStream, Config).ToArray(); } private static IEnumerable getProcessingRubyTags(string text, TokenStream tokenStream, JaRubyTagGeneratorConfig config) { // Reset the stream and convert all result tokenStream.Reset(); while (true) { // Read next token tokenStream.ClearAttributes(); tokenStream.IncrementToken(); // Get result and offset var charTermAttribute = tokenStream.GetAttribute(); var offsetAttribute = tokenStream.GetAttribute(); // Get parsed result, result is Katakana. string katakana = charTermAttribute.ToString(); if (string.IsNullOrEmpty(katakana)) break; // Convert to Hiragana as default. string hiragana = JpStringUtils.ToHiragana(katakana); if (!config.EnableDuplicatedRuby.Value) { // Not add duplicated ruby if same as parent. string parentText = text[offsetAttribute.StartOffset..offsetAttribute.EndOffset]; if (parentText == katakana || parentText == hiragana) continue; } // Make tag yield return new RubyTag { Text = config.RubyAsKatakana.Value ? katakana : hiragana, StartIndex = offsetAttribute.StartOffset, EndIndex = offsetAttribute.EndOffset - 1, }; } // Dispose tokenStream.End(); tokenStream.Dispose(); } } ================================================ FILE: osu.Game.Rulesets.Karaoke/Edit/Generator/Lyrics/RubyTags/Ja/JaRubyTagGeneratorConfig.cs ================================================ // Copyright (c) andy840119 . Licensed under the GPL Licence. // See the LICENCE file in the repository root for full licence text. using osu.Framework.Bindables; namespace osu.Game.Rulesets.Karaoke.Edit.Generator.Lyrics.RubyTags.Ja; public class JaRubyTagGeneratorConfig : RubyTagGeneratorConfig { /// /// Generate ruby as Katakana. /// [ConfigSource("Ruby as Katakana", "Ruby as Katakana.")] public Bindable RubyAsKatakana { get; } = new BindableBool(); /// /// Generate ruby even it's same as lyric. /// [ConfigSource("Enable duplicated ruby.", "Enable output duplicated ruby even it's match with lyric.")] public Bindable EnableDuplicatedRuby { get; } = new BindableBool(); } ================================================ FILE: osu.Game.Rulesets.Karaoke/Edit/Generator/Lyrics/RubyTags/RubyTagGenerator.cs ================================================ // Copyright (c) andy840119 . Licensed under the GPL Licence. // See the LICENCE file in the repository root for full licence text. using osu.Framework.Localisation; using osu.Game.Rulesets.Karaoke.Objects; namespace osu.Game.Rulesets.Karaoke.Edit.Generator.Lyrics.RubyTags; public abstract class RubyTagGenerator : LyricPropertyGenerator where TConfig : RubyTagGeneratorConfig, new() { protected RubyTagGenerator(TConfig config) : base(config) { } protected override LocalisableString? GetInvalidMessageFromItem(Lyric item) { if (string.IsNullOrWhiteSpace(item.Text)) return "Lyric should not be empty."; return null; } } ================================================ FILE: osu.Game.Rulesets.Karaoke/Edit/Generator/Lyrics/RubyTags/RubyTagGeneratorConfig.cs ================================================ // Copyright (c) andy840119 . Licensed under the GPL Licence. // See the LICENCE file in the repository root for full licence text. namespace osu.Game.Rulesets.Karaoke.Edit.Generator.Lyrics.RubyTags; public abstract class RubyTagGeneratorConfig : GeneratorConfig; ================================================ FILE: osu.Game.Rulesets.Karaoke/Edit/Generator/Lyrics/RubyTags/RubyTagGeneratorSelector.cs ================================================ // Copyright (c) andy840119 . Licensed under the GPL Licence. // See the LICENCE file in the repository root for full licence text. using System.Globalization; using osu.Game.Rulesets.Karaoke.Configuration; using osu.Game.Rulesets.Karaoke.Edit.Generator.Lyrics.RubyTags.Ja; using osu.Game.Rulesets.Karaoke.Objects; namespace osu.Game.Rulesets.Karaoke.Edit.Generator.Lyrics.RubyTags; public class RubyTagGeneratorSelector : LyricGeneratorSelector { public RubyTagGeneratorSelector(KaraokeRulesetEditGeneratorConfigManager generatorConfigManager) : base(generatorConfigManager) { RegisterGenerator(new CultureInfo(17)); RegisterGenerator(new CultureInfo(1041)); } } ================================================ FILE: osu.Game.Rulesets.Karaoke/Edit/Generator/Lyrics/TimeTags/Ja/JaTimeTagGenerator.cs ================================================ // Copyright (c) andy840119 . Licensed under the GPL Licence. // See the LICENCE file in the repository root for full licence text. using System.Collections.Generic; using System.Linq; using osu.Framework.Graphics.Sprites; using osu.Game.Rulesets.Karaoke.Objects; using osu.Game.Rulesets.Karaoke.Utils; namespace osu.Game.Rulesets.Karaoke.Edit.Generator.Lyrics.TimeTags.Ja; public class JaTimeTagGenerator : TimeTagGenerator { public JaTimeTagGenerator(JaTimeTagGeneratorConfig config) : base(config) { } /// /// Thanks for RhythmKaTTE's author writing this logic into C#.
/// http://juna-idler.blogspot.com/2016/05/rhythmkatte-version-01.html ///
protected override void TimeTagLogic(Lyric lyric, List timeTags) { timeTags.AddRange(generateTimeTagByText(lyric.Text)); foreach (var ruby in lyric.RubyTags) { // remove exist time tag timeTags.RemoveAll(x => x.Index > new TextIndex(ruby.StartIndex) && x.Index < new TextIndex(ruby.EndIndex, TextIndex.IndexState.End)); // add new time tags created from ruby var rubyTags = generateTimeTagByText(ruby.Text); var shiftingTimeTags = rubyTags.Select((x, _) => new TimeTag(new TextIndex(ruby.StartIndex, x.Index.State), x.Time)); timeTags.AddRange(shiftingTimeTags); } } private IEnumerable generateTimeTagByText(string text) { if (string.IsNullOrEmpty(text)) yield break; for (int i = 1; i < text.Length; i++) { char c = text[i]; char pc = text[i - 1]; if (CharUtils.IsSpacing(c) && Config.CheckWhiteSpace.Value) { // ignore continuous white space. if (CharUtils.IsSpacing(pc)) continue; var timeTag = Config.CheckWhiteSpaceKeyUp.Value ? new TimeTag(new TextIndex(i - 1, TextIndex.IndexState.End)) : new TimeTag(new TextIndex(i)); if (CharUtils.IsEnglish(pc)) { if (Config.CheckWhiteSpaceAlphabet.Value) yield return timeTag; } else if (char.IsDigit(pc)) { if (Config.CheckWhiteSpaceDigit.Value) yield return timeTag; } else if (CharUtils.IsAsciiSymbol(pc)) { if (Config.CheckWhiteSpaceAsciiSymbol.Value) yield return timeTag; } else { yield return timeTag; } } else if (CharUtils.IsEnglish(c) || char.IsNumber(c) || CharUtils.IsAsciiSymbol(c)) { if (CharUtils.IsSpacing(pc) || (!CharUtils.IsEnglish(pc) && !char.IsNumber(pc) && !CharUtils.IsAsciiSymbol(pc))) { yield return new TimeTag(new TextIndex(i)); } } else if (CharUtils.IsSpacing(pc)) { yield return new TimeTag(new TextIndex(i)); } else { switch (c) { case 'ゃ': case 'ゅ': case 'ょ': case 'ャ': case 'ュ': case 'ョ': case 'ぁ': case 'ぃ': case 'ぅ': case 'ぇ': case 'ぉ': case 'ァ': case 'ィ': case 'ゥ': case 'ェ': case 'ォ': case 'ー': case '~': break; case 'ん': if (Config.Checkん.Value) yield return new TimeTag(new TextIndex(i)); break; case 'っ': if (Config.Checkっ.Value) yield return new TimeTag(new TextIndex(i)); break; default: yield return new TimeTag(new TextIndex(i)); break; } } } } } ================================================ FILE: osu.Game.Rulesets.Karaoke/Edit/Generator/Lyrics/TimeTags/Ja/JaTimeTagGeneratorConfig.cs ================================================ // Copyright (c) andy840119 . Licensed under the GPL Licence. // See the LICENCE file in the repository root for full licence text. using osu.Framework.Bindables; using osu.Game.Rulesets.Karaoke.Objects; namespace osu.Game.Rulesets.Karaoke.Edit.Generator.Lyrics.TimeTags.Ja; public class JaTimeTagGeneratorConfig : TimeTagGeneratorConfig { /// /// Add the if character is "ん" /// [ConfigCategory(CATEGORY_CHECK_CHARACTER)] [ConfigSource("Check ん", "Check ん or not.")] public Bindable Checkん { get; } = new BindableBool(true); /// /// Add the if character is "っ" /// [ConfigCategory(CATEGORY_CHECK_CHARACTER)] [ConfigSource("Check っ", "Check っ or not.")] public Bindable Checkっ { get; } = new BindableBool(); /// /// Add the if spacing is next of the alphabet.
/// This feature will work only if enable the ///
[ConfigCategory(CATEGORY_CHECK_WHITE_SPACE)] [ConfigSource("Check white space alphabet", "Check white space alphabet.")] public Bindable CheckWhiteSpaceAlphabet { get; } = new BindableBool(); /// /// Add the if spacing is next of the digit.
/// This feature will work only if enable the ///
[ConfigCategory(CATEGORY_CHECK_WHITE_SPACE)] [ConfigSource("Check white space digit", "Check white space digit.")] public Bindable CheckWhiteSpaceDigit { get; } = new BindableBool(); /// /// Add the if spacing is next of the symbol.
/// This feature will work only if enable the ///
[ConfigCategory(CATEGORY_CHECK_WHITE_SPACE)] [ConfigSource("Check white space ascii symbol", "Check white space ascii symbol.")] public Bindable CheckWhiteSpaceAsciiSymbol { get; } = new BindableBool(); public JaTimeTagGeneratorConfig() { CheckLineEndKeyUp.Default = true; CheckLineEndKeyUp.SetDefault(); CheckWhiteSpace.Default = true; CheckWhiteSpace.SetDefault(); CheckWhiteSpaceKeyUp.Default = true; CheckWhiteSpaceKeyUp.SetDefault(); } } ================================================ FILE: osu.Game.Rulesets.Karaoke/Edit/Generator/Lyrics/TimeTags/TimeTagGenerator.cs ================================================ // Copyright (c) andy840119 . Licensed under the GPL Licence. // See the LICENCE file in the repository root for full licence text. using System.Collections.Generic; using System.Linq; using osu.Framework.Graphics.Sprites; using osu.Framework.Localisation; using osu.Game.Rulesets.Karaoke.Objects; namespace osu.Game.Rulesets.Karaoke.Edit.Generator.Lyrics.TimeTags; public abstract class TimeTagGenerator : LyricPropertyGenerator where TConfig : TimeTagGeneratorConfig, new() { protected TimeTagGenerator(TConfig config) : base(config) { } protected override LocalisableString? GetInvalidMessageFromItem(Lyric item) { if (string.IsNullOrEmpty(item.Text)) return "Lyric should not be empty."; return null; } protected sealed override TimeTag[] GenerateFromItem(Lyric item) { var timeTags = new List(); string text = item.Text; if (string.IsNullOrEmpty(text)) return timeTags.ToArray(); if (string.IsNullOrWhiteSpace(text)) { if (Config.CheckBlankLine.Value) timeTags.Add(new TimeTag(new TextIndex(0))); return timeTags.ToArray(); } // create tag at start of lyric timeTags.Add(new TimeTag(new TextIndex(0))); if (Config.CheckLineEndKeyUp.Value) timeTags.Add(new TimeTag(new TextIndex(text.Length - 1, TextIndex.IndexState.End))); TimeTagLogic(item, timeTags); return timeTags.OrderBy(x => x.Index).ToArray(); } protected abstract void TimeTagLogic(Lyric lyric, List timeTags); } ================================================ FILE: osu.Game.Rulesets.Karaoke/Edit/Generator/Lyrics/TimeTags/TimeTagGeneratorConfig.cs ================================================ // Copyright (c) andy840119 . Licensed under the GPL Licence. // See the LICENCE file in the repository root for full licence text. using osu.Framework.Bindables; using osu.Game.Rulesets.Karaoke.Objects; namespace osu.Game.Rulesets.Karaoke.Edit.Generator.Lyrics.TimeTags; public abstract class TimeTagGeneratorConfig : GeneratorConfig { protected const string CATEGORY_CHECK_CHARACTER = "Character checking"; protected const string CATEGORY_CHECK_LINE_END = "Line end checking"; protected const string CATEGORY_CHECK_WHITE_SPACE = "White space checking"; /// /// Will create a at the first of the lyric if only contains spacing in the . /// [ConfigCategory(CATEGORY_CHECK_CHARACTER)] [ConfigSource("Check blank line", "Check blank line or not.")] public Bindable CheckBlankLine { get; } = new BindableBool(); /// /// Add end at the end of the . /// [ConfigCategory(CATEGORY_CHECK_LINE_END)] [ConfigSource("Use key-up time tag in line end", "Use key-up time tag in line end")] public Bindable CheckLineEndKeyUp { get; } = new BindableBool(); /// /// Will add the if meet the spacing. /// [ConfigCategory(CATEGORY_CHECK_WHITE_SPACE)] [ConfigSource("Check white space", "Check white space")] public Bindable CheckWhiteSpace { get; } = new BindableBool(); /// /// Add the end instead.
/// This feature will work only if enable the . ///
[ConfigCategory(CATEGORY_CHECK_WHITE_SPACE)] [ConfigSource("Use key-up", "Use key-up")] public Bindable CheckWhiteSpaceKeyUp { get; } = new BindableBool(); } ================================================ FILE: osu.Game.Rulesets.Karaoke/Edit/Generator/Lyrics/TimeTags/TimeTagGeneratorSelector.cs ================================================ // Copyright (c) andy840119 . Licensed under the GPL Licence. // See the LICENCE file in the repository root for full licence text. using System.Globalization; using osu.Game.Rulesets.Karaoke.Configuration; using osu.Game.Rulesets.Karaoke.Edit.Generator.Lyrics.TimeTags.Ja; using osu.Game.Rulesets.Karaoke.Edit.Generator.Lyrics.TimeTags.Zh; using osu.Game.Rulesets.Karaoke.Objects; namespace osu.Game.Rulesets.Karaoke.Edit.Generator.Lyrics.TimeTags; public class TimeTagGeneratorSelector : LyricGeneratorSelector { public TimeTagGeneratorSelector(KaraokeRulesetEditGeneratorConfigManager generatorConfigManager) : base(generatorConfigManager) { RegisterGenerator(new CultureInfo(17)); RegisterGenerator(new CultureInfo(1041)); RegisterGenerator(new CultureInfo(1028)); } } ================================================ FILE: osu.Game.Rulesets.Karaoke/Edit/Generator/Lyrics/TimeTags/Zh/ZhTimeTagGenerator.cs ================================================ // Copyright (c) andy840119 . Licensed under the GPL Licence. // See the LICENCE file in the repository root for full licence text. using System.Collections.Generic; using osu.Framework.Graphics.Sprites; using osu.Game.Rulesets.Karaoke.Objects; using osu.Game.Rulesets.Karaoke.Utils; namespace osu.Game.Rulesets.Karaoke.Edit.Generator.Lyrics.TimeTags.Zh; public class ZhTimeTagGenerator : TimeTagGenerator { public ZhTimeTagGenerator(ZhTimeTagGeneratorConfig config) : base(config) { } protected override void TimeTagLogic(Lyric lyric, List timeTags) { string text = lyric.Text; for (int i = 1; i < text.Length; i++) { if (CharUtils.IsChinese(text[i])) { timeTags.Add(new TimeTag(new TextIndex(i))); } } } } ================================================ FILE: osu.Game.Rulesets.Karaoke/Edit/Generator/Lyrics/TimeTags/Zh/ZhTimeTagGeneratorConfig.cs ================================================ // Copyright (c) andy840119 . Licensed under the GPL Licence. // See the LICENCE file in the repository root for full licence text. namespace osu.Game.Rulesets.Karaoke.Edit.Generator.Lyrics.TimeTags.Zh; public class ZhTimeTagGeneratorConfig : TimeTagGeneratorConfig; ================================================ FILE: osu.Game.Rulesets.Karaoke/Edit/Generator/PropertyDetector.cs ================================================ // Copyright (c) andy840119 . Licensed under the GPL Licence. // See the LICENCE file in the repository root for full licence text. using osu.Framework.Localisation; namespace osu.Game.Rulesets.Karaoke.Edit.Generator; public abstract class PropertyDetector : PropertyDetector where TConfig : GeneratorConfig, new() { protected readonly TConfig Config; protected PropertyDetector(TConfig config) { Config = config; } } public abstract class PropertyDetector { /// /// Determined if detect from is supported. /// /// /// public bool CanDetect(TItem item) => GetInvalidMessage(item) == null; /// /// Will get the invalid message if from the is not able to be detected. /// /// /// public LocalisableString? GetInvalidMessage(TItem item) => GetInvalidMessageFromItem(item); /// /// Detect the from the . /// /// /// public TProperty Detect(TItem item) { if (!CanDetect(item)) throw new DetectorNotSupportedException(); return DetectFromItem(item); } protected abstract LocalisableString? GetInvalidMessageFromItem(TItem item); protected abstract TProperty DetectFromItem(TItem item); } ================================================ FILE: osu.Game.Rulesets.Karaoke/Edit/Generator/PropertyGenerator.cs ================================================ // Copyright (c) andy840119 . Licensed under the GPL Licence. // See the LICENCE file in the repository root for full licence text. using osu.Framework.Localisation; namespace osu.Game.Rulesets.Karaoke.Edit.Generator; public abstract class PropertyGenerator : PropertyGenerator where TConfig : GeneratorConfig, new() { protected readonly TConfig Config; protected PropertyGenerator(TConfig config) { Config = config; } } public abstract class PropertyGenerator { /// /// Determined if generate from is supported. /// /// /// public bool CanGenerate(TItem item) => GetInvalidMessage(item) == null; /// /// Will get the invalid message if from the is not able to be generated. /// /// /// public LocalisableString? GetInvalidMessage(TItem item) => GetInvalidMessageFromItem(item); /// /// Generate the from the . /// /// /// public TProperty Generate(TItem item) { if (!CanGenerate(item)) throw new GeneratorNotSupportedException(); return GenerateFromItem(item); } protected abstract LocalisableString? GetInvalidMessageFromItem(TItem item); protected abstract TProperty GenerateFromItem(TItem item); } ================================================ FILE: osu.Game.Rulesets.Karaoke/Edit/Generator/Stages/Classic/ClassicLyricLayoutCategoryGenerator.cs ================================================ // Copyright (c) andy840119 . Licensed under the GPL Licence. // See the LICENCE file in the repository root for full licence text. using System; using System.Collections.Generic; using System.Linq; using osu.Framework.Localisation; using osu.Game.Rulesets.Karaoke.Beatmaps; using osu.Game.Rulesets.Karaoke.Objects; using osu.Game.Rulesets.Karaoke.Stages.Infos.Classic; namespace osu.Game.Rulesets.Karaoke.Edit.Generator.Stages.Classic; public class ClassicLyricLayoutCategoryGenerator : StageInfoPropertyGenerator { public ClassicLyricLayoutCategoryGenerator(ClassicLyricLayoutCategoryGeneratorConfig config) : base(config) { } protected override LocalisableString? GetInvalidMessageFromItem(KaraokeBeatmap item) { var lyrics = item.HitObjects.OfType().ToArray(); if (!lyrics.Any()) return "Should have lyric in the beatmap."; return null; } protected override ClassicLyricLayoutCategory GenerateFromItem(KaraokeBeatmap item) { int rowAmount = Config.LyricRowAmount.Value; bool applyMappingToTheLyric = Config.ApplyMappingToTheLyric.Value; var lyrics = item.HitObjects.OfType().ToArray(); var layoutCategory = new ClassicLyricLayoutCategory(); // create the element first. var layouts = mappingLayoutsToLyric(layoutCategory, rowAmount).ToArray(); if (!applyMappingToTheLyric) return layoutCategory; // then, mapping to the lyric. for (int i = 0; i < lyrics.Length; i++) { var lyric = lyrics.ElementAt(i); var layout = layouts.ElementAt(i % rowAmount); layoutCategory.AddToMapping(layout, lyric); } return layoutCategory; } private IEnumerable mappingLayoutsToLyric(ClassicLyricLayoutCategory category, int amount) { switch (amount) { case 4: yield return addElementWithLine(category, 3, ClassicLyricLayoutAlignment.Left); yield return addElementWithLine(category, 2, ClassicLyricLayoutAlignment.Right); yield return addElementWithLine(category, 1, ClassicLyricLayoutAlignment.Left); yield return addElementWithLine(category, 0, ClassicLyricLayoutAlignment.Right); yield break; case 3: yield return addElementWithLine(category, 2, ClassicLyricLayoutAlignment.Left); yield return addElementWithLine(category, 1, ClassicLyricLayoutAlignment.Center); yield return addElementWithLine(category, 0, ClassicLyricLayoutAlignment.Right); yield break; case 2: yield return addElementWithLine(category, 1, ClassicLyricLayoutAlignment.Left); yield return addElementWithLine(category, 0, ClassicLyricLayoutAlignment.Right); yield break; default: throw new InvalidOperationException(); } } private ClassicLyricLayout addElementWithLine(ClassicLyricLayoutCategory category, int line, ClassicLyricLayoutAlignment alignment) { float horizontalMargin = Config.HorizontalMargin.Value; return category.AddElement(x => { x.Name = $"{generateName(alignment)} {x.ID}"; x.Alignment = alignment; x.HorizontalMargin = horizontalMargin; x.Line = line; }); static string generateName(ClassicLyricLayoutAlignment alignment) => alignment switch { ClassicLyricLayoutAlignment.Left => "Left", ClassicLyricLayoutAlignment.Center => "Center", ClassicLyricLayoutAlignment.Right => "Right", _ => throw new ArgumentOutOfRangeException(nameof(alignment), alignment, null), }; } } ================================================ FILE: osu.Game.Rulesets.Karaoke/Edit/Generator/Stages/Classic/ClassicLyricLayoutCategoryGeneratorConfig.cs ================================================ // Copyright (c) andy840119 . Licensed under the GPL Licence. // See the LICENCE file in the repository root for full licence text. using osu.Framework.Bindables; using osu.Game.Rulesets.Karaoke.Stages.Infos.Classic; namespace osu.Game.Rulesets.Karaoke.Edit.Generator.Stages.Classic; public class ClassicLyricLayoutCategoryGeneratorConfig : GeneratorConfig { /// /// How may lyric can be in the stage at the same time. /// [ConfigSource("Lyric amount", "How may lyric can be in the stage at the same time.")] public BindableInt LyricRowAmount { get; } = new(2) { MinValue = 2, MaxValue = 4, }; /// /// Should auto-create the mapping to the lyric or mapping by user. /// [ConfigSource("Apply mapping to the lyric", "Auto-apply the mapping or mapping by user.")] public BindableBool ApplyMappingToTheLyric { get; } = new(true); /// /// Adjust the in the /// [ConfigSource("Horizontal margin", "The margin between lyric and the border of the playfield.")] public BindableFloat HorizontalMargin { get; } = new() { MinValue = 32, MaxValue = 100, }; } ================================================ FILE: osu.Game.Rulesets.Karaoke/Edit/Generator/Stages/Classic/ClassicLyricTimingInfoGenerator.cs ================================================ // Copyright (c) andy840119 . Licensed under the GPL Licence. // See the LICENCE file in the repository root for full licence text. using System.Linq; using osu.Framework.Localisation; using osu.Game.Rulesets.Karaoke.Beatmaps; using osu.Game.Rulesets.Karaoke.Objects; using osu.Game.Rulesets.Karaoke.Stages.Infos.Classic; namespace osu.Game.Rulesets.Karaoke.Edit.Generator.Stages.Classic; public class ClassicLyricTimingInfoGenerator : StageInfoPropertyGenerator { public ClassicLyricTimingInfoGenerator(ClassicLyricTimingInfoGeneratorConfig config) : base(config) { } protected override LocalisableString? GetInvalidMessageFromItem(KaraokeBeatmap item) { var lyrics = item.HitObjects.OfType().ToArray(); if (!lyrics.Any()) return "Should have lyric in the beatmap."; return null; } protected override ClassicLyricTimingInfo GenerateFromItem(KaraokeBeatmap item) { int lyricAmount = Config.LyricRowAmount.Value; var lyrics = item.HitObjects.OfType().Where(x => x.TimeValid).ToArray(); var timingInfo = new ClassicLyricTimingInfo(); // lazy to generate the info if the lyric amount is not enough. if (lyrics.Length < lyricAmount) return timingInfo; // add start timing info. var firstTimingPoint = timingInfo.AddTimingPoint(); for (int i = 0; i < lyricAmount; i++) { var showLyric = lyrics.ElementAt(i); timingInfo.AddToMapping(firstTimingPoint, showLyric); } // should hide the current and show the next n lyric if touch the lyric end time. for (int i = 0; i < lyrics.Length - lyricAmount; i++) { var disappearLyric = lyrics.ElementAt(i); var showLyric = lyrics.ElementAt(i + lyricAmount); var timingPoint = timingInfo.AddTimingPoint(x => x.Time = disappearLyric.EndTime); timingInfo.AddToMapping(timingPoint, disappearLyric); timingInfo.AddToMapping(timingPoint, showLyric); } // add end timing info. var lastTimingPoint = timingInfo.AddTimingPoint(x => x.Time = lyrics.Last().EndTime); for (int i = lyrics.Length - lyricAmount; i < lyrics.Length; i++) { var disappearLyric = lyrics.ElementAt(i); timingInfo.AddToMapping(lastTimingPoint, disappearLyric); } return timingInfo; } } ================================================ FILE: osu.Game.Rulesets.Karaoke/Edit/Generator/Stages/Classic/ClassicLyricTimingInfoGeneratorConfig.cs ================================================ // Copyright (c) andy840119 . Licensed under the GPL Licence. // See the LICENCE file in the repository root for full licence text. using osu.Framework.Bindables; namespace osu.Game.Rulesets.Karaoke.Edit.Generator.Stages.Classic; public class ClassicLyricTimingInfoGeneratorConfig : GeneratorConfig { /// /// How may lyric can be in the stage at the same time. /// [ConfigSource("Lyric amount", "How may lyric can be in the stage at the same time.")] public BindableInt LyricRowAmount { get; } = new(2) { MinValue = 2, MaxValue = 4, }; } ================================================ FILE: osu.Game.Rulesets.Karaoke/Edit/Generator/Stages/Classic/ClassicStageInfoGenerator.cs ================================================ // Copyright (c) andy840119 . Licensed under the GPL Licence. // See the LICENCE file in the repository root for full licence text. using System.Linq; using osu.Framework.Localisation; using osu.Game.Rulesets.Karaoke.Beatmaps; using osu.Game.Rulesets.Karaoke.Objects; using osu.Game.Rulesets.Karaoke.Stages.Infos; using osu.Game.Rulesets.Karaoke.Stages.Infos.Classic; namespace osu.Game.Rulesets.Karaoke.Edit.Generator.Stages.Classic; public class ClassicStageInfoGenerator : StageInfoGenerator { public ClassicStageInfoGenerator(ClassicStageInfoGeneratorConfig config) : base(config) { } protected override LocalisableString? GetInvalidMessageFromItem(KaraokeBeatmap item) { var lyrics = item.HitObjects.OfType().ToArray(); if (!lyrics.Any()) return "Should have lyric in the beatmap."; return null; } protected override StageInfo GenerateFromItem(KaraokeBeatmap item) { int lyricRowAmount = Config.LyricRowAmount.Value; // it's OK not to get the config in the config manager. var layoutCategoryGenerator = new ClassicLyricLayoutCategoryGenerator(new ClassicLyricLayoutCategoryGeneratorConfig { LyricRowAmount = { Value = lyricRowAmount, }, }); // it's OK not to get the config in the config manager. var timingInfoGenerator = new ClassicLyricTimingInfoGenerator(new ClassicLyricTimingInfoGeneratorConfig { LyricRowAmount = { Value = lyricRowAmount, }, }); return new ClassicStageInfo { LyricLayoutCategory = layoutCategoryGenerator.Generate(item), LyricTimingInfo = timingInfoGenerator.Generate(item), }; } } ================================================ FILE: osu.Game.Rulesets.Karaoke/Edit/Generator/Stages/Classic/ClassicStageInfoGeneratorConfig.cs ================================================ // Copyright (c) andy840119 . Licensed under the GPL Licence. // See the LICENCE file in the repository root for full licence text. using osu.Framework.Bindables; namespace osu.Game.Rulesets.Karaoke.Edit.Generator.Stages.Classic; public class ClassicStageInfoGeneratorConfig : StageInfoGeneratorConfig { /// /// How may lyric can be in the stage at the same time. /// [ConfigSource("Lyric amount", "How may lyric can be in the stage at the same time.")] public BindableInt LyricRowAmount { get; } = new(2) { MinValue = 2, MaxValue = 4, }; } ================================================ FILE: osu.Game.Rulesets.Karaoke/Edit/Generator/Stages/Preview/PreviewStageInfoGenerator.cs ================================================ // Copyright (c) andy840119 . Licensed under the GPL Licence. // See the LICENCE file in the repository root for full licence text. using osu.Framework.Localisation; using osu.Game.Rulesets.Karaoke.Beatmaps; using osu.Game.Rulesets.Karaoke.Stages.Infos; using osu.Game.Rulesets.Karaoke.Stages.Infos.Preview; namespace osu.Game.Rulesets.Karaoke.Edit.Generator.Stages.Preview; public class PreviewStageInfoGenerator : StageInfoGenerator { public PreviewStageInfoGenerator(PreviewStageInfoGeneratorConfig config) : base(config) { } protected override LocalisableString? GetInvalidMessageFromItem(KaraokeBeatmap item) { return null; } protected override StageInfo GenerateFromItem(KaraokeBeatmap item) { return new PreviewStageInfo(); } } ================================================ FILE: osu.Game.Rulesets.Karaoke/Edit/Generator/Stages/Preview/PreviewStageInfoGeneratorConfig.cs ================================================ // Copyright (c) andy840119 . Licensed under the GPL Licence. // See the LICENCE file in the repository root for full licence text. namespace osu.Game.Rulesets.Karaoke.Edit.Generator.Stages.Preview; public class PreviewStageInfoGeneratorConfig : StageInfoGeneratorConfig; ================================================ FILE: osu.Game.Rulesets.Karaoke/Edit/Generator/Stages/StageInfoGenerator.cs ================================================ // Copyright (c) andy840119 . Licensed under the GPL Licence. // See the LICENCE file in the repository root for full licence text. using osu.Game.Rulesets.Karaoke.Beatmaps; using osu.Game.Rulesets.Karaoke.Stages.Infos; namespace osu.Game.Rulesets.Karaoke.Edit.Generator.Stages; public abstract class StageInfoGenerator : PropertyGenerator where TStageInfoConfig : StageInfoGeneratorConfig, new() { protected StageInfoGenerator(TStageInfoConfig config) : base(config) { } } ================================================ FILE: osu.Game.Rulesets.Karaoke/Edit/Generator/Stages/StageInfoGeneratorConfig.cs ================================================ // Copyright (c) andy840119 . Licensed under the GPL Licence. // See the LICENCE file in the repository root for full licence text. namespace osu.Game.Rulesets.Karaoke.Edit.Generator.Stages; public class StageInfoGeneratorConfig : GeneratorConfig; ================================================ FILE: osu.Game.Rulesets.Karaoke/Edit/Generator/Stages/StageInfoGeneratorSelector.cs ================================================ // Copyright (c) andy840119 . Licensed under the GPL Licence. // See the LICENCE file in the repository root for full licence text. using System; using osu.Framework.Localisation; using osu.Game.Rulesets.Karaoke.Beatmaps; using osu.Game.Rulesets.Karaoke.Configuration; using osu.Game.Rulesets.Karaoke.Edit.Generator.Stages.Classic; using osu.Game.Rulesets.Karaoke.Edit.Generator.Stages.Preview; using osu.Game.Rulesets.Karaoke.Stages.Infos; using osu.Game.Rulesets.Karaoke.Stages.Infos.Classic; using osu.Game.Rulesets.Karaoke.Stages.Infos.Preview; namespace osu.Game.Rulesets.Karaoke.Edit.Generator.Stages; public class StageInfoGeneratorSelector : GeneratorSelector where TStageInfo : StageInfo { public StageInfoGeneratorSelector(KaraokeRulesetEditGeneratorConfigManager generatorConfigManager) : base(generatorConfigManager) { registerGenerator(typeof(ClassicStageInfo)); registerGenerator(typeof(PreviewStageInfo)); } private void registerGenerator(Type type) where TGenerator : StageInfoGenerator where TConfig : StageInfoGeneratorConfig, new() { RegisterGenerator(_ => type == typeof(TStageInfo)); } protected override LocalisableString? GetInvalidMessageFromItem(KaraokeBeatmap item) { if (!TryGetGenerator(item, out var generator)) return "Sorry, the stage does not support auto-generate."; return generator.GetInvalidMessage(item); } protected override StageInfo GenerateFromItem(KaraokeBeatmap item) { if (!TryGetGenerator(item, out var generator)) throw new GeneratorNotSupportedException(); return generator.Generate(item); } } ================================================ FILE: osu.Game.Rulesets.Karaoke/Edit/Generator/Stages/StageInfoPropertyGenerator.cs ================================================ // Copyright (c) andy840119 . Licensed under the GPL Licence. // See the LICENCE file in the repository root for full licence text. using osu.Game.Rulesets.Karaoke.Beatmaps; namespace osu.Game.Rulesets.Karaoke.Edit.Generator.Stages; /// /// Base interface of the auto-generator. /// /// /// public abstract class StageInfoPropertyGenerator : PropertyGenerator where TConfig : GeneratorConfig, new() { protected StageInfoPropertyGenerator(TConfig config) : base(config) { } } ================================================ FILE: osu.Game.Rulesets.Karaoke/Edit/KaraokeBeatmapVerifier.cs ================================================ // Copyright (c) andy840119 . Licensed under the GPL Licence. // See the LICENCE file in the repository root for full licence text. using System.Collections.Generic; using System.Linq; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Edit.Checks.Components; using osu.Game.Rulesets.Karaoke.Edit.Checks; namespace osu.Game.Rulesets.Karaoke.Edit; public class KaraokeBeatmapVerifier : IBeatmapVerifier { private readonly List checks = new() { new CheckBeatmapAvailableTranslations(), new CheckClassicStageInfo(), new CheckBeatmapNoteInfo(), new CheckBeatmapPageInfo(), new CheckLyricLanguage(), new CheckLyricReferenceLyric(), new CheckLyricRubyTag(), new CheckLyricSinger(), new CheckLyricText(), new CheckLyricTimeTag(), new CheckLyricTranslations(), new CheckNoteReferenceLyric(), new CheckNoteText(), new CheckNoteTime(), }; public IEnumerable Run(BeatmapVerifierContext context) => checks.SelectMany(check => check.Run(context)); } ================================================ FILE: osu.Game.Rulesets.Karaoke/Edit/KaraokeBlueprintContainer.cs ================================================ // Copyright (c) andy840119 . Licensed under the GPL Licence. // See the LICENCE file in the repository root for full licence text. using System; using System.Collections.Generic; using osu.Framework.Input.Events; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Karaoke.Edit.Blueprints.Lyrics; using osu.Game.Rulesets.Karaoke.Edit.Blueprints.Notes; using osu.Game.Rulesets.Karaoke.Objects; using osu.Game.Rulesets.Objects; using osu.Game.Screens.Edit.Compose.Components; using osuTK; namespace osu.Game.Rulesets.Karaoke.Edit; public partial class KaraokeBlueprintContainer : ComposeBlueprintContainer { public KaraokeBlueprintContainer(HitObjectComposer composer) : base(composer) { } public override HitObjectSelectionBlueprint CreateHitObjectBlueprintFor(HitObject hitObject) => hitObject switch { Note note => new NoteSelectionBlueprint(note), Lyric lyric => new LyricSelectionBlueprint(lyric), _ => throw new ArgumentOutOfRangeException(nameof(hitObject)), }; protected override SelectionHandler CreateSelectionHandler() => new KaraokeSelectionHandler(); protected override bool TryMoveBlueprints(DragEvent e, IList<(SelectionBlueprint blueprint, Vector2[] originalSnapPositions)> blueprints) => false; } ================================================ FILE: osu.Game.Rulesets.Karaoke/Edit/KaraokeEditorPlayfield.cs ================================================ // Copyright (c) andy840119 . Licensed under the GPL Licence. // See the LICENCE file in the repository root for full licence text. using osu.Game.Rulesets.Karaoke.UI; using osu.Game.Rulesets.Karaoke.UI.Scrolling; namespace osu.Game.Rulesets.Karaoke.Edit; public partial class KaraokeEditorPlayfield : KaraokePlayfield { protected override ScrollingNotePlayfield CreateNotePlayfield(int columns) => new EditorNotePlayfield(columns); } ================================================ FILE: osu.Game.Rulesets.Karaoke/Edit/KaraokeHitObjectComposer.cs ================================================ // Copyright (c) andy840119 . Licensed under the GPL Licence. // See the LICENCE file in the repository root for full licence text. using System; using System.Collections.Generic; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.UserInterface; using osu.Framework.Testing; using osu.Game.Beatmaps; using osu.Game.Graphics.UserInterface; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Edit.Tools; using osu.Game.Rulesets.Karaoke.Beatmaps; using osu.Game.Rulesets.Karaoke.Configuration; using osu.Game.Rulesets.Karaoke.Edit.ChangeHandlers.Beatmaps; using osu.Game.Rulesets.Karaoke.Edit.ChangeHandlers.Lyrics; using osu.Game.Rulesets.Karaoke.Edit.ChangeHandlers.Notes; using osu.Game.Rulesets.Karaoke.Edit.Components.Menus; using osu.Game.Rulesets.Karaoke.Edit.Debugging; using osu.Game.Rulesets.Karaoke.Objects; using osu.Game.Rulesets.Karaoke.Skinning.Fonts; using osu.Game.Rulesets.Karaoke.UI; using osu.Game.Rulesets.Karaoke.UI.Position; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.UI; using osu.Game.Screens.Edit; using osu.Game.Screens.Edit.Components.Menus; using osu.Game.Screens.Edit.Compose.Components; using osuTK; namespace osu.Game.Rulesets.Karaoke.Edit; public partial class KaraokeHitObjectComposer : HitObjectComposer { private DrawableKaraokeEditorRuleset drawableRuleset = null!; [Cached] private readonly KaraokeRulesetEditConfigManager editConfigManager; [Cached] private readonly KaraokeRulesetEditGeneratorConfigManager generatorConfigManager; [Cached] private readonly FontManager fontManager; [Cached(typeof(IKaraokeBeatmapResourcesProvider))] private KaraokeBeatmapResourcesProvider karaokeBeatmapResourcesProvider; [Cached(typeof(ILyricRubyTagsChangeHandler))] private readonly LyricRubyTagsChangeHandler lyricRubyTagsChangeHandler; [Cached(typeof(INotePositionInfo))] private readonly NotePositionInfo notePositionInfo; [Cached(typeof(INotesChangeHandler))] private readonly NotesChangeHandler notesChangeHandler; [Cached(typeof(INotePropertyChangeHandler))] private readonly NotePropertyChangeHandler notePropertyChangeHandler; [Cached(typeof(ILyricSingerChangeHandler))] private readonly LyricSingerChangeHandler lyricSingerChangeHandler; [Cached(typeof(IBeatmapSingersChangeHandler))] private readonly BeatmapSingersChangeHandler beatmapSingersChangeHandler; [Cached] private readonly DebugBeatmapManager debugBeatmapManager; [Resolved] private Editor editor { get; set; } = null!; public KaraokeHitObjectComposer(Ruleset ruleset) : base(ruleset) { editConfigManager = new KaraokeRulesetEditConfigManager(); generatorConfigManager = new KaraokeRulesetEditGeneratorConfigManager(); // Duplicated registration because selection handler need to use it. AddInternal(fontManager = new FontManager()); AddInternal(karaokeBeatmapResourcesProvider = new KaraokeBeatmapResourcesProvider()); AddInternal(lyricRubyTagsChangeHandler = new LyricRubyTagsChangeHandler()); AddInternal(notePositionInfo = new NotePositionInfo()); AddInternal(notesChangeHandler = new NotesChangeHandler()); AddInternal(notePropertyChangeHandler = new NotePropertyChangeHandler()); AddInternal(lyricSingerChangeHandler = new LyricSingerChangeHandler()); AddInternal(beatmapSingersChangeHandler = new BeatmapSingersChangeHandler()); AddInternal(debugBeatmapManager = new DebugBeatmapManager()); } [BackgroundDependencyLoader] private void load() { CreateMenuBar(); } private DependencyContainer dependencies = null!; protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) => dependencies = new DependencyContainer(base.CreateChildDependencies(parent)); public new KaraokePlayfield Playfield => drawableRuleset.Playfield; protected override Playfield? PlayfieldAtScreenSpacePosition(Vector2 screenSpacePosition) { // Only note and lyric playfield can interact with mouse input. if (Playfield.NotePlayfield.ReceivePositionalInputAt(screenSpacePosition)) return Playfield.NotePlayfield; if (Playfield.LyricPlayfield.ReceivePositionalInputAt(screenSpacePosition)) return Playfield.LyricPlayfield; return null; } protected override DrawableRuleset CreateDrawableRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList mods) { drawableRuleset = new DrawableKaraokeEditorRuleset(ruleset, beatmap, mods); // This is the earliest we can cache the scrolling info to ourselves, before masks are added to the hierarchy and inject it dependencies.CacheAs(drawableRuleset.ScrollingInfo); return drawableRuleset; } protected override ComposeBlueprintContainer CreateBlueprintContainer() => new KaraokeBlueprintContainer(this); protected void CreateMenuBar() { var editorMenuBar = editor.ChildrenOfType().FirstOrDefault(); if (editorMenuBar == null) return; Schedule(() => { editorMenuBar.Items = new List(editorMenuBar.Items) { new("Config") { Items = Array.Empty(), }, new("Tools") { Items = new MenuItem[] { // todo: remove this menu until we have a better way to edit skin. new KaraokeSkinEditorMenu(editor, null!, "Skin editor"), new KaraokeEditorMenu(editor, "Karaoke editor"), }, }, new("Debug") { Items = new MenuItem[] { new EditorMenuItem("Override beatmap as json format", MenuItemType.Destructive, () => debugBeatmapManager.OverrideTheBeatmapWithJsonFormat()), new EditorMenuItem("Save beatmap to new difficulty as json format", MenuItemType.Destructive, () => debugBeatmapManager.SaveToNewDifficulty()), new OsuMenuItemSpacer(), new EditorMenuItem("Export to json", MenuItemType.Destructive, () => debugBeatmapManager.ExportToJson()), new EditorMenuItem("Export to json beatmap", MenuItemType.Destructive, () => debugBeatmapManager.ExportToJsonBeatmap()), }, }, }; }); } protected override IReadOnlyList CompositionTools => Array.Empty(); protected override IEnumerable CreateTernaryButtons() => Array.Empty(); } ================================================ FILE: osu.Game.Rulesets.Karaoke/Edit/KaraokeSelectionHandler.cs ================================================ // Copyright (c) andy840119 . Licensed under the GPL Licence. // See the LICENCE file in the repository root for full licence text. using System.Collections.Generic; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Graphics.UserInterface; using osu.Game.Graphics.UserInterface; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Karaoke.Edit.Blueprints.Lyrics; using osu.Game.Rulesets.Karaoke.Edit.Blueprints.Notes; using osu.Game.Rulesets.Karaoke.Edit.ChangeHandlers.Lyrics; using osu.Game.Rulesets.Karaoke.Edit.ChangeHandlers.Notes; using osu.Game.Rulesets.Karaoke.Edit.Components.ContextMenu; using osu.Game.Rulesets.Karaoke.Objects; using osu.Game.Rulesets.Karaoke.UI.Components; using osu.Game.Rulesets.Karaoke.UI.Position; using osu.Game.Rulesets.Karaoke.UI.Scrolling; using osu.Game.Rulesets.Objects; using osu.Game.Screens.Edit; using osu.Game.Screens.Edit.Compose.Components; using osuTK; namespace osu.Game.Rulesets.Karaoke.Edit; public partial class KaraokeSelectionHandler : EditorSelectionHandler { [Resolved] private EditorBeatmap beatmap { get; set; } = null!; [Resolved] private INotePositionInfo notePositionInfo { get; set; } = null!; [Resolved] private HitObjectComposer composer { get; set; } = null!; [Resolved] private INotesChangeHandler notesChangeHandler { get; set; } = null!; [Resolved] private INotePropertyChangeHandler notePropertyChangeHandler { get; set; } = null!; [Resolved] private ILyricSingerChangeHandler lyricSingerChangeHandler { get; set; } = null!; protected ScrollingNotePlayfield NotePlayfield => ((KaraokeHitObjectComposer)composer).Playfield.NotePlayfield; protected override IEnumerable GetContextMenuItemsForSelection(IEnumerable> selection) { if (selection.All(x => x is LyricSelectionBlueprint)) { return new[] { createSingerMenuItem(), }; } if (EditorBeatmap.SelectedHitObjects.All(x => x is Note) && EditorBeatmap.SelectedHitObjects.Count > 1) { var menu = new List(); var selectedObject = EditorBeatmap.SelectedHitObjects.Cast().OrderBy(x => x.StartTime).ToArray(); // Set multi note display property menu.Add(createMultiNoteDisplayPropertyMenuItem(selectedObject)); // Combine multi note if they has same start and end index. var firstObject = selectedObject.FirstOrDefault(); if (firstObject != null && selectedObject.All(x => x.ReferenceTimeTagIndex == firstObject.ReferenceTimeTagIndex)) menu.Add(createCombineNoteMenuItem()); return menu; } return new List(); } private MenuItem createMultiNoteDisplayPropertyMenuItem(IReadOnlyCollection selectedObject) { bool display = selectedObject.Count(x => x.Display) >= selectedObject.Count(x => !x.Display); string displayText = display ? "Hide" : "Show"; return new OsuMenuItem($"{displayText} {selectedObject.Count} notes.", display ? MenuItemType.Destructive : MenuItemType.Standard, () => { notePropertyChangeHandler.ChangeDisplayState(!display); }); } private MenuItem createCombineNoteMenuItem() { return new OsuMenuItem("Combine", MenuItemType.Standard, () => { notesChangeHandler.Combine(); }); } private MenuItem createSingerMenuItem() { return new SingerContextMenu(beatmap, lyricSingerChangeHandler, "Singer"); } public override bool HandleMovement(MoveSelectionEvent moveEvent) { // Only note can be moved. if (moveEvent.Blueprint is not NoteSelectionBlueprint noteSelectionBlueprint) return false; var lastTone = noteSelectionBlueprint.HitObject.Tone; performColumnMovement(lastTone, moveEvent); return true; } private void performColumnMovement(Tone lastTone, MoveSelectionEvent moveEvent) { if (moveEvent.Blueprint is not NoteSelectionBlueprint) return; var calculator = notePositionInfo.Calculator; // get center position var screenSpacePosition = moveEvent.Blueprint.ScreenSpaceSelectionPoint + moveEvent.ScreenSpaceDelta; var position = NotePlayfield.ToLocalSpace(screenSpacePosition); var centerPosition = new Vector2(position.X, position.Y - NotePlayfield.Height / 2); // get delta position float lastCenterPosition = calculator.YPositionAt(lastTone); float delta = centerPosition.Y - lastCenterPosition; // get offset tone. const float trigger_height = ScrollingNotePlayfield.COLUMN_SPACING + DefaultColumnBackground.COLUMN_HEIGHT; var offset = delta switch { > trigger_height => -new Tone { Half = true }, < 0 => new Tone { Half = true }, _ => default, }; if (offset == default(Tone)) return; notePropertyChangeHandler.OffsetTone(offset); } } ================================================ FILE: osu.Game.Rulesets.Karaoke/Edit/Setup/Components/FormLanguageList.cs ================================================ // Copyright (c) andy840119 . Licensed under the GPL Licence. // See the LICENCE file in the repository root for full licence text. using System; using System.Collections.Specialized; using System.Globalization; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.UserInterface; using osu.Framework.Input.Events; using osu.Framework.Localisation; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterfaceV2; using osu.Game.Overlays; using osu.Game.Rulesets.Karaoke.Graphics.UserInterfaceV2; using osu.Game.Rulesets.Karaoke.Utils; using osuTK; namespace osu.Game.Rulesets.Karaoke.Edit.Setup.Components; public partial class FormLanguageList : CompositeDrawable { public BindableList Languages { get; } = new(); public LocalisableString Caption { get; init; } public LocalisableString HintText { get; init; } private Box background = null!; private FormFieldCaption caption = null!; private FillFlowContainer flow = null!; [Resolved] private OverlayColourProvider colourProvider { get; set; } = null!; [BackgroundDependencyLoader] private void load() { RelativeSizeAxes = Axes.X; AutoSizeAxes = Axes.Y; Masking = true; CornerRadius = 5; AddLanguageButton button; InternalChildren = new Drawable[] { background = new Box { RelativeSizeAxes = Axes.Both, Colour = colourProvider.Background5, }, new FillFlowContainer { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, Padding = new MarginPadding(9), Spacing = new Vector2(7), Direction = FillDirection.Vertical, Children = new Drawable[] { caption = new FormFieldCaption { Caption = Caption, TooltipText = HintText, }, flow = new FillFlowContainer { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, Direction = FillDirection.Full, Spacing = new Vector2(5), Child = button = new AddLanguageButton { Action = languageInsertionRequested, }, }, }, }, }; flow.SetLayoutPosition(button, float.MaxValue); } protected override void LoadComplete() { base.LoadComplete(); Languages.BindCollectionChanged((_, args) => { if (args.Action != NotifyCollectionChangedAction.Replace) updateLanguages(); }, true); updateState(); } protected override bool OnHover(HoverEvent e) { updateState(); return true; } protected override void OnHoverLost(HoverLostEvent e) { base.OnHoverLost(e); updateState(); } private void updateState() { background.Colour = colourProvider.Background5; caption.Colour = colourProvider.Content2; BorderThickness = IsHovered ? 2 : 0; if (IsHovered) BorderColour = colourProvider.Light4; } private void updateLanguages() { flow.RemoveAll(d => d is LanguageDisplay, true); foreach (var language in Languages) { flow.Add(new LanguageDisplay { Current = { Value = language }, DeleteRequested = languageDeletionRequested, }); } } private void languageInsertionRequested(CultureInfo language) { if (!Languages.Contains(language)) Languages.Add(language); } private void languageDeletionRequested(CultureInfo language) => Languages.Remove(language); private partial class LanguageDisplay : CompositeDrawable, IHasCurrentValue { /// /// Invoked when the user has requested the corresponding to this /// public Action? DeleteRequested; private readonly BindableWithCurrent current = new(); public Bindable Current { get => current.Current; set => current.Current = value; } private readonly Box background; private readonly OsuSpriteText languageName; public LanguageDisplay() { AutoSizeAxes = Axes.X; Height = 30; Masking = true; CornerRadius = 5; InternalChildren = new Drawable[] { background = new Box { RelativeSizeAxes = Axes.Both, }, languageName = new OsuSpriteText { Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, Padding = new MarginPadding { Left = 10, Right = 32 }, }, new IconButton { Icon = FontAwesome.Solid.Times, Anchor = Anchor.CentreRight, Origin = Anchor.CentreRight, Size = new Vector2(16), Margin = new MarginPadding { Right = 10 }, Action = () => { DeleteRequested?.Invoke(Current.Value); }, }, }; current.BindValueChanged(x => { languageName.Text = CultureInfoUtils.GetLanguageDisplayText(x.NewValue); }); } [BackgroundDependencyLoader] private void load(OsuColour colours) { background.Colour = colours.BlueDarker; languageName.Colour = colours.BlueLighter; } } internal partial class AddLanguageButton : CompositeDrawable, IHasPopover { public Action? Action; private readonly Bindable currentLanguage = new(); public AddLanguageButton() { Size = new Vector2(35); InternalChild = new IconButton { Action = this.ShowPopover, Icon = FontAwesome.Solid.Plus, }; currentLanguage.BindValueChanged(e => { this.HidePopover(); var language = e.NewValue; if (language == null) return; Action?.Invoke(language); currentLanguage.Value = null; }); } public Popover GetPopover() => new LanguageSelectorPopover(currentLanguage) { EnableEmptyOption = false, }; } } ================================================ FILE: osu.Game.Rulesets.Karaoke/Edit/Setup/Components/FormSingerList.cs ================================================ // Copyright (c) andy840119 . Licensed under the GPL Licence. // See the LICENCE file in the repository root for full licence text. using System; using System.Collections.Specialized; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.UserInterface; using osu.Framework.Input.Events; using osu.Framework.Localisation; using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterfaceV2; using osu.Game.Overlays; using osu.Game.Rulesets.Karaoke.Beatmaps.Metadatas; using osu.Game.Rulesets.Karaoke.Beatmaps.Utils; using osu.Game.Rulesets.Karaoke.Graphics.Cursor; using osu.Game.Rulesets.Karaoke.Graphics.Drawables; using osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Singers.Detail; using osuTK; namespace osu.Game.Rulesets.Karaoke.Edit.Setup.Components; public partial class FormSingerList : CompositeDrawable { public BindableList Singers { get; } = new(); public LocalisableString Caption { get; init; } public LocalisableString HintText { get; init; } private Box background = null!; private FormFieldCaption caption = null!; private FillFlowContainer flow = null!; [Resolved] private OverlayColourProvider colourProvider { get; set; } = null!; [BackgroundDependencyLoader] private void load() { RelativeSizeAxes = Axes.X; AutoSizeAxes = Axes.Y; Masking = true; CornerRadius = 5; AddSingerButton button; InternalChildren = new Drawable[] { background = new Box { RelativeSizeAxes = Axes.Both, Colour = colourProvider.Background5, }, new FillFlowContainer { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, Padding = new MarginPadding(9), Spacing = new Vector2(7), Direction = FillDirection.Vertical, Children = new Drawable[] { caption = new FormFieldCaption { Caption = Caption, TooltipText = HintText, }, flow = new FillFlowContainer { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, Direction = FillDirection.Full, Spacing = new Vector2(10), Child = button = new AddSingerButton { Action = singerInsertionRequested, }, }, }, }, }; flow.SetLayoutPosition(button, float.MaxValue); } protected override void LoadComplete() { base.LoadComplete(); Singers.BindCollectionChanged((_, args) => { if (args.Action != NotifyCollectionChangedAction.Replace) updateSingers(); }, true); updateState(); } protected override bool OnHover(HoverEvent e) { updateState(); return true; } protected override void OnHoverLost(HoverLostEvent e) { base.OnHoverLost(e); updateState(); } private void updateState() { background.Colour = colourProvider.Background5; caption.Colour = colourProvider.Content2; BorderThickness = IsHovered ? 2 : 0; if (IsHovered) BorderColour = colourProvider.Light4; } private void updateSingers() { flow.RemoveAll(d => d is SingerDisplay, true); foreach (var singer in Singers) { flow.Add(new SingerDisplay { Current = { Value = singer }, DeleteRequested = languageDeletionRequested, }); } } private void singerInsertionRequested() { var singer = new Singer { Name = "New singer", }; Singers.Add(singer); } private void languageDeletionRequested(Singer singer) => Singers.Remove(singer); /// /// A component which displays a singer along with related description text. /// private partial class SingerDisplay : CompositeDrawable, IHasCurrentValue, IHasContextMenu, IHasPopover { /// /// Invoked when the user has requested the singer corresponding to this .
/// to be removed from its palette. ///
public Action? DeleteRequested; private readonly BindableWithCurrent current = new(); private OsuSpriteText singerName = null!; public Bindable Current { get => current.Current; set => current.Current = value; } [BackgroundDependencyLoader] private void load() { AutoSizeAxes = Axes.Y; Width = 50; InternalChild = new FillFlowContainer { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, Direction = FillDirection.Vertical, Spacing = new Vector2(0, 10), Children = new Drawable[] { new SingerCircle { Current = { BindTarget = Current }, }, singerName = new OsuSpriteText { Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, }, }, }; Current.BindValueChanged(singer => singerName.Text = singer.NewValue?.Name ?? "unknown singer", true); } protected override bool OnClick(ClickEvent e) { this.ShowPopover(); return base.OnClick(e); } public MenuItem[] ContextMenuItems => new MenuItem[] { new OsuMenuItem("Edit singer info", MenuItemType.Standard, this.ShowPopover), new OsuMenuItem("Delete", MenuItemType.Destructive, () => { DeleteRequested?.Invoke(Current.Value); }), }; public Popover GetPopover() => new SingerEditPopover(Current.Value); private partial class SingerCircle : Container, IHasCustomTooltip { public Bindable Current { get; } = new(); private readonly DrawableSingerAvatar singerAvatar; public SingerCircle() { RelativeSizeAxes = Axes.X; Height = 50; CornerRadius = 25; Masking = true; BorderThickness = 5; Children = new Drawable[] { singerAvatar = new DrawableSingerAvatar { Anchor = Anchor.Centre, Origin = Anchor.Centre, RelativeSizeAxes = Axes.Both, }, }; } protected override void LoadComplete() { base.LoadComplete(); Current.BindValueChanged(_ => updateSinger(), true); } private void updateSinger() { BorderColour = SingerUtils.GetContentColour(Current.Value); singerAvatar.Singer = Current.Value; } public ITooltip GetCustomTooltip() => new SingerToolTip(); public Singer TooltipContent => Current.Value; } } internal partial class AddSingerButton : CompositeDrawable { public Action Action { set => circularButton.Action = value; } private readonly OsuClickableContainer circularButton; public AddSingerButton() { AutoSizeAxes = Axes.Y; Width = 50; InternalChild = new FillFlowContainer { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, Direction = FillDirection.Vertical, Spacing = new Vector2(0, 10), Children = new Drawable[] { circularButton = new OsuClickableContainer { RelativeSizeAxes = Axes.X, Height = 50, CornerRadius = 25, Masking = true, BorderThickness = 5, Children = new Drawable[] { new Box { RelativeSizeAxes = Axes.Both, Colour = Colour4.Transparent, AlwaysPresent = true, }, new SpriteIcon { Anchor = Anchor.Centre, Origin = Anchor.Centre, Size = new Vector2(20), Icon = FontAwesome.Solid.Plus, }, }, }, new OsuSpriteText { Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, Text = "New", }, }, }; } [BackgroundDependencyLoader] private void load(OsuColour colours) { circularButton.BorderColour = colours.BlueDarker; } } } ================================================ FILE: osu.Game.Rulesets.Karaoke/Edit/Setup/KaraokeNoteSection.cs ================================================ // Copyright (c) andy840119 . Licensed under the GPL Licence. // See the LICENCE file in the repository root for full licence text. using System; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Localisation; using osu.Game.Graphics.UserInterfaceV2; using osu.Game.Rulesets.Karaoke.Beatmaps; using osu.Game.Screens.Edit.Setup; namespace osu.Game.Rulesets.Karaoke.Edit.Setup; public partial class KaraokeNoteSection : SetupSection { public override LocalisableString Title => "Note"; private FormCheckBox scorable = null!; [BackgroundDependencyLoader] private void load() { Children = new Drawable[] { scorable = new FormCheckBox { Caption = "Scorable", HintText = "Will not show score playfield if the option is unchecked.", Current = { Value = true }, }, }; scorable.Current.BindValueChanged(_ => updateValues()); } private void updateValues() { if (Beatmap.PlayableBeatmap is not KaraokeBeatmap karaokeBeatmap) throw new InvalidOperationException(); karaokeBeatmap.Scorable = scorable.Current.Value; } } ================================================ FILE: osu.Game.Rulesets.Karaoke/Edit/Setup/KaraokeSingerSection.cs ================================================ // Copyright (c) andy840119 . Licensed under the GPL Licence. // See the LICENCE file in the repository root for full licence text. using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Localisation; using osu.Game.Rulesets.Karaoke.Beatmaps; using osu.Game.Rulesets.Karaoke.Edit.ChangeHandlers.Beatmaps; using osu.Game.Rulesets.Karaoke.Edit.Setup.Components; using osu.Game.Screens.Edit.Setup; namespace osu.Game.Rulesets.Karaoke.Edit.Setup; public partial class KaraokeSingerSection : SetupSection { public override LocalisableString Title => "Singers"; [Cached(typeof(IKaraokeBeatmapResourcesProvider))] private KaraokeBeatmapResourcesProvider karaokeBeatmapResourcesProvider = new(); private readonly BeatmapSingersChangeHandler changeHandler = new(); private FormSingerList singerList = null!; [BackgroundDependencyLoader] private void load() { AddInternal(karaokeBeatmapResourcesProvider); AddInternal(changeHandler); Children = new Drawable[] { singerList = new FormSingerList { Caption = "Singer list", HintText = "All the singers in beatmap.", }, }; if (Beatmap.BeatmapSkin != null) singerList.Singers.BindTo(changeHandler.Singers); } } ================================================ FILE: osu.Game.Rulesets.Karaoke/Edit/Setup/KaraokeTranslationSection.cs ================================================ // Copyright (c) andy840119 . Licensed under the GPL Licence. // See the LICENCE file in the repository root for full licence text. using System; using System.Collections.Specialized; using System.Globalization; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Localisation; using osu.Game.Rulesets.Karaoke.Edit.ChangeHandlers.Beatmaps; using osu.Game.Rulesets.Karaoke.Edit.Setup.Components; using osu.Game.Screens.Edit.Setup; namespace osu.Game.Rulesets.Karaoke.Edit.Setup; public partial class KaraokeTranslationSection : SetupSection { public override LocalisableString Title => "Translation"; [Cached(typeof(IBeatmapTranslationsChangeHandler))] private readonly BeatmapTranslationsChangeHandler changeHandler = new(); private FormLanguageList singerList = null!; [BackgroundDependencyLoader] private void load() { AddInternal(changeHandler); Children = new Drawable[] { singerList = new FormLanguageList { Caption = "Translation list", HintText = "All the lyric translation in beatmap.", }, }; singerList.Languages.AddRange(changeHandler.Languages); singerList.Languages.BindCollectionChanged((_, args) => { switch (args.Action) { case NotifyCollectionChangedAction.Add: foreach (var language in args.NewItems?.Cast() ?? Array.Empty()) { changeHandler.Add(language); } break; case NotifyCollectionChangedAction.Remove: foreach (var language in args.OldItems?.Cast() ?? Array.Empty()) { changeHandler.Remove(language); } break; } }); } } ================================================ FILE: osu.Game.Rulesets.Karaoke/Edit/Utils/EditorBeatmapUtils.cs ================================================ // Copyright (c) andy840119 . Licensed under the GPL Licence. // See the LICENCE file in the repository root for full licence text. using System; using System.Collections.Generic; using System.Linq; using osu.Game.Rulesets.Karaoke.Beatmaps; using osu.Game.Rulesets.Karaoke.Objects; using osu.Game.Screens.Edit; namespace osu.Game.Rulesets.Karaoke.Edit.Utils; public static class EditorBeatmapUtils { public static IEnumerable GetAllReferenceLyrics(EditorBeatmap editorBeatmap, Lyric referencedLyric) => editorBeatmap.HitObjects.OfType().Where(x => x.ReferenceLyric == referencedLyric); public static IEnumerable GetNotesByLyric(EditorBeatmap editorBeatmap, Lyric lyric) => editorBeatmap.HitObjects.OfType().Where(x => x.ReferenceLyric == lyric); public static KaraokeBeatmap GetPlayableBeatmap(EditorBeatmap editorBeatmap) { if (editorBeatmap.PlayableBeatmap is not KaraokeBeatmap karaokeBeatmap) throw new InvalidCastException(); return karaokeBeatmap; } } ================================================ FILE: osu.Game.Rulesets.Karaoke/Edit/Utils/HitObjectWritableUtils.cs ================================================ // Copyright (c) andy840119 . Licensed under the GPL Licence. // See the LICENCE file in the repository root for full licence text. using System; using System.Linq; using osu.Game.Rulesets.Karaoke.Objects; using osu.Game.Rulesets.Karaoke.Objects.Properties; using osu.Game.Rulesets.Karaoke.Objects.Types; namespace osu.Game.Rulesets.Karaoke.Edit.Utils; public static class HitObjectWritableUtils { #region Remove lyrics. public static bool IsRemoveLyricLocked(Lyric lyric) => GetRemoveLyricLockedBy(lyric) != null; public static LockLyricPropertyBy? GetRemoveLyricLockedBy(Lyric lyric) { bool lockedByState = isRemoveLyricLockedByState(lyric.Lock); if (lockedByState) return LockLyricPropertyBy.LockState; return null; } private static bool isRemoveLyricLockedByState(LockState lockState) => lockState != LockState.None; #endregion #region Lyric property public static bool IsWriteLyricPropertyLocked(Lyric lyric, params string[] propertyNames) => GetLyricPropertyLockedBy(lyric, propertyNames) != null; public static bool IsWriteLyricPropertyLocked(Lyric lyric, string propertyName) => GetLyricPropertyLockedBy(lyric, propertyName) != null; public static LockLyricPropertyBy? GetLyricPropertyLockedBy(Lyric lyric, params string[] propertyNames) { var reasons = propertyNames.Select(x => GetLyricPropertyLockedBy(lyric, x)) .Where(x => x != null) .OfType() .ToArray(); if (reasons.Contains(LockLyricPropertyBy.ReferenceLyricConfig)) return LockLyricPropertyBy.ReferenceLyricConfig; if (reasons.Contains(LockLyricPropertyBy.LockState)) return LockLyricPropertyBy.LockState; return null; } public static LockLyricPropertyBy? GetLyricPropertyLockedBy(Lyric lyric, string propertyName) { bool lockedByConfig = isWriteLyricPropertyLockedByConfig(lyric.ReferenceLyricConfig, propertyName); if (lockedByConfig) return LockLyricPropertyBy.ReferenceLyricConfig; bool lockedByState = isWriteLyricPropertyLockedByState(lyric.Lock, propertyName); if (lockedByState) return LockLyricPropertyBy.LockState; return null; } private static bool isWriteLyricPropertyLockedByState(LockState lockState, string propertyName) { // partial lock will only lock some property change like text because they are easy to be modified. // fully lock will basically lock all lyric properties. return propertyName switch { nameof(Lyric.ID) => false, // although the id is not changeable, but it's not locked by config. nameof(Lyric.Text) => lockState > LockState.None, nameof(Lyric.TimeTags) => lockState > LockState.None, nameof(Lyric.RubyTags) => lockState > LockState.None, nameof(Lyric.StartTime) => lockState > LockState.Partial, nameof(Lyric.Duration) => lockState > LockState.Partial, nameof(Lyric.TimeValid) => lockState > LockState.Partial, nameof(Lyric.SingerIds) => lockState > LockState.Partial, nameof(Lyric.Translations) => lockState > LockState.Partial, nameof(Lyric.Language) => lockState > LockState.Partial, nameof(Lyric.Order) => false, // order can always be changed. nameof(Lyric.Lock) => false, // order can always be changed. nameof(Lyric.ReferenceLyric) or nameof(Lyric.ReferenceLyricId) => lockState > LockState.Partial, nameof(Lyric.ReferenceLyricConfig) => lockState > LockState.Partial, // base class nameof(Lyric.Samples) => false, _ => throw new NotSupportedException(), }; } private static bool isWriteLyricPropertyLockedByConfig(IReferenceLyricPropertyConfig? config, string propertyName) { return config switch { ReferenceLyricConfig => false, SyncLyricConfig syncLyricConfig => propertyName switch { nameof(Lyric.ID) => false, // although the id is not changeable, but it's not locked by config. nameof(Lyric.Text) => true, nameof(Lyric.TimeTags) => syncLyricConfig.SyncTimeTagProperty, nameof(Lyric.RubyTags) => true, nameof(Lyric.StartTime) => false, nameof(Lyric.Duration) => false, nameof(Lyric.TimeValid) => false, nameof(Lyric.SingerIds) => syncLyricConfig.SyncSingerProperty, nameof(Lyric.Translations) => true, nameof(Lyric.Language) => true, nameof(Lyric.Order) => true, nameof(Lyric.Lock) => true, nameof(Lyric.ReferenceLyric) or nameof(Lyric.ReferenceLyricId) => false, nameof(Lyric.ReferenceLyricConfig) => false, // base class nameof(Lyric.Samples) => false, _ => throw new NotSupportedException(), }, null => false, _ => throw new NotSupportedException(), }; } #endregion #region Create or remove notes. public static bool IsCreateOrRemoveNoteLocked(Lyric lyric) => GetCreateOrRemoveNoteLockedBy(lyric) != null; public static LockLyricPropertyBy? GetCreateOrRemoveNoteLockedBy(Lyric lyric) { bool lockedByConfig = isCreateOrRemoveNoteLocked(lyric.ReferenceLyricConfig); if (lockedByConfig) return LockLyricPropertyBy.ReferenceLyricConfig; return null; } private static bool isCreateOrRemoveNoteLocked(IReferenceLyricPropertyConfig? config) { // todo: implementation. return config switch { ReferenceLyricConfig => false, SyncLyricConfig => true, null => false, _ => throw new NotSupportedException(), }; } #endregion #region Note property public static bool IsWriteNotePropertyLocked(Note note, params string[] propertyNames) => GetNotePropertyLockedBy(note, propertyNames) != null; public static bool IsWriteNotePropertyLocked(Note note, string propertyName) => GetNotePropertyLockedBy(note, propertyName) != null; public static LockNotePropertyBy? GetNotePropertyLockedBy(Note note, params string[] propertyNames) { var reasons = propertyNames.Select(x => GetNotePropertyLockedBy(note, x)) .Where(x => x != null) .OfType() .ToArray(); if (reasons.Contains(LockNotePropertyBy.ReferenceLyricConfig)) return LockNotePropertyBy.ReferenceLyricConfig; return null; } public static LockNotePropertyBy? GetNotePropertyLockedBy(Note note, string propertyName) { var lyric = note.ReferenceLyric; bool lockByReferenceLyricConfig = lyric != null && isWriteNotePropertyLockedByReferenceLyric(lyric, propertyName); if (lockByReferenceLyricConfig) return LockNotePropertyBy.ReferenceLyricConfig; return null; } private static bool isWriteNotePropertyLockedByReferenceLyric(Lyric lyric, string propertyName) { // todo: implement. return false; } #endregion } public enum LockLyricPropertyBy { ReferenceLyricConfig, LockState, } public enum LockNotePropertyBy { ReferenceLyricConfig, } ================================================ FILE: osu.Game.Rulesets.Karaoke/Edit/Utils/LockStateUtils.cs ================================================ // Copyright (c) andy840119 . Licensed under the GPL Licence. // See the LICENCE file in the repository root for full licence text. using System.Collections.Generic; using System.Linq; using osu.Game.Rulesets.Karaoke.Objects.Types; namespace osu.Game.Rulesets.Karaoke.Edit.Utils; public static class LockStateUtils { public static TLock[] FindUnlockObjects(IEnumerable objects) where TLock : IHasLock => objects.Where(x => x.Lock == LockState.None).ToArray(); } ================================================ FILE: osu.Game.Rulesets.Karaoke/Edit/Utils/ValueChangedEventUtils.cs ================================================ // Copyright (c) andy840119 . Licensed under the GPL Licence. // See the LICENCE file in the repository root for full licence text. using osu.Framework.Bindables; using osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics; using osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.CaretPosition; using osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.States; namespace osu.Game.Rulesets.Karaoke.Edit.Utils; public static class ValueChangedEventUtils { public static bool LyricChanged(ValueChangedEvent e) { var oldLyric = e.OldValue?.Lyric; var newLyric = e.NewValue?.Lyric; return oldLyric != newLyric; } public static bool LyricChanged(ValueChangedEvent e) { var oldRangeCaret = e.OldValue; var newRangeCaret = e.NewValue; return oldRangeCaret?.Start.Lyric != newRangeCaret?.Start.Lyric || oldRangeCaret?.End.Lyric != newRangeCaret?.End.Lyric; } public static bool EditModeChanged(ValueChangedEvent e) { if (e.OldValue.Default ^ e.NewValue.Default) return true; return e.OldValue.Mode != e.NewValue.Mode; } } ================================================ FILE: osu.Game.Rulesets.Karaoke/Edit/Utils/ZoomableScrollContainerUtils.cs ================================================ // Copyright (c) andy840119 . Licensed under the GPL Licence. // See the LICENCE file in the repository root for full licence text. using System; using osu.Game.Screens.Edit; namespace osu.Game.Rulesets.Karaoke.Edit.Utils; public static class ZoomableScrollContainerUtils { public static float GetZoomLevelForVisibleMilliseconds(EditorClock editorClock, double milliseconds) => Math.Max(1, (float)(editorClock.TrackLength / milliseconds)); } ================================================ FILE: osu.Game.Rulesets.Karaoke/Extensions/EnumerableExtensions.cs ================================================ // Copyright (c) andy840119 . Licensed under the GPL Licence. // See the LICENCE file in the repository root for full licence text. using System; using System.Collections.Generic; using System.Linq; namespace osu.Game.Rulesets.Karaoke.Extensions; public static class EnumerableExtensions { /// /// Retrieves the item after a pivot from an . /// /// The type of the items stored in the collection. /// The collection to iterate on. /// The pivot value. /// Match action /// The item in appearing after , or null if no such item exists. public static T? GetNextMatch(this IEnumerable collection, T pivot, Func action) where T : notnull { return collection.SkipWhile(i => !EqualityComparer.Default.Equals(i, pivot)).Skip(1).SkipWhile(x => !action(x)).FirstOrDefault(); } /// /// Retrieves the item before a pivot from an . /// /// The type of the items stored in the collection. /// The collection to iterate on. /// The pivot value. /// Match action /// The item in appearing before , or null if no such item exists. public static T? GetPreviousMatch(this IEnumerable collection, T pivot, Func action) where T : notnull { return collection.Reverse().SkipWhile(i => !EqualityComparer.Default.Equals(i, pivot)).Skip(1).SkipWhile(x => !action(x)).FirstOrDefault(); } /// /// Convert [][] to [,] /// /// /// /// public static T[,] To2DArray(this IEnumerable> source) { var data = source .Select(x => x.ToArray()) .ToArray(); var res = new T[data.Length, data.Max(x => x.Length)]; for (int i = 0; i < data.Length; ++i) { for (int j = 0; j < data[i].Length; ++j) { res[i, j] = data[i][j]; } } return res; } public static int IndexOf(this IEnumerable array, T value) { return array.ToList().IndexOf(value); } } ================================================ FILE: osu.Game.Rulesets.Karaoke/Extensions/OsuGameExtensions.cs ================================================ // Copyright (c) andy840119 . Licensed under the GPL Licence. // See the LICENCE file in the repository root for full licence text. using System.Linq; using osu.Framework.Allocation; using osu.Framework.Graphics.Containers; using osu.Framework.Testing; using osu.Game.Overlays; namespace osu.Game.Rulesets.Karaoke.Extensions; /// /// Collect dirty logic to get target drawable from /// public static class OsuGameExtensions { public static KaraokeRuleset GetRuleset(this DependencyContainer dependencies) { var rulesets = dependencies.Get().AvailableRulesets.Select(info => info.CreateInstance()); return rulesets.OfType().First(); } private static Container? getBasePlacementContainer(this OsuGame game) => game.Children.OfType().FirstOrDefault(c => c.ChildrenOfType().Any()); public static Container? GetChangelogPlacementContainer(this OsuGame game) { // will place the container where components of an WaveOverlayContainer type exist return game.getBasePlacementContainer()?.Children.OfType().FirstOrDefault(c => c.Children.OfType().Any()); } public static SettingsOverlay? GetSettingsOverlay(this OsuGame game) => game.getBasePlacementContainer()?.ChildrenOfType().FirstOrDefault(); } ================================================ FILE: osu.Game.Rulesets.Karaoke/Extensions/RegexExtensions.cs ================================================ // Copyright (c) andy840119 . Licensed under the GPL Licence. // See the LICENCE file in the repository root for full licence text. using System.Text.RegularExpressions; using osu.Game.Rulesets.Karaoke.Utils; namespace osu.Game.Rulesets.Karaoke.Extensions; public static class RegexExtensions { public static TType GetGroupValue(this Match match, string key, bool useDefaultValueIfEmpty = true) { string value = match.Groups[key].Value; // if got empty value, should change to null. return TypeUtils.ChangeType(string.IsNullOrEmpty(value) ? null : value)!; } } ================================================ FILE: osu.Game.Rulesets.Karaoke/Extensions/ScrollContainerExtensions.cs ================================================ // Copyright (c) andy840119 . Licensed under the GPL Licence. // See the LICENCE file in the repository root for full licence text. using System; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osuTK; namespace osu.Game.Rulesets.Karaoke.Extensions; public static class ScrollContainerExtensions { /// /// Extend the to scroll into view with spacing. /// /// /// /// /// /// public static void ScrollIntoViewWithSpacing(this ScrollContainer container, Drawable d, MarginPadding p, bool animated = true) where T : Drawable { double childPos0 = Math.Clamp(container.GetChildPosInContent(d, -new Vector2(p.Left, p.Top)), 0, container.AvailableContent); double childPos1 = Math.Clamp(container.GetChildPosInContent(d, d.DrawSize + new Vector2(p.Right, p.Bottom)), 0, container.AvailableContent); int scrollDim = container.ScrollDirection == Direction.Horizontal ? 0 : 1; double minPos = Math.Min(childPos0, childPos1); double maxPos = Math.Max(childPos0, childPos1); if (minPos < container.Current || (minPos > container.Current && d.DrawSize[scrollDim] > container.DisplayableContent)) container.ScrollTo(minPos, animated); else if (maxPos > container.Current + container.DisplayableContent) container.ScrollTo(maxPos - container.DisplayableContent, animated); } } ================================================ FILE: osu.Game.Rulesets.Karaoke/Extensions/TrickyCompositeDrawableExtension.cs ================================================ // Copyright (c) andy840119 . Licensed under the GPL Licence. // See the LICENCE file in the repository root for full licence text. using System.Collections.Generic; using System.Reflection; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; namespace osu.Game.Rulesets.Karaoke.Extensions; /// /// It's a tricky extension to get all non-public methods. /// Should be removed eventually. /// public static class TrickyCompositeDrawableExtension { public static IReadOnlyList? GetInternalChildren(this CompositeDrawable compositeDrawable) { // see this shit to access internal property. // https://stackoverflow.com/a/7575615/4105113 var prop = compositeDrawable.GetType().GetProperty("InternalChildren", BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public); if (prop == null) return null; return (IReadOnlyList)prop.GetValue(compositeDrawable)!; } } ================================================ FILE: osu.Game.Rulesets.Karaoke/Extensions/TypeExtensions.cs ================================================ // Copyright (c) andy840119 . Licensed under the GPL Licence. // See the LICENCE file in the repository root for full licence text. using System; using System.Linq; namespace osu.Game.Rulesets.Karaoke.Extensions; public static class TypeExtensions { /// /// Returns 's /// with the assembly version, culture and public key token values removed. /// /// /// This method is usually used in extensibility scenarios (i.e. for custom rulesets or skins) /// when a version-agnostic identifier associated with a C# class - potentially originating from /// an external assembly - is needed. /// Leaving only the type and assembly names in such a scenario allows to preserve compatibility /// across assembly versions. /// public static string GetInvariantInstantiationInfo(this Type type) { string? assemblyQualifiedName = type.AssemblyQualifiedName; if (assemblyQualifiedName == null) throw new ArgumentException($"{type}'s assembly-qualified name is null. Ensure that it is a concrete type and not a generic type parameter.", nameof(type)); return string.Join(',', assemblyQualifiedName.Split(',').Take(2)); } } ================================================ FILE: osu.Game.Rulesets.Karaoke/Flags/FlagState.cs ================================================ // Copyright (c) andy840119 . Licensed under the GPL Licence. // See the LICENCE file in the repository root for full licence text. using System; using System.Linq; namespace osu.Game.Rulesets.Karaoke.Flags; public class FlagState where TFlag : struct, Enum { #region Public interface private int value; public bool Invalidate(TFlag flags) { if (!CanInvalidate(flags)) return false; value &= ~Convert.ToInt32(flags); return true; } public void InvalidateAll() { foreach (TFlag flag in Enum.GetValues(typeof(TFlag))) { Invalidate(flag); } } public bool Validate(TFlag flags) { if (!CanValidate(flags)) return false; value |= Convert.ToInt32(flags); return true; } public void ValidateAll() { foreach (TFlag flag in Enum.GetValues(typeof(TFlag))) { Validate(flag); } } public bool IsValid(TFlag flags) { return (value & Convert.ToInt32(flags)) == Convert.ToInt32(flags); } public TFlag[] GetAllValidFlags() => Enum.GetValues().Where(IsValid).ToArray(); public TFlag[] GetAllInvalidFlags() => Enum.GetValues().Where(x => !IsValid(x)).ToArray(); #endregion protected virtual bool CanInvalidate(TFlag flags) => true; protected virtual bool CanValidate(TFlag flags) => true; } ================================================ FILE: osu.Game.Rulesets.Karaoke/Graphics/Containers/OrderRearrangeableListContainer.cs ================================================ // Copyright (c) andy840119 . Licensed under the GPL Licence. // See the LICENCE file in the repository root for full licence text. using System; using System.Collections.Specialized; using System.Diagnostics; using System.Linq; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Graphics.Containers; using osuTK; namespace osu.Game.Rulesets.Karaoke.Graphics.Containers; public abstract partial class OrderRearrangeableListContainer : OsuRearrangeableListContainer { public event Action? OnOrderChanged; protected abstract Vector2 Spacing { get; } protected OrderRearrangeableListContainer() { // this collection change event cannot directly register in parent bindable. // So register in here. Items.CollectionChanged += collectionChanged; } private void collectionChanged(object? sender, NotifyCollectionChangedEventArgs e) { switch (e.Action) { // should get the event if user change the position. case NotifyCollectionChangedAction.Move: Debug.Assert(e.NewItems != null); var item = e.NewItems.OfType().First(); int newIndex = e.NewStartingIndex; OnOrderChanged?.Invoke(item, newIndex); break; } } protected sealed override FillFlowContainer> CreateListFillFlowContainer() => base.CreateListFillFlowContainer().With(x => x.Spacing = Spacing); private bool displayBottomDrawable; private Drawable? bottomDrawable; public bool DisplayBottomDrawable { get => displayBottomDrawable; set { if (displayBottomDrawable == value) return; displayBottomDrawable = value; if (displayBottomDrawable) { bottomDrawable = CreateBottomDrawable(); if (bottomDrawable == null) return; bottomDrawable.Anchor |= Anchor.y2; bottomDrawable.Origin |= Anchor.y2; // because scroll container only follow list container size, so change the margin to let content bigger. ListContainer.Margin = new MarginPadding { Bottom = bottomDrawable.Height + Spacing.Y }; ScrollContainer.Add(bottomDrawable); } else { if (bottomDrawable == null) return; ListContainer.Margin = new MarginPadding(); ScrollContainer.Remove(bottomDrawable, true); } } } protected virtual Drawable? CreateBottomDrawable() => null; } ================================================ FILE: osu.Game.Rulesets.Karaoke/Graphics/Containers/RearrangeableTextFlowListContainer.cs ================================================ // Copyright (c) andy840119 . Licensed under the GPL Licence. // See the LICENCE file in the repository root for full licence text. using System; using System.Collections.Generic; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Input.Events; using osu.Framework.Localisation; using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osuTK; using osuTK.Graphics; namespace osu.Game.Rulesets.Karaoke.Graphics.Containers; /// /// Implement most feature for searchable text container. /// /// public partial class RearrangeableTextFlowListContainer : OsuRearrangeableListContainer { public readonly Bindable SelectedSet = new(); public Action? RequestSelection; private SearchContainer> searchContainer = null!; protected sealed override FillFlowContainer> CreateListFillFlowContainer() => searchContainer = new SearchContainer> { Spacing = new Vector2(0, 3), LayoutDuration = 200, LayoutEasing = Easing.OutQuint, }; public void Filter(string text) { searchContainer.SearchTerm = text; } protected sealed override OsuRearrangeableListItem CreateOsuDrawable(TModel item) => CreateDrawable(item).With(d => { d.SelectedSet.BindTarget = SelectedSet; d.RequestSelection = set => RequestSelection?.Invoke(set); }); protected new virtual DrawableTextListItem CreateDrawable(TModel item) => new(item); public partial class DrawableTextListItem : OsuRearrangeableListItem, IFilterable { public readonly Bindable SelectedSet = new(); public Action? RequestSelection; private TextFlowContainer text = null!; private Color4 selectedColour; public DrawableTextListItem(TModel item) : base(item) { Padding = new MarginPadding { Left = 5 }; ShowDragHandle.Value = false; } [BackgroundDependencyLoader] private void load(OsuColour colours) { selectedColour = colours.Yellow; HandleColour = colours.Gray5; } protected override void LoadComplete() { base.LoadComplete(); SelectedSet.BindValueChanged(set => { bool oldValueMatched = EqualityComparer.Default.Equals(set.OldValue, Model); bool newValueMatched = EqualityComparer.Default.Equals(set.NewValue, Model); if (!oldValueMatched && !newValueMatched) return; text.FadeColour(newValueMatched ? selectedColour : Color4.White, FADE_DURATION); }, true); } protected sealed override Drawable CreateContent() => text = new OsuTextFlowContainer { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, }.With(x => { Schedule(() => { // should create the text after BDL loaded. CreateDisplayContent(x, Model); }); }); protected override bool OnClick(ClickEvent e) { RequestSelection?.Invoke(Model); return true; } public virtual IEnumerable FilterTerms => new[] { new LocalisableString(Model?.ToString() ?? string.Empty), }; protected virtual void CreateDisplayContent(OsuTextFlowContainer textFlowContainer, TModel model) => textFlowContainer.AddText(model?.ToString() ?? string.Empty); private bool matchingFilter = true; public bool MatchingFilter { get => matchingFilter; set { if (matchingFilter == value) return; matchingFilter = value; updateFilter(); } } private void updateFilter() => this.FadeTo(MatchingFilter ? 1 : 0, 200); public bool FilteringActive { get; set; } } } ================================================ FILE: osu.Game.Rulesets.Karaoke/Graphics/Cursor/BackgroundToolTip.cs ================================================ // Copyright (c) andy840119 . Licensed under the GPL Licence. // See the LICENCE file in the repository root for full licence text. using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Shapes; using osu.Game.Graphics; using osuTK; namespace osu.Game.Rulesets.Karaoke.Graphics.Cursor; public abstract partial class BackgroundToolTip : VisibilityContainer, ITooltip { protected const int BORDER = 5; private readonly Box background; private readonly Container content; protected override Container Content => content; protected virtual float ContentPadding => 10; protected BackgroundToolTip() { AutoSizeAxes = Axes.Both; Masking = true; CornerRadius = BORDER; InternalChildren = new[] { background = new Box { RelativeSizeAxes = Axes.Both, }, SetBackground(), content = new Container { AutoSizeAxes = Axes.Both, AutoSizeDuration = 200, AutoSizeEasing = Easing.OutQuint, Padding = new MarginPadding(ContentPadding), }, }; } [BackgroundDependencyLoader] private void load(OsuColour colours) { background.Colour = colours.Gray3; } public abstract void SetContent(T content); protected virtual Drawable SetBackground() => Empty(); public void Move(Vector2 pos) => Position = pos; protected override void PopIn() => this.FadeIn(200, Easing.OutQuint); protected override void PopOut() => this.FadeOut(200, Easing.OutQuint); } ================================================ FILE: osu.Game.Rulesets.Karaoke/Graphics/Cursor/LyricToolTip.cs ================================================ // Copyright (c) andy840119 . Licensed under the GPL Licence. // See the LICENCE file in the repository root for full licence text. using osu.Framework.Graphics; using osu.Framework.Graphics.Sprites; using osu.Game.Rulesets.Karaoke.Graphics.Sprites; using osu.Game.Rulesets.Karaoke.Objects; namespace osu.Game.Rulesets.Karaoke.Graphics.Cursor; public partial class LyricTooltip : BackgroundToolTip { private Lyric? lastLyric; public override void SetContent(Lyric lyric) { if (lyric == lastLyric) return; lastLyric = lyric; Child = new DrawableLyricSpriteText(lyric) { Margin = new MarginPadding(10), Font = new FontUsage(size: 32), TopTextFont = new FontUsage(size: 12), BottomTextFont = new FontUsage(size: 12), }; } } ================================================ FILE: osu.Game.Rulesets.Karaoke/Graphics/Cursor/SingerToolTip.cs ================================================ // Copyright (c) andy840119 . Licensed under the GPL Licence. // See the LICENCE file in the repository root for full licence text. using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Rulesets.Karaoke.Beatmaps; using osu.Game.Rulesets.Karaoke.Beatmaps.Metadatas; using osu.Game.Rulesets.Karaoke.Beatmaps.Metadatas.Types; using osu.Game.Rulesets.Karaoke.Graphics.Drawables; using osuTK; using osuTK.Graphics; namespace osu.Game.Rulesets.Karaoke.Graphics.Cursor; public partial class SingerToolTip : BackgroundToolTip { private const int avatar_size = 60; private const int main_text_size = 24; private const int sub_text_size = 12; private readonly IBindable bindableName = new Bindable(); private readonly IBindable bindableRomanisation = new Bindable(); private readonly IBindable bindableEnglishName = new Bindable(); private readonly IBindable bindableDescription = new Bindable(); [Cached(typeof(IKaraokeBeatmapResourcesProvider))] private KaraokeBeatmapResourcesProvider karaokeBeatmapResourcesProvider; private readonly DrawableSingerAvatar avatar; private readonly OsuSpriteText name; private readonly OsuSpriteText englishName; private readonly OsuSpriteText romanisation; private readonly OsuSpriteText description; public SingerToolTip() { // we need to inject this provide in the tooltip because in will need in the drawable singer avatar. // and it's not able to get in the BDL due to tooltip container is in the osu.game level. AddInternal(karaokeBeatmapResourcesProvider = new KaraokeBeatmapResourcesProvider()); Child = new FillFlowContainer { AutoSizeAxes = Axes.Y, Width = 300, Direction = FillDirection.Vertical, Spacing = new Vector2(15), Children = new Drawable[] { new GridContainer { Name = "Basic info", RelativeSizeAxes = Axes.X, Height = avatar_size, ColumnDimensions = new[] { new Dimension(GridSizeMode.Absolute, avatar_size), new Dimension(), }, Content = new[] { new Drawable[] { avatar = new DrawableSingerAvatar { Name = "Avatar", Size = new Vector2(avatar_size), }, new Container { RelativeSizeAxes = Axes.Both, Padding = new MarginPadding { Left = 5 }, Children = new Drawable[] { new FillFlowContainer { Name = "Singer name", RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, Direction = FillDirection.Vertical, Spacing = new Vector2(1), Children = new[] { name = new TruncatingSpriteText { Name = "Singer name", Font = OsuFont.GetFont(weight: FontWeight.Bold, size: main_text_size), RelativeSizeAxes = Axes.X, ShowTooltip = false, }, romanisation = new TruncatingSpriteText { Name = "Romanisation", Font = OsuFont.GetFont(weight: FontWeight.Bold, size: sub_text_size), RelativeSizeAxes = Axes.X, ShowTooltip = false, }, }, }, englishName = new TruncatingSpriteText { Name = "English name", Anchor = Anchor.BottomLeft, Origin = Anchor.BottomLeft, Font = OsuFont.GetFont(weight: FontWeight.Bold, size: sub_text_size), RelativeSizeAxes = Axes.X, ShowTooltip = false, }, }, }, }, }, }, description = new OsuSpriteText { RelativeSizeAxes = Axes.X, AllowMultiline = true, Colour = Color4.White.Opacity(0.75f), Font = OsuFont.Default.With(size: 14), Name = "Description", }, }, }; bindableName.BindValueChanged(e => name.Text = e.NewValue, true); bindableRomanisation.BindValueChanged(e => romanisation.Text = string.IsNullOrEmpty(e.NewValue) ? string.Empty : $"({e.NewValue})", true); bindableEnglishName.BindValueChanged(e => englishName.Text = e.NewValue, true); bindableDescription.BindValueChanged(e => description.Text = string.IsNullOrEmpty(e.NewValue) ? "" : e.NewValue, true); } private ISinger? lastSinger; public override void SetContent(ISinger singer) { if (singer == lastSinger) return; avatar.Singer = singer; lastSinger = singer; // todo: other type of singer(e.g: sub-singer) might display different info. if (singer is not Singer s) return; bindableName.UnbindBindings(); bindableRomanisation.UnbindBindings(); bindableEnglishName.UnbindBindings(); bindableDescription.UnbindBindings(); bindableName.BindTo(s.NameBindable); bindableRomanisation.BindTo(s.RomanisationBindable); bindableEnglishName.BindTo(s.EnglishNameBindable); bindableDescription.BindTo(s.DescriptionBindable); } } ================================================ FILE: osu.Game.Rulesets.Karaoke/Graphics/Drawables/DrawableCircleSingerAvatar.cs ================================================ // Copyright (c) andy840119 . Licensed under the GPL Licence. // See the LICENCE file in the repository root for full licence text. using System; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Game.Graphics; using osu.Game.Rulesets.Karaoke.Beatmaps.Metadatas; using osu.Game.Rulesets.Karaoke.Beatmaps.Metadatas.Types; using osu.Game.Rulesets.Karaoke.Beatmaps.Utils; namespace osu.Game.Rulesets.Karaoke.Graphics.Drawables; public partial class DrawableCircleSingerAvatar : DrawableSingerAvatar { private readonly IBindable bindableHue = new Bindable(); [BackgroundDependencyLoader] private void load(OsuColour colour) { Masking = true; CornerRadius = Math.Min(DrawSize.X, DrawSize.Y) / 2f; BorderThickness = 5; bindableHue.BindValueChanged(_ => { BorderColour = Singer != null ? SingerUtils.GetContentColour(Singer) : colour.Gray0; }, true); } public override ISinger? Singer { get => base.Singer; set { base.Singer = value; bindableHue.UnbindBindings(); if (value is Singer singer) bindableHue.BindTo(singer.HueBindable); } } } ================================================ FILE: osu.Game.Rulesets.Karaoke/Graphics/Drawables/DrawableSingerAvatar.cs ================================================ // Copyright (c) andy840119 . Licensed under the GPL Licence. // See the LICENCE file in the repository root for full licence text. using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Textures; using osu.Game.Rulesets.Karaoke.Beatmaps; using osu.Game.Rulesets.Karaoke.Beatmaps.Metadatas; using osu.Game.Rulesets.Karaoke.Beatmaps.Metadatas.Types; namespace osu.Game.Rulesets.Karaoke.Graphics.Drawables; public partial class DrawableSingerAvatar : CompositeDrawable { private readonly IBindable bindableAvatarFile = new Bindable(); private readonly Sprite avatar; public DrawableSingerAvatar() { InternalChild = avatar = new Sprite { RelativeSizeAxes = Axes.Both, FillMode = FillMode.Fit, Anchor = Anchor.Centre, Origin = Anchor.Centre, }; } [BackgroundDependencyLoader] private void load(LargeTextureStore textures, IKaraokeBeatmapResourcesProvider karaokeBeatmapResourcesProvider) { bindableAvatarFile.BindValueChanged(_ => { if (singer == null) avatar.Texture = getDefaultAvatar(); else avatar.Texture = karaokeBeatmapResourcesProvider.GetSingerAvatar(singer) ?? getDefaultAvatar(); avatar.FadeInFromZero(500); }, true); Texture getDefaultAvatar() => textures.Get("Online/avatar-guest"); } private ISinger? singer; public virtual ISinger? Singer { get => singer; set { singer = value; if (singer is not Singer s) return; bindableAvatarFile.UnbindBindings(); bindableAvatarFile.BindTo(s.AvatarFileBindable); } } } ================================================ FILE: osu.Game.Rulesets.Karaoke/Graphics/Drawables/SingerDisplay.cs ================================================ // Copyright (c) andy840119 . Licensed under the GPL Licence. // See the LICENCE file in the repository root for full licence text. using System; using System.Collections.Generic; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.UserInterface; using osu.Framework.Input.Events; using osu.Game.Graphics.Containers; using osu.Game.Rulesets.Karaoke.Beatmaps.Metadatas; using osu.Game.Rulesets.Karaoke.Beatmaps.Metadatas.Types; using osu.Game.Rulesets.Karaoke.Graphics.Cursor; using osuTK; namespace osu.Game.Rulesets.Karaoke.Graphics.Drawables; public partial class SingerDisplay : CompositeDrawable, IHasCurrentValue> { private const int fade_duration = 1000; public bool DisplayUnrankedText = true; public ExpansionMode ExpansionMode = ExpansionMode.ExpandOnHover; private readonly Bindable> current = new(); public Bindable> Current { get => current; set { ArgumentNullException.ThrowIfNull(value); current.UnbindBindings(); current.BindTo(value); } } private readonly FillFlowContainer iconsContainer; public SingerDisplay() { AutoSizeAxes = Axes.Both; InternalChild = new FillFlowContainer { Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, AutoSizeAxes = Axes.Both, Direction = FillDirection.Vertical, Children = new Drawable[] { iconsContainer = new ReverseChildIDFillFlowContainer { Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, AutoSizeAxes = Axes.Both, Direction = FillDirection.Horizontal, }, }, }; Current.ValueChanged += singers => { iconsContainer.Clear(); foreach (var singer in singers.NewValue) { iconsContainer.Add(new DrawableSinger { Singer = singer, Name = "Avatar", Size = new Vector2(32), }); } if (IsLoaded) appearTransform(); }; } protected override void Dispose(bool isDisposing) { base.Dispose(isDisposing); Current.UnbindAll(); } protected override void LoadComplete() { base.LoadComplete(); appearTransform(); iconsContainer.FadeInFromZero(fade_duration, Easing.OutQuint); } private void appearTransform() { expand(); using (iconsContainer.BeginDelayedSequence(1200)) contract(); } private void expand() { if (ExpansionMode != ExpansionMode.AlwaysContracted) iconsContainer.TransformSpacingTo(new Vector2(5, 0), 500, Easing.OutQuint); } private void contract() { if (ExpansionMode != ExpansionMode.AlwaysExpanded) iconsContainer.TransformSpacingTo(new Vector2(-25, 0), 500, Easing.OutQuint); } protected override bool OnHover(HoverEvent e) { expand(); return base.OnHover(e); } protected override void OnHoverLost(HoverLostEvent e) { contract(); base.OnHoverLost(e); } private partial class DrawableSinger : DrawableCircleSingerAvatar, IHasCustomTooltip { public ITooltip GetCustomTooltip() => new SingerToolTip(); public ISinger? TooltipContent => Singer; } } public enum ExpansionMode { /// /// The will expand only when hovered. /// ExpandOnHover, /// /// The will always be expanded. /// AlwaysExpanded, /// /// The will always be contracted. /// AlwaysContracted, } ================================================ FILE: osu.Game.Rulesets.Karaoke/Graphics/KaraokeIcon.cs ================================================ // Copyright (c) andy840119 . Licensed under the GPL Licence. // See the LICENCE file in the repository root for full licence text. using osu.Framework.Graphics.Sprites; using osu.Game.Graphics; namespace osu.Game.Rulesets.Karaoke.Graphics; public static class KaraokeIcon { public static IconUsage Get(int icon) => new((char)icon, "osuFont"); // ruleset icons in circles public static IconUsage RulesetKaraoke => FontAwesome.Solid.PlayCircle; // mod icons public static IconUsage ModDisableNote => FontAwesome.Solid.Eraser; public static IconUsage ModHiddenNote => OsuIcon.ModHidden; public static IconUsage ModHiddenRuby => FontAwesome.Solid.Gem; public static IconUsage ModPractice => FontAwesome.Solid.Music; public static IconUsage ModAutoPlayBySinger => FontAwesome.Solid.Music; } ================================================ FILE: osu.Game.Rulesets.Karaoke/Graphics/Shapes/CornerBackground.cs ================================================ // Copyright (c) andy840119 . Licensed under the GPL Licence. // See the LICENCE file in the repository root for full licence text. using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; namespace osu.Game.Rulesets.Karaoke.Graphics.Shapes; public partial class CornerBackground : CompositeDrawable { public CornerBackground() { Masking = true; CornerRadius = 5; AddInternal(new Box { RelativeSizeAxes = Axes.Both }); } } ================================================ FILE: osu.Game.Rulesets.Karaoke/Graphics/Shapes/RightTriangle.cs ================================================ // Copyright (c) andy840119 . Licensed under the GPL Licence. // See the LICENCE file in the repository root for full licence text. using System; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Primitives; using osu.Framework.Graphics.Rendering; using osu.Framework.Graphics.Sprites; using osuTK; namespace osu.Game.Rulesets.Karaoke.Graphics.Shapes; public partial class RightTriangle : Sprite { /// /// Creates a new right triangle with a white pixel as texture. /// public RightTriangle() { // Setting the texture would normally set a size of (1, 1), but since the texture is set from BDL it needs to be set here instead. // RelativeSizeAxes may not behave as expected if this is not done. Size = Vector2.One; } [BackgroundDependencyLoader] private void load(IRenderer renderer) { Texture ??= renderer.WhitePixel; } public override RectangleF BoundingBox => toTriangle(ToParentSpace(LayoutRectangle), RightAngleDirection).AABBFloat; private TriangleRightAngleDirection rightAngleDirection = TriangleRightAngleDirection.BottomLeft; public TriangleRightAngleDirection RightAngleDirection { get => rightAngleDirection; set { rightAngleDirection = value; Invalidate(); } } private static Triangle toTriangle(Quad q, TriangleRightAngleDirection rightAngleDirection) => rightAngleDirection switch { TriangleRightAngleDirection.TopLeft => new Triangle(q.TopLeft, q.TopRight, q.BottomLeft), TriangleRightAngleDirection.TopRight => new Triangle(q.TopLeft, q.TopRight, q.BottomRight), TriangleRightAngleDirection.BottomLeft => new Triangle(q.TopLeft, q.BottomLeft, q.BottomRight), TriangleRightAngleDirection.BottomRight => new Triangle(q.TopRight, q.BottomLeft, q.BottomRight), _ => throw new ArgumentOutOfRangeException(nameof(rightAngleDirection), rightAngleDirection, null), }; public override bool Contains(Vector2 screenSpacePos) => toTriangle(ScreenSpaceDrawQuad, RightAngleDirection).Contains(screenSpacePos); protected override DrawNode CreateDrawNode() => new TriangleDrawNode(this); private class TriangleDrawNode : SpriteDrawNode { protected new RightTriangle Source => (RightTriangle)base.Source; private TriangleRightAngleDirection rightAngleDirection; public TriangleDrawNode(RightTriangle source) : base(source) { } public override void ApplyState() { base.ApplyState(); rightAngleDirection = Source.RightAngleDirection; } protected override void Blit(IRenderer renderer) { if (DrawRectangle.Width == 0 || DrawRectangle.Height == 0) return; renderer.DrawTriangle(Texture, toTriangle(ScreenSpaceDrawQuad, rightAngleDirection), DrawColourInfo.Colour, null, null, new Vector2(InflationAmount.X / DrawRectangle.Width, InflationAmount.Y / DrawRectangle.Height), TextureCoords); } protected override void BlitOpaqueInterior(IRenderer renderer) { if (DrawRectangle.Width == 0 || DrawRectangle.Height == 0) return; var triangle = toTriangle(ConservativeScreenSpaceDrawQuad, rightAngleDirection); if (renderer.IsMaskingActive) renderer.DrawClipped(ref triangle, Texture, DrawColourInfo.Colour); else renderer.DrawTriangle(Texture, triangle, DrawColourInfo.Colour); } } } public enum TriangleRightAngleDirection { TopLeft, TopRight, BottomLeft, BottomRight, } ================================================ FILE: osu.Game.Rulesets.Karaoke/Graphics/Sprites/DisplayLyricProcessor.cs ================================================ // Copyright (c) andy840119 . Licensed under the GPL Licence. // See the LICENCE file in the repository root for full licence text. using System; using System.Collections.Generic; using osu.Framework.Graphics.Sprites; using osu.Game.Rulesets.Karaoke.Graphics.Sprites.Processor; using osu.Game.Rulesets.Karaoke.Objects; namespace osu.Game.Rulesets.Karaoke.Graphics.Sprites; public class DisplayLyricProcessor : IDisposable { public Action>? TopTextChanged; public Action? CenterTextChanged; public Action>? BottomTextChanged; public Action>? TimeTagsChanged; private BaseDisplayProcessor? processor; private readonly Lyric lyric; public DisplayLyricProcessor(Lyric lyric) { this.lyric = lyric; reloadProcessor(); } private LyricDisplayType displayType = LyricDisplayType.Lyric; public LyricDisplayType DisplayType { get => displayType; set { if (displayType == value) return; displayType = value; reloadProcessor(); } } private LyricDisplayProperty displayProperty = LyricDisplayProperty.Both; public LyricDisplayProperty DisplayProperty { get => displayProperty; set { if (displayProperty == value) return; displayProperty = value; reloadProcessor(); } } private void reloadProcessor() { // re-create the processor. processor?.Dispose(); processor = GetLyricDisplayProcessor(lyric, DisplayType, DisplayProperty); processor = DisplayType switch { LyricDisplayType.Lyric => new LyricFirstDisplayProcessor(lyric, DisplayProperty), LyricDisplayType.RomanisedSyllable => new RomanisedSyllableFirstDisplayProcessor(lyric, DisplayProperty), _ => throw new ArgumentOutOfRangeException(), }; // pass the change event. processor.TopTextChanged = x => TopTextChanged?.Invoke(x); processor.CenterTextChanged = x => CenterTextChanged?.Invoke(x); processor.BottomTextChanged = x => BottomTextChanged?.Invoke(x); processor.TimeTagsChanged = x => TimeTagsChanged?.Invoke(x); // trigger update all after update the processor. UpdateAll(); } /// /// Should call this method after Action is bind outside. /// public void UpdateAll() { if (processor == null) throw new InvalidOperationException("Processor should not be null."); processor.UpdateAll(); // should trigger top text update even not display. if (!displayProperty.HasFlag(LyricDisplayProperty.TopText)) { TopTextChanged?.Invoke(Array.Empty()); } // should trigger bottom text update even not display. if (!displayProperty.HasFlag(LyricDisplayProperty.BottomText)) { BottomTextChanged?.Invoke(Array.Empty()); } } public void Dispose() { processor?.Dispose(); } public static BaseDisplayProcessor GetLyricDisplayProcessor(Lyric lyric, LyricDisplayType displayType, LyricDisplayProperty displayProperty) => displayType switch { LyricDisplayType.Lyric => new LyricFirstDisplayProcessor(lyric, displayProperty), LyricDisplayType.RomanisedSyllable => new RomanisedSyllableFirstDisplayProcessor(lyric, displayProperty), _ => throw new ArgumentOutOfRangeException(), }; } ================================================ FILE: osu.Game.Rulesets.Karaoke/Graphics/Sprites/DrawableKaraokeSpriteText.cs ================================================ // Copyright (c) andy840119 . Licensed under the GPL Licence. // See the LICENCE file in the repository root for full licence text. using osu.Framework.Allocation; using osu.Framework.Graphics.Shaders; using osu.Framework.Graphics.Sprites; using osu.Game.Rulesets.Karaoke.Objects; using osu.Game.Rulesets.Karaoke.Skinning.Tools; namespace osu.Game.Rulesets.Karaoke.Graphics.Sprites; public partial class DrawableKaraokeSpriteText : DrawableKaraokeSpriteText { public DrawableKaraokeSpriteText(Lyric lyric) : base(lyric) { } } public abstract partial class DrawableKaraokeSpriteText : KaraokeSpriteText where TSpriteText : LyricSpriteText, new() { private readonly DisplayLyricProcessor processor; [Resolved] private ShaderManager? shaderManager { get; set; } protected DrawableKaraokeSpriteText(Lyric lyric) { processor = new DisplayLyricProcessor(lyric) { TopTextChanged = topTexts => { TopTexts = topTexts; OnPropertyChanged(); }, CenterTextChanged = text => { Text = text; OnPropertyChanged(); }, BottomTextChanged = bottomTexts => { BottomTexts = bottomTexts; OnPropertyChanged(); }, TimeTagsChanged = timeTags => { TimeTags = timeTags; OnPropertyChanged(); }, }; processor.UpdateAll(); } public LyricDisplayType DisplayType { get => processor.DisplayType; set => processor.DisplayType = value; } public LyricDisplayProperty DisplayProperty { get => processor.DisplayProperty; set => processor.DisplayProperty = value; } // not a good practice but child class need to know the property changed. protected virtual void OnPropertyChanged() { } protected override void Dispose(bool isDisposing) { base.Dispose(isDisposing); processor.Dispose(); } public void UpdateStyle(LyricStyle style) { // for prevent issue Collection was modified; enumeration operation may not execute. Schedule(() => { LeftLyricTextShaders = SkinConverterTool.ConvertLeftSideShader(shaderManager, style); RightLyricTextShaders = SkinConverterTool.ConvertRightSideShader(shaderManager, style); }); } } ================================================ FILE: osu.Game.Rulesets.Karaoke/Graphics/Sprites/DrawableLyricSpriteText.cs ================================================ // Copyright (c) andy840119 . Licensed under the GPL Licence. // See the LICENCE file in the repository root for full licence text. using osu.Framework.Graphics.Sprites; using osu.Game.Rulesets.Karaoke.Objects; namespace osu.Game.Rulesets.Karaoke.Graphics.Sprites; public partial class DrawableLyricSpriteText : LyricSpriteText { private readonly DisplayLyricProcessor processor; public DrawableLyricSpriteText(Lyric lyric) { processor = new DisplayLyricProcessor(lyric) { TopTextChanged = topTexts => { TopTexts = topTexts; }, CenterTextChanged = text => { Text = text; }, BottomTextChanged = bottomTexts => { BottomTexts = bottomTexts; }, }; processor.UpdateAll(); } public LyricDisplayType DisplayType { get => processor.DisplayType; set => processor.DisplayType = value; } public LyricDisplayProperty DisplayProperty { get => processor.DisplayProperty; set => processor.DisplayProperty = value; } protected override void Dispose(bool isDisposing) { base.Dispose(isDisposing); processor.Dispose(); } } ================================================ FILE: osu.Game.Rulesets.Karaoke/Graphics/Sprites/LyricDisplayProperty.cs ================================================ // Copyright (c) andy840119 . Licensed under the GPL Licence. // See the LICENCE file in the repository root for full licence text. using osu.Game.Rulesets.Karaoke.Objects; namespace osu.Game.Rulesets.Karaoke.Graphics.Sprites; public enum LyricDisplayProperty { /// /// Display main text only. /// None = 1 << 0, /// /// Display as top text. /// TopText = 1 << 1, /// /// Display bottom text. /// /// /// Display the as bottom text if .
/// Display the as bottom text if .
///
BottomText = 1 << 2, /// /// Display both top and bottom text. /// Both = TopText | BottomText, } ================================================ FILE: osu.Game.Rulesets.Karaoke/Graphics/Sprites/LyricDisplayType.cs ================================================ // Copyright (c) andy840119 . Licensed under the GPL Licence. // See the LICENCE file in the repository root for full licence text. using osu.Game.Rulesets.Karaoke.Objects; namespace osu.Game.Rulesets.Karaoke.Graphics.Sprites; public enum LyricDisplayType { /// /// Display the lyric as center of the text. /// /// /// Top:
/// Center:
/// Bottom:
///
Lyric, /// /// Display the romanised lyric as center of the text. /// /// /// Top:
/// Center:
/// Bottom:
///
RomanisedSyllable, } ================================================ FILE: osu.Game.Rulesets.Karaoke/Graphics/Sprites/LyricStyle.cs ================================================ // Copyright (c) andy840119 . Licensed under the GPL Licence. // See the LICENCE file in the repository root for full licence text. using System; using System.Collections.Generic; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics.Shaders; using osuTK; namespace osu.Game.Rulesets.Karaoke.Graphics.Sprites; public class LyricStyle { public static LyricStyle CreateDefault() => new() { LeftLyricTextShaders = new ICustomizedShader[] { new StepShader { Name = "Step shader", StepShaders = new ICustomizedShader[] { new OutlineShader { Radius = 3, OutlineColour = Color4Extensions.FromHex("#CCA532"), }, new ShadowShader { ShadowColour = Color4Extensions.FromHex("#6B5B2D"), ShadowOffset = new Vector2(3), }, }, }, }, RightLyricTextShaders = new ICustomizedShader[] { new StepShader { Name = "Step shader", StepShaders = new ICustomizedShader[] { new OutlineShader { Radius = 3, OutlineColour = Color4Extensions.FromHex("#5932CC"), }, new ShadowShader { ShadowColour = Color4Extensions.FromHex("#3D2D6B"), ShadowOffset = new Vector2(3), }, }, }, }, }; public IReadOnlyList LeftLyricTextShaders = Array.Empty(); public IReadOnlyList RightLyricTextShaders = Array.Empty(); } ================================================ FILE: osu.Game.Rulesets.Karaoke/Graphics/Sprites/Processor/BaseDisplayProcessor.cs ================================================ // Copyright (c) andy840119 . Licensed under the GPL Licence. // See the LICENCE file in the repository root for full licence text. using System; using System.Collections.Generic; using System.Linq; using osu.Framework.Bindables; using osu.Framework.Graphics.Sprites; using osu.Game.Rulesets.Karaoke.Objects; namespace osu.Game.Rulesets.Karaoke.Graphics.Sprites.Processor; public abstract class BaseDisplayProcessor : IDisposable { public Action>? TopTextChanged; public Action? CenterTextChanged; public Action>? BottomTextChanged; public Action>? TimeTagsChanged; private readonly Lyric lyric; private readonly LyricDisplayProperty displayProperty; private readonly AvailableProperty availableProperty; protected BaseDisplayProcessor(Lyric lyric, LyricDisplayProperty displayProperty) { this.lyric = lyric; this.displayProperty = displayProperty; availableProperty = new AvailableProperty(lyric); ProcessBindableChange(availableProperty); } protected abstract void ProcessBindableChange(AvailableProperty property); public void UpdateAll() { UpdateTopText(); UpdateCenterText(); UpdateBottomText(); UpdateTimeTags(); } protected void UpdateTopText() { if (!displayProperty.HasFlag(LyricDisplayProperty.TopText)) return; TopTextChanged?.Invoke(CalculateTopTexts(lyric).ToArray()); } protected void UpdateCenterText() { CenterTextChanged?.Invoke(CalculateCenterText(lyric)); } protected void UpdateBottomText() { if (!displayProperty.HasFlag(LyricDisplayProperty.BottomText)) return; BottomTextChanged?.Invoke(CalculateBottomTexts(lyric).ToArray()); } protected void UpdateTimeTags() { TimeTagsChanged?.Invoke(CalculateTimeTags(lyric)); } protected abstract IEnumerable CalculateTopTexts(Lyric lyric); protected abstract string CalculateCenterText(Lyric lyric); protected abstract IEnumerable CalculateBottomTexts(Lyric lyric); protected abstract IReadOnlyDictionary CalculateTimeTags(Lyric lyric); public void Dispose() { availableProperty.Dispose(); } protected class AvailableProperty : IDisposable { public readonly IBindableList RubyTagsBindable = new BindableList(); public readonly IBindable RubyTagsVersion = new Bindable(); public readonly IBindable TextBindable = new Bindable(); public readonly IBindableList TimeTagsBindable = new BindableList(); public readonly IBindable TimeTagsRomanisationVersion = new Bindable(); public readonly IBindable TimeTagsTimingVersion = new Bindable(); public AvailableProperty(Lyric lyric) { RubyTagsBindable.BindTo(lyric.RubyTagsBindable); RubyTagsVersion.BindTo(lyric.RubyTagsVersion); TextBindable.BindTo(lyric.TextBindable); TimeTagsBindable.BindTo(lyric.TimeTagsBindable); TimeTagsRomanisationVersion.BindTo(lyric.TimeTagsRomanisationVersion); TimeTagsTimingVersion.BindTo(lyric.TimeTagsTimingVersion); } public void Dispose() { RubyTagsBindable.UnbindAll(); RubyTagsVersion.UnbindAll(); TextBindable.UnbindAll(); TimeTagsBindable.UnbindAll(); TimeTagsRomanisationVersion.UnbindAll(); TimeTagsTimingVersion.UnbindAll(); } } } ================================================ FILE: osu.Game.Rulesets.Karaoke/Graphics/Sprites/Processor/LyricFirstDisplayProcessor.cs ================================================ // Copyright (c) andy840119 . Licensed under the GPL Licence. // See the LICENCE file in the repository root for full licence text. using System.Collections.Generic; using System.Linq; using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Graphics.Sprites; using osu.Game.Rulesets.Karaoke.Objects; using osu.Game.Rulesets.Karaoke.Objects.Utils; using osu.Game.Rulesets.Karaoke.Utils; namespace osu.Game.Rulesets.Karaoke.Graphics.Sprites.Processor; public class LyricFirstDisplayProcessor : BaseDisplayProcessor { public LyricFirstDisplayProcessor(Lyric lyric, LyricDisplayProperty displayProperty) : base(lyric, displayProperty) { } protected override void ProcessBindableChange(AvailableProperty property) { property.RubyTagsBindable.BindCollectionChanged((_, _) => { // Ruby only display at top. UpdateTopText(); }); property.RubyTagsVersion.BindValueChanged(_ => { // Ruby only display at top. UpdateTopText(); }); property.TextBindable.BindValueChanged(_ => { UpdateAll(); }); property.TimeTagsBindable.BindCollectionChanged((_, _) => { // If create/remove the time-tag, romanised syllable might be affected. UpdateBottomText(); UpdateTimeTags(); }); property.TimeTagsRomanisationVersion.BindValueChanged(_ => { UpdateBottomText(); }); property.TimeTagsTimingVersion.BindValueChanged(_ => { UpdateTimeTags(); }); } protected override IEnumerable CalculateTopTexts(Lyric lyric) { return lyric.RubyTags.Select(toPositionText); static PositionText toPositionText(RubyTag rubyTag) => new(rubyTag.Text, rubyTag.StartIndex, rubyTag.EndIndex); } protected override string CalculateCenterText(Lyric lyric) => lyric.Text; protected override IEnumerable CalculateBottomTexts(Lyric lyric) { var startTimeTag = lyric.TimeTags.FirstOrDefault(); if (startTimeTag == null) yield break; string collectedRomanisedSyllable = string.Empty; // split the text by first romanisation syllable. foreach (var timeTag in lyric.TimeTags) { // collecting the romanised syllable. collectedRomanisedSyllable += timeTag.RomanisedSyllable; if (lyric.TimeTags.Last() == timeTag) { // should return the collected romanised syllable if is the last one. yield return toPositionText(startTimeTag.Index, timeTag.Index, collectedRomanisedSyllable); } else if (lyric.TimeTags.GetNext(timeTag).FirstSyllable) { // should return the collected romanised syllable before timeTag with first syllable. yield return toPositionText(startTimeTag.Index, timeTag.Index, collectedRomanisedSyllable); startTimeTag = lyric.TimeTags.GetNext(timeTag); collectedRomanisedSyllable = string.Empty; } } yield break; static PositionText toPositionText(TextIndex startIndex, TextIndex endIndex, string text) { int startCharIndex = TextIndexUtils.ToCharIndex(startIndex); int endCharIndex = TextIndexUtils.ToCharIndex(endIndex); return new PositionText(text, startCharIndex, endCharIndex); } } protected override IReadOnlyDictionary CalculateTimeTags(Lyric lyric) { return TimeTagsUtils.ToTimeBasedDictionary(lyric.TimeTags); } } ================================================ FILE: osu.Game.Rulesets.Karaoke/Graphics/Sprites/Processor/RomanisedSyllableFirstDisplayProcessor.cs ================================================ // Copyright (c) andy840119 . Licensed under the GPL Licence. // See the LICENCE file in the repository root for full licence text. using System.Collections.Generic; using System.Linq; using osu.Framework.Graphics.Sprites; using osu.Game.Rulesets.Karaoke.Objects; namespace osu.Game.Rulesets.Karaoke.Graphics.Sprites.Processor; public class RomanisedSyllableFirstDisplayProcessor : BaseDisplayProcessor { public RomanisedSyllableFirstDisplayProcessor(Lyric lyric, LyricDisplayProperty displayProperty) : base(lyric, displayProperty) { // Note: some of the properties are not implemented yet because we are not sure people actually use it. } protected override void ProcessBindableChange(AvailableProperty property) { property.RubyTagsBindable.BindCollectionChanged((_, _) => { // Ruby only display at top. UpdateTopText(); }); property.RubyTagsVersion.BindValueChanged(_ => { // Ruby only display at top. UpdateTopText(); }); property.TextBindable.BindValueChanged(_ => { // Text only display at bottom. UpdateBottomText(); }); property.TimeTagsBindable.BindCollectionChanged((_, _) => { // Ruby change might affect the center text, which will affect all property. UpdateAll(); }); property.TimeTagsRomanisationVersion.BindValueChanged(_ => { // Ruby change might affect the center text, which will affect all property. UpdateAll(); }); property.TimeTagsTimingVersion.BindValueChanged(_ => { UpdateTimeTags(); }); } protected override IEnumerable CalculateTopTexts(Lyric lyric) { // todo: implementation needed. yield break; } protected override string CalculateCenterText(Lyric lyric) => string.Join("", lyric.TimeTags.Select((x, i) => { bool hasEmptySpace = i != 0 && x.FirstSyllable; return hasEmptySpace ? " " + x.RomanisedSyllable : x.RomanisedSyllable; })); protected override IEnumerable CalculateBottomTexts(Lyric lyric) { // todo: implementation needed. yield break; } protected override IReadOnlyDictionary CalculateTimeTags(Lyric lyric) { // todo: implementation needed. return new Dictionary(); } } ================================================ FILE: osu.Game.Rulesets.Karaoke/Graphics/UserInterface/BindableBoolMenuItem.cs ================================================ // Copyright (c) andy840119 . Licensed under the GPL Licence. // See the LICENCE file in the repository root for full licence text. using osu.Framework.Bindables; using osu.Game.Graphics.UserInterface; namespace osu.Game.Rulesets.Karaoke.Graphics.UserInterface; public class BindableBoolMenuItem : ToggleMenuItem { public BindableBoolMenuItem(string text, Bindable bindable) : base(text) { State.BindTo(bindable); } } ================================================ FILE: osu.Game.Rulesets.Karaoke/Graphics/UserInterface/BindableEnumMenuItem.cs ================================================ // Copyright (c) andy840119 . Licensed under the GPL Licence. // See the LICENCE file in the repository root for full licence text. using System; using System.Collections.Generic; using System.Linq; using osu.Framework.Bindables; using osu.Framework.Extensions; using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Graphics.UserInterface; using osu.Game.Graphics.UserInterface; namespace osu.Game.Rulesets.Karaoke.Graphics.UserInterface; public class BindableEnumMenuItem : MenuItem where T : struct, Enum { private readonly Bindable bindableEnum = new(); public BindableEnumMenuItem(string text, Bindable bindable) : base(text) { Items = createMenuItems(); bindableEnum.BindTo(bindable); bindableEnum.BindValueChanged(e => { var newSelection = e.NewValue; Items.OfType().ForEach(x => { bool match = x.Text.Value == GetName(newSelection); x.State.Value = match; }); }, true); } private ToggleMenuItem[] createMenuItems() { return ValidEnums.Select(e => { var item = new ToggleMenuItem(GetName(e), MenuItemType.Standard, _ => UpdateSelection(e)); return item; }).ToArray(); } protected virtual IEnumerable ValidEnums => Enum.GetValues(); protected string GetName(T selection) => selection.GetDescription(); protected virtual void UpdateSelection(T selection) { bindableEnum.Value = selection; } } ================================================ FILE: osu.Game.Rulesets.Karaoke/Graphics/UserInterfaceV2/FontSelector.cs ================================================ // Copyright (c) andy840119 . Licensed under the GPL Licence. // See the LICENCE file in the repository root for full licence text. using System; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Rendering; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.UserInterface; using osu.Framework.IO.Stores; using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; using osu.Game.Rulesets.Karaoke.Bindables; using osu.Game.Rulesets.Karaoke.Graphics.Containers; using osu.Game.Rulesets.Karaoke.Graphics.Shapes; using osu.Game.Rulesets.Karaoke.IO.Stores; using osu.Game.Rulesets.Karaoke.Skinning.Fonts; using osu.Game.Rulesets.Karaoke.Utils; using osuTK.Graphics; namespace osu.Game.Rulesets.Karaoke.Graphics.UserInterfaceV2; public partial class FontSelector : CompositeDrawable, IHasCurrentValue { private readonly SpriteText previewText; private readonly FontFamilyPropertyList familyProperty; private readonly FontPropertyList weightProperty; private readonly FontPropertyList fontSizeProperty; private readonly OsuCheckbox fixedWidthCheckbox; private readonly BindableWithCurrent current = new(); private readonly BindableList fonts = new(); [Resolved] private FontStore fontStore { get; set; } = null!; private KaraokeLocalFontStore localFontStore = null!; public Bindable Current { get => current.Current; set { current.Current = value; // should calculate available size until has bindable text. fontSizeProperty.Items.Clear(); if (value is BindableFontUsage bindableFontUsage) { fontSizeProperty.Items.AddRange(FontUtils.DefaultFontSize(bindableFontUsage.MinFontSize, bindableFontUsage.MaxFontSize)); } else { fontSizeProperty.Items.AddRange(FontUtils.DefaultFontSize()); } } } public FontSelector() { InternalChild = new GridContainer { RelativeSizeAxes = Axes.Both, RowDimensions = new[] { new Dimension(GridSizeMode.Relative, 0.4f), new Dimension(), }, Content = new[] { new Drawable[] { previewText = new OsuSpriteText { Anchor = Anchor.Centre, Origin = Anchor.Centre, Text = "カラオケ, karaoke", }, }, new Drawable[] { new Container { Padding = new MarginPadding(10), RelativeSizeAxes = Axes.Both, Child = new GridContainer { RelativeSizeAxes = Axes.Both, ColumnDimensions = new[] { new Dimension(GridSizeMode.Relative, 0.5f), new Dimension(GridSizeMode.Relative, 0.3f), new Dimension(GridSizeMode.Relative, 0.2f), }, Content = new[] { new Drawable[] { familyProperty = new FontFamilyPropertyList { Name = "Font family selection area", RelativeSizeAxes = Axes.Both, }, weightProperty = new FontPropertyList { Name = "Font widget selection area", RelativeSizeAxes = Axes.Both, }, new GridContainer { RelativeSizeAxes = Axes.Both, RowDimensions = new[] { new Dimension(), new Dimension(GridSizeMode.Absolute, 48), }, Content = new[] { new Drawable[] { fontSizeProperty = new FontPropertyList { Name = "Font size selection area", RelativeSizeAxes = Axes.Both, }, }, new Drawable[] { fixedWidthCheckbox = new OsuCheckbox { Name = "Font fixed width selection area", RelativeSizeAxes = Axes.X, Padding = new MarginPadding(10), LabelText = "FixedWidth", }, }, }, }, }, }, }, }, }, }, }; fonts.BindCollectionChanged((_, b) => { // re-calculate if source changed. Schedule(() => { string[]? oldFamilies = b.OldItems?.OfType().Select(x => x.Family).Distinct().ToArray(); string[]? newFamilies = b.NewItems?.OfType().Select(x => x.Family).Distinct().ToArray(); if (oldFamilies != null) { familyProperty.Items.RemoveAll(x => oldFamilies.Contains(x)); } if (newFamilies != null) { familyProperty.Items.AddRange(newFamilies); } // should reset family selection if user select the font that will be removed or added. string? currentFamily = familyProperty.Current.Value; bool resetFamily = oldFamilies?.Contains(currentFamily) ?? false; if (resetFamily) { familyProperty.Current.Value = familyProperty.Items.FirstOrDefault(); } }); }); familyProperty.Current.BindValueChanged(x => { performChange(); // re-calculate if family changed. string[] weight = fonts.Where(f => f.Family == x.NewValue).Select(f => f.Weight).OfType().Distinct().ToArray(); weightProperty.Items.Clear(); weightProperty.Items.AddRange(weight); // set to first or empty if change new family. weightProperty.Current.Value = weight.FirstOrDefault(); }); weightProperty.Current.BindValueChanged(_ => performChange()); fontSizeProperty.Current.BindValueChanged(_ => performChange()); fixedWidthCheckbox.Current.BindValueChanged(_ => performChange()); } [BackgroundDependencyLoader] private void load(FontManager fontManager, IRenderer renderer) { fonts.BindTo(fontManager.Fonts); // create local font store and import those files localFontStore = new KaraokeLocalFontStore(fontManager, renderer); fontStore.AddStore(localFontStore); Current.BindValueChanged(e => { var newFont = e.NewValue; familyProperty.Current.Value = newFont.Family; weightProperty.Current.Value = newFont.Weight; fontSizeProperty.Current.Value = newFont.Size; fixedWidthCheckbox.Current.Value = newFont.FixedWidth; }, true); } private void performChange() { var fontUsage = generateFontUsage(); // add font to local font store for preview purpose. localFontStore.ClearFont(); localFontStore.AddFont(fontUsage); previewText.Font = fontUsage; // write-back the value. Current.Value = fontUsage; } private FontUsage generateFontUsage() { string? family = familyProperty.Current.Value; string? weight = weightProperty.Current.Value; float size = fontSizeProperty.Current.Value; bool fixedWidth = fixedWidthCheckbox.Current.Value; return new FontUsage(family, size, weight, false, fixedWidth); } protected override void Dispose(bool isDisposing) { base.Dispose(isDisposing); fontStore.RemoveStore(localFontStore); } internal partial class FontFamilyPropertyList : FontPropertyList { protected override RearrangeableTextFlowListContainer CreateRearrangeableListContainer() => new RearrangeableFontFamilyListContainer(); private partial class RearrangeableFontFamilyListContainer : RearrangeableTextFlowListContainer { protected override DrawableTextListItem CreateDrawable(string? item) => new DrawableFontFamilyListItem(item); private partial class DrawableFontFamilyListItem : DrawableTextListItem { [Resolved] private FontManager fontManager { get; set; } = null!; public DrawableFontFamilyListItem(string? item) : base(item) { } protected override void CreateDisplayContent(OsuTextFlowContainer textFlowContainer, string? model) { textFlowContainer.TextAnchor = Anchor.BottomLeft; textFlowContainer.AddText(model ?? string.Empty); var matchedFormat = fontManager.Fonts .Where(x => x.Family == Model).Select(x => x.FontFormat) .Distinct() .ToArray(); foreach (var format in matchedFormat) { textFlowContainer.AddText(" "); textFlowContainer.AddArbitraryDrawable(new FontFormatBadge(format)); } } } } private partial class FontFormatBadge : CompositeDrawable { private readonly FontFormat fontFormat; private readonly Box box; private readonly OsuSpriteText badgeText; public FontFormatBadge(FontFormat fontFormat) { this.fontFormat = fontFormat; AutoSizeAxes = Axes.Both; Masking = true; CornerRadius = 3; InternalChildren = new Drawable[] { box = new Box { RelativeSizeAxes = Axes.Both, }, badgeText = new OsuSpriteText { Font = OsuFont.Default.With(size: 10), Margin = new MarginPadding { Vertical = 1, Horizontal = 3, }, }, }; } [BackgroundDependencyLoader] private void load(OsuColour colours) { box.Colour = fontFormat switch { FontFormat.Internal => colours.Gray7, FontFormat.Fnt => colours.Pink, FontFormat.Ttf => colours.Blue, _ => throw new ArgumentOutOfRangeException(nameof(fontFormat)), }; // todo : might apply translation. badgeText.Text = fontFormat.ToString(); } } } internal partial class FontPropertyList : CompositeDrawable { private readonly CornerBackground background; private readonly TextPropertySearchTextBox filter; private readonly RearrangeableTextFlowListContainer propertyFlowList; private readonly BindableWithCurrent current = new(); public Bindable Current { get => current.Current; set => current.Current = value; } public BindableList Items => propertyFlowList.Items; public FontPropertyList() { InternalChild = new Container { Padding = new MarginPadding(10), RelativeSizeAxes = Axes.Both, Children = new Drawable[] { background = new CornerBackground { RelativeSizeAxes = Axes.Both, }, new GridContainer { RelativeSizeAxes = Axes.Both, RowDimensions = new[] { new Dimension(GridSizeMode.Absolute, 40), new Dimension(), }, Content = new[] { new Drawable[] { filter = new TextPropertySearchTextBox { RelativeSizeAxes = Axes.X, }, }, new Drawable[] { propertyFlowList = CreateRearrangeableListContainer().With(x => { x.RelativeSizeAxes = Axes.Both; x.RequestSelection = item => { Current.Value = item; }; }), }, }, }, }, }; filter.Current.BindValueChanged(e => propertyFlowList.Filter(e.NewValue)); Current.BindValueChanged(e => propertyFlowList.SelectedSet.Value = e.NewValue); } protected virtual RearrangeableTextFlowListContainer CreateRearrangeableListContainer() => new(); [BackgroundDependencyLoader] private void load(OsuColour colours) { background.Colour = colours.ContextMenuGray; } private partial class TextPropertySearchTextBox : SearchTextBox { protected override Color4 SelectionColour => Color4.Gray; public TextPropertySearchTextBox() { PlaceholderText = "Search..."; } } } } ================================================ FILE: osu.Game.Rulesets.Karaoke/Graphics/UserInterfaceV2/LabelledColourSelector.cs ================================================ // Copyright (c) andy840119 . Licensed under the GPL Licence. // See the LICENCE file in the repository root for full licence text. using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.UserInterface; using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterfaceV2; using osuTK; namespace osu.Game.Rulesets.Karaoke.Graphics.UserInterfaceV2; // refactor this shit public partial class LabelledColourSelector : LabelledComponent { public LabelledColourSelector() : base(true) { } protected override ColourSelectorDisplay CreateComponent() => new(); public partial class ColourSelectorDisplay : CompositeDrawable, IHasCurrentValue, IHasPopover { private readonly BindableWithCurrent current = new(); private Box fill = null!; private OsuSpriteText colourHexCode = null!; public Bindable Current { get => current.Current; set => current.Current = value; } [BackgroundDependencyLoader] private void load(OsuColour colours) { AutoSizeAxes = Axes.Y; RelativeSizeAxes = Axes.X; InternalChild = new FillFlowContainer { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, Direction = FillDirection.Vertical, Spacing = new Vector2(0, 10), Children = new Drawable[] { new OsuClickableContainer { RelativeSizeAxes = Axes.X, Height = 60, CornerRadius = 10, Masking = true, BorderThickness = 2f, BorderColour = colours.Gray5, Children = new Drawable[] { fill = new Box { RelativeSizeAxes = Axes.Both, }, colourHexCode = new OsuSpriteText { Anchor = Anchor.Centre, Origin = Anchor.Centre, Font = OsuFont.Default.With(size: 12), }, }, Action = this.ShowPopover, }, }, }; } protected override void LoadComplete() { base.LoadComplete(); current.BindValueChanged(_ => updateColour(), true); } private void updateColour() { fill.Colour = current.Value; colourHexCode.Text = current.Value.ToHex(); colourHexCode.Colour = OsuColour.ForegroundTextColourFor(current.Value); } public Popover GetPopover() => new OsuPopover(false) { Child = new OsuColourPicker { Current = { BindTarget = Current }, }, }; } } ================================================ FILE: osu.Game.Rulesets.Karaoke/Graphics/UserInterfaceV2/LabelledHueSelector.cs ================================================ // Copyright (c) andy840119 . Licensed under the GPL Licence. // See the LICENCE file in the repository root for full licence text. using System; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Effects; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.UserInterface; using osu.Game.Graphics.UserInterfaceV2; using osuTK; namespace osu.Game.Rulesets.Karaoke.Graphics.UserInterfaceV2; public partial class LabelledHueSelector : LabelledComponent { public LabelledHueSelector() : base(true) { } protected override OsuHueSelector CreateComponent() => new(); private static EdgeEffectParameters createShadowParameters() => new() { Type = EdgeEffectType.Shadow, Offset = new Vector2(0, 1), Radius = 3, Colour = Colour4.Black.Opacity(0.3f), }; /// /// Copied from /// public partial class OsuHueSelector : HSVColourPicker.HueSelector, IHasCurrentValue { private const float corner_radius = 10; private const float control_border_thickness = 3; public Bindable Current { get => Hue; set { ArgumentNullException.ThrowIfNull(value); Hue.UnbindBindings(); Hue.BindTo(value); } } public OsuHueSelector() { SliderBar.CornerRadius = corner_radius; SliderBar.Masking = true; } protected override Drawable CreateSliderNub() => new SliderNub(this); private partial class SliderNub : CompositeDrawable { private readonly Bindable hue; private readonly Box fill; public SliderNub(OsuHueSelector osuHueSelector) { hue = osuHueSelector.Hue.GetBoundCopy(); InternalChild = new CircularContainer { Height = 35, Width = 10, Origin = Anchor.Centre, Anchor = Anchor.Centre, Masking = true, BorderColour = Colour4.White, BorderThickness = control_border_thickness, EdgeEffect = createShadowParameters(), Child = fill = new Box { RelativeSizeAxes = Axes.Both, }, }; } protected override void LoadComplete() { hue.BindValueChanged(h => fill.Colour = Colour4.FromHSV(h.NewValue, 1, 1), true); } } } } ================================================ FILE: osu.Game.Rulesets.Karaoke/Graphics/UserInterfaceV2/LabelledImageSelector.cs ================================================ // Copyright (c) andy840119 . Licensed under the GPL Licence. // See the LICENCE file in the repository root for full licence text. using osu.Game.Graphics.UserInterfaceV2; namespace osu.Game.Rulesets.Karaoke.Graphics.UserInterfaceV2; /// /// A labelled text box which reveals an inline file chooser when clicked. /// Will be replaced after has official one. /// public partial class LabelledImageSelector : LabelledTextBox; ================================================ FILE: osu.Game.Rulesets.Karaoke/Graphics/UserInterfaceV2/LabelledRealTimeSliderBar.cs ================================================ // Copyright (c) andy840119 . Licensed under the GPL Licence. // See the LICENCE file in the repository root for full licence text. using System.Numerics; using osu.Framework.Graphics; using osu.Game.Graphics.UserInterfaceV2; using osu.Game.Overlays.Settings; namespace osu.Game.Rulesets.Karaoke.Graphics.UserInterfaceV2; public partial class LabelledRealTimeSliderBar : LabelledSliderBar where TNumber : struct, INumber, IMinMaxValue { protected override SettingsSlider CreateComponent() => base.CreateComponent().With(x => x.TransferValueOnCommit = false); } ================================================ FILE: osu.Game.Rulesets.Karaoke/Graphics/UserInterfaceV2/LanguageSelector.cs ================================================ // Copyright (c) andy840119 . Licensed under the GPL Licence. // See the LICENCE file in the repository root for full licence text. using System.Collections.Generic; using System.Globalization; using System.Linq; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.UserInterface; using osu.Framework.Input.Events; using osu.Framework.Localisation; using osu.Game.Graphics.Containers; using osu.Game.Graphics.UserInterface; using osu.Game.Rulesets.Karaoke.Graphics.Containers; using osu.Game.Rulesets.Karaoke.Utils; using osuTK.Graphics; namespace osu.Game.Rulesets.Karaoke.Graphics.UserInterfaceV2; public partial class LanguageSelector : CompositeDrawable, IHasCurrentValue { private readonly LanguageSelectionSearchTextBox filter; private readonly RearrangeableLanguageListContainer languageList; private readonly BindableWithCurrent current = new(); public Bindable Current { get => current.Current; set => current.Current = value; } public override bool AcceptsFocus => true; public override bool RequestsFocus => true; public LanguageSelector() { InternalChild = new GridContainer { RelativeSizeAxes = Axes.Both, RowDimensions = new[] { new Dimension(GridSizeMode.Absolute, 40), new Dimension(), }, Content = new[] { new Drawable[] { filter = new LanguageSelectionSearchTextBox { RelativeSizeAxes = Axes.X, }, }, new Drawable[] { languageList = new RearrangeableLanguageListContainer { RelativeSizeAxes = Axes.Both, RequestSelection = item => { Current.Value = item.CultureInfo; }, }, }, }, }; filter.Current.BindValueChanged(e => languageList.Filter(e.NewValue)); Current.BindValueChanged(e => { // we need to wait until language list loaded. Schedule(() => { languageList.SelectedSet.Value = new LanguageModel(e.NewValue); }); }, true); reloadLanguageList(); } protected override void OnFocus(FocusEvent e) { base.OnFocus(e); GetContainingFocusManager().ChangeFocus(filter); } private bool enableEmptyOption; public bool EnableEmptyOption { get => enableEmptyOption; set { enableEmptyOption = value; reloadLanguageList(); } } private void reloadLanguageList() { var languages = CultureInfoUtils.GetAvailableLanguages().Select(x => new LanguageModel(x)); languageList.Items.Clear(); if (EnableEmptyOption) { languageList.Items.Insert(0, new LanguageModel(null)); } languageList.Items.AddRange(languages); } private partial class LanguageSelectionSearchTextBox : SearchTextBox { protected override Color4 SelectionColour => Color4.Gray; public LanguageSelectionSearchTextBox() { PlaceholderText = "type in keywords..."; } } private partial class RearrangeableLanguageListContainer : RearrangeableTextFlowListContainer { protected override DrawableTextListItem CreateDrawable(LanguageModel item) => new DrawableLanguageListItem(item); private partial class DrawableLanguageListItem : DrawableTextListItem { public DrawableLanguageListItem(LanguageModel item) : base(item) { } public override IEnumerable FilterTerms { get { var cultureInfo = Model.CultureInfo; yield return new LocalisableString(CultureInfoUtils.GetLanguageDisplayText(cultureInfo)); if (cultureInfo == null) { yield return new LocalisableString(string.Empty); } else { yield return new LocalisableString(cultureInfo.Name); yield return new LocalisableString(cultureInfo.DisplayName); yield return new LocalisableString(cultureInfo.EnglishName); } } } protected override void CreateDisplayContent(OsuTextFlowContainer textFlowContainer, LanguageModel model) { textFlowContainer.AddText(CultureInfoUtils.GetLanguageDisplayText(model.CultureInfo)); } } } /// /// use this struct to warp the because is not able support null value. /// private struct LanguageModel { public LanguageModel(CultureInfo? cultureInfo) { CultureInfo = cultureInfo; } public CultureInfo? CultureInfo { get; } } } ================================================ FILE: osu.Game.Rulesets.Karaoke/Graphics/UserInterfaceV2/LanguageSelectorPopover.cs ================================================ // Copyright (c) andy840119 . Licensed under the GPL Licence. // See the LICENCE file in the repository root for full licence text. using System.Globalization; using osu.Framework.Bindables; using osu.Game.Graphics.UserInterfaceV2; namespace osu.Game.Rulesets.Karaoke.Graphics.UserInterfaceV2; public partial class LanguageSelectorPopover : OsuPopover { private readonly LanguageSelector languageSelector; public LanguageSelectorPopover(Bindable bindable) { Child = languageSelector = new LanguageSelector { Width = 260, Height = 400, Current = bindable, }; } public bool EnableEmptyOption { get => languageSelector.EnableEmptyOption; set => languageSelector.EnableEmptyOption = value; } protected override void LoadComplete() { base.LoadComplete(); GetContainingFocusManager().ChangeFocus(languageSelector); } } ================================================ FILE: osu.Game.Rulesets.Karaoke/IO/Archives/CachedFontArchiveReader.cs ================================================ // Copyright (c) andy840119 . Licensed under the GPL Licence. // See the LICENCE file in the repository root for full licence text. using System.Collections.Generic; using System.IO; using System.Linq; using osu.Framework.IO.Stores; using osu.Game.IO.Archives; using SharpCompress.Archives; using SharpCompress.Archives.Zip; namespace osu.Game.Rulesets.Karaoke.IO.Archives; /// /// For reading cached font reader. /// Cached font will be saved as xxx.cached fnt into cached folder. /// And notice that this class is just copied from /// public class CachedFontArchiveReader : ArchiveReader { private readonly Stream archiveStream; private readonly IWritableArchive archive; public CachedFontArchiveReader(Stream archiveStream, string name) : base(name) { this.archiveStream = archiveStream; archive = ZipArchive.OpenArchive(archiveStream); } public override Stream GetStream(string name) { // will search .fnt file or image in here. string file = Path.HasExtension(name) ? name : $"{name}.bin"; var entry = archive.Entries.SingleOrDefault(e => e.Key == file); if (entry == null) throw new FileNotFoundException(); // allow seeking var copy = new MemoryStream(); using (var s = entry.OpenEntryStream()) s.CopyTo(copy); copy.Position = 0; return copy; } public override void Dispose() { archive.Dispose(); archiveStream.Dispose(); } public override IEnumerable Filenames => archive.Entries.Select(e => e.Key).ExcludeSystemFileNames(); } ================================================ FILE: osu.Game.Rulesets.Karaoke/IO/Serialization/Converters/ColourConverter.cs ================================================ // Copyright (c) andy840119 . Licensed under the GPL Licence. // See the LICENCE file in the repository root for full licence text. using System; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using osu.Framework.Extensions.Color4Extensions; using osuTK.Graphics; namespace osu.Game.Rulesets.Karaoke.IO.Serialization.Converters; public class ColourConverter : JsonConverter { public override Color4 ReadJson(JsonReader reader, Type objectType, Color4 existingValue, bool hasExistingValue, JsonSerializer serializer) { var obj = JToken.Load(reader); string? value = obj.Value(); if (value == null) return new Color4(); return Color4Extensions.FromHex(value); } public override void WriteJson(JsonWriter writer, Color4 value, JsonSerializer serializer) { writer.WriteValue(value.ToHex()); } } ================================================ FILE: osu.Game.Rulesets.Karaoke/IO/Serialization/Converters/CultureInfoConverter.cs ================================================ // Copyright (c) andy840119 . Licensed under the GPL Licence. // See the LICENCE file in the repository root for full licence text. using System; using System.Globalization; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using osu.Game.Rulesets.Karaoke.Utils; namespace osu.Game.Rulesets.Karaoke.IO.Serialization.Converters; public class CultureInfoConverter : JsonConverter { public override CultureInfo? ReadJson(JsonReader reader, Type objectType, CultureInfo? existingValue, bool hasExistingValue, JsonSerializer serializer) { var obj = JToken.Load(reader); int? value = obj.Value(); if (value == null) return null; return CultureInfoUtils.CreateLoadCultureInfoById(value.Value); } public override void WriteJson(JsonWriter writer, CultureInfo? value, JsonSerializer serializer) { int? id = value != null ? CultureInfoUtils.GetSaveCultureInfoId(value) : null; writer.WriteValue(id); } } ================================================ FILE: osu.Game.Rulesets.Karaoke/IO/Serialization/Converters/DictionaryConverter.cs ================================================ // Copyright (c) andy840119 . Licensed under the GPL Licence. // See the LICENCE file in the repository root for full licence text. using System; using System.Collections.Generic; using System.Linq; using Newtonsoft.Json; using Newtonsoft.Json.Linq; namespace osu.Game.Rulesets.Karaoke.IO.Serialization.Converters; public abstract class DictionaryConverter : JsonConverter> where TKey : notnull { public sealed override IDictionary ReadJson(JsonReader reader, Type objectType, IDictionary? existingValue, bool hasExistingValue, JsonSerializer serializer) { var obj = JArray.Load(reader); return obj.OfType().ToDictionary( x => deserializeKey((JProperty)x.First!), x => deserializeValue((JProperty)x.Last!) ); TKey deserializeKey(JProperty token) => serializer.Deserialize(token.Value.CreateReader())!; TValue deserializeValue(JProperty token) => serializer.Deserialize(token.Value.CreateReader())!; } public override void WriteJson(JsonWriter writer, IDictionary? value, JsonSerializer serializer) { ArgumentNullException.ThrowIfNull(value); writer.WriteStartArray(); foreach (var keyValuePair in value) { var jObject = JObject.FromObject(keyValuePair, serializer); jObject.WriteTo(writer); } writer.WriteEndArray(); } } ================================================ FILE: osu.Game.Rulesets.Karaoke/IO/Serialization/Converters/ElementIdConverter.cs ================================================ // Copyright (c) andy840119 . Licensed under the GPL Licence. // See the LICENCE file in the repository root for full licence text. using System; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using osu.Game.Rulesets.Karaoke.Beatmaps; namespace osu.Game.Rulesets.Karaoke.IO.Serialization.Converters; public class ElementIdConverter : JsonConverter { public override ElementId? ReadJson(JsonReader reader, Type objectType, ElementId? existingValue, bool hasExistingValue, JsonSerializer serializer) { var obj = JToken.Load(reader); string? value = obj.Value(); return createElementId(value); } private static ElementId? createElementId(string? str) => str switch { null => null, "" => ElementId.Empty, _ => new ElementId(str), }; public override void WriteJson(JsonWriter writer, ElementId? value, JsonSerializer serializer) { string? id = value.ToString(); writer.WriteValue(id); } } ================================================ FILE: osu.Game.Rulesets.Karaoke/IO/Serialization/Converters/FontUsageConverter.cs ================================================ // Copyright (c) andy840119 . Licensed under the GPL Licence. // See the LICENCE file in the repository root for full licence text. using System; using System.Linq; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using osu.Framework.Graphics.Sprites; namespace osu.Game.Rulesets.Karaoke.IO.Serialization.Converters; public class FontUsageConverter : JsonConverter { private const float default_text_size = 20; public override FontUsage ReadJson(JsonReader reader, Type objectType, FontUsage existingValue, bool hasExistingValue, JsonSerializer serializer) { var obj = JToken.Load(reader); var properties = obj.Children().OfType().ToArray(); if (!properties.Any()) return new FontUsage(size: default_text_size); var font = new FontUsage(size: default_text_size); return properties.Aggregate(font, (current, property) => property.Name switch { "family" => current.With(property.Value.ToObject()), "weight" => current.With(weight: property.Value.ToObject()), "size" => current.With(size: property.Value.ToObject()), "italics" => current.With(italics: property.Value.ToObject()), "fixedWidth" => current.With(fixedWidth: property.Value.ToObject()), _ => current, }); } public override void WriteJson(JsonWriter writer, FontUsage value, JsonSerializer serializer) { writer.WriteStartObject(); if (!string.IsNullOrEmpty(value.Family)) { writer.WritePropertyName("family"); writer.WriteValue(value.Family); } if (!string.IsNullOrEmpty(value.Weight)) { writer.WritePropertyName("weight"); writer.WriteValue(value.Weight); } if (value.Size != default_text_size) { writer.WritePropertyName("size"); writer.WriteValue(value.Size); } if (value.Italics) { writer.WritePropertyName("italics"); writer.WriteValue(true); } if (value.FixedWidth) { writer.WritePropertyName("fixedWidth"); writer.WriteValue(true); } writer.WriteEndObject(); } } ================================================ FILE: osu.Game.Rulesets.Karaoke/IO/Serialization/Converters/GenericTypeConverter.cs ================================================ // Copyright (c) andy840119 . Licensed under the GPL Licence. // See the LICENCE file in the repository root for full licence text. using System; using System.Linq; using System.Reflection; using Newtonsoft.Json; using Newtonsoft.Json.Linq; namespace osu.Game.Rulesets.Karaoke.IO.Serialization.Converters; public abstract class GenericTypeConverter : GenericTypeConverter { protected override string GetNameByType(MemberInfo type) => type.Name; } public abstract class GenericTypeConverter : JsonConverter where TTypeName : notnull { public sealed override TType ReadJson(JsonReader reader, Type objectType, TType? existingValue, bool hasExistingValue, JsonSerializer serializer) { var jObject = JObject.Load(reader); var type = objectType != typeof(TType) ? objectType : getTypeByProperties(jObject); var newReader = jObject.CreateReader(); var instance = (TType)Activator.CreateInstance(type)!; serializer.Populate(newReader, instance); PostProcessValue(instance, jObject, serializer); return instance; Type getTypeByProperties(JObject jObj) { var elementType = GetValueFromProperty(serializer, jObj, "$type"); return GetTypeByName(elementType); } } protected static TPropertyType GetValueFromProperty(JsonSerializer serializer, JObject jObject, string propertyName) { var jProperties = jObject.Children().OfType().ToArray(); var value = jProperties.FirstOrDefault(x => x.Name == propertyName)?.Value; if (value == null) throw new ArgumentNullException(nameof(value)); var elementType = value.ToObject(serializer); if (elementType == null) throw new InvalidCastException(nameof(elementType)); return elementType; } protected virtual void PostProcessValue(TType existingValue, JObject jObject, JsonSerializer serializer) { } public sealed override void WriteJson(JsonWriter writer, TType? value, JsonSerializer serializer) { ArgumentNullException.ThrowIfNull(value); var resolver = serializer.ContractResolver; // follow: https://stackoverflow.com/a/59329703 // not a good way but seems there's no better choice. serializer.Converters.Remove(this); serializer.ContractResolver = new WritablePropertiesOnlyResolver(); var jObject = JObject.FromObject(value, serializer); serializer.Converters.Add(this); serializer.ContractResolver = resolver; jObject.AddFirst(new JProperty("$type", GetNameByType(value.GetType()))); PostProcessJObject(jObject, value, serializer); jObject.WriteTo(writer); } protected virtual void PostProcessJObject(JObject jObject, TType value, JsonSerializer serializer) { } protected abstract Type GetTypeByName(TTypeName name); protected abstract TTypeName GetNameByType(MemberInfo type); } ================================================ FILE: osu.Game.Rulesets.Karaoke/IO/Serialization/Converters/KaraokeSkinElementConverter.cs ================================================ // Copyright (c) andy840119 . Licensed under the GPL Licence. // See the LICENCE file in the repository root for full licence text. using System; using System.Reflection; using osu.Game.Rulesets.Karaoke.Skinning.Elements; namespace osu.Game.Rulesets.Karaoke.IO.Serialization.Converters; public class KaraokeSkinElementConverter : GenericTypeConverter { protected override Type GetTypeByName(ElementType name) => GetObjectType(name); protected override ElementType GetNameByType(MemberInfo type) => GetElementType(type); public static ElementType GetElementType(MemberInfo elementType) => elementType switch { _ when elementType == typeof(LyricFontInfo) => ElementType.LyricFontInfo, _ when elementType == typeof(NoteStyle) => ElementType.NoteStyle, _ => throw new NotSupportedException(), }; public static Type GetObjectType(ElementType elementType) => elementType switch { ElementType.LyricFontInfo => typeof(LyricFontInfo), ElementType.NoteStyle => typeof(NoteStyle), _ => throw new NotSupportedException(), }; } ================================================ FILE: osu.Game.Rulesets.Karaoke/IO/Serialization/Converters/LyricConverter.cs ================================================ // Copyright (c) andy840119 . Licensed under the GPL Licence. // See the LICENCE file in the repository root for full licence text. using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using osu.Game.Extensions; using osu.Game.Rulesets.Karaoke.Objects; using osu.Game.Rulesets.Karaoke.Objects.Properties; namespace osu.Game.Rulesets.Karaoke.IO.Serialization.Converters; public class LyricConverter : JsonConverter { public override Lyric ReadJson(JsonReader reader, Type objectType, Lyric? existingValue, bool hasExistingValue, JsonSerializer serializer) { var jObject = JObject.Load(reader); var newReader = jObject.CreateReader(); var instance = Activator.CreateInstance(); serializer.Populate(newReader, instance); return instance; } public override void WriteJson(JsonWriter writer, Lyric? value, JsonSerializer serializer) { ArgumentNullException.ThrowIfNull(value); // follow: https://stackoverflow.com/a/59329703 // not a good way but seems there's no better choice. serializer.Converters.Remove(this); var jObject = JObject.FromObject(value, serializer); serializer.Converters.Add(this); // should remove some properties from jObject if has the config. if (value.ReferenceLyricConfig != null) { Debug.Assert(value.ReferenceLyricConfig != null); // note: should convert into snake case. string[] removedProperties = removePropertyNamesByConfig(value.ReferenceLyricConfig) .Select(x => x.ToSnakeCase()) .ToArray(); foreach (string removedProperty in removedProperties) { jObject.Remove(removedProperty); } } jObject.WriteTo(writer); } private IEnumerable removePropertyNamesByConfig(IReferenceLyricPropertyConfig config) { switch (config) { case ReferenceLyricConfig: yield break; case SyncLyricConfig syncLyricConfig: yield return nameof(Lyric.Text); if (syncLyricConfig.SyncTimeTagProperty) yield return nameof(Lyric.TimeTags); yield return nameof(Lyric.RubyTags); yield return nameof(Lyric.StartTime); yield return nameof(Lyric.Duration); yield return nameof(Lyric.EndTime); if (syncLyricConfig.SyncSingerProperty) yield return nameof(Lyric.SingerIds); yield return nameof(Lyric.Translations); yield return nameof(Lyric.Language); yield return nameof(Lyric.Order); yield break; default: throw new ArgumentOutOfRangeException(nameof(config), config, "unknown config."); } } } ================================================ FILE: osu.Game.Rulesets.Karaoke/IO/Serialization/Converters/ReferenceLyricPropertyConfigConverter.cs ================================================ // Copyright (c) andy840119 . Licensed under the GPL Licence. // See the LICENCE file in the repository root for full licence text. using System; using System.Diagnostics; using System.Reflection; using osu.Game.Rulesets.Karaoke.Objects.Properties; namespace osu.Game.Rulesets.Karaoke.IO.Serialization.Converters; public class ReferenceLyricPropertyConfigConverter : GenericTypeConverter { protected override Type GetTypeByName(string name) { var assembly = Assembly.GetExecutingAssembly(); var type = assembly.GetType($"osu.Game.Rulesets.Karaoke.Objects.Properties.{name}"); Debug.Assert(type != null); return type; } } ================================================ FILE: osu.Game.Rulesets.Karaoke/IO/Serialization/Converters/RubyTagConverter.cs ================================================ // Copyright (c) andy840119 . Licensed under the GPL Licence. // See the LICENCE file in the repository root for full licence text. using System; using System.Text.RegularExpressions; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using osu.Game.Rulesets.Karaoke.Extensions; using osu.Game.Rulesets.Karaoke.Objects; namespace osu.Game.Rulesets.Karaoke.IO.Serialization.Converters; public class RubyTagConverter : JsonConverter { public override RubyTag ReadJson(JsonReader reader, Type objectType, RubyTag? existingValue, bool hasExistingValue, JsonSerializer serializer) { var obj = JToken.Load(reader); string? value = obj.Value(); if (string.IsNullOrEmpty(value)) return new RubyTag(); var regex = new Regex(@"\[(?[-0-9]+)(?:,(?[-0-9]+))?\]:(?.*$)"); var result = regex.Match(value); if (!result.Success) return new RubyTag(); return new RubyTag { StartIndex = result.GetGroupValue("start"), EndIndex = result.GetGroupValue("end") ?? result.GetGroupValue("start"), Text = result.GetGroupValue("ruby"), }; } public override void WriteJson(JsonWriter writer, RubyTag? value, JsonSerializer serializer) { if (value == null) throw new ArgumentNullException(nameof(value)); string str = value.StartIndex == value.EndIndex ? $"[{value.StartIndex}]:{value.Text}" : $"[{value.StartIndex},{value.EndIndex}]:{value.Text}"; writer.WriteValue(str); } } ================================================ FILE: osu.Game.Rulesets.Karaoke/IO/Serialization/Converters/RubyTagsConverter.cs ================================================ // Copyright (c) andy840119 . Licensed under the GPL Licence. // See the LICENCE file in the repository root for full licence text. using System.Collections.Generic; using osu.Game.Rulesets.Karaoke.Objects; using osu.Game.Rulesets.Karaoke.Objects.Utils; namespace osu.Game.Rulesets.Karaoke.IO.Serialization.Converters; public class RubyTagsConverter : SortableJsonConverter { protected override IEnumerable GetSortedValue(IEnumerable objects) => RubyTagsUtils.Sort(objects); } ================================================ FILE: osu.Game.Rulesets.Karaoke/IO/Serialization/Converters/ShaderConverter.cs ================================================ // Copyright (c) andy840119 . Licensed under the GPL Licence. // See the LICENCE file in the repository root for full licence text. using System; using System.Diagnostics; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using osu.Framework.Graphics.Shaders; using osu.Game.Rulesets.Karaoke.Utils; namespace osu.Game.Rulesets.Karaoke.IO.Serialization.Converters; public class ShaderConverter : GenericTypeConverter { protected override void PostProcessJObject(JObject jObject, ICustomizedShader value, JsonSerializer serializer) { var childShader = getShadersFromParent(value, serializer); if (childShader != null) { jObject.Remove("step_shaders"); jObject.Add("step_shaders", childShader); } static JArray? getShadersFromParent(ICustomizedShader shader, JsonSerializer serializer) { if (shader is not StepShader stepShader) return null; return JArray.FromObject(stepShader.StepShaders, serializer); } } protected override Type GetTypeByName(string name) { // only get name from font var assembly = AssemblyUtils.GetAssemblyByName("osu.Framework.KaraokeFont"); Debug.Assert(assembly != null); var type = assembly.GetType($"osu.Framework.Graphics.Shaders.{name}"); Debug.Assert(type != null); return type; } } ================================================ FILE: osu.Game.Rulesets.Karaoke/IO/Serialization/Converters/SortableJsonConverter.cs ================================================ // Copyright (c) andy840119 . Licensed under the GPL Licence. // See the LICENCE file in the repository root for full licence text. using System; using System.Collections.Generic; using System.Linq; using Newtonsoft.Json; using Newtonsoft.Json.Linq; namespace osu.Game.Rulesets.Karaoke.IO.Serialization.Converters; public abstract class SortableJsonConverter : JsonConverter> { public sealed override IEnumerable ReadJson(JsonReader reader, Type objectType, IEnumerable? existingValue, bool hasExistingValue, JsonSerializer serializer) { var obj = JArray.Load(reader); var timeTags = obj.Select(x => serializer.Deserialize(x.CreateReader())!); return GetSortedValue(timeTags); } public override void WriteJson(JsonWriter writer, IEnumerable? value, JsonSerializer serializer) { ArgumentNullException.ThrowIfNull(value); // see: https://stackoverflow.com/questions/3330989/order-of-serialized-fields-using-json-net var sortedTimeTags = GetSortedValue(value); writer.WriteStartArray(); foreach (var timeTag in sortedTimeTags) { serializer.Serialize(writer, timeTag); } writer.WriteEndArray(); } protected abstract IEnumerable GetSortedValue(IEnumerable objects); } ================================================ FILE: osu.Game.Rulesets.Karaoke/IO/Serialization/Converters/StageInfoConverter.cs ================================================ // Copyright (c) andy840119 . Licensed under the GPL Licence. // See the LICENCE file in the repository root for full licence text. using System; using System.Reflection; using osu.Game.Rulesets.Karaoke.Stages.Infos; using osu.Game.Rulesets.Karaoke.Stages.Infos.Classic; using osu.Game.Rulesets.Karaoke.Stages.Infos.Preview; namespace osu.Game.Rulesets.Karaoke.IO.Serialization.Converters; public class StageInfoConverter : GenericTypeConverter { private const string classic_stage = "classic"; private const string preview_stage = "preview"; protected override string GetNameByType(MemberInfo type) => type switch { Type t when t == typeof(ClassicStageInfo) => classic_stage, Type t when t == typeof(PreviewStageInfo) => preview_stage, _ => throw new InvalidOperationException(), }; protected override Type GetTypeByName(string name) => name switch { classic_stage => typeof(ClassicStageInfo), preview_stage => typeof(PreviewStageInfo), _ => throw new InvalidOperationException(), }; } ================================================ FILE: osu.Game.Rulesets.Karaoke/IO/Serialization/Converters/TimeTagConverter.cs ================================================ // Copyright (c) andy840119 . Licensed under the GPL Licence. // See the LICENCE file in the repository root for full licence text. using System; using System.Text.RegularExpressions; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using osu.Framework.Graphics.Sprites; using osu.Game.Rulesets.Karaoke.Extensions; using osu.Game.Rulesets.Karaoke.Objects; using osu.Game.Rulesets.Karaoke.Utils; namespace osu.Game.Rulesets.Karaoke.IO.Serialization.Converters; public class TimeTagConverter : JsonConverter { public override TimeTag ReadJson(JsonReader reader, Type objectType, TimeTag? existingValue, bool hasExistingValue, JsonSerializer serializer) { var obj = JToken.Load(reader); string? value = obj.Value(); if (string.IsNullOrEmpty(value)) return new TimeTag(new TextIndex()); var regex = new Regex("(?[-0-9]+),(?start|end)]:(?