Task 078: .CC File Format
Task 078: .CC File Format
1. Properties of the .CC File Format Intrinsic to Its File System
The .CC file format, as used in King's Bounty, is a binary archive format designed to store multiple files in a compact structure without encryption or subdirectories. It employs a simple header followed by compressed file data, facilitating a flat file system-like organization. The format supports up to 140 files, with filenames replaced by hashes for identification, offsets for direct access, and sizes for boundary definition. This structure enables efficient random access to individual files within the archive, mimicking basic file system operations such as listing and extraction without a hierarchical directory tree.
The intrinsic properties, derived from the format's header and entry structure, are as follows:
- Number of Files: A 16-bit unsigned integer (UINT16LE) at offset 0x00, indicating the total count of stored files (maximum observed: 136; format limit: 140).
- File Entries Array: An array of fixed-size entries (8 bytes each) starting at offset 0x02, with one entry per file. Each entry contains:
- Filename Hash: A 16-bit unsigned integer (UINT16LE) at entry offset 0x00, a computed hash of the original filename (using a specific algorithm involving byte normalization, swapping, rotation, and addition; original names not stored).
- File Offset: A 24-bit unsigned integer (UINT24LE) at entry offset 0x02, specifying the absolute byte offset from the start of the .CC file to the beginning of the file's compressed data (little-endian, 3 bytes).
- Compressed Size: A 24-bit unsigned integer (UINT24LE) at entry offset 0x05, indicating the length in bytes of the compressed data for that file (little-endian, 3 bytes).
- File System Characteristics:
- Flat structure: No support for subdirectories or nesting; all files are at the root level.
- Hashed identification: Filenames are not preserved; retrieval relies on pre-known hashes, limiting direct naming but enhancing compactness.
- Random access: Offsets allow seeking to specific files without sequential parsing.
- Allocation: Offsets are monotonically increasing, with data following the header; no free space management or fragmentation handling.
- Compression integration: Each file's data begins with a 32-bit unsigned integer (UINT32LE) uncompressed size, followed by LZW-compressed content (9-12 bit codewords, dictionary reset on code 0x0100), but this is per-file and not a global property.
- No metadata: Lacks version numbers, timestamps, permissions, or extended attributes; identification relies on header size (at least 1122 bytes for full entries) and offset validity.
These properties ensure a lightweight, game-oriented file system embedded in a single file, prioritizing storage efficiency over flexibility.
2. Two Direct Download Links for Files of Format .CC
Direct downloads for individual .CC files are uncommon due to their bundling in game archives, but the following links provide zipped game distributions containing multiple .CC files (e.g., 416.CC for King's Bounty and XEEN.CC for related formats). Extract the archives to access the .CC files:
- https://www.myabandonware.com/game/king-s-bounty-xg/download/2r5-king-s-bounty-1-0 (King's Bounty, contains 416.CC and other .CC archives)
- https://www.myabandonware.com/game/might-and-magic-world-of-xeen-2n7/download/2n7-might-and-magic-world-of-xeen-1-0 (World of Xeen, contains XEEN.CC)
3. Ghost Blog Embedded HTML JavaScript for Drag-and-Drop .CC File Analysis
The following is an embeddable HTML snippet with JavaScript, suitable for integration into a Ghost blog post (e.g., via the HTML card). It creates a drag-and-drop zone for .CC files, parses the header properties using the File API and ArrayBuffer, and displays them on screen. No external libraries are required; it handles UINT24LE parsing manually.
Drag and drop a .CC file here to analyze its properties.
This code reads the file asynchronously, decodes the properties, and outputs them in a formatted text block. For writing, extend with a download link using Blob (not implemented here for brevity).
4. Python Class for .CC File Handling
The following Python class opens a .CC file, decodes and reads the properties, prints them to the console, and includes a basic write method to reconstruct the header from properties (data sections omitted for simplicity, as they require compression).
import struct
import os
class CCFile:
def __init__(self, filename):
self.filename = filename
self.num_files = 0
self.entries = [] # List of (hash, offset, comp_size)
self.load()
def load(self):
with open(self.filename, 'rb') as f:
data = f.read()
offset = 0
self.num_files = struct.unpack_from('<H', data, offset)[0] # UINT16LE
offset += 2
print(f"Number of Files: {self.num_files}")
print("\nFile Entries:")
for i in range(self.num_files):
hash_val, = struct.unpack_from('<H', data, offset) # UINT16LE
offset += 2
off_bytes = struct.unpack_from('<BBB', data, offset) # 3 bytes for UINT24LE
file_offset = off_bytes[0] | (off_bytes[1] << 8) | (off_bytes[2] << 16)
offset += 3
size_bytes = struct.unpack_from('<BBB', data, offset) # 3 bytes for UINT24LE
comp_size = size_bytes[0] | (size_bytes[1] << 8) | (size_bytes[2] << 16)
offset += 3
self.entries.append((hash_val, file_offset, comp_size))
print(f"Entry {i+1}:")
print(f" Hash: 0x{hash_val:04X}")
print(f" Offset: 0x{file_offset:06X}")
print(f" Compressed Size: {comp_size} bytes")
def write_header(self, output_filename, entries=None):
"""Writes the header to a new file using current or provided entries. Data sections must be appended manually."""
if entries is None:
entries = self.entries
num_files = len(entries)
header_size = 2 + 8 * num_files
with open(output_filename, 'wb') as f:
f.write(struct.pack('<H', num_files)) # UINT16LE
for hash_val, file_offset, comp_size in entries:
# Adjust offsets if needed; assumes data follows header
actual_offset = header_size + sum(comp_size for _, _, cs in entries[:entries.index((hash_val, file_offset, comp_size))]) if file_offset == 0 else file_offset
f.write(struct.pack('<H', hash_val)) # UINT16LE
f.write(struct.pack('<BBB', actual_offset & 0xFF, (actual_offset >> 8) & 0xFF, (actual_offset >> 16) & 0xFF)) # UINT24LE
f.write(struct.pack('<BBB', comp_size & 0xFF, (comp_size >> 8) & 0xFF, (comp_size >> 16) & 0xFF)) # UINT24LE
print(f"Header written to {output_filename}")
Usage: cc = CCFile('example.cc')
to load and print. Call cc.write_header('output.cc')
to write the header.
5. Java Class for .CC File Handling
The following Java class uses NIO for file I/O, decodes the properties upon instantiation, prints them to the console, and provides a write method for the header.
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;
public class CCFile {
private String filename;
private int numFiles;
private List<Entry> entries = new ArrayList<>();
public static class Entry {
int hash;
long offset; // UINT24 as long
int compSize;
public Entry(int hash, long offset, int compSize) {
this.hash = hash;
this.offset = offset;
this.compSize = compSize;
}
}
public CCFile(String filename) {
this.filename = filename;
load();
}
private void load() {
Path path = Paths.get(filename);
try (RandomAccessFile file = new RandomAccessFile(filename, "r")) {
byte[] buffer = new byte[(int) Files.size(path)];
file.readFully(buffer);
ByteBuffer bb = ByteBuffer.wrap(buffer).order(ByteOrder.LITTLE_ENDIAN);
numFiles = bb.getShort() & 0xFFFF; // UINT16LE
System.out.println("Number of Files: " + numFiles);
System.out.println("\nFile Entries:");
int globalOffset = 2;
for (int i = 0; i < numFiles; i++) {
int hash = bb.getShort(globalOffset) & 0xFFFF; // UINT16LE
globalOffset += 2;
int off1 = bb.get(globalOffset) & 0xFF;
int off2 = bb.get(globalOffset + 1) & 0xFF;
int off3 = bb.get(globalOffset + 2) & 0xFF;
long fileOffset = ((off3 << 16L) | (off2 << 8L) | off1); // UINT24LE
globalOffset += 3;
int size1 = bb.get(globalOffset) & 0xFF;
int size2 = bb.get(globalOffset + 1) & 0xFF;
int size3 = bb.get(globalOffset + 2) & 0xFF;
int compSize = (size3 << 16) | (size2 << 8) | size1; // UINT24LE
globalOffset += 3;
entries.add(new Entry(hash, fileOffset, compSize));
System.out.println("Entry " + (i + 1) + ":");
System.out.println(" Hash: 0x" + Integer.toHexString(hash).toUpperCase().toString().toUpperCase());
System.out.println(" Offset: 0x" + Long.toHexString(fileOffset).toUpperCase().substring(0, 6));
System.out.println(" Compressed Size: " + compSize + " bytes");
}
} catch (IOException e) {
e.printStackTrace();
}
}
public void writeHeader(String outputFilename, List<Entry> customEntries) {
// Simplified header write; data append required separately
int num = customEntries.size();
int headerSize = 2 + 8 * num;
try (java.io.FileOutputStream fos = new java.io.FileOutputStream(outputFilename)) {
ByteBuffer bb = ByteBuffer.allocate(headerSize).order(ByteOrder.LITTLE_ENDIAN);
bb.putShort((short) num); // UINT16LE
long currentOffset = headerSize;
for (Entry e : customEntries) {
bb.putShort((short) e.hash); // UINT16LE
int off = (int) currentOffset; // Assume sequential
bb.put((byte) (off & 0xFF));
bb.put((byte) ((off >> 8) & 0xFF));
bb.put((byte) ((off >> 16) & 0xFF)); // UINT24LE
bb.put((byte) (e.compSize & 0xFF));
bb.put((byte) ((e.compSize >> 8) & 0xFF));
bb.put((byte) ((e.compSize >> 16) & 0xFF)); // UINT24LE
currentOffset += e.compSize;
}
fos.write(bb.array());
System.out.println("Header written to " + outputFilename);
} catch (IOException e) {
e.printStackTrace();
}
}
}
Usage: new CCFile("example.cc");
to load and print. Use writeHeader
with a list of Entry objects for header output.
6. JavaScript Class for .CC File Handling (Node.js)
The following Node.js class uses the fs
module to open a .CC file, decode and read the properties, print them to the console, and includes a write method for the header.
const fs = require('fs').promises;
const path = require('path');
class CCFile {
constructor(filename) {
this.filename = filename;
this.numFiles = 0;
this.entries = [];
this.load();
}
async load() {
try {
const buffer = await fs.readFile(this.filename);
let offset = 0;
this.numFiles = buffer.readUInt16LE(offset); // UINT16LE
offset += 2;
console.log(`Number of Files: ${this.numFiles}`);
console.log('\nFile Entries:');
for (let i = 0; i < this.numFiles; i++) {
const hash = buffer.readUInt16LE(offset); // UINT16LE
offset += 2;
const fileOffset = buffer.readUIntLE(offset, 3); // UINT24LE (custom, as readUIntLE supports length)
offset += 3;
const compSize = buffer.readUIntLE(offset, 3); // UINT24LE
offset += 3;
this.entries.push({ hash, offset: fileOffset, compSize });
console.log(`Entry ${i + 1}:`);
console.log(` Hash: 0x${hash.toString(16).toUpperCase().padStart(4, '0')}`);
console.log(` Offset: 0x${fileOffset.toString(16).toUpperCase().padStart(6, '0')}`);
console.log(` Compressed Size: ${compSize} bytes`);
}
} catch (err) {
console.error('Error loading file:', err);
}
}
async writeHeader(outputFilename, customEntries = null) {
if (customEntries === null) customEntries = this.entries;
const num = customEntries.length;
const headerSize = 2 + 8 * num;
const buffer = Buffer.alloc(headerSize);
let bufOffset = 0;
buffer.writeUInt16LE(num, bufOffset); // UINT16LE
bufOffset += 2;
let currentOffset = headerSize;
for (const entry of customEntries) {
buffer.writeUInt16LE(entry.hash, bufOffset); // UINT16LE
bufOffset += 2;
const off = currentOffset; // Sequential assumption
buffer.writeUIntLE(off, bufOffset, 3); // UINT24LE
bufOffset += 3;
buffer.writeUIntLE(entry.compSize, bufOffset, 3); // UINT24LE
bufOffset += 3;
currentOffset += entry.compSize;
}
await fs.writeFile(outputFilename, buffer);
console.log(`Header written to ${outputFilename}`);
}
}
module.exports = CCFile;
Usage: const cc = new CCFile('example.cc');
to load and print. Call await cc.writeHeader('output.cc');
for header output. Run with Node.js.
7. C Code for .CC File Handling (Functions as "Class")
The following C code implements functions simulating a class: load_cc
opens and decodes a .CC file, printing properties to stdout; write_cc_header
writes the header to a file. Compile with gcc cc_handler.c -o cc_handler
.
#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>
typedef struct {
uint16_t hash;
uint32_t offset; // UINT24 as uint32_t
uint32_t comp_size; // UINT24 as uint32_t
} CCEntry;
typedef struct {
uint16_t num_files;
CCEntry *entries;
} CCFile;
void load_cc(const char *filename) {
FILE *f = fopen(filename, "rb");
if (!f) {
perror("Error opening file");
return;
}
uint8_t header[2];
fread(header, 1, 2, f);
uint16_t num_files = (uint16_t)(header[0] | (header[1] << 8)); // UINT16LE
printf("Number of Files: %u\n", num_files);
printf("\nFile Entries:\n");
CCFile cc = {num_files, malloc(num_files * sizeof(CCEntry))};
for (uint16_t i = 0; i < num_files; i++) {
uint8_t entry[8];
fread(entry, 1, 8, f);
uint16_t hash = (uint16_t)(entry[0] | (entry[1] << 8)); // UINT16LE
uint32_t file_offset = (uint32_t)(entry[2] | (entry[3] << 8) | (entry[4] << 16)); // UINT24LE
uint32_t comp_size = (uint32_t)(entry[5] | (entry[6] << 8) | (entry[7] << 16)); // UINT24LE
cc.entries[i].hash = hash;
cc.entries[i].offset = file_offset;
cc.entries[i].comp_size = comp_size;
printf("Entry %u:\n", i + 1);
printf(" Hash: 0x%04X\n", hash);
printf(" Offset: 0x%06X\n", file_offset);
printf(" Compressed Size: %u bytes\n", comp_size);
}
fclose(f);
free(cc.entries); // Clean up
}
void write_cc_header(const char *output_filename, CCFile *cc) {
FILE *f = fopen(output_filename, "wb");
if (!f) {
perror("Error writing file");
return;
}
uint8_t num_buf[2];
num_buf[0] = (uint8_t)(cc->num_files & 0xFF);
num_buf[1] = (uint8_t)((cc->num_files >> 8) & 0xFF);
fwrite(num_buf, 1, 2, f); // UINT16LE
uint32_t current_offset = 2 + 8 * cc->num_files;
for (uint16_t i = 0; i < cc->num_files; i++) {
uint8_t entry_buf[8];
// Hash UINT16LE
entry_buf[0] = (uint8_t)(cc->entries[i].hash & 0xFF);
entry_buf[1] = (uint8_t)((cc->entries[i].hash >> 8) & 0xFF);
// Offset UINT24LE (use current_offset for sequential)
uint32_t off = current_offset;
entry_buf[2] = (uint8_t)(off & 0xFF);
entry_buf[3] = (uint8_t)((off >> 8) & 0xFF);
entry_buf[4] = (uint8_t)((off >> 16) & 0xFF);
// Comp size UINT24LE
entry_buf[5] = (uint8_t)(cc->entries[i].comp_size & 0xFF);
entry_buf[6] = (uint8_t)((cc->entries[i].comp_size >> 8) & 0xFF);
entry_buf[7] = (uint8_t)((cc->entries[i].comp_size >> 16) & 0xFF);
fwrite(entry_buf, 1, 8, f);
current_offset += cc->entries[i].comp_size;
}
fclose(f);
printf("Header written to %s\n", output_filename);
}
// Example usage in main:
// int main() { load_cc("example.cc"); return 0; }
Usage: Call load_cc("example.cc");
to open, decode, and print. Prepare a CCFile
struct and call write_cc_header("output.cc", &cc);
for header output. Data sections must be appended post-header.