2025-02-08 19:58:54 -05:00
|
|
|
#include "iwifile.h"
|
|
|
|
|
#include "ddsfile.h"
|
|
|
|
|
|
|
|
|
|
IWIFile::IWIFile() :
|
|
|
|
|
fileStem(),
|
|
|
|
|
header(),
|
|
|
|
|
info(),
|
|
|
|
|
mipmaps() {
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
IWIFile::IWIFile(const QString &aFilePath) :
|
|
|
|
|
fileStem(aFilePath.split('.').first().split('/').last()) {
|
|
|
|
|
|
|
|
|
|
QFile file(aFilePath);
|
|
|
|
|
if (!file.open(QIODevice::ReadOnly)) {
|
|
|
|
|
qDebug() << "Error: Unable to open IWI file" << aFilePath;
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
QDataStream in(&file);
|
|
|
|
|
in.setByteOrder(QDataStream::LittleEndian);
|
|
|
|
|
|
|
|
|
|
// Read header
|
|
|
|
|
in.readRawData(reinterpret_cast<char*>(&header), sizeof(IWIHeader));
|
|
|
|
|
|
|
|
|
|
if (!header.isSupported()) {
|
|
|
|
|
qDebug() << "Error: Unsupported IWI version!";
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Adjust seek position for MW2/MW3
|
|
|
|
|
if (header.Version == 0x08) {
|
|
|
|
|
file.seek(0x08);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Read info
|
|
|
|
|
in.readRawData(reinterpret_cast<char*>(&info), sizeof(IWIInfo));
|
|
|
|
|
|
|
|
|
|
// Determine offset array size
|
|
|
|
|
QVector<qint32> offsets((header.Version == 0x1B) ? 8 : 4);
|
|
|
|
|
|
|
|
|
|
if (header.Version == 0x0D || header.Version == 0x1B) {
|
|
|
|
|
file.seek(header.Version == 0x1B ? 0x20 : 0x10);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Read offsets
|
|
|
|
|
for (qint32 &offset : offsets) {
|
|
|
|
|
in >> offset;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Compute mipmaps
|
|
|
|
|
qint64 currentPos = file.pos();
|
|
|
|
|
qint64 fileSize = file.size();
|
|
|
|
|
QVector<quint64> positions = QVector<quint64>();
|
|
|
|
|
|
|
|
|
|
for (int i = 0; i < offsets.size(); i++) {
|
|
|
|
|
IWIMipmap m;
|
|
|
|
|
if (i == 0) {
|
|
|
|
|
m.offset = offsets[i];
|
|
|
|
|
m.size = fileSize - offsets[i];
|
|
|
|
|
} else if (i == offsets.size() - 1) {
|
|
|
|
|
m.offset = currentPos;
|
|
|
|
|
m.size = offsets[i] - currentPos;
|
|
|
|
|
} else {
|
|
|
|
|
m.offset = offsets[i];
|
|
|
|
|
m.size = offsets[i - 1] - offsets[i];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
quint64 pos = file.pos();
|
|
|
|
|
|
|
|
|
|
// Read mipmap
|
|
|
|
|
file.seek(m.offset);
|
|
|
|
|
m.data = file.read(m.size);
|
|
|
|
|
|
|
|
|
|
file.seek(pos);
|
|
|
|
|
mipmaps.append(m);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Close file
|
|
|
|
|
file.close();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
IWIFile::IWIFile(DDSFile &aDDSFile) {
|
|
|
|
|
// Ensure the DDS file has mipmaps
|
|
|
|
|
if (aDDSFile.mipmaps.isEmpty()) {
|
|
|
|
|
qDebug() << "Error: No mipmaps found in DDS file!";
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Create IWIFile object
|
|
|
|
|
fileStem = aDDSFile.fileStem;
|
|
|
|
|
|
|
|
|
|
// Set header (Magic & Version)
|
|
|
|
|
memcpy(header.Magic, "IWi", 3);
|
|
|
|
|
header.Version = 0x06; // Default to CoD4/CoD5 version
|
|
|
|
|
|
|
|
|
|
// Set IWI Texture Info
|
|
|
|
|
info.Format = static_cast<quint8>(aDDSFile.header.pixelFormat.format);
|
|
|
|
|
info.Width = aDDSFile.header.width;
|
|
|
|
|
info.Height = aDDSFile.header.height;
|
|
|
|
|
info.Depth = 1; // Default depth for 2D textures
|
|
|
|
|
info.Usage = 0; // No specific usage
|
|
|
|
|
|
|
|
|
|
// Convert DDS mipmaps to IWI mipmaps
|
|
|
|
|
int offset = 28; // Standard offset start
|
|
|
|
|
for (const auto& ddsMipmap : aDDSFile.mipmaps) {
|
|
|
|
|
IWIMipmap iwiMipmap;
|
|
|
|
|
iwiMipmap.offset = offset;
|
|
|
|
|
iwiMipmap.size = ddsMipmap.data.size();
|
|
|
|
|
iwiMipmap.data = ddsMipmap.data;
|
|
|
|
|
|
|
|
|
|
offset += iwiMipmap.size;
|
|
|
|
|
mipmaps.append(iwiMipmap);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
IWIFile::IWIFile(const IWIFile &iwiFile) {
|
|
|
|
|
fileStem = iwiFile.fileStem;
|
|
|
|
|
header = iwiFile.header;
|
|
|
|
|
info = iwiFile.info;
|
|
|
|
|
mipmaps = iwiFile.mipmaps;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
IWIFile &IWIFile::operator=(const IWIFile &other) {
|
|
|
|
|
if (this != &other) {
|
|
|
|
|
fileStem = other.fileStem;
|
|
|
|
|
header = other.header;
|
|
|
|
|
info = other.info;
|
|
|
|
|
mipmaps = other.mipmaps;
|
|
|
|
|
}
|
|
|
|
|
return *this;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
IWIFile::~IWIFile() {
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void IWIFile::SetupExportDirs() {
|
|
|
|
|
QDir dir = QDir::currentPath();
|
|
|
|
|
if (!dir.exists("exports/")) {
|
|
|
|
|
dir.mkdir("exports/");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!dir.exists("exports/iwi/")) {
|
|
|
|
|
dir.mkdir("exports/iwi/");
|
|
|
|
|
}
|
|
|
|
|
if (!dir.exists("exports/dds/")) {
|
|
|
|
|
dir.mkdir("exports/dds/");
|
|
|
|
|
}
|
|
|
|
|
if (!dir.exists("exports/png/")) {
|
|
|
|
|
dir.mkdir("exports/png/");
|
|
|
|
|
}
|
|
|
|
|
if (!dir.exists("exports/jpg/")) {
|
|
|
|
|
dir.mkdir("exports/jpg/");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool IWIFile::SaveIWI() {
|
|
|
|
|
SetupExportDirs();
|
|
|
|
|
QFile file("exports/iwi/" + fileStem + ".iwi");
|
|
|
|
|
if (!file.open(QIODevice::WriteOnly)) {
|
|
|
|
|
qDebug() << "Error: Unable to write IWI file " << fileStem + ".iwi";
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
QDataStream out(&file);
|
|
|
|
|
out.setByteOrder(QDataStream::LittleEndian);
|
|
|
|
|
|
|
|
|
|
// Write IWI Header
|
|
|
|
|
out.writeRawData(reinterpret_cast<const char*>(&header), sizeof(IWIHeader));
|
|
|
|
|
|
|
|
|
|
// Adjust seek position for MW2/MW3
|
|
|
|
|
if (header.Version == 0x08) {
|
|
|
|
|
file.seek(0x08);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Write IWI Texture Info
|
|
|
|
|
out.writeRawData(reinterpret_cast<const char*>(&info), sizeof(IWIInfo));
|
|
|
|
|
|
|
|
|
|
// Determine number of mipmaps based on version
|
|
|
|
|
QVector<qint32> offsets((header.Version == 0x1B) ? 8 : 4);
|
2025-04-23 00:09:35 -04:00
|
|
|
size_t currentOffset = file.pos() + (offsets.size() * sizeof(qint32));
|
2025-02-08 19:58:54 -05:00
|
|
|
|
|
|
|
|
// Compute offsets
|
|
|
|
|
for (int i = 0; i < offsets.size(); i++) {
|
2025-04-23 00:09:35 -04:00
|
|
|
offsets[i] = static_cast<qint32>(currentOffset);
|
2025-02-08 19:58:54 -05:00
|
|
|
if (mipmaps.size() && i < mipmaps.size()) {
|
|
|
|
|
currentOffset += mipmaps[i].size;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Seek and write offsets
|
|
|
|
|
if (header.Version == 0x0D || header.Version == 0x1B) {
|
|
|
|
|
file.seek(header.Version == 0x1B ? 0x20 : 0x10);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (const qint32 &offset : offsets) {
|
|
|
|
|
out << offset;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (auto mipmap : mipmaps) {
|
|
|
|
|
file.seek(mipmap.offset);
|
|
|
|
|
file.write(mipmap.data);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
file.close();
|
|
|
|
|
|
|
|
|
|
qDebug() << "Successfully wrote IWI file: " << fileStem + ".iwi";
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool IWIFile::SaveDDS() {
|
|
|
|
|
SetupExportDirs();
|
|
|
|
|
QFile file("exports/dds/" + fileStem + ".dds");
|
|
|
|
|
if (!file.open(QIODevice::WriteOnly)) {
|
|
|
|
|
qDebug() << "Error: Unable to write DDS file " << fileStem + ".dds";
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
DDSHeader ddsHeader = {};
|
|
|
|
|
ddsHeader.magic = 0x20534444; // 'DDS '
|
|
|
|
|
ddsHeader.size = 124;
|
|
|
|
|
ddsHeader.flags = DDSD_CAPS | DDSD_HEIGHT | DDSD_WIDTH | DDSD_PIXELFORMAT | DDSD_LINEARSIZE;
|
|
|
|
|
ddsHeader.height = info.Height;
|
|
|
|
|
ddsHeader.width = info.Width;
|
|
|
|
|
ddsHeader.depth = 0;
|
|
|
|
|
ddsHeader.mipMapCount = mipmaps.size();
|
|
|
|
|
|
|
|
|
|
DDSPixelFormat ddsPixelFormat = DDSFile::CalculatePixelFormat(info.Format);
|
|
|
|
|
if (ddsPixelFormat.flags & DDPF_FOURCC) {
|
|
|
|
|
ddsHeader.flags |= DDSD_LINEARSIZE;
|
|
|
|
|
} else {
|
|
|
|
|
ddsHeader.flags |= DDSD_PITCH;
|
|
|
|
|
}
|
|
|
|
|
ddsHeader.pixelFormat = ddsPixelFormat;
|
|
|
|
|
|
|
|
|
|
// Calculate pitch/linear size
|
|
|
|
|
if (ddsPixelFormat.flags & DDPF_FOURCC) {
|
|
|
|
|
int blockSize = (ddsPixelFormat.format == 0x31545844) ? 8 : 16;
|
|
|
|
|
ddsHeader.pitchOrLinearSize = fmax(1, (ddsHeader.width + 3) / 4) * blockSize * (ddsHeader.height / 4);
|
|
|
|
|
} else {
|
|
|
|
|
ddsHeader.pitchOrLinearSize = ddsHeader.width * (ddsPixelFormat.rgbBitCount / 8);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
DDSCaps ddsCaps = {};
|
|
|
|
|
ddsCaps.caps1 = DDSCAPS_TEXTURE;
|
|
|
|
|
ddsCaps.caps2 = 0;
|
|
|
|
|
ddsCaps.dDSX = 0;
|
|
|
|
|
ddsCaps.reserved = 0;
|
|
|
|
|
ddsHeader.caps = ddsCaps;
|
|
|
|
|
|
|
|
|
|
QDataStream out(&file);
|
|
|
|
|
out.setByteOrder(QDataStream::LittleEndian);
|
|
|
|
|
|
|
|
|
|
// Write DDS header
|
|
|
|
|
out.writeRawData(reinterpret_cast<const char*>(&ddsHeader), sizeof(DDSHeader));
|
|
|
|
|
|
|
|
|
|
for (auto mipmap : mipmaps) {
|
|
|
|
|
if (!mipmap.data.size()) { continue; }
|
|
|
|
|
|
|
|
|
|
// Write mipmap data
|
|
|
|
|
out.writeRawData(reinterpret_cast<const char*>(mipmap.data.constData()), mipmap.data.size());
|
|
|
|
|
}
|
|
|
|
|
file.close();
|
|
|
|
|
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool IWIFile::SavePNG() {
|
|
|
|
|
DDSFile ddsFile(*this);
|
|
|
|
|
if (!ddsFile.SavePNG()) {
|
|
|
|
|
qDebug() << "Error: Failed to save PNG file: " << fileStem + ".png";
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool IWIFile::SaveJPG() {
|
|
|
|
|
DDSFile ddsFile(*this);
|
|
|
|
|
if (!ddsFile.SavePNG()) {
|
|
|
|
|
qDebug() << "Error: Failed to save JPG file: " << fileStem + ".jpg";
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Check if the IWI version is supported
|
|
|
|
|
bool IWIHeader::isSupported() const {
|
|
|
|
|
return (Magic[0] == 'I' && Magic[1] == 'W' && Magic[2] == 'i') &&
|
|
|
|
|
supportedVersions.contains(Version);
|
|
|
|
|
}
|