Task 490: .OSU File Format

Task 490: .OSU File Format

1. List of all properties of the .OSU file format

The .OSU file format is a text-based format used for osu! beatmaps. It starts with a version line (e.g., "osu file format v14") and is divided into sections denoted by square brackets. Properties are primarily key:value pairs in most sections, while some sections (Events, TimingPoints, HitObjects) use comma-separated lines for data structures. The properties "intrinsic to its file system" appear to refer to the core fields and structures that define the file's content and layout. Based on the specifications, here is a comprehensive list of all properties by section:

File Header:

  • Version (e.g., "osu file format v14" – specifies the format version, required as the first line).

[General] (General beatmap information, key:value pairs):

  • AudioFilename (string: path to audio file).
  • AudioLeadIn (integer: milliseconds of silence before audio starts).
  • AudioHash (string: deprecated hash).
  • PreviewTime (integer: milliseconds when audio preview starts).
  • Countdown (integer: countdown speed; 0 = none, 1 = normal, 2 = half, 3 = double).
  • SampleSet (string: default sample set; "Normal", "Soft", "Drum").
  • StackLeniency (decimal: stacking threshold multiplier, 0–1).
  • Mode (integer: game mode; 0 = osu!, 1 = taiko, 2 = catch, 3 = mania).
  • LetterboxInBreaks (0 or 1: letterboxing during breaks).
  • StoryFireInFront (0 or 1: deprecated).
  • UseSkinSprites (0 or 1: allow storyboard to use user skin sprites).
  • AlwaysShowPlayfield (0 or 1: deprecated).
  • OverlayPosition (string: hit circle overlay position; "NoChange", "Below", "Above").
  • SkinPreference (string: preferred skin).
  • EpilepsyWarning (0 or 1: show flashing color warning).
  • CountdownOffset (integer: beats before first hit object for countdown).
  • SpecialStyle (0 or 1: special key layout for mania).
  • WidescreenStoryboard (0 or 1: widescreen storyboard support).
  • SamplesMatchPlaybackRate (0 or 1: samples change with speed mods).

[Editor] (Editor settings, key:value pairs):

  • Bookmarks (comma-separated integers: bookmark times in ms).
  • DistanceSpacing (decimal: distance snap multiplier).
  • BeatDivisor (integer: beat snap divisor).
  • GridSize (integer: grid size).
  • TimelineZoom (decimal: timeline scale factor).

[Metadata] (Beatmap identification, key:value pairs):

  • Title (string: romanized title).
  • TitleUnicode (string: unicode title).
  • Artist (string: romanized artist).
  • ArtistUnicode (string: unicode artist).
  • Creator (string: mapper's username).
  • Version (string: difficulty name).
  • Source (string: song source).
  • Tags (space-separated strings: search tags).
  • BeatmapID (integer: difficulty ID).
  • BeatmapSetID (integer: beatmap set ID).

[Difficulty] (Difficulty settings, key:value pairs):

  • HPDrainRate (decimal: HP drain, 0–10).
  • CircleSize (decimal: circle size, 0–10).
  • OverallDifficulty (decimal: overall difficulty, 0–10).
  • ApproachRate (decimal: approach rate, 0–10).
  • SliderMultiplier (decimal: base slider velocity in hundreds of osu!pixels per beat).
  • SliderTickRate (decimal: slider ticks per beat).

[Events] (Graphic events, comma-separated lines):

  • Background (format: 0,0,filename,xOffset,yOffset – background image).
  • Video (format: Video,startTime,filename,xOffset,yOffset – video playback).
  • Break (format: 2,startTime,endTime – break period).
  • Storyboard events (complex scripting for storyboards; can be in .osu or separate .osb file).

[TimingPoints] (Timing sections, comma-separated lines):

  • Time (integer: start time in ms).
  • BeatLength (decimal: beat duration in ms for uninherited, or negative inverse slider velocity % for inherited).
  • Meter (integer: beats per measure).
  • SampleSet (integer: hit object sample set; 0=default, 1=normal, 2=soft, 3=drum).
  • SampleIndex (integer: custom sample index).
  • Volume (integer: volume %).
  • Uninherited (0 or 1: uninherited timing point).
  • Effects (integer: bit flags for kiai (bit 0) and omit first barline (bit 3)).

[Colours] (Colors, key:value pairs):

  • Combo# (comma-separated RGB integers: combo colors, # is 1+).
  • SliderTrackOverride (comma-separated RGB integers: slider track color).
  • SliderBorder (comma-separated RGB integers: slider border color).

[HitObjects] (Hit objects, comma-separated lines):

  • X (integer: x-position).
  • Y (integer: y-position).
  • Time (integer: hit time in ms).
  • Type (integer: bit flags; bit0=circle, bit1=slider, bit2=new combo, bit3=spinner, bits4-6=combo skip, bit7=mania hold).
  • HitSound (integer: bit flags; bit0=normal, bit1=whistle, bit2=finish, bit3=clap).
  • ObjectParams (type-specific: e.g., for sliders: curveType|points,repeats,length,edgeHitSounds,edgeAdditions; for spinners: endTime).
  • HitSample (colon-separated: normalSet:additionSet:index:volume:filename).

.OSU files are typically contained within .OSZ archives for distribution, but here are two direct download links to .OSZ files (which contain .OSU files and can be extracted with any zip tool):

3. Ghost blog embedded HTML JavaScript for drag and drop .OSU file dump

Below is an HTML page with embedded JavaScript that can be embedded in a Ghost blog post (or any HTML-enabled blog). It creates a drop zone for dragging a .OSU file, parses it, and dumps all properties to the screen in a readable format.

.OSU File Parser
Drag and drop a .OSU file here

4. Python class for .OSU file handling

class OsuFileHandler:
    def __init__(self, filepath):
        self.filepath = filepath
        self.sections = {}
        self.version = ''

    def read(self):
        with open(self.filepath, 'r', encoding='utf-8') as f:
            content = f.readlines()
        current_section = ''
        for line in content:
            line = line.strip()
            if not line or line.startswith('//'):
                continue
            if line.startswith('osu file format v'):
                self.version = line
            elif line.startswith('[') and line.endsWith(']'):
                current_section = line
                self.sections[current_section] = []
            elif current_section:
                self.sections[current_section].append(line)
        return self.sections

    def print_properties(self):
        print(f'Version: {self.version}')
        for section, lines in self.sections.items():
            print(f'\n{section}')
            for line in lines:
                print(f'  {line}')

    def write(self, new_filepath=None):
        filepath = new_filepath or self.filepath
        with open(filepath, 'w', encoding='utf-8') as f:
            f.write(self.version + '\n')
            for section, lines in self.sections.items():
                f.write(f'\n{section}\n')
                for line in lines:
                    f.write(line + '\n')

# Example usage:
# handler = OsuFileHandler('example.osu')
# handler.read()
# handler.print_properties()
# handler.sections['[General]'].append('NewKey:Value')  # Modify
# handler.write('modified.osu')

5. Java class for .OSU file handling

import java.io.*;
import java.util.*;

public class OsuFileHandler {
    private String filepath;
    private Map<String, List<String>> sections = new LinkedHashMap<>();
    private String version = "";

    public OsuFileHandler(String filepath) {
        this.filepath = filepath;
    }

    public Map<String, List<String>> read() throws IOException {
        try (BufferedReader br = new BufferedReader(new FileReader(filepath))) {
            String line;
            String currentSection = "";
            while ((line = br.readLine()) != null) {
                line = line.trim();
                if (line.isEmpty() || line.startsWith("//")) continue;
                if (line.startsWith("osu file format v")) {
                    version = line;
                } else if (line.startsWith("[") && line.endsWith("]")) {
                    currentSection = line;
                    sections.put(currentSection, new ArrayList<>());
                } else if (!currentSection.isEmpty()) {
                    sections.get(currentSection).add(line);
                }
            }
        }
        return sections;
    }

    public void printProperties() {
        System.out.println("Version: " + version);
        for (Map.Entry<String, List<String>> entry : sections.entrySet()) {
            System.out.println("\n" + entry.getKey());
            for (String prop : entry.getValue()) {
                System.out.println("  " + prop);
            }
        }
    }

    public void write(String newFilepath) throws IOException {
        if (newFilepath == null) newFilepath = filepath;
        try (BufferedWriter bw = new BufferedWriter(new FileWriter(newFilepath))) {
            bw.write(version + "\n");
            for (Map.Entry<String, List<String>> entry : sections.entrySet()) {
                bw.write("\n" + entry.getKey() + "\n");
                for (String prop : entry.getValue()) {
                    bw.write(prop + "\n");
                }
            }
        }
    }

    // Example usage:
    // public static void main(String[] args) throws IOException {
    //     OsuFileHandler handler = new OsuFileHandler("example.osu");
    //     handler.read();
    //     handler.printProperties();
    //     handler.sections.get("[General]").add("NewKey:Value"); // Modify
    //     handler.write("modified.osu");
    // }
}

6. JavaScript class for .OSU file handling

This is a Node.js compatible class (requires fs module).

const fs = require('fs');

class OsuFileHandler {
  constructor(filepath) {
    this.filepath = filepath;
    this.sections = {};
    this.version = '';
  }

  read() {
    const content = fs.readFileSync(this.filepath, 'utf-8').split('\n');
    let currentSection = '';
    content.forEach(line => {
      line = line.trim();
      if (!line || line.startsWith('//')) return;
      if (line.startsWith('osu file format v')) {
        this.version = line;
      } else if (line.startsWith('[') && line.endsWith(']')) {
        currentSection = line;
        this.sections[currentSection] = [];
      } else if (currentSection) {
        this.sections[currentSection].push(line);
      }
    });
    return this.sections;
  }

  printProperties() {
    console.log(`Version: ${this.version}`);
    for (const [section, lines] of Object.entries(this.sections)) {
      console.log(`\n${section}`);
      lines.forEach(l => console.log(`  ${l}`));
    }
  }

  write(newFilepath = this.filepath) {
    let output = `${this.version}\n`;
    for (const [section, lines] of Object.entries(this.sections)) {
      output += `\n${section}\n`;
      lines.forEach(l => output += `${l}\n`);
    }
    fs.writeFileSync(newFilepath, output, 'utf-8');
  }
}

// Example usage:
// const handler = new OsuFileHandler('example.osu');
// handler.read();
// handler.printProperties();
// handler.sections['[General]'].push('NewKey:Value'); // Modify
// handler.write('modified.osu');

7. C "class" (struct with functions) for .OSU file handling

C doesn't have classes, so this uses a struct with associated functions.

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define MAX_SECTIONS 10
#define MAX_LINES 1000
#define MAX_LINE_LEN 256

typedef struct {
    char *filepath;
    char version[50];
    char sections[MAX_SECTIONS][50];
    char *lines[MAX_SECTIONS][MAX_LINES];
    int lineCounts[MAX_SECTIONS];
    int sectionCount;
} OsuFileHandler;

OsuFileHandler* createHandler(const char *filepath) {
    OsuFileHandler *handler = malloc(sizeof(OsuFileHandler));
    handler->filepath = strdup(filepath);
    handler->sectionCount = 0;
    memset(handler->version, 0, 50);
    for (int i = 0; i < MAX_SECTIONS; i++) {
        handler->lineCounts[i] = 0;
    }
    return handler;
}

void read(OsuFileHandler *handler) {
    FILE *file = fopen(handler->filepath, "r");
    if (!file) {
        printf("Error opening file\n");
        return;
    }
    char line[MAX_LINE_LEN];
    int currentSection = -1;
    while (fgets(line, sizeof(line), file)) {
        char *trimmed = strtok(line, "\r\n");
        if (!trimmed || strlen(trimmed) == 0 || strncmp(trimmed, "//", 2) == 0) continue;
        if (strncmp(trimmed, "osu file format v", 17) == 0) {
            strcpy(handler->version, trimmed);
        } else if (trimmed[0] == '[' && trimmed[strlen(trimmed)-1] == ']') {
            currentSection++;
            strcpy(handler->sections[currentSection], trimmed);
            handler->sectionCount = currentSection + 1;
        } else if (currentSection >= 0) {
            int idx = handler->lineCounts[currentSection]++;
            handler->lines[currentSection][idx] = strdup(trimmed);
        }
    }
    fclose(file);
}

void printProperties(OsuFileHandler *handler) {
    printf("Version: %s\n", handler->version);
    for (int i = 0; i < handler->sectionCount; i++) {
        printf("\n%s\n", handler->sections[i]);
        for (int j = 0; j < handler->lineCounts[i]; j++) {
            printf("  %s\n", handler->lines[i][j]);
        }
    }
}

void write(OsuFileHandler *handler, const char *newFilepath) {
    const char *path = newFilepath ? newFilepath : handler->filepath;
    FILE *file = fopen(path, "w");
    if (!file) {
        printf("Error writing file\n");
        return;
    }
    fprintf(file, "%s\n", handler->version);
    for (int i = 0; i < handler->sectionCount; i++) {
        fprintf(file, "\n%s\n", handler->sections[i]);
        for (int j = 0; j < handler->lineCounts[i]; j++) {
            fprintf(file, "%s\n", handler->lines[i][j]);
        }
    }
    fclose(file);
}

void freeHandler(OsuFileHandler *handler) {
    free(handler->filepath);
    for (int i = 0; i < handler->sectionCount; i++) {
        for (int j = 0; j < handler->lineCounts[i]; j++) {
            free(handler->lines[i][j]);
        }
    }
    free(handler);
}

// Example usage:
// int main() {
//     OsuFileHandler *handler = createHandler("example.osu");
//     read(handler);
//     printProperties(handler);
//     // Modify: handler->lines[0][handler->lineCounts[0]++] = strdup("NewKey:Value");
//     write(handler, "modified.osu");
//     freeHandler(handler);
//     return 0;
// }