ppy/osu · error · IOException
Bad ticks count read!
Error message
Bad ticks count read!
What it means
Thrown by SerializationReader.ReadDateTime after reading an Int64 ticks value; if ticks < 0 the stream contains an invalid DateTime (DateTime ticks must be non-negative). This is a defence against corrupt/truncated legacy replay/replay-frame data in the osu! serialisation format.
Source
Thrown at osu.Game/IO/Legacy/SerializationReader.cs:60
return Array.Empty<byte>();
}
/// <summary> Reads a char array from the buffer, handling nulls and the array length. </summary>
public char[] ReadCharArray()
{
int len = ReadInt32();
if (len > 0) return ReadChars(len);
if (len < 0) return null;
return Array.Empty<char>();
}
/// <summary> Reads a DateTime from the buffer. </summary>
public DateTime ReadDateTime()
{
long ticks = ReadInt64();
if (ticks < 0) throw new IOException("Bad ticks count read!");
return new DateTime(ticks, DateTimeKind.Utc);
}
/// <summary> Reads a generic list from the buffer. </summary>
public IList<T> ReadBList<T>(bool skipErrors = false) where T : ILegacySerializable, new()
{
int count = ReadInt32();
if (count < 0) return null;
IList<T> d = new List<T>(count);
SerializationReader sr = new SerializationReader(BaseStream);
for (int i = 0; i < count; i++)
{
T obj = new T();
View on GitHub (pinned to d9c73e12ad)
Solutions
- Validate the replay file integrity/version before deserialising; reject files with an unknown/corrupt header.
- Ensure prior reads consumed the correct number of bytes for the format version (a misaligned read makes the Int64 decode garbage).
- Wrap deserialisation in try/catch(IOException) and treat the replay as unreadable, keeping the score record but dropping the replay.
Example fix
// before
public DateTime ReadDateTime()
{
long ticks = ReadInt64();
if (ticks < 0) throw new IOException("Bad ticks count read!");
return new DateTime(ticks, DateTimeKind.Utc);
}
// after (caller-side graceful handling)
try
{
DateTime ts = reader.ReadDateTime();
}
catch (IOException)
{
Logger.Log("Replay stream corrupt at DateTime field; replay discarded.", LoggingTarget.Database, LogLevel.Important);
return;
} Defensive patterns
Strategy: try-catch
Validate before calling
// Validate stream length/version header before reading fields.
if (stream.Length < expectedMinLength) throw new InvalidDataException("Replay stream too short."); Try / catch
try { var dt = reader.ReadDateTime(); }
catch (IOException) { /* discard corrupt replay, keep score metadata */ } Prevention
- Read legacy blobs only from sources matching the expected format version.
- Keep reader calls in try/catch(IOException) so one corrupt field doesn't crash import.
- Log the stream offset when corrupt data is hit to aid diagnosis.
When it happens
Trigger: Reading a DateTime field from a legacy serialised stream (.osr replay or related binary blob) where the 8-byte ticks value decodes to a negative number — caused by a truncated, swapped, or corrupt byte stream, or a version mismatch in the replay format.
Common situations: Parsing a damaged replay file, a replay from an incompatible game version, or a hand-crafted/edited binary. Also when the reader's position is wrong (misaligned reads earlier consuming the wrong bytes).
Related errors
- Deserialization of arbitrary type is not supported.
- input .lzma is too short
- Can't Read 1
- Unknown hit object type.
- Unknown file format (no content)
AI-assisted analysis of ppy/osu@d9c73e12ad (2026-08-13).
Data as JSON: /api/errors/d0fda7284fbcb720.
Report an issue: GitHub.