├── Docs ├── templates │ └── modern │ │ ├── public │ │ ├── main.css │ │ └── main.js │ │ ├── partials │ │ ├── collection.tmpl.partial │ │ ├── item.tmpl.partial │ │ ├── customMREFContent.tmpl.partial │ │ ├── enum.tmpl.partial │ │ ├── namespace.tmpl.partial │ │ └── class.memberpage.tmpl.partial │ │ ├── src │ │ ├── main.js │ │ ├── mixins.scss │ │ ├── search.scss │ │ ├── highlight.scss │ │ ├── helper.test.ts │ │ ├── docfx.ts │ │ ├── options.d.ts │ │ ├── docfx.scss │ │ ├── highlight.ts │ │ ├── markdown.scss │ │ ├── toc.scss │ │ ├── theme.ts │ │ ├── dotnet.scss │ │ ├── helper.ts │ │ └── nav.scss │ │ ├── ApiPage.html.primary.tmpl │ │ └── ApiPage.html.primary.js ├── toc.yml ├── Properties │ └── launchSettings.json ├── .config │ └── dotnet-tools.json ├── articles │ ├── toc.yml │ ├── configuration.md │ ├── events.md │ ├── improving-performance.md │ └── dynamic-syntax.md ├── docfx.json ├── Docs.csproj ├── Program.cs └── index.md ├── _config.yml ├── ChartTools ├── Lyrics │ ├── Vocals.cs │ ├── Tracks │ │ ├── VocalsTrack.cs │ │ └── StandardVocalsTrack.cs │ ├── PhraseMarker.cs │ ├── VocalsNote.cs │ └── VocalsPitch.cs ├── IO │ ├── Appliables │ │ ├── ISongAppliable.cs │ │ └── IInstrumentAppliable.cs │ ├── Sections │ │ ├── Section.cs │ │ ├── ReservedSectionHeader.cs │ │ ├── ReservedSectionHeaderSet.cs │ │ └── SectionSet.cs │ ├── Exceptions │ │ ├── EntryException.cs │ │ ├── SectionException.cs │ │ ├── LineException.cs │ │ └── ParseException.cs │ ├── Anchor.cs │ ├── Configuration │ │ ├── ConfigurationExceptions.cs │ │ ├── Common │ │ │ ├── ICommonWritingConfiguration.cs │ │ │ ├── ICommonReadingConfiguration.cs │ │ │ └── ICommonConfiguration.cs │ │ ├── ReadingConfiguration.cs │ │ ├── WritingConfiguration.cs │ │ └── Session.cs │ ├── ISerializerDataProvider.cs │ ├── Parsers │ │ ├── TextParser.cs │ │ ├── SectionParser.cs │ │ └── FileParser.cs │ ├── Ini │ │ ├── IniFileWriter.cs │ │ ├── IniKeySerializableAttribute.cs │ │ ├── IniFileReader.cs │ │ ├── IniSerializer.cs │ │ └── IniFile.cs │ ├── Chart │ │ ├── Configuration │ │ │ ├── Sessions │ │ │ │ ├── ChartSession.cs │ │ │ │ ├── ChartReadingSession.cs │ │ │ │ └── ChartWritingSession.cs │ │ │ ├── CommonChartConfiguration.cs │ │ │ ├── ChartWritingConfiguration.cs │ │ │ └── ChartReadingConfiguration.cs │ │ ├── Parsing │ │ │ ├── ChartParser.cs │ │ │ ├── VariableInstrumentTrackParser.cs │ │ │ ├── UnknownSectionParser.cs │ │ │ ├── GlobalEventParser.cs │ │ │ ├── StandardTrackParser.cs │ │ │ ├── DrumsTrackParser.cs │ │ │ ├── GHLTrackParser.cs │ │ │ ├── MetadataParser.cs │ │ │ └── SyncTrackParser.cs │ │ ├── Serializing │ │ │ ├── UnknownSectionSerializer.cs │ │ │ ├── ChartKeySerializableAttribute.cs │ │ │ ├── GlobalEventSerializer.cs │ │ │ ├── SyncTrackSerializer.cs │ │ │ ├── TrackObjectGroupSerializer.cs │ │ │ ├── MetadataSerializer.cs │ │ │ └── TrackSerializer.cs │ │ ├── Providers │ │ │ ├── EventProvider.cs │ │ │ ├── SpecialPhraseProvider.cs │ │ │ ├── TimeSignatureProvider.cs │ │ │ ├── TempoProvider.cs │ │ │ ├── SyncTrackProvider.cs │ │ │ └── ChordProvider.cs │ │ ├── ChartFileWriter.cs │ │ ├── Entries │ │ │ ├── NoteData.cs │ │ │ └── TrackObjectEntry.cs │ │ ├── ChartSectionSet.cs │ │ └── ChartFileReader.cs │ ├── Serializers │ │ ├── GroupSerializer.cs │ │ ├── Serializer.cs │ │ └── KeySerializable.cs │ ├── TextEntry.cs │ ├── Formatting │ │ ├── FormattingEnums.cs │ │ └── FormattingRules.cs │ ├── ValueParser.cs │ ├── DirectoryHandler.cs │ ├── Components │ │ └── ComponentList.cs │ ├── Sources │ │ ├── ReadingDataSource.cs │ │ ├── DataSource.cs │ │ └── WritingDataSource.cs │ ├── FileReader.cs │ └── TextFileReader.cs ├── Instruments │ ├── Drums.cs │ ├── GHLInstrument.cs │ └── StandardInstrument.cs ├── Metadata │ ├── UnidentifiedMetadata.cs │ ├── Charter.cs │ └── StreamCollection.cs ├── TrackObjects │ ├── TrackObjectBase.cs │ ├── ILongTrackObject.cs │ ├── ITrackObject.cs │ └── IReadOnlyTrackObject.cs ├── Extensions │ ├── EqualityComparison.cs │ ├── Collections │ │ ├── EagerEnumerable.cs │ │ ├── IInitializable.cs │ │ ├── Delayed │ │ │ ├── DelayedEnumerableSource.cs │ │ │ ├── DelayedEnumerable.cs │ │ │ └── DelayedEnumerator.cs │ │ └── Alternating │ │ │ └── SerialAlternatingEnumerable.cs │ ├── IInitializable.cs │ ├── EnumCache.cs │ ├── FuncEqualityComparer.cs │ ├── Linq │ │ ├── SectionReplacement.cs │ │ └── CollectionExtensions.cs │ └── StringExtensions.cs ├── Notes │ ├── INote.cs │ ├── DrumsNote.cs │ └── LaneNote.cs ├── IEmptyVerifiable.cs ├── IReadOnlyLongObject.cs ├── ILongObject.cs ├── ChartTools.csproj.user ├── Exceptions │ ├── UndefinedEnumException.cs │ ├── Validator.cs │ └── DesynchronizedAnchorException.cs ├── Events │ ├── EventArgumentHelper.cs │ ├── LocalEvent.cs │ ├── EventTypeHeaderHelper.cs │ └── Event.cs ├── Tools │ ├── LengthMerger.cs │ ├── PropertyMerger.cs │ └── Printer.cs ├── Sync │ ├── TimeSignature.cs │ └── Tempo.cs ├── Special │ ├── SpecialPhrase.cs │ ├── InstrumentSpecialPhrase.cs │ └── TrackSpecialPhrase.cs ├── ChartTools.csproj ├── Tracks │ ├── UniqueTrackObjectCollection.cs │ └── Track.cs ├── Chords │ ├── Chord.cs │ ├── DrumsChord.cs │ ├── StandardChord.cs │ └── GHLChord.cs └── GlobalSuppressions.cs ├── ChartTools.Tests ├── Formatting.cs ├── ChartTools.Tests.csproj ├── AlternatingTests.cs └── SystemExtensionsTests.cs ├── .gitignore ├── .github ├── workflows │ └── docfx.yml └── ISSUE_TEMPLATE │ └── bug_report.md ├── attribution.txt ├── README.md └── ChartTools.sln /Docs/templates/modern/public/main.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /Docs/templates/modern/public/main.js: -------------------------------------------------------------------------------- 1 | export default {} 2 | -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | remote_theme: pages-themes/cayman@v0.2.0 2 | plugins: 3 | - jekyll-remote-theme 4 | -------------------------------------------------------------------------------- /ChartTools/Lyrics/Vocals.cs: -------------------------------------------------------------------------------- 1 | namespace ChartTools.Lyrics; 2 | 3 | public class Vocals 4 | { 5 | public StandardVocalsTrack Standard { get; set; } 6 | } 7 | -------------------------------------------------------------------------------- /ChartTools/IO/Appliables/ISongAppliable.cs: -------------------------------------------------------------------------------- 1 | namespace ChartTools.IO; 2 | 3 | internal interface ISongAppliable 4 | { 5 | public void ApplyToSong(Song song); 6 | } 7 | -------------------------------------------------------------------------------- /Docs/toc.yml: -------------------------------------------------------------------------------- 1 | - name: Articles 2 | href: articles/ 3 | - name: Api Documentation 4 | href: api/ 5 | - name: Repository 6 | href: https://github.com/theboxybear/charttools 7 | -------------------------------------------------------------------------------- /ChartTools/IO/Sections/Section.cs: -------------------------------------------------------------------------------- 1 | namespace ChartTools.IO.Sections; 2 | 3 | public class Section(string header) : List 4 | { 5 | public string Header { get; } = header; 6 | } 7 | -------------------------------------------------------------------------------- /ChartTools.Tests/Formatting.cs: -------------------------------------------------------------------------------- 1 | namespace ChartTools.Tests; 2 | 3 | public static class Formatting 4 | { 5 | public static string FormatCollection(IEnumerable items) => string.Join(' ', items); 6 | } 7 | -------------------------------------------------------------------------------- /ChartTools/Instruments/Drums.cs: -------------------------------------------------------------------------------- 1 | namespace ChartTools; 2 | 3 | public record Drums : Instrument 4 | { 5 | protected override InstrumentIdentity GetIdentity() => InstrumentIdentity.Drums; 6 | } 7 | -------------------------------------------------------------------------------- /ChartTools/IO/Exceptions/EntryException.cs: -------------------------------------------------------------------------------- 1 | namespace ChartTools.IO; 2 | 3 | public class EntryException : FormatException 4 | { 5 | public EntryException() : base("Cannot divide line into entry elements.") { } 6 | } 7 | -------------------------------------------------------------------------------- /Docs/templates/modern/partials/collection.tmpl.partial: -------------------------------------------------------------------------------- 1 | {{!Licensed to the .NET Foundation under one or more agreements. The .NET Foundation licenses this file to you under the MIT license.}} 2 | 3 | {{>partials/class}} 4 | -------------------------------------------------------------------------------- /Docs/templates/modern/src/main.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Licensed to the .NET Foundation under one or more agreements. 3 | * The .NET Foundation licenses this file to you under the MIT license. 4 | */ 5 | 6 | export default {} 7 | -------------------------------------------------------------------------------- /Docs/templates/modern/partials/item.tmpl.partial: -------------------------------------------------------------------------------- 1 | {{!Licensed to the .NET Foundation under one or more agreements. The .NET Foundation licenses this file to you under the MIT license.}} 2 | 3 | {{>partials/class.header}} 4 | -------------------------------------------------------------------------------- /ChartTools/Lyrics/Tracks/VocalsTrack.cs: -------------------------------------------------------------------------------- 1 | namespace ChartTools.Lyrics; 2 | 3 | public abstract class VocalsTrack(IList? markers = null) 4 | { 5 | public IList Phrases { get; } = markers ?? []; 6 | } 7 | -------------------------------------------------------------------------------- /Docs/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "profiles": { 3 | "Docs": { 4 | "commandName": "Project", 5 | "environmentVariables": { 6 | "SiteDir": "$(ProjectDir)\\" 7 | } 8 | } 9 | } 10 | } -------------------------------------------------------------------------------- /ChartTools/IO/Appliables/IInstrumentAppliable.cs: -------------------------------------------------------------------------------- 1 | namespace ChartTools.IO; 2 | 3 | internal interface IInstrumentAppliable 4 | where TChord : Chord 5 | { 6 | public void ApplyToInstrument(Instrument instrument); 7 | } 8 | -------------------------------------------------------------------------------- /ChartTools/Metadata/UnidentifiedMetadata.cs: -------------------------------------------------------------------------------- 1 | namespace ChartTools; 2 | 3 | public struct UnidentifiedMetadata 4 | { 5 | public string Key { get; init; } 6 | public string? Value { get; set; } 7 | public FileType Origin { get; set; } 8 | } 9 | -------------------------------------------------------------------------------- /Docs/templates/modern/ApiPage.html.primary.tmpl: -------------------------------------------------------------------------------- 1 | {{!Licensed to the .NET Foundation under one or more agreements. The .NET Foundation licenses this file to you under the MIT license.}} 2 | {{!master(layout/_master.tmpl)}} 3 | 4 | {{{content}}} 5 | -------------------------------------------------------------------------------- /ChartTools/IO/Anchor.cs: -------------------------------------------------------------------------------- 1 | namespace ChartTools.IO; 2 | 3 | internal readonly struct Anchor(uint position, TimeSpan value) : IReadOnlyTrackObject 4 | { 5 | public uint Position { get; } = position; 6 | 7 | public TimeSpan Value { get; } = value; 8 | } 9 | -------------------------------------------------------------------------------- /ChartTools/TrackObjects/TrackObjectBase.cs: -------------------------------------------------------------------------------- 1 | namespace ChartTools; 2 | 3 | public abstract class TrackObjectBase(uint position) : ITrackObject 4 | { 5 | public virtual uint Position { get; set; } = position; 6 | 7 | public TrackObjectBase() : this(0) { } 8 | } 9 | -------------------------------------------------------------------------------- /Docs/.config/dotnet-tools.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1, 3 | "isRoot": true, 4 | "tools": { 5 | "docfx": { 6 | "version": "2.78.3", 7 | "commands": [ 8 | "docfx" 9 | ], 10 | "rollForward": false 11 | } 12 | } 13 | } -------------------------------------------------------------------------------- /Docs/templates/modern/ApiPage.html.primary.js: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | 4 | exports.transform = function (model) { 5 | return model; 6 | } 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vs/ 2 | .vscode/ 3 | ignore/ 4 | build/* 5 | */obj 6 | */bin 7 | TestResults/ 8 | Debug/ 9 | ChartTools_Debug.sln 10 | Docs/DROP/ 11 | Docs/TEMP/ 12 | Docs/packages/ 13 | Docs/_site/ 14 | Docs/log.txt 15 | Docs/api/*.yml 16 | Docs/api/.manifest 17 | .idea/ 18 | -------------------------------------------------------------------------------- /ChartTools/IO/Configuration/ConfigurationExceptions.cs: -------------------------------------------------------------------------------- 1 | namespace ChartTools.IO.Configuration; 2 | 3 | internal static class ConfigurationExceptions 4 | { 5 | public static ArgumentException UnsupportedPolicy(Enum policy) => new("Policy is not supported.", $"{policy}"); 6 | } 7 | -------------------------------------------------------------------------------- /ChartTools/IO/Sections/ReservedSectionHeader.cs: -------------------------------------------------------------------------------- 1 | namespace ChartTools.IO.Sections; 2 | 3 | public readonly struct ReservedSectionHeader(string header, string dataSource) 4 | { 5 | public string Header { get; } = header; 6 | public string DataSource { get; } = dataSource; 7 | } 8 | -------------------------------------------------------------------------------- /ChartTools/Extensions/EqualityComparison.cs: -------------------------------------------------------------------------------- 1 | namespace ChartTools.Extensions; 2 | 3 | /// 4 | /// equivalent to the delegate 5 | /// 6 | public delegate bool EqualityComparison(T a, T b); 7 | -------------------------------------------------------------------------------- /ChartTools/TrackObjects/ILongTrackObject.cs: -------------------------------------------------------------------------------- 1 | namespace ChartTools; 2 | 3 | public interface ILongTrackObject : ITrackObject, ILongObject 4 | { 5 | /// 6 | /// Tick number marking the end of the object 7 | /// 8 | public uint EndPosition => Position + Length; 9 | } 10 | -------------------------------------------------------------------------------- /ChartTools/IO/ISerializerDataProvider.cs: -------------------------------------------------------------------------------- 1 | using ChartTools.IO.Configuration; 2 | 3 | namespace ChartTools.IO; 4 | 5 | internal interface ISerializerDataProvider where TSession : Session 6 | { 7 | public IEnumerable ProvideFor(IEnumerable source, TSession session); 8 | } 9 | -------------------------------------------------------------------------------- /ChartTools/IO/Parsers/TextParser.cs: -------------------------------------------------------------------------------- 1 | namespace ChartTools.IO.Parsing; 2 | 3 | internal abstract class TextParser(string header) : SectionParser(header) 4 | { 5 | protected override Exception GetHandleInnerException(string item, Exception innerException) 6 | => new LineException(item, innerException); 7 | } 8 | -------------------------------------------------------------------------------- /ChartTools/Notes/INote.cs: -------------------------------------------------------------------------------- 1 | namespace ChartTools; 2 | 3 | /// 4 | /// Interface of notes with a numerical identity 5 | /// 6 | public interface INote : ILongObject 7 | { 8 | /// 9 | /// Numerical value of the note identity 10 | /// 11 | public byte Index { get; set; } 12 | } 13 | -------------------------------------------------------------------------------- /ChartTools/IO/Configuration/Common/ICommonWritingConfiguration.cs: -------------------------------------------------------------------------------- 1 | namespace ChartTools.IO.Configuration.Common; 2 | 3 | public interface ICommonWritingConfiguration : ICommonConfiguration 4 | { 5 | /// 6 | public UnsupportedModifierPolicy UnsupportedModifierPolicy { get; init; } 7 | } 8 | -------------------------------------------------------------------------------- /ChartTools/IEmptyVerifiable.cs: -------------------------------------------------------------------------------- 1 | namespace ChartTools; 2 | 3 | /// 4 | /// Adds support for a property defining if an object is empty 5 | /// 6 | public interface IEmptyVerifiable 7 | { 8 | /// 9 | /// if containing no data 10 | /// 11 | public bool IsEmpty { get; } 12 | } 13 | -------------------------------------------------------------------------------- /ChartTools/TrackObjects/ITrackObject.cs: -------------------------------------------------------------------------------- 1 | namespace ChartTools; 2 | 3 | /// 4 | public interface ITrackObject : IReadOnlyTrackObject 5 | { 6 | /// 7 | public new uint Position { get; set; } 8 | 9 | uint IReadOnlyTrackObject.Position => Position; 10 | } 11 | -------------------------------------------------------------------------------- /ChartTools/IO/Ini/IniFileWriter.cs: -------------------------------------------------------------------------------- 1 | using ChartTools.IO.Sources; 2 | 3 | namespace ChartTools.IO.Ini; 4 | 5 | internal class IniFileWriter(WritingDataSource source, params ReadOnlySpan> serializers) 6 | : TextFileWriter(source, [], serializers) 7 | { 8 | protected override bool EndReplace(string line) => line.StartsWith('['); 9 | } 10 | -------------------------------------------------------------------------------- /ChartTools/IReadOnlyLongObject.cs: -------------------------------------------------------------------------------- 1 | namespace ChartTools; 2 | 3 | 4 | /// 5 | /// Interface for objects with a defined length in ticks where the length is read-only 6 | /// 7 | public interface IReadOnlyLongObject 8 | { 9 | /// 10 | /// Length of the object in ticks 11 | /// 12 | public uint Length { get; } 13 | } 14 | -------------------------------------------------------------------------------- /ChartTools/ILongObject.cs: -------------------------------------------------------------------------------- 1 | namespace ChartTools; 2 | 3 | /// 4 | /// Interface for objects with a defined length in ticks 5 | /// 6 | public interface ILongObject : IReadOnlyLongObject 7 | { 8 | /// 9 | public new uint Length { get; set; } 10 | 11 | uint IReadOnlyLongObject.Length => Length; 12 | } 13 | -------------------------------------------------------------------------------- /ChartTools/IO/Chart/Configuration/Sessions/ChartSession.cs: -------------------------------------------------------------------------------- 1 | using ChartTools.IO.Configuration; 2 | using ChartTools.IO.Formatting; 3 | 4 | namespace ChartTools.IO.Chart.Configuration.Sessions; 5 | 6 | internal abstract class ChartSession(FormattingRules? formatting) : Session(formatting) 7 | { 8 | public override abstract CommonChartConfiguration Configuration { get; } 9 | } 10 | -------------------------------------------------------------------------------- /Docs/articles/toc.yml: -------------------------------------------------------------------------------- 1 | - name: Getting started 2 | href: getting-started.md 3 | - name: Events 4 | href: events.md 5 | - name: Lyrics 6 | href: lyrics.md 7 | - name: Tools 8 | href: tools.md 9 | - name: Dynamic syntax 10 | href: dynamic-syntax.md 11 | - name: Configuration 12 | href: configuration.md 13 | - name: Improving performance 14 | href: improving-performance.md -------------------------------------------------------------------------------- /ChartTools/ChartTools.csproj.user: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | <_LastSelectedProfileId>C:\Users\joujo\OneDrive\Documents\GitHub\charttools\ChartTools\Properties\PublishProfiles\FolderProfile.pubxml 5 | 6 | -------------------------------------------------------------------------------- /ChartTools/IO/Configuration/ReadingConfiguration.cs: -------------------------------------------------------------------------------- 1 | using ChartTools.IO.Chart; 2 | using ChartTools.IO.Chart.Configuration; 3 | 4 | namespace ChartTools.IO.Configuration; 5 | 6 | public class ReadingConfiguration 7 | { 8 | public static readonly ReadingConfiguration Default = new(); 9 | 10 | public ChartReadingConfiguration Chart { get; set; } = ChartFile.DefaultReadConfig; 11 | } 12 | -------------------------------------------------------------------------------- /ChartTools/IO/Configuration/WritingConfiguration.cs: -------------------------------------------------------------------------------- 1 | using ChartTools.IO.Chart; 2 | using ChartTools.IO.Chart.Configuration; 3 | 4 | namespace ChartTools.IO.Configuration; 5 | 6 | public class WritingConfiguration 7 | { 8 | public static readonly WritingConfiguration Default = new(); 9 | 10 | public ChartWritingConfiguration Chart { get; set; } = ChartFile.DefaultWriteConfig; 11 | } 12 | -------------------------------------------------------------------------------- /ChartTools/IO/Sections/ReservedSectionHeaderSet.cs: -------------------------------------------------------------------------------- 1 | using System.Collections; 2 | 3 | namespace ChartTools.IO.Sections; 4 | 5 | public class ReservedSectionHeaderSet(IEnumerable headers) : IEnumerable 6 | { 7 | public IEnumerator GetEnumerator() => headers.GetEnumerator(); 8 | 9 | IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); 10 | } 11 | -------------------------------------------------------------------------------- /ChartTools/IO/Chart/Parsing/ChartParser.cs: -------------------------------------------------------------------------------- 1 | using ChartTools.IO.Chart.Configuration.Sessions; 2 | using ChartTools.IO.Parsing; 3 | 4 | namespace ChartTools.IO.Chart.Parsing; 5 | 6 | internal abstract class ChartParser(ChartReadingSession session, string header) : TextParser(header), ISongAppliable 7 | { 8 | public ChartReadingSession Session { get; } = session; 9 | 10 | public abstract void ApplyToSong(Song song); 11 | } 12 | -------------------------------------------------------------------------------- /ChartTools/Lyrics/Tracks/StandardVocalsTrack.cs: -------------------------------------------------------------------------------- 1 | using ChartTools.Events; 2 | 3 | namespace ChartTools.Lyrics; 4 | 5 | public class StandardVocalsTrack(IList? phrases = null, IList? notes = null) : VocalsTrack(phrases) 6 | { 7 | public IList Notes { get; } = notes ?? []; 8 | 9 | public IEnumerable ToGlobalEvents() => PhraseExtensions.ToGlobalEvents(Phrases, Notes); 10 | } 11 | -------------------------------------------------------------------------------- /ChartTools/Exceptions/UndefinedEnumException.cs: -------------------------------------------------------------------------------- 1 | namespace ChartTools; 2 | 3 | /// 4 | /// Exception thrown when using an value that is not defined 5 | /// 6 | public class UndefinedEnumException(Enum value) : ArgumentException($"{value.GetType().Name} \"{value}\" is not defined.") 7 | { 8 | /// 9 | /// Value used 10 | /// 11 | public Enum Value { get; } = value; 12 | } 13 | -------------------------------------------------------------------------------- /ChartTools/Instruments/GHLInstrument.cs: -------------------------------------------------------------------------------- 1 | namespace ChartTools; 2 | 3 | public record GHLInstrument : Instrument 4 | { 5 | public new GHLInstrumentIdentity InstrumentIdentity { get; init; } 6 | 7 | public GHLInstrument() { } 8 | public GHLInstrument(GHLInstrumentIdentity identity) => InstrumentIdentity = identity; 9 | 10 | protected override InstrumentIdentity GetIdentity() => (InstrumentIdentity)InstrumentIdentity; 11 | } 12 | -------------------------------------------------------------------------------- /ChartTools/Exceptions/Validator.cs: -------------------------------------------------------------------------------- 1 | namespace ChartTools; 2 | 3 | internal static class Validator 4 | { 5 | /// 6 | /// Validates that an value is defined. 7 | /// 8 | /// 9 | public static void ValidateEnum(T value) where T : struct, Enum 10 | { 11 | if (!Enum.IsDefined(value)) 12 | throw new UndefinedEnumException(value); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /Docs/templates/modern/partials/customMREFContent.tmpl.partial: -------------------------------------------------------------------------------- 1 | {{!Licensed to the .NET Foundation under one or more agreements. The .NET Foundation licenses this file to you under the MIT license.}} 2 | {{!Add your own custom template for the content for ManagedReference here}} 3 | {{#_splitReference}} 4 | {{#isCollection}} 5 | {{>partials/collection}} 6 | {{/isCollection}} 7 | {{#isItem}} 8 | {{>partials/item}} 9 | {{/isItem}} 10 | {{/_splitReference}} 11 | -------------------------------------------------------------------------------- /ChartTools/IO/Configuration/Common/ICommonReadingConfiguration.cs: -------------------------------------------------------------------------------- 1 | namespace ChartTools.IO.Configuration.Common; 2 | 3 | /// 4 | /// Reading options common to all file formats 5 | /// 6 | public interface ICommonReadingConfiguration : ICommonConfiguration 7 | { 8 | /// 9 | /// Policy for handling unknown sections in a file 10 | /// 11 | public UnknownSectionPolicy UnknownSectionPolicy { get; } 12 | } 13 | -------------------------------------------------------------------------------- /ChartTools/Events/EventArgumentHelper.cs: -------------------------------------------------------------------------------- 1 | namespace ChartTools.Events; 2 | 3 | public class EventArgumentHelper 4 | { 5 | public static class Global 6 | { 7 | public static class Lighting 8 | { 9 | public const string 10 | Blackout = "(blackout)", 11 | Chase = "(chase)", 12 | Color1 = "(color1)", 13 | Color2 = "(color2)", 14 | Flare = "(flare)", 15 | Strobe = "(strobe)", 16 | Sweep = "(sweep)"; 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /ChartTools/TrackObjects/IReadOnlyTrackObject.cs: -------------------------------------------------------------------------------- 1 | namespace ChartTools; 2 | 3 | /// 4 | /// Object located on a track 5 | /// 6 | public interface IReadOnlyTrackObject 7 | { 8 | /// 9 | /// Tick number on the track. 10 | /// 11 | /// A tick represents a subdivision of a beat. The number of subdivisions per beat is stored in . 12 | public uint Position { get; } 13 | } 14 | -------------------------------------------------------------------------------- /ChartTools/IO/Ini/IniKeySerializableAttribute.cs: -------------------------------------------------------------------------------- 1 | namespace ChartTools.IO.Ini; 2 | 3 | public class IniKeySerializableAttribute(string key) : KeySerializableAttribute(key) 4 | { 5 | public override FileType Format => FileType.Ini; 6 | 7 | protected override string GetValueString(object propValue) => propValue.ToString()!; 8 | 9 | public static IEnumerable<(string key, string value)> GetSerializable(object source) 10 | => GetSerializable(source); 11 | } 12 | -------------------------------------------------------------------------------- /ChartTools/Tools/LengthMerger.cs: -------------------------------------------------------------------------------- 1 | namespace ChartTools.Tools; 2 | 3 | public static class LengthMerger 4 | { 5 | public static T MergeLengths(this IEnumerable objects, T? target = null) 6 | where T : class, ILongTrackObject 7 | { 8 | uint 9 | start = objects.Min(o => o.Position), 10 | end = objects.Max(o => o.EndPosition); 11 | 12 | target ??= objects.First(); 13 | 14 | target.Position = start; 15 | target.Length = end - start; 16 | 17 | return target; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /ChartTools/IO/Chart/Serializing/UnknownSectionSerializer.cs: -------------------------------------------------------------------------------- 1 | using ChartTools.IO.Chart.Configuration.Sessions; 2 | using ChartTools.IO.Sections; 3 | 4 | namespace ChartTools.IO.Chart.Serializing; 5 | 6 | internal class UnknownSectionSerializer(string header, Section content, ChartWritingSession session) : Serializer, string>(header, content) 7 | { 8 | public ChartWritingSession Session { get; } = session; 9 | 10 | public override IEnumerable Serialize() => Content; 11 | } 12 | -------------------------------------------------------------------------------- /ChartTools/IO/Chart/Parsing/VariableInstrumentTrackParser.cs: -------------------------------------------------------------------------------- 1 | using ChartTools.IO.Chart.Configuration.Sessions; 2 | 3 | namespace ChartTools.IO.Chart.Parsing; 4 | 5 | internal abstract class VariableInstrumentTrackParser(Difficulty difficulty, TInstEnum instrument, ChartReadingSession session, string header) 6 | : TrackParser(difficulty, session, header) 7 | where TChord : Chord, new() 8 | where TInstEnum : Enum 9 | { 10 | public TInstEnum Instrument { get; } = instrument; 11 | } 12 | -------------------------------------------------------------------------------- /ChartTools/IO/Chart/Providers/EventProvider.cs: -------------------------------------------------------------------------------- 1 | using ChartTools.Events; 2 | using ChartTools.IO.Chart.Configuration.Sessions; 3 | using ChartTools.IO.Chart.Entries; 4 | 5 | namespace ChartTools.IO.Chart.Providers; 6 | 7 | internal class EventProvider : ISerializerDataProvider 8 | { 9 | public IEnumerable ProvideFor(IEnumerable source, ChartWritingSession session) 10 | => source.Select(e => new TrackObjectEntry(e.Position, "E", $"\"{e.EventData}\"")); 11 | } 12 | -------------------------------------------------------------------------------- /ChartTools/IO/Chart/ChartFileWriter.cs: -------------------------------------------------------------------------------- 1 | using ChartTools.IO.Sources; 2 | 3 | namespace ChartTools.IO.Chart; 4 | 5 | internal class ChartFileWriter(WritingDataSource source, IEnumerable? removedHeaders, params ReadOnlySpan> serializers) 6 | : TextFileWriter(source, removedHeaders, serializers) 7 | { 8 | protected override string? PreSerializerContent => "{"; 9 | protected override string? PostSerializerContent => "}"; 10 | 11 | protected override bool EndReplace(string line) => line.StartsWith('['); 12 | } 13 | -------------------------------------------------------------------------------- /ChartTools/IO/Serializers/GroupSerializer.cs: -------------------------------------------------------------------------------- 1 | namespace ChartTools.IO; 2 | 3 | internal abstract class GroupSerializer(string header, TContent content) 4 | : Serializer(header, content) 5 | { 6 | protected abstract IEnumerable[] LaunchProviders(); 7 | protected abstract IEnumerable CombineProviderResults(IEnumerable[] results); 8 | 9 | public override IEnumerable Serialize() => CombineProviderResults(LaunchProviders()); 10 | } 11 | -------------------------------------------------------------------------------- /ChartTools/Extensions/Collections/EagerEnumerable.cs: -------------------------------------------------------------------------------- 1 | using System.Collections; 2 | 3 | namespace ChartTools.Internal.Collections; 4 | 5 | internal class EagerEnumerable(Task> source) : IEnumerable 6 | { 7 | private IEnumerable? m_items; 8 | 9 | public IEnumerator GetEnumerator() 10 | { 11 | if (m_items is null) 12 | { 13 | source.Wait(); 14 | m_items = source.Result; 15 | } 16 | 17 | return m_items.GetEnumerator(); 18 | } 19 | 20 | IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); 21 | } 22 | -------------------------------------------------------------------------------- /ChartTools/IO/Chart/Providers/SpecialPhraseProvider.cs: -------------------------------------------------------------------------------- 1 | using ChartTools.IO.Chart.Configuration.Sessions; 2 | using ChartTools.IO.Chart.Entries; 3 | 4 | namespace ChartTools.IO.Chart.Providers; 5 | 6 | internal class SpeicalPhraseProvider : ISerializerDataProvider 7 | { 8 | public IEnumerable ProvideFor(IEnumerable source, ChartWritingSession session) 9 | => source.Select(sp => new TrackObjectEntry(sp.Position, "S", $"{sp.TypeCode} {sp.Length}")); 10 | } 11 | -------------------------------------------------------------------------------- /ChartTools/Exceptions/DesynchronizedAnchorException.cs: -------------------------------------------------------------------------------- 1 | namespace ChartTools; 2 | 3 | /// 4 | /// Exception thrown when an invalid operation is performed on a desynchronized anchored . 5 | /// 6 | public class DesynchronizedAnchorException(in TimeSpan anchor, string message) : Exception(message) 7 | { 8 | public TimeSpan Anchor { get; } = anchor; 9 | 10 | public DesynchronizedAnchorException(in TimeSpan anchor) 11 | : this(anchor, $"Invalid operation performed with desynchronized anchored tempo at {anchor}.") { } 12 | } 13 | -------------------------------------------------------------------------------- /ChartTools/Extensions/IInitializable.cs: -------------------------------------------------------------------------------- 1 | namespace ChartTools.Extensions; 2 | 3 | /// 4 | /// Defines an object that can be initialized 5 | /// 6 | public interface IInitializable 7 | { 8 | /// 9 | /// Has already been initialized 10 | /// 11 | public bool Initialized { get; } 12 | 13 | /// 14 | /// Does required initialization if not already done. 15 | /// 16 | /// if the object was not initialized prior to calling. 17 | public bool Initialize(); 18 | } 19 | -------------------------------------------------------------------------------- /Docs/templates/modern/src/mixins.scss: -------------------------------------------------------------------------------- 1 | /** 2 | * Licensed to the .NET Foundation under one or more agreements. 3 | * The .NET Foundation licenses this file to you under the MIT license. 4 | */ 5 | 6 | @mixin adjust-icon { 7 | font-family: bootstrap-icons; 8 | position: relative; 9 | margin-right: 0.5em; 10 | top: 0.2em; 11 | font-size: 1.25em; 12 | font-weight: normal; 13 | } 14 | 15 | @mixin underline-on-hover { 16 | text-decoration: none; 17 | 18 | &:hover, &:focus { 19 | text-decoration: underline; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /ChartTools/IO/Exceptions/SectionException.cs: -------------------------------------------------------------------------------- 1 | namespace ChartTools.IO; 2 | 3 | public class SectionException(string header, Exception innerException) : Exception($"Section \"{header}\" {innerException.Message}") 4 | { 5 | public string Header { get; } = header; 6 | 7 | public static SectionException EarlyEnd(string header) => new(header, new InvalidDataException("Section did not end within the provided lines")); 8 | 9 | public static SectionException MissingRequired(string header) => new(header, new InvalidDataException("Required section could not be found.")); 10 | } 11 | -------------------------------------------------------------------------------- /ChartTools/Lyrics/PhraseMarker.cs: -------------------------------------------------------------------------------- 1 | namespace ChartTools.Lyrics; 2 | 3 | /// 4 | /// Marker defining the position and length of a vocals phrase. 5 | /// 6 | /// 7 | public class PhraseMarker(uint position) : TrackObjectBase(position), ILongTrackObject 8 | { 9 | /// 10 | /// Manual length of the phrase defining a phrase end marker where applicable. 11 | /// 12 | /// A value of 0 defines the length to be up to the next phrase start. 13 | public uint Length { get; set; } 14 | } 15 | -------------------------------------------------------------------------------- /Docs/templates/modern/partials/enum.tmpl.partial: -------------------------------------------------------------------------------- 1 | {{!Licensed to the .NET Foundation under one or more agreements. The .NET Foundation licenses this file to you under the MIT license.}} 2 | 3 | {{>partials/class.header}} 4 | 5 | {{#seealso.0}} 6 |

{{__global.seealso}}

7 |
8 | {{/seealso.0}} 9 | {{#seealso}} 10 | {{#isCref}} 11 |
{{{type.specName.0.value}}}
12 | {{/isCref}} 13 | {{^isCref}} 14 |
{{{url}}}
15 | {{/isCref}} 16 | {{/seealso}} 17 | {{#seealso.0}} 18 |
19 | {{/seealso.0}} 20 | -------------------------------------------------------------------------------- /ChartTools/IO/Serializers/Serializer.cs: -------------------------------------------------------------------------------- 1 | namespace ChartTools.IO; 2 | 3 | internal abstract class Serializer(string header) 4 | { 5 | public string Header { get; } = header; 6 | 7 | public abstract IEnumerable Serialize(); 8 | public async Task> SerializeAsync() 9 | => await Task.Run(() => Serialize().ToArray()).ConfigureAwait(false); 10 | } 11 | 12 | internal abstract class Serializer(string header, TContent content) : Serializer(header) 13 | { 14 | public TContent Content { get; } = content; 15 | } 16 | -------------------------------------------------------------------------------- /ChartTools/IO/Chart/Configuration/CommonChartConfiguration.cs: -------------------------------------------------------------------------------- 1 | using ChartTools.IO.Configuration; 2 | using ChartTools.IO.Configuration.Common; 3 | 4 | namespace ChartTools.IO.Chart.Configuration; 5 | 6 | public abstract record CommonChartConfiguration : ICommonConfiguration 7 | { 8 | public DuplicateTrackObjectPolicy DuplicateTrackObjectPolicy { get; init; } 9 | public OverlappingSpecialPhrasePolicy OverlappingStarPowerPolicy { get; init; } 10 | public SnappedNotesPolicy SnappedNotesPolicy { get; init; } 11 | public SoloNoStarPowerPolicy SoloNoStarPowerPolicy { get; init; } 12 | } 13 | -------------------------------------------------------------------------------- /ChartTools/Extensions/Collections/IInitializable.cs: -------------------------------------------------------------------------------- 1 | namespace ChartTools.Extensions.Collections; 2 | 3 | /// 4 | /// Defines an object that can be initialized 5 | /// 6 | public interface IInitializable 7 | { 8 | /// 9 | /// Has already been initialized 10 | /// 11 | public bool Initialized { get; } 12 | 13 | /// 14 | /// Does required initialization if not already done. 15 | /// 16 | /// if the object was not initialized prior to calling. 17 | public bool Initialize(); 18 | } 19 | -------------------------------------------------------------------------------- /ChartTools/IO/Exceptions/LineException.cs: -------------------------------------------------------------------------------- 1 | namespace ChartTools.IO; 2 | 3 | /// 4 | /// thrown when a line of a text file cannot be parsed correctly. 5 | /// 6 | /// Invalid line string 7 | /// Error with the line 8 | public class LineException(string line, Exception innerException) : FormatException($"Line \"{line}\" {innerException.Message}", innerException) 9 | { 10 | /// 11 | /// Line that caused the exception 12 | /// 13 | public string Line { get; } = line; 14 | } 15 | -------------------------------------------------------------------------------- /ChartTools/IO/Ini/IniFileReader.cs: -------------------------------------------------------------------------------- 1 | using ChartTools.IO.Parsing; 2 | using ChartTools.IO.Sources; 3 | 4 | namespace ChartTools.IO.Ini; 5 | 6 | internal class IniFileReader(ReadingDataSource source, Metadata? existing) : TextFileReader(source) 7 | { 8 | public override IEnumerable Parsers => base.Parsers.Cast(); 9 | 10 | protected override TextParser? GetParser(string header) 11 | => header.Equals(IniFormatting.Header, StringComparison.OrdinalIgnoreCase) ? new IniParser(existing) : null; 12 | 13 | protected override bool IsSectionStart(string line) => !line.StartsWith('['); 14 | } 15 | -------------------------------------------------------------------------------- /ChartTools/IO/Parsers/SectionParser.cs: -------------------------------------------------------------------------------- 1 | namespace ChartTools.IO.Parsing; 2 | 3 | internal abstract class SectionParser(string header) : FileParser 4 | { 5 | public string Header { get; } = header; 6 | 7 | protected override Exception GetHandleException(T item, Exception innerException) 8 | => new SectionException(Header, GetHandleInnerException(item, innerException)); 9 | protected abstract Exception GetHandleInnerException(T item, Exception innerException); 10 | protected override Exception GetFinalizeException(Exception innerException) 11 | => new SectionException(Header, innerException); 12 | } 13 | -------------------------------------------------------------------------------- /ChartTools/IO/Chart/Parsing/UnknownSectionParser.cs: -------------------------------------------------------------------------------- 1 | using ChartTools.IO.Chart.Configuration.Sessions; 2 | using ChartTools.IO.Sections; 3 | 4 | namespace ChartTools.IO.Chart.Parsing; 5 | 6 | internal class UnknownSectionParser(ChartReadingSession session, string header) : ChartParser(session, header) 7 | { 8 | public override Section Result => GetResult(result); 9 | private readonly Section result = new(header); 10 | 11 | public override void ApplyToSong(Song song) => (song.UnknownChartSections ??= []).Add(Result); 12 | protected override void HandleItem(string item) => result.Add(item); 13 | } 14 | -------------------------------------------------------------------------------- /ChartTools/IO/Chart/Serializing/ChartKeySerializableAttribute.cs: -------------------------------------------------------------------------------- 1 | namespace ChartTools.IO.Chart.Serializing; 2 | 3 | public class ChartKeySerializableAttribute(string key) : KeySerializableAttribute(key) 4 | { 5 | public override FileType Format => FileType.Chart; 6 | 7 | protected override string GetValueString(object propValue) 8 | { 9 | string propString = propValue.ToString()!; 10 | return propValue is string ? $"\"{propString}\"" : propString; 11 | } 12 | 13 | public static IEnumerable<(string key, string value)> GetSerializable(object source) 14 | => GetSerializable(source); 15 | } 16 | -------------------------------------------------------------------------------- /ChartTools/IO/Chart/Serializing/GlobalEventSerializer.cs: -------------------------------------------------------------------------------- 1 | using ChartTools.Events; 2 | using ChartTools.IO.Chart.Configuration.Sessions; 3 | using ChartTools.IO.Chart.Entries; 4 | using ChartTools.IO.Chart.Providers; 5 | 6 | namespace ChartTools.IO.Chart.Serializing; 7 | 8 | internal class GlobalEventSerializer(IEnumerable content, ChartWritingSession session) 9 | : TrackObjectGroupSerializer>(ChartFormatting.GlobalEventHeader, content, session) 10 | { 11 | protected override IEnumerable[] LaunchProviders() 12 | => [ new EventProvider().ProvideFor(Content, Session) ]; 13 | } 14 | -------------------------------------------------------------------------------- /ChartTools/IO/Chart/Serializing/SyncTrackSerializer.cs: -------------------------------------------------------------------------------- 1 | using ChartTools.IO.Chart.Configuration.Sessions; 2 | using ChartTools.IO.Chart.Entries; 3 | using ChartTools.IO.Chart.Providers; 4 | 5 | namespace ChartTools.IO.Chart.Serializing; 6 | 7 | internal class SyncTrackSerializer(SyncTrack content, ChartWritingSession session) 8 | : TrackObjectGroupSerializer(ChartFormatting.SyncTrackHeader, content, session) 9 | { 10 | protected override IEnumerable[] LaunchProviders() 11 | => [new TempoProvider().ProvideFor(Content.Tempo, Session), new TimeSignatureProvider().ProvideFor(Content.TimeSignatures, Session)]; 12 | } 13 | -------------------------------------------------------------------------------- /ChartTools/IO/Chart/Providers/TimeSignatureProvider.cs: -------------------------------------------------------------------------------- 1 | using ChartTools.IO.Chart.Entries; 2 | 3 | namespace ChartTools.IO.Chart.Providers; 4 | 5 | internal class TimeSignatureProvider : SyncTrackProvider 6 | { 7 | protected override string ObjectType => "time signature"; 8 | 9 | protected override IEnumerable GetEntries(TimeSignature item) 10 | { 11 | byte writtenDenominator = (byte)Math.Log2(item.Denominator); 12 | string data = item.Numerator.ToString(); 13 | 14 | if (writtenDenominator == 1) 15 | data += ' ' + writtenDenominator.ToString(); 16 | 17 | yield return new(item.Position, "TS", data); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /ChartTools/IO/Chart/Serializing/TrackObjectGroupSerializer.cs: -------------------------------------------------------------------------------- 1 | using ChartTools.Extensions.Linq; 2 | using ChartTools.IO.Chart.Configuration.Sessions; 3 | using ChartTools.IO.Chart.Entries; 4 | 5 | namespace ChartTools.IO.Chart.Serializing; 6 | 7 | internal abstract class TrackObjectGroupSerializer(string header, T content, ChartWritingSession session) 8 | : GroupSerializer(header, content) 9 | { 10 | public ChartWritingSession Session { get; } = session; 11 | 12 | protected override IEnumerable CombineProviderResults(IEnumerable[] results) 13 | => results.AlternateBy(entry => entry.Position).Select(entry => entry.ToString()); 14 | } -------------------------------------------------------------------------------- /ChartTools/Extensions/EnumCache.cs: -------------------------------------------------------------------------------- 1 | namespace ChartTools.Extensions; 2 | 3 | /// 4 | /// Holds a cache of defined values for an enum where is to be called frequently. 5 | /// 6 | /// Type of enum 7 | internal static class EnumCache where T : struct, Enum 8 | { 9 | /// 10 | /// Cached values 11 | /// 12 | /// Generates the cache on first call. 13 | public static T[] Values => _values ??= [.. Enum.GetValues()]; 14 | private static T[]? _values; 15 | 16 | /// 17 | /// Clears the cache. 18 | /// 19 | public static void Clear() => _values = null; 20 | } 21 | -------------------------------------------------------------------------------- /Docs/templates/modern/src/search.scss: -------------------------------------------------------------------------------- 1 | /** 2 | * Licensed to the .NET Foundation under one or more agreements. 3 | * The .NET Foundation licenses this file to you under the MIT license. 4 | */ 5 | 6 | #search-results { 7 | line-height: 1.8; 8 | 9 | >.search-list { 10 | font-size: .9em; 11 | color: $secondary; 12 | } 13 | 14 | >.sr-items { 15 | flex: 1; 16 | 17 | .sr-item { 18 | margin-bottom: 1.5em; 19 | 20 | >.item-title { 21 | font-size: x-large; 22 | } 23 | 24 | >.item-href { 25 | color: #093; 26 | font-size: small; 27 | } 28 | 29 | >.item-brief { 30 | font-size: small; 31 | } 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /ChartTools/Metadata/Charter.cs: -------------------------------------------------------------------------------- 1 | using ChartTools.IO.Chart; 2 | using ChartTools.IO.Chart.Serializing; 3 | using ChartTools.IO.Ini; 4 | 5 | namespace ChartTools; 6 | 7 | /// 8 | /// Creator of the chart 9 | /// 10 | public class Charter 11 | { 12 | /// 13 | /// Name of the creator 14 | /// 15 | [ChartKeySerializable(ChartFormatting.Charter)] 16 | public string? Name { get; set; } 17 | 18 | /// 19 | /// Location of the image file to use as an icon in the Clone Hero song browser 20 | /// 21 | [IniKeySerializable(IniFormatting.Icon)] 22 | public string? Icon { get; set; } 23 | 24 | public override string ToString() => Name ?? string.Empty; 25 | } 26 | -------------------------------------------------------------------------------- /.github/workflows/docfx.yml: -------------------------------------------------------------------------------- 1 | jobs: 2 | generate_and_publish_docs: 3 | runs-on: ubuntu-latest 4 | name: Generate and publish the docs 5 | steps: 6 | - uses: actions/checkout@v1 7 | name: Checkout code 8 | - uses: nunit/docfx-action@v4.1.0 9 | name: Build Documentation 10 | with: 11 | args: Docs/docfx.json --debug 12 | - uses: maxheld83/ghpages@master 13 | name: Publish Documentation on GitHub Pages 14 | env: 15 | BUILD_DIR: Docs/_site # docfx's default output directory is _site 16 | GH_PAT: ${{ secrets.GH_PAT }} # See https://github.com/maxheld83/ghpages 17 | 18 | name: DocFx Clone, Build And Push 19 | on: 20 | push: 21 | branches: 22 | - stable 23 | -------------------------------------------------------------------------------- /ChartTools/IO/Chart/Parsing/GlobalEventParser.cs: -------------------------------------------------------------------------------- 1 | using ChartTools.Events; 2 | using ChartTools.IO.Chart.Configuration.Sessions; 3 | using ChartTools.IO.Chart.Entries; 4 | 5 | namespace ChartTools.IO.Chart.Parsing; 6 | 7 | internal class GlobalEventParser(ChartReadingSession session) : ChartParser(session, ChartFormatting.GlobalEventHeader) 8 | { 9 | public override List Result => GetResult(result); 10 | private readonly List result = []; 11 | 12 | protected override void HandleItem(string line) 13 | { 14 | TrackObjectEntry entry = new(line); 15 | result.Add(new(entry.Position, entry.Data.Trim('"'))); 16 | } 17 | 18 | public override void ApplyToSong(Song song) => song.GlobalEvents = Result; 19 | } 20 | -------------------------------------------------------------------------------- /ChartTools/IO/Chart/Providers/TempoProvider.cs: -------------------------------------------------------------------------------- 1 | using ChartTools.IO.Chart.Entries; 2 | 3 | namespace ChartTools.IO.Chart.Providers; 4 | 5 | internal class TempoProvider : SyncTrackProvider 6 | { 7 | protected override string ObjectType => "tempo marker"; 8 | 9 | protected override IEnumerable GetEntries(Tempo item) 10 | { 11 | if (item.Anchor is not null) 12 | yield return item.PositionSynced 13 | ? new(item.Position, "A", ChartFormatting.Float((float)item.Anchor.Value.TotalSeconds)) 14 | : throw new DesynchronizedAnchorException(item.Anchor.Value, $"Cannot write desynchronized anchored tempo at {item.Anchor}."); 15 | yield return new(item.Position, "B", ChartFormatting.Float(item.Value)); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Docs/templates/modern/partials/namespace.tmpl.partial: -------------------------------------------------------------------------------- 1 | {{!Licensed to the .NET Foundation under one or more agreements. The .NET Foundation licenses this file to you under the MIT license.}} 2 | 3 |

{{>partials/title}}

4 |
{{{summary}}}
5 |
{{{conceptual}}}
6 |
{{{remarks}}}
7 | 8 | {{#children}} 9 |

{{>partials/namespaceSubtitle}}

10 | {{#children}} 11 |
12 |
13 |
{{{summary}}}
14 |
15 | {{/children}} 16 | {{/children}} 17 | -------------------------------------------------------------------------------- /ChartTools/Extensions/Collections/Delayed/DelayedEnumerableSource.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Concurrent; 2 | 3 | namespace ChartTools.Extensions.Collections; 4 | 5 | public class DelayedEnumerableSource : IDisposable 6 | { 7 | public ConcurrentQueue Buffer { get; } = new(); 8 | 9 | public DelayedEnumerable Enumerable { get; } 10 | 11 | public bool AwaitingItems { get; private set; } = true; 12 | 13 | public DelayedEnumerableSource() => Enumerable = new(this); 14 | 15 | ~DelayedEnumerableSource() 16 | => Dispose(); 17 | 18 | public void Add(T item) => Buffer.Enqueue(item); 19 | 20 | public void EndAwait() => AwaitingItems = false; 21 | 22 | public void Dispose() 23 | { 24 | AwaitingItems = false; 25 | GC.SuppressFinalize(this); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Docs/templates/modern/src/highlight.scss: -------------------------------------------------------------------------------- 1 | /** 2 | * Licensed to the .NET Foundation under one or more agreements. 3 | * The .NET Foundation licenses this file to you under the MIT license. 4 | */ 5 | 6 | @import "highlight.js/scss/vs"; 7 | 8 | @include color-mode(dark) { 9 | /* stylelint-disable-next-line no-invalid-position-at-import-rule */ 10 | @import "highlight.js/scss/vs2015"; 11 | } 12 | 13 | .hljs { 14 | background-color: #f5f5f5; 15 | 16 | @media print { 17 | overflow-x: hidden; 18 | text-wrap: pretty; 19 | } 20 | } 21 | 22 | /* For code snippet line highlight */ 23 | pre > code .line-highlight { 24 | background-color: yellow; 25 | } 26 | 27 | @include color-mode(dark) { 28 | pre > code .line-highlight { 29 | background-color: #4a4a00; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /ChartTools/IO/Configuration/Common/ICommonConfiguration.cs: -------------------------------------------------------------------------------- 1 | namespace ChartTools.IO.Configuration.Common; 2 | 3 | /// 4 | /// Configuration object to direct the reading or writing of a file 5 | /// 6 | /// If , the default configuration for the file format will be used. 7 | public interface ICommonConfiguration 8 | { 9 | /// 10 | public DuplicateTrackObjectPolicy DuplicateTrackObjectPolicy { get; } 11 | 12 | /// 13 | public OverlappingSpecialPhrasePolicy OverlappingStarPowerPolicy { get; } 14 | 15 | /// 16 | public SnappedNotesPolicy SnappedNotesPolicy { get; } 17 | } 18 | -------------------------------------------------------------------------------- /ChartTools/IO/TextEntry.cs: -------------------------------------------------------------------------------- 1 | namespace ChartTools.IO; 2 | 3 | /// 4 | /// Line of text file data 5 | /// 6 | internal readonly struct TextEntry 7 | { 8 | /// 9 | /// Text before the equal sign 10 | /// 11 | public string Key { get; } 12 | 13 | /// 14 | /// Text after the equal sign 15 | /// 16 | public string? Value { get; } 17 | 18 | public TextEntry(string key, string value) 19 | { 20 | Key = key; 21 | Value = value; 22 | } 23 | 24 | public TextEntry(string line) 25 | { 26 | string[] split = line.Split('=', 2, StringSplitOptions.RemoveEmptyEntries); 27 | 28 | if (split.Length < 1) 29 | throw new EntryException(); 30 | 31 | Key = split[0].Trim(); 32 | Value = split.Length < 2 ? null : split[1].Trim(); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Severity: Minor|Moredate|Major** 11 | 12 | Describe the bug 13 | 14 | **Steps to reproduce** 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Actual behavior** 24 | 25 | **Additional context** 26 | Add any other context about the problem here. 27 | 28 | **Chart link** 29 | [Link to download a chart showcasing the bug. (Cloud service or Chorus Encore). Copyrighted audio should be avoided.](url here) 30 | 31 | **Screenshot (Optional)** 32 | Paste an image here to embed it as an `![image]` 33 | -------------------------------------------------------------------------------- /ChartTools/Events/LocalEvent.cs: -------------------------------------------------------------------------------- 1 | namespace ChartTools.Events; 2 | 3 | /// 4 | /// Event specific to an instrument and difficulty 5 | /// 6 | public class LocalEvent : Event 7 | { 8 | public bool IsSoloEvent => EventType is EventTypeHelper.Local.Solo or EventTypeHelper.Local.SoloEnd; 9 | 10 | public bool IsOwFaceEvent => EventType.StartsWith(EventTypeHeaderHelper.Local.OwFace); 11 | 12 | 13 | /// 14 | public LocalEvent(uint position) : base(position) { } 15 | 16 | /// 17 | public LocalEvent(uint position, string data) : base(position, data) { } 18 | 19 | /// 20 | public LocalEvent(uint position, string type, string argument = "") : base(position, type, argument) { } 21 | } 22 | -------------------------------------------------------------------------------- /ChartTools/Sync/TimeSignature.cs: -------------------------------------------------------------------------------- 1 | namespace ChartTools; 2 | 3 | /// 4 | /// Marker that alters the time signature 5 | /// 6 | /// Value of 7 | /// Value of 8 | /// Value of 9 | public class TimeSignature(uint position, byte numerator, byte denominator) : ITrackObject 10 | { 11 | /// " 12 | public uint Position { get; set; } = position; 13 | 14 | /// 15 | /// Value of a beat 16 | /// 17 | public byte Numerator { get; set; } = numerator; 18 | 19 | /// 20 | /// Beats per measure 21 | /// 22 | public byte Denominator { get; set; } = denominator; 23 | } 24 | -------------------------------------------------------------------------------- /ChartTools/IO/Formatting/FormattingEnums.cs: -------------------------------------------------------------------------------- 1 | namespace ChartTools.IO.Formatting; 2 | 3 | /// 4 | /// Key used to serialize 5 | /// 6 | [Flags] 7 | public enum AlbumTrackKey : byte 8 | { 9 | /// 10 | /// Use 11 | /// 12 | AlbumTrack, 13 | /// 14 | /// Use 15 | /// 16 | Track 17 | } 18 | 19 | /// 20 | /// Key used to serialize 21 | /// 22 | [Flags] 23 | public enum CharterKey : byte 24 | { 25 | /// 26 | /// Use 27 | /// 28 | Charter, 29 | /// 30 | /// Use 31 | /// 32 | Frets 33 | } 34 | 35 | -------------------------------------------------------------------------------- /Docs/templates/modern/partials/class.memberpage.tmpl.partial: -------------------------------------------------------------------------------- 1 | {{!Licensed to the .NET Foundation under one or more agreements. The .NET Foundation licenses this file to you under the MIT license.}} 2 | 3 | {{>partials/class.header}} 4 | 5 | {{#children}} 6 |

{{>partials/classSubtitle}}

7 | 8 | {{#children}} 9 |
10 |
11 |
{{{summary}}}
12 |
13 | {{/children}} 14 | 15 | {{/children}} 16 | 17 | {{#seealso.0}} 18 |

{{__global.seealso}}

19 |
20 | {{/seealso.0}} 21 | {{#seealso}} 22 | {{#isCref}} 23 |
{{{type.specName.0.value}}}
24 | {{/isCref}} 25 | {{^isCref}} 26 |
{{{url}}}
27 | {{/isCref}} 28 | {{/seealso}} 29 | {{#seealso.0}} 30 |
31 | {{/seealso.0}} 32 | -------------------------------------------------------------------------------- /ChartTools/Special/SpecialPhrase.cs: -------------------------------------------------------------------------------- 1 | namespace ChartTools; 2 | 3 | /// 4 | /// Base class for phrases that define an in-game event with a duration such as star power. 5 | /// 6 | /// 7 | /// Base constructor of special phrases. 8 | /// 9 | /// Position of the phrase 10 | /// Effect of the phrase 11 | /// Duration in ticks 12 | public abstract class SpecialPhrase(uint position, byte typeCode, uint length = 0) : ILongTrackObject 13 | { 14 | public uint Position { get; set; } = position; 15 | 16 | /// 17 | /// Numerical value of the phrase type 18 | /// 19 | public byte TypeCode { get; set; } = typeCode; 20 | 21 | /// 22 | /// Duration of the phrase in ticks 23 | /// 24 | public uint Length { get; set; } = length; 25 | } 26 | -------------------------------------------------------------------------------- /ChartTools/Extensions/FuncEqualityComparer.cs: -------------------------------------------------------------------------------- 1 | namespace ChartTools.Extensions; 2 | 3 | /// 4 | /// Delegate-based 5 | /// 6 | public class FuncEqualityComparer : IEqualityComparer 7 | { 8 | /// 9 | /// Method used to compare two objects 10 | /// 11 | public EqualityComparison Comparison { get; } 12 | 13 | /// 14 | /// Creates a new instance. 15 | /// 16 | /// Method used to compare two objects 17 | public FuncEqualityComparer(EqualityComparison comparison) 18 | { 19 | if (comparison is null) 20 | throw new ArgumentNullException(nameof(comparison), "The comparison is null"); 21 | 22 | Comparison = comparison; 23 | } 24 | 25 | public bool Equals(T? x, T? y) => Comparison(x, y); 26 | 27 | public int GetHashCode(T obj) => obj!.GetHashCode(); 28 | } 29 | -------------------------------------------------------------------------------- /Docs/docfx.json: -------------------------------------------------------------------------------- 1 | { 2 | "metadata": [ 3 | { 4 | "src": [ 5 | { 6 | "src": "../ChartTools", 7 | "files": [ 8 | "ChartTools.csproj" 9 | ] 10 | } 11 | ], 12 | "dest": "api" 13 | } 14 | ], 15 | "build": { 16 | "content": [ 17 | { 18 | "files": [ 19 | "**/*.{md,yml}" 20 | ], 21 | "exclude": [ 22 | "_site/**", 23 | "obj/**" 24 | ] 25 | } 26 | ], 27 | "resource": [ 28 | { 29 | "files": [ 30 | "images/**" 31 | ] 32 | } 33 | ], 34 | "dest": "_site", 35 | "template": [ 36 | "default", 37 | "modern" 38 | ], 39 | "globalMetadata": { 40 | "_appName": "ChartTools", 41 | "_appTitle": "ChartTools", 42 | "_enableSearch": true, 43 | "pdf": false 44 | } 45 | } 46 | } -------------------------------------------------------------------------------- /ChartTools/Extensions/Collections/Delayed/DelayedEnumerable.cs: -------------------------------------------------------------------------------- 1 | using System.Collections; 2 | 3 | namespace ChartTools.Extensions.Collections; 4 | 5 | public class DelayedEnumerable : IEnumerable 6 | { 7 | private readonly DelayedEnumerator m_enumerator; 8 | private readonly DelayedEnumerableSource m_source; 9 | 10 | /// 11 | /// if there are more items to be received 12 | /// 13 | public bool AwaitingItems => m_source.AwaitingItems; 14 | 15 | internal DelayedEnumerable(DelayedEnumerableSource source) 16 | { 17 | m_source = source; 18 | m_enumerator = new(source); 19 | } 20 | 21 | public IEnumerable EnumerateSynchronously() 22 | { 23 | while (AwaitingItems); 24 | return m_source.Buffer; 25 | } 26 | 27 | public IEnumerator GetEnumerator() => m_enumerator; 28 | 29 | IEnumerator IEnumerable.GetEnumerator() => m_enumerator; 30 | } 31 | -------------------------------------------------------------------------------- /ChartTools/Extensions/Collections/Delayed/DelayedEnumerator.cs: -------------------------------------------------------------------------------- 1 | using System.Collections; 2 | 3 | namespace ChartTools.Extensions.Collections; 4 | 5 | internal class DelayedEnumerator(DelayedEnumerableSource source) : IEnumerator 6 | { 7 | public T? Current { get; private set; } 8 | object? IEnumerator.Current => Current; 9 | public bool AwaitingItems => source.AwaitingItems; 10 | 11 | private bool WaitForItems() 12 | { 13 | while (source.Buffer.IsEmpty) 14 | if (!AwaitingItems && source.Buffer.IsEmpty) 15 | return false; 16 | 17 | return true; 18 | } 19 | 20 | public bool MoveNext() 21 | { 22 | if (!WaitForItems()) 23 | return false; 24 | 25 | if (!source.Buffer.TryDequeue(out T? item)) 26 | return false; 27 | 28 | Current = item; 29 | 30 | return true; 31 | } 32 | 33 | void IEnumerator.Reset() => throw new InvalidOperationException(); 34 | 35 | void IDisposable.Dispose() { } 36 | } 37 | -------------------------------------------------------------------------------- /ChartTools/Extensions/Linq/SectionReplacement.cs: -------------------------------------------------------------------------------- 1 | namespace ChartTools.Extensions.Linq; 2 | 3 | /// 4 | /// Defines a set of rules for performing a section replacement operation in a collection. 5 | /// 6 | /// Items to replace with 7 | /// Method that defines if a source marks the start of the section to replace 8 | /// Method that defines if a source item marks the end of the section to replace 9 | /// The replacement should be appended to the collection if the section to replace is not found 10 | /// Items matching the and predicates should not be included in the output. 11 | public readonly record struct SectionReplacement(IEnumerable Replacement, Predicate StartReplace, Predicate EndReplace, bool AddIfMissing); 12 | -------------------------------------------------------------------------------- /ChartTools/IO/Exceptions/ParseException.cs: -------------------------------------------------------------------------------- 1 | namespace ChartTools.IO; 2 | 3 | /// 4 | /// thrown when a parse error is encountered. 5 | /// 6 | /// 7 | /// 8 | /// 9 | public class ParseException(string? obj, string target, Type type) : FormatException($"Cannot convert {target} \"{obj}\" to {type.Name}") 10 | { 11 | /// 12 | /// Verbal description of the data being parsed 13 | /// 14 | public string? Object { get; } = obj; 15 | 16 | /// 17 | /// Verbal description of the parse destination 18 | /// 19 | public string Target { get; } = target; 20 | 21 | /// 22 | /// Target type being parsed to 23 | /// 24 | public Type Type { get; } = type; 25 | } 26 | -------------------------------------------------------------------------------- /ChartTools/IO/Chart/Configuration/Sessions/ChartReadingSession.cs: -------------------------------------------------------------------------------- 1 | using ChartTools.IO.Components; 2 | using ChartTools.IO.Configuration; 3 | 4 | namespace ChartTools.IO.Chart.Configuration.Sessions; 5 | 6 | internal class ChartReadingSession(ComponentList components, ChartReadingConfiguration? config) : ChartSession(null) 7 | { 8 | public ComponentList Components { get; set; } = components; 9 | 10 | public override ChartReadingConfiguration Configuration { get; } = config ?? ChartFile.DefaultReadConfig; 11 | 12 | public bool HandleTempolessAnchor(Anchor anchor) => Configuration.TempolessAnchorPolicy switch 13 | { 14 | TempolessAnchorPolicy.ThrowException => throw new Exception($"Tempo anchor at position {anchor.Position} does not have a parent tempo marker."), 15 | TempolessAnchorPolicy.Ignore => false, 16 | TempolessAnchorPolicy.Create => true, 17 | _ => throw ConfigurationExceptions.UnsupportedPolicy(Configuration.TempolessAnchorPolicy) 18 | }; 19 | } 20 | -------------------------------------------------------------------------------- /ChartTools/Events/EventTypeHeaderHelper.cs: -------------------------------------------------------------------------------- 1 | namespace ChartTools.Events; 2 | 3 | /// 4 | /// Provides helper strings for composite event types 5 | /// 6 | public static class EventTypeHeaderHelper 7 | { 8 | /// 9 | /// Helpers for 10 | /// 11 | public static class Global 12 | { 13 | public const string 14 | BassistMovement = "bass_", 15 | Crowd = "crowd_", 16 | DrummerMovement = "drum_", 17 | GuitaristMovement = "gtr_", 18 | GuitaristSolo = "solo_", 19 | GuitaristWail = "wail_", 20 | KeyboardMovement = "keys_", 21 | Phrase = "phrase_", 22 | SingerMovement = "sing_", 23 | Sync = "sync_"; 24 | } 25 | 26 | /// 27 | /// Helpers for 28 | /// 29 | public static class Local 30 | { 31 | public const string 32 | GHL6 = "ghl_6", 33 | OwFace = "ow_face_"; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /ChartTools/Notes/DrumsNote.cs: -------------------------------------------------------------------------------- 1 | namespace ChartTools; 2 | 3 | /// 4 | /// Note played by drums 5 | /// 6 | public class DrumsNote(DrumsLane lane) : LaneNote(lane) 7 | { 8 | public DrumsNote() : this(default) { } 9 | 10 | private bool m_isCymbal = false; 11 | 12 | /// 13 | /// if the cymbal must be hit instead of the pad on supported drum sets 14 | /// 15 | /// notes cannot be cymbal. 16 | public bool IsCymbal 17 | { 18 | get => m_isCymbal; 19 | set 20 | { 21 | if ((Lane == DrumsLane.Red || Lane == DrumsLane.Green5Lane) && value) 22 | throw new InvalidOperationException("Red and 5-lane green notes cannot be cymbal."); 23 | 24 | m_isCymbal = value; 25 | } 26 | } 27 | 28 | /// 29 | /// Determines if the note is played by kicking 30 | /// 31 | public bool IsKick => Lane is DrumsLane.Kick or DrumsLane.DoubleKick; 32 | } 33 | -------------------------------------------------------------------------------- /ChartTools/Instruments/StandardInstrument.cs: -------------------------------------------------------------------------------- 1 | namespace ChartTools; 2 | 3 | public record StandardInstrument : Instrument 4 | { 5 | public new StandardInstrumentIdentity InstrumentIdentity { get; init; } 6 | 7 | /// 8 | /// Format of lead guitar and bass. Not applicable to other instruments. 9 | /// 10 | public MidiInstrumentOrigin MidiOrigin 11 | { 12 | get => m_midiOrigin; 13 | set 14 | { 15 | if (value is MidiInstrumentOrigin.GuitarHero1 && InstrumentIdentity is not StandardInstrumentIdentity.LeadGuitar) 16 | throw new ArgumentException($"{InstrumentIdentity} is not supported by Guitar Hero 1.", nameof(value)); 17 | 18 | m_midiOrigin = value; 19 | } 20 | } 21 | private MidiInstrumentOrigin m_midiOrigin; 22 | 23 | public StandardInstrument() { } 24 | 25 | public StandardInstrument(StandardInstrumentIdentity identity) => InstrumentIdentity = identity; 26 | 27 | protected override InstrumentIdentity GetIdentity() => (InstrumentIdentity)InstrumentIdentity; 28 | } 29 | -------------------------------------------------------------------------------- /ChartTools/Notes/LaneNote.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.CompilerServices; 2 | 3 | namespace ChartTools; 4 | 5 | public abstract class LaneNote : INote, ILongObject 6 | { 7 | /// 8 | public abstract byte Index { get; set; } 9 | 10 | /// 11 | /// Maximum length the note can be held for extra points 12 | /// 13 | public uint Sustain { get; set; } 14 | 15 | uint ILongObject.Length 16 | { 17 | get => Sustain; 18 | set => Sustain = value; 19 | } 20 | } 21 | 22 | public class LaneNote(TLane lane) : LaneNote 23 | where TLane : struct, Enum 24 | { 25 | public LaneNote() : this(default) { } 26 | 27 | public TLane Lane 28 | { 29 | get => lane; 30 | set => lane = value; 31 | } 32 | 33 | /// 34 | public override byte Index 35 | { 36 | get => Unsafe.As(ref lane); 37 | set => lane = Unsafe.As(ref value); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /ChartTools.Tests/ChartTools.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net9.0 5 | preview 6 | false 7 | enable 8 | Debug;Release 9 | enable 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | all 18 | runtime; build; native; contentfiles; analyzers; buildtransitive 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /ChartTools/IO/Chart/Configuration/ChartWritingConfiguration.cs: -------------------------------------------------------------------------------- 1 | using ChartTools.IO.Configuration; 2 | using ChartTools.IO.Configuration.Common; 3 | 4 | namespace ChartTools.IO.Chart.Configuration; 5 | 6 | public record ChartWritingConfiguration : CommonChartConfiguration, ICommonWritingConfiguration 7 | { 8 | public UnsupportedModifierPolicy UnsupportedModifierPolicy { get; init; } 9 | public ChartWritingConfiguration() : this(true) { } 10 | 11 | internal ChartWritingConfiguration(bool setDefaults) 12 | { 13 | if (setDefaults) 14 | { 15 | UnsupportedModifierPolicy = ChartFile.DefaultWriteConfig.UnsupportedModifierPolicy; 16 | DuplicateTrackObjectPolicy = ChartFile.DefaultWriteConfig.DuplicateTrackObjectPolicy; 17 | OverlappingStarPowerPolicy = ChartFile.DefaultWriteConfig.OverlappingStarPowerPolicy; 18 | SnappedNotesPolicy = ChartFile.DefaultWriteConfig.SnappedNotesPolicy; 19 | SoloNoStarPowerPolicy = ChartFile.DefaultWriteConfig.SoloNoStarPowerPolicy; 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /ChartTools/IO/Chart/Entries/NoteData.cs: -------------------------------------------------------------------------------- 1 | namespace ChartTools.IO.Chart.Entries; 2 | 3 | /// 4 | /// Line of chart data representing a 5 | /// 6 | internal readonly ref struct NoteData 7 | { 8 | /// 9 | /// Value of 10 | /// 11 | internal readonly byte Index; 12 | 13 | /// 14 | /// Value of 15 | /// 16 | internal readonly uint SustainLength; 17 | 18 | /// 19 | /// Creates an instance of . 20 | /// 21 | /// Data section of the line in the file 22 | /// 23 | internal NoteData(string data) 24 | { 25 | string[] split = data.Split(' ', 2, StringSplitOptions.RemoveEmptyEntries); 26 | 27 | if (split.Length < 2) 28 | throw new EntryException(); 29 | 30 | Index = ValueParser.ParseByte(split[0], "note index"); 31 | SustainLength = ValueParser.ParseUint(split[1], "sustain length"); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /ChartTools/Extensions/StringExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace ChartTools.Extensions; 2 | 3 | /// 4 | /// Provides additional methods to string 5 | /// 6 | internal static class StringExtensions 7 | { 8 | /// 9 | public static string VerbalEnumerate(this IEnumerable items, string lastItemPreceder) 10 | => VerbalEnumerate(lastItemPreceder, [.. items]); 11 | 12 | /// 13 | /// Enumerates items with commas and a set word preceding the last item. 14 | /// 15 | /// Word to place before the last item 16 | /// 17 | public static string VerbalEnumerate(string lastItemPreceder, ReadOnlySpan items) => items.Length switch 18 | { 19 | 0 => string.Empty, // "" 20 | 1 => items[0], // "Item1" 21 | 2 => $"{items[0]} {lastItemPreceder} {items[1]}", // "Item1 lastItemPreceder Item2" 22 | _ => $"{string.Join(", ", items[..^2])} {lastItemPreceder} {items[^1]}" // "Item1, Item2 lastItemPreceder Item3" 23 | }; 24 | } 25 | -------------------------------------------------------------------------------- /Docs/Docs.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net9.0 5 | enable 6 | enable 7 | Exe 8 | Always 9 | Debug;Release 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /ChartTools/IO/Chart/Configuration/Sessions/ChartWritingSession.cs: -------------------------------------------------------------------------------- 1 | using ChartTools.IO.Chart.Entries; 2 | using ChartTools.IO.Configuration; 3 | using ChartTools.IO.Formatting; 4 | 5 | namespace ChartTools.IO.Chart.Configuration.Sessions; 6 | 7 | internal class ChartWritingSession(ChartWritingConfiguration? config, FormattingRules? formatting) : ChartSession(formatting) 8 | { 9 | public override ChartWritingConfiguration Configuration { get; } = config ?? ChartFile.DefaultWriteConfig; 10 | 11 | public IEnumerable GetUnsupportedModifierChordEntries(Chord? previous, Chord current) 12 | => Configuration.UnsupportedModifierPolicy switch 13 | { 14 | UnsupportedModifierPolicy.ThrowException => throw new Exception($"Chord at position {current.Position} as an unsupported modifier for the chart format."), 15 | UnsupportedModifierPolicy.IgnoreChord => [], 16 | UnsupportedModifierPolicy.IgnoreModifier => current.GetChartNoteData(), 17 | UnsupportedModifierPolicy.Convert => current.GetChartModifierData(previous, this), 18 | _ => throw ConfigurationExceptions.UnsupportedPolicy(Configuration.UnsupportedModifierPolicy) 19 | }; 20 | } 21 | -------------------------------------------------------------------------------- /ChartTools/IO/Chart/Providers/SyncTrackProvider.cs: -------------------------------------------------------------------------------- 1 | using ChartTools.IO.Chart.Entries; 2 | using ChartTools.Extensions.Linq; 3 | using ChartTools.IO.Chart.Configuration.Sessions; 4 | 5 | namespace ChartTools.IO.Chart.Providers; 6 | 7 | internal abstract class SyncTrackProvider 8 | : ISerializerDataProvider 9 | where T : ITrackObject 10 | { 11 | protected abstract string ObjectType { get; } 12 | 13 | public IEnumerable ProvideFor(IEnumerable source, ChartWritingSession session) 14 | { 15 | List orderedPositions = []; 16 | 17 | foreach (T item in source) 18 | { 19 | if (session.HandleDuplicate(item.Position, ObjectType, () => 20 | { 21 | int index = orderedPositions.BinarySearchIndex(item.Position, out bool exactMatch); 22 | 23 | if (!exactMatch) 24 | orderedPositions.Insert(index, item.Position); 25 | 26 | return exactMatch; 27 | })) 28 | foreach (TrackObjectEntry entry in GetEntries(item)) 29 | yield return entry; 30 | 31 | orderedPositions.Add(item.Position); 32 | } 33 | } 34 | 35 | protected abstract IEnumerable GetEntries(T item); 36 | } 37 | -------------------------------------------------------------------------------- /ChartTools/IO/Chart/Serializing/MetadataSerializer.cs: -------------------------------------------------------------------------------- 1 | namespace ChartTools.IO.Chart.Serializing; 2 | 3 | internal class MetadataSerializer(Metadata content) : Serializer(ChartFormatting.MetadataHeader, content) 4 | { 5 | public override IEnumerable Serialize() 6 | { 7 | if (Content is null) 8 | yield break; 9 | 10 | IEnumerable<(string key, string value)> props = ChartKeySerializableAttribute.GetSerializable(Content) 11 | .Concat(ChartKeySerializableAttribute.GetSerializable(Content.Formatting)) 12 | .Concat(ChartKeySerializableAttribute.GetSerializable(Content.Charter) 13 | .Concat(ChartKeySerializableAttribute.GetSerializable(Content.InstrumentDifficulties)) 14 | .Concat(ChartKeySerializableAttribute.GetSerializable(Content.Streams))); 15 | 16 | foreach ((string key, string value) in props) 17 | yield return ChartFormatting.Line(key, value); 18 | 19 | if (Content.Year is not null) 20 | yield return ChartFormatting.Line("Year", $"\", {Content.Year}\""); 21 | 22 | foreach (UnidentifiedMetadata data in Content.UnidentifiedData.Where(d => d.Origin == FileType.Chart)) 23 | yield return ChartFormatting.Line(data.Key, data.Value); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /attribution.txt: -------------------------------------------------------------------------------- 1 | https://melanchall.github.io/drywetmidi/ 2 | 3 | MIT License 4 | 5 | Copyright (c) 2018 Maxim Dobroselsky 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in all 15 | copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | SOFTWARE. 24 | -------------------------------------------------------------------------------- /ChartTools/ChartTools.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net9.0 5 | enable 6 | enable 7 | True 8 | True 9 | False 10 | Library for editing Clone Hero song files in .NET 11 | 12 | README.md 13 | https://github.com/TheBoxyBear/charttools 14 | en-CA 15 | LICENSE 16 | True 17 | TheBoxyBear 18 | 19 | 20 | 21 | 22 | True 23 | \ 24 | 25 | 26 | True 27 | \ 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /ChartTools/IO/Chart/Providers/ChordProvider.cs: -------------------------------------------------------------------------------- 1 | using ChartTools.Extensions.Linq; 2 | using ChartTools.IO.Chart.Configuration.Sessions; 3 | using ChartTools.IO.Chart.Entries; 4 | 5 | namespace ChartTools.IO.Chart.Providers; 6 | 7 | internal class ChordProvider : ISerializerDataProvider 8 | { 9 | public IEnumerable ProvideFor(IEnumerable source, ChartWritingSession session) 10 | { 11 | List orderedPositions = []; 12 | Chord? previousChord = null; 13 | 14 | foreach (Chord chord in source) 15 | { 16 | if (session.HandleDuplicate(chord.Position, "chord", () => 17 | { 18 | int index = orderedPositions.BinarySearchIndex(chord.Position, out bool exactMatch); 19 | 20 | if (!exactMatch) 21 | orderedPositions.Insert(index, chord.Position); 22 | 23 | return exactMatch; 24 | })) 25 | foreach (TrackObjectEntry entry in (chord.ChartSupportedModifiers 26 | ? chord.GetChartModifierData(previousChord, session) 27 | : session.GetUnsupportedModifierChordEntries(previousChord, chord)).Concat(chord.GetChartNoteData())) 28 | yield return entry; 29 | 30 | previousChord = chord; 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /ChartTools/Tracks/UniqueTrackObjectCollection.cs: -------------------------------------------------------------------------------- 1 | using System.Collections; 2 | 3 | namespace ChartTools.Extensions.Collections; 4 | 5 | /// 6 | /// Set of track objects where each one must have a different position 7 | /// 8 | public class UniqueTrackObjectCollection(IEnumerable? items = null) : ICollection 9 | where T : ITrackObject 10 | { 11 | private readonly Dictionary items = items is null ? [] : items.ToDictionary(i => i.Position); 12 | 13 | public int Count => items.Count; 14 | 15 | bool ICollection.IsReadOnly => false; 16 | 17 | private void RemoveDuplicate(T item) => items.Remove(item.Position); 18 | 19 | public void Add(T item) 20 | { 21 | RemoveDuplicate(item); 22 | items.Add(item.Position, item); 23 | } 24 | 25 | public void Clear() => items.Clear(); 26 | 27 | public bool Contains(T item) => items.ContainsKey(item.Position); 28 | 29 | public void CopyTo(T[] array, int arrayIndex) => items.Values.CopyTo(array, arrayIndex); 30 | 31 | public bool Remove(T item) => items.Remove(item.Position); 32 | 33 | public IEnumerator GetEnumerator() => items.Values.GetEnumerator(); 34 | 35 | IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); 36 | } 37 | -------------------------------------------------------------------------------- /ChartTools/IO/Parsers/FileParser.cs: -------------------------------------------------------------------------------- 1 | namespace ChartTools.IO; 2 | 3 | internal abstract class FileParser 4 | { 5 | public bool ResultReady { get; private set; } 6 | public abstract object? Result { get; } 7 | 8 | public async Task StartAsyncParse(IEnumerable items) 9 | { 10 | await Task.Run(() => ParseBase(items)).ConfigureAwait(false); 11 | 12 | try { FinalizeParse(); } 13 | catch (Exception e) { throw GetFinalizeException(e); } 14 | } 15 | public void Parse(IEnumerable items) 16 | { 17 | ParseBase(items); 18 | 19 | try { FinalizeParse(); } 20 | catch (Exception e) { throw GetFinalizeException(e); } 21 | } 22 | private void ParseBase(IEnumerable items) 23 | { 24 | foreach (T item in items) 25 | try { HandleItem(item); } 26 | catch (Exception e) { throw GetHandleException(item, e); } 27 | } 28 | 29 | protected abstract void HandleItem(T item); 30 | 31 | protected virtual void FinalizeParse() => ResultReady = true; 32 | 33 | protected TResult GetResult(TResult result) 34 | => ResultReady ? result : throw new Exception("Result is not ready."); 35 | 36 | protected abstract Exception GetHandleException(T item, Exception innerException); 37 | 38 | protected abstract Exception GetFinalizeException(Exception innerException); 39 | } 40 | -------------------------------------------------------------------------------- /ChartTools/IO/Serializers/KeySerializable.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | 3 | namespace ChartTools.IO; 4 | 5 | /// 6 | /// Indicates that a property should be serialized with a specific key in a specific file format 7 | /// 8 | /// 9 | public abstract class KeySerializableAttribute(string key) : Attribute 10 | { 11 | /// 12 | /// Target format 13 | /// 14 | public abstract FileType Format { get; } 15 | 16 | /// 17 | /// Target key 18 | /// 19 | public string Key { get; } = key; 20 | 21 | /// 22 | /// Generates groups of non-null property values and their serialization keys. 23 | /// 24 | /// Object containing the properties 25 | protected static IEnumerable<(string key, string value)> GetSerializable(object source) 26 | where TAttribute : KeySerializableAttribute => 27 | from prop in source.GetType().GetProperties() 28 | let att = prop.GetCustomAttribute() 29 | where att is not null 30 | let value = prop.GetValue(source) 31 | where value is not null 32 | select (att.Key, att.GetValueString(value)); 33 | 34 | protected abstract string GetValueString(object propValue); 35 | } 36 | -------------------------------------------------------------------------------- /ChartTools/IO/ValueParser.cs: -------------------------------------------------------------------------------- 1 | namespace ChartTools.IO; 2 | 3 | internal static class ValueParser 4 | { 5 | public delegate bool TryParse(string? input, out T result); 6 | public static T Parse(string? value, string target, TryParse tryParse) where T : struct 7 | => tryParse(value, out T result) ? result : throw new ParseException(value, target, typeof(T)); 8 | 9 | public static bool ParseBool(string? value, string target) => Parse(value, target, bool.TryParse); 10 | public static byte ParseByte(string? value, string target) => Parse(value, target, byte.TryParse); 11 | public static sbyte ParseSbyte(string? value, string target) => Parse(value, target, sbyte.TryParse); 12 | public static short ParseShort(string? value, string target) => Parse(value, target, short.TryParse); 13 | public static ushort ParseUshort(string? value, string target) => Parse(value, target, ushort.TryParse); 14 | public static int ParseInt(string? value, string target) => Parse(value, target, int.TryParse); 15 | public static uint ParseUint(string? value, string target) => Parse(value, target, uint.TryParse); 16 | public static float ParseFloat(string? value, string target) => Parse(value, target, float.TryParse); 17 | } 18 | -------------------------------------------------------------------------------- /ChartTools/IO/DirectoryHandler.cs: -------------------------------------------------------------------------------- 1 | using ChartTools.IO.Formatting; 2 | using ChartTools.IO.Ini; 3 | 4 | namespace ChartTools.IO; 5 | 6 | public record DirectoryResult(T Result, Metadata Metadata); 7 | 8 | internal static class DirectoryHandler 9 | { 10 | public static DirectoryResult FromDirectory(string directory, Func read) 11 | { 12 | string 13 | iniPath = directory + @"\song.ini", 14 | chartPath = directory + @"\notes.chart"; 15 | 16 | Metadata iniMetadata = File.Exists(iniPath) ? IniFile.ReadMetadata(iniPath) : new(); 17 | 18 | T? value = default; 19 | 20 | if (File.Exists(chartPath)) 21 | value = read(chartPath, iniMetadata.Formatting); 22 | 23 | return new(value, iniMetadata); 24 | } 25 | public static async Task> FromDirectoryAsync( 26 | string directory, Func> read, CancellationToken cancellationToken) 27 | { 28 | string 29 | iniPath = directory + @"\song.ini", 30 | chartPath = directory + @"\notes.chart"; 31 | 32 | Metadata iniMetadata = File.Exists(iniPath) ? await IniFile.ReadMetadataAsync(iniPath, null, cancellationToken) : new(); 33 | 34 | T? value = default; 35 | 36 | if (File.Exists(chartPath)) 37 | value = await read(chartPath, iniMetadata.Formatting).ConfigureAwait(false); 38 | 39 | return new(value, iniMetadata); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /ChartTools/IO/Configuration/Session.cs: -------------------------------------------------------------------------------- 1 | using ChartTools.IO.Configuration.Common; 2 | using ChartTools.IO.Formatting; 3 | 4 | namespace ChartTools.IO.Configuration; 5 | 6 | internal abstract class Session(FormattingRules? formatting) 7 | { 8 | public abstract ICommonConfiguration Configuration { get; } 9 | 10 | public FormattingRules Formatting { get; set; } = formatting ?? new(); 11 | 12 | public bool HandleDuplicate(uint position, string objectType, Func checkDuplicate) => Configuration.DuplicateTrackObjectPolicy switch 13 | { 14 | DuplicateTrackObjectPolicy.ThrowException => checkDuplicate() 15 | ? throw new Exception($"Duplicate {objectType} on position {position}.") 16 | : true, 17 | DuplicateTrackObjectPolicy.IncludeAll => true, 18 | DuplicateTrackObjectPolicy.IncludeFirst => !checkDuplicate(), 19 | _ => throw ConfigurationExceptions.UnsupportedPolicy(Configuration.DuplicateTrackObjectPolicy), 20 | }; 21 | 22 | public bool HandleSnap(uint origin, uint position) => Configuration.SnappedNotesPolicy switch 23 | { 24 | SnappedNotesPolicy.ThrowException => throw new Exception($"Note at position {position} is within snapping distance from chord at position {origin}"), 25 | SnappedNotesPolicy.Snap => true, 26 | SnappedNotesPolicy.Ignore => false, 27 | _ => throw ConfigurationExceptions.UnsupportedPolicy(Configuration.SnappedNotesPolicy) 28 | }; 29 | } 30 | -------------------------------------------------------------------------------- /ChartTools/IO/Chart/Configuration/ChartReadingConfiguration.cs: -------------------------------------------------------------------------------- 1 | using ChartTools.IO.Configuration; 2 | using ChartTools.IO.Configuration.Common; 3 | 4 | namespace ChartTools.IO.Chart.Configuration; 5 | 6 | /// 7 | /// Set of options that control how a chart file is read 8 | /// 9 | public record ChartReadingConfiguration : CommonChartConfiguration, ICommonReadingConfiguration 10 | { 11 | /// 12 | public TempolessAnchorPolicy TempolessAnchorPolicy { get; init; } 13 | 14 | /// /> 15 | public UnknownSectionPolicy UnknownSectionPolicy { get; init; } 16 | 17 | public ChartReadingConfiguration() : this(true) { } 18 | 19 | internal ChartReadingConfiguration(bool setDefaults) 20 | { 21 | if (setDefaults) 22 | { 23 | DuplicateTrackObjectPolicy = ChartFile.DefaultReadConfig.DuplicateTrackObjectPolicy; 24 | OverlappingStarPowerPolicy = ChartFile.DefaultReadConfig.OverlappingStarPowerPolicy; 25 | SnappedNotesPolicy = ChartFile.DefaultReadConfig.SnappedNotesPolicy; 26 | SoloNoStarPowerPolicy = ChartFile.DefaultReadConfig.SoloNoStarPowerPolicy; 27 | TempolessAnchorPolicy = ChartFile.DefaultReadConfig.TempolessAnchorPolicy; 28 | UnknownSectionPolicy = ChartFile.DefaultReadConfig.UnknownSectionPolicy; 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Docs/templates/modern/src/helper.test.ts: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | 4 | import { breakWord, isSameURL } from './helper' 5 | 6 | test('break text', () => { 7 | expect(breakWord('Other APIs')).toEqual(['Other APIs']) 8 | expect(breakWord('System.CodeDom')).toEqual(['System.', 'Code', 'Dom']) 9 | expect(breakWord('System.Collections.Dictionary')).toEqual(['System.', 'Collections.', 'Dictionary<', 'string,', ' object>']) 10 | expect(breakWord('https://github.com/dotnet/docfx')).toEqual(['https://github.', 'com/', 'dotnet/', 'docfx']) 11 | }) 12 | 13 | test('is same URL', () => { 14 | expect(isSameURL({ pathname: '/' }, { pathname: '/' })).toBeTruthy() 15 | expect(isSameURL({ pathname: '/index.html' }, { pathname: '/' })).toBeTruthy() 16 | expect(isSameURL({ pathname: '/a/index.html' }, { pathname: '/a' })).toBeTruthy() 17 | expect(isSameURL({ pathname: '/a/index.html' }, { pathname: '/a/' })).toBeTruthy() 18 | expect(isSameURL({ pathname: '/a' }, { pathname: '/a/' })).toBeTruthy() 19 | expect(isSameURL({ pathname: '/a/foo.html' }, { pathname: '/a/foo' })).toBeTruthy() 20 | expect(isSameURL({ pathname: '/a/foo/' }, { pathname: '/a/foo' })).toBeTruthy() 21 | expect(isSameURL({ pathname: '/a/foo/index.html' }, { pathname: '/a/foo' })).toBeTruthy() 22 | 23 | expect(isSameURL({ pathname: '/a/foo/index.html' }, { pathname: '/a/bar' })).toBeFalsy() 24 | }) 25 | -------------------------------------------------------------------------------- /ChartTools/Chords/Chord.cs: -------------------------------------------------------------------------------- 1 | using ChartTools.IO.Chart.Configuration.Sessions; 2 | using ChartTools.IO.Chart.Entries; 3 | 4 | using System.Runtime.CompilerServices; 5 | 6 | namespace ChartTools; 7 | 8 | public abstract class Chord(uint position) : ITrackObject 9 | { 10 | public uint Position { get; set; } = position; 11 | 12 | public abstract IReadOnlyList Notes { get; } 13 | 14 | public abstract bool OpenExclusivity { get; } 15 | 16 | internal abstract bool ChartSupportedModifiers { get; } 17 | 18 | public abstract LaneNote CreateNote(byte index, uint length); 19 | 20 | internal abstract IEnumerable GetChartNoteData(); 21 | internal abstract IEnumerable GetChartModifierData(Chord? previous, ChartWritingSession session); 22 | } 23 | 24 | public abstract class Chord : Chord 25 | where TNote : LaneNote, new() 26 | where TLane : struct, Enum 27 | where TModifiers : struct, Enum 28 | { 29 | public override LaneNoteCollection Notes { get; } 30 | 31 | public TModifiers Modifiers { get; set; } 32 | 33 | internal abstract TModifiers DefaultModifiers { get; } 34 | 35 | public Chord(uint position) : base(position) 36 | => Notes = new(OpenExclusivity); 37 | 38 | public override LaneNote CreateNote(byte index, uint sustain) 39 | { 40 | TNote note = new() 41 | { 42 | Lane = Unsafe.As(ref index), 43 | Sustain = sustain 44 | }; 45 | 46 | Notes.Add(note); 47 | return note; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /ChartTools/Special/InstrumentSpecialPhrase.cs: -------------------------------------------------------------------------------- 1 | namespace ChartTools; 2 | 3 | /// 4 | /// Phrase related to an instrument that triggers an in-game event. 5 | /// 6 | public class InstrumentSpecialPhrase : SpecialPhrase 7 | { 8 | /// 9 | /// Type of the phrase that drives the gameplay effect 10 | /// 11 | public InstrumentSpecialPhraseType Type 12 | { 13 | get 14 | { 15 | InstrumentSpecialPhraseType typeEnum = (InstrumentSpecialPhraseType)TypeCode; 16 | return Enum.IsDefined(typeEnum) ? typeEnum : InstrumentSpecialPhraseType.Unknown; 17 | } 18 | set => TypeCode = value == InstrumentSpecialPhraseType.Unknown 19 | ? throw new ArgumentException($"{InstrumentSpecialPhraseType.Unknown} is not a valid explicit value.", nameof(value)) 20 | : (byte)value; 21 | } 22 | 23 | /// 24 | /// Creates an instance of . 25 | /// 26 | /// Effect of the phrase 27 | /// 28 | public InstrumentSpecialPhrase(uint position, InstrumentSpecialPhraseType type, uint length = 0) : base(position, (byte)type, length) { } 29 | 30 | /// 31 | /// 32 | /// 33 | /// 34 | public InstrumentSpecialPhrase(uint position, byte typeCode, uint length = 0) : base(position, typeCode, length) { } 35 | } 36 | -------------------------------------------------------------------------------- /ChartTools/Extensions/Linq/CollectionExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace ChartTools.Extensions.Linq; 2 | 3 | public static class CollectionExtensions 4 | { 5 | public static int BinarySearchIndex(this IList source, TKey target, Func keySelector, out bool exactMatch) where TKey : notnull, IComparable 6 | { 7 | int 8 | left = 0, 9 | right = source.Count - 1, 10 | middle, 11 | index = 0; 12 | 13 | while (left <= right) 14 | { 15 | middle = (left + right) / 2; 16 | 17 | switch (keySelector(source[middle]).CompareTo(target)) 18 | { 19 | case -1: 20 | index = left = middle + 1; 21 | break; 22 | case 0: 23 | exactMatch = true; 24 | return middle; 25 | case 1: 26 | index = right = middle - 1; 27 | break; 28 | } 29 | } 30 | 31 | exactMatch = false; 32 | return index; 33 | } 34 | 35 | public static int BinarySearchIndex(this IList source, T target, out bool exactMatch) where T : notnull, IComparable 36 | => BinarySearchIndex(source, target, t => t, out exactMatch); 37 | 38 | /// 39 | /// Removes all items in a that meet a condition 40 | /// 41 | /// Collection to remove items from 42 | /// Function that determines which items to remove 43 | public static void RemoveWhere(this ICollection source, Predicate predicate) 44 | { 45 | foreach (T item in source.Where(i => predicate(i))) 46 | source.Remove(item); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /ChartTools/Special/TrackSpecialPhrase.cs: -------------------------------------------------------------------------------- 1 | namespace ChartTools; 2 | 3 | /// 4 | /// Phrase related to a track that triggers an in-game event. 5 | /// 6 | public class TrackSpecialPhrase : SpecialPhrase 7 | { 8 | /// 9 | /// Type of the phrase that drives the gameplay effect 10 | /// 11 | public TrackSpecialPhraseType Type 12 | { 13 | get 14 | { 15 | TrackSpecialPhraseType typeEnum = (TrackSpecialPhraseType)TypeCode; 16 | return Enum.IsDefined(typeEnum) ? typeEnum : TrackSpecialPhraseType.Unknown; 17 | } 18 | set => TypeCode = value == TrackSpecialPhraseType.Unknown 19 | ? throw new ArgumentException($"{TrackSpecialPhraseType.Unknown} is not a valid explicit value.", nameof(value)) 20 | : (byte)value; 21 | } 22 | 23 | public bool IsFaceOff => Type is TrackSpecialPhraseType.Player1FaceOff or TrackSpecialPhraseType.Player2FaceOff; 24 | 25 | /// 26 | /// Creates an instance of . 27 | /// 28 | /// Effect of the phrase 29 | /// 30 | public TrackSpecialPhrase(uint position, TrackSpecialPhraseType type, uint length = 0) : base(position, (byte)type, length) { } 31 | 32 | /// 33 | /// 34 | /// 35 | /// 36 | public TrackSpecialPhrase(uint position, byte typeCode, uint length = 0) : base(position, typeCode, length) { } 37 | } 38 | -------------------------------------------------------------------------------- /ChartTools/IO/Chart/Parsing/StandardTrackParser.cs: -------------------------------------------------------------------------------- 1 | using ChartTools.IO.Chart.Configuration.Sessions; 2 | using ChartTools.IO.Chart.Entries; 3 | 4 | namespace ChartTools.IO.Chart.Parsing; 5 | 6 | internal class StandardTrackParser(Difficulty difficulty, StandardInstrumentIdentity instrument, ChartReadingSession session, string header) 7 | : VariableInstrumentTrackParser(difficulty, instrument, session, header) 8 | { 9 | public override void ApplyToSong(Song song) 10 | { 11 | StandardInstrument? inst = song.Instruments.Get(Instrument); 12 | 13 | if (inst is null) 14 | song.Instruments.Set(inst = new(Instrument)); 15 | 16 | ApplyToInstrument(inst); 17 | } 18 | 19 | protected override void HandleNoteEntry(StandardChord chord, in NoteData data) 20 | { 21 | switch (data.Index) 22 | { 23 | // Colored note 24 | case < 5: 25 | AddNote(new((StandardLane)(data.Index + 1)) { Sustain = data.SustainLength }); 26 | break; 27 | case 5: 28 | AddModifier(StandardChordModifiers.HopoInvert); 29 | return; 30 | case 6: 31 | AddModifier(StandardChordModifiers.Tap); 32 | return; 33 | case 7: 34 | AddNote(new(StandardLane.Open) { Sustain = data.SustainLength }); 35 | break; 36 | } 37 | 38 | void AddNote(LaneNote note) 39 | { 40 | if (CanAddNote(note.Index)) 41 | chord.Notes.Add(note); 42 | } 43 | 44 | void AddModifier(StandardChordModifiers modifier) 45 | { 46 | if (CanAddModifier(chord.Modifiers, modifier)) 47 | chord.Modifiers |= modifier; 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /ChartTools/IO/Components/ComponentList.cs: -------------------------------------------------------------------------------- 1 | namespace ChartTools.IO.Components; 2 | 3 | /// 4 | /// Set of components to include in a read/write operation 5 | /// 6 | public record ComponentList() 7 | { 8 | /// 9 | /// Creates a new with only non-instrument components included. 10 | /// 11 | public static ComponentList Global() => new() 12 | { 13 | Metadata = true, 14 | SyncTrack = true, 15 | GlobalEvents = true 16 | }; 17 | 18 | /// 19 | /// Creates a new with only all components included. 20 | /// 21 | public static ComponentList Full() => new() 22 | { 23 | Metadata = true, 24 | SyncTrack = true, 25 | GlobalEvents = true, 26 | Vocals = true, 27 | Instruments = InstrumentComponentList.Full() 28 | }; 29 | 30 | /// 31 | /// Include the 32 | /// 33 | public bool Metadata { get; set; } 34 | 35 | /// 36 | /// Include the 37 | /// 38 | public bool SyncTrack { get; set; } 39 | 40 | /// 41 | /// Include the set of 42 | /// 43 | public bool GlobalEvents { get; set; } 44 | 45 | /// 46 | /// Include the track 47 | /// 48 | public bool Vocals { get; set; } 49 | 50 | /// 51 | public InstrumentComponentList Instruments { get; set; } = new(); 52 | } 53 | -------------------------------------------------------------------------------- /Docs/templates/modern/src/docfx.ts: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | 4 | import 'bootstrap' 5 | import { options } from './helper' 6 | import { highlight } from './highlight' 7 | import { renderMarkdown } from './markdown' 8 | import { enableSearch } from './search' 9 | import { renderToc } from './toc' 10 | import { initTheme } from './theme' 11 | import { renderBreadcrumb, renderInThisArticle, renderNavbar } from './nav' 12 | 13 | import 'bootstrap-icons/font/bootstrap-icons.scss' 14 | import './docfx.scss' 15 | 16 | declare global { 17 | interface Window { 18 | docfx: { 19 | ready?: boolean, 20 | searchReady?: boolean, 21 | searchResultReady?: boolean, 22 | } 23 | } 24 | } 25 | 26 | async function init() { 27 | window.docfx = window.docfx || {} 28 | 29 | const { start } = await options() 30 | start?.() 31 | 32 | const pdfmode = navigator.userAgent.indexOf('docfx/pdf') >= 0 33 | if (pdfmode) { 34 | await Promise.all([ 35 | renderMarkdown(), 36 | highlight() 37 | ]) 38 | } else { 39 | await Promise.all([ 40 | initTheme(), 41 | enableSearch(), 42 | renderInThisArticle(), 43 | renderMarkdown(), 44 | renderNav(), 45 | highlight() 46 | ]) 47 | } 48 | 49 | window.docfx.ready = true 50 | 51 | async function renderNav() { 52 | const [navbar, toc] = await Promise.all([renderNavbar(), renderToc()]) 53 | renderBreadcrumb([...navbar, ...toc]) 54 | } 55 | } 56 | 57 | init().catch(console.error) 58 | -------------------------------------------------------------------------------- /ChartTools/IO/Chart/Entries/TrackObjectEntry.cs: -------------------------------------------------------------------------------- 1 | namespace ChartTools.IO.Chart.Entries; 2 | 3 | /// 4 | /// Line of chart file data representing a 5 | /// 6 | internal readonly struct TrackObjectEntry : IReadOnlyTrackObject 7 | { 8 | /// 9 | /// Value of 10 | /// 11 | public uint Position { get; } 12 | 13 | /// 14 | /// Type code of 15 | /// 16 | public string Type { get; } 17 | 18 | /// 19 | /// Additional data 20 | /// 21 | public string Data { get; } 22 | 23 | /// 24 | /// Creates an instance of see. 25 | /// 26 | /// Line in the file 27 | /// 28 | public TrackObjectEntry(string line) 29 | { 30 | TextEntry entry = new(line); 31 | 32 | if (entry.Value is null) 33 | throw new LineException(line, new FormatException("Line has no object data.")); 34 | 35 | string[] split = entry.Value.Split(' ', 2, StringSplitOptions.RemoveEmptyEntries); 36 | 37 | if (split.Length < 2) 38 | throw new LineException(line, new EntryException()); 39 | 40 | Type = split[0]; 41 | Data = split[1]; 42 | 43 | Position = ValueParser.ParseUint(entry.Key, "position"); 44 | } 45 | public TrackObjectEntry(uint position, string type, string data) 46 | { 47 | Position = position; 48 | Type = type; 49 | Data = data; 50 | } 51 | 52 | public override string ToString() => ChartFormatting.Line(Position.ToString(), $"{Type} {Data}"); 53 | } 54 | -------------------------------------------------------------------------------- /ChartTools/GlobalSuppressions.cs: -------------------------------------------------------------------------------- 1 | // This file is used by Code Analysis to maintain SuppressMessage 2 | // attributes that are applied to this project. 3 | // Project-level suppressions either have no target or are given 4 | // a specific target and scoped to a namespace, type, member, etc. 5 | 6 | using System.Diagnostics.CodeAnalysis; 7 | 8 | [assembly: SuppressMessage("Minor Code Smell", "S101:Types should be named in PascalCase", Justification = "GHL is an acronym", Scope = "type", Target = "~T:ChartTools.GHLChord")] 9 | 10 | [assembly: SuppressMessage("Style", "IDE0130:Namespace does not match folder structure", Justification = "Files should be able to be better organized without bloating namespaces", Scope = "namespace", Target = "~N:ChartTools")] 11 | [assembly: SuppressMessage("Style", "IDE0130:Namespace does not match folder structure", Justification = "Files should be able to be better organized without bloating namespaces", Scope = "namespace", Target = "~N:ChartTools.Extensions.Collections")] 12 | [assembly: SuppressMessage("Style", "IDE0130:Namespace does not match folder structure", Justification = "Files should be able to be better organized without bloating namespaces", Scope = "namespace", Target = "~N:ChartTools.IO")] 13 | [assembly: SuppressMessage("Style", "IDE0130:Namespace does not match folder structure", Justification = "Files should be able to be better organized without bloating namespaces", Scope = "namespace", Target = "~N:ChartTools.IO.Parsing")] 14 | [assembly: SuppressMessage("Design", "CA1069:Enums values should not be duplicated", Justification = "Music theory involves collisions between sharps and flats", Scope = "type", Target = "~T:ChartTools.Lyrics.VocalsPitchValue")] 15 | -------------------------------------------------------------------------------- /Docs/templates/modern/src/options.d.ts: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | 4 | import BootstrapIcons from 'bootstrap-icons/font/bootstrap-icons.json' 5 | import { HLJSApi } from 'highlight.js' 6 | import { AnchorJSOptions } from 'anchor-js' 7 | import { MermaidConfig } from 'mermaid' 8 | 9 | export type Theme = 'light' | 'dark' | 'auto' 10 | 11 | export type IconLink = { 12 | /** A [bootstrap-icons](https://icons.getbootstrap.com/) name */ 13 | icon: keyof typeof BootstrapIcons, 14 | 15 | /** The URL of this icon link */ 16 | href: string, 17 | 18 | /** The title of this icon link shown on mouse hover */ 19 | title?: string 20 | } 21 | 22 | /** 23 | * Enables customization of the website through the global `window.docfx` object. 24 | */ 25 | export type DocfxOptions = { 26 | /** Configures the default theme */ 27 | defaultTheme?: Theme, 28 | 29 | /** A list of icons to show in the header next to the theme picker */ 30 | iconLinks?: IconLink[], 31 | 32 | /** Configures [anchor-js](https://www.bryanbraun.com/anchorjs#options) options */ 33 | anchors?: AnchorJSOptions, 34 | 35 | /** Configures mermaid diagram options */ 36 | mermaid?: MermaidConfig, 37 | 38 | /** A list of [lunr languages](https://github.com/MihaiValentin/lunr-languages#readme) such as fr, es for full text search */ 39 | lunrLanguages?: string[], 40 | 41 | /** Hooks to app start event */ 42 | start?: () => void, 43 | 44 | /** Configures [hightlight.js](https://highlightjs.org/) */ 45 | configureHljs?: (hljs: HLJSApi) => void, 46 | 47 | /** Specifies if an image should show as lightbox when clicked. */ 48 | showLightbox?: (image: HTMLImageElement) => boolean, 49 | } 50 | -------------------------------------------------------------------------------- /ChartTools/IO/Ini/IniSerializer.cs: -------------------------------------------------------------------------------- 1 | using ChartTools.IO.Formatting; 2 | 3 | namespace ChartTools.IO.Ini; 4 | 5 | internal class IniSerializer(Metadata content) : Serializer(IniFormatting.Header, content) 6 | { 7 | public override IEnumerable Serialize() 8 | { 9 | if (Content is null) 10 | yield break; 11 | 12 | IEnumerable<(string key, string value)> props = IniKeySerializableAttribute.GetSerializable(Content) 13 | .Concat(IniKeySerializableAttribute.GetSerializable(Content.Formatting)) 14 | .Concat(IniKeySerializableAttribute.GetSerializable(Content.Charter) 15 | .Concat(IniKeySerializableAttribute.GetSerializable(Content.InstrumentDifficulties))); 16 | 17 | foreach ((string key, string value) in props) 18 | yield return IniFormatting.Line(key, value.ToString()); 19 | 20 | foreach (UnidentifiedMetadata data in Content.UnidentifiedData.Where(x => x.Origin is FileType.Ini)) 21 | yield return IniFormatting.Line(data.Key, data.Value); 22 | 23 | if (Content.AlbumTrack is not null) 24 | { 25 | if (Content.Formatting.AlbumTrackKey.HasFlag(AlbumTrackKey.Track)) 26 | yield return IniFormatting.Line(IniFormatting.Track, Content.AlbumTrack.ToString()); 27 | 28 | if (Content.Formatting.AlbumTrackKey.HasFlag(AlbumTrackKey.AlbumTrack)) 29 | yield return IniFormatting.Line(IniFormatting.AlbumTrack, Content.AlbumTrack.ToString()); 30 | } 31 | 32 | if (Content.Charter is not null) 33 | { 34 | if (Content.Formatting.CharterKey.HasFlag(CharterKey.Charter)) 35 | yield return IniFormatting.Line(IniFormatting.Charter, Content.Charter.Name?.ToString()); 36 | 37 | if (Content.Formatting.CharterKey.HasFlag(CharterKey.Frets)) 38 | yield return IniFormatting.Line(IniFormatting.Frets, Content.Charter.Name?.ToString()); 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /ChartTools/IO/Chart/Parsing/DrumsTrackParser.cs: -------------------------------------------------------------------------------- 1 | using ChartTools.Extensions.Linq; 2 | using ChartTools.IO.Chart.Configuration.Sessions; 3 | using ChartTools.IO.Chart.Entries; 4 | 5 | namespace ChartTools.IO.Chart.Parsing; 6 | 7 | internal class DrumsTrackParser(Difficulty difficulty, ChartReadingSession session, string header) 8 | : TrackParser(difficulty, session, header) 9 | { 10 | public override void ApplyToSong(Song song) 11 | { 12 | song.Instruments.Drums ??= new(); 13 | ApplyToInstrument(song.Instruments.Drums); 14 | } 15 | 16 | protected override void HandleNoteEntry(DrumsChord chord, in NoteData data) 17 | { 18 | switch (data.Index) 19 | { 20 | // Note 21 | case < 5: 22 | AddNote(new DrumsNote((DrumsLane)data.Index) { Sustain = data.SustainLength }); 23 | break; 24 | // Double kick 25 | case 32: 26 | AddNote(new DrumsNote(DrumsLane.DoubleKick)); 27 | break; 28 | // Cymbal 29 | case > 65 and < 69: 30 | // NoteIndex of the note to set as cymbal 31 | byte seekedIndex = (byte)(data.Index - 64); 32 | 33 | if (chord.Notes.TryGetFirst(n => n.Index == seekedIndex, out DrumsNote? note)) 34 | { 35 | if (Session.HandleDuplicate(chord.Position, "drums note cymbal marker", () => note.IsCymbal)) 36 | note.IsCymbal = true; 37 | } 38 | else 39 | AddNote(new((DrumsLane)seekedIndex) { IsCymbal = true, Sustain = data.SustainLength }); 40 | break; 41 | case 109: 42 | AddModifier(DrumsChordModifiers.Flam); 43 | break; 44 | } 45 | 46 | void AddNote(DrumsNote note) 47 | { 48 | if (CanAddNote(note.Index)) 49 | chord.Notes.Add(note); 50 | } 51 | 52 | void AddModifier(DrumsChordModifiers modifier) 53 | { 54 | if (CanAddModifier(chord.Modifiers, modifier)) 55 | chord.Modifiers |= modifier; 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /ChartTools/IO/Chart/Parsing/GHLTrackParser.cs: -------------------------------------------------------------------------------- 1 | using ChartTools.IO.Chart.Configuration.Sessions; 2 | using ChartTools.IO.Chart.Entries; 3 | 4 | namespace ChartTools.IO.Chart.Parsing; 5 | 6 | internal class GHLTrackParser(Difficulty difficulty, GHLInstrumentIdentity instrument, ChartReadingSession session, string header) 7 | : VariableInstrumentTrackParser(difficulty, instrument, session, header) 8 | { 9 | public override void ApplyToSong(Song song) 10 | { 11 | GHLInstrument? inst = song.Instruments.Get(Instrument); 12 | 13 | if (inst is null) 14 | song.Instruments.Set(inst = new(Instrument)); 15 | 16 | ApplyToInstrument(inst); 17 | } 18 | 19 | protected override void HandleNoteEntry(GHLChord chord, in NoteData data) 20 | { 21 | switch (data.Index) 22 | { 23 | // White notes 24 | case < 3: 25 | AddNote(new LaneNote((GHLLane)(data.Index + 4)) { Sustain = data.SustainLength }); 26 | break; 27 | // Black 1 and 2 28 | case < 5: 29 | AddNote(new LaneNote((GHLLane)(data.Index - 2)) { Sustain = data.SustainLength }); 30 | break; 31 | case 5: 32 | AddModifier(GHLChordModifiers.HopoInvert); 33 | return; 34 | case 6: 35 | AddModifier(GHLChordModifiers.Tap); 36 | return; 37 | case 7: 38 | AddNote(new LaneNote(GHLLane.Open) { Sustain = data.SustainLength }); 39 | break; 40 | case 8: 41 | AddNote(new LaneNote(GHLLane.Black3) { Sustain = data.SustainLength }); 42 | break; 43 | } 44 | 45 | void AddNote(LaneNote note) 46 | { 47 | if (CanAddNote(note.Index)) 48 | chord.Notes.Add(note); 49 | } 50 | 51 | void AddModifier(GHLChordModifiers modifier) 52 | { 53 | if (CanAddModifier(chord.Modifiers, modifier)) 54 | chord.Modifiers |= modifier; 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /ChartTools/IO/Sources/ReadingDataSource.cs: -------------------------------------------------------------------------------- 1 | namespace ChartTools.IO.Sources; 2 | 3 | /// 4 | /// Represents a source of data to read from that can be used as a . 5 | /// 6 | /// Can be implicitly converted from a or . 7 | public class ReadingDataSource : DataSource 8 | { 9 | /// 10 | /// Creates a from a . 11 | /// 12 | /// Stream to source from. Must be readable through . 13 | /// The stream is not readable. 14 | /// The stream is kept alive after the lifetime of the . 15 | public ReadingDataSource(Stream stream) : base(stream) 16 | { 17 | if (!stream.CanRead) 18 | throw new ArgumentException("Stream is unable to be read.", nameof(stream)); 19 | } 20 | 21 | /// 22 | /// Creates a from a file path. 23 | /// 24 | /// Path of the file to source from 25 | /// 26 | /// Initializes the stream as a with and . 27 | /// The stream is disposed when disposing the . 28 | /// 29 | public ReadingDataSource(string path) : base(path, FileMode.Open, FileAccess.Read, FileShare.Write) { } 30 | 31 | /// 32 | public static implicit operator ReadingDataSource(Stream stream) => new(stream); 33 | 34 | /// 35 | public static implicit operator ReadingDataSource(string path) => new(path); 36 | } 37 | -------------------------------------------------------------------------------- /ChartTools/IO/Sources/DataSource.cs: -------------------------------------------------------------------------------- 1 | namespace ChartTools.IO.Sources; 2 | 3 | /// 4 | /// Represents data sources that can be used as a . This is an abstract class. 5 | /// 6 | /// Stream to source from. Derived types can impose minimum read/write access. 7 | public abstract class DataSource(Stream stream) : IDisposable 8 | { 9 | /// 10 | /// Source file path if one was used to create the source 11 | /// 12 | public string? Path { get; } 13 | 14 | /// 15 | /// Usable stream provided or generated 16 | /// 17 | /// Read/write access defined by the source's runtime type 18 | public Stream Stream { get; } = stream; 19 | 20 | /// 21 | /// The stream should be disposed along with the source. 22 | /// 23 | private readonly bool _disposeStream = false; 24 | 25 | /// 26 | /// Base constructor to create a from a file path 27 | /// 28 | /// File path 29 | /// How the file should be opened 30 | /// Read/write access 31 | /// External permissions while the file is open 32 | /// Initializes the stream as a that is disposed with the source. 33 | public DataSource(string path, FileMode mode, FileAccess access, FileShare share) : this(new FileStream(path, mode, access, share)) 34 | { 35 | Path = path; 36 | _disposeStream = true; 37 | } 38 | 39 | /// 40 | /// Disposes the source with the stream if generated by the source. 41 | /// 42 | public virtual void Dispose() 43 | { 44 | if (_disposeStream) 45 | Stream.Dispose(); 46 | 47 | GC.SuppressFinalize(this); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /Docs/Program.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | 3 | using Docfx; 4 | using Docfx.Dotnet; 5 | 6 | // Run this project to build and deploy the documentation website to preview on localhost. Website generated with DocFX https://dotnet.github.io/docfx/ 7 | // The API reference section is defined by yaml files in the /api directory - These files are generated from XML documentation in the code and should not be manually modified! (therefore are gitignored) 8 | // The Articles section is defined by markdown files in the /articles directory. 9 | 10 | // If localhost returns 404, try running `dotnet tool restore` from the project directory. 11 | 12 | string? dir = Environment.GetEnvironmentVariable("SiteDir"); 13 | string? config = dir + @"\docfx.json"; 14 | 15 | var content = File.ReadAllText(config); 16 | 17 | Console.WriteLine("------- Building site with DocFx -------"); 18 | 19 | // TODO Only build api if the assembly is more recent than the last site build 20 | await DotnetApiCatalog.GenerateManagedReferenceYamlFiles(config); 21 | 22 | await Docset.Build(config); 23 | 24 | Console.WriteLine("------- Build done -------"); 25 | Console.WriteLine(); 26 | 27 | using Process cmd = new() 28 | { 29 | StartInfo = new("dotnet", @$"docfx serve {dir}\_site") 30 | { 31 | RedirectStandardInput = true, 32 | RedirectStandardOutput = true, 33 | CreateNoWindow = true, 34 | UseShellExecute = false 35 | } 36 | }; 37 | 38 | // Process must be closed with Ctrl-C or will remain open in the background blocking port 8080. 39 | // If this happens (Windows): 40 | // netstat -aof | findstr :8080 41 | // taskkill / f / pid 42 | Process.Start(new ProcessStartInfo("http://localhost:8080") { UseShellExecute = true }); 43 | 44 | cmd.Start(); 45 | 46 | string? line = null; 47 | 48 | while ((line = cmd.StandardOutput.ReadLine()) is not null) 49 | Console.WriteLine(line); 50 | 51 | cmd.WaitForExit(); 52 | -------------------------------------------------------------------------------- /ChartTools/IO/Sections/SectionSet.cs: -------------------------------------------------------------------------------- 1 | using System.Collections; 2 | 3 | namespace ChartTools.IO.Sections; 4 | 5 | public abstract class SectionSet : IList> 6 | { 7 | private readonly List> m_sections = []; 8 | 9 | public abstract ReservedSectionHeaderSet ReservedHeaders { get; } 10 | 11 | #region IList 12 | public int Count => m_sections.Count; 13 | 14 | public bool IsReadOnly => false; 15 | 16 | public Section this[int index] 17 | { 18 | get => m_sections[index]; 19 | set 20 | { 21 | CheckHeader(value.Header); 22 | m_sections[index] = value; 23 | } 24 | } 25 | 26 | public int IndexOf(Section item) => m_sections.IndexOf(item); 27 | 28 | public void Insert(int index, Section item) 29 | { 30 | CheckHeader(item.Header); 31 | m_sections.Insert(index, item); 32 | } 33 | 34 | public void RemoveAt(int index) => m_sections.RemoveAt(index); 35 | 36 | public void Add(Section item) 37 | { 38 | CheckHeader(item.Header); 39 | m_sections.Add(item); 40 | } 41 | 42 | public void Clear() => m_sections.Clear(); 43 | 44 | public bool Contains(Section item) => m_sections.Contains(item); 45 | 46 | public void CopyTo(Section[] array, int arrayIndex) => m_sections.CopyTo(array, arrayIndex); 47 | 48 | public bool Remove(Section item) => m_sections.Remove(item); 49 | 50 | public IEnumerator> GetEnumerator() => m_sections.GetEnumerator(); 51 | 52 | IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); 53 | #endregion 54 | 55 | public Section? Get(string header) 56 | { 57 | CheckHeader(header); 58 | return m_sections.FirstOrDefault(s => s.Header == header); 59 | } 60 | 61 | private void CheckHeader(string header) 62 | { 63 | foreach (ReservedSectionHeader reserved in ReservedHeaders) 64 | if (reserved.Header == header) 65 | throw new Exception($"Header {header} is already modeled under {reserved.DataSource}"); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /Docs/templates/modern/src/docfx.scss: -------------------------------------------------------------------------------- 1 | /** 2 | * Licensed to the .NET Foundation under one or more agreements. 3 | * The .NET Foundation licenses this file to you under the MIT license. 4 | */ 5 | 6 | $enable-important-utilities: false; 7 | $container-max-widths: ( 8 | xxl: 1768px 9 | ) !default; 10 | 11 | @import "mixins"; 12 | @import "bootstrap/scss/bootstrap"; 13 | @import "highlight"; 14 | @import "layout"; 15 | @import "nav"; 16 | @import "toc"; 17 | @import "markdown"; 18 | @import "search"; 19 | @import "dotnet"; 20 | 21 | h1, 22 | h2, 23 | h3, 24 | h4, 25 | h5, 26 | h6, 27 | .xref, 28 | .text-break { 29 | word-wrap: break-word; 30 | word-break: break-word; 31 | } 32 | 33 | .divider { 34 | margin: 0 5px; 35 | color: #ccc; 36 | } 37 | 38 | article { 39 | // For REST API view source link 40 | span.small.pull-right { 41 | float: right; 42 | } 43 | 44 | img { 45 | max-width: 100%; 46 | height: auto; 47 | } 48 | } 49 | 50 | .codewrapper { 51 | position: relative; 52 | } 53 | 54 | .sample-response .response-content { 55 | max-height: 200px; 56 | } 57 | 58 | @media (width <= 768px) { 59 | #mobile-indicator { 60 | display: block; 61 | } 62 | 63 | .mobile-hide { 64 | display: none; 65 | } 66 | 67 | /* workaround for #hashtag url is no longer needed */ 68 | h1::before, 69 | h2::before, 70 | h3::before, 71 | h4::before { 72 | content: ""; 73 | display: none; 74 | } 75 | } 76 | 77 | @media print { 78 | @page { 79 | margin: .4in; 80 | } 81 | } 82 | 83 | .pdftoc { 84 | ul { 85 | list-style: none; 86 | } 87 | 88 | a { 89 | display: flex; 90 | text-decoration: none; 91 | color: var(--bs-body-color); 92 | 93 | .spacer { 94 | flex: 1; 95 | border-bottom: 1px dashed var(--bs-body-color); 96 | margin: .4em; 97 | } 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /ChartTools.Tests/AlternatingTests.cs: -------------------------------------------------------------------------------- 1 | using ChartTools.Extensions.Collections.Alternating; 2 | 3 | using Microsoft.VisualStudio.TestTools.UnitTesting; 4 | 5 | namespace ChartTools.Tests; 6 | 7 | [TestClass] 8 | public class SerialAlternatingTests 9 | { 10 | static readonly byte[] testArrayA = [ 1, 6, 2 ]; 11 | static readonly byte[] testArrayB = [ 3, 5, 6 ]; 12 | const string expected = "1 3 6 5 2 6"; 13 | 14 | [TestMethod] public void CreateEnumerableNull() 15 | => Assert.ThrowsException(() => new SerialAlternatingEnumerable(null!)); 16 | [TestMethod] public void CreateEnumerableEmpty() 17 | => Assert.ThrowsException(() => new SerialAlternatingEnumerable()); 18 | 19 | [TestMethod] public void Enumerate() 20 | => Assert.AreEqual(expected, Formatting.FormatCollection(new SerialAlternatingEnumerable(testArrayA, testArrayB))); 21 | } 22 | 23 | [TestClass] 24 | public class OrderedAlternatingTests 25 | { 26 | static readonly Func keyGetter = n => n; 27 | static readonly byte[] testArrayA = [1, 6, 2]; 28 | static readonly byte[] testArrayB = [3, 5, 6]; 29 | const string expected = "1 3 5 6 2 6"; 30 | 31 | [TestMethod] public void CreateEnumerableNullKeyGetter() 32 | => Assert.ThrowsException(() => new OrderedAlternatingEnumerable(null!, null!)); 33 | 34 | [TestMethod] public void CreateEnumerableNullEnumerables() 35 | => Assert.ThrowsException(() => new OrderedAlternatingEnumerable(null!, null!)); 36 | [TestMethod] public void CreateEnumerableEmptyEnumerables() 37 | => Assert.ThrowsException(() => new OrderedAlternatingEnumerable(keyGetter)); 38 | 39 | [TestMethod] public void Enumerate() 40 | => Assert.AreEqual(expected, Formatting.FormatCollection(new OrderedAlternatingEnumerable(keyGetter, testArrayA, testArrayB))); 41 | } 42 | -------------------------------------------------------------------------------- /ChartTools/Events/Event.cs: -------------------------------------------------------------------------------- 1 | namespace ChartTools.Events; 2 | 3 | /// 4 | /// Marker that defines an occurrence at a given point in a song. 5 | /// 6 | public abstract class Event(uint position) : ITrackObject 7 | { 8 | public uint Position { get; set; } = position; 9 | 10 | private string m_eventType = "Default"; 11 | 12 | /// 13 | /// Type of event as it is written in the file 14 | /// 15 | public string EventType 16 | { 17 | get => m_eventType; 18 | set 19 | { 20 | if (string.IsNullOrEmpty(value)) 21 | throw new FormatException("Event type is empty"); 22 | 23 | if (value.Contains(' ')) 24 | throw new FormatException("Event types cannot contain spaces"); 25 | 26 | m_eventType = value; 27 | } 28 | } 29 | 30 | /// 31 | /// Additional data to modify the outcome of the event 32 | /// 33 | /// A lack of argument is represented as an empty string. 34 | public string Argument { get; set; } = string.Empty; 35 | 36 | /// 37 | /// Combined event type and arguments where the first word is the type. 38 | /// 39 | public string EventData 40 | { 41 | get => Argument == string.Empty ? EventType : string.Join(' ', EventType, Argument); 42 | set 43 | { 44 | // Can possibly be optimized with a stack array 45 | string[] split = value.Split(' ', 2); 46 | 47 | 48 | EventType = split[0]; 49 | Argument = split.Length > 1 ? split[1] : string.Empty; 50 | } 51 | } 52 | 53 | public bool? ToggleState => EventType.EndsWith(EventTypeHelper.Common.ToggleOn) 54 | ? true : (EventType.EndsWith(EventTypeHelper.Common.ToggleOff) ? false : null); 55 | 56 | public Event(uint position, string data) 57 | : this(position) => EventData = data; 58 | 59 | public Event(uint position, string type, string argument) 60 | : this(position) 61 | { 62 | EventType = type; 63 | Argument = argument; 64 | } 65 | 66 | public override string ToString() => EventData; 67 | } 68 | -------------------------------------------------------------------------------- /ChartTools/Chords/DrumsChord.cs: -------------------------------------------------------------------------------- 1 | using ChartTools.IO.Chart; 2 | using ChartTools.IO.Chart.Configuration.Sessions; 3 | using ChartTools.IO.Chart.Entries; 4 | 5 | namespace ChartTools; 6 | 7 | /// 8 | /// Set of notes played simultaneously by drums 9 | /// 10 | public sealed class DrumsChord : Chord 11 | { 12 | /// 13 | /// 14 | /// 15 | /// Always for 16 | public override bool OpenExclusivity => false; 17 | 18 | internal override DrumsChordModifiers DefaultModifiers => DrumsChordModifiers.None; 19 | 20 | internal override bool ChartSupportedModifiers => true; 21 | 22 | public DrumsChord() : base(0) { } 23 | 24 | /// 25 | public DrumsChord(uint position) : base(position) { } 26 | 27 | /// 28 | /// Notes to add 29 | public DrumsChord(uint position, params ReadOnlySpan notes) : base(position) 30 | => Notes.AddRange(notes); 31 | 32 | /// 33 | public DrumsChord(uint position, params ReadOnlySpan notes) : base(position) 34 | => Notes.AddRange(notes); 35 | 36 | internal override IEnumerable GetChartNoteData() 37 | { 38 | foreach (DrumsNote note in Notes) 39 | { 40 | yield return ChartFormatting.NoteEntry(Position, note.Lane is DrumsLane.DoubleKick ? (byte)32 : note.Index, note.Sustain); 41 | 42 | if (note.IsCymbal) 43 | yield return ChartFormatting.NoteEntry(Position, (byte)(note.Lane + 64), 0); 44 | } 45 | } 46 | 47 | internal override IEnumerable GetChartModifierData(Chord? previous, ChartWritingSession session) 48 | { 49 | if (Modifiers.HasFlag(DrumsChordModifiers.Flam)) 50 | yield return ChartFormatting.NoteEntry(Position, 109, 0); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /Docs/templates/modern/src/highlight.ts: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | 4 | import { options } from './helper' 5 | 6 | export async function highlight() { 7 | const codeBlocks = document.querySelectorAll('pre code') 8 | if (codeBlocks.length <= 0) { 9 | return 10 | } 11 | 12 | const { default: hljs } = await import('highlight.js') 13 | const { configureHljs } = await options() 14 | configureHljs?.(hljs) 15 | 16 | document.querySelectorAll('pre code').forEach(block => { 17 | hljs.highlightElement(block as HTMLElement) 18 | }) 19 | 20 | document.querySelectorAll('pre code[highlight-lines]').forEach(block => { 21 | if (block.innerHTML === '') { 22 | return 23 | } 24 | 25 | const queryString = block.getAttribute('highlight-lines') 26 | if (!queryString) { 27 | return 28 | } 29 | 30 | const lines = block.innerHTML.split('\n') 31 | const ranges = queryString.split(',') 32 | for (const range of ranges) { 33 | let start = 0 34 | let end = 0 35 | const found = range.match(/^(\d+)-(\d+)?$/) 36 | if (found) { 37 | // consider region as `{startlinenumber}-{endlinenumber}`, in which {endlinenumber} is optional 38 | start = +found[1] 39 | end = +found[2] 40 | if (isNaN(end) || end > lines.length) { 41 | end = lines.length 42 | } 43 | } else { 44 | // consider region as a sigine line number 45 | if (isNaN(Number(range))) { 46 | continue 47 | } 48 | start = +range 49 | end = start 50 | } 51 | if (start <= 0 || end <= 0 || start > end || start > lines.length) { 52 | // skip current region if invalid 53 | continue 54 | } 55 | lines[start - 1] = '' + lines[start - 1] 56 | lines[end - 1] = lines[end - 1] + '' 57 | } 58 | 59 | block.innerHTML = lines.join('\n') 60 | }) 61 | } 62 | -------------------------------------------------------------------------------- /ChartTools/Lyrics/VocalsNote.cs: -------------------------------------------------------------------------------- 1 | namespace ChartTools.Lyrics; 2 | 3 | /// 4 | /// Note of a vocals track defining the pitch and displayed text of a single syllable. 5 | /// 6 | public class VocalsNote(uint position, VocalsPitch pitch, string? text = null) 7 | : INote, ILongTrackObject 8 | { 9 | public VocalsNote(uint position, VocalsPitchValue pitch = VocalsPitchValue.None, string? text = null) 10 | : this(position, new VocalsPitch(pitch), text) { } 11 | 12 | public uint Position { get; set; } = position; 13 | 14 | public uint Length { get; set; } 15 | 16 | public VocalsPitch Pitch { get; set; } = pitch; 17 | 18 | public byte Index 19 | { 20 | get => (byte)Pitch.Value; 21 | set => Pitch = (VocalsPitchValue)value; 22 | } 23 | 24 | /// 25 | /// Raw text data of the syllable 26 | /// 27 | public string RawText { get; set; } = text ?? string.Empty; 28 | 29 | /// 30 | /// Syllable text formatted to its in-game appearance 31 | /// 32 | /// Some special characters may remain. See Vocals format documentation for more information. 33 | // Duplicates the string up to four times. Can be optimized by editing a char buffer directly and rebuilding a string from it. 34 | // Low-level equivalents of Replace and Trim may also exist for char collections. 35 | public string DisplayedText => RawText 36 | .Replace("-", "") 37 | .Replace('=', '-') 38 | .Replace('§', '‿') 39 | .Trim('+', '#', '^', '*', '%'); 40 | 41 | /// 42 | /// if is the last syllable or the only syllable of its word 43 | /// 44 | public bool IsWordEnd 45 | { 46 | get => RawText.Length == 0 || RawText[^1] is '§' or '_' or not '-' and not '='; 47 | set 48 | { 49 | if (value) 50 | { 51 | if (!IsWordEnd) 52 | RawText = RawText[..^1]; 53 | } 54 | else if (IsWordEnd) 55 | RawText += '-'; 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /ChartTools/Chords/StandardChord.cs: -------------------------------------------------------------------------------- 1 | using ChartTools.IO.Chart; 2 | using ChartTools.IO.Chart.Configuration.Sessions; 3 | using ChartTools.IO.Chart.Entries; 4 | 5 | namespace ChartTools; 6 | 7 | /// 8 | /// Set of notes played simultaneously by a standard five-fret instrument 9 | /// 10 | public class StandardChord : Chord, StandardLane, StandardChordModifiers> 11 | { 12 | /// 13 | /// 14 | /// 15 | /// Always for 16 | public override bool OpenExclusivity => true; 17 | 18 | internal override StandardChordModifiers DefaultModifiers => StandardChordModifiers.None; 19 | 20 | internal override bool ChartSupportedModifiers => !Modifiers.HasFlag(StandardChordModifiers.ExplicitHopo); 21 | 22 | public StandardChord() : base(0) { } 23 | 24 | public StandardChord(uint position) : base(position) { } 25 | 26 | public StandardChord(uint position, params ReadOnlySpan> notes) : this(position) 27 | => Notes.AddRange(notes); 28 | 29 | public StandardChord(uint position, params ReadOnlySpan notes) : this(position) 30 | => Notes.AddRange(notes); 31 | 32 | internal override IEnumerable GetChartNoteData() 33 | => Notes.Select(note => ChartFormatting.NoteEntry(Position, note.Lane == StandardLane.Open ? (byte)7 : (byte)(note.Lane - 1), note.Sustain)); 34 | 35 | internal override IEnumerable GetChartModifierData(Chord? previous, ChartWritingSession session) 36 | { 37 | bool isInvert = Modifiers.HasFlag(StandardChordModifiers.HopoInvert); 38 | 39 | if (Modifiers.HasFlag(StandardChordModifiers.ExplicitHopo) && 40 | (previous is null || previous.Position <= session.Formatting.ChartHopoFrequency) != isInvert || isInvert) 41 | yield return ChartFormatting.NoteEntry(Position, 5, 0); 42 | if (Modifiers.HasFlag(StandardChordModifiers.Tap)) 43 | yield return ChartFormatting.NoteEntry(Position, 6, 0); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ChartTools 2 | ChartTools is a .NET library with the purpose of modeling song files for plastic guitar video games like Guitar Hero, Rock Band and Clone Hero. It currently supports reading of .chart and .ini files. 3 | 4 | If you find any bugs, you can report them in the [Issues section](https://github.com/TheBoxyBear/ChartTools/issues) of the repository. Make sure to use the "bug" label. 5 | 6 | As this project is in development, it should only be used with charts with a backup available. **I am not responsible for damages to charts!** 7 | 8 | ## Getting Started 9 | For an overview on installation and taking your first steps with ChartTools, see [Getting Started](Docs/articles/getting-started.md). A GitHub Pages website is available with detailed articles and API documentation. 10 | 11 | ## Contributing 12 | If you like to contribute to the development of ChartTools, feel free to comment on an issue, submit a pull request or submit your own issues. 13 | 14 | To test your code, copy the solution file and rename it to `ChartTools_Debug.sln`. From that solution, you can add new projects under the `Debug` directory. The debug solution and all its additional projects will automatically be git-ignored. 15 | 16 | ## License and Attribution 17 | This project is licensed under the GNU General Public License 3.0. See [LICENSE](LICENSE) for details. 18 | 19 | This project makes use of one or more third-party libraries to aid in functionality, see [attribution.txt](attribution.txt) for details. 20 | 21 | ## Special Thanks 22 | - [FireFox](https://github.com/FireFox2000000) for making the Moonscraper editor open-source 23 | - [TheNathannator](https://github.com/TheNathannator) for their direct contributions. 24 | - [Matthew Sitton](https://github.com/mdsitton), lead developer of Clone Hero for sharing their in-depth knowledge and general programming wisdom. 25 | - Members of the [Clone Hero Discord](https://discord.gg/clone-hero) and [Moonscraper Discord](https://discord.gg/wdnD83APhE), including but not limited to DarkAngel2096, drumbs (TheNathannator), FireFox, Kanske, mdsitton, Spachi, and XEntombmentX for their help in researching. 26 | -------------------------------------------------------------------------------- /ChartTools/Sync/Tempo.cs: -------------------------------------------------------------------------------- 1 | namespace ChartTools; 2 | 3 | /// 4 | /// Marker that alters the tempo 5 | /// 6 | public class Tempo(uint position, float value) : ITrackObject 7 | { 8 | /// 9 | /// Parent map the marker is contained 10 | /// 11 | public TempoMap? Map 12 | { 13 | get => m_map; 14 | internal set 15 | { 16 | if (value is not null) 17 | PositionSynced = false; 18 | 19 | m_map = value; 20 | } 21 | } 22 | private TempoMap? m_map; 23 | 24 | /// 25 | /// Only refer to the position if is . 26 | public uint Position 27 | { 28 | get => m_position; 29 | set 30 | { 31 | m_position = value; 32 | 33 | if (Anchor is not null) 34 | PositionSynced = false; 35 | } 36 | } 37 | private uint m_position = position; 38 | 39 | /// 40 | /// New tempo in beats per minute 41 | /// 42 | public float Value { get; set; } = value; 43 | 44 | /// 45 | /// Locks the tempo to a specific real-time position independent of the sync track. 46 | /// 47 | public TimeSpan? Anchor 48 | { 49 | get => m_anchor; 50 | set 51 | { 52 | bool valueNull = value is null; 53 | 54 | if (valueNull) 55 | { 56 | if (m_anchor is not null) 57 | Map?.RemoveAnchor(this); 58 | } 59 | else if (m_anchor is null) 60 | Map?.AddAnchor(this); 61 | 62 | m_anchor = value; 63 | PositionSynced = valueNull; 64 | } 65 | } 66 | private TimeSpan? m_anchor; 67 | 68 | /// 69 | /// Indicates if the tick position is up to date with . 70 | /// 71 | /// if the marker has no anchor. 72 | public bool PositionSynced { get; private set; } = true; 73 | 74 | public Tempo(TimeSpan anchor, float value) : this(0, value) => Anchor = anchor; 75 | 76 | internal void SyncPosition(uint position) 77 | { 78 | m_position = position; 79 | PositionSynced = true; 80 | } 81 | 82 | internal void DesyncPosition() => PositionSynced = false; 83 | } 84 | -------------------------------------------------------------------------------- /ChartTools.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.0.31612.314 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ChartTools.Tests", "ChartTools.Tests\ChartTools.Tests.csproj", "{A5A22025-AFC9-49D2-A338-CAE8EA750E18}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Docs", "Docs\Docs.csproj", "{B5A19C40-6CCD-48B2-827F-0D24B6271530}" 9 | EndProject 10 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ChartTools", "ChartTools\ChartTools.csproj", "{487BDC85-A45E-4896-9F1F-1A8EC145BD19}" 11 | EndProject 12 | Global 13 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 14 | Debug|Any CPU = Debug|Any CPU 15 | Release|Any CPU = Release|Any CPU 16 | EndGlobalSection 17 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 18 | {A5A22025-AFC9-49D2-A338-CAE8EA750E18}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 19 | {A5A22025-AFC9-49D2-A338-CAE8EA750E18}.Debug|Any CPU.Build.0 = Debug|Any CPU 20 | {A5A22025-AFC9-49D2-A338-CAE8EA750E18}.Release|Any CPU.ActiveCfg = Release|Any CPU 21 | {A5A22025-AFC9-49D2-A338-CAE8EA750E18}.Release|Any CPU.Build.0 = Release|Any CPU 22 | {B5A19C40-6CCD-48B2-827F-0D24B6271530}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 23 | {B5A19C40-6CCD-48B2-827F-0D24B6271530}.Debug|Any CPU.Build.0 = Debug|Any CPU 24 | {B5A19C40-6CCD-48B2-827F-0D24B6271530}.Release|Any CPU.ActiveCfg = Release|Any CPU 25 | {B5A19C40-6CCD-48B2-827F-0D24B6271530}.Release|Any CPU.Build.0 = Release|Any CPU 26 | {487BDC85-A45E-4896-9F1F-1A8EC145BD19}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 27 | {487BDC85-A45E-4896-9F1F-1A8EC145BD19}.Debug|Any CPU.Build.0 = Debug|Any CPU 28 | {487BDC85-A45E-4896-9F1F-1A8EC145BD19}.Release|Any CPU.ActiveCfg = Release|Any CPU 29 | {487BDC85-A45E-4896-9F1F-1A8EC145BD19}.Release|Any CPU.Build.0 = Release|Any CPU 30 | EndGlobalSection 31 | GlobalSection(SolutionProperties) = preSolution 32 | HideSolutionNode = FALSE 33 | EndGlobalSection 34 | GlobalSection(ExtensibilityGlobals) = postSolution 35 | SolutionGuid = {BBCCC1EF-9696-4588-8FA9-7924F6507D21} 36 | EndGlobalSection 37 | EndGlobal 38 | -------------------------------------------------------------------------------- /ChartTools/IO/Chart/ChartSectionSet.cs: -------------------------------------------------------------------------------- 1 | using ChartTools.Extensions; 2 | using ChartTools.IO.Sections; 3 | 4 | namespace ChartTools.IO.Chart; 5 | 6 | public class ChartSection : SectionSet 7 | { 8 | public static readonly ReservedSectionHeaderSet DefaultReservedHeaders; 9 | 10 | public override ReservedSectionHeaderSet ReservedHeaders => DefaultReservedHeaders; 11 | 12 | static ChartSection() 13 | { 14 | List headers = 15 | [ 16 | new(ChartFormatting.MetadataHeader, nameof(Song.Metadata)), 17 | new(ChartFormatting.SyncTrackHeader, nameof(Song.SyncTrack)), 18 | new(ChartFormatting.GlobalEventHeader, nameof(Song.GlobalEvents)) 19 | ]; 20 | 21 | Dictionary instrumentSources = new() 22 | { 23 | { 24 | ChartFormatting.InstrumentHeaderNames[InstrumentIdentity.StandardLeadGuitar], 25 | nameof(Song.Instruments.StandardLeadGuitar) 26 | }, 27 | { 28 | ChartFormatting.InstrumentHeaderNames[InstrumentIdentity.StandardRhythmGuitar], 29 | nameof(Song.Instruments.StandardRhythmGuitar) 30 | }, 31 | { 32 | ChartFormatting.InstrumentHeaderNames[InstrumentIdentity.StandardCoopGuitar], 33 | nameof(Song.Instruments.StandardCoopGuitar) 34 | }, 35 | { 36 | ChartFormatting.InstrumentHeaderNames[InstrumentIdentity.StandardBass], 37 | nameof(Song.Instruments.StandardBass) 38 | }, 39 | { 40 | ChartFormatting.InstrumentHeaderNames[InstrumentIdentity.StandardKeys], 41 | nameof(Song.Instruments.StandardKeys) 42 | }, 43 | { 44 | ChartFormatting.InstrumentHeaderNames[InstrumentIdentity.GHLLeadGuitar], 45 | nameof(Song.Instruments.GHLLeadGuitar) 46 | }, 47 | { 48 | ChartFormatting.InstrumentHeaderNames[InstrumentIdentity.GHLBass], 49 | nameof(Song.Instruments.GHLBass) 50 | }, 51 | { 52 | ChartFormatting.InstrumentHeaderNames[InstrumentIdentity.Drums], 53 | nameof(Song.Instruments.Drums) 54 | } 55 | }; 56 | 57 | headers.AddRange(instrumentSources.SelectMany(pair => 58 | from diff in EnumCache.Values 59 | select new ReservedSectionHeader(ChartFormatting.Header(pair.Value, diff), $"{pair.Value}.{diff}"))); 60 | 61 | DefaultReservedHeaders = new(headers); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /Docs/articles/configuration.md: -------------------------------------------------------------------------------- 1 | # Configuration 2 | 3 | Read and write operations in ChartTools feature customizable behavior for how they handle various error cases. These behaviors can be defines throguh the use of configuration objects. 4 | 5 | The following snippet uses a [ChartReadingConfiguration](~/api/ChartTools.IO.Chart.Configuration.ChartReadingConfiguration) to only include the first of duplicate [track objects](~/api/ChartTools.ITrackObject): 6 | 7 | ```csharp 8 | using ChartTools.IO.Chart.Configuration; 9 | 10 | Song song = ChartFile.ReadSong("notes.chart", new ChartReadingConfiguration() 11 | { 12 | DuplicateTrackObjectPolicy = DuplicateTrackObjectPolicy.IncludeFirst 13 | }); 14 | ``` 15 | 16 | When working with a non-specified file format, a [ReadingConfiguration](~/api/ChartTools.IO.Configuration.ReadingConfiguration) or [WritingConfiguration](~/api/ChartTools.IO.Configuration.WritingConfiguration) is used instead. These configuration envelop multiple configurations for the various file formats. 17 | 18 | Here is the same example as above, but using a `Song.FromFile`: 19 | 20 | ```csharp 21 | using ChartTools.IO.Configuration; 22 | 23 | Song song = Song.FromFile("notes.chart", new ReadingConfiguration() 24 | { 25 | Chart = new ChartReadingConfiguration() 26 | { 27 | DuplicateTrackObjectPolicy = DuplicateTrackObjectPolicy.IncludeFirst 28 | } 29 | }); 30 | ``` 31 | 32 | ## Default configurations 33 | Each file format defines default configurations for properties with no value specified or when no configuration object is provided. These defaults can be overwritten through the respective file class. 34 | 35 | In this ecxample, replacing the default prior to reading the song has the same effect as the original example: 36 | 37 | ```csharp 38 | using ChartTools.IO.Chart.Configuration; 39 | 40 | ChartFile.DefaultReadConfig = ChartFile.DefaultReadConfig with 41 | { 42 | DuplicateTrackObjectPolicy = DuplicateTrackObjectPolicy.IncludeFirst 43 | }; 44 | 45 | Song song = ChartFile.ReadSong("notes.chart"); 46 | ``` 47 | 48 | > [!NOTE] 49 | > To prevent breaking ongoing read/write operations, configurations are immutable. Hence, the above snippet uses a `with` expression to create a new configuration based on the default. 50 | 51 | -------------------------------------------------------------------------------- /ChartTools/IO/FileReader.cs: -------------------------------------------------------------------------------- 1 | using ChartTools.Extensions.Collections; 2 | using ChartTools.IO.Sources; 3 | 4 | namespace ChartTools.IO; 5 | 6 | internal abstract class FileReader(ReadingDataSource source) : IDisposable 7 | { 8 | public DataSource Source { get; } = source; 9 | 10 | public bool IsReading { get; protected set; } 11 | 12 | public abstract IEnumerable> Parsers { get; } 13 | 14 | public abstract void Read(); 15 | public abstract Task ReadAsync(CancellationToken cancellationToken); 16 | 17 | protected void CheckBusy() 18 | { 19 | if (IsReading) 20 | throw new InvalidOperationException("Cannot start read operation while the reader is busy."); 21 | } 22 | 23 | public virtual void Dispose() => Source.Dispose(); 24 | } 25 | 26 | internal abstract class FileReader(ReadingDataSource source) : FileReader(source) 27 | where TParser : FileParser 28 | { 29 | public record ParserContentGroup(TParser Parser, DelayedEnumerableSource Source); 30 | 31 | public override IEnumerable Parsers => parserGroups.Select(g => g.Parser); 32 | 33 | protected readonly List parserGroups = []; 34 | protected readonly List parseTasks = []; 35 | 36 | protected abstract TParser? GetParser(string header); 37 | 38 | public override void Read() 39 | { 40 | CheckBusy(); 41 | IsReading = true; 42 | 43 | parserGroups.Clear(); 44 | parseTasks.Clear(); 45 | 46 | ReadBase(false, CancellationToken.None); 47 | 48 | foreach (ParserContentGroup group in parserGroups) 49 | group.Parser.Parse(group.Source.Enumerable.EnumerateSynchronously()); 50 | 51 | IsReading = false; 52 | } 53 | 54 | public override async Task ReadAsync(CancellationToken cancellationToken) 55 | { 56 | CheckBusy(); 57 | IsReading = true; 58 | 59 | ReadBase(true, cancellationToken); 60 | await Task.WhenAll(parseTasks).ConfigureAwait(false); 61 | 62 | IsReading = false; 63 | } 64 | 65 | protected abstract void ReadBase(bool async, CancellationToken cancellationToken); 66 | 67 | public override void Dispose() 68 | { 69 | foreach (ParserContentGroup group in parserGroups) 70 | group.Source.Dispose(); 71 | 72 | foreach (Task task in parseTasks) 73 | task.Dispose(); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /ChartTools/Tools/PropertyMerger.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using ChartTools.Extensions.Linq; 3 | 4 | namespace ChartTools.Tools; 5 | 6 | /// 7 | /// Provides methods to merge properties between two instances 8 | /// 9 | public static class PropertyMerger 10 | { 11 | /// 12 | /// Replaces the property values of an instance with the first non-null equivalent from other instances. 13 | /// 14 | /// If overwriteNonNull is , only replaces property values that are null in the original instance. 15 | /// Item to assign the property values to 16 | /// If , only replaces property values that are null in the original instance. 17 | /// Items to pull new property values from in order of priority 18 | public static void Merge(T current, bool overwriteNonNull, bool deepMerge, params IEnumerable newValues) 19 | { 20 | T? newValue = current; 21 | 22 | Type 23 | stringType = typeof(string), 24 | nullableType = typeof(Nullable); 25 | 26 | foreach (PropertyInfo prop in GetProperties(typeof(T))) 27 | MergeValue(current, prop, GetValues(newValues.Cast(), prop)); 28 | 29 | void MergeValue(object? source, PropertyInfo prop, IEnumerable newValues) 30 | { 31 | object? value = prop.GetValue(source); 32 | 33 | if (deepMerge && !prop.PropertyType.IsPrimitive && prop.PropertyType != stringType && Nullable.GetUnderlyingType(prop.PropertyType) is null) 34 | { 35 | if (value is not null) 36 | foreach (PropertyInfo deepProp in GetProperties(prop.PropertyType)) 37 | MergeValue(value, deepProp, GetValues(newValues, deepProp)); 38 | } 39 | else if (value is null || overwriteNonNull) 40 | { 41 | object? newVal = newValues.FirstOrDefault(newVal => newVal is not null); 42 | 43 | if (newVal is not null) 44 | prop.SetValue(source, newVal); 45 | } 46 | } 47 | 48 | IEnumerable GetProperties(Type type) => type.GetProperties().Where(i => i.CanWrite); 49 | IEnumerable GetValues(IEnumerable sources, PropertyInfo prop) => sources.Select(prop.GetValue).NonNull(); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /Docs/templates/modern/src/markdown.scss: -------------------------------------------------------------------------------- 1 | /** 2 | * Licensed to the .NET Foundation under one or more agreements. 3 | * The .NET Foundation licenses this file to you under the MIT license. 4 | */ 5 | 6 | /* External link icon */ 7 | a.external[href]::after { 8 | font-family: bootstrap-icons; 9 | content: "\F1C5"; 10 | font-size: .6rem; 11 | margin: 0 .2em; 12 | display: inline-block; 13 | } 14 | 15 | /* Blockquote */ 16 | blockquote { 17 | border-style: solid; 18 | border-width: 0 0 0 3px; 19 | border-color: $secondary-border-subtle; 20 | margin: 1.2em 0 2em; 21 | padding: 0 .8em; 22 | display: block 23 | } 24 | 25 | @include color-mode(dark) { 26 | blockquote { 27 | border-color: $secondary-border-subtle-dark; 28 | } 29 | } 30 | 31 | /* Alerts */ 32 | .alert { 33 | break-inside: avoid; 34 | } 35 | 36 | .alert h5 { 37 | text-transform: uppercase; 38 | font-weight: bold; 39 | font-size: 1rem; 40 | 41 | &::before { 42 | @include adjust-icon; 43 | } 44 | } 45 | 46 | .alert:not(:has(h5))>p:last-child { 47 | margin-block-end: 0; 48 | } 49 | 50 | .alert-info h5::before { 51 | content: "\F431"; 52 | } 53 | 54 | .alert-warning h5::before { 55 | content: "\F333"; 56 | } 57 | 58 | .alert-danger h5::before { 59 | content: "\F623"; 60 | } 61 | 62 | /* For Embedded Video */ 63 | div.embeddedvideo { 64 | padding-top: 56.25%; 65 | position: relative; 66 | width: 100%; 67 | margin-bottom: 1em; 68 | } 69 | 70 | div.embeddedvideo iframe { 71 | position: absolute; 72 | inset: 0; 73 | width: 100%; 74 | height: 100%; 75 | } 76 | 77 | /* For code actions */ 78 | pre { 79 | position: relative; 80 | 81 | >.code-action { 82 | display: none; 83 | position: absolute; 84 | top: .25rem; 85 | right: .2rem; 86 | 87 | .bi-check-lg { 88 | font-size: 1.2rem; 89 | } 90 | } 91 | 92 | &:hover { 93 | >.code-action { 94 | display: block; 95 | } 96 | } 97 | } 98 | 99 | /* For tabbed content */ 100 | .tabGroup { 101 | margin-bottom: 1rem; 102 | 103 | >section { 104 | margin: 0; 105 | padding: 1rem; 106 | border-top: 0; 107 | border-top-left-radius: 0; 108 | border-top-right-radius: 0; 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /ChartTools/Tools/Printer.cs: -------------------------------------------------------------------------------- 1 | namespace ChartTools.Tools; 2 | 3 | internal static class Printer 4 | { 5 | private readonly struct ConsoleContent(string content, ConsoleColor color) 6 | { 7 | public string Content { get; } = content; 8 | public ConsoleColor Color { get; } = color; 9 | } 10 | 11 | public static void PrintTrack(Track track) 12 | { 13 | List> content = []; 14 | uint[] sustainEnds = new uint[6]; 15 | ConsoleColor[] laneColors = 16 | [ 17 | ConsoleColor.Green, 18 | ConsoleColor.Red, 19 | ConsoleColor.Yellow, 20 | ConsoleColor.Blue, 21 | ConsoleColor.DarkYellow 22 | ]; 23 | 24 | foreach (StandardChord chord in track.Chords.Where(c => c.Notes.Count > 0).OrderBy(t => t.Position)) 25 | { 26 | LaneNote? open = chord.Notes[StandardLane.Open]; 27 | List lineContent = []; 28 | 29 | if (open is not null) 30 | { 31 | lineContent.Add(new("-----", ConsoleColor.Magenta)); 32 | 33 | SetSustainEnd(open); 34 | 35 | for (int i = 1; i < sustainEnds.Length; i++) 36 | sustainEnds[i] = chord.Position; 37 | } 38 | else 39 | { 40 | if (chord.Notes.Count == 0) 41 | lineContent.Add(new(sustainEnds[0] >= chord.Position ? " | " : " ", ConsoleColor.Magenta)); 42 | else 43 | for (int i = 1; i < 6; i++) 44 | { 45 | LaneNote? note = chord.Notes[(StandardLane)i]; 46 | string text; 47 | 48 | if (note is null) 49 | text = sustainEnds[i] >= chord.Position ? "|" : " "; 50 | else 51 | { 52 | text = "O"; 53 | SetSustainEnd(note); 54 | } 55 | 56 | lineContent.Add(new(text, laneColors[i - 1])); 57 | } 58 | } 59 | 60 | content.Add(lineContent); 61 | 62 | void SetSustainEnd(LaneNote note) => sustainEnds[(int)note.Lane] = chord.Position + note.Sustain; 63 | } 64 | 65 | PrintLines(content); 66 | } 67 | 68 | private static void PrintLines(IEnumerable> content) 69 | { 70 | foreach (IEnumerable line in content.Reverse()) 71 | { 72 | Console.WriteLine(); 73 | 74 | foreach (ConsoleContent ct in line) 75 | { 76 | Console.ForegroundColor = ct.Color; 77 | Console.Write(ct.Content); 78 | } 79 | } 80 | 81 | Console.ResetColor(); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /Docs/templates/modern/src/toc.scss: -------------------------------------------------------------------------------- 1 | /** 2 | * Licensed to the .NET Foundation under one or more agreements. 3 | * The .NET Foundation licenses this file to you under the MIT license. 4 | */ 5 | 6 | $expand-stub-width: .85rem; 7 | 8 | .toc { 9 | min-width: 0; 10 | width: 100%; 11 | height: 100%; 12 | display: flex; 13 | flex-direction: column; 14 | 15 | ul { 16 | font-size: 14px; 17 | flex-direction: column; 18 | list-style-type: none; 19 | padding-left: 0; 20 | overflow-wrap: break-word; 21 | } 22 | 23 | li { 24 | font-weight: normal; 25 | margin: .6em 0; 26 | padding-left: $expand-stub-width; 27 | position: relative; 28 | } 29 | 30 | li > a { 31 | display: inline; 32 | 33 | @include underline-on-hover; 34 | } 35 | 36 | li > ul { 37 | display: none; 38 | } 39 | 40 | li.expanded > ul { 41 | display: block; 42 | } 43 | 44 | .expand-stub::before { 45 | display: inline-block; 46 | width: $expand-stub-width; 47 | cursor: pointer; 48 | font-family: bootstrap-icons; 49 | font-size: .8em; 50 | content: "\F285"; 51 | position: absolute; 52 | margin-top: .2em; 53 | margin-left: -$expand-stub-width; 54 | transition: transform 0.35s ease; 55 | transform-origin: .5em 50%; 56 | 57 | @media (prefers-reduced-motion) { 58 | & { 59 | transition: none; 60 | } 61 | } 62 | } 63 | 64 | li.expanded > .expand-stub::before { 65 | transform: rotate(90deg); 66 | } 67 | 68 | span.name-only { 69 | font-weight: 600; 70 | display: inline-block; 71 | margin: .4rem 0; 72 | } 73 | 74 | form.filter { 75 | display: flex; 76 | position: relative; 77 | align-items: center; 78 | margin-bottom: .5rem; 79 | 80 | >i.bi { 81 | position: absolute; 82 | left: .6rem; 83 | opacity: .5; 84 | } 85 | 86 | >input { 87 | padding-left: 2rem; 88 | } 89 | } 90 | 91 | >.no-result { 92 | font-size: .9em; 93 | color: $secondary; 94 | } 95 | 96 | a.pdf-link { 97 | @include underline-on-hover; 98 | 99 | &::before { 100 | content: "\F756"; 101 | display: inline-block; 102 | 103 | @include adjust-icon; 104 | } 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /Docs/templates/modern/src/theme.ts: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | 4 | import { html } from 'lit-html' 5 | import { Theme } from './options' 6 | import { loc, options } from './helper' 7 | 8 | function setTheme(theme: Theme) { 9 | localStorage.setItem('theme', theme) 10 | if (theme === 'auto') { 11 | document.documentElement.setAttribute('data-bs-theme', window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light') 12 | } else { 13 | document.documentElement.setAttribute('data-bs-theme', theme) 14 | } 15 | } 16 | 17 | async function getDefaultTheme() { 18 | return localStorage.getItem('theme') as Theme || (await options()).defaultTheme || 'auto' 19 | } 20 | 21 | export async function initTheme() { 22 | setTheme(await getDefaultTheme()) 23 | } 24 | 25 | export function onThemeChange(callback: (theme: 'light' | 'dark') => void) { 26 | return new MutationObserver(() => callback(getTheme())) 27 | .observe(document.documentElement, { attributes: true, attributeFilter: ['data-bs-theme'] }) 28 | } 29 | 30 | export function getTheme(): 'light' | 'dark' { 31 | return document.documentElement.getAttribute('data-bs-theme') as 'light' | 'dark' 32 | } 33 | 34 | export async function themePicker(refresh: () => void) { 35 | const theme = await getDefaultTheme() 36 | const icon = theme === 'light' ? 'sun' : theme === 'dark' ? 'moon' : 'circle-half' 37 | 38 | return html` 39 | ` 49 | 50 | function changeTheme(e, theme: Theme) { 51 | e.preventDefault() 52 | setTheme(theme) 53 | refresh() 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /Docs/articles/events.md: -------------------------------------------------------------------------------- 1 | # Events 2 | Events are track objects with custom data that drive various elements of gameplay. This guide will cover how to use events as well as the helpers provided by ChartTools. 3 | 4 | ## Class structure 5 | Events are stored using [Event](~/api/ChartTools.Events.Event) as a base class, containing the position, event type and an optional argument. The type and argument can also be set simultaneously through the [EventData](~/api/ChartTools.Events.Event#ChartTools_Events_Event_EventData) property. 6 | 7 | ChartTools distinguishes between global events (stored under [Song](~/api/ChartTools.Song)) and local events (stored under [Track](~/api/ChartTools.Track)) using the respective [GlobalEvent](~/api/ChartTools.Events.GlobalEvent) and [LocalEvent](~/api/ChartTools.Events.LocalEvent) classes, both deriving from [Event](~/api/ChartTools.Events.Event). This allows for better type safety and for the classes to provide helper properties for some complex event types. 8 | 9 | ## Event helpers 10 | Event types and arguments are stored as `string`, allowing for future-proofing and supporting custom events from sources that are not officially supported. 11 | 12 | For event types which are supported, CharTools provides helpers in the form of string constants under the static [EventTypeHelper.Global](~/api/ChartTools.Events.EventTypeHeaderHelper.Global) and [EventTypeHelper.Local](~/api/ChartTools.Events.EventTypeHeaderHelper.Local) classes. In a future version, usage details of helpers will be accessible from the documentation included with the assembly. 13 | 14 | ```csharp 15 | GlobalEvent globalEvent = new(0, EventTypeHelper.Global.MusicStart, null); 16 | ``` 17 | 18 | Some event types are part of a category defined by a prefix to the type. Helpers are provided for these groups under the static [EventTypeHeaderHelper](~/api/ChartTools.Events.EventTypeHeaderHelper) class. Helper properties are also defined for groups from supported sources. 19 | 20 | ```csharp 21 | bool isCrowd = globalEvent.EventType.StartsWith(EventTypeHeaderHelper.Global.Crowd); 22 | bool isCrowd2 = globalEvent.IsCrowdEvent; 23 | ``` 24 | 25 | Some event types can be modified using predefined arguments. For such values, helpers are provided under the static [EventArgumentHelper](~/api/ChartTools.Events.EventArgumentHelper) class. 26 | 27 | ```csharp 28 | GlobalEvent globalEvent = new(0, EventTypeHelper.Global.Lighting, EventArgumentHelper.Global.Lighting.Strobe); 29 | ``` 30 | -------------------------------------------------------------------------------- /ChartTools/Metadata/StreamCollection.cs: -------------------------------------------------------------------------------- 1 | using ChartTools.IO.Chart; 2 | using ChartTools.IO.Chart.Serializing; 3 | 4 | namespace ChartTools; 5 | 6 | /// 7 | /// Set of audio files to play and mute during gameplay 8 | /// 9 | /// Instrument audio may be muted when chords of the respective instrument are missed 10 | public class StreamCollection 11 | { 12 | /// 13 | /// Location of the base audio file 14 | /// 15 | [ChartKeySerializable(ChartFormatting.MusicStream)] 16 | public string? Music { get; set; } 17 | 18 | /// 19 | /// Location of the guitar audio file 20 | /// 21 | [ChartKeySerializable(ChartFormatting.GuitarStream)] 22 | public string? Guitar { get; set; } 23 | 24 | /// 25 | /// Location of the bass audio 26 | /// 27 | [ChartKeySerializable(ChartFormatting.BassStream)] 28 | public string? Bass { get; set; } 29 | 30 | /// 31 | /// Location of the rhythm guitar audio file 32 | /// 33 | [ChartKeySerializable(ChartFormatting.RhythmStream)] 34 | public string? Rhythm { get; set; } 35 | 36 | /// 37 | /// Location of the keys audio file 38 | /// 39 | [ChartKeySerializable(ChartFormatting.KeysStream)] 40 | public string? Keys { get; set; } 41 | 42 | /// 43 | /// Location of the drums' kicks audio file 44 | /// 45 | /// Can include all drums audio 46 | [ChartKeySerializable(ChartFormatting.DrumStream)] 47 | public string? Drum { get; set; } 48 | 49 | /// 50 | /// Location of the drums' snares audio file 51 | /// 52 | /// Can include all drums audio except kicks 53 | [ChartKeySerializable(ChartFormatting.Drum2Stream)] 54 | public string? Drum2 { get; set; } 55 | 56 | /// 57 | /// Location of the drum's toms audio file 58 | /// 59 | /// Can include toms and cymbals 60 | [ChartKeySerializable(ChartFormatting.Drum3Stream)] 61 | public string? Drum3 { get; set; } 62 | 63 | /// 64 | /// Location of the drum's cymbals audio file 65 | /// 66 | [ChartKeySerializable(ChartFormatting.Drum4Stream)] 67 | public string? Drum4 { get; set; } 68 | /// 69 | /// Location of the vocals audio file 70 | /// 71 | [ChartKeySerializable(ChartFormatting.VocalStream)] 72 | public string? Vocals { get; set; } 73 | /// 74 | /// Location of the crowd reaction audio file 75 | /// 76 | [ChartKeySerializable(ChartFormatting.CrowdStream)] 77 | public string? Crowd { get; set; } 78 | } 79 | -------------------------------------------------------------------------------- /Docs/templates/modern/src/dotnet.scss: -------------------------------------------------------------------------------- 1 | /** 2 | * Licensed to the .NET Foundation under one or more agreements. 3 | * The .NET Foundation licenses this file to you under the MIT license. 4 | */ 5 | 6 | body[data-yaml-mime="ManagedReference"] article, body[data-yaml-mime="ApiPage"] article { 7 | h1[data-uid] { 8 | position: relative; 9 | padding-right: 1.6rem; 10 | } 11 | 12 | h3[data-uid] { 13 | position: relative; 14 | font-weight: 400; 15 | margin-top: 3rem; 16 | padding-bottom: 5px; 17 | padding-right: 1.6rem; 18 | } 19 | 20 | h2.section { 21 | margin-top: 3rem; 22 | 23 | +h3[data-uid], +a+h3[data-uid] { 24 | margin-top: 1rem; 25 | } 26 | } 27 | 28 | h4.section { 29 | font-weight: 300; 30 | margin-top: 1.6rem; 31 | } 32 | 33 | dl>dt { 34 | font-weight: normal; 35 | } 36 | 37 | dl>dd { 38 | margin-left: 1rem; 39 | } 40 | 41 | dl.typelist { 42 | >dt { 43 | font-weight: 600; 44 | } 45 | 46 | >dd { 47 | margin-left: 0; 48 | } 49 | 50 | >dd>div { 51 | display: inline-block; 52 | 53 | &:not(:last-child)::after { 54 | content: ', '; 55 | } 56 | } 57 | 58 | &.inheritance>dd>div:not(:last-child)::after { 59 | font-family: bootstrap-icons; 60 | content: '\F12C'; 61 | position: relative; 62 | top: .2em; 63 | opacity: .8; 64 | } 65 | } 66 | 67 | dl.parameters { 68 | >dt { 69 | margin: 1em 0; 70 | 71 | &>code { 72 | margin-right: .2em; 73 | font-size: 1em; 74 | } 75 | } 76 | } 77 | 78 | div.facts { 79 | font-size: 14px; 80 | margin: 2rem 0 1rem; 81 | 82 | >dl { 83 | margin: 0; 84 | 85 | >dd { 86 | margin-left: .25rem; 87 | display: inline-block; 88 | } 89 | 90 | >dt { 91 | display: inline-block; 92 | } 93 | 94 | >dt::after { 95 | content: ":"; 96 | } 97 | } 98 | } 99 | 100 | .header-action { 101 | position: absolute; 102 | right: 0; 103 | bottom: .2rem; 104 | font-size: 1.2rem; 105 | } 106 | 107 | @media print { 108 | .header-action { 109 | display: none; 110 | } 111 | } 112 | 113 | td.term { 114 | font-weight: 600; 115 | } 116 | 117 | summary { 118 | display: block; 119 | cursor: inherit; 120 | } 121 | 122 | li>span.term { 123 | font-weight: 600; 124 | 125 | &::after { 126 | content: '-'; 127 | margin: 0 .5em; 128 | } 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /Docs/index.md: -------------------------------------------------------------------------------- 1 | # ChartTools 2 | ChartTools is a .NET library with the purpose of modeling song files for plastic guitar video games like Guitar Hero, Rock Band and Clone Hero. It currently supports reading of .chart and .ini files. 3 | 4 | If you find any bugs, you can report them in the [Issues section](https://github.com/TheBoxyBear/ChartTools/issues) of the repository. Make sure to use the "bug" label. 5 | 6 | As this project is in development, it should only be used with charts with a backup available. **I am not responsible for damages to charts!** 7 | 8 | ## Getting Started 9 | For an overview on installation and taking your first steps with ChartTools, see [Articles](~/articles/getting-started). A GitHub Pages website is available with detailed articles and API documentation. 10 | 11 | ## Contributing 12 | If you like to contribute to the development of ChartTools, feel free to comment on an issue, submit a pull request or submit your own issues. 13 | 14 | To test your code, copy the solution file and rename it to `ChartTools_Debug.sln`. From that solution, you can add new projects under the `Debug` directory. The debug solution and all its additional projects will automatically be git-ignored. 15 | 16 | ### Documentation 17 | The solution includes a `Docs` project that can be executed to build and deploy locally on port 8080. Remember to terminate the local server with `Ctrl+C` before closing as it can prevent later executions from using the port. If this occurs, run 18 | 19 | ```bash 20 | netstat -aof | findstr :8080 21 | taskkill /f /pid 22 | ``` 23 | 24 | where `PID` is the right-most ID in the output of `netstat`. 25 | 26 | ## License and Attribution 27 | This project is licensed under the GNU General Public License 3.0. See [LICENSE](https://github.com/TheBoxyBear/charttools/blob/stable/LICENSE) for details. 28 | 29 | This project makes use of one or more third-party libraries to aid in functionality, see [attribution.txt](https://github.com/TheBoxyBear/charttools/blob/stable/attribution.txt) for details. 30 | 31 | ## Special Thanks 32 | - [FireFox](https://github.com/FireFox2000000) for making the Moonscraper editor open-source 33 | - [TheNathannator](https://github.com/TheNathannator) for their direct contributions. 34 | - [Matthew Sitton](https://github.com/mdsitton), lead developer of Clone Hero for sharing their in-depth knowledge and general programming wisdom. 35 | - Members of the [Clone Hero Discord](https://discord.gg/clone-hero) and [Moonscraper Discord](https://discord.gg/wdnD83APhE), including but not limited to DarkAngel2096, drumbs (TheNathannator), FireFox, Kanske, mdsitton, Spachi, and XEntombmentX for their help in researching. 36 | -------------------------------------------------------------------------------- /ChartTools/IO/TextFileReader.cs: -------------------------------------------------------------------------------- 1 | using ChartTools.Extensions.Collections; 2 | using ChartTools.IO.Parsing; 3 | using ChartTools.IO.Sources; 4 | 5 | namespace ChartTools.IO; 6 | 7 | internal abstract class TextFileReader(ReadingDataSource source) : FileReader(source) 8 | { 9 | public virtual bool DefinedSectionEnd { get; } = false; 10 | 11 | protected bool _disposeReader = false; 12 | 13 | protected override void ReadBase(bool async, CancellationToken cancellationToken) 14 | { 15 | using StreamReader reader = new(Source.Stream, leaveOpen: true); 16 | 17 | ParserContentGroup? currentGroup = null; 18 | string line = string.Empty; 19 | 20 | while (ReadLine()) 21 | { 22 | // Find section 23 | while (!line.StartsWith('[')) 24 | if (!ReadLine()) 25 | return; 26 | 27 | if (async && cancellationToken.IsCancellationRequested) 28 | { 29 | Dispose(); 30 | return; 31 | } 32 | 33 | string header = line; 34 | TextParser? parser = GetParser(header); 35 | 36 | if (parser is not null) 37 | { 38 | DelayedEnumerableSource source = new(); 39 | 40 | parserGroups.Add(currentGroup = new(parser, source)); 41 | 42 | if (async) 43 | { 44 | if (cancellationToken.IsCancellationRequested) 45 | { 46 | Dispose(); 47 | return; 48 | } 49 | 50 | parseTasks.Add(parser.StartAsyncParse(source.Enumerable)); 51 | } 52 | } 53 | 54 | // Move to the start of the entries 55 | do 56 | if (!AdvanceSection()) 57 | { 58 | FinishSection(); 59 | return; 60 | } 61 | while (!IsSectionStart(line)); 62 | 63 | AdvanceSection(); 64 | 65 | // Read until end 66 | while (!IsSectionEnd(line)) 67 | { 68 | currentGroup?.Source.Add(line); 69 | 70 | if (!AdvanceSection()) 71 | { 72 | FinishSection(); 73 | return; 74 | } 75 | } 76 | 77 | FinishSection(); 78 | 79 | void FinishSection() 80 | { 81 | if (cancellationToken.IsCancellationRequested) 82 | { 83 | Dispose(); 84 | return; 85 | } 86 | 87 | currentGroup?.Source.EndAwait(); 88 | } 89 | 90 | bool AdvanceSection() => ReadLine() || (DefinedSectionEnd ? throw SectionException.EarlyEnd(header) : false); 91 | } 92 | 93 | bool ReadLine() 94 | { 95 | string? newLine; 96 | 97 | while ((newLine = reader.ReadLine()) == string.Empty) ; 98 | 99 | if (newLine is null) 100 | return false; 101 | 102 | line = newLine.Trim(); 103 | return true; 104 | } 105 | } 106 | 107 | protected abstract bool IsSectionStart(string line); 108 | 109 | protected virtual bool IsSectionEnd(string line) => false; 110 | } 111 | -------------------------------------------------------------------------------- /Docs/templates/modern/src/helper.ts: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | 4 | import { html, TemplateResult } from 'lit-html' 5 | import { DocfxOptions } from './options' 6 | 7 | export async function options(): Promise { 8 | return await import('./main.js').then(m => m.default) as DocfxOptions 9 | } 10 | 11 | /** 12 | * Get the value of an HTML meta tag. 13 | */ 14 | export function meta(name: string): string { 15 | return (document.querySelector(`meta[name="${name}"]`) as HTMLMetaElement)?.content 16 | } 17 | 18 | /** 19 | * Gets the localized text. 20 | * @param id key in token.json 21 | * @param args arguments to replace in the localized text 22 | */ 23 | export function loc(id: string, args?: { [key: string]: string }): string { 24 | let result = meta(`loc:${id}`) || id 25 | if (args) { 26 | for (const key in args) { 27 | result = result.replace(`{${key}}`, args[key]) 28 | } 29 | } 30 | return result 31 | } 32 | 33 | /** 34 | * Add into long word. 35 | */ 36 | export function breakWord(text?: string): string[] { 37 | if (!text) { 38 | return [] 39 | } 40 | const regex = /([a-z0-9])([A-Z]+[a-z])|([a-zA-Z0-9][.,/<>_])/g 41 | const result = [] 42 | let start = 0 43 | while (true) { 44 | const match = regex.exec(text) 45 | if (!match) { 46 | break 47 | } 48 | const index = match.index + (match[1] || match[3]).length 49 | result.push(text.slice(start, index)) 50 | start = index 51 | } 52 | if (start < text.length) { 53 | result.push(text.slice(start)) 54 | } 55 | return result 56 | } 57 | 58 | /** 59 | * Add into long word. 60 | */ 61 | export function breakWordLit(text?: string): TemplateResult { 62 | const result = [] 63 | breakWord(text).forEach(word => { 64 | if (result.length > 0) { 65 | result.push(html``) 66 | } 67 | result.push(html`${word}`) 68 | }) 69 | return html`${result}` 70 | } 71 | 72 | /** 73 | * Check if the url is external. 74 | * @param url The url to check. 75 | * @returns True if the url is external. 76 | */ 77 | export function isExternalHref(url: URL): boolean { 78 | return url.hostname !== window.location.hostname || url.protocol !== window.location.protocol 79 | } 80 | 81 | /** 82 | * Determines if two URLs should be considered the same. 83 | */ 84 | export function isSameURL(a: { pathname: string }, b: { pathname: string }): boolean { 85 | return normalizeUrlPath(a) === normalizeUrlPath(b) 86 | 87 | function normalizeUrlPath(url: { pathname: string }): string { 88 | return url.pathname 89 | .replace(/\/index\.html$/gi, '/') 90 | .replace(/\.html$/gi, '') 91 | .replace(/\/$/gi, '') 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /ChartTools/IO/Chart/ChartFileReader.cs: -------------------------------------------------------------------------------- 1 | using ChartTools.IO.Chart.Configuration.Sessions; 2 | using ChartTools.IO.Chart.Parsing; 3 | using ChartTools.IO.Components; 4 | using ChartTools.IO.Configuration; 5 | using ChartTools.IO.Sources; 6 | 7 | namespace ChartTools.IO.Chart; 8 | 9 | /// 10 | /// Reader of text file that sends read lines to subscribers of its events. 11 | /// 12 | internal class ChartFileReader(ReadingDataSource source, ChartReadingSession session) : TextFileReader(source) 13 | { 14 | public ChartReadingSession Session { get; } = session; 15 | 16 | public override IEnumerable Parsers => base.Parsers.Cast(); 17 | 18 | public override bool DefinedSectionEnd => true; 19 | 20 | public Metadata? ExistingMetadata { get; set; } 21 | 22 | protected override ChartParser? GetParser(string header) 23 | { 24 | switch (header) 25 | { 26 | case ChartFormatting.MetadataHeader: 27 | return Session.Components.Metadata ? new MetadataParser(ExistingMetadata) : null; 28 | case ChartFormatting.GlobalEventHeader: 29 | // Vocals are read from global events in chart files. Gets converted to vocals when assembling the song object 30 | return Session.Components.GlobalEvents || Session.Components.Vocals ? new GlobalEventParser(Session) : null; 31 | case ChartFormatting.SyncTrackHeader: 32 | return Session.Components.SyncTrack ? new SyncTrackParser(Session) : null; 33 | default: 34 | if (ChartFormatting.DrumsTrackHeaders.TryGetValue(header, out Difficulty diff)) 35 | return Session.Components.Instruments.Drums.HasFlag(diff.ToSet()) 36 | ? new DrumsTrackParser(diff, Session, header) : null; 37 | else if (ChartFormatting.GHLTrackHeaders.TryGetValue(header, out (Difficulty, GHLInstrumentIdentity) ghlTuple)) 38 | return Session.Components.Instruments.Map(ghlTuple.Item2).HasFlag(ghlTuple.Item1.ToSet()) 39 | ? new GHLTrackParser(ghlTuple.Item1, ghlTuple.Item2, Session, header) : null; 40 | else if (ChartFormatting.StandardTrackHeaders.TryGetValue(header, out (Difficulty, StandardInstrumentIdentity) standardTuple)) 41 | return Session.Components.Instruments.Map(standardTuple.Item2).HasFlag(standardTuple.Item1.ToSet()) 42 | ? new StandardTrackParser(standardTuple.Item1, standardTuple.Item2, Session, header) : null; 43 | else 44 | { 45 | return Session.Configuration.UnknownSectionPolicy == UnknownSectionPolicy.ThrowException 46 | ? throw new Exception($"Unknown section with header \"{header}\". Consider using {UnknownSectionPolicy.Store} to avoid this error.") 47 | : new UnknownSectionParser(Session, header); 48 | } 49 | } 50 | } 51 | 52 | protected override bool IsSectionStart(string line) => line == "{"; 53 | protected override bool IsSectionEnd(string line) => ChartFormatting.IsSectionEnd(line); 54 | } 55 | -------------------------------------------------------------------------------- /ChartTools/IO/Chart/Serializing/TrackSerializer.cs: -------------------------------------------------------------------------------- 1 | using ChartTools.Events; 2 | using ChartTools.Extensions.Linq; 3 | using ChartTools.IO.Chart.Configuration.Sessions; 4 | using ChartTools.IO.Chart.Entries; 5 | using ChartTools.IO.Chart.Providers; 6 | using ChartTools.IO.Configuration; 7 | using ChartTools.Tools; 8 | 9 | namespace ChartTools.IO.Chart.Serializing; 10 | 11 | internal class TrackSerializer(Track content, ChartWritingSession session) 12 | : TrackObjectGroupSerializer(ChartFormatting.Header(content.ParentInstrument!.InstrumentIdentity, content.Difficulty), content, session) 13 | { 14 | public override IEnumerable Serialize() 15 | => LaunchProviders().AlternateBy(entry => entry.Position).Select(entry => entry.ToString()); 16 | 17 | protected override IEnumerable[] LaunchProviders() 18 | { 19 | ApplyOverlappingSpecialPhrasePolicy(Content.SpecialPhrases, Session.Configuration.OverlappingStarPowerPolicy); 20 | 21 | // Convert solo and soloend events into star power 22 | if (Session.Configuration.SoloNoStarPowerPolicy == SoloNoStarPowerPolicy.Convert && Content.SpecialPhrases.Count == 0 && Content.LocalEvents is not null) 23 | { 24 | TrackSpecialPhrase? starPower = null; 25 | 26 | foreach (LocalEvent e in Content.LocalEvents) 27 | switch (e.EventType) 28 | { 29 | case EventTypeHelper.Local.Solo: 30 | if (starPower is not null) 31 | { 32 | starPower.Length = e.Position - starPower.Position; 33 | Content.SpecialPhrases.Add(starPower); 34 | } 35 | 36 | starPower = new(e.Position, TrackSpecialPhraseType.StarPowerGain); 37 | break; 38 | case EventTypeHelper.Local.SoloEnd when starPower is not null: 39 | 40 | starPower.Length = e.Position - starPower.Position; 41 | Content.SpecialPhrases.Add(starPower); 42 | 43 | starPower = null; 44 | break; 45 | } 46 | 47 | Content.LocalEvents.RemoveWhere(e => e.IsSoloEvent); 48 | } 49 | 50 | return 51 | [ 52 | new ChordProvider().ProvideFor(Content.Chords.Cast(), Session), 53 | new SpeicalPhraseProvider().ProvideFor(Content.SpecialPhrases, Session), 54 | Content.LocalEvents is null ? [] : new EventProvider().ProvideFor(Content.LocalEvents, Session) 55 | ]; 56 | } 57 | 58 | private static void ApplyOverlappingSpecialPhrasePolicy( 59 | IEnumerable specialPhrases, OverlappingSpecialPhrasePolicy policy) 60 | { 61 | switch (policy) 62 | { 63 | case OverlappingSpecialPhrasePolicy.Cut: 64 | specialPhrases.CutLengths(); 65 | break; 66 | case OverlappingSpecialPhrasePolicy.ThrowException: 67 | foreach ((TrackSpecialPhrase previous, TrackSpecialPhrase current) in specialPhrases.RelativeLoopSkipFirst()) 68 | if (Optimizer.LengthNeedsCut(previous, current)) 69 | throw new Exception($"Overlapping star power phrases at position {current.Position}."); 70 | break; 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /ChartTools/Chords/GHLChord.cs: -------------------------------------------------------------------------------- 1 | using ChartTools.IO.Chart; 2 | using ChartTools.IO.Chart.Configuration.Sessions; 3 | using ChartTools.IO.Chart.Entries; 4 | 5 | namespace ChartTools; 6 | 7 | /// 8 | /// Set of notes played simultaneously by a Guitar Hero Live instrument 9 | /// 10 | public sealed class GHLChord : Chord, GHLLane, GHLChordModifiers> 11 | { 12 | /// "/> 13 | /// Always for 14 | public override bool OpenExclusivity => true; 15 | 16 | internal override GHLChordModifiers DefaultModifiers => GHLChordModifiers.None; 17 | 18 | internal override bool ChartSupportedModifiers => !Modifiers.HasFlag(GHLChordModifiers.ExplicitHopo); 19 | 20 | /// 21 | /// Creates an instance of at position 0. 22 | /// 23 | public GHLChord() : base(0) { } 24 | 25 | /// 26 | /// Creates an instance of at the specified position. 27 | /// 28 | /// Position of the chord 29 | public GHLChord(uint position) : base(position) { } 30 | 31 | /// 32 | /// Creates an instance of with a specified position and notes. 33 | /// 34 | /// Position of the chord 35 | /// Set of notes to add 36 | public GHLChord(uint position, params ReadOnlySpan> notes) : base(position) 37 | => Notes.AddRange(notes); 38 | 39 | /// 40 | /// Creates an instance of with a specified position and notes. 41 | /// 42 | /// Position of the chord 43 | /// Set of notes to add by lane 44 | public GHLChord(uint position, params ReadOnlySpan notes) : base(position) 45 | => Notes.AddRange(notes); 46 | 47 | internal override IEnumerable GetChartNoteData() 48 | => Notes.Select(note => ChartFormatting.NoteEntry(Position, note.Lane switch 49 | { 50 | GHLLane.Open => 7, 51 | GHLLane.Black1 => 3, 52 | GHLLane.Black2 => 4, 53 | GHLLane.Black3 => 8, 54 | GHLLane.White1 => 0, 55 | GHLLane.White2 => 1, 56 | GHLLane.White3 => 2, 57 | }, note.Sustain)); 58 | 59 | internal override IEnumerable GetChartModifierData(Chord? previous, ChartWritingSession session) 60 | { 61 | bool isInvert = Modifiers.HasFlag(GHLChordModifiers.HopoInvert); 62 | 63 | if (Modifiers.HasFlag(GHLChordModifiers.ExplicitHopo) && (previous is null || previous.Position <= session.Formatting.ChartHopoFrequency) != isInvert || isInvert) 64 | yield return ChartFormatting.NoteEntry(Position, 5, 0); 65 | if (Modifiers.HasFlag(GHLChordModifiers.Tap)) 66 | yield return ChartFormatting.NoteEntry(Position, 6, 0); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /ChartTools/IO/Chart/Parsing/MetadataParser.cs: -------------------------------------------------------------------------------- 1 | namespace ChartTools.IO.Chart.Parsing; 2 | 3 | internal class MetadataParser(Metadata? existing = null) : ChartParser(null! /* Session not used */, ChartFormatting.MetadataHeader) 4 | { 5 | public override Metadata Result => GetResult(result); 6 | private readonly Metadata result = existing ?? new(); 7 | 8 | protected override void HandleItem(string line) 9 | { 10 | TextEntry entry = new(line); 11 | string? value = entry.Value?.Trim('"'); 12 | 13 | switch (entry.Key) 14 | { 15 | case ChartFormatting.Title: 16 | result.Title = value; 17 | break; 18 | case ChartFormatting.Artist: 19 | result.Artist = value; 20 | break; 21 | case ChartFormatting.Charter: 22 | result.Charter.Name = value; 23 | break; 24 | case ChartFormatting.Album: 25 | result.Album = value; 26 | break; 27 | case ChartFormatting.Year: 28 | result.Year = ValueParser.ParseUshort(value?.TrimStart(','), "year"); 29 | break; 30 | case ChartFormatting.AudioOffset: 31 | result.AudioOffset = TimeSpan.FromMilliseconds(ValueParser.ParseFloat(value, "audio offset") * 1000); 32 | break; 33 | case ChartFormatting.Difficulty: 34 | result.Difficulty = ValueParser.ParseSbyte(value, "difficulty"); 35 | break; 36 | case ChartFormatting.PreviewStart: 37 | result.PreviewStart = ValueParser.ParseUint(value, "preview start"); 38 | break; 39 | case ChartFormatting.PreviewEnd: 40 | result.PreviewEnd = ValueParser.ParseUint(value, "preview end"); 41 | break; 42 | case ChartFormatting.Genre: 43 | result.Genre = value; 44 | break; 45 | case ChartFormatting.MediaType: 46 | result.MediaType = value; 47 | break; 48 | case ChartFormatting.MusicStream: 49 | result.Streams.Music = value; 50 | break; 51 | case ChartFormatting.GuitarStream: 52 | result.Streams.Guitar = value; 53 | break; 54 | case ChartFormatting.BassStream: 55 | result.Streams.Bass = value; 56 | break; 57 | case ChartFormatting.RhythmStream: 58 | result.Streams.Rhythm = value; 59 | break; 60 | case ChartFormatting.KeysStream: 61 | result.Streams.Keys = value; 62 | break; 63 | case ChartFormatting.DrumStream: 64 | result.Streams.Drum = value; 65 | break; 66 | case ChartFormatting.Drum2Stream: 67 | result.Streams.Drum2 = value; 68 | break; 69 | case ChartFormatting.Drum3Stream: 70 | result.Streams.Drum3 = value; 71 | break; 72 | case ChartFormatting.Drum4Stream: 73 | result.Streams.Drum4 = value; 74 | break; 75 | case ChartFormatting.VocalStream: 76 | result.Streams.Vocals = value; 77 | break; 78 | case ChartFormatting.CrowdStream: 79 | result.Streams.Crowd = value; 80 | break; 81 | default: 82 | result.UnidentifiedData.Add(new() { Key = entry.Key, Value = entry.Value, Origin = FileType.Chart }); 83 | break; 84 | } 85 | } 86 | 87 | public override void ApplyToSong(Song song) => song.Metadata = Result; 88 | } 89 | -------------------------------------------------------------------------------- /Docs/templates/modern/src/nav.scss: -------------------------------------------------------------------------------- 1 | /** 2 | * Licensed to the .NET Foundation under one or more agreements. 3 | * The .NET Foundation licenses this file to you under the MIT license. 4 | */ 5 | 6 | .breadcrumb { 7 | font-size: 14px; 8 | 9 | a { 10 | @include underline-on-hover; 11 | } 12 | } 13 | 14 | .next-article { 15 | display: flex; 16 | 17 | &:not(:has(div)) { 18 | border-top-width: 0; 19 | } 20 | 21 | &:has(div) { 22 | margin-top: 3rem; 23 | padding-top: 1rem; 24 | } 25 | 26 | &>div { 27 | flex: 1; 28 | 29 | &.next { 30 | text-align: right; 31 | } 32 | 33 | &>span { 34 | opacity: .66; 35 | font-size: 14px; 36 | } 37 | 38 | &>a { 39 | display: block; 40 | } 41 | } 42 | } 43 | 44 | .navbar { 45 | padding: 0; 46 | 47 | .navbar-brand { 48 | display: flex; 49 | align-items: center; 50 | } 51 | 52 | .navbar-nav { 53 | display: flex; 54 | flex-wrap: nowrap; 55 | } 56 | 57 | #navbar { 58 | display: flex; 59 | flex: 1; 60 | justify-content: flex-end; 61 | 62 | form { 63 | display: flex; 64 | position: relative; 65 | align-items: center; 66 | 67 | >i.bi { 68 | position: absolute; 69 | left: .8rem; 70 | opacity: .5; 71 | } 72 | 73 | >input { 74 | padding-left: 2.5rem; 75 | } 76 | 77 | &.search { 78 | order: 50; 79 | } 80 | 81 | &.icons { 82 | margin-left: auto; 83 | } 84 | } 85 | } 86 | 87 | @include media-breakpoint-down(md) { 88 | #navbar { 89 | flex-direction: column; 90 | align-items: flex-start; 91 | 92 | form { 93 | margin: 1rem 0 0; 94 | 95 | &.search { 96 | align-self: stretch; 97 | order: 30; 98 | } 99 | 100 | &.icons { 101 | align-self: center; 102 | order: 40; 103 | margin: 1rem 0; 104 | } 105 | } 106 | } 107 | } 108 | } 109 | 110 | .affix { 111 | font-size: 14px; 112 | 113 | h5 { 114 | display: inline-block; 115 | font-weight: 300; 116 | text-transform: uppercase; 117 | padding: 1em 0 .5em; 118 | font-size: 14px; 119 | letter-spacing: 2px; 120 | } 121 | 122 | h6 { 123 | font-size: 14px; 124 | } 125 | 126 | ul { 127 | flex-direction: column; 128 | list-style-type: none; 129 | padding-left: 0; 130 | margin-left: 0; 131 | 132 | h6 { 133 | margin-top: 1rem; 134 | } 135 | 136 | li { 137 | margin: .4rem 0; 138 | 139 | a { 140 | @include underline-on-hover; 141 | } 142 | } 143 | } 144 | } 145 | 146 | .contribution { 147 | margin-top: 2rem; 148 | 149 | a.edit-link { 150 | @include underline-on-hover; 151 | 152 | &::before { 153 | content: "\F4CA"; 154 | display: inline-block; 155 | 156 | @include adjust-icon; 157 | } 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /ChartTools/Tracks/Track.cs: -------------------------------------------------------------------------------- 1 | using ChartTools.Events; 2 | 3 | using System.Diagnostics; 4 | 5 | namespace ChartTools; 6 | 7 | /// 8 | /// Base class for tracks 9 | /// 10 | [DebuggerDisplay("{Difficulty}")] 11 | public abstract record Track : IEmptyVerifiable 12 | { 13 | /// 14 | public bool IsEmpty => Chords.Count == 0 && LocalEvents.Count == 0 && SpecialPhrases.Count == 0; 15 | 16 | /// 17 | /// Difficulty of the track 18 | /// 19 | public Difficulty Difficulty { get; init; } 20 | 21 | /// 22 | /// Instrument containing the track 23 | /// 24 | public Instrument? ParentInstrument => GetInstrument(); 25 | 26 | /// 27 | /// Events specific to the 28 | /// 29 | public List LocalEvents { get; } = []; 30 | 31 | /// 32 | /// Set of special phrases 33 | /// 34 | public List SpecialPhrases { get; } = []; 35 | 36 | /// 37 | /// Groups of notes of the same position 38 | /// 39 | public abstract IReadOnlyList Chords { get; } 40 | 41 | protected abstract IReadOnlyList GetChords(); 42 | 43 | internal IEnumerable SoloToStarPower(bool removeEvents) 44 | { 45 | if (LocalEvents is null) 46 | yield break; 47 | 48 | foreach (LocalEvent e in LocalEvents.OrderBy(e => e.Position)) 49 | { 50 | TrackSpecialPhrase? phrase = null; 51 | 52 | switch (e.EventType) 53 | { 54 | case EventTypeHelper.Local.Solo: 55 | phrase = new(e.Position, TrackSpecialPhraseType.StarPowerGain); 56 | break; 57 | case EventTypeHelper.Local.SoloEnd: 58 | if (phrase is not null) 59 | { 60 | phrase.Length = e.Position - phrase.Position; 61 | yield return phrase; 62 | phrase = null; 63 | } 64 | break; 65 | } 66 | } 67 | 68 | if (removeEvents) 69 | LocalEvents.RemoveAll(e => e.IsSoloEvent); 70 | } 71 | 72 | protected abstract Instrument? GetInstrument(); 73 | } 74 | 75 | /// 76 | /// Set of chords for a instrument at a certain difficulty 77 | /// 78 | public record Track : Track 79 | where TChord : Chord 80 | { 81 | /// 82 | /// Chords making up the difficulty track. 83 | /// 84 | public override List Chords { get; } = []; 85 | 86 | /// 87 | /// Instrument the track is held in. 88 | /// 89 | public new Instrument? ParentInstrument { get; init; } 90 | 91 | /// 92 | /// Gets the chords as a read-only list of the base interface. 93 | /// 94 | /// 95 | protected override IReadOnlyList GetChords() => Chords; 96 | 97 | /// 98 | /// Gets the parent instrument as an instance of the base type. 99 | /// 100 | protected override Instrument? GetInstrument() => ParentInstrument; 101 | } 102 | -------------------------------------------------------------------------------- /ChartTools/IO/Ini/IniFile.cs: -------------------------------------------------------------------------------- 1 | using ChartTools.Extensions.Linq; 2 | using ChartTools.IO.Sources; 3 | 4 | namespace ChartTools.IO.Ini; 5 | 6 | /// 7 | /// Provides methods for reading and writing ini files 8 | /// 9 | public static class IniFile 10 | { 11 | /// 12 | /// Reads the from an ini target. 13 | /// 14 | /// File path or stream to read from 15 | /// from another target to combine with 16 | /// object provided as the parameter, or a new instance if passed . 17 | public static Metadata ReadMetadata(ReadingDataSource source, Metadata? existing = null) 18 | { 19 | using IniFileReader reader = new(source, existing); 20 | reader.Read(); 21 | 22 | return reader.Parsers.TryGetFirst(out IniParser? parser) 23 | ? parser.Result 24 | : throw SectionException.MissingRequired(IniFormatting.Header); 25 | } 26 | 27 | /// 28 | /// Reads the from an ini target asynchronously. 29 | /// 30 | /// File path or stream to read from 31 | /// from another target to combine with 32 | /// Token used for cancellation 33 | /// object provided as the parameter, or a new instance if passed . 34 | public static async Task ReadMetadataAsync( 35 | ReadingDataSource source, Metadata? existing = null, CancellationToken cancellationToken = default) 36 | { 37 | using IniFileReader reader = new(source, existing); 38 | await reader.ReadAsync(cancellationToken); 39 | 40 | return reader.Parsers.TryGetFirst(out IniParser? parser) 41 | ? parser.Result 42 | : throw SectionException.MissingRequired(IniFormatting.Header); 43 | } 44 | 45 | /// 46 | /// Writes the to an ini target. 47 | /// 48 | /// File path or stream to write to 49 | /// to write 50 | public static void WriteMetadata(WritingDataSource source, Metadata metadata) 51 | { 52 | using IniFileWriter writer = new(source, new IniSerializer(metadata)); 53 | writer.Write(); 54 | } 55 | 56 | /// 57 | /// Writes the to an ini target asynchronously. 58 | /// 59 | /// File path or stream to write to 60 | /// to write 61 | /// Token used for cancellation 62 | public static Task WriteMetadataAsync(WritingDataSource source, Metadata metadata, CancellationToken cancellationToken = default) 63 | { 64 | using IniFileWriter writer = new(source, new IniSerializer(metadata)); 65 | return writer.WriteAsync(cancellationToken); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /ChartTools/Lyrics/VocalsPitch.cs: -------------------------------------------------------------------------------- 1 | namespace ChartTools.Lyrics; 2 | 3 | /// 4 | /// Wrapper type for with helper properties to get the pitch and key 5 | /// 6 | /// 7 | /// Creates a pitch from a raw pitch value. 8 | /// 9 | /// 10 | public readonly struct VocalsPitch(VocalsPitchValue value) : IEquatable, IEquatable 11 | { 12 | /// 13 | /// Pitch value 14 | /// 15 | public VocalsPitchValue Value { get; } = value; 16 | 17 | /// 18 | /// Key excluding the octave 19 | /// 20 | public VocalsKey Key => (VocalsKey)((int)Value & 0x0F); 21 | 22 | /// 23 | /// Octave number 24 | /// 25 | public byte Octave => (byte)(((int)Value & 0xF0) >> 4); 26 | 27 | public VocalsPitch() : this(VocalsPitchValue.None) { } 28 | 29 | #region Equals 30 | /// 31 | /// Indicates if two pitches have the same value. 32 | /// 33 | /// Pitch to compare 34 | public bool Equals(VocalsPitch other) => Value == other.Value; 35 | 36 | /// 37 | /// Indicates if a pitch has a value equal to a raw pitch value. 38 | /// 39 | /// Value to compare 40 | public bool Equals(VocalsPitchValue other) => Value == other; 41 | 42 | /// 43 | /// Indicates if an object is a raw pitch value or wrapper and the value is equal. 44 | /// 45 | /// Source of value 46 | public override bool Equals(object? obj) => obj is VocalsPitchValue value && Equals(value) || obj is VocalsPitch wrapper && Equals(wrapper); 47 | #endregion 48 | 49 | #region Operators 50 | /// 51 | /// Converts a raw pitch value to a matching wrapper. 52 | /// 53 | /// Pitch value 54 | public static implicit operator VocalsPitch(VocalsPitchValue pitch) => new(pitch); 55 | 56 | public static implicit operator VocalsPitchValue(VocalsPitch pitch) => pitch.Value; 57 | 58 | /// 59 | public static bool operator ==(VocalsPitch left, VocalsPitch right) => left.Equals(right); 60 | 61 | /// 62 | /// Indicates if two pitches don't have the same value. 63 | /// 64 | public static bool operator !=(VocalsPitch left, VocalsPitch right) => !(left == right); 65 | 66 | /// 67 | /// Indicates if the left pitch has a lower value than the right pitch according to music theory. 68 | /// 69 | public static bool operator <(VocalsPitch left, VocalsPitch right) => left.Value < right.Value; 70 | 71 | /// 72 | /// Indicates if the left pitch has a higher value than the right pitch according to music theory. 73 | /// 74 | public static bool operator >(VocalsPitch left, VocalsPitch right) => left.Value > right.Value; 75 | #endregion 76 | 77 | /// 78 | /// Returns the hash code for the pitch value. 79 | /// 80 | public override int GetHashCode() => (int)Value; 81 | } 82 | -------------------------------------------------------------------------------- /ChartTools/IO/Formatting/FormattingRules.cs: -------------------------------------------------------------------------------- 1 | using ChartTools.IO.Chart; 2 | using ChartTools.IO.Chart.Serializing; 3 | using ChartTools.IO.Ini; 4 | 5 | namespace ChartTools.IO.Formatting; 6 | 7 | /// 8 | /// Rules defined in song.ini that affect how the song data file is read and written 9 | /// 10 | /// Property summaries provided by Nathan Hurst. 11 | public class FormattingRules 12 | { 13 | public AlbumTrackKey AlbumTrackKey { get; set; } 14 | public CharterKey CharterKey { get; set; } 15 | 16 | /// 17 | /// Number of values per beat 18 | /// 19 | [ChartKeySerializable(ChartFormatting.Resolution)] 20 | public uint? Resolution { get; set; } 21 | public uint TrueResolution => Resolution ?? 480; 22 | 23 | /// 24 | /// Overrides the default sustain cutoff threshold with the specified number of ticks. 25 | /// 26 | [IniKeySerializable(IniFormatting.SustainCutoff)] 27 | public uint? SustainCutoff { get; set; } 28 | 29 | /// 30 | /// Overrides the natural HOPO threshold with the specified number of ticks. 31 | /// 32 | [IniKeySerializable(IniFormatting.HopoFrequency)] 33 | public uint? HopoFrequency { get; set; } 34 | 35 | internal uint ChartHopoFrequency => (uint)(65 / 192f * TrueResolution); 36 | 37 | #region Star power 38 | /// 39 | /// Overrides the Star Power phrase MIDI note for .mid charts. 40 | /// 41 | [IniKeySerializable(IniFormatting.MultiplierNote)] 42 | public byte? MultiplierNote { get; set; } 43 | /// 44 | /// (PhaseShift) Overrides the Star Power phrase MIDI note for .mid charts. 45 | /// 46 | [IniKeySerializable(IniFormatting.StarPowerNote)] 47 | public byte? StarPowerNote { get; set; } 48 | public byte? TrueStarPowerNote => StarPowerNote ?? MultiplierNote; 49 | #endregion 50 | 51 | #region SysEx 52 | /// 53 | /// (PhaseShift) Indicates if the chart uses SysEx events for sliders/tap notes. 54 | /// 55 | [IniKeySerializable(IniFormatting.SysExSliders)] 56 | public bool? SysExSliders { get; set; } 57 | 58 | /// 59 | /// (PhaseShift) Indicates if the chart uses SysEx events for Drums Real hi-hat pedal control. 60 | /// 61 | [IniKeySerializable(IniFormatting.SysExHighHat)] 62 | public bool? SysExHighHat { get; set; } 63 | 64 | /// 65 | /// (PhaseShift) Indicates if the chart uses SysEx events for Drums Real rimshot hits. 66 | /// 67 | [IniKeySerializable(IniFormatting.Rimshot)] 68 | public bool? SysExRimshot { get; set; } 69 | 70 | /// 71 | /// (PhaseShift) Indicates if the chart uses SysEx events for open notes. 72 | /// 73 | [IniKeySerializable(IniFormatting.SysExOpenBass)] 74 | public bool? SysExOpenBass { get; set; } 75 | 76 | /// 77 | /// (PhaseShift) Indicates if the chart uses SysEx events for Pro Guitar/Bass slide directions. 78 | /// 79 | [IniKeySerializable(IniFormatting.SysExProSlide)] 80 | public bool? SysexProSlide { get; set; } 81 | #endregion 82 | } -------------------------------------------------------------------------------- /ChartTools/Extensions/Collections/Alternating/SerialAlternatingEnumerable.cs: -------------------------------------------------------------------------------- 1 | using System.Collections; 2 | 3 | namespace ChartTools.Extensions.Collections.Alternating; 4 | 5 | /// 6 | /// Enumerable where items are yielded by alternating from a set of enumerables 7 | /// 8 | /// Type of the enumerated items 9 | public class SerialAlternatingEnumerable : IEnumerable 10 | { 11 | /// 12 | protected IEnumerable[] Enumerables { get; } 13 | 14 | /// 15 | /// Creates an instance of 16 | /// 17 | /// Enumerables to pull items from 18 | /// 19 | /// 20 | public SerialAlternatingEnumerable(params ReadOnlySpan> enumerables) 21 | { 22 | if (enumerables.Length == 0) 23 | throw new ArgumentException("No enumerables provided."); 24 | 25 | Enumerables = [..enumerables]; 26 | } 27 | 28 | /// 29 | public IEnumerator GetEnumerator() => new Enumerator([.. Enumerables.Select(e => e.GetEnumerator())])!; 30 | 31 | /// 32 | IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); 33 | 34 | /// 35 | /// Enumerator that yields items by alternating through a set of enumerators 36 | /// 37 | /// Enumerators to alternate between 38 | /// 39 | /// 40 | private class Enumerator(params ReadOnlySpan> enumerators) : IEnumerator 41 | { 42 | /// 43 | /// Enumerators to alternate between 44 | /// 45 | private IEnumerator[] Enumerators { get; } = [..enumerators]; 46 | 47 | /// 48 | /// Position of the next enumerator to pull from 49 | /// 50 | private int index; 51 | 52 | /// 53 | /// Item to use in the iteration 54 | /// 55 | public T? Current { get; private set; } 56 | 57 | /// 58 | object? IEnumerator.Current => Current; 59 | 60 | /// 61 | public void Dispose() 62 | { 63 | foreach (IEnumerator enumerator in Enumerators) 64 | enumerator.Dispose(); 65 | } 66 | 67 | /// 68 | public bool MoveNext() 69 | { 70 | int startingIndex = index; 71 | return SearchEnumerator(); 72 | 73 | bool SearchEnumerator() 74 | { 75 | IEnumerator enumerator = Enumerators[index]; 76 | 77 | if (enumerator.MoveNext()) 78 | { 79 | Current = enumerator.Current; 80 | 81 | // Move to the next enumerator 82 | if (++index == Enumerators.Length) 83 | index = 0; 84 | 85 | return true; 86 | } 87 | 88 | // End if looped back around to the first enumerator checked, else check the next enumerator 89 | return index != startingIndex && SearchEnumerator(); 90 | } 91 | } 92 | 93 | /// 94 | public void Reset() 95 | { 96 | // Reset every enumerator 97 | foreach (IEnumerator enumerator in Enumerators) 98 | enumerator.Reset(); 99 | 100 | index = 0; 101 | } 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /Docs/articles/improving-performance.md: -------------------------------------------------------------------------------- 1 | # Improving Performance 2 | This guide will cover alternate techniques that will improve performance when using ChartTools. 3 | 4 | ## Configuration 5 | By default, IO operations make multiple integrity checks to resolve errors. These checks can be configured or skipped by using a [Readingconfiguration](~/api/ChartTools.IO.Configuration.ReadingConfiguration) or [WritingConfiguration](~/api/ChartTools.IO.Configuration.WritingConfiguration) object. [Learn more about configuring IO operations](~/articles/configuration). 6 | 7 | The following example reads a song while bypassing checks for duplicate track objects: 8 | 9 | ```csharp 10 | using ChartTools.IO.Configuration; 11 | 12 | Song.FromFile("notes.chart", new ReadingConfiguration { Chart = new() { DuplicateTrackObjectPolicy = DuplicateTrackObjectPolicy.IncludeAll } }); 13 | ``` 14 | 15 | ## Targeted formats 16 | By default, the target format of an IO operation is determined by the file extension. You can bypass the extension check by using the respective file class located under [ChartTools.IO](~/api/ChartTools.IO). 17 | 18 | ```csharp 19 | using ChartTools.IO.Chart; 20 | using ChartToole.IO.Ini; 21 | 22 | Song song = ChartFile.ReadSong("notes.chart"); 23 | Metadata metadata = IniFile.ReadMetadata("song.ini"); 24 | ``` 25 | 26 | When working with a specific format, the file path can be swapped for a [Stream](https://learn.microsoft.com/dotnet/api/system.io.stream). 27 | 28 | ```csharp 29 | Stream fs = File.Open("notes.chart", FileMode.Open, FileAccess.Read); 30 | Song song = ChartFile.ReadSong(fs); 31 | ``` 32 | 33 | ## Single components 34 | Rather than performing IO operation on entire songs, such operations can be made on individual components. When writing a component to an existing file, the parts of the file regarding the component will be modified. 35 | 36 | ```csharp 37 | Metadata metadata = IniFile.ReadMetadata("song.ini"); 38 | SyncTrack guitar = ChartFile.ReadSyncTrack("notes.chart"); 39 | ``` 40 | 41 | ### Component lists 42 | If multiple components are needed, they can be combined in a single operation using a [ComponentList](~/api/ChartTools.IO.Components.ComponentList). 43 | 44 | ```csharp 45 | using ChartTools.IO.Chart; 46 | using ChartTools.IO.Components; 47 | 48 | Song song = ChartFile.ReadComponents("notes.chart", new ComponentList() 49 | { 50 | Metadata = true, 51 | SyncTrack = true, 52 | GlobalEvents = true, 53 | Instruments = new InstrumentComponentList() 54 | { 55 | StandardLeadGuitar = DifficultySet.All, 56 | StandardBass = DifficultySet.Easy | DifficultySet.Expert 57 | } 58 | }); 59 | ``` 60 | 61 | ## Asynchronous operations 62 | Every IO operation can be performed asynchronously by appending `Async` to the name of a method. 63 | 64 | ```charp 65 | Task readTask = Song.FromDirectoryAsync(directory); 66 | ``` 67 | 68 | Asynchronous operations support a [CancellationToken](https://learn.microsoft.com/dotnet/api/system.threading.cancellationtoken) as an optional parameter. If omitted. [CancellationToken.None](https://learn.microsoft.com/dotnet/api/system.threading.cancellationtoken.none#system-threading-cancellationtoken-none) will be used. 69 | 70 | ```csharp 71 | Task readTask = Song.FromDirectoryAsync(directory, , ); 72 | ``` 73 | 74 | Asynchronous operations make heavy use of multi-threading and are beneficial even if the result is to be awaited immediately. -------------------------------------------------------------------------------- /ChartTools/IO/Sources/WritingDataSource.cs: -------------------------------------------------------------------------------- 1 | namespace ChartTools.IO.Sources; 2 | 3 | /// 4 | /// Represents a combination of a data source that can be written to and a to combine data with. 5 | /// 6 | /// Can be implicitly converted from a or . 7 | public class WritingDataSource : DataSource 8 | { 9 | /// 10 | /// Source of existing data to combine during write operations 11 | /// 12 | /// Can point to the same file or share a instance with the . 13 | public ReadingDataSource? Existing { get; } 14 | 15 | /// 16 | /// Creates a from a . 17 | /// 18 | /// Stream to source from. Must be readable through and seekable through . 19 | /// 20 | /// The stream is not seekable and/or writable 21 | /// The stream is kept alive after the lifetime of the . 22 | public WritingDataSource(Stream stream, ReadingDataSource? existing = null) : base(stream) 23 | { 24 | if (!stream.CanSeek || !stream.CanWrite) 25 | throw new ArgumentException("Stream is not seekable or writable", nameof(stream)); 26 | 27 | Existing = existing; 28 | } 29 | 30 | /// 31 | /// Creates a from a file path. 32 | /// 33 | /// Path of the file to source from 34 | /// . If , uses the writing path if the file already exists. 35 | /// 36 | /// Initializes the stream as a with and . 37 | /// The stream is disposed when disposing the . 38 | /// 39 | public WritingDataSource(string path, ReadingDataSource? existing = null) 40 | : base(path, FileMode.OpenOrCreate, FileAccess.Write, FileShare.Read) 41 | { 42 | Existing = existing is not null ? existing : 43 | (File.Exists(path) ? new(path) : null); 44 | } 45 | 46 | /// 47 | /// Disposes the reading source if provided and stream if generated. 48 | /// 49 | public override void Dispose() 50 | { 51 | base.Dispose(); 52 | Existing?.Dispose(); 53 | } 54 | 55 | /// 56 | /// Creates a from a as a target and source of existing data. 57 | /// 58 | /// Stream to create from 59 | /// 60 | public static implicit operator WritingDataSource(Stream stream) => new(stream, stream); 61 | 62 | /// 63 | /// Creates a from a file path as a target and source of existing data. 64 | /// 65 | /// Path to create from 66 | /// 67 | public static implicit operator WritingDataSource(string path) => new(path, path); 68 | } 69 | -------------------------------------------------------------------------------- /Docs/articles/dynamic-syntax.md: -------------------------------------------------------------------------------- 1 | # Dynamic Syntax 2 | This guide will cover an alternate and more flexible syntax for accessing components. 3 | 4 | ## Using the dynamic syntax 5 | ChartTools supports a dynamic syntax to retrieve instruments and tracks using identity enums instead of explicit properties. 6 | 7 | ```csharp 8 | StandardInstrument guitar = song.Instruments.Get(StandardInstrumentIdentity.LeadGuitar); 9 | Instrument bass = song.Instruments.Get(InstrumentIdentity.StandardBass); 10 | 11 | Track easyGuitar = guitar.GetTrack(Difficulty.Easy); 12 | Track easyBass = bass.GetTrack(Difficulty.Easy); 13 | ``` 14 | 15 | The dynamic syntax uses three enums to get instruments: 16 | 17 | - [StandardInstrumentIdentity](~/api/ChartTools.StandardInstrumentIdentity) - Instruments using standard chords 18 | - [GHLInstrumentIdentity](~/api/ChartTools.GHLInstrumentIdentity) - Instruments using Guitar Hero Live chords 19 | - [InstrumentIdentity](~/api/ChartTools.InstrumentIdentity) - All instruments including drums 20 | 21 | Drums do not an enum for their chord types as they are the only instrument using their respective chords. 22 | 23 | ## Generic vs. non-generic 24 | When an instrument is obtained dynamically using the [InstrumentIdentity](~/api/ChartTools.InstrumentIdentity) enum, the returned object is of type [Instrument](~/api/ChartTools.Instrument). When a track is obtained from a non-generic instrument, either dynamically or explicitly through a property, the track will be of type [Track](~/api/ChartTools.Track). This concept extends to chords and notes. 25 | 26 | When working with a non-generic track, the following rules apply: 27 | - Chords cannot be added or removed. The position of existing chords can be modified. 28 | - Notes can be created using `CreateNote` 29 | - A note's identity can be obtained through the read-only [Index](~/api/ChartTools.INote#ChartTools_INote_Index) property. 30 | - A note's sustain can be modified. 31 | - Local events and special phrases have no restrictions. 32 | 33 | Being the base types of the generic counterparts, non-generic instruments, tracks, chords and notes can be cast to a generic version: 34 | 35 | ```csharp 36 | Instrument instrument = song.Instruments.Get(InstrumentIdentity.StandardLeadGuitar); 37 | StandardInstrument stadardInstrument = (StandardInstrument)instrument; 38 | 39 | Track track = instrument.GetTrack(Difficulty.Easy); 40 | Track standardTrack = (Track)track; 41 | 42 | Chord chord = track.Chords[0]; 43 | StandardChord standardChord = (StandardChord)chord; 44 | 45 | INote note = chord.Notes[0]; 46 | LaneNote standardNote = (LaneNote)note; 47 | ``` 48 | 49 | The dynamic syntax can also be used to set and read instruments and tracks. 50 | 51 | ```csharp 52 | // Setting components 53 | song.Instruments.Set(guitar); 54 | song.Instruments.Set(guitar with { InstrumentIdentity = StandardInstrumentIdentity.Bass }); 55 | 56 | song.Instruments.StandardLeadGuitar.SetTrack(new() { Difficulty = Difficulty.Easy }); 57 | ``` 58 | 59 | When setting an instrument, the target is determined by the [InstrumentIdentity](~/api/ChartTools.Instrument#ChartTools_Instrument_InstrumentIdentity) property of the new instrument, which can be overridden using a `with` statement. Similarly, the target difficulty when setting a track is determined by the track's [Difficulty](~/api/ChartTools.Track#ChartTools_Track_Difficulty) property, also overridable through `with`. 60 | 61 | -------------------------------------------------------------------------------- /ChartTools.Tests/SystemExtensionsTests.cs: -------------------------------------------------------------------------------- 1 | using ChartTools.Extensions.Linq; 2 | using Microsoft.VisualStudio.TestTools.UnitTesting; 3 | 4 | namespace ChartTools.Tests; 5 | 6 | [TestClass] 7 | public class SystemExtensionsTests 8 | { 9 | static readonly bool[] trueArray = [true, true]; 10 | static readonly bool[] falseArray = [false, false]; 11 | 12 | [TestMethod] public void FirstOrDefaultNullPredicate() 13 | => Assert.ThrowsException(() => trueArray.FirstOrDefault(null!, false)); 14 | 15 | [TestMethod] public void OutFirstOrDefaultNullPredicate() 16 | => Assert.ThrowsException(() => trueArray.FirstOrDefault(null!, false, out bool returnedDefault)); 17 | 18 | [TestMethod] public void FirstOrDefaultExistingItem() => Assert.AreEqual(true, trueArray.FirstOrDefault(b => b, false)); 19 | 20 | [TestMethod] public void OutFirstOrDefaultExistingItem() 21 | { 22 | Assert.AreEqual(true, trueArray.FirstOrDefault(b => b, false, out bool returnedDefault)); 23 | Assert.IsFalse(returnedDefault); 24 | } 25 | 26 | [TestMethod] public void FirstOrDefaultNonExistentItem() => Assert.AreEqual(true, trueArray.FirstOrDefault(b => !b, true)); 27 | 28 | [TestMethod] public void OutFirstOrDefaultNonExistentItem() 29 | { 30 | Assert.AreEqual(true, trueArray.FirstOrDefault(b => !b, true, out bool returnedDefault)); 31 | Assert.IsTrue(returnedDefault); 32 | } 33 | 34 | [TestMethod] public void TryGetFirstNullPredicate() 35 | => Assert.ThrowsException(() => trueArray.TryGetFirst(null!, out bool b)); 36 | 37 | [TestMethod] public void TryGetFirstNoItems() 38 | { 39 | Assert.IsFalse(Array.Empty().TryGetFirst(b => b, out bool item)); 40 | Assert.AreEqual(default, item); 41 | } 42 | 43 | [TestMethod] public void TryGetFirstNonExistentItem() 44 | { 45 | Assert.IsFalse(falseArray.TryGetFirst(b => b, out bool item)); 46 | Assert.AreEqual(default, item); 47 | } 48 | 49 | [TestMethod] public void TryGetFirstExistentItem() 50 | { 51 | Assert.IsTrue(trueArray.TryGetFirst(b => b, out bool item)); 52 | Assert.AreEqual(true, item); 53 | } 54 | 55 | [TestMethod] public void ReplaceNullPredicate() 56 | => Assert.ThrowsException(() => trueArray.Replace(null!, false).ToArray()); 57 | 58 | [TestMethod] public void ReplaceNoMatch() 59 | => Assert.AreEqual(string.Join(' ', falseArray), string.Join(' ', falseArray.Replace(b => b, true))); 60 | 61 | [TestMethod] public void ReplaceMatch() 62 | { 63 | int[] numbers = [.. Enumerable.Range(0, 10)]; 64 | Assert.AreEqual("0 1 2 3 4 5 0 0 0 0", string.Join(' ', numbers.Replace(n => n > 5, 0))); 65 | } 66 | 67 | [TestMethod] public void ReplaceSectionNullStartReplace() 68 | => Assert.ThrowsException(() => trueArray.ReplaceSection(new([], null!, b => true, true)).ToArray()); 69 | 70 | [TestMethod] public void ReplaceSectionNullEndReplace() 71 | => Assert.ThrowsException(() => trueArray.ReplaceSection(new([], b => true, null!, true)).ToArray()); 72 | 73 | [TestMethod] public void ReplaceSectionNeverStart() 74 | => Assert.AreEqual(Formatting.FormatCollection(trueArray), Formatting.FormatCollection(trueArray.ReplaceSection(new(falseArray, b => false, b => true, false)))); 75 | } 76 | -------------------------------------------------------------------------------- /ChartTools/IO/Chart/Parsing/SyncTrackParser.cs: -------------------------------------------------------------------------------- 1 | using ChartTools.Extensions.Linq; 2 | using ChartTools.IO.Chart.Configuration.Sessions; 3 | using ChartTools.IO.Chart.Entries; 4 | 5 | namespace ChartTools.IO.Chart.Parsing; 6 | 7 | internal class SyncTrackParser(ChartReadingSession session) 8 | : ChartParser(session, ChartFormatting.SyncTrackHeader) 9 | { 10 | public override SyncTrack Result => GetResult(result); 11 | private readonly SyncTrack result = new(); 12 | 13 | private readonly List tempos = [], orderedTempos = []; 14 | private readonly List orderedAnchors = []; 15 | private readonly List orderedSignatures = []; 16 | 17 | protected override void HandleItem(string line) 18 | { 19 | TrackObjectEntry entry = new(line); 20 | 21 | switch (entry.Type) 22 | { 23 | case "TS": // Time signature 24 | if (CheckDuplicate(orderedSignatures, "time signature", out int newIndex)) 25 | break; 26 | 27 | string[] split = ChartFormatting.SplitData(entry.Data); 28 | 29 | byte 30 | numerator = ValueParser.ParseByte(split[0], "numerator"), 31 | denominator = 4; 32 | 33 | // Denominator is only written if not equal to 4 34 | if (split.Length >= 2) 35 | denominator = (byte)Math.Pow(2, ValueParser.ParseByte(split[1], "denominator")); 36 | 37 | TimeSignature signature = new(entry.Position, numerator, denominator); 38 | 39 | result.TimeSignatures.Add(signature); 40 | orderedSignatures.Insert(newIndex, signature); 41 | break; 42 | case "B": // Tempo 43 | if (CheckDuplicate(orderedTempos, "tempo marker", out newIndex)) 44 | break; 45 | 46 | // Floats are written by rounding to the 3rd decimal and removing the decimal point 47 | float value = ValueParser.ParseFloat(entry.Data, "value") / 1000; 48 | Tempo tempo = new(entry.Position, value); 49 | 50 | tempos.Add(tempo); 51 | orderedTempos.Add(tempo); 52 | break; 53 | case "A": // Anchor 54 | if (CheckDuplicate(orderedAnchors, "tempo anchor", out newIndex)) 55 | break; 56 | 57 | // Floats are written by rounding to the 3rd decimal and removing the decimal point 58 | TimeSpan anchor = TimeSpan.FromSeconds(ValueParser.ParseFloat(entry.Data, "anchor") / 1000); 59 | 60 | orderedAnchors.Insert(newIndex, new(entry.Position, anchor)); 61 | break; 62 | } 63 | 64 | bool CheckDuplicate(IList existing, string objectType, out int newIndex) where T : IReadOnlyTrackObject 65 | { 66 | int index = 0; 67 | bool result = !Session.HandleDuplicate(entry.Position, objectType, () => 68 | { 69 | index = existing.BinarySearchIndex(entry.Position, t => t.Position, out bool exactMatch); 70 | 71 | return exactMatch; 72 | }); 73 | 74 | newIndex = index; 75 | 76 | return result; 77 | } 78 | } 79 | 80 | protected override void FinalizeParse() 81 | { 82 | foreach (Anchor anchor in orderedAnchors) 83 | { 84 | // Find the marker matching the position in case it was already added through a mention of value 85 | int markerIndex = orderedTempos.BinarySearchIndex(anchor.Position, t => t.Position, out bool markerFound); 86 | 87 | if (markerFound) 88 | { 89 | orderedTempos[markerIndex].Anchor = anchor.Value; 90 | orderedTempos.RemoveAt(markerIndex); 91 | } 92 | else if (Session.HandleTempolessAnchor(anchor)) 93 | result.Tempo.Add(new(anchor.Position, 0) { Anchor = anchor.Value }); 94 | } 95 | 96 | base.FinalizeParse(); 97 | } 98 | 99 | public override void ApplyToSong(Song song) 100 | { 101 | song.SyncTrack = Result; 102 | song.SyncTrack.Tempo.AddRange(tempos); 103 | } 104 | } 105 | --------------------------------------------------------------------------------