Task 486: .OSK File Format

Task 486: .OSK File Format

1. List of all the properties of this file format intrinsic to its file system

The .OSK file format is used for osu! skins. It is essentially a ZIP archive (with the .osk extension) containing the contents of an osu! skin folder. The core "properties" are defined in a configuration file named skin.ini inside the archive, which is an INI-format text file with sections, keys, and values that control the skin's behavior, colors, fonts, and mode-specific settings. These properties are intrinsic to the format as they define how the game interprets the skin's assets (images, sounds, etc.).

Here is a comprehensive list of all properties from the skin.ini specifications, grouped by section. Values types are noted (e.g., text, integer, RGB, boolean as 0/1). Defaults are provided where applicable.

[General]

  • Name: text (skin name). Default: "Unknown".
  • Author: text (skin creator). Default: empty.
  • Version: version number (e.g., 1.0, 2.7) or "latest". Default: 1.0 if unspecified.
  • AnimationFramerate: positive integer or -1. Default: -1.
  • AllowSliderBallTint: 0 or 1. Default: 0.
  • ComboBurstRandom: 0 or 1. Default: 0.
  • CursorCentre: 0 or 1. Default: 1.
  • CursorExpand: 0 or 1. Default: 1.
  • CursorRotate: 0 or 1. Default: 1.
  • CursorTrailRotate: 0 or 1. Default: 1.
  • CustomComboBurstSounds: comma-separated positive integers. Default: empty.
  • HitCircleOverlayAboveNumber: 0 or 1. Default: 1.
  • LayeredHitSounds: 0 or 1. Default: 1.
  • SliderBallFlip: 0 or 1. Default: 1.
  • SpinnerFadePlayfield: 0 or 1. Default: 0.
  • SpinnerFrequencyModulate: 0 or 1. Default: 1.
  • SpinnerNoBlink: 0 or 1. Default: 0.

[Colours]

  • Combo1: RGB. Default: 255,192,0.
  • Combo2: RGB. Default: 0,202,0.
  • Combo3: RGB. Default: 18,124,255.
  • Combo4: RGB. Default: 242,24,57.
  • Combo5: RGB. Default: empty.
  • Combo6: RGB. Default: empty.
  • Combo7: RGB. Default: empty.
  • Combo8: RGB. Default: empty.
  • InputOverlayText: RGB. Default: 0,0,0.
  • MenuGlow: RGB. Default: 0,78,155.
  • SliderBall: RGB. Default: 2,170,255.
  • SliderBorder: RGB. Default: 255,255,255.
  • SliderTrackOverride: RGB. Default: current combo color.
  • SongSelectActiveText: RGB. Default: 0,0,0.
  • SongSelectInactiveText: RGB. Default: 255,255,255.
  • SpinnerBackground: RGB. Default: 100,100,100.
  • StarBreakAdditive: RGB. Default: 255,182,193.

[Fonts]

  • HitCirclePrefix: text (prefix for hit circle numbers). Default: "default".
  • HitCircleOverlap: integer. Default: -2.
  • ScorePrefix: text (prefix for score numbers). Default: "score".
  • ScoreOverlap: integer. Default: 0.
  • ComboPrefix: text (prefix for combo numbers). Default: "score".
  • ComboOverlap: integer. Default: 0.

[CatchTheBeat]

  • HyperDash: RGB. Default: 255,0,0.
  • HyperDashFruit: RGB. Default: HyperDash value.
  • HyperDashAfterImage: RGB. Default: HyperDash value.

[Mania]

These properties can be specified per key count (1 to 18), with separate [Mania] sections for each Keys value. Properties include comma-separated lists for multi-column settings (e.g., for 4K, a list of 4 values).

  • Keys: integer (1-18). Required for each mania section.
  • ColumnStart: number. Default: varies by key count.
  • BarlineHeight: number. Default: 1.2.
  • WidthForNoteHeightScale: number. Default: 0.
  • ColumnWidth: comma-separated numbers. Default: 30 per column.
  • ColumnLineWidth: comma-separated numbers. Default: 0 (no lines).
  • JudgementLine: 0 or 1. Default: 0.
  • ColourColumnLine: RGB(a). Default: 255,255,255,255.
  • ColourBarline: RGB(a). Default: 255,255,255,200.
  • ColourJudgementLine: RGB. Default: 255,255,255.
  • ColourKeyWarning: RGB. Default: 0,0,0.
  • ColourHold: RGB(a). Default: 255,255,0,255.
  • ColourBreak: RGB. Default: 255,0,0.
  • ColumnLight: comma-separated RGB. Default: empty (no light).
  • KeyImage: comma-separated text (image prefixes). Default: empty.
  • NoteImage: comma-separated text (image prefixes). Default: empty.
  • StageLeft: text (image). Default: empty.
  • StageRight: text (image). Default: empty.
  • StageBottom: text (image). Default: empty.
  • StageHint: text (image). Default: empty.
  • StageLight: text (image). Default: empty.
  • LightingN: text (image). Default: empty.
  • LightingL: text (image). Default: empty.
  • WarningArrow: text (image). Default: empty.
  • Hit0: text (image). Default: empty.
  • Hit50: text (image). Default: empty.
  • Hit100: text (image). Default: empty.
  • Hit200: text (image). Default: empty.
  • Hit300: text (image). Default: empty.
  • Hit300g: text (image). Default: empty.
  • KeyFlipWhenUpsideDown: comma-separated 0 or 1. Default: 1 per key.
  • NoteFlipWhenUpsideDown: comma-separated 0 or 1. Default: 1 per key.
  • NoteBodyStyle: comma-separated integer (1-3). Default: 1 per key.
  • Colour: comma-separated RGB. Default: 0,0,0 per column.
  • ColourLight: comma-separated RGB. Default: 255,255,255 per column.
  • UpsideDown: 0 or 1. Default: 0.
  • LeftStage: text (image). Default: empty.
  • RightStage: text (image). Default: empty.
  • BottomStage: text (image). Default: empty.
  • LightPosition: comma-separated integers. Default: hit position.
  • LightFramePerSecond: positive integer. Default: AnimationFramerate value.
  • HitPosition: integer. Default: 402.
  • ScorePosition: integer. Default: 100.
  • ComboPosition: integer. Default: 110.
  • JudgementPosition: integer. Default: 400.
  • SpecialStyle: integer (0-2). Default: 0.
  • ComboBurstStyle: integer (1-3). Default: 1.
  • SplitStages: integer. Default: 0.
  • StageSeparation: integer. Default: 40.
  • SeparateScore: 0 or 1. Default: 1.
  • KeysUnderNotes: 0 or 1. Default: 0.
  • RatingUnderNotes: 0 or 1. Default: 0.
  • KeyWarningDim: integer (0-100). Default: 50.
  • MinColumnWidth: number. Default: 0.

The format may include additional asset files (e.g., .png, .wav) referenced by these properties, but the properties themselves are the INI keys.

3. Ghost blog embedded html javascript that allows a user to drag n drop a file of format .OSK and it will dump to screen all these properties

OSK Parser
Drag and drop .OSK file here

4. Python class that can open any file of format .OSK and decode read and write and print to console all the properties from the above list

import zipfile
from configparser import ConfigParser
import io
import sys

class OSKHandler:
    def __init__(self, filepath):
        self.filepath = filepath
        self.properties = {}
        self.read()

    def read(self):
        with zipfile.ZipFile(self.filepath, 'r') as z:
            ini_file = next((f for f in z.namelist() if f.lower().endsWith('skin.ini')), None)
            if ini_file:
                with z.open(ini_file) as f:
                    content = f.read().decode('utf-8')
                    config = ConfigParser(allow_no_value=True)
                    config.read_file(io.StringIO(content))
                    for section in config.sections():
                        self.properties[section] = dict(config[section])
            else:
                print("No skin.ini found")

    def print_properties(self):
        for section, props in self.properties.items():
            print(f"[{section}]")
            for key, value in props.items():
                print(f"{key}: {value}")
            print()

    def write(self, new_properties):
        self.properties.update(new_properties)
        with zipfile.ZipFile(self.filepath + '.new', 'w') as z_new:
            with zipfile.ZipFile(self.filepath, 'r') as z_old:
                for item in z_old.infolist():
                    if item.filename.lower().endswith('skin.ini'):
                        ini_content = self._generate_ini()
                        z_new.writestr(item, ini_content)
                    else:
                        z_new.writestr(item, z_old.read(item.filename))

    def _generate_ini(self):
        config = ConfigParser()
        for section, props in self.properties.items():
            config[section] = props
        with io.StringIO() as buf:
            config.write(buf)
            return buf.getvalue().encode('utf-8')

# Example usage:
# handler = OSKHandler('example.osk')
# handler.print_properties()
# handler.write({'General': {'Name': 'New Name'}})

5. Java class that can open any file of format .OSK and decode read and write and print to console all the properties from the above list

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

public class OSKHandler {
    private String filepath;
    private Map<String, Map<String, String>> properties = new HashMap<>();

    public OSKHandler(String filepath) {
        this.filepath = filepath;
        read();
    }

    public void read() {
        try (ZipFile z = new ZipFile(filepath)) {
            Optional<ZipEntry> iniEntry = z.stream()
                    .filter(e -> e.getName().toLowerCase().endsWith("skin.ini"))
                    .findFirst();
            if (iniEntry.isPresent()) {
                try (BufferedReader reader = new BufferedReader(new InputStreamReader(z.getInputStream(iniEntry.get())))) {
                    String line, section = "";
                    while ((line = reader.readLine()) != null) {
                        line = line.trim();
                        if (line.startsWith("[") && line.endsWith("]")) {
                            section = line.substring(1, line.length() - 1);
                            properties.put(section, new HashMap<>());
                        } else if (line.contains(":")) {
                            String[] parts = line.split(":", 2);
                            if (!section.isEmpty()) {
                                properties.get(section).put(parts[0].trim(), parts.length > 1 ? parts[1].trim() : "");
                            }
                        }
                    }
                }
            } else {
                System.out.println("No skin.ini found");
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public void printProperties() {
        for (Map.Entry<String, Map<String, String>> section : properties.entrySet()) {
            System.out.println("[" + section.getKey() + "]");
            for (Map.Entry<String, String> prop : section.getValue().entrySet()) {
                System.out.println(prop.getKey() + ": " + prop.getValue());
            }
            System.out.println();
        }
    }

    public void write(Map<String, Map<String, String>> newProperties) {
        properties.putAll(newProperties);
        try (ZipFile zOld = new ZipFile(filepath);
             ZipOutputStream zNew = new ZipOutputStream(new FileOutputStream(filepath + ".new"))) {
            for (Enumeration<? extends ZipEntry> entries = zOld.entries(); entries.hasMoreElements(); ) {
                ZipEntry entry = entries.nextElement();
                zNew.putNextEntry(new ZipEntry(entry.getName()));
                if (entry.getName().toLowerCase().endsWith("skin.ini")) {
                    byte[] iniBytes = generateIni().getBytes("UTF-8");
                    zNew.write(iniBytes, 0, iniBytes.length);
                } else {
                    try (InputStream is = zOld.getInputStream(entry)) {
                        byte[] buffer = new byte[1024];
                        int len;
                        while ((len = is.read(buffer)) > 0) {
                            zNew.write(buffer, 0, len);
                        }
                    }
                }
                zNew.closeEntry();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    private String generateIni() {
        StringBuilder sb = new StringBuilder();
        for (Map.Entry<String, Map<String, String>> section : properties.entrySet()) {
            sb.append("[").append(section.getKey()).append("]\n");
            for (Map.Entry<String, String> prop : section.getValue().entrySet()) {
                sb.append(prop.getKey()).append(": ").append(prop.getValue()).append("\n");
            }
            sb.append("\n");
        }
        return sb.toString();
    }

    // Example usage:
    // public static void main(String[] args) {
    //     OSKHandler handler = new OSKHandler("example.osk");
    //     handler.printProperties();
    //     Map<String, Map<String, String>> updates = new HashMap<>();
    //     Map<String, String> gen = new HashMap<>();
    //     gen.put("Name", "New Name");
    //     updates.put("General", gen);
    //     handler.write(updates);
    // }
}

6. Javascript class that can open any file of format .OSK and decode read and write and print to console all the properties from the above list

const fs = require('fs');
const JSZip = require('jszip');

class OSKHandler {
  constructor(filepath) {
    this.filepath = filepath;
    this.properties = {};
  }

  async read() {
    const data = fs.readFileSync(this.filepath);
    const zip = await JSZip.loadAsync(data);
    const iniFile = Object.keys(zip.files).find(name => name.toLowerCase().endsWith('skin.ini'));
    if (iniFile) {
      const content = await zip.file(iniFile).async('string');
      this.properties = this.parseIni(content);
    } else {
      console.log('No skin.ini found');
    }
  }

  printProperties() {
    console.log(this.properties);
  }

  parseIni(content) {
    const result = {};
    const lines = content.split(/\r?\n/);
    let section = '';
    lines.forEach(line => {
      line = line.trim();
      if (line.startsWith('[') && line.endsWith(']')) {
        section = line.slice(1, -1);
        result[section] = {};
      } else if (line.includes(':')) {
        const [key, value] = line.split(':').map(s => s.trim());
        if (section) {
          result[section][key] = value || '';
        }
      }
    });
    return result;
  }

  async write(newProperties) {
    Object.assign(this.properties, newProperties);
    const data = fs.readFileSync(this.filepath);
    const zip = await JSZip.loadAsync(data);
    const iniFile = Object.keys(zip.files).find(name => name.toLowerCase().endsWith('skin.ini'));
    if (iniFile) {
      zip.file(iniFile, this.generateIni());
    }
    const newContent = await zip.generateAsync({type: 'nodebuffer'});
    fs.writeFileSync(this.filepath + '.new', newContent);
  }

  generateIni() {
    let content = '';
    for (const [section, props] of Object.entries(this.properties)) {
      content += `[${section}]\n`;
      for (const [key, value] of Object.entries(props)) {
        content += `${key}: ${value}\n`;
      }
      content += '\n';
    }
    return content;
  }
}

// Example usage:
// const handler = new OSKHandler('example.osk');
// await handler.read();
// handler.printProperties();
// await handler.write({ General: { Name: 'New Name' } });

7. C class that can open any file of format .OSK and decode read and write and print to console all the properties from the above list

(Note: C does not have built-in classes like C++, so this is implemented as a struct with functions, akin to a class in object-oriented C. For ZIP handling, assumes minizip or similar library is linked; here, pseudocode for simplicity as native C ZIP parsing is complex without libraries.)

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <zip.h>  // Assume libzip or minizip is installed and linked

typedef struct {
    char* filepath;
    // Use a simple array of sections and key-value pairs for properties (limited to 100 sections, 100 props each)
    char sections[100][50];
    char keys[100][100][50];
    char values[100][100][256];
    int section_count;
    int prop_counts[100];
} OSKHandler;

void init_osk(OSKHandler* handler, const char* filepath) {
    handler->filepath = strdup(filepath);
    handler->section_count = 0;
    memset(handler->prop_counts, 0, sizeof(handler->prop_counts));
    read_osk(handler);
}

void read_osk(OSKHandler* handler) {
    struct zip* z = zip_open(handler->filepath, ZIP_RDONLY, NULL);
    if (z == NULL) {
        printf("Failed to open ZIP\n");
        return;
    }
    const char* ini_name = NULL;
    int num_entries = zip_get_num_entries(z, 0);
    for (int i = 0; i < num_entries; i++) {
        const char* name = zip_get_name(z, i, 0);
        if (strcasestr(name, "skin.ini")) {
            ini_name = name;
            break;
        }
    }
    if (ini_name) {
        struct zip_file* f = zip_fopen(z, ini_name, 0);
        if (f) {
            struct zip_stat st;
            zip_stat(z, ini_name, 0, &st);
            char* content = malloc(st.size + 1);
            zip_fread(f, content, st.size);
            content[st.size] = '\0';
            zip_fclose(f);
            parse_ini(handler, content);
            free(content);
        }
    } else {
        printf("No skin.ini found\n");
    }
    zip_close(z);
}

void parse_ini(OSKHandler* handler, const char* content) {
    char line[512];
    int sec_idx = -1;
    const char* ptr = content;
    while (*ptr) {
        int len = 0;
        while (*ptr && *ptr != '\n' && len < 511) {
            line[len++] = *ptr++;
        }
        if (*ptr == '\n') ptr++;
        line[len] = '\0';
        char* trim = strtok(line, " \t\r\n");
        if (trim) {
            if (trim[0] == '[' && trim[strlen(trim)-1] == ']') {
                sec_idx++;
                strncpy(handler->sections[sec_idx], trim + 1, strlen(trim) - 2);
                handler->sections[sec_idx][strlen(trim) - 2] = '\0';
                handler->section_count = sec_idx + 1;
            } else if (strchr(trim, ':')) {
                if (sec_idx >= 0) {
                    char* key = strtok(trim, ":");
                    char* val = strtok(NULL, "\r\n");
                    strncpy(handler->keys[sec_idx][handler->prop_counts[sec_idx]], key, 49);
                    strncpy(handler->values[sec_idx][handler->prop_counts[sec_idx]], val ? val : "", 255);
                    handler->prop_counts[sec_idx]++;
                }
            }
        }
    }
}

void print_properties(OSKHandler* handler) {
    for (int i = 0; i < handler->section_count; i++) {
        printf("[%s]\n", handler->sections[i]);
        for (int j = 0; j < handler->prop_counts[i]; j++) {
            printf("%s: %s\n", handler->keys[i][j], handler->values[i][j]);
        }
        printf("\n");
    }
}

// Write function (simplified: recreates ZIP with updated ini)
void write_osk(OSKHandler* handler, const char* new_section, const char* new_key, const char* new_value) {
    // Find and update property (simplified for one update)
    for (int i = 0; i < handler->section_count; i++) {
        if (strcmp(handler->sections[i], new_section) == 0) {
            for (int j = 0; j < handler->prop_counts[i]; j++) {
                if (strcmp(handler->keys[i][j], new_key) == 0) {
                    strncpy(handler->values[i][j], new_value, 255);
                    break;
                }
            }
            break;
        }
    }
    // Rebuild ZIP (open old, copy all except ini, add new ini)
    struct zip* z_old = zip_open(handler->filepath, ZIP_RDONLY, NULL);
    char new_path[256];
    snprintf(new_path, sizeof(new_path), "%s.new", handler->filepath);
    struct zip* z_new = zip_open(new_path, ZIP_CREATE, NULL);
    int num_entries = zip_get_num_entries(z_old, 0);
    for (int i = 0; i < num_entries; i++) {
        const char* name = zip_get_name(z_old, i, 0);
        if (strcasestr(name, "skin.ini")) {
            char* new_ini = generate_ini(handler);
            zip_source* src = zip_source_buffer(z_new, new_ini, strlen(new_ini), 1);
            zip_add(z_new, name, src);
        } else {
            zip_source* src = zip_source_zip(z_new, z_old, i, 0, 0, -1);
            zip_add(z_new, name, src);
        }
    }
    zip_close(z_new);
    zip_close(z_old);
}

char* generate_ini(OSKHandler* handler) {
    char* buf = malloc(1024 * 1024); // Assume enough
    char* ptr = buf;
    for (int i = 0; i < handler->section_count; i++) {
        ptr += sprintf(ptr, "[%s]\n", handler->sections[i]);
        for (int j = 0; j < handler->prop_counts[i]; j++) {
            ptr += sprintf(ptr, "%s: %s\n", handler->keys[i][j], handler->values[i][j]);
        }
        ptr += sprintf(ptr, "\n");
    }
    return buf;
}

void free_osk(OSKHandler* handler) {
    free(handler->filepath);
}

// Example usage:
// int main() {
//     OSKHandler handler;
//     init_osk(&handler, "example.osk");
//     print_properties(&handler);
//     write_osk(&handler, "General", "Name", "New Name");
//     free_osk(&handler);
//     return 0;
// }