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

  1. Validate the replay file integrity/version before deserialising; reject files with an unknown/corrupt header.
  2. Ensure prior reads consumed the correct number of bytes for the format version (a misaligned read makes the Int64 decode garbage).
  3. 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

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


AI-assisted analysis of ppy/osu@d9c73e12ad (2026-08-13). Data as JSON: /api/errors/d0fda7284fbcb720. Report an issue: GitHub.