Skip to content

Commit 9130453

Browse files
committed
xbescanner: Adds mechanism to load save game icons.
1 parent 33ecacf commit 9130453

11 files changed

Lines changed: 249 additions & 23 deletions

File tree

.gitmodules

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,6 @@
44
[submodule "3rdparty/NaturalSort"]
55
path = 3rdparty/NaturalSort
66
url = https://github.com/scopeInfinity/NaturalSort.git
7+
[submodule "3rdparty/s3tc-dxt-decompression"]
8+
path = 3rdparty/s3tc-dxt-decompression
9+
url = https://github.com/Benjamin-Dobell/s3tc-dxt-decompression.git

3rdparty/s3tc-dxt-decompression

Submodule s3tc-dxt-decompression added at 17074c2

Includes/menu.cpp

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -156,8 +156,9 @@ MenuXbe::MenuXbe(MenuNode* parent, std::string const& label, std::string const&
156156
updateScanningLabel();
157157
XBEScanner::scanPath(
158158
remainingScanPaths.front(),
159-
[this](bool succeeded, std::list<XBEScanner::XBEInfo> const& items,
160-
long long duration) { this->onScanCompleted(succeeded, items, duration); });
159+
[this](bool succeeded, std::list<XBEInfo> const& items, long long duration) {
160+
this->onScanCompleted(succeeded, items, duration);
161+
});
161162
}
162163
}
163164

@@ -220,7 +221,7 @@ void MenuXbe::updateScanningLabel() {
220221
}
221222

222223
void MenuXbe::onScanCompleted(bool succeeded,
223-
std::list<XBEScanner::XBEInfo> const& items,
224+
std::list<XBEInfo> const& items,
224225
long long duration) {
225226
(void)duration;
226227
std::string path = remainingScanPaths.front();
@@ -237,8 +238,9 @@ void MenuXbe::onScanCompleted(bool succeeded,
237238
updateScanningLabel();
238239
XBEScanner::scanPath(
239240
remainingScanPaths.front(),
240-
[this](bool succeeded, std::list<XBEScanner::XBEInfo> const& items,
241-
long long duration) { this->onScanCompleted(succeeded, items, duration); });
241+
[this](bool succeeded, std::list<XBEInfo> const& items, long long duration) {
242+
this->onScanCompleted(succeeded, items, duration);
243+
});
242244
return;
243245
}
244246

@@ -249,7 +251,9 @@ void MenuXbe::createChildren() {
249251
std::vector<std::shared_ptr<MenuItem>> newChildren;
250252

251253
for (auto& info: discoveredItems) {
252-
newChildren.push_back(std::make_shared<MenuLaunch>(info.name, info.path));
254+
XPR0Image saveIcon;
255+
info.loadCompressedSaveGameIcon(saveIcon);
256+
newChildren.push_back(std::make_shared<MenuLaunch>(info.title, info.path, saveIcon));
253257
}
254258

255259
std::sort(begin(newChildren), end(newChildren),
@@ -285,8 +289,12 @@ void MenuXbe::createChildren() {
285289
/******************************************************************************************
286290
MenuLaunch
287291
******************************************************************************************/
288-
MenuLaunch::MenuLaunch(std::string const& label, std::string const& path) :
289-
MenuItem(label), path(path) {
292+
MenuLaunch::MenuLaunch(std::string const& label, std::string path) :
293+
MenuItem(label), path(std::move(path)), image() {
294+
}
295+
296+
MenuLaunch::MenuLaunch(std::string const& label, std::string path, XPR0Image image) :
297+
MenuItem(label), path(std::move(path)), image(std::move(image)) {
290298
}
291299

292300
MenuLaunch::~MenuLaunch() {

Includes/menu.hpp

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
#include "config.hpp"
99
#include "font.h"
1010
#include "subApp.h"
11+
#include "xbeInfo.h"
1112
#include "xbeScanner.h"
1213

1314
class MenuNode;
@@ -76,14 +77,12 @@ class MenuXbe : public MenuNode {
7677
private:
7778
void superscroll(bool moveToPrevious);
7879
void updateScanningLabel();
79-
void onScanCompleted(bool succeeded,
80-
std::list<XBEScanner::XBEInfo> const& items,
81-
long long duration);
80+
void onScanCompleted(bool succeeded, std::list<XBEInfo> const& items, long long duration);
8281
void createChildren();
8382

8483
std::mutex childNodesLock;
8584
std::list<std::string> remainingScanPaths;
86-
std::vector<XBEScanner::XBEInfo> discoveredItems;
85+
std::vector<XBEInfo> discoveredItems;
8786

8887
// Map of first letter to index of the first child in childNodes whose label starts with
8988
// that letter.
@@ -92,12 +91,14 @@ class MenuXbe : public MenuNode {
9291

9392
class MenuLaunch : public MenuItem {
9493
public:
95-
MenuLaunch(std::string const& label, std::string const& path);
94+
MenuLaunch(std::string const& label, std::string path);
95+
MenuLaunch(std::string const& label, std::string path, XPR0Image image);
9696
~MenuLaunch() override;
9797
void execute(Menu*) override;
9898

9999
protected:
100100
std::string path;
101+
XPR0Image image;
101102
};
102103

103104
class MenuExec : public MenuItem {

Includes/xbeInfo.cpp

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
#include "xbeInfo.h"
2+
#include "infoLog.h"
3+
4+
XBEInfo::Icon XBEInfo::loadSaveGameIcon() const {
5+
Icon ret;
6+
if (saveGameXPROffset <= 0 || saveGameXPRSize <= 0) {
7+
return ret;
8+
}
9+
10+
XPR0Image compressedImage;
11+
if (!loadCompressedSaveGameIcon(compressedImage)) {
12+
InfoLog::outputLine("Failed to load save game icon from %s", path.c_str());
13+
return ret;
14+
}
15+
16+
if (!compressedImage.decompress(ret.imageData)) {
17+
InfoLog::outputLine("Failed to decompress save game icon from %s", path.c_str());
18+
return ret;
19+
}
20+
21+
ret.width = compressedImage.width;
22+
ret.height = compressedImage.height;
23+
24+
return ret;
25+
}
26+
27+
bool XBEInfo::loadCompressedSaveGameIcon(XPR0Image& image) const {
28+
image.clear();
29+
FILE* xbeFile = fopen(path.c_str(), "rb");
30+
if (!xbeFile) {
31+
return false;
32+
}
33+
34+
fseek(xbeFile, saveGameXPROffset, SEEK_SET);
35+
std::vector<uint8_t> buffer(saveGameXPRSize);
36+
size_t bytesRead = fread(buffer.data(), 1, saveGameXPRSize, xbeFile);
37+
fclose(xbeFile);
38+
39+
if (bytesRead != saveGameXPRSize) {
40+
InfoLog::outputLine("Failed to read save game image from %s", path.c_str());
41+
return false;
42+
}
43+
44+
return image.parse(buffer);
45+
}

Includes/xbeInfo.h

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
#ifndef NEVOLUTIONX_XBEINFO_H
2+
#define NEVOLUTIONX_XBEINFO_H
3+
4+
#include <string>
5+
#include <vector>
6+
#include "xpr0Image.h"
7+
8+
class XBEInfo {
9+
public:
10+
// TODO: See if the DXT1 compressed image can be used directly by the hardware instead.
11+
struct Icon {
12+
// imageData is always 32-bit color.
13+
std::vector<unsigned char> imageData;
14+
uint32_t width{ 0 };
15+
uint32_t height{ 0 };
16+
};
17+
18+
XBEInfo(std::string xbeTitle, std::string xbePath, long xprOffset, size_t xprSize) :
19+
title(std::move(xbeTitle)), path(std::move(xbePath)), saveGameXPROffset(xprOffset),
20+
saveGameXPRSize(xprSize) {}
21+
22+
Icon loadSaveGameIcon() const;
23+
bool loadCompressedSaveGameIcon(XPR0Image& image) const;
24+
25+
std::string title;
26+
std::string path;
27+
28+
private:
29+
long saveGameXPROffset{ 0 };
30+
size_t saveGameXPRSize{ 0 };
31+
};
32+
33+
#endif // NEVOLUTIONX_XBEINFO_H

Includes/xbeScanner.cpp

Lines changed: 43 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,10 @@
77
#define XBE_TYPE_MAGIC (0x48454258)
88
#define SECTORSIZE 0x1000
99

10-
static bool scan(std::string const& path, std::vector<XBEScanner::XBEInfo>& ret);
10+
static std::pair<DWORD, DWORD> getSaveImageFileOffset(FILE* file,
11+
DWORD imageBase,
12+
PXBE_SECTION_HEADER firstSectionHeader,
13+
DWORD numberOfSections);
1114

1215
XBEScanner* XBEScanner::singleton = nullptr;
1316

@@ -150,7 +153,44 @@ void XBEScanner::QueueItem::processFile(const std::string& xbePath) {
150153
if (!strlen(xbeName)) {
151154
strncpy(xbeName, findData.cFileName, sizeof(xbeName) - 1);
152155
}
156+
157+
auto firstSectionHeader = reinterpret_cast<PXBE_SECTION_HEADER>(
158+
xbeData.data() + (DWORD)xbe->PointerToSectionTable - xbe->ImageBase);
159+
std::pair<int, int> saveImageInfo = getSaveImageFileOffset(
160+
xbeFile, xbe->ImageBase, firstSectionHeader, xbe->NumberOfSections);
161+
153162
fclose(xbeFile);
154163

155-
results.emplace_back(xbeName, xbePath);
156-
}
164+
results.emplace_back(xbeName, xbePath, saveImageInfo.first, saveImageInfo.second);
165+
}
166+
167+
// Retrieves the FileAddress and FileSize members of the "$$XTIMAGE" section, which points
168+
// to an XPR0 compressed icon for save games.
169+
//
170+
// NOTE: This will seek within the given file, if it is important to maintain the current
171+
// read position it should be saved before calling this function.
172+
static std::pair<DWORD, DWORD> getSaveImageFileOffset(FILE* file,
173+
DWORD imageBase,
174+
PXBE_SECTION_HEADER firstSectionHeader,
175+
DWORD numberOfSections) {
176+
static const char SAVE_IMAGE_SECTION_NAME[] = "$$XTIMAGE";
177+
static const int SECTION_NAME_SIZE = sizeof(SAVE_IMAGE_SECTION_NAME);
178+
179+
char nameBuffer[SECTION_NAME_SIZE] = { 0 };
180+
for (DWORD i = 0; i < numberOfSections; ++i) {
181+
PXBE_SECTION_HEADER header = firstSectionHeader + i;
182+
long nameOffset = reinterpret_cast<long>(header->SectionName) - imageBase;
183+
fseek(file, nameOffset, SEEK_SET);
184+
size_t read_bytes = fread(nameBuffer, 1, SECTION_NAME_SIZE, file);
185+
if (read_bytes != SECTION_NAME_SIZE) {
186+
return std::make_pair(-1, -1);
187+
}
188+
189+
if (nameBuffer[SECTION_NAME_SIZE - 1] == 0
190+
&& !strcmp(nameBuffer, SAVE_IMAGE_SECTION_NAME)) {
191+
return std::make_pair(header->FileAddress, header->FileSize);
192+
}
193+
}
194+
195+
return std::make_pair(-1, -1);
196+
}

Includes/xbeScanner.h

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
#include <thread>
1010
#include <utility>
1111
#include <vector>
12+
#include "xbeInfo.h"
1213

1314
// TODO(#110): Reenable threading once hardware accelerated rendering is in place.
1415
// The current software-backed SDL approach causes the scanner thread to be starved, leading
@@ -20,12 +21,6 @@
2021
// direct subdirectories containing XBE files.
2122
class XBEScanner {
2223
public:
23-
struct XBEInfo {
24-
XBEInfo(std::string n, std::string p) : name(std::move(n)), path(std::move(p)) {}
25-
std::string name;
26-
std::string path;
27-
};
28-
2924
// (bool succeeded, std::list<XBEInfo> const& xbes, long long scanDuration)
3025
typedef std::function<void(bool, std::list<XBEInfo> const&, long long)> Callback;
3126

Includes/xpr0Image.cpp

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
#include "xpr0Image.h"
2+
#include "3rdparty/s3tc-dxt-decompression/s3tc.h"
3+
#include "infoLog.h"
4+
5+
static const uint32_t XPR0_MAGIC = 0x30525058;
6+
7+
bool XPR0Image::parse(const std::vector<uint8_t>& buffer) {
8+
auto& header = *reinterpret_cast<XPRHeader const*>(buffer.data());
9+
if (header.magic != XPR0_MAGIC) {
10+
InfoLog::outputLine("Unexpected magic bytes %X in XPR0", header.magic);
11+
return false;
12+
}
13+
14+
static const uint32_t FORMAT_MASK = 0x0000FF00;
15+
format = header.resourceInfo.format & FORMAT_MASK;
16+
17+
static const uint32_t FORMAT_DXT1 = 0x00000C00;
18+
// TODO: Investigate whether formats other than DXT1 are ever used.
19+
if (format != FORMAT_DXT1) {
20+
InfoLog::outputLine("Unexpected format %X (!=DXT1) in XPR0", header.resourceInfo.format);
21+
return false;
22+
}
23+
24+
uint32_t dataSize = header.totalSize - header.headerSize;
25+
if (dataSize > buffer.size()) {
26+
InfoLog::outputLine("Buffer size too small (%u < %u) in XPR0", buffer.size(), dataSize);
27+
}
28+
29+
static const uint32_t UV_SIZE_MASK = 0x0FF00000;
30+
static const uint32_t U_SHIFT = 20;
31+
static const uint32_t V_SHIFT = 24;
32+
const uint32_t sizeInfo = header.resourceInfo.format & UV_SIZE_MASK;
33+
width = 1 << ((sizeInfo >> U_SHIFT) & 0x0F);
34+
height = 1 << ((sizeInfo >> V_SHIFT) & 0x0F);
35+
36+
auto imageDataStart = buffer.cbegin() + static_cast<int>(header.headerSize);
37+
imageData = std::vector<uint8_t>(imageDataStart, buffer.cend());
38+
39+
return true;
40+
}
41+
42+
bool XPR0Image::decompress(std::vector<uint8_t>& output) const {
43+
output.resize(width * height * 4);
44+
return decompress(output.data());
45+
}
46+
47+
bool XPR0Image::decompress(uint8_t* output) const {
48+
BlockDecompressImageDXT1(width, height, imageData.data(), (unsigned long*)output);
49+
return true;
50+
}

Includes/xpr0Image.h

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
#ifndef NEVOLUTIONX_XPR0IMAGE_H
2+
#define NEVOLUTIONX_XPR0IMAGE_H
3+
4+
#include <vector>
5+
6+
// Encapsulates information about an XPR0 image.
7+
class XPR0Image {
8+
public:
9+
struct ResourceInfo {
10+
uint32_t common;
11+
uint32_t data;
12+
uint32_t lock;
13+
uint32_t format;
14+
uint32_t size;
15+
};
16+
17+
struct XPRHeader {
18+
uint32_t magic;
19+
uint32_t totalSize;
20+
uint32_t headerSize;
21+
ResourceInfo resourceInfo;
22+
uint32_t endOfHeader; // Should always == 0xFFFFFFFF
23+
};
24+
25+
// Populates this XPR0Image from the given data buffer.
26+
bool parse(std::vector<uint8_t> const& buffer);
27+
28+
// Copies 32bpp decompressed image data into the given `output` buffer.
29+
//
30+
// Returns true if the operation succeded, false if there was an error.
31+
bool decompress(std::vector<uint8_t>& output) const;
32+
33+
bool decompress(uint8_t* output) const;
34+
35+
void clear() {
36+
width = height = format = 0;
37+
imageData.clear();
38+
}
39+
40+
uint32_t width{ 0 };
41+
uint32_t height{ 0 };
42+
uint32_t format;
43+
std::vector<uint8_t> imageData;
44+
};
45+
46+
47+
#endif // NEVOLUTIONX_XPR0IMAGE_H

0 commit comments

Comments
 (0)