Task .346: .KSH File Format

Task .346: .KSH File Format

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

The .KSH file format is a text-based format used for rhythm game charts in K-Shoot MANIA (a Sound Voltex simulator). It is not a binary format but a structured text file with specific line types and options. The phrase "intrinsic to its file system" appears to refer to the intrinsic properties of the file format's structure (e.g., options, lines, and parameters). Based on the specifications, the properties are the option parameters (key=value pairs), grouped into header options (before the first bar line) and body options (after). Other structural elements include chart lines, bar lines, definition lines, and comment lines.

Header Options (Metadata Properties)

  • title: Song title (string, default: "").
  • title_img: Image filename for the song title (string, default: "").
  • artist: Artist name (string, default: "").
  • artist_img: Image filename for the artist name (string, default: "").
  • effect: Chart author name (string, default: "").
  • jacket: Jacket image filename or preset (string, default: "").
  • illustrator: Jacket image author name (string, default: "").
  • difficulty: Chart difficulty (string, values: "light", "challenge", "extended", "infinite"; default: "light").
  • level: Chart level (integer, 1-20; default: "1").
  • t: Tempo (BPM) (float or string range; default: "120").
  • to: Standard tempo for Hi-speed (float; default: "0").
  • beat: Time signature (string, e.g., "4/4"; default: "4/4").
  • m: Song filename(s) (string, semicolon-separated; default: "").
  • mvol: Song volume percentage (integer; default: "100").
  • o: Song offset in milliseconds (integer; default: "0").
  • bg: Background image name or preset (string, semicolon-separated; default: "desert").
  • layer: Animation layer name or preset (string; default: "arrow").
  • po: Preview offset in milliseconds (integer; default: "0").
  • plength: Preview length in milliseconds (integer; default: "0").
  • ver: Format version (integer, e.g., 170; inferred from spec, no explicit default).

Body Options (Chart-Specific Properties)

  • t: Tempo change (float).
  • beat: Time signature change (string, e.g., "4/4").
  • chokkakuvol: Laser slam volume percentage (integer, 0-100).
  • chokkakuse: Laser slam sound type (string, "up", "down", "swing", "mute").
  • pfiltergain: Peaking filter gain for laser effects (integer, 0-100; default: 50).
  • stop: Pause length (integer, in 192nds per measure).
  • tilt: Lane tilt type or value (string or float, e.g., "normal", "bigger").
  • zoom_top: Camera control for top lane (integer).
  • zoom_bottom: Camera control for bottom lane (integer).
  • zoom_side: Camera control for side position (integer).
  • center_split: Middle highway margin (integer).
  • laserrange_l: Left laser range (string, "2x" or "1x").
  • laserrange_r: Right laser range (string, "2x" or "1x").
  • fx-l: Left FX audio effect (string, e.g., "Retrigger;8").
  • fx-r: Right FX audio effect (string, e.g., "Gate;4").
  • fx-l_param1: Legacy parameter for left FX (integer).
  • fx-r_param1: Legacy parameter for right FX (integer).
  • fx-l_se: Left FX key sound (string, ";").
  • fx-r_se: Right FX key sound (string, ";").

Other Structural Properties

  • Chart Lines: Note data format "||" (e.g., "0110|22|--").
  • Bar Lines: Measure separator ("--").
  • Definition Lines: User-defined effects (e.g., "#define_fx_l  ").
  • Comment Lines: Comments starting with "//".
  • Encoding: UTF-8 with BOM recommended.
  • Line Spin: Optional spin in chart lines (e.g., "@(192" for clockwise spin of 192 duration).

These properties define the file's structure and content.

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

Here is a complete HTML page with embedded JavaScript for a simple drag-and-drop interface. Drop a .KSH file onto the designated area, and it will parse the text, extract all header and body options (properties), and display them on the screen. It handles basic parsing of lines, ignoring comments and empty lines, and separates header and body based on the first bar line ("--").

KSH File Property Dumper
Drag and drop .KSH file here

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

Here is a Python class KSHFile. It can open a .KSH file, parse (decode) its content, read properties, print them to console, and write a new or modified .KSH file. Parsing handles header/body separation, options, chart lines, definitions, and comments.

class KSHFile:
    def __init__(self, filepath=None):
        self.header_props = {}
        self.body_measures = []  # List of dicts for each measure's props and chart lines
        self.comments = []  # List of comment lines
        self.definitions = []  # List of definition lines
        if filepath:
            self.read(filepath)

    def read(self, filepath):
        with open(filepath, 'r', encoding='utf-8-sig') as f:
            lines = f.readlines()
        in_body = false
        current_measure = {}
        current_chart_lines = []
        for line in lines:
            line = line.strip()
            if line == '' :
                continue
            if line.starts_with('//'):
                self.comments.append(line)
                continue
            if line == '--':
                if current_measure or current_chart_lines:
                    current_measure['chart_lines'] = current_chart_lines
                    self.body_measures.append(current_measure)
                current_measure = {}
                current_chart_lines = []
                in_body = True
                continue
            if line.starts_with('#define'):
                self.definitions.append(line)
                continue
            match = re.match(r'^([^=]+)=(.*)$', line)
            if match:
                key = match.group(1).strip()
                value = match.group(2).strip()
                if in_body:
                    current_measure[key] = value
                else:
                    self.header_props[key] = value
            else:
                # Assume chart line
                if in_body:
                    current_chart_lines.append(line)
        if current_measure or current_chart_lines:
            current_measure['chart_lines'] = current_chart_lines
            self.body_measures.append(current_measure)

    def print_properties(self):
        print("Header Properties:")
        for key, value in self.header_props.items():
            print(f"{key}: {value}")
        print("\nBody Measures:")
        for idx, measure in enumerate(self.body_measures, 1):
            print(f"Measure {idx}:")
            for key, value in measure.items():
                if key == 'chart_lines':
                    print("  chart_lines:")
                    for cl in value:
                        print(f"    {cl}")
                else:
                    print(f"  {key}: {value}")
        if self.definitions:
            print("\nDefinitions:")
            for def_line in self.definitions:
                print(def_line)
        if self.comments:
            print("\nComments:")
            for comment in self.comments:
                print(comment)

    def write(self, filepath):
        with open(filepath, 'w', encoding='utf-8-sig') as f:
            # Write header
            for key, value in self.header_props.items():
                f.write(f"{key}={value}\n")
            # Write definitions if in header (assumed)
            for def_line in self.definitions:
                f.write(f"{def_line}\n")
            # Write body
            for measure in self.body_measures:
                f.write("--\n")
                for key, value in measure.items():
                    if key == 'chart_lines':
                        for cl in value:
                            f.write(f"{cl}\n")
                    else:
                        f.write(f"{key}={value}\n")
            # Comments not written back, as they are ignored in spec

# Example usage
if __name__ == "__main__":
    ksh = KSHFile("example.ksh")
    ksh.print_properties()
    ksh.write("output.ksh")

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

Here is a Java class KSHFile. It uses Scanner for reading and PrintWriter for writing. Parsing is similar to the Python version.

import java.io.File;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Scanner;

public class KSHFile {
    private Map<String, String> headerProps = new HashMap<>();
    private List<Map<String, Object>> bodyMeasures = new ArrayList<>(); // Each measure: props and chart_lines (List<String>)
    private List<String> comments = new ArrayList<>();
    private List<String> definitions = new ArrayList<>();

    public KSHFile(String filepath) {
        if (filepath != null) {
            read(filepath);
        }
    }

    public void read(String filepath) {
        try (Scanner scanner = new Scanner(new File(filepath), "UTF-8")) {
            boolean inBody = false;
            Map<String, Object> currentMeasure = new HashMap<>();
            List<String> currentChartLines = new ArrayList<>();
            while (scanner.hasNextLine()) {
                String line = scanner.nextLine().trim();
                if (line.isEmpty()) continue;
                if (line.startsWith("//")) {
                    comments.add(line);
                    continue;
                }
                if (line.equals("--")) {
                    if (!currentMeasure.isEmpty() || !currentChartLines.isEmpty()) {
                        currentMeasure.put("chart_lines", new ArrayList<>(currentChartLines));
                        bodyMeasures.add(currentMeasure);
                    }
                    currentMeasure = new HashMap<>();
                    currentChartLines.clear();
                    inBody = true;
                    continue;
                }
                if (line.startsWith("#define")) {
                    definitions.add(line);
                    continue;
                }
                String[] parts = line.split("=", 2);
                if (parts.length == 2) {
                    String key = parts[0].trim();
                    String value = parts[1].trim();
                    if (inBody) {
                        currentMeasure.put(key, value);
                    } else {
                        headerProps.put(key, value);
                    }
                } else {
                    if (inBody) {
                        currentChartLines.add(line);
                    }
                }
            }
            if (!currentMeasure.isEmpty() || !currentChartLines.isEmpty()) {
                currentMeasure.put("chart_lines", new ArrayList<>(currentChartLines));
                bodyMeasures.add(currentMeasure);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public void printProperties() {
        System.out.println("Header Properties:");
        for (Map.Entry<String, String> entry : headerProps.entrySet()) {
            System.out.println(entry.getKey() + ": " + entry.getValue());
        }
        System.out.println("\nBody Measures:");
        for (int i = 0; i < bodyMeasures.size(); i++) {
            System.out.println("Measure " + (i + 1) + ":");
            Map<String, Object> measure = bodyMeasures.get(i);
            for (Map.Entry<String, Object> entry : measure.entrySet()) {
                if (entry.getKey().equals("chart_lines")) {
                    System.out.println("  chart_lines:");
                    List<String> lines = (List<String>) entry.getValue();
                    for (String cl : lines) {
                        System.out.println("    " + cl);
                    }
                } else {
                    System.out.println("  " + entry.getKey() + ": " + entry.getValue());
                }
            }
        }
        if (!definitions.isEmpty()) {
            System.out.println("\nDefinitions:");
            for (String def : definitions) {
                System.out.println(def);
            }
        }
        if (!comments.isEmpty()) {
            System.out.println("\nComments:");
            for (String comment : comments) {
                System.out.println(comment);
            }
        }
    }

    public void write(String filepath) {
        try (PrintWriter writer = new PrintWriter(filepath, "UTF-8")) {
            // Header
            for (Map.Entry<String, String> entry : headerProps.entrySet()) {
                writer.println(entry.getKey() + "=" + entry.getValue());
            }
            // Definitions (assumed in header)
            for (String def : definitions) {
                writer.println(def);
            }
            // Body
            for (Map<String, Object> measure : bodyMeasures) {
                writer.println("--");
                for (Map.Entry<String, Object> entry : measure.entrySet()) {
                    if (entry.getKey().equals("chart_lines")) {
                        List<String> lines = (List<String>) entry.getValue();
                        for (String cl : lines) {
                            writer.println(cl);
                        }
                    } else {
                        writer.println(entry.getKey() + "=" + entry.getValue());
                    }
                }
            }
            // Comments not written
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
        KSHFile ksh = new KSHFile("example.ksh");
        ksh.printProperties();
        ksh.write("output.ksh");
    }
}

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

Here is a JavaScript class KSHFile. It uses FileReader for reading (browser context) and console.log for printing. For writing, it generates a blob and triggers a download. Assume running in a browser.

class KSHFile {
    constructor(filepath = null) {
        this.headerProps = {};
        this.bodyMeasures = []; // Array of objects for each measure
        this.comments = [];
        this.definitions = [];
        if (filepath) {
            this.read(filepath);
        }
    }

    read(file) {
        const reader = new FileReader();
        reader.onload = (event) => {
            const content = event.target.result;
            const lines = content.split('\n').map(line => line.trim());
            let inBody = false;
            let currentMeasure = {};
            let currentChartLines = [];
            lines.forEach(line => {
                if (line === '' ) return;
                if (line.startsWith('//')) {
                    this.comments.push(line);
                    return;
                }
                if (line === '--') {
                    if (Object.keys(currentMeasure).length > 0 || currentChartLines.length > 0) {
                        currentMeasure.chart_lines = [...currentChartLines];
                        this.bodyMeasures.push(currentMeasure);
                    }
                    currentMeasure = {};
                    currentChartLines = [];
                    inBody = true;
                    return;
                }
                if (line.startsWith('#define')) {
                    this.definitions.push(line);
                    return;
                }
                const match = line.match(/^([^=]+)=(.*)$/);
                if (match) {
                    const key = match[1].trim();
                    const value = match[2].trim();
                    if (inBody) {
                        currentMeasure[key] = value;
                    } else {
                        this.headerProps[key] = value;
                    }
                } else {
                    if (inBody) {
                        currentChartLines.push(line);
                    }
                }
            });
            if (Object.keys(currentMeasure).length > 0 || currentChartLines.length > 0) {
                currentMeasure.chart_lines = [...currentChartLines];
                this.bodyMeasures.push(currentMeasure);
            }
        };
        reader.readAsText(file);
    }

    printProperties() {
        console.log('Header Properties:');
        for (const [key, value] of Object.entries(this.headerProps)) {
            console.log(`${key}: ${value}`);
        }
        console.log('\nBody Measures:');
        this.bodyMeasures.forEach((measure, index) => {
            console.log(`Measure ${index + 1}:`);
            for (const [key, value] of Object.entries(measure)) {
                if (key === 'chart_lines') {
                    console.log('  chart_lines:');
                    value.forEach(cl => console.log(`    ${cl}`));
                } else {
                    console.log(`  ${key}: ${value}`);
                }
            }
        });
        if (this.definitions.length > 0) {
            console.log('\nDefinitions:');
            this.definitions.forEach(def => console.log(def));
        }
        if (this.comments.length > 0) {
            console.log('\nComments:');
            this.comments.forEach(comment => console.log(comment));
        }
    }

    write(filename = 'output.ksh') {
        let content = '';
        // Header
        for (const [key, value] of Object.entries(this.headerProps)) {
            content += `${key}=${value}\n`;
        }
        // Definitions
        this.definitions.forEach(def => {
            content += `${def}\n`;
        });
        // Body
        this.bodyMeasures.forEach(measure => {
            content += '--\n';
            for (const [key, value] of Object.entries(measure)) {
                if (key === 'chart_lines') {
                    value.forEach(cl => {
                        content += `${cl}\n`;
                    });
                } else {
                    content += `${key}=${value}\n`;
                }
            }
        });
        // Download
        const blob = new Blob([content], { type: 'text/plain' });
        const url = URL.createObjectURL(blob);
        const a = document.createElement('a');
        a.href = url;
        a.download = filename;
        a.click();
        URL.revokeObjectURL(url);
    }
}

// Example usage (in browser, with file input)
const input = document.createElement('input');
input.type = 'file';
input.onchange = (e) => {
    const file = e.target.files[0];
    if (file.name.endsWith('.ksh')) {
        const ksh = new KSHFile();
        ksh.read(file);
        setTimeout(() => { // Wait for onload
            ksh.printProperties();
            ksh.write();
        }, 500);
    }
};
input.click();

7. Write a c class that can open any file of format .KSH and decode read and write and print to console all the properties from the above list

Here is a C++ class KSHFile. It uses fstream for reading/writing and std::cout for printing. Parsing is line-based using std::getline.

#include <iostream>
#include <fstream>
#include <string>
#include <vector>
#include <map>
#include <sstream>

class KSHFile {
private:
    std::map<std::string, std::string> headerProps;
    std::vector<std::map<std::string, std::string>> bodyMeasures; // Each measure's props
    std::vector<std::string> bodyChartLinesPerMeasure; // Parallel vector for chart lines (simplified)
    std::vector<std::string> comments;
    std::vector<std::string> definitions;

public:
    KSHFile(const std::string& filepath = "") {
        if (!filepath.empty()) {
            read(filepath);
        }
    }

    void read(const std::string& filepath) {
        std::ifstream file(filepath, std::ios::in | std::ios::binary); // For BOM handling
        if (!file) {
            std::cerr << "Failed to open file." << std::endl;
            return;
        }
        // Skip BOM if present
        char bom[3];
        file.read(bom, 3);
        if (!(bom[0] == (char)0xEF && bom[1] == (char)0xBB && bom[2] == (char)0xBF)) {
            file.seekg(0);
        }
        std::string line;
        bool inBody = false;
        std::map<std::string, std::string> currentMeasure;
        std::vector<std::string> currentChartLines;
        int measureIndex = -1;
        while (std::getline(file, line)) {
            line.erase(std::remove(line.begin(), line.end(), '\r'), line.end()); // Trim CR
            if (line.empty()) continue;
            if (line.substr(0, 2) == "//") {
                comments.push_back(line);
                continue;
            }
            if (line == "--") {
                if (!currentMeasure.empty() || !currentChartLines.empty()) {
                    bodyMeasures.push_back(currentMeasure);
                    bodyChartLinesPerMeasure.push_back(currentChartLines);
                }
                currentMeasure.clear();
                currentChartLines.clear();
                inBody = true;
                measureIndex++;
                continue;
            }
            if (line.substr(0, 7) == "#define") {
                definitions.push_back(line);
                continue;
            }
            size_t eqPos = line.find('=');
            if (eqPos != std::string::npos) {
                std::string key = line.substr(0, eqPos);
                std::string value = line.substr(eqPos + 1);
                // Trim spaces
                key.erase(0, key.find_first_not_of(" \t"));
                key.erase(key.find_last_not_of(" \t") + 1);
                value.erase(0, value.find_first_not_of(" \t"));
                value.erase(value.find_last_not_of(" \t") + 1);
                if (inBody) {
                    currentMeasure[key] = value;
                } else {
                    headerProps[key] = value;
                }
            } else {
                if (inBody) {
                    currentChartLines.push_back(line);
                }
            }
        }
        if (!currentMeasure.empty() || !currentChartLines.empty()) {
            bodyMeasures.push_back(currentMeasure);
            bodyChartLinesPerMeasure.push_back(currentChartLines);
        }
    }

    void printProperties() {
        std::cout << "Header Properties:" << std::endl;
        for (const auto& pair : headerProps) {
            std::cout << pair.first << ": " << pair.second << std::endl;
        }
        std::cout << "\nBody Measures:" << std::endl;
        for (size_t i = 0; i < bodyMeasures.size(); ++i) {
            std::cout << "Measure " << (i + 1) << ":" << std::endl;
            for (const auto& pair : bodyMeasures[i]) {
                std::cout << "  " << pair.first << ": " << pair.second << std::endl;
            }
            std::cout << "  chart_lines:" << std::endl;
            for (const auto& cl : bodyChartLinesPerMeasure[i]) {
                std::cout << "    " << cl << std::endl;
            }
        }
        if (!definitions.empty()) {
            std::cout << "\nDefinitions:" << std::endl;
            for (const auto& def : definitions) {
                std::cout << def << std::endl;
            }
        }
        if (!comments.empty()) {
            std::cout << "\nComments:" << std::endl;
            for (const auto& comment : comments) {
                std::cout << comment << std::endl;
            }
        }
    }

    void write(const std::string& filepath) {
        std::ofstream file(filepath, std::ios::out | std::ios::binary);
        if (!file) {
            std::cerr << "Failed to write file." << std::endl;
            return;
        }
        // Write BOM
        file << (char)0xEF << (char)0xBB << (char)0xBF;
        // Header
        for (const auto& pair : headerProps) {
            file << pair.first << "=" << pair.second << "\n";
        }
        // Definitions
        for (const auto& def : definitions) {
            file << def << "\n";
        }
        // Body
        for (size_t i = 0; i < bodyMeasures.size(); ++i) {
            file << "--\n";
            for (const auto& pair : bodyMeasures[i]) {
                file << pair.first << "=" << pair.second << "\n";
            }
            for (const auto& cl : bodyChartLinesPerMeasure[i]) {
                file << cl << "\n";
            }
        }
        // Comments not written
    }
};

int main() {
    KSHFile ksh("example.ksh");
    ksh.printProperties();
    ksh.write("output.ksh");
    return 0;
}