Task 111: .CRAFT File Format

Task 111: .CRAFT File Format

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

The .CRAFT file format is used in Kerbal Space Program (KSP) to store spacecraft designs. It is a plain text file in a hierarchical, key-value format similar to INI or ConfigNode structure, with top-level properties describing the overall vessel and nested "PART" sections for individual components. Properties are intrinsic to the file's structure and include metadata, vessel info, part positions, attachments, staging, modules, resources, events, and actions. Based on format specifications from KSP documentation, forums, and parsers, here is a comprehensive list of properties:

Top-Level Properties (Vessel Metadata)

  • ship: The name of the ship/vessel.
  • version: The KSP version the file was created in (e.g., "1.12.5").
  • description: A text description of the craft.
  • type: The editor type (e.g., "VAB" for Vehicle Assembly Building or "SPH" for Spaceplane Hangar).
  • size: The overall dimensions of the craft (e.g., "x,y,z" in meters).
  • persistentId: A unique identifier for the vessel.
  • rot: Rotation quaternion for the entire vessel (e.g., "0,0,0,1").
  • missionID: Identifier for associated mission (optional).
  • repeatGrounding: Boolean flag for grounding behavior (optional, often false).
  • PART: Nested blocks (one or more) describing individual parts.

Part-Level Properties (Within Each PART { ... } Block)

  • name: The base name of the part (from part.cfg, e.g., "fuelTank").
  • partName: Explicit part type (often "Part").
  • persistentId: Unique ID for this part instance.
  • uid: Unsigned integer ID (often a large number like 4294967295, decrementing randomly).
  • parent: UID of the parent part this is attached to.
  • position: Position coordinates (e.g., "x,y,z" in meters relative to root).
  • rotation: Rotation quaternion (e.g., "x,y,z,w").
  • mirror: Mirroring vector (e.g., "1,1,1").
  • symMethod: Symmetry method (e.g., "Radial").
  • istg: Inverse stage number (highest activates first in staging).
  • resPriority: Resource priority (integer, often 0).
  • dstg: Decoupler stage number.
  • sqor: Staging order-related value (often matches istg or -1).
  • sepI: Separation index.
  • sidx: Stage activation index (starts at 0, -1 for non-staged parts like tanks).
  • attm: Attachment mode (0 for stacked, 1 for radial/surface).
  • sameVesselCollision: Boolean for collision detection within vessel.
  • sym: List of symmetric part UIDs (for symmetry groups, n-way linked list).
  • srfN: Surface attachment info (e.g., "srfAttach,parentUID,-1").
  • attN: Node attachments (e.g., "top,childUID,-1" or "bottom,childUID,-1").
  • mass: Mass of the part.
  • temp: Internal temperature.
  • tempExt: External temperature.
  • tempExtUnexp: Unexposed external temperature.
  • staticPressureAtm: Atmospheric pressure.
  • expt: Experiment state (optional).
  • state: Part state (e.g., 0).
  • PreFailState: Pre-failure state (boolean).
  • attached: Attachment status (true/false).
  • autostrutMode: Autostrut mode (e.g., "Off", "Root", "Heaviest").
  • rigidAttachment: Rigid attachment flag (true/false).
  • flag: Associated flag URL or ID.
  • rTrf: Transform reference (e.g., part name).
  • modCost: Modification cost adjustment.
  • modMass: Modification mass adjustment.
  • modSize: Modification size vector (e.g., "0,0,0").
  • link: One-way link to child/attached parts (linked list, no cycles).

Sub-Properties Within Parts

  • EVENTS { ... }: Nested block for events (e.g., MakeReference { guiActive = True, guiName = Make Reference, active = True }).
  • Common sub-keys: guiActive, guiActiveUncommand, guiActiveUnfocused, guiIcon, guiName, category, guiActiveEditor, externalToEVAOnly, active, unfocusedRange.
  • ACTIONS { ... }: Nested block for actions (e.g., OnAction { active = True }).
  • Common sub-keys: actionName, active.
  • PARTDATA { ... }: Optional nested data for part-specific info (rare).
  • MODULE { ... }: Nested blocks (zero or more) for part modules (e.g., engines, wheels).
  • name: Module name (e.g., "ModuleEngines").
  • isEnabled: Boolean.
  • Module-specific keys (vary by module, e.g., stagingEnabled, throttleLocked, Events { ... }, Actions { ... }).
  • RESOURCE { ... }: Nested blocks for resources (e.g., fuel).
  • name: Resource type (e.g., "LiquidFuel").
  • amount: Current amount.
  • maxAmount: Maximum amount.
  • flowState: Boolean.
  • isTweakable: Boolean.
  • hideFlow: Boolean.
  • isVisible: Boolean.
  • flowMode: Flow mode (e.g., "All_Vessel").

These properties define the file's structure and are parsed hierarchically. The format is human-readable but requires careful handling of nesting and quaternions for positions/rotations.

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

Here's an HTML snippet with embedded JavaScript that can be embedded in a Ghost blog post. It creates a drag-and-drop area where users can drop a .CRAFT file. The script reads the file, parses it into a tree structure (top-level and parts), and dumps all properties to the screen in a readable format.

Drag and drop a .CRAFT file here


4. Python class for .CRAFT file handling

import re
import json  # For pretty-printing

class CraftFileHandler:
    def __init__(self, filepath):
        self.filepath = filepath
        self.data = {'topLevel': {}, 'parts': []}

    def read(self):
        with open(self.filepath, 'r') as f:
            content = f.read()
        lines = content.split('\n')
        current_part = None
        current_sub = None
        in_block = False
        block_type = ''

        for line in lines:
            line = line.strip()
            if not line:
                continue
            if line == 'PART':
                current_part = {}
                self.data['parts'].append(current_part)
                in_block = True
                block_type = 'PART'
            elif re.match(r'^(EVENTS|ACTIONS|MODULE|RESOURCE|PARTDATA)$', line):
                block_type = line
                current_sub = {}
                if current_part:
                    current_part[block_type.lower()] = current_sub
                in_block = True
            elif line == '{':
                continue  # Block start
            elif line == '}':
                in_block = False
                current_sub = None
                if block_type == 'PART':
                    current_part = None
                block_type = ''
            elif in_block:
                if '=' in line:
                    key, value = [s.strip() for s in line.split('=', 1)]
                    if current_sub:
                        current_sub[key] = value
                    elif current_part:
                        current_part[key] = value
            else:
                if '=' in line:
                    key, value = [s.strip() for s in line.split('=', 1)]
                    self.data['topLevel'][key] = value

    def print_properties(self):
        print(json.dumps(self.data, indent=4))

    def write(self, output_path=None):
        if not output_path:
            output_path = self.filepath
        with open(output_path, 'w') as f:
            for key, value in self.data['topLevel'].items():
                f.write(f"{key} = {value}\n")
            for part in self.data['parts']:
                f.write("PART\n{\n")
                for key, value in part.items():
                    if isinstance(value, dict):
                        f.write(f"{key.upper()}\n{{\n")
                        for sub_key, sub_value in value.items():
                            f.write(f"    {sub_key} = {sub_value}\n")
                        f.write("}\n")
                    else:
                        f.write(f"    {key} = {value}\n")
                f.write("}\n")

# Example usage:
# handler = CraftFileHandler('example.craft')
# handler.read()
# handler.print_properties()
# handler.write('modified.craft')

5. Java class for .CRAFT file handling

import java.io.*;
import java.util.*;
import com.google.gson.Gson;  // Assume Gson for pretty-printing (add dependency if needed)

public class CraftFileHandler {
    private String filepath;
    private Map<String, Object> data = new HashMap<>();

    public CraftFileHandler(String filepath) {
        this.filepath = filepath;
        data.put("topLevel", new HashMap<String, String>());
        data.put("parts", new ArrayList<Map<String, Object>>());
    }

    @SuppressWarnings("unchecked")
    public void read() throws IOException {
        try (BufferedReader reader = new BufferedReader(new FileReader(filepath))) {
            String line;
            Map<String, String> topLevel = (Map<String, String>) data.get("topLevel");
            List<Map<String, Object>> parts = (List<Map<String, Object>>) data.get("parts");
            Map<String, Object> currentPart = null;
            Map<String, String> currentSub = null;
            boolean inBlock = false;
            String blockType = "";

            while ((line = reader.readLine()) != null) {
                line = line.trim();
                if (line.isEmpty()) continue;
                if (line.equals("PART")) {
                    currentPart = new HashMap<>();
                    parts.add(currentPart);
                    inBlock = true;
                    blockType = "PART";
                } else if (line.matches("^(EVENTS|ACTIONS|MODULE|RESOURCE|PARTDATA)$")) {
                    blockType = line;
                    currentSub = new HashMap<>();
                    if (currentPart != null) currentPart.put(blockType.toLowerCase(), currentSub);
                    inBlock = true;
                } else if (line.equals("{")) {
                    continue;
                } else if (line.equals("}")) {
                    inBlock = false;
                    currentSub = null;
                    if (blockType.equals("PART")) currentPart = null;
                    blockType = "";
                } else if (inBlock && line.contains("=")) {
                    String[] split = line.split("=", 2);
                    String key = split[0].trim();
                    String value = split[1].trim();
                    if (currentSub != null) {
                        currentSub.put(key, value);
                    } else if (currentPart != null) {
                        currentPart.put(key, value);
                    }
                } else if (line.contains("=")) {
                    String[] split = line.split("=", 2);
                    String key = split[0].trim();
                    String value = split[1].trim();
                    topLevel.put(key, value);
                }
            }
        }
    }

    public void printProperties() {
        Gson gson = new Gson();
        System.out.println(gson.toJson(data));
    }

    public void write(String outputPath) throws IOException {
        if (outputPath == null) outputPath = filepath;
        try (BufferedWriter writer = new BufferedWriter(new FileWriter(outputPath))) {
            @SuppressWarnings("unchecked")
            Map<String, String> topLevel = (Map<String, String>) data.get("topLevel");
            for (Map.Entry<String, String> entry : topLevel.entrySet()) {
                writer.write(entry.getKey() + " = " + entry.getValue() + "\n");
            }
            @SuppressWarnings("unchecked")
            List<Map<String, Object>> parts = (List<Map<String, Object>>) data.get("parts");
            for (Map<String, Object> part : parts) {
                writer.write("PART\n{\n");
                for (Map.Entry<String, Object> entry : part.entrySet()) {
                    if (entry.getValue() instanceof Map) {
                        writer.write(entry.getKey().toUpperCase() + "\n{\n");
                        @SuppressWarnings("unchecked")
                        Map<String, String> sub = (Map<String, String>) entry.getValue();
                        for (Map.Entry<String, String> subEntry : sub.entrySet()) {
                            writer.write("    " + subEntry.getKey() + " = " + subEntry.getValue() + "\n");
                        }
                        writer.write("}\n");
                    } else {
                        writer.write("    " + entry.getKey() + " = " + entry.getValue() + "\n");
                    }
                }
                writer.write("}\n");
            }
        }
    }

    // Example usage:
    // public static void main(String[] args) throws IOException {
    //     CraftFileHandler handler = new CraftFileHandler("example.craft");
    //     handler.read();
    //     handler.printProperties();
    //     handler.write("modified.craft");
    // }
}

6. JavaScript class for .CRAFT file handling

const fs = require('fs');  // For Node.js file I/O

class CraftFileHandler {
  constructor(filepath) {
    this.filepath = filepath;
    this.data = { topLevel: {}, parts: [] };
  }

  read() {
    const content = fs.readFileSync(this.filepath, 'utf8');
    const lines = content.split('\n').map(line => line.trim());
    let currentPart = null;
    let currentSub = null;
    let inBlock = false;
    let blockType = '';

    lines.forEach(line => {
      if (!line) return;
      if (line === 'PART') {
        currentPart = {};
        this.data.parts.push(currentPart);
        inBlock = true;
        blockType = 'PART';
      } else if (/^(EVENTS|ACTIONS|MODULE|RESOURCE|PARTDATA)$/.test(line)) {
        blockType = line;
        currentSub = {};
        if (currentPart) currentPart[blockType.toLowerCase()] = currentSub;
        inBlock = true;
      } else if (line === '{') {
        return;
      } else if (line === '}') {
        inBlock = false;
        currentSub = null;
        if (blockType === 'PART') currentPart = null;
        blockType = '';
      } else if (inBlock && line.includes('=')) {
        const [key, value] = line.split('=').map(s => s.trim());
        if (currentSub) {
          currentSub[key] = value;
        } else if (currentPart) {
          currentPart[key] = value;
        }
      } else if (line.includes('=')) {
        const [key, value] = line.split('=').map(s => s.trim());
        this.data.topLevel[key] = value;
      }
    });
  }

  printProperties() {
    console.log(JSON.stringify(this.data, null, 2));
  }

  write(outputPath = this.filepath) {
    let output = '';
    for (const [key, value] of Object.entries(this.data.topLevel)) {
      output += `${key} = ${value}\n`;
    }
    this.data.parts.forEach(part => {
      output += 'PART\n{\n';
      for (const [key, value] of Object.entries(part)) {
        if (typeof value === 'object') {
          output += `${key.toUpperCase()}\n{\n`;
          for (const [subKey, subValue] of Object.entries(value)) {
            output += `    ${subKey} = ${subValue}\n`;
          }
          output += '}\n';
        } else {
          output += `    ${key} = ${value}\n`;
        }
      }
      output += '}\n';
    });
    fs.writeFileSync(outputPath, output);
  }
}

// Example usage:
// const handler = new CraftFileHandler('example.craft');
// handler.read();
// handler.printProperties();
// handler.write('modified.craft');

7. C class for .CRAFT file handling

(Note: Assuming C++ for "class" support, as pure C doesn't have classes. This uses std::map for properties and file I/O.)

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

class CraftFileHandler {
private:
    std::string filepath;
    std::map<std::string, std::string> topLevel;
    std::vector<std::map<std::string, std::map<std::string, std::string>>> parts;  // parts[part][key or subblock][value or submap]

public:
    CraftFileHandler(const std::string& fp) : filepath(fp) {}

    void read() {
        std::ifstream file(filepath);
        if (!file.is_open()) {
            std::cerr << "Error opening file\n";
            return;
        }
        std::string line;
        std::map<std::string, std::map<std::string, std::string>> currentPart;
        std::map<std::string, std::string> currentSub;
        bool inBlock = false;
        std::string blockType = "";

        while (std::getline(file, line)) {
            line.erase(0, line.find_first_not_of(" \t"));  // Trim
            line.erase(line.find_last_not_of(" \t") + 1);
            if (line.empty()) continue;
            if (line == "PART") {
                if (!currentPart.empty()) parts.push_back(currentPart);
                currentPart.clear();
                inBlock = true;
                blockType = "PART";
            } else if (line == "EVENTS" || line == "ACTIONS" || line == "MODULE" || line == "RESOURCE" || line == "PARTDATA") {
                blockType = line;
                currentSub.clear();
                inBlock = true;
            } else if (line == "{") {
                continue;
            } else if (line == "}") {
                if (!currentSub.empty() && !blockType.empty() && blockType != "PART") {
                    currentPart[blockType.ToLower()] = currentSub;  // Pseudo toLower
                }
                inBlock = false;
                currentSub.clear();
                if (blockType == "PART") {
                    parts.push_back(currentPart);
                    currentPart.clear();
                }
                blockType = "";
            } else if (inBlock) {
                size_t pos = line.find('=');
                if (pos != std::string::npos) {
                    std::string key = line.substr(0, pos);
                    std::string value = line.substr(pos + 1);
                    key.erase(key.find_last_not_of(" \t") + 1);
                    value.erase(0, value.find_first_not_of(" \t"));
                    if (!currentSub.empty()) {
                        currentSub[key] = value;
                    } else {
                        currentPart[key] = {{"value", value}};  // Simplified
                    }
                }
            } else {
                size_t pos = line.find('=');
                if (pos != std::string::npos) {
                    std::string key = line.substr(0, pos);
                    std::string value = line.substr(pos + 1);
                    key.erase(key.find_last_not_of(" \t") + 1);
                    value.erase(0, value.find_first_not_of(" \t"));
                    topLevel[key] = value;
                }
            }
        }
        if (!currentPart.empty()) parts.push_back(currentPart);
        file.close();
    }

    void printProperties() {
        std::cout << "topLevel: {\n";
        for (const auto& kv : topLevel) {
            std::cout << "  " << kv.first << ": " << kv.second << "\n";
        }
        std::cout << "}\nparts: [\n";
        for (const auto& part : parts) {
            std::cout << "  {\n";
            for (const auto& pkv : part) {
                if (pkv.second.size() == 1 && pkv.second.count("value")) {
                    std::cout << "    " << pkv.first << ": " << pkv.second.at("value") << "\n";
                } else {
                    std::cout << "    " << pkv.first << ": {\n";
                    for (const auto& subkv : pkv.second) {
                        std::cout << "      " << subkv.first << ": " << subkv.second << "\n";
                    }
                    std::cout << "    }\n";
                }
            }
            std::cout << "  }\n";
        }
        std::cout << "]\n";
    }

    void write(const std::string& outputPath) {
        std::ofstream file(outputPath.empty() ? filepath : outputPath);
        if (!file.is_open()) {
            std::cerr << "Error opening file for write\n";
            return;
        }
        for (const auto& kv : topLevel) {
            file << kv.first << " = " << kv.second << "\n";
        }
        for (const auto& part : parts) {
            file << "PART\n{\n";
            for (const auto& pkv : part) {
                if (pkv.second.size() == 1 && pkv.second.count("value")) {
                    file << "    " << pkv.first << " = " << pkv.second.at("value") << "\n";
                } else {
                    file << "    " << pkv.first << "\n    {\n";
                    for (const auto& subkv : pkv.second) {
                        file << "        " << subkv.first << " = " << subkv.second << "\n";
                    }
                    file << "    }\n";
                }
            }
            file << "}\n";
        }
        file.close();
    }
};

// Example usage:
// int main() {
//     CraftFileHandler handler("example.craft");
//     handler.read();
//     handler.printProperties();
//     handler.write("modified.craft");
//     return 0;
// }