Ok. Time to resurrect this post.
So, i decided to try it again. Basically VEC/VOC/BLB/PRK/RBT etc are all same kind of graphic. A buddy of mine on #openapoc channel on freenode (check them out, they are trying to bring xcom apocalypse back) helped a great deal in deciphering the format.
As it turns out it is a basic paletted image, column-major one, with 256 byte header, header consisting of 2 packs of uint16_t pairs, each 64 size, which control column begins/ends.
I'll attach linux sourcecode for the converter and some results.
Now, looks like wall/floor/ceiling textures themselves were embedded into .exe after all.


Code: Select all
#include <iostream>
#include <fstream>
#include <cstdint>
#include <string>
#include <memory>
#include <array>
#include <algorithm>
#include <cstring>
#include <png++/png.hpp>
struct data_pair
{
uint8_t row_skip;
uint8_t copy_count;
};
static_assert(sizeof(data_pair) == 2, "data_pair wrong size");
struct image
{
std::array<uint16_t,64> offsets;
std::array<data_pair,64> data_pairs;
uint8_t data[];
};
static_assert(sizeof(image) == 256, "image wrong size");
struct colour
{
uint8_t r, g, b;
};
static_assert(sizeof(colour) == 3, "colour wrong size");
struct palette
{
colour c[256];
};
static_assert(sizeof(palette) == 256*3, "palette wrong size");
int main(int argc, char **argv)
{
if (argc != 3)
{
std::cerr << "Must specify input and palette files\nTo example:\n vec_decode.exe VEC_1 BT_PAL\n";
return 1;
}
std::ifstream inFile(argv[1]);
if (!inFile)
{
std::cerr << "Failed to open \"" << argv[1] << "\"\n";
return 1;
}
inFile.seekg(0, inFile.end);
size_t length = inFile.tellg();
inFile.seekg(0, inFile.beg);
std::unique_ptr<char[]> data(new char[length]);
if (!inFile.read(data.get(), length))
{
std::cerr << "Failed to read " << length << " bytes from \"" << argv[1] << "\"\n";
return 1;
}
std::cerr << "Read " << length << " bytes from \"" << argv[1] << "\"\n";
palette p;
std::ifstream inPalette(argv[2]);
if (!inPalette)
{
std::cerr << "Failed to open palette \"" << argv[2] << "\"\n";
return 1;
}
if (!inPalette.read(reinterpret_cast<char*>(&p), sizeof(p)))
{
std::cerr << "Failed to read palette from \"" << argv[2] << "\"\n";
return 1;
}
struct image *img = reinterpret_cast<struct image*>(data.get());
int sizeX = 64;
int sizeY = 64;
png::image<png::rgba_pixel> png(sizeX,sizeY);
//Each record describes a single column
for (int record = 0; record < 64; record++)
{
//There are 'copy_count' pixels in each column, starting after skipping 'row_skip' transparent pixels
for (int b = 0; b < img->data_pairs[record].copy_count; b++)
{
int offset = img->offsets[record] + b;
//The offset is in the file, but the data[] array starts at the end of the (256-byte) header, so take the header size off the offset
offset -= 256;
assert (offset < length);
uint8_t idx = img->data[offset];
png::rgba_pixel pix;
//index 255 is transparent
if (idx == 255)
pix = {0,0,0,0};
else
pix = {p.c[idx].r, p.c[idx].g, p.c[idx].b, 255};
int y = img->data_pairs[record].row_skip + b;
int x = record;
png[y][x] = pix;
}
//Any pixels after a column's 'copy_count' are transparent too
}
png.write("out.png");
return 0;
}