diff --git a/codxe.vcxproj b/codxe.vcxproj
index 0c208ef1..d99351f7 100644
--- a/codxe.vcxproj
+++ b/codxe.vcxproj
@@ -79,6 +79,11 @@
+
+
+
+
+
@@ -134,6 +139,7 @@
+
@@ -203,6 +209,12 @@
+
+
+
+
+
+
@@ -262,6 +274,7 @@
+
diff --git a/resources/iw3/_codxe/mods/codjumper/highmip/~weapon_rpg7_spec-rgb&weapon_~06163c5b.dds b/resources/iw3/_codxe/mods/codjumper/highmip/~weapon_rpg7_spec-rgb&weapon_~06163c5b.dds
deleted file mode 100644
index d1f2e9e2..00000000
Binary files a/resources/iw3/_codxe/mods/codjumper/highmip/~weapon_rpg7_spec-rgb&weapon_~06163c5b.dds and /dev/null differ
diff --git a/resources/iw3/_codxe/mods/codjumper/images/~weapon_rpg7_spec-rgb&weapon_~06163c5b.dds b/resources/iw3/_codxe/mods/codjumper/images/~weapon_rpg7_spec-rgb&weapon_~06163c5b.dds
index 905ca9b9..d64b27d3 100644
Binary files a/resources/iw3/_codxe/mods/codjumper/images/~weapon_rpg7_spec-rgb&weapon_~06163c5b.dds and b/resources/iw3/_codxe/mods/codjumper/images/~weapon_rpg7_spec-rgb&weapon_~06163c5b.dds differ
diff --git a/src/common/config.cpp b/src/common/config.cpp
index d3754b9a..9d94b97b 100644
--- a/src/common/config.cpp
+++ b/src/common/config.cpp
@@ -11,12 +11,12 @@
const char *CONFIG_PATH = "game:\\_codxe\\codxe.json";
const char *MOD_DIR = "game:\\_codxe\\mods";
+const char *USERRAW_DIR = "game:\\_codxe\\userraw";
const char *DUMP_DIR = "game:\\_codxe\\dump";
// Default values
std::string Config::active_mod = "";
-bool Config::dump_rawfile = false;
-bool Config::dump_map_ents = false;
+bool Config::dump_assets = false;
std::string Config::mod_base_path = "";
namespace
@@ -203,8 +203,7 @@ Config::~Config()
// Reset to defaults on cleanup
active_mod = "";
mod_base_path = "";
- dump_rawfile = false;
- dump_map_ents = false;
+ dump_assets = false;
DbgPrint("[codxe][Config] Configuration unloaded\n");
}
@@ -243,13 +242,9 @@ bool Config::LoadFromJson(const char *jsonBuffer, DWORD bufferSize)
wcstombs(narrowValue, valueBuffer, sizeof(narrowValue));
active_mod = narrowValue;
}
- else if (wcscmp(propertyName, L"dump_rawfile") == 0)
+ else if (wcscmp(propertyName, L"dump_assets") == 0)
{
- dump_rawfile = (jsonTokenType == Json_True);
- }
- else if (wcscmp(propertyName, L"dump_map_ents") == 0)
- {
- dump_map_ents = (jsonTokenType == Json_True);
+ dump_assets = (jsonTokenType == Json_True);
}
else
{
@@ -262,8 +257,7 @@ bool Config::LoadFromJson(const char *jsonBuffer, DWORD bufferSize)
DbgPrint("[codxe][Config] Configuration loaded:\n");
DbgPrint(" Active Mod: %s\n", active_mod.c_str());
- DbgPrint(" Dump Raw Scripts: %s\n", dump_rawfile ? "true" : "false");
- DbgPrint(" Dump Map Entities: %s\n", dump_map_ents ? "true" : "false");
+ DbgPrint(" Dump Assets: %s\n", dump_assets ? "true" : "false");
if (!active_mod.empty())
{
diff --git a/src/common/config.h b/src/common/config.h
index 7fdc37a9..15c06f0e 100644
--- a/src/common/config.h
+++ b/src/common/config.h
@@ -4,6 +4,7 @@
extern const char *CONFIG_PATH;
extern const char *MOD_DIR;
+extern const char *USERRAW_DIR;
extern const char *DUMP_DIR;
bool DirectoryExists(const char *path);
@@ -23,8 +24,7 @@ class Config : public Module
static std::string active_mod;
static std::string mod_base_path;
- static bool dump_rawfile;
- static bool dump_map_ents;
+ static bool dump_assets;
static std::string GetModBasePath();
static const char *GetModBasePathCStr();
diff --git a/src/game/iw2/mp/components/scr_parser.cpp b/src/game/iw2/mp/components/scr_parser.cpp
index 5a53821f..d6fa3cfd 100644
--- a/src/game/iw2/mp/components/scr_parser.cpp
+++ b/src/game/iw2/mp/components/scr_parser.cpp
@@ -15,7 +15,7 @@ char *Scr_AddSourceBuffer_Hook(const char *filename, const char *extFilename, co
archive);
};
- if (Config::dump_rawfile)
+ if (Config::dump_assets)
{
DbgPrint("GSCLoader: Dumping script %s\n", extFilename);
auto contents = callOriginal();
diff --git a/src/game/iw2/sp/components/scr_parser.cpp b/src/game/iw2/sp/components/scr_parser.cpp
index 8f49c3ab..c18d144e 100644
--- a/src/game/iw2/sp/components/scr_parser.cpp
+++ b/src/game/iw2/sp/components/scr_parser.cpp
@@ -15,7 +15,7 @@ char *Scr_AddSourceBuffer_Hook(const char *filename, const char *extFilename, co
archive);
};
- if (Config::dump_rawfile)
+ if (Config::dump_assets)
{
DbgPrint("GSCLoader: Dumping script %s\n", extFilename);
auto contents = callOriginal();
diff --git a/src/game/iw3/mp/components/image_loader.cpp b/src/game/iw3/mp/components/image_loader.cpp
index dc157c14..cfc99b54 100644
--- a/src/game/iw3/mp/components/image_loader.cpp
+++ b/src/game/iw3/mp/components/image_loader.cpp
@@ -2,606 +2,390 @@
#include "common/config.h"
#include "command.h"
#include "image_loader.h"
-
-// Forgive me for this dreadful code. It was hacked together until semi working and not touched since.
-// TODO: refactor and generalise for the other games.
+#include "image/dds_loader.h"
+#include "image/dds_writer.h"
+#include "image/texture_layout.h"
+#include "image/xenos_texture.h"
namespace
{
+std::set g_streamedImageReplacements;
-// TODO: MAKEFOURCC('D', 'X', 'T', '1');
-// DDS Constants
-const uint32_t DDS_MAGIC = MAKEFOURCC('D', 'D', 'S', ' ');
-const uint32_t DDS_HEADER_SIZE = 124;
-const uint32_t DDS_PIXEL_FORMAT_SIZE = 32;
-const uint32_t DDSD_CAPS = 0x1;
-const uint32_t DDSD_HEIGHT = 0x2;
-const uint32_t DDSD_WIDTH = 0x4;
-const uint32_t DDSD_PIXELFORMAT = 0x1000;
-const uint32_t DDSD_LINEARSIZE = 0x80000;
-const uint32_t DDPF_FOURCC = 0x4;
-const uint32_t DDPF_RGB = 0x40;
-const uint32_t DDPF_ALPHAPIXELS = 0x1;
-const uint32_t DDSCAPS_TEXTURE = 0x1000;
-const uint32_t DDSCAPS_MIPMAP = 0x400000;
-const uint32_t DDPF_LUMINANCE = 0x20000;
-
-// DDS Pixel Formats (FourCC Codes)
-const uint32_t DXT1_FOURCC = MAKEFOURCC('D', 'X', 'T', '1');
-const uint32_t DXT3_FOURCC = MAKEFOURCC('D', 'X', 'T', '3');
-const uint32_t DXT5_FOURCC = MAKEFOURCC('D', 'X', 'T', '5');
-const uint32_t DXN_FOURCC = MAKEFOURCC('A', 'T', 'I', '2'); // (DXN / BC5)
-
-// Additional DDS Cubemap Flags
-const uint32_t DDSCAPS2_CUBEMAP = 0x200;
-const uint32_t DDSCAPS2_CUBEMAP_POSITIVEX = 0x400;
-const uint32_t DDSCAPS2_CUBEMAP_NEGATIVEX = 0x800;
-const uint32_t DDSCAPS2_CUBEMAP_POSITIVEY = 0x1000;
-const uint32_t DDSCAPS2_CUBEMAP_NEGATIVEY = 0x2000;
-const uint32_t DDSCAPS2_CUBEMAP_POSITIVEZ = 0x4000;
-const uint32_t DDSCAPS2_CUBEMAP_NEGATIVEZ = 0x8000;
-
-// DDS Header Structure (with inline endian swapping)
-struct DDSHeader
-{
- uint32_t magic;
- uint32_t size;
- uint32_t flags;
- uint32_t height;
- uint32_t width;
- uint32_t pitchOrLinearSize;
- uint32_t depth;
- uint32_t mipMapCount;
- uint32_t reserved1[11];
- struct
- {
- uint32_t size;
- uint32_t flags;
- uint32_t fourCC;
- uint32_t rgbBitCount;
- uint32_t rBitMask;
- uint32_t gBitMask;
- uint32_t bBitMask;
- uint32_t aBitMask;
- } pixelFormat;
- uint32_t caps;
- uint32_t caps2;
- uint32_t caps3;
- uint32_t caps4;
- uint32_t reserved2;
-};
-
-static_assert(sizeof(DDSHeader) == 128, "");
-
-struct DDSImage
+std::string extract_filename(const char *filename)
{
- DDSHeader header;
- std::vector data;
-};
+ std::string path(filename);
-struct StaticDDSImage
-{
- DDSHeader header;
- uint8_t *data;
- DWORD dataSize;
-};
+ // Find last backslash '\' or forward slash '/'
+ size_t lastSlash = path.find_last_of("\\/");
+ size_t start = (lastSlash == std::string::npos) ? 0 : lastSlash + 1;
-uint8_t g_staticDDSData[1024 * 1024];
+ // Find last dot '.' (extension separator)
+ size_t lastDot = path.find_last_of('.');
+ size_t end = (lastDot == std::string::npos || lastDot < start) ? path.length() : lastDot;
-// Function to swap all necessary fields from little-endian to big-endian
-void SwapDDSHeaderEndian(DDSHeader &header)
-{
- header.magic = _byteswap_ulong(header.magic);
- header.size = _byteswap_ulong(header.size);
- header.flags = _byteswap_ulong(header.flags);
- header.height = _byteswap_ulong(header.height);
- header.width = _byteswap_ulong(header.width);
- header.pitchOrLinearSize = _byteswap_ulong(header.pitchOrLinearSize);
- header.depth = _byteswap_ulong(header.depth);
- header.mipMapCount = _byteswap_ulong(header.mipMapCount);
-
- for (int i = 0; i < 11; i++)
- header.reserved1[i] = _byteswap_ulong(header.reserved1[i]);
-
- header.pixelFormat.size = _byteswap_ulong(header.pixelFormat.size);
- header.pixelFormat.flags = _byteswap_ulong(header.pixelFormat.flags);
- header.pixelFormat.fourCC = _byteswap_ulong(header.pixelFormat.fourCC);
- header.pixelFormat.rgbBitCount = _byteswap_ulong(header.pixelFormat.rgbBitCount);
- header.pixelFormat.rBitMask = _byteswap_ulong(header.pixelFormat.rBitMask);
- header.pixelFormat.gBitMask = _byteswap_ulong(header.pixelFormat.gBitMask);
- header.pixelFormat.bBitMask = _byteswap_ulong(header.pixelFormat.bBitMask);
- header.pixelFormat.aBitMask = _byteswap_ulong(header.pixelFormat.aBitMask);
-
- header.caps = _byteswap_ulong(header.caps);
- header.caps2 = _byteswap_ulong(header.caps2);
- header.caps3 = _byteswap_ulong(header.caps3);
- header.caps4 = _byteswap_ulong(header.caps4);
- header.reserved2 = _byteswap_ulong(header.reserved2);
+ return path.substr(start, end - start);
}
-DDSImage ReadDDSFile(const std::string &filepath)
+bool Validate2DReplacementData(const iw3::mp::GfxImage *image, const image::DdsImage &ddsImage, GPUTEXTUREFORMAT format,
+ uint32_t ddsFirstMipLevel, uint32_t replacementLevelCount, size_t *requiredDDSSize,
+ size_t *requiredTextureBytes)
{
- DDSImage ddsImage;
- std::ifstream file(filepath, std::ios::binary);
-
- if (!file.is_open())
- {
- DbgPrint("ERROR: Unable to open file: %s\n", filepath.c_str());
- return ddsImage; // Return empty DDSImage
- }
-
- // Read DDS header (raw, little-endian)
- file.read(reinterpret_cast(&ddsImage.header), sizeof(DDSHeader));
+ const size_t ddsMipOffset =
+ image::CalculateDdsMipOffset(ddsImage.header.dwWidth, ddsImage.header.dwHeight, format, ddsFirstMipLevel);
+ const size_t requiredLinearSize = image::CalculateRequiredLinearDataSize(
+ ddsImage.header.dwWidth, ddsImage.header.dwHeight, format, ddsFirstMipLevel, replacementLevelCount, 1u);
+ *requiredDDSSize = ddsMipOffset + requiredLinearSize;
+ if (requiredLinearSize == 0 || (ddsFirstMipLevel > 0 && ddsMipOffset == 0))
+ return false;
- // Swap only the magic number to big-endian for proper validation
- uint32_t magicSwapped = _byteswap_ulong(ddsImage.header.magic);
+ if (ddsImage.data.size() < *requiredDDSSize)
+ return false;
- if (magicSwapped != 0x20534444) // 'DDS ' in big-endian
- {
- DbgPrint("ERROR: Invalid DDS file: %s\n", filepath.c_str());
- file.close();
- return ddsImage;
- }
+ const uint32_t baseSize =
+ image::xenos_texture::CalculateBaseSize(image->texture.basemap, image->width, image->height, 1u);
+ const size_t mipBytes =
+ image::CalculateRequiredMipTextureBytes(image->width, image->height, format, 1u, replacementLevelCount, 1u);
+ *requiredTextureBytes = static_cast(baseSize) + mipBytes;
+ const int cardMemory = image->cardMemory.platform[0];
+ if (cardMemory > 0 && *requiredTextureBytes > static_cast(cardMemory))
+ return false;
- // Swap header fields to big-endian for Xbox 360
- SwapDDSHeaderEndian(ddsImage.header);
+ return true;
+}
- // Move to end of file to get total file size
- file.seekg(0, std::ios::end);
- std::streampos fileSize = file.tellg();
+bool ValidateCubeReplacementData(const iw3::mp::GfxImage *image, const image::DdsImage &ddsImage,
+ GPUTEXTUREFORMAT format, uint32_t faceSize, uint32_t tiledBaseSize,
+ size_t *requiredDDSSize)
+{
+ *requiredDDSSize = static_cast(faceSize) * 6u;
+ if (faceSize == 0 || *requiredDDSSize == 0)
+ return false;
- // Ensure fileSize is valid before proceeding
- if (fileSize == std::streampos(-1))
- {
- DbgPrint("ERROR: Failed to determine file size.\n");
- file.close();
- return ddsImage;
- }
+ if (ddsImage.data.size() < *requiredDDSSize)
+ return false;
- // Move back to after the header
- file.seekg(sizeof(DDSHeader), std::ios::beg);
+ const int cardMemory = image->cardMemory.platform[0];
+ if (cardMemory > 0 && static_cast(tiledBaseSize) > static_cast(cardMemory))
+ return false;
- // Compute data size safely
- size_t dataSize = static_cast(fileSize) - sizeof(DDSHeader);
+ return true;
+}
- // Read image data
- ddsImage.data.resize(dataSize);
- file.read(reinterpret_cast(ddsImage.data.data()), dataSize);
+} // namespace
- file.close();
+namespace iw3
+{
+namespace mp
+{
+namespace
+{
+const char *HIGHMIP_DIR = "D:\\highmip";
- // Debug output
- DbgPrint("INFO: DDS file '%s' loaded successfully.\n", filepath.c_str());
- DbgPrint(" Resolution: %ux%u\n", ddsImage.header.width, ddsImage.header.height);
- DbgPrint(" MipMaps: %u\n", ddsImage.header.mipMapCount);
- DbgPrint(" Data Size: %u bytes\n", static_cast(dataSize));
+std::string GetSanitizedImageName(const char *imageName)
+{
+ if (imageName == NULL)
+ return std::string();
- return ddsImage;
+ std::string sanitizedName = imageName;
+ sanitizedName.erase(std::remove_if(sanitizedName.begin(), sanitizedName.end(), [](char c) { return c == '*'; }),
+ sanitizedName.end());
+ return sanitizedName;
}
-StaticDDSImage ReadDDSFileStatic(const char *filepath)
+std::string GetImageDumpPath(const char *imageName)
{
- StaticDDSImage ddsImage = {};
+ return std::string(DUMP_DIR) + "\\images\\" + GetSanitizedImageName(imageName) + ".dds";
+}
- HANDLE file = CreateFileA(filepath, GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE, nullptr, OPEN_EXISTING,
- FILE_ATTRIBUTE_NORMAL, nullptr);
- if (file == INVALID_HANDLE_VALUE)
- {
- DbgPrint("ERROR: Unable to open file: %s\n", filepath);
- return ddsImage;
- }
+std::string GetImageReplacementPath(const char *imageName)
+{
+ const std::string userPath = std::string(USERRAW_DIR) + "\\images\\" + imageName + ".dds";
+ if (filesystem::file_exists(userPath))
+ return userPath;
- DWORD bytesRead = 0;
- if (!ReadFile(file, &ddsImage.header, sizeof(DDSHeader), &bytesRead, nullptr) || bytesRead != sizeof(DDSHeader))
- {
- DbgPrint("ERROR: Failed to read DDS header: %s\n", filepath);
- CloseHandle(file);
- return ddsImage;
- }
+ return Config::GetModBasePath() + "\\images\\" + imageName + ".dds";
+}
- const uint32_t magicSwapped = _byteswap_ulong(ddsImage.header.magic);
- if (magicSwapped != 0x20534444)
- {
- DbgPrint("ERROR: Invalid DDS file: %s\n", filepath);
- CloseHandle(file);
- return ddsImage;
- }
+bool ReadBinaryFile(const std::string &path, std::vector *buffer)
+{
+ std::ifstream file(path.c_str(), std::ios::binary | std::ios::ate);
+ if (!file)
+ return false;
- SwapDDSHeaderEndian(ddsImage.header);
+ const std::streamsize size = file.tellg();
+ if (size < 0)
+ return false;
- const DWORD fileSize = GetFileSize(file, nullptr);
- if (fileSize == INVALID_FILE_SIZE || fileSize <= sizeof(DDSHeader))
- {
- DbgPrint("ERROR: Failed to determine DDS file size: %s\n", filepath);
- CloseHandle(file);
- return ddsImage;
- }
+ file.seekg(0, std::ios::beg);
+ buffer->resize(static_cast(size));
+ if (size == 0)
+ return true;
- const DWORD dataSize = fileSize - sizeof(DDSHeader);
- if (dataSize > sizeof(g_staticDDSData))
- {
- DbgPrint("ERROR: DDS file too large for static buffer: %s size=%u max=%u\n", filepath, dataSize,
- static_cast(sizeof(g_staticDDSData)));
- CloseHandle(file);
- return ddsImage;
- }
+ return file.read(reinterpret_cast(buffer->data()), size) != 0;
+}
- if (!ReadFile(file, g_staticDDSData, dataSize, &bytesRead, nullptr) || bytesRead != dataSize)
+std::map CollectHighMipFiles()
+{
+ std::map highMipFiles;
+ const std::vector files = filesystem::list_files_in_directory(HIGHMIP_DIR);
+ for (size_t i = 0; i < files.size(); ++i)
{
- DbgPrint("ERROR: Failed to read DDS data: %s\n", filepath);
- CloseHandle(file);
- return ddsImage;
+ const std::string assetName = extract_filename(files[i].c_str());
+ highMipFiles[assetName] = std::string(HIGHMIP_DIR) + "\\" + files[i];
}
- CloseHandle(file);
-
- ddsImage.data = g_staticDDSData;
- ddsImage.dataSize = dataSize;
-
- DbgPrint("INFO: DDS file '%s' loaded successfully.\n", filepath);
- DbgPrint(" Resolution: %ux%u\n", ddsImage.header.width, ddsImage.header.height);
- DbgPrint(" MipMaps: %u\n", ddsImage.header.mipMapCount);
- DbgPrint(" Data Size: %u bytes\n", ddsImage.dataSize);
-
- return ddsImage;
+ return highMipFiles;
}
-std::string extract_filename(const char *filename)
+std::string FindHighMipPathForImage(const std::map &highMipFiles, const char *imageName)
{
- std::string path(filename);
+ if (imageName == NULL)
+ return std::string();
- // Find last backslash '\' or forward slash '/'
- size_t lastSlash = path.find_last_of("\\/");
- size_t start = (lastSlash == std::string::npos) ? 0 : lastSlash + 1;
+ std::map::const_iterator highMip = highMipFiles.find(imageName);
+ if (highMip != highMipFiles.end())
+ return highMip->second;
- // Find last dot '.' (extension separator)
- size_t lastDot = path.find_last_of('.');
- size_t end = (lastDot == std::string::npos || lastDot < start) ? path.length() : lastDot;
+ const std::string sanitizedName = GetSanitizedImageName(imageName);
+ highMip = highMipFiles.find(sanitizedName);
+ if (highMip != highMipFiles.end())
+ return highMip->second;
- return path.substr(start, end - start);
+ return std::string();
}
-void GPUEndianSwapTexture(std::vector &pixelData, GPUENDIAN endianType)
+bool Image_DumpHighMip(const GfxImage *image, const std::string &highMipPath)
{
- switch (endianType)
+ if (image->mapType != MAPTYPE_2D)
+ return false;
+
+ std::vector highMipData;
+ if (!ReadBinaryFile(highMipPath, &highMipData))
{
- case GPUENDIAN_8IN16:
- XGEndianSwapMemory(pixelData.data(), pixelData.data(), XGENDIAN_8IN16, 2, pixelData.size() / 2);
- break;
- case GPUENDIAN_8IN32:
- XGEndianSwapMemory(pixelData.data(), pixelData.data(), XGENDIAN_8IN32, 4, pixelData.size() / 4);
- break;
- case GPUENDIAN_16IN32:
- XGEndianSwapMemory(pixelData.data(), pixelData.data(), XGENDIAN_16IN32, 4, pixelData.size() / 4);
- break;
+ Com_PrintError(CON_CHANNEL_ERROR, "Could not read highmip for image '%s': %s\n", image->name,
+ highMipPath.c_str());
+ return false;
}
-}
-
-UINT CalculateMipLevelSize(UINT width, UINT height, UINT mipLevel, GPUTEXTUREFORMAT format)
-{
- // Calculate dimensions for the requested mip level
- UINT mipWidth = max(1, width >> mipLevel);
- UINT mipHeight = max(1, height >> mipLevel);
- // Calculate size based on format
- UINT blockSize;
- switch (format)
+ const uint32_t width = static_cast(image->width) * 2u;
+ const uint32_t height = static_cast(image->height) * 2u;
+ const uint32_t format = image->texture.basemap->Format.DataFormat;
+ const uint32_t linearLevelSize = image::xenos_texture::CalculateLinearLevelSize(width, height, 0u, format);
+ const uint32_t tiledLevelSize = image::xenos_texture::CalculateTiledLevelSize(width, height, 0u, format, 0u);
+ const uint32_t rowPitch = image::xenos_texture::CalculateLinearRowPitch(width, 0u, format);
+ if (linearLevelSize == 0 || tiledLevelSize == 0 || rowPitch == 0)
{
- case GPUTEXTUREFORMAT_DXT1:
- blockSize = 8; // 8 bytes per 4x4 block
- break;
- case GPUTEXTUREFORMAT_DXT2_3:
- blockSize = 16; // 16 bytes per 4x4 block
- break;
- case GPUTEXTUREFORMAT_DXT4_5:
- blockSize = 16; // 16 bytes per 4x4 block
- break;
- case GPUTEXTUREFORMAT_DXN:
- blockSize = 16; // 16 bytes per 4x4 block (two 8-byte channels)
- break;
- default:
- DbgPrint("CalculateMipLevelSize: Unsupported format %d\n", format);
- return 0;
+ Com_PrintError(CON_CHANNEL_ERROR, "Skipping image '%s': unsupported highmip format %u\n", image->name, format);
+ return false;
}
- // For block-compressed formats, calculate number of blocks
- // Each block is 4x4 pixels, so we need to round up to nearest block
- UINT blocksWide = (mipWidth + 3) / 4;
- UINT blocksHigh = (mipHeight + 3) / 4;
-
- // Calculate total size in bytes
- UINT sizeInBytes = blocksWide * blocksHigh * blockSize;
-
- return sizeInBytes;
-}
-
-} // namespace
+ if (highMipData.size() < tiledLevelSize)
+ {
+ Com_PrintError(CON_CHANNEL_ERROR, "Skipping image '%s': highmip data is too small (have %u, need %u)\n",
+ image->name, static_cast(highMipData.size()), tiledLevelSize);
+ return false;
+ }
-namespace iw3
-{
-namespace mp
-{
-void Image_DbgPrint(const GfxImage *image)
-{
- const int format = image->texture.basemap->Format.DataFormat;
- char *format_str;
- switch (format)
+ image::DDS_HEADER header;
+ if (!image::CreateDdsHeader(header, width, height, image->depth, 1u, linearLevelSize, image::DDSCAPS_TEXTURE, 0u,
+ format))
{
- case GPUTEXTUREFORMAT_DXT1:
- format_str = "DXT1";
- break;
- case GPUTEXTUREFORMAT_DXT2_3:
- format_str = "DXT2_3";
- break;
- case GPUTEXTUREFORMAT_DXT4_5:
- format_str = "DXT4_5";
- break;
- case GPUTEXTUREFORMAT_DXN:
- format_str = "DXN";
- break;
- case GPUTEXTUREFORMAT_8:
- format_str = "8";
- break;
- case GPUTEXTUREFORMAT_8_8:
- format_str = "8_8";
- break;
- case GPUTEXTUREFORMAT_8_8_8_8:
- format_str = "8_8_8_8";
- break;
- default:
- format_str = "UNKNOWN";
- break;
+ Com_PrintError(CON_CHANNEL_ERROR, "Skipping image '%s': unsupported highmip format %u\n", image->name, format);
+ return false;
}
- XGTEXTURE_DESC SourceDesc;
- XGGetTextureDesc(image->texture.basemap, 0, &SourceDesc);
- BOOL IsBorderTexture = XGIsBorderTexture(image->texture.basemap);
- UINT MipTailBaseLevel = XGGetMipTailBaseLevel(SourceDesc.Width, SourceDesc.Height, IsBorderTexture);
+ const std::string outputPath = GetImageDumpPath(image->name);
+ std::ofstream outputFile(outputPath.c_str(), std::ios::binary);
+ if (!outputFile)
+ {
+ Com_PrintError(CON_CHANNEL_ERROR, "Could not create DDS for image '%s': %s\n", image->name, outputPath.c_str());
+ return false;
+ }
- // SourceDesc.BitsPerPixel;
- // SourceDesc.BytesPerBlock;
+ image::WriteDdsHeader(outputFile, header);
- UINT MipLevelCount = image->texture.basemap->GetLevelCount();
+ std::vector tiledData(highMipData.begin(), highMipData.begin() + tiledLevelSize);
+ image::xenos_texture::ApplyGpuEndian(tiledData.data(), tiledData.size(),
+ static_cast(image->texture.basemap->Format.Endian));
- UINT BaseSize;
- XGGetTextureLayout(image->texture.basemap, 0, &BaseSize, 0, 0, 0, 0, 0, 0, 0, 0);
+ std::vector linearData(linearLevelSize);
+ if (!image::xenos_texture::UntileTextureLevel(width, height, 0u, format, 0u, linearData.data(), linearData.size(),
+ rowPitch, tiledData.data(), tiledData.size()))
+ {
+ Com_PrintError(CON_CHANNEL_ERROR, "Could not decode highmip for image '%s'\n", image->name);
+ return false;
+ }
- Com_Printf(CON_CHANNEL_CONSOLEONLY,
- "Image_DbgPrint: Dumping image Name='%s', Type=%d, Dimensions=%dx%d, MipLevels=%d, MipTailBaseLevel=%d, "
- "Format=%s, BitsPerPixel=%d, BytesPerBlock=%d, Endian=%d, BaseSize=%d\n",
- image->name, image->mapType, image->width, image->height, MipLevelCount, MipTailBaseLevel, format_str,
- SourceDesc.BitsPerPixel, SourceDesc.BytesPerBlock, image->texture.basemap->Format.Endian, BaseSize);
+ outputFile.write(reinterpret_cast(linearData.data()), linearData.size());
+ Com_Printf(CON_CHANNEL_CONSOLEONLY, "Dumped image '%s' (highmip)\n", image->name);
+ return true;
}
+} // namespace
-void Image_Dump(const GfxImage *image)
+void Image_Dump(const GfxImage *image, const std::string &highMipPath)
{
// TODO: cleanup empty files if failed
- Com_Printf(CON_CHANNEL_CONSOLEONLY, "Image_Dump: Dumping image '%s'\n", image->name);
-
if (!image)
{
- Com_PrintError(CON_CHANNEL_ERROR, "Image_Dump: Null GfxImage!\n");
+ Com_PrintError(CON_CHANNEL_ERROR, "Skipping image: null asset\n");
+ return;
+ }
+
+ if (image->name == NULL)
+ {
+ Com_PrintError(CON_CHANNEL_ERROR, "Skipping image: missing name\n");
return;
}
if (!image->pixels || image->baseSize == 0)
{
- Com_PrintError(CON_CHANNEL_ERROR, "Image_Dump: Image '%s' has no valid pixel data!\n", image->name);
+ Com_PrintError(CON_CHANNEL_ERROR, "Skipping image '%s': no pixel data\n", image->name);
return;
}
if (image->mapType != MAPTYPE_2D && image->mapType != MAPTYPE_CUBE)
{
- Com_PrintError(CON_CHANNEL_ERROR, "Image_Dump: Unsupported map type %d!\n", image->mapType);
+ Com_PrintError(CON_CHANNEL_ERROR, "Skipping image '%s': unsupported map type %d\n", image->name,
+ image->mapType);
return;
}
- UINT BaseSize;
- XGGetTextureLayout(image->texture.basemap, 0, &BaseSize, 0, 0, 0, 0, 0, 0, 0, 0);
-
- DDSHeader header;
- memset(&header, 0, sizeof(DDSHeader));
+ if (!highMipPath.empty() && Image_DumpHighMip(image, highMipPath))
+ return;
- header.magic = _byteswap_ulong(DDS_MAGIC);
- header.size = _byteswap_ulong(DDS_HEADER_SIZE);
- header.flags = _byteswap_ulong(DDSD_CAPS | DDSD_HEIGHT | DDSD_WIDTH | DDSD_PIXELFORMAT | DDSD_LINEARSIZE);
- header.height = _byteswap_ulong(image->height);
- header.width = _byteswap_ulong(image->width);
- header.depth = _byteswap_ulong(image->depth);
- header.mipMapCount = _byteswap_ulong(image->texture.basemap->GetLevelCount());
+ const uint32_t faceCount = image->mapType == MAPTYPE_CUBE ? 6u : 1u;
+ uint32_t BaseSize =
+ image::xenos_texture::CalculateBaseSize(image->texture.basemap, image->width, image->height, faceCount);
auto format = image->texture.basemap->Format.DataFormat;
- switch (format)
+ uint32_t caps2 = 0u;
+ if (image->mapType == mp::MAPTYPE_CUBE)
{
- case GPUTEXTUREFORMAT_DXT1:
- header.pixelFormat.fourCC = _byteswap_ulong(DXT1_FOURCC);
- header.pitchOrLinearSize = BaseSize;
- break;
- case GPUTEXTUREFORMAT_DXT2_3:
- header.pixelFormat.fourCC = _byteswap_ulong(DXT3_FOURCC);
- header.pitchOrLinearSize = BaseSize;
- break;
- case GPUTEXTUREFORMAT_DXT4_5:
- header.pixelFormat.fourCC = _byteswap_ulong(DXT5_FOURCC);
- header.pitchOrLinearSize = BaseSize;
- break;
- case GPUTEXTUREFORMAT_DXN:
- header.pixelFormat.fourCC = _byteswap_ulong(DXN_FOURCC);
- header.pitchOrLinearSize = BaseSize;
- break;
- case GPUTEXTUREFORMAT_8:
- header.pixelFormat.flags = _byteswap_ulong(DDPF_LUMINANCE);
- header.pixelFormat.rgbBitCount = _byteswap_ulong(8);
- header.pixelFormat.rBitMask = _byteswap_ulong(0x000000FF);
- header.pixelFormat.gBitMask = 0;
- header.pixelFormat.bBitMask = 0;
- header.pixelFormat.aBitMask = 0;
- header.pitchOrLinearSize = BaseSize;
- break;
- case GPUTEXTUREFORMAT_8_8:
- header.pixelFormat.flags = _byteswap_ulong(DDPF_LUMINANCE | DDPF_ALPHAPIXELS);
- header.pixelFormat.rgbBitCount = _byteswap_ulong(16);
- header.pixelFormat.rBitMask = _byteswap_ulong(0x000000FF);
- header.pixelFormat.gBitMask = _byteswap_ulong(0x0000FF00);
- header.pixelFormat.bBitMask = 0;
- header.pixelFormat.aBitMask = 0;
- header.pitchOrLinearSize = BaseSize;
- break;
- case GPUTEXTUREFORMAT_8_8_8_8:
- header.pixelFormat.flags = _byteswap_ulong(DDPF_RGB | DDPF_ALPHAPIXELS);
- header.pixelFormat.rgbBitCount = _byteswap_ulong(32);
- header.pixelFormat.rBitMask = _byteswap_ulong(0x00FF0000);
- header.pixelFormat.gBitMask = _byteswap_ulong(0x0000FF00);
- header.pixelFormat.bBitMask = _byteswap_ulong(0x000000FF);
- header.pixelFormat.aBitMask = _byteswap_ulong(0xFF000000);
- header.pitchOrLinearSize = BaseSize;
- break;
- default:
- Com_PrintError(CON_CHANNEL_ERROR, "Image_Dump: Unsupported texture format %d!\n", format);
- return;
+ caps2 = image::DDSCAPS2_CUBEMAP | image::DDSCAPS2_CUBEMAP_POSITIVEX | image::DDSCAPS2_CUBEMAP_NEGATIVEX |
+ image::DDSCAPS2_CUBEMAP_POSITIVEY | image::DDSCAPS2_CUBEMAP_NEGATIVEY |
+ image::DDSCAPS2_CUBEMAP_POSITIVEZ | image::DDSCAPS2_CUBEMAP_NEGATIVEZ;
}
- // Set texture capabilities
- header.caps = _byteswap_ulong(DDSCAPS_TEXTURE | DDSCAPS_MIPMAP);
-
- // Handle Cubemaps
- if (image->mapType == mp::MAPTYPE_CUBE)
+ image::DDS_HEADER header;
+ if (!image::CreateDdsHeader(header, image->width, image->height, image->depth,
+ image::xenos_texture::GetTextureLevelCount(image->texture.basemap), BaseSize,
+ image::DDSCAPS_TEXTURE | image::DDSCAPS_MIPMAP, caps2, format))
{
- header.caps2 = _byteswap_ulong(DDSCAPS2_CUBEMAP | DDSCAPS2_CUBEMAP_POSITIVEX | DDSCAPS2_CUBEMAP_NEGATIVEX |
- DDSCAPS2_CUBEMAP_POSITIVEY | DDSCAPS2_CUBEMAP_NEGATIVEY |
- DDSCAPS2_CUBEMAP_POSITIVEZ | DDSCAPS2_CUBEMAP_NEGATIVEZ);
+ Com_PrintError(CON_CHANNEL_ERROR, "Skipping image '%s': unsupported texture format %d\n", image->name, format);
+ return;
}
- std::string filename = std::string(DUMP_DIR) + "\\" + "images";
- std::string sanitized_name = image->name;
-
- // Remove invalid characters
- sanitized_name.erase(std::remove_if(sanitized_name.begin(), sanitized_name.end(), [](char c) { return c == '*'; }),
- sanitized_name.end());
-
- filename += "\\" + sanitized_name + ".dds";
+ const std::string filename = GetImageDumpPath(image->name);
- std::ofstream file(filename, std::ios::binary);
+ std::ofstream file(filename.c_str(), std::ios::binary);
if (!file)
{
- Com_PrintError(CON_CHANNEL_ERROR, "Image_Dump: Failed to open file: %s\n", filename.c_str());
+ Com_PrintError(CON_CHANNEL_ERROR, "Could not create DDS for image '%s': %s\n", image->name, filename.c_str());
return;
}
if (image->mapType == MAPTYPE_CUBE)
{
- file.write(reinterpret_cast(&header), sizeof(DDSHeader));
+ image::WriteDdsHeader(file, header);
- unsigned int face_size = 0;
- unsigned int rowPitch = 0;
const GPUTEXTUREFORMAT format = static_cast(image->texture.basemap->Format.DataFormat);
-
- switch (format)
+ const uint32_t linearFaceSize =
+ image::xenos_texture::CalculateLinearLevelSize(image->width, image->height, 0u, format);
+ const uint32_t tiledFaceSize = image::xenos_texture::CalculateTiledLevelSize(
+ image->width, image->height, 0u, format, image->texture.basemap->Format.Pitch);
+ const uint32_t rowPitch = image::xenos_texture::CalculateLinearRowPitch(image->width, 0u, format);
+ if (linearFaceSize == 0 || tiledFaceSize == 0 || rowPitch == 0)
{
- case GPUTEXTUREFORMAT_DXT1:
- face_size = (image->width / 4) * (image->height / 4) * 8;
- rowPitch = (image->width / 4) * 8; // 8 bytes per 4x4 block
- break;
- case GPUTEXTUREFORMAT_8_8_8_8:
- face_size = image->width * image->height * 4;
- rowPitch = image->width * 4; // 4 bytes per pixel
- break;
- default:
- Com_PrintError(CON_CHANNEL_ERROR, "Image_Dump: Unsupported cube map format %d!\n", format);
+ Com_PrintError(CON_CHANNEL_ERROR, "Skipping cubemap '%s': unsupported texture format %d\n", image->name,
+ format);
return;
}
// TODO: handle mip levels per face for cubemaps
for (int i = 0; i < 6; i++)
{
- unsigned char *face_pixels = image->pixels + (i * face_size); // Offset for each face
+ const size_t faceOffset = static_cast(i) * tiledFaceSize;
+ if (faceOffset + tiledFaceSize > image->baseSize)
+ {
+ Com_PrintError(CON_CHANNEL_ERROR, "Skipping cubemap '%s': pixel data is too small (have %u, need %u)\n",
+ image->name, image->baseSize, static_cast(faceOffset + tiledFaceSize));
+ return;
+ }
+
+ unsigned char *face_pixels = image->pixels + faceOffset; // Offset for each face
- std::vector swappedFace(face_pixels, face_pixels + face_size);
- GPUEndianSwapTexture(swappedFace, static_cast(image->texture.basemap->Format.Endian));
+ std::vector swappedFace(face_pixels, face_pixels + tiledFaceSize);
+ image::xenos_texture::ApplyGpuEndian(swappedFace.data(), swappedFace.size(),
+ static_cast(image->texture.basemap->Format.Endian));
// Create buffer for linear texture data
- std::vector linearFace(face_size);
-
- // Convert tiled texture to linear layout using XGUntileTextureLevel
- XGUntileTextureLevel(image->width, // Width
- image->height, // Height
- 0, // Level (base level)
- static_cast(format), // GpuFormat
- 0, // Flags (no special flags)
- linearFace.data(), // pDestination (linear output)
- rowPitch, // RowPitch
- nullptr, // pPoint (no offset)
- swappedFace.data(), // pSource (tiled input)
- nullptr // pRect (entire texture)
- );
+ std::vector linearFace(linearFaceSize);
+
+ if (!image::xenos_texture::UntileTextureLevel(
+ image->width, image->height, 0, static_cast(format), image->texture.basemap->Format.Pitch,
+ linearFace.data(), linearFace.size(), rowPitch, swappedFace.data(), swappedFace.size()))
+ {
+ Com_PrintError(CON_CHANNEL_ERROR, "Could not decode cubemap '%s' face %d\n", image->name, i);
+ return;
+ }
file.write(reinterpret_cast(linearFace.data()), linearFace.size());
}
file.close();
+ Com_Printf(CON_CHANNEL_CONSOLEONLY, "Dumped image '%s'\n", image->name);
}
else if (image->mapType == MAPTYPE_2D)
{
// TODO: write mip levels
- file.write(reinterpret_cast(&header), sizeof(DDSHeader));
+ image::WriteDdsHeader(file, header);
- std::vector pixelData(image->pixels, image->pixels + image->baseSize);
+ auto format = image->texture.basemap->Format.DataFormat;
+ const uint32_t linearLevelSize =
+ image::xenos_texture::CalculateLinearLevelSize(image->width, image->height, 0u, format);
+ const uint32_t tiledLevelSize = image::xenos_texture::CalculateTiledLevelSize(
+ image->width, image->height, 0u, format, image->texture.basemap->Format.Pitch);
+ const uint32_t rowPitch = image::xenos_texture::CalculateLinearRowPitch(image->width, 0u, format);
+ if (linearLevelSize == 0 || tiledLevelSize == 0 || rowPitch == 0)
+ {
+ Com_PrintError(CON_CHANNEL_ERROR, "Skipping image '%s': unsupported texture format %d\n", image->name,
+ format);
+ return;
+ }
- GPUEndianSwapTexture(pixelData, static_cast(image->texture.basemap->Format.Endian));
+ if (image->baseSize < tiledLevelSize)
+ {
+ Com_PrintError(CON_CHANNEL_ERROR, "Skipping image '%s': pixel data is too small (have %u, need %u)\n",
+ image->name, image->baseSize, tiledLevelSize);
+ return;
+ }
- // Create a linear data buffer to hold the untiled texture
- std::vector linearData(image->baseSize);
+ std::vector pixelData(image->pixels, image->pixels + tiledLevelSize);
- // Calculate row pitch based on format
- UINT rowPitch;
- auto format = image->texture.basemap->Format.DataFormat;
+ image::xenos_texture::ApplyGpuEndian(pixelData.data(), pixelData.size(),
+ static_cast(image->texture.basemap->Format.Endian));
+
+ // Create a linear data buffer to hold the untiled texture
+ std::vector linearData(linearLevelSize);
- switch (format)
+ if (!image::xenos_texture::UntileTextureLevel(image->width, image->height, 0, static_cast(format),
+ image->texture.basemap->Format.Pitch, linearData.data(),
+ linearData.size(), rowPitch, pixelData.data(), pixelData.size()))
{
- case GPUTEXTUREFORMAT_DXT1:
- case GPUTEXTUREFORMAT_DXT2_3:
- case GPUTEXTUREFORMAT_DXT4_5:
- case GPUTEXTUREFORMAT_DXN:
- // Block compressed formats use 4x4 blocks
- rowPitch = ((image->width + 3) / 4) * (format == GPUTEXTUREFORMAT_DXT1 ? 8 : 16);
- break;
- case GPUTEXTUREFORMAT_8:
- rowPitch = image->width;
- break;
- case GPUTEXTUREFORMAT_8_8:
- rowPitch = image->width * 2;
- break;
- case GPUTEXTUREFORMAT_8_8_8_8:
- rowPitch = image->width * 4;
- break;
- default:
- Com_PrintError(CON_CHANNEL_ERROR, "Image_Dump: Unsupported texture format %d!\n", format);
+ Com_PrintError(CON_CHANNEL_ERROR, "Could not decode image '%s'\n", image->name);
return;
}
- DbgPrint("Image_Dump: rowPitch=%d\n", rowPitch);
-
- // Call XGUntileTextureLevel to convert the tiled texture to linear format
- XGUntileTextureLevel(image->width, // Width
- image->height, // Height
- 0, // Level (base level 0)
- static_cast(format), // GpuFormat
- XGTILE_NONPACKED, // Flags (no special flags)
- linearData.data(), // pDestination
- rowPitch, // RowPitch (calculated based on format)
- nullptr, // pPoint (no offset)
- pixelData.data(), // pSource
- nullptr // pRect (entire texture)
- );
-
file.write(reinterpret_cast(linearData.data()), linearData.size());
file.close();
+ Com_Printf(CON_CHANNEL_CONSOLEONLY, "Dumped image '%s'\n", image->name);
}
else
{
- Com_PrintError(CON_CHANNEL_ERROR, "Image_Dump: Unsupported map type %d!\n", image->mapType);
+ Com_PrintError(CON_CHANNEL_ERROR, "Skipping image '%s': unsupported map type %d\n", image->name,
+ image->mapType);
return;
}
}
@@ -620,379 +404,250 @@ void Cmd_imagedump()
CreateDirectoryA(DUMP_DIR, 0);
CreateDirectoryA((std::string(DUMP_DIR) + "\\images").c_str(), 0);
- CreateDirectoryA((std::string(DUMP_DIR) + "\\highmip").c_str(), 0);
+ const std::map highMipFiles = CollectHighMipFiles();
for (unsigned int i = 0; i < imageList.count; i++)
{
auto image = imageList.image[i];
- Image_DbgPrint(image);
+ if (image == NULL)
+ continue;
- Image_Dump(image);
+ Image_Dump(image, FindHighMipPathForImage(highMipFiles, image->name));
}
+}
- auto highmips = filesystem::list_files_in_directory("D:\\highmip");
- for (size_t i = 0; i < highmips.size(); ++i)
+bool Image_Replace_2D(GfxImage *image, const image::DdsImage &ddsImage, uint32_t ddsFirstMipLevel)
+{
+ if (image->mapType != MAPTYPE_2D)
{
- const std::string &filepath = "D:\\highmip\\" + highmips[i];
- Com_Printf(CON_CHANNEL_CONSOLEONLY, "Dumping highmip file: %s\n", filepath.c_str());
- std::string assetName = extract_filename(filepath.c_str());
- auto asset = DB_FindXAssetEntry(ASSET_TYPE_IMAGE, assetName.c_str());
- if (!asset)
- {
- Com_PrintError(CON_CHANNEL_ERROR, "Image '%s' not found in asset list!\n", assetName.c_str());
- continue;
- }
+ Com_PrintError(CON_CHANNEL_ERROR, "Image '%s' is not a 2D map!\n", image->name);
+ return false;
+ }
- auto image = asset->entry.asset.header.image;
+ const GPUTEXTUREFORMAT format = static_cast(image->texture.basemap->Format.DataFormat);
+ const uint32_t levelCount = image::xenos_texture::GetTextureLevelCount(image->texture.basemap);
+ const uint32_t mipTailBaseLevel = image->texture.basemap->Format.PackedMips != 0
+ ? image::xenos_texture::GetMipTailBaseLevel(image->width, image->height)
+ : levelCount;
+ const uint32_t ddsMipCount = ddsImage.GetMipCount();
+ if (ddsFirstMipLevel >= ddsMipCount)
+ {
+ Com_PrintError(CON_CHANNEL_ERROR, "Image '%s' replacement DDS has no mip %u: mipCount=%u\n", image->name,
+ ddsFirstMipLevel, ddsMipCount);
+ return false;
+ }
- std::ifstream input_file(filepath,
- std::ios::binary | std::ios::ate); // Open file in binary mode and seek to end
- if (!input_file)
- {
- Com_PrintError(CON_CHANNEL_ERROR, "Image_Dump: Failed to open file: %s\n", filepath.c_str());
- continue;
- }
+ const uint32_t replaceLevelCount = min(levelCount, ddsMipCount - ddsFirstMipLevel);
+ const uint32_t nonPackedLevelCount = max(1u, min(replaceLevelCount, mipTailBaseLevel));
+ unsigned char *baseData = image::xenos_texture::GetTextureBase(image->texture.basemap, image->pixels);
+ unsigned char *mipData = image::xenos_texture::GetTextureMipBase(image->texture.basemap, baseData, image->width,
+ image->height, format, 1u);
- std::streamsize size = input_file.tellg();
- if (size < 0)
+ size_t requiredDDSSize = 0;
+ size_t requiredTextureBytes = 0;
+ if (!Validate2DReplacementData(image, ddsImage, format, ddsFirstMipLevel, nonPackedLevelCount, &requiredDDSSize,
+ &requiredTextureBytes))
+ {
+ if (requiredDDSSize == 0)
{
- Com_PrintError(CON_CHANNEL_ERROR, "Image_Dump: Failed to determine file size: %s\n", filepath.c_str());
- continue;
+ Com_PrintError(CON_CHANNEL_ERROR, "Image '%s' has unsupported replacement format %d!\n", image->name,
+ format);
}
-
- input_file.seekg(0, std::ios::beg);
- std::vector buffer(static_cast(size));
-
- if (input_file.read(reinterpret_cast(buffer.data()), size))
+ else if (ddsImage.data.size() < requiredDDSSize)
{
- Com_Printf(CON_CHANNEL_CONSOLEONLY, "Read %d bytes from file.\n", size);
+ Com_PrintError(CON_CHANNEL_ERROR, "Image '%s' DDS data is too small: have=%u need=%u for %u mip levels\n",
+ image->name, static_cast(ddsImage.data.size()),
+ static_cast(requiredDDSSize), nonPackedLevelCount);
}
else
{
- Com_PrintError(CON_CHANNEL_ERROR, "Image_Dump: Error reading file: %s\n", filepath.c_str());
- continue;
+ Com_PrintError(CON_CHANNEL_ERROR, "Image '%s' replacement exceeds texture memory: have=%u need=%u\n",
+ image->name, static_cast(image->cardMemory.platform[0]),
+ static_cast(requiredTextureBytes));
}
- auto width = image->width * 2;
- auto height = image->height * 2;
- auto baseSize = width * height * 4;
-
- DDSHeader header;
- memset(&header, 0, sizeof(DDSHeader));
-
- header.magic = _byteswap_ulong(DDS_MAGIC);
- header.size = _byteswap_ulong(DDS_HEADER_SIZE);
- header.flags = _byteswap_ulong(DDSD_CAPS | DDSD_HEIGHT | DDSD_WIDTH | DDSD_PIXELFORMAT | DDSD_LINEARSIZE);
- header.width = _byteswap_ulong(width);
- header.height = _byteswap_ulong(height);
- header.depth = _byteswap_ulong(image->depth);
- header.mipMapCount = _byteswap_ulong(1);
- header.caps = _byteswap_ulong(DDSCAPS_TEXTURE);
- header.pitchOrLinearSize = baseSize;
-
- auto format = image->texture.basemap->Format.DataFormat;
- switch (format)
- {
- case GPUTEXTUREFORMAT_DXT1:
- header.pixelFormat.fourCC = _byteswap_ulong(DXT1_FOURCC);
-
- break;
- case GPUTEXTUREFORMAT_DXT2_3:
- header.pixelFormat.fourCC = _byteswap_ulong(DXT3_FOURCC);
-
- break;
- case GPUTEXTUREFORMAT_DXT4_5:
- header.pixelFormat.fourCC = _byteswap_ulong(DXT5_FOURCC);
-
- break;
- case GPUTEXTUREFORMAT_DXN:
- header.pixelFormat.fourCC = _byteswap_ulong(DXN_FOURCC);
-
- break;
- case GPUTEXTUREFORMAT_8:
- header.pixelFormat.flags = _byteswap_ulong(DDPF_LUMINANCE);
- header.pixelFormat.rgbBitCount = _byteswap_ulong(8);
- header.pixelFormat.rBitMask = _byteswap_ulong(0x000000FF);
- header.pixelFormat.gBitMask = 0;
- header.pixelFormat.bBitMask = 0;
- header.pixelFormat.aBitMask = 0;
-
- break;
- case GPUTEXTUREFORMAT_8_8:
- header.pixelFormat.flags = _byteswap_ulong(DDPF_LUMINANCE | DDPF_ALPHAPIXELS);
- header.pixelFormat.rgbBitCount = _byteswap_ulong(16);
- header.pixelFormat.rBitMask = _byteswap_ulong(0x000000FF);
- header.pixelFormat.gBitMask = _byteswap_ulong(0x0000FF00);
- header.pixelFormat.bBitMask = 0;
- header.pixelFormat.aBitMask = 0;
-
- break;
- case GPUTEXTUREFORMAT_8_8_8_8:
- header.pixelFormat.flags = _byteswap_ulong(DDPF_RGB | DDPF_ALPHAPIXELS);
- header.pixelFormat.rgbBitCount = _byteswap_ulong(32);
- header.pixelFormat.rBitMask = _byteswap_ulong(0x00FF0000);
- header.pixelFormat.gBitMask = _byteswap_ulong(0x0000FF00);
- header.pixelFormat.bBitMask = _byteswap_ulong(0x000000FF);
- header.pixelFormat.aBitMask = _byteswap_ulong(0xFF000000);
-
- break;
- default:
- Com_PrintError(CON_CHANNEL_ERROR, "Image_Dump: Unsupported texture format %d!\n", format);
- return;
- }
-
- // TODO: add sanity checks for format, size, etc.
- // TODO: handle filenames with unsupported characters for Windows
-
- auto output_filepath = std::string(DUMP_DIR) + "\\highmip\\" + assetName + ".dds";
-
- std::ofstream output_file(output_filepath, std::ios::binary);
- if (!output_file)
- {
- Com_PrintError(CON_CHANNEL_ERROR, "Image_Dump: Failed to open file: %s\n", output_filepath.c_str());
- return;
- }
-
- output_file.write(reinterpret_cast(&header), sizeof(DDSHeader));
-
- GPUEndianSwapTexture(buffer, static_cast(image->texture.basemap->Format.Endian));
-
- // Calculate row pitch based on format
- UINT rowPitch;
-
- switch (format)
- {
- case GPUTEXTUREFORMAT_DXT1:
- rowPitch = (width / 4) * 8; // 8 bytes per 4x4 block
- break;
- case GPUTEXTUREFORMAT_DXT2_3:
- case GPUTEXTUREFORMAT_DXT4_5:
- case GPUTEXTUREFORMAT_DXN:
- rowPitch = (width / 4) * 16; // 16 bytes per 4x4 block
- break;
- case GPUTEXTUREFORMAT_8:
- rowPitch = width; // 1 byte per pixel
- break;
- case GPUTEXTUREFORMAT_8_8:
- rowPitch = width * 2; // 2 bytes per pixel
- break;
- case GPUTEXTUREFORMAT_8_8_8_8:
- rowPitch = width * 4; // 4 bytes per pixel
- break;
- default:
- rowPitch = width * 4; // Default to 4 bytes per pixel
- break;
- }
-
- // Create a buffer for linear texture data
- std::vector linearData(buffer.size());
- std::vector bufferAsUint8(buffer.begin(), buffer.end());
-
- // Convert tiled texture to linear layout
- XGUntileTextureLevel(width, // Width
- height, // Height
- 0, // Level (base level)
- static_cast(format), // GpuFormat
- 0, // Flags (no special flags)
- linearData.data(), // pDestination (linear output)
- rowPitch, // RowPitch
- nullptr, // pPoint (no offset)
- bufferAsUint8.data(), // pSource (tiled input)
- nullptr // pRect (entire texture)
- );
-
- output_file.write(reinterpret_cast(linearData.data()), linearData.size());
- output_file.close();
-
- Com_Printf(CON_CHANNEL_CONSOLEONLY, "Dumped highmip file: %s\n", output_filepath.c_str());
+ return false;
}
-}
-void Image_Replace_2D(GfxImage *image, const DDSImage &ddsImage)
-{
- if (image->mapType != MAPTYPE_2D)
+ if (baseData == NULL || mipData == NULL)
{
- Com_PrintError(CON_CHANNEL_ERROR, "Image '%s' is not a 2D map!\n", image->name);
- return;
+ Com_PrintError(CON_CHANNEL_ERROR, "Image_Replace_2D: Image '%s' has no valid texture memory!\n", image->name);
+ return false;
}
- // Get base texture layout
- UINT baseAddress, baseSize, mipAddress, mipSize;
-
- XGGetTextureLayout(image->texture.basemap, &baseAddress, &baseSize, 0, 0, 0, &mipAddress, &mipSize, 0, 0, 0);
-
- XGTEXTURE_DESC TextureDesc;
- XGGetTextureDesc(image->texture.basemap, 0, &TextureDesc);
-
- UINT mipTailBaseLevel =
- XGGetMipTailBaseLevel(TextureDesc.Width, TextureDesc.Height, XGIsBorderTexture(image->texture.basemap));
+ uint32_t ddsOffset =
+ image::CalculateDdsMipOffset(ddsImage.header.dwWidth, ddsImage.header.dwHeight, format, ddsFirstMipLevel);
- UINT ddsOffset = 0;
-
- for (UINT mipLevel = 0; mipLevel < mipTailBaseLevel; mipLevel++)
+ for (uint32_t localMipLevel = 0; localMipLevel < nonPackedLevelCount; localMipLevel++)
{
- UINT widthInBlocks = max(1, TextureDesc.WidthInBlocks >> mipLevel);
- UINT rowPitch = widthInBlocks * TextureDesc.BytesPerBlock;
- // UINT levelSize = rowPitch * heightInBlocks;
- UINT ddsMipLevelSize =
- CalculateMipLevelSize(image->width, image->height, mipLevel,
- static_cast(image->texture.basemap->Format.DataFormat));
-
- if (ddsMipLevelSize == 0)
+ const uint32_t ddsMipLevel = ddsFirstMipLevel + localMipLevel;
+ uint32_t rowPitch = image::xenos_texture::CalculateLinearRowPitch(ddsImage.header.dwWidth, ddsMipLevel, format);
+ uint32_t ddsMipLevelSize = image::xenos_texture::CalculateLinearLevelSize(
+ ddsImage.header.dwWidth, ddsImage.header.dwHeight, ddsMipLevel, format);
+ uint32_t tiledMipLevelSize = image::xenos_texture::CalculateTiledLevelSize(
+ image->width, image->height, localMipLevel, format, image->texture.basemap->Format.Pitch);
+
+ if (ddsMipLevelSize == 0 || tiledMipLevelSize == 0 || rowPitch == 0)
{
- DbgPrint(" [ERROR] Unsupported format %d for mip level %u! Skipping...\n",
- image->texture.basemap->Format.DataFormat, mipLevel);
- break;
+ Com_PrintError(CON_CHANNEL_ERROR, "Image_Replace_2D: Unsupported format %d for image '%s' mip level %u\n",
+ image->texture.basemap->Format.DataFormat, image->name, localMipLevel);
+ return false;
}
// Ensure we're not reading out of bounds
if (ddsOffset + ddsMipLevelSize > ddsImage.data.size())
{
- DbgPrint(" [ERROR] Mip Level %u exceeds DDS data size! Skipping...\n", mipLevel);
- break;
+ Com_PrintError(CON_CHANNEL_ERROR, "Image_Replace_2D: Image '%s' mip level %u exceeds DDS data size\n",
+ image->name, ddsMipLevel);
+ return false;
}
std::vector levelData(ddsImage.data.begin() + ddsOffset,
ddsImage.data.begin() + ddsOffset + ddsMipLevelSize);
- GPUEndianSwapTexture(levelData, static_cast(image->texture.basemap->Format.Endian));
-
- DbgPrint("Image_Replace_2D: Mip Level %d - Row Pitch=%u\n", mipLevel, rowPitch);
+ image::xenos_texture::ApplyGpuEndian(levelData.data(), levelData.size(),
+ static_cast(image->texture.basemap->Format.Endian));
- UINT address = baseAddress;
- if (mipLevel > 0)
+ unsigned char *destination = baseData;
+ if (localMipLevel > 0)
{
- UINT mipLevelOffset = XGGetMipLevelOffset(image->texture.basemap, 0, mipLevel);
- address = mipAddress + mipLevelOffset;
+ destination = mipData + image::xenos_texture::CalculateMipLevelOffset(image->width, image->height,
+ localMipLevel, format, 1u);
}
- DbgPrint("Image_Replace_2D: Writing mip level %d to address 0x%08X - levelSize=%u\n", mipLevel, address,
- ddsMipLevelSize);
+ std::vector tiledData(tiledMipLevelSize);
+
+ if (!image::xenos_texture::TileTextureLevel(image->width, image->height, localMipLevel, format,
+ image->texture.basemap->Format.Pitch, tiledData.data(),
+ tiledData.size(), levelData.data(), levelData.size(), rowPitch))
+ {
+ Com_PrintError(
+ CON_CHANNEL_ERROR,
+ "Image_Replace_2D: Failed to tile mip level %d for image '%s' rowPitch=%u sourceSize=%u destSize=%u\n",
+ localMipLevel, image->name, rowPitch, static_cast(levelData.size()), tiledMipLevelSize);
+ return false;
+ }
- // // Write the base level
- XGTileTextureLevel(TextureDesc.Width, TextureDesc.Height, mipLevel, image->texture.basemap->Format.DataFormat,
- XGTILE_NONPACKED, // Use non-packed mode (likely required for this texture)
- reinterpret_cast(address), // Destination (tiled GPU memory for Base)
- nullptr, // No offset (tile the whole image)
- levelData.data(), // Source mip level data
- rowPitch, // Row pitch of source image (should match DDS format)
- nullptr // No subrectangle (tile the full image)
- );
+ memcpy(destination, tiledData.data(), tiledMipLevelSize);
ddsOffset += ddsMipLevelSize;
}
+
+ return true;
}
-void Image_Replace_Cube(GfxImage *image, const DDSImage &ddsImage)
+bool Image_Replace_Cube(GfxImage *image, const image::DdsImage &ddsImage)
{
if (image->mapType != MAPTYPE_CUBE)
{
Com_PrintError(CON_CHANNEL_ERROR, "Image '%s' is not a cube map!\n", image->name);
- return;
+ return false;
}
const GPUTEXTUREFORMAT format = static_cast(image->texture.basemap->Format.DataFormat);
- // Check can we get the base size here and /6 for face size
- unsigned int face_size = 0;
- unsigned int rowPitch = 0;
+ unsigned int face_size = image::xenos_texture::CalculateLinearLevelSize(image->width, image->height, 0, format);
+ unsigned int rowPitch = image::xenos_texture::CalculateLinearRowPitch(image->width, 0, format);
+ unsigned int tiledFaceSize = image::xenos_texture::CalculateTiledLevelSize(image->width, image->height, 0, format,
+ image->texture.basemap->Format.Pitch);
+ unsigned int tiledBaseSize =
+ image::xenos_texture::CalculateBaseSize(image->texture.basemap, image->width, image->height, 6u);
+ unsigned char *baseData = image::xenos_texture::GetTextureBase(image->texture.basemap, image->pixels);
- switch (format)
+ if (face_size == 0 || rowPitch == 0 || tiledFaceSize == 0 || tiledBaseSize < tiledFaceSize * 6u)
{
- case GPUTEXTUREFORMAT_DXT1:
- face_size = (image->width / 4) * (image->height / 4) * 8;
- rowPitch = (image->width / 4) * 8; // 8 bytes per 4x4 block
- break;
- case GPUTEXTUREFORMAT_8_8_8_8:
- face_size = image->width * image->height * 4;
- rowPitch = image->width * 4; // 4 bytes per pixel
- break;
- default:
Com_PrintError(CON_CHANNEL_ERROR, "Image '%s' has unsupported format %d!\n", image->name, format);
- return;
+ return false;
+ }
+
+ if (baseData == NULL)
+ {
+ Com_PrintError(CON_CHANNEL_ERROR, "Image_Replace_Cube: Image '%s' has no valid texture memory!\n", image->name);
+ return false;
+ }
+
+ size_t requiredDDSSize = 0;
+ if (!ValidateCubeReplacementData(image, ddsImage, format, face_size, tiledBaseSize, &requiredDDSSize))
+ {
+ if (requiredDDSSize == 0)
+ {
+ Com_PrintError(CON_CHANNEL_ERROR, "Image '%s' has unsupported cube replacement format %d!\n", image->name,
+ format);
+ }
+ else if (ddsImage.data.size() < requiredDDSSize)
+ {
+ Com_PrintError(CON_CHANNEL_ERROR,
+ "Image_Replace_Cube: Image '%s' DDS is too small for 6 faces: have=%u need=%u\n",
+ image->name, static_cast(ddsImage.data.size()),
+ static_cast(requiredDDSSize));
+ }
+ else
+ {
+ Com_PrintError(CON_CHANNEL_ERROR, "Image '%s' cube replacement exceeds texture memory: have=%u need=%u\n",
+ image->name, static_cast(image->cardMemory.platform[0]), tiledBaseSize);
+ }
+
+ return false;
}
for (int i = 0; i < 6; i++)
{
const unsigned char *face_pixels = ddsImage.data.data() + (i * face_size);
+ unsigned char *face_destination = baseData + (i * tiledFaceSize);
// Create a buffer for the tiled texture data
- std::vector tiledData(face_size);
-
- // Convert the linear texture to tiled format using XGTileTextureLevel
- XGTileTextureLevel(image->width, // Width
- image->height, // Height
- 0, // Level (base level)
- static_cast(format), // GpuFormat
- 0, // Flags (no special flags)
- tiledData.data(), // pDestination (tiled output)
- nullptr, // pPoint (no offset)
- face_pixels, // pSource (linear input)
- rowPitch, // RowPitch
- nullptr // pRect (entire texture)
- );
-
- GPUEndianSwapTexture(tiledData, static_cast(image->texture.basemap->Format.Endian));
+ std::vector tiledData(tiledFaceSize);
+
+ if (!image::xenos_texture::TileTextureLevel(image->width, image->height, 0, static_cast(format),
+ image->texture.basemap->Format.Pitch, tiledData.data(),
+ tiledData.size(), face_pixels, face_size, rowPitch))
+ {
+ Com_PrintError(
+ CON_CHANNEL_ERROR,
+ "Image_Replace_Cube: Failed to tile image '%s' face %d rowPitch=%u sourceSize=%u destSize=%u\n",
+ image->name, i, rowPitch, face_size, tiledFaceSize);
+ return false;
+ }
+
+ image::xenos_texture::ApplyGpuEndian(tiledData.data(), tiledData.size(),
+ static_cast(image->texture.basemap->Format.Endian));
// Copy the data to the image
- memcpy(image->pixels + (i * face_size), tiledData.data(), face_size);
+ memcpy(face_destination, tiledData.data(), tiledFaceSize);
}
+
+ return true;
}
void Image_Replace(GfxImage *image)
{
- const std::string replacement_base_dir = Config::GetModBasePath() + "\\images";
- const std::string replacement_path = replacement_base_dir + "\\" + image->name + ".dds";
+ const std::string replacement_path = GetImageReplacementPath(image->name);
if (!filesystem::file_exists(replacement_path))
{
- Com_PrintError(CON_CHANNEL_ERROR, "File does not exist: %s\n", replacement_path.c_str());
return;
}
- StaticDDSImage staticDDSImage = ReadDDSFileStatic(replacement_path.c_str());
- if (staticDDSImage.data == nullptr)
- {
- Com_PrintError(CON_CHANNEL_ERROR, "Failed to load DDS file: %s\n", replacement_path.c_str());
- return;
- }
-
- DbgPrint("[codxe][image_loader] DDS loaded for isolation: %s bytes=%u\n", replacement_path.c_str(),
- static_cast(staticDDSImage.dataSize));
- return;
-
- DDSImage ddsImage = ReadDDSFile(replacement_path.c_str());
+ image::DdsImage ddsImage = image::LoadDdsFromFile(replacement_path.c_str());
if (ddsImage.data.empty())
{
Com_PrintError(CON_CHANNEL_ERROR, "Failed to load DDS file: %s\n", replacement_path.c_str());
return;
}
- if (image->width != ddsImage.header.width || image->height != ddsImage.header.height)
+ if (ddsImage.header.dwSize != image::DDS_HEADER_SIZE ||
+ ddsImage.header.ddspf.dwSize != image::DDS_PIXEL_FORMAT_SIZE)
{
- Com_PrintError(CON_CHANNEL_ERROR, "Image '%s' dimensions do not match DDS file: %s\n", image->name,
- replacement_path.c_str());
+ Com_PrintError(CON_CHANNEL_ERROR, "Image '%s' has an invalid DDS header: size=%u pixelFormatSize=%u\n",
+ image->name, ddsImage.header.dwSize, ddsImage.header.ddspf.dwSize);
return;
}
GPUTEXTUREFORMAT ddsFormat;
- switch (ddsImage.header.pixelFormat.fourCC)
+ if (!ddsImage.GetGpuFormat(&ddsFormat))
{
- case DXT1_FOURCC:
- ddsFormat = GPUTEXTUREFORMAT_DXT1;
- break;
- case DXT3_FOURCC:
- ddsFormat = GPUTEXTUREFORMAT_DXT2_3;
- break;
- case DXT5_FOURCC:
- ddsFormat = GPUTEXTUREFORMAT_DXT4_5;
- break;
- case DXN_FOURCC:
- ddsFormat = GPUTEXTUREFORMAT_DXN;
- break;
- default:
- Com_PrintError(CON_CHANNEL_ERROR, "Image '%s' has an unsupported DDS format: 0x%X\n", image->name,
- ddsImage.header.pixelFormat.fourCC);
+ Com_PrintError(CON_CHANNEL_ERROR, "Image '%s' has an unsupported DDS format: flags=0x%X fourCC=0x%X\n",
+ image->name, ddsImage.header.ddspf.dwFlags, ddsImage.header.ddspf.dwFourCC);
return;
}
@@ -1004,23 +659,100 @@ void Image_Replace(GfxImage *image)
return;
}
+ const bool ddsIsCubemap = ddsImage.IsCubemap();
+ const bool ddsMatchesImageDimensions =
+ image->width == ddsImage.header.dwWidth && image->height == ddsImage.header.dwHeight;
+ const bool ddsMatchesStreamDimensions = image->streaming && image->mapType == MAPTYPE_2D && !ddsIsCubemap &&
+ ddsImage.header.dwWidth == static_cast(image->width) * 2u &&
+ ddsImage.header.dwHeight == static_cast(image->height) * 2u;
+ uint32_t ddsFirstMipLevel = 0;
+
+ if (image->mapType == MAPTYPE_2D && ddsIsCubemap)
+ {
+ Com_PrintError(CON_CHANNEL_ERROR, "Image '%s' is 2D but replacement DDS is a cubemap!\n", image->name);
+ return;
+ }
+
+ if (image->streaming && image->mapType == MAPTYPE_2D)
+ {
+ if (!ddsMatchesStreamDimensions)
+ {
+ Com_PrintError(CON_CHANNEL_ERROR,
+ "Streamed image '%s' replacement must include the streamed mip: expected=%ux%u got=%ux%u "
+ "%s\n",
+ image->name, static_cast(image->width) * 2u,
+ static_cast(image->height) * 2u, ddsImage.header.dwWidth, ddsImage.header.dwHeight,
+ replacement_path.c_str());
+ return;
+ }
+
+ const uint32_t ddsMipCount = ddsImage.GetMipCount();
+ if (ddsMipCount < 2u)
+ {
+ Com_PrintError(CON_CHANNEL_ERROR,
+ "Image '%s' replacement DDS starts at the streamed mip but has no resident mip: "
+ "%ux%u mipCount=%u\n",
+ image->name, ddsImage.header.dwWidth, ddsImage.header.dwHeight, ddsMipCount);
+ return;
+ }
+
+ ddsFirstMipLevel = 1u;
+ }
+ else if (!ddsMatchesImageDimensions)
+ {
+ Com_PrintError(CON_CHANNEL_ERROR, "Image '%s' dimensions do not match DDS file: image=%ux%u dds=%ux%u %s\n",
+ image->name, image->width, image->height, ddsImage.header.dwWidth, ddsImage.header.dwHeight,
+ replacement_path.c_str());
+ return;
+ }
+
+ if (image->mapType == MAPTYPE_CUBE && !ddsIsCubemap)
+ {
+ const uint32_t faceSize =
+ image::xenos_texture::CalculateLinearLevelSize(image->width, image->height, 0, ddsFormat);
+ if (faceSize == 0 || ddsImage.data.size() < static_cast(faceSize) * 6u)
+ {
+ Com_PrintError(CON_CHANNEL_ERROR,
+ "Image '%s' is a cubemap but replacement DDS is not a valid 6-face cubemap!\n", image->name);
+ return;
+ }
+
+ Com_Printf(CON_CHANNEL_CONSOLEONLY,
+ "Image '%s' replacement DDS has no cubemap caps but contains enough data for 6 sequential faces; "
+ "accepting it.\n",
+ image->name);
+ }
+
+ bool replaced = false;
if (image->mapType == MAPTYPE_2D)
{
- Image_Replace_2D(image, ddsImage);
+ replaced = Image_Replace_2D(image, ddsImage, ddsFirstMipLevel);
}
else if (image->mapType == MAPTYPE_CUBE)
{
- Image_Replace_Cube(image, ddsImage);
+ replaced = Image_Replace_Cube(image, ddsImage);
}
else
{
Com_PrintError(CON_CHANNEL_ERROR, "Image '%s' is not a 2D or cube map!\n", image->name);
return;
}
+
+ if (replaced)
+ {
+ if (ddsFirstMipLevel > 0)
+ {
+ g_streamedImageReplacements.insert(image->name);
+ }
+
+ Com_Printf(CON_CHANNEL_CONSOLEONLY, "Replaced image '%s'\n", image->name);
+ }
}
void Load_images()
{
+ g_streamedImageReplacements.clear();
+
const int MAX_IMAGES = 2048;
XAssetHeader assets[MAX_IMAGES];
const auto count = DB_GetAllXAssetOfType_FastFile(ASSET_TYPE_IMAGE, assets, MAX_IMAGES);
@@ -1051,50 +783,142 @@ bool R_StreamLoadHighMipReplacement(const char *filename, unsigned int bytesToRe
auto image = asset->entry.asset.header.image;
- // Use new ConfigModule API
- std::string replacement_path = Config::GetModBasePath() + "\\highmip" + "\\" + asset_name + ".dds";
- std::ifstream file(replacement_path, std::ios::binary | std::ios::ate);
- if (!file)
+ if (image == NULL || image->texture.basemap == NULL || image->mapType != MAPTYPE_2D)
{
return false;
}
- std::streamsize file_size = file.tellg();
- file.seekg(0, std::ios::beg);
+ const uint32_t highMipWidth = static_cast(image->width) * 2u;
+ const uint32_t highMipHeight = static_cast(image->height) * 2u;
+ const bool blockStockStream = g_streamedImageReplacements.find(asset_name) != g_streamedImageReplacements.end();
- if (file_size - 0x80 != bytesToRead) // 0x80 is the size of the DDS header
+ if (!blockStockStream)
{
- Com_PrintError(CON_CHANNEL_ERROR, "R_StreamLoadHighMipReplacement: File size mismatch: %s\n", replacement_path);
return false;
}
- std::vector ddsHeader(0x80);
- file.read(reinterpret_cast(ddsHeader.data()), 0x80);
+ const auto tryReplaceHighMipDDS = [&](const std::string &replacement_path, bool quietDimensionMismatch) -> bool
+ {
+ image::DdsImage ddsImage = image::LoadDdsFromFile(replacement_path.c_str());
+ if (ddsImage.data.empty())
+ {
+ Com_PrintError(CON_CHANNEL_ERROR, "R_StreamLoadHighMipReplacement: Failed to load DDS file: %s\n",
+ replacement_path.c_str());
+ return false;
+ }
+
+ if (ddsImage.header.dwSize != image::DDS_HEADER_SIZE ||
+ ddsImage.header.ddspf.dwSize != image::DDS_PIXEL_FORMAT_SIZE)
+ {
+ Com_PrintError(CON_CHANNEL_ERROR,
+ "R_StreamLoadHighMipReplacement: Image '%s' has an invalid DDS header: size=%u "
+ "pixelFormatSize=%u\n",
+ asset_name.c_str(), ddsImage.header.dwSize, ddsImage.header.ddspf.dwSize);
+ return false;
+ }
+
+ GPUTEXTUREFORMAT ddsFormat;
+ if (!ddsImage.GetGpuFormat(&ddsFormat))
+ {
+ Com_PrintError(CON_CHANNEL_ERROR,
+ "R_StreamLoadHighMipReplacement: Image '%s' has an unsupported DDS format: flags=0x%X "
+ "fourCC=0x%X\n",
+ asset_name.c_str(), ddsImage.header.ddspf.dwFlags, ddsImage.header.ddspf.dwFourCC);
+ return false;
+ }
+
+ if (static_cast(image->texture.basemap->Format.DataFormat) != static_cast(ddsFormat))
+ {
+ Com_PrintError(CON_CHANNEL_ERROR,
+ "R_StreamLoadHighMipReplacement: Image '%s' format does not match DDS: expected=%u "
+ "got=%u\n",
+ asset_name.c_str(), static_cast(image->texture.basemap->Format.DataFormat),
+ static_cast(ddsFormat));
+ return false;
+ }
+
+ if (ddsImage.header.dwWidth != highMipWidth || ddsImage.header.dwHeight != highMipHeight)
+ {
+ if (!quietDimensionMismatch)
+ {
+ Com_PrintError(CON_CHANNEL_ERROR,
+ "R_StreamLoadHighMipReplacement: Image '%s' dimensions do not match streamed mip: "
+ "expected=%ux%u got=%ux%u\n",
+ asset_name.c_str(), highMipWidth, highMipHeight, ddsImage.header.dwWidth,
+ ddsImage.header.dwHeight);
+ }
+
+ return false;
+ }
+
+ const uint32_t ddsMipCount = ddsImage.GetMipCount();
+ if (ddsMipCount < 2u)
+ {
+ Com_PrintError(CON_CHANNEL_ERROR,
+ "R_StreamLoadHighMipReplacement: Image '%s' replacement DDS must include stream and "
+ "resident mips: mipCount=%u\n",
+ asset_name.c_str(), ddsMipCount);
+ return false;
+ }
+
+ const uint32_t sourceSize = image::xenos_texture::CalculateLinearLevelSize(
+ ddsImage.header.dwWidth, ddsImage.header.dwHeight, 0u, ddsFormat);
+ const uint32_t rowPitch = image::xenos_texture::CalculateLinearRowPitch(ddsImage.header.dwWidth, 0u, ddsFormat);
+ const uint32_t tiledSize = image::xenos_texture::CalculateTiledLevelSize(
+ ddsImage.header.dwWidth, ddsImage.header.dwHeight, 0u, ddsFormat, 0u);
+
+ if (sourceSize == 0 || rowPitch == 0 || tiledSize == 0)
+ {
+ Com_PrintError(CON_CHANNEL_ERROR,
+ "R_StreamLoadHighMipReplacement: Image '%s' has unsupported replacement format %u\n",
+ asset_name.c_str(), static_cast(ddsFormat));
+ return false;
+ }
+
+ if (ddsImage.data.size() < sourceSize)
+ {
+ Com_PrintError(CON_CHANNEL_ERROR,
+ "R_StreamLoadHighMipReplacement: Image '%s' DDS data is too small: have=%u need=%u\n",
+ asset_name.c_str(), static_cast(ddsImage.data.size()), sourceSize);
+ return false;
+ }
- // TODO: check if file is DDS and has correct format and dimensions
+ if (tiledSize != bytesToRead)
+ {
+ Com_PrintError(CON_CHANNEL_ERROR,
+ "R_StreamLoadHighMipReplacement: Image '%s' stream size mismatch: expected=%u got=%u\n",
+ asset_name.c_str(), bytesToRead, tiledSize);
+ return false;
+ }
- std::vector buffer;
- buffer.resize(static_cast(bytesToRead));
- file.read(reinterpret_cast(buffer.data()), bytesToRead);
+ std::vector buffer(ddsImage.data.begin(), ddsImage.data.begin() + sourceSize);
+ image::xenos_texture::ApplyGpuEndian(buffer.data(), buffer.size(),
+ static_cast(image->texture.basemap->Format.Endian));
- GPUEndianSwapTexture(buffer, static_cast(image->texture.basemap->Format.Endian));
+ if (!image::xenos_texture::TileTextureLevel(ddsImage.header.dwWidth, ddsImage.header.dwHeight, 0u, ddsFormat,
+ 0u, outData, bytesToRead, buffer.data(), buffer.size(), rowPitch))
+ {
+ Com_PrintError(CON_CHANNEL_ERROR, "R_StreamLoadHighMipReplacement: Failed to tile image '%s'\n",
+ asset_name.c_str());
+ return false;
+ }
- XGTEXTURE_DESC textureDesc;
- XGGetTextureDesc(image->texture.basemap, 0, &textureDesc);
+ Com_Printf(CON_CHANNEL_CONSOLEONLY, "Replaced streamed image '%s'\n", asset_name.c_str());
+ return true;
+ };
- // High mip are 2x the size of the original image
- auto width = image->width * 2;
- auto height = image->height * 2;
- auto rowPitch = textureDesc.RowPitch * 2;
+ const std::string combined_path = GetImageReplacementPath(asset_name.c_str());
+ if (filesystem::file_exists(combined_path) && tryReplaceHighMipDDS(combined_path, true))
+ {
+ return true;
+ }
- XGTileTextureLevel(width, height, 0, image->texture.basemap->Format.DataFormat,
- XGTILE_NONPACKED, // Use non-packed mode (likely required for this texture)
- outData, // Destination (tiled GPU memory for Base)
- nullptr, // No offset (tile the whole image)
- buffer.data(), // Source mip level data
- rowPitch, // Row pitch of source image (should match DDS format)
- nullptr // No subrectangle (tile the full image)
- );
+ Com_PrintError(CON_CHANNEL_ERROR, "R_StreamLoadHighMipReplacement: Blocking stock stream for replaced image '%s'\n",
+ asset_name.c_str());
+ if (outData != NULL && bytesToRead > 0)
+ {
+ memset(outData, 0, bytesToRead);
+ }
return true;
}
@@ -1108,7 +932,7 @@ int R_StreamLoadFileSynchronously_Hook(const char *filename, unsigned int bytesT
return 1;
}
- // Fallback to original path if modified path failed
+ // Let the game handle stock streams only when we did not accept a streamed replacement for this image.
return R_StreamLoadFileSynchronously_Detour.GetOriginal()(
filename, bytesToRead, outData);
}
@@ -1119,7 +943,7 @@ image_loader::image_loader()
CG_RegisterGraphics_Detour = Detour(CG_RegisterGraphics, CG_RegisterGraphics_Hook);
CG_RegisterGraphics_Detour.Install();
- // Load highmip texture replacements from active mod folder
+ // Load streamed mip texture replacements from active mod folder
R_StreamLoadFileSynchronously_Detour = Detour(R_StreamLoadFileSynchronously, R_StreamLoadFileSynchronously_Hook);
R_StreamLoadFileSynchronously_Detour.Install();
diff --git a/src/game/iw3/mp/components/scr_parser.cpp b/src/game/iw3/mp/components/scr_parser.cpp
index e7d6dbc0..f8bcf7c4 100644
--- a/src/game/iw3/mp/components/scr_parser.cpp
+++ b/src/game/iw3/mp/components/scr_parser.cpp
@@ -113,7 +113,7 @@ char *Scr_AddSourceBuffer_Hook(const char *filename, const char *extFilename, co
archive);
};
- if (Config::dump_rawfile)
+ if (Config::dump_assets)
{
char *contents = callOriginal();
if (contents != nullptr)
diff --git a/src/game/iw3/sp/components/scr_parser.cpp b/src/game/iw3/sp/components/scr_parser.cpp
index 47e12fb5..85810d59 100644
--- a/src/game/iw3/sp/components/scr_parser.cpp
+++ b/src/game/iw3/sp/components/scr_parser.cpp
@@ -15,7 +15,7 @@ char *Scr_AddSourceBuffer_Hook(const char *filename, const char *extFilename, co
archive);
};
- if (Config::dump_rawfile)
+ if (Config::dump_assets)
{
DbgPrint("GSCLoader: Dumping script %s\n", extFilename);
auto contents = callOriginal();
diff --git a/src/game/iw4/mp_tu6/components/clipmap.cpp b/src/game/iw4/mp_tu6/components/clipmap.cpp
index cf7a517a..99b3e468 100644
--- a/src/game/iw4/mp_tu6/components/clipmap.cpp
+++ b/src/game/iw4/mp_tu6/components/clipmap.cpp
@@ -6,10 +6,7 @@
iw4::mp_tu6::dvar_t *noclip_brushes = nullptr;
std::vector original_brush_contents;
-Detour DB_LinkXAssetEntry1_Detour;
-
-iw4::mp_tu6::XAssetEntryPoolEntry *DB_LinkXAssetEntry1_Hook(iw4::mp_tu6::XAssetType type,
- iw4::mp_tu6::XAssetHeader *header)
+void OnDBLinkXAssetPre(iw4::mp_tu6::XAssetType &type, iw4::mp_tu6::XAssetHeader *header)
{
// Register once
// TODO: Move to dvar init event
@@ -19,9 +16,6 @@ iw4::mp_tu6::XAssetEntryPoolEntry *DB_LinkXAssetEntry1_Hook(iw4::mp_tu6::XAssetT
"noclip_brushes", "", 0x10, "Brush indices to disable playerclip. Use '*' for all, '' to restore");
}
- iw4::mp_tu6::XAssetEntryPoolEntry *entry =
- DB_LinkXAssetEntry1_Detour.GetOriginal()(type, header);
-
if (type == iw4::mp_tu6::ASSET_TYPE_CLIPMAP_MP)
{
// Resize the vector to match the number of brushes
@@ -34,8 +28,6 @@ iw4::mp_tu6::XAssetEntryPoolEntry *DB_LinkXAssetEntry1_Hook(iw4::mp_tu6::XAssetT
header->clipMap->brushContents[i]; // Assuming this is the field you want to save
}
}
-
- return entry;
}
void RestoreBrushContents()
@@ -133,8 +125,7 @@ void DisablePlayerClipOnIntersectingBrushes(iw4::mp_tu6::scr_entref_t entref)
clipmap::clipmap()
{
- DB_LinkXAssetEntry1_Detour = Detour(iw4::mp_tu6::DB_LinkXAssetEntry1, DB_LinkXAssetEntry1_Hook);
- DB_LinkXAssetEntry1_Detour.Install();
+ Events::OnDBLinkXAssetPre(OnDBLinkXAssetPre);
iw4::mp_tu6::Scr_AddMethod("disableplayercliponintersectingbrushes", DisablePlayerClipOnIntersectingBrushes,
iw4::mp_tu6::BUILTIN_ANY);
@@ -179,5 +170,4 @@ clipmap::clipmap()
clipmap::~clipmap()
{
- DB_LinkXAssetEntry1_Detour.Remove();
}
diff --git a/src/game/iw4/mp_tu6/components/events.cpp b/src/game/iw4/mp_tu6/components/events.cpp
index 70177444..3ea6eedb 100644
--- a/src/game/iw4/mp_tu6/components/events.cpp
+++ b/src/game/iw4/mp_tu6/components/events.cpp
@@ -63,6 +63,41 @@ void Events::OnCmdInit(const std::function &callback)
Detour Events::Cmd_Init_Detour;
+std::vector>
+ Events::db_linkxasset_pre_callbacks;
+std::vector> Events::db_linkxasset_post_callbacks;
+Detour Events::DB_LinkXAssetEntry1_Detour;
+
+iw4::mp_tu6::XAssetEntryPoolEntry *Events::DB_LinkXAssetEntry1_Hook(iw4::mp_tu6::XAssetType type,
+ iw4::mp_tu6::XAssetHeader *header)
+{
+ for (auto it = db_linkxasset_pre_callbacks.begin(); it != db_linkxasset_pre_callbacks.end(); ++it)
+ {
+ (*it)(type, header);
+ }
+
+ iw4::mp_tu6::XAssetEntryPoolEntry *entry =
+ DB_LinkXAssetEntry1_Detour.GetOriginal()(type, header);
+
+ for (auto it = db_linkxasset_post_callbacks.begin(); it != db_linkxasset_post_callbacks.end(); ++it)
+ {
+ (*it)(entry);
+ }
+
+ return entry;
+}
+
+void Events::OnDBLinkXAssetPre(
+ const std::function &callback)
+{
+ db_linkxasset_pre_callbacks.emplace_back(callback);
+}
+
+void Events::OnDBLinkXAssetPost(const std::function &callback)
+{
+ db_linkxasset_post_callbacks.emplace_back(callback);
+}
+
std::vector> Events::vmshutdown_callbacks;
void Events::Scr_ShutdownSystem_Hook(unsigned __int8 sys)
@@ -93,6 +128,9 @@ Events::Events()
Cmd_Init_Detour = Detour(iw4::mp_tu6::Cmd_Init, Cmd_Init_Hook);
Cmd_Init_Detour.Install();
+ DB_LinkXAssetEntry1_Detour = Detour(iw4::mp_tu6::DB_LinkXAssetEntry1, DB_LinkXAssetEntry1_Hook);
+ DB_LinkXAssetEntry1_Detour.Install();
+
Scr_ShutdownSystem_Detour = Detour(iw4::mp_tu6::Scr_ShutdownSystem, Scr_ShutdownSystem_Hook);
Scr_ShutdownSystem_Detour.Install();
}
@@ -102,10 +140,13 @@ Events::~Events()
Com_InitDvars_Detour.Remove();
CG_DrawActive_Detour.Remove();
Cmd_Init_Detour.Remove();
+ DB_LinkXAssetEntry1_Detour.Remove();
Scr_ShutdownSystem_Detour.Remove();
com_initdvars_callbacks.clear();
cg_drawactive_callbacks.clear();
cmdinit_callbacks.clear();
+ db_linkxasset_pre_callbacks.clear();
+ db_linkxasset_post_callbacks.clear();
vmshutdown_callbacks.clear();
}
diff --git a/src/game/iw4/mp_tu6/components/events.h b/src/game/iw4/mp_tu6/components/events.h
index aa3fff09..65c212c9 100644
--- a/src/game/iw4/mp_tu6/components/events.h
+++ b/src/game/iw4/mp_tu6/components/events.h
@@ -16,6 +16,9 @@ class Events : public Module
static void OnDvarInit(const std::function &callback);
static void OnCG_DrawActive(const std::function &callback);
static void OnCmdInit(const std::function &callback);
+ static void OnDBLinkXAssetPre(
+ const std::function &callback);
+ static void OnDBLinkXAssetPost(const std::function &callback);
static void OnVMShutdown(const std::function &callback);
private:
@@ -31,6 +34,13 @@ class Events : public Module
static Detour Cmd_Init_Detour;
static void Cmd_Init_Hook();
+ static std::vector>
+ db_linkxasset_pre_callbacks;
+ static std::vector> db_linkxasset_post_callbacks;
+ static Detour DB_LinkXAssetEntry1_Detour;
+ static iw4::mp_tu6::XAssetEntryPoolEntry *DB_LinkXAssetEntry1_Hook(iw4::mp_tu6::XAssetType type,
+ iw4::mp_tu6::XAssetHeader *header);
+
static std::vector> vmshutdown_callbacks;
static Detour Scr_ShutdownSystem_Detour;
static void Scr_ShutdownSystem_Hook(unsigned __int8 sys);
diff --git a/src/game/iw4/mp_tu6/components/image_loader.cpp b/src/game/iw4/mp_tu6/components/image_loader.cpp
new file mode 100644
index 00000000..020c5065
--- /dev/null
+++ b/src/game/iw4/mp_tu6/components/image_loader.cpp
@@ -0,0 +1,1413 @@
+#include "pch.h"
+#include "common/config.h"
+#include "events.h"
+#include "image_loader.h"
+#include "image/dds_loader.h"
+#include "image/dds_writer.h"
+#include "image/texture_layout.h"
+#include "image/xenos_texture.h"
+
+#include
+
+#ifndef INVALID_FILE_ATTRIBUTES
+#define INVALID_FILE_ATTRIBUTES ((DWORD) - 1)
+#endif
+
+#ifdef PtrToUint
+#undef PtrToUint
+#endif
+
+namespace
+{
+namespace game = iw4::mp_tu6;
+
+const uint32_t STREAM_PIXEL_SIZE_MASK = 0x3FFFFFF;
+const uint32_t MAX_STREAM_COMPRESSED_SIZE = 64u * 1024u * 1024u;
+const uint32_t DDS_FILE_HEADER_SIZE = sizeof(uint32_t) + sizeof(image::DDS_HEADER);
+const uint32_t REPLACEMENT_ROW_SCRATCH_SIZE = 64u * 1024u;
+
+typedef image::DdsImage DDSImage;
+
+struct ZlibStream
+{
+ unsigned __int8 *next_in;
+ unsigned int avail_in;
+ unsigned int total_in;
+ unsigned __int8 *next_out;
+ unsigned int avail_out;
+ unsigned int total_out;
+ char *msg;
+ void *state;
+ unsigned __int8 *(__fastcall *zalloc)(unsigned __int8 *opaque, unsigned int items, unsigned int size);
+ void(__fastcall *zfree)(unsigned __int8 *opaque, unsigned __int8 *ptr);
+ unsigned __int8 *opaque;
+ int data_type;
+};
+static_assert(sizeof(ZlibStream) == 48, "");
+
+game::dvar_t *dump_assets = nullptr;
+unsigned char g_replacementRowScratch[REPLACEMENT_ROW_SCRATCH_SIZE];
+
+struct DdsDataFile
+{
+ HANDLE handle;
+ std::string path;
+ uint32_t dataSize;
+
+ DdsDataFile(const std::string &filePath, uint32_t fileDataSize)
+ : handle(CreateFileA(filePath.c_str(), GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE, nullptr,
+ OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr)),
+ path(filePath), dataSize(fileDataSize)
+ {
+ }
+
+ ~DdsDataFile()
+ {
+ if (handle != INVALID_HANDLE_VALUE)
+ CloseHandle(handle);
+ }
+
+ bool IsValid() const
+ {
+ return handle != INVALID_HANDLE_VALUE;
+ }
+
+ bool SeekData(uint32_t dataOffset, uint32_t size) const
+ {
+ if (dataOffset > dataSize || size > dataSize - dataOffset)
+ {
+ game::Com_Printf(0, "DDS range outside file '%s': dataOffset=%u size=%u dataSize=%u\n", path.c_str(),
+ dataOffset, size, dataSize);
+ return false;
+ }
+
+ const DWORD fileOffset = static_cast(DDS_FILE_HEADER_SIZE) + dataOffset;
+ SetLastError(NO_ERROR);
+ const DWORD seekResult = SetFilePointer(handle, fileOffset, nullptr, FILE_BEGIN);
+ if (seekResult == INVALID_SET_FILE_POINTER && GetLastError() != NO_ERROR)
+ {
+ game::Com_Printf(0, "DDS seek failed '%s': offset=%u error=0x%08X\n", path.c_str(), fileOffset,
+ GetLastError());
+ return false;
+ }
+
+ return true;
+ }
+};
+
+struct DdsRowReadState
+{
+ DdsDataFile *file;
+ GPUENDIAN endian;
+};
+
+uint32_t PtrToUint(const void *ptr)
+{
+ return static_cast(reinterpret_cast(ptr));
+}
+
+void PrintImageError(const char *format, ...)
+{
+ char buffer[1024];
+ va_list args;
+ va_start(args, format);
+ _vsnprintf(buffer, sizeof(buffer), format, args);
+ va_end(args);
+ buffer[sizeof(buffer) - 1] = '\0';
+
+ game::Com_Printf(0, "%s", buffer);
+}
+
+void PrintImageInfo(const char *format, ...)
+{
+ char buffer[1024];
+ va_list args;
+ va_start(args, format);
+ _vsnprintf(buffer, sizeof(buffer), format, args);
+ va_end(args);
+ buffer[sizeof(buffer) - 1] = '\0';
+
+ game::Com_Printf(0, "%s", buffer);
+}
+
+bool ReadDdsTileRow(uint32_t rowIndex, unsigned char *rowBuffer, uint32_t rowPitch, void *userData)
+{
+ DdsRowReadState *state = static_cast(userData);
+ if (state == nullptr || state->file == nullptr || rowBuffer == nullptr || rowPitch == 0)
+ return false;
+
+ DWORD bytesRead = 0;
+ if (!ReadFile(state->file->handle, rowBuffer, rowPitch, &bytesRead, nullptr) || bytesRead != rowPitch)
+ {
+ PrintImageError("DDS row read failed '%s': row=%u size=%u actual=%u error=0x%08X\n",
+ state->file->path.c_str(), rowIndex, rowPitch, bytesRead, GetLastError());
+ return false;
+ }
+
+ image::xenos_texture::ApplyGpuEndian(rowBuffer, rowPitch, state->endian);
+ return true;
+}
+
+bool TileDdsLevelToTexture(DdsDataFile &ddsFile, uint32_t dataOffset, uint32_t expectedDataSize,
+ uint32_t sourceWidth, uint32_t sourceHeight, uint32_t sourceMipLevel,
+ uint32_t destinationWidth, uint32_t destinationHeight, uint32_t destinationMipLevel,
+ GPUTEXTUREFORMAT format, uint32_t basePitch, unsigned char *destination,
+ uint32_t destinationSize, GPUENDIAN endian)
+{
+ if (destination == nullptr || destinationSize == 0)
+ return false;
+
+ const uint32_t sourceRowPitch =
+ image::xenos_texture::CalculateLinearRowPitch(sourceWidth, sourceMipLevel, format);
+ const uint32_t sourceLevelSize =
+ image::xenos_texture::CalculateLinearLevelSize(sourceWidth, sourceHeight, sourceMipLevel, format);
+ if (sourceRowPitch == 0 || sourceLevelSize == 0 || sourceLevelSize != expectedDataSize)
+ return false;
+
+ if (sourceRowPitch > sizeof(g_replacementRowScratch))
+ {
+ PrintImageError("DDS row scratch too small '%s': rowPitch=%u scratch=%u\n", ddsFile.path.c_str(),
+ sourceRowPitch, static_cast(sizeof(g_replacementRowScratch)));
+ return false;
+ }
+
+ if (!ddsFile.SeekData(dataOffset, expectedDataSize))
+ return false;
+
+ DdsRowReadState rowState = {&ddsFile, endian};
+ memset(destination, 0, destinationSize);
+ return image::xenos_texture::TileTextureLevelFromRows(
+ destinationWidth, destinationHeight, destinationMipLevel, format, basePitch, destination, destinationSize,
+ sourceRowPitch, g_replacementRowScratch, sizeof(g_replacementRowScratch), ReadDdsTileRow, &rowState);
+}
+
+bool ImageFileExists(const std::string &path)
+{
+ const DWORD attributes = GetFileAttributesA(path.c_str());
+ return attributes != INVALID_FILE_ATTRIBUTES && (attributes & FILE_ATTRIBUTE_DIRECTORY) == 0;
+}
+
+std::string GetReplacementDirectory()
+{
+ return Config::GetModBasePath() + "\\images";
+}
+
+std::string GetUserReplacementDirectory()
+{
+ return std::string(USERRAW_DIR) + "\\images";
+}
+
+bool ReadDDSHeader(const std::string &filepath, DDSImage *out, uint32_t *dataSize)
+{
+ if (out == nullptr)
+ return false;
+
+ *out = DDSImage();
+ return image::LoadDdsHeaderFromFile(filepath, &out->header, dataSize);
+}
+
+std::string GetReplacementPath(const char *imageName)
+{
+ const std::string userPath = GetUserReplacementDirectory() + "\\" + imageName + ".dds";
+ if (ImageFileExists(userPath))
+ return userPath;
+
+ return GetReplacementDirectory() + "\\" + imageName + ".dds";
+}
+
+bool ValidateDDSHeaderFields(const game::GfxImage *image, const DDSImage &ddsImage, GPUTEXTUREFORMAT *ddsFormat)
+{
+ if (ddsImage.header.dwSize != image::DDS_HEADER_SIZE ||
+ ddsImage.header.ddspf.dwSize != image::DDS_PIXEL_FORMAT_SIZE)
+ {
+ PrintImageError("image '%s' has an invalid DDS header: size=%u pixelFormatSize=%u\n", image->name,
+ ddsImage.header.dwSize, ddsImage.header.ddspf.dwSize);
+ return false;
+ }
+
+ if (!ddsImage.GetGpuFormat(ddsFormat))
+ {
+ PrintImageError("image '%s' has an unsupported DDS format: flags=0x%X fourCC=0x%X bitCount=%u\n", image->name,
+ ddsImage.header.ddspf.dwFlags, ddsImage.header.ddspf.dwFourCC,
+ ddsImage.header.ddspf.dwRGBBitCount);
+ return false;
+ }
+
+ const uint32_t imageFormat = image->texture.basemap.Format.DataFormat;
+ if (imageFormat != static_cast(*ddsFormat))
+ {
+ PrintImageError("image '%s' format does not match DDS: expected=%u got=%u\n", image->name, imageFormat,
+ static_cast(*ddsFormat));
+ return false;
+ }
+
+ return true;
+}
+
+bool Validate2DReplacementData(const game::GfxImage *image, GPUTEXTUREFORMAT format, size_t ddsDataSize,
+ uint32_t replacementLevelCount, size_t *requiredDDSSize, size_t *requiredTextureBytes)
+{
+ *requiredDDSSize =
+ image::CalculateRequiredLinearDataSize(image->width, image->height, format, 0u, replacementLevelCount, 1u);
+ if (*requiredDDSSize == 0)
+ return false;
+
+ if (ddsDataSize < *requiredDDSSize)
+ return false;
+
+ const D3DBaseTexture *texture = &image->texture.basemap;
+ const uint32_t baseSize = image::xenos_texture::CalculateBaseSize(texture, image->width, image->height, 1u);
+ const size_t mipBytes =
+ image::CalculateRequiredMipTextureBytes(image->width, image->height, format, 1u, replacementLevelCount, 1u);
+
+ *requiredTextureBytes = static_cast(baseSize) + mipBytes;
+ const int cardMemory = image->cardMemory.platform[0];
+ if (cardMemory > 0 && *requiredTextureBytes > static_cast(cardMemory))
+ return false;
+
+ return true;
+}
+
+bool ValidateResidentMipCount(const game::GfxImage *image, const DDSImage &ddsImage, uint32_t textureLevelCount)
+{
+ const uint32_t ddsMipCount = ddsImage.GetMipCount();
+ if (ddsMipCount != textureLevelCount)
+ {
+ PrintImageError("image '%s' mip count does not match DDS: image=%u dds=%u\n", image->name, textureLevelCount,
+ ddsMipCount);
+ return false;
+ }
+
+ return true;
+}
+
+bool ValidateDDSDataSize(const game::GfxImage *image, const image::DDS_HEADER &header, size_t dataSize,
+ GPUTEXTUREFORMAT format, uint32_t mipCount, uint32_t faceCount)
+{
+ const size_t expectedSize = image::CalculateRequiredLinearDataSize(
+ header.dwWidth, header.dwHeight, format, 0u, mipCount, faceCount);
+ if (expectedSize == 0)
+ {
+ PrintImageError("image '%s' has unsupported DDS dimensions or format\n", image->name);
+ return false;
+ }
+
+ if (dataSize != expectedSize)
+ {
+ PrintImageError("image '%s' DDS data size does not match shape: have=%u expected=%u mips=%u faces=%u\n",
+ image->name, static_cast(dataSize), static_cast(expectedSize),
+ mipCount, faceCount);
+ return false;
+ }
+
+ return true;
+}
+
+bool ValidateCubeReplacementData(const game::GfxImage *image, size_t ddsDataSize, GPUTEXTUREFORMAT format,
+ uint32_t faceSize, uint32_t tiledBaseSize, size_t *requiredDDSSize)
+{
+ *requiredDDSSize = static_cast(faceSize) * 6u;
+ if (faceSize == 0 || *requiredDDSSize == 0)
+ return false;
+
+ if (ddsDataSize != *requiredDDSSize)
+ return false;
+
+ const int cardMemory = image->cardMemory.platform[0];
+ if (cardMemory > 0 && static_cast(tiledBaseSize) > static_cast(cardMemory))
+ return false;
+
+ return true;
+}
+
+bool ImageHasStreamedParts(const game::GfxImage *image)
+{
+ if (image->streaming)
+ return true;
+
+ for (uint32_t imagePartIndex = 0; imagePartIndex < 4u; ++imagePartIndex)
+ {
+ if ((image->streams[imagePartIndex].pixelSize & STREAM_PIXEL_SIZE_MASK) != 0)
+ return true;
+ }
+
+ return false;
+}
+
+GPUTEXTUREFORMAT GetImageGpuFormat(const game::GfxImage *image)
+{
+ return static_cast(image->texture.basemap.Format.DataFormat);
+}
+
+uint32_t GetImageBasePitch(const game::GfxImage *image, bool streamed)
+{
+ if (streamed)
+ return 0u;
+
+ return image->texture.basemap.Format.Pitch;
+}
+
+GPUENDIAN GetImageEndian(const game::GfxImage *image)
+{
+ return static_cast(image->texture.basemap.Format.Endian);
+}
+
+uint32_t GetImageLevelCount(const game::GfxImage *image, bool streamed)
+{
+ uint32_t levelCount = max(1u, static_cast(image->levelCount));
+
+ if (!streamed && image->texture.basemap.Format.PackedMips != 0)
+ {
+ const uint32_t mipTailBaseLevel = image::xenos_texture::GetMipTailBaseLevel(image->width, image->height);
+ levelCount = max(1u, min(levelCount, mipTailBaseLevel));
+ }
+
+ return levelCount;
+}
+
+std::string GetSanitizedImageName(const char *imageName)
+{
+ if (imageName == nullptr)
+ return std::string();
+
+ std::string sanitizedName;
+ for (const char *current = imageName; *current != '\0'; ++current)
+ {
+ const char c = *current;
+ if (c == '*')
+ continue;
+
+ if (c == '/' || c == '\\' || c == ':' || c == '?' || c == '"' || c == '<' || c == '>' || c == '|')
+ sanitizedName.push_back('_');
+ else
+ sanitizedName.push_back(c);
+ }
+
+ return sanitizedName;
+}
+
+std::string GetDumpZoneName(const char *zoneName)
+{
+ const std::string sanitizedZoneName = GetSanitizedImageName(zoneName);
+ return sanitizedZoneName.empty() ? "unknown" : sanitizedZoneName;
+}
+
+std::string GetImageDumpPath(const char *imageName, const char *zoneName)
+{
+ return std::string(DUMP_DIR) + "\\" + GetDumpZoneName(zoneName) + "\\images\\" + GetSanitizedImageName(imageName) +
+ ".dds";
+}
+
+void EnsureImageDumpDirectory(const char *zoneName)
+{
+ CreateDirectoryA(DUMP_DIR, nullptr);
+
+ const std::string zoneDirectory = std::string(DUMP_DIR) + "\\" + GetDumpZoneName(zoneName);
+ CreateDirectoryA(zoneDirectory.c_str(), nullptr);
+ CreateDirectoryA((zoneDirectory + "\\images").c_str(), nullptr);
+}
+
+const char *GetZoneName(uint32_t zoneIndex)
+{
+ if (zoneIndex >= game::g_zoneCount)
+ return nullptr;
+
+ const char *zoneName = game::g_zones[zoneIndex].file.name;
+ if (zoneName[0] == '\0')
+ return nullptr;
+
+ return zoneName;
+}
+
+bool ReadFileRange(const std::string &path, uint32_t offset, uint32_t size, std::vector *buffer)
+{
+ if (buffer == nullptr || size == 0)
+ return false;
+
+ HANDLE file = CreateFileA(path.c_str(), GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE, nullptr, OPEN_EXISTING,
+ FILE_ATTRIBUTE_NORMAL, nullptr);
+ if (file == INVALID_HANDLE_VALUE)
+ return false;
+
+ SetLastError(NO_ERROR);
+ const DWORD seekResult = SetFilePointer(file, offset, nullptr, FILE_BEGIN);
+ if (seekResult == INVALID_SET_FILE_POINTER && GetLastError() != NO_ERROR)
+ {
+ CloseHandle(file);
+ return false;
+ }
+
+ buffer->assign(size, 0);
+
+ DWORD bytesRead = 0;
+ const BOOL readOk = ReadFile(file, &(*buffer)[0], size, &bytesRead, nullptr);
+ CloseHandle(file);
+
+ if (!readOk || bytesRead != size)
+ {
+ buffer->clear();
+ return false;
+ }
+
+ return true;
+}
+
+bool GetImagePoolIndex(const game::GfxImage *image, uint32_t *imageIndex)
+{
+ if (image == nullptr || imageIndex == nullptr)
+ return false;
+
+ const uint32_t imageAddress = PtrToUint(image);
+ const uint32_t poolAddress = PtrToUint(game::g_gfxImagePool);
+ const uint32_t poolSize = sizeof(game::GfxImage) * game::g_gfxImagePoolSize;
+ if (imageAddress < poolAddress || imageAddress >= poolAddress + poolSize)
+ return false;
+
+ const uint32_t imageOffset = imageAddress - poolAddress;
+ if ((imageOffset % sizeof(game::GfxImage)) != 0)
+ return false;
+
+ *imageIndex = imageOffset / sizeof(game::GfxImage);
+ return true;
+}
+
+const game::GfxSubImageStream *GetImageStreamSources(const game::GfxImage *image)
+{
+ uint32_t imageIndex = 0;
+ if (!GetImagePoolIndex(image, &imageIndex))
+ return nullptr;
+
+ return game::g_imageStreams[imageIndex].part;
+}
+
+bool GetImageFilePath(const game::GfxSubImageStream &source, std::string *path)
+{
+ if (path == nullptr || source.file == nullptr || source.file->name[0] == '\0')
+ return false;
+
+ char filePath[MAX_PATH];
+ _snprintf(filePath, sizeof(filePath), "game:\\%s.pak", source.file->name);
+ filePath[sizeof(filePath) - 1] = '\0';
+ *path = filePath;
+ return true;
+}
+
+unsigned __int8 *__fastcall ImageZlibAlloc(unsigned __int8 *opaque, unsigned int items, unsigned int size)
+{
+ (void)opaque;
+
+ if (items == 0 || size == 0 || items > 0xFFFFFFFFu / size)
+ return nullptr;
+
+ return static_cast(malloc(items * size));
+}
+
+void __fastcall ImageZlibFree(unsigned __int8 *opaque, unsigned __int8 *ptr)
+{
+ (void)opaque;
+ free(ptr);
+}
+
+bool InflateImageStream(const std::vector &compressedData, uint32_t expectedSize,
+ std::vector *inflatedData)
+{
+ if (inflatedData == nullptr || compressedData.empty() || expectedSize == 0)
+ return false;
+
+ inflatedData->assign(expectedSize, 0);
+ ZlibStream stream;
+ memset(&stream, 0, sizeof(stream));
+
+ stream.next_in = const_cast(&compressedData[0]);
+ stream.avail_in = static_cast(compressedData.size());
+ stream.next_out = &(*inflatedData)[0];
+ stream.avail_out = expectedSize;
+ stream.zalloc = ImageZlibAlloc;
+ stream.zfree = ImageZlibFree;
+
+ int result = game::Zlib_InflateInit(&stream, "1.1.4", sizeof(stream));
+ if (result == 0)
+ {
+ const int inflateResult = game::Zlib_Inflate(&stream, 4);
+ if (inflateResult == 1)
+ {
+ result = game::Zlib_InflateEnd(&stream);
+ }
+ else
+ {
+ game::Zlib_InflateEnd(&stream);
+ result = inflateResult != 0 ? inflateResult : -5;
+ }
+ }
+
+ if (result != 0 || stream.total_out == 0 || stream.total_out > expectedSize)
+ {
+ inflatedData->clear();
+ return false;
+ }
+
+ inflatedData->resize(stream.total_out);
+ return true;
+}
+
+bool WriteUntiledLevel(std::ofstream &file, const game::GfxImage *image, uint32_t width, uint32_t height,
+ uint32_t mipLevel, GPUTEXTUREFORMAT format, uint32_t basePitch, const unsigned char *tiledPixels,
+ uint32_t tiledSize, GPUENDIAN endian)
+{
+ const uint32_t linearLevelSize = image::xenos_texture::CalculateLinearLevelSize(width, height, mipLevel, format);
+ const uint32_t rowPitch = image::xenos_texture::CalculateLinearRowPitch(width, mipLevel, format);
+ if (linearLevelSize == 0 || rowPitch == 0 || tiledSize == 0)
+ return false;
+
+ std::vector tiledData(tiledPixels, tiledPixels + tiledSize);
+ image::xenos_texture::ApplyGpuEndian(&tiledData[0], tiledData.size(), endian);
+
+ std::vector linearData(linearLevelSize);
+ if (!image::xenos_texture::UntileTextureLevel(width, height, mipLevel, format, basePitch, &linearData[0],
+ linearData.size(), rowPitch, &tiledData[0], tiledData.size()))
+ return false;
+
+ file.write(reinterpret_cast(&linearData[0]), linearData.size());
+ return true;
+}
+
+bool Dump2DImage(const game::GfxImage *image, bool streamed, const char *zoneName)
+{
+ if (image->pixels == nullptr || image->cardMemory.platform[0] <= 0)
+ return false;
+
+ const GPUTEXTUREFORMAT format = GetImageGpuFormat(image);
+ const uint32_t levelCount = GetImageLevelCount(image, streamed);
+ const uint32_t basePitch = GetImageBasePitch(image, streamed);
+ const GPUENDIAN endian = GetImageEndian(image);
+ const uint32_t linearBaseSize =
+ image::xenos_texture::CalculateLinearLevelSize(image->width, image->height, 0u, format);
+ const uint32_t tiledBaseSize =
+ image::xenos_texture::CalculateTiledLevelSize(image->width, image->height, 0u, format, basePitch);
+ if (linearBaseSize == 0 || tiledBaseSize == 0)
+ return false;
+
+ const size_t requiredBytes =
+ static_cast(tiledBaseSize) +
+ image::CalculateRequiredMipTextureBytes(image->width, image->height, format, 1u, levelCount, 1u);
+ if (requiredBytes > static_cast(image->cardMemory.platform[0]))
+ return false;
+
+ image::DDS_HEADER header;
+ const uint32_t caps =
+ image::DDSCAPS_TEXTURE | (levelCount > 1u ? image::DDSCAPS_COMPLEX | image::DDSCAPS_MIPMAP : 0u);
+ if (!image::CreateDdsHeader(header, image->width, image->height, image->depth, levelCount, linearBaseSize, caps, 0u,
+ format))
+ return false;
+
+ if (levelCount > 1u)
+ header.dwFlags |= image::DDSD_MIPMAPCOUNT;
+
+ EnsureImageDumpDirectory(zoneName);
+ const std::string filename = GetImageDumpPath(image->name, zoneName);
+ std::ofstream file(filename.c_str(), std::ios::binary);
+ if (!file)
+ {
+ PrintImageError("Could not create DDS for image '%s': %s\n", image->name, filename.c_str());
+ return false;
+ }
+
+ image::WriteDdsHeader(file, header);
+
+ const unsigned char *baseData = image->pixels;
+ const unsigned char *mipData = baseData + tiledBaseSize;
+ for (uint32_t mipLevel = 0; mipLevel < levelCount; ++mipLevel)
+ {
+ const uint32_t tiledLevelSize =
+ image::xenos_texture::CalculateTiledLevelSize(image->width, image->height, mipLevel, format, basePitch);
+ const unsigned char *source = baseData;
+ if (mipLevel > 0)
+ {
+ source = mipData +
+ image::xenos_texture::CalculateMipLevelOffset(image->width, image->height, mipLevel, format, 1u);
+ }
+
+ if (!WriteUntiledLevel(file, image, image->width, image->height, mipLevel, format, basePitch, source,
+ tiledLevelSize, endian))
+ return false;
+ }
+
+ PrintImageInfo("Dumped image '%s'%s\n", image->name, streamed ? " (streamed)" : "");
+ return true;
+}
+
+bool DumpCubeImage(const game::GfxImage *image, bool streamed, const char *zoneName)
+{
+ if (image->pixels == nullptr || image->cardMemory.platform[0] <= 0)
+ return false;
+
+ const GPUTEXTUREFORMAT format = GetImageGpuFormat(image);
+ const uint32_t basePitch = GetImageBasePitch(image, streamed);
+ const GPUENDIAN endian = GetImageEndian(image);
+ const uint32_t linearFaceSize =
+ image::xenos_texture::CalculateLinearLevelSize(image->width, image->height, 0u, format);
+ const uint32_t tiledFaceSize =
+ image::xenos_texture::CalculateTiledLevelSize(image->width, image->height, 0u, format, basePitch);
+ if (linearFaceSize == 0 || tiledFaceSize == 0)
+ return false;
+
+ const size_t requiredBytes = static_cast(tiledFaceSize) * 6u;
+ if (requiredBytes > static_cast(image->cardMemory.platform[0]))
+ return false;
+
+ const uint32_t caps2 = image::DDSCAPS2_CUBEMAP | image::DDSCAPS2_CUBEMAP_POSITIVEX |
+ image::DDSCAPS2_CUBEMAP_NEGATIVEX | image::DDSCAPS2_CUBEMAP_POSITIVEY |
+ image::DDSCAPS2_CUBEMAP_NEGATIVEY | image::DDSCAPS2_CUBEMAP_POSITIVEZ |
+ image::DDSCAPS2_CUBEMAP_NEGATIVEZ;
+
+ image::DDS_HEADER header;
+ if (!image::CreateDdsHeader(header, image->width, image->height, image->depth, 1u, linearFaceSize,
+ image::DDSCAPS_TEXTURE | image::DDSCAPS_COMPLEX, caps2, format))
+ return false;
+
+ EnsureImageDumpDirectory(zoneName);
+ const std::string filename = GetImageDumpPath(image->name, zoneName);
+ std::ofstream file(filename.c_str(), std::ios::binary);
+ if (!file)
+ {
+ PrintImageError("Could not create DDS for image '%s': %s\n", image->name, filename.c_str());
+ return false;
+ }
+
+ image::WriteDdsHeader(file, header);
+
+ for (uint32_t faceIndex = 0; faceIndex < 6u; ++faceIndex)
+ {
+ const unsigned char *source = image->pixels + static_cast(faceIndex) * tiledFaceSize;
+ if (!WriteUntiledLevel(file, image, image->width, image->height, 0u, format, basePitch, source, tiledFaceSize,
+ endian))
+ return false;
+ }
+
+ PrintImageInfo("Dumped image '%s'%s\n", image->name, streamed ? " (streamed)" : "");
+ return true;
+}
+
+uint32_t GetStreamPartLevelCount(const game::GfxImage *image, uint32_t imagePartIndex)
+{
+ const uint32_t streamLevelCount = image->streams[imagePartIndex].pixelSize >> 26;
+ if (streamLevelCount != 0)
+ return streamLevelCount;
+
+ return max(1u, static_cast(image->levelCount));
+}
+
+bool DumpStreamPartFromData(const game::GfxImage *image, uint32_t imagePartIndex, const char *zoneName,
+ const std::vector &pixelData)
+{
+ if (imagePartIndex >= 4u || pixelData.empty())
+ return false;
+
+ const game::GfxImageStreamData &streamData = image->streams[imagePartIndex];
+ if (streamData.width == 0 || streamData.height == 0)
+ return false;
+
+ game::GfxImage streamImage = *image;
+ streamImage.width = streamData.width;
+ streamImage.height = streamData.height;
+ streamImage.levelCount = static_cast(GetStreamPartLevelCount(image, imagePartIndex));
+ streamImage.cardMemory.platform[0] = static_cast(pixelData.size());
+ streamImage.pixels = const_cast(&pixelData[0]);
+
+ if (streamImage.mapType == game::MAPTYPE_2D)
+ return Dump2DImage(&streamImage, true, zoneName);
+
+ if (streamImage.mapType == game::MAPTYPE_CUBE)
+ return DumpCubeImage(&streamImage, true, zoneName);
+
+ return false;
+}
+
+bool TryReadStreamPartPixels(const game::GfxImage *image, uint32_t imagePartIndex, std::vector *pixelData)
+{
+ if (imagePartIndex >= 4u || pixelData == nullptr)
+ return false;
+
+ const game::GfxSubImageStream *sources = GetImageStreamSources(image);
+ if (sources == nullptr)
+ return false;
+
+ const game::GfxImageStreamData &streamData = image->streams[imagePartIndex];
+ const uint32_t expectedSize = streamData.pixelSize & STREAM_PIXEL_SIZE_MASK;
+ if (expectedSize == 0)
+ return false;
+
+ const game::GfxSubImageStream &source = sources[imagePartIndex];
+ if (source.fileOffsetEnd <= source.fileOffset)
+ return false;
+
+ const uint32_t compressedSize = source.fileOffsetEnd - source.fileOffset;
+ if (compressedSize > MAX_STREAM_COMPRESSED_SIZE)
+ return false;
+
+ std::string imageFilePath;
+ if (!GetImageFilePath(source, &imageFilePath))
+ return false;
+
+ std::vector compressedData;
+ if (!ReadFileRange(imageFilePath, source.fileOffset, compressedSize, &compressedData))
+ return false;
+
+ return InflateImageStream(compressedData, expectedSize, pixelData);
+}
+
+bool TryDumpStreamPartEager(const game::GfxImage *image, uint32_t imagePartIndex, const char *zoneName)
+{
+ if (imagePartIndex >= 4u)
+ return false;
+
+ const game::GfxImageStreamData &streamData = image->streams[imagePartIndex];
+ if ((streamData.pixelSize & STREAM_PIXEL_SIZE_MASK) == 0)
+ return false;
+
+ std::vector pixelData;
+ if (!TryReadStreamPartPixels(image, imagePartIndex, &pixelData))
+ return false;
+
+ return DumpStreamPartFromData(image, imagePartIndex, zoneName, pixelData);
+}
+
+bool TryDumpStreamedImageEager(const game::GfxImage *image, const char *zoneName)
+{
+ bool usedParts[4] = {false, false, false, false};
+
+ for (uint32_t attempt = 0; attempt < 4u; ++attempt)
+ {
+ uint32_t bestPart = 4u;
+ uint32_t bestArea = 0;
+
+ for (uint32_t imagePartIndex = 0; imagePartIndex < 4u; ++imagePartIndex)
+ {
+ if (usedParts[imagePartIndex])
+ continue;
+
+ const game::GfxImageStreamData &streamData = image->streams[imagePartIndex];
+ if ((streamData.pixelSize & STREAM_PIXEL_SIZE_MASK) == 0)
+ continue;
+
+ const uint32_t imageArea =
+ static_cast(streamData.width) * static_cast(streamData.height);
+ if (bestPart == 4u || imageArea > bestArea)
+ {
+ bestPart = imagePartIndex;
+ bestArea = imageArea;
+ }
+ }
+
+ if (bestPart == 4u)
+ break;
+
+ usedParts[bestPart] = true;
+ if (TryDumpStreamPartEager(image, bestPart, zoneName))
+ return true;
+ }
+
+ return false;
+}
+
+bool Image_Dump(game::GfxImage *image, const char *zoneName)
+{
+ if (ImageHasStreamedParts(image))
+ return TryDumpStreamedImageEager(image, zoneName);
+
+ if (image->mapType == game::MAPTYPE_2D)
+ return Dump2DImage(image, false, zoneName);
+
+ if (image->mapType == game::MAPTYPE_CUBE)
+ return DumpCubeImage(image, false, zoneName);
+
+ return false;
+}
+
+void RegisterDvars()
+{
+ dump_assets = game::Dvar_RegisterBool("dump_assets", Config::dump_assets, 0, "Dump assets as they are loaded.");
+}
+
+bool Image_Replace_2D(game::GfxImage *image, const DDSImage &ddsImage, DdsDataFile &ddsFile, uint32_t ddsDataSize)
+{
+ if (image->mapType != game::MAPTYPE_2D)
+ {
+ PrintImageError("image '%s' is not a 2D map\n", image->name);
+ return false;
+ }
+
+ const D3DBaseTexture *texture = &image->texture.basemap;
+ const GPUTEXTUREFORMAT format = static_cast(texture->Format.DataFormat);
+ const uint32_t levelCount = image::xenos_texture::GetTextureLevelCount(texture);
+ const uint32_t mipTailBaseLevel = texture->Format.PackedMips != 0
+ ? image::xenos_texture::GetMipTailBaseLevel(image->width, image->height)
+ : levelCount;
+ if (!ValidateResidentMipCount(image, ddsImage, levelCount))
+ return false;
+ if (!ValidateDDSDataSize(image, ddsImage.header, ddsDataSize, format, levelCount, 1u))
+ return false;
+
+ const uint32_t nonPackedLevelCount = max(1u, min(levelCount, mipTailBaseLevel));
+ unsigned char *baseData = image::xenos_texture::GetTextureBase(texture, image->pixels);
+ unsigned char *mipData =
+ image::xenos_texture::GetTextureMipBase(texture, baseData, image->width, image->height, format, 1u);
+
+ size_t requiredDDSSize = 0;
+ size_t requiredTextureBytes = 0;
+ if (!Validate2DReplacementData(image, format, ddsDataSize, nonPackedLevelCount, &requiredDDSSize,
+ &requiredTextureBytes))
+ {
+ if (requiredDDSSize == 0)
+ {
+ PrintImageError("image '%s' has unsupported replacement format %u\n", image->name,
+ static_cast(format));
+ }
+ else if (ddsDataSize < requiredDDSSize)
+ {
+ PrintImageError("image '%s' DDS data is too small: have=%u need=%u for %u mip levels\n", image->name,
+ static_cast(ddsDataSize), static_cast(requiredDDSSize),
+ nonPackedLevelCount);
+ }
+ else
+ {
+ PrintImageError("image '%s' replacement exceeds texture memory: have=%u need=%u\n", image->name,
+ static_cast(image->cardMemory.platform[0]),
+ static_cast(requiredTextureBytes));
+ }
+
+ return false;
+ }
+
+ if (baseData == nullptr || mipData == nullptr)
+ {
+ PrintImageError("image '%s' has no valid texture memory\n", image->name);
+ return false;
+ }
+
+ uint32_t ddsOffset = 0;
+
+ for (uint32_t mipLevel = 0; mipLevel < nonPackedLevelCount; ++mipLevel)
+ {
+ const uint32_t rowPitch = image::xenos_texture::CalculateLinearRowPitch(image->width, mipLevel, format);
+ const uint32_t ddsMipLevelSize =
+ image::xenos_texture::CalculateLinearLevelSize(image->width, image->height, mipLevel, format);
+ const uint32_t tiledMipLevelSize = image::xenos_texture::CalculateTiledLevelSize(
+ image->width, image->height, mipLevel, format, texture->Format.Pitch);
+
+ if (ddsMipLevelSize == 0 || tiledMipLevelSize == 0 || rowPitch == 0)
+ {
+ PrintImageError("unsupported format %u for image '%s' mip level %u\n", texture->Format.DataFormat,
+ image->name, mipLevel);
+ return false;
+ }
+
+ if (static_cast(ddsOffset) + ddsMipLevelSize > ddsDataSize)
+ {
+ PrintImageError("image '%s' mip level %u exceeds DDS data size\n", image->name, mipLevel);
+ return false;
+ }
+
+ unsigned char *destination = baseData;
+ if (mipLevel > 0)
+ {
+ destination = mipData + image::xenos_texture::CalculateMipLevelOffset(image->width, image->height, mipLevel,
+ format, 1u);
+ }
+
+ if (!TileDdsLevelToTexture(ddsFile, ddsOffset, ddsMipLevelSize, image->width, image->height, mipLevel,
+ image->width, image->height, mipLevel, format, texture->Format.Pitch, destination,
+ tiledMipLevelSize, static_cast(texture->Format.Endian)))
+ {
+ PrintImageError("failed to tile image '%s' mip level %u\n", image->name, mipLevel);
+ return false;
+ }
+
+ ddsOffset += ddsMipLevelSize;
+ }
+
+ return true;
+}
+
+bool Image_Replace_Cube(game::GfxImage *image, const DDSImage &ddsImage, DdsDataFile &ddsFile, uint32_t ddsDataSize)
+{
+ if (image->mapType != game::MAPTYPE_CUBE)
+ {
+ PrintImageError("image '%s' is not a cube map\n", image->name);
+ return false;
+ }
+
+ const D3DBaseTexture *texture = &image->texture.basemap;
+ const GPUTEXTUREFORMAT format = static_cast(texture->Format.DataFormat);
+ const uint32_t levelCount = image::xenos_texture::GetTextureLevelCount(texture);
+ const uint32_t faceSize = image::xenos_texture::CalculateLinearLevelSize(image->width, image->height, 0u, format);
+ const uint32_t rowPitch = image::xenos_texture::CalculateLinearRowPitch(image->width, 0u, format);
+ const uint32_t tiledFaceSize =
+ image::xenos_texture::CalculateTiledLevelSize(image->width, image->height, 0u, format, texture->Format.Pitch);
+ const uint32_t tiledBaseSize = image::xenos_texture::CalculateBaseSize(texture, image->width, image->height, 6u);
+ unsigned char *baseData = image::xenos_texture::GetTextureBase(texture, image->pixels);
+
+ if (faceSize == 0 || rowPitch == 0 || tiledFaceSize == 0 || tiledBaseSize < tiledFaceSize * 6u)
+ {
+ PrintImageError("image '%s' has unsupported cube format %u\n", image->name, static_cast(format));
+ return false;
+ }
+
+ if (baseData == nullptr)
+ {
+ PrintImageError("image '%s' has no valid cube texture memory\n", image->name);
+ return false;
+ }
+
+ if (levelCount != 1u || ddsImage.GetMipCount() != 1u)
+ {
+ PrintImageError("image '%s' cube replacement must be base-level only: imageMips=%u ddsMips=%u\n", image->name,
+ levelCount, ddsImage.GetMipCount());
+ return false;
+ }
+
+ if (!ValidateDDSDataSize(image, ddsImage.header, ddsDataSize, format, 1u, 6u))
+ return false;
+
+ size_t requiredDDSSize = 0;
+ if (!ValidateCubeReplacementData(image, ddsDataSize, format, faceSize, tiledBaseSize, &requiredDDSSize))
+ {
+ if (ddsDataSize < requiredDDSSize)
+ {
+ PrintImageError("image '%s' DDS is too small for 6 cube faces: have=%u need=%u\n", image->name,
+ static_cast(ddsDataSize), static_cast(requiredDDSSize));
+ }
+ else
+ {
+ PrintImageError("image '%s' cube replacement exceeds texture memory: have=%u need=%u\n", image->name,
+ static_cast(image->cardMemory.platform[0]), tiledBaseSize);
+ }
+
+ return false;
+ }
+
+ for (uint32_t faceIndex = 0; faceIndex < 6u; ++faceIndex)
+ {
+ const uint32_t faceOffset = faceIndex * faceSize;
+ unsigned char *faceDestination = baseData + (faceIndex * tiledFaceSize);
+
+ if (!TileDdsLevelToTexture(ddsFile, faceOffset, faceSize, image->width, image->height, 0u, image->width,
+ image->height, 0u, format, texture->Format.Pitch, faceDestination, tiledFaceSize,
+ static_cast(texture->Format.Endian)))
+ {
+ PrintImageError("failed to tile cube image '%s' face %u\n", image->name, faceIndex);
+ return false;
+ }
+ }
+
+ return true;
+}
+
+bool ValidateReplacementShape(const game::GfxImage *image, const DDSImage &ddsImage, uint32_t ddsDataSize)
+{
+ const bool ddsIsCubemap = ddsImage.IsCubemap();
+
+ if (image->mapType == game::MAPTYPE_2D && ddsIsCubemap)
+ {
+ PrintImageError("image '%s' is 2D but replacement DDS is a cubemap\n", image->name);
+ return false;
+ }
+
+ if (image->mapType == game::MAPTYPE_CUBE && !ddsIsCubemap)
+ {
+ GPUTEXTUREFORMAT ddsFormat;
+ if (!ddsImage.GetGpuFormat(&ddsFormat))
+ return false;
+
+ const uint32_t faceSize = image::xenos_texture::CalculateLinearLevelSize(
+ ddsImage.header.dwWidth, ddsImage.header.dwHeight, 0u, ddsFormat);
+ if (faceSize == 0 || ddsDataSize < static_cast(faceSize) * 6u)
+ {
+ PrintImageError("image '%s' is a cubemap but replacement DDS is not a valid 6-face cubemap\n", image->name);
+ return false;
+ }
+ }
+
+ return true;
+}
+
+void Image_Replace(game::GfxImage *image)
+{
+ if (image == nullptr || image->name == nullptr)
+ return;
+
+ const std::string replacementPath = GetReplacementPath(image->name);
+ if (!ImageFileExists(replacementPath))
+ return;
+
+ if (ImageHasStreamedParts(image))
+ return;
+
+ if (image->pixels == nullptr || image->cardMemory.platform[0] <= 0)
+ {
+ PrintImageError("image '%s' replacement exists but resident texture memory is not available\n", image->name);
+ return;
+ }
+
+ DDSImage ddsImage;
+ uint32_t ddsDataSize = 0;
+ if (!ReadDDSHeader(replacementPath, &ddsImage, &ddsDataSize))
+ {
+ PrintImageError("failed to load DDS header for image '%s': %s\n", image->name, replacementPath.c_str());
+ return;
+ }
+
+ GPUTEXTUREFORMAT ddsFormat;
+ if (!ValidateDDSHeaderFields(image, ddsImage, &ddsFormat))
+ return;
+
+ if (image->width != ddsImage.header.dwWidth || image->height != ddsImage.header.dwHeight)
+ {
+ PrintImageError("image '%s' dimensions do not match DDS: image=%ux%u dds=%ux%u\n", image->name, image->width,
+ image->height, ddsImage.header.dwWidth, ddsImage.header.dwHeight);
+ return;
+ }
+
+ if (!ValidateReplacementShape(image, ddsImage, ddsDataSize))
+ return;
+
+ DdsDataFile ddsFile(replacementPath, ddsDataSize);
+ if (!ddsFile.IsValid())
+ {
+ PrintImageError("failed to open DDS data for image '%s': %s error=0x%08X\n", image->name,
+ replacementPath.c_str(), GetLastError());
+ return;
+ }
+
+ bool replaced = false;
+ if (image->mapType == game::MAPTYPE_2D)
+ replaced = Image_Replace_2D(image, ddsImage, ddsFile, ddsDataSize);
+ else if (image->mapType == game::MAPTYPE_CUBE)
+ replaced = Image_Replace_Cube(image, ddsImage, ddsFile, ddsDataSize);
+ else
+ PrintImageError("image '%s' is not a 2D or cube map\n", image->name);
+
+ if (replaced)
+ PrintImageInfo("replaced image '%s' (resident)\n", image->name);
+}
+
+bool FindMipLevelForDimensions(const DDSImage &ddsImage, uint32_t width, uint32_t height, uint32_t *mipLevel)
+{
+ const uint32_t ddsMipCount = ddsImage.GetMipCount();
+
+ for (uint32_t currentMip = 0; currentMip < ddsMipCount; ++currentMip)
+ {
+ if (image::GetMipDimension(ddsImage.header.dwWidth, currentMip) == width &&
+ image::GetMipDimension(ddsImage.header.dwHeight, currentMip) == height)
+ {
+ *mipLevel = currentMip;
+ return true;
+ }
+ }
+
+ return false;
+}
+
+bool ValidateStreamReplacementData(const game::GfxImage *image, const DDSImage &ddsImage, size_t ddsDataSize,
+ GPUTEXTUREFORMAT format, uint32_t startMipLevel, uint32_t levelCount,
+ uint32_t basePitch, size_t *requiredDDSSize, size_t *requiredTextureBytes)
+{
+ *requiredDDSSize = image::CalculateRequiredLinearDataSize(ddsImage.header.dwWidth, ddsImage.header.dwHeight, format,
+ startMipLevel, levelCount, 1u);
+ if (*requiredDDSSize == 0)
+ return false;
+
+ const uint32_t ddsOffset =
+ image::CalculateDdsMipOffset(ddsImage.header.dwWidth, ddsImage.header.dwHeight, format, startMipLevel);
+ if (static_cast(ddsOffset) + *requiredDDSSize > ddsDataSize)
+ return false;
+
+ *requiredTextureBytes = 0;
+ for (uint32_t localMipLevel = 0; localMipLevel < levelCount; ++localMipLevel)
+ {
+ const uint32_t levelSize = image::xenos_texture::CalculateTiledLevelSize(image->width, image->height,
+ localMipLevel, format, basePitch);
+ if (levelSize == 0)
+ return false;
+
+ *requiredTextureBytes += levelSize;
+ }
+
+ const int cardMemory = image->cardMemory.platform[0];
+ if (cardMemory <= 0 || *requiredTextureBytes > static_cast(cardMemory))
+ return false;
+
+ return true;
+}
+
+bool Image_Replace_StreamCubePart(game::GfxImage *image, DdsDataFile &ddsFile, const DDSImage &ddsImage,
+ size_t ddsDataSize, GPUTEXTUREFORMAT ddsFormat, uint32_t imagePartIndex)
+{
+ if (imagePartIndex != 0u)
+ {
+ PrintImageError("streamed cube image '%s' has unsupported part %u\n", image->name, imagePartIndex);
+ return false;
+ }
+
+ if (image->width != ddsImage.header.dwWidth || image->height != ddsImage.header.dwHeight)
+ {
+ PrintImageError("streamed cube image '%s' dimensions do not match DDS: image=%ux%u dds=%ux%u\n", image->name,
+ image->width, image->height, ddsImage.header.dwWidth, ddsImage.header.dwHeight);
+ return false;
+ }
+
+ const uint32_t ddsMipCount = ddsImage.GetMipCount();
+ if (image->levelCount != 1u || ddsMipCount != 1u)
+ {
+ PrintImageError("streamed cube image '%s' must be base-level only: imageMips=%u ddsMips=%u\n", image->name,
+ static_cast(image->levelCount), ddsMipCount);
+ return false;
+ }
+
+ if (!ValidateDDSDataSize(image, ddsImage.header, ddsDataSize, ddsFormat, 1u, 6u))
+ return false;
+
+ const uint32_t rowPitch = image::xenos_texture::CalculateLinearRowPitch(image->width, 0u, ddsFormat);
+ const uint32_t faceSize =
+ image::xenos_texture::CalculateLinearLevelSize(image->width, image->height, 0u, ddsFormat);
+ const uint32_t tiledFaceSize =
+ image::xenos_texture::CalculateTiledLevelSize(image->width, image->height, 0u, ddsFormat, 0u);
+ const size_t requiredTextureBytes = static_cast(tiledFaceSize) * 6u;
+
+ if (rowPitch == 0 || faceSize == 0 || tiledFaceSize == 0)
+ {
+ PrintImageError("streamed cube image '%s' has unsupported format %u\n", image->name,
+ static_cast(ddsFormat));
+ return false;
+ }
+
+ if (image->cardMemory.platform[0] <= 0 || requiredTextureBytes > static_cast(image->cardMemory.platform[0]))
+ {
+ PrintImageError("streamed cube image '%s' replacement exceeds texture memory: have=%u need=%u\n", image->name,
+ static_cast(image->cardMemory.platform[0]),
+ static_cast(requiredTextureBytes));
+ return false;
+ }
+
+ for (uint32_t faceIndex = 0; faceIndex < 6u; ++faceIndex)
+ {
+ const uint32_t faceOffset = faceIndex * faceSize;
+ unsigned char *faceDestination = image->pixels + static_cast(faceIndex) * tiledFaceSize;
+
+ if (!TileDdsLevelToTexture(ddsFile, faceOffset, faceSize, image->width, image->height, 0u, image->width,
+ image->height, 0u, ddsFormat, 0u, faceDestination, tiledFaceSize,
+ static_cast(image->texture.basemap.Format.Endian)))
+ {
+ PrintImageError("failed to tile streamed cube image '%s' face %u\n", image->name, faceIndex);
+ return false;
+ }
+ }
+
+ return true;
+}
+
+bool Image_Replace_StreamPart(game::GfxImage *image, DdsDataFile &ddsFile, const DDSImage &ddsImage,
+ size_t ddsDataSize, uint32_t imagePartIndex)
+{
+ if (image == nullptr || image->name == nullptr || imagePartIndex >= 4u)
+ return false;
+
+ if (image->pixels == nullptr)
+ {
+ PrintImageError("streamed image '%s' part %u replacement exists but pixel memory is not available\n",
+ image->name, imagePartIndex);
+ return false;
+ }
+
+ GPUTEXTUREFORMAT ddsFormat;
+ if (!ValidateDDSHeaderFields(image, ddsImage, &ddsFormat))
+ return false;
+
+ if (image->mapType == game::MAPTYPE_CUBE)
+ return Image_Replace_StreamCubePart(image, ddsFile, ddsImage, ddsDataSize, ddsFormat, imagePartIndex);
+
+ if (image->mapType != game::MAPTYPE_2D)
+ {
+ PrintImageError("streamed image '%s' is not a supported 2D or cube map\n", image->name);
+ return false;
+ }
+
+ if (ddsImage.IsCubemap())
+ {
+ PrintImageError("streamed image '%s' is 2D but replacement DDS is a cubemap\n", image->name);
+ return false;
+ }
+
+ const uint32_t ddsMipCount = ddsImage.GetMipCount();
+ if (!ValidateDDSDataSize(image, ddsImage.header, ddsDataSize, ddsFormat, ddsMipCount, 1u))
+ return false;
+
+ uint32_t startMipLevel = 0;
+ if (!FindMipLevelForDimensions(ddsImage, image->width, image->height, &startMipLevel))
+ {
+ PrintImageError("streamed image '%s' part %u dimensions do not exist in DDS: part=%ux%u dds=%ux%u\n",
+ image->name, imagePartIndex, image->width, image->height, ddsImage.header.dwWidth,
+ ddsImage.header.dwHeight);
+ return false;
+ }
+
+ const game::GfxImageStreamData &streamData = image->streams[imagePartIndex];
+ uint32_t levelCount = streamData.pixelSize >> 26;
+ if (levelCount == 0)
+ levelCount = max(1u, static_cast(image->levelCount));
+
+ const D3DBaseTexture *texture = &image->texture.basemap;
+ const uint32_t textureLevelCount = image::xenos_texture::GetTextureLevelCount(texture);
+ levelCount = max(1u, min(levelCount, textureLevelCount));
+
+ const uint32_t mipTailBaseLevel = texture->Format.PackedMips != 0
+ ? image::xenos_texture::GetMipTailBaseLevel(image->width, image->height)
+ : levelCount;
+ const uint32_t replaceLevelCount = max(1u, min(levelCount, mipTailBaseLevel));
+
+ if (startMipLevel + replaceLevelCount > ddsMipCount)
+ {
+ PrintImageError("streamed image '%s' part %u needs %u DDS mips from mip %u, but DDS has %u\n", image->name,
+ imagePartIndex, replaceLevelCount, startMipLevel, ddsMipCount);
+ return false;
+ }
+
+ size_t requiredDDSSize = 0;
+ size_t requiredTextureBytes = 0;
+ const uint32_t streamBasePitch = 0u;
+ if (!ValidateStreamReplacementData(image, ddsImage, ddsDataSize, ddsFormat, startMipLevel, replaceLevelCount,
+ streamBasePitch, &requiredDDSSize, &requiredTextureBytes))
+ {
+ PrintImageError("streamed image '%s' part %u replacement size is invalid: ddsNeed=%u textureNeed=%u card=%u\n",
+ image->name, imagePartIndex, static_cast(requiredDDSSize),
+ static_cast(requiredTextureBytes),
+ static_cast(image->cardMemory.platform[0]));
+ return false;
+ }
+
+ uint32_t ddsOffset =
+ image::CalculateDdsMipOffset(ddsImage.header.dwWidth, ddsImage.header.dwHeight, ddsFormat, startMipLevel);
+ size_t destinationOffset = 0;
+
+ for (uint32_t localMipLevel = 0; localMipLevel < replaceLevelCount; ++localMipLevel)
+ {
+ const uint32_t globalMipLevel = startMipLevel + localMipLevel;
+ const uint32_t rowPitch =
+ image::xenos_texture::CalculateLinearRowPitch(ddsImage.header.dwWidth, globalMipLevel, ddsFormat);
+ const uint32_t ddsMipLevelSize = image::xenos_texture::CalculateLinearLevelSize(
+ ddsImage.header.dwWidth, ddsImage.header.dwHeight, globalMipLevel, ddsFormat);
+ const uint32_t tiledMipLevelSize = image::xenos_texture::CalculateTiledLevelSize(
+ image->width, image->height, localMipLevel, ddsFormat, streamBasePitch);
+
+ if (rowPitch == 0 || ddsMipLevelSize == 0 || tiledMipLevelSize == 0)
+ {
+ PrintImageError("streamed image '%s' part %u has unsupported format %u at mip %u\n", image->name,
+ imagePartIndex, static_cast(ddsFormat), localMipLevel);
+ return false;
+ }
+
+ if (static_cast(ddsOffset) + ddsMipLevelSize > ddsDataSize ||
+ destinationOffset + tiledMipLevelSize > static_cast(image->cardMemory.platform[0]))
+ {
+ PrintImageError("streamed image '%s' part %u mip %u exceeds source or destination bounds\n", image->name,
+ imagePartIndex, localMipLevel);
+ return false;
+ }
+
+ unsigned char *destination = image->pixels + destinationOffset;
+
+ if (!TileDdsLevelToTexture(ddsFile, ddsOffset, ddsMipLevelSize, ddsImage.header.dwWidth,
+ ddsImage.header.dwHeight, globalMipLevel, image->width, image->height,
+ localMipLevel, ddsFormat, streamBasePitch, destination, tiledMipLevelSize,
+ static_cast(image->texture.basemap.Format.Endian)))
+ {
+ PrintImageError("failed to tile streamed image '%s' part %u mip %u\n", image->name, imagePartIndex,
+ localMipLevel);
+ return false;
+ }
+
+ ddsOffset += ddsMipLevelSize;
+ destinationOffset += tiledMipLevelSize;
+ }
+
+ return true;
+}
+
+void TryReplaceStreamPart(game::GfxImage *image, uint32_t imagePartIndex)
+{
+ if (image == nullptr || image->name == nullptr)
+ return;
+
+ const std::string replacementPath = GetReplacementPath(image->name);
+ const bool replacementExists = ImageFileExists(replacementPath);
+
+ if (imagePartIndex >= 4u)
+ return;
+
+ if (!replacementExists)
+ return;
+
+ DDSImage ddsImage;
+ uint32_t ddsDataSize = 0;
+ if (!ReadDDSHeader(replacementPath, &ddsImage, &ddsDataSize))
+ {
+ PrintImageError("failed to load DDS header for image '%s': %s\n", image->name, replacementPath.c_str());
+ return;
+ }
+
+ DdsDataFile ddsFile(replacementPath, ddsDataSize);
+ if (!ddsFile.IsValid())
+ {
+ PrintImageError("failed to open DDS data for streamed image '%s': %s error=0x%08X\n", image->name,
+ replacementPath.c_str(), GetLastError());
+ return;
+ }
+
+ if (Image_Replace_StreamPart(image, ddsFile, ddsImage, ddsDataSize, imagePartIndex))
+ PrintImageInfo("replaced image '%s' (streamed part %u)\n", image->name, imagePartIndex);
+}
+
+void OnDBLinkXAssetPre(game::XAssetType &type, game::XAssetHeader *header)
+{
+ if (type == game::ASSET_TYPE_IMAGE && header != nullptr)
+ Image_Replace(header->image);
+}
+
+void OnDBLinkXAssetPost(game::XAssetEntryPoolEntry *poolEntry)
+{
+ if (dump_assets == nullptr || !dump_assets->current.enabled || poolEntry == nullptr)
+ return;
+
+ const game::XAssetEntry &entry = poolEntry->entry;
+ if (entry.asset.type != game::ASSET_TYPE_IMAGE)
+ return;
+
+ game::GfxImage *image = entry.asset.header.image;
+ Image_Dump(image, GetZoneName(entry.zoneIndex));
+}
+
+Detour ImageCache_InitImage_Detour;
+
+void ImageCache_InitImage_Hook(game::GfxImage *image, game::GfxImage *remoteImage, unsigned __int8 *pixels,
+ unsigned int imagePartIndex)
+{
+ ImageCache_InitImage_Detour.GetOriginal()(image, remoteImage, pixels, imagePartIndex);
+ TryReplaceStreamPart(image, imagePartIndex);
+}
+} // namespace
+
+image_loader::image_loader()
+{
+ Events::OnDvarInit(RegisterDvars);
+ Events::OnDBLinkXAssetPre(OnDBLinkXAssetPre);
+ Events::OnDBLinkXAssetPost(OnDBLinkXAssetPost);
+
+ ImageCache_InitImage_Detour = Detour(iw4::mp_tu6::ImageCache_InitImage, ImageCache_InitImage_Hook);
+ ImageCache_InitImage_Detour.Install();
+}
+
+image_loader::~image_loader()
+{
+ ImageCache_InitImage_Detour.Remove();
+ dump_assets = nullptr;
+}
diff --git a/src/game/iw4/mp_tu6/components/image_loader.h b/src/game/iw4/mp_tu6/components/image_loader.h
new file mode 100644
index 00000000..b78ef026
--- /dev/null
+++ b/src/game/iw4/mp_tu6/components/image_loader.h
@@ -0,0 +1,15 @@
+#pragma once
+
+#include "pch.h"
+
+class image_loader : public Module
+{
+ public:
+ image_loader();
+ ~image_loader();
+
+ const char *get_name() override
+ {
+ return "image_loader";
+ };
+};
diff --git a/src/game/iw4/mp_tu6/components/mpsp.cpp b/src/game/iw4/mp_tu6/components/mpsp.cpp
index bca57864..b507bd43 100644
--- a/src/game/iw4/mp_tu6/components/mpsp.cpp
+++ b/src/game/iw4/mp_tu6/components/mpsp.cpp
@@ -2,6 +2,7 @@
#include "pch.h"
#include "mpsp.h"
+#include "events.h"
#include "unordered_map"
namespace iw4
@@ -30,37 +31,6 @@ bool is_mp_fastfile(const char *name)
struct internal_state;
-struct Sys_File
-{
- void *handle;
- int startOffset;
-};
-
-struct DBFile
-{
- Sys_File handle;
- char name[64];
-};
-
-struct XBlock
-{
- unsigned __int8 *data;
- unsigned int size;
-};
-
-struct XZoneMemory
-{
- XBlock blocks[6];
-};
-
-struct XZone
-{
- DBFile file;
- int flags;
- int allocType;
- XZoneMemory mem;
-};
-
struct _OVERLAPPED
{
unsigned int Internal;
@@ -112,7 +82,6 @@ const char **g_assetNames = reinterpret_cast(0x82442298);
int *g_poolSize = reinterpret_cast(0x82442588);
const DB_LoadData *g_load = reinterpret_cast(0x82678600);
const unsigned int *g_zoneIndex = reinterpret_cast(0x827ADAE4);
-const XZone *g_zones = reinterpret_cast(0x829D8048);
GameWorldMp *gameWorldMp = reinterpret_cast(0x82DFD010);
typedef int (*Com_sprintf_t)(char *dest, unsigned int size, const char *fmt, ...);
@@ -263,6 +232,11 @@ void dump(MapEnts *asset)
{
std::string buffer;
+ if (!Config::dump_assets)
+ {
+ return;
+ }
+
if (!asset || !asset->name || asset->name[0] == '\0')
{
return;
@@ -384,8 +358,7 @@ void override_(RawFile *asset)
} // namespace Asset
-Detour DB_LinkXAssetEntry1_Detour;
-XAssetEntryPoolEntry *DB_LinkXAssetEntry1_Hook(XAssetType type, XAssetHeader *header)
+void OnDBLinkXAssetPre(XAssetType &type, XAssetHeader *header)
{
XAsset xasset;
xasset.type = type;
@@ -453,10 +426,6 @@ XAssetEntryPoolEntry *DB_LinkXAssetEntry1_Hook(XAssetType type, XAssetHeader *he
DB_SetXAssetName(&xasset, ",CGAME_UNKNOWN");
}
}
-
- XAssetEntryPoolEntry *entry = DB_LinkXAssetEntry1_Detour.GetOriginal()(type, header);
-
- return entry;
}
void DB_ReallocXAssetPool(XAssetType type, unsigned int newSize)
@@ -535,8 +504,7 @@ mpsp::mpsp()
#endif
// Modify some assets before linking
- DB_LinkXAssetEntry1_Detour = Detour(DB_LinkXAssetEntry1, DB_LinkXAssetEntry1_Hook);
- DB_LinkXAssetEntry1_Detour.Install();
+ ::Events::OnDBLinkXAssetPre(OnDBLinkXAssetPre);
// Rewrite some strings on the fly
Com_sprintf_Detour = Detour(Com_sprintf, Com_sprintf_Hook);
@@ -550,8 +518,6 @@ mpsp::~mpsp()
CL_ConsolePrint_Detour.Remove();
#endif
- DB_LinkXAssetEntry1_Detour.Remove();
-
Com_sprintf_Detour.Remove();
}
diff --git a/src/game/iw4/mp_tu6/components/scr_parser.cpp b/src/game/iw4/mp_tu6/components/scr_parser.cpp
index 3e8f9710..6b7931b6 100644
--- a/src/game/iw4/mp_tu6/components/scr_parser.cpp
+++ b/src/game/iw4/mp_tu6/components/scr_parser.cpp
@@ -12,7 +12,7 @@ char *Scr_AddSourceBuffer_Hook(const char *filename, const char *extFilename)
auto callOriginal = [&]()
{ return Scr_AddSourceBuffer_Detour.GetOriginal()(filename, extFilename); };
- if (Config::dump_rawfile)
+ if (Config::dump_assets)
{
auto contents = callOriginal();
// Dump the script to a file
diff --git a/src/game/iw4/mp_tu6/main.cpp b/src/game/iw4/mp_tu6/main.cpp
index 5cca6963..d33a4ca9 100644
--- a/src/game/iw4/mp_tu6/main.cpp
+++ b/src/game/iw4/mp_tu6/main.cpp
@@ -6,6 +6,7 @@
#include "components/events.h"
#include "components/g_client_fields.h"
#include "components/g_scr_main.h"
+#include "components/image_loader.h"
#include "components/mpsp.h"
#include "components/mr.h"
#include "components/patches.h"
@@ -30,6 +31,7 @@ IW4_MP_TU6_Plugin::IW4_MP_TU6_Plugin()
RegisterModule(new console());
RegisterModule(new g_client_fields());
RegisterModule(new g_scr_main());
+ RegisterModule(new image_loader());
RegisterModule(new mpsp());
RegisterModule(new MovementRecorder());
RegisterModule(new patches());
diff --git a/src/game/iw4/mp_tu6/structs.h b/src/game/iw4/mp_tu6/structs.h
index 244b66c8..f3a83e37 100644
--- a/src/game/iw4/mp_tu6/structs.h
+++ b/src/game/iw4/mp_tu6/structs.h
@@ -1807,6 +1807,99 @@ enum XAssetType : __int32
ASSET_TYPE_ASSETLIST = 0x2A,
};
+enum MapType : unsigned __int8
+{
+ MAPTYPE_NONE = 0x0,
+ MAPTYPE_INVALID1 = 0x1,
+ MAPTYPE_1D = 0x2,
+ MAPTYPE_2D = 0x3,
+ MAPTYPE_3D = 0x4,
+ MAPTYPE_CUBE = 0x5,
+ MAPTYPE_COUNT = 0x6,
+};
+
+struct Picmip
+{
+ unsigned __int8 platform[2];
+};
+
+struct CardMemory
+{
+ int platform[1];
+};
+
+struct GfxTexture
+{
+ D3DBaseTexture basemap;
+};
+static_assert(sizeof(GfxTexture) == 0x34, "");
+
+struct GfxImageStreamData
+{
+ unsigned __int16 width;
+ unsigned __int16 height;
+ unsigned int pixelSize;
+};
+static_assert(sizeof(GfxImageStreamData) == 0x8, "");
+
+struct GfxImage
+{
+ GfxTexture texture;
+ unsigned __int8 semantic;
+ unsigned __int8 category;
+ bool cached;
+ unsigned __int8 flags;
+ MapType mapType;
+ Picmip picmip;
+ bool noPicmip;
+ CardMemory cardMemory;
+ unsigned __int16 width;
+ unsigned __int16 height;
+ unsigned __int16 depth;
+ unsigned __int8 levelCount;
+ bool streaming;
+ unsigned __int8 *pixels;
+ GfxImageStreamData streams[4];
+ const char *name;
+};
+static_assert(sizeof(GfxImage) == 0x70, "");
+static_assert(offsetof(GfxImage, texture) == 0x0, "");
+static_assert(offsetof(GfxImage, mapType) == 0x38, "");
+static_assert(offsetof(GfxImage, cardMemory) == 0x3C, "");
+static_assert(offsetof(GfxImage, width) == 0x40, "");
+static_assert(offsetof(GfxImage, pixels) == 0x48, "");
+static_assert(offsetof(GfxImage, streams) == 0x4C, "");
+static_assert(offsetof(GfxImage, name) == 0x6C, "");
+
+struct Sys_File
+{
+ void *handle;
+ int startOffset;
+};
+static_assert(sizeof(Sys_File) == 0x8, "");
+
+struct DBFile
+{
+ Sys_File handle;
+ char name[64];
+};
+static_assert(sizeof(DBFile) == 0x48, "");
+static_assert(offsetof(DBFile, name) == 0x8, "");
+
+struct GfxSubImageStream
+{
+ unsigned int fileOffset;
+ unsigned int fileOffsetEnd;
+ DBFile *file;
+};
+static_assert(sizeof(GfxSubImageStream) == 0xC, "");
+
+struct GfxImageStream
+{
+ GfxSubImageStream part[4];
+};
+static_assert(sizeof(GfxImageStream) == 0x30, "");
+
struct cplane_s;
struct cStaticModel_s;
struct ClipMaterial
@@ -2007,6 +2100,7 @@ static_assert(offsetof(StringTable, values) == 0xC, "");
union XAssetHeader
{
+ GfxImage *image;
clipMap_t *clipMap;
GameWorldSp *gameWorldSp;
GameWorldMp *gameWorldMp;
@@ -2022,6 +2116,30 @@ struct XAsset
XAssetHeader header;
};
+struct XBlock
+{
+ unsigned __int8 *data;
+ unsigned int size;
+};
+static_assert(sizeof(XBlock) == 0x8, "");
+
+struct XZoneMemory
+{
+ XBlock blocks[6];
+};
+static_assert(sizeof(XZoneMemory) == 0x30, "");
+
+struct XZone
+{
+ DBFile file;
+ int flags;
+ int allocType;
+ XZoneMemory mem;
+};
+static_assert(sizeof(XZone) == 0x80, "");
+static_assert(offsetof(XZone, file) == 0x0, "");
+static_assert(offsetof(XZone, mem) == 0x50, "");
+
struct __declspec(align(4)) XAssetEntry
{
XAsset asset;
@@ -2030,12 +2148,15 @@ struct __declspec(align(4)) XAssetEntry
unsigned __int16 nextHash;
unsigned __int16 nextOverride;
};
+static_assert(sizeof(XAssetEntry) == 0x10, "");
+static_assert(offsetof(XAssetEntry, zoneIndex) == 0x8, "");
union XAssetEntryPoolEntry
{
XAssetEntry entry;
XAssetEntryPoolEntry *next;
};
+static_assert(sizeof(XAssetEntryPoolEntry) == 0x10, "");
} // namespace mp_tu6
} // namespace iw4
diff --git a/src/game/iw4/mp_tu6/symbols.h b/src/game/iw4/mp_tu6/symbols.h
index ee6e7c7d..3cb31764 100644
--- a/src/game/iw4/mp_tu6/symbols.h
+++ b/src/game/iw4/mp_tu6/symbols.h
@@ -146,6 +146,19 @@ static Key_SetCatcher_t Key_SetCatcher = reinterpret_cast(0x82
typedef Material *(*Material_RegisterHandle_t)(const char *name);
static Material_RegisterHandle_t Material_RegisterHandle = reinterpret_cast(0x823C2FF8);
+typedef void (*ImageCache_InitImage_t)(GfxImage *image, GfxImage *remoteImage, unsigned __int8 *pixels,
+ unsigned int imagePartIndex);
+static ImageCache_InitImage_t ImageCache_InitImage = reinterpret_cast(0x823DE448);
+
+typedef int (*Zlib_InflateInit_t)(void *stream, const char *version, int streamSize);
+static Zlib_InflateInit_t Zlib_InflateInit = reinterpret_cast(0x82344E90);
+
+typedef int (*Zlib_Inflate_t)(void *stream, int flush);
+static Zlib_Inflate_t Zlib_Inflate = reinterpret_cast(0x82344EA0);
+
+typedef int (*Zlib_InflateEnd_t)(void *stream);
+static Zlib_InflateEnd_t Zlib_InflateEnd = reinterpret_cast(0x82344CB8);
+
static auto R_CheckDvarModified = reinterpret_cast(0x823DDD78);
typedef void (*R_AddCmdDrawStretchPic_t)(float x, float y, float w, float h, float s0, float t0, float s1, float t1,
@@ -290,6 +303,13 @@ static auto CL_CreateNewCommands = reinterpret_cast(0x83052680);
static auto fields = reinterpret_cast(0x82027518);
+static auto g_assetEntryPool = reinterpret_cast(0x82839700);
+static const unsigned int g_assetEntryPoolSize = 34000;
+static auto g_gfxImagePool = reinterpret_cast(0x828C058C);
+static const unsigned int g_gfxImagePoolSize = 0xE00;
+static auto g_imageStreams = reinterpret_cast(0x82C91600);
+static auto g_zones = reinterpret_cast(0x829D8048);
+static const unsigned int g_zoneCount = 33;
static auto g_entities = reinterpret_cast(0x82E2A580);
static auto level = reinterpret_cast(0x82FF2F08);
static auto sharedUiInfo = reinterpret_cast(0x836A3AC0);
diff --git a/src/game/iw4/sp/components/clipmap.cpp b/src/game/iw4/sp/components/clipmap.cpp
index f155db9c..77f6e6d3 100644
--- a/src/game/iw4/sp/components/clipmap.cpp
+++ b/src/game/iw4/sp/components/clipmap.cpp
@@ -18,7 +18,7 @@ void Load_clipMap_t_Hook(bool atStreamStart)
auto mapEnts = (*varclipMap_t)->mapEnts;
// Dump map entities if enabled
- if (Config::dump_map_ents)
+ if (Config::dump_assets)
{
std::string dumpPath = va("%s\\%s.ents", DUMP_DIR, mapEnts->name); // IW4x naming convention
std::replace(dumpPath.begin(), dumpPath.end(), '/', '\\');
diff --git a/src/game/iw4/sp/components/scr_parser.cpp b/src/game/iw4/sp/components/scr_parser.cpp
index fca12990..2b44d4dd 100644
--- a/src/game/iw4/sp/components/scr_parser.cpp
+++ b/src/game/iw4/sp/components/scr_parser.cpp
@@ -12,7 +12,7 @@ char *Scr_AddSourceBuffer_Hook(const char *filename, const char *extFilename)
auto callOriginal = [&]()
{ return Scr_AddSourceBuffer_Detour.GetOriginal()(filename, extFilename); };
- if (Config::dump_rawfile)
+ if (Config::dump_assets)
{
auto contents = callOriginal();
// Dump the script to a file
diff --git a/src/game/qos/mp/components/scr_parser.cpp b/src/game/qos/mp/components/scr_parser.cpp
index 119a3bb1..5d2ce8b0 100644
--- a/src/game/qos/mp/components/scr_parser.cpp
+++ b/src/game/qos/mp/components/scr_parser.cpp
@@ -15,7 +15,7 @@ char *Scr_AddSourceBuffer_Hook(const char *filename, const char *extFilename, co
archive);
};
- if (Config::dump_rawfile)
+ if (Config::dump_assets)
{
DbgPrint("GSCLoader: Dumping script %s\n", extFilename);
auto contents = callOriginal();
diff --git a/src/game/qos/sp/components/scr_parser.cpp b/src/game/qos/sp/components/scr_parser.cpp
index 891e4d4f..4a9a67fc 100644
--- a/src/game/qos/sp/components/scr_parser.cpp
+++ b/src/game/qos/sp/components/scr_parser.cpp
@@ -15,7 +15,7 @@ char *Scr_AddSourceBuffer_Hook(const char *filename, const char *extFilename, co
archive);
};
- if (Config::dump_rawfile)
+ if (Config::dump_assets)
{
DbgPrint("GSCLoader: Dumping script %s\n", extFilename);
auto contents = callOriginal();
diff --git a/src/game/t4/mp/components/gsc_loader.cpp b/src/game/t4/mp/components/gsc_loader.cpp
index 49120d54..ea3ee487 100644
--- a/src/game/t4/mp/components/gsc_loader.cpp
+++ b/src/game/t4/mp/components/gsc_loader.cpp
@@ -16,7 +16,7 @@ char *GSCLoader::Scr_AddSourceBuffer_Hook(scriptInstance_t a1, const char *filen
codePos, archive);
};
- if (Config::dump_rawfile)
+ if (Config::dump_assets)
{
auto contents = callOriginal();
// Dump the script to a file
diff --git a/src/game/t4/mp/components/image_loader.cpp b/src/game/t4/mp/components/image_loader.cpp
index a81e9fbc..29ea765b 100644
--- a/src/game/t4/mp/components/image_loader.cpp
+++ b/src/game/t4/mp/components/image_loader.cpp
@@ -1,558 +1,615 @@
#include "pch.h"
+#include "common/config.h"
#include "image_loader.h"
+#include "image/dds_loader.h"
+#include "image/texture_layout.h"
+#include "image/xenos_texture.h"
namespace
{
-void GPUEndianSwapTexture(std::vector &pixelData, GPUENDIAN endianType)
+namespace game = t4::mp;
+
+const int CON_CHANNEL_ERROR = 1;
+const int CON_CHANNEL_CONSOLEONLY = 7;
+
+std::set g_streamedImageReplacements;
+
+typedef image::DdsImage DDSImage;
+
+DDSImage ReadDDSFile(const std::string &filepath)
{
- switch (endianType)
- {
- case GPUENDIAN_8IN16:
- XGEndianSwapMemory(pixelData.data(), pixelData.data(), XGENDIAN_8IN16, 2, pixelData.size() / 2);
- break;
- case GPUENDIAN_8IN32:
- XGEndianSwapMemory(pixelData.data(), pixelData.data(), XGENDIAN_8IN32, 4, pixelData.size() / 4);
- break;
- case GPUENDIAN_16IN32:
- XGEndianSwapMemory(pixelData.data(), pixelData.data(), XGENDIAN_16IN32, 4, pixelData.size() / 4);
- break;
- }
+ return image::LoadDdsFromFile(filepath);
}
-// Function to swap all necessary fields from little-endian to big-endian
-void SwapDDSHeaderEndian(DDSHeader &header)
+std::string GetReplacementPath(const char *imageName)
{
- header.magic = _byteswap_ulong(header.magic);
- header.size = _byteswap_ulong(header.size);
- header.flags = _byteswap_ulong(header.flags);
- header.height = _byteswap_ulong(header.height);
- header.width = _byteswap_ulong(header.width);
- header.pitchOrLinearSize = _byteswap_ulong(header.pitchOrLinearSize);
- header.depth = _byteswap_ulong(header.depth);
- header.mipMapCount = _byteswap_ulong(header.mipMapCount);
-
- for (int i = 0; i < 11; i++)
- header.reserved1[i] = _byteswap_ulong(header.reserved1[i]);
-
- header.pixelFormat.size = _byteswap_ulong(header.pixelFormat.size);
- header.pixelFormat.flags = _byteswap_ulong(header.pixelFormat.flags);
- header.pixelFormat.fourCC = _byteswap_ulong(header.pixelFormat.fourCC);
- header.pixelFormat.rgbBitCount = _byteswap_ulong(header.pixelFormat.rgbBitCount);
- header.pixelFormat.rBitMask = _byteswap_ulong(header.pixelFormat.rBitMask);
- header.pixelFormat.gBitMask = _byteswap_ulong(header.pixelFormat.gBitMask);
- header.pixelFormat.bBitMask = _byteswap_ulong(header.pixelFormat.bBitMask);
- header.pixelFormat.aBitMask = _byteswap_ulong(header.pixelFormat.aBitMask);
-
- header.caps = _byteswap_ulong(header.caps);
- header.caps2 = _byteswap_ulong(header.caps2);
- header.caps3 = _byteswap_ulong(header.caps3);
- header.caps4 = _byteswap_ulong(header.caps4);
- header.reserved2 = _byteswap_ulong(header.reserved2);
+ const std::string userPath = std::string(USERRAW_DIR) + "\\images\\" + imageName + ".dds";
+ if (filesystem::file_exists(userPath))
+ return userPath;
+
+ return Config::GetModBasePath() + "\\images\\" + imageName + ".dds";
}
-DDSImage ReadDDSFile(const std::string &filepath)
+bool ValidateDDSHeader(const game::GfxImage *image, const DDSImage &ddsImage, const std::string &path,
+ GPUTEXTUREFORMAT *ddsFormat)
{
- DDSImage ddsImage;
- std::ifstream file(filepath, std::ios::binary);
-
- if (!file.is_open())
+ if (ddsImage.data.empty())
{
- DbgPrint("ERROR: Unable to open file: %s\n", filepath.c_str());
- return ddsImage; // Return empty DDSImage
+ game::Com_PrintError(CON_CHANNEL_ERROR, "Image_Replace: Failed to load DDS file for image '%s': %s\n",
+ image->name, path.c_str());
+ return false;
}
- // Read DDS header (raw, little-endian)
- file.read(reinterpret_cast(&ddsImage.header), sizeof(DDSHeader));
-
- // Swap only the magic number to big-endian for proper validation
- uint32_t magicSwapped = _byteswap_ulong(ddsImage.header.magic);
-
- if (magicSwapped != 0x20534444) // 'DDS ' in big-endian
+ if (ddsImage.header.dwSize != image::DDS_HEADER_SIZE ||
+ ddsImage.header.ddspf.dwSize != image::DDS_PIXEL_FORMAT_SIZE)
{
- DbgPrint("ERROR: Invalid DDS file: %s\n", filepath.c_str());
- file.close();
- return ddsImage;
+ game::Com_PrintError(CON_CHANNEL_ERROR, "Image '%s' has an invalid DDS header: size=%u pixelFormatSize=%u\n",
+ image->name, ddsImage.header.dwSize, ddsImage.header.ddspf.dwSize);
+ return false;
}
- // Swap header fields to big-endian for Xbox 360
- SwapDDSHeaderEndian(ddsImage.header);
-
- // Move to end of file to get total file size
- file.seekg(0, std::ios::end);
- std::streampos fileSize = file.tellg();
+ if (!ddsImage.GetGpuFormat(ddsFormat))
+ {
+ game::Com_PrintError(CON_CHANNEL_ERROR, "Image '%s' has an unsupported DDS format: flags=0x%X fourCC=0x%X\n",
+ image->name, ddsImage.header.ddspf.dwFlags, ddsImage.header.ddspf.dwFourCC);
+ return false;
+ }
- // Ensure fileSize is valid before proceeding
- if (fileSize == std::streampos(-1))
+ if (static_cast(image->texture.basemap->Format.DataFormat) != static_cast(*ddsFormat))
{
- DbgPrint("ERROR: Failed to determine file size.\n");
- file.close();
- return ddsImage;
+ game::Com_PrintError(CON_CHANNEL_ERROR, "Image '%s' format does not match DDS file: Expected %u, Got %u\n",
+ image->name, static_cast(image->texture.basemap->Format.DataFormat),
+ static_cast(*ddsFormat));
+ return false;
}
- // Move back to after the header
- file.seekg(sizeof(DDSHeader), std::ios::beg);
+ return true;
+}
- // Compute data size safely
- size_t dataSize = static_cast(fileSize) - sizeof(DDSHeader);
+bool Validate2DReplacementData(const game::GfxImage *image, const DDSImage &ddsImage, GPUTEXTUREFORMAT format,
+ uint32_t ddsFirstMipLevel, uint32_t replacementLevelCount, size_t *requiredDDSSize,
+ size_t *requiredTextureBytes)
+{
+ const size_t ddsMipOffset =
+ image::CalculateDdsMipOffset(ddsImage.header.dwWidth, ddsImage.header.dwHeight, format, ddsFirstMipLevel);
+ const size_t requiredLinearSize = image::CalculateRequiredLinearDataSize(
+ ddsImage.header.dwWidth, ddsImage.header.dwHeight, format, ddsFirstMipLevel, replacementLevelCount, 1u);
+ *requiredDDSSize = ddsMipOffset + requiredLinearSize;
+ if (requiredLinearSize == 0 || (ddsFirstMipLevel > 0 && ddsMipOffset == 0))
+ return false;
+
+ if (ddsImage.data.size() < *requiredDDSSize)
+ return false;
+
+ const uint32_t baseSize =
+ image::xenos_texture::CalculateBaseSize(image->texture.basemap, image->width, image->height, 1u);
+ const size_t mipBytes =
+ image::CalculateRequiredMipTextureBytes(image->width, image->height, format, 1u, replacementLevelCount, 1u);
+ *requiredTextureBytes = static_cast(baseSize) + mipBytes;
+ const int cardMemory = image->cardMemory.platform[0];
+ if (cardMemory > 0 && *requiredTextureBytes > static_cast(cardMemory))
+ return false;
+
+ return true;
+}
- // Read image data
- ddsImage.data.resize(dataSize);
- file.read(reinterpret_cast(ddsImage.data.data()), dataSize);
+bool ValidateCubeReplacementData(const game::GfxImage *image, const DDSImage &ddsImage, GPUTEXTUREFORMAT format,
+ uint32_t faceSize, uint32_t tiledBaseSize, size_t *requiredDDSSize)
+{
+ *requiredDDSSize = static_cast(faceSize) * 6u;
+ if (faceSize == 0 || *requiredDDSSize == 0)
+ return false;
- file.close();
+ if (ddsImage.data.size() < *requiredDDSSize)
+ return false;
- // Debug output
- DbgPrint("INFO: DDS file '%s' loaded successfully.\n", filepath.c_str());
- DbgPrint(" Resolution: %ux%u\n", ddsImage.header.width, ddsImage.header.height);
- DbgPrint(" MipMaps: %u\n", ddsImage.header.mipMapCount);
- DbgPrint(" Data Size: %u bytes\n", static_cast(dataSize));
+ const int cardMemory = image->cardMemory.platform[0];
+ if (cardMemory > 0 && static_cast(tiledBaseSize) > static_cast(cardMemory))
+ return false;
- return ddsImage;
+ return true;
}
-} // namespace
-namespace t4
+bool Image_Replace_2D(game::GfxImage *image, const DDSImage &ddsImage, uint32_t ddsFirstMipLevel)
{
-namespace mp
-{
-void ImageLoader::DumpGfxImage(const GfxImage *image)
-{
- DbgPrint("Image_Dump: Dumping image '%s'\n", image->name);
-
- if (!image)
+ if (image->mapType != game::MAPTYPE_2D)
{
- DbgPrint("Image_Dump: Null GfxImage!\n");
- return;
+ game::Com_PrintError(CON_CHANNEL_ERROR, "Image '%s' is not a 2D map!\n", image->name);
+ return false;
}
- if (!image->pixels || image->baseSize == 0)
+ const GPUTEXTUREFORMAT format = static_cast(image->texture.basemap->Format.DataFormat);
+ const uint32_t levelCount = image::xenos_texture::GetTextureLevelCount(image->texture.basemap);
+ const uint32_t mipTailBaseLevel = image->texture.basemap->Format.PackedMips != 0
+ ? image::xenos_texture::GetMipTailBaseLevel(image->width, image->height)
+ : levelCount;
+ const uint32_t ddsMipCount = ddsImage.GetMipCount();
+ if (ddsFirstMipLevel >= ddsMipCount)
{
- DbgPrint("Image_Dump: Image '%s' has no valid pixel data!\n", image->name);
- return;
+ game::Com_PrintError(CON_CHANNEL_ERROR, "Image '%s' replacement DDS has no mip %u: mipCount=%u\n", image->name,
+ ddsFirstMipLevel, ddsMipCount);
+ return false;
}
- if (image->mapType != MAPTYPE_2D && image->mapType != MAPTYPE_CUBE)
+ const uint32_t replaceLevelCount = min(levelCount, ddsMipCount - ddsFirstMipLevel);
+ const uint32_t nonPackedLevelCount = max(1u, min(replaceLevelCount, mipTailBaseLevel));
+ unsigned char *baseData = image::xenos_texture::GetTextureBase(image->texture.basemap, image->pixels);
+ unsigned char *mipData = image::xenos_texture::GetTextureMipBase(image->texture.basemap, baseData, image->width,
+ image->height, format, 1u);
+
+ size_t requiredDDSSize = 0;
+ size_t requiredTextureBytes = 0;
+ if (!Validate2DReplacementData(image, ddsImage, format, ddsFirstMipLevel, nonPackedLevelCount, &requiredDDSSize,
+ &requiredTextureBytes))
{
- DbgPrint("Image_Dump: Unsupported map type %d!\n", image->mapType);
- return;
+ if (requiredDDSSize == 0)
+ {
+ game::Com_PrintError(CON_CHANNEL_ERROR, "Image '%s' has unsupported replacement format %u!\n", image->name,
+ static_cast(format));
+ }
+ else if (ddsImage.data.size() < requiredDDSSize)
+ {
+ game::Com_PrintError(CON_CHANNEL_ERROR,
+ "Image '%s' DDS data is too small: have=%u need=%u for %u mip levels\n", image->name,
+ static_cast(ddsImage.data.size()),
+ static_cast(requiredDDSSize), nonPackedLevelCount);
+ }
+ else
+ {
+ game::Com_PrintError(CON_CHANNEL_ERROR, "Image '%s' replacement exceeds texture memory: have=%u need=%u\n",
+ image->name, static_cast(image->cardMemory.platform[0]),
+ static_cast(requiredTextureBytes));
+ }
+
+ return false;
}
- UINT BaseSize;
- XGGetTextureLayout(image->texture.basemap, 0, &BaseSize, 0, 0, 0, 0, 0, 0, 0, 0);
-
- DDSHeader header;
- memset(&header, 0, sizeof(DDSHeader));
-
- header.magic = _byteswap_ulong(DDS_MAGIC);
- header.size = _byteswap_ulong(DDS_HEADER_SIZE);
- header.flags = _byteswap_ulong(DDSD_CAPS | DDSD_HEIGHT | DDSD_WIDTH | DDSD_PIXELFORMAT | DDSD_LINEARSIZE);
- header.height = _byteswap_ulong(image->height);
- header.width = _byteswap_ulong(image->width);
- header.depth = _byteswap_ulong(image->depth);
- header.mipMapCount = _byteswap_ulong(image->texture.basemap->GetLevelCount());
-
- auto format = image->texture.basemap->Format.DataFormat;
- switch (format)
- {
- case GPUTEXTUREFORMAT_DXT1:
- header.pixelFormat.fourCC = _byteswap_ulong(DXT1_FOURCC);
- header.pitchOrLinearSize = BaseSize;
- break;
- case GPUTEXTUREFORMAT_DXT2_3:
- header.pixelFormat.fourCC = _byteswap_ulong(DXT3_FOURCC);
- header.pitchOrLinearSize = BaseSize;
- break;
- case GPUTEXTUREFORMAT_DXT4_5:
- header.pixelFormat.fourCC = _byteswap_ulong(DXT5_FOURCC);
- header.pitchOrLinearSize = BaseSize;
- break;
- case GPUTEXTUREFORMAT_DXN:
- header.pixelFormat.fourCC = _byteswap_ulong(DXN_FOURCC);
- header.pitchOrLinearSize = BaseSize;
- break;
- case GPUTEXTUREFORMAT_8:
- header.pixelFormat.flags = _byteswap_ulong(DDPF_LUMINANCE);
- header.pixelFormat.rgbBitCount = _byteswap_ulong(8);
- header.pixelFormat.rBitMask = _byteswap_ulong(0x000000FF);
- header.pixelFormat.gBitMask = 0;
- header.pixelFormat.bBitMask = 0;
- header.pixelFormat.aBitMask = 0;
- header.pitchOrLinearSize = BaseSize;
- break;
- case GPUTEXTUREFORMAT_8_8:
- header.pixelFormat.flags = _byteswap_ulong(DDPF_LUMINANCE | DDPF_ALPHAPIXELS);
- header.pixelFormat.rgbBitCount = _byteswap_ulong(16);
- header.pixelFormat.rBitMask = _byteswap_ulong(0x000000FF);
- header.pixelFormat.gBitMask = _byteswap_ulong(0x0000FF00);
- header.pixelFormat.bBitMask = 0;
- header.pixelFormat.aBitMask = 0;
- header.pitchOrLinearSize = BaseSize;
- break;
- case GPUTEXTUREFORMAT_8_8_8_8:
- header.pixelFormat.flags = _byteswap_ulong(DDPF_RGB | DDPF_ALPHAPIXELS);
- header.pixelFormat.rgbBitCount = _byteswap_ulong(32);
- header.pixelFormat.rBitMask = _byteswap_ulong(0x00FF0000);
- header.pixelFormat.gBitMask = _byteswap_ulong(0x0000FF00);
- header.pixelFormat.bBitMask = _byteswap_ulong(0x000000FF);
- header.pixelFormat.aBitMask = _byteswap_ulong(0xFF000000);
- header.pitchOrLinearSize = BaseSize;
- break;
- default:
- DbgPrint("Image_Dump: Unsupported texture format %d!\n", format);
- return;
+ if (baseData == NULL || mipData == NULL)
+ {
+ game::Com_PrintError(CON_CHANNEL_ERROR, "Image_Replace_2D: Image '%s' has no valid texture memory!\n",
+ image->name);
+ return false;
}
- // Set texture capabilities
- header.caps = _byteswap_ulong(DDSCAPS_TEXTURE | DDSCAPS_MIPMAP);
+ uint32_t ddsOffset =
+ image::CalculateDdsMipOffset(ddsImage.header.dwWidth, ddsImage.header.dwHeight, format, ddsFirstMipLevel);
- // Handle Cubemaps
- if (image->mapType == mp::MAPTYPE_CUBE)
+ for (uint32_t localMipLevel = 0; localMipLevel < nonPackedLevelCount; localMipLevel++)
{
- header.caps2 = _byteswap_ulong(DDSCAPS2_CUBEMAP | DDSCAPS2_CUBEMAP_POSITIVEX | DDSCAPS2_CUBEMAP_NEGATIVEX |
- DDSCAPS2_CUBEMAP_POSITIVEY | DDSCAPS2_CUBEMAP_NEGATIVEY |
- DDSCAPS2_CUBEMAP_POSITIVEZ | DDSCAPS2_CUBEMAP_NEGATIVEZ);
- }
+ const uint32_t ddsMipLevel = ddsFirstMipLevel + localMipLevel;
+ const uint32_t rowPitch =
+ image::xenos_texture::CalculateLinearRowPitch(ddsImage.header.dwWidth, ddsMipLevel, format);
+ const uint32_t ddsMipLevelSize = image::xenos_texture::CalculateLinearLevelSize(
+ ddsImage.header.dwWidth, ddsImage.header.dwHeight, ddsMipLevel, format);
+ const uint32_t tiledMipLevelSize = image::xenos_texture::CalculateTiledLevelSize(
+ image->width, image->height, localMipLevel, format, image->texture.basemap->Format.Pitch);
+
+ if (ddsMipLevelSize == 0 || tiledMipLevelSize == 0 || rowPitch == 0)
+ {
+ game::Com_PrintError(CON_CHANNEL_ERROR,
+ "Image_Replace_2D: Unsupported format %u for image '%s' mip level %u\n",
+ image->texture.basemap->Format.DataFormat, image->name, localMipLevel);
+ return false;
+ }
- std::string filename = "game:\\_codxe\\dump\\images\\";
- std::string sanitized_name = image->name;
+ if (static_cast(ddsOffset) + ddsMipLevelSize > ddsImage.data.size())
+ {
+ game::Com_PrintError(CON_CHANNEL_ERROR, "Image_Replace_2D: Image '%s' mip level %u exceeds DDS data size\n",
+ image->name, ddsMipLevel);
+ return false;
+ }
+
+ std::vector levelData(ddsImage.data.begin() + ddsOffset,
+ ddsImage.data.begin() + ddsOffset + ddsMipLevelSize);
+ image::xenos_texture::ApplyGpuEndian(&levelData[0], levelData.size(),
+ static_cast(image->texture.basemap->Format.Endian));
- // Remove invalid characters
- sanitized_name.erase(std::remove_if(sanitized_name.begin(), sanitized_name.end(), [](char c) { return c == '*'; }),
- sanitized_name.end());
+ unsigned char *destination = baseData;
+ if (localMipLevel > 0)
+ {
+ destination = mipData + image::xenos_texture::CalculateMipLevelOffset(image->width, image->height,
+ localMipLevel, format, 1u);
+ }
- filename += sanitized_name + ".dds";
+ std::vector tiledData(tiledMipLevelSize);
+ if (!image::xenos_texture::TileTextureLevel(image->width, image->height, localMipLevel, format,
+ image->texture.basemap->Format.Pitch, &tiledData[0],
+ tiledData.size(), &levelData[0], levelData.size(), rowPitch))
+ {
+ game::Com_PrintError(CON_CHANNEL_ERROR, "Image_Replace_2D: Failed to tile mip level %u for image '%s'\n",
+ localMipLevel, image->name);
+ return false;
+ }
+
+ memcpy(destination, &tiledData[0], tiledMipLevelSize);
+ ddsOffset += ddsMipLevelSize;
+ }
- std::ofstream file(filename, std::ios::binary);
- if (!file)
+ return true;
+}
+
+bool Image_Replace_Cube(game::GfxImage *image, const DDSImage &ddsImage)
+{
+ if (image->mapType != game::MAPTYPE_CUBE)
{
- DbgPrint("Image_Dump: Failed to open file: %s\n", filename.c_str());
- return;
+ game::Com_PrintError(CON_CHANNEL_ERROR, "Image '%s' is not a cube map!\n", image->name);
+ return false;
}
- if (image->mapType == MAPTYPE_CUBE)
+ const GPUTEXTUREFORMAT format = static_cast(image->texture.basemap->Format.DataFormat);
+ const uint32_t faceSize = image::xenos_texture::CalculateLinearLevelSize(image->width, image->height, 0u, format);
+ const uint32_t rowPitch = image::xenos_texture::CalculateLinearRowPitch(image->width, 0u, format);
+ const uint32_t tiledFaceSize = image::xenos_texture::CalculateTiledLevelSize(
+ image->width, image->height, 0u, format, image->texture.basemap->Format.Pitch);
+ const uint32_t tiledBaseSize =
+ image::xenos_texture::CalculateBaseSize(image->texture.basemap, image->width, image->height, 6u);
+ unsigned char *baseData = image::xenos_texture::GetTextureBase(image->texture.basemap, image->pixels);
+
+ if (faceSize == 0 || rowPitch == 0 || tiledFaceSize == 0 || tiledBaseSize < tiledFaceSize * 6u)
{
- file.write(reinterpret_cast(&header), sizeof(DDSHeader));
+ game::Com_PrintError(CON_CHANNEL_ERROR, "Image '%s' has unsupported format %u!\n", image->name,
+ static_cast(format));
+ return false;
+ }
- unsigned int face_size = 0;
- unsigned int rowPitch = 0;
- const GPUTEXTUREFORMAT format = static_cast