mirror of
https://github.com/whoahq/whoa.git
synced 2026-02-01 00:02:45 +03:00
added direct mpq archive reading support
This commit is contained in:
parent
da51f7e4fc
commit
a3d69ff2e7
@ -26,6 +26,9 @@ set(CMAKE_CXX_STANDARD 11)
|
|||||||
|
|
||||||
include(lib/system/cmake/system.cmake)
|
include(lib/system/cmake/system.cmake)
|
||||||
|
|
||||||
|
find_package(ZLIB REQUIRED)
|
||||||
|
find_package(BZip2 REQUIRED)
|
||||||
|
|
||||||
# Some templates abuse offsetof
|
# Some templates abuse offsetof
|
||||||
if(WHOA_SYSTEM_LINUX OR WHOA_SYSTEM_MAC)
|
if(WHOA_SYSTEM_LINUX OR WHOA_SYSTEM_MAC)
|
||||||
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wno-invalid-offsetof")
|
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wno-invalid-offsetof")
|
||||||
|
|||||||
@ -22,7 +22,7 @@ Assuming all went well, you should see a `dist/bin` directory appear in the `bui
|
|||||||
|
|
||||||
## Running
|
## Running
|
||||||
|
|
||||||
Whoa doesn't currently support reading from MPQ archives. Instead, it assumes you are launching the Whoa executable from the root of a fully extracted MPQ archive set for World of Warcraft 3.3.5a (build 12340). You can obtain a valid set of MPQ archives to extract by installing World of Warcraft 3.3.5a from legally purchased original install media. Whoa does not provide any copy of game data.
|
Whoa can read MPQ archives directly, so launch it from the root of a World of Warcraft 3.3.5a (build 12340) install (the `Data` directory contains MPQs). Fully extracted MPQ data also works. You can obtain a valid set of MPQ archives by installing World of Warcraft 3.3.5a from legally purchased original install media. Whoa does not provide any copy of game data.
|
||||||
|
|
||||||
Assuming all goes well, you should be greeted by the login screen, complete with its flying dragon animation loop.
|
Assuming all goes well, you should be greeted by the login screen, complete with its flying dragon animation loop.
|
||||||
|
|
||||||
|
|||||||
@ -20,6 +20,7 @@ namespace Texture {
|
|||||||
int32_t s_createBlpAsync; // Invented name
|
int32_t s_createBlpAsync; // Invented name
|
||||||
MipBits* s_mipBits;
|
MipBits* s_mipBits;
|
||||||
int32_t s_mipBitsValid;
|
int32_t s_mipBitsValid;
|
||||||
|
size_t s_mipBitsSize;
|
||||||
TSHashTable<CTexture, HASHKEY_TEXTUREFILE> s_textureCache;
|
TSHashTable<CTexture, HASHKEY_TEXTUREFILE> s_textureCache;
|
||||||
|
|
||||||
EGxTexFormat s_pixelFormatToGxTexFormat[10] = {
|
EGxTexFormat s_pixelFormatToGxTexFormat[10] = {
|
||||||
@ -380,6 +381,28 @@ MipBits* TextureAllocMippedImg(PIXEL_FORMAT pixelFormat, uint32_t width, uint32_
|
|||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool MippedImgSet(MipBits* image, uint32_t fourCC, uint32_t width, uint32_t height) {
|
||||||
|
uint32_t levelCount = CalcLevelCount(width, height);
|
||||||
|
uint32_t levelDataSize = CalcLevelOffset(levelCount, width, height, fourCC);
|
||||||
|
size_t imageSize = (sizeof(MipBits::mip) * levelCount) + levelDataSize + MIPPED_IMG_ALIGN;
|
||||||
|
|
||||||
|
if (Texture::s_mipBitsSize && imageSize > Texture::s_mipBitsSize) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto imageData = reinterpret_cast<char*>(image);
|
||||||
|
auto mipBase = imageData + (sizeof(MipBits::mip) * levelCount);
|
||||||
|
auto alignedMipBase = static_cast<char*>(ALIGN_PTR(mipBase, MIPPED_IMG_ALIGN));
|
||||||
|
|
||||||
|
uint32_t levelOffset = 0;
|
||||||
|
for (int32_t level = 0; level < levelCount; level++) {
|
||||||
|
image->mip[level] = reinterpret_cast<C4Pixel*>(alignedMipBase + levelOffset);
|
||||||
|
levelOffset += CalcLevelSize(level, width, height, fourCC);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
void GxTexUpdate(CGxTex* texId, int32_t minX, int32_t minY, int32_t maxX, int32_t maxY, int32_t immediate) {
|
void GxTexUpdate(CGxTex* texId, int32_t minX, int32_t minY, int32_t maxX, int32_t maxY, int32_t immediate) {
|
||||||
CiRect rect = { minY, minX, maxY, maxX };
|
CiRect rect = { minY, minX, maxY, maxX };
|
||||||
GxTexUpdate(texId, rect, immediate);
|
GxTexUpdate(texId, rect, immediate);
|
||||||
@ -646,6 +669,11 @@ int32_t PumpBlpTextureAsync(CTexture* texture, void* buf) {
|
|||||||
|
|
||||||
GetTextureFormats(&pixFormat, &gxTexFormat, preferredFormat, alphaSize);
|
GetTextureFormats(&pixFormat, &gxTexFormat, preferredFormat, alphaSize);
|
||||||
|
|
||||||
|
if (image.m_header.colorEncoding == COLOR_PAL) {
|
||||||
|
pixFormat = PIXEL_ARGB8888;
|
||||||
|
gxTexFormat = GxTex_Argb8888;
|
||||||
|
}
|
||||||
|
|
||||||
int32_t mipLevel = texture->bestMip;
|
int32_t mipLevel = texture->bestMip;
|
||||||
|
|
||||||
Texture::s_mipBitsValid = 1;
|
Texture::s_mipBitsValid = 1;
|
||||||
@ -1123,6 +1151,7 @@ void TextureIncreasePriority(CTexture* texture) {
|
|||||||
void TextureInitialize() {
|
void TextureInitialize() {
|
||||||
uint32_t v0 = MippedImgCalcSize(2, 1024, 1024);
|
uint32_t v0 = MippedImgCalcSize(2, 1024, 1024);
|
||||||
Texture::s_mipBits = reinterpret_cast<MipBits*>(SMemAlloc(v0, __FILE__, __LINE__, 0));
|
Texture::s_mipBits = reinterpret_cast<MipBits*>(SMemAlloc(v0, __FILE__, __LINE__, 0));
|
||||||
|
Texture::s_mipBitsSize = v0;
|
||||||
|
|
||||||
// TODO
|
// TODO
|
||||||
// - rest of function
|
// - rest of function
|
||||||
|
|||||||
@ -49,6 +49,8 @@ CGxTex* TextureAllocGxTex(EGxTexTarget, uint32_t, uint32_t, uint32_t, EGxTexForm
|
|||||||
|
|
||||||
MipBits* TextureAllocMippedImg(PIXEL_FORMAT pixelFormat, uint32_t width, uint32_t height);
|
MipBits* TextureAllocMippedImg(PIXEL_FORMAT pixelFormat, uint32_t width, uint32_t height);
|
||||||
|
|
||||||
|
bool MippedImgSet(MipBits* image, uint32_t fourCC, uint32_t width, uint32_t height);
|
||||||
|
|
||||||
HTEXTURE TextureCacheGetTexture(char*, char*, CGxTexFlags);
|
HTEXTURE TextureCacheGetTexture(char*, char*, CGxTexFlags);
|
||||||
|
|
||||||
HTEXTURE TextureCacheGetTexture(const CImVector&);
|
HTEXTURE TextureCacheGetTexture(const CImVector&);
|
||||||
|
|||||||
@ -29,8 +29,67 @@ int32_t CBLPFile::Lock2(const char* fileName, PIXEL_FORMAT format, uint32_t mipL
|
|||||||
|
|
||||||
switch (this->m_header.colorEncoding) {
|
switch (this->m_header.colorEncoding) {
|
||||||
case COLOR_PAL:
|
case COLOR_PAL:
|
||||||
// TODO
|
if (format != PIXEL_ARGB8888) {
|
||||||
return 0;
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
uint32_t width = this->m_header.width >> mipLevel;
|
||||||
|
uint32_t height = this->m_header.height >> mipLevel;
|
||||||
|
|
||||||
|
if (!width) {
|
||||||
|
width = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!height) {
|
||||||
|
height = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t pixelCount = width * height;
|
||||||
|
auto indices = reinterpret_cast<uint8_t*>(mipData);
|
||||||
|
const uint8_t* alpha = nullptr;
|
||||||
|
|
||||||
|
if (this->m_header.alphaSize) {
|
||||||
|
alpha = indices + pixelCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
const BlpPalPixel* palette = this->m_header.extended.palette;
|
||||||
|
auto out = reinterpret_cast<uint32_t*>(data);
|
||||||
|
|
||||||
|
for (uint32_t i = 0; i < pixelCount; ++i) {
|
||||||
|
const auto& pal = palette[indices[i]];
|
||||||
|
uint8_t a = 0xFF;
|
||||||
|
|
||||||
|
switch (this->m_header.alphaSize) {
|
||||||
|
case 1: {
|
||||||
|
uint8_t byte = alpha[i >> 3];
|
||||||
|
a = ((byte >> (i & 7)) & 0x1) ? 0xFF : 0x00;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 4: {
|
||||||
|
uint8_t byte = alpha[i >> 1];
|
||||||
|
uint8_t nibble = (i & 1) ? (byte >> 4) : (byte & 0x0F);
|
||||||
|
a = static_cast<uint8_t>((nibble << 4) | nibble);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 8:
|
||||||
|
a = alpha[i];
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
out[i] = (static_cast<uint32_t>(a) << 24)
|
||||||
|
| (static_cast<uint32_t>(static_cast<uint8_t>(pal.r)) << 16)
|
||||||
|
| (static_cast<uint32_t>(static_cast<uint8_t>(pal.g)) << 8)
|
||||||
|
| static_cast<uint32_t>(static_cast<uint8_t>(pal.b));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return 1;
|
||||||
|
|
||||||
case COLOR_DXT:
|
case COLOR_DXT:
|
||||||
switch (format) {
|
switch (format) {
|
||||||
@ -96,8 +155,9 @@ int32_t CBLPFile::LockChain2(const char* fileName, PIXEL_FORMAT format, MipBits*
|
|||||||
v14 = 1;
|
v14 = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO
|
if (!MippedImgSet(images, format, v14, v13)) {
|
||||||
// MippedImgSet(format, v14, v13, mipLevel);
|
return 0;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
uint32_t v9 = this->m_header.height >> mipLevel;
|
uint32_t v9 = this->m_header.height >> mipLevel;
|
||||||
|
|
||||||
|
|||||||
@ -19,6 +19,8 @@ add_library(util STATIC
|
|||||||
target_include_directories(util
|
target_include_directories(util
|
||||||
PRIVATE
|
PRIVATE
|
||||||
${CMAKE_SOURCE_DIR}/src
|
${CMAKE_SOURCE_DIR}/src
|
||||||
|
${ZLIB_INCLUDE_DIRS}
|
||||||
|
${BZIP2_INCLUDE_DIRS}
|
||||||
)
|
)
|
||||||
|
|
||||||
target_link_libraries(util
|
target_link_libraries(util
|
||||||
@ -28,6 +30,8 @@ target_link_libraries(util
|
|||||||
common
|
common
|
||||||
storm
|
storm
|
||||||
tempest
|
tempest
|
||||||
|
${ZLIB_LIBRARIES}
|
||||||
|
${BZIP2_LIBRARIES}
|
||||||
)
|
)
|
||||||
|
|
||||||
if(WHOA_SYSTEM_LINUX OR WHOA_SYSTEM_MAC)
|
if(WHOA_SYSTEM_LINUX OR WHOA_SYSTEM_MAC)
|
||||||
|
|||||||
549
src/util/Mpq.cpp
Normal file
549
src/util/Mpq.cpp
Normal file
@ -0,0 +1,549 @@
|
|||||||
|
#include "util/Mpq.hpp"
|
||||||
|
#include <bzlib.h>
|
||||||
|
#include <zlib.h>
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cctype>
|
||||||
|
#include <cstring>
|
||||||
|
#include <fstream>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
#include <sys/stat.h>
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
constexpr uint32_t ID_MPQ = 0x1A51504D;
|
||||||
|
constexpr uint32_t ID_MPQ_USERDATA = 0x1B51504D;
|
||||||
|
|
||||||
|
constexpr uint32_t MPQ_FILE_IMPLODE = 0x00000100;
|
||||||
|
constexpr uint32_t MPQ_FILE_COMPRESSED = 0x00000200;
|
||||||
|
constexpr uint32_t MPQ_FILE_ENCRYPTED = 0x00010000;
|
||||||
|
constexpr uint32_t MPQ_FILE_SINGLE_UNIT = 0x01000000;
|
||||||
|
|
||||||
|
constexpr uint8_t MPQ_COMP_ZLIB = 0x02;
|
||||||
|
constexpr uint8_t MPQ_COMP_BZIP2 = 0x08;
|
||||||
|
|
||||||
|
#pragma pack(push, 1)
|
||||||
|
struct MpqUserDataHeader {
|
||||||
|
uint32_t id;
|
||||||
|
uint32_t userDataSize;
|
||||||
|
uint32_t headerOffset;
|
||||||
|
uint32_t userDataHeaderSize;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct MpqHeaderV2 {
|
||||||
|
uint32_t id;
|
||||||
|
uint32_t headerSize;
|
||||||
|
uint32_t archiveSize;
|
||||||
|
uint16_t formatVersion;
|
||||||
|
uint16_t sectorSizeShift;
|
||||||
|
uint32_t hashTablePos;
|
||||||
|
uint32_t blockTablePos;
|
||||||
|
uint32_t hashTableSize;
|
||||||
|
uint32_t blockTableSize;
|
||||||
|
uint64_t hiBlockTablePos64;
|
||||||
|
uint16_t hashTablePosHi;
|
||||||
|
uint16_t blockTablePosHi;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct MpqHashEntry {
|
||||||
|
uint32_t hashA;
|
||||||
|
uint32_t hashB;
|
||||||
|
uint16_t locale;
|
||||||
|
uint16_t platform;
|
||||||
|
uint32_t blockIndex;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct MpqBlockEntry {
|
||||||
|
uint32_t filePos;
|
||||||
|
uint32_t compressedSize;
|
||||||
|
uint32_t fileSize;
|
||||||
|
uint32_t flags;
|
||||||
|
};
|
||||||
|
#pragma pack(pop)
|
||||||
|
|
||||||
|
uint32_t g_cryptTable[0x500];
|
||||||
|
bool g_cryptReady = false;
|
||||||
|
uint16_t g_locale = 0;
|
||||||
|
void PrepareCryptTable() {
|
||||||
|
uint32_t seed = 0x00100001;
|
||||||
|
for (uint32_t i = 0; i < 0x100; ++i) {
|
||||||
|
for (uint32_t j = 0; j < 5; ++j) {
|
||||||
|
seed = (seed * 125 + 3) % 0x2AAAAB;
|
||||||
|
uint32_t temp1 = (seed & 0xFFFF) << 16;
|
||||||
|
seed = (seed * 125 + 3) % 0x2AAAAB;
|
||||||
|
uint32_t temp2 = (seed & 0xFFFF);
|
||||||
|
g_cryptTable[i + j * 0x100] = temp1 | temp2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
g_cryptReady = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t HashString(const std::string& input, uint32_t type) {
|
||||||
|
if (!g_cryptReady) {
|
||||||
|
PrepareCryptTable();
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t seed1 = 0x7FED7FED;
|
||||||
|
uint32_t seed2 = 0xEEEEEEEE;
|
||||||
|
|
||||||
|
for (char ch : input) {
|
||||||
|
uint8_t c = static_cast<uint8_t>(std::toupper(static_cast<unsigned char>(ch)));
|
||||||
|
seed1 = g_cryptTable[(type << 8) + c] ^ (seed1 + seed2);
|
||||||
|
seed2 = c + seed1 + seed2 + (seed2 << 5) + 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
return seed1;
|
||||||
|
}
|
||||||
|
|
||||||
|
void DecryptBlock(uint32_t* data, size_t length, uint32_t key) {
|
||||||
|
if (!g_cryptReady) {
|
||||||
|
PrepareCryptTable();
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t seed1 = key;
|
||||||
|
uint32_t seed2 = 0xEEEEEEEE;
|
||||||
|
|
||||||
|
for (size_t i = 0; i < length; ++i) {
|
||||||
|
seed2 += g_cryptTable[0x400 + (seed1 & 0xFF)];
|
||||||
|
uint32_t value = data[i];
|
||||||
|
uint32_t decoded = value ^ (seed1 + seed2);
|
||||||
|
data[i] = decoded;
|
||||||
|
seed1 = ((~seed1 << 21) + 0x11111111) | (seed1 >> 11);
|
||||||
|
seed2 = decoded + seed2 + (seed2 << 5) + 3;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string NormalizePath(const char* filename) {
|
||||||
|
std::string path = filename ? filename : "";
|
||||||
|
for (char& ch : path) {
|
||||||
|
if (ch == '/') {
|
||||||
|
ch = '\\';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool FileExists(const std::string& path) {
|
||||||
|
std::ifstream file(path, std::ios::binary);
|
||||||
|
return file.is_open();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool DirectoryExists(const std::string& path) {
|
||||||
|
struct stat st = {};
|
||||||
|
return stat(path.c_str(), &st) == 0 && (st.st_mode & S_IFDIR);
|
||||||
|
}
|
||||||
|
|
||||||
|
uint16_t LocaleFromString(const std::string& locale) {
|
||||||
|
if (locale == "enUS") return 0x0409;
|
||||||
|
if (locale == "enGB") return 0x0809;
|
||||||
|
if (locale == "frFR") return 0x040C;
|
||||||
|
if (locale == "deDE") return 0x0407;
|
||||||
|
if (locale == "esES") return 0x0C0A;
|
||||||
|
if (locale == "esMX") return 0x080A;
|
||||||
|
if (locale == "ruRU") return 0x0419;
|
||||||
|
if (locale == "koKR") return 0x0412;
|
||||||
|
if (locale == "zhCN") return 0x0804;
|
||||||
|
if (locale == "zhTW") return 0x0404;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool DecompressZlib(const uint8_t* in, size_t inSize, uint8_t* out, size_t outSize) {
|
||||||
|
uLongf destLen = static_cast<uLongf>(outSize);
|
||||||
|
int result = uncompress(out, &destLen, in, static_cast<uLongf>(inSize));
|
||||||
|
return result == Z_OK && destLen == outSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool DecompressBzip2(const uint8_t* in, size_t inSize, uint8_t* out, size_t outSize) {
|
||||||
|
unsigned int destLen = static_cast<unsigned int>(outSize);
|
||||||
|
int result = BZ2_bzBuffToBuffDecompress(
|
||||||
|
reinterpret_cast<char*>(out),
|
||||||
|
&destLen,
|
||||||
|
const_cast<char*>(reinterpret_cast<const char*>(in)),
|
||||||
|
static_cast<unsigned int>(inSize),
|
||||||
|
0,
|
||||||
|
0
|
||||||
|
);
|
||||||
|
return result == BZ_OK && destLen == outSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool DecompressBlock(const uint8_t* in, size_t inSize, uint8_t* out, size_t outSize) {
|
||||||
|
if (inSize == outSize) {
|
||||||
|
std::memcpy(out, in, outSize);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (inSize == 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t type = in[0];
|
||||||
|
const uint8_t* payload = in + 1;
|
||||||
|
size_t payloadSize = inSize - 1;
|
||||||
|
|
||||||
|
if (type == 0) {
|
||||||
|
if (payloadSize != outSize) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
std::memcpy(out, payload, outSize);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((type & (type - 1)) != 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type == MPQ_COMP_ZLIB) {
|
||||||
|
return DecompressZlib(payload, payloadSize, out, outSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type == MPQ_COMP_BZIP2) {
|
||||||
|
return DecompressBzip2(payload, payloadSize, out, outSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct MpqArchive {
|
||||||
|
std::ifstream stream;
|
||||||
|
uint64_t baseOffset = 0;
|
||||||
|
uint32_t sectorSize = 0;
|
||||||
|
uint32_t hashTableSize = 0;
|
||||||
|
uint32_t blockTableSize = 0;
|
||||||
|
std::vector<MpqHashEntry> hashTable;
|
||||||
|
std::vector<MpqBlockEntry> blockTable;
|
||||||
|
|
||||||
|
bool Open(const std::string& archivePath) {
|
||||||
|
stream.open(archivePath, std::ios::binary);
|
||||||
|
if (!stream.is_open()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
MpqUserDataHeader userHeader = {};
|
||||||
|
MpqHeaderV2 header = {};
|
||||||
|
|
||||||
|
stream.read(reinterpret_cast<char*>(&userHeader), sizeof(userHeader));
|
||||||
|
if (!stream.good()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (userHeader.id == ID_MPQ_USERDATA) {
|
||||||
|
baseOffset = userHeader.headerOffset;
|
||||||
|
} else {
|
||||||
|
baseOffset = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
stream.seekg(static_cast<std::streamoff>(baseOffset), std::ios::beg);
|
||||||
|
stream.read(reinterpret_cast<char*>(&header), sizeof(header));
|
||||||
|
if (!stream.good()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (header.id != ID_MPQ || header.headerSize < 32) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (header.formatVersion > 1) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
sectorSize = 512u << header.sectorSizeShift;
|
||||||
|
hashTableSize = header.hashTableSize;
|
||||||
|
blockTableSize = header.blockTableSize;
|
||||||
|
|
||||||
|
uint64_t hashTablePos = baseOffset + header.hashTablePos;
|
||||||
|
uint64_t blockTablePos = baseOffset + header.blockTablePos;
|
||||||
|
|
||||||
|
if (header.headerSize >= 44) {
|
||||||
|
hashTablePos = baseOffset + (static_cast<uint64_t>(header.hashTablePosHi) << 32) + header.hashTablePos;
|
||||||
|
blockTablePos = baseOffset + (static_cast<uint64_t>(header.blockTablePosHi) << 32) + header.blockTablePos;
|
||||||
|
}
|
||||||
|
|
||||||
|
hashTable.resize(hashTableSize);
|
||||||
|
blockTable.resize(blockTableSize);
|
||||||
|
|
||||||
|
stream.seekg(static_cast<std::streamoff>(hashTablePos), std::ios::beg);
|
||||||
|
stream.read(reinterpret_cast<char*>(hashTable.data()), hashTableSize * sizeof(MpqHashEntry));
|
||||||
|
|
||||||
|
stream.seekg(static_cast<std::streamoff>(blockTablePos), std::ios::beg);
|
||||||
|
stream.read(reinterpret_cast<char*>(blockTable.data()), blockTableSize * sizeof(MpqBlockEntry));
|
||||||
|
|
||||||
|
if (!stream.good()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
DecryptBlock(reinterpret_cast<uint32_t*>(hashTable.data()), hashTableSize * sizeof(MpqHashEntry) / 4, HashString("(hash table)", 3));
|
||||||
|
DecryptBlock(reinterpret_cast<uint32_t*>(blockTable.data()), blockTableSize * sizeof(MpqBlockEntry) / 4, HashString("(block table)", 3));
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const MpqBlockEntry* FindBlock(const std::string& filename) const {
|
||||||
|
if (hashTable.empty()) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t hashA = HashString(filename, 1);
|
||||||
|
uint32_t hashB = HashString(filename, 2);
|
||||||
|
uint32_t hashStart = HashString(filename, 0) & (hashTableSize - 1);
|
||||||
|
|
||||||
|
const MpqHashEntry* neutral = nullptr;
|
||||||
|
|
||||||
|
for (uint32_t i = 0; i < hashTableSize; ++i) {
|
||||||
|
uint32_t index = (hashStart + i) & (hashTableSize - 1);
|
||||||
|
const MpqHashEntry& entry = hashTable[index];
|
||||||
|
if (entry.blockIndex == 0xFFFFFFFF) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (entry.hashA == hashA && entry.hashB == hashB) {
|
||||||
|
if (g_locale == 0) {
|
||||||
|
if (entry.blockIndex < blockTable.size()) {
|
||||||
|
return &blockTable[entry.blockIndex];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (entry.locale == g_locale) {
|
||||||
|
if (entry.blockIndex < blockTable.size()) {
|
||||||
|
return &blockTable[entry.blockIndex];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (entry.locale == 0 && !neutral) {
|
||||||
|
neutral = &entry;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (neutral && neutral->blockIndex < blockTable.size()) {
|
||||||
|
return &blockTable[neutral->blockIndex];
|
||||||
|
}
|
||||||
|
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ReadFile(const std::string& filename, uint8_t** data, size_t* size) {
|
||||||
|
const MpqBlockEntry* block = FindBlock(filename);
|
||||||
|
if (!block) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (block->flags & MPQ_FILE_ENCRYPTED) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (block->flags & MPQ_FILE_IMPLODE) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint64_t filePos = baseOffset + block->filePos;
|
||||||
|
uint32_t fileSize = block->fileSize;
|
||||||
|
|
||||||
|
std::vector<uint8_t> buffer(fileSize);
|
||||||
|
|
||||||
|
if (block->flags & MPQ_FILE_SINGLE_UNIT) {
|
||||||
|
std::vector<uint8_t> raw(block->compressedSize);
|
||||||
|
stream.seekg(static_cast<std::streamoff>(filePos), std::ios::beg);
|
||||||
|
stream.read(reinterpret_cast<char*>(raw.data()), raw.size());
|
||||||
|
if (!stream.good()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (block->flags & MPQ_FILE_COMPRESSED) {
|
||||||
|
if (!DecompressBlock(raw.data(), raw.size(), buffer.data(), buffer.size())) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (raw.size() != buffer.size()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
std::memcpy(buffer.data(), raw.data(), buffer.size());
|
||||||
|
}
|
||||||
|
} else if ((block->flags & MPQ_FILE_COMPRESSED) == 0) {
|
||||||
|
stream.seekg(static_cast<std::streamoff>(filePos), std::ios::beg);
|
||||||
|
stream.read(reinterpret_cast<char*>(buffer.data()), buffer.size());
|
||||||
|
if (!stream.good()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
uint32_t sectorCount = (fileSize + sectorSize - 1) / sectorSize;
|
||||||
|
uint32_t tableSize = (sectorCount + 1) * sizeof(uint32_t);
|
||||||
|
std::vector<uint32_t> offsets(sectorCount + 1);
|
||||||
|
|
||||||
|
stream.seekg(static_cast<std::streamoff>(filePos), std::ios::beg);
|
||||||
|
stream.read(reinterpret_cast<char*>(offsets.data()), tableSize);
|
||||||
|
if (!stream.good()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (uint32_t i = 0; i < sectorCount; ++i) {
|
||||||
|
uint32_t start = offsets[i];
|
||||||
|
uint32_t end = offsets[i + 1];
|
||||||
|
if (end < start) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t compSize = end - start;
|
||||||
|
uint32_t remaining = fileSize - i * sectorSize;
|
||||||
|
uint32_t outSize = std::min(sectorSize, remaining);
|
||||||
|
|
||||||
|
std::vector<uint8_t> raw(compSize);
|
||||||
|
stream.seekg(static_cast<std::streamoff>(filePos + start), std::ios::beg);
|
||||||
|
stream.read(reinterpret_cast<char*>(raw.data()), raw.size());
|
||||||
|
if (!stream.good()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t* outPtr = buffer.data() + i * sectorSize;
|
||||||
|
if (!DecompressBlock(raw.data(), raw.size(), outPtr, outSize)) {
|
||||||
|
if (raw.size() == outSize) {
|
||||||
|
std::memcpy(outPtr, raw.data(), outSize);
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
*size = buffer.size();
|
||||||
|
auto output = new uint8_t[buffer.size()];
|
||||||
|
std::memcpy(output, buffer.data(), buffer.size());
|
||||||
|
*data = output;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
struct MpqManager {
|
||||||
|
bool initialized = false;
|
||||||
|
std::string dataRoot;
|
||||||
|
std::string locale;
|
||||||
|
std::vector<MpqArchive> archives;
|
||||||
|
|
||||||
|
void Initialize() {
|
||||||
|
if (initialized) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
initialized = true;
|
||||||
|
|
||||||
|
if (DirectoryExists("Data")) {
|
||||||
|
dataRoot = "Data";
|
||||||
|
} else if (DirectoryExists("../Data")) {
|
||||||
|
dataRoot = "../Data";
|
||||||
|
} else {
|
||||||
|
dataRoot = "Data";
|
||||||
|
}
|
||||||
|
|
||||||
|
const char* locales[] = {
|
||||||
|
"enUS",
|
||||||
|
"enGB",
|
||||||
|
"frFR",
|
||||||
|
"deDE",
|
||||||
|
"esES",
|
||||||
|
"esMX",
|
||||||
|
"ruRU",
|
||||||
|
"koKR",
|
||||||
|
"zhCN",
|
||||||
|
"zhTW"
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const char* loc : locales) {
|
||||||
|
std::string probe = dataRoot + "/" + loc + "/locale-" + loc + ".MPQ";
|
||||||
|
if (FileExists(probe)) {
|
||||||
|
locale = loc;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (locale.empty()) {
|
||||||
|
locale = "enUS";
|
||||||
|
}
|
||||||
|
|
||||||
|
g_locale = LocaleFromString(locale);
|
||||||
|
|
||||||
|
AddArchives();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool OpenArchive(const std::string& path) {
|
||||||
|
MpqArchive archive;
|
||||||
|
if (!archive.Open(path)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
archives.push_back(std::move(archive));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void AddIfExists(const std::string& path) {
|
||||||
|
if (FileExists(path)) {
|
||||||
|
OpenArchive(path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void AddPatchSeries(const std::string& prefix) {
|
||||||
|
for (int i = 9; i >= 1; --i) {
|
||||||
|
std::string name = prefix + std::to_string(i) + ".MPQ";
|
||||||
|
AddIfExists(name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void AddArchives() {
|
||||||
|
AddPatchSeries(dataRoot + "/patch-");
|
||||||
|
AddIfExists(dataRoot + "/patch.MPQ");
|
||||||
|
|
||||||
|
std::string localeRoot = dataRoot + "/" + locale;
|
||||||
|
AddPatchSeries(localeRoot + "/patch-" + locale + "-");
|
||||||
|
AddIfExists(localeRoot + "/patch-" + locale + ".MPQ");
|
||||||
|
|
||||||
|
AddIfExists(dataRoot + "/common.MPQ");
|
||||||
|
AddIfExists(dataRoot + "/common-2.MPQ");
|
||||||
|
AddIfExists(dataRoot + "/expansion.MPQ");
|
||||||
|
AddIfExists(dataRoot + "/lichking.MPQ");
|
||||||
|
|
||||||
|
AddIfExists(localeRoot + "/lichking-speech-" + locale + ".MPQ");
|
||||||
|
AddIfExists(localeRoot + "/expansion-speech-" + locale + ".MPQ");
|
||||||
|
AddIfExists(localeRoot + "/lichking-locale-" + locale + ".MPQ");
|
||||||
|
AddIfExists(localeRoot + "/expansion-locale-" + locale + ".MPQ");
|
||||||
|
AddIfExists(localeRoot + "/speech-" + locale + ".MPQ");
|
||||||
|
AddIfExists(localeRoot + "/locale-" + locale + ".MPQ");
|
||||||
|
AddIfExists(localeRoot + "/base-" + locale + ".MPQ");
|
||||||
|
AddIfExists(localeRoot + "/backup-" + locale + ".MPQ");
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ReadFile(const char* filename, uint8_t** data, size_t* size) {
|
||||||
|
Initialize();
|
||||||
|
|
||||||
|
std::string normalized = NormalizePath(filename);
|
||||||
|
for (auto& archive : archives) {
|
||||||
|
if (archive.ReadFile(normalized, data, size)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool HasFile(const char* filename) {
|
||||||
|
Initialize();
|
||||||
|
|
||||||
|
std::string normalized = NormalizePath(filename);
|
||||||
|
for (const auto& archive : archives) {
|
||||||
|
if (archive.FindBlock(normalized)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
MpqManager& GetManager() {
|
||||||
|
static MpqManager manager;
|
||||||
|
return manager;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace Mpq {
|
||||||
|
bool FileExists(const char* filename) {
|
||||||
|
return GetManager().HasFile(filename);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ReadFile(const char* filename, uint8_t** data, size_t* size) {
|
||||||
|
return GetManager().ReadFile(filename, data, size);
|
||||||
|
}
|
||||||
|
}
|
||||||
12
src/util/Mpq.hpp
Normal file
12
src/util/Mpq.hpp
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
#ifndef UTIL_MPQ_HPP
|
||||||
|
#define UTIL_MPQ_HPP
|
||||||
|
|
||||||
|
#include <cstddef>
|
||||||
|
#include <cstdint>
|
||||||
|
|
||||||
|
namespace Mpq {
|
||||||
|
bool FileExists(const char* filename);
|
||||||
|
bool ReadFile(const char* filename, uint8_t** data, size_t* size);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif
|
||||||
@ -1,15 +1,25 @@
|
|||||||
#include "util/SFile.hpp"
|
#include "util/SFile.hpp"
|
||||||
|
#include <algorithm>
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
#include <limits>
|
#include <limits>
|
||||||
#include <storm/Memory.hpp>
|
#include <storm/Memory.hpp>
|
||||||
#include <storm/String.hpp>
|
#include <storm/String.hpp>
|
||||||
|
#include "util/Mpq.hpp"
|
||||||
|
|
||||||
// TODO Proper implementation
|
// TODO Proper implementation
|
||||||
int32_t SFile::Close(SFile* file) {
|
int32_t SFile::Close(SFile* file) {
|
||||||
delete file->m_filename;
|
delete file->m_filename;
|
||||||
|
|
||||||
file->m_stream->close();
|
if (file->m_stream) {
|
||||||
delete file->m_stream;
|
file->m_stream->close();
|
||||||
|
delete file->m_stream;
|
||||||
|
file->m_stream = nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (file->m_buffer) {
|
||||||
|
delete[] file->m_buffer;
|
||||||
|
file->m_buffer = nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
delete file;
|
delete file;
|
||||||
|
|
||||||
@ -18,6 +28,10 @@ int32_t SFile::Close(SFile* file) {
|
|||||||
|
|
||||||
// TODO Proper implementation
|
// TODO Proper implementation
|
||||||
int32_t SFile::FileExists(const char* filename) {
|
int32_t SFile::FileExists(const char* filename) {
|
||||||
|
if (Mpq::FileExists(filename)) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
auto pathLen = SStrLen(filename);
|
auto pathLen = SStrLen(filename);
|
||||||
char path[STORM_MAX_PATH];
|
char path[STORM_MAX_PATH];
|
||||||
SStrCopy(path, filename, sizeof(path));
|
SStrCopy(path, filename, sizeof(path));
|
||||||
@ -49,43 +63,36 @@ int32_t SFile::IsStreamingTrial() {
|
|||||||
|
|
||||||
// TODO Proper implementation
|
// TODO Proper implementation
|
||||||
int32_t SFile::Load(SArchive* archive, const char* filename, void** buffer, size_t* bytes, size_t extraBytes, uint32_t flags, SOVERLAPPED* overlapped) {
|
int32_t SFile::Load(SArchive* archive, const char* filename, void** buffer, size_t* bytes, size_t extraBytes, uint32_t flags, SOVERLAPPED* overlapped) {
|
||||||
auto pathLen = SStrLen(filename);
|
SFile* file = nullptr;
|
||||||
char path[STORM_MAX_PATH];
|
|
||||||
SStrCopy(path, filename, sizeof(path));
|
|
||||||
|
|
||||||
for (int32_t i = 0; i < pathLen; ++i) {
|
if (!SFile::OpenEx(archive, filename, flags, &file) || !file) {
|
||||||
if (path[i] == '\\') {
|
|
||||||
path[i] = '/';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
std::ifstream file (path, std::ios::in | std::ios::binary | std::ios::ate);
|
|
||||||
size_t size;
|
|
||||||
char* data;
|
|
||||||
|
|
||||||
if (file.is_open()) {
|
|
||||||
size = static_cast<size_t>(file.tellg());
|
|
||||||
|
|
||||||
if (bytes) {
|
|
||||||
*bytes = size;
|
|
||||||
}
|
|
||||||
|
|
||||||
data = new char[size + extraBytes];
|
|
||||||
|
|
||||||
file.seekg(0, std::ios::beg);
|
|
||||||
file.read(data, size);
|
|
||||||
file.close();
|
|
||||||
|
|
||||||
if (extraBytes) {
|
|
||||||
memset(data + size, 0, extraBytes);
|
|
||||||
}
|
|
||||||
|
|
||||||
*buffer = data;
|
|
||||||
|
|
||||||
return 1;
|
|
||||||
} else {
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
size_t size = static_cast<size_t>(SFile::GetFileSize(file, nullptr));
|
||||||
|
|
||||||
|
if (bytes) {
|
||||||
|
*bytes = size;
|
||||||
|
}
|
||||||
|
|
||||||
|
char* data = new char[size + extraBytes];
|
||||||
|
size_t bytesRead = 0;
|
||||||
|
|
||||||
|
int32_t result = SFile::Read(file, data, size, &bytesRead, nullptr, nullptr);
|
||||||
|
SFile::Close(file);
|
||||||
|
|
||||||
|
if (!result) {
|
||||||
|
delete[] data;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (extraBytes) {
|
||||||
|
memset(data + size, 0, extraBytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
*buffer = data;
|
||||||
|
|
||||||
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
int32_t SFile::Open(const char* filename, SFile** file) {
|
int32_t SFile::Open(const char* filename, SFile** file) {
|
||||||
@ -94,6 +101,25 @@ int32_t SFile::Open(const char* filename, SFile** file) {
|
|||||||
|
|
||||||
// TODO Proper implementation
|
// TODO Proper implementation
|
||||||
int32_t SFile::OpenEx(SArchive* archive, const char* filename, uint32_t flags, SFile** file) {
|
int32_t SFile::OpenEx(SArchive* archive, const char* filename, uint32_t flags, SFile** file) {
|
||||||
|
if (!archive) {
|
||||||
|
uint8_t* data = nullptr;
|
||||||
|
size_t size = 0;
|
||||||
|
|
||||||
|
if (Mpq::ReadFile(filename, &data, &size)) {
|
||||||
|
SFile* fileptr = new SFile;
|
||||||
|
|
||||||
|
fileptr->m_filename = strdup(filename);
|
||||||
|
fileptr->m_stream = nullptr;
|
||||||
|
fileptr->m_buffer = data;
|
||||||
|
fileptr->m_offset = 0;
|
||||||
|
fileptr->m_size = static_cast<std::streamsize>(size);
|
||||||
|
|
||||||
|
*file = fileptr;
|
||||||
|
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
auto pathLen = SStrLen(filename);
|
auto pathLen = SStrLen(filename);
|
||||||
char path[STORM_MAX_PATH];
|
char path[STORM_MAX_PATH];
|
||||||
SStrCopy(path, filename, sizeof(path));
|
SStrCopy(path, filename, sizeof(path));
|
||||||
@ -107,6 +133,8 @@ int32_t SFile::OpenEx(SArchive* archive, const char* filename, uint32_t flags, S
|
|||||||
SFile* fileptr = new SFile;
|
SFile* fileptr = new SFile;
|
||||||
|
|
||||||
fileptr->m_filename = strdup(filename);
|
fileptr->m_filename = strdup(filename);
|
||||||
|
fileptr->m_buffer = nullptr;
|
||||||
|
fileptr->m_offset = 0;
|
||||||
|
|
||||||
std::ifstream* stream = new std::ifstream(path, std::ios::in | std::ios::binary | std::ios::ate);
|
std::ifstream* stream = new std::ifstream(path, std::ios::in | std::ios::binary | std::ios::ate);
|
||||||
|
|
||||||
@ -133,7 +161,20 @@ int32_t SFile::OpenEx(SArchive* archive, const char* filename, uint32_t flags, S
|
|||||||
|
|
||||||
// TODO Proper implementation
|
// TODO Proper implementation
|
||||||
int32_t SFile::Read(SFile* file, void* buffer, size_t bytestoread, size_t* bytesread, SOVERLAPPED* overlapped, TASYNCPARAMBLOCK* asyncparam) {
|
int32_t SFile::Read(SFile* file, void* buffer, size_t bytestoread, size_t* bytesread, SOVERLAPPED* overlapped, TASYNCPARAMBLOCK* asyncparam) {
|
||||||
file->m_stream->read((char*)buffer, bytestoread);
|
if (file->m_buffer) {
|
||||||
|
size_t available = static_cast<size_t>(file->m_size) - file->m_offset;
|
||||||
|
size_t toRead = std::min(bytestoread, available);
|
||||||
|
std::memcpy(buffer, file->m_buffer + file->m_offset, toRead);
|
||||||
|
file->m_offset += toRead;
|
||||||
|
|
||||||
|
if (bytesread) {
|
||||||
|
*bytesread = toRead;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
file->m_stream->read(static_cast<char*>(buffer), bytestoread);
|
||||||
|
|
||||||
if (bytesread) {
|
if (bytesread) {
|
||||||
*bytesread = file->m_stream->gcount();
|
*bytesread = file->m_stream->gcount();
|
||||||
|
|||||||
@ -1,10 +1,9 @@
|
|||||||
#ifndef UTIL_S_FILE_HPP
|
#ifndef UTIL_S_FILE_HPP
|
||||||
#define UTIL_S_FILE_HPP
|
#define UTIL_S_FILE_HPP
|
||||||
|
|
||||||
|
#include <cstddef>
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
#include <cstdlib>
|
|
||||||
#include <fstream>
|
#include <fstream>
|
||||||
#include <iostream>
|
|
||||||
|
|
||||||
class SArchive;
|
class SArchive;
|
||||||
struct SOVERLAPPED;
|
struct SOVERLAPPED;
|
||||||
@ -27,6 +26,8 @@ class SFile {
|
|||||||
// Member variables
|
// Member variables
|
||||||
const char* m_filename;
|
const char* m_filename;
|
||||||
std::ifstream* m_stream; // TODO Proper implementation
|
std::ifstream* m_stream; // TODO Proper implementation
|
||||||
|
uint8_t* m_buffer;
|
||||||
|
size_t m_offset;
|
||||||
std::streamsize m_size; // TODO Proper implementation
|
std::streamsize m_size; // TODO Proper implementation
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user