Task 114: .CS File Format

Task 114: .CS File Format

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

The .CS file format is the compiled SCM (Script) format used by CLEO for GTA games, particularly GTA San Andreas. It is a binary format consisting of header chunks linked by jump instructions, followed by script code. The intrinsic properties (metadata extracted from the header chunks) are as follows:

  • Target Game Byte (1 byte): Identifies the target game (e.g., 's' for San Andreas).
  • Number of Used Models (4 bytes): The number of models/objects defined in the file.
  • Model Names (variable): A list of model names, each 24 bytes long (padded with nulls if shorter).
  • Main Script Size (4 bytes): Size of the main script section in bytes.
  • Largest Mission Script Size (4 bytes): Size of the largest individual mission script.
  • Number of Mission Scripts (2 bytes): Total number of mission scripts (typically up to 200).
  • Number of Exclusive Mission Scripts (2 bytes): Number of exclusive (special) mission scripts.
  • Largest Number of Mission Script Local Variables (4 bytes): Maximum local variables used in any mission or external script.
  • Mission Offsets (variable): A list of 4-byte offsets pointing to each mission script's starting position.
  • Largest Streamed Script Size (4 bytes): Size of the largest streamed (external) script.
  • Number of Streamed Scripts (4 bytes): Total number of streamed scripts.
  • Streamed Script Details (variable): For each streamed script, a 28-byte structure consisting of filename (20 bytes, null-padded), offset (4 bytes), and size (4 bytes).
  • Unknown Field (4 bytes): An unused or unknown 4-byte integer.
  • Global Variable Space Size (4 bytes): Allocated size for global variables in bytes.
  • Build Number (4 bytes): The build or version number of the script compiler.

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

.CS File Properties Dumper
Drag and drop a .CS file here

4. Python class for .CS file handling

import struct

class CSFile:
    def __init__(self, filename):
        self.filename = filename
        self.properties = {}
        self.data = None
        self.parse()

    def parse(self):
        with open(self.filename, 'rb') as f:
            self.data = f.read()
        position = 0

        def read_uint8(pos):
            return self.data[pos], pos + 1

        def read_uint16_le(pos):
            return struct.unpack_from('<H', self.data, pos)[0], pos + 2

        def read_int32_le(pos):
            return struct.unpack_from('<i', self.data, pos)[0], pos + 4

        def read_uint32_le(pos):
            return struct.unpack_from('<I', self.data, pos)[0], pos + 4

        def read_string(pos, length):
            str_bytes = self.data[pos:pos + length]
            return str_bytes.split(b'\x00')[0].decode('utf-8'), pos + length

        def parse_jump(pos):
            opcode, pos = read_uint16_le(pos)
            if opcode != 0x0002:
                raise ValueError('Invalid jump opcode')
            type_code, pos = read_uint8(pos)
            if type_code != 0x01:
                raise ValueError('Invalid jump type')
            offset, pos = read_int32_le(pos)
            return offset

        # First chunk
        offset = parse_jump(position)
        position = offset
        target_game, position = read_uint8(position)
        self.properties['targetGame'] = chr(target_game)

        # Second chunk: Models
        offset = parse_jump(position)
        position = offset
        chunk_index, position = read_uint8(position)
        if chunk_index != 0:
            raise ValueError('Invalid chunk 0')
        num_models, position = read_uint32_le(position)
        self.properties['numModels'] = num_models
        self.properties['modelNames'] = []
        for _ in range(num_models):
            name, position = read_string(position, 24)
            self.properties['modelNames'].append(name)

        # Third chunk: Missions
        offset = parse_jump(position)
        position = offset
        chunk_index, position = read_uint8(position)
        if chunk_index != 1:
            raise ValueError('Invalid chunk 1')
        main_size, position = read_uint32_le(position)
        self.properties['mainSize'] = main_size
        largest_mission, position = read_uint32_le(position)
        self.properties['largestMissionSize'] = largest_mission
        num_missions, position = read_uint16_le(position)
        self.properties['numMissions'] = num_missions
        num_exclusive, position = read_uint16_le(position)
        self.properties['numExclusiveMissions'] = num_exclusive
        largest_locals, position = read_uint32_le(position)
        self.properties['largestMissionLocals'] = largest_locals
        self.properties['missionOffsets'] = []
        for _ in range(num_missions):
            off, position = read_uint32_le(position)
            self.properties['missionOffsets'].append(off)

        # Fourth chunk: Streamed
        offset = parse_jump(position)
        position = offset
        chunk_index, position = read_uint8(position)
        if chunk_index != 2:
            raise ValueError('Invalid chunk 2')
        largest_streamed, position = read_uint32_le(position)
        self.properties['largestStreamedSize'] = largest_streamed
        num_streamed, position = read_uint32_le(position)
        self.properties['numStreamed'] = num_streamed
        self.properties['streamedDetails'] = []
        for _ in range(num_streamed):
            name, position = read_string(position, 20)
            off, position = read_uint32_le(position)
            size, position = read_uint32_le(position)
            self.properties['streamedDetails'].append({'name': name, 'offset': off, 'size': size})

        # Fifth chunk: Unknown
        offset = parse_jump(position)
        position = offset
        chunk_index, position = read_uint8(position)
        if chunk_index != 3:
            raise ValueValue('Invalid chunk 3')
        unknown, position = read_uint32_le(position)
        self.properties['unknown'] = unknown

        # Sixth chunk: Global and build
        offset = parse_jump(position)
        position = offset
        chunk_index, position = read_uint8(position)
        if chunk_index != 4:
            raise ValueError('Invalid chunk 4')
        global_size, position = read_uint32_le(position)
        self.properties['globalVarSize'] = global_size
        build, position = read_uint32_le(position)
        self.properties['buildNumber'] = build

    def print_properties(self):
        for key, value in self.properties.items():
            if isinstance(value, list):
                print(f"{key}: {value}")
            else:
                print(f"{key}: {value}")

    def write(self, new_filename=None):
        if new_filename is None:
            new_filename = self.filename
        with open(new_filename, 'wb') as f:
            f.write(self.data)  # Writes back the original data; modify self.data for changes

# Example usage
# cs = CSFile('example.cs')
# cs.print_properties()
# cs.write('modified.cs')

5. Java class for .CS file handling

import java.io.*;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.ArrayList;
import java.util.List;

public class CSFile {
    private String filename;
    private byte[] data;
    private final ByteBuffer buffer;
    private final List<Object> properties = new ArrayList<>();

    public CSFile(String filename) throws IOException {
        this.filename = filename;
        try (FileInputStream fis = new FileInputStream(filename)) {
            data = fis.readAllBytes();
        }
        buffer = ByteBuffer.wrap(data).order(ByteOrder.LITTLE_ENDIAN);
        parse();
    }

    private int parseJump() throws IOException {
        int opcode = buffer.getShort() & 0xFFFF;
        if (opcode != 0x0002) throw new IOException("Invalid jump opcode");
        int type = buffer.get() & 0xFF;
        if (type != 0x01) throw new IOException("Invalid jump type");
        return buffer.getInt();
    }

    private void parse() throws IOException {
        buffer.position(0);

        // First chunk
        int offset = parseJump();
        buffer.position(offset);
        char targetGame = (char) buffer.get();
        properties.add("targetGame: " + targetGame);

        // Second chunk: Models
        offset = parseJump();
        buffer.position(offset);
        int chunkIndex = buffer.get() & 0xFF;
        if (chunkIndex != 0) throw new IOException("Invalid chunk 0");
        int numModels = buffer.getInt();
        properties.add("numModels: " + numModels);
        List<String> modelNames = new ArrayList<>();
        for (int i = 0; i < numModels; i++) {
            byte[] nameBytes = new byte[24];
            buffer.get(nameBytes);
            String name = new String(nameBytes).trim();
            modelNames.add(name);
        }
        properties.add("modelNames: " + modelNames);

        // Third chunk: Missions
        offset = parseJump();
        buffer.position(offset);
        chunkIndex = buffer.get() & 0xFF;
        if (chunkIndex != 1) throw new IOException("Invalid chunk 1");
        int mainSize = buffer.getInt();
        properties.add("mainSize: " + mainSize);
        int largestMissionSize = buffer.getInt();
        properties.add("largestMissionSize: " + largestMissionSize);
        int numMissions = buffer.getShort() & 0xFFFF;
        properties.add("numMissions: " + numMissions);
        int numExclusive = buffer.getShort() & 0xFFFF;
        properties.add("numExclusiveMissions: " + numExclusive);
        int largestLocals = buffer.getInt();
        properties.add("largestMissionLocals: " + largestLocals);
        List<Integer> missionOffsets = new ArrayList<>();
        for (int i = 0; i < numMissions; i++) {
            missionOffsets.add(buffer.getInt());
        }
        properties.add("missionOffsets: " + missionOffsets);

        // Fourth chunk: Streamed
        offset = parseJump();
        buffer.position(offset);
        chunkIndex = buffer.get() & 0xFF;
        if (chunkIndex != 2) throw new IOException("Invalid chunk 2");
        int largestStreamed = buffer.getInt();
        properties.add("largestStreamedSize: " + largestStreamed);
        int numStreamed = buffer.getInt();
        properties.add("numStreamed: " + numStreamed);
        List<String> streamedDetails = new ArrayList<>();
        for (int i = 0; i < numStreamed; i++) {
            byte[] nameBytes = new byte[20];
            buffer.get(nameBytes);
            String name = new String(nameBytes).trim();
            int off = buffer.getInt();
            int size = buffer.getInt();
            streamedDetails.add("{" + name + ", offset: " + off + ", size: " + size + "}");
        }
        properties.add("streamedDetails: " + streamedDetails);

        // Fifth chunk: Unknown
        offset = parseJump();
        buffer.position(offset);
        chunkIndex = buffer.get() & 0xFF;
        if (chunkIndex != 3) throw new IOException("Invalid chunk 3");
        int unknown = buffer.getInt();
        properties.add("unknown: " + unknown);

        // Sixth chunk: Global and build
        offset = parseJump();
        buffer.position(offset);
        chunkIndex = buffer.get() & 0xFF;
        if (chunkIndex != 4) throw new IOException("Invalid chunk 4");
        int globalSize = buffer.getInt();
        properties.add("globalVarSize: " + globalSize);
        int build = buffer.getInt();
        properties.add("buildNumber: " + build);
    }

    public void printProperties() {
        for (Object prop : properties) {
            System.out.println(prop);
        }
    }

    public void write(String newFilename) throws IOException {
        if (newFilename == null) newFilename = filename;
        try (FileOutputStream fos = new FileOutputStream(newFilename)) {
            fos.write(data);  // Writes original data; update 'data' for modifications
        }
    }

    // Example usage
    // public static void main(String[] args) throws IOException {
    //     CSFile cs = new CSFile("example.cs");
    //     cs.printProperties();
    //     cs.write("modified.cs");
    // }
}

6. JavaScript class for .CS file handling (Node.js)

const fs = require('fs');

class CSFile {
    constructor(filename) {
        this.filename = filename;
        this.properties = {};
        this.data = fs.readFileSync(filename);
        this.position = 0;
        this.parse();
    }

    readUint8() {
        return this.data[this.position++];
    }

    readUint16LE() {
        return this.data.readUInt16LE(this.position, this.position += 2);
    }

    readInt32LE() {
        return this.data.readInt32LE(this.position, this.position += 4);
    }

    readUint32LE() {
        return this.data.readUInt32LE(this.position, this.position += 4);
    }

    readString(length) {
        const strBuffer = this.data.slice(this.position, this.position + length);
        this.position += length;
        return strBuffer.toString('utf8').split('\0')[0].trim();
    }

    parseJump() {
        const opcode = this.readUint16LE();
        if (opcode !== 0x0002) throw new Error('Invalid jump opcode');
        const type = this.readUint8();
        if (type !== 0x01) throw new Error('Invalid jump type');
        return this.readInt32LE();
    }

    parse() {
        // First chunk
        let offset = this.parseJump();
        this.position = offset;
        this.properties.targetGame = String.fromCharCode(this.readUint8());

        // Second chunk: Models
        offset = this.parseJump();
        this.position = offset;
        let chunkIndex = this.readUint8();
        if (chunkIndex !== 0) throw new Error('Invalid chunk 0');
        this.properties.numModels = this.readUint32LE();
        this.properties.modelNames = [];
        for (let i = 0; i < this.properties.numModels; i++) {
            this.properties.modelNames.push(this.readString(24));
        }

        // Third chunk: Missions
        offset = this.parseJump();
        this.position = offset;
        chunkIndex = this.readUint8();
        if (chunkIndex !== 1) throw new Error('Invalid chunk 1');
        this.properties.mainSize = this.readUint32LE();
        this.properties.largestMissionSize = this.readUint32LE();
        this.properties.numMissions = this.readUint16LE();
        this.properties.numExclusiveMissions = this.readUint16LE();
        this.properties.largestMissionLocals = this.readUint32LE();
        this.properties.missionOffsets = [];
        for (let i = 0; i < this.properties.numMissions; i++) {
            this.properties.missionOffsets.push(this.readUint32LE());
        }

        // Fourth chunk: Streamed
        offset = this.parseJump();
        this.position = offset;
        chunkIndex = this.readUint8();
        if (chunkIndex !== 2) throw new Error('Invalid chunk 2');
        this.properties.largestStreamedSize = this.readUint32LE();
        this.properties.numStreamed = this.readUint32LE();
        this.properties.streamedDetails = [];
        for (let i = 0; i < this.properties.numStreamed; i++) {
            const name = this.readString(20);
            const off = this.readUint32LE();
            const size = this.readUint32LE();
            this.properties.streamedDetails.push({name, offset: off, size});
        }

        // Fifth chunk: Unknown
        offset = this.parseJump();
        this.position = offset;
        chunkIndex = this.readUint8();
        if (chunkIndex !== 3) throw new Error('Invalid chunk 3');
        this.properties.unknown = this.readUint32LE();

        // Sixth chunk: Global and build
        offset = this.parseJump();
        this.position = offset;
        chunkIndex = this.readUint8();
        if (chunkIndex !== 4) throw new Error('Invalid chunk 4');
        this.properties.globalVarSize = this.readUint32LE();
        this.properties.buildNumber = this.readUint32LE();
    }

    printProperties() {
        for (const [key, value] of Object.entries(this.properties)) {
            if (Array.isArray(value)) {
                console.log(`${key}: ${JSON.stringify(value)}`);
            } else {
                console.log(`${key}: ${value}`);
            }
        }
    }

    write(newFilename = this.filename) {
        fs.writeFileSync(newFilename, this.data);  // Writes original; modify this.data for changes
    }
}

// Example usage
// const cs = new CSFile('example.cs');
// cs.printProperties();
// cs.write('modified.cs');

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

#include <fstream>
#include <iostream>
#include <vector>
#include <string>
#include <cstdint>
#include <cstring>

class CSFile {
private:
    std::string filename;
    std::vector<uint8_t> data;
    std::vector<std::string> properties;

    uint8_t readUint8(size_t& pos) {
        return data[pos++];
    }

    uint16_t readUint16LE(size_t& pos) {
        uint16_t val = (data[pos + 1] << 8) | data[pos];
        pos += 2;
        return val;
    }

    int32_t readInt32LE(size_t& pos) {
        int32_t val = (data[pos + 3] << 24) | (data[pos + 2] << 16) | (data[pos + 1] << 8) | data[pos];
        pos += 4;
        return val;
    }

    uint32_t readUint32LE(size_t& pos) {
        uint32_t val = (data[pos + 3] << 24) | (data[pos + 2] << 16) | (data[pos + 1] << 8) | data[pos];
        pos += 4;
        return val;
    }

    std::string readString(size_t& pos, size_t length) {
        std::string str(reinterpret_cast<char*>(&data[pos]), length);
        pos += length;
        str.erase(str.find('\0'));
        return str;
    }

    int32_t parseJump(size_t& pos) {
        uint16_t opcode = readUint16LE(pos);
        if (opcode != 0x0002) throw std::runtime_error("Invalid jump opcode");
        uint8_t type = readUint8(pos);
        if (type != 0x01) throw std::runtime_error("Invalid jump type");
        return readInt32LE(pos);
    }

public:
    CSFile(const std::string& fn) : filename(fn) {
        std::ifstream file(filename, std::ios::binary | std::ios::ate);
        size_t size = file.tellg();
        data.resize(size);
        file.seekg(0);
        file.read(reinterpret_cast<char*>(data.data()), size);
        size_t pos = 0;
        parse(pos);
    }

    void parse(size_t& pos) {
        // First chunk
        int32_t offset = parseJump(pos);
        pos = offset;
        char targetGame = readUint8(pos);
        properties.push_back("targetGame: " + std::string(1, targetGame));

        // Second chunk: Models
        offset = parseJump(pos);
        pos = offset;
        uint8_t chunkIndex = readUint8(pos);
        if (chunkIndex != 0) throw std::runtime_error("Invalid chunk 0");
        uint32_t numModels = readUint32LE(pos);
        properties.push_back("numModels: " + std::to_string(numModels));
        std::string modelNamesStr = "modelNames: [";
        for (uint32_t i = 0; i < numModels; ++i) {
            std::string name = readString(pos, 24);
            modelNamesStr += name + ", ";
        }
        if (numModels > 0) modelNamesStr.erase(modelNamesStr.size() - 2);
        modelNamesStr += "]";
        properties.push_back(modelNamesStr);

        // Third chunk: Missions
        offset = parseJump(pos);
        pos = offset;
        chunkIndex = readUint8(pos);
        if (chunkIndex != 1) throw std::runtime_error("Invalid chunk 1");
        uint32_t mainSize = readUint32LE(pos);
        properties.push_back("mainSize: " + std::to_string(mainSize));
        uint32_t largestMissionSize = readUint32LE(pos);
        properties.push_back("largestMissionSize: " + std::to_string(largestMissionSize));
        uint16_t numMissions = readUint16LE(pos);
        properties.push_back("numMissions: " + std::to_string(numMissions));
        uint16_t numExclusive = readUint16LE(pos);
        properties.push_back("numExclusiveMissions: " + std::to_string(numExclusive));
        uint32_t largestLocals = readUint32LE(pos);
        properties.push_back("largestMissionLocals: " + std::to_string(largestLocals));
        std::string offsetsStr = "missionOffsets: [";
        for (uint16_t i = 0; i < numMissions; ++i) {
            uint32_t off = readUint32LE(pos);
            offsetsStr += std::to_string(off) + ", ";
        }
        if (numMissions > 0) offsetsStr.erase(offsetsStr.size() - 2);
        offsetsStr += "]";
        properties.push_back(offsetsStr);

        // Fourth chunk: Streamed
        offset = parseJump(pos);
        pos = offset;
        chunkIndex = readUint8(pos);
        if (chunkIndex != 2) throw std::runtime_error("Invalid chunk 2");
        uint32_t largestStreamed = readUint32LE(pos);
        properties.push_back("largestStreamedSize: " + std::to_string(largestStreamed));
        uint32_t numStreamed = readUint32LE(pos);
        properties.push_back("numStreamed: " + std::to_string(numStreamed));
        std::string streamedStr = "streamedDetails: [";
        for (uint32_t i = 0; i < numStreamed; ++i) {
            std::string name = readString(pos, 20);
            uint32_t off = readUint32LE(pos);
            uint32_t size = readUint32LE(pos);
            streamedStr += "{" + name + ", offset: " + std::to_string(off) + ", size: " + std::to_string(size) + "}, ";
        }
        if (numStreamed > 0) streamedStr.erase(streamedStr.size() - 2);
        streamedStr += "]";
        properties.push_back(streamedStr);

        // Fifth chunk: Unknown
        offset = parseJump(pos);
        pos = offset;
        chunkIndex = readUint8(pos);
        if (chunkIndex != 3) throw std::runtime_error("Invalid chunk 3");
        uint32_t unknown = readUint32LE(pos);
        properties.push_back("unknown: " + std::to_string(unknown));

        // Sixth chunk: Global and build
        offset = parseJump(pos);
        pos = offset;
        chunkIndex = readUint8(pos);
        if (chunkIndex != 4) throw std::runtime_error("Invalid chunk 4");
        uint32_t globalSize = readUint32LE(pos);
        properties.push_back("globalVarSize: " + std::to_string(globalSize));
        uint32_t build = readUint32LE(pos);
        properties.push_back("buildNumber: " + std::to_string(build));
    }

    void printProperties() {
        for (const auto& prop : properties) {
            std::cout << prop << std::endl;
        }
    }

    void write(const std::string& newFilename) {
        std::ofstream file(newFilename.empty() ? filename : newFilename, std::ios::binary);
        file.write(reinterpret_cast<const char*>(data.data()), data.size());
        // Modify data vector for changes before writing
    }
};

// Example usage
// int main() {
//     try {
//         CSFile cs("example.cs");
//         cs.printProperties();
//         cs.write("modified.cs");
//     } catch (const std::exception& e) {
//         std::cerr << e.what() << std::endl;
//     }
//     return 0;
// }