From 38953d1daa84486ba139626b546567028af76b9d Mon Sep 17 00:00:00 2001 From: Michael Oliver Date: Wed, 17 Dec 2025 13:35:58 +0000 Subject: [PATCH 1/9] feat: dump iw4 xbox fastfile data --- src/ZoneCommon/Game/IW4/ZoneConstantsIW4.h | 15 ++ .../Game/IW4/ZoneLoaderFactoryIW4.cpp | 199 ++++++++++++------ 2 files changed, 155 insertions(+), 59 deletions(-) diff --git a/src/ZoneCommon/Game/IW4/ZoneConstantsIW4.h b/src/ZoneCommon/Game/IW4/ZoneConstantsIW4.h index d405e4cf..af1af733 100644 --- a/src/ZoneCommon/Game/IW4/ZoneConstantsIW4.h +++ b/src/ZoneCommon/Game/IW4/ZoneConstantsIW4.h @@ -18,6 +18,7 @@ namespace IW4 static constexpr const char* MAGIC_IW4X = "IW4x"; static constexpr int ZONE_VERSION = 276; static constexpr int IW4X_ZONE_VERSION = 3; + static constexpr int ZONE_VERSION_XENON = 269; static_assert(std::char_traits::length(MAGIC_SIGNED_INFINITY_WARD) == sizeof(ZoneHeader::m_magic)); static_assert(std::char_traits::length(MAGIC_SIGNED_OAT) == sizeof(ZoneHeader::m_magic)); @@ -40,6 +41,20 @@ namespace IW4 0x77, 0xCD, 0x62, 0x7D, 0x9D, 0x40, 0x26, 0x44, 0x4B, 0x3B, 0x0A, 0x89, 0x02, 0x03, 0x01, 0x00, 0x01, }; + inline static const uint8_t RSA_PUBLIC_KEY_INFINITY_WARD_XENON[270]{ + 48u, 130u, 1u, 10u, 2u, 130u, 1u, 1u, 0u, 205u, 150u, 80u, 200u, 178u, 78u, 16u, 228u, 121u, 5u, 65u, 110u, 155u, 239u, + 206u, 81u, 196u, 45u, 158u, 201u, 209u, 132u, 35u, 240u, 172u, 34u, 36u, 24u, 233u, 157u, 40u, 203u, 175u, 180u, 127u, 96u, 179u, + 103u, 50u, 67u, 252u, 15u, 169u, 112u, 120u, 66u, 204u, 169u, 134u, 64u, 202u, 50u, 202u, 130u, 155u, 12u, 99u, 176u, 81u, 137u, + 20u, 41u, 234u, 21u, 51u, 63u, 123u, 235u, 102u, 237u, 247u, 22u, 247u, 69u, 6u, 198u, 65u, 98u, 222u, 0u, 117u, 250u, 140u, + 143u, 229u, 191u, 115u, 203u, 117u, 44u, 68u, 9u, 80u, 216u, 30u, 81u, 226u, 88u, 178u, 86u, 111u, 229u, 245u, 192u, 32u, 79u, + 95u, 85u, 143u, 141u, 209u, 55u, 204u, 232u, 59u, 9u, 6u, 44u, 93u, 185u, 123u, 142u, 227u, 199u, 131u, 203u, 143u, 64u, 99u, + 137u, 174u, 107u, 15u, 222u, 56u, 94u, 215u, 244u, 101u, 140u, 48u, 90u, 139u, 158u, 162u, 66u, 3u, 247u, 128u, 171u, 223u, 160u, + 143u, 149u, 160u, 187u, 182u, 29u, 163u, 189u, 59u, 111u, 66u, 104u, 221u, 66u, 251u, 230u, 50u, 2u, 59u, 8u, 238u, 52u, 60u, + 143u, 30u, 232u, 89u, 17u, 8u, 9u, 41u, 190u, 105u, 177u, 212u, 85u, 16u, 111u, 243u, 146u, 23u, 9u, 62u, 112u, 160u, 26u, + 230u, 125u, 109u, 49u, 208u, 248u, 51u, 223u, 240u, 41u, 85u, 98u, 29u, 95u, 62u, 141u, 58u, 29u, 90u, 19u, 176u, 249u, 137u, + 137u, 145u, 143u, 6u, 186u, 58u, 55u, 22u, 197u, 125u, 137u, 239u, 239u, 99u, 78u, 243u, 137u, 25u, 6u, 137u, 241u, 123u, 220u, + 245u, 110u, 54u, 10u, 139u, 200u, 196u, 93u, 194u, 125u, 19u, 157u, 2u, 3u, 1u, 0u, 1u}; + static constexpr size_t AUTHED_CHUNK_SIZE = 0x2000; static constexpr unsigned AUTHED_CHUNK_COUNT_PER_GROUP = 256; diff --git a/src/ZoneLoading/Game/IW4/ZoneLoaderFactoryIW4.cpp b/src/ZoneLoading/Game/IW4/ZoneLoaderFactoryIW4.cpp index 78116b19..7ef435f6 100644 --- a/src/ZoneLoading/Game/IW4/ZoneLoaderFactoryIW4.cpp +++ b/src/ZoneLoading/Game/IW4/ZoneLoaderFactoryIW4.cpp @@ -12,6 +12,7 @@ #include "Loading/Processor/ProcessorInflate.h" #include "Loading/Steps/StepAddProcessor.h" #include "Loading/Steps/StepAllocXBlocks.h" +#include "Loading/Steps/StepDumpData.h" #include "Loading/Steps/StepLoadHash.h" #include "Loading/Steps/StepLoadSignature.h" #include "Loading/Steps/StepLoadZoneContent.h" @@ -23,14 +24,26 @@ #include "Loading/Steps/StepVerifyMagic.h" #include "Loading/Steps/StepVerifySignature.h" #include "Utils/ClassUtils.h" +#include "Utils/Endianness.h" #include "Utils/Logging/Log.h" #include #include +#include #include #include using namespace IW4; +namespace fs = std::filesystem; + +class StepLogPosition final : public ILoadingStep +{ +public: + void PerformStep(ZoneLoader& zoneLoader, ILoadingStream& stream) override + { + con::info("Stream position before dump: 0x{:X}", stream.Pos()); + } +}; namespace { @@ -42,13 +55,50 @@ namespace std::optional InspectZoneHeaderIw4(const ZoneHeader& header) { - if (header.m_version != ZoneConstants::ZONE_VERSION) - return std::nullopt; - if (!memcmp(header.m_magic, ZoneConstants::MAGIC_IW4X, std::char_traits::length(ZoneConstants::MAGIC_IW4X))) + if (endianness::FromLittleEndian(header.m_version) == ZoneConstants::ZONE_VERSION) { - if (*reinterpret_cast(&header.m_magic[std::char_traits::length(ZoneConstants::MAGIC_IW4X)]) - == ZoneConstants::IW4X_ZONE_VERSION) + if (!memcmp(header.m_magic, ZoneConstants::MAGIC_IW4X, std::char_traits::length(ZoneConstants::MAGIC_IW4X))) + { + if (*reinterpret_cast(&header.m_magic[std::char_traits::length(ZoneConstants::MAGIC_IW4X)]) + == ZoneConstants::IW4X_ZONE_VERSION) + { + return ZoneLoaderInspectionResultIW4{ + .m_generic_result = + ZoneLoaderInspectionResult{ + .m_game_id = GameId::IW4, + .m_endianness = GameEndianness::LE, + .m_word_size = GameWordSize::ARCH_32, + .m_platform = GamePlatform::PC, + .m_is_official = false, + .m_is_signed = false, + .m_is_encrypted = false, + }, + .m_is_iw4x = true, + }; + } + + return std::nullopt; + } + + if (!memcmp(header.m_magic, ZoneConstants::MAGIC_SIGNED_INFINITY_WARD, std::char_traits::length(ZoneConstants::MAGIC_SIGNED_INFINITY_WARD))) + { + return ZoneLoaderInspectionResultIW4{ + .m_generic_result = + ZoneLoaderInspectionResult{ + .m_game_id = GameId::IW4, + .m_endianness = GameEndianness::LE, + .m_word_size = GameWordSize::ARCH_32, + .m_platform = GamePlatform::PC, + .m_is_official = true, + .m_is_signed = true, + .m_is_encrypted = false, + }, + .m_is_iw4x = false, + }; + } + + if (!memcmp(header.m_magic, ZoneConstants::MAGIC_UNSIGNED, std::char_traits::length(ZoneConstants::MAGIC_UNSIGNED))) { return ZoneLoaderInspectionResultIW4{ .m_generic_result = @@ -61,45 +111,44 @@ namespace .m_is_signed = false, .m_is_encrypted = false, }, - .m_is_iw4x = true, + .m_is_iw4x = false, }; } - - return std::nullopt; } - - if (!memcmp(header.m_magic, ZoneConstants::MAGIC_SIGNED_INFINITY_WARD, std::char_traits::length(ZoneConstants::MAGIC_SIGNED_INFINITY_WARD))) + else if (endianness::FromBigEndian(header.m_version) == ZoneConstants::ZONE_VERSION_XENON) { - return ZoneLoaderInspectionResultIW4{ - .m_generic_result = - ZoneLoaderInspectionResult{ - .m_game_id = GameId::IW4, - .m_endianness = GameEndianness::LE, - .m_word_size = GameWordSize::ARCH_32, - .m_platform = GamePlatform::PC, - .m_is_official = true, - .m_is_signed = true, - .m_is_encrypted = false, - }, - .m_is_iw4x = false, - }; - } - - if (!memcmp(header.m_magic, ZoneConstants::MAGIC_UNSIGNED, std::char_traits::length(ZoneConstants::MAGIC_UNSIGNED))) - { - return ZoneLoaderInspectionResultIW4{ - .m_generic_result = - ZoneLoaderInspectionResult{ - .m_game_id = GameId::IW4, - .m_endianness = GameEndianness::LE, - .m_word_size = GameWordSize::ARCH_32, - .m_platform = GamePlatform::PC, - .m_is_official = false, - .m_is_signed = false, - .m_is_encrypted = false, - }, - .m_is_iw4x = false, - }; + if (!memcmp(header.m_magic, ZoneConstants::MAGIC_UNSIGNED, std::char_traits::length(ZoneConstants::MAGIC_UNSIGNED))) + { + return ZoneLoaderInspectionResultIW4{ + .m_generic_result = + ZoneLoaderInspectionResult{ + .m_game_id = GameId::IW4, + .m_endianness = GameEndianness::BE, + .m_word_size = GameWordSize::ARCH_32, + .m_platform = GamePlatform::XBOX, + .m_is_official = false, + .m_is_signed = false, + .m_is_encrypted = false, + }, + .m_is_iw4x = false, + }; + } + if (!memcmp(header.m_magic, ZoneConstants::MAGIC_SIGNED_INFINITY_WARD, std::char_traits::length(ZoneConstants::MAGIC_SIGNED_INFINITY_WARD))) + { + return ZoneLoaderInspectionResultIW4{ + .m_generic_result = + ZoneLoaderInspectionResult{ + .m_game_id = GameId::IW4, + .m_endianness = GameEndianness::BE, + .m_word_size = GameWordSize::ARCH_32, + .m_platform = GamePlatform::XBOX, + .m_is_official = true, + .m_is_signed = true, + .m_is_encrypted = false, + }, + .m_is_iw4x = false, + }; + } } return std::nullopt; @@ -121,16 +170,27 @@ namespace #undef XBLOCK_DEF } - std::unique_ptr SetupRsa(const bool isOfficial) + std::unique_ptr SetupRsa(const bool isOfficial, const GamePlatform platform) { if (isOfficial) { auto rsa = cryptography::CreateRsa(cryptography::HashingAlgorithm::RSA_HASH_SHA256, cryptography::RsaPaddingMode::RSA_PADDING_PSS); - if (!rsa->SetKey(ZoneConstants::RSA_PUBLIC_KEY_INFINITY_WARD, sizeof(ZoneConstants::RSA_PUBLIC_KEY_INFINITY_WARD))) + if (platform == GamePlatform::PC) { - con::error("Invalid public key for signature checking"); - return nullptr; + if (!rsa->SetKey(ZoneConstants::RSA_PUBLIC_KEY_INFINITY_WARD, sizeof(ZoneConstants::RSA_PUBLIC_KEY_INFINITY_WARD))) + { + con::error("Invalid public key for signature checking"); + return nullptr; + } + } + else if (platform == GamePlatform::XBOX) + { + if (!rsa->SetKey(ZoneConstants::RSA_PUBLIC_KEY_INFINITY_WARD_XENON, sizeof(ZoneConstants::RSA_PUBLIC_KEY_INFINITY_WARD_XENON))) + { + con::error("Invalid public key for signature checking"); + return nullptr; + } } return rsa; @@ -151,7 +211,9 @@ namespace return; // If file is signed setup a RSA instance. - auto rsa = SetupRsa(inspectResult.m_generic_result.m_is_official); + auto rsa = SetupRsa(inspectResult.m_generic_result.m_is_official, inspectResult.m_generic_result.m_platform); + + zoneLoader.AddLoadingStep(std::make_unique()); zoneLoader.AddLoadingStep(step::CreateStepVerifyMagic(ZoneConstants::MAGIC_AUTH_HEADER)); zoneLoader.AddLoadingStep(step::CreateStepSkipBytes(4)); // Skip reserved @@ -227,6 +289,11 @@ std::unique_ptr ZoneLoaderFactory::CreateLoaderForHeader(const ZoneH // Skip timestamp zoneLoader->AddLoadingStep(step::CreateStepSkipBytes(8)); + if (inspectResult->m_generic_result.m_platform == GamePlatform::XBOX) + { + zoneLoader->AddLoadingStep(step::CreateStepSkipBytes(16)); + } + // Add steps for loading the auth header which also contain the signature of the zone if it is signed. AddAuthHeaderSteps(*inspectResult, *zoneLoader, fileName); @@ -238,21 +305,35 @@ std::unique_ptr ZoneLoaderFactory::CreateLoaderForHeader(const ZoneH zoneLoader->AddLoadingStep(step::CreateStepSkipBytes(1)); } - // Start of the XFile struct - zoneLoader->AddLoadingStep(step::CreateStepLoadZoneSizes()); - zoneLoader->AddLoadingStep(step::CreateStepAllocXBlocks()); + if (inspectResult->m_generic_result.m_endianness == GameEndianness::LE) + { + // Start of the XFile struct + zoneLoader->AddLoadingStep(step::CreateStepLoadZoneSizes()); + zoneLoader->AddLoadingStep(step::CreateStepAllocXBlocks()); - // Start of the zone content - zoneLoader->AddLoadingStep(step::CreateStepLoadZoneContent( - [zonePtr](ZoneInputStream& stream) - { - return std::make_unique(*zonePtr, stream); - }, - 32u, - ZoneConstants::OFFSET_BLOCK_BIT_COUNT, - ZoneConstants::INSERT_BLOCK, - zonePtr->Memory(), - std::move(progressCallback))); + // Start of the zone content + zoneLoader->AddLoadingStep(step::CreateStepLoadZoneContent( + [zonePtr](ZoneInputStream& stream) + { + return std::make_unique(*zonePtr, stream); + }, + 32u, + ZoneConstants::OFFSET_BLOCK_BIT_COUNT, + ZoneConstants::INSERT_BLOCK, + zonePtr->Memory(), + std::move(progressCallback))); + } + else + { + fs::path dumpFileNamePath = fs::path(fileName).filename(); + dumpFileNamePath.replace_extension(".dat"); + std::string dumpFileName = dumpFileNamePath.string(); + con::warn("Dumping xbox assets is not supported, making a full fastfile data dump to {}", dumpFileName); + + zoneLoader->AddLoadingStep(std::make_unique()); + + zoneLoader->AddLoadingStep(step::CreateStepDumpData(dumpFileName, 0xFFFFFFFF)); + } return zoneLoader; } From b19d7bfc75c2b560fdd34d648be9cd2847a2c779 Mon Sep 17 00:00:00 2001 From: Michael Oliver Date: Wed, 17 Dec 2025 13:42:01 +0000 Subject: [PATCH 2/9] cleanup --- .../Game/IW4/ZoneLoaderFactoryIW4.cpp | 16 ++-------------- 1 file changed, 2 insertions(+), 14 deletions(-) diff --git a/src/ZoneLoading/Game/IW4/ZoneLoaderFactoryIW4.cpp b/src/ZoneLoading/Game/IW4/ZoneLoaderFactoryIW4.cpp index 7ef435f6..7bbc53bc 100644 --- a/src/ZoneLoading/Game/IW4/ZoneLoaderFactoryIW4.cpp +++ b/src/ZoneLoading/Game/IW4/ZoneLoaderFactoryIW4.cpp @@ -36,14 +36,7 @@ using namespace IW4; namespace fs = std::filesystem; -class StepLogPosition final : public ILoadingStep -{ -public: - void PerformStep(ZoneLoader& zoneLoader, ILoadingStream& stream) override - { - con::info("Stream position before dump: 0x{:X}", stream.Pos()); - } -}; + namespace { @@ -213,8 +206,6 @@ namespace // If file is signed setup a RSA instance. auto rsa = SetupRsa(inspectResult.m_generic_result.m_is_official, inspectResult.m_generic_result.m_platform); - zoneLoader.AddLoadingStep(std::make_unique()); - zoneLoader.AddLoadingStep(step::CreateStepVerifyMagic(ZoneConstants::MAGIC_AUTH_HEADER)); zoneLoader.AddLoadingStep(step::CreateStepSkipBytes(4)); // Skip reserved @@ -289,7 +280,7 @@ std::unique_ptr ZoneLoaderFactory::CreateLoaderForHeader(const ZoneH // Skip timestamp zoneLoader->AddLoadingStep(step::CreateStepSkipBytes(8)); - if (inspectResult->m_generic_result.m_platform == GamePlatform::XBOX) + if (inspectResult->m_generic_result.m_platform == GamePlatform::XBOX) // skip 16 unknown bytes on Xbox { zoneLoader->AddLoadingStep(step::CreateStepSkipBytes(16)); } @@ -329,9 +320,6 @@ std::unique_ptr ZoneLoaderFactory::CreateLoaderForHeader(const ZoneH dumpFileNamePath.replace_extension(".dat"); std::string dumpFileName = dumpFileNamePath.string(); con::warn("Dumping xbox assets is not supported, making a full fastfile data dump to {}", dumpFileName); - - zoneLoader->AddLoadingStep(std::make_unique()); - zoneLoader->AddLoadingStep(step::CreateStepDumpData(dumpFileName, 0xFFFFFFFF)); } From ea161c658f354826a01c45549775cb6d56416f6f Mon Sep 17 00:00:00 2001 From: Michael Oliver Date: Wed, 17 Dec 2025 13:47:35 +0000 Subject: [PATCH 3/9] convert rsa key to hexadecimal --- src/ZoneCommon/Game/IW4/ZoneConstantsIW4.h | 25 +++++++++++----------- 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/src/ZoneCommon/Game/IW4/ZoneConstantsIW4.h b/src/ZoneCommon/Game/IW4/ZoneConstantsIW4.h index af1af733..99ffd343 100644 --- a/src/ZoneCommon/Game/IW4/ZoneConstantsIW4.h +++ b/src/ZoneCommon/Game/IW4/ZoneConstantsIW4.h @@ -42,18 +42,19 @@ namespace IW4 }; inline static const uint8_t RSA_PUBLIC_KEY_INFINITY_WARD_XENON[270]{ - 48u, 130u, 1u, 10u, 2u, 130u, 1u, 1u, 0u, 205u, 150u, 80u, 200u, 178u, 78u, 16u, 228u, 121u, 5u, 65u, 110u, 155u, 239u, - 206u, 81u, 196u, 45u, 158u, 201u, 209u, 132u, 35u, 240u, 172u, 34u, 36u, 24u, 233u, 157u, 40u, 203u, 175u, 180u, 127u, 96u, 179u, - 103u, 50u, 67u, 252u, 15u, 169u, 112u, 120u, 66u, 204u, 169u, 134u, 64u, 202u, 50u, 202u, 130u, 155u, 12u, 99u, 176u, 81u, 137u, - 20u, 41u, 234u, 21u, 51u, 63u, 123u, 235u, 102u, 237u, 247u, 22u, 247u, 69u, 6u, 198u, 65u, 98u, 222u, 0u, 117u, 250u, 140u, - 143u, 229u, 191u, 115u, 203u, 117u, 44u, 68u, 9u, 80u, 216u, 30u, 81u, 226u, 88u, 178u, 86u, 111u, 229u, 245u, 192u, 32u, 79u, - 95u, 85u, 143u, 141u, 209u, 55u, 204u, 232u, 59u, 9u, 6u, 44u, 93u, 185u, 123u, 142u, 227u, 199u, 131u, 203u, 143u, 64u, 99u, - 137u, 174u, 107u, 15u, 222u, 56u, 94u, 215u, 244u, 101u, 140u, 48u, 90u, 139u, 158u, 162u, 66u, 3u, 247u, 128u, 171u, 223u, 160u, - 143u, 149u, 160u, 187u, 182u, 29u, 163u, 189u, 59u, 111u, 66u, 104u, 221u, 66u, 251u, 230u, 50u, 2u, 59u, 8u, 238u, 52u, 60u, - 143u, 30u, 232u, 89u, 17u, 8u, 9u, 41u, 190u, 105u, 177u, 212u, 85u, 16u, 111u, 243u, 146u, 23u, 9u, 62u, 112u, 160u, 26u, - 230u, 125u, 109u, 49u, 208u, 248u, 51u, 223u, 240u, 41u, 85u, 98u, 29u, 95u, 62u, 141u, 58u, 29u, 90u, 19u, 176u, 249u, 137u, - 137u, 145u, 143u, 6u, 186u, 58u, 55u, 22u, 197u, 125u, 137u, 239u, 239u, 99u, 78u, 243u, 137u, 25u, 6u, 137u, 241u, 123u, 220u, - 245u, 110u, 54u, 10u, 139u, 200u, 196u, 93u, 194u, 125u, 19u, 157u, 2u, 3u, 1u, 0u, 1u}; + 0x30, 0x82, 0x01, 0x0A, 0x02, 0x82, 0x01, 0x01, 0x00, 0xCD, 0x96, 0x50, 0xC8, 0xB2, 0x4E, 0x10, 0xE4, 0x79, 0x05, 0x41, 0x6E, 0x9B, 0xEF, + 0xCE, 0x51, 0xC4, 0x2D, 0x9E, 0xC9, 0xD1, 0x84, 0x23, 0xF0, 0xAC, 0x22, 0x24, 0x18, 0xE9, 0x9D, 0x28, 0xCB, 0xAF, 0xB4, 0x7F, 0x60, 0xB3, + 0x67, 0x32, 0x43, 0xFC, 0x0F, 0xA9, 0x70, 0x78, 0x42, 0xCC, 0xA9, 0x86, 0x40, 0xCA, 0x32, 0xCA, 0x82, 0x9B, 0x0C, 0x63, 0xB0, 0x51, 0x89, + 0x14, 0x29, 0xEA, 0x15, 0x33, 0x3F, 0x7B, 0xEB, 0x66, 0xED, 0xF7, 0x16, 0xF7, 0x45, 0x06, 0xC6, 0x41, 0x62, 0xDE, 0x00, 0x75, 0xFA, 0x8C, + 0x8F, 0xE5, 0xBF, 0x73, 0xCB, 0x75, 0x2C, 0x44, 0x09, 0x50, 0xD8, 0x1E, 0x51, 0xE2, 0x58, 0xB2, 0x56, 0x6F, 0xE5, 0xF5, 0xC0, 0x20, 0x4F, + 0x5F, 0x55, 0x8F, 0x8D, 0xD1, 0x37, 0xCC, 0xE8, 0x3B, 0x09, 0x06, 0x2C, 0x5D, 0xB9, 0x7B, 0x8E, 0xE3, 0xC7, 0x83, 0xCB, 0x8F, 0x40, 0x63, + 0x89, 0xAE, 0x6B, 0x0F, 0xDE, 0x38, 0x5E, 0xD7, 0xF4, 0x65, 0x8C, 0x30, 0x5A, 0x8B, 0x9E, 0xA2, 0x42, 0x03, 0xF7, 0x80, 0xAB, 0xDF, 0xA0, + 0x8F, 0x95, 0xA0, 0xBB, 0xB6, 0x1D, 0xA3, 0xBD, 0x3B, 0x6F, 0x42, 0x68, 0xDD, 0x42, 0xFB, 0xE6, 0x32, 0x02, 0x3B, 0x08, 0xEE, 0x34, 0x3C, + 0x8F, 0x1E, 0xE8, 0x59, 0x11, 0x08, 0x09, 0x29, 0xBE, 0x69, 0xB1, 0xD4, 0x55, 0x10, 0x6F, 0xF3, 0x92, 0x17, 0x09, 0x3E, 0x70, 0xA0, 0x1A, + 0xE6, 0x7D, 0x6D, 0x31, 0xD0, 0xF8, 0x33, 0xDF, 0xF0, 0x29, 0x55, 0x62, 0x1D, 0x5F, 0x3E, 0x8D, 0x3A, 0x1D, 0x5A, 0x13, 0xB0, 0xF9, 0x89, + 0x89, 0x91, 0x8F, 0x06, 0xBA, 0x3A, 0x37, 0x16, 0xC5, 0x7D, 0x89, 0xEF, 0xEF, 0x63, 0x4E, 0xF3, 0x89, 0x19, 0x06, 0x89, 0xF1, 0x7B, 0xDC, + 0xF5, 0x6E, 0x36, 0x0A, 0x8B, 0xC8, 0xC4, 0x5D, 0xC2, 0x7D, 0x13, 0x9D, 0x02, 0x03, 0x01, 0x00, 0x01, + }; static constexpr size_t AUTHED_CHUNK_SIZE = 0x2000; static constexpr unsigned AUTHED_CHUNK_COUNT_PER_GROUP = 256; From 541e16391ef08bf1c78fcefd22ca9914c03fb2d9 Mon Sep 17 00:00:00 2001 From: Michael Oliver Date: Wed, 17 Dec 2025 13:48:17 +0000 Subject: [PATCH 4/9] formatting --- src/ZoneLoading/Game/IW4/ZoneLoaderFactoryIW4.cpp | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/ZoneLoading/Game/IW4/ZoneLoaderFactoryIW4.cpp b/src/ZoneLoading/Game/IW4/ZoneLoaderFactoryIW4.cpp index 7bbc53bc..2ff4f94f 100644 --- a/src/ZoneLoading/Game/IW4/ZoneLoaderFactoryIW4.cpp +++ b/src/ZoneLoading/Game/IW4/ZoneLoaderFactoryIW4.cpp @@ -36,8 +36,6 @@ using namespace IW4; namespace fs = std::filesystem; - - namespace { struct ZoneLoaderInspectionResultIW4 From e1e48463866fc32880fd47e502f15a18d80458e0 Mon Sep 17 00:00:00 2001 From: Michael Oliver Date: Wed, 17 Dec 2025 13:48:46 +0000 Subject: [PATCH 5/9] update comment --- src/ZoneLoading/Game/IW4/ZoneLoaderFactoryIW4.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ZoneLoading/Game/IW4/ZoneLoaderFactoryIW4.cpp b/src/ZoneLoading/Game/IW4/ZoneLoaderFactoryIW4.cpp index 2ff4f94f..36c032da 100644 --- a/src/ZoneLoading/Game/IW4/ZoneLoaderFactoryIW4.cpp +++ b/src/ZoneLoading/Game/IW4/ZoneLoaderFactoryIW4.cpp @@ -278,7 +278,7 @@ std::unique_ptr ZoneLoaderFactory::CreateLoaderForHeader(const ZoneH // Skip timestamp zoneLoader->AddLoadingStep(step::CreateStepSkipBytes(8)); - if (inspectResult->m_generic_result.m_platform == GamePlatform::XBOX) // skip 16 unknown bytes on Xbox + if (inspectResult->m_generic_result.m_platform == GamePlatform::XBOX) // TODO: find out what this is { zoneLoader->AddLoadingStep(step::CreateStepSkipBytes(16)); } From 47bb1493ed2c5a76071c976109d786c5fc625f30 Mon Sep 17 00:00:00 2001 From: Michael Oliver Date: Sun, 21 Dec 2025 20:59:38 +0000 Subject: [PATCH 6/9] add a step to skip xasset stream files --- .../Game/IW4/ZoneLoaderFactoryIW4.cpp | 60 ++++++++++++++++++- 1 file changed, 58 insertions(+), 2 deletions(-) diff --git a/src/ZoneLoading/Game/IW4/ZoneLoaderFactoryIW4.cpp b/src/ZoneLoading/Game/IW4/ZoneLoaderFactoryIW4.cpp index 36c032da..062177fb 100644 --- a/src/ZoneLoading/Game/IW4/ZoneLoaderFactoryIW4.cpp +++ b/src/ZoneLoading/Game/IW4/ZoneLoaderFactoryIW4.cpp @@ -278,9 +278,65 @@ std::unique_ptr ZoneLoaderFactory::CreateLoaderForHeader(const ZoneH // Skip timestamp zoneLoader->AddLoadingStep(step::CreateStepSkipBytes(8)); - if (inspectResult->m_generic_result.m_platform == GamePlatform::XBOX) // TODO: find out what this is + if (inspectResult->m_generic_result.m_platform == GamePlatform::XBOX) { - zoneLoader->AddLoadingStep(step::CreateStepSkipBytes(16)); + + // Xbox fastfiles have additional header data before the auth header: + // - 4 bytes: language flags bitmask + // - 4 bytes: image count + // - sizeof(XAssetStreamFile) * image count (for each language in the bitmask) + // - 4 bytes: unknown + // - 4 bytes: unknown + // struct XAssetStreamFile // sizeof=0xC + // { + // unsigned int fileIndex; + // unsigned int offset; + // unsigned int offsetEnd; + // }; + + struct SkipXAssetStreamFiles : public ILoadingStep + { + void PerformStep(ZoneLoader& zoneLoader, ILoadingStream& stream) override + { + uint32_t languageFlags; + stream.Load(&languageFlags, sizeof(languageFlags)); + languageFlags = endianness::FromBigEndian(languageFlags); + + uint32_t imageCount; + stream.Load(&imageCount, sizeof(imageCount)); + imageCount = endianness::FromBigEndian(imageCount); + + // Count how many languages are set in the bitmask + uint32_t languageCount = 0; + for (int i = 0; i < 15; i++) + { + if (languageFlags & (1 << i)) + { + languageCount++; + } + } + + // Skip image stream file data (12 bytes per image per language) + const size_t imageDataSize = 12 * imageCount * languageCount; + if (imageDataSize > 0) + { + std::vector tempBuffer(std::min(imageDataSize, size_t(8192))); + size_t skipped = 0; + while (skipped < imageDataSize) + { + const size_t toSkip = std::min(imageDataSize - skipped, tempBuffer.size()); + stream.Load(tempBuffer.data(), toSkip); + skipped += toSkip; + } + } + + // Skip the final 8 bytes (2 unknown 4-byte values) + uint8_t finalBytes[8]; + stream.Load(finalBytes, sizeof(finalBytes)); + } + }; + + zoneLoader->AddLoadingStep(std::make_unique()); } // Add steps for loading the auth header which also contain the signature of the zone if it is signed. From 5a0b255d92dea6043c83a3ecd693edf5ba80adeb Mon Sep 17 00:00:00 2001 From: Michael Oliver Date: Sun, 21 Dec 2025 21:35:20 +0000 Subject: [PATCH 7/9] tidy --- .../Game/IW4/ZoneLoaderFactoryIW4.cpp | 59 +-------------- .../Steps/StepSkipZoneImageHeaders.cpp | 75 +++++++++++++++++++ .../Loading/Steps/StepSkipZoneImageHeaders.h | 10 +++ 3 files changed, 87 insertions(+), 57 deletions(-) create mode 100644 src/ZoneLoading/Loading/Steps/StepSkipZoneImageHeaders.cpp create mode 100644 src/ZoneLoading/Loading/Steps/StepSkipZoneImageHeaders.h diff --git a/src/ZoneLoading/Game/IW4/ZoneLoaderFactoryIW4.cpp b/src/ZoneLoading/Game/IW4/ZoneLoaderFactoryIW4.cpp index 062177fb..768f9cd9 100644 --- a/src/ZoneLoading/Game/IW4/ZoneLoaderFactoryIW4.cpp +++ b/src/ZoneLoading/Game/IW4/ZoneLoaderFactoryIW4.cpp @@ -19,6 +19,7 @@ #include "Loading/Steps/StepLoadZoneSizes.h" #include "Loading/Steps/StepRemoveProcessor.h" #include "Loading/Steps/StepSkipBytes.h" +#include "Loading/Steps/StepSkipZoneImageHeaders.h" #include "Loading/Steps/StepVerifyFileName.h" #include "Loading/Steps/StepVerifyHash.h" #include "Loading/Steps/StepVerifyMagic.h" @@ -280,63 +281,7 @@ std::unique_ptr ZoneLoaderFactory::CreateLoaderForHeader(const ZoneH if (inspectResult->m_generic_result.m_platform == GamePlatform::XBOX) { - - // Xbox fastfiles have additional header data before the auth header: - // - 4 bytes: language flags bitmask - // - 4 bytes: image count - // - sizeof(XAssetStreamFile) * image count (for each language in the bitmask) - // - 4 bytes: unknown - // - 4 bytes: unknown - // struct XAssetStreamFile // sizeof=0xC - // { - // unsigned int fileIndex; - // unsigned int offset; - // unsigned int offsetEnd; - // }; - - struct SkipXAssetStreamFiles : public ILoadingStep - { - void PerformStep(ZoneLoader& zoneLoader, ILoadingStream& stream) override - { - uint32_t languageFlags; - stream.Load(&languageFlags, sizeof(languageFlags)); - languageFlags = endianness::FromBigEndian(languageFlags); - - uint32_t imageCount; - stream.Load(&imageCount, sizeof(imageCount)); - imageCount = endianness::FromBigEndian(imageCount); - - // Count how many languages are set in the bitmask - uint32_t languageCount = 0; - for (int i = 0; i < 15; i++) - { - if (languageFlags & (1 << i)) - { - languageCount++; - } - } - - // Skip image stream file data (12 bytes per image per language) - const size_t imageDataSize = 12 * imageCount * languageCount; - if (imageDataSize > 0) - { - std::vector tempBuffer(std::min(imageDataSize, size_t(8192))); - size_t skipped = 0; - while (skipped < imageDataSize) - { - const size_t toSkip = std::min(imageDataSize - skipped, tempBuffer.size()); - stream.Load(tempBuffer.data(), toSkip); - skipped += toSkip; - } - } - - // Skip the final 8 bytes (2 unknown 4-byte values) - uint8_t finalBytes[8]; - stream.Load(finalBytes, sizeof(finalBytes)); - } - }; - - zoneLoader->AddLoadingStep(std::make_unique()); + zoneLoader->AddLoadingStep(step::CreateStepSkipZoneImageHeaders()); } // Add steps for loading the auth header which also contain the signature of the zone if it is signed. diff --git a/src/ZoneLoading/Loading/Steps/StepSkipZoneImageHeaders.cpp b/src/ZoneLoading/Loading/Steps/StepSkipZoneImageHeaders.cpp new file mode 100644 index 00000000..89d002d9 --- /dev/null +++ b/src/ZoneLoading/Loading/Steps/StepSkipZoneImageHeaders.cpp @@ -0,0 +1,75 @@ +#include "StepSkipZoneImageHeaders.h" + +#include "Utils/Endianness.h" + +#include +#include +#include + +namespace +{ + class StepSkipZoneImageHeaders final : public ILoadingStep + { + public: + void PerformStep(ZoneLoader& zoneLoader, ILoadingStream& stream) override + { + + // Xbox fastfiles have additional header data before the auth header: + // - 4 bytes: language flags bitmask + // - 4 bytes: image count + // - sizeof(XAssetStreamFile) * image count (for each language in the bitmask) + // - 4 bytes: unknown + // - 4 bytes: unknown + // struct XAssetStreamFile // sizeof=0xC + // { + // unsigned int fileIndex; + // unsigned int offset; + // unsigned int offsetEnd; + // }; + + uint32_t languageFlags; + stream.Load(&languageFlags, sizeof(languageFlags)); + languageFlags = endianness::FromBigEndian(languageFlags); + + uint32_t imageCount; + stream.Load(&imageCount, sizeof(imageCount)); + imageCount = endianness::FromBigEndian(imageCount); + + // Count how many languages are set in the bitmask + uint32_t languageCount = 0; + for (int i = 0; i < 15; i++) + { + if (languageFlags & (1 << i)) + { + languageCount++; + } + } + + // Skip image stream file data (12 bytes per image per language) + const size_t imageDataSize = 12 * imageCount * languageCount; + if (imageDataSize > 0) + { + std::vector tempBuffer(std::min(imageDataSize, size_t(8192))); + size_t skipped = 0; + while (skipped < imageDataSize) + { + const size_t toSkip = std::min(imageDataSize - skipped, tempBuffer.size()); + stream.Load(tempBuffer.data(), toSkip); + skipped += toSkip; + } + } + + // Skip the final 8 bytes (2 unknown 4-byte values) + uint8_t finalBytes[8]; + stream.Load(finalBytes, sizeof(finalBytes)); + } + }; +} // namespace + +namespace step +{ + std::unique_ptr CreateStepSkipZoneImageHeaders() + { + return std::make_unique(); + } +} // namespace step diff --git a/src/ZoneLoading/Loading/Steps/StepSkipZoneImageHeaders.h b/src/ZoneLoading/Loading/Steps/StepSkipZoneImageHeaders.h new file mode 100644 index 00000000..77782cad --- /dev/null +++ b/src/ZoneLoading/Loading/Steps/StepSkipZoneImageHeaders.h @@ -0,0 +1,10 @@ +#pragma once + +#include "Loading/ILoadingStep.h" + +#include + +namespace step +{ + std::unique_ptr CreateStepSkipZoneImageHeaders(); +} From 8b1afcafaa47512d8f9909031f624b25d6c38f15 Mon Sep 17 00:00:00 2001 From: Jan Laupetin Date: Tue, 23 Dec 2025 00:41:09 +0100 Subject: [PATCH 8/9] chore: rename iw4 zone constants for zone version --- src/ZoneCommon/Game/IW4/ZoneConstantsIW4.h | 4 ++-- src/ZoneLoading/Game/IW4/ZoneLoaderFactoryIW4.cpp | 4 ++-- src/ZoneWriting/Game/IW4/ZoneWriterFactoryIW4.cpp | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/ZoneCommon/Game/IW4/ZoneConstantsIW4.h b/src/ZoneCommon/Game/IW4/ZoneConstantsIW4.h index 99ffd343..a9940448 100644 --- a/src/ZoneCommon/Game/IW4/ZoneConstantsIW4.h +++ b/src/ZoneCommon/Game/IW4/ZoneConstantsIW4.h @@ -16,8 +16,8 @@ namespace IW4 static constexpr const char* MAGIC_SIGNED_OAT = "ABff0100"; static constexpr const char* MAGIC_UNSIGNED = "IWffu100"; static constexpr const char* MAGIC_IW4X = "IW4x"; - static constexpr int ZONE_VERSION = 276; - static constexpr int IW4X_ZONE_VERSION = 3; + static constexpr int ZONE_VERSION_PC = 276; + static constexpr int ZONE_VERSION_IW4x = 3; static constexpr int ZONE_VERSION_XENON = 269; static_assert(std::char_traits::length(MAGIC_SIGNED_INFINITY_WARD) == sizeof(ZoneHeader::m_magic)); diff --git a/src/ZoneLoading/Game/IW4/ZoneLoaderFactoryIW4.cpp b/src/ZoneLoading/Game/IW4/ZoneLoaderFactoryIW4.cpp index 768f9cd9..2b0d29df 100644 --- a/src/ZoneLoading/Game/IW4/ZoneLoaderFactoryIW4.cpp +++ b/src/ZoneLoading/Game/IW4/ZoneLoaderFactoryIW4.cpp @@ -48,12 +48,12 @@ namespace std::optional InspectZoneHeaderIw4(const ZoneHeader& header) { - if (endianness::FromLittleEndian(header.m_version) == ZoneConstants::ZONE_VERSION) + if (endianness::FromLittleEndian(header.m_version) == ZoneConstants::ZONE_VERSION_PC) { if (!memcmp(header.m_magic, ZoneConstants::MAGIC_IW4X, std::char_traits::length(ZoneConstants::MAGIC_IW4X))) { if (*reinterpret_cast(&header.m_magic[std::char_traits::length(ZoneConstants::MAGIC_IW4X)]) - == ZoneConstants::IW4X_ZONE_VERSION) + == ZoneConstants::ZONE_VERSION_IW4x) { return ZoneLoaderInspectionResultIW4{ .m_generic_result = diff --git a/src/ZoneWriting/Game/IW4/ZoneWriterFactoryIW4.cpp b/src/ZoneWriting/Game/IW4/ZoneWriterFactoryIW4.cpp index 0bb35364..0f9f1daa 100644 --- a/src/ZoneWriting/Game/IW4/ZoneWriterFactoryIW4.cpp +++ b/src/ZoneWriting/Game/IW4/ZoneWriterFactoryIW4.cpp @@ -39,7 +39,7 @@ namespace ZoneHeader CreateHeaderForParams(const bool isSecure, const bool isOfficial) { ZoneHeader header{}; - header.m_version = ZoneConstants::ZONE_VERSION; + header.m_version = ZoneConstants::ZONE_VERSION_PC; if (isSecure) { From 26d922410b457372812bd5eb4df29c9b4781b8bb Mon Sep 17 00:00:00 2001 From: Jan Laupetin Date: Tue, 23 Dec 2025 01:09:33 +0100 Subject: [PATCH 9/9] chore: small adjustments to iw4 zone loading --- src/ZoneCommon/Game/IW4/ZoneConstantsIW4.h | 2 +- .../Game/IW4/ZoneLoaderFactoryIW4.cpp | 28 ++++++++++--------- 2 files changed, 16 insertions(+), 14 deletions(-) diff --git a/src/ZoneCommon/Game/IW4/ZoneConstantsIW4.h b/src/ZoneCommon/Game/IW4/ZoneConstantsIW4.h index a9940448..cb36b045 100644 --- a/src/ZoneCommon/Game/IW4/ZoneConstantsIW4.h +++ b/src/ZoneCommon/Game/IW4/ZoneConstantsIW4.h @@ -26,7 +26,7 @@ namespace IW4 static_assert(std::char_traits::length(MAGIC_IW4X) == sizeof(ZoneHeader::m_magic) - sizeof(uint32_t)); static constexpr const char* MAGIC_AUTH_HEADER = "IWffs100"; - inline static const uint8_t RSA_PUBLIC_KEY_INFINITY_WARD[]{ + inline static const uint8_t RSA_PUBLIC_KEY_INFINITY_WARD_PC[]{ 0x30, 0x82, 0x01, 0x0A, 0x02, 0x82, 0x01, 0x01, 0x00, 0xA5, 0x86, 0xCC, 0x18, 0xA9, 0x12, 0x17, 0x4F, 0x3A, 0xC9, 0x0C, 0xD2, 0x38, 0x5D, 0xDB, 0x67, 0x62, 0xA4, 0xE3, 0xD4, 0x42, 0x05, 0x8A, 0x57, 0x0C, 0x31, 0x4E, 0x19, 0xE4, 0xBA, 0x89, 0x73, 0x13, 0xDB, 0x72, 0x25, 0x63, 0xB1, 0x2F, 0xD7, 0xF1, 0x08, 0x48, 0x34, 0x06, 0xD7, 0x84, 0x5F, 0xC8, 0xCF, 0x2F, 0xB6, 0xA3, 0x5A, 0x8F, 0x7E, 0xAA, 0x9D, 0x51, 0xE7, diff --git a/src/ZoneLoading/Game/IW4/ZoneLoaderFactoryIW4.cpp b/src/ZoneLoading/Game/IW4/ZoneLoaderFactoryIW4.cpp index 2b0d29df..14d46ef4 100644 --- a/src/ZoneLoading/Game/IW4/ZoneLoaderFactoryIW4.cpp +++ b/src/ZoneLoading/Game/IW4/ZoneLoaderFactoryIW4.cpp @@ -47,7 +47,6 @@ namespace std::optional InspectZoneHeaderIw4(const ZoneHeader& header) { - if (endianness::FromLittleEndian(header.m_version) == ZoneConstants::ZONE_VERSION_PC) { if (!memcmp(header.m_magic, ZoneConstants::MAGIC_IW4X, std::char_traits::length(ZoneConstants::MAGIC_IW4X))) @@ -168,21 +167,25 @@ namespace { auto rsa = cryptography::CreateRsa(cryptography::HashingAlgorithm::RSA_HASH_SHA256, cryptography::RsaPaddingMode::RSA_PADDING_PSS); + bool keySetSuccessful; if (platform == GamePlatform::PC) { - if (!rsa->SetKey(ZoneConstants::RSA_PUBLIC_KEY_INFINITY_WARD, sizeof(ZoneConstants::RSA_PUBLIC_KEY_INFINITY_WARD))) - { - con::error("Invalid public key for signature checking"); - return nullptr; - } + keySetSuccessful = rsa->SetKey(ZoneConstants::RSA_PUBLIC_KEY_INFINITY_WARD_PC, sizeof(ZoneConstants::RSA_PUBLIC_KEY_INFINITY_WARD_PC)); } else if (platform == GamePlatform::XBOX) { - if (!rsa->SetKey(ZoneConstants::RSA_PUBLIC_KEY_INFINITY_WARD_XENON, sizeof(ZoneConstants::RSA_PUBLIC_KEY_INFINITY_WARD_XENON))) - { - con::error("Invalid public key for signature checking"); - return nullptr; - } + keySetSuccessful = rsa->SetKey(ZoneConstants::RSA_PUBLIC_KEY_INFINITY_WARD_XENON, sizeof(ZoneConstants::RSA_PUBLIC_KEY_INFINITY_WARD_XENON)); + } + else + { + con::error("No public key for platform"); + return nullptr; + } + + if (!keySetSuccessful) + { + con::error("Invalid public key for signature checking"); + return nullptr; } return rsa; @@ -279,10 +282,9 @@ std::unique_ptr ZoneLoaderFactory::CreateLoaderForHeader(const ZoneH // Skip timestamp zoneLoader->AddLoadingStep(step::CreateStepSkipBytes(8)); + // Xbox fastfiles have an additional header of all included images outside the zone data if (inspectResult->m_generic_result.m_platform == GamePlatform::XBOX) - { zoneLoader->AddLoadingStep(step::CreateStepSkipZoneImageHeaders()); - } // Add steps for loading the auth header which also contain the signature of the zone if it is signed. AddAuthHeaderSteps(*inspectResult, *zoneLoader, fileName);