From 2fe3ce5ce115bf30e3ba58c458f181ad1cd6d770 Mon Sep 17 00:00:00 2001 From: TheBoxyBear <15822255+TheBoxyBear@users.noreply.github.com> Date: Wed, 3 Jan 2024 16:36:45 -0500 Subject: [PATCH] Unify single main project in .net 8 --- ChartTools.Tests/AlternatingTests.cs | 45 +- ChartTools.Tests/ChartTools.Tests.csproj | 15 +- ChartTools.Tests/SystemExtensionsTests.cs | 6 +- ChartTools.sln | 37 +- ChartTools/ChartTools.csproj | 13 + {source => ChartTools}/Chords/DrumsChord.cs | 0 {source => ChartTools}/Chords/GHLChord.cs | 0 {source => ChartTools}/Chords/IChord.cs | 0 {source => ChartTools}/Chords/LaneChord.cs | 0 .../Chords/LaneChordGeneric.cs | 0 {source => ChartTools}/Chords/Phrase.cs | 0 .../Chords/StandardChord.cs | 0 {source => ChartTools}/Enums.cs | 0 {source => ChartTools}/Events/Event.cs | 0 .../Events/EventArgumentHelper.cs | 0 ChartTools/Events/EventExtensions.cs | 63 ++ .../Events/EventTypeHeaderHelper.cs | 0 .../Events/EventTypeHelper.cs | 0 {source => ChartTools}/Events/GlobalEvent.cs | 0 {source => ChartTools}/Events/LocalEvent.cs | 0 .../DesynchronizedAnchorException.cs | 0 .../Exceptions/UndefinedEnumException.cs | 0 .../Exceptions/Validator.cs | 0 .../OrderedAlternatingEnumerable.cs | 150 ++++ .../SerialAlternatingEnumerable.cs | 116 +++ .../Collections/Delayed/DelayedEnumerable.cs | 0 .../Delayed/DelayedEnumerableSource.cs | 0 .../Collections/Delayed/DelayedEnumerator.cs | 0 .../Extensions/Collections/EagerEnumerable.cs | 0 .../Extensions/Collections/IInitializable.cs | 0 .../Extensions/EnumCache.cs | 0 .../Extensions/EqualityComparison.cs | 0 .../Extensions/FuncEqualityComparer.cs | 0 .../Extensions/IInitializable.cs | 0 .../Extensions/Linq/CollectionExtensions.cs | 0 .../Extensions/Linq/EnumerableExtensions.cs | 376 ++++++++++ .../Extensions/Linq/SectionReplacement.cs | 0 .../Extensions/StringExtensions.cs | 0 {source => ChartTools}/GlobalSuppressions.cs | 0 {source => ChartTools}/IEmptyVerifiable.cs | 0 {source => ChartTools}/ILongObject.cs | 0 {source => ChartTools}/IO/Anchor.cs | 0 .../IO/Appliables/IInstrumentAppliable.cs | 0 .../IO/Appliables/ISongAppliable.cs | 0 {source => ChartTools}/IO/Chart/ChartFile.cs | 0 .../IO/Chart/ChartFileReader.cs | 0 .../IO/Chart/ChartFileWriter.cs | 0 .../IO/Chart/ChartFormatting.cs | 0 .../IO/Chart/ChartSectionSet.cs | 0 .../IO/Chart/Entries/NoteData.cs | 0 .../IO/Chart/Entries/TrackObjectEntry.cs | 0 .../IO/Chart/Parsing/ChartParser.cs | 0 .../IO/Chart/Parsing/DrumsTrackParser.cs | 0 .../IO/Chart/Parsing/GHLTrackParser.cs | 0 .../IO/Chart/Parsing/GlobalEventParser.cs | 0 .../IO/Chart/Parsing/MetadataParser.cs | 0 .../IO/Chart/Parsing/StandardTrackParser.cs | 0 .../IO/Chart/Parsing/SyncTrackParser.cs | 0 .../IO/Chart/Parsing/TrackParser.cs | 0 .../IO/Chart/Parsing/UnknownSectionParser.cs | 0 .../Parsing/VariableInstrumentTrackParser.cs | 0 .../IO/Chart/Providers/ChordProvider.cs | 0 .../IO/Chart/Providers/EventProvider.cs | 0 .../Chart/Providers/SpecialPhraseProvider.cs | 0 .../IO/Chart/Providers/SyncTrackProvider.cs | 0 .../IO/Chart/Providers/TempoProvider.cs | 0 .../Chart/Providers/TimeSignatureProvider.cs | 0 .../ChartKeySerializableAttribute.cs | 0 .../Serializing/GlobalEventSerializer.cs | 0 .../Chart/Serializing/MetadataSerializer.cs | 0 .../Chart/Serializing/SyncTrackSerializer.cs | 0 .../Serializing/TrackObjectGroupSerializer.cs | 12 + .../IO/Chart/Serializing/TrackSerializer.cs | 72 ++ .../Serializing/UnknownSectionSerializer.cs | 0 .../IO/Configuration/CommonConfiguration.cs | 0 .../Configuration/ConfigurationExceptions.cs | 0 .../IO/Configuration/Enums.cs | 0 .../IO/Configuration/ReadingConfiguration.cs | 0 .../Configuration/Sessions/ReadingSession.cs | 0 .../IO/Configuration/Sessions/Session.cs | 0 .../Configuration/Sessions/WritingSession.cs | 0 .../IO/Configuration/WritingConfiguration.cs | 0 {source => ChartTools}/IO/DirectoryHandler.cs | 0 .../IO/Exceptions/EntryException.cs | 0 .../IO/Exceptions/LineException.cs | 0 .../IO/Exceptions/ParseException.cs | 0 .../IO/Exceptions/SectionException.cs | 0 {source => ChartTools}/IO/ExtensionHandler.cs | 0 {source => ChartTools}/IO/FileReader.cs | 0 .../IO/Formatting/FormattingEnums.cs | 0 .../IO/Formatting/FormattingRules.cs | 0 .../IO/ISerializerDataProvider.cs | 0 {source => ChartTools}/IO/Ini/IniFile.cs | 0 .../IO/Ini/IniFileReader.cs | 0 .../IO/Ini/IniFileWriter.cs | 0 .../IO/Ini/IniFormatting.cs | 0 .../IO/Ini/IniKeySerializableAttribute.cs | 0 {source => ChartTools}/IO/Ini/IniParser.cs | 0 .../IO/Ini/IniSerializer.cs | 0 .../IO/Parsers/FileParser.cs | 0 .../IO/Parsers/SectionParser.cs | 0 .../IO/Parsers/TextParser.cs | 0 .../IO/Sections/ReservedSectionHeader.cs | 0 .../IO/Sections/ReservedSectionHeaderSet.cs | 0 {source => ChartTools}/IO/Sections/Section.cs | 0 .../IO/Sections/SectionSet.cs | 0 .../IO/Serializers/GroupSerializer.cs | 0 .../IO/Serializers/KeySerializable.cs | 0 .../IO/Serializers/Serializer.cs | 0 {source => ChartTools}/IO/TextEntry.cs | 0 {source => ChartTools}/IO/TextFileReader.cs | 0 {source => ChartTools}/IO/TextFileWriter.cs | 0 {source => ChartTools}/IO/ValueParser.cs | 0 {source => ChartTools}/IReadOnlyLongObject.cs | 0 {source => ChartTools}/Instruments/Drums.cs | 0 .../Instruments/GHLInstrument.cs | 0 .../Instruments/Instrument.cs | 0 .../Instruments/InstrumentGeneric.cs | 0 .../Instruments/InstrumentSet.cs | 0 .../Instruments/StandardInstrument.cs | 0 {source => ChartTools}/Instruments/Vocals.cs | 0 {source => ChartTools}/Metadata/Charter.cs | 0 .../Metadata/InstrumentDifficultySet.cs | 0 {source => ChartTools}/Metadata/Metadata.cs | 0 .../Metadata/StreamCollection.cs | 0 .../Metadata/UnidentifiedMetadata.cs | 0 {source => ChartTools}/Notes/DrumsNote.cs | 0 {source => ChartTools}/Notes/INote.cs | 0 {source => ChartTools}/Notes/LaneNote.cs | 0 .../Notes/LaneNoteCollection.cs | 0 .../Notes/LaneNoteGeneric.cs | 0 {source => ChartTools}/Notes/Syllable.cs | 0 {source => ChartTools}/Notes/VocalsPitch.cs | 0 {source => ChartTools}/Song.cs | 0 .../Special/InstrumentSpecialPhrase.cs | 0 .../Special/SpecialPhrase.cs | 0 .../Special/TrackSpecialPhrase.cs | 0 {source => ChartTools}/Sync/SyncTrack.cs | 0 {source => ChartTools}/Sync/Tempo.cs | 0 {source => ChartTools}/Sync/TempoMap.cs | 0 {source => ChartTools}/Sync/TimeSignature.cs | 0 {source => ChartTools}/Tools/LengthMerger.cs | 0 {source => ChartTools}/Tools/Optimizer.cs | 0 {source => ChartTools}/Tools/Printer.cs | 0 .../Tools/PropertyMerger.cs | 0 {source => ChartTools}/Tools/TempoRescaler.cs | 0 .../TrackObjects/ILongTrackObject.cs | 0 .../TrackObjects/IReadOnlyTrackObject.cs | 0 .../TrackObjects/ITrackObject.cs | 0 .../TrackObjects/TrackObjectBase.cs | 0 {source => ChartTools}/Tracks/Track.cs | 0 {source => ChartTools}/Tracks/TrackGeneric.cs | 0 .../Tracks/UniqueTrackObjectCollection.cs | 0 {source => ChartTools}/source.projitems | 0 Docs/Docs.csproj | 6 +- Docs/docfx.json | 2 +- Net6/Net6.csproj | 33 - Net7/Net7.csproj | 34 - {source => source - Copie}/ChartTools.shproj | 0 source - Copie/Chords/DrumsChord.cs | 59 ++ source - Copie/Chords/GHLChord.cs | 62 ++ source - Copie/Chords/IChord.cs | 15 + source - Copie/Chords/LaneChord.cs | 28 + source - Copie/Chords/LaneChordGeneric.cs | 32 + source - Copie/Chords/Phrase.cs | 89 +++ source - Copie/Chords/StandardChord.cs | 53 ++ source - Copie/Enums.cs | 457 +++++++++++ source - Copie/Events/Event.cs | 65 ++ source - Copie/Events/EventArgumentHelper.cs | 19 + .../Events/EventExtensions.cs | 0 .../Events/EventTypeHeaderHelper.cs | 26 + source - Copie/Events/EventTypeHelper.cs | 66 ++ source - Copie/Events/GlobalEvent.cs | 39 + source - Copie/Events/LocalEvent.cs | 16 + .../DesynchronizedAnchorException.cs | 12 + .../Exceptions/UndefinedEnumException.cs | 16 + source - Copie/Exceptions/Validator.cs | 14 + .../Ordered/OrderedAlternatingEnumerable.cs | 0 .../Ordered/OrderedAlternatingEnumerator.cs | 0 .../Serial/SerialAlternatingEnumerable.cs | 0 .../Serial/SerialAlternatingEnumerator.cs | 0 .../Collections/Delayed/DelayedEnumerable.cs | 31 + .../Delayed/DelayedEnumerableSource.cs | 37 + .../Collections/Delayed/DelayedEnumerator.cs | 37 + .../Extensions/Collections/EagerEnumerable.cs | 23 + .../Extensions/Collections/IInitializable.cs | 17 + source - Copie/Extensions/EnumCache.cs | 9 + .../Extensions/EqualityComparison.cs | 6 + .../Extensions/FuncEqualityComparer.cs | 27 + source - Copie/Extensions/IInitializable.cs | 17 + .../Extensions/Linq/CollectionExtensions.cs | 42 ++ .../Extensions/Linq/EnumerableExtensions.cs | 0 .../Extensions/Linq/SectionReplacement.cs | 11 + source - Copie/Extensions/StringExtensions.cs | 22 + source - Copie/GlobalSuppressions.cs | 8 + source - Copie/IEmptyVerifiable.cs | 12 + source - Copie/ILongObject.cs | 9 + source - Copie/IO/Anchor.cs | 13 + .../IO/Appliables/IInstrumentAppliable.cs | 6 + .../IO/Appliables/ISongAppliable.cs | 6 + source - Copie/IO/Chart/ChartFile.cs | 707 ++++++++++++++++++ source - Copie/IO/Chart/ChartFileReader.cs | 17 + source - Copie/IO/Chart/ChartFileWriter.cs | 11 + source - Copie/IO/Chart/ChartFormatting.cs | 78 ++ source - Copie/IO/Chart/ChartSectionSet.cs | 37 + source - Copie/IO/Chart/Entries/NoteData.cs | 32 + .../IO/Chart/Entries/TrackObjectEntry.cs | 51 ++ .../IO/Chart/Parsing/ChartParser.cs | 11 + .../IO/Chart/Parsing/DrumsTrackParser.cs | 50 ++ .../IO/Chart/Parsing/GHLTrackParser.cs | 49 ++ .../IO/Chart/Parsing/GlobalEventParser.cs | 21 + .../IO/Chart/Parsing/MetadataParser.cs | 90 +++ .../IO/Chart/Parsing/StandardTrackParser.cs | 42 ++ .../IO/Chart/Parsing/SyncTrackParser.cs | 104 +++ .../IO/Chart/Parsing/TrackParser.cs | 114 +++ .../IO/Chart/Parsing/UnknownSectionParser.cs | 14 + .../Parsing/VariableInstrumentTrackParser.cs | 9 + .../IO/Chart/Providers/ChordProvider.cs | 31 + .../IO/Chart/Providers/EventProvider.cs | 10 + .../Chart/Providers/SpecialPhraseProvider.cs | 9 + .../IO/Chart/Providers/SyncTrackProvider.cs | 34 + .../IO/Chart/Providers/TempoProvider.cs | 17 + .../Chart/Providers/TimeSignatureProvider.cs | 19 + .../ChartKeySerializableAttribute.cs | 16 + .../Serializing/GlobalEventSerializer.cs | 13 + .../Chart/Serializing/MetadataSerializer.cs | 27 + .../Chart/Serializing/SyncTrackSerializer.cs | 12 + .../Serializing/TrackObjectGroupSerializer.cs | 0 .../IO/Chart/Serializing/TrackSerializer.cs | 0 .../Serializing/UnknownSectionSerializer.cs | 11 + .../IO/Configuration/CommonConfiguration.cs | 17 + .../Configuration/ConfigurationExceptions.cs | 6 + source - Copie/IO/Configuration/Enums.cs | 242 ++++++ .../IO/Configuration/ReadingConfiguration.cs | 25 + .../Configuration/Sessions/ReadingSession.cs | 22 + .../IO/Configuration/Sessions/Session.cs | 35 + .../Configuration/Sessions/WritingSession.cs | 25 + .../IO/Configuration/WritingConfiguration.cs | 12 + source - Copie/IO/DirectoryHandler.cs | 36 + .../IO/Exceptions/EntryException.cs | 6 + source - Copie/IO/Exceptions/LineException.cs | 7 + .../IO/Exceptions/ParseException.cs | 15 + .../IO/Exceptions/SectionException.cs | 11 + source - Copie/IO/ExtensionHandler.cs | 114 +++ source - Copie/IO/FileReader.cs | 79 ++ .../IO/Formatting/FormattingEnums.cs | 5 + .../IO/Formatting/FormattingRules.cs | 113 +++ source - Copie/IO/ISerializerDataProvider.cs | 8 + source - Copie/IO/Ini/IniFile.cs | 47 ++ source - Copie/IO/Ini/IniFileReader.cs | 10 + source - Copie/IO/Ini/IniFileWriter.cs | 8 + source - Copie/IO/Ini/IniFormatting.cs | 47 ++ .../IO/Ini/IniKeySerializableAttribute.cs | 13 + source - Copie/IO/Ini/IniParser.cs | 141 ++++ source - Copie/IO/Ini/IniSerializer.cs | 34 + source - Copie/IO/Parsers/FileParser.cs | 54 ++ source - Copie/IO/Parsers/SectionParser.cs | 14 + source - Copie/IO/Parsers/TextParser.cs | 10 + .../IO/Sections/ReservedSectionHeader.cs | 13 + .../IO/Sections/ReservedSectionHeaderSet.cs | 13 + source - Copie/IO/Sections/Section.cs | 7 + source - Copie/IO/Sections/SectionSet.cs | 56 ++ .../IO/Serializers/GroupSerializer.cs | 13 + .../IO/Serializers/KeySerializable.cs | 24 + source - Copie/IO/Serializers/Serializer.cs | 26 + source - Copie/IO/TextEntry.cs | 33 + source - Copie/IO/TextFileReader.cs | 93 +++ source - Copie/IO/TextFileWriter.cs | 66 ++ source - Copie/IO/ValueParser.cs | 16 + source - Copie/IReadOnlyLongObject.cs | 9 + source - Copie/Instruments/Drums.cs | 25 + source - Copie/Instruments/GHLInstrument.cs | 30 + source - Copie/Instruments/Instrument.cs | 156 ++++ .../Instruments/InstrumentGeneric.cs | 121 +++ source - Copie/Instruments/InstrumentSet.cs | 163 ++++ .../Instruments/StandardInstrument.cs | 51 ++ source - Copie/Instruments/Vocals.cs | 20 + source - Copie/Metadata/Charter.cs | 25 + .../Metadata/InstrumentDifficultySet.cs | 80 ++ source - Copie/Metadata/Metadata.cs | 225 ++++++ source - Copie/Metadata/StreamCollection.cs | 70 ++ .../Metadata/UnidentifiedMetadata.cs | 8 + source - Copie/Notes/DrumsNote.cs | 32 + source - Copie/Notes/INote.cs | 9 + source - Copie/Notes/LaneNote.cs | 16 + source - Copie/Notes/LaneNoteCollection.cs | 95 +++ source - Copie/Notes/LaneNoteGeneric.cs | 24 + source - Copie/Notes/Syllable.cs | 61 ++ source - Copie/Notes/VocalsPitch.cs | 72 ++ source - Copie/Song.cs | 123 +++ .../Special/InstrumentSpecialPhrase.cs | 37 + source - Copie/Special/SpecialPhrase.cs | 28 + source - Copie/Special/TrackSpecialPhrase.cs | 39 + source - Copie/Sync/SyncTrack.cs | 40 + source - Copie/Sync/Tempo.cs | 86 +++ source - Copie/Sync/TempoMap.cs | 208 ++++++ source - Copie/Sync/TimeSignature.cs | 28 + source - Copie/Tools/LengthMerger.cs | 16 + source - Copie/Tools/Optimizer.cs | 153 ++++ source - Copie/Tools/Printer.cs | 89 +++ source - Copie/Tools/PropertyMerger.cs | 49 ++ source - Copie/Tools/TempoRescaler.cs | 103 +++ .../TrackObjects/ILongTrackObject.cs | 9 + .../TrackObjects/IReadOnlyTrackObject.cs | 15 + source - Copie/TrackObjects/ITrackObject.cs | 10 + .../TrackObjects/TrackObjectBase.cs | 9 + source - Copie/Tracks/Track.cs | 133 ++++ source - Copie/Tracks/TrackGeneric.cs | 26 + .../Tracks/UniqueTrackObjectCollection.cs | 39 + source - Copie/source.projitems | 166 ++++ source/ChartTools/ChartTools.csproj | 9 + source/ChartTools/Class1.cs | 7 + .../obj/ChartTools.csproj.nuget.dgspec.json | 67 ++ .../obj/ChartTools.csproj.nuget.g.props | 16 + .../obj/ChartTools.csproj.nuget.g.targets | 2 + ...CoreApp,Version=v8.0.AssemblyAttributes.cs | 4 + .../Debug/net8.0/ChartTools.AssemblyInfo.cs | 23 + .../ChartTools.AssemblyInfoInputs.cache | 1 + ....GeneratedMSBuildEditorConfig.editorconfig | 13 + .../Debug/net8.0/ChartTools.GlobalUsings.g.cs | 8 + .../obj/Debug/net8.0/ChartTools.assets.cache | Bin 0 -> 228 bytes source/ChartTools/obj/project.assets.json | 73 ++ source/ChartTools/obj/project.nuget.cache | 8 + 323 files changed, 8315 insertions(+), 147 deletions(-) create mode 100644 ChartTools/ChartTools.csproj rename {source => ChartTools}/Chords/DrumsChord.cs (100%) rename {source => ChartTools}/Chords/GHLChord.cs (100%) rename {source => ChartTools}/Chords/IChord.cs (100%) rename {source => ChartTools}/Chords/LaneChord.cs (100%) rename {source => ChartTools}/Chords/LaneChordGeneric.cs (100%) rename {source => ChartTools}/Chords/Phrase.cs (100%) rename {source => ChartTools}/Chords/StandardChord.cs (100%) rename {source => ChartTools}/Enums.cs (100%) rename {source => ChartTools}/Events/Event.cs (100%) rename {source => ChartTools}/Events/EventArgumentHelper.cs (100%) create mode 100644 ChartTools/Events/EventExtensions.cs rename {source => ChartTools}/Events/EventTypeHeaderHelper.cs (100%) rename {source => ChartTools}/Events/EventTypeHelper.cs (100%) rename {source => ChartTools}/Events/GlobalEvent.cs (100%) rename {source => ChartTools}/Events/LocalEvent.cs (100%) rename {source => ChartTools}/Exceptions/DesynchronizedAnchorException.cs (100%) rename {source => ChartTools}/Exceptions/UndefinedEnumException.cs (100%) rename {source => ChartTools}/Exceptions/Validator.cs (100%) create mode 100644 ChartTools/Extensions/Collections/Alternating/OrderedAlternatingEnumerable.cs create mode 100644 ChartTools/Extensions/Collections/Alternating/SerialAlternatingEnumerable.cs rename {source => ChartTools}/Extensions/Collections/Delayed/DelayedEnumerable.cs (100%) rename {source => ChartTools}/Extensions/Collections/Delayed/DelayedEnumerableSource.cs (100%) rename {source => ChartTools}/Extensions/Collections/Delayed/DelayedEnumerator.cs (100%) rename {source => ChartTools}/Extensions/Collections/EagerEnumerable.cs (100%) rename {source => ChartTools}/Extensions/Collections/IInitializable.cs (100%) rename {source => ChartTools}/Extensions/EnumCache.cs (100%) rename {source => ChartTools}/Extensions/EqualityComparison.cs (100%) rename {source => ChartTools}/Extensions/FuncEqualityComparer.cs (100%) rename {source => ChartTools}/Extensions/IInitializable.cs (100%) rename {source => ChartTools}/Extensions/Linq/CollectionExtensions.cs (100%) create mode 100644 ChartTools/Extensions/Linq/EnumerableExtensions.cs rename {source => ChartTools}/Extensions/Linq/SectionReplacement.cs (100%) rename {source => ChartTools}/Extensions/StringExtensions.cs (100%) rename {source => ChartTools}/GlobalSuppressions.cs (100%) rename {source => ChartTools}/IEmptyVerifiable.cs (100%) rename {source => ChartTools}/ILongObject.cs (100%) rename {source => ChartTools}/IO/Anchor.cs (100%) rename {source => ChartTools}/IO/Appliables/IInstrumentAppliable.cs (100%) rename {source => ChartTools}/IO/Appliables/ISongAppliable.cs (100%) rename {source => ChartTools}/IO/Chart/ChartFile.cs (100%) rename {source => ChartTools}/IO/Chart/ChartFileReader.cs (100%) rename {source => ChartTools}/IO/Chart/ChartFileWriter.cs (100%) rename {source => ChartTools}/IO/Chart/ChartFormatting.cs (100%) rename {source => ChartTools}/IO/Chart/ChartSectionSet.cs (100%) rename {source => ChartTools}/IO/Chart/Entries/NoteData.cs (100%) rename {source => ChartTools}/IO/Chart/Entries/TrackObjectEntry.cs (100%) rename {source => ChartTools}/IO/Chart/Parsing/ChartParser.cs (100%) rename {source => ChartTools}/IO/Chart/Parsing/DrumsTrackParser.cs (100%) rename {source => ChartTools}/IO/Chart/Parsing/GHLTrackParser.cs (100%) rename {source => ChartTools}/IO/Chart/Parsing/GlobalEventParser.cs (100%) rename {source => ChartTools}/IO/Chart/Parsing/MetadataParser.cs (100%) rename {source => ChartTools}/IO/Chart/Parsing/StandardTrackParser.cs (100%) rename {source => ChartTools}/IO/Chart/Parsing/SyncTrackParser.cs (100%) rename {source => ChartTools}/IO/Chart/Parsing/TrackParser.cs (100%) rename {source => ChartTools}/IO/Chart/Parsing/UnknownSectionParser.cs (100%) rename {source => ChartTools}/IO/Chart/Parsing/VariableInstrumentTrackParser.cs (100%) rename {source => ChartTools}/IO/Chart/Providers/ChordProvider.cs (100%) rename {source => ChartTools}/IO/Chart/Providers/EventProvider.cs (100%) rename {source => ChartTools}/IO/Chart/Providers/SpecialPhraseProvider.cs (100%) rename {source => ChartTools}/IO/Chart/Providers/SyncTrackProvider.cs (100%) rename {source => ChartTools}/IO/Chart/Providers/TempoProvider.cs (100%) rename {source => ChartTools}/IO/Chart/Providers/TimeSignatureProvider.cs (100%) rename {source => ChartTools}/IO/Chart/Serializing/ChartKeySerializableAttribute.cs (100%) rename {source => ChartTools}/IO/Chart/Serializing/GlobalEventSerializer.cs (100%) rename {source => ChartTools}/IO/Chart/Serializing/MetadataSerializer.cs (100%) rename {source => ChartTools}/IO/Chart/Serializing/SyncTrackSerializer.cs (100%) create mode 100644 ChartTools/IO/Chart/Serializing/TrackObjectGroupSerializer.cs create mode 100644 ChartTools/IO/Chart/Serializing/TrackSerializer.cs rename {source => ChartTools}/IO/Chart/Serializing/UnknownSectionSerializer.cs (100%) rename {source => ChartTools}/IO/Configuration/CommonConfiguration.cs (100%) rename {source => ChartTools}/IO/Configuration/ConfigurationExceptions.cs (100%) rename {source => ChartTools}/IO/Configuration/Enums.cs (100%) rename {source => ChartTools}/IO/Configuration/ReadingConfiguration.cs (100%) rename {source => ChartTools}/IO/Configuration/Sessions/ReadingSession.cs (100%) rename {source => ChartTools}/IO/Configuration/Sessions/Session.cs (100%) rename {source => ChartTools}/IO/Configuration/Sessions/WritingSession.cs (100%) rename {source => ChartTools}/IO/Configuration/WritingConfiguration.cs (100%) rename {source => ChartTools}/IO/DirectoryHandler.cs (100%) rename {source => ChartTools}/IO/Exceptions/EntryException.cs (100%) rename {source => ChartTools}/IO/Exceptions/LineException.cs (100%) rename {source => ChartTools}/IO/Exceptions/ParseException.cs (100%) rename {source => ChartTools}/IO/Exceptions/SectionException.cs (100%) rename {source => ChartTools}/IO/ExtensionHandler.cs (100%) rename {source => ChartTools}/IO/FileReader.cs (100%) rename {source => ChartTools}/IO/Formatting/FormattingEnums.cs (100%) rename {source => ChartTools}/IO/Formatting/FormattingRules.cs (100%) rename {source => ChartTools}/IO/ISerializerDataProvider.cs (100%) rename {source => ChartTools}/IO/Ini/IniFile.cs (100%) rename {source => ChartTools}/IO/Ini/IniFileReader.cs (100%) rename {source => ChartTools}/IO/Ini/IniFileWriter.cs (100%) rename {source => ChartTools}/IO/Ini/IniFormatting.cs (100%) rename {source => ChartTools}/IO/Ini/IniKeySerializableAttribute.cs (100%) rename {source => ChartTools}/IO/Ini/IniParser.cs (100%) rename {source => ChartTools}/IO/Ini/IniSerializer.cs (100%) rename {source => ChartTools}/IO/Parsers/FileParser.cs (100%) rename {source => ChartTools}/IO/Parsers/SectionParser.cs (100%) rename {source => ChartTools}/IO/Parsers/TextParser.cs (100%) rename {source => ChartTools}/IO/Sections/ReservedSectionHeader.cs (100%) rename {source => ChartTools}/IO/Sections/ReservedSectionHeaderSet.cs (100%) rename {source => ChartTools}/IO/Sections/Section.cs (100%) rename {source => ChartTools}/IO/Sections/SectionSet.cs (100%) rename {source => ChartTools}/IO/Serializers/GroupSerializer.cs (100%) rename {source => ChartTools}/IO/Serializers/KeySerializable.cs (100%) rename {source => ChartTools}/IO/Serializers/Serializer.cs (100%) rename {source => ChartTools}/IO/TextEntry.cs (100%) rename {source => ChartTools}/IO/TextFileReader.cs (100%) rename {source => ChartTools}/IO/TextFileWriter.cs (100%) rename {source => ChartTools}/IO/ValueParser.cs (100%) rename {source => ChartTools}/IReadOnlyLongObject.cs (100%) rename {source => ChartTools}/Instruments/Drums.cs (100%) rename {source => ChartTools}/Instruments/GHLInstrument.cs (100%) rename {source => ChartTools}/Instruments/Instrument.cs (100%) rename {source => ChartTools}/Instruments/InstrumentGeneric.cs (100%) rename {source => ChartTools}/Instruments/InstrumentSet.cs (100%) rename {source => ChartTools}/Instruments/StandardInstrument.cs (100%) rename {source => ChartTools}/Instruments/Vocals.cs (100%) rename {source => ChartTools}/Metadata/Charter.cs (100%) rename {source => ChartTools}/Metadata/InstrumentDifficultySet.cs (100%) rename {source => ChartTools}/Metadata/Metadata.cs (100%) rename {source => ChartTools}/Metadata/StreamCollection.cs (100%) rename {source => ChartTools}/Metadata/UnidentifiedMetadata.cs (100%) rename {source => ChartTools}/Notes/DrumsNote.cs (100%) rename {source => ChartTools}/Notes/INote.cs (100%) rename {source => ChartTools}/Notes/LaneNote.cs (100%) rename {source => ChartTools}/Notes/LaneNoteCollection.cs (100%) rename {source => ChartTools}/Notes/LaneNoteGeneric.cs (100%) rename {source => ChartTools}/Notes/Syllable.cs (100%) rename {source => ChartTools}/Notes/VocalsPitch.cs (100%) rename {source => ChartTools}/Song.cs (100%) rename {source => ChartTools}/Special/InstrumentSpecialPhrase.cs (100%) rename {source => ChartTools}/Special/SpecialPhrase.cs (100%) rename {source => ChartTools}/Special/TrackSpecialPhrase.cs (100%) rename {source => ChartTools}/Sync/SyncTrack.cs (100%) rename {source => ChartTools}/Sync/Tempo.cs (100%) rename {source => ChartTools}/Sync/TempoMap.cs (100%) rename {source => ChartTools}/Sync/TimeSignature.cs (100%) rename {source => ChartTools}/Tools/LengthMerger.cs (100%) rename {source => ChartTools}/Tools/Optimizer.cs (100%) rename {source => ChartTools}/Tools/Printer.cs (100%) rename {source => ChartTools}/Tools/PropertyMerger.cs (100%) rename {source => ChartTools}/Tools/TempoRescaler.cs (100%) rename {source => ChartTools}/TrackObjects/ILongTrackObject.cs (100%) rename {source => ChartTools}/TrackObjects/IReadOnlyTrackObject.cs (100%) rename {source => ChartTools}/TrackObjects/ITrackObject.cs (100%) rename {source => ChartTools}/TrackObjects/TrackObjectBase.cs (100%) rename {source => ChartTools}/Tracks/Track.cs (100%) rename {source => ChartTools}/Tracks/TrackGeneric.cs (100%) rename {source => ChartTools}/Tracks/UniqueTrackObjectCollection.cs (100%) rename {source => ChartTools}/source.projitems (100%) delete mode 100644 Net6/Net6.csproj delete mode 100644 Net7/Net7.csproj rename {source => source - Copie}/ChartTools.shproj (100%) create mode 100644 source - Copie/Chords/DrumsChord.cs create mode 100644 source - Copie/Chords/GHLChord.cs create mode 100644 source - Copie/Chords/IChord.cs create mode 100644 source - Copie/Chords/LaneChord.cs create mode 100644 source - Copie/Chords/LaneChordGeneric.cs create mode 100644 source - Copie/Chords/Phrase.cs create mode 100644 source - Copie/Chords/StandardChord.cs create mode 100644 source - Copie/Enums.cs create mode 100644 source - Copie/Events/Event.cs create mode 100644 source - Copie/Events/EventArgumentHelper.cs rename {source => source - Copie}/Events/EventExtensions.cs (100%) create mode 100644 source - Copie/Events/EventTypeHeaderHelper.cs create mode 100644 source - Copie/Events/EventTypeHelper.cs create mode 100644 source - Copie/Events/GlobalEvent.cs create mode 100644 source - Copie/Events/LocalEvent.cs create mode 100644 source - Copie/Exceptions/DesynchronizedAnchorException.cs create mode 100644 source - Copie/Exceptions/UndefinedEnumException.cs create mode 100644 source - Copie/Exceptions/Validator.cs rename {source => source - Copie}/Extensions/Collections/Alternating/Ordered/OrderedAlternatingEnumerable.cs (100%) rename {source => source - Copie}/Extensions/Collections/Alternating/Ordered/OrderedAlternatingEnumerator.cs (100%) rename {source => source - Copie}/Extensions/Collections/Alternating/Serial/SerialAlternatingEnumerable.cs (100%) rename {source => source - Copie}/Extensions/Collections/Alternating/Serial/SerialAlternatingEnumerator.cs (100%) create mode 100644 source - Copie/Extensions/Collections/Delayed/DelayedEnumerable.cs create mode 100644 source - Copie/Extensions/Collections/Delayed/DelayedEnumerableSource.cs create mode 100644 source - Copie/Extensions/Collections/Delayed/DelayedEnumerator.cs create mode 100644 source - Copie/Extensions/Collections/EagerEnumerable.cs create mode 100644 source - Copie/Extensions/Collections/IInitializable.cs create mode 100644 source - Copie/Extensions/EnumCache.cs create mode 100644 source - Copie/Extensions/EqualityComparison.cs create mode 100644 source - Copie/Extensions/FuncEqualityComparer.cs create mode 100644 source - Copie/Extensions/IInitializable.cs create mode 100644 source - Copie/Extensions/Linq/CollectionExtensions.cs rename {source => source - Copie}/Extensions/Linq/EnumerableExtensions.cs (100%) create mode 100644 source - Copie/Extensions/Linq/SectionReplacement.cs create mode 100644 source - Copie/Extensions/StringExtensions.cs create mode 100644 source - Copie/GlobalSuppressions.cs create mode 100644 source - Copie/IEmptyVerifiable.cs create mode 100644 source - Copie/ILongObject.cs create mode 100644 source - Copie/IO/Anchor.cs create mode 100644 source - Copie/IO/Appliables/IInstrumentAppliable.cs create mode 100644 source - Copie/IO/Appliables/ISongAppliable.cs create mode 100644 source - Copie/IO/Chart/ChartFile.cs create mode 100644 source - Copie/IO/Chart/ChartFileReader.cs create mode 100644 source - Copie/IO/Chart/ChartFileWriter.cs create mode 100644 source - Copie/IO/Chart/ChartFormatting.cs create mode 100644 source - Copie/IO/Chart/ChartSectionSet.cs create mode 100644 source - Copie/IO/Chart/Entries/NoteData.cs create mode 100644 source - Copie/IO/Chart/Entries/TrackObjectEntry.cs create mode 100644 source - Copie/IO/Chart/Parsing/ChartParser.cs create mode 100644 source - Copie/IO/Chart/Parsing/DrumsTrackParser.cs create mode 100644 source - Copie/IO/Chart/Parsing/GHLTrackParser.cs create mode 100644 source - Copie/IO/Chart/Parsing/GlobalEventParser.cs create mode 100644 source - Copie/IO/Chart/Parsing/MetadataParser.cs create mode 100644 source - Copie/IO/Chart/Parsing/StandardTrackParser.cs create mode 100644 source - Copie/IO/Chart/Parsing/SyncTrackParser.cs create mode 100644 source - Copie/IO/Chart/Parsing/TrackParser.cs create mode 100644 source - Copie/IO/Chart/Parsing/UnknownSectionParser.cs create mode 100644 source - Copie/IO/Chart/Parsing/VariableInstrumentTrackParser.cs create mode 100644 source - Copie/IO/Chart/Providers/ChordProvider.cs create mode 100644 source - Copie/IO/Chart/Providers/EventProvider.cs create mode 100644 source - Copie/IO/Chart/Providers/SpecialPhraseProvider.cs create mode 100644 source - Copie/IO/Chart/Providers/SyncTrackProvider.cs create mode 100644 source - Copie/IO/Chart/Providers/TempoProvider.cs create mode 100644 source - Copie/IO/Chart/Providers/TimeSignatureProvider.cs create mode 100644 source - Copie/IO/Chart/Serializing/ChartKeySerializableAttribute.cs create mode 100644 source - Copie/IO/Chart/Serializing/GlobalEventSerializer.cs create mode 100644 source - Copie/IO/Chart/Serializing/MetadataSerializer.cs create mode 100644 source - Copie/IO/Chart/Serializing/SyncTrackSerializer.cs rename {source => source - Copie}/IO/Chart/Serializing/TrackObjectGroupSerializer.cs (100%) rename {source => source - Copie}/IO/Chart/Serializing/TrackSerializer.cs (100%) create mode 100644 source - Copie/IO/Chart/Serializing/UnknownSectionSerializer.cs create mode 100644 source - Copie/IO/Configuration/CommonConfiguration.cs create mode 100644 source - Copie/IO/Configuration/ConfigurationExceptions.cs create mode 100644 source - Copie/IO/Configuration/Enums.cs create mode 100644 source - Copie/IO/Configuration/ReadingConfiguration.cs create mode 100644 source - Copie/IO/Configuration/Sessions/ReadingSession.cs create mode 100644 source - Copie/IO/Configuration/Sessions/Session.cs create mode 100644 source - Copie/IO/Configuration/Sessions/WritingSession.cs create mode 100644 source - Copie/IO/Configuration/WritingConfiguration.cs create mode 100644 source - Copie/IO/DirectoryHandler.cs create mode 100644 source - Copie/IO/Exceptions/EntryException.cs create mode 100644 source - Copie/IO/Exceptions/LineException.cs create mode 100644 source - Copie/IO/Exceptions/ParseException.cs create mode 100644 source - Copie/IO/Exceptions/SectionException.cs create mode 100644 source - Copie/IO/ExtensionHandler.cs create mode 100644 source - Copie/IO/FileReader.cs create mode 100644 source - Copie/IO/Formatting/FormattingEnums.cs create mode 100644 source - Copie/IO/Formatting/FormattingRules.cs create mode 100644 source - Copie/IO/ISerializerDataProvider.cs create mode 100644 source - Copie/IO/Ini/IniFile.cs create mode 100644 source - Copie/IO/Ini/IniFileReader.cs create mode 100644 source - Copie/IO/Ini/IniFileWriter.cs create mode 100644 source - Copie/IO/Ini/IniFormatting.cs create mode 100644 source - Copie/IO/Ini/IniKeySerializableAttribute.cs create mode 100644 source - Copie/IO/Ini/IniParser.cs create mode 100644 source - Copie/IO/Ini/IniSerializer.cs create mode 100644 source - Copie/IO/Parsers/FileParser.cs create mode 100644 source - Copie/IO/Parsers/SectionParser.cs create mode 100644 source - Copie/IO/Parsers/TextParser.cs create mode 100644 source - Copie/IO/Sections/ReservedSectionHeader.cs create mode 100644 source - Copie/IO/Sections/ReservedSectionHeaderSet.cs create mode 100644 source - Copie/IO/Sections/Section.cs create mode 100644 source - Copie/IO/Sections/SectionSet.cs create mode 100644 source - Copie/IO/Serializers/GroupSerializer.cs create mode 100644 source - Copie/IO/Serializers/KeySerializable.cs create mode 100644 source - Copie/IO/Serializers/Serializer.cs create mode 100644 source - Copie/IO/TextEntry.cs create mode 100644 source - Copie/IO/TextFileReader.cs create mode 100644 source - Copie/IO/TextFileWriter.cs create mode 100644 source - Copie/IO/ValueParser.cs create mode 100644 source - Copie/IReadOnlyLongObject.cs create mode 100644 source - Copie/Instruments/Drums.cs create mode 100644 source - Copie/Instruments/GHLInstrument.cs create mode 100644 source - Copie/Instruments/Instrument.cs create mode 100644 source - Copie/Instruments/InstrumentGeneric.cs create mode 100644 source - Copie/Instruments/InstrumentSet.cs create mode 100644 source - Copie/Instruments/StandardInstrument.cs create mode 100644 source - Copie/Instruments/Vocals.cs create mode 100644 source - Copie/Metadata/Charter.cs create mode 100644 source - Copie/Metadata/InstrumentDifficultySet.cs create mode 100644 source - Copie/Metadata/Metadata.cs create mode 100644 source - Copie/Metadata/StreamCollection.cs create mode 100644 source - Copie/Metadata/UnidentifiedMetadata.cs create mode 100644 source - Copie/Notes/DrumsNote.cs create mode 100644 source - Copie/Notes/INote.cs create mode 100644 source - Copie/Notes/LaneNote.cs create mode 100644 source - Copie/Notes/LaneNoteCollection.cs create mode 100644 source - Copie/Notes/LaneNoteGeneric.cs create mode 100644 source - Copie/Notes/Syllable.cs create mode 100644 source - Copie/Notes/VocalsPitch.cs create mode 100644 source - Copie/Song.cs create mode 100644 source - Copie/Special/InstrumentSpecialPhrase.cs create mode 100644 source - Copie/Special/SpecialPhrase.cs create mode 100644 source - Copie/Special/TrackSpecialPhrase.cs create mode 100644 source - Copie/Sync/SyncTrack.cs create mode 100644 source - Copie/Sync/Tempo.cs create mode 100644 source - Copie/Sync/TempoMap.cs create mode 100644 source - Copie/Sync/TimeSignature.cs create mode 100644 source - Copie/Tools/LengthMerger.cs create mode 100644 source - Copie/Tools/Optimizer.cs create mode 100644 source - Copie/Tools/Printer.cs create mode 100644 source - Copie/Tools/PropertyMerger.cs create mode 100644 source - Copie/Tools/TempoRescaler.cs create mode 100644 source - Copie/TrackObjects/ILongTrackObject.cs create mode 100644 source - Copie/TrackObjects/IReadOnlyTrackObject.cs create mode 100644 source - Copie/TrackObjects/ITrackObject.cs create mode 100644 source - Copie/TrackObjects/TrackObjectBase.cs create mode 100644 source - Copie/Tracks/Track.cs create mode 100644 source - Copie/Tracks/TrackGeneric.cs create mode 100644 source - Copie/Tracks/UniqueTrackObjectCollection.cs create mode 100644 source - Copie/source.projitems create mode 100644 source/ChartTools/ChartTools.csproj create mode 100644 source/ChartTools/Class1.cs create mode 100644 source/ChartTools/obj/ChartTools.csproj.nuget.dgspec.json create mode 100644 source/ChartTools/obj/ChartTools.csproj.nuget.g.props create mode 100644 source/ChartTools/obj/ChartTools.csproj.nuget.g.targets create mode 100644 source/ChartTools/obj/Debug/net8.0/.NETCoreApp,Version=v8.0.AssemblyAttributes.cs create mode 100644 source/ChartTools/obj/Debug/net8.0/ChartTools.AssemblyInfo.cs create mode 100644 source/ChartTools/obj/Debug/net8.0/ChartTools.AssemblyInfoInputs.cache create mode 100644 source/ChartTools/obj/Debug/net8.0/ChartTools.GeneratedMSBuildEditorConfig.editorconfig create mode 100644 source/ChartTools/obj/Debug/net8.0/ChartTools.GlobalUsings.g.cs create mode 100644 source/ChartTools/obj/Debug/net8.0/ChartTools.assets.cache create mode 100644 source/ChartTools/obj/project.assets.json create mode 100644 source/ChartTools/obj/project.nuget.cache diff --git a/ChartTools.Tests/AlternatingTests.cs b/ChartTools.Tests/AlternatingTests.cs index c18c2393..6e9dbc23 100644 --- a/ChartTools.Tests/AlternatingTests.cs +++ b/ChartTools.Tests/AlternatingTests.cs @@ -1,4 +1,4 @@ -using ChartTools.Extensions.Collections; +using ChartTools.Extensions.Collections.Alternating; using Microsoft.VisualStudio.TestTools.UnitTesting; @@ -7,39 +7,22 @@ namespace ChartTools.Tests; [TestClass] public class SerialAlternatingTests { - static readonly byte[] testArrayA = new byte[] { 1, 6, 2 }; - static readonly byte[] testArrayB = new byte[] { 3, 5, 6 }; + static readonly byte[] testArrayA = [ 1, 6, 2 ]; + static readonly byte[] testArrayB = [ 3, 5, 6 ]; const string expected = "1 3 6 5 2 6"; [TestMethod] public void CreateEnumerableNull() => Assert.ThrowsException(() => new SerialAlternatingEnumerable(null!)); [TestMethod] public void CreateEnumerableEmpty() => Assert.ThrowsException(() => new SerialAlternatingEnumerable()); - [TestMethod] public void CreateEnumeratorNull() => Assert.ThrowsException(() => new SerialAlternatingEnumerator(null!)); - [TestMethod] public void CreateEnumeratorEmpty() => Assert.ThrowsException(() => new SerialAlternatingEnumerator()); - - [TestMethod] public void EnumerateFromEnumerable() => Assert.AreEqual(expected, Formatting.FormatCollection(new SerialAlternatingEnumerable(testArrayA, testArrayB))); - - [TestMethod] public void EnumerateFromEnumertor() - { - var enumerator = new SerialAlternatingEnumerator(testArrayA.AsEnumerable().GetEnumerator(), testArrayB.AsEnumerable().GetEnumerator()); - var output = new List(6); - - for (int i = 0; i < 6; i++) - { - Assert.IsTrue(enumerator.MoveNext()); - output.Add(enumerator.Current); - } - - Assert.AreEqual(expected, Formatting.FormatCollection(output)); - } + [TestMethod] public void Enumerate() => Assert.AreEqual(expected, Formatting.FormatCollection(new SerialAlternatingEnumerable(testArrayA, testArrayB))); } [TestClass] public class OrderedAlternatingTests { static readonly Func keyGetter = n => n; - static readonly byte[] testArrayA = new byte[] { 1, 6, 2 }; - static readonly byte[] testArrayB = new byte[] { 3, 5, 6 }; + static readonly byte[] testArrayA = [1, 6, 2]; + static readonly byte[] testArrayB = [3, 5, 6]; const string expected = "1 3 5 6 2 6"; [TestMethod] public void CreateEnumerableNullKeyGetter() => Assert.ThrowsException(() => new OrderedAlternatingEnumerable(null!, null!)); @@ -47,19 +30,5 @@ public class OrderedAlternatingTests [TestMethod] public void CreateEnumerableNullEnumerables() => Assert.ThrowsException(() => new OrderedAlternatingEnumerable(null!, null!)); [TestMethod] public void CreateEnumerableEmptyEnumerables() => Assert.ThrowsException(() => new OrderedAlternatingEnumerable(keyGetter)); - [TestMethod] public void EnumerateFromEnumerable() => Assert.AreEqual(expected, Formatting.FormatCollection(new OrderedAlternatingEnumerable(keyGetter, testArrayA, testArrayB))); - - [TestMethod] public void EnumerateFromEnumertor() - { - var enumerator = new OrderedAlternatingEnumerator(keyGetter, testArrayA.AsEnumerable().GetEnumerator(), testArrayB.AsEnumerable().GetEnumerator()); - var output = new List(6); - - for (int i = 0; i < 6; i++) - { - Assert.IsTrue(enumerator.MoveNext()); - output.Add(enumerator.Current); - } - - Assert.AreEqual(expected, Formatting.FormatCollection(output)); - } + [TestMethod] public void Enumerate() => Assert.AreEqual(expected, Formatting.FormatCollection(new OrderedAlternatingEnumerable(keyGetter, testArrayA, testArrayB))); } diff --git a/ChartTools.Tests/ChartTools.Tests.csproj b/ChartTools.Tests/ChartTools.Tests.csproj index 17c0864c..98413670 100644 --- a/ChartTools.Tests/ChartTools.Tests.csproj +++ b/ChartTools.Tests/ChartTools.Tests.csproj @@ -1,7 +1,7 @@ - net7.0 + net8.0 preview false enable @@ -10,16 +10,17 @@ - - - - - + + + + all runtime; build; native; contentfiles; analyzers; buildtransitive - + + + diff --git a/ChartTools.Tests/SystemExtensionsTests.cs b/ChartTools.Tests/SystemExtensionsTests.cs index 7cee4d16..2ca72873 100644 --- a/ChartTools.Tests/SystemExtensionsTests.cs +++ b/ChartTools.Tests/SystemExtensionsTests.cs @@ -6,9 +6,9 @@ namespace ChartTools.Tests; [TestClass] public class SystemExtensionsTests { - static readonly bool[] trueArray = new bool[] { true, true }; - static readonly bool[] falseArray = new bool[] { false, false }; - static readonly bool[] mixBoolArray = new bool[] { true, false }; + static readonly bool[] trueArray = [true, true]; + static readonly bool[] falseArray = [false, false]; + static readonly bool[] mixBoolArray = [true, false]; [TestMethod] public void AllNoBools() => Assert.AreEqual(true, Array.Empty().All()); [TestMethod] public void AllNoFalse() => Assert.AreEqual(true, trueArray.All()); diff --git a/ChartTools.sln b/ChartTools.sln index f223a4b6..0d2cbe5d 100644 --- a/ChartTools.sln +++ b/ChartTools.sln @@ -5,15 +5,9 @@ VisualStudioVersion = 17.0.31612.314 MinimumVisualStudioVersion = 10.0.40219.1 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ChartTools.Tests", "ChartTools.Tests\ChartTools.Tests.csproj", "{A5A22025-AFC9-49D2-A338-CAE8EA750E18}" EndProject -Project("{D954291E-2A0B-460D-934E-DC6B0785DB48}") = "ChartTools", "source\ChartTools.shproj", "{54924BC3-4A10-45AD-AF3C-F17494E403E1}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Net6", "Net6\Net6.csproj", "{965901B2-4DD8-4DAF-836F-A0DFF21DB9E4}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Debug", "Debug\Debug.csproj", "{A309B8D1-538E-4C63-93B7-A56125B169E9}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Docs", "Docs\Docs.csproj", "{B5A19C40-6CCD-48B2-827F-0D24B6271530}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Net7", "Net7\Net7.csproj", "{07D226A8-7464-4DE0-A384-A26F37153CDA}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ChartTools", "ChartTools\ChartTools.csproj", "{487BDC85-A45E-4896-9F1F-1A8EC145BD19}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -27,27 +21,16 @@ Global {A5A22025-AFC9-49D2-A338-CAE8EA750E18}.Docs|Any CPU.ActiveCfg = Docs|Any CPU {A5A22025-AFC9-49D2-A338-CAE8EA750E18}.Release|Any CPU.ActiveCfg = Release|Any CPU {A5A22025-AFC9-49D2-A338-CAE8EA750E18}.Release|Any CPU.Build.0 = Release|Any CPU - {965901B2-4DD8-4DAF-836F-A0DFF21DB9E4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {965901B2-4DD8-4DAF-836F-A0DFF21DB9E4}.Debug|Any CPU.Build.0 = Debug|Any CPU - {965901B2-4DD8-4DAF-836F-A0DFF21DB9E4}.Docs|Any CPU.ActiveCfg = Debug|Any CPU - {965901B2-4DD8-4DAF-836F-A0DFF21DB9E4}.Docs|Any CPU.Build.0 = Debug|Any CPU - {965901B2-4DD8-4DAF-836F-A0DFF21DB9E4}.Release|Any CPU.ActiveCfg = Release|Any CPU - {965901B2-4DD8-4DAF-836F-A0DFF21DB9E4}.Release|Any CPU.Build.0 = Release|Any CPU - {A309B8D1-538E-4C63-93B7-A56125B169E9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {A309B8D1-538E-4C63-93B7-A56125B169E9}.Debug|Any CPU.Build.0 = Debug|Any CPU - {A309B8D1-538E-4C63-93B7-A56125B169E9}.Docs|Any CPU.ActiveCfg = Docs|Any CPU - {A309B8D1-538E-4C63-93B7-A56125B169E9}.Release|Any CPU.ActiveCfg = Release|Any CPU - {A309B8D1-538E-4C63-93B7-A56125B169E9}.Release|Any CPU.Build.0 = Release|Any CPU {B5A19C40-6CCD-48B2-827F-0D24B6271530}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {B5A19C40-6CCD-48B2-827F-0D24B6271530}.Docs|Any CPU.ActiveCfg = Release|Any CPU {B5A19C40-6CCD-48B2-827F-0D24B6271530}.Docs|Any CPU.Build.0 = Release|Any CPU {B5A19C40-6CCD-48B2-827F-0D24B6271530}.Release|Any CPU.ActiveCfg = Release|Any CPU - {07D226A8-7464-4DE0-A384-A26F37153CDA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {07D226A8-7464-4DE0-A384-A26F37153CDA}.Debug|Any CPU.Build.0 = Debug|Any CPU - {07D226A8-7464-4DE0-A384-A26F37153CDA}.Docs|Any CPU.ActiveCfg = Debug|Any CPU - {07D226A8-7464-4DE0-A384-A26F37153CDA}.Docs|Any CPU.Build.0 = Debug|Any CPU - {07D226A8-7464-4DE0-A384-A26F37153CDA}.Release|Any CPU.ActiveCfg = Release|Any CPU - {07D226A8-7464-4DE0-A384-A26F37153CDA}.Release|Any CPU.Build.0 = Release|Any CPU + {487BDC85-A45E-4896-9F1F-1A8EC145BD19}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {487BDC85-A45E-4896-9F1F-1A8EC145BD19}.Debug|Any CPU.Build.0 = Debug|Any CPU + {487BDC85-A45E-4896-9F1F-1A8EC145BD19}.Docs|Any CPU.ActiveCfg = Debug|Any CPU + {487BDC85-A45E-4896-9F1F-1A8EC145BD19}.Docs|Any CPU.Build.0 = Debug|Any CPU + {487BDC85-A45E-4896-9F1F-1A8EC145BD19}.Release|Any CPU.ActiveCfg = Release|Any CPU + {487BDC85-A45E-4896-9F1F-1A8EC145BD19}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -55,10 +38,4 @@ Global GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {BBCCC1EF-9696-4588-8FA9-7924F6507D21} EndGlobalSection - GlobalSection(SharedMSBuildProjectFiles) = preSolution - source\source.projitems*{07d226a8-7464-4de0-a384-a26f37153cda}*SharedItemsImports = 5 - source\source.projitems*{54924bc3-4a10-45ad-af3c-f17494e403e1}*SharedItemsImports = 13 - source\source.projitems*{965901b2-4dd8-4daf-836f-a0dff21db9e4}*SharedItemsImports = 5 - source\source.projitems*{a5a22025-afc9-49d2-a338-cae8ea750e18}*SharedItemsImports = 5 - EndGlobalSection EndGlobal diff --git a/ChartTools/ChartTools.csproj b/ChartTools/ChartTools.csproj new file mode 100644 index 00000000..2c89b07f --- /dev/null +++ b/ChartTools/ChartTools.csproj @@ -0,0 +1,13 @@ + + + + net8.0 + enable + enable + + + + + + + diff --git a/source/Chords/DrumsChord.cs b/ChartTools/Chords/DrumsChord.cs similarity index 100% rename from source/Chords/DrumsChord.cs rename to ChartTools/Chords/DrumsChord.cs diff --git a/source/Chords/GHLChord.cs b/ChartTools/Chords/GHLChord.cs similarity index 100% rename from source/Chords/GHLChord.cs rename to ChartTools/Chords/GHLChord.cs diff --git a/source/Chords/IChord.cs b/ChartTools/Chords/IChord.cs similarity index 100% rename from source/Chords/IChord.cs rename to ChartTools/Chords/IChord.cs diff --git a/source/Chords/LaneChord.cs b/ChartTools/Chords/LaneChord.cs similarity index 100% rename from source/Chords/LaneChord.cs rename to ChartTools/Chords/LaneChord.cs diff --git a/source/Chords/LaneChordGeneric.cs b/ChartTools/Chords/LaneChordGeneric.cs similarity index 100% rename from source/Chords/LaneChordGeneric.cs rename to ChartTools/Chords/LaneChordGeneric.cs diff --git a/source/Chords/Phrase.cs b/ChartTools/Chords/Phrase.cs similarity index 100% rename from source/Chords/Phrase.cs rename to ChartTools/Chords/Phrase.cs diff --git a/source/Chords/StandardChord.cs b/ChartTools/Chords/StandardChord.cs similarity index 100% rename from source/Chords/StandardChord.cs rename to ChartTools/Chords/StandardChord.cs diff --git a/source/Enums.cs b/ChartTools/Enums.cs similarity index 100% rename from source/Enums.cs rename to ChartTools/Enums.cs diff --git a/source/Events/Event.cs b/ChartTools/Events/Event.cs similarity index 100% rename from source/Events/Event.cs rename to ChartTools/Events/Event.cs diff --git a/source/Events/EventArgumentHelper.cs b/ChartTools/Events/EventArgumentHelper.cs similarity index 100% rename from source/Events/EventArgumentHelper.cs rename to ChartTools/Events/EventArgumentHelper.cs diff --git a/ChartTools/Events/EventExtensions.cs b/ChartTools/Events/EventExtensions.cs new file mode 100644 index 00000000..18af6130 --- /dev/null +++ b/ChartTools/Events/EventExtensions.cs @@ -0,0 +1,63 @@ +using ChartTools.IO; +using ChartTools.IO.Chart; +using ChartTools.Extensions.Linq; +using ChartTools.Lyrics; + +namespace ChartTools.Events; + +/// +/// Provides additional methods for events. +/// +public static class EventExtensions +{ + public static void ToFile(this IEnumerable events, string path) => ExtensionHandler.Write(path, events, (".chart", (path, events) => ChartFile.ReplaceGlobalEvents(path, events))); + public static async Task ToFileAsync(this IEnumerable events, string path, CancellationToken cancellationToken) => await ExtensionHandler.WriteAsync(path, events, (".chart", (path, events) => ChartFile.ReplaceGlobalEventsAsync(path, events, cancellationToken))); + + /// + /// Gets the lyrics from an enumerable of + /// + /// Enumerable of + public static IEnumerable GetLyrics(this IEnumerable globalEvents) + { + Phrase? phrase = null; + + foreach (GlobalEvent globalEvent in globalEvents.OrderBy(e => e.Position)) + switch (globalEvent.EventType) + { + // Change active phrase + case EventTypeHelper.Global.PhraseStart: + if (phrase is not null) + yield return phrase; + + phrase = new Phrase(globalEvent.Position); + break; + // Add syllable to the active phrase using the event argument + case EventTypeHelper.Global.Lyric: + phrase?.Syllables.Add(new(globalEvent.Position - phrase.Position, VocalPitchValue.None) { RawText = globalEvent.Argument ?? string.Empty }); + break; + // Set length of active phrase + case EventTypeHelper.Global.PhraseEnd: + if (phrase is not null) + phrase.LengthOverride = globalEvent.Position - phrase.Position; + break; + } + + if (phrase is not null) + yield return phrase; + } + /// + /// Gets a set of where phrase and lyric events are replaced with the events making up a set of . + /// + /// Enumerable of + public static IEnumerable SetLyrics(this IEnumerable events, IEnumerable lyrics) + { + IEnumerable[] collections = + [ + events.Where(e => !e.IsLyricEvent), + lyrics.SelectMany(p => p.ToGlobalEvents()) + ]; + + foreach (var globalEvent in collections.AlternateBy(i => i.Position)) + yield return globalEvent; + } +} diff --git a/source/Events/EventTypeHeaderHelper.cs b/ChartTools/Events/EventTypeHeaderHelper.cs similarity index 100% rename from source/Events/EventTypeHeaderHelper.cs rename to ChartTools/Events/EventTypeHeaderHelper.cs diff --git a/source/Events/EventTypeHelper.cs b/ChartTools/Events/EventTypeHelper.cs similarity index 100% rename from source/Events/EventTypeHelper.cs rename to ChartTools/Events/EventTypeHelper.cs diff --git a/source/Events/GlobalEvent.cs b/ChartTools/Events/GlobalEvent.cs similarity index 100% rename from source/Events/GlobalEvent.cs rename to ChartTools/Events/GlobalEvent.cs diff --git a/source/Events/LocalEvent.cs b/ChartTools/Events/LocalEvent.cs similarity index 100% rename from source/Events/LocalEvent.cs rename to ChartTools/Events/LocalEvent.cs diff --git a/source/Exceptions/DesynchronizedAnchorException.cs b/ChartTools/Exceptions/DesynchronizedAnchorException.cs similarity index 100% rename from source/Exceptions/DesynchronizedAnchorException.cs rename to ChartTools/Exceptions/DesynchronizedAnchorException.cs diff --git a/source/Exceptions/UndefinedEnumException.cs b/ChartTools/Exceptions/UndefinedEnumException.cs similarity index 100% rename from source/Exceptions/UndefinedEnumException.cs rename to ChartTools/Exceptions/UndefinedEnumException.cs diff --git a/source/Exceptions/Validator.cs b/ChartTools/Exceptions/Validator.cs similarity index 100% rename from source/Exceptions/Validator.cs rename to ChartTools/Exceptions/Validator.cs diff --git a/ChartTools/Extensions/Collections/Alternating/OrderedAlternatingEnumerable.cs b/ChartTools/Extensions/Collections/Alternating/OrderedAlternatingEnumerable.cs new file mode 100644 index 00000000..d661d2a9 --- /dev/null +++ b/ChartTools/Extensions/Collections/Alternating/OrderedAlternatingEnumerable.cs @@ -0,0 +1,150 @@ +using ChartTools.Extensions.Linq; + +using System.Collections; + +namespace ChartTools.Extensions.Collections.Alternating; + +/// +/// Enumerable where items are pulled from a set of enumerables in order using a key +/// +/// Type of the enumerated items +/// Type of the key used to determine the order +public class OrderedAlternatingEnumerable : IEnumerable where TKey : IComparable +{ + /// + /// Enumerables to alternate between + /// + private IEnumerable[] Enumerables { get; } + /// + /// Method that retrieves the key from an item + /// + private Func KeyGetter { get; } + + /// + /// Creates an instance of . + /// + /// Method that retrieves the key from an item + /// Enumerables to alternate between + /// + /// + public OrderedAlternatingEnumerable(Func keyGetter, params IEnumerable?[] enumerables) + { + if (keyGetter is null) + throw new ArgumentNullException(nameof(keyGetter)); + if (enumerables is null) + throw new ArgumentNullException(nameof(enumerables)); + if (enumerables.Length == 0) + throw new ArgumentException("No enumerables provided."); + + KeyGetter = keyGetter; + Enumerables = enumerables.NonNull().ToArray(); + } + + /// + public IEnumerator GetEnumerator() => new Enumerator(KeyGetter, Enumerables.Select(e => e.GetEnumerator()).ToArray()); + /// + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + /// + /// Enumerator that yields items from a set of enumerators in order using a key + /// + private class Enumerator : IInitializable, IEnumerator + { + private IEnumerator[] Enumerators { get; } + /// + /// Method that retrieves the key from an item + /// + private Func KeyGetter { get; } + /// + public bool Initialized { get; private set; } + + /// Currently alternated item following a call + public T Current { get; private set; } + /// + object? IEnumerator.Current => Current; + + /// + /// for indexes where MoveNext previously returned + /// + readonly bool[] endsReached; + + /// + /// Creates a new instance of . + /// + /// Method that retrieves the key from an item + /// Enumerators to alternate between + public Enumerator(Func keyGetter, params IEnumerator[] enumerators) + { + if (keyGetter is null) + Enumerators = enumerators.NonNull().ToArray(); + KeyGetter = keyGetter; + endsReached = new bool[enumerators.Length]; + } + ~Enumerator() => Dispose(false); + + /// + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + public virtual void Dispose(bool disposing) + { + foreach (IEnumerator enumerator in Enumerators) + enumerator.Dispose(); + } + + /// + public bool MoveNext() + { + // Index of the enumerators with items left + LinkedList usableEnumerators = new(); + + Initialize(); + + for (int i = 0; i < Enumerators.Length; i++) + if (!endsReached[i]) + usableEnumerators.AddLast(i); + + if (usableEnumerators.Count == 0) + return false; + + // Get the index of the enumerators whose current item yields the smallest key + int minIndex = usableEnumerators.MinBy(i => KeyGetter(Enumerators[i].Current)); + + // Get the enumerator of this index and set its current item as Current + IEnumerator minEnumerator = Enumerators[minIndex]; + Current = minEnumerator.Current; + + // Mark the enumerator as having reached its end if the next item can't be pulled + if (!minEnumerator.MoveNext()) + endsReached[minIndex] = true; + + return true; + } + + /// + public bool Initialize() + { + if (Initialized) + return false; + + for (int i = 0; i < Enumerators.Length; i++) + endsReached[i] = !Enumerators[i].MoveNext(); + + return Initialized = true; + } + + /// + public void Reset() + { + foreach (IEnumerator enumerator in Enumerators) + enumerator.Reset(); + + for (int i = 0; i < endsReached.Length; i++) + endsReached[i] = false; + + Initialized = false; + } + } +} diff --git a/ChartTools/Extensions/Collections/Alternating/SerialAlternatingEnumerable.cs b/ChartTools/Extensions/Collections/Alternating/SerialAlternatingEnumerable.cs new file mode 100644 index 00000000..d730c482 --- /dev/null +++ b/ChartTools/Extensions/Collections/Alternating/SerialAlternatingEnumerable.cs @@ -0,0 +1,116 @@ +using ChartTools.Extensions.Linq; + +using System.Collections; + +namespace ChartTools.Extensions.Collections.Alternating; + +/// +/// Enumerable where items are yielded by alternating from a set of enumerables +/// +/// Type of the enumerated items +public class SerialAlternatingEnumerable : IEnumerable +{ + /// + protected IEnumerable[] Enumerables { get; } + + /// + /// Creates an instance of + /// + /// Enumerables to pull items from + /// + /// + public SerialAlternatingEnumerable(params IEnumerable?[] enumerables) + { + if (enumerables is null) + throw new ArgumentNullException(nameof(enumerables)); + if (enumerables.Length == 0) + throw new ArgumentException("No enumerables provided."); + + Enumerables = enumerables.NonNull().ToArray(); + } + + /// + public IEnumerator GetEnumerator() => new Enumerator(Enumerables.Select(e => e.GetEnumerator()).ToArray())!; + /// + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + /// + /// Enumerator that yields items by alternating through a set of enumerators + /// + private class Enumerator : IEnumerator + { + /// + /// Enumerators to alternate between + /// + private IEnumerator[] Enumerators { get; } + /// + /// Position of the next enumerator to pull from + /// + private int index; + + /// + /// Item to use in the iteration + /// + public T? Current { get; private set; } + /// + object? IEnumerator.Current => Current; + + /// + /// Creates an instance of + /// + /// Enumerators to alternate between + /// + /// + public Enumerator(params IEnumerator[] enumerators) => Enumerators = enumerators.NonNull().ToArray(); + + /// + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + public virtual void Dispose(bool disposing) + { + foreach (IEnumerator enumerator in Enumerators) + enumerator.Dispose(); + } + ~Enumerator() => Dispose(false); + + /// + public bool MoveNext() + { + int startingIndex = index; + return SearchEnumerator(); + + bool SearchEnumerator() + { + IEnumerator enumerator = Enumerators[index]; + + if (enumerator.MoveNext()) + { + Current = enumerator.Current; + + // Move to the next enumerator + if (++index == Enumerators.Length) + index = 0; + + return true; + } + + // End if looped back around to the first enumerator checked, else check the next enumerator + return index != startingIndex && SearchEnumerator(); + } + } + + /// + public void Reset() + { + // Reset every enumerator + foreach (IEnumerator enumerator in Enumerators) + enumerator.Reset(); + + index = 0; + } + } + +} diff --git a/source/Extensions/Collections/Delayed/DelayedEnumerable.cs b/ChartTools/Extensions/Collections/Delayed/DelayedEnumerable.cs similarity index 100% rename from source/Extensions/Collections/Delayed/DelayedEnumerable.cs rename to ChartTools/Extensions/Collections/Delayed/DelayedEnumerable.cs diff --git a/source/Extensions/Collections/Delayed/DelayedEnumerableSource.cs b/ChartTools/Extensions/Collections/Delayed/DelayedEnumerableSource.cs similarity index 100% rename from source/Extensions/Collections/Delayed/DelayedEnumerableSource.cs rename to ChartTools/Extensions/Collections/Delayed/DelayedEnumerableSource.cs diff --git a/source/Extensions/Collections/Delayed/DelayedEnumerator.cs b/ChartTools/Extensions/Collections/Delayed/DelayedEnumerator.cs similarity index 100% rename from source/Extensions/Collections/Delayed/DelayedEnumerator.cs rename to ChartTools/Extensions/Collections/Delayed/DelayedEnumerator.cs diff --git a/source/Extensions/Collections/EagerEnumerable.cs b/ChartTools/Extensions/Collections/EagerEnumerable.cs similarity index 100% rename from source/Extensions/Collections/EagerEnumerable.cs rename to ChartTools/Extensions/Collections/EagerEnumerable.cs diff --git a/source/Extensions/Collections/IInitializable.cs b/ChartTools/Extensions/Collections/IInitializable.cs similarity index 100% rename from source/Extensions/Collections/IInitializable.cs rename to ChartTools/Extensions/Collections/IInitializable.cs diff --git a/source/Extensions/EnumCache.cs b/ChartTools/Extensions/EnumCache.cs similarity index 100% rename from source/Extensions/EnumCache.cs rename to ChartTools/Extensions/EnumCache.cs diff --git a/source/Extensions/EqualityComparison.cs b/ChartTools/Extensions/EqualityComparison.cs similarity index 100% rename from source/Extensions/EqualityComparison.cs rename to ChartTools/Extensions/EqualityComparison.cs diff --git a/source/Extensions/FuncEqualityComparer.cs b/ChartTools/Extensions/FuncEqualityComparer.cs similarity index 100% rename from source/Extensions/FuncEqualityComparer.cs rename to ChartTools/Extensions/FuncEqualityComparer.cs diff --git a/source/Extensions/IInitializable.cs b/ChartTools/Extensions/IInitializable.cs similarity index 100% rename from source/Extensions/IInitializable.cs rename to ChartTools/Extensions/IInitializable.cs diff --git a/source/Extensions/Linq/CollectionExtensions.cs b/ChartTools/Extensions/Linq/CollectionExtensions.cs similarity index 100% rename from source/Extensions/Linq/CollectionExtensions.cs rename to ChartTools/Extensions/Linq/CollectionExtensions.cs diff --git a/ChartTools/Extensions/Linq/EnumerableExtensions.cs b/ChartTools/Extensions/Linq/EnumerableExtensions.cs new file mode 100644 index 00000000..e9f4b480 --- /dev/null +++ b/ChartTools/Extensions/Linq/EnumerableExtensions.cs @@ -0,0 +1,376 @@ +using ChartTools.Extensions.Collections.Alternating; +using System.Collections; + +namespace ChartTools.Extensions.Linq; + +public static class EnumerableExtensions +{ + /// + /// Checks that all booleans in a collection are . + /// + /// Source of booleans + /// if all booleans are or the collection is empty + public static bool All(this IEnumerable source) + { + foreach (bool b in source) + if (!b) + return false; + + return true; + } + + /// + /// Checks if any boolean in a collection is . + /// + /// Source of booleans + public static bool Any(this IEnumerable source) + { + foreach (bool b in source) + if (b) + return true; + + return false; + } + + #region First + /// + /// if no items meeting the condition were found + public static T? FirstOrDefault(this IEnumerable source, Predicate predicate, T? defaultValue, out bool returnedDefault) + { + if (predicate is null) + throw new ArgumentNullException(nameof(predicate)); + + foreach (T item in source) + if (predicate(item)) + { + returnedDefault = false; + return item; + } + + returnedDefault = true; + return defaultValue; + } + /// + /// Tries to get the first item that meet a condition from en enumerable. + /// + /// Method that returns if a given item meets the condition + /// Found item + /// if an item was found + public static bool TryGetFirst(this IEnumerable source, Predicate predicate, out T item) + { + if (predicate is null) + throw new ArgumentNullException(nameof(predicate)); + + foreach (T t in source) + if (predicate(t)) + { + item = t; + return true; + } + + item = default!; + return false; + } + /// + /// Tries to get the first element of a collection. + /// + /// Source of items + /// Found item + /// if an item was found + public static bool TryGetFirst(this IEnumerable source, out T result) + { + using var enumerator = source.GetEnumerator(); + var success = enumerator.MoveNext(); + + result = success ? enumerator.Current : default!; + return success; + } + /// + /// Tries to get the first item of a given type in a collection. + /// + /// Source of items + /// Found item + /// if an item was found + public static bool TryGetFirstOfType(this IEnumerable source, out TResult result) => source.OfType().TryGetFirst(out result); + #endregion + + /// + /// Excludes items. + /// + public static IEnumerable NonNull(this IEnumerable source) => source.Where(t => t is not null)!; + public static IEnumerable NonNull(this IEnumerable source) where T : struct + { + foreach (var item in source) + if (item.HasValue) + yield return item.Value; + } + + #region Replace + /// + /// Replaces items that meet a condition with another item. + /// + /// The IEnumerable<out T> to replace the items of + /// A function that determines if an item must be replaced + /// The item to replace items with + public static IEnumerable Replace(this IEnumerable source, Predicate predicate, T replacement) + { + if (predicate is null) + throw new ArgumentNullException(nameof(predicate)); + + foreach (T item in source) + yield return predicate(item) ? replacement : item; + } + + /// + /// Replaces a section with other items. + /// + /// Items that match startReplace or endReplace are not included in the returned items. + /// Items to replace a section in + public static IEnumerable ReplaceSection(this IEnumerable source, SectionReplacement replacement) + { + if (replacement.StartReplace is null) + throw new NullReferenceException(nameof(replacement.StartReplace)); + if (replacement.EndReplace is null) + throw new NullReferenceException(nameof(replacement.EndReplace)); + + IEnumerator itemsEnumerator = source.GetEnumerator(); + + // Initialize the enumerator + if (!itemsEnumerator.MoveNext()) + { + // Return the replacement + if (replacement.AddIfMissing) + foreach (T item in replacement.Replacement) + yield return item; + + yield break; + } + + // Return original until startReplace + while (!replacement.StartReplace(itemsEnumerator.Current)) + { + yield return itemsEnumerator.Current; + + if (!itemsEnumerator.MoveNext()) + { + // Return the replacement + if (replacement.AddIfMissing) + foreach (T item in replacement.Replacement) + yield return item; + yield break; + } + } + + // Return replacement + foreach (T item in replacement.Replacement) + yield return item; + + // Find the end of the section to replace + do + if (!itemsEnumerator.MoveNext()) + yield break; + while (replacement.EndReplace(itemsEnumerator.Current)); + + // Return the rest + while (itemsEnumerator.MoveNext()) + yield return itemsEnumerator.Current; + } + + /// + /// Replaces multiple sections of items. + /// + /// Items that match startReplace or endReplace are not included in the returned items. + /// Items to replace sections in + public static IEnumerable ReplaceSections(this IEnumerable source, IEnumerable> replacements) + { + if (replacements is null || !replacements.Any()) + { + foreach (T item in source) + yield return item; + yield break; + } + + List> replacementList = replacements.ToList(); + using IEnumerator itemsEnumerator = source.GetEnumerator(); + + if (!itemsEnumerator.MoveNext()) + { + foreach (var item in AddMissing()) + yield return item; + + yield break; + } + + do + { + // Find a matching replacement start + if (replacementList.TryGetFirst(r => r.StartReplace(itemsEnumerator.Current), out var replacement)) + { + // Move to the end of the section to replace + do + if (!itemsEnumerator.MoveNext()) + { + foreach (var item in AddMissing()) + yield return item; + yield break; + } + while (!replacement.EndReplace(itemsEnumerator.Current)); + + // Return the replacement + foreach (T item in replacement.Replacement) + yield return item; + + replacementList.Remove(replacement); + } + else + { + yield return itemsEnumerator.Current; + + if (!itemsEnumerator.MoveNext()) + { + foreach (var item in AddMissing()) + yield return item; + yield break; + } + } + } + // Continue until all replacements are applied + while (replacementList.Count > 0); + + // Return the rest of the items + while (itemsEnumerator.MoveNext()) + yield return itemsEnumerator.Current; + + IEnumerable AddMissing() + { + // Return remaining replacements + foreach (var replacement in replacementList.Where(r => r.AddIfMissing)) + // Return the replacement + foreach (T item in replacement.Replacement) + yield return item; + } + } + /// + /// Removes a section of items. + /// + /// Items that match startRemove or endRemove + /// Source items to remove a section of + /// Function that determines the start of the section to replace + /// Function that determines the end of the section to replace + public static IEnumerable RemoveSection(this IEnumerable source, Predicate startRemove, Predicate endRemove) + { + IEnumerator itemsEnumerator = source.GetEnumerator(); + + // Initialize the enumerator + if (!itemsEnumerator.MoveNext()) + yield break; + + // Move to the start of items to remove + while (!startRemove(itemsEnumerator.Current)) + if (!itemsEnumerator.MoveNext()) + yield break; + + // Skip items to remove + do + if (!itemsEnumerator.MoveNext()) + yield break; + while (!endRemove(itemsEnumerator.Current)); + + // Return the rest + while (itemsEnumerator.MoveNext()) + yield return itemsEnumerator.Current; + } + #endregion + + /// + /// Loops through a set of objects and returns a set of tuples containing the current object and the previous one. + /// + /// Items to loop through + /// Value of the previous item in the first call of the action + public static IEnumerable<(T? previous, T current)> RelativeLoop(this IEnumerable source, T? firstPrevious = default) + { + var previousItem = firstPrevious; + + foreach (var item in source) + { + yield return (previousItem, item); + previousItem = item; + } + } + public static IEnumerable<(T previous, T current)> RelativeLoopSkipFirst(this IEnumerable source) + { + using var enumerator = source.GetEnumerator(); + + if (enumerator.MoveNext()) + yield break; + + var previous = enumerator.Current; + + while (enumerator.MoveNext()) + yield return (previous, enumerator.Current); + } + + #region Unique + /// + /// Returns distinct elements of a sequence using a method to determine the equality of elements + /// + /// Method that determines if two elements are the same + public static IEnumerable Distinct(this IEnumerable source, EqualityComparison comparison) => source.Distinct(new FuncEqualityComparer(comparison)); + public static bool Unique(this IEnumerable source) => UniqueFromDistinct(source.Distinct()); + public static bool UniqueBy(this IEnumerable source, Func selector) => UniqueFromDistinct(source.DistinctBy(selector)); + private static bool UniqueFromDistinct(IEnumerable distinct) => !distinct.Skip(1).Any(); + #endregion + + #region MinMax + /// + /// Finds the items for which a function returns the smallest or greatest value based on a comparison. + /// + /// Items to find the minimum or maximum of + /// Function that gets the key to use in the comparison from an item + /// Function that returns if the second item defeats the first + private static IEnumerable ManyMinMaxBy(this IEnumerable source, Func selector, Func comparison) where TKey : IComparable + { + TKey minMaxKey; + + using (IEnumerator enumerator = source.GetEnumerator()) + { + if (!enumerator.MoveNext()) + throw new ArgumentException("The enumerable has no items.", nameof(source)); + + minMaxKey = selector(enumerator.Current); + + while (enumerator.MoveNext()) + { + TKey key = selector(enumerator.Current); + + if (comparison(key, minMaxKey)) + minMaxKey = key; + } + } + return source.Where(t => selector(t).CompareTo(minMaxKey) == 0); + } + /// + /// Finds the items for which a function returns the smallest value. + /// + /// Items to find the minimum or maximum of + /// Function that gets the key to use in the comparison from an item + public static IEnumerable ManyMinBy(this IEnumerable source, Func selector) where TKey : IComparable => ManyMinMaxBy(source, selector, (key, mmkey) => key.CompareTo(mmkey) < 0); + /// + /// Finds the items for which a function returns the greatest value. + /// + /// Items to find the minimum or maximum of + /// Function that gets the key to use in the comparison from an item + public static IEnumerable ManyMaxBy(this IEnumerable source, Func selector) where TKey : IComparable => ManyMinMaxBy(source, selector, (key, mmkey) => key.CompareTo(mmkey) > 0); + #endregion + + public static async IAsyncEnumerable ToAsyncEnumerable(this IEnumerable source) + { + foreach (var item in source) + yield return await Task.FromResult(item); + } + + #region Collections + public static IEnumerable Alternate(this IEnumerable> source) => new SerialAlternatingEnumerable(source.ToArray()); + public static IEnumerable AlternateBy(this IEnumerable> source, Func selector) where TKey : IComparable => new OrderedAlternatingEnumerable(selector, source.ToArray()); + #endregion +} diff --git a/source/Extensions/Linq/SectionReplacement.cs b/ChartTools/Extensions/Linq/SectionReplacement.cs similarity index 100% rename from source/Extensions/Linq/SectionReplacement.cs rename to ChartTools/Extensions/Linq/SectionReplacement.cs diff --git a/source/Extensions/StringExtensions.cs b/ChartTools/Extensions/StringExtensions.cs similarity index 100% rename from source/Extensions/StringExtensions.cs rename to ChartTools/Extensions/StringExtensions.cs diff --git a/source/GlobalSuppressions.cs b/ChartTools/GlobalSuppressions.cs similarity index 100% rename from source/GlobalSuppressions.cs rename to ChartTools/GlobalSuppressions.cs diff --git a/source/IEmptyVerifiable.cs b/ChartTools/IEmptyVerifiable.cs similarity index 100% rename from source/IEmptyVerifiable.cs rename to ChartTools/IEmptyVerifiable.cs diff --git a/source/ILongObject.cs b/ChartTools/ILongObject.cs similarity index 100% rename from source/ILongObject.cs rename to ChartTools/ILongObject.cs diff --git a/source/IO/Anchor.cs b/ChartTools/IO/Anchor.cs similarity index 100% rename from source/IO/Anchor.cs rename to ChartTools/IO/Anchor.cs diff --git a/source/IO/Appliables/IInstrumentAppliable.cs b/ChartTools/IO/Appliables/IInstrumentAppliable.cs similarity index 100% rename from source/IO/Appliables/IInstrumentAppliable.cs rename to ChartTools/IO/Appliables/IInstrumentAppliable.cs diff --git a/source/IO/Appliables/ISongAppliable.cs b/ChartTools/IO/Appliables/ISongAppliable.cs similarity index 100% rename from source/IO/Appliables/ISongAppliable.cs rename to ChartTools/IO/Appliables/ISongAppliable.cs diff --git a/source/IO/Chart/ChartFile.cs b/ChartTools/IO/Chart/ChartFile.cs similarity index 100% rename from source/IO/Chart/ChartFile.cs rename to ChartTools/IO/Chart/ChartFile.cs diff --git a/source/IO/Chart/ChartFileReader.cs b/ChartTools/IO/Chart/ChartFileReader.cs similarity index 100% rename from source/IO/Chart/ChartFileReader.cs rename to ChartTools/IO/Chart/ChartFileReader.cs diff --git a/source/IO/Chart/ChartFileWriter.cs b/ChartTools/IO/Chart/ChartFileWriter.cs similarity index 100% rename from source/IO/Chart/ChartFileWriter.cs rename to ChartTools/IO/Chart/ChartFileWriter.cs diff --git a/source/IO/Chart/ChartFormatting.cs b/ChartTools/IO/Chart/ChartFormatting.cs similarity index 100% rename from source/IO/Chart/ChartFormatting.cs rename to ChartTools/IO/Chart/ChartFormatting.cs diff --git a/source/IO/Chart/ChartSectionSet.cs b/ChartTools/IO/Chart/ChartSectionSet.cs similarity index 100% rename from source/IO/Chart/ChartSectionSet.cs rename to ChartTools/IO/Chart/ChartSectionSet.cs diff --git a/source/IO/Chart/Entries/NoteData.cs b/ChartTools/IO/Chart/Entries/NoteData.cs similarity index 100% rename from source/IO/Chart/Entries/NoteData.cs rename to ChartTools/IO/Chart/Entries/NoteData.cs diff --git a/source/IO/Chart/Entries/TrackObjectEntry.cs b/ChartTools/IO/Chart/Entries/TrackObjectEntry.cs similarity index 100% rename from source/IO/Chart/Entries/TrackObjectEntry.cs rename to ChartTools/IO/Chart/Entries/TrackObjectEntry.cs diff --git a/source/IO/Chart/Parsing/ChartParser.cs b/ChartTools/IO/Chart/Parsing/ChartParser.cs similarity index 100% rename from source/IO/Chart/Parsing/ChartParser.cs rename to ChartTools/IO/Chart/Parsing/ChartParser.cs diff --git a/source/IO/Chart/Parsing/DrumsTrackParser.cs b/ChartTools/IO/Chart/Parsing/DrumsTrackParser.cs similarity index 100% rename from source/IO/Chart/Parsing/DrumsTrackParser.cs rename to ChartTools/IO/Chart/Parsing/DrumsTrackParser.cs diff --git a/source/IO/Chart/Parsing/GHLTrackParser.cs b/ChartTools/IO/Chart/Parsing/GHLTrackParser.cs similarity index 100% rename from source/IO/Chart/Parsing/GHLTrackParser.cs rename to ChartTools/IO/Chart/Parsing/GHLTrackParser.cs diff --git a/source/IO/Chart/Parsing/GlobalEventParser.cs b/ChartTools/IO/Chart/Parsing/GlobalEventParser.cs similarity index 100% rename from source/IO/Chart/Parsing/GlobalEventParser.cs rename to ChartTools/IO/Chart/Parsing/GlobalEventParser.cs diff --git a/source/IO/Chart/Parsing/MetadataParser.cs b/ChartTools/IO/Chart/Parsing/MetadataParser.cs similarity index 100% rename from source/IO/Chart/Parsing/MetadataParser.cs rename to ChartTools/IO/Chart/Parsing/MetadataParser.cs diff --git a/source/IO/Chart/Parsing/StandardTrackParser.cs b/ChartTools/IO/Chart/Parsing/StandardTrackParser.cs similarity index 100% rename from source/IO/Chart/Parsing/StandardTrackParser.cs rename to ChartTools/IO/Chart/Parsing/StandardTrackParser.cs diff --git a/source/IO/Chart/Parsing/SyncTrackParser.cs b/ChartTools/IO/Chart/Parsing/SyncTrackParser.cs similarity index 100% rename from source/IO/Chart/Parsing/SyncTrackParser.cs rename to ChartTools/IO/Chart/Parsing/SyncTrackParser.cs diff --git a/source/IO/Chart/Parsing/TrackParser.cs b/ChartTools/IO/Chart/Parsing/TrackParser.cs similarity index 100% rename from source/IO/Chart/Parsing/TrackParser.cs rename to ChartTools/IO/Chart/Parsing/TrackParser.cs diff --git a/source/IO/Chart/Parsing/UnknownSectionParser.cs b/ChartTools/IO/Chart/Parsing/UnknownSectionParser.cs similarity index 100% rename from source/IO/Chart/Parsing/UnknownSectionParser.cs rename to ChartTools/IO/Chart/Parsing/UnknownSectionParser.cs diff --git a/source/IO/Chart/Parsing/VariableInstrumentTrackParser.cs b/ChartTools/IO/Chart/Parsing/VariableInstrumentTrackParser.cs similarity index 100% rename from source/IO/Chart/Parsing/VariableInstrumentTrackParser.cs rename to ChartTools/IO/Chart/Parsing/VariableInstrumentTrackParser.cs diff --git a/source/IO/Chart/Providers/ChordProvider.cs b/ChartTools/IO/Chart/Providers/ChordProvider.cs similarity index 100% rename from source/IO/Chart/Providers/ChordProvider.cs rename to ChartTools/IO/Chart/Providers/ChordProvider.cs diff --git a/source/IO/Chart/Providers/EventProvider.cs b/ChartTools/IO/Chart/Providers/EventProvider.cs similarity index 100% rename from source/IO/Chart/Providers/EventProvider.cs rename to ChartTools/IO/Chart/Providers/EventProvider.cs diff --git a/source/IO/Chart/Providers/SpecialPhraseProvider.cs b/ChartTools/IO/Chart/Providers/SpecialPhraseProvider.cs similarity index 100% rename from source/IO/Chart/Providers/SpecialPhraseProvider.cs rename to ChartTools/IO/Chart/Providers/SpecialPhraseProvider.cs diff --git a/source/IO/Chart/Providers/SyncTrackProvider.cs b/ChartTools/IO/Chart/Providers/SyncTrackProvider.cs similarity index 100% rename from source/IO/Chart/Providers/SyncTrackProvider.cs rename to ChartTools/IO/Chart/Providers/SyncTrackProvider.cs diff --git a/source/IO/Chart/Providers/TempoProvider.cs b/ChartTools/IO/Chart/Providers/TempoProvider.cs similarity index 100% rename from source/IO/Chart/Providers/TempoProvider.cs rename to ChartTools/IO/Chart/Providers/TempoProvider.cs diff --git a/source/IO/Chart/Providers/TimeSignatureProvider.cs b/ChartTools/IO/Chart/Providers/TimeSignatureProvider.cs similarity index 100% rename from source/IO/Chart/Providers/TimeSignatureProvider.cs rename to ChartTools/IO/Chart/Providers/TimeSignatureProvider.cs diff --git a/source/IO/Chart/Serializing/ChartKeySerializableAttribute.cs b/ChartTools/IO/Chart/Serializing/ChartKeySerializableAttribute.cs similarity index 100% rename from source/IO/Chart/Serializing/ChartKeySerializableAttribute.cs rename to ChartTools/IO/Chart/Serializing/ChartKeySerializableAttribute.cs diff --git a/source/IO/Chart/Serializing/GlobalEventSerializer.cs b/ChartTools/IO/Chart/Serializing/GlobalEventSerializer.cs similarity index 100% rename from source/IO/Chart/Serializing/GlobalEventSerializer.cs rename to ChartTools/IO/Chart/Serializing/GlobalEventSerializer.cs diff --git a/source/IO/Chart/Serializing/MetadataSerializer.cs b/ChartTools/IO/Chart/Serializing/MetadataSerializer.cs similarity index 100% rename from source/IO/Chart/Serializing/MetadataSerializer.cs rename to ChartTools/IO/Chart/Serializing/MetadataSerializer.cs diff --git a/source/IO/Chart/Serializing/SyncTrackSerializer.cs b/ChartTools/IO/Chart/Serializing/SyncTrackSerializer.cs similarity index 100% rename from source/IO/Chart/Serializing/SyncTrackSerializer.cs rename to ChartTools/IO/Chart/Serializing/SyncTrackSerializer.cs diff --git a/ChartTools/IO/Chart/Serializing/TrackObjectGroupSerializer.cs b/ChartTools/IO/Chart/Serializing/TrackObjectGroupSerializer.cs new file mode 100644 index 00000000..df834059 --- /dev/null +++ b/ChartTools/IO/Chart/Serializing/TrackObjectGroupSerializer.cs @@ -0,0 +1,12 @@ +using ChartTools.Extensions.Linq; +using ChartTools.IO.Chart.Entries; +using ChartTools.IO.Configuration.Sessions; + +namespace ChartTools.IO.Chart.Serializing; + +internal abstract class TrackObjectGroupSerializer : GroupSerializer +{ + public TrackObjectGroupSerializer(string header, T content, WritingSession session) : base(header, content, session) { } + + protected override IEnumerable CombineProviderResults(IEnumerable[] results) => results.AlternateBy(entry => entry.Position).Select(entry => entry.ToString()); +} \ No newline at end of file diff --git a/ChartTools/IO/Chart/Serializing/TrackSerializer.cs b/ChartTools/IO/Chart/Serializing/TrackSerializer.cs new file mode 100644 index 00000000..d4175157 --- /dev/null +++ b/ChartTools/IO/Chart/Serializing/TrackSerializer.cs @@ -0,0 +1,72 @@ +using ChartTools.Events; +using ChartTools.Extensions.Linq; +using ChartTools.IO.Chart.Entries; +using ChartTools.IO.Chart.Providers; +using ChartTools.IO.Configuration; +using ChartTools.IO.Configuration.Sessions; +using ChartTools.Tools; + +namespace ChartTools.IO.Chart.Serializing; + +internal class TrackSerializer : TrackObjectGroupSerializer +{ + public TrackSerializer(Track content, WritingSession session) : base(ChartFormatting.Header(content.ParentInstrument!.InstrumentIdentity, content.Difficulty), content, session) { } + + public override IEnumerable Serialize() => LaunchProviders().AlternateBy(entry => entry.Position).Select(entry => entry.ToString()); + + protected override IEnumerable[] LaunchProviders() + { + ApplyOverlappingSpecialPhrasePolicy(Content.SpecialPhrases, session.Configuration.OverlappingStarPowerPolicy); + + // Convert solo and soloend events into star power + if (session.Configuration.SoloNoStarPowerPolicy == SoloNoStarPowerPolicy.Convert && Content.SpecialPhrases.Count == 0 && Content.LocalEvents is not null) + { + TrackSpecialPhrase? starPower = null; + + foreach (var e in Content.LocalEvents) + switch (e.EventType) + { + case EventTypeHelper.Local.Solo: + if (starPower is not null) + { + starPower.Length = e.Position - starPower.Position; + Content.SpecialPhrases.Add(starPower); + } + + starPower = new(e.Position, TrackSpecialPhraseType.StarPowerGain); + break; + case EventTypeHelper.Local.SoloEnd when starPower is not null: + + starPower.Length = e.Position - starPower.Position; + Content.SpecialPhrases.Add(starPower); + + starPower = null; + break; + } + + Content.LocalEvents.RemoveWhere(e => e.IsSoloEvent); + } + + return new IEnumerable[] + { + new ChordProvider().ProvideFor(Content.Chords.Cast(), session), + new SpeicalPhraseProvider().ProvideFor(Content.SpecialPhrases, session!), + Content.LocalEvents is null ? Enumerable.Empty() : new EventProvider().ProvideFor(Content.LocalEvents!, session!) + }; + } + + private static void ApplyOverlappingSpecialPhrasePolicy(IEnumerable specialPhrases, OverlappingSpecialPhrasePolicy policy) + { + switch (policy) + { + case OverlappingSpecialPhrasePolicy.Cut: + specialPhrases.CutLengths(); + break; + case OverlappingSpecialPhrasePolicy.ThrowException: + foreach ((var previous, var current) in specialPhrases.RelativeLoopSkipFirst()) + if (Optimizer.LengthNeedsCut(previous, current)) + throw new Exception($"Overlapping star power phrases at position {current!.Position}."); + break; + } + } +} diff --git a/source/IO/Chart/Serializing/UnknownSectionSerializer.cs b/ChartTools/IO/Chart/Serializing/UnknownSectionSerializer.cs similarity index 100% rename from source/IO/Chart/Serializing/UnknownSectionSerializer.cs rename to ChartTools/IO/Chart/Serializing/UnknownSectionSerializer.cs diff --git a/source/IO/Configuration/CommonConfiguration.cs b/ChartTools/IO/Configuration/CommonConfiguration.cs similarity index 100% rename from source/IO/Configuration/CommonConfiguration.cs rename to ChartTools/IO/Configuration/CommonConfiguration.cs diff --git a/source/IO/Configuration/ConfigurationExceptions.cs b/ChartTools/IO/Configuration/ConfigurationExceptions.cs similarity index 100% rename from source/IO/Configuration/ConfigurationExceptions.cs rename to ChartTools/IO/Configuration/ConfigurationExceptions.cs diff --git a/source/IO/Configuration/Enums.cs b/ChartTools/IO/Configuration/Enums.cs similarity index 100% rename from source/IO/Configuration/Enums.cs rename to ChartTools/IO/Configuration/Enums.cs diff --git a/source/IO/Configuration/ReadingConfiguration.cs b/ChartTools/IO/Configuration/ReadingConfiguration.cs similarity index 100% rename from source/IO/Configuration/ReadingConfiguration.cs rename to ChartTools/IO/Configuration/ReadingConfiguration.cs diff --git a/source/IO/Configuration/Sessions/ReadingSession.cs b/ChartTools/IO/Configuration/Sessions/ReadingSession.cs similarity index 100% rename from source/IO/Configuration/Sessions/ReadingSession.cs rename to ChartTools/IO/Configuration/Sessions/ReadingSession.cs diff --git a/source/IO/Configuration/Sessions/Session.cs b/ChartTools/IO/Configuration/Sessions/Session.cs similarity index 100% rename from source/IO/Configuration/Sessions/Session.cs rename to ChartTools/IO/Configuration/Sessions/Session.cs diff --git a/source/IO/Configuration/Sessions/WritingSession.cs b/ChartTools/IO/Configuration/Sessions/WritingSession.cs similarity index 100% rename from source/IO/Configuration/Sessions/WritingSession.cs rename to ChartTools/IO/Configuration/Sessions/WritingSession.cs diff --git a/source/IO/Configuration/WritingConfiguration.cs b/ChartTools/IO/Configuration/WritingConfiguration.cs similarity index 100% rename from source/IO/Configuration/WritingConfiguration.cs rename to ChartTools/IO/Configuration/WritingConfiguration.cs diff --git a/source/IO/DirectoryHandler.cs b/ChartTools/IO/DirectoryHandler.cs similarity index 100% rename from source/IO/DirectoryHandler.cs rename to ChartTools/IO/DirectoryHandler.cs diff --git a/source/IO/Exceptions/EntryException.cs b/ChartTools/IO/Exceptions/EntryException.cs similarity index 100% rename from source/IO/Exceptions/EntryException.cs rename to ChartTools/IO/Exceptions/EntryException.cs diff --git a/source/IO/Exceptions/LineException.cs b/ChartTools/IO/Exceptions/LineException.cs similarity index 100% rename from source/IO/Exceptions/LineException.cs rename to ChartTools/IO/Exceptions/LineException.cs diff --git a/source/IO/Exceptions/ParseException.cs b/ChartTools/IO/Exceptions/ParseException.cs similarity index 100% rename from source/IO/Exceptions/ParseException.cs rename to ChartTools/IO/Exceptions/ParseException.cs diff --git a/source/IO/Exceptions/SectionException.cs b/ChartTools/IO/Exceptions/SectionException.cs similarity index 100% rename from source/IO/Exceptions/SectionException.cs rename to ChartTools/IO/Exceptions/SectionException.cs diff --git a/source/IO/ExtensionHandler.cs b/ChartTools/IO/ExtensionHandler.cs similarity index 100% rename from source/IO/ExtensionHandler.cs rename to ChartTools/IO/ExtensionHandler.cs diff --git a/source/IO/FileReader.cs b/ChartTools/IO/FileReader.cs similarity index 100% rename from source/IO/FileReader.cs rename to ChartTools/IO/FileReader.cs diff --git a/source/IO/Formatting/FormattingEnums.cs b/ChartTools/IO/Formatting/FormattingEnums.cs similarity index 100% rename from source/IO/Formatting/FormattingEnums.cs rename to ChartTools/IO/Formatting/FormattingEnums.cs diff --git a/source/IO/Formatting/FormattingRules.cs b/ChartTools/IO/Formatting/FormattingRules.cs similarity index 100% rename from source/IO/Formatting/FormattingRules.cs rename to ChartTools/IO/Formatting/FormattingRules.cs diff --git a/source/IO/ISerializerDataProvider.cs b/ChartTools/IO/ISerializerDataProvider.cs similarity index 100% rename from source/IO/ISerializerDataProvider.cs rename to ChartTools/IO/ISerializerDataProvider.cs diff --git a/source/IO/Ini/IniFile.cs b/ChartTools/IO/Ini/IniFile.cs similarity index 100% rename from source/IO/Ini/IniFile.cs rename to ChartTools/IO/Ini/IniFile.cs diff --git a/source/IO/Ini/IniFileReader.cs b/ChartTools/IO/Ini/IniFileReader.cs similarity index 100% rename from source/IO/Ini/IniFileReader.cs rename to ChartTools/IO/Ini/IniFileReader.cs diff --git a/source/IO/Ini/IniFileWriter.cs b/ChartTools/IO/Ini/IniFileWriter.cs similarity index 100% rename from source/IO/Ini/IniFileWriter.cs rename to ChartTools/IO/Ini/IniFileWriter.cs diff --git a/source/IO/Ini/IniFormatting.cs b/ChartTools/IO/Ini/IniFormatting.cs similarity index 100% rename from source/IO/Ini/IniFormatting.cs rename to ChartTools/IO/Ini/IniFormatting.cs diff --git a/source/IO/Ini/IniKeySerializableAttribute.cs b/ChartTools/IO/Ini/IniKeySerializableAttribute.cs similarity index 100% rename from source/IO/Ini/IniKeySerializableAttribute.cs rename to ChartTools/IO/Ini/IniKeySerializableAttribute.cs diff --git a/source/IO/Ini/IniParser.cs b/ChartTools/IO/Ini/IniParser.cs similarity index 100% rename from source/IO/Ini/IniParser.cs rename to ChartTools/IO/Ini/IniParser.cs diff --git a/source/IO/Ini/IniSerializer.cs b/ChartTools/IO/Ini/IniSerializer.cs similarity index 100% rename from source/IO/Ini/IniSerializer.cs rename to ChartTools/IO/Ini/IniSerializer.cs diff --git a/source/IO/Parsers/FileParser.cs b/ChartTools/IO/Parsers/FileParser.cs similarity index 100% rename from source/IO/Parsers/FileParser.cs rename to ChartTools/IO/Parsers/FileParser.cs diff --git a/source/IO/Parsers/SectionParser.cs b/ChartTools/IO/Parsers/SectionParser.cs similarity index 100% rename from source/IO/Parsers/SectionParser.cs rename to ChartTools/IO/Parsers/SectionParser.cs diff --git a/source/IO/Parsers/TextParser.cs b/ChartTools/IO/Parsers/TextParser.cs similarity index 100% rename from source/IO/Parsers/TextParser.cs rename to ChartTools/IO/Parsers/TextParser.cs diff --git a/source/IO/Sections/ReservedSectionHeader.cs b/ChartTools/IO/Sections/ReservedSectionHeader.cs similarity index 100% rename from source/IO/Sections/ReservedSectionHeader.cs rename to ChartTools/IO/Sections/ReservedSectionHeader.cs diff --git a/source/IO/Sections/ReservedSectionHeaderSet.cs b/ChartTools/IO/Sections/ReservedSectionHeaderSet.cs similarity index 100% rename from source/IO/Sections/ReservedSectionHeaderSet.cs rename to ChartTools/IO/Sections/ReservedSectionHeaderSet.cs diff --git a/source/IO/Sections/Section.cs b/ChartTools/IO/Sections/Section.cs similarity index 100% rename from source/IO/Sections/Section.cs rename to ChartTools/IO/Sections/Section.cs diff --git a/source/IO/Sections/SectionSet.cs b/ChartTools/IO/Sections/SectionSet.cs similarity index 100% rename from source/IO/Sections/SectionSet.cs rename to ChartTools/IO/Sections/SectionSet.cs diff --git a/source/IO/Serializers/GroupSerializer.cs b/ChartTools/IO/Serializers/GroupSerializer.cs similarity index 100% rename from source/IO/Serializers/GroupSerializer.cs rename to ChartTools/IO/Serializers/GroupSerializer.cs diff --git a/source/IO/Serializers/KeySerializable.cs b/ChartTools/IO/Serializers/KeySerializable.cs similarity index 100% rename from source/IO/Serializers/KeySerializable.cs rename to ChartTools/IO/Serializers/KeySerializable.cs diff --git a/source/IO/Serializers/Serializer.cs b/ChartTools/IO/Serializers/Serializer.cs similarity index 100% rename from source/IO/Serializers/Serializer.cs rename to ChartTools/IO/Serializers/Serializer.cs diff --git a/source/IO/TextEntry.cs b/ChartTools/IO/TextEntry.cs similarity index 100% rename from source/IO/TextEntry.cs rename to ChartTools/IO/TextEntry.cs diff --git a/source/IO/TextFileReader.cs b/ChartTools/IO/TextFileReader.cs similarity index 100% rename from source/IO/TextFileReader.cs rename to ChartTools/IO/TextFileReader.cs diff --git a/source/IO/TextFileWriter.cs b/ChartTools/IO/TextFileWriter.cs similarity index 100% rename from source/IO/TextFileWriter.cs rename to ChartTools/IO/TextFileWriter.cs diff --git a/source/IO/ValueParser.cs b/ChartTools/IO/ValueParser.cs similarity index 100% rename from source/IO/ValueParser.cs rename to ChartTools/IO/ValueParser.cs diff --git a/source/IReadOnlyLongObject.cs b/ChartTools/IReadOnlyLongObject.cs similarity index 100% rename from source/IReadOnlyLongObject.cs rename to ChartTools/IReadOnlyLongObject.cs diff --git a/source/Instruments/Drums.cs b/ChartTools/Instruments/Drums.cs similarity index 100% rename from source/Instruments/Drums.cs rename to ChartTools/Instruments/Drums.cs diff --git a/source/Instruments/GHLInstrument.cs b/ChartTools/Instruments/GHLInstrument.cs similarity index 100% rename from source/Instruments/GHLInstrument.cs rename to ChartTools/Instruments/GHLInstrument.cs diff --git a/source/Instruments/Instrument.cs b/ChartTools/Instruments/Instrument.cs similarity index 100% rename from source/Instruments/Instrument.cs rename to ChartTools/Instruments/Instrument.cs diff --git a/source/Instruments/InstrumentGeneric.cs b/ChartTools/Instruments/InstrumentGeneric.cs similarity index 100% rename from source/Instruments/InstrumentGeneric.cs rename to ChartTools/Instruments/InstrumentGeneric.cs diff --git a/source/Instruments/InstrumentSet.cs b/ChartTools/Instruments/InstrumentSet.cs similarity index 100% rename from source/Instruments/InstrumentSet.cs rename to ChartTools/Instruments/InstrumentSet.cs diff --git a/source/Instruments/StandardInstrument.cs b/ChartTools/Instruments/StandardInstrument.cs similarity index 100% rename from source/Instruments/StandardInstrument.cs rename to ChartTools/Instruments/StandardInstrument.cs diff --git a/source/Instruments/Vocals.cs b/ChartTools/Instruments/Vocals.cs similarity index 100% rename from source/Instruments/Vocals.cs rename to ChartTools/Instruments/Vocals.cs diff --git a/source/Metadata/Charter.cs b/ChartTools/Metadata/Charter.cs similarity index 100% rename from source/Metadata/Charter.cs rename to ChartTools/Metadata/Charter.cs diff --git a/source/Metadata/InstrumentDifficultySet.cs b/ChartTools/Metadata/InstrumentDifficultySet.cs similarity index 100% rename from source/Metadata/InstrumentDifficultySet.cs rename to ChartTools/Metadata/InstrumentDifficultySet.cs diff --git a/source/Metadata/Metadata.cs b/ChartTools/Metadata/Metadata.cs similarity index 100% rename from source/Metadata/Metadata.cs rename to ChartTools/Metadata/Metadata.cs diff --git a/source/Metadata/StreamCollection.cs b/ChartTools/Metadata/StreamCollection.cs similarity index 100% rename from source/Metadata/StreamCollection.cs rename to ChartTools/Metadata/StreamCollection.cs diff --git a/source/Metadata/UnidentifiedMetadata.cs b/ChartTools/Metadata/UnidentifiedMetadata.cs similarity index 100% rename from source/Metadata/UnidentifiedMetadata.cs rename to ChartTools/Metadata/UnidentifiedMetadata.cs diff --git a/source/Notes/DrumsNote.cs b/ChartTools/Notes/DrumsNote.cs similarity index 100% rename from source/Notes/DrumsNote.cs rename to ChartTools/Notes/DrumsNote.cs diff --git a/source/Notes/INote.cs b/ChartTools/Notes/INote.cs similarity index 100% rename from source/Notes/INote.cs rename to ChartTools/Notes/INote.cs diff --git a/source/Notes/LaneNote.cs b/ChartTools/Notes/LaneNote.cs similarity index 100% rename from source/Notes/LaneNote.cs rename to ChartTools/Notes/LaneNote.cs diff --git a/source/Notes/LaneNoteCollection.cs b/ChartTools/Notes/LaneNoteCollection.cs similarity index 100% rename from source/Notes/LaneNoteCollection.cs rename to ChartTools/Notes/LaneNoteCollection.cs diff --git a/source/Notes/LaneNoteGeneric.cs b/ChartTools/Notes/LaneNoteGeneric.cs similarity index 100% rename from source/Notes/LaneNoteGeneric.cs rename to ChartTools/Notes/LaneNoteGeneric.cs diff --git a/source/Notes/Syllable.cs b/ChartTools/Notes/Syllable.cs similarity index 100% rename from source/Notes/Syllable.cs rename to ChartTools/Notes/Syllable.cs diff --git a/source/Notes/VocalsPitch.cs b/ChartTools/Notes/VocalsPitch.cs similarity index 100% rename from source/Notes/VocalsPitch.cs rename to ChartTools/Notes/VocalsPitch.cs diff --git a/source/Song.cs b/ChartTools/Song.cs similarity index 100% rename from source/Song.cs rename to ChartTools/Song.cs diff --git a/source/Special/InstrumentSpecialPhrase.cs b/ChartTools/Special/InstrumentSpecialPhrase.cs similarity index 100% rename from source/Special/InstrumentSpecialPhrase.cs rename to ChartTools/Special/InstrumentSpecialPhrase.cs diff --git a/source/Special/SpecialPhrase.cs b/ChartTools/Special/SpecialPhrase.cs similarity index 100% rename from source/Special/SpecialPhrase.cs rename to ChartTools/Special/SpecialPhrase.cs diff --git a/source/Special/TrackSpecialPhrase.cs b/ChartTools/Special/TrackSpecialPhrase.cs similarity index 100% rename from source/Special/TrackSpecialPhrase.cs rename to ChartTools/Special/TrackSpecialPhrase.cs diff --git a/source/Sync/SyncTrack.cs b/ChartTools/Sync/SyncTrack.cs similarity index 100% rename from source/Sync/SyncTrack.cs rename to ChartTools/Sync/SyncTrack.cs diff --git a/source/Sync/Tempo.cs b/ChartTools/Sync/Tempo.cs similarity index 100% rename from source/Sync/Tempo.cs rename to ChartTools/Sync/Tempo.cs diff --git a/source/Sync/TempoMap.cs b/ChartTools/Sync/TempoMap.cs similarity index 100% rename from source/Sync/TempoMap.cs rename to ChartTools/Sync/TempoMap.cs diff --git a/source/Sync/TimeSignature.cs b/ChartTools/Sync/TimeSignature.cs similarity index 100% rename from source/Sync/TimeSignature.cs rename to ChartTools/Sync/TimeSignature.cs diff --git a/source/Tools/LengthMerger.cs b/ChartTools/Tools/LengthMerger.cs similarity index 100% rename from source/Tools/LengthMerger.cs rename to ChartTools/Tools/LengthMerger.cs diff --git a/source/Tools/Optimizer.cs b/ChartTools/Tools/Optimizer.cs similarity index 100% rename from source/Tools/Optimizer.cs rename to ChartTools/Tools/Optimizer.cs diff --git a/source/Tools/Printer.cs b/ChartTools/Tools/Printer.cs similarity index 100% rename from source/Tools/Printer.cs rename to ChartTools/Tools/Printer.cs diff --git a/source/Tools/PropertyMerger.cs b/ChartTools/Tools/PropertyMerger.cs similarity index 100% rename from source/Tools/PropertyMerger.cs rename to ChartTools/Tools/PropertyMerger.cs diff --git a/source/Tools/TempoRescaler.cs b/ChartTools/Tools/TempoRescaler.cs similarity index 100% rename from source/Tools/TempoRescaler.cs rename to ChartTools/Tools/TempoRescaler.cs diff --git a/source/TrackObjects/ILongTrackObject.cs b/ChartTools/TrackObjects/ILongTrackObject.cs similarity index 100% rename from source/TrackObjects/ILongTrackObject.cs rename to ChartTools/TrackObjects/ILongTrackObject.cs diff --git a/source/TrackObjects/IReadOnlyTrackObject.cs b/ChartTools/TrackObjects/IReadOnlyTrackObject.cs similarity index 100% rename from source/TrackObjects/IReadOnlyTrackObject.cs rename to ChartTools/TrackObjects/IReadOnlyTrackObject.cs diff --git a/source/TrackObjects/ITrackObject.cs b/ChartTools/TrackObjects/ITrackObject.cs similarity index 100% rename from source/TrackObjects/ITrackObject.cs rename to ChartTools/TrackObjects/ITrackObject.cs diff --git a/source/TrackObjects/TrackObjectBase.cs b/ChartTools/TrackObjects/TrackObjectBase.cs similarity index 100% rename from source/TrackObjects/TrackObjectBase.cs rename to ChartTools/TrackObjects/TrackObjectBase.cs diff --git a/source/Tracks/Track.cs b/ChartTools/Tracks/Track.cs similarity index 100% rename from source/Tracks/Track.cs rename to ChartTools/Tracks/Track.cs diff --git a/source/Tracks/TrackGeneric.cs b/ChartTools/Tracks/TrackGeneric.cs similarity index 100% rename from source/Tracks/TrackGeneric.cs rename to ChartTools/Tracks/TrackGeneric.cs diff --git a/source/Tracks/UniqueTrackObjectCollection.cs b/ChartTools/Tracks/UniqueTrackObjectCollection.cs similarity index 100% rename from source/Tracks/UniqueTrackObjectCollection.cs rename to ChartTools/Tracks/UniqueTrackObjectCollection.cs diff --git a/source/source.projitems b/ChartTools/source.projitems similarity index 100% rename from source/source.projitems rename to ChartTools/source.projitems diff --git a/Docs/Docs.csproj b/Docs/Docs.csproj index 8a8b783f..e2e58e9e 100644 --- a/Docs/Docs.csproj +++ b/Docs/Docs.csproj @@ -1,7 +1,7 @@  - net7.0 + net8.0 enable enable Exe @@ -15,6 +15,10 @@ + + + + all diff --git a/Docs/docfx.json b/Docs/docfx.json index 1f4940b4..b85babb9 100644 --- a/Docs/docfx.json +++ b/Docs/docfx.json @@ -6,7 +6,7 @@ "files": [ "**.csproj" ], - "src": "../Net6" + "src": "../source" } ], "dest": "api", diff --git a/Net6/Net6.csproj b/Net6/Net6.csproj deleted file mode 100644 index 16962b28..00000000 --- a/Net6/Net6.csproj +++ /dev/null @@ -1,33 +0,0 @@ - - - - net6.0 - enable - OnBuildSuccess - ChartTools - ChartTools - embedded - False - True - $(OutDir)ChartTools.xml - $(ProjectDir)..\build\net5 - Debug;Release;Docs - enable - - - - - - - - - - - - - - - - - - diff --git a/Net7/Net7.csproj b/Net7/Net7.csproj deleted file mode 100644 index a1a363e4..00000000 --- a/Net7/Net7.csproj +++ /dev/null @@ -1,34 +0,0 @@ - - - - net7.0 - preview - enable - enable - OnOutputUpdated - ChartTools - ChartTools - embedded - False - True - $(OutDir)ChartTools.xml - $(ProjectDir)..\build\net6 - Debug;Release;Docs - - - - - - - - - - - - - - - - - - diff --git a/source/ChartTools.shproj b/source - Copie/ChartTools.shproj similarity index 100% rename from source/ChartTools.shproj rename to source - Copie/ChartTools.shproj diff --git a/source - Copie/Chords/DrumsChord.cs b/source - Copie/Chords/DrumsChord.cs new file mode 100644 index 00000000..0e2464ac --- /dev/null +++ b/source - Copie/Chords/DrumsChord.cs @@ -0,0 +1,59 @@ +using ChartTools.IO.Chart; +using ChartTools.IO.Chart.Entries; +using ChartTools.IO.Configuration.Sessions; + +namespace ChartTools; + +/// +/// Set of notes played simultaneously by drums +/// +public class DrumsChord : LaneChord +{ + public override bool OpenExclusivity => false; + + internal override DrumsChordModifiers DefaultModifiers => DrumsChordModifiers.None; + internal override bool ChartSupportedModifiers => true; + + public DrumsChord() : base() { } + + /// + public DrumsChord(uint position) : base(position) { } + /// + /// Notes to add + public DrumsChord(uint position, params DrumsNote[] notes) : base(position) + { + if (notes is null) + throw new ArgumentNullException(nameof(notes)); + + foreach (DrumsNote note in notes) + Notes.Add(note); + } + /// + public DrumsChord(uint position, params DrumsLane[] notes) : base(position) + { + if (notes is null) + throw new ArgumentNullException(nameof(notes)); + + foreach (DrumsLane note in notes) + Notes.Add(new DrumsNote(note)); + } + + protected override IReadOnlyCollection GetNotes() => Notes; + + internal override IEnumerable GetChartNoteData() + { + foreach (DrumsNote note in Notes) + { + yield return ChartFormatting.NoteEntry(Position, note.Lane == DrumsLane.DoubleKick ? (byte)32 : note.Index, note.Sustain); + + if (note.IsCymbal) + yield return ChartFormatting.NoteEntry(Position, (byte)(note.Lane + 64), 0); + } + } + + internal override IEnumerable GetChartModifierData(LaneChord? previous, WritingSession session) + { + if (Modifiers.HasFlag(DrumsChordModifiers.Flam)) + yield return ChartFormatting.NoteEntry(Position, 109, 0); + } +} diff --git a/source - Copie/Chords/GHLChord.cs b/source - Copie/Chords/GHLChord.cs new file mode 100644 index 00000000..b714e630 --- /dev/null +++ b/source - Copie/Chords/GHLChord.cs @@ -0,0 +1,62 @@ +using ChartTools.IO.Chart; +using ChartTools.IO.Chart.Entries; +using ChartTools.IO.Configuration.Sessions; + +namespace ChartTools; + +/// +/// Set of notes played simultaneously by a Guitar Hero Live instrument +/// +public class GHLChord : LaneChord, GHLLane, GHLChordModifiers> +{ + public override bool OpenExclusivity => true; + + internal override GHLChordModifiers DefaultModifiers => GHLChordModifiers.None; + internal override bool ChartSupportedModifiers => !Modifiers.HasFlag(GHLChordModifiers.ExplicitHopo); + + public GHLChord() : base() { } + /// + public GHLChord(uint position) : base(position) { } + /// + /// Notes to add + public GHLChord(uint position, params LaneNote[] notes) : base(position) + { + if (notes is null) + throw new ArgumentNullException(nameof(notes)); + + foreach (var note in notes) + Notes.Add(note); + } + /// + public GHLChord(uint position, params GHLLane[] notes) : base(position) + { + if (notes is null) + throw new ArgumentNullException(nameof(notes)); + + foreach (GHLLane note in notes) + Notes.Add(new LaneNote(note)); + } + + protected override IReadOnlyCollection GetNotes() => Notes; + + internal override IEnumerable GetChartNoteData() => Notes.Select(note => ChartFormatting.NoteEntry(Position, note.Lane switch + { + GHLLane.Open => 7, + GHLLane.Black1 => 3, + GHLLane.Black2 => 4, + GHLLane.Black3 => 8, + GHLLane.White1 => 0, + GHLLane.White2 => 1, + GHLLane.White3 => 2, + }, note.Sustain)); + + internal override IEnumerable GetChartModifierData(LaneChord? previous, WritingSession session) + { + var isInvert = Modifiers.HasFlag(GHLChordModifiers.HopoInvert); + + if (Modifiers.HasFlag(GHLChordModifiers.ExplicitHopo) && (previous is null || previous.Position <= session.Formatting!.TrueHopoFrequency) != isInvert || isInvert) + yield return ChartFormatting.NoteEntry(Position, 5, 0); + if (Modifiers.HasFlag(GHLChordModifiers.Tap)) + yield return ChartFormatting.NoteEntry(Position, 6, 0); + } +} diff --git a/source - Copie/Chords/IChord.cs b/source - Copie/Chords/IChord.cs new file mode 100644 index 00000000..1fccc55f --- /dev/null +++ b/source - Copie/Chords/IChord.cs @@ -0,0 +1,15 @@ +namespace ChartTools; + +/// +/// Set of notes tied together. +/// +/// Depending on the chord type, notes will be on the same position or in sequence. +public interface IChord : ITrackObject +{ + /// + /// Read-only set of the notes in the chord. + /// + public IReadOnlyCollection Notes { get; } + + public INote CreateNote(byte index, uint length = 0); +} diff --git a/source - Copie/Chords/LaneChord.cs b/source - Copie/Chords/LaneChord.cs new file mode 100644 index 00000000..4f7492a6 --- /dev/null +++ b/source - Copie/Chords/LaneChord.cs @@ -0,0 +1,28 @@ +using ChartTools.IO.Chart.Entries; +using ChartTools.IO.Configuration.Sessions; + +namespace ChartTools; + +public abstract class LaneChord : TrackObjectBase, IChord +{ + public IReadOnlyCollection Notes => GetNotes(); + IReadOnlyCollection IChord.Notes => GetNotes(); + + /// + /// Defines if open notes can be mixed with other notes for this chord type. indicated open notes cannot be mixed. + /// + public abstract bool OpenExclusivity { get; } + + internal abstract bool ChartSupportedModifiers { get; } + + public LaneChord() : base() { } + public LaneChord(uint position) : base(position) { } + + public abstract LaneNote CreateNote(byte index, uint sustain = 0); + INote IChord.CreateNote(byte index, uint sustain) => CreateNote(index, sustain); + + protected abstract IReadOnlyCollection GetNotes(); + + internal abstract IEnumerable GetChartNoteData(); + internal abstract IEnumerable GetChartModifierData(LaneChord? previous, WritingSession session); +} diff --git a/source - Copie/Chords/LaneChordGeneric.cs b/source - Copie/Chords/LaneChordGeneric.cs new file mode 100644 index 00000000..43bf7d44 --- /dev/null +++ b/source - Copie/Chords/LaneChordGeneric.cs @@ -0,0 +1,32 @@ +using System.Runtime.CompilerServices; + +namespace ChartTools; + +public abstract class LaneChord : LaneChord, IChord + where TNote : LaneNote, new() + where TLane : struct, Enum + where TModifiers : struct, Enum +{ + /// + /// Notes in the chord + /// + public new LaneNoteCollection Notes { get; } + + public TModifiers Modifiers { get; set; } + internal abstract TModifiers DefaultModifiers { get; } + + public LaneChord() : base() => Notes = new(OpenExclusivity); + public LaneChord(uint position) : base(position) => Notes = new(OpenExclusivity); + + public override LaneNote CreateNote(byte index, uint sustain = 0) + { + var note = new TNote() + { + Lane = Unsafe.As(ref index), + Sustain = sustain + }; + + Notes.Add(note); + return note; + } +} diff --git a/source - Copie/Chords/Phrase.cs b/source - Copie/Chords/Phrase.cs new file mode 100644 index 00000000..92050139 --- /dev/null +++ b/source - Copie/Chords/Phrase.cs @@ -0,0 +1,89 @@ +using ChartTools.Events; + +namespace ChartTools.Lyrics; + +public class Phrase : TrackObjectBase, IChord, ILongTrackObject +{ + public List Syllables { get; } = new(); + IReadOnlyCollection IChord.Notes => Syllables; + + /// + /// End of the phrase as defined by + /// + public uint EndPosition => Position + Length; + + public uint Length => LengthOverride ?? SyllableEndOffset; + uint ILongObject.Length + { + get => Length; + set => LengthOverride = value; + } + + public uint? LengthOverride + { + get => _lengthOverride; + set + { + if (value is not null && value < SyllableEndOffset) + throw new ArgumentException("Length must be large enough to fit all syllables.", nameof(value)); + + _lengthOverride = value; + } + } + private uint? _lengthOverride; + + /// + /// Offset of the first syllable + /// + public uint SyllableStartOffset => Syllables.Count == 0 ? 0 : Syllables.Select(s => s.PositionOffset).Min(); + /// + /// Offset of the end of the last syllable + /// + public uint SyllableEndOffset => Syllables.Count == 0 ? 0 : Syllables.Select(s => s.EndPositionOffset).Max(); + /// + /// Start position of the first syllable + /// + public uint SyllableStartPosition => SyllableStartOffset + Position; + /// + /// End position of the last syllable + /// + public uint SyllableEndPosition => SyllableEndOffset + Position; + + /// + /// Gets the raw text of all syllables as a single string with spaces between syllables + /// + public string RawText => BuildText(n => n.RawText); + public string DisplayedText => BuildText(n => n.DisplayedText); + + public Phrase(uint position) : base(position) { } + + public IEnumerable ToGlobalEvents() + { + yield return new(Position, EventTypeHelper.Global.PhraseStart); + + foreach (var syllable in Syllables) + yield return new(Position + syllable.PositionOffset, EventTypeHelper.Global.Lyric, syllable.RawText); + } + + private string BuildText(Func textSelector) => string.Concat(Syllables.Select(n => n.IsWordEnd ? textSelector(n) + ' ' : textSelector(n))); + + INote IChord.CreateNote(byte index, uint length) + { + var syllable = new Syllable(0, (VocalPitchValue)index) { Length = length }; + Syllables.Add(syllable); + return syllable; + } +} + +/// +/// Provides additional methods to +/// +public static class PhraseExtensions +{ + /// + /// Converts a set of to a set of making up the phrases. + /// + /// Phrases to convert into global events + /// Global events making up the phrases + public static IEnumerable ToGlobalEvents(this IEnumerable source) => source.SelectMany(p => p.ToGlobalEvents()); +} diff --git a/source - Copie/Chords/StandardChord.cs b/source - Copie/Chords/StandardChord.cs new file mode 100644 index 00000000..065d1ebd --- /dev/null +++ b/source - Copie/Chords/StandardChord.cs @@ -0,0 +1,53 @@ +using ChartTools.IO.Chart; +using ChartTools.IO.Chart.Entries; +using ChartTools.IO.Configuration.Sessions; + +namespace ChartTools; + +/// +/// Set of notes played simultaneously by a standard five-fret instrument +/// +public class StandardChord : LaneChord, StandardLane, StandardChordModifiers> +{ + public override bool OpenExclusivity => true; + + internal override StandardChordModifiers DefaultModifiers => StandardChordModifiers.None; + internal override bool ChartSupportedModifiers => !Modifiers.HasFlag(StandardChordModifiers.ExplicitHopo); + + public StandardChord() : base() { } + /// + public StandardChord(uint position) : base(position) { } + /// + /// Notes to add + public StandardChord(uint position, params LaneNote[] notes) : this(position) + { + if (notes is null) + throw new ArgumentNullException(nameof(notes)); + + foreach (var note in notes) + Notes.Add(note); + } + /// + public StandardChord(uint position, params StandardLane[] notes) : this(position) + { + if (notes is null) + throw new ArgumentNullException(nameof(notes)); + + foreach (StandardLane note in notes) + Notes.Add(new LaneNote(note)); + } + + protected override IReadOnlyCollection GetNotes() => Notes; + + internal override IEnumerable GetChartNoteData() => Notes.Select(note => ChartFormatting.NoteEntry(Position, note.Lane == StandardLane.Open ? (byte)7 : (byte)(note.Lane - 1), note.Sustain)); + + internal override IEnumerable GetChartModifierData(LaneChord? previous, WritingSession session) + { + bool isInvert = Modifiers.HasFlag(StandardChordModifiers.HopoInvert); + + if (Modifiers.HasFlag(StandardChordModifiers.ExplicitHopo) && (previous is null || previous.Position <= session.Formatting!.TrueHopoFrequency) != isInvert || isInvert) + yield return ChartFormatting.NoteEntry(Position, 5, 0); + if (Modifiers.HasFlag(StandardChordModifiers.Tap)) + yield return ChartFormatting.NoteEntry(Position, 6, 0); + } +} diff --git a/source - Copie/Enums.cs b/source - Copie/Enums.cs new file mode 100644 index 00000000..bcfb062e --- /dev/null +++ b/source - Copie/Enums.cs @@ -0,0 +1,457 @@ +namespace ChartTools +{ + /// + /// Difficulty levels + /// + public enum Difficulty : byte + { + /// + /// Easy difficulty + /// + Easy, + /// + /// Medium difficulty + /// + Medium, + /// + /// Hard difficulty + /// + Hard, + /// + /// Expert difficulty + /// + Expert + } + /// + /// Modifier that affects the way the chord can be played + /// + [Flags] + public enum DrumsChordModifiers : byte + { + /// + None, + /// + /// *Unsupported* + /// + Accent, + /// + /// *Unsupported* + /// + Ghost, + Flam = 4 + } + /// + /// Drums pads and pedals for a + /// + public enum DrumsLane : byte + { + /// + /// Kick note, shown as a purple line + /// + Kick, + /// + /// Red pad + /// + Red, + /// + /// Yellow pad + /// + Yellow, + /// + /// Blue pad + /// + Blue, + /// + /// Green when playing with four pads, orange when playing with five pads + /// + Green4Lane_Orange5Lane, + /// + /// Green when playing with five pad, otherwise converted to + /// + Green5Lane, + /// + /// that only appears when playing with multiple pedals + /// + /// In Clone Hero, double kicks are enabled with the "2x Kick" modifier and are not limited to a single difficulty. + DoubleKick + } + public enum FileType : byte { Chart, Ini, MIDI } + /// + /// Modifier that affects how a can be played + /// + [Flags] + public enum GHLChordModifiers : byte + { + /// + None = 0, + /// + ExplicitHopo = 1, + /// + HopoInvert = 2, + /// + Tap = 4 + } + /// + /// Guitar Hero Live instruments + /// + /// Casting to will match the instrument. + public enum GHLInstrumentIdentity : byte { Guitar = 1, Bass } + /// + /// Frets for a GHL note + /// + public enum GHLLane : byte { Open, Black1, Black2, Black3, White1, White2, White3 } + + /// + /// Origins of an instrument + /// + public enum MidiInstrumentOrigin : byte + { + NA, + Unknown, + GuitarHero1, + GuitarHero2 = 4, + GuitarHero2Uncertain = Unknown | GuitarHero2, + RockBand = 6, + RockBandUncertain = Unknown | RockBand, + } + + /// + /// All instruments + /// + public enum InstrumentIdentity : byte { Drums, GHLGuitar, GHLBass, LeadGuitar, RhythmGuitar, CoopGuitar, Bass, Keys, Vocals } + public enum InstrumentType : byte { Drums, GHL, Standard, Vocals } + + /// + /// Modifier that affects how a can be played + /// + /// + [Flags] + public enum StandardChordModifiers : byte + { + /// + /// No modifier + /// + None = 0, + /// + /// The Hopo state is not relative to the previous chord. + /// + ExplicitHopo = 1, + /// + /// Forced Hopo if is set, otherwise inverts the natural state relative to the previous chord + /// + HopoInvert = 2, + ForcedHopo = ExplicitHopo | HopoInvert, + ForcedStrum = ExplicitHopo, + /// + /// The chord can be played without strumming + /// + Tap = 4, + Big = 8 + } + /// + /// Standard five-fret instruments + /// + /// + public enum StandardInstrumentIdentity : byte { LeadGuitar = 3, RhythmGuitar, CoopGuitar, Bass, Keys } + /// + /// Frets for a standard note + /// + public enum StandardLane : byte { Open, Green, Red, Yellow, Blue, Orange } + /// + /// Types of + /// + public enum TrackSpecialPhraseType : byte + { + /// + /// The is not a recognized phrase type + /// + Unknown, + /// + /// Grants star power if all notes are hit + /// + StarPowerGain, + /// + /// Allows the activation of star power + /// + StarPowerActivation, + Player1FaceOff, + Player2FaceOff, + Trill, + Tremolo, + DrumsRoll = 65, + DrumsDoubleRoll = 66 + } + /// + /// Types of + /// + public enum InstrumentSpecialPhraseType : byte + { + Unknown, + BigRockEnding + } +} + +namespace ChartTools.Lyrics +{ + /// + /// Pitch values for + /// + public enum VocalPitchValue : byte + { + /// + /// No pitch + /// + None = 0, + /// + /// Second C (lowest pitch) + /// + C2 = 0x20 | VocalsKey.C, + /// + /// Second C# + /// + CSharp2 = 0x20 | VocalsKey.CSharp, + /// + /// Second D + /// + D2 = 0x20 | VocalsKey.D, + /// + /// Second E-flat + /// + Eb2 = 0x20 | VocalsKey.Eb, + /// + /// Second E + /// + E2 = 0x20 | VocalsKey.E, + /// + /// Second F + /// + F2 = 0x20 | VocalsKey.F, + /// + /// Second F# + /// + FSharp2 = 0x20 | VocalsKey.FSharp, + /// + /// Second G + /// + G2 = 0x20 | VocalsKey.G, + /// + /// Second G# + /// + GSharp2 = 0x20 | VocalsKey.GSharp, + /// + /// Second A + /// + A2 = 0x20 | VocalsKey.A, + /// + /// Second B-flat + /// + Bb2 = 0x20 | VocalsKey.Bb, + /// + /// Second B + /// + B2 = 0x20 | VocalsKey.B, + /// + /// Third C + /// + C3 = 0x30 | VocalsKey.C, + /// + /// Third C# + /// + CSharp3 = 0x30 | VocalsKey.CSharp, + /// + /// Third D + /// + D3 = 0x30 | VocalsKey.D, + /// + /// Third E-flat + /// + Eb3 = 0x30 | VocalsKey.Eb, + /// + /// Third E + /// + E3 = 0x30 | VocalsKey.E, + /// + /// Third F + /// + F3 = 0x30 | VocalsKey.F, + /// + /// Third F# + /// + FSharp3 = 0x30 | VocalsKey.FSharp, + /// + /// Third G + /// + G3 = 0x30 | VocalsKey.G, + /// + /// Third G# + /// + GSharp3 = 0x30 | VocalsKey.GSharp, + /// + /// Third A + /// + A3 = 0x30 | VocalsKey.A, + /// + /// Third B-flat + /// + Bb3 = 0x30 | VocalsKey.Bb, + /// + /// Third B + /// + B3 = 0x30 | VocalsKey.B, + /// + /// Third C + /// + C4 = 0x40 | VocalsKey.C, + /// + /// Fourth C# + /// + CSharp4 = 0x40 | VocalsKey.CSharp, + /// + /// Fourth D + /// + D4 = 0x40 | VocalsKey.D, + /// + /// Fourth E-flat + /// + Eb4 = 0x40 | VocalsKey.Eb, + /// + /// Fourth E + /// + E4 = 0x40 | VocalsKey.E, + /// + /// Fourth F + /// + F4 = 0x40 | VocalsKey.F, + /// + /// Fourth F# + /// + FSharp4 = 0x40 | VocalsKey.FSharp, + /// + /// Fourth G + /// + G4 = 0x40 | VocalsKey.G, + /// + /// Fourth G# + /// + GSharp4 = 0x40 | VocalsKey.GSharp, + /// + /// Fourth A + /// + A4 = 0x40 | VocalsKey.A, + /// + /// Fourth B-flat + /// + Bb4 = 0x40 | VocalsKey.Bb, + /// + /// Fourth B + /// + B4 = 0x40 | VocalsKey.B, + /// + /// Fifth + /// + C5 = 0x50 | VocalsKey.C, + /// + /// Fifth C# + /// + CSharp5 = 0x50 | VocalsKey.CSharp, + /// + /// Fifth D + /// + D5 = 0x50 | VocalsKey.D, + /// + /// Fifth E-flat + /// + Eb5 = 0x50 | VocalsKey.Eb, + /// + /// Fifth E + /// + E5 = 0x50 | VocalsKey.E, + /// + /// Fifth F + /// + F5 = 0x50 | VocalsKey.F, + /// + /// Fifth F# + /// + FSharp5 = 0x50 | VocalsKey.FSharp, + /// + /// Fifth G + /// + G5 = 0x50 | VocalsKey.G, + /// + /// Fifth G# + /// + GSharp5 = 0x50 | VocalsKey.GSharp, + /// + /// Fifth A + /// + A5 = 0x50 | VocalsKey.A, + /// + /// Fifth B-flat + /// + Bb5 = 0x50 | VocalsKey.Bb, + /// + /// Fifth B + /// + B5 = 0x50 | VocalsKey.B, + /// + /// Sixth C (highest pitch) + /// + C6 = 0x60 | VocalsKey.C + } + + /// + /// Keys making up without the octave + /// + public enum VocalsKey : byte + { + /// + /// C key (Do) + /// + C, + /// + /// C# key + /// + CSharp, + /// + /// D key (Ré) + /// + D, + /// + /// E-flat key + /// + Eb, + /// + /// E key (Mi) + /// + E, + /// + /// F key (Fa) + /// + F, + /// + /// F# key + /// + FSharp, + /// + /// G key (Sol) + /// + G, + /// + /// G# key + /// + GSharp, + /// + /// A key (La) + /// + A, + /// + /// B-flat key + /// + Bb, + /// + /// B key (Si) + /// + B + } +} diff --git a/source - Copie/Events/Event.cs b/source - Copie/Events/Event.cs new file mode 100644 index 00000000..9d52c990 --- /dev/null +++ b/source - Copie/Events/Event.cs @@ -0,0 +1,65 @@ +namespace ChartTools.Events; + +/// +/// Marker that defines an occurrence at a given point in a song. +/// +public abstract class Event : TrackObjectBase +{ + private string _eventType = "Default"; + /// + /// Type of event as it is written in the file + /// + public string EventType + { + get => _eventType; + set + { + if (string.IsNullOrEmpty(value)) + throw new FormatException("Event type is empty"); + + if (value.Contains(' ')) + throw new FormatException("Event types cannot contain spaces"); + + _eventType = value; + } + } + + private string? _argument = null; + /// + /// Additional data to modify the outcome of the event + /// + /// A lack of argument is represented as an empty string. + public string? Argument + { + get => _argument; + set => _argument = value ?? string.Empty; + } + + public string EventData + { + get => Argument is null ? EventType : string.Join(' ', EventType, Argument); + set + { + var split = value.Split(' ', 2, StringSplitOptions.None); + + EventType = split[0]; + Argument = split.Length > 1 ? split[1] : string.Empty; + } + } + + public bool? ToggleState => EventType.EndsWith(EventTypeHelper.Common.ToggleOn) ? true : (EventType.EndsWith(EventTypeHelper.Common.ToggleOff) ? false : null); + + /// + /// + public Event(uint position, string data) : base(position) => EventData = data; + /// + /// + /// + public Event(uint position, string type, string? argument) : base(position) + { + EventType = type; + Argument = argument; + } + + public override string ToString() => EventData; +} diff --git a/source - Copie/Events/EventArgumentHelper.cs b/source - Copie/Events/EventArgumentHelper.cs new file mode 100644 index 00000000..f2f98c83 --- /dev/null +++ b/source - Copie/Events/EventArgumentHelper.cs @@ -0,0 +1,19 @@ +namespace ChartTools.Events; + +public class EventArgumentHelper +{ + public static class Global + { + public static class Lighting + { + public const string + Blackout = "(blackout)", + Chase = "(chase)", + Color1 = "(color1)", + Color2 = "(color2)", + Flare = "(flare)", + Strobe = "(strobe)", + Sweep = "(sweep)"; + } + } +} diff --git a/source/Events/EventExtensions.cs b/source - Copie/Events/EventExtensions.cs similarity index 100% rename from source/Events/EventExtensions.cs rename to source - Copie/Events/EventExtensions.cs diff --git a/source - Copie/Events/EventTypeHeaderHelper.cs b/source - Copie/Events/EventTypeHeaderHelper.cs new file mode 100644 index 00000000..ff3c9175 --- /dev/null +++ b/source - Copie/Events/EventTypeHeaderHelper.cs @@ -0,0 +1,26 @@ +namespace ChartTools.Events; + +public static class EventTypeHeaderHelper +{ + public static class Global + { + public const string + BassistMovement = "bass_", + Crowd = "crowd_", + DrummerMovement = "drum_", + GuitaristMovement = "gtr_", + GuitaristSolo = "solo_", + GuitaristWail = "wail_", + KeyboardMovement = "keys_", + Phrase = "phrase_", + SingerMovement = "sing_", + Sync = "sync_"; + } + + public static class Local + { + public const string + GHL6 = "ghl_6", + OwFace = "ow_face_"; + } +} diff --git a/source - Copie/Events/EventTypeHelper.cs b/source - Copie/Events/EventTypeHelper.cs new file mode 100644 index 00000000..cdc033a1 --- /dev/null +++ b/source - Copie/Events/EventTypeHelper.cs @@ -0,0 +1,66 @@ +namespace ChartTools.Events; + +public static class EventTypeHelper +{ + public static class Common + { + public const string ToggleOn = "on"; + public const string ToggleOff = "off"; + } + + public static class Global + { + public const string + BandJump = "band_jump", + BassistIdle = EventTypeHeaderHelper.Global.BassistMovement + Common.ToggleOff, + BassistMove = EventTypeHeaderHelper.Global.BassistMovement + Common.ToggleOn, + DrummerIdle = EventTypeHeaderHelper.Global.DrummerMovement + Common.ToggleOff, + DrummerAll = EventTypeHeaderHelper.Global.DrummerMovement + "allbeat", + DrummerDouble = EventTypeHeaderHelper.Global.DrummerMovement + "double", + DrummerHalf = EventTypeHeaderHelper.Global.DrummerMovement + "half", + DrummerMove = EventTypeHeaderHelper.Global.DrummerMovement + Common.ToggleOn, + Chorus = "chorus", + CrowdLightersFast = EventTypeHeaderHelper.Global.Crowd + "lighters_fast", + CrowdLightersOff = EventTypeHeaderHelper.Global.Crowd + "lighters_off", + CrowdLightersSlow = EventTypeHeaderHelper.Global.Crowd + "lighters_slow", + CrowdHalfTempo = EventTypeHeaderHelper.Global.Crowd + "half_tempo", + CrowdNormalTempo = EventTypeHeaderHelper.Global.Crowd + "normal_tempo", + CrowdDoubleTempo = EventTypeHeaderHelper.Global.Crowd + "double_tempo", + End = "end", + GuitaristIdle = EventTypeHeaderHelper.Global.GuitaristMovement + Common.ToggleOff, + GuitaristMove = EventTypeHeaderHelper.Global.GuitaristMovement + Common.ToggleOn, + GuitaristSoloOn = EventTypeHeaderHelper.Global.GuitaristSolo + Common.ToggleOn, + GuitaristSoloOff = EventTypeHeaderHelper.Global.GuitaristSolo + Common.ToggleOff, + GuitaristWailOn = EventTypeHeaderHelper.Global.GuitaristWail + Common.ToggleOn, + GuitaristWailOff = EventTypeHeaderHelper.Global.GuitaristWail + Common.ToggleOff, + HalfTempo = "half_tempo", + Idle = "idle", + KeyboardIdle = EventTypeHeaderHelper.Global.SingerMovement + Common.ToggleOff, + KeyboardMove = EventTypeHeaderHelper.Global.SingerMovement + Common.ToggleOn, + Lighting = "lighting", + Lyric = "lyric", + MusicStart = "music_start", + NotrmalTempo = "normal_tempo", + Play = "play", + PhraseStart = EventTypeHeaderHelper.Global.Phrase + "start", + PhraseEnd = EventTypeHeaderHelper.Global.Phrase + "end", + RB2CHSection = "section", + RB3Section = "prc_", + SingerIdle = EventTypeHeaderHelper.Global.SingerMovement + Common.ToggleOff, + SingerMove= EventTypeHeaderHelper.Global.SingerMovement + Common.ToggleOn, + SyncHeadBang = EventTypeHeaderHelper.Global.Sync + "head_bang", + SyncWag = EventTypeHeaderHelper.Global.Sync + "wag", + Verse = "verse"; + } + + public static class Local + { + public const string + GHL6 = EventTypeHeaderHelper.Local.GHL6, + GHL6Forced = EventTypeHeaderHelper.Local.GHL6 + "_forced", + OwFaceOn = EventTypeHeaderHelper.Local.OwFace + Common.ToggleOn, + OwFaceOff = EventTypeHeaderHelper.Local.OwFace + Common.ToggleOff, + Solo = "solo", + SoloEnd = "soloend"; + } +} diff --git a/source - Copie/Events/GlobalEvent.cs b/source - Copie/Events/GlobalEvent.cs new file mode 100644 index 00000000..fb102f75 --- /dev/null +++ b/source - Copie/Events/GlobalEvent.cs @@ -0,0 +1,39 @@ +using ChartTools.IO; +using ChartTools.IO.Chart; + +namespace ChartTools.Events; + +/// +/// Event common to all instruments +/// +public class GlobalEvent : Event +{ + public bool IsBassistMovementEvent => EventType.StartsWith(EventTypeHeaderHelper.Global.BassistMovement); + public bool IsCrowdEvent => EventType.StartsWith(EventTypeHeaderHelper.Global.Crowd); + public bool IsDrummerMovementEvent => EventType.StartsWith(EventTypeHeaderHelper.Global.DrummerMovement); + public bool IsGuitaristMovementEvent => EventType.StartsWith(EventTypeHeaderHelper.Global.GuitaristMovement) || EventType.StartsWith(EventTypeHeaderHelper.Global.GuitaristSolo); + public bool IsGuitaristSoloEvent => EventType.StartsWith(EventTypeHeaderHelper.Global.GuitaristSolo); + public bool IsKeyboardMovementEvent => EventType.StartsWith(EventTypeHeaderHelper.Global.KeyboardMovement); + public bool IsLyricEvent => IsPhraseEvent || EventType == EventTypeHelper.Global.Lyric; + public bool IsPhraseEvent => EventType.StartsWith(EventTypeHeaderHelper.Global.Phrase); + public bool IsSectionEvent => EventType is EventTypeHelper.Global.RB2CHSection or EventTypeHelper.Global.RB3Section; + public bool IsSyncEvent => EventType.StartsWith(EventTypeHeaderHelper.Global.Sync); + public bool IsWailEvent => EventType.StartsWith(EventTypeHeaderHelper.Global.GuitaristWail); + + /// + public GlobalEvent(uint position, string data) : base(position, data) { } + /// + public GlobalEvent(uint position, string type, string? argument = null) : base(position, type, argument) { } + + /// + /// Reads global events from a file. + /// + /// Path of the file + public static IEnumerable FromFile(string path) => ExtensionHandler.Read>(path, (".chart", ChartFile.ReadGlobalEvents)); + /// + /// Reads global events from a file asynchronously using multitasking. + /// + /// + /// Token to request cancellation + public static async Task> FromFileAsync(string path, CancellationToken cancellationToken) => await ExtensionHandler.ReadAsync>(path, (".chart", path => ChartFile.ReadGlobalEventsAsync(path, cancellationToken))); +} diff --git a/source - Copie/Events/LocalEvent.cs b/source - Copie/Events/LocalEvent.cs new file mode 100644 index 00000000..0b85e316 --- /dev/null +++ b/source - Copie/Events/LocalEvent.cs @@ -0,0 +1,16 @@ +namespace ChartTools.Events; + +/// +/// Event specific to an instrument and difficulty +/// +public class LocalEvent : Event +{ + public bool IsSoloEvent => EventType is EventTypeHelper.Local.Solo or EventTypeHelper.Local.SoloEnd; + public bool IsOwFaceEvent => EventType.StartsWith(EventTypeHeaderHelper.Local.OwFace); + + /// + public LocalEvent(uint position, string data) : base(position, data) { } + /// + public LocalEvent(uint position, string type, string? argument = null) : base(position, type, argument) { } + +} diff --git a/source - Copie/Exceptions/DesynchronizedAnchorException.cs b/source - Copie/Exceptions/DesynchronizedAnchorException.cs new file mode 100644 index 00000000..9e7d5cb1 --- /dev/null +++ b/source - Copie/Exceptions/DesynchronizedAnchorException.cs @@ -0,0 +1,12 @@ +namespace ChartTools; + +/// +/// Exception thrown when an invalid operation is performed on a desynchronized anchored . +/// +public class DesynchronizedAnchorException : Exception +{ + public TimeSpan Anchor { get; } + + public DesynchronizedAnchorException(TimeSpan anchor) : this(anchor, $"Invalid operation performed with desynchronized anchored tempo at {anchor}.") { } + public DesynchronizedAnchorException(TimeSpan anchor, string message) : base(message) => Anchor = anchor; +} diff --git a/source - Copie/Exceptions/UndefinedEnumException.cs b/source - Copie/Exceptions/UndefinedEnumException.cs new file mode 100644 index 00000000..36575ba3 --- /dev/null +++ b/source - Copie/Exceptions/UndefinedEnumException.cs @@ -0,0 +1,16 @@ +namespace ChartTools; + +/// +/// Exception thrown when using an value that is not defined +/// +public class UndefinedEnumException : ArgumentException +{ + /// + /// Value used + /// + public Enum Value { get; } + + public UndefinedEnumException(Enum value) : base($"{value.GetType().Name} \"{value}\" is not defined.") => Value = value; + + +} diff --git a/source - Copie/Exceptions/Validator.cs b/source - Copie/Exceptions/Validator.cs new file mode 100644 index 00000000..da33f26a --- /dev/null +++ b/source - Copie/Exceptions/Validator.cs @@ -0,0 +1,14 @@ +namespace ChartTools; + +internal static class Validator +{ + /// + /// Validates that an value is defined. + /// + /// + public static void ValidateEnum(Enum value) + { + if (!Enum.IsDefined(value.GetType(), value)) + throw new UndefinedEnumException(value); + } +} diff --git a/source/Extensions/Collections/Alternating/Ordered/OrderedAlternatingEnumerable.cs b/source - Copie/Extensions/Collections/Alternating/Ordered/OrderedAlternatingEnumerable.cs similarity index 100% rename from source/Extensions/Collections/Alternating/Ordered/OrderedAlternatingEnumerable.cs rename to source - Copie/Extensions/Collections/Alternating/Ordered/OrderedAlternatingEnumerable.cs diff --git a/source/Extensions/Collections/Alternating/Ordered/OrderedAlternatingEnumerator.cs b/source - Copie/Extensions/Collections/Alternating/Ordered/OrderedAlternatingEnumerator.cs similarity index 100% rename from source/Extensions/Collections/Alternating/Ordered/OrderedAlternatingEnumerator.cs rename to source - Copie/Extensions/Collections/Alternating/Ordered/OrderedAlternatingEnumerator.cs diff --git a/source/Extensions/Collections/Alternating/Serial/SerialAlternatingEnumerable.cs b/source - Copie/Extensions/Collections/Alternating/Serial/SerialAlternatingEnumerable.cs similarity index 100% rename from source/Extensions/Collections/Alternating/Serial/SerialAlternatingEnumerable.cs rename to source - Copie/Extensions/Collections/Alternating/Serial/SerialAlternatingEnumerable.cs diff --git a/source/Extensions/Collections/Alternating/Serial/SerialAlternatingEnumerator.cs b/source - Copie/Extensions/Collections/Alternating/Serial/SerialAlternatingEnumerator.cs similarity index 100% rename from source/Extensions/Collections/Alternating/Serial/SerialAlternatingEnumerator.cs rename to source - Copie/Extensions/Collections/Alternating/Serial/SerialAlternatingEnumerator.cs diff --git a/source - Copie/Extensions/Collections/Delayed/DelayedEnumerable.cs b/source - Copie/Extensions/Collections/Delayed/DelayedEnumerable.cs new file mode 100644 index 00000000..57550b50 --- /dev/null +++ b/source - Copie/Extensions/Collections/Delayed/DelayedEnumerable.cs @@ -0,0 +1,31 @@ +using System.Collections; + +namespace ChartTools.Extensions.Collections; + +public class DelayedEnumerable : IEnumerable, IDisposable +{ + private readonly DelayedEnumerator enumerator; + private readonly DelayedEnumerableSource source; + + /// + /// if there are more items to be received + /// + public bool AwaitingItems => source.AwaitingItems; + + internal DelayedEnumerable(DelayedEnumerableSource source) + { + this.source = source; + enumerator = new(source); + } + + public IEnumerable EnumerateSynchronously() + { + while (AwaitingItems); + return source.Buffer; + } + + public IEnumerator GetEnumerator() => enumerator; + IEnumerator IEnumerable.GetEnumerator() => enumerator; + + public void Dispose() => enumerator.Dispose(); +} diff --git a/source - Copie/Extensions/Collections/Delayed/DelayedEnumerableSource.cs b/source - Copie/Extensions/Collections/Delayed/DelayedEnumerableSource.cs new file mode 100644 index 00000000..14eb2763 --- /dev/null +++ b/source - Copie/Extensions/Collections/Delayed/DelayedEnumerableSource.cs @@ -0,0 +1,37 @@ +using System.Collections.Concurrent; + +namespace ChartTools.Extensions.Collections; + +public class DelayedEnumerableSource : IDisposable +{ + private bool disposed; + + public ConcurrentQueue Buffer { get; } = new(); + public DelayedEnumerable Enumerable { get; } + public bool AwaitingItems { get; private set; } = true; + + public DelayedEnumerableSource() => Enumerable = new(this); + + public void Add(T item) => Buffer.Enqueue(item); + public void EndAwait() => AwaitingItems = false; + + protected virtual void Dispose(bool disposing) + { + if (!disposed) + { + if (disposing) + AwaitingItems = false; + + Enumerable.Dispose(); + disposed = true; + } + } + + ~DelayedEnumerableSource() => Dispose(false); + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } +} diff --git a/source - Copie/Extensions/Collections/Delayed/DelayedEnumerator.cs b/source - Copie/Extensions/Collections/Delayed/DelayedEnumerator.cs new file mode 100644 index 00000000..793313d6 --- /dev/null +++ b/source - Copie/Extensions/Collections/Delayed/DelayedEnumerator.cs @@ -0,0 +1,37 @@ +using System.Collections; + +namespace ChartTools.Extensions.Collections; + +internal class DelayedEnumerator : IEnumerator +{ + public T Current { get; private set; } + object? IEnumerator.Current => Current; + public bool AwaitingItems => source.AwaitingItems; + + private readonly DelayedEnumerableSource source; + + internal DelayedEnumerator(DelayedEnumerableSource source) => this.source = source; + + private bool WaitForItems() + { + while (source.Buffer.IsEmpty) + if (!AwaitingItems && source.Buffer.IsEmpty) + return false; + + return true; + } + public bool MoveNext() + { + if (!WaitForItems()) + return false; + + source.Buffer.TryDequeue(out T? item); + Current = item!; + + return true; + } + + public void Dispose() => GC.SuppressFinalize(this); + + public void Reset() => throw new InvalidOperationException(); +} diff --git a/source - Copie/Extensions/Collections/EagerEnumerable.cs b/source - Copie/Extensions/Collections/EagerEnumerable.cs new file mode 100644 index 00000000..73d428e1 --- /dev/null +++ b/source - Copie/Extensions/Collections/EagerEnumerable.cs @@ -0,0 +1,23 @@ +using System.Collections; + +namespace ChartTools.Internal.Collections; + +internal class EagerEnumerable : IEnumerable +{ + private IEnumerable? items; + private readonly Task> source; + + public EagerEnumerable(Task> source) => this.source = source; + + public IEnumerator GetEnumerator() + { + if (items is null) + { + source.Wait(); + items = source.Result; + } + + return items.GetEnumerator(); + } + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); +} diff --git a/source - Copie/Extensions/Collections/IInitializable.cs b/source - Copie/Extensions/Collections/IInitializable.cs new file mode 100644 index 00000000..37a67eb0 --- /dev/null +++ b/source - Copie/Extensions/Collections/IInitializable.cs @@ -0,0 +1,17 @@ +namespace ChartTools.Extensions.Collections; + +/// +/// Defines an object that can be initialized +/// +public interface IInitializable +{ + /// + /// Has already been initialized + /// + public bool Initialized { get; } + /// + /// Does required initialization if not already done. + /// + /// if the object was not initialized prior to calling. + public bool Initialize(); +} diff --git a/source - Copie/Extensions/EnumCache.cs b/source - Copie/Extensions/EnumCache.cs new file mode 100644 index 00000000..52ee9d34 --- /dev/null +++ b/source - Copie/Extensions/EnumCache.cs @@ -0,0 +1,9 @@ +namespace ChartTools.Extensions; + +internal static class EnumCache where T : struct, Enum +{ + public static T[] Values => _values ??= Enum.GetValues().ToArray(); + private static T[]? _values; + + public static void Clear() => _values = null; +} diff --git a/source - Copie/Extensions/EqualityComparison.cs b/source - Copie/Extensions/EqualityComparison.cs new file mode 100644 index 00000000..b99e1def --- /dev/null +++ b/source - Copie/Extensions/EqualityComparison.cs @@ -0,0 +1,6 @@ +namespace ChartTools.Extensions; + +/// +/// equivalent to the delegate +/// +public delegate bool EqualityComparison(T a, T b); diff --git a/source - Copie/Extensions/FuncEqualityComparer.cs b/source - Copie/Extensions/FuncEqualityComparer.cs new file mode 100644 index 00000000..214df323 --- /dev/null +++ b/source - Copie/Extensions/FuncEqualityComparer.cs @@ -0,0 +1,27 @@ +namespace ChartTools.Extensions; + +/// +/// Delegate-based +/// +public class FuncEqualityComparer : IEqualityComparer +{ + /// + /// Method used to compare two objects + /// + public EqualityComparison Comparison { get; } + + /// + /// Creates a new instance. + /// + /// Method used to compare two objects + public FuncEqualityComparer(EqualityComparison comparison) + { + if (comparison is null) + throw new ArgumentNullException(nameof(comparison), "The comparison is null"); + + Comparison = comparison; + } + + public bool Equals(T? x, T? y) => Comparison(x, y); + public int GetHashCode(T obj) => obj!.GetHashCode(); +} diff --git a/source - Copie/Extensions/IInitializable.cs b/source - Copie/Extensions/IInitializable.cs new file mode 100644 index 00000000..4997f2db --- /dev/null +++ b/source - Copie/Extensions/IInitializable.cs @@ -0,0 +1,17 @@ +namespace ChartTools.Extensions; + +/// +/// Defines an object that can be initialized +/// +public interface IInitializable +{ + /// + /// Has already been initialized + /// + public bool Initialized { get; } + /// + /// Does required initialization if not already done. + /// + /// if the object was not initialized prior to calling. + public bool Initialize(); +} diff --git a/source - Copie/Extensions/Linq/CollectionExtensions.cs b/source - Copie/Extensions/Linq/CollectionExtensions.cs new file mode 100644 index 00000000..ca9c0eda --- /dev/null +++ b/source - Copie/Extensions/Linq/CollectionExtensions.cs @@ -0,0 +1,42 @@ +namespace ChartTools.Extensions.Linq; + +public static class CollectionExtensions +{ + public static int BinarySearchIndex(this IList source, TKey target, Func keySelector, out bool exactMatch) where TKey : notnull, IComparable + { + int left = 0, right = source.Count - 1, middle, index = 0; + + while (left <= right) + { + middle = (left + right) / 2; + + switch (keySelector(source[middle]).CompareTo(target)) + { + case -1: + index = left = middle + 1; + break; + case 0: + exactMatch = true; + return middle; + case 1: + index = right = middle - 1; + break; + } + } + + exactMatch = false; + return index; + } + public static int BinarySearchIndex(this IList source, T target, out bool exactMatch) where T : notnull, IComparable => BinarySearchIndex(source, target, t => t, out exactMatch); + + /// + /// Removes all items in a that meet a condition + /// + /// Collection to remove items from + /// Function that determines which items to remove + public static void RemoveWhere(this ICollection source, Predicate predicate) + { + foreach (T item in source.Where(i => predicate(i))) + source.Remove(item); + } +} diff --git a/source/Extensions/Linq/EnumerableExtensions.cs b/source - Copie/Extensions/Linq/EnumerableExtensions.cs similarity index 100% rename from source/Extensions/Linq/EnumerableExtensions.cs rename to source - Copie/Extensions/Linq/EnumerableExtensions.cs diff --git a/source - Copie/Extensions/Linq/SectionReplacement.cs b/source - Copie/Extensions/Linq/SectionReplacement.cs new file mode 100644 index 00000000..ed2b35b4 --- /dev/null +++ b/source - Copie/Extensions/Linq/SectionReplacement.cs @@ -0,0 +1,11 @@ +namespace ChartTools.Extensions.Linq +{ + /// + /// Replacement for a section of items in a collection + /// + /// Items to replace with + /// Method that defines if a source marks the start of the section to replace + /// Method that defines if a source item marks the end of the section to replace + /// The replacement should be appended to the collection if the section to replace is not found + public readonly record struct SectionReplacement(IEnumerable Replacement, Predicate StartReplace, Predicate EndReplace, bool AddIfMissing); +} diff --git a/source - Copie/Extensions/StringExtensions.cs b/source - Copie/Extensions/StringExtensions.cs new file mode 100644 index 00000000..09b1edf9 --- /dev/null +++ b/source - Copie/Extensions/StringExtensions.cs @@ -0,0 +1,22 @@ +namespace ChartTools.Extensions; + +/// +/// Provides additional methods to string +/// +internal static class StringExtensions +{ + /// + public static string VerbalEnumerate(this IEnumerable items, string lastItemPreceder) => VerbalEnumerate(lastItemPreceder, items.ToArray()); + /// + /// Enumerates items with commas and a set word preceding the last item. + /// + /// Word to place before the last item + /// + public static string VerbalEnumerate(string lastItemPreceder, params string[] items) => items is null ? throw new ArgumentNullException(nameof(items)) : items.Length switch + { + 0 => string.Empty, // "" + 1 => items[0], // "Item1" + 2 => $"{items[0]} {lastItemPreceder} {items[1]}", // "Item1 lastItemPreceder Item2" + _ => $"{string.Join(", ", items, items.Length - 1)} {lastItemPreceder} {items[^0]}" // "Item1, Item2 lastItemPreceder Item3" + }; +} diff --git a/source - Copie/GlobalSuppressions.cs b/source - Copie/GlobalSuppressions.cs new file mode 100644 index 00000000..bf762268 --- /dev/null +++ b/source - Copie/GlobalSuppressions.cs @@ -0,0 +1,8 @@ +// This file is used by Code Analysis to maintain SuppressMessage +// attributes that are applied to this project. +// Project-level suppressions either have no target or are given +// a specific target and scoped to a namespace, type, member, etc. + +using System.Diagnostics.CodeAnalysis; + +[assembly: SuppressMessage("Minor Code Smell", "S101:Types should be named in PascalCase", Justification = "GHL is an accronym", Scope = "type", Target = "~T:ChartTools.GHLChord")] diff --git a/source - Copie/IEmptyVerifiable.cs b/source - Copie/IEmptyVerifiable.cs new file mode 100644 index 00000000..a77c5ecd --- /dev/null +++ b/source - Copie/IEmptyVerifiable.cs @@ -0,0 +1,12 @@ +namespace ChartTools; + +/// +/// Adds support for a property defining if an object is empty +/// +public interface IEmptyVerifiable +{ + /// + /// if containing no data + /// + public bool IsEmpty { get; } +} diff --git a/source - Copie/ILongObject.cs b/source - Copie/ILongObject.cs new file mode 100644 index 00000000..8edfa160 --- /dev/null +++ b/source - Copie/ILongObject.cs @@ -0,0 +1,9 @@ +namespace ChartTools; + +public interface ILongObject : IReadOnlyLongObject +{ + /// + public new uint Length { get; set; } + + uint IReadOnlyLongObject.Length => Length; +} diff --git a/source - Copie/IO/Anchor.cs b/source - Copie/IO/Anchor.cs new file mode 100644 index 00000000..4f5eaaf2 --- /dev/null +++ b/source - Copie/IO/Anchor.cs @@ -0,0 +1,13 @@ +namespace ChartTools.IO; + +internal readonly struct Anchor : IReadOnlyTrackObject +{ + public uint Position { get; } + public TimeSpan Value { get; } + + public Anchor(uint position, TimeSpan value) + { + Position = position; + Value = value; + } +} diff --git a/source - Copie/IO/Appliables/IInstrumentAppliable.cs b/source - Copie/IO/Appliables/IInstrumentAppliable.cs new file mode 100644 index 00000000..ea00b2c2 --- /dev/null +++ b/source - Copie/IO/Appliables/IInstrumentAppliable.cs @@ -0,0 +1,6 @@ +namespace ChartTools.IO; + +internal interface IInstrumentAppliable where TChord : IChord +{ + public void ApplyToInstrument(Instrument instrument); +} diff --git a/source - Copie/IO/Appliables/ISongAppliable.cs b/source - Copie/IO/Appliables/ISongAppliable.cs new file mode 100644 index 00000000..645fd2be --- /dev/null +++ b/source - Copie/IO/Appliables/ISongAppliable.cs @@ -0,0 +1,6 @@ +namespace ChartTools.IO; + +internal interface ISongAppliable +{ + public void ApplyToSong(Song song); +} diff --git a/source - Copie/IO/Chart/ChartFile.cs b/source - Copie/IO/Chart/ChartFile.cs new file mode 100644 index 00000000..87790eb9 --- /dev/null +++ b/source - Copie/IO/Chart/ChartFile.cs @@ -0,0 +1,707 @@ +using ChartTools.Events; +using ChartTools.Extensions; +using ChartTools.Extensions.Linq; +using ChartTools.IO.Chart.Parsing; +using ChartTools.IO.Chart.Serializing; +using ChartTools.IO.Configuration; +using ChartTools.IO.Configuration.Sessions; +using ChartTools.IO.Formatting; +using ChartTools.Lyrics; + +namespace ChartTools.IO.Chart; + +/// +/// Provides methods for reading and writing chart files +/// +public static class ChartFile +{ + /// + /// Default configuration to use for reading when the provided configuration is + /// + public static ReadingConfiguration DefaultReadConfig { get; set; } = new() + { + DuplicateTrackObjectPolicy = DuplicateTrackObjectPolicy.ThrowException, + SoloNoStarPowerPolicy = SoloNoStarPowerPolicy.Convert + }; + /// + /// Default configuration to use for writing when the provided configuration is + /// + public static WritingConfiguration DefaultWriteConfig { get; set; } = new() + { + SoloNoStarPowerPolicy = SoloNoStarPowerPolicy.Convert, + EventSource = TrackObjectSource.Merge, + StarPowerSource = TrackObjectSource.Merge, + UnsupportedModifierPolicy = UnsupportedModifierPolicy.ThrowException + }; + + #region Reading + #region Song + /// + /// Creates a for parsing a section based on the header. + /// + /// + private static ChartParser GetSongParser(string header, ReadingSession session) + { + switch (header) + { + case ChartFormatting.MetadataHeader: + return new MetadataParser(); + case ChartFormatting.GlobalEventHeader: + return new GlobalEventParser(session); + case ChartFormatting.SyncTrackHeader: + return new SyncTrackParser(session); + default: + if (drumsTrackHeaders.TryGetValue(header, out Difficulty diff)) + return new DrumsTrackParser(diff, session, header); + else if (ghlTrackHeaders.TryGetValue(header, out (Difficulty, GHLInstrumentIdentity) ghlTuple)) + return new GHLTrackParser(ghlTuple.Item1, ghlTuple.Item2, session, header); + else if (standardTrackHeaders.TryGetValue(header, out (Difficulty, StandardInstrumentIdentity) standardTuple)) + return new StandardTrackParser(standardTuple.Item1, standardTuple.Item2, session, header); + else + { + return session.Configuration.UnknownSectionPolicy == UnknownSectionPolicy.ThrowException + ? throw new Exception($"Unknown section with header \"{header}\". Consider using {UnknownSectionPolicy.Store} to avoid this error.") + : new UnknownSectionParser(session, header); + } + } + } + + /// + /// Combines the results from the parsers of a into a . + /// + /// Reader to get the parsers from + private static Song CreateSongFromReader(ChartFileReader reader) + { + Song song = new(); + + foreach (var parser in reader.Parsers) + parser.ApplyToSong(song); + + return song; + } + + /// + /// + /// + public static Song ReadSong(string path, ReadingConfiguration? config = default, FormattingRules? formatting = default) + { + var session = new ReadingSession(config ?? DefaultReadConfig, formatting ?? new()); + var reader = new ChartFileReader(path, header => GetSongParser(header, session)); + + reader.Read(); + return CreateSongFromReader(reader); + } + + /// + /// + /// + /// + public static async Task ReadSongAsync(string path, ReadingConfiguration? config = default, FormattingRules? formatting = default, CancellationToken cancellationToken = default) + { + var session = new ReadingSession(config ?? DefaultReadConfig, formatting ?? new()); + var reader = new ChartFileReader(path, header => GetSongParser(header, session)); + + await reader.ReadAsync(cancellationToken); + return CreateSongFromReader(reader); + } + #endregion + #region Instruments + /// + /// Combines the results from the parsers in a into an instrument. + /// + private static TInst? CreateInstrumentFromReader(ChartFileReader reader) where TInst : Instrument, new() where TChord : IChord, new() + { + TInst? output = null; + + foreach (var parser in reader.Parsers) + (output ??= new()).SetTrack(((TrackParser)parser).Result!); + + return output; + } + + /// + /// Reads an instrument from a chart file. + /// + /// Instance of containing all data about the given instrument + /// if the file contains no data for the given instrument + /// + /// Path of the file to read + /// Instrument to read + /// + /// + /// + /// + public static Instrument? ReadInstrument(string path, InstrumentIdentity instrument, ReadingConfiguration? config = default, FormattingRules? formatting = default) + { + if (instrument == InstrumentIdentity.Drums) + return ReadDrums(path, config, formatting); + if (Enum.IsDefined((GHLInstrumentIdentity)instrument)) + return ReadInstrument(path, (GHLInstrumentIdentity)instrument, config, formatting); + return Enum.IsDefined((StandardInstrumentIdentity)instrument) + ? ReadInstrument(path, (StandardInstrumentIdentity)instrument, config, formatting) + : throw new UndefinedEnumException(instrument); + } + public static async Task ReadInstrumentAsync(string path, InstrumentIdentity instrument, ReadingConfiguration? config = default, FormattingRules? formatting = default, CancellationToken cancellationToken = default) + { + if (instrument == InstrumentIdentity.Drums) + return await ReadDrumsAsync(path, config, formatting, cancellationToken); + if (Enum.IsDefined((GHLInstrumentIdentity)instrument)) + return await ReadInstrumentAsync(path, (GHLInstrumentIdentity)instrument, config, formatting, cancellationToken); + return Enum.IsDefined((StandardInstrumentIdentity)instrument) + ? await ReadInstrumentAsync(path, (StandardInstrumentIdentity)instrument, config, formatting, cancellationToken) + : throw new UndefinedEnumException(instrument); + } + #region Vocals + /// + /// Reads vocals from the global events in a chart file. + /// + /// Instance of where TChord is containing lyric and timing data + /// if the file contains no drums data + /// + /// Path of the file to read + public static Vocals? ReadVocals(string path) => BuildVocals(ReadGlobalEvents(path)); + public static async Task ReadVocalsAsync(string path, CancellationToken cancellationToken = default) => BuildVocals(await ReadGlobalEventsAsync(path, cancellationToken)); + private static Vocals? BuildVocals(List events) + { + var lyrics = events.GetLyrics().ToArray(); + + if (lyrics.Length == 0) + return null; + + var instument = new Vocals(); + + foreach (var diff in EnumCache.Values) + { + var track = instument.CreateTrack(diff); + track.Chords.AddRange(lyrics); + } + + return instument; + } + #endregion + #region Drums + private static DrumsTrackParser? GetAnyDrumsTrackParser(string header, ReadingSession session) => drumsTrackHeaders.TryGetValue(header, out Difficulty difficulty) + ? new(difficulty, session, header) + : null; + /// + /// Reads drums from a chart file. + /// + /// Instance of where TChord is containing all drums data + /// if the file contains no drums data + /// + /// Path of the file to read + /// + /// + public static Drums? ReadDrums(string path, ReadingConfiguration? config = default, FormattingRules? formatting = default) + { + var session = new ReadingSession(config ?? DefaultReadConfig, formatting ?? new()); + var reader = new ChartFileReader(path, header => GetAnyDrumsTrackParser(header, session)); + + reader.Read(); + return CreateInstrumentFromReader(reader); + } + public static async Task ReadDrumsAsync(string path, ReadingConfiguration? config = default, FormattingRules? formatting = default, CancellationToken cancellationToken = default) + { + var session = new ReadingSession(config ?? DefaultReadConfig, formatting ?? new()); + var reader = new ChartFileReader(path, header => GetAnyDrumsTrackParser(header, session)); + + await reader.ReadAsync(cancellationToken); + return CreateInstrumentFromReader(reader); + } + #endregion + #region GHL + private static GHLTrackParser? GetAnyGHLTrackParser(string header, GHLInstrumentIdentity instrument, ReadingSession session) => ghlTrackHeaders.TryGetValue(header, out (Difficulty, GHLInstrumentIdentity) tuple) && tuple.Item2 == instrument + ? new(tuple.Item1, tuple.Item2, session, header) + : null; + /// + /// Reads a Guitar Hero Live instrument from a chart file. + /// + /// Instance of where TChord is containing all data about the given instrument + /// if the file has no data for the given instrument + /// + /// Path of the file to read + /// + public static GHLInstrument? ReadInstrument(string path, GHLInstrumentIdentity instrument, ReadingConfiguration? config = default, FormattingRules? formatting = default) + { + Validator.ValidateEnum(instrument); + + var session = new ReadingSession(config ?? DefaultReadConfig, formatting ?? new()); + var reader = new ChartFileReader(path, header => GetAnyGHLTrackParser(header, instrument, session)); + + reader.Read(); + return CreateInstrumentFromReader(reader); + } + public static async Task ReadInstrumentAsync(string path, GHLInstrumentIdentity instrument, ReadingConfiguration? config = default, FormattingRules? formatting = default, CancellationToken cancellationToken = default) + { + Validator.ValidateEnum(instrument); + + var session = new ReadingSession(config ?? DefaultReadConfig, formatting ?? new()); + var reader = new ChartFileReader(path, header => GetAnyGHLTrackParser(header, instrument, session)); + + await reader.ReadAsync(cancellationToken); + return CreateInstrumentFromReader(reader); + } + #endregion + #region Standard + private static StandardTrackParser? GetAnyStandardTrackParser(string header, StandardInstrumentIdentity instrument, ReadingSession session) => standardTrackHeaders.TryGetValue(header, out (Difficulty, StandardInstrumentIdentity) tuple) && tuple.Item2 == instrument + ? new(tuple.Item1, tuple.Item2, session, header) + : null; + /// + /// + /// + /// + public static StandardInstrument? ReadInstrument(string path, StandardInstrumentIdentity instrument, ReadingConfiguration? config = default, FormattingRules? formatting = default) + { + Validator.ValidateEnum(instrument); + + var session = new ReadingSession(config ?? DefaultReadConfig, formatting ?? new()); + var reader = new ChartFileReader(path, header => GetAnyStandardTrackParser(header, instrument, session)); + + reader.Read(); + return CreateInstrumentFromReader(reader); + } + /// + /// + /// + /// + /// + public static async Task ReadInstrumentAsync(string path, StandardInstrumentIdentity instrument, ReadingConfiguration? config = default, FormattingRules? formatting = default, CancellationToken cancellationToken = default) + { + Validator.ValidateEnum(instrument); + + var session = new ReadingSession(config ?? DefaultReadConfig, formatting ?? new()); + var reader = new ChartFileReader(path, header => GetAnyStandardTrackParser(header, instrument, session)); + + await reader.ReadAsync(cancellationToken); + return CreateInstrumentFromReader(reader); + } + #endregion + #endregion + #region Tracks + /// + /// + /// + /// + /// + public static Track ReadTrack(string path, InstrumentIdentity instrument, Difficulty difficulty, ReadingConfiguration? config = default, FormattingRules? formatting = default) + { + if (instrument == InstrumentIdentity.Drums) + return ReadDrumsTrack(path, difficulty, config, formatting); + if (Enum.IsDefined((GHLInstrumentIdentity)instrument)) + return ReadTrack(path, (GHLInstrumentIdentity)instrument, difficulty, config, formatting); + if (Enum.IsDefined((StandardInstrumentIdentity)instrument)) + return ReadTrack(path, (StandardInstrumentIdentity)instrument, difficulty, config, formatting); + + throw new UndefinedEnumException(instrument); + } + /// + /// + /// + /// + /// + /// + public static async Task ReadTrackAsync(string path, InstrumentIdentity instrument, Difficulty difficulty, ReadingConfiguration? config = default, FormattingRules? formatting = default, CancellationToken cancellationToken = default) + { + if (instrument == InstrumentIdentity.Drums) + return await ReadDrumsTrackAsync(path, difficulty, config, formatting, cancellationToken); + if (Enum.IsDefined((GHLInstrumentIdentity)instrument)) + return await ReadTrackAsync(path, (GHLInstrumentIdentity)instrument, difficulty, config, formatting, cancellationToken); + if (Enum.IsDefined((StandardInstrumentIdentity)instrument)) + return await ReadTrackAsync(path, (StandardInstrumentIdentity)instrument, difficulty, config, formatting, cancellationToken); + + throw new UndefinedEnumException(instrument); + } + #region Drums + /// + /// Creates a is the header matches the requested standard track, otherwise . + /// + /// Header of the part + /// Header to compare against + /// Difficulty identity to provide the parser + /// Session to provide the parser + private static DrumsTrackParser? GetDrumsTrackParser(string header, string seekedHeader, Difficulty difficulty, ReadingSession session) => header == seekedHeader ? new(difficulty, session, header) : null; + /// + /// Headers for drums tracks + /// + private static readonly Dictionary drumsTrackHeaders = EnumCache.Values.ToDictionary(diff => ChartFormatting.Header(ChartFormatting.DrumsHeaderName, diff)); + /// + /// + /// + /// + public static Track ReadDrumsTrack(string path, Difficulty difficulty, ReadingConfiguration? config = default, FormattingRules? formatting = default) + { + Validator.ValidateEnum(difficulty); + + var seekedHeader = ChartFormatting.Header(InstrumentIdentity.Drums, difficulty); + var session = new ReadingSession(config ?? DefaultReadConfig, formatting ?? new()); + var reader = new ChartFileReader(path, header => GetDrumsTrackParser(header, seekedHeader, difficulty, session)); + + reader.Read(); + return reader.Parsers.TryGetFirstOfType(out DrumsTrackParser? parser) ? parser!.Result! : new(); + } + /// + /// + /// + /// + /// + public static async Task> ReadDrumsTrackAsync(string path, Difficulty difficulty, ReadingConfiguration? config = default, FormattingRules? formatting = default, CancellationToken cancellationToken = default) + { + Validator.ValidateEnum(difficulty); + + var seekedHeader = ChartFormatting.Header(ChartFormatting.DrumsHeaderName, difficulty); + var session = new ReadingSession(config ?? DefaultReadConfig, formatting ?? new()); + var reader = new ChartFileReader(path, header => GetDrumsTrackParser(header, seekedHeader, difficulty, session)); + + await reader.ReadAsync(cancellationToken); + return reader.Parsers.TryGetFirstOfType(out DrumsTrackParser? parser) ? parser!.Result! : new(); + } + #endregion + #region GHL + /// + /// Creates a is the header matches the requested standard track, otherwise . + /// + /// Header of the part + /// Header to compare against + /// Instrument identity to provide the parser + /// Difficulty identity to provide the parser + /// Session to provide the parser + private static GHLTrackParser? GetGHLTrackParser(string header, string seekedHeader, GHLInstrumentIdentity instrument, Difficulty difficulty, ReadingSession session) => header == seekedHeader ? new(difficulty, instrument, session, header) : null; + /// + /// Headers for GHL tracks + /// + private static readonly Dictionary ghlTrackHeaders = GetTrackCombinations(Enum.GetValues()).ToDictionary(tuple => ChartFormatting.Header(tuple.instrument, tuple.difficulty)); + /// + /// + /// + /// + /// + public static Track ReadTrack(string path, GHLInstrumentIdentity instrument, Difficulty difficulty, ReadingConfiguration? config = default, FormattingRules? formatting = default) + { + Validator.ValidateEnum(instrument); + Validator.ValidateEnum(difficulty); + + var seekedHeader = ChartFormatting.Header(instrument, difficulty); + var session = new ReadingSession(config ?? DefaultReadConfig, formatting ?? new()); + var reader = new ChartFileReader(path, header => GetGHLTrackParser(header, seekedHeader, instrument, difficulty, session)); + + reader.Read(); + return reader.Parsers.TryGetFirstOfType(out GHLTrackParser? parser) ? parser!.Result : new(); + } + /// + /// + /// + /// + /// + /// + public static async Task> ReadTrackAsync(string path, GHLInstrumentIdentity instrument, Difficulty difficulty, ReadingConfiguration? config, FormattingRules? formatting = default, CancellationToken cancellationToken = default) + { + Validator.ValidateEnum(instrument); + Validator.ValidateEnum(difficulty); + + var seekedHeader = ChartFormatting.Header(instrument, difficulty); + var session = new ReadingSession(config ?? DefaultReadConfig, formatting ?? new()); + var reader = new ChartFileReader(path, header => GetGHLTrackParser(header, seekedHeader, instrument, difficulty, session)); + + await reader.ReadAsync(cancellationToken); + return reader.Parsers.TryGetFirstOfType(out GHLTrackParser? parser) ? parser!.Result : new(); + } + #endregion + #region Standard + /// + /// Creates a is the header matches the requested standard track, otherwise . + /// + /// Header of the part + /// Header to compare against + /// Instrument identity to provide the parser + /// Difficulty identity to provide the parser + /// Session to provide the parser + private static StandardTrackParser? GetStandardTrackParser(string header, string seekedHeader, StandardInstrumentIdentity instrument, Difficulty difficulty, ReadingSession session) => header == seekedHeader ? new(difficulty, instrument, session, header) : null; + + /// + /// Headers for standard tracks + /// + private static readonly Dictionary standardTrackHeaders = GetTrackCombinations(Enum.GetValues()).ToDictionary(tuple => ChartFormatting.Header((InstrumentIdentity)tuple.instrument, tuple.difficulty)); + + /// + /// + /// + /// + /// + public static Track ReadTrack(string path, StandardInstrumentIdentity instrument, Difficulty difficulty, ReadingConfiguration? config = default, FormattingRules? formatting = default) + { + Validator.ValidateEnum(instrument); + Validator.ValidateEnum(difficulty); + + var seekedHeader = ChartFormatting.Header(instrument, difficulty); + var session = new ReadingSession(config ?? DefaultReadConfig, formatting ?? new()); + var reader = new ChartFileReader(path, header => GetStandardTrackParser(header, seekedHeader, instrument, difficulty, session)); + + reader.Read(); + return reader.Parsers.TryGetFirstOfType(out StandardTrackParser? parser) ? parser!.Result! : new(); + } + /// + /// + /// + /// + /// + /// + /// + public static async Task> ReadTrackAsync(string path, StandardInstrumentIdentity instrument, Difficulty difficulty, ReadingConfiguration? config = default, FormattingRules? formatting = default, CancellationToken cancellationToken = default) + { + Validator.ValidateEnum(instrument); + Validator.ValidateEnum(difficulty); + + var seekedHeader = ChartFormatting.Header(instrument, difficulty); + var session = new ReadingSession(config ?? DefaultReadConfig, formatting ?? new()); + var reader = new ChartFileReader(path, header => GetStandardTrackParser(header, seekedHeader, instrument, difficulty, session)); + + await reader.ReadAsync(cancellationToken); + return reader.Parsers.TryGetFirstOfType(out StandardTrackParser? parser) ? parser!.Result! : new(); + } + #endregion + #endregion + #region Metadata + private static MetadataParser? GetMetadataParser(string header) => header == ChartFormatting.MetadataHeader ? new() : null; + /// + /// Reads metadata from a chart file. + /// + /// Path of the file to read + public static Metadata ReadMetadata(string path) + { + var reader = new ChartFileReader(path, header => GetMetadataParser(header)); + reader.Read(); + return reader.Parsers.TryGetFirstOfType(out MetadataParser? parser) ? parser!.Result : new(); + } + #endregion + #region Global events + /// + /// Creates a if the header matches the sync track header, otherwise . + /// + private static GlobalEventParser? GetGlobalEventParser(string header) => header == ChartFormatting.GlobalEventHeader ? new(null!) : null; + + /// + /// + public static List ReadGlobalEvents(string path) + { + var reader = new ChartFileReader(path, GetGlobalEventParser); + reader.Read(); + return reader.Parsers.TryGetFirstOfType(out GlobalEventParser? parser) ? parser!.Result! : new(); + } + /// + /// + /// + /// + public static async Task> ReadGlobalEventsAsync(string path, CancellationToken cancellationToken = default) + { + var reader = new ChartFileReader(path, GetGlobalEventParser); + await reader.ReadAsync(cancellationToken); + return reader.Parsers.TryGetFirstOfType(out GlobalEventParser? parser) ? parser!.Result! : new(); + } + + /// + /// Reads lyrics from a chart file. + /// + /// Enumerable of containing the lyrics from the file + /// Path of the file to read + /// + public static IEnumerable ReadLyrics(string path) => ReadGlobalEvents(path).GetLyrics(); + /// + /// Reads lyrics from a chart file asynchronously using multitasking. + /// + /// + /// Token to request cancellation + public static async Task> ReadLyricsAsync(string path, CancellationToken cancellationToken = default) => (await ReadGlobalEventsAsync(path, cancellationToken)).GetLyrics(); + #endregion + #region Sync track + /// + /// Creates a if the header matches the sync track header, otherwise . + /// + private static SyncTrackParser? GetSyncTrackParser(string header, ReadingSession session) => header == ChartFormatting.SyncTrackHeader ? new(session) : null; + + /// + /// + /// + public static SyncTrack ReadSyncTrack(string path, ReadingConfiguration? config, FormattingRules? formatting = default) + { + config ??= DefaultReadConfig; + + var reader = new ChartFileReader(path, (header) => GetSyncTrackParser(header, new(config ?? DefaultReadConfig, formatting ?? new()))); + reader.Read(); + return reader.Parsers.TryGetFirstOfType(out SyncTrackParser? syncTrackParser) ? syncTrackParser!.Result! : new(); + } + /// + /// + /// + /// + public static async Task ReadSyncTrackAsync(string path, ReadingConfiguration? config = default, CancellationToken cancellationToken = default) + { + config ??= DefaultReadConfig; + + var reader = new ChartFileReader(path, (header) => GetSyncTrackParser(header, new(config ?? DefaultReadConfig, null))); + await reader.ReadAsync(cancellationToken); + return reader.Parsers.TryGetFirstOfType(out SyncTrackParser? syncTrackParser) ? syncTrackParser!.Result! : new(); + } + #endregion + #endregion + + #region Writing + /// + /// Writes a song to a chart file. + /// + /// Path of the file to write + /// Song to write + public static void WriteSong(string path, Song song, WritingConfiguration? config = default) + { + var writer = GetSongWriter(path, song, new(config ?? DefaultWriteConfig, song.Metadata.Formatting)); + writer.Write(); + } + public static async Task WriteSongAsync(string path, Song song, WritingConfiguration? config = default, CancellationToken cancellationToken = default) + { + var writer = GetSongWriter(path, song, new(config ?? DefaultWriteConfig, song.Metadata.Formatting)); + await writer.WriteAsync(cancellationToken); + } + private static ChartFileWriter GetSongWriter(string path, Song song, WritingSession session) + { + var instruments = song.Instruments.NonNull().ToArray(); + var serializers = new List>(instruments.Length + 2); + var removedHeaders = new List(); + + serializers.Add(new MetadataSerializer(song.Metadata)); + + if (!song.SyncTrack.IsEmpty) + serializers.Add(new SyncTrackSerializer(song.SyncTrack, session)); + else + removedHeaders.Add(ChartFormatting.SyncTrackHeader); + + if (song.GlobalEvents.Count > 0) + serializers.Add(new GlobalEventSerializer(song.GlobalEvents, session)); + else + removedHeaders.Add(ChartFormatting.GlobalEventHeader); + + var difficulties = EnumCache.Values; + + // Remove headers for null instruments + removedHeaders.AddRange((from identity in Enum.GetValues() + where instruments.Any(instrument => instrument.InstrumentIdentity == identity) + let instrumentName = ChartFormatting.InstrumentHeaderNames[identity] + let headers = from diff in difficulties + select ChartFormatting.Header(identity, diff) + select headers).SelectMany(h => h)); + + foreach (var instrument in instruments) + { + var instrumentName = ChartFormatting.InstrumentHeaderNames[instrument.InstrumentIdentity]; + var tracks = instrument.GetExistingTracks().ToArray(); + + serializers.AddRange(tracks.Select(t => new TrackSerializer(t, session))); + removedHeaders.AddRange(difficulties.Where(diff => !tracks.Any(t => t.Difficulty == diff)).Select(diff => ChartFormatting.Header(instrumentName, diff))); + } + + if (song.UnknownChartSections is not null) + serializers.AddRange(song.UnknownChartSections.Select(s => new UnknownSectionSerializer(s.Header, s, session))); + + return new(path, removedHeaders, serializers.ToArray()); + } + + /// + /// Replaces an instrument in a file. + /// + /// Path of the file to write + public static void ReplaceInstrument(string path, Instrument instrument, WritingConfiguration? config = default, FormattingRules? formatting = default) + { + var writer = GetInstrumentWriter(path, instrument, new(config ?? DefaultWriteConfig, formatting ?? new())); + writer.Write(); + } + public static async Task ReplaceInstrumentAsync(string path, Instrument instrument, WritingConfiguration? config = default, FormattingRules? formatting = default, CancellationToken cancellationToken = default) + { + var writer = GetInstrumentWriter(path, instrument, new(config ?? DefaultWriteConfig, formatting ?? new())); + await writer.WriteAsync(cancellationToken); + } + private static ChartFileWriter GetInstrumentWriter(string path, Instrument instrument, WritingSession session) + { + if (!Enum.IsDefined(instrument.InstrumentIdentity)) + throw new ArgumentException("Instrument cannot be written because its identity is unknown.", nameof(instrument)); + + var instrumentName = ChartFormatting.InstrumentHeaderNames[instrument.InstrumentIdentity]; + var tracks = instrument.GetExistingTracks().ToArray(); + + return new(path, + EnumCache.Values.Where(d => !tracks.Any(t => t.Difficulty == d)).Select(d => ChartFormatting.Header(instrumentName, d)), + tracks.Select(t => new TrackSerializer(t, session)).ToArray()); + } + + public static void ReplaceTrack(string path, Track track, WritingConfiguration? config = default, FormattingRules? formatting = default) + { + var writer = GetTrackWriter(path, track, new(config ?? DefaultWriteConfig, formatting ?? new())); + writer.Write(); + } + public static async Task ReplaceTrackAsync(string path, Track track, WritingConfiguration? config = default, FormattingRules? formatting = default, CancellationToken cancellationToken = default) + { + var writer = GetTrackWriter(path, track, new(config ?? DefaultWriteConfig, formatting ?? new())); + await writer.WriteAsync(cancellationToken); + } + private static ChartFileWriter GetTrackWriter(string path, Track track, WritingSession session) + { + if (track.ParentInstrument is null) + throw new ArgumentNullException(nameof(track), "Cannot write track because it does not belong to an instrument."); + if (!Enum.IsDefined(track.ParentInstrument.InstrumentIdentity)) + throw new ArgumentException("Cannot write track because the instrument it belongs to is unknown.", nameof(track)); + + return new(path, null, new TrackSerializer(track, session)); + } + + /// + /// Replaces the metadata in a file. + /// + /// Path of the file to read + /// Metadata to write + public static void ReplaceMetadata(string path, Metadata metadata) + { + var writer = GetMetadataWriter(path, metadata); + writer.Write(); + } + private static ChartFileWriter GetMetadataWriter(string path, Metadata metadata) => new(path, null, new MetadataSerializer(metadata)); + + /// + /// Replaces the global events in a file. + /// + /// Path of the file to write + /// Events to use as a replacement + public static void ReplaceGlobalEvents(string path, IEnumerable events) + { + var writer = GetGlobalEventWriter(path, events, new(DefaultWriteConfig, null)); + writer.Write(); + } + public static async Task ReplaceGlobalEventsAsync(string path, IEnumerable events, CancellationToken cancellationToken = default) + { + var writer = GetGlobalEventWriter(path, events, new(DefaultWriteConfig, null)); + await writer.WriteAsync(cancellationToken); + } + private static ChartFileWriter GetGlobalEventWriter(string path, IEnumerable events, WritingSession session) => new(path, null, new GlobalEventSerializer(events, session)); + + /// + /// Replaces the sync track in a file. + /// + /// Path of the file to write + /// Sync track to write + /// + public static void ReplaceSyncTrack(string path, SyncTrack syncTrack, WritingConfiguration? config = default) + { + var writer = GetSyncTrackWriter(path, syncTrack, new(config ?? DefaultWriteConfig, null)); + writer.Write(); + } + /// + public static async Task ReplaceSyncTrackAsync(string path, SyncTrack syncTrack, WritingConfiguration? config = default, CancellationToken cancellationToken = default) + { + var writer = GetSyncTrackWriter(path, syncTrack, new(config ?? DefaultWriteConfig, null)); + await writer.WriteAsync(cancellationToken); + } + private static ChartFileWriter GetSyncTrackWriter(string path, SyncTrack syncTrack, WritingSession session) => new(path, null, new SyncTrackSerializer(syncTrack, session)); + #endregion + + /// + /// Gets all the combinations of instruments and difficulties. + /// + /// Enum containing the instruments + private static IEnumerable<(Difficulty difficulty, TInstEnum instrument)> GetTrackCombinations(IEnumerable instruments) => from difficulty in EnumCache.Values from instrument in instruments select (difficulty, instrument); +} diff --git a/source - Copie/IO/Chart/ChartFileReader.cs b/source - Copie/IO/Chart/ChartFileReader.cs new file mode 100644 index 00000000..d27b3e66 --- /dev/null +++ b/source - Copie/IO/Chart/ChartFileReader.cs @@ -0,0 +1,17 @@ +using ChartTools.IO.Chart.Parsing; + +namespace ChartTools.IO.Chart; + +/// +/// Reader of text file that sends read lines to subscribers of its events. +/// +internal class ChartFileReader : TextFileReader +{ + public override IEnumerable Parsers => base.Parsers.Cast(); + public override bool DefinedSectionEnd => true; + + public ChartFileReader(string path, Func parserGetter) : base(path, parserGetter) { } + + protected override bool IsSectionStart(string line) => line == "{"; + protected override bool IsSectionEnd(string line) => ChartFormatting.IsSectionEnd(line); +} diff --git a/source - Copie/IO/Chart/ChartFileWriter.cs b/source - Copie/IO/Chart/ChartFileWriter.cs new file mode 100644 index 00000000..c92a8396 --- /dev/null +++ b/source - Copie/IO/Chart/ChartFileWriter.cs @@ -0,0 +1,11 @@ +namespace ChartTools.IO.Chart; + +internal class ChartFileWriter : TextFileWriter +{ + protected override string? PreSerializerContent => "{"; + protected override string? PostSerializerContent => "}"; + + public ChartFileWriter(string path, IEnumerable? removedHeaders, params Serializer[] serializers) : base(path, removedHeaders, serializers) { } + + protected override bool EndReplace(string line) => line.StartsWith('['); +} diff --git a/source - Copie/IO/Chart/ChartFormatting.cs b/source - Copie/IO/Chart/ChartFormatting.cs new file mode 100644 index 00000000..a7dceaf2 --- /dev/null +++ b/source - Copie/IO/Chart/ChartFormatting.cs @@ -0,0 +1,78 @@ +using ChartTools.IO.Chart.Entries; + +namespace ChartTools.IO.Chart; + +internal static class ChartFormatting +{ + public const string DrumsHeaderName = "Drums", + MetadataHeader = "[Song]", + SyncTrackHeader = "[SyncTrack]", + GlobalEventHeader = "[Events]", + Title = "Name", + Artist = "Artist", + Charter = "Charter", + Album = "Album", + Year = "Year", + AudioOffset = "Offset", + Resolution = "Resolution", + Difficulty = "Difficulty", + PreviewStart = "PreviewStart", + PreviewEnd = "PreviewEnd", + Genre = "Genre", + MediaType = "MediaType", + MusicStream = "MusicStream", + GuitarStream = "GuitarStream", + BassStream = "BassStream", + RhythmStream = "RhythmStream", + KeysStream = "KeysStream", + DrumStream = "DrumStream", + Drum2Stream = "Drum2Stream", + Drum3Stream = "Drum3Stream", + Drum4Stream = "Drum4Stream", + VocalStream = "VocalStream", + CrowdStream = "CrowdStream"; + + /// + /// Part names of without the difficulty + /// + public static readonly Dictionary InstrumentHeaderNames = new() + { + { InstrumentIdentity.Drums, DrumsHeaderName }, + { InstrumentIdentity.GHLGuitar, "GHLGuitar" }, + { InstrumentIdentity.GHLBass, "GHLBass" }, + { InstrumentIdentity.LeadGuitar, "Single" }, + { InstrumentIdentity.RhythmGuitar, "DoubleRhythm" }, + { InstrumentIdentity.CoopGuitar, "DoubleGuitar" }, + { InstrumentIdentity.Bass, "DoubleBass" }, + { InstrumentIdentity.Keys, "Keyboard" } + }; + + public static string Header(Enum instrument, Difficulty difficulty) => Header((InstrumentIdentity)instrument, difficulty); + public static string Header(InstrumentIdentity instrument, Difficulty difficulty) => Header(InstrumentHeaderNames[instrument], difficulty); + public static string Header(string instrumentName, Difficulty difficulty) => Header(difficulty.ToString() + instrumentName); + public static string Header(string name) => $"[{name}]"; + + public static string Line(string header, string? value) => value is null ? string.Empty : $" {header} = {value}"; + + /// + /// Gets the written data for a note. + /// + /// Position of the parent + /// Value of + /// Value of + public static TrackObjectEntry NoteEntry(uint position, byte index, uint sustain) => new(position, "N", $"{index} {sustain}"); + + /// + /// Gets the written value of a float. + /// + /// Value to get the written equivalent of + public static string Float(float value) => ((int)(value * 1000)).ToString().Replace(".", "").Replace(",", ""); + + public static bool IsSectionEnd(string line) => line == "}"; + + /// + /// Splits the data of an entry. + /// + /// Data portion of a + internal static string[] SplitData(string data) => data.Split(' ', 2, StringSplitOptions.RemoveEmptyEntries); +} diff --git a/source - Copie/IO/Chart/ChartSectionSet.cs b/source - Copie/IO/Chart/ChartSectionSet.cs new file mode 100644 index 00000000..fc8358ed --- /dev/null +++ b/source - Copie/IO/Chart/ChartSectionSet.cs @@ -0,0 +1,37 @@ +using ChartTools.Extensions; +using ChartTools.IO.Sections; + +namespace ChartTools.IO.Chart; + +public class ChartSection : SectionSet +{ + public static readonly ReservedSectionHeaderSet DefaultReservedHeaders; + public override ReservedSectionHeaderSet ReservedHeaders => DefaultReservedHeaders; + + static ChartSection() + { + var headers = new List + { + new(ChartFormatting.MetadataHeader, nameof(Song.Metadata)), + new(ChartFormatting.SyncTrackHeader, nameof(Song.SyncTrack)), + new(ChartFormatting.GlobalEventHeader, nameof(Song.GlobalEvents)) + }; + + var instrumentSources = new Dictionary() + { + { ChartFormatting.InstrumentHeaderNames[InstrumentIdentity.LeadGuitar], nameof(Song.Instruments.LeadGuitar) }, + { ChartFormatting.InstrumentHeaderNames[InstrumentIdentity.RhythmGuitar], nameof(Song.Instruments.RhythmGuitar) }, + { ChartFormatting.InstrumentHeaderNames[InstrumentIdentity.CoopGuitar], nameof(Song.Instruments.CoopGuitar) }, + { ChartFormatting.InstrumentHeaderNames[InstrumentIdentity.Bass], nameof(Song.Instruments.Bass) }, + { ChartFormatting.InstrumentHeaderNames[InstrumentIdentity.Keys], nameof(Song.Instruments.Keys) }, + { ChartFormatting.InstrumentHeaderNames[InstrumentIdentity.GHLGuitar], nameof(Song.Instruments.GHLGuitar) }, + { ChartFormatting.InstrumentHeaderNames[InstrumentIdentity.GHLBass], nameof(Song.Instruments.GHLBass) }, + { ChartFormatting.InstrumentHeaderNames[InstrumentIdentity.Drums], nameof(Song.Instruments.Drums) } + }; + + headers.AddRange(instrumentSources.SelectMany(pair => from diff in EnumCache.Values select new ReservedSectionHeader(ChartFormatting.Header(pair.Value, diff), $"{pair.Value}.{diff}"))); + + DefaultReservedHeaders = new(headers); + } + public ChartSection() : base() { } +} diff --git a/source - Copie/IO/Chart/Entries/NoteData.cs b/source - Copie/IO/Chart/Entries/NoteData.cs new file mode 100644 index 00000000..2d2d0c72 --- /dev/null +++ b/source - Copie/IO/Chart/Entries/NoteData.cs @@ -0,0 +1,32 @@ +namespace ChartTools.IO.Chart.Entries; + +/// +/// Line of chart data representing a +/// +internal readonly ref struct NoteData +{ + /// + /// Value of + /// + internal byte Index { get; } + /// + /// Value of + /// + internal uint SustainLength { get; } + + /// + /// Creates an instance of . + /// + /// Data section of the line in the file + /// + internal NoteData(string data) + { + string[] split = data.Split(' ', 2, StringSplitOptions.RemoveEmptyEntries); + + if (split.Length < 2) + throw new EntryException(); + + Index = ValueParser.ParseByte(split[0], "note index"); + SustainLength = ValueParser.ParseUint(split[1], "sustain length"); + } +} diff --git a/source - Copie/IO/Chart/Entries/TrackObjectEntry.cs b/source - Copie/IO/Chart/Entries/TrackObjectEntry.cs new file mode 100644 index 00000000..3559f700 --- /dev/null +++ b/source - Copie/IO/Chart/Entries/TrackObjectEntry.cs @@ -0,0 +1,51 @@ +namespace ChartTools.IO.Chart.Entries; + +/// +/// Line of chart file data representing a +/// +internal readonly struct TrackObjectEntry : IReadOnlyTrackObject +{ + /// + /// Value of + /// + public uint Position { get; } + /// + /// Type code of + /// + public string Type { get; } + /// + /// Additional data + /// + public string Data { get; } + + /// + /// Creates an instance of see. + /// + /// Line in the file + /// + public TrackObjectEntry(string line) + { + TextEntry entry = new(line); + + if (entry.Value is null) + throw new LineException(line, new FormatException("Line has no object data.")); + + string[] split = entry.Value.Split(' ', 2, StringSplitOptions.RemoveEmptyEntries); + + if (split.Length < 2) + throw new LineException(line, new EntryException()); + + Type = split[0]; + Data = split[1]; + + Position = ValueParser.ParseUint(entry.Key, "position"); + } + public TrackObjectEntry(uint position, string type, string data) + { + Position = position; + Type = type; + Data = data; + } + + public override string ToString() => ChartFormatting.Line(Position.ToString(), $"{Type} {Data}"); +} diff --git a/source - Copie/IO/Chart/Parsing/ChartParser.cs b/source - Copie/IO/Chart/Parsing/ChartParser.cs new file mode 100644 index 00000000..a5d73d7b --- /dev/null +++ b/source - Copie/IO/Chart/Parsing/ChartParser.cs @@ -0,0 +1,11 @@ +using ChartTools.IO.Configuration.Sessions; +using ChartTools.IO.Parsing; + +namespace ChartTools.IO.Chart.Parsing; + +internal abstract class ChartParser : TextParser, ISongAppliable +{ + protected ChartParser(ReadingSession session, string header) : base(session, header) { } + + public abstract void ApplyToSong(Song song); +} diff --git a/source - Copie/IO/Chart/Parsing/DrumsTrackParser.cs b/source - Copie/IO/Chart/Parsing/DrumsTrackParser.cs new file mode 100644 index 00000000..86651e44 --- /dev/null +++ b/source - Copie/IO/Chart/Parsing/DrumsTrackParser.cs @@ -0,0 +1,50 @@ +using ChartTools.Extensions.Linq; +using ChartTools.IO.Chart.Entries; +using ChartTools.IO.Configuration.Sessions; + +namespace ChartTools.IO.Chart.Parsing; + +internal class DrumsTrackParser : TrackParser +{ + public DrumsTrackParser(Difficulty difficulty, ReadingSession session, string header) : base(difficulty, session, header) { } + + public override void ApplyToSong(Song song) + { + song.Instruments.Drums ??= new(); + ApplyToInstrument(song.Instruments.Drums); + } + + protected override void HandleNoteEntry(DrumsChord chord, NoteData data) + { + switch (data.Index) + { + // Note + case < 5: + AddNote(new DrumsNote((DrumsLane)data.Index) { Sustain = data.SustainLength }); + break; + // Double kick + case 32: + AddNote(new DrumsNote(DrumsLane.DoubleKick)); + break; + // Cymbal + case > 65 and < 69: + // NoteIndex of the note to set as cymbal + byte seekedIndex = (byte)(data.Index - 64); + + if (chord.Notes.TryGetFirst(n => n.Index == seekedIndex, out DrumsNote note)) + { + if (session.DuplicateTrackObjectProcedure(chord.Position, "drums note cymbal marker", () => note.IsCymbal)) + note.IsCymbal = true; + } + else + AddNote(new DrumsNote((DrumsLane)seekedIndex) { IsCymbal = true, Sustain = data.SustainLength }); + break; + case 109: + AddModifier(DrumsChordModifiers.Flam); + break; + } + + void AddNote(DrumsNote note) => HandleAddNote(note, () => chord.Notes.Add(note)); + void AddModifier(DrumsChordModifiers modifier) => HandleAddModifier(chord.Modifiers, modifier, () => chord.Modifiers |= modifier); + } +} diff --git a/source - Copie/IO/Chart/Parsing/GHLTrackParser.cs b/source - Copie/IO/Chart/Parsing/GHLTrackParser.cs new file mode 100644 index 00000000..b2a1244d --- /dev/null +++ b/source - Copie/IO/Chart/Parsing/GHLTrackParser.cs @@ -0,0 +1,49 @@ +using ChartTools.IO.Chart.Entries; +using ChartTools.IO.Configuration.Sessions; + +namespace ChartTools.IO.Chart.Parsing; + +internal class GHLTrackParser : VariableInstrumentTrackParser +{ + public GHLTrackParser(Difficulty difficulty, GHLInstrumentIdentity instrument, ReadingSession session, string header) : base(difficulty, instrument, session, header) { } + + public override void ApplyToSong(Song song) + { + var inst = song.Instruments.Get(Instrument); + + if (inst is null) + song.Instruments.Set(inst = new(Instrument)); + + ApplyToInstrument(inst); + } + + protected override void HandleNoteEntry(GHLChord chord, NoteData data) + { + switch (data.Index) + { + // White notes + case < 3: + AddNote(new LaneNote((GHLLane)(data.Index + 4)) { Sustain = data.SustainLength }); + break; + // Black 1 and 2 + case < 5: + AddNote(new LaneNote((GHLLane)(data.Index - 2)) { Sustain = data.SustainLength }); + break; + case 5: + AddModifier(GHLChordModifiers.HopoInvert); + return; + case 6: + AddModifier(GHLChordModifiers.Tap); + return; + case 7: + AddNote(new LaneNote(GHLLane.Open) { Sustain = data.SustainLength }); + break; + case 8: + AddNote(new LaneNote(GHLLane.Black3) { Sustain = data.SustainLength }); + break; + } + + void AddNote(LaneNote note) => HandleAddNote(note, () => chord.Notes.Add(note)); + void AddModifier(GHLChordModifiers modifier) => HandleAddModifier(chord.Modifiers, modifier, () => chord.Modifiers |= modifier); + } +} diff --git a/source - Copie/IO/Chart/Parsing/GlobalEventParser.cs b/source - Copie/IO/Chart/Parsing/GlobalEventParser.cs new file mode 100644 index 00000000..15e8cddf --- /dev/null +++ b/source - Copie/IO/Chart/Parsing/GlobalEventParser.cs @@ -0,0 +1,21 @@ +using ChartTools.Events; +using ChartTools.IO.Chart.Entries; +using ChartTools.IO.Configuration.Sessions; + +namespace ChartTools.IO.Chart.Parsing; + +internal class GlobalEventParser : ChartParser +{ + public override List Result => GetResult(result); + private readonly List result = new(); + + public GlobalEventParser(ReadingSession session) : base(session, ChartFormatting.GlobalEventHeader) { } + + protected override void HandleItem(string line) + { + TrackObjectEntry entry = new(line); + result.Add(new(entry.Position, entry.Data.Trim('"'))); + } + + public override void ApplyToSong(Song song) => song.GlobalEvents = Result; +} diff --git a/source - Copie/IO/Chart/Parsing/MetadataParser.cs b/source - Copie/IO/Chart/Parsing/MetadataParser.cs new file mode 100644 index 00000000..f42c7883 --- /dev/null +++ b/source - Copie/IO/Chart/Parsing/MetadataParser.cs @@ -0,0 +1,90 @@ +namespace ChartTools.IO.Chart.Parsing; + +internal class MetadataParser : ChartParser +{ + public override Metadata Result => GetResult(result); + private readonly Metadata result = new(); + + public MetadataParser(Metadata? existing = null) : base(null!, ChartFormatting.MetadataHeader) => result = existing ?? new(); + + protected override void HandleItem(string line) + { + TextEntry entry = new(line); + var value = entry.Value?.Trim('"'); + + switch (entry.Key) + { + case ChartFormatting.Title: + result.Title = value; + break; + case ChartFormatting.Artist: + result.Artist = value; + break; + case ChartFormatting.Charter: + result.Charter.Name = value; + break; + case ChartFormatting.Album: + result.Album = value; + break; + case ChartFormatting.Year: + result.Year = ValueParser.ParseUshort(value?.TrimStart(','), "year"); + break; + case ChartFormatting.AudioOffset: + result.AudioOffset = TimeSpan.FromMilliseconds(ValueParser.ParseFloat(value, "audio offset") * 1000); + break; + case ChartFormatting.Difficulty: + result.Difficulty = ValueParser.ParseSbyte(value, "difficulty"); + break; + case ChartFormatting.PreviewStart: + result.PreviewStart = ValueParser.ParseUint(value, "preview start"); + break; + case ChartFormatting.PreviewEnd: + result.PreviewEnd = ValueParser.ParseUint(value, "preview end"); + break; + case ChartFormatting.Genre: + result.Genre = value; + break; + case ChartFormatting.MediaType: + result.MediaType = value; + break; + case ChartFormatting.MusicStream: + result.Streams.Music = value; + break; + case ChartFormatting.GuitarStream: + result.Streams.Guitar = value; + break; + case ChartFormatting.BassStream: + result.Streams.Bass = value; + break; + case ChartFormatting.RhythmStream: + result.Streams.Rhythm = value; + break; + case ChartFormatting.KeysStream: + result.Streams.Keys = value; + break; + case ChartFormatting.DrumStream: + result.Streams.Drum = value; + break; + case ChartFormatting.Drum2Stream: + result.Streams.Drum2 = value; + break; + case ChartFormatting.Drum3Stream: + result.Streams.Drum3 = value; + break; + case ChartFormatting.Drum4Stream: + result.Streams.Drum4 = value; + break; + case ChartFormatting.VocalStream: + result.Streams.Vocals = value; + break; + case ChartFormatting.CrowdStream: + result.Streams.Crowd = value; + break; + default: + result.UnidentifiedData.Add(new() { Key = entry.Key, Value = entry.Value, Origin = FileType.Chart }); + break; + } + } + + public override void ApplyToSong(Song song) => song.Metadata = Result; +} diff --git a/source - Copie/IO/Chart/Parsing/StandardTrackParser.cs b/source - Copie/IO/Chart/Parsing/StandardTrackParser.cs new file mode 100644 index 00000000..dde1e28f --- /dev/null +++ b/source - Copie/IO/Chart/Parsing/StandardTrackParser.cs @@ -0,0 +1,42 @@ +using ChartTools.IO.Chart.Entries; +using ChartTools.IO.Configuration.Sessions; + +namespace ChartTools.IO.Chart.Parsing; + +internal class StandardTrackParser : VariableInstrumentTrackParser +{ + public StandardTrackParser(Difficulty difficulty, StandardInstrumentIdentity instrument, ReadingSession session, string header) : base(difficulty, instrument, session, header) { } + + public override void ApplyToSong(Song song) + { + var inst = song.Instruments.Get(Instrument); + + if (inst is null) + song.Instruments.Set(inst = new(Instrument)); + + ApplyToInstrument(inst); + } + + protected override void HandleNoteEntry(StandardChord chord, NoteData data) + { + switch (data.Index) + { + // Colored note + case < 5: + AddNote(new((StandardLane)(data.Index + 1)) { Sustain = data.SustainLength }); + break; + case 5: + AddModifier(StandardChordModifiers.HopoInvert); + return; + case 6: + AddModifier(StandardChordModifiers.Tap); + return; + case 7: + AddNote(new(StandardLane.Open) { Sustain = data.SustainLength }); + break; + } + + void AddNote(LaneNote note) => HandleAddNote(note, () => chord.Notes.Add(note)); + void AddModifier(StandardChordModifiers modifier) => HandleAddModifier(chord.Modifiers, modifier, () => chord.Modifiers |= modifier); + } +} diff --git a/source - Copie/IO/Chart/Parsing/SyncTrackParser.cs b/source - Copie/IO/Chart/Parsing/SyncTrackParser.cs new file mode 100644 index 00000000..586b632a --- /dev/null +++ b/source - Copie/IO/Chart/Parsing/SyncTrackParser.cs @@ -0,0 +1,104 @@ +using ChartTools.Extensions.Linq; +using ChartTools.IO.Chart.Entries; +using ChartTools.IO.Configuration.Sessions; + +namespace ChartTools.IO.Chart.Parsing; + +internal class SyncTrackParser : ChartParser +{ + public override SyncTrack Result => GetResult(result); + private readonly SyncTrack result = new(); + + private readonly List tempos = new(), orderedTempos = new(); + private readonly List orderedAnchors = new(); + private readonly List orderedSignatures = new(); + + public SyncTrackParser(ReadingSession session) : base(session, ChartFormatting.SyncTrackHeader) { } + + protected override void HandleItem(string line) + { + TrackObjectEntry entry = new(line); + + switch (entry.Type) + { + case "TS": // Time signature + if (CheckDuplicate(orderedSignatures, "time signature", out int newIndex)) + break; + + string[] split = ChartFormatting.SplitData(entry.Data); + + var numerator = ValueParser.ParseByte(split[0], "numerator"); + byte denominator = 4; + + // Denominator is only written if not equal to 4 + if (split.Length >= 2) + denominator = (byte)Math.Pow(2, ValueParser.ParseByte(split[1], "denominator")); + + var signature = new TimeSignature(entry.Position, numerator, denominator); + + result.TimeSignatures.Add(signature); + orderedSignatures.Insert(newIndex, signature); + break; + case "B": // Tempo + if (CheckDuplicate(orderedTempos, "tempo marker", out newIndex)) + break; + + // Floats are written by rounding to the 3rd decimal and removing the decimal point + var value = ValueParser.ParseFloat(entry.Data, "value") / 1000; + var tempo = new Tempo(entry.Position, value); + + tempos.Add(tempo); + orderedTempos.Add(tempo); + break; + case "A": // Anchor + if (CheckDuplicate(orderedAnchors, "tempo anchor", out newIndex)) + break; + + // Floats are written by rounding to the 3rd decimal and removing the decimal point + var anchor = TimeSpan.FromSeconds(ValueParser.ParseFloat(entry.Data, "anchor") / 1000); + + orderedAnchors.Insert(newIndex, new(entry.Position, anchor)); + break; + } + + bool CheckDuplicate(IList existing, string objectType, out int newIndex) where T : IReadOnlyTrackObject + { + var index = 0; + var result = !session.DuplicateTrackObjectProcedure(entry.Position, objectType, () => + { + index = existing.BinarySearchIndex(entry.Position, t => t.Position, out bool exactMatch); + + return exactMatch; + }); + + newIndex = index; + + return result; + } + } + + protected override void FinaliseParse() + { + foreach (var anchor in orderedAnchors) + { + // Find the marker matching the position in case it was already added through a mention of value + var markerIndex = orderedTempos.BinarySearchIndex(anchor.Position, t => t.Position, out bool markerFound); + + if (markerFound) + { + orderedTempos[markerIndex].Anchor = anchor.Value; + orderedTempos.RemoveAt(markerIndex); + } + else if (session.TempolessAnchorProcedure(anchor)) + result.Tempo.Add(new(anchor.Position, 0) { Anchor = anchor.Value }); + } + + base.FinaliseParse(); + } + + public override void ApplyToSong(Song song) + { + song.SyncTrack = Result; + song.SyncTrack.Tempo.AddRange(tempos); + } +} diff --git a/source - Copie/IO/Chart/Parsing/TrackParser.cs b/source - Copie/IO/Chart/Parsing/TrackParser.cs new file mode 100644 index 00000000..501eba98 --- /dev/null +++ b/source - Copie/IO/Chart/Parsing/TrackParser.cs @@ -0,0 +1,114 @@ +using ChartTools.IO.Chart.Entries; +using ChartTools.IO.Configuration; +using ChartTools.IO.Configuration.Sessions; +using ChartTools.Extensions.Linq; +using ChartTools.Tools; + +namespace ChartTools.IO.Chart.Parsing; + +internal abstract class TrackParser : ChartParser, IInstrumentAppliable where TChord : IChord, new() +{ + public Difficulty Difficulty { get; } + + public override Track Result => GetResult(result); + private readonly Track result; + + private TChord? chord; + private bool newChord = true; + private readonly List orderedChords = new(); + + public TrackParser(Difficulty difficulty, ReadingSession session, string header) : base(session, header) + { + Difficulty = difficulty; + result = new() { Difficulty = difficulty }; + } + + protected override void HandleItem(string line) + { + TrackObjectEntry entry = new(line); + + switch (entry.Type) + { + // Local event + case "E": + result.LocalEvents.Add(new(entry.Position, entry.Data)); + break; + // Note or chord modifier + case "N": + var newIndex = 0; + + // Find the parent chord or create it + if (chord is null) + { + chord = new() { Position = entry.Position }; + newIndex = orderedChords.Count; + } + else if (entry.Position == chord.Position) + newChord = false; + else + { + newIndex = orderedChords.BinarySearchIndex(entry.Position, c => c.Position, out bool exactMatch); + + if (newChord = !exactMatch) + chord = new() { Position = entry.Position }; + } + + HandleNoteEntry(chord!, new(entry.Data)); + + if (newChord) + { + result.Chords.Add(chord!); + orderedChords.Insert(newIndex, chord!); + } + + break; + // Star power + case "S": + var split = ChartFormatting.SplitData(entry.Data); + + var typeCode = ValueParser.ParseByte(split[0], "type code"); + var length = ValueParser.ParseUint(split[1], "length"); + + result.SpecialPhrases.Add(new(entry.Position, typeCode, length)); + break; + } + + if (session!.Configuration.SoloNoStarPowerPolicy == SoloNoStarPowerPolicy.Convert) + result.SpecialPhrases.AddRange(result.SoloToStarPower(true)); + } + + protected abstract void HandleNoteEntry(TChord chord, NoteData data); + protected void HandleAddNote(INote note, Action add) + { + if (session.DuplicateTrackObjectProcedure(chord!.Position, "note", () => chord!.Notes.Any(n => n.Index == note.Index))) + add(); + } + protected void HandleAddModifier(Enum existingModifier, Enum modifier, Action add) + { + if (session.DuplicateTrackObjectProcedure(chord!.Position, "chord modifier", () => existingModifier.HasFlag(modifier))) + add(); + } + + protected override void FinaliseParse() + { + ApplyOverlappingSpecialPhrasePolicy(result.SpecialPhrases, session!.Configuration.OverlappingStarPowerPolicy); + base.FinaliseParse(); + } + + public void ApplyToInstrument(Instrument instrument) => instrument.SetTrack(Result); + + private static void ApplyOverlappingSpecialPhrasePolicy(IEnumerable specialPhrases, OverlappingSpecialPhrasePolicy policy) + { + switch (policy) + { + case OverlappingSpecialPhrasePolicy.Cut: + specialPhrases.CutLengths(); + break; + case OverlappingSpecialPhrasePolicy.ThrowException: + foreach ((var previous, var current) in specialPhrases.RelativeLoopSkipFirst()) + if (Optimizer.LengthNeedsCut(previous, current)) + throw new Exception($"Overlapping star power phrases at position {current!.Position}. Consider using {nameof(OverlappingSpecialPhrasePolicy.Cut)} to avoid this error."); + break; + } + } +} diff --git a/source - Copie/IO/Chart/Parsing/UnknownSectionParser.cs b/source - Copie/IO/Chart/Parsing/UnknownSectionParser.cs new file mode 100644 index 00000000..69d75eb1 --- /dev/null +++ b/source - Copie/IO/Chart/Parsing/UnknownSectionParser.cs @@ -0,0 +1,14 @@ +using ChartTools.IO.Configuration.Sessions; +using ChartTools.IO.Sections; + +namespace ChartTools.IO.Chart.Parsing; + +internal class UnknownSectionParser : ChartParser +{ + public override Section Result => GetResult(result); + private readonly Section result; + public UnknownSectionParser(ReadingSession session, string header) : base(session, header) => result = new(header); + + public override void ApplyToSong(Song song) => (song.UnknownChartSections ??= new()).Add(Result); + protected override void HandleItem(string item) => result.Add(item); +} diff --git a/source - Copie/IO/Chart/Parsing/VariableInstrumentTrackParser.cs b/source - Copie/IO/Chart/Parsing/VariableInstrumentTrackParser.cs new file mode 100644 index 00000000..b3f81d16 --- /dev/null +++ b/source - Copie/IO/Chart/Parsing/VariableInstrumentTrackParser.cs @@ -0,0 +1,9 @@ +using ChartTools.IO.Configuration.Sessions; + +namespace ChartTools.IO.Chart.Parsing; + +internal abstract class VariableInstrumentTrackParser : TrackParser where TChord : IChord, new() where TInstEnum : Enum +{ + public TInstEnum Instrument { get; } + public VariableInstrumentTrackParser(Difficulty difficulty, TInstEnum instrument, ReadingSession session, string header) : base(difficulty, session, header) => Instrument = instrument; +} diff --git a/source - Copie/IO/Chart/Providers/ChordProvider.cs b/source - Copie/IO/Chart/Providers/ChordProvider.cs new file mode 100644 index 00000000..c93ccd96 --- /dev/null +++ b/source - Copie/IO/Chart/Providers/ChordProvider.cs @@ -0,0 +1,31 @@ +using ChartTools.IO.Chart.Entries; +using ChartTools.IO.Configuration.Sessions; +using ChartTools.Extensions.Linq; + +namespace ChartTools.IO.Chart.Providers; + +internal class ChordProvider: ISerializerDataProvider +{ + public IEnumerable ProvideFor(IEnumerable source, WritingSession session) + { + List orderedPositions = new(); + LaneChord? previousChord = null; + + foreach (var chord in source) + { + if (session.DuplicateTrackObjectProcedure(chord.Position, "chord", () => + { + var index = orderedPositions.BinarySearchIndex(chord.Position, out bool exactMatch); + + if (!exactMatch) + orderedPositions.Insert(index, chord.Position); + + return exactMatch; + })) + foreach (var entry in (chord.ChartSupportedModifiers ? chord.GetChartModifierData(previousChord, session) : session.GetChordEntries(previousChord, chord)).Concat(chord.GetChartNoteData())) + yield return entry; + + previousChord = chord; + } + } +} diff --git a/source - Copie/IO/Chart/Providers/EventProvider.cs b/source - Copie/IO/Chart/Providers/EventProvider.cs new file mode 100644 index 00000000..84bfe2cf --- /dev/null +++ b/source - Copie/IO/Chart/Providers/EventProvider.cs @@ -0,0 +1,10 @@ +using ChartTools.Events; +using ChartTools.IO.Chart.Entries; +using ChartTools.IO.Configuration.Sessions; + +namespace ChartTools.IO.Chart.Providers; + +internal class EventProvider : ISerializerDataProvider +{ + public IEnumerable ProvideFor(IEnumerable source, WritingSession session) => source.Select(e => new TrackObjectEntry(e.Position, "E", $"\"{e.EventData}\"")); +} diff --git a/source - Copie/IO/Chart/Providers/SpecialPhraseProvider.cs b/source - Copie/IO/Chart/Providers/SpecialPhraseProvider.cs new file mode 100644 index 00000000..d90ed4be --- /dev/null +++ b/source - Copie/IO/Chart/Providers/SpecialPhraseProvider.cs @@ -0,0 +1,9 @@ +using ChartTools.IO.Chart.Entries; +using ChartTools.IO.Configuration.Sessions; + +namespace ChartTools.IO.Chart.Providers; + +internal class SpeicalPhraseProvider : ISerializerDataProvider +{ + public IEnumerable ProvideFor(IEnumerable source, WritingSession session) => source.Select(sp => new TrackObjectEntry(sp.Position, "S", $"{sp.TypeCode} {sp.Length}")); +} diff --git a/source - Copie/IO/Chart/Providers/SyncTrackProvider.cs b/source - Copie/IO/Chart/Providers/SyncTrackProvider.cs new file mode 100644 index 00000000..c4085448 --- /dev/null +++ b/source - Copie/IO/Chart/Providers/SyncTrackProvider.cs @@ -0,0 +1,34 @@ +using ChartTools.IO.Chart.Entries; +using ChartTools.IO.Configuration.Sessions; +using ChartTools.Extensions.Linq; + +namespace ChartTools.IO.Chart.Providers; + +internal abstract class SyncTrackProvider : ISerializerDataProvider where T : TrackObjectBase +{ + protected abstract string ObjectType { get; } + + public IEnumerable ProvideFor(IEnumerable source, WritingSession session) + { + List orderedPositions = new(); + + foreach (var item in source) + { + if (session.DuplicateTrackObjectProcedure(item.Position, ObjectType, () => + { + var index = orderedPositions.BinarySearchIndex(item.Position, out bool exactMatch); + + if (!exactMatch) + orderedPositions.Insert(index, item.Position); + + return exactMatch; + })) + foreach (var entry in GetEntries(item)) + yield return entry; + + orderedPositions.Add(item.Position); + } + } + + protected abstract IEnumerable GetEntries(T item); +} diff --git a/source - Copie/IO/Chart/Providers/TempoProvider.cs b/source - Copie/IO/Chart/Providers/TempoProvider.cs new file mode 100644 index 00000000..45599de3 --- /dev/null +++ b/source - Copie/IO/Chart/Providers/TempoProvider.cs @@ -0,0 +1,17 @@ +using ChartTools.IO.Chart.Entries; + +namespace ChartTools.IO.Chart.Providers; + +internal class TempoProvider : SyncTrackProvider +{ + protected override string ObjectType => "tempo marker"; + + protected override IEnumerable GetEntries(Tempo item) + { + if (item.Anchor is not null) + yield return item.PositionSynced + ? new(item.Position, "A", ChartFormatting.Float((float)item.Anchor.Value.TotalSeconds)) + : throw new DesynchronizedAnchorException(item.Anchor.Value, $"Cannot write desynchronized anchored tempo at {item.Anchor}."); + yield return new(item.Position, "B", ChartFormatting.Float(item.Value)); + } +} diff --git a/source - Copie/IO/Chart/Providers/TimeSignatureProvider.cs b/source - Copie/IO/Chart/Providers/TimeSignatureProvider.cs new file mode 100644 index 00000000..047fb200 --- /dev/null +++ b/source - Copie/IO/Chart/Providers/TimeSignatureProvider.cs @@ -0,0 +1,19 @@ +using ChartTools.IO.Chart.Entries; + +namespace ChartTools.IO.Chart.Providers; + +internal class TimeSignatureProvider : SyncTrackProvider +{ + protected override string ObjectType => "time signature"; + + protected override IEnumerable GetEntries(TimeSignature item) + { + byte writtenDenominator = (byte)Math.Log2(item.Denominator); + string data = item.Numerator.ToString(); + + if (writtenDenominator == 1) + data += ' ' + writtenDenominator.ToString(); + + yield return new(item.Position, "TS", data); + } +} diff --git a/source - Copie/IO/Chart/Serializing/ChartKeySerializableAttribute.cs b/source - Copie/IO/Chart/Serializing/ChartKeySerializableAttribute.cs new file mode 100644 index 00000000..41382f6f --- /dev/null +++ b/source - Copie/IO/Chart/Serializing/ChartKeySerializableAttribute.cs @@ -0,0 +1,16 @@ +namespace ChartTools.IO.Chart.Serializing; + +public class ChartKeySerializableAttribute : KeySerializableAttribute +{ + public override FileType Format => FileType.Chart; + + public ChartKeySerializableAttribute(string key) : base(key) { } + + protected override string GetValueString(object propValue) + { + var propString = propValue.ToString()!; + return propValue is string ? $"\"{propString}\"" : propString; + } + + public static IEnumerable<(string key, string value)> GetSerializable(object source) => GetSerializable(source); +} diff --git a/source - Copie/IO/Chart/Serializing/GlobalEventSerializer.cs b/source - Copie/IO/Chart/Serializing/GlobalEventSerializer.cs new file mode 100644 index 00000000..8fb46f54 --- /dev/null +++ b/source - Copie/IO/Chart/Serializing/GlobalEventSerializer.cs @@ -0,0 +1,13 @@ +using ChartTools.Events; +using ChartTools.IO.Chart.Entries; +using ChartTools.IO.Chart.Providers; +using ChartTools.IO.Configuration.Sessions; + +namespace ChartTools.IO.Chart.Serializing; + +internal class GlobalEventSerializer : TrackObjectGroupSerializer> +{ + public GlobalEventSerializer(IEnumerable content, WritingSession session) : base(ChartFormatting.GlobalEventHeader, content, session) { } + + protected override IEnumerable[] LaunchProviders() => new IEnumerable[] { new EventProvider().ProvideFor(Content, session!) }; +} diff --git a/source - Copie/IO/Chart/Serializing/MetadataSerializer.cs b/source - Copie/IO/Chart/Serializing/MetadataSerializer.cs new file mode 100644 index 00000000..47b15cd5 --- /dev/null +++ b/source - Copie/IO/Chart/Serializing/MetadataSerializer.cs @@ -0,0 +1,27 @@ +namespace ChartTools.IO.Chart.Serializing; + +internal class MetadataSerializer : Serializer +{ + public MetadataSerializer(Metadata content) : base(ChartFormatting.MetadataHeader, content, new(ChartFile.DefaultWriteConfig, content.Formatting)) { } + + public override IEnumerable Serialize() + { + if (Content is null) + yield break; + + var props = ChartKeySerializableAttribute.GetSerializable(Content) + .Concat(ChartKeySerializableAttribute.GetSerializable(Content.Formatting)) + .Concat(ChartKeySerializableAttribute.GetSerializable(Content.Charter) + .Concat(ChartKeySerializableAttribute.GetSerializable(Content.InstrumentDifficulties)) + .Concat(ChartKeySerializableAttribute.GetSerializable(Content.Streams))); + + foreach ((var key, var value) in props) + yield return ChartFormatting.Line(key, value); + + if (Content.Year is not null) + yield return ChartFormatting.Line("Year", $"\", {Content.Year}\""); + + foreach (var data in Content.UnidentifiedData.Where(d => d.Origin == FileType.Chart)) + yield return ChartFormatting.Line(data.Key, data.Value); + } +} diff --git a/source - Copie/IO/Chart/Serializing/SyncTrackSerializer.cs b/source - Copie/IO/Chart/Serializing/SyncTrackSerializer.cs new file mode 100644 index 00000000..d644afc9 --- /dev/null +++ b/source - Copie/IO/Chart/Serializing/SyncTrackSerializer.cs @@ -0,0 +1,12 @@ +using ChartTools.IO.Chart.Entries; +using ChartTools.IO.Chart.Providers; +using ChartTools.IO.Configuration.Sessions; + +namespace ChartTools.IO.Chart.Serializing; + +internal class SyncTrackSerializer : TrackObjectGroupSerializer +{ + public SyncTrackSerializer(SyncTrack content, WritingSession session) : base(ChartFormatting.SyncTrackHeader, content, session) { } + + protected override IEnumerable[] LaunchProviders() => new IEnumerable[] { new TempoProvider().ProvideFor(Content.Tempo, session), new TimeSignatureProvider().ProvideFor(Content.TimeSignatures, session) }; +} diff --git a/source/IO/Chart/Serializing/TrackObjectGroupSerializer.cs b/source - Copie/IO/Chart/Serializing/TrackObjectGroupSerializer.cs similarity index 100% rename from source/IO/Chart/Serializing/TrackObjectGroupSerializer.cs rename to source - Copie/IO/Chart/Serializing/TrackObjectGroupSerializer.cs diff --git a/source/IO/Chart/Serializing/TrackSerializer.cs b/source - Copie/IO/Chart/Serializing/TrackSerializer.cs similarity index 100% rename from source/IO/Chart/Serializing/TrackSerializer.cs rename to source - Copie/IO/Chart/Serializing/TrackSerializer.cs diff --git a/source - Copie/IO/Chart/Serializing/UnknownSectionSerializer.cs b/source - Copie/IO/Chart/Serializing/UnknownSectionSerializer.cs new file mode 100644 index 00000000..582fcf7a --- /dev/null +++ b/source - Copie/IO/Chart/Serializing/UnknownSectionSerializer.cs @@ -0,0 +1,11 @@ +using ChartTools.IO.Configuration.Sessions; +using ChartTools.IO.Sections; + +namespace ChartTools.IO.Chart.Serializing; + +internal class UnknownSectionSerializer : Serializer, string> +{ + public UnknownSectionSerializer(string header, Section content, WritingSession session) : base(header, content, session) { } + + public override IEnumerable Serialize() => Content; +} diff --git a/source - Copie/IO/Configuration/CommonConfiguration.cs b/source - Copie/IO/Configuration/CommonConfiguration.cs new file mode 100644 index 00000000..d1239f83 --- /dev/null +++ b/source - Copie/IO/Configuration/CommonConfiguration.cs @@ -0,0 +1,17 @@ +namespace ChartTools.IO.Configuration; + +/// +/// Configuration object to direct the reading or writing of a file +/// +/// If , the default configuration for the file format will be used. +public record CommonConfiguration +{ + /// + public DuplicateTrackObjectPolicy DuplicateTrackObjectPolicy { get; init; } + /// + public OverlappingSpecialPhrasePolicy OverlappingStarPowerPolicy { get; init; } + /// + public SnappedNotesPolicy SnappedNotesPolicy { get; init; } + /// + public SoloNoStarPowerPolicy SoloNoStarPowerPolicy { get; init; } +} diff --git a/source - Copie/IO/Configuration/ConfigurationExceptions.cs b/source - Copie/IO/Configuration/ConfigurationExceptions.cs new file mode 100644 index 00000000..261e2ffb --- /dev/null +++ b/source - Copie/IO/Configuration/ConfigurationExceptions.cs @@ -0,0 +1,6 @@ +namespace ChartTools.IO.Configuration; + +internal static class ConfigurationExceptions +{ + public static ArgumentException UnsupportedPolicy(Enum policy) => new("Policy is not supported.", $"{policy}"); +} diff --git a/source - Copie/IO/Configuration/Enums.cs b/source - Copie/IO/Configuration/Enums.cs new file mode 100644 index 00000000..80a9ed80 --- /dev/null +++ b/source - Copie/IO/Configuration/Enums.cs @@ -0,0 +1,242 @@ +using ChartTools.Events; +using Melanchall.DryWetMidi.Core; + +namespace ChartTools.IO.Configuration; + +/// +/// Defines how duplicate track objects are handled. +/// +public enum DuplicateTrackObjectPolicy : byte +{ + /// + /// Throw an exception with the position. + /// + ThrowException, + /// + /// Include only the first object + /// + IncludeFirst, + /// + /// Include all objects. + /// + IncludeAll, +} +/// +/// Define where lyrics are obtained when writing a format that defines lyrics as events. +/// +public enum LyricEventSource : byte +{ + /// + /// Obtain lyrics from . + /// + GlobalEvents, + /// + /// Obtain lyrics from the instrument. + /// + Vocals +} +public enum MisalignedBigRockMarkersPolicy : byte +{ + ThrowException, + IgnoreAll, + IncludeFirst, + Combine +} +public enum MissingBigRockMarkerPolicy : byte +{ + ThrowException, + IgnoreAll, + IgnoreMissing +} +/// +/// Defines how overlapping star power phrases should be handled. +/// +public enum OverlappingSpecialPhrasePolicy : byte +{ + /// + /// Throw an exception. + /// + ThrowException, + /// + /// Ignore the overlapping phrase. + /// + Ignore, + /// + /// Cut the length of the first phrase to the start of the next one. + /// + Cut, +} +/// +/// Defines how a tempo anchor with no parent marker is handled. +/// +public enum TempolessAnchorPolicy +{ + /// + /// Throw an exception. + /// + ThrowException, + /// + /// Ignore the anchor. + /// + Ignore, + /// + /// Create a tempo marker with the anchor. + /// + Create +} +/// +/// Defines how notes within ticks of each other are handled during a Midi operation. +/// +public enum SnappedNotesPolicy : byte +{ + /// + /// Throw an exception. + /// + ThrowException, + /// + /// Combine the notes as a single chord at the position of the earlier note. + /// + Snap, + /// + /// Leave each note as its own chord. + /// + Ignore +} +/// +/// Defines how or events are handled when there are no star power phrases. +/// +/// is always used when star power phrases are present. +public enum SoloNoStarPowerPolicy : byte +{ + /// + /// Store the events under . + /// + StoreAsEvents, + /// + /// Convert the space between the and event to a star power phrase. + /// + Convert +} +/// +/// Difficulty of the to serve as a source of for track objects for which the target format requires these objects to be the same across all difficulties. +/// +/// Can be cast from . +public enum TrackObjectSource : byte +{ + /// + /// Use the objects from the track. + /// + Easy, + /// + /// Use the objects from the track. + /// + Medium, + /// + /// Use the objects from the track. + /// + Hard, + /// + /// Use the objects from the track. + /// + Expert, + /// + /// Combine the unique track objects from all the tracks in the instrument. + /// + Merge, +} +/// +/// Defines how lead guitar and bass and handled when the Midi mapping is uncertain. +/// +public enum UncertainGuitarBassFormatPolicy : byte +{ + /// + /// Throw an exception. + /// + ThrowException, + /// + /// Use the format that was defaulted to when reading. + /// + /// Policy is invalid when reading. + UseReadingDefault, + /// + /// Default to the Guitar Hero 2 format. + /// + UseGuitarHero2, + /// + /// Default to the Rock Band format. + /// + UseRockBand +} +/// +/// Defines how unknown sections or Midi chunks are handled. +/// +public enum UnknownSectionPolicy : byte +{ + /// + /// Throw an exception with the section or chunk header. + /// + ThrowException, + /// + /// Store the raw data to be included when writing. + /// + Store +} +/// +/// Defines chord modifiers not supported by the target format are handled. +/// +public enum UnsupportedModifierPolicy : byte +{ + /// + /// Throw an exception with the modifier index. + /// + ThrowException, + /// + /// Convert the modifier to one supported by the format. + /// + /// Will throw an exception if the modifier cannot be converted. + Convert, + /// + /// Ignore the modifier. + /// + IgnoreModifier, + /// + /// Ignore the chord containing the modifier. + /// + IgnoreChord, +} +/// +/// Defines how track object defined with a with no matching are handled when reading Midi. +/// +public enum UnopenedTrackObjectPolicy : byte +{ + /// + /// Throw an exception with the event position and index. + /// + ThrowException, + /// + /// Create a track object at the position of the closing event and a length of 0. + /// + Create, + /// + /// Ignore the event. + /// + Ignore +} +/// +/// Defines how track object defined with a with no matching are handled when reading Midi. +/// +public enum UnclosedTrackObjectPolicy : byte +{ + /// + /// Throw an exception with the event position and index. + /// + ThrowException, + /// + /// Include the track object with a length going up to the next track object opening of the same index. + /// + Include, + /// + /// Ignore the event and track object. + /// + Ignore +} diff --git a/source - Copie/IO/Configuration/ReadingConfiguration.cs b/source - Copie/IO/Configuration/ReadingConfiguration.cs new file mode 100644 index 00000000..35194948 --- /dev/null +++ b/source - Copie/IO/Configuration/ReadingConfiguration.cs @@ -0,0 +1,25 @@ +using Melanchall.DryWetMidi.Core; + +namespace ChartTools.IO.Configuration; + +/// +/// Configuration object to direct the reading of a file +/// +/// +public record ReadingConfiguration : CommonConfiguration +{ + public bool IgnoreInvalidMidiEventType { get; set; } + public MisalignedBigRockMarkersPolicy MisalignedBigRockMarkersPolicy { get; set; } + public MissingBigRockMarkerPolicy MissingBigRockMarkerPolicy { get; set; } + public UnopenedTrackObjectPolicy UnopenedTrackObjectPolicy { get; set; } + public UnclosedTrackObjectPolicy UnclosedTracjObjectPolicy { get; set; } + public UnknownSectionPolicy UnknownSectionPolicy { get; set; } + public TempolessAnchorPolicy TempolessAnchorPolicy { get; set; } + public UncertainGuitarBassFormatPolicy UncertainGuitarBassFormatPolicy { get; set; } + + /// + /// Configuration object to customize how DryWetMidi reads Midi file before being parsed + /// + /// Setting to will use default settings + public ReadingSettings? MidiFirstPassReadingSettings { get; set; } +} diff --git a/source - Copie/IO/Configuration/Sessions/ReadingSession.cs b/source - Copie/IO/Configuration/Sessions/ReadingSession.cs new file mode 100644 index 00000000..fa64c2d6 --- /dev/null +++ b/source - Copie/IO/Configuration/Sessions/ReadingSession.cs @@ -0,0 +1,22 @@ +using ChartTools.IO.Formatting; + +namespace ChartTools.IO.Configuration.Sessions; + +internal class ReadingSession : Session +{ + public delegate bool TempolessAnchorHandler(Anchor anchor); + + public override ReadingConfiguration Configuration { get; } + public TempolessAnchorHandler TempolessAnchorProcedure { get; private set; } + public ReadingSession(ReadingConfiguration config, FormattingRules? formatting) : base(formatting) + { + Configuration = config; + TempolessAnchorProcedure = anchor => (TempolessAnchorProcedure = Configuration.TempolessAnchorPolicy switch + { + TempolessAnchorPolicy.ThrowException => anchor => throw new Exception($"Tempo anchor at position {anchor.Position} does not have a parent tempo marker."), + TempolessAnchorPolicy.Ignore => anchor => false, + TempolessAnchorPolicy.Create => anchor => true, + _ => throw ConfigurationExceptions.UnsupportedPolicy(Configuration.TempolessAnchorPolicy) + })(anchor); + } +} diff --git a/source - Copie/IO/Configuration/Sessions/Session.cs b/source - Copie/IO/Configuration/Sessions/Session.cs new file mode 100644 index 00000000..2b214884 --- /dev/null +++ b/source - Copie/IO/Configuration/Sessions/Session.cs @@ -0,0 +1,35 @@ +using ChartTools.IO.Formatting; + +namespace ChartTools.IO.Configuration.Sessions; + +internal abstract class Session +{ + public delegate bool DuplicateTrackObjectHandler(uint position, string objectType, Func checkDuplciate); + public delegate bool SnappedNotesHandler(uint origin, uint position); + + public DuplicateTrackObjectHandler DuplicateTrackObjectProcedure { get; private set; } + public SnappedNotesHandler SnappedNotesProcedure { get; private set; } + public virtual CommonConfiguration Configuration { get; } = new(); + public FormattingRules? Formatting { get; set; } + + public Session(FormattingRules? formatting) + { + Formatting = formatting; + + DuplicateTrackObjectProcedure = (position, objectType, checkDuplicate) => (DuplicateTrackObjectProcedure = Configuration.DuplicateTrackObjectPolicy switch + { + DuplicateTrackObjectPolicy.ThrowException => (position, objectType, checkDuplicate) => checkDuplicate() + ? throw new Exception($"Duplicate {objectType} on position {position}.") : true, + DuplicateTrackObjectPolicy.IncludeAll => (_, _, _) => true, + DuplicateTrackObjectPolicy.IncludeFirst => (_, _, checkDuplicate) => !checkDuplicate(), + _ => throw ConfigurationExceptions.UnsupportedPolicy(Configuration.DuplicateTrackObjectPolicy) + })(position, objectType, checkDuplicate); + SnappedNotesProcedure = (origin, position) => (SnappedNotesProcedure = Configuration.SnappedNotesPolicy switch + { + SnappedNotesPolicy.ThrowException => (origin, position) => throw new Exception($"Note at position {position} is within snapping distance from chord at position {origin}"), + SnappedNotesPolicy.Snap => (_, _) => true, + SnappedNotesPolicy.Ignore => (_, _) => false, + _ => throw ConfigurationExceptions.UnsupportedPolicy(Configuration.SnappedNotesPolicy) + })(origin, position); + } +} diff --git a/source - Copie/IO/Configuration/Sessions/WritingSession.cs b/source - Copie/IO/Configuration/Sessions/WritingSession.cs new file mode 100644 index 00000000..9ed4d2be --- /dev/null +++ b/source - Copie/IO/Configuration/Sessions/WritingSession.cs @@ -0,0 +1,25 @@ +using ChartTools.IO.Formatting; +using ChartTools.IO.Chart.Entries; + +namespace ChartTools.IO.Configuration.Sessions; + +internal class WritingSession : Session +{ + public delegate IEnumerable ChordEntriesGetter(LaneChord? previous, LaneChord current); + + public override WritingConfiguration Configuration { get; } + public ChordEntriesGetter GetChordEntries { get; private set; } + + public WritingSession(WritingConfiguration config, FormattingRules? formatting) : base(formatting) + { + Configuration = config; + GetChordEntries = (previous, chord) => (GetChordEntries = Configuration.UnsupportedModifierPolicy switch + { + UnsupportedModifierPolicy.IgnoreChord => (_, _) => Enumerable.Empty(), + UnsupportedModifierPolicy.ThrowException => (_, chord) => throw new Exception($"Chord at position {chord.Position} as an unsupported modifier for the chart format."), + UnsupportedModifierPolicy.IgnoreModifier => (_, chord) => chord.GetChartNoteData(), + UnsupportedModifierPolicy.Convert => (previous, chord) => chord.GetChartModifierData(previous, this), + _ => throw ConfigurationExceptions.UnsupportedPolicy(Configuration.UnsupportedModifierPolicy) + })(previous, chord); + } +} diff --git a/source - Copie/IO/Configuration/WritingConfiguration.cs b/source - Copie/IO/Configuration/WritingConfiguration.cs new file mode 100644 index 00000000..6e816269 --- /dev/null +++ b/source - Copie/IO/Configuration/WritingConfiguration.cs @@ -0,0 +1,12 @@ +namespace ChartTools.IO.Configuration; + +public record WritingConfiguration : CommonConfiguration +{ + /// + /// Defines which difficulty to get local events from + /// + public TrackObjectSource EventSource { get; init; } + public TrackObjectSource StarPowerSource { get; init; } + /// + public UnsupportedModifierPolicy UnsupportedModifierPolicy { get; init; } +} diff --git a/source - Copie/IO/DirectoryHandler.cs b/source - Copie/IO/DirectoryHandler.cs new file mode 100644 index 00000000..ac7fe117 --- /dev/null +++ b/source - Copie/IO/DirectoryHandler.cs @@ -0,0 +1,36 @@ +using ChartTools.IO.Formatting; +using ChartTools.IO.Ini; + +namespace ChartTools.IO; + +public record DirectoryResult(T Result, Metadata Metadata); + +internal static class DirectoryHandler +{ + public static DirectoryResult FromDirectory(string directory, Func read) + { + var iniPath = directory + @"\song.ini"; + var chartPath = directory + @"\notes.chart"; + var iniMetadata = File.Exists(iniPath) ? IniFile.ReadMetadata(iniPath) : new(); + + T? value = default; + + if (File.Exists(chartPath)) + value = read(chartPath, iniMetadata.Formatting); + + return new(value, iniMetadata); + } + public static async Task> FromDirectoryAsync(string directory, Func> read, CancellationToken cancellationToken) + { + var iniPath = directory + @"\song.ini"; + var chartPath = directory + @"\notes.chart"; + var iniMetadata = File.Exists(iniPath) ? await IniFile.ReadMetadataAsync(iniPath, null, cancellationToken) : new(); + + T? value = default; + + if (File.Exists(chartPath)) + value = await read(chartPath, iniMetadata.Formatting); + + return new(value, iniMetadata); + } +} diff --git a/source - Copie/IO/Exceptions/EntryException.cs b/source - Copie/IO/Exceptions/EntryException.cs new file mode 100644 index 00000000..a6e0f528 --- /dev/null +++ b/source - Copie/IO/Exceptions/EntryException.cs @@ -0,0 +1,6 @@ +namespace ChartTools.IO; + +public class EntryException : FormatException +{ + public EntryException() : base("Cannot divide line into entry elements.") { } +} diff --git a/source - Copie/IO/Exceptions/LineException.cs b/source - Copie/IO/Exceptions/LineException.cs new file mode 100644 index 00000000..b26878e1 --- /dev/null +++ b/source - Copie/IO/Exceptions/LineException.cs @@ -0,0 +1,7 @@ +namespace ChartTools.IO; + +public class LineException : FormatException +{ + public string Line { get; } + public LineException(string line, Exception innerException) : base($"Line \"{line}\" {innerException.Message}", innerException) => Line = line; +} diff --git a/source - Copie/IO/Exceptions/ParseException.cs b/source - Copie/IO/Exceptions/ParseException.cs new file mode 100644 index 00000000..13fc8c0a --- /dev/null +++ b/source - Copie/IO/Exceptions/ParseException.cs @@ -0,0 +1,15 @@ +namespace ChartTools.IO; + +public class ParseException : FormatException +{ + public string? Object { get; } + public string Target { get; } + public Type Type { get; } + + public ParseException(string? obj, string target, Type type) : base($"Cannot convert {target} \"{obj}\" to {type.Name}") + { + Object = obj; + Target = target; + Type = type; + } +} diff --git a/source - Copie/IO/Exceptions/SectionException.cs b/source - Copie/IO/Exceptions/SectionException.cs new file mode 100644 index 00000000..ae32e9ee --- /dev/null +++ b/source - Copie/IO/Exceptions/SectionException.cs @@ -0,0 +1,11 @@ +namespace ChartTools.IO; + +public class SectionException : Exception +{ + public string Header { get; } + + public SectionException(string header, Exception innerException) : base($"Section \"{header}\" {innerException.Message}") => Header = header; + + public static SectionException EarlyEnd(string header) => new(header, new InvalidDataException("Section did not end within the provided lines")); + public static SectionException MissingRequired(string header) => new(header, new InvalidDataException("Required section could not be found.")); +} diff --git a/source - Copie/IO/ExtensionHandler.cs b/source - Copie/IO/ExtensionHandler.cs new file mode 100644 index 00000000..5aa1685f --- /dev/null +++ b/source - Copie/IO/ExtensionHandler.cs @@ -0,0 +1,114 @@ +using ChartTools.Extensions; + +namespace ChartTools.IO; + +/// +/// Read method that returns no value. +/// +/// File path +public delegate void VoidRead(string path); +/// +/// Read method that generates an object of the target type +/// +/// File path +public delegate T Read(string path); +/// +/// Asynchronous read method that generates an object of the target type +/// +/// Output type +/// File path +public delegate Task AsyncRead(string path); +/// +/// Write method hat takes an object of a target type +/// +/// Target type +/// File path +/// Object to write +public delegate void Write(string path, T content); +/// +/// Write method hat takes an object of a target type +/// +/// Target type +/// File path +/// Object to write +public delegate Task AsyncWrite(string path, T content); + +/// +/// Provides methods for reading and writing files based on the extension +/// +internal static class ExtensionHandler +{ + #region Reading + /// + /// Reads a file using the method that matches the extension. + /// + /// Path of the file to read + /// Array of tuples representing the supported extensions + public static void Read(string path, params (string extension, VoidRead readMetod)[] readers) + { + string extension = Path.GetExtension(path); + (string extension, VoidRead readMethod) reader = readers.FirstOrDefault(r => r.extension == extension); + + if (reader == default) + throw GetException(extension, readers.Select(r => r.extension)); + + reader.readMethod(path); + } + /// + /// Reads a file using the method that matches the extension and generates an output object. + /// + /// Type of the generated object + /// File path + /// set of tuples containing the supported extensions and the matching read method + public static T Read(string path, params (string extension, Read readMethod)[] readers) + { + string extension = Path.GetExtension(path); + (string extension, Read readMethod) reader = readers.FirstOrDefault(r => r.extension == extension); + + return reader == default ? throw GetException(extension, readers.Select(r => r.extension)) : reader.readMethod(path); + } + public static async Task ReadAsync(string path, params (string extension, AsyncRead readMethod)[] readers) + { + string extension = Path.GetExtension(path); + (string extension, AsyncRead readMethod) reader = readers.FirstOrDefault(r => r.extension == extension); + + return reader == default ? throw GetException(extension, readers.Select(r => r.extension)) : await reader.readMethod(path); + } + #endregion + + #region Writing + /// + /// Writes an object to a file using the method that matches the extension. + /// + /// Path of the file to write + /// Item to write + /// Array of tupples representing the supported extensions + /// + public static void Write(string path, T content, params (string extension, Write writeMethod)[] writers) + { + string extension = Path.GetExtension(path); + (string extension, Write writeMethod) writer = writers.FirstOrDefault(w => w.extension == extension); + + if (writer == default) + throw GetException(extension, writers.Select(w => w.extension)); + + writer.writeMethod(path, content); + } + public static async Task WriteAsync(string path, T content, params (string extension, AsyncWrite writeMethod)[] writers) + { + string extension = Path.GetExtension(path); + (string extension, AsyncWrite writeMethod) writer = writers.FirstOrDefault(w => w.extension == extension); + + if (writer == default) + throw GetException(extension, writers.Select(w => w.extension)); + + await writer.writeMethod(path, content); + } + #endregion + + /// + /// Gets the exception to throw if the extension has no method that handles it. + /// + /// Instance of to throw + private static Exception GetException(string extension, IEnumerable supportedExtensions) => new ArgumentException($"\"{extension}\" is not a supported extension. File must be {supportedExtensions.VerbalEnumerate("or")}."); +} diff --git a/source - Copie/IO/FileReader.cs b/source - Copie/IO/FileReader.cs new file mode 100644 index 00000000..c327592e --- /dev/null +++ b/source - Copie/IO/FileReader.cs @@ -0,0 +1,79 @@ +using ChartTools.Extensions.Collections; + +namespace ChartTools.IO; + +internal abstract class FileReader : IDisposable +{ + public string Path { get; } + public bool IsReading { get; protected set; } + public abstract IEnumerable> Parsers { get; } + + public FileReader(string path) => Path = path; + + public abstract void Read(); + public abstract Task ReadAsync(CancellationToken cancellationToken); + + protected void CheckBusy() + { + if (IsReading) + throw new InvalidOperationException("Cannot start read operation while the reader is busy."); + } + + public abstract void Dispose(); +} + +internal abstract class FileReader : FileReader where TParser : FileParser +{ + public record ParserContentGroup(TParser Parser, DelayedEnumerableSource Source); + + public override IEnumerable Parsers => parserGroups.Select(g => g.Parser); + + protected readonly List parserGroups = new(); + protected readonly List parseTasks = new(); + protected readonly Func parserGetter; + + public FileReader(string path, Func parserGetter) : base(path) => this.parserGetter = parserGetter; + + public override void Read() + { + CheckBusy(); + IsReading = true; + + ReadBase(false, CancellationToken.None); + + foreach (var group in parserGroups) + group.Parser.Parse(group.Source.Enumerable.EnumerateSynchronously()); + + IsReading = false; + } + public override async Task ReadAsync(CancellationToken cancellationToken) + { + CheckBusy(); + IsReading = true; + + ReadBase(true, cancellationToken); + await Task.WhenAll(parseTasks); + + IsReading = false; + } + + protected abstract void ReadBase(bool read, CancellationToken cancellationToken); + + public void Reset() + { + parseTasks.Clear(); + parserGroups.Clear(); + } + + public override async void Dispose() + { + foreach (var group in parserGroups) + group.Source.Dispose(); + + foreach (var task in parseTasks) + { + await task; + task.Dispose(); + } + } +} diff --git a/source - Copie/IO/Formatting/FormattingEnums.cs b/source - Copie/IO/Formatting/FormattingEnums.cs new file mode 100644 index 00000000..ce0c64e5 --- /dev/null +++ b/source - Copie/IO/Formatting/FormattingEnums.cs @@ -0,0 +1,5 @@ +namespace ChartTools.IO.Formatting; + +[Flags] public enum AlbumTrackKey : byte { AlbumTrack, Track } +[Flags] public enum CharterKey : byte { Charter, Frets } +public enum HopoFrequencyStep : byte { TwentyFourth, Sixteenth, Twelveth, Eight, Sixth, Fourth } diff --git a/source - Copie/IO/Formatting/FormattingRules.cs b/source - Copie/IO/Formatting/FormattingRules.cs new file mode 100644 index 00000000..13a72529 --- /dev/null +++ b/source - Copie/IO/Formatting/FormattingRules.cs @@ -0,0 +1,113 @@ +using ChartTools.IO.Chart; +using ChartTools.IO.Chart.Serializing; +using ChartTools.IO.Ini; + +namespace ChartTools.IO.Formatting; + +/// +/// Rules defined in song.ini that affect how the song data file is read and written +/// +/// Property summaries provided by Nathan Hurst. +public class FormattingRules +{ + public AlbumTrackKey AlbumTrackKey { get; set; } + public CharterKey CharterKey { get; set; } + + /// + /// Number of values per beat + /// + [ChartKeySerializable(ChartFormatting.Resolution)] + public uint? Resolution { get; set; } + public uint TrueResolution => Resolution ?? 480; + + /// + /// Overrides the default sustain cutoff threshold with the specified number of ticks. + /// + [IniKeySerializable(IniFormatting.SustainCutoff)] + public uint? SustainCutoff { get; set; } + + #region Hopo frequency + /// + /// Overrides the natural HOPO threshold with the specified number of ticks. + /// + [IniKeySerializable(IniFormatting.HopoFrequency)] + public uint? HopoFrequency { get; set; } + /// + /// (FoFiX) Overrides the natural HOPO threshold using numbers from 0 to 5. + /// + [IniKeySerializable(IniFormatting.HopoFrequencyStep)] + public HopoFrequencyStep? HopoFrequencyStep { get; set; } + /// + /// (FoFiX) Overrides the natural HOPO threshold to be a 1/8th step. + /// + [IniKeySerializable(IniFormatting.ForceEightHopoFrequency)] + public bool? ForceEightHopoFrequency { get; set; } + + public uint? TrueHopoFrequency + { + get + { + if (HopoFrequency is not null) + return HopoFrequency.Value; + + if (HopoFrequencyStep is not null) + return TrueResolution / (uint)(HopoFrequencyStep.Value switch + { + Formatting.HopoFrequencyStep.Fourth => 4, + Formatting.HopoFrequencyStep.Eight => 8, + Formatting.HopoFrequencyStep.Twelveth => 12, + Formatting.HopoFrequencyStep.Sixteenth => 16, + _ => throw new System.Exception($"{HopoFrequencyStep} is not a valid hopo frequency step.") + }); + + return ForceEightHopoFrequency is true ? TrueResolution / 8 : null; + } + } + #endregion + + #region Star power + /// + /// Overrides the Star Power phrase MIDI note for .mid charts. + /// + [IniKeySerializable(IniFormatting.MultiplierNote)] + public byte? MultiplierNote { get; set; } + /// + /// (PhaseShift) Overrides the Star Power phrase MIDI note for .mid charts. + /// + [IniKeySerializable(IniFormatting.StarPowerNote)] + public byte? StarPowerNote { get; set; } + public byte? TrueStarPowerNote => StarPowerNote ?? MultiplierNote; + #endregion + + #region SysEx + /// + /// (PhaseShift) Indicates if the chart uses SysEx events for sliders/tap notes. + /// + [IniKeySerializable(IniFormatting.SysExSliders)] + public bool? SysExSliders { get; set; } + + /// + /// (PhaseShift) Indicates if the chart uses SysEx events for Drums Real hi-hat pedal control. + /// + [IniKeySerializable(IniFormatting.SysExHighHat)] + public bool? SysExHighHat { get; set; } + + /// + /// (PhaseShift) Indicates if the chart uses SysEx events for Drums Real rimshot hits. + /// + [IniKeySerializable(IniFormatting.Rimshot)] + public bool? SysExRimshot { get; set; } + + /// + /// (PhaseShift) Indicates if the chart uses SysEx events for open notes. + /// + [IniKeySerializable(IniFormatting.SysExOpenBass)] + public bool? SysExOpenBass { get; set; } + + /// + /// (PhaseShift) Indicates if the chart uses SysEx events for Pro Guitar/Bass slide directions. + /// + [IniKeySerializable(IniFormatting.SysExProSlide)] + public bool? SysexProSlide { get; set; } + #endregion +} \ No newline at end of file diff --git a/source - Copie/IO/ISerializerDataProvider.cs b/source - Copie/IO/ISerializerDataProvider.cs new file mode 100644 index 00000000..5e6fb5f0 --- /dev/null +++ b/source - Copie/IO/ISerializerDataProvider.cs @@ -0,0 +1,8 @@ +using ChartTools.IO.Configuration.Sessions; + +namespace ChartTools.IO; + +internal interface ISerializerDataProvider +{ + public IEnumerable ProvideFor(IEnumerable source, WritingSession session); +} diff --git a/source - Copie/IO/Ini/IniFile.cs b/source - Copie/IO/Ini/IniFile.cs new file mode 100644 index 00000000..fd960875 --- /dev/null +++ b/source - Copie/IO/Ini/IniFile.cs @@ -0,0 +1,47 @@ +using ChartTools.Extensions.Linq; +using ChartTools.IO.Formatting; +using ChartTools.IO.Configuration; + +namespace ChartTools.IO.Ini; + +/// +/// Provides methods for reading and writing ini files +/// +public static class IniFile +{ + /// + /// + /// A new instance of if is , otherwise the same reference. + public static Metadata ReadMetadata(string path, Metadata? existing = null) + { + var reader = new IniFileReader(path, header => header.Equals(IniFormatting.Header, StringComparison.OrdinalIgnoreCase) ? new(existing) : null); + reader.Read(); + + return reader.Parsers.TryGetFirst(out var parser) + ? parser!.Result + : throw SectionException.MissingRequired(IniFormatting.Header); + } + /// + /// + /// A new instance of if is , otherwise the same reference. + public static async Task ReadMetadataAsync(string path, Metadata? existing = null, CancellationToken cancellationToken = default) + { + var reader = new IniFileReader(path, header => header.Equals(IniFormatting.Header, StringComparison.OrdinalIgnoreCase) ? new(existing) : null); + await reader.ReadAsync(cancellationToken); + + return reader.Parsers.TryGetFirst(out var parser) + ? parser!.Result + : throw SectionException.MissingRequired(IniFormatting.Header); + } + + /// + /// Writes the metadata in a file. + /// + /// Path of the file to read + /// Metadata to write + public static void WriteMetadata(string path, Metadata metadata) + { + var writer = new IniFileWriter(path, new IniSerializer(metadata)); + writer.Write(); + } +} diff --git a/source - Copie/IO/Ini/IniFileReader.cs b/source - Copie/IO/Ini/IniFileReader.cs new file mode 100644 index 00000000..9638629b --- /dev/null +++ b/source - Copie/IO/Ini/IniFileReader.cs @@ -0,0 +1,10 @@ +namespace ChartTools.IO.Ini; + +internal class IniFileReader : TextFileReader +{ + public override IEnumerable Parsers => base.Parsers.Cast(); + + public IniFileReader(string path, Func parserGetter) : base(path, parserGetter) { } + + protected override bool IsSectionStart(string line) => !line.StartsWith('['); +} diff --git a/source - Copie/IO/Ini/IniFileWriter.cs b/source - Copie/IO/Ini/IniFileWriter.cs new file mode 100644 index 00000000..2fc5775a --- /dev/null +++ b/source - Copie/IO/Ini/IniFileWriter.cs @@ -0,0 +1,8 @@ +namespace ChartTools.IO.Ini; + +internal class IniFileWriter : TextFileWriter +{ + public IniFileWriter(string path, params Serializer[] serializers) : base(path, Enumerable.Empty(), serializers) { } + + protected override bool EndReplace(string line) => line.StartsWith('['); +} diff --git a/source - Copie/IO/Ini/IniFormatting.cs b/source - Copie/IO/Ini/IniFormatting.cs new file mode 100644 index 00000000..079989ab --- /dev/null +++ b/source - Copie/IO/Ini/IniFormatting.cs @@ -0,0 +1,47 @@ +namespace ChartTools.IO.Ini; + +public static class IniFormatting +{ + public const string + Header = "[song]", + Title = "name", + Artist = "artist", + Album = "album", + AlbumTrack = "album_track", + Track = "track", + Playlist = "playlist", + SubPlaylist = "sub_playlist", + PlaylistTrack = "playlis_track", + Genre = "genre", + Year = "year", + Charter = "charter", + Frets = "frets", + Icon = "icon", + PreviewStart = "preview_start_time", + PreviewEnd = "preview_end_time", + AudioOffset = "delay", + VideoOffset = "video_start_time", + Length = "song_length", + LoadingText = "loading_text", + Difficulty = "diff_band", + Modchart = "modchart", + SustainCutoff = "sustain_cutoff_threshold", + HopoFrequency = "hopo_frequency", + HopoFrequencyStep = "hopofreq", + ForceEightHopoFrequency = "eighthnote_hopo", + MultiplierNote = "multiplier_note", + StarPowerNote = "star_power_note", + SysExSliders = "sysex_slider", + SysExHighHat = "sysex_high_hat_ctrl", + Rimshot = "sysex_rimshot", + SysExOpenBass = "sysex_open_bass", + SysExProSlide = "sysex_pro_slide", + GuitarDifficulty = "diff_guitar", + BassDifficulty = "diff_bass", + DrumsDifficulty = "diff_drums", + KeysDifficulty = "diff_keys", + GHLGuitarDifficulty = "diff_guitarghl", + GHLBassDifficulty = "diff_bassghl"; + + public static string Line(string key, string value) => $"{key} = {value}"; +} diff --git a/source - Copie/IO/Ini/IniKeySerializableAttribute.cs b/source - Copie/IO/Ini/IniKeySerializableAttribute.cs new file mode 100644 index 00000000..db9e88f8 --- /dev/null +++ b/source - Copie/IO/Ini/IniKeySerializableAttribute.cs @@ -0,0 +1,13 @@ +namespace ChartTools.IO.Ini +{ + public class IniKeySerializableAttribute : KeySerializableAttribute + { + public override FileType Format => FileType.Ini; + + public IniKeySerializableAttribute(string key) : base(key) { } + + protected override string GetValueString(object propValue) => propValue.ToString()!; + + public static IEnumerable<(string key, string value)> GetSerializable(object source) => GetSerializable(source); + } +} diff --git a/source - Copie/IO/Ini/IniParser.cs b/source - Copie/IO/Ini/IniParser.cs new file mode 100644 index 00000000..b11f766a --- /dev/null +++ b/source - Copie/IO/Ini/IniParser.cs @@ -0,0 +1,141 @@ +using ChartTools.IO.Formatting; +using ChartTools.IO.Parsing; +using ChartTools.Tools; + +namespace ChartTools.IO.Ini; + +internal class IniParser : TextParser, ISongAppliable +{ + public override Metadata Result => GetResult(result); + private readonly Metadata result; + + public IniParser(Metadata? existing = null) : base(null!, IniFormatting.Header) => result = existing ?? new(); + + protected override void HandleItem(string item) + { + var entry = new TextEntry(item); + + if (entry.Value is null) + return; + + switch (entry.Key) + { + case IniFormatting.Title: + result.Title = entry.Value; + break; + case IniFormatting.Artist: + result.Artist = entry.Value; + break; + case IniFormatting.Album: + result.Album = entry.Value; + break; + case IniFormatting.AlbumTrack: + ParseAlbumTrack(); + result.Formatting.AlbumTrackKey |= AlbumTrackKey.AlbumTrack; + break; + case IniFormatting.Track: + ParseAlbumTrack(); + result.Formatting.AlbumTrackKey |= AlbumTrackKey.Track; + break; + case IniFormatting.Playlist: + result.Playlist = entry.Value; + break; + case IniFormatting.SubPlaylist: + result.SubPlaylist = entry.Value; + break; + case IniFormatting.PlaylistTrack: + result.PlaylistTrack = ValueParser.ParseUshort(entry.Value, "playlist track"); + break; + case IniFormatting.Year: + result.Year = ValueParser.ParseUshort(entry.Value, "year"); + break; + case IniFormatting.Genre: + result.Genre = entry.Value; + break; + case IniFormatting.Charter: + ParseCharter(); + result.Formatting.CharterKey |= CharterKey.Charter; + break; + case IniFormatting.Frets: + ParseCharter(); + result.Formatting.CharterKey |= CharterKey.Frets; + break; + case IniFormatting.Icon: + result.Charter.Icon = entry.Value; + break; + case IniFormatting.PreviewStart: + result.PreviewStart = entry.Value.StartsWith('-') ? null : ValueParser.ParseUint(entry.Value, "preview start"); + break; + case IniFormatting.PreviewEnd: + result.PreviewEnd = entry.Value.StartsWith('-') ? null : ValueParser.ParseUint(entry.Value, "preview end"); + break; + case IniFormatting.AudioOffset: + result.AudioOffset = TimeSpan.FromMilliseconds(ValueParser.ParseInt(entry.Value, "audio offset")); + break; + case IniFormatting.VideoOffset: + result.VideoOffset = TimeSpan.FromMilliseconds(ValueParser.ParseInt(entry.Value, "video offset")); + break; + case IniFormatting.Length: + result.Length = ValueParser.ParseUint(entry.Value, "song length"); + break; + case IniFormatting.Difficulty: + result.Difficulty = ValueParser.ParseSbyte(entry.Value, "difficulty"); + break; + case IniFormatting.LoadingText: + result.LoadingText = entry.Value; + break; + case IniFormatting.Modchart: + result.IsModchart = ValueParser.ParseInt(entry.Value, "modchart") == 1; + break; + case IniFormatting.GuitarDifficulty: + result.InstrumentDifficulties.Guitar = ValueParser.ParseSbyte(entry.Value, "guitar difficulty"); + break; + case IniFormatting.BassDifficulty: + result.InstrumentDifficulties.Bass = ValueParser.ParseSbyte(entry.Value, "bass difficulty"); + break; + case IniFormatting.DrumsDifficulty: + result.InstrumentDifficulties.Drums = ValueParser.ParseSbyte(entry.Value, "drums difficulty"); + break; + case IniFormatting.KeysDifficulty: + result.InstrumentDifficulties.Keys = ValueParser.ParseSbyte(entry.Value, "keys difficulty"); + break; + case IniFormatting.GHLGuitarDifficulty: + result.InstrumentDifficulties.GHLGuitar = ValueParser.ParseSbyte(entry.Value, "GHL guitar difficulty"); + break; + case IniFormatting.GHLBassDifficulty: + result.InstrumentDifficulties.GHLBass = ValueParser.ParseSbyte(entry.Value, "GHL bass difficulty"); + break; + case IniFormatting.SustainCutoff: + result.Formatting.SustainCutoff = ValueParser.ParseUint(entry.Value, "sustain cutoff"); + break; + case IniFormatting.HopoFrequency: + result.Formatting.HopoFrequency = ValueParser.ParseUint(entry.Value, "hopo frequency"); + break; + case IniFormatting.HopoFrequencyStep: + result.Formatting.HopoFrequencyStep = (HopoFrequencyStep)ValueParser.ParseByte(entry.Value, "hopo frequency step"); + break; + case IniFormatting.ForceEightHopoFrequency: + result.Formatting.ForceEightHopoFrequency = ValueParser.ParseBool(entry.Value, "force eight hopo frequency"); + break; + default: + if (entry.Value is not null) + result.UnidentifiedData.Add(new() { Key = entry.Key, Value = entry.Value, Origin = FileType.Ini }); + break; + } + + void ParseAlbumTrack() => ValueParser.ParseUshort(entry.Value, "album track"); + void ParseCharter() + { + result.Charter ??= new(); + result.Charter.Name = entry.Value; + } + } + + public void ApplyToSong(Song song) + { + if (song.Metadata is null) + song.Metadata = Result; + else + PropertyMerger.Merge(song.Metadata, false, true, Result); + } +} diff --git a/source - Copie/IO/Ini/IniSerializer.cs b/source - Copie/IO/Ini/IniSerializer.cs new file mode 100644 index 00000000..fd1334f0 --- /dev/null +++ b/source - Copie/IO/Ini/IniSerializer.cs @@ -0,0 +1,34 @@ +using ChartTools.IO.Formatting; + +namespace ChartTools.IO.Ini; + +internal class IniSerializer : Serializer +{ + public IniSerializer(Metadata content) : base(IniFormatting.Header, content, null!) { } + + public override IEnumerable Serialize() + { + if (Content is null) + yield break; + + var props = IniKeySerializableAttribute.GetSerializable(Content) + .Concat(IniKeySerializableAttribute.GetSerializable(Content.Formatting)) + .Concat(IniKeySerializableAttribute.GetSerializable(Content.Charter) + .Concat(IniKeySerializableAttribute.GetSerializable(Content.InstrumentDifficulties))); + + foreach ((var key, var value) in props) + yield return IniFormatting.Line(key, value.ToString()); + + foreach (var data in Content.UnidentifiedData) + yield return IniFormatting.Line(data.Key, data.Value); + + if (Content.AlbumTrack is not null) + { + if (Content.Formatting.AlbumTrackKey.HasFlag(AlbumTrackKey.Track)) + yield return IniFormatting.Line(IniFormatting.Track, Content.AlbumTrack.ToString()!); + + if (Content.Formatting.AlbumTrackKey.HasFlag(AlbumTrackKey.AlbumTrack)) + yield return IniFormatting.Line(IniFormatting.AlbumTrack, Content.AlbumTrack.ToString()!); + } + } +} diff --git a/source - Copie/IO/Parsers/FileParser.cs b/source - Copie/IO/Parsers/FileParser.cs new file mode 100644 index 00000000..84fe1c8e --- /dev/null +++ b/source - Copie/IO/Parsers/FileParser.cs @@ -0,0 +1,54 @@ +using ChartTools.IO.Configuration.Sessions; + +namespace ChartTools.IO; + +internal abstract class FileParser +{ + public bool ResultReady { get; private set; } + public abstract object? Result { get; } + protected ReadingSession session; + + public FileParser(ReadingSession session) => this.session = session; + + public async Task StartAsyncParse(IEnumerable items) + { + await Task.Run(() => ParseBase(items)); + +#if CRASH_SOURCE + FinaliseParse(); +#else + try { FinaliseParse(); } + catch (Exception e) { throw GetFinalizeException(e); } +#endif + } + public void Parse(IEnumerable items) + { + ParseBase(items); + +#if CRASH_SOURCE + FinaliseParse(); +#else + try { FinaliseParse(); } + catch (Exception e) { throw GetFinalizeException(e); } +#endif + } + private void ParseBase(IEnumerable items) + { + foreach (var item in items) +#if CRASH_SOURCE + HandleItem(item); +#else + try { HandleItem(item); } + catch (Exception e) { throw GetHandleException(item, e); } +#endif + } + + protected abstract void HandleItem(T item); + + protected virtual void FinaliseParse() => ResultReady = true; + + protected TResult GetResult(TResult result) => ResultReady ? result : throw new Exception("Result is not ready."); + + protected abstract Exception GetHandleException(T item, Exception innerException); + protected abstract Exception GetFinalizeException(Exception innerException); +} diff --git a/source - Copie/IO/Parsers/SectionParser.cs b/source - Copie/IO/Parsers/SectionParser.cs new file mode 100644 index 00000000..6efa526b --- /dev/null +++ b/source - Copie/IO/Parsers/SectionParser.cs @@ -0,0 +1,14 @@ +using ChartTools.IO.Configuration.Sessions; + +namespace ChartTools.IO.Parsing; + +internal abstract class SectionParser : FileParser +{ + public string Header { get; } + + public SectionParser(ReadingSession session, string header) : base(session) => Header = header; + + protected override Exception GetHandleException(T item, Exception innerException) => new SectionException(Header, GetHandleInnerException(item, innerException)); + protected abstract Exception GetHandleInnerException(T item, Exception innerException); + protected override Exception GetFinalizeException(Exception innerException) => new SectionException(Header, innerException); +} diff --git a/source - Copie/IO/Parsers/TextParser.cs b/source - Copie/IO/Parsers/TextParser.cs new file mode 100644 index 00000000..cc872d09 --- /dev/null +++ b/source - Copie/IO/Parsers/TextParser.cs @@ -0,0 +1,10 @@ +using ChartTools.IO.Configuration.Sessions; + +namespace ChartTools.IO.Parsing; + +internal abstract class TextParser : SectionParser +{ + protected TextParser(ReadingSession session, string header) : base(session, header) { } + + protected override Exception GetHandleInnerException(string item, Exception innerException) => new LineException(item, innerException); +} diff --git a/source - Copie/IO/Sections/ReservedSectionHeader.cs b/source - Copie/IO/Sections/ReservedSectionHeader.cs new file mode 100644 index 00000000..0b0ba104 --- /dev/null +++ b/source - Copie/IO/Sections/ReservedSectionHeader.cs @@ -0,0 +1,13 @@ +namespace ChartTools.IO.Sections; + +public readonly struct ReservedSectionHeader +{ + public string Header { get; } + public string DataSource { get; } + + public ReservedSectionHeader(string header, string dataSource) + { + Header = header; + DataSource = dataSource; + } +} diff --git a/source - Copie/IO/Sections/ReservedSectionHeaderSet.cs b/source - Copie/IO/Sections/ReservedSectionHeaderSet.cs new file mode 100644 index 00000000..a7629318 --- /dev/null +++ b/source - Copie/IO/Sections/ReservedSectionHeaderSet.cs @@ -0,0 +1,13 @@ +using System.Collections; + +namespace ChartTools.IO.Sections; + +public class ReservedSectionHeaderSet : IEnumerable +{ + private readonly IEnumerable _headers; + + public ReservedSectionHeaderSet(IEnumerable headers) => _headers = headers; + + public IEnumerator GetEnumerator() => _headers.GetEnumerator(); + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); +} diff --git a/source - Copie/IO/Sections/Section.cs b/source - Copie/IO/Sections/Section.cs new file mode 100644 index 00000000..ead0d0ec --- /dev/null +++ b/source - Copie/IO/Sections/Section.cs @@ -0,0 +1,7 @@ +namespace ChartTools.IO.Sections; + +public class Section : List +{ + public string Header { get; } + public Section(string header) => Header = header; +} diff --git a/source - Copie/IO/Sections/SectionSet.cs b/source - Copie/IO/Sections/SectionSet.cs new file mode 100644 index 00000000..59e9bd2a --- /dev/null +++ b/source - Copie/IO/Sections/SectionSet.cs @@ -0,0 +1,56 @@ +using System.Collections; + +namespace ChartTools.IO.Sections; + +public abstract class SectionSet : IList> +{ + private readonly List> _sections = new(); + public abstract ReservedSectionHeaderSet ReservedHeaders { get; } + + #region IList + public int Count => _sections.Count; + public bool IsReadOnly => false; + + public Section this[int index] + { + get => _sections[index]; + set + { + CheckHeader(value.Header); + _sections[index] = value; + } + } + + public int IndexOf(Section item) => _sections.IndexOf(item); + public void Insert(int index, Section item) + { + CheckHeader(item.Header); + _sections.Insert(index, item); + } + public void RemoveAt(int index) => _sections.RemoveAt(index); + public void Add(Section item) + { + CheckHeader(item.Header); + _sections.Add(item); + } + public void Clear() => _sections.Clear(); + public bool Contains(Section item) => _sections.Contains(item); + public void CopyTo(Section[] array, int arrayIndex) => _sections.CopyTo(array, arrayIndex); + public bool Remove(Section item) => _sections.Remove(item); + public IEnumerator> GetEnumerator() => _sections.GetEnumerator(); + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + #endregion + + public Section? Get(string header) + { + CheckHeader(header); + return _sections.FirstOrDefault(s => s.Header == header); + } + + private void CheckHeader(string header) + { + foreach (var reserved in ReservedHeaders) + if (reserved.Header == header) + throw new Exception($"Header {header} is already modeled under {reserved.DataSource}"); + } +} diff --git a/source - Copie/IO/Serializers/GroupSerializer.cs b/source - Copie/IO/Serializers/GroupSerializer.cs new file mode 100644 index 00000000..649fbab4 --- /dev/null +++ b/source - Copie/IO/Serializers/GroupSerializer.cs @@ -0,0 +1,13 @@ +using ChartTools.IO.Configuration.Sessions; + +namespace ChartTools.IO; + +internal abstract class GroupSerializer : Serializer +{ + public GroupSerializer(string header, TContent content, WritingSession session) : base(header, content, session) { } + + protected abstract IEnumerable[] LaunchProviders(); + protected abstract IEnumerable CombineProviderResults(IEnumerable[] results); + + public override IEnumerable Serialize() => CombineProviderResults(LaunchProviders()); +} diff --git a/source - Copie/IO/Serializers/KeySerializable.cs b/source - Copie/IO/Serializers/KeySerializable.cs new file mode 100644 index 00000000..b575abaa --- /dev/null +++ b/source - Copie/IO/Serializers/KeySerializable.cs @@ -0,0 +1,24 @@ +using System.Reflection; + +namespace ChartTools.IO; + +public abstract class KeySerializableAttribute : Attribute +{ + public abstract FileType Format { get; } + public string Key { get; } + + public KeySerializableAttribute(string key) => Key = key; + + /// + /// Generates groups of non-null property values and their serialization keys. + /// + /// Object containing the properties + protected static IEnumerable<(string key, string value)> GetSerializable(object source) where TAttribute : KeySerializableAttribute => from prop in source.GetType().GetProperties() + let att = prop.GetCustomAttribute() + where att is not null + let value = prop.GetValue(source) + where value is not null + select (att.Key, att.GetValueString(value)); + + protected abstract string GetValueString(object propValue); +} diff --git a/source - Copie/IO/Serializers/Serializer.cs b/source - Copie/IO/Serializers/Serializer.cs new file mode 100644 index 00000000..051c91bf --- /dev/null +++ b/source - Copie/IO/Serializers/Serializer.cs @@ -0,0 +1,26 @@ +using ChartTools.IO.Configuration.Sessions; + +namespace ChartTools.IO; + +internal abstract class Serializer +{ + protected WritingSession session; + + public string Header { get; } + + public Serializer(string header, WritingSession session) + { + Header = header; + this.session = session; + } + + public abstract IEnumerable Serialize(); + public async Task> SerializeAsync() => await Task.Run(() => Serialize().ToArray()); +} + +internal abstract class Serializer : Serializer +{ + public TContent Content { get; } + + public Serializer(string header, TContent content, WritingSession session) : base(header, session) => Content = content; +} diff --git a/source - Copie/IO/TextEntry.cs b/source - Copie/IO/TextEntry.cs new file mode 100644 index 00000000..12e87ffd --- /dev/null +++ b/source - Copie/IO/TextEntry.cs @@ -0,0 +1,33 @@ +namespace ChartTools.IO +{ + /// + /// Line of text file data + /// + internal readonly struct TextEntry + { + /// + /// Text before the equal sign + /// + public string Key { get; } + /// + /// Text after the equal sign + /// + public string? Value { get; } + + public TextEntry(string key, string value) + { + Key = key; + Value = value; + } + public TextEntry(string line) + { + string[] split = line.Split('=', 2, StringSplitOptions.RemoveEmptyEntries); + + if (split.Length < 1) + throw new EntryException(); + + Key = split[0].Trim(); + Value = split.Length < 2 ? null : split[1].Trim(); + } + } +} diff --git a/source - Copie/IO/TextFileReader.cs b/source - Copie/IO/TextFileReader.cs new file mode 100644 index 00000000..e75f3659 --- /dev/null +++ b/source - Copie/IO/TextFileReader.cs @@ -0,0 +1,93 @@ +using ChartTools.Extensions.Collections; +using ChartTools.IO.Parsing; + +namespace ChartTools.IO; + +internal abstract class TextFileReader : FileReader +{ + public virtual bool DefinedSectionEnd { get; } = false; + + public TextFileReader(string path, Func parserGetter) : base(path, parserGetter) { } + + protected override void ReadBase(bool async, CancellationToken cancellationToken) + { + ParserContentGroup? currentGroup = null; + using var enumerator = File.ReadLines(Path).Where(s => !string.IsNullOrEmpty(s)).Select(s => s.Trim()).GetEnumerator(); + + while (enumerator.MoveNext()) + { + // Find part + while (!enumerator.Current.StartsWith('[')) + if (enumerator.MoveNext()) + return; + + if (async && cancellationToken.IsCancellationRequested) + { + Dispose(); + return; + } + + var header = enumerator.Current; + var parser = parserGetter(header); + + if (parser is not null) + { + var source = new DelayedEnumerableSource(); + + parserGroups.Add(currentGroup = new(parser, source)); + + if (async) + { + if (cancellationToken.IsCancellationRequested) + { + Dispose(); + return; + } + + parseTasks.Add(parser.StartAsyncParse(source.Enumerable)); + } + } + + // Move to the start of the entries + do + if (!AdvanceSection()) + { + Finish(); + return; + } + while (!IsSectionStart(enumerator.Current)); + + AdvanceSection(); + + // Read until end + while (!IsSectionEnd(enumerator.Current)) + { + currentGroup?.Source.Add(enumerator.Current); + + if (!AdvanceSection()) + { + Finish(); + return; + } + } + + Finish(); + + void Finish() + { + if (cancellationToken.IsCancellationRequested) + { + Dispose(); + return; + } + + currentGroup?.Source.EndAwait(); + } + + bool AdvanceSection() => enumerator.MoveNext() || (DefinedSectionEnd ? throw SectionException.EarlyEnd(header) : false); + } + } + + protected abstract bool IsSectionStart(string line); + protected virtual bool IsSectionEnd(string line) => false; +} diff --git a/source - Copie/IO/TextFileWriter.cs b/source - Copie/IO/TextFileWriter.cs new file mode 100644 index 00000000..d262d66b --- /dev/null +++ b/source - Copie/IO/TextFileWriter.cs @@ -0,0 +1,66 @@ +using ChartTools.Extensions.Linq; +using ChartTools.Internal.Collections; + +namespace ChartTools.IO; + +internal abstract class TextFileWriter +{ + public string Path { get; } + protected virtual string? PreSerializerContent => null; + protected virtual string? PostSerializerContent => null; + + private readonly List> serializers; + private readonly string tempPath = System.IO.Path.GetTempFileName(); + private readonly IEnumerable? removedHeaders; + + public TextFileWriter(string path, IEnumerable? removedHeaders, params Serializer[] serializers) + { + Path = path; + this.serializers = serializers.ToList(); + this.removedHeaders = removedHeaders; + } + + private IEnumerable> AddRemoveReplacements(IEnumerable> replacements) => removedHeaders is null ? replacements : replacements.Concat(removedHeaders.Select(header => new SectionReplacement(Enumerable.Empty(), line => line == header, EndReplace, false))); + + private IEnumerable Wrap(string header, IEnumerable lines) + { + yield return header; + + if (PreSerializerContent is not null) + yield return PreSerializerContent; + + foreach (var line in lines) + yield return line; + + if (PostSerializerContent is not null) + yield return PostSerializerContent; + } + + public void Write() + { + using (var writer = new StreamWriter(tempPath)) + foreach (var line in GetLines(serializer => serializer.Serialize())) + writer.WriteLine(line); + + File.Copy(tempPath, Path, true); + File.Delete(tempPath); + } + public async Task WriteAsync(CancellationToken cancellationToken) + { + using (var writer = new StreamWriter(tempPath)) + foreach (var line in GetLines(serializer => new EagerEnumerable(serializer.SerializeAsync()))) + await writer.WriteLineAsync(line); + + if (cancellationToken.IsCancellationRequested) + File.Delete(tempPath); + else + File.Move(tempPath, Path, true); + } + + private IEnumerable GetLines(Func, IEnumerable> getSerializerLines) => File.Exists(Path) + ? File.ReadLines(Path) + .ReplaceSections(AddRemoveReplacements(serializers.Select(serializer => new SectionReplacement(Wrap(serializer.Header, getSerializerLines(serializer)), line => line == serializer.Header, EndReplace, true)))) + : serializers.SelectMany(serializer => Wrap(serializer.Header, serializer.Serialize())); + + protected abstract bool EndReplace(string line); +} diff --git a/source - Copie/IO/ValueParser.cs b/source - Copie/IO/ValueParser.cs new file mode 100644 index 00000000..0b983c4e --- /dev/null +++ b/source - Copie/IO/ValueParser.cs @@ -0,0 +1,16 @@ +namespace ChartTools.IO; + +internal static class ValueParser +{ + public delegate bool TryParse(string? input, out T result); + public static T Parse(string? value, string target, TryParse tryParse) where T : struct => tryParse(value, out T result) ? result : throw new ParseException(value, target, typeof(T)); + + public static bool ParseBool(string? value, string target) => Parse(value, target, bool.TryParse); + public static byte ParseByte(string? value, string target) => Parse(value, target, byte.TryParse); + public static sbyte ParseSbyte(string? value, string target) => Parse(value, target, sbyte.TryParse); + public static short ParseShort(string? value, string target) => Parse(value, target, short.TryParse); + public static ushort ParseUshort(string? value, string target) => Parse(value, target, ushort.TryParse); + public static int ParseInt(string? value, string target) => Parse(value, target, int.TryParse); + public static uint ParseUint(string? value, string target) => Parse(value, target, uint.TryParse); + public static float ParseFloat(string? value, string target) => Parse(value, target, float.TryParse); +} diff --git a/source - Copie/IReadOnlyLongObject.cs b/source - Copie/IReadOnlyLongObject.cs new file mode 100644 index 00000000..2e62a2b8 --- /dev/null +++ b/source - Copie/IReadOnlyLongObject.cs @@ -0,0 +1,9 @@ +namespace ChartTools; + +public interface IReadOnlyLongObject +{ + /// + /// Length of the object in ticks + /// + public uint Length { get; } +} diff --git a/source - Copie/Instruments/Drums.cs b/source - Copie/Instruments/Drums.cs new file mode 100644 index 00000000..9d17c83f --- /dev/null +++ b/source - Copie/Instruments/Drums.cs @@ -0,0 +1,25 @@ +using ChartTools.IO; +using ChartTools.IO.Chart; +using ChartTools.IO.Configuration; +using ChartTools.IO.Formatting; + +namespace ChartTools; + +public record Drums : Instrument +{ + protected override InstrumentIdentity GetIdentity() => InstrumentIdentity.Drums; + + #region File reading + [Obsolete($"Use {nameof(ChartFile.ReadDrums)}.")] + public static Drums? FromFile(string path, ReadingConfiguration? config = default, FormattingRules? formatting = default) => ExtensionHandler.Read(path, (".chart", path => ChartFile.ReadDrums(path, config, formatting))); + + [Obsolete($"Use {nameof(ChartFile.ReadDrumsAsync)}.")] + public static async Task FromFileAsync(string path, ReadingConfiguration? config = default, FormattingRules? formatting = default, CancellationToken cancellationToken = default) => await ExtensionHandler.ReadAsync(path, (".chart", path => ChartFile.ReadDrumsAsync(path, config, formatting, cancellationToken))); + + [Obsolete($"Use {nameof(ChartFile.ReadDrums)} with {nameof(Metadata.Formatting)}.")] + public static DirectoryResult FromDirectory(string directory, ReadingConfiguration? config = default) => DirectoryHandler.FromDirectory(directory, (path, formatting) => FromFile(path, config, formatting)); + + [Obsolete($"Use {nameof(ChartFile.ReadDrumsAsync)} with {nameof(Metadata.Formatting)}.")] + public static Task> FromDirectoryAsync(string directory, ReadingConfiguration? config = default, CancellationToken cancellationToken = default) => DirectoryHandler.FromDirectoryAsync(directory, async (path, formatting) => await FromFileAsync(path, config, formatting, cancellationToken), cancellationToken); + #endregion +} diff --git a/source - Copie/Instruments/GHLInstrument.cs b/source - Copie/Instruments/GHLInstrument.cs new file mode 100644 index 00000000..567f847f --- /dev/null +++ b/source - Copie/Instruments/GHLInstrument.cs @@ -0,0 +1,30 @@ +using ChartTools.IO; +using ChartTools.IO.Chart; +using ChartTools.IO.Configuration; +using ChartTools.IO.Formatting; + +namespace ChartTools; + +public record GHLInstrument : Instrument +{ + public new GHLInstrumentIdentity InstrumentIdentity { get; init; } + + public GHLInstrument() { } + public GHLInstrument(GHLInstrumentIdentity identity) => InstrumentIdentity = identity; + + protected override InstrumentIdentity GetIdentity() => (InstrumentIdentity)InstrumentIdentity; + + #region File reading + [Obsolete($"Use {nameof(ChartFile.ReadInstrument)}.")] + public static GHLInstrument? FromFile(string path, GHLInstrumentIdentity instrument, ReadingConfiguration? config = default, FormattingRules? formatting = default) => ExtensionHandler.Read(path, (".chart", path => ChartFile.ReadInstrument(path, instrument, config, formatting))); + + [Obsolete($"Use {nameof(ChartFile.ReadInstrumentAsync)}.")] + public static async Task FromFileAsync(string path, GHLInstrumentIdentity instrument, ReadingConfiguration? config = default, FormattingRules? formatting = default, CancellationToken cancellationToken = default) => await ExtensionHandler.ReadAsync(path, (".chart", path => ChartFile.ReadInstrumentAsync(path, instrument, config, formatting, cancellationToken))); + + [Obsolete($"Use {nameof(ChartFile.ReadInstrument)} with {nameof(Metadata.Formatting)}.")] + public static DirectoryResult FromDirectory(string directory, GHLInstrumentIdentity instrument, ReadingConfiguration? config = default) => DirectoryHandler.FromDirectory(directory, (path, formatting) => FromFile(path, instrument, config, formatting)); + + [Obsolete($"Use {nameof(ChartFile.ReadInstrumentAsync)} with {nameof(Metadata.Formatting)}.")] + public static Task> FromDirectoryAsync(string directory, GHLInstrumentIdentity instrument, ReadingConfiguration? config = default, CancellationToken cancellationToken = default) => DirectoryHandler.FromDirectoryAsync(directory, async (path, formatting) => await FromFileAsync(path, instrument, config, formatting, cancellationToken), cancellationToken); + #endregion +} diff --git a/source - Copie/Instruments/Instrument.cs b/source - Copie/Instruments/Instrument.cs new file mode 100644 index 00000000..14e123d0 --- /dev/null +++ b/source - Copie/Instruments/Instrument.cs @@ -0,0 +1,156 @@ +using ChartTools.Extensions.Linq; +using ChartTools.IO; +using ChartTools.IO.Chart; +using ChartTools.IO.Configuration; +using ChartTools.IO.Formatting; + +using DiffEnum = ChartTools.Difficulty; + +namespace ChartTools; + +/// +/// Base class for instruments +/// +public abstract record Instrument : IEmptyVerifiable +{ + /// + public bool IsEmpty => GetExistingTracks().All(t => t.IsEmpty); + + /// + /// Identity of the instrument the object belongs to + /// + public InstrumentIdentity InstrumentIdentity => GetIdentity(); + + /// + /// Type of instrument + /// + public InstrumentType InstrumentType + { + get + { + if (_instrumentType is not null) + return _instrumentType.Value; + + _instrumentType = InstrumentIdentity switch + { + InstrumentIdentity.Drums => InstrumentType.Drums, + InstrumentIdentity.LeadGuitar or InstrumentIdentity.RhythmGuitar or InstrumentIdentity.Bass or InstrumentIdentity.CoopGuitar or InstrumentIdentity.GHLBass or InstrumentIdentity.Keys => InstrumentType.Standard, + InstrumentIdentity.GHLGuitar or InstrumentIdentity.GHLBass => InstrumentType.GHL, + InstrumentIdentity.Vocals => InstrumentType.Vocals, + _ => throw new InvalidDataException($"Instrument identity {InstrumentIdentity} does not belong to an instrument type.") + }; + + return _instrumentType.Value; + } + } + private InstrumentType? _instrumentType; + + /// + /// Set of special phrases applied to all difficulties + /// + public List SpecialPhrases { get; set; } = new(); + + /// + public sbyte? GetDifficulty(InstrumentDifficultySet difficulties) => difficulties.GetDifficulty(InstrumentIdentity); + /// + public void SetDifficulty(InstrumentDifficultySet difficulties, sbyte? difficulty) => difficulties.SetDifficulty(InstrumentIdentity, difficulty); + + /// + /// Easy track + /// + public Track? Easy => GetEasy(); + /// + /// Medium track + /// + public Track? Medium => GetMedium(); + /// + /// Hard track + /// + public Track? Hard => GetHard(); + /// + /// Expert track + /// + public Track? Expert => GetExpert(); + + /// + /// Gets the track matching a difficulty. + /// + public abstract Track? GetTrack(DiffEnum difficulty); + + protected abstract Track? GetEasy(); + protected abstract Track? GetMedium(); + protected abstract Track? GetHard(); + protected abstract Track? GetExpert(); + + /// + /// Creates a track + /// + /// Difficulty of the track + public abstract Track CreateTrack(DiffEnum difficulty); + /// + /// Removes a track. + /// + /// Difficulty of the target track + public abstract bool RemoveTrack(DiffEnum difficulty); + + /// + /// Creates an array containing all tracks. + /// + public virtual Track?[] GetTracks() => new Track?[] { Easy, Medium, Hard, Expert }; + /// + /// Creates an array containing all tracks with data. + /// + public virtual IEnumerable GetExistingTracks() => GetTracks().NonNull().Where(t => !t.IsEmpty); + + protected abstract InstrumentIdentity GetIdentity(); + + /// + /// Gives all tracks the same local events. + /// + public void ShareLocalEvents(TrackObjectSource source) => ShareEventsStarPower(source, track => track.LocalEvents); + /// + /// Gives all tracks the same star power + /// + public void ShareStarPower(TrackObjectSource source) => ShareEventsStarPower(source, track => track.SpecialPhrases); + private void ShareEventsStarPower(TrackObjectSource source, Func> collectionGetter) where T : TrackObjectBase + { + var collections = GetExistingTracks().Select(track => collectionGetter(track)).ToArray(); + + var objects = (source switch + { + TrackObjectSource.Easy => collections[0], + TrackObjectSource.Medium => collections[1], + TrackObjectSource.Hard => collections[2], + TrackObjectSource.Expert => collections[3], + TrackObjectSource.Merge => collections.SelectMany(col => col).Distinct(), + _ => throw new UndefinedEnumException(source) + }).ToArray(); + + foreach (var collection in collections) + { + collection.Clear(); + collection.AddRange(objects); + } + } + + #region IO + #region Reading + [Obsolete($"Use {nameof(ChartFile.ReadInstrument)}.")] + public static Instrument? FromFile(string path, InstrumentIdentity instrument, ReadingConfiguration? config = default, FormattingRules? formatting = default) => ExtensionHandler.Read(path, (".chart", path => ChartFile.ReadInstrument(path, instrument, config, formatting))); + [Obsolete($"Use {nameof(ChartFile.ReadInstrumentAsync)}.")] + public static async Task FromFileAsync(string path, InstrumentIdentity instrument, ReadingConfiguration? config = default, FormattingRules? formatting = default, CancellationToken cancellationToken = default) => await ExtensionHandler.ReadAsync(path, (".chart", path => ChartFile.ReadInstrumentAsync(path, instrument, config, formatting, cancellationToken))); + + [Obsolete($"Use {nameof(ChartFile.ReadInstrument)} with {nameof(Metadata.Formatting)}.")] + public static DirectoryResult FromDirectory(string directory, InstrumentIdentity instrument, ReadingConfiguration? config = default) => DirectoryHandler.FromDirectory(directory, (path, formatting) => FromFile(path, instrument, config, formatting)); + [Obsolete($"Use {nameof(ChartFile.ReadInstrumentAsync)} with {nameof(Metadata.Formatting)}.")] + public static Task> FromDirectoryAsync(string directory, InstrumentIdentity instrument, ReadingConfiguration? config = default, CancellationToken cancellationToken = default) => DirectoryHandler.FromDirectoryAsync(directory, async (path, formatting) => await FromFileAsync(path, instrument, config, formatting, cancellationToken), cancellationToken); + #endregion + + [Obsolete($"Use {nameof(ChartFile.ReplaceInstrument)}.")] + public void ToFile(string path, WritingConfiguration? config = default, FormattingRules? formatting = default) => ExtensionHandler.Write(path, this, (".chart", (path, inst) => ChartFile.ReplaceInstrument(path, inst, config, formatting))); + [Obsolete($"Use {nameof(ChartFile.ReplaceInstrumentAsync)}.")] + public async Task ToFileAsync(string path, WritingConfiguration? config = default, FormattingRules? formatting = default, CancellationToken cancellationToken = default) => await ExtensionHandler.WriteAsync(path, this, (".chart", (path, inst) => ChartFile.ReplaceInstrumentAsync(path, inst, config, formatting, cancellationToken))); + #endregion + + public override string ToString() => InstrumentIdentity.ToString(); +} diff --git a/source - Copie/Instruments/InstrumentGeneric.cs b/source - Copie/Instruments/InstrumentGeneric.cs new file mode 100644 index 00000000..cd6c3088 --- /dev/null +++ b/source - Copie/Instruments/InstrumentGeneric.cs @@ -0,0 +1,121 @@ +namespace ChartTools; + +/// +/// Set of tracks common to an instrument +/// +public abstract record Instrument : Instrument where TChord : IChord +{ + /// + /// Easy track + /// + public new Track? Easy + { + get => _easy; + set => _easy = value is null ? null : value with { Difficulty = Difficulty.Easy, ParentInstrument = this }; + } + private Track? _easy; + + /// + /// Medium track + /// + public new Track? Medium + { + get => _medium; + set => _medium = value is null ? null : value with { Difficulty = Difficulty.Medium, ParentInstrument = this }; + } + private Track? _medium; + + /// + /// Hard track + /// + public new Track? Hard + { + get => _hard; + set => _hard = value is null ? null : value with { Difficulty = Difficulty.Hard, ParentInstrument = this }; + } + private Track? _hard; + + /// + /// Expert track + /// + public new Track? Expert + { + get => _expert; + set => _expert = value is null ? null : value with { Difficulty = Difficulty.Expert, ParentInstrument = this }; + } + private Track? _expert; + + /// + /// Gets the that matches a + /// + public override Track? GetTrack(Difficulty difficulty) => difficulty switch + { + Difficulty.Easy => Easy, + Difficulty.Medium => Medium, + Difficulty.Hard => Hard, + ChartTools.Difficulty.Expert => Expert, + _ => throw new UndefinedEnumException(difficulty) + }; + + /// + public override Track CreateTrack(Difficulty difficulty) => difficulty switch + { + Difficulty.Easy => Easy = new(), + Difficulty.Medium => Medium = new(), + Difficulty.Hard => Hard = new(), + Difficulty.Expert => Expert = new(), + _ => throw new UndefinedEnumException(difficulty) + }; + /// + public override bool RemoveTrack(Difficulty difficulty) + { + bool found; + + switch (difficulty) + { + case Difficulty.Easy: + found = _easy is not null; + _easy = null; + return found; + case Difficulty.Medium: + found = _medium is not null; + _medium = null; + return found; + case Difficulty.Hard: + found = _hard is not null; + _hard = null; + return found; + case Difficulty.Expert: + found = _expert is not null; + _expert = null; + return found; + default: + throw new UndefinedEnumException(difficulty); + } + } + + protected override Track? GetEasy() => Easy; + protected override Track? GetMedium() => Medium; + protected override Track? GetHard() => Hard; + protected override Track? GetExpert() => Expert; + + public override Track?[] GetTracks() => new Track?[] { Easy, Medium, Hard, Expert }; + public override IEnumerable> GetExistingTracks() => base.GetExistingTracks().Cast>(); + + /// + /// Sets a track for a given . + /// + /// Track instance assigned to the instrument. Changed made to the passed reference will not be reflected in the instrument. + /// + /// + public Track SetTrack(Track track) => track is null + ? throw new ArgumentNullException(nameof(track)) + : track.Difficulty switch + { + Difficulty.Easy => _easy = track with { ParentInstrument = this }, + Difficulty.Medium => _medium = track with { ParentInstrument = this }, + Difficulty.Hard => _hard = track with { ParentInstrument = this }, + Difficulty.Expert => _expert = track with { ParentInstrument = this }, + _ => throw new UndefinedEnumException(track.Difficulty) + }; +} diff --git a/source - Copie/Instruments/InstrumentSet.cs b/source - Copie/Instruments/InstrumentSet.cs new file mode 100644 index 00000000..361b57d7 --- /dev/null +++ b/source - Copie/Instruments/InstrumentSet.cs @@ -0,0 +1,163 @@ +using ChartTools.Extensions.Linq; + +using System.Collections; + +namespace ChartTools; + +/// +/// Set of all instruments +/// +public class InstrumentSet : IEnumerable +{ + /// + /// Set of drums tracks + /// + public Drums? Drums { get; set; } + /// + /// Set of Guitar Hero Live guitar tracks + /// + public GHLInstrument? GHLGuitar + { + get => _ghlGuitar; + set => _ghlGuitar = value is null ? value : value with { InstrumentIdentity = GHLInstrumentIdentity.Guitar }; + } + private GHLInstrument? _ghlGuitar; + /// + /// Set of Guitar Hero Live bass tracks + /// + public GHLInstrument? GHLBass + { + get => _ghlBass; + set => _ghlBass = value is null ? value : value with { InstrumentIdentity = GHLInstrumentIdentity.Bass }; + } + private GHLInstrument? _ghlBass; + /// + /// Set of lead guitar tracks + /// + public StandardInstrument? LeadGuitar + { + get => _leadGuitar; + set => _leadGuitar = value is null ? value : value with { InstrumentIdentity = StandardInstrumentIdentity.LeadGuitar }; + } + private StandardInstrument? _leadGuitar; + /// + /// Set of rhythm guitar tracks + /// + public StandardInstrument? RhythmGuitar + { + get => _rhythmGuitar; + set => _rhythmGuitar = value is null ? value : value with { InstrumentIdentity = StandardInstrumentIdentity.RhythmGuitar }; + } + private StandardInstrument? _rhythmGuitar; + /// + /// Set of coop guitar tracks + /// + public StandardInstrument? CoopGuitar + { + get => _coopGuitar; + set => _coopGuitar = value is null ? value : value with { InstrumentIdentity = StandardInstrumentIdentity.CoopGuitar }; + } + private StandardInstrument? _coopGuitar; + /// + /// Set of bass tracks + /// + public StandardInstrument? Bass + { + get => _bass; + set => _bass = value is null ? value : value with { InstrumentIdentity = StandardInstrumentIdentity.Bass }; + } + private StandardInstrument? _bass; + /// + /// Set of keyboard tracks + /// + public StandardInstrument? Keys + { + get => _keys; + set => _keys = value is null ? value : value with { InstrumentIdentity = StandardInstrumentIdentity.Keys }; + } + private StandardInstrument? _keys; + public Vocals? Vocals { get; set; } + + /// + /// Gets property value for an from a value. + /// + /// Instance of from the + /// Instrument to get + public Instrument? Get(InstrumentIdentity instrument) => instrument switch + { + InstrumentIdentity.Drums => Drums, + InstrumentIdentity.GHLGuitar => GHLGuitar, + InstrumentIdentity.GHLBass => GHLBass, + InstrumentIdentity.LeadGuitar => LeadGuitar, + InstrumentIdentity.RhythmGuitar => RhythmGuitar, + InstrumentIdentity.CoopGuitar => CoopGuitar, + InstrumentIdentity.Bass => Bass, + InstrumentIdentity.Keys => Keys, + InstrumentIdentity.Vocals => Vocals, + _ => throw new UndefinedEnumException(instrument) + }; + /// + /// Gets property value for an from a value. + /// + /// /// Instrument to get + /// Instance of where TChord is from the . + public GHLInstrument? Get(GHLInstrumentIdentity instrument) + { + Validator.ValidateEnum(instrument); + return Get((InstrumentIdentity)instrument) as GHLInstrument; + } + /// + /// Gets property value for an from a value. + /// + /// Instrument to get + /// Instance of where TChord is from the . + public StandardInstrument? Get(StandardInstrumentIdentity instrument) + { + Validator.ValidateEnum(instrument); + return Get((InstrumentIdentity)instrument) as StandardInstrument; + } + + public IEnumerable Existing() => this.NonNull().Where(instrument => !instrument.IsEmpty); + + public void Set(StandardInstrument instrument) + { + switch (instrument.InstrumentIdentity) + { + case StandardInstrumentIdentity.LeadGuitar: + _leadGuitar = instrument; + break; + case StandardInstrumentIdentity.RhythmGuitar: + _rhythmGuitar = instrument; + break; + case StandardInstrumentIdentity.CoopGuitar: + _coopGuitar = instrument; + break; + case StandardInstrumentIdentity.Bass: + _bass = instrument; + break; + case StandardInstrumentIdentity.Keys: + _keys = instrument; + break; + default: + throw new UndefinedEnumException(instrument.InstrumentIdentity); + } + } + public void Set(GHLInstrument instrument) + { + switch (instrument.InstrumentIdentity) + { + case GHLInstrumentIdentity.Guitar: + GHLGuitar = instrument; + break; + case GHLInstrumentIdentity.Bass: + GHLBass = instrument; + break; + default: + throw new UndefinedEnumException(instrument.InstrumentIdentity); + } + } + + public IEnumerator GetEnumerator() => new Instrument?[] { Drums, GHLGuitar, GHLBass, LeadGuitar, RhythmGuitar, CoopGuitar, Bass, Keys }.NonNull().GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); +} diff --git a/source - Copie/Instruments/StandardInstrument.cs b/source - Copie/Instruments/StandardInstrument.cs new file mode 100644 index 00000000..b1fe9681 --- /dev/null +++ b/source - Copie/Instruments/StandardInstrument.cs @@ -0,0 +1,51 @@ +using ChartTools.IO; +using ChartTools.IO.Chart; +using ChartTools.IO.Configuration; +using ChartTools.IO.Formatting; + +namespace ChartTools; + +public record StandardInstrument : Instrument +{ + public new StandardInstrumentIdentity InstrumentIdentity { get; init; } + + + /// + /// Format of lead guitar and bass. Not applicable to other instruments. + /// + public MidiInstrumentOrigin MidiOrigin + { + get => midiOrigin; + set + { + if (value is MidiInstrumentOrigin.GuitarHero1 && InstrumentIdentity is not StandardInstrumentIdentity.LeadGuitar) + throw new ArgumentException($"{InstrumentIdentity} is not supported by Guitar Hero 1.", nameof(value)); + + midiOrigin = value; + } + } + private MidiInstrumentOrigin midiOrigin; + + public StandardInstrument() { } + public StandardInstrument(StandardInstrumentIdentity identity) => InstrumentIdentity = identity; + + protected override InstrumentIdentity GetIdentity() => (InstrumentIdentity)InstrumentIdentity; + + #region File reading + [Obsolete($"Use {nameof(ChartFile.ReadInstrument)}.")] + public static StandardInstrument? FromFile(string path, StandardInstrumentIdentity instrument, ReadingConfiguration? config = default, FormattingRules? formatting = default) + { + Validator.ValidateEnum(instrument); + return ExtensionHandler.Read(path, (".chart", path => ChartFile.ReadInstrument(path, instrument, config, formatting))); + } + + [Obsolete($"Use {nameof(ChartFile.ReadInstrumentAsync)}.")] + public static async Task FromFileAsync(string path, StandardInstrumentIdentity instrument, ReadingConfiguration? config = default, FormattingRules? formatting = default, CancellationToken cancellationToken = default) => await ExtensionHandler.ReadAsync(path, (".chart", path => ChartFile.ReadInstrumentAsync(path, instrument, config, formatting, cancellationToken))); + + [Obsolete($"Use {nameof(ChartFile.ReadInstrument)} with {nameof(Metadata.Formatting)}.")] + public static DirectoryResult FromDirectory(string directory, StandardInstrumentIdentity instrument, ReadingConfiguration? config = default) => DirectoryHandler.FromDirectory(directory, (path, formatting) => FromFile(path, instrument, config, formatting)); + + [Obsolete($"Use {nameof(ChartFile.ReadInstrumentAsync)} with {nameof(Metadata.Formatting)}.")] + public static Task> FromDirectoryAsync(string directory, StandardInstrumentIdentity instrument, ReadingConfiguration? config = default, CancellationToken cancellationToken = default) => DirectoryHandler.FromDirectoryAsync(directory, async (path, formatting) => await FromFileAsync(path, instrument, config, formatting, cancellationToken), cancellationToken); + #endregion +} diff --git a/source - Copie/Instruments/Vocals.cs b/source - Copie/Instruments/Vocals.cs new file mode 100644 index 00000000..780d7d63 --- /dev/null +++ b/source - Copie/Instruments/Vocals.cs @@ -0,0 +1,20 @@ +using ChartTools.IO; +using ChartTools.IO.Chart; +using ChartTools.IO.Configuration; +using ChartTools.IO.Formatting; +using ChartTools.Lyrics; + +namespace ChartTools; + +public record Vocals : Instrument +{ + protected override InstrumentIdentity GetIdentity() => InstrumentIdentity.Vocals; + + #region File reading + [Obsolete($"Use {nameof(ChartFile.ReadVocals)}.")] + public static Vocals? FromFile(string path, ReadingConfiguration? config = default, FormattingRules? formatting = default) => ExtensionHandler.Read(path, (".chart", path => ChartFile.ReadVocals(path))); + + [Obsolete($"Use {nameof(ChartFile.ReadVocalsAsync)}.")] + public static async Task FromFileAsync(string path, ReadingConfiguration? config = default, FormattingRules? formatting = default, CancellationToken cancellationToken = default) => await ExtensionHandler.ReadAsync(path, (".chart", path => ChartFile.ReadVocalsAsync(path))); + #endregion +} diff --git a/source - Copie/Metadata/Charter.cs b/source - Copie/Metadata/Charter.cs new file mode 100644 index 00000000..f2d6105f --- /dev/null +++ b/source - Copie/Metadata/Charter.cs @@ -0,0 +1,25 @@ +using ChartTools.IO.Chart; +using ChartTools.IO.Chart.Serializing; +using ChartTools.IO.Ini; + +namespace ChartTools; + +/// +/// Creator of the chart +/// +public class Charter +{ + /// + /// Name of the creator + /// + [ChartKeySerializable(ChartFormatting.Charter)] + public string? Name { get; set; } + + /// + /// Location of the image file to use as an icon in the Clone Hero song browser + /// + [IniKeySerializable(IniFormatting.Icon)] + public string? Icon { get; set; } + + public override string ToString() => Name ?? string.Empty; +} diff --git a/source - Copie/Metadata/InstrumentDifficultySet.cs b/source - Copie/Metadata/InstrumentDifficultySet.cs new file mode 100644 index 00000000..112c5670 --- /dev/null +++ b/source - Copie/Metadata/InstrumentDifficultySet.cs @@ -0,0 +1,80 @@ +using ChartTools.IO.Ini; + +using System.Reflection; + +namespace ChartTools; + +/// +/// Stores the estimated difficulties for instruments +/// +public class InstrumentDifficultySet +{ + /// + /// Difficulty of , and + /// + [IniKeySerializable(IniFormatting.GuitarDifficulty)] + public sbyte? Guitar { get; set; } + /// + /// Difficulty of + /// + [IniKeySerializable(IniFormatting.BassDifficulty)] + public sbyte? Bass { get; set; } + /// + /// Difficulty of + /// + [IniKeySerializable(IniFormatting.DrumsDifficulty)] + public sbyte? Drums { get; set; } + /// + /// Difficulty of + /// + [IniKeySerializable(IniFormatting.KeysDifficulty)] + public sbyte? Keys { get; set; } + /// + /// Difficulty of + /// + [IniKeySerializable(IniFormatting.GHLGuitarDifficulty)] + public sbyte? GHLGuitar { get; set; } + /// + /// Difficulty of + /// + [IniKeySerializable(IniFormatting.GHLBassDifficulty)] + public sbyte? GHLBass { get; set; } + + /// + /// Gets the difficulty for an . + /// + public sbyte? GetDifficulty(InstrumentIdentity identity) => GetDifficultyProperty(identity, out var info) ? (sbyte?)info!.GetValue(this) : null; + /// + /// Sets the difficulty for an . + /// + public void SetDifficulty(InstrumentIdentity identity, sbyte? difficulty) + { + if (GetDifficultyProperty(identity, out var info)) + info!.SetValue(this, difficulty); + } + + private bool GetDifficultyProperty(InstrumentIdentity identity, out PropertyInfo? info) + { + Validator.ValidateEnum(identity); + var propName = identity switch + { + InstrumentIdentity.LeadGuitar or InstrumentIdentity.CoopGuitar or InstrumentIdentity.RhythmGuitar => nameof(Guitar), + InstrumentIdentity.Bass => nameof(Bass), + InstrumentIdentity.Drums => nameof(Drums), + InstrumentIdentity.Keys => nameof(Keys), + InstrumentIdentity.GHLGuitar => nameof(GHLGuitar), + InstrumentIdentity.GHLBass => nameof(GHLBass), + _ => null + }; + + if (propName is null) + { + info = null; + return false; + } + + info = typeof(InstrumentDifficultySet).GetProperty(propName); + + return true; + } +} diff --git a/source - Copie/Metadata/Metadata.cs b/source - Copie/Metadata/Metadata.cs new file mode 100644 index 00000000..cb9d63c9 --- /dev/null +++ b/source - Copie/Metadata/Metadata.cs @@ -0,0 +1,225 @@ +using ChartTools.Extensions; +using ChartTools.IO; +using ChartTools.IO.Chart; +using ChartTools.IO.Chart.Serializing; +using ChartTools.IO.Formatting; +using ChartTools.IO.Ini; + +namespace ChartTools; + +/// +/// Set of miscellaneous information about a +/// +public class Metadata +{ + #region Properties + /// + /// Title of the + /// + [ChartKeySerializable(ChartFormatting.Title)] + [IniKeySerializable(IniFormatting.Title)] + public string? Title { get; set; } + + /// + /// Artist or band behind the + /// + [ChartKeySerializable(ChartFormatting.Artist)] + [IniKeySerializable(IniFormatting.Artist)] + public string? Artist { get; set; } + + /// + /// Album featuring the + /// + [ChartKeySerializable(ChartFormatting.Album)] + [IniKeySerializable(IniFormatting.Album)] + public string? Album { get; set; } + + /// + /// Track number of the song within the album + /// + public ushort? AlbumTrack { get; set; } + + /// + /// Playlist that the song should show up in + /// + [IniKeySerializable(IniFormatting.Playlist)] + public string? Playlist { get; set; } + + /// + /// Sub-playlist that the song should show up in + /// + [IniKeySerializable(IniFormatting.SubPlaylist)] + public string? SubPlaylist { get; set; } + + /// + /// Track number of the song within the playlist/setlist + /// + [IniKeySerializable(IniFormatting.PlaylistTrack)] + public ushort? PlaylistTrack { get; set; } + + /// + /// Year of release + /// + [IniKeySerializable(IniFormatting.Year)] + public ushort? Year { get; set; } + + /// + /// Genre of the + /// + [ChartKeySerializable(ChartFormatting.Genre)] + [IniKeySerializable(IniFormatting.Genre)] + public string? Genre { get; set; } + + /// + /// Creator of the chart + /// + public Charter Charter + { + get => _charter; + set => _charter = value ?? throw new ArgumentNullException(nameof(value)); + } + private Charter _charter = new(); + + /// + /// Start time in milliseconds of the preview in the Clone Hero song browser + /// + [ChartKeySerializable(ChartFormatting.PreviewStart)] + [IniKeySerializable(IniFormatting.PreviewStart)] + public uint? PreviewStart { get; set; } + + /// + /// End time in milliseconds of the preview in the Clone Hero song browser + /// + [ChartKeySerializable(ChartFormatting.PreviewEnd)] + [IniKeySerializable(IniFormatting.PreviewEnd)] + public uint? PreviewEnd { get; set; } + + /// + /// Duration in milliseconds of the preview in the Clone Hero song browser + /// + public uint PreviewLength + { + get + { + if (PreviewEnd is null) + return 30000; + + return PreviewStart is null ? PreviewEnd.Value : PreviewEnd.Value - PreviewStart.Value; + } + } + + /// + /// Overall difficulty of the song + /// + [ChartKeySerializable(ChartFormatting.Difficulty)] + [IniKeySerializable(IniFormatting.Difficulty)] + public sbyte? Difficulty { get; set; } + /// + public InstrumentDifficultySet InstrumentDifficulties + { + get => _instrumentDifficulties; + set => _instrumentDifficulties = value ?? throw new ArgumentNullException(nameof(value)); + } + private InstrumentDifficultySet _instrumentDifficulties = new(); + /// + /// Type of media the audio track comes from + /// + [ChartKeySerializable(ChartFormatting.MediaType)] + public string? MediaType { get; set; } + + /// + /// Offset of the audio track. A higher value makes the audio start sooner. + /// + [ChartKeySerializable(ChartFormatting.AudioOffset)] + [IniKeySerializable(IniFormatting.AudioOffset)] + public TimeSpan? AudioOffset { get; set; } + + /// + /// Paths of audio files + /// + public StreamCollection Streams + { + get => _streams; + set => _streams = value ?? throw new ArgumentNullException(nameof(value)); + } + private StreamCollection _streams = new(); + + /// + /// Offset of the background video. A higher value makes the video start sooner. + /// + public TimeSpan? VideoOffset { get; set; } + + /// + /// Length of the song in milliseconds + /// + [IniKeySerializable(IniFormatting.Length)] + public uint? Length { get; set; } + + /// + /// Text to be displayed on the load screen + /// + [IniKeySerializable(IniFormatting.LoadingText)] + public string? LoadingText { get; set; } + + /// + /// The song is a modchart + /// + [IniKeySerializable(IniFormatting.Modchart)] + public bool IsModchart { get; set; } + + + private FormattingRules _formatting = new(); + /// + public FormattingRules Formatting + { + get => _formatting; + set => _formatting = value ?? throw new ArgumentNullException(nameof(value)); + } + + /// + /// Unrecognized metadata + /// + /// When writing, these will only be written if the target format matches the origin + public HashSet UnidentifiedData { get; } = new(new FuncEqualityComparer((a, b) => a.Key == b.Key)); + #endregion + + public void ReadFile(string path) => Read(path, this); + /// + /// Reads the metadata from a file. + /// + /// Path of the file to read + /// + /// + /// + /// + /// + public static Metadata FromFile(string path) => Read(path); + + private static Metadata Read(string path, Metadata? existing = null) => ExtensionHandler.Read(path, (".chart", ChartFile.ReadMetadata), (".ini", path => IniFile.ReadMetadata(path, existing))); + + /// + /// Reads the metadata from multiple files. + /// + /// Each file has less priority than the preceding. + /// Paths of the files to read + /// + /// + /// + /// + /// + public static Metadata? FromFiles(params string[] paths) + { + // No files provided + if (paths is null || paths.Length == 0) + throw new ArgumentException("No provided paths"); + + var data = FromFile(paths[0]); + + foreach (var path in paths[1..]) + data.ReadFile(path); + + return data; + } + + public void ToFile(string path) => ExtensionHandler.Write(path, this, (".ini", IniFile.WriteMetadata)); +} diff --git a/source - Copie/Metadata/StreamCollection.cs b/source - Copie/Metadata/StreamCollection.cs new file mode 100644 index 00000000..d419fdbc --- /dev/null +++ b/source - Copie/Metadata/StreamCollection.cs @@ -0,0 +1,70 @@ +using ChartTools.IO.Chart; +using ChartTools.IO.Chart.Serializing; + +namespace ChartTools; + +/// +/// Set of audio files to play and mute during gameplay +/// +/// Instrument audio may be muted when chords of the respective instrument are missed +public class StreamCollection +{ + /// + /// Location of the base audio file + /// + [ChartKeySerializable(ChartFormatting.MusicStream)] + public string? Music { get; set; } + /// + /// Location of the guitar audio file + /// + [ChartKeySerializable(ChartFormatting.GuitarStream)] + public string? Guitar { get; set; } + /// + /// Location of the bass audio + /// + [ChartKeySerializable(ChartFormatting.BassStream)] + public string? Bass { get; set; } + /// + /// Location of the rhythm guitar audio file + /// + [ChartKeySerializable(ChartFormatting.RhythmStream)] + public string? Rhythm { get; set; } + /// + /// Location of the keys audio file + /// + [ChartKeySerializable(ChartFormatting.KeysStream)] + public string? Keys { get; set; } + /// + /// Location of the drums' kicks audio file + /// + /// Can include all drums audio + [ChartKeySerializable(ChartFormatting.DrumStream)] + public string? Drum { get; set; } + /// + /// Location of the drums' snares audio file + /// + /// Can include all drums audio except kicks + [ChartKeySerializable(ChartFormatting.Drum2Stream)] + public string? Drum2 { get; set; } + /// + /// Location of the drum's toms audio file + /// + /// Can include toms and cymbals + [ChartKeySerializable(ChartFormatting.Drum3Stream)] + public string? Drum3 { get; set; } + /// + /// Location of the drum's cymbals audio file + /// + [ChartKeySerializable(ChartFormatting.Drum4Stream)] + public string? Drum4 { get; set; } + /// + /// Location of the vocals audio file + /// + [ChartKeySerializable(ChartFormatting.VocalStream)] + public string? Vocals { get; set; } + /// + /// Location of the crowd reaction audio file + /// + [ChartKeySerializable(ChartFormatting.CrowdStream)] + public string? Crowd { get; set; } +} diff --git a/source - Copie/Metadata/UnidentifiedMetadata.cs b/source - Copie/Metadata/UnidentifiedMetadata.cs new file mode 100644 index 00000000..2716e6ed --- /dev/null +++ b/source - Copie/Metadata/UnidentifiedMetadata.cs @@ -0,0 +1,8 @@ +namespace ChartTools; + +public struct UnidentifiedMetadata +{ + public string Key { get; init; } + public string? Value { get; set; } + public FileType Origin { get; set; } +} diff --git a/source - Copie/Notes/DrumsNote.cs b/source - Copie/Notes/DrumsNote.cs new file mode 100644 index 00000000..8a0e5da2 --- /dev/null +++ b/source - Copie/Notes/DrumsNote.cs @@ -0,0 +1,32 @@ +namespace ChartTools; + +/// +/// Note played by drums +/// +public class DrumsNote : LaneNote +{ + private bool _isCymbal = false; + /// + /// if the cymbal must be hit instead of the pad on supported drum sets + /// + /// notes cannot be cymbal. + public bool IsCymbal + { + get => _isCymbal; + set + { + if ((Lane == DrumsLane.Red || Lane == DrumsLane.Green5Lane) && value) + throw new InvalidOperationException("Red and 5-lane green notes cannot be cymbal."); + + _isCymbal = value; + } + } + + public DrumsNote() : base() { } + public DrumsNote(DrumsLane lane) : base(lane) { } + + /// + /// Determines if the note is played by kicking + /// + public bool IsKick => Lane is DrumsLane.Kick or DrumsLane.DoubleKick; +} diff --git a/source - Copie/Notes/INote.cs b/source - Copie/Notes/INote.cs new file mode 100644 index 00000000..6934db0f --- /dev/null +++ b/source - Copie/Notes/INote.cs @@ -0,0 +1,9 @@ +namespace ChartTools; + +public interface INote : ILongObject +{ + /// + /// Numerical value of the note identity + /// + public byte Index { get; } +} diff --git a/source - Copie/Notes/LaneNote.cs b/source - Copie/Notes/LaneNote.cs new file mode 100644 index 00000000..e86be78c --- /dev/null +++ b/source - Copie/Notes/LaneNote.cs @@ -0,0 +1,16 @@ +namespace ChartTools; + +public abstract class LaneNote : INote +{ + public abstract byte Index { get; } + + /// + /// Maximum length the note can be held for extra points + /// + public uint Sustain { get; set; } + uint ILongObject.Length + { + get => Sustain; + set => Sustain = value; + } +} diff --git a/source - Copie/Notes/LaneNoteCollection.cs b/source - Copie/Notes/LaneNoteCollection.cs new file mode 100644 index 00000000..305b972d --- /dev/null +++ b/source - Copie/Notes/LaneNoteCollection.cs @@ -0,0 +1,95 @@ +using System.Collections; + +namespace ChartTools; + +public class LaneNoteCollection : ICollection, IReadOnlyList where TNote : LaneNote, new() where TLane : struct, Enum +{ + private readonly List _notes = new(); + + /// + /// If , trying to combine an open note with other notes will remove the current ones. + /// + public bool OpenExclusivity { get; } + public int Count => _notes.Count; + bool ICollection.IsReadOnly => false; + + public LaneNoteCollection(bool openExclusivity) => OpenExclusivity = openExclusivity; + + public void Add(TLane lane) => AddNonNull(new TNote() { Lane = lane }); + /// + /// Adds a note to the . + /// + /// Adding a note that already exists will overwrite the existing note. + /// If is , combining an open note with other notes will remove the current ones. + /// + /// Note to add + public void Add(TNote note) => AddNonNull(note ?? throw new ArgumentNullException(nameof(note))); + private void AddNonNull(TNote note) + { + if (OpenExclusivity && (note.Index == 0 || Count == 1 && this[0].Index == 0)) // An open note is present and needs to be removed + Clear(); + + _notes.Add(note); + } + + public void Clear() => _notes.Clear(); + + /// + /// Determines if any note matches the lane of a given note. + /// + /// + public bool Contains(TNote note) => note is null ? throw new ArgumentNullException(nameof(note)) : Contains(note.Lane); + /// + /// Determines if any note matches a given lane. + /// + public bool Contains(TLane lane) => _notes.Any(note => note.Lane.Equals(lane)); + /// + /// Determines if any note matches a given index. + /// + public bool Contains(byte index) => _notes.Any(note => note.Index == index); + + public void CopyTo(TNote[] array, int arrayIndex) => _notes.CopyTo(array, arrayIndex); + + /// + /// Removes the note that matches the lane of a given note. + /// + /// if a matching note was found. + public bool Remove(TNote note) => Remove(note.Lane); + /// + /// Removes the note that matches a given lane. + /// + /// if a matching note was found. + public bool Remove(TLane lane) => Remove(n => n.Lane.Equals(lane)); + /// + /// Removes the note that matches a given index. + /// + /// if a matching note was found. + public bool Remove(byte index) => Remove(n => n.Index == index); + private bool Remove(Predicate match) + { + var index = _notes.FindIndex(match); + + if (index is -1) + return false; + + _notes.RemoveAt(index); + return true; + } + + /// + /// Gets the note matching a given lane. + /// + /// Lane of the note + /// Note with the lane if present, otherwise . + public TNote? this[TLane lane] => _notes.FirstOrDefault(n => n.Lane.Equals(lane)); + /// + /// Gets the note at a given index based on order or addition. + /// + /// Index of the note in the collection, not to be confused with . + /// Note at the index + /// + public TNote this[int index] => _notes[index]; + + public IEnumerator GetEnumerator() => _notes.GetEnumerator(); + IEnumerator IEnumerable.GetEnumerator() => _notes.GetEnumerator(); +} diff --git a/source - Copie/Notes/LaneNoteGeneric.cs b/source - Copie/Notes/LaneNoteGeneric.cs new file mode 100644 index 00000000..c973cd07 --- /dev/null +++ b/source - Copie/Notes/LaneNoteGeneric.cs @@ -0,0 +1,24 @@ +using System.Runtime.CompilerServices; + +namespace ChartTools; + +/// +/// Base class for notes +/// +public class LaneNote : LaneNote where TLane : struct, Enum +{ + public override byte Index => Unsafe.As(ref _lane); + public TLane Lane + { + get => _lane; + init => _lane = value; + } + private TLane _lane; + + public LaneNote() { } + public LaneNote(TLane lane, uint sustain = 0) + { + Lane = lane; + Sustain = sustain; + } +} diff --git a/source - Copie/Notes/Syllable.cs b/source - Copie/Notes/Syllable.cs new file mode 100644 index 00000000..07fc4100 --- /dev/null +++ b/source - Copie/Notes/Syllable.cs @@ -0,0 +1,61 @@ +namespace ChartTools.Lyrics; + +/// +/// Karaoke step of a +/// +public class Syllable : INote +{ + /// + /// Position offset from the + /// + public uint PositionOffset { get; set; } + /// + /// Position offset of the end from the + /// + public uint EndPositionOffset => PositionOffset + Length; + /// + /// Duration of the syllable in ticks + /// + public uint Length { get; set; } + + /// + /// Pitch to sing + /// + /// Although the octave is specified, some games only require the player to match the key.

Chart files do not support pitches.
+ public VocalsPitch Pitch { get; set; } = new(); + public byte Index => (byte)Pitch.Value; + + private string _rawText = string.Empty; + /// + /// Unformatted text data + /// + /// Setting to will set to an empty string. + public string RawText + { + get => _rawText; + set => _rawText = value is null ? string.Empty : value; + } + /// + /// Text formatted to its in-game appearance + /// + public string DisplayedText => RawText.Replace("-", "").Replace('=', '-').Trim('+', '#', '^', '*'); + /// + /// if is the last syllable or the only syllable of its word + /// + public bool IsWordEnd + { + get => RawText.Length == 0 || RawText[^1] is '§' or '_' or not '-' and not '='; + set + { + if (value) + { + if (!IsWordEnd) + RawText = RawText[..^1]; + } + else if (IsWordEnd) + RawText += '-'; + } + } + public Syllable(uint offset) => PositionOffset = offset; + public Syllable(uint offset, VocalsPitch pitch) : this(offset) => Pitch = pitch; +} diff --git a/source - Copie/Notes/VocalsPitch.cs b/source - Copie/Notes/VocalsPitch.cs new file mode 100644 index 00000000..5fc02652 --- /dev/null +++ b/source - Copie/Notes/VocalsPitch.cs @@ -0,0 +1,72 @@ +namespace ChartTools.Lyrics; + +/// +/// Wrapper type for with helper properties to get the pitch and key +/// +public readonly struct VocalsPitch : IEquatable, IEquatable +{ + /// + /// Pitch value + /// + public VocalPitchValue Value { get; } + /// + /// Key excluding the octave + /// + public VocalsKey Key => (VocalsKey)((int)Value & 0x0F); + /// + /// Octave number + /// + public byte Octave => (byte)(((int)Value & 0xF0) >> 4); + + /// + /// Creates a pitch from a raw pitch value. + /// + /// + public VocalsPitch(VocalPitchValue value) => Value = value; + + #region Equals + /// + /// Indicates if two pitches have the same value. + /// + /// Pitch to compare + public bool Equals(VocalsPitch other) => Value == other.Value; + /// + /// Indicates if a pitch has a value equal to a raw pitch value. + /// + /// Value to compare + public bool Equals(VocalPitchValue other) => Value == other; + /// + /// Indicates if an object is a raw pitch value or wrapper and the value is equal. + /// + /// Source of value + public override bool Equals(object? obj) => obj is VocalPitchValue value && Equals(value) || obj is VocalsPitch wrapper && Equals(wrapper); + #endregion + + #region Operators + /// + /// Converts a raw pitch value to a matching wrapper. + /// + /// Pitch value + public static implicit operator VocalsPitch(VocalPitchValue pitch) => new(pitch); + + /// + public static bool operator ==(VocalsPitch left, VocalsPitch right) => left.Equals(right); + /// + /// Indicates if two pitches don't have the same value. + /// + public static bool operator !=(VocalsPitch left, VocalsPitch right) => !(left == right); + /// + /// Indicates if the left pitch has a lower value than the right pitch according to music theory. + /// + public static bool operator <(VocalsPitch left, VocalsPitch right) => left.Value < right.Value; + /// + /// Indicates if the left pitch has a higher value than the right pitch according to music theory. + /// + public static bool operator >(VocalsPitch left, VocalsPitch right) => left.Value > right.Value; + #endregion + + /// + /// Returns the hash code for the pitch value. + /// + public override int GetHashCode() => (int)Value; +} diff --git a/source - Copie/Song.cs b/source - Copie/Song.cs new file mode 100644 index 00000000..e428b6b4 --- /dev/null +++ b/source - Copie/Song.cs @@ -0,0 +1,123 @@ +using ChartTools.IO; +using ChartTools.IO.Chart; +using ChartTools.IO.Ini; +using ChartTools.Lyrics; + +using ChartTools.IO.Configuration; +using ChartTools.Events; +using ChartTools.Tools; +using ChartTools.IO.Formatting; + +namespace ChartTools; + +/// +/// Song playable in Clone Hero +/// +public class Song +{ + /// + /// Set of information about the song not unrelated to instruments, syncing or events + /// + public Metadata Metadata + { + get => _metadata; + set => _metadata = value ?? throw new ArgumentNullException(nameof(value)); + } + private Metadata _metadata = new(); + + /// + public FormattingRules Formatting + { + get => _formatting; + set => _formatting = value ?? throw new ArgumentNullException(nameof(value)); + } + private FormattingRules _formatting = new(); + + /// + public SyncTrack SyncTrack + { + get => _syncTrack; + set => _syncTrack = value ?? throw new ArgumentNullException(nameof(value)); + } + private SyncTrack _syncTrack = new(); + + /// + /// List of events common to all instruments + /// + public List GlobalEvents + { + get => _globalEvents; + set => _globalEvents = value ?? throw new ArgumentNullException(nameof(value)); + } + private List _globalEvents = new(); + + /// + public InstrumentSet Instruments + { + get => _instruments; + set => _instruments = value ?? throw new ArgumentNullException(nameof(value)); + } + private InstrumentSet _instruments = new(); + + public ChartSection? UnknownChartSections { get; set; } + + #region Reading + /// + /// Reads all elements of a from a file. + /// + /// Path of the file + /// + /// + public static Song FromFile(string path, ReadingConfiguration? config = default, FormattingRules? formatting = default) => ExtensionHandler.Read(path, (".chart", path => ChartFile.ReadSong(path, config, formatting)), (".ini", path => new Song { Metadata = IniFile.ReadMetadata(path) })); + /// + /// Reads all elements of a from a file asynchronously using multitasking. + /// + /// + /// /// + /// Token to request cancellation + public static async Task FromFileAsync(string path, ReadingConfiguration? config = default, FormattingRules? formatting = default, CancellationToken cancellationToken = default) => await ExtensionHandler.ReadAsync(path, (".chart", path => ChartFile.ReadSongAsync(path, config, formatting, cancellationToken))); + + public static Song FromDirectory(string directory, ReadingConfiguration? config = default) + { + (var song, var metadata) = DirectoryHandler.FromDirectory(directory, (path, formatting) => FromFile(path, config, formatting)); + song ??= new(); + + PropertyMerger.Merge(song.Metadata, true, true, metadata); + + return song; + } + public static async Task FromDirectoryAsync(string directory, ReadingConfiguration? config = default, CancellationToken cancellationToken = default) + { + (var song, var metadata) = await DirectoryHandler.FromDirectoryAsync(directory, async (path, formatting) => await FromFileAsync(path, config, formatting, cancellationToken), cancellationToken); + song ??= new(); + + PropertyMerger.Merge(song.Metadata, true, true, metadata); + + return song; + } + #endregion + + /// + /// Writes the to a file. + /// + /// + /// + /// + /// + /// + /// + /// + /// + public void ToFile(string path, WritingConfiguration? config = default) => ExtensionHandler.Write(path, this, (".chart", (path, song) => ChartFile.WriteSong(path, song, config))); + public async Task ToFileAsync(string path, WritingConfiguration? config = default, CancellationToken cancellationToken = default) => await ExtensionHandler.WriteAsync(path, this, (".chart", (path, song) => ChartFile.WriteSongAsync(path, song, config, cancellationToken))); + + /// + /// Retrieves the lyrics from the global events. + /// + public IEnumerable GetLyrics() => GlobalEvents is null ? Enumerable.Empty() : GlobalEvents.GetLyrics(); + /// + /// Replaces phrase and lyric events from with the ones making up a set of . + /// + /// Phrases to use as a replacement + public void SetLyrics(IEnumerable phrases) => GlobalEvents = (GlobalEvents ?? new()).SetLyrics(phrases).ToList(); +} diff --git a/source - Copie/Special/InstrumentSpecialPhrase.cs b/source - Copie/Special/InstrumentSpecialPhrase.cs new file mode 100644 index 00000000..7e052cb0 --- /dev/null +++ b/source - Copie/Special/InstrumentSpecialPhrase.cs @@ -0,0 +1,37 @@ +namespace ChartTools; + +/// +/// Phrase related to an instrument that triggers an in-game event. +/// +public class InstrumentSpecialPhrase : SpecialPhrase +{ + /// + /// Type of the phrase that drives the gameplay effect + /// + public InstrumentSpecialPhraseType Type + { + get + { + var typeEnum = (InstrumentSpecialPhraseType)TypeCode; + return Enum.IsDefined(typeEnum) ? typeEnum : InstrumentSpecialPhraseType.Unknown; + } + set => TypeCode = value == InstrumentSpecialPhraseType.Unknown ? throw new ArgumentException($"{InstrumentSpecialPhraseType.Unknown} is not a valid explicit value.", nameof(value)) : (byte)value; + } + + /// + /// Creates an instance of . + /// + /// Effect of the phrase + /// + public InstrumentSpecialPhrase(uint position, InstrumentSpecialPhraseType type, uint length = 0) : base(position, (byte)type, length) { } + /// + /// + /// + /// + public InstrumentSpecialPhrase(uint position, byte typeCode, uint length = 0) : base(position, typeCode, length) { } + + public override bool Equals(object? obj) => Equals(obj as TrackSpecialPhrase); + public bool Equals(TrackSpecialPhrase? other) => base.Equals(other); + + public override int GetHashCode() => base.GetHashCode(); +} diff --git a/source - Copie/Special/SpecialPhrase.cs b/source - Copie/Special/SpecialPhrase.cs new file mode 100644 index 00000000..1c7e9a5d --- /dev/null +++ b/source - Copie/Special/SpecialPhrase.cs @@ -0,0 +1,28 @@ +namespace ChartTools; + +/// +/// Base class for phrases that define an in-game event with a duration such as star power. +/// +public abstract class SpecialPhrase : TrackObjectBase, ILongTrackObject +{ + /// + /// Numerical value of the phrase type + /// + public byte TypeCode { get; set; } + /// + /// Duration of the phrase in ticks + /// + public uint Length { get; set; } + + /// + /// Base constructor of special phrases. + /// + /// Position of the phrase + /// Effect of the phrase + /// Duration in ticks + public SpecialPhrase(uint position, byte typeCode, uint length = 0) : base(position) + { + TypeCode = typeCode; + Length = length; + } +} diff --git a/source - Copie/Special/TrackSpecialPhrase.cs b/source - Copie/Special/TrackSpecialPhrase.cs new file mode 100644 index 00000000..273535b1 --- /dev/null +++ b/source - Copie/Special/TrackSpecialPhrase.cs @@ -0,0 +1,39 @@ +namespace ChartTools; + +/// +/// Phrase related to a track that triggers an in-game event. +/// +public class TrackSpecialPhrase : SpecialPhrase +{ + /// + /// Type of the phrase that drives the gameplay effect + /// + public TrackSpecialPhraseType Type + { + get + { + var typeEnum = (TrackSpecialPhraseType)TypeCode; + return Enum.IsDefined(typeEnum) ? typeEnum : TrackSpecialPhraseType.Unknown; + } + set => TypeCode = value == TrackSpecialPhraseType.Unknown ? throw new ArgumentException($"{TrackSpecialPhraseType.Unknown} is not a valid explicit value.", nameof(value)) : (byte)value; + } + + public bool IsFaceOff => Type is TrackSpecialPhraseType.Player1FaceOff or TrackSpecialPhraseType.Player2FaceOff; + + /// + /// Creates an instance of . + /// + /// Effect of the phrase + /// + public TrackSpecialPhrase(uint position, TrackSpecialPhraseType type, uint length = 0) : base(position, (byte)type, length) { } + /// + /// + /// + /// + public TrackSpecialPhrase(uint position, byte typeCode, uint length = 0) : base(position, typeCode, length) { } + + public override bool Equals(object? obj) => Equals(obj as TrackSpecialPhrase); + public bool Equals(TrackSpecialPhrase? other) => base.Equals(other); + + public override int GetHashCode() => base.GetHashCode(); +} diff --git a/source - Copie/Sync/SyncTrack.cs b/source - Copie/Sync/SyncTrack.cs new file mode 100644 index 00000000..a48712b0 --- /dev/null +++ b/source - Copie/Sync/SyncTrack.cs @@ -0,0 +1,40 @@ +using ChartTools.IO; +using ChartTools.IO.Chart; +using ChartTools.IO.Configuration; + +namespace ChartTools; + +/// +/// Set of markers that define the time signature and tempo +/// +public class SyncTrack : IEmptyVerifiable +{ + /// + public bool IsEmpty => Tempo.Count == 0 && TimeSignatures.Count == 0; + + /// + /// Tempo markers + /// + public TempoMap Tempo { get; } = new(); + /// + /// Time signature markers + /// + public List TimeSignatures { get; } = new(); + + /// + /// Reads a from a file. + /// + /// Path of the file + /// + public static SyncTrack FromFile(string path, ReadingConfiguration? config = default) => ExtensionHandler.Read(path, (".chart", path => ChartFile.ReadSyncTrack(path, config))); + /// + /// Reads a from a file asynchronously using multitasking. + /// + /// + /// Token to request cancellation + /// + /// + public static async Task FromFileAsync(string path, ReadingConfiguration? config = default, CancellationToken cancellationToken = default) => await ExtensionHandler.ReadAsync(path, (".chart", path => ChartFile.ReadSyncTrackAsync(path, config, cancellationToken))); + public void ToFile(string path, WritingConfiguration? config = default) => ExtensionHandler.Write(path, this, (".chart", (path, track) => ChartFile.ReplaceSyncTrack(path, track, config))); + public async Task ToFileAsync(string path, WritingConfiguration? config = default, CancellationToken cancellationToken = default) => await ExtensionHandler.WriteAsync(path, this, (".chart", (path, track) => ChartFile.ReplaceSyncTrackAsync(path, track, config, cancellationToken))); +} diff --git a/source - Copie/Sync/Tempo.cs b/source - Copie/Sync/Tempo.cs new file mode 100644 index 00000000..e708d202 --- /dev/null +++ b/source - Copie/Sync/Tempo.cs @@ -0,0 +1,86 @@ +namespace ChartTools; + +/// +/// Marker that alters the tempo +/// +public class Tempo : TrackObjectBase +{ + /// + /// Parent map the marker is contained + /// + public TempoMap? Map + { + get => _map; + internal set + { + if (value is not null) + PositionSynced = false; + + _map = value; + } + } + private TempoMap? _map; + + /// + /// Only refer to the position if is . + public override uint Position + { + get => _position; + set + { + _position = value; + + if (Anchor is not null) + PositionSynced = false; + } + } + private uint _position; + + /// + /// New tempo in beats per minute + /// + public float Value { get; set; } + + /// + /// Locks the tempo to a specific real-time position independent of the sync track. + /// + public TimeSpan? Anchor + { + get => _anchor; + set + { + var valueNull = value is null; + + if (valueNull) + { + if (_anchor is not null) + Map?.RemoveAnchor(this); + } + else if (_anchor is null) + Map?.AddAnchor(this); + + _anchor = value; + PositionSynced = valueNull; + } + } + private TimeSpan? _anchor; + + /// + /// Indicates if the tick position is up to date with . + /// + /// if the marker has no anchor. + public bool PositionSynced { get; private set; } = true; + + /// + /// Creates an instance of . + /// + public Tempo(uint position, float value) : base(position) => Value = value; + public Tempo(TimeSpan anchor, float value) : this(0, value) => Anchor = anchor; + + internal void SyncPosition(uint position) + { + _position = position; + PositionSynced = true; + } + internal void DesyncPosition() => PositionSynced = false; +} diff --git a/source - Copie/Sync/TempoMap.cs b/source - Copie/Sync/TempoMap.cs new file mode 100644 index 00000000..59f0c05f --- /dev/null +++ b/source - Copie/Sync/TempoMap.cs @@ -0,0 +1,208 @@ +using System.Collections; + +namespace ChartTools; + +/// +/// Set of tempo markers that handles synchronism of anchored tempos. +/// +public class TempoMap : IList +{ + private readonly List _items = new(); + private readonly List _anchors = new(); + + public Tempo this[int index] + { + get => _items[index]; + set => _items[index] = value; + } + public int Count => _items.Count; + bool ICollection.IsReadOnly => false; + /// + /// Indicates if all anchored markers are synchronized. + /// + public bool Synchronized { get; private set; } + + private void AddBase(Tempo item) + { + item.Map = this; + + if (item.Anchor is not null) + _anchors.Add(item); + } + public void Add(Tempo item) + { + if (item is null) + throw new ArgumentNullException(nameof(item)); + + _items.Add(item); + + AddBase(item); + Desync(); + } + public void AddRange(IEnumerable items) + { + foreach (var item in items) + { + _items.Add(item); + AddBase(item); + } + + Desync(); + } + public void Clear() => _items.Clear(); + public void Clear(bool detachMap) + { + if (detachMap) + foreach (var tempo in _items) + tempo.Map = null; + + _items.Clear(); + } + public bool Contains(Tempo item) => _items.Contains(item); + public void CopyTo(Tempo[] array, int arrayIndex) => _items.CopyTo(array, arrayIndex); + public int IndexOf(Tempo item) => _items.IndexOf(item); + public void Insert(int index, Tempo item) + { + _items.Insert(index, item); + + AddBase(item); + Desync(); + } + public void InsertRange(int index, IEnumerable items) + { + foreach (var item in items) + { + _items.Insert(index, item); + AddBase(item); + } + + Desync(); + } + public bool Remove(Tempo item) => Remove(item, false); + public bool Remove(Tempo item, bool detachMap) + { + if (detachMap) + item.Map = null; + + if (item.Anchor is not null) + _anchors.Remove(item); + + var found = _items.Remove(item); + Desync(); + return found; + } + public void RemoveAt(int index) + { + _items.RemoveAt(index); + + var item = _items[index]; + if (item.Anchor is not null) + _anchors.Remove(item); + + Desync(); + } + public void RemoveAt(int index, bool detachMap) + { + if (detachMap) + { + var tempo = _items[index]; + tempo.Map = null; + } + + _items.RemoveAt(index); + + var item = _items[index]; + if (item.Anchor is not null) + _anchors.Remove(item); + + Desync(); + } + + public IEnumerator GetEnumerator() => _items.GetEnumerator(); + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + /// + /// Synchronizes anchored markers by calculating their tick position. + /// + /// + /// + /// + public void Synchronize(uint resolution, bool desyncedPreOrdered = false) + { + if (Synchronized) + return; + + List synced = new(); + List desynced = new(); + + // Split synced and desynced. Sync 0 anchors. + foreach (var tempo in _items) + { + if (tempo.PositionSynced) + synced.Add(tempo); + else if (tempo.Anchor!.Value == TimeSpan.Zero) + { + tempo.SyncPosition(0); + synced.Add(tempo); + } + else + desynced.Add(tempo); + } + + if (desynced.Count == 0) + return; + + using var syncedEnumerator = (desyncedPreOrdered ? (IEnumerable)synced : synced.OrderBy(t => t.Position)).GetEnumerator(); + + if (!syncedEnumerator.MoveNext() || syncedEnumerator.Current.Position != 0) + throw new Exception("A tempo marker at position or anchor zero is required to sync anchors."); + + using var desyncedEnumerator = desynced.OrderBy(t => t.Anchor).GetEnumerator(); + + syncedEnumerator.MoveNext(); + desyncedEnumerator.MoveNext(); + + var previous = syncedEnumerator.Current; + var previousMs = 0ul; + + while (syncedEnumerator.MoveNext()) + while (TryInsertDesynced(syncedEnumerator.Current)) + if (!desyncedEnumerator.MoveNext()) + return; + + while (desyncedEnumerator.MoveNext()) + SyncAnchor(); + return; + + bool TryInsertDesynced(Tempo next) + { + var deltaMs = previous.Value * 50 / 3 * ((next.Position - previous.Position) / resolution); + + if (desyncedEnumerator.Current.Anchor!.Value.TotalMilliseconds - previousMs <= deltaMs) + { + SyncAnchor(); + return true; + } + + previous = next; + return false; + } + void SyncAnchor() + { + var desynced = desyncedEnumerator.Current; + desynced.SyncPosition((uint)((desynced.Anchor!.Value.TotalMilliseconds - previousMs) * previous.Value * resolution / 240000)); + + previous = desynced; + } + } + internal void Desync() + { + foreach (var tempo in _anchors) + tempo.DesyncPosition(); + + Synchronized = false; + } + + internal void AddAnchor(Tempo item) => _anchors.Add(item); + internal void RemoveAnchor(Tempo item) => _anchors.Remove(item); +} diff --git a/source - Copie/Sync/TimeSignature.cs b/source - Copie/Sync/TimeSignature.cs new file mode 100644 index 00000000..a5870cec --- /dev/null +++ b/source - Copie/Sync/TimeSignature.cs @@ -0,0 +1,28 @@ +namespace ChartTools; + +/// +/// Marker that alters the time signature +/// +public class TimeSignature : TrackObjectBase +{ + /// + /// Value of a beat + /// + public byte Numerator { get; set; } + /// + /// Beats per measure + /// + public byte Denominator { get; set; } + + /// + /// Creates an instance of . + /// + /// Value of + /// Value of + /// Value of + public TimeSignature(uint position, byte numerator, byte denominator) : base(position) + { + Numerator = numerator; + Denominator = denominator; + } +} diff --git a/source - Copie/Tools/LengthMerger.cs b/source - Copie/Tools/LengthMerger.cs new file mode 100644 index 00000000..e7fd610a --- /dev/null +++ b/source - Copie/Tools/LengthMerger.cs @@ -0,0 +1,16 @@ +namespace ChartTools.Tools; + +public static class LengthMerger +{ + public static T MergeLengths(this IEnumerable objects, T? target = null) where T : class, ILongTrackObject + { + var start = objects.Min(o => o.Position); + var end = objects.Max(o => o.EndPosition); + target ??= objects.First(); + + target.Position = start; + target.Length = end - start; + + return target; + } +} diff --git a/source - Copie/Tools/Optimizer.cs b/source - Copie/Tools/Optimizer.cs new file mode 100644 index 00000000..8814e272 --- /dev/null +++ b/source - Copie/Tools/Optimizer.cs @@ -0,0 +1,153 @@ +using ChartTools.Extensions.Linq; +using ChartTools.IO.Formatting; + +using System.Data; + +namespace ChartTools.Tools; + +/// +/// Provides methods for simplifying songs +/// +public static class Optimizer +{ + internal static bool LengthNeedsCut(ILongTrackObject current, ILongTrackObject next) => current.Position + current.Length > next.Position; + + /// + /// Cuts short sustains that exceed the position of the next note preventing the sustain from continuing. + /// + /// Chords to cut the sustains of + /// Skip ordering of chords by position + public static void CutSustains(this IEnumerable chords, bool preOrdered = false) where T : LaneChord + { + var sustains = new Dictionary(); + + foreach (var chord in GetOrdered(chords, preOrdered)) + { + if (chord.Notes.Count == 0) + continue; + + using var noteEnumerator = chord.Notes.GetEnumerator(); + noteEnumerator.MoveNext(); + + var note = noteEnumerator.Current; + + if (chord.OpenExclusivity) + { + if (noteEnumerator.Current.Index == 0) // Open stops all sustains + foreach ((var position, var sustained) in sustains.Values) + { + if (position + sustained.Sustain > chord.Position) + sustained.Sustain = chord.Position; + + sustains.Remove(noteEnumerator.Current.Index); + } + else + RemoveSustain(0); // Non-opens stops open sustain + } + else + RemoveSustain(note.Index); + + AddSustain(); + + while (noteEnumerator.MoveNext()) + { + note = noteEnumerator.Current; + + RemoveSustain(note.Index); + AddSustain(); + } + + void AddSustain() + { + if (noteEnumerator.Current.Sustain > 0) + sustains[noteEnumerator.Current.Index] = (chord.Position, noteEnumerator.Current); + } + void RemoveSustain(byte index) + { + if (sustains.TryGetValue(index, out var sustained)) + { + sustained.Item2.Sustain = chord.Position; + sustains.Remove(index); + } + } + } + } + + /// + /// Cuts lengths of special phrases based on the numeric value of the type. + /// + /// Set of phrases + /// Skip ordering of phrases by position + /// Passed phrases ordered by position and grouped by type + /// + public static List[] CutSpecialLengths(IEnumerable phrases, bool preOrdered = false) where T : SpecialPhrase + { + if (typeof(T) == typeof(SpecialPhrase)) + throw new InvalidOperationException($"Collection must be of a type deriving from {nameof(SpecialPhrase)}."); + + var output = phrases.GroupBy(p => p.TypeCode).Select(g => g.ToList()).ToArray(); + + foreach (var grouping in output) + grouping.CutLengths(preOrdered); + + return output; + } + + /// + /// Cuts short long track objects that exceed the start of the next one. + /// + /// Set of long track objects + /// Skip ordering of objects by position + public static void CutLengths(this IEnumerable objects, bool preOrdered = false) where T : ILongTrackObject + { + foreach ((var current, var next) in GetOrdered(objects, preOrdered).RelativeLoopSkipFirst()) + if (LengthNeedsCut(current, next)) + next.Length = current.Position - current.Position; + } + + /// + /// Removes redundant tempo markers. + /// + /// Tempo markers without anchors. + /// Skip ordering of markers by position. + /// + /// If some markers may be anchored, use the overload with a resolution. + public static void RemoveUneeded(this ICollection markers, bool preOrdered = false) + { + if (markers.TryGetFirst(m => !m.PositionSynced, out var marker)) + throw new DesynchronizedAnchorException(marker.Anchor!.Value, $"Collection contains a desynchronized anchored tempo at {marker.Anchor}. Resolution needed to synchronize anchors."); + + foreach ((var previous, var current) in GetOrdered(markers, preOrdered).RelativeLoopSkipFirst()) + if (previous.Value == current.Value) + markers.Remove(current); + } + /// + /// Removes redundant tempo markers by syncing the position of anchored markers. + /// + /// Set of markers + /// Resolution from + /// Skip ordering of desynced markers by position + public static void RemoveUneeded(this TempoMap markers, uint resolution, bool desyncedPreOrdered = false) + { + markers.Synchronize(resolution, desyncedPreOrdered); + + foreach ((var previous, var current) in markers.OrderBy(m => m.Position).RelativeLoopSkipFirst()) + if (current.Value == previous!.Value) + markers.Remove(current); + } + + /// + /// Removes redundant time signature markers. + /// + /// Time signatures to remove the unneeded from + /// Skip ordering of markers by position + /// Passed markers, ordered by position. Same instance if is and is . + public static void RemoveUnneeded(this ICollection signatures, bool preOrdered = false) + { + foreach ((var previous, var current) in GetOrdered(signatures, preOrdered).RelativeLoopSkipFirst()) + if (previous.Numerator == current.Numerator && previous.Denominator == current.Denominator) + signatures.Remove(current); + } + + private static IEnumerable GetOrdered(IEnumerable items, bool preOredered) where T : ITrackObject => preOredered ? items : items.OrderBy(i => i.Position); +} diff --git a/source - Copie/Tools/Printer.cs b/source - Copie/Tools/Printer.cs new file mode 100644 index 00000000..f43918dd --- /dev/null +++ b/source - Copie/Tools/Printer.cs @@ -0,0 +1,89 @@ +namespace ChartTools.Tools; + +public static class Printer +{ + private readonly struct ConsoleContent + { + public string Content { get; } + public ConsoleColor Color { get; } + + public ConsoleContent(string content, ConsoleColor color) + { + Content = content; + Color = color; + } + } + + public static void PrintTrack(Track track) + { + var content = new List>(); + uint[] sustainEnds = new uint[6]; + ConsoleColor[] laneColors = new ConsoleColor[] + { + ConsoleColor.Green, + ConsoleColor.Red, + ConsoleColor.Yellow, + ConsoleColor.Blue, + ConsoleColor.DarkYellow + }; + + foreach (var chord in track.Chords.Where(c => c.Notes.Count > 0).OrderBy(t => t.Position)) + { + var open = chord.Notes[StandardLane.Open]; + var lineContent = new List(); + + if (open is not null) + { + lineContent.Add(new("-----", ConsoleColor.Magenta)); + + SetSustainEnd(open); + + for (int i = 1; i < sustainEnds.Length; i++) + sustainEnds[i] = chord.Position; + } + else + { + if (chord.Notes.Count == 0) + lineContent.Add(new(sustainEnds[0] >= chord.Position ? " | " : " ", ConsoleColor.Magenta)); + else + for (int i = 1; i < 6; i++) + { + var note = chord.Notes[(StandardLane)i]; + string text; + + if (note is null) + text = sustainEnds[i] >= chord.Position ? "|" : " "; + else + { + text = "O"; + SetSustainEnd(note); + } + + lineContent.Add(new(text, laneColors[i - 1])); + } + } + + content.Add(lineContent); + + void SetSustainEnd(LaneNote note) => sustainEnds[(int)note.Lane] = chord.Position + note.Sustain; + } + + PrintLines(content); + } + + private static void PrintLines(IEnumerable> content) + { + foreach (var line in content.Reverse()) + { + Console.WriteLine(); + + foreach (var ct in line) + { + Console.ForegroundColor = ct.Color; + Console.Write(ct.Content); + } + } + + Console.ResetColor(); + } +} diff --git a/source - Copie/Tools/PropertyMerger.cs b/source - Copie/Tools/PropertyMerger.cs new file mode 100644 index 00000000..2930013b --- /dev/null +++ b/source - Copie/Tools/PropertyMerger.cs @@ -0,0 +1,49 @@ +using System.Reflection; +using ChartTools.Extensions.Linq; + +namespace ChartTools.Tools; + +/// +/// Provides methods to merge properties between two instances +/// +public static class PropertyMerger +{ + /// + /// Replaces the property values of an instance with the first non-null equivalent from other instances. + /// + /// If overwriteNonNull is , only replaces property values that are null in the original instance. + /// Item to assign the property values to + /// If , only replaces property values that are null in the original instance. + /// Items to pull new property values from in order of priority + public static void Merge(this T current, bool overwriteNonNull, bool deepMerge, params T[] newValues) + { + T? newValue = current; + var stringType = typeof(string); + var nullableType = typeof(Nullable); + + foreach (var prop in GetProperties(typeof(T))) + MergeValue(current, prop, GetValues(newValues.Cast(), prop)); + + void MergeValue(object? source, PropertyInfo prop, IEnumerable newValues) + { + var value = prop.GetValue(source); + + if (deepMerge && !prop.PropertyType.IsPrimitive && prop.PropertyType != stringType && Nullable.GetUnderlyingType(prop.PropertyType) is null) + { + if (value is not null) + foreach (var deepProp in GetProperties(prop.PropertyType)) + MergeValue(value, deepProp, GetValues(newValues, deepProp)); + } + else if (value is null || overwriteNonNull) + { + var newVal = newValues.FirstOrDefault(newVal => newVal is not null); + + if (newVal is not null) + prop.SetValue(source, newVal); + } + } + + IEnumerable GetProperties(Type type) => type.GetProperties().Where(i => i.CanWrite); + IEnumerable GetValues(IEnumerable sources, PropertyInfo prop) => sources.Select(s => prop.GetValue(s)).NonNull(); + } +} diff --git a/source - Copie/Tools/TempoRescaler.cs b/source - Copie/Tools/TempoRescaler.cs new file mode 100644 index 00000000..54ebef73 --- /dev/null +++ b/source - Copie/Tools/TempoRescaler.cs @@ -0,0 +1,103 @@ +namespace ChartTools.Tools; + +public static class TempoRescaler +{ + /// + /// Rescales the length a long object. + /// + /// Object to rescale + /// Positive number where 1 is the current scale. + public static void Rescale(this ILongObject obj, float scale) => obj.Length = (uint)(obj.Length * scale); + /// + /// Rescales the position of a track object + /// + /// Object to rescale + /// Positive number where 1 is the current scale. + public static void Rescale(this ITrackObject trackObject, float scale) => trackObject.Position = (uint)(trackObject.Position * scale); + /// + /// Rescales the position and length of a long track object + /// + /// Object to rescale + /// Positive number where 1 is the current scale. + public static void Rescale(this ILongTrackObject trackObject, float scale) + { + trackObject.Position = (uint)(trackObject.Position * scale); + trackObject.Length = (uint)(trackObject.Length * scale); + } + + /// + /// Rescales the position and value of a tempo marker. + /// + /// Marker to rescale + /// Positive number where 1 is the current scale. + public static void Rescale(this Tempo tempo, float scale) + { + tempo.Position = (uint)(tempo.Position * scale); + tempo.Value *= scale; + } + /// + /// Rescales the position of a chord and sustain of its notes. + /// + /// Chord to rescale + /// Positive number where 1 is the current scale. + public static void Rescale(this IChord chord, float scale) + { + chord.Position = (uint)(chord.Position * scale); + + foreach (var note in chord.Notes) + note.Rescale(scale); + } + /// + /// Rescales the chords in a track. + /// + /// Source of chords + /// Positive number where 1 is the current scale. + public static void Rescale(this Track track, float scale) + { + foreach (var chord in track.Chords) + Rescale(chord, scale); + + if (track.LocalEvents is not null) + foreach (var e in track.LocalEvents) + e.Rescale(scale); + } + /// + /// Rescales all tracks in an instrument. + /// + /// Source of the tracks + /// Positive number where 1 is the current scale. + public static void Rescale(this Instrument instrument, float scale) + { + foreach (var track in instrument.GetExistingTracks()) + track.Rescale(scale); + } + + /// + /// Rescales the tempo and time signatures in a song. + /// + /// Source of markers + /// Positive number where 1 is the current scale. + public static void Rescale(this SyncTrack syncTrack, float scale) + { + foreach (var tempo in syncTrack.Tempo) + tempo.Rescale(scale); + foreach (var signature in syncTrack.TimeSignatures) + signature.Rescale(scale); + } + /// + /// Rescales all instruments, tempo and time signatures. + /// + /// Source of objects + /// Positive number where 1 is the current scale. + public static void Rescale(this Song song, float scale) + { + foreach (var instrument in song.Instruments) + instrument.Rescale(scale); + + song.SyncTrack?.Rescale(scale); + + if (song.GlobalEvents is not null) + foreach (var e in song.GlobalEvents) + e.Rescale(scale); + } +} diff --git a/source - Copie/TrackObjects/ILongTrackObject.cs b/source - Copie/TrackObjects/ILongTrackObject.cs new file mode 100644 index 00000000..3e2464d0 --- /dev/null +++ b/source - Copie/TrackObjects/ILongTrackObject.cs @@ -0,0 +1,9 @@ +namespace ChartTools; + +public interface ILongTrackObject : ITrackObject, ILongObject +{ + /// + /// Tick number marking the end of the object + /// + public uint EndPosition => Position + Length; +} diff --git a/source - Copie/TrackObjects/IReadOnlyTrackObject.cs b/source - Copie/TrackObjects/IReadOnlyTrackObject.cs new file mode 100644 index 00000000..61ec6efc --- /dev/null +++ b/source - Copie/TrackObjects/IReadOnlyTrackObject.cs @@ -0,0 +1,15 @@ +namespace ChartTools; + +/// +/// Object located on a track +/// +public interface IReadOnlyTrackObject : IEquatable +{ + /// + /// Tick number on the track. + /// + /// A tick represents a subdivision of a beat. The number of subdivisions per beat is stored in . + public uint Position { get; } + + bool IEquatable.Equals(IReadOnlyTrackObject? other) => other is not null && other.Position == Position; +} diff --git a/source - Copie/TrackObjects/ITrackObject.cs b/source - Copie/TrackObjects/ITrackObject.cs new file mode 100644 index 00000000..2598e676 --- /dev/null +++ b/source - Copie/TrackObjects/ITrackObject.cs @@ -0,0 +1,10 @@ +namespace ChartTools; + +/// +public interface ITrackObject : IReadOnlyTrackObject +{ + /// + public new uint Position { get; set; } + + uint IReadOnlyTrackObject.Position => Position; +} diff --git a/source - Copie/TrackObjects/TrackObjectBase.cs b/source - Copie/TrackObjects/TrackObjectBase.cs new file mode 100644 index 00000000..a572237c --- /dev/null +++ b/source - Copie/TrackObjects/TrackObjectBase.cs @@ -0,0 +1,9 @@ +namespace ChartTools; + +public abstract class TrackObjectBase : ITrackObject +{ + public virtual uint Position { get; set; } + + public TrackObjectBase() : this(0) { } + public TrackObjectBase(uint position) => Position = position; +} diff --git a/source - Copie/Tracks/Track.cs b/source - Copie/Tracks/Track.cs new file mode 100644 index 00000000..8210fec8 --- /dev/null +++ b/source - Copie/Tracks/Track.cs @@ -0,0 +1,133 @@ +using ChartTools.Events; +using ChartTools.IO; +using ChartTools.IO.Chart; +using ChartTools.IO.Configuration; +using ChartTools.IO.Formatting; + +namespace ChartTools; + +/// +/// Base class for tracks +/// +public abstract record Track : IEmptyVerifiable +{ + /// + public bool IsEmpty => Chords.Count == 0 && LocalEvents.Count == 0 && SpecialPhrases.Count == 0; + + /// + /// Difficulty of the track + /// + public Difficulty Difficulty { get; init; } + /// + /// Instrument containing the track + /// + public Instrument? ParentInstrument => GetInstrument(); + /// + /// Events specific to the + /// + public List LocalEvents { get; } = new(); + /// + /// Set of special phrases + /// + public List SpecialPhrases { get; } = new(); + + /// + /// Groups of notes of the same position + /// + public IReadOnlyList Chords => GetChords(); + + protected abstract IReadOnlyList GetChords(); + + internal IEnumerable SoloToStarPower(bool removeEvents) + { + if (LocalEvents is null) + yield break; + + foreach (LocalEvent e in LocalEvents.OrderBy(e => e.Position)) + { + TrackSpecialPhrase? phrase = null; + + switch (e.EventType) + { + case EventTypeHelper.Local.Solo: + phrase = new(e.Position, TrackSpecialPhraseType.StarPowerGain); + break; + case EventTypeHelper.Local.SoloEnd: + if (phrase is not null) + { + phrase.Length = e.Position - phrase.Position; + yield return phrase; + phrase = null; + } + break; + } + } + + if (removeEvents) + LocalEvents.RemoveAll(e => e.IsSoloEvent); + } + + protected abstract Instrument? GetInstrument(); + + #region File reading + #region Single file + [Obsolete($"Use {nameof(ChartFile.ReadTrack)}.")] + public static Track FromFile(string path, InstrumentIdentity instrument, Difficulty difficulty, ReadingConfiguration? config = default, FormattingRules? formatting = default) => ExtensionHandler.Read(path, (".chart", path => ChartFile.ReadTrack(path, instrument, difficulty, config, formatting))); + + [Obsolete($"Use {nameof(ChartFile.ReadTrackAsync)}.")] + public static async Task FromFileAsync(string path, InstrumentIdentity instrument, Difficulty difficulty, ReadingConfiguration? config = default, FormattingRules? formatting = default, CancellationToken cancellationToken = default) => await ExtensionHandler.ReadAsync(path, (".chart", path => ChartFile.ReadTrackAsync(path, instrument, difficulty, config, formatting, cancellationToken))); + + [Obsolete($"Use {nameof(ChartFile.ReadDrumsTrack)}.")] + public static Track FromFile(string path, Difficulty difficulty, ReadingConfiguration? config = default, FormattingRules? formatting = default) => ExtensionHandler.Read>(path, (".chart", path => ChartFile.ReadDrumsTrack(path, difficulty, config, formatting))); + + [Obsolete($"Use {nameof(ChartFile.ReadDrumsTrackAsync)}.")] + public static async Task> FromFileAsync(string path, Difficulty difficulty, ReadingConfiguration? config = default, FormattingRules? formatting = default, CancellationToken cancellationToken = default) => await ExtensionHandler.ReadAsync>(path, (".chart", path => ChartFile.ReadDrumsTrackAsync(path, difficulty, config, formatting, cancellationToken))); + + [Obsolete($"Use {nameof(ChartFile.ReadTrack)}.")] + public static Track FromFile(string path, GHLInstrumentIdentity instrument, Difficulty difficulty, ReadingConfiguration? config = default, FormattingRules? formatting = default) => ExtensionHandler.Read>(path, (".chart", path => ChartFile.ReadTrack(path, instrument, difficulty, config, formatting))); + + [Obsolete($"Use {nameof(ChartFile.ReadTrackAsync)}.")] + public static async Task> FromFileAsync(string path, GHLInstrumentIdentity instrument, Difficulty difficulty, ReadingConfiguration? config = default, FormattingRules? formatting = default, CancellationToken cancellationToken = default) => await ExtensionHandler.ReadAsync>(path, (".chart", path => ChartFile.ReadTrackAsync(path, instrument, difficulty, config, formatting, cancellationToken))); + + [Obsolete($"Use {nameof(ChartFile.ReadTrack)}.")] + public static Track FromFile(string path, StandardInstrumentIdentity instrument, Difficulty difficulty, ReadingConfiguration? config = default, FormattingRules? formatting = default) => ExtensionHandler.Read>(path, (".chart", path => ChartFile.ReadTrack(path, instrument, difficulty, config, formatting))); + + [Obsolete($"Use {nameof(ChartFile.ReadTrackAsync)}.")] + public static async Task> FromFileAsync(string path, StandardInstrumentIdentity instrument, Difficulty difficulty, ReadingConfiguration? config = default, FormattingRules? formatting = default, CancellationToken cancellationToken = default) => await ExtensionHandler.ReadAsync>(path, (".chart", path => ChartFile.ReadTrackAsync(path, instrument, difficulty, config, formatting, cancellationToken))); + #endregion + + #region Directory + [Obsolete($"Use {nameof(ChartFile.ReadTrack)} with {nameof(Metadata.Formatting)}.")] + public static DirectoryResult FromDirectory(string directory, InstrumentIdentity instrument, Difficulty difficulty, ReadingConfiguration? config = default) => DirectoryHandler.FromDirectory(directory, (path, formatting) => FromFile(path, instrument, difficulty, config, formatting)); + + [Obsolete($"Use {nameof(ChartFile.ReadTrackAsync)} with {nameof(Metadata.Formatting)}.")] + public static async Task> FromDirectoryAsync(string directory, InstrumentIdentity instrument, Difficulty difficulty, ReadingConfiguration? config = default, CancellationToken cancellationToken = default) => await DirectoryHandler.FromDirectoryAsync(directory, async (path, formatting) => await FromFileAsync(path, instrument, difficulty, config, formatting, cancellationToken), cancellationToken); + + [Obsolete($"Use {nameof(ChartFile.ReadDrumsTrack)} with {nameof(Metadata.Formatting)}.")] + public static DirectoryResult?> FromDirectory(string directory, Difficulty difficulty, ReadingConfiguration? config = default) => DirectoryHandler.FromDirectory(directory, (path, formatting) => FromFile(path, difficulty, config, formatting)); + + [Obsolete($"Use {nameof(ChartFile.ReadDrumsTrackAsync)} with {nameof(Metadata.Formatting)}.")] + public static async Task?>> FromDirectoryAsync(string directory, Difficulty difficulty, ReadingConfiguration? config = default, CancellationToken cancellationToken = default) => await DirectoryHandler.FromDirectoryAsync(directory, async (path, formatting) => await FromFileAsync(path, difficulty, config, formatting, cancellationToken), cancellationToken); + + [Obsolete($"Use {nameof(ChartFile.ReadTrack)} with {nameof(Metadata.Formatting)}.")] + public static DirectoryResult?> FromDirectory(string directory, GHLInstrumentIdentity instrument, Difficulty difficulty, ReadingConfiguration? config = default) => DirectoryHandler.FromDirectory(directory, (path, formatting) => FromFile(path, instrument, difficulty, config, formatting)); + + [Obsolete($"Use {nameof(ChartFile.ReadTrackAsync)} with {nameof(Metadata.Formatting)}.")] + public static async Task?>> FromDirectoryAsync(string directory, GHLInstrumentIdentity instrument, Difficulty difficulty, ReadingConfiguration? config = default, CancellationToken cancellationToken = default) => await DirectoryHandler.FromDirectoryAsync(directory, async (path, formatting) => await FromFileAsync(path, instrument, difficulty, config, formatting, cancellationToken), cancellationToken); + + [Obsolete($"Use {nameof(ChartFile.ReadTrack)} with {nameof(Metadata.Formatting)}.")] + public static DirectoryResult?> FromDirectory(string directory, StandardInstrumentIdentity instrument, Difficulty difficulty, ReadingConfiguration? config = default) => DirectoryHandler.FromDirectory(directory, (path, formatting) => FromFile(path, instrument, difficulty, config, formatting)); + + [Obsolete($"Use {nameof(ChartFile.ReadTrackAsync)} with {nameof(Metadata.Formatting)}.")] + public static async Task?>> FromDirectoryAsync(string directory, StandardInstrumentIdentity instrument, Difficulty difficulty, ReadingConfiguration? config = default, CancellationToken cancellationToken = default) => await DirectoryHandler.FromDirectoryAsync(directory, async (path, formatting) => await FromFileAsync(path, instrument, difficulty, config, formatting, cancellationToken), cancellationToken); + #endregion + #endregion + + [Obsolete($"Use {nameof(ChartFile.ReplaceTrack)}.")] + public void ToFile(string path, WritingConfiguration? config = default, FormattingRules? formatting = default) => ExtensionHandler.Write(path, this, (".chart", (path, track) => ChartFile.ReplaceTrack(path, track, config, formatting))); + + [Obsolete($"Use {nameof(ChartFile.ReplaceTrackAsync)}.")] + public async Task ToFileAsync(string path, WritingConfiguration? config = default,FormattingRules? formatting = default, CancellationToken cancellationToken = default) => await ExtensionHandler.WriteAsync(path, this, (".chart", (path, track) => ChartFile.ReplaceTrackAsync(path, track, config, formatting, cancellationToken))); + + public override string ToString() => Difficulty.ToString(); +} diff --git a/source - Copie/Tracks/TrackGeneric.cs b/source - Copie/Tracks/TrackGeneric.cs new file mode 100644 index 00000000..97c1b598 --- /dev/null +++ b/source - Copie/Tracks/TrackGeneric.cs @@ -0,0 +1,26 @@ +namespace ChartTools; + +/// +/// Set of chords for a instrument at a certain difficulty +/// +public record Track : Track where TChord : IChord +{ + /// + /// Chords making up the difficulty track. + /// + public new List Chords { get; } = new(); + /// + /// Instrument the track is held in. + /// + public new Instrument? ParentInstrument { get; init; } + + /// + /// Gets the chords as a read-only list of the base interface. + /// + /// + protected override IReadOnlyList GetChords() => (IReadOnlyList)Chords; + /// + /// Gets the parent instrument as an instance of the base type. + /// + protected override Instrument? GetInstrument() => ParentInstrument; +} diff --git a/source - Copie/Tracks/UniqueTrackObjectCollection.cs b/source - Copie/Tracks/UniqueTrackObjectCollection.cs new file mode 100644 index 00000000..5c73b2b9 --- /dev/null +++ b/source - Copie/Tracks/UniqueTrackObjectCollection.cs @@ -0,0 +1,39 @@ +using System.Collections; + +namespace ChartTools.Extensions.Collections; + +/// +/// Set of track objects where each one must have a different position +/// +public class UniqueTrackObjectCollection : ICollection where T : ITrackObject +{ + private readonly Dictionary items; + + public UniqueTrackObjectCollection(IEnumerable? items = null) => this.items = items is null ? new() : items.ToDictionary(i => i.Position); + + public int Count => items.Count; + bool ICollection.IsReadOnly => false; + + private void RemoveDuplicate(T item) + { + if (items.ContainsKey(item.Position)) + items.Remove(item.Position); + } + + public void Add(T item) + { + RemoveDuplicate(item); + items.Add(item.Position, item); + } + + public void Clear() => items.Clear(); + + public bool Contains(T item) => items.ContainsKey(item.Position); + + public void CopyTo(T[] array, int arrayIndex) => items.Values.CopyTo(array, arrayIndex); + + public bool Remove(T item) => items.Remove(item.Position); + + public IEnumerator GetEnumerator() => items.Values.GetEnumerator(); + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); +} diff --git a/source - Copie/source.projitems b/source - Copie/source.projitems new file mode 100644 index 00000000..f45ab8a1 --- /dev/null +++ b/source - Copie/source.projitems @@ -0,0 +1,166 @@ + + + + $(MSBuildAllProjects);$(MSBuildThisFileFullPath) + true + 54924bc3-4a10-45ad-af3c-f17494e403e1 + + + ChartTools + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/source/ChartTools/ChartTools.csproj b/source/ChartTools/ChartTools.csproj new file mode 100644 index 00000000..fa71b7ae --- /dev/null +++ b/source/ChartTools/ChartTools.csproj @@ -0,0 +1,9 @@ + + + + net8.0 + enable + enable + + + diff --git a/source/ChartTools/Class1.cs b/source/ChartTools/Class1.cs new file mode 100644 index 00000000..45152ce6 --- /dev/null +++ b/source/ChartTools/Class1.cs @@ -0,0 +1,7 @@ +namespace ChartTools +{ + public class Class1 + { + + } +} diff --git a/source/ChartTools/obj/ChartTools.csproj.nuget.dgspec.json b/source/ChartTools/obj/ChartTools.csproj.nuget.dgspec.json new file mode 100644 index 00000000..7900ff52 --- /dev/null +++ b/source/ChartTools/obj/ChartTools.csproj.nuget.dgspec.json @@ -0,0 +1,67 @@ +{ + "format": 1, + "restore": { + "C:\\Users\\joujo\\OneDrive\\Documents\\GitHub\\charttools\\source\\ChartTools\\ChartTools.csproj": {} + }, + "projects": { + "C:\\Users\\joujo\\OneDrive\\Documents\\GitHub\\charttools\\source\\ChartTools\\ChartTools.csproj": { + "version": "1.0.0", + "restore": { + "projectUniqueName": "C:\\Users\\joujo\\OneDrive\\Documents\\GitHub\\charttools\\source\\ChartTools\\ChartTools.csproj", + "projectName": "ChartTools", + "projectPath": "C:\\Users\\joujo\\OneDrive\\Documents\\GitHub\\charttools\\source\\ChartTools\\ChartTools.csproj", + "packagesPath": "C:\\Users\\joujo\\.nuget\\packages\\", + "outputPath": "C:\\Users\\joujo\\OneDrive\\Documents\\GitHub\\charttools\\source\\ChartTools\\obj\\", + "projectStyle": "PackageReference", + "fallbackFolders": [ + "C:\\Program Files (x86)\\Microsoft Visual Studio\\Shared\\NuGetPackages" + ], + "configFilePaths": [ + "C:\\Users\\joujo\\AppData\\Roaming\\NuGet\\NuGet.Config", + "C:\\Program Files (x86)\\NuGet\\Config\\Microsoft.VisualStudio.FallbackLocation.config", + "C:\\Program Files (x86)\\NuGet\\Config\\Microsoft.VisualStudio.Offline.config" + ], + "originalTargetFrameworks": [ + "net8.0" + ], + "sources": { + "C:\\Program Files (x86)\\Microsoft SDKs\\NuGetPackages\\": {}, + "https://api.nuget.org/v3/index.json": {} + }, + "frameworks": { + "net8.0": { + "targetAlias": "net8.0", + "projectReferences": {} + } + }, + "warningProperties": { + "warnAsError": [ + "NU1605" + ] + } + }, + "frameworks": { + "net8.0": { + "targetAlias": "net8.0", + "imports": [ + "net461", + "net462", + "net47", + "net471", + "net472", + "net48", + "net481" + ], + "assetTargetFallback": true, + "warn": true, + "frameworkReferences": { + "Microsoft.NETCore.App": { + "privateAssets": "all" + } + }, + "runtimeIdentifierGraphPath": "C:\\Program Files\\dotnet\\sdk\\8.0.100/PortableRuntimeIdentifierGraph.json" + } + } + } + } +} \ No newline at end of file diff --git a/source/ChartTools/obj/ChartTools.csproj.nuget.g.props b/source/ChartTools/obj/ChartTools.csproj.nuget.g.props new file mode 100644 index 00000000..4b04bbd9 --- /dev/null +++ b/source/ChartTools/obj/ChartTools.csproj.nuget.g.props @@ -0,0 +1,16 @@ + + + + True + NuGet + $(MSBuildThisFileDirectory)project.assets.json + $(UserProfile)\.nuget\packages\ + C:\Users\joujo\.nuget\packages\;C:\Program Files (x86)\Microsoft Visual Studio\Shared\NuGetPackages + PackageReference + 6.8.0 + + + + + + \ No newline at end of file diff --git a/source/ChartTools/obj/ChartTools.csproj.nuget.g.targets b/source/ChartTools/obj/ChartTools.csproj.nuget.g.targets new file mode 100644 index 00000000..3dc06ef3 --- /dev/null +++ b/source/ChartTools/obj/ChartTools.csproj.nuget.g.targets @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/source/ChartTools/obj/Debug/net8.0/.NETCoreApp,Version=v8.0.AssemblyAttributes.cs b/source/ChartTools/obj/Debug/net8.0/.NETCoreApp,Version=v8.0.AssemblyAttributes.cs new file mode 100644 index 00000000..2217181c --- /dev/null +++ b/source/ChartTools/obj/Debug/net8.0/.NETCoreApp,Version=v8.0.AssemblyAttributes.cs @@ -0,0 +1,4 @@ +// +using System; +using System.Reflection; +[assembly: global::System.Runtime.Versioning.TargetFrameworkAttribute(".NETCoreApp,Version=v8.0", FrameworkDisplayName = ".NET 8.0")] diff --git a/source/ChartTools/obj/Debug/net8.0/ChartTools.AssemblyInfo.cs b/source/ChartTools/obj/Debug/net8.0/ChartTools.AssemblyInfo.cs new file mode 100644 index 00000000..b6abbcb1 --- /dev/null +++ b/source/ChartTools/obj/Debug/net8.0/ChartTools.AssemblyInfo.cs @@ -0,0 +1,23 @@ +//------------------------------------------------------------------------------ +// +// Ce code a été généré par un outil. +// Version du runtime :4.0.30319.42000 +// +// Les modifications apportées à ce fichier peuvent provoquer un comportement incorrect et seront perdues si +// le code est régénéré. +// +//------------------------------------------------------------------------------ + +using System; +using System.Reflection; + +[assembly: System.Reflection.AssemblyCompanyAttribute("ChartTools")] +[assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")] +[assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")] +[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0")] +[assembly: System.Reflection.AssemblyProductAttribute("ChartTools")] +[assembly: System.Reflection.AssemblyTitleAttribute("ChartTools")] +[assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")] + +// Généré par la classe MSBuild WriteCodeFragment. + diff --git a/source/ChartTools/obj/Debug/net8.0/ChartTools.AssemblyInfoInputs.cache b/source/ChartTools/obj/Debug/net8.0/ChartTools.AssemblyInfoInputs.cache new file mode 100644 index 00000000..1392b3c9 --- /dev/null +++ b/source/ChartTools/obj/Debug/net8.0/ChartTools.AssemblyInfoInputs.cache @@ -0,0 +1 @@ +6376c159ad7a773a0301f1fd812ca5f03224f4d8b58bcf9fe09a803f035ce768 diff --git a/source/ChartTools/obj/Debug/net8.0/ChartTools.GeneratedMSBuildEditorConfig.editorconfig b/source/ChartTools/obj/Debug/net8.0/ChartTools.GeneratedMSBuildEditorConfig.editorconfig new file mode 100644 index 00000000..1a643956 --- /dev/null +++ b/source/ChartTools/obj/Debug/net8.0/ChartTools.GeneratedMSBuildEditorConfig.editorconfig @@ -0,0 +1,13 @@ +is_global = true +build_property.TargetFramework = net8.0 +build_property.TargetPlatformMinVersion = +build_property.UsingMicrosoftNETSdkWeb = +build_property.ProjectTypeGuids = +build_property.InvariantGlobalization = +build_property.PlatformNeutralAssembly = +build_property.EnforceExtendedAnalyzerRules = +build_property._SupportedPlatformList = Linux,macOS,Windows +build_property.RootNamespace = ChartTools +build_property.ProjectDir = C:\Users\joujo\OneDrive\Documents\GitHub\charttools\source\ChartTools\ +build_property.EnableComHosting = +build_property.EnableGeneratedComInterfaceComImportInterop = diff --git a/source/ChartTools/obj/Debug/net8.0/ChartTools.GlobalUsings.g.cs b/source/ChartTools/obj/Debug/net8.0/ChartTools.GlobalUsings.g.cs new file mode 100644 index 00000000..8578f3d0 --- /dev/null +++ b/source/ChartTools/obj/Debug/net8.0/ChartTools.GlobalUsings.g.cs @@ -0,0 +1,8 @@ +// +global using global::System; +global using global::System.Collections.Generic; +global using global::System.IO; +global using global::System.Linq; +global using global::System.Net.Http; +global using global::System.Threading; +global using global::System.Threading.Tasks; diff --git a/source/ChartTools/obj/Debug/net8.0/ChartTools.assets.cache b/source/ChartTools/obj/Debug/net8.0/ChartTools.assets.cache new file mode 100644 index 0000000000000000000000000000000000000000..f3b1f19024dec94746e99fa2be9d73a0d67264d2 GIT binary patch literal 228 zcmWIWc6a1qU|{Inv0~Ep%)0kY59PcbY|n}~QmK^L6~mu!$LPvJ=>=jd9{|-53z&ck z<(;i!LW@(2ies|!OSAH0^zus6Q%hnB5|gtN(^HFMKxVlE)dUpfrxzvWD!66lq!ufv z8d+3Wn5i0B#`tC?7v&e{r