Task 686: .SSC File Format

Task 686: .SSC File Format

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

The .SSC (Solar System Catalog) file format is a plain-text format used by the Celestia space simulation software to define celestial objects (e.g., planets, moons, asteroids, spacecraft) and their attributes within a solar system. It is structured as a series of object definitions, each starting with an optional type prefix (e.g., "AltSurface", "Location", "ReferencePoint", "Modify", "Replace"), followed by a quoted name, a quoted path to the parent object(s), and a block of properties enclosed in curly braces {}. Properties are key-value pairs, with values being numbers, strings, booleans, or nested blocks. Comments start with #. Properties can be top-level (for the object) or nested within sub-structures like Atmosphere, EllipticalOrbit, Rings, etc.

Based on the specifications, here is a comprehensive list of all known properties intrinsic to the .SSC format (alphabetical order, including deprecated ones for completeness). These define the object's appearance, orbit, rotation, atmosphere, and other characteristics. Sub-properties are indented under their parent blocks.

  • Albedo (float): Deprecated; geometric albedo (0.0-1.0) for brightness when rendered as a point.
  • Atmosphere (block): Defines the object's atmosphere.
  • Absorption (array [R G B]): Amount of RGB light absorbed.
  • CloudHeight (float): Cloud altitude in km.
  • CloudMap (string): Path to cloud texture file.
  • CloudNormalMap (string): Path to cloud normal map for bump effects.
  • CloudSpeed (float): Cloud rotation speed in km/h.
  • Height (float): Atmosphere thickness in km.
  • Lower (array [R G B]): Deprecated; color near surface.
  • Mie (float): Mie scattering amount for haze.
  • MieAsymmetry (float): Forward/backward scattering bias.
  • MieScaleHeight (float): Vertical scale for Mie scattering.
  • Rayleigh (array [R G B]): Rayleigh scattering amounts for RGB.
  • Sky (array [R G B]): Deprecated; sky color from inside.
  • Sunset (array [R G B]): Deprecated; sunset color.
  • Upper (array [R G B]): Deprecated; color near top.
  • Beginning (float or string): Julian date or "YYYY MM DD HH:MM:SS" when the object starts existing.
  • BlendTexture (boolean): If true, blends texture with Color.
  • BondAlbedo (float): Bond albedo (0.0-1.0) for temperature/reflectivity.
  • BumpHeight (float): Scale of bump map relief (default 2.0).
  • BumpMap (string): Path to bump map texture.
  • Class (string): Object type (e.g., "planet", "moon", "dwarfplanet", "asteroid", "comet", "spacecraft", "surfacefeature", "component", "diffuse", "invisible", "minormoon").
  • Clickable (boolean): If true, object can be selected by clicking (default true, except for "diffuse").
  • Color (array [R G B]): Base color tint (0.0-1.0 per channel).
  • CustomOrbit (string): Name of built-in orbit calculation.
  • CustomRotation (string): Name of built-in rotation model (e.g., IAU model).
  • EllipticalOrbit (block): Defines an elliptical orbit.
  • ArgOfPericenter (float): Argument of pericenter in degrees.
  • AscendingNode (float): Longitude of ascending node in degrees.
  • Eccentricity (float): Orbit eccentricity (0.0-1.0).
  • Epoch (float): Julian date for orbital elements.
  • Inclination (float): Orbit inclination in degrees.
  • MeanAnomaly (float): Mean anomaly at epoch in degrees.
  • MeanLongitude (float): Mean longitude at epoch in degrees (alternative to MeanAnomaly).
  • Period (float): Orbital period in days.
  • SemiMajorAxis (float): Semi-major axis in AU (for stars) or km (for planets/moons).
  • EllipticalRotation (block): Deprecated; similar to UniformRotation for elliptical rotation.
  • Emissive (boolean): If true, object emits light (no shadows).
  • EmissiveColor (array [R G B]): Color of emitted light.
  • Ending (float or string): Julian date or "YYYY MM DD HH:MM:SS" when the object ceases existing.
  • GeomAlbedo (float): Geometric albedo (0.0-1.0) for distant brightness.
  • HazeColor (array [R G B]): Color of atmospheric haze.
  • HazeDensity (float): Density of haze (0.0-1.0).
  • InfoURL (string): URL for more information about the object.
  • Mass (float): Mass in kg.
  • Mesh (string): Path to 3D model file (e.g., .3ds, .cmod).
  • NightTexture (string): Path to night-side texture.
  • NormalMap (string): Path to normal map for surface bump effects.
  • Oblateness (float): Flattening (0.0 for sphere, >0 for oblate).
  • Obliquity (float): Deprecated; use rotation parameters.
  • OverlayTexture (string): Path to overlay texture (e.g., labels).
  • PrecessionRate (float): Precession rate in degrees per day.
  • Radius (float): Radius in km; influences class if not specified.
  • Rings (block): Defines ring system.
  • Color (array [R G B]): Ring color.
  • Inner (float): Inner radius in km.
  • Outer (float): Outer radius in km.
  • Texture (string): Path to ring texture.
  • RotationOffset (float): Offset for rotation in degrees.
  • RotationPeriod (float): Rotation period in hours (positive for direct, negative for retrograde).
  • SpecularColor (array [R G B]): Color of specular highlights.
  • SpecularPower (float): Shininess (higher = sharper highlights).
  • SpecularTexture (string): Path to specular map.
  • Texture (string): Path to main surface texture.
  • UniformRotation (block): Defines uniform rotation.
  • AscendingNode (float): Longitude of ascending node in degrees.
  • Inclination (float): Axial tilt in degrees.
  • MeridianAngle (float): Rotation at epoch in degrees.
  • Period (float): Rotation period in hours.
  • Visible (boolean): If false, object is not rendered (default true).

Additional prefixes for special definitions: AltSurface (alternative textures), Location (surface labels with coords), ReferencePoint (orbital points), Modify/Replace (edit existing objects).

3. Ghost blog embedded html javascript for drag n drop .SSC file dump

Here's a self-contained HTML page with embedded JavaScript. It creates a drop zone; when a .SSC file is dropped, it reads the file as text, parses it into objects and their properties (handling nested blocks and comments), and dumps them to the screen in a readable format (e.g., JSON-like structure).

SSC File Dumper
Drag and drop a .SSC file here

4. Python class for .SSC file handling

import json
import re

class SSCFile:
    def __init__(self, filename=None):
        self.objects = []
        if filename:
            self.read(filename)

    def read(self, filename):
        with open(filename, 'r') as f:
            text = f.read()
        lines = [line.strip() for line in text.split('\n') if line.strip() and not line.strip().startswith('#')]
        current_object = None
        current_block = None
        stack = []
        i = 0
        while i < len(lines):
            line = lines[i]
            if '{' in line:
                parts = re.split(r'\s+', line.split('{')[0].strip())
                parts = [p.strip('"') for p in parts]
                if len(parts) >= 2:
                    current_object = {'type': parts[0] if parts[0] in ['AltSurface', 'Location', 'ReferencePoint', 'Modify', 'Replace'] else '', 'name': parts[0] if not current_object else parts[1], 'path': parts[-1], 'properties': {}}
                    self.objects.append(current_object)
                    current_block = current_object['properties']
                    stack = [current_block]
                else:
                    key = parts[0]
                    current_block[key] = {}
                    stack.append(current_block[key])
                    current_block = current_block[key]
            elif line == '}':
                stack.pop()
                current_block = stack[-1] if stack else None
            else:
                match = re.match(r'(\w+)\s+(.*)', line)
                if match:
                    key, value = match.groups()
                    value = value.strip()
                    if value.startswith('['):
                        current_block[key] = [float(v) for v in re.findall(r'[\d.]+', value)]
                    elif re.match(r'^\d+(\.\d+)?$', value):
                        current_block[key] = float(value)
                    elif value.lower() in ['true', 'false']:
                        current_block[key] = value.lower() == 'true'
                    elif re.match(r'^\d{4}\s+\d{1,2}\s+\d{1,2}', value):
                        current_block[key] = value  # date string
                    else:
                        current_block[key] = value.strip('"')
            i += 1

    def write(self, filename):
        with open(filename, 'w') as f:
            for obj in self.objects:
                type_prefix = f'{obj["type"]} ' if obj["type"] else ''
                f.write(f'{type_prefix}"{obj["name"]}" "{obj["path"]}" {{\n')
                self._write_properties(f, obj['properties'], indent=1)
                f.write('}\n\n')

    def _write_properties(self, f, props, indent):
        for key, value in props.items():
            ind = '  ' * indent
            if isinstance(value, dict):
                f.write(f'{ind}{key} {{\n')
                self._write_properties(f, value, indent + 1)
                f.write(f'{ind}}}\n')
            elif isinstance(value, list):
                f.write(f'{ind}{key} [ {" ".join(map(str, value))} ]\n')
            elif isinstance(value, bool):
                f.write(f'{ind}{key} {str(value).lower()}\n')
            else:
                if isinstance(value, str) and ' ' in value and not value.startswith('"'):
                    value = f'"{value}"'
                f.write(f'{ind}{key} {value}\n')

    def print_properties(self):
        print(json.dumps(self.objects, indent=2))

# Example usage:
# ssc = SSCFile('example.ssc')
# ssc.print_properties()
# ssc.write('output.ssc')

5. Java class for .SSC file handling

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

public class SSCFile {
    private List<Map<String, Object>> objects = new ArrayList<>();

    public SSCFile(String filename) throws IOException {
        if (filename != null) {
            read(filename);
        }
    }

    public void read(String filename) throws IOException {
        BufferedReader reader = new BufferedReader(new FileReader(filename));
        String line;
        Stack<Map<String, Object>> stack = new Stack<>();
        Map<String, Object> currentObject = null;
        Map<String, Object> currentBlock = null;
        while ((line = reader.readLine()) != null) {
            line = line.trim();
            if (line.isEmpty() || line.startsWith("#")) continue;
            if (line.contains("{")) {
                String[] parts = line.split("\\{")[0].trim().split("\\s+");
                for (int i = 0; i < parts.length; i++) parts[i] = parts[i].replace("\"", "");
                if (parts.length >= 2) {
                    currentObject = new HashMap<>();
                    String type = "";
                    String name = parts[0];
                    if (Arrays.asList("AltSurface", "Location", "ReferencePoint", "Modify", "Replace").contains(parts[0])) {
                        type = parts[0];
                        name = parts[1];
                    }
                    currentObject.put("type", type);
                    currentObject.put("name", name);
                    currentObject.put("path", parts[parts.length - 1]);
                    currentObject.put("properties", new HashMap<String, Object>());
                    objects.add(currentObject);
                    currentBlock = (Map<String, Object>) currentObject.get("properties");
                    stack = new Stack<>();
                    stack.push(currentBlock);
                } else {
                    String key = parts[0];
                    Map<String, Object> newBlock = new HashMap<>();
                    currentBlock.put(key, newBlock);
                    stack.push(newBlock);
                    currentBlock = newBlock;
                }
            } else if (line.equals("}")) {
                stack.pop();
                currentBlock = stack.isEmpty() ? null : stack.peek();
            } else if (currentBlock != null) {
                Matcher matcher = Pattern.compile("(\\w+)\\s+(.*)").matcher(line);
                if (matcher.matches()) {
                    String key = matcher.group(1);
                    String value = matcher.group(2).trim();
                    if (value.startsWith("[")) {
                        List<Float> list = new ArrayList<>();
                        Matcher numMatcher = Pattern.compile("[\\d.]+").matcher(value);
                        while (numMatcher.find()) list.add(Float.parseFloat(numMatcher.group()));
                        currentBlock.put(key, list);
                    } else {
                        try {
                            currentBlock.put(key, Float.parseFloat(value));
                        } catch (NumberFormatException e) {
                            if (value.toLowerCase().equals("true") || value.toLowerCase().equals("false")) {
                                currentBlock.put(key, Boolean.parseBoolean(value));
                            } else {
                                currentBlock.put(key, value.replace("\"", ""));
                            }
                        }
                    }
                }
            }
        }
        reader.close();
    }

    public void write(String filename) throws IOException {
        BufferedWriter writer = new BufferedWriter(new FileWriter(filename));
        for (Map<String, Object> obj : objects) {
            String typePrefix = obj.get("type").toString().isEmpty() ? "" : obj.get("type") + " ";
            writer.write(typePrefix + "\"" + obj.get("name") + "\" \"" + obj.get("path") + "\" {\n");
            writeProperties(writer, (Map<String, Object>) obj.get("properties"), 1);
            writer.write("}\n\n");
        }
        writer.close();
    }

    private void writeProperties(BufferedWriter writer, Map<String, Object> props, int indent) throws IOException {
        String indStr = "  ".repeat(indent);
        for (Map.Entry<String, Object> entry : props.entrySet()) {
            String key = entry.getKey();
            Object value = entry.getValue();
            if (value instanceof Map) {
                writer.write(indStr + key + " {\n");
                writeProperties(writer, (Map<String, Object>) value, indent + 1);
                writer.write(indStr + "}\n");
            } else if (value instanceof List) {
                writer.write(indStr + key + " [ " + ((List<?>) value).stream().map(Object::toString).reduce((a, b) -> a + " " + b).orElse("") + " ]\n");
            } else if (value instanceof Boolean) {
                writer.write(indStr + key + " " + value.toString().toLowerCase() + "\n");
            } else {
                String valStr = value.toString();
                if (valStr.contains(" ") && !valStr.startsWith("\"")) valStr = "\"" + valStr + "\"";
                writer.write(indStr + key + " " + valStr + "\n");
            }
        }
    }

    public void printProperties() {
        System.out.println(objects);  // Simple print; use Gson for pretty JSON if needed
    }

    // Example usage:
    // public static void main(String[] args) throws IOException {
    //     SSCFile ssc = new SSCFile("example.ssc");
    //     ssc.printProperties();
    //     ssc.write("output.ssc");
    // }
}

6. Javascript class for .SSC file handling

class SSCFile {
    constructor(filename = null) {
        this.objects = [];
        if (filename) {
            this.read(filename);
        }
    }

    async read(filename) {
        const response = await fetch(filename);
        const text = await response.text();
        const lines = text.split('\n').map(l => l.trim()).filter(l => l && !l.startsWith('#'));
        let currentObject = null;
        let currentBlock = null;
        let stack = [];
        for (let line of lines) {
            if (line.includes('{')) {
                const parts = line.split('{')[0].trim().split(/\s+/).map(p => p.replace(/"/g, ''));
                if (parts.length >= 2) {
                    const type = ['AltSurface', 'Location', 'ReferencePoint', 'Modify', 'Replace'].includes(parts[0]) ? parts[0] : '';
                    const name = type ? parts[1] : parts[0];
                    currentObject = { type, name, path: parts[parts.length - 1], properties: {} };
                    this.objects.push(currentObject);
                    currentBlock = currentObject.properties;
                    stack = [currentBlock];
                } else {
                    const key = parts[0];
                    currentBlock[key] = {};
                    stack.push(currentBlock[key]);
                    currentBlock = currentBlock[key];
                }
            } else if (line === '}') {
                stack.pop();
                currentBlock = stack.length ? stack[stack.length - 1] : null;
            } else if (currentBlock) {
                const [key, ...valueParts] = line.split(/\s+/);
                let value = valueParts.join(' ').trim();
                if (value.startsWith('[')) {
                    currentBlock[key] = value.replace(/[\[\]]/g, '').split(/\s+/).map(parseFloat);
                } else if (!isNaN(parseFloat(value))) {
                    currentBlock[key] = parseFloat(value);
                } else if (value.toLowerCase() === 'true' || value.toLowerCase() === 'false') {
                    currentBlock[key] = value.toLowerCase() === 'true';
                } else {
                    currentBlock[key] = value.replace(/"/g, '');
                }
            }
        }
    }

    write() {
        let output = '';
        for (let obj of this.objects) {
            const typePrefix = obj.type ? `${obj.type} ` : '';
            output += `${typePrefix}"${obj.name}" "${obj.path}" {\n`;
            output += this._writeProperties(obj.properties, 1);
            output += '}\n\n';
        }
        return output;
    }

    _writeProperties(props, indent) {
        let str = '';
        const ind = '  '.repeat(indent);
        for (let [key, value] of Object.entries(props)) {
            if (typeof value === 'object' && !Array.isArray(value)) {
                str += `${ind}${key} {\n`;
                str += this._writeProperties(value, indent + 1);
                str += `${ind}}\n`;
            } else if (Array.isArray(value)) {
                str += `${ind}${key} [ ${value.join(' ')} ]\n`;
            } else if (typeof value === 'boolean') {
                str += `${ind}${key} ${value ? 'true' : 'false'}\n`;
            } else {
                let valStr = value.toString();
                if (valStr.includes(' ') && !valStr.startsWith('"')) valStr = `"${valStr}"`;
                str += `${ind}${key} ${valStr}\n`;
            }
        }
        return str;
    }

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

// Example usage:
// const ssc = new SSCFile();
// await ssc.read('example.ssc');
// ssc.printProperties();
// console.log(ssc.write());  // Outputs to console; save to file as needed

7. C class (using C++ for class support) for .SSC file handling

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

class SSCFile {
private:
    std::vector<std::map<std::string, std::any>> objects;  // Requires C++17 for std::any

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

    void read(const std::string& filename) {
        std::ifstream file(filename);
        std::string line;
        std::vector<std::map<std::string, std::any>*> stack;
        std::map<std::string, std::any>* currentObject = nullptr;
        std::map<std::string, std::any>* currentBlock = nullptr;
        while (std::getline(file, line)) {
            line = std::regex_replace(line, std::regex("^\\s+|\\s+$"), "");
            if (line.empty() || line[0] == '#') continue;
            if (line.find('{') != std::string::npos) {
                std::string header = line.substr(0, line.find('{'));
                header = std::regex_replace(header, std::regex("^\\s+|\\s+$"), "");
                std::vector<std::string> parts;
                std::stringstream ss(header);
                std::string part;
                while (ss >> part) {
                    part = std::regex_replace(part, std::regex("\""), "");
                    parts.push_back(part);
                }
                if (parts.size() >= 2) {
                    std::map<std::string, std::any> newObj;
                    std::string type = "";
                    std::string name = parts[0];
                    std::vector<std::string> types = {"AltSurface", "Location", "ReferencePoint", "Modify", "Replace"};
                    if (std::find(types.begin(), types.end(), parts[0]) != types.end()) {
                        type = parts[0];
                        name = parts[1];
                    }
                    newObj["type"] = type;
                    newObj["name"] = name;
                    newObj["path"] = parts.back();
                    newObj["properties"] = std::map<std::string, std::any>{};
                    objects.push_back(newObj);
                    currentObject = &objects.back();
                    currentBlock = &std::any_cast<std::map<std::string, std::any>&>((*currentObject)["properties"]);
                    stack.clear();
                    stack.push_back(currentBlock);
                } else {
                    std::string key = parts[0];
                    (*currentBlock)[key] = std::map<std::string, std::any>{};
                    stack.push_back(&std::any_cast<std::map<std::string, std::any>&>((*currentBlock)[key]));
                    currentBlock = stack.back();
                }
            } else if (line == "}") {
                stack.pop_back();
                currentBlock = stack.empty() ? nullptr : stack.back();
            } else if (currentBlock) {
                std::smatch match;
                if (std::regex_match(line, match, std::regex("(\\w+)\\s+(.*)"))) {
                    std::string key = match[1];
                    std::string value = match[2];
                    value = std::regex_replace(value, std::regex("^\\s+|\\s+$"), "");
                    if (value[0] == '[') {
                        std::vector<float> list;
                        std::smatch numMatch;
                        std::string nums = value.substr(1, value.size() - 2);
                        std::regex numRegex("[\\d.]+");
                        auto iter = std::sregex_iterator(nums.begin(), nums.end(), numRegex);
                        for (auto i = iter; i != std::sregex_iterator(); ++i) {
                            list.push_back(std::stof((*i).str()));
                        }
                        (*currentBlock)[key] = list;
                    } else {
                        try {
                            (*currentBlock)[key] = std::stof(value);
                        } catch (...) {
                            if (value == "true" || value == "false") {
                                (*currentBlock)[key] = (value == "true");
                            } else {
                                value = std::regex_replace(value, std::regex("\""), "");
                                (*currentBlock)[key] = value;
                            }
                        }
                    }
                }
            }
        }
    }

    void write(const std::string& filename) {
        std::ofstream file(filename);
        for (auto& obj : objects) {
            std::string typePrefix = std::any_cast<std::string>(obj["type"]).empty() ? "" : std::any_cast<std::string>(obj["type"]) + " ";
            file << typePrefix << "\"" << std::any_cast<std::string>(obj["name"]) << "\" \"" << std::any_cast<std::string>(obj["path"]) << "\" {\n";
            writeProperties(file, std::any_cast<std::map<std::string, std::any>>(obj["properties"]), 1);
            file << "}\n\n";
        }
    }

private:
    void writeProperties(std::ofstream& file, const std::map<std::string, std::any>& props, int indent) {
        std::string ind(indent * 2, ' ');
        for (const auto& [key, value] : props) {
            if (value.type() == typeid(std::map<std::string, std::any>)) {
                file << ind << key << " {\n";
                writeProperties(file, std::any_cast<std::map<std::string, std::any>>(value), indent + 1);
                file << ind << "}\n";
            } else if (value.type() == typeid(std::vector<float>)) {
                file << ind << key << " [";
                for (auto v : std::any_cast<std::vector<float>>(value)) file << " " << v;
                file << " ]\n";
            } else if (value.type() == typeid(bool)) {
                file << ind << key << " " << (std::any_cast<bool>(value) ? "true" : "false") << "\n";
            } else {
                std::string valStr = std::any_cast<std::string>(value);  // Assume string for non-numeric
                if (valStr.find(' ') != std::string::npos && valStr[0] != '"') valStr = "\"" + valStr + "\"";
                file << ind << key << " " << valStr << "\n";
            }
        }
    }

public:
    void printProperties() {
        // Simple console output; implement JSON-like if needed
        for (const auto& obj : objects) {
            std::cout << "{ \"type\": \"" << std::any_cast<std::string>(obj.at("type")) << "\", \"name\": \"" << std::any_cast<std::string>(obj.at("name")) << "\", \"path\": \"" << std::any_cast<std::string>(obj.at("path")) << "\", \"properties\": { ... } }" << std::endl;
        }
    }
};

// Example usage:
// int main() {
//     SSCFile ssc("example.ssc");
//     ssc.printProperties();
//     ssc.write("output.ssc");
//     return 0;
// }