Task 094: .CMOD File Format

Task 094: .CMOD File Format

File Format Specifications for .CMOD

The .CMOD file format is used by Celestia, an open-source 3D astronomy simulation software, for 3D models. It supports both ASCII (text-based) and binary versions. The ASCII version is documented in detail, while the binary version is a more compact serialized form used for performance. The structure includes a header, materials, and meshes with vertices, normals, textures, and primitive groups. The binary version uses little-endian byte order, uint32 for counts and enums, floats for floating-point values, and strings as uint32 length followed by characters (no null terminator). The spec is derived from Celestia documentation and source code.

List of all properties intrinsic to the file format:

  • Header: A 16-byte magic string, either "#celmodel__ascii" for ASCII or "#celmodel_binary" for binary.
  • Version: Implicit in header (no explicit version field, but format evolved, e.g., v1.5.0 added features like point sprites).
  • Materials (count implicit in ASCII, uint32 in binary):
  • Diffuse color: 3 floats (R, G, B).
  • Specular color: 3 floats (R, G, B).
  • Emissive color: 3 floats (R, G, B).
  • Specular power: 1 float.
  • Opacity: 1 float.
  • Blend mode: Enum (normal or add for additive blending).
  • Texture0: String (base texture filename, may include wildcard like "*.png").
  • Normalmap: String (normal map texture filename).
  • Specularmap: String (specular map texture filename).
  • Emissivemap: String (emissive map texture filename).
  • Meshes (count implicit in ASCII, uint32 in binary):
  • Vertex description (attribute count as uint32 in binary):
  • Semantic: Enum (position, normal, color0, color1, tangent, texcoord0, texcoord1, texcoord2, texcoord3, pointsize).
  • Format: Enum (f1 = 1 float, f2 = 2 floats, f3 = 3 floats, f4 = 4 floats, ub4 = 4 unsigned bytes).
  • Vertex count: Unsigned integer.
  • Vertex stride: Calculated from formats (bytes per vertex).
  • Vertex data: Array of values based on description (floats or bytes).
  • Primitive group count: Unsigned integer.
  • Primitive groups:
  • Type: Enum (trilist = triangle list, tristrip = triangle strip, trifan = triangle fan, linelist = line list, linestrip = line strip, pointlist = point list, spritelist = sprite list).
  • Material index: Unsigned integer (reference to material library).
  • Index count: Unsigned integer.
  • Indices: Array of unsigned integers (vertex indices).

Two direct download links for .CMOD files:

Ghost blog embedded HTML JavaScript for drag and drop .CMOD file dump:

CMOD File Dumper
Drag and drop .CMOD file here

  

  1. Python class for .CMOD:
import struct
import os

class CMODHandler:
    def __init__(self, filepath):
        self.filepath = filepath
        self.properties = {'materials': [], 'meshes': []}
        self.is_binary = False
        self.parse()

    def parse(self):
        with open(self.filepath, 'rb') as f:
            header = f.read(16).decode('ascii', errors='ignore')
            if header == '#celmodel_binary':
                self.is_binary = True
                self.parse_binary(f)
            elif header == '#celmodel__ascii':
                self.parse_ascii(f.read().decode('ascii'))
            else:
                print('Invalid CMOD file.')

    def parse_ascii(self, text):
        lines = [l.strip() for l in text.split('\n') if l.strip() and not l.strip().startsWith('#')]
        current_material = None
        current_mesh = None
        in_material = False
        in_mesh = False
        in_vertex_desc = False
        for line in lines:
            tokens = line.split()
            if tokens[0] == 'material':
                current_material = {}
                in_material = True
            elif tokens[0] == 'end_material':
                self.properties['materials'].append(current_material)
                in_material = False
            elif tokens[0] == 'mesh':
                current_mesh = {'vertex_desc': [], 'vertices_count': 0, 'prim_groups': []}
                in_mesh = True
            elif tokens[0] == 'end_mesh':
                self.properties['meshes'].append(current_mesh)
                in_mesh = False
            elif tokens[0] == 'vertexdesc':
                in_vertex_desc = True
            elif tokens[0] == 'end_vertexdesc':
                in_vertex_desc = False
            elif tokens[0] == 'vertices':
                current_mesh['vertices_count'] = int(tokens[1])
            elif tokens[0] in ['trilist', 'tristrip', 'trifan', 'linelist', 'linestrip', 'pointlist', 'spritelist']:
                group = {'type': tokens[0], 'material_index': int(tokens[1]), 'index_count': int(tokens[2]), 'indices': [int(i) for i in tokens[3:]]}
                current_mesh['prim_groups'].append(group)
            else:
                if in_material:
                    if tokens[0] == 'blend':
                        current_material['blend'] = tokens[1]
                    elif tokens[0] in ['opacity', 'specpower']:
                        current_material[tokens[0]] = float(tokens[1])
                    elif len(tokens) == 4:
                        current_material[tokens[0]] = [float(x) for x in tokens[1:]]
                    elif len(tokens) == 2:
                        current_material[tokens[0]] = tokens[1].replace('"', '')
                elif in_vertex_desc and len(tokens) == 2:
                    current_mesh['vertex_desc'].append({'semantic': tokens[0], 'format': tokens[1]})
        # Vertex data parsing omitted for brevity, as it's variable length

    def parse_binary(self, f):
        # Basic binary parsing based on format (incomplete for demonstration)
        version = struct.unpack('<I', f.read(4))[0]  # Assume version if present, but not in spec
        material_count = struct.unpack('<I', f.read(4))[0]
        for _ in range(material_count):
            material = {}
            blend = struct.unpack('<I', f.read(4))[0]  # Enum
            material['blend'] = 'add' if blend == 1 else 'normal'
            material['diffuse'] = struct.unpack('<fff', f.read(12))
            material['emissive'] = struct.unpack('<fff', f.read(12))
            material['specular'] = struct.unpack('<fff', f.read(12))
            material['specpower'] = struct.unpack('<f', f.read(4))[0]
            material['opacity'] = struct.unpack('<f', f.read(4))[0]
            # Textures: for each, uint32 count of textures, then for each: uint32 length, chars
            tex_count = struct.unpack('<I', f.read(4))[0]
            for i in range(tex_count):
                length = struct.unpack('<I', f.read(4))[0]
                name = f.read(length).decode('ascii')
                material[f'texture{i}'] = name
            self.properties['materials'].append(material)
        mesh_count = struct.unpack('<I', f.read(4))[0]
        for _ in range(mesh_count):
            mesh = {}
            attr_count = struct.unpack('<I', f.read(4))[0]
            mesh['vertex_desc'] = []
            for __ in range(attr_count):
                semantic = struct.unpack('<I', f.read(4))[0]
                format = struct.unpack('<I', f.read(4))[0]
                mesh['vertex_desc'].append({'semantic': semantic, 'format': format})
            mesh['vertices_count'] = struct.unpack('<I', f.read(4))[0]
            # Vertex data: calculate stride, read raw bytes
            stride = sum(self.format_size(fmt['format']) for fmt in mesh['vertex_desc'])  # Custom function for size
            vertex_data = f.read(mesh['vertices_count'] * stride)
            # Prim groups
            group_count = struct.unpack('<I', f.read(4))[0]
            mesh['prim_groups'] = []
            for __ in range(group_count):
                group = {}
                group['type'] = struct.unpack('<I', f.read(4))[0]
                group['material_index'] = struct.unpack('<I', f.read(4))[0]
                group['index_count'] = struct.unpack('<I', f.read(4))[0]
                group['indices'] = struct.unpack('<' + 'I' * group['index_count'], f.read(4 * group['index_count']))
                mesh['prim_groups'].append(group)
            self.properties['meshes'].append(mesh)

    def format_size(self, fmt):
        if fmt == 'f1': return 4
        if fmt == 'f2': return 8
        if fmt == 'f3': return 12
        if fmt == 'f4': return 16
        if fmt == 'ub4': return 4
        return 0

    def print_properties(self):
        print(self.properties)

    def write(self, new_filepath):
        # Simple write for ASCII (binary omitted for brevity)
        with open(new_filepath, 'w') as f:
            f.write('#celmodel__ascii\n')
            for mat in self.properties['materials']:
                f.write('material\n')
                for key, val in mat.items():
                    if isinstance(val, list):
                        f.write(f'{key} {" ".join(str(v) for v in val)}\n')
                    else:
                        f.write(f'{key} {val}\n')
                f.write('end_material\n')
            for mesh in self.properties['meshes']:
                f.write('mesh\n')
                f.write('vertexdesc\n')
                for desc in mesh['vertex_desc']:
                    f.write(f'{desc["semantic"]} {desc["format"]}\n')
                f.write('end_vertexdesc\n')
                f.write(f'vertices {mesh["vertices_count"]}\n')
                # Vertex data write omitted
                for group in mesh['prim_groups']:
                    f.write(f'{group["type"]} {group["material_index"]} {group["index_count"]} {" ".join(str(i) for i in group["indices"])}\n')
                f.write('end_mesh\n')

# Example usage
# handler = CMODHandler('example.cmod')
# handler.print_properties()
# handler.write('new.cmod')
  1. Java class for .CMOD:
import java.io.*;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.charset.StandardCharsets;
import java.util.*;

public class CMODHandler {
    private String filepath;
    private Map<String, List<Map<String, Object>>> properties = new HashMap<>();
    private boolean isBinary = false;

    public CMODHandler(String filepath) {
        this.filepath = filepath;
        properties.put("materials", new ArrayList<>());
        properties.put("meshes", new ArrayList<>());
        parse();
    }

    private void parse() {
        try (FileInputStream fis = new FileInputStream(filepath)) {
            byte[] headerBytes = new byte[16];
            fis.read(headerBytes);
            String header = new String(headerBytes, StandardCharsets.ASCII);
            if (header.equals("#celmodel_binary")) {
                isBinary = true;
                parseBinary(fis);
            } else if (header.equals("#celmodel__ascii")) {
                parseAscii(new BufferedReader(new InputStreamReader(new FileInputStream(filepath))).lines().toList());
            } else {
                System.out.println("Invalid CMOD file.");
            }
        } catch (IOException e) {
            e.printStackPrint();
        }
    }

    private void parseAscii(List<String> lines) {
        lines = lines.stream().map(String::trim).filter(l -> !l.isEmpty() && !l.startsWith("#")).toList();
        Map<String, Object> currentMaterial = null;
        Map<String, Object> currentMesh = null;
        boolean inMaterial = false;
        boolean inMesh = false;
        boolean inVertexDesc = false;
        for (String line : lines) {
            String[] tokens = line.split("\\s+");
            switch (tokens[0]) {
                case "material":
                    currentMaterial = new HashMap<>();
                    inMaterial = true;
                    break;
                case "end_material":
                    properties.get("materials").add(currentMaterial);
                    inMaterial = false;
                    break;
                case "mesh":
                    currentMesh = new HashMap<>();
                    currentMesh.put("vertex_desc", new ArrayList<Map<String, String>>());
                    currentMesh.put("prim_groups", new ArrayList<Map<String, Object>>());
                    inMesh = true;
                    break;
                case "end_mesh":
                    properties.get("meshes").add(currentMesh);
                    inMesh = false;
                    break;
                case "vertexdesc":
                    inVertexDesc = true;
                    break;
                case "end_vertexdesc":
                    inVertexDesc = false;
                    break;
                case "vertices":
                    currentMesh.put("vertices_count", Integer.parseInt(tokens[1]));
                    break;
                case "trilist": case "tristrip": case "trifan": case "linelist": case "linestrip": case "pointlist": case "spritelist":
                    Map<String, Object> group = new HashMap<>();
                    group.put("type", tokens[0]);
                    group.put("material_index", Integer.parseInt(tokens[1]));
                    group.put("index_count", Integer.parseInt(tokens[2]));
                    List<Integer> indices = new ArrayList<>();
                    for (int i = 3; i < tokens.length; i++) {
                        indices.add(Integer.parseInt(tokens[i]));
                    }
                    group.put("indices", indices);
                    ((List) currentMesh.get("prim_groups")).add(group);
                    break;
                default:
                    if (inMaterial) {
                        if (tokens[0].equals("blend")) {
                            currentMaterial.put("blend", tokens[1]);
                        } else if (tokens[0].equals("opacity") || tokens[0].equals("specpower")) {
                            currentMaterial.put(tokens[0], Float.parseFloat(tokens[1]));
                        } else if (tokens.length == 4) {
                            float[] color = new float[3];
                            for (int i = 1; i < 4; i++) color[i-1] = Float.parseFloat(tokens[i]);
                            currentMaterial.put(tokens[0], color);
                        } else if (tokens.length == 2) {
                            currentMaterial.put(tokens[0], tokens[1].replace("\"", ""));
                        }
                    } else if (inVertexDesc && tokens.length == 2) {
                        Map<String, String> desc = new HashMap<>();
                        desc.put("semantic", tokens[0]);
                        desc.put("format", tokens[1]);
                        ((List) currentMesh.get("vertex_desc")).add(desc);
                    }
                    // Vertex data skipped
            }
        }
    }

    private void parseBinary(InputStream fis) throws IOException {
        ByteBuffer buffer = ByteBuffer.allocate(1024*1024); // Assume size
        byte[] bytes = fis.readAllBytes();
        buffer.put(bytes);
        buffer.flip();
        buffer.order(ByteOrder.LITTLE_ENDIAN);
        int materialCount = buffer.getInt();
        for (int i = 0; i < materialCount; i++) {
            Map<String, Object> material = new HashMap<>();
            int blend = buffer.getInt();
            material.put("blend", blend == 1 ? "add" : "normal");
            float[] diffuse = new float[3];
            for (int j = 0; j < 3; j++) diffuse[j] = buffer.getFloat();
            material.put("diffuse", diffuse);
            float[] emissive = new float[3];
            for (int j = 0; j < 3; j++) emissive[j] = buffer.getFloat();
            material.put("emissive", emissive);
            float[] specular = new float[3];
            for (int j = 0; j < 3; j++) specular[j] = buffer.getFloat();
            material.put("specular", specular);
            material.put("specpower", buffer.getFloat());
            material.put("opacity", buffer.getFloat());
            int texCount = buffer.getInt();
            for (int k = 0; k < texCount; k++) {
                int length = buffer.getInt();
                byte[] nameBytes = new byte[length];
                buffer.get(nameBytes);
                material.put("texture" + k, new String(nameBytes, StandardCharsets.ASCII));
            }
            properties.get("materials").add(material);
        }
        int meshCount = buffer.getInt();
        for (int i = 0; i < meshCount; i++) {
            Map<String, Object> mesh = new HashMap<>();
            int attrCount = buffer.getInt();
            List<Map<String, Integer>> vertexDesc = new ArrayList<>();
            for (int j = 0; j < attrCount; j++) {
                Map<String, Integer> desc = new HashMap<>();
                desc.put("semantic", buffer.getInt());
                desc.put("format", buffer.getInt());
                vertexDesc.add(desc);
            }
            mesh.put("vertex_desc", vertexDesc);
            int vertexCount = buffer.getInt();
            mesh.put("vertices_count", vertexCount);
            // Stride calculation and vertex data read omitted
            int groupCount = buffer.getInt();
            List<Map<String, Object>> primGroups = new ArrayList<>();
            for (int j = 0; j < groupCount; j++) {
                Map<String, Object> group = new HashMap<>();
                group.put("type", buffer.getInt());
                group.put("material_index", buffer.getInt());
                int indexCount = buffer.getInt();
                group.put("index_count", indexCount);
                List<Integer> indices = new ArrayList<>();
                for (int k = 0; k < indexCount; k++) {
                    indices.add(buffer.getInt());
                }
                group.put("indices", indices);
                primGroups.add(group);
            }
            mesh.put("prim_groups", primGroups);
            properties.get("meshes").add(mesh);
        }
    }

    public void printProperties() {
        System.out.println(properties);
    }

    public void write(String newFilepath) throws IOException {
        // Simple ASCII write (binary omitted)
        try (PrintWriter writer = new PrintWriter(newFilepath)) {
            writer.println("#celmodel__ascii");
            for (Map<String, Object> mat : properties.get("materials")) {
                writer.println("material");
                for (Map.Entry<String, Object> entry : mat.entrySet()) {
                    if (entry.getValue() instanceof float[]) {
                        float[] val = (float[]) entry.getValue();
                        writer.println(entry.getKey() + " " + val[0] + " " + val[1] + " " + val[2]);
                    } else {
                        writer.println(entry.getKey() + " " + entry.getValue());
                    }
                }
                writer.println("end_material");
            }
            for (Map<String, Object> mesh : properties.get("meshes")) {
                writer.println("mesh");
                writer.println("vertexdesc");
                for (Map<String, String> desc : (List<Map<String, String>>) mesh.get("vertex_desc")) {
                    writer.println(desc.get("semantic") + " " + desc.get("format"));
                }
                writer.println("end_vertexdesc");
                writer.println("vertices " + mesh.get("vertices_count"));
                // Vertex data write omitted
                for (Map<String, Object> group : (List<Map<String, Object>>) mesh.get("prim_groups")) {
                    writer.print(group.get("type") + " " + group.get("material_index") + " " + group.get("index_count"));
                    for (Integer i : (List<Integer>) group.get("indices")) {
                        writer.print(" " + i);
                    }
                    writer.println();
                }
                writer.println("end_mesh");
            }
        }
    }

    // Example usage
    // public static void main(String[] args) {
    //     CMODHandler handler = new CMODHandler("example.cmod");
    //     handler.printProperties();
    //     handler.write("new.cmod");
    // }
}
  1. JavaScript class for .CMOD:
class CMODHandler {
  constructor(filepath) {
    this.filepath = filepath;
    this.properties = { materials: [], meshes: [] };
    this.isBinary = false;
    // Note: In JS, reading file requires FileReader or fetch, but for console, assume sync read with node fs
  }

  parse() {
    // For browser, use FileReader; for node, use fs
    const fs = require('fs'); // Assume node
    const data = fs.readFileSync(this.filepath);
    const header = data.toString('ascii', 0, 16);
    if (header === '#celmodel_binary') {
      this.isBinary = true;
      this.parseBinary(data);
    } else if (header === '#celmodel__ascii') {
      this.parseAscii(data.toString('ascii'));
    } else {
      console.log('Invalid CMOD file.');
    }
  }

  parseAscii(text) {
    const lines = text.split('\n').map(l => l.trim()).filter(l => l && !l.startsWith('#'));
    let currentMaterial = null;
    let currentMesh = null;
    let inMaterial = false;
    let inMesh = false;
    let inVertexDesc = false;
    lines.forEach(line => {
      const tokens = line.split(/\s+/);
      switch (tokens[0]) {
        case 'material':
          currentMaterial = {};
          inMaterial = true;
          break;
        case 'end_material':
          this.properties.materials.push(currentMaterial);
          inMaterial = false;
          break;
        case 'mesh':
          currentMesh = { vertexDesc: [], verticesCount: 0, primGroups: [] };
          inMesh = true;
          break;
        case 'end_mesh':
          this.properties.meshes.push(currentMesh);
          inMesh = false;
          break;
        case 'vertexdesc':
          inVertexDesc = true;
          break;
        case 'end_vertexdesc':
          inVertexDesc = false;
          break;
        case 'vertices':
          currentMesh.verticesCount = parseInt(tokens[1]);
          break;
        case 'trilist': case 'tristrip': case 'trifan': case 'linelist': case 'linestrip': case 'pointlist': case 'spritelist':
          const group = { type: tokens[0], materialIndex: parseInt(tokens[1]), indexCount: parseInt(tokens[2]), indices: tokens.slice(3).map(Number) };
          currentMesh.primGroups.push(group);
          break;
        default:
          if (inMaterial) {
            if (tokens[0] === 'blend') currentMaterial.blend = tokens[1];
            else if (tokens[0] === 'opacity' || tokens[0] === 'specpower') currentMaterial[tokens[0]] = parseFloat(tokens[1]);
            else if (tokens.length === 4) currentMaterial[tokens[0]] = tokens.slice(1).map(parseFloat);
            else if (tokens.length === 2) currentMaterial[tokens[0]] = tokens[1].replace(/"/g, '');
          } else if (inVertexDesc && tokens.length === 2) {
            currentMesh.vertexDesc.push({ semantic: tokens[0], format: tokens[1] });
          }
      }
    });
  }

  parseBinary(data) {
    const view = new DataView(data.buffer);
    let offset = 16; // Skip header
    const materialCount = view.getUint32(offset, true); offset += 4;
    for (let i = 0; i < materialCount; i++) {
      const material = {};
      const blend = view.getUint32(offset, true); offset += 4;
      material.blend = blend === 1 ? 'add' : 'normal';
      material.diffuse = [view.getFloat32(offset, true), view.getFloat32(offset+4, true), view.getFloat32(offset+8, true)]; offset += 12;
      material.emissive = [view.getFloat32(offset, true), view.getFloat32(offset+4, true), view.getFloat32(offset+8, true)]; offset += 12;
      material.specular = [view.getFloat32(offset, true), view.getFloat32(offset+4, true), view.getFloat32(offset+8, true)]; offset += 12;
      material.specpower = view.getFloat32(offset, true); offset += 4;
      material.opacity = view.getFloat32(offset, true); offset += 4;
      const texCount = view.getUint32(offset, true); offset += 4;
      for (let k = 0; k < texCount; k++) {
        const length = view.getUint32(offset, true); offset += 4;
        const name = new TextDecoder().decode(data.subarray(offset, offset + length));
        material[`texture${k}`] = name;
        offset += length;
      }
      this.properties.materials.push(material);
    }
    const meshCount = view.getUint32(offset, true); offset += 4;
    for (let i = 0; i < meshCount; i++) {
      const mesh = {};
      const attrCount = view.getUint32(offset, true); offset += 4;
      mesh.vertexDesc = [];
      for (let j = 0; j < attrCount; j++) {
        const semantic = view.getUint32(offset, true); offset += 4;
        const format = view.getUint32(offset, true); offset += 4;
        mesh.vertexDesc.push({ semantic, format });
      }
      mesh.verticesCount = view.getUint32(offset, true); offset += 4;
      // Vertex data offset += verticesCount * stride (omitted)
      const groupCount = view.getUint32(offset, true); offset += 4;
      mesh.primGroups = [];
      for (let j = 0; j < groupCount; j++) {
        const group = {};
        group.type = view.getUint32(offset, true); offset += 4;
        group.materialIndex = view.getUint32(offset, true); offset += 4;
        const indexCount = view.getUint32(offset, true); offset += 4;
        group.indexCount = indexCount;
        group.indices = [];
        for (let k = 0; k < indexCount; k++) {
          group.indices.push(view.getUint32(offset, true)); offset += 4;
        }
        mesh.primGroups.push(group);
      }
      this.properties.meshes.push(mesh);
    }
  }

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

  write(newFilepath) {
    let content = '#celmodel__ascii\n';
    this.properties.materials.forEach(mat => {
      content += 'material\n';
      Object.entries(mat).forEach(([key, val]) => {
        if (Array.isArray(val)) content += `${key} ${val.join(' ')}\n`;
        else content += `${key} ${val}\n`;
      });
      content += 'end_material\n';
    });
    this.properties.meshes.forEach(mesh => {
      content += 'mesh\n';
      content += 'vertexdesc\n';
      mesh.vertexDesc.forEach(desc => {
        content += `${desc.semantic} ${desc.format}\n`;
      });
      content += 'end_vertexdesc\n';
      content += `vertices ${mesh.verticesCount}\n`;
      // Vertex data omitted
      mesh.primGroups.forEach(group => {
        content += `${group.type} ${group.materialIndex} ${group.indexCount} ${group.indices.join(' ')}\n`;
      });
      content += 'end_mesh\n';
    });
    const fs = require('fs');
    fs.writeFileSync(newFilepath, content);
  }
}

// Example
// const handler = new CMODHandler('example.cmod');
// handler.parse();
// handler.printProperties();
// handler.write('new.cmod');
  1. C class for .CMOD:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

// Simple structs for properties
typedef struct {
  char* key;
  void* value; // Can be float[3], float, char*
} Property;

typedef struct {
  Property* props;
  int count;
} Material;

typedef struct {
  char* semantic;
  char* format;
} VertexDesc;

typedef struct {
  char* type;
  int materialIndex;
  int indexCount;
  int* indices;
} PrimGroup;

typedef struct {
  VertexDesc* vertexDesc;
  int vertexDescCount;
  int verticesCount;
  PrimGroup* primGroups;
  int primGroupCount;
} Mesh;

typedef struct {
  Material* materials;
  int materialCount;
  Mesh* meshes;
  int meshCount;
} CMODProperties;

void initProperties(CMODProperties* props) {
  props->materials = NULL;
  props->materialCount = 0;
  props->meshes = NULL;
  props->meshCount = 0;
}

void freeProperties(CMODProperties* props) {
  // Free memory (omitted for brevity)
}

int isBinary(FILE* f) {
  char header[17];
  fseek(f, 0, SEEK_SET);
  fread(header, 16, 1, f);
  header[16] = '\0';
  if (strcmp(header, "#celmodel_binary") == 0) return 1;
  if (strcmp(header, "#celmodel__ascii") == 0) return 0;
  return -1;
}

void parseAscii(FILE* f, CMODProperties* props) {
  fseek(f, 0, SEEK_SET);
  char line[1024];
  Material* currentMaterial = NULL;
  Mesh* currentMesh = NULL;
  int inMaterial = 0, inMesh = 0, inVertexDesc = 0;
  while (fgets(line, sizeof(line), f)) {
    char* trim = strtok(line, "\n ");
    if (trim == NULL || trim[0] == '#') continue;
    if (strcmp(trim, "material") == 0) {
      props->materials = realloc(props->materials, sizeof(Material) * (++props->materialCount));
      currentMaterial = &props->materials[props->materialCount - 1];
      currentMaterial->props = NULL;
      currentMaterial->count = 0;
      inMaterial = 1;
    } else if (strcmp(trim, "end_material") == 0) {
      inMaterial = 0;
    } else if (strcmp(trim, "mesh") == 0) {
      props->meshes = realloc(props->meshes, sizeof(Mesh) * (++props->meshCount));
      currentMesh = &props->meshes[props->meshCount - 1];
      currentMesh->vertexDesc = NULL;
      currentMesh->vertexDescCount = 0;
      currentMesh->verticesCount = 0;
      currentMesh->primGroups = NULL;
      currentMesh->primGroupCount = 0;
      inMesh = 1;
    } else if (strcmp(trim, "end_mesh") == 0) {
      inMesh = 0;
    } else if (strcmp(trim, "vertexdesc") == 0) {
      inVertexDesc = 1;
    } else if (strcmp(trim, "end_vertexdesc") == 0) {
      inVertexDesc = 0;
    } else if (strcmp(trim, "vertices") == 0) {
      currentMesh->verticesCount = atoi(strtok(NULL, " "));
    } else if (strcmp(trim, "trilist") == 0 || strcmp(trim, "tristrip") == 0 || strcmp(trim, "trifan") == 0 ||
               strcmp(trim, "linelist") == 0 || strcmp(trim, "linestrip") == 0 || strcmp(trim, "pointlist") == 0 || strcmp(trim, "spritelist") == 0) {
      PrimGroup group;
      group.type = strdup(trim);
      group.materialIndex = atoi(strtok(NULL, " "));
      group.indexCount = atoi(strtok(NULL, " "));
      group.indices = malloc(sizeof(int) * group.indexCount);
      for (int i = 0; i < group.indexCount; i++) {
        group.indices[i] = atoi(strtok(NULL, " "));
      }
      currentMesh->primGroups = realloc(currentMesh->primGroups, sizeof(PrimGroup) * (++currentMesh->primGroupCount));
      currentMesh->primGroups[currentMesh->primGroupCount - 1] = group;
    } else if (inMaterial) {
      // Parse property (omitted details)
    } else if (inVertexDesc) {
      VertexDesc desc;
      desc.semantic = strdup(trim);
      desc.format = strdup(strtok(NULL, " "));
      currentMesh->vertexDesc = realloc(currentMesh->vertexDesc, sizeof(VertexDesc) * (++currentMesh->vertexDescCount));
      currentMesh->vertexDesc[currentMesh->vertexDescCount - 1] = desc;
    }
  }
}

void parseBinary(FILE* f, CMODProperties* props) {
  fseek(f, 16, SEEK_SET);
  unsigned int materialCount;
  fread(&materialCount, sizeof(unsigned int), 1, f);
  props->materials = malloc(sizeof(Material) * materialCount);
  props->materialCount = materialCount;
  for (int i = 0; i < materialCount; i++) {
    Material* mat = &props->materials[i];
    unsigned int blend;
    fread(&blend, sizeof(unsigned int), 1, f);
    // Similar for colors, floats, textures (length + chars)
    // Omitted for brevity
  }
  unsigned int meshCount;
  fread(&meshCount, sizeof(unsigned int), 1, f);
  props->meshes = malloc(sizeof(Mesh) * meshCount);
  props->meshCount = meshCount;
  for (int i = 0; i < meshCount; i++) {
    Mesh* mesh = &props->meshes[i];
    unsigned int attrCount;
    fread(&attrCount, sizeof(unsigned int), 1, f);
    mesh->vertexDesc = malloc(sizeof(VertexDesc) * attrCount);
    mesh->vertexDescCount = attrCount;
    for (int j = 0; j < attrCount; j++) {
      unsigned int semantic, format;
      fread(&semantic, sizeof(unsigned int), 1, f);
      fread(&format, sizeof(unsigned int), 1, f);
      // Map to strings if needed
    }
    fread(&mesh->verticesCount, sizeof(unsigned int), 1, f);
    // Skip vertex data
    unsigned int groupCount;
    fread(&groupCount, sizeof(unsigned int), 1, f);
    mesh->primGroups = malloc(sizeof(PrimGroup) * groupCount);
    mesh->primGroupCount = groupCount;
    for (int j = 0; j < groupCount; j++) {
      PrimGroup* group = &mesh->primGroups[j];
      unsigned int type;
      fread(&type, sizeof(unsigned int), 1, f);
      fread(&group->materialIndex, sizeof(unsigned int), 1, f);
      fread(&group->indexCount, sizeof(unsigned int), 1, f);
      group->indices = malloc(sizeof(int) * group->indexCount);
      for (int k = 0; k < group->indexCount; k++) {
        fread(&group->indices[k], sizeof(unsigned int), 1, f);
      }
    }
  }
}

void printProperties(const CMODProperties* props) {
  printf("Materials: %d\n", props->materialCount);
  // Print details
  printf("Meshes: %d\n", props->meshCount);
  for (int i = 0; i < props->meshCount; i++) {
    Mesh mesh = props->meshes[i];
    printf("Mesh %d: Vertices %d, PrimGroups %d\n", i, mesh.verticesCount, mesh.primGroupCount);
    // Print more
  }
}

void writeCMOD(const CMODProperties* props, const char* newFilepath) {
  FILE* f = fopen(newFilepath, "w");
  fprintf(f, "#celmodel__ascii\n");
  for (int i = 0; i < props->materialCount; i++) {
    fprintf(f, "material\n");
    // Write props
    fprintf(f, "end_material\n");
  }
  for (int i = 0; i < props->meshCount; i++) {
    Mesh mesh = props->meshes[i];
    fprintf(f, "mesh\n");
    fprintf(f, "vertexdesc\n");
    for (int j = 0; j < mesh.vertexDescCount; j++) {
      fprintf(f, "%s %s\n", mesh.vertexDesc[j].semantic, mesh.vertexDesc[j].format);
    }
    fprintf(f, "end_vertexdesc\n");
    fprintf(f, "vertices %d\n", mesh.verticesCount);
    // Vertex data
    for (int j = 0; j < mesh.primGroupCount; j++) {
      PrimGroup group = mesh.primGroups[j];
      fprintf(f, "%s %d %d", group.type, group.materialIndex, group.indexCount);
      for (int k = 0; k < group.indexCount; k++) {
        fprintf(f, " %d", group.indices[k]);
      }
      fprintf(f, "\n");
    }
    fprintf(f, "end_mesh\n");
  }
  fclose(f);
}

// Example
// int main() {
//   FILE* f = fopen("example.cmod", "rb");
//   CMODProperties props;
//   initProperties(&props);
//   if (isBinary(f) == 1) parseBinary(f, &props);
//   else parseAscii(f, &props);
//   printProperties(&props);
//   writeCMOD(&props, "new.cmod");
//   freeProperties(&props);
//   fclose(f);
//   return 0;
// }