Upon investigating an user report in #6091 that indicated that viewing
replays using the Perfect mod would also display the Sudden Death mod
icon despite Perfect being the more restrictive of the two, it turned
out that the logic of importing legacy scores was missing that corner
case. A similar case of Double Time/Nightcore mutual exclusion was
handled, but PF/SD was missed.
Add analogous handling of PF/SD legacy mods for all four rulesets,
and additionally cover a tiny fraction of all cases with unit tests.
The most problematic cases (NC+HD and PF+SD) are covered in all four
basic rulesets.
After the preparatory introduction of LineBufferedReader, it is now
possible to introduce registration of fallback decoders that won't drop
input supplied in the first line of the file.
A fallback decoder is used when the magic in the first line of the file
does not match any of the other known decoders. In such a case,
the fallback decoder is constructed and provided a LineBufferedReader
instance. The process of matching magic only peeks the first non-empty
line, so it is available for re-reading in Decode() using ReadLine().
There can be only one fallback decoder per type; a second attempt of
registering a fallback will result in an exception to avoid bugs.
To address the issue of parsing failing on badly or non-headered files,
set the legacy decoders for Beatmaps and Storyboards as the fallbacks.
Due to non-trivial logic, several new, passing unit tests with possible
edge cases also included.
Add a line-buffered reader decorator operating on StreamReader
instances. The decorator has two main operations - PeekLine(), which
allows to see the next line in the stream without consuming it,
ReadLine(), which consumes and returns the next line in the stream, and
ReadToEnd() which reads all the remaining text in the stream (including
the unconsumed peeked line). Peeking line-per-line uses an internal
queue of lines that have been read ahead from the underlying stream.
The addition of the line-buffered reader is a workaround solution to
a problem with decoding. At current selecting a decoder works by
irreversibly reading the first line from the stream and looking for
a magic string that indicates the type of decoder to use.
It might however be possible for a file to be valid in format, just
missing a header. In such a case a lack of a line-buffered reader makes
it impossible to reparse the content of that first line. Introducing it
will however allow to peek the first line for magic first.
- If magic is found in the first line, GetDecoder() will peek it and
use it to return the correct Decoder instance. Note that in the case
of JsonBeatmapDecoder the magic is the opening JSON object brace,
and therefore must not be consumed.
- If magic is not found, the fallback decoder will be able to consume
it using ReadLine() in Decode().
This commit additionally contains basic unit tests for the reader.
Suggested-by: Aergwyn <aergwyn@t-online.de>