Skip to content

Commit 7395885

Browse files
committed
xbescanner: Adds mechanism to load save game icons.
1 parent 2936285 commit 7395885

File tree

12 files changed

+256
-23
lines changed

12 files changed

+256
-23
lines changed

.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

CMakeLists.txt

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,13 +34,16 @@ add_executable(${PROJECT_NAME}
3434
Includes/settingsMenu.cpp
3535
Includes/subAppRouter.cpp
3636
Includes/subsystems.cpp
37-
Includes/timing.cpp
3837
Includes/timeMenu.cpp
38+
Includes/timing.cpp
3939
Includes/videoMenu.cpp
4040
Includes/wipeCache.cpp
41+
Includes/xbeInfo.cpp
4142
Includes/xbeLauncher.cpp
4243
Includes/xbeScanner.cpp
44+
Includes/xpr0Image.cpp
4345
3rdparty/SDL_FontCache/SDL_FontCache.c
46+
3rdparty/s3tc-dxt-decompression/s3tc.cpp
4447
)
4548

4649
set_property(TARGET ${PROJECT_NAME} PROPERTY CXX_STANDARD 17)

Includes/menu.cpp

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

@@ -221,7 +222,7 @@ void MenuXbe::updateScanningLabel() {
221222
}
222223

223224
void MenuXbe::onScanCompleted(bool succeeded,
224-
std::list<XBEScanner::XBEInfo> const& items,
225+
std::list<XBEInfo> const& items,
225226
long long duration) {
226227
(void)duration;
227228
std::string path = remainingScanPaths.front();
@@ -238,8 +239,9 @@ void MenuXbe::onScanCompleted(bool succeeded,
238239
updateScanningLabel();
239240
XBEScanner::scanPath(
240241
remainingScanPaths.front(),
241-
[this](bool succeeded, std::list<XBEScanner::XBEInfo> const& items,
242-
long long duration) { this->onScanCompleted(succeeded, items, duration); });
242+
[this](bool succeeded, std::list<XBEInfo> const& items, long long duration) {
243+
this->onScanCompleted(succeeded, items, duration);
244+
});
243245
return;
244246
}
245247

@@ -250,7 +252,9 @@ void MenuXbe::createChildren() {
250252
std::vector<std::shared_ptr<MenuItem>> newChildren;
251253

252254
for (auto& info: discoveredItems) {
253-
newChildren.push_back(std::make_shared<MenuLaunch>(info.name, info.path));
255+
XPR0Image saveIcon;
256+
info.loadCompressedSaveGameIcon(saveIcon);
257+
newChildren.push_back(std::make_shared<MenuLaunch>(info.title, info.path, saveIcon));
254258
}
255259

256260
std::sort(begin(newChildren), end(newChildren),
@@ -286,8 +290,12 @@ void MenuXbe::createChildren() {
286290
/******************************************************************************************
287291
MenuLaunch
288292
******************************************************************************************/
289-
MenuLaunch::MenuLaunch(std::string const& label, std::string const& path) :
290-
MenuItem(label), path(path) {
293+
MenuLaunch::MenuLaunch(std::string const& label, std::string path) :
294+
MenuItem(label), path(std::move(path)), image() {
295+
}
296+
297+
MenuLaunch::MenuLaunch(std::string const& label, std::string path, XPR0Image image) :
298+
MenuItem(label), path(std::move(path)), image(std::move(image)) {
291299
}
292300

293301
MenuLaunch::~MenuLaunch() {

Includes/menu.hpp

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

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

8584
std::mutex childNodesLock;
8685
std::list<std::string> remainingScanPaths;
87-
std::vector<XBEScanner::XBEInfo> discoveredItems;
86+
std::vector<XBEInfo> discoveredItems;
8887

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

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

100100
protected:
101101
std::string path;
102+
XPR0Image image;
102103
};
103104

104105
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: 46 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,12 @@
1111
#define XBE_TYPE_MAGIC (0x48454258)
1212
#define SECTORSIZE 0x1000
1313

14-
static bool scan(std::string const& path, std::vector<XBEScanner::XBEInfo>& ret);
14+
#ifdef NXDK
15+
static std::pair<DWORD, DWORD> getSaveImageFileOffset(FILE* file,
16+
DWORD imageBase,
17+
PXBE_SECTION_HEADER firstSectionHeader,
18+
DWORD numberOfSections);
19+
#endif
1520

1621
XBEScanner* XBEScanner::singleton = nullptr;
1722

@@ -163,8 +168,47 @@ void XBEScanner::QueueItem::processFile(const std::string& xbePath) {
163168
if (!strlen(xbeName)) {
164169
strncpy(xbeName, findData.cFileName, sizeof(xbeName) - 1);
165170
}
171+
172+
auto firstSectionHeader = reinterpret_cast<PXBE_SECTION_HEADER>(
173+
xbeData.data() + (DWORD)xbe->PointerToSectionTable - xbe->ImageBase);
174+
std::pair<int, int> saveImageInfo = getSaveImageFileOffset(
175+
xbeFile, xbe->ImageBase, firstSectionHeader, xbe->NumberOfSections);
176+
166177
fclose(xbeFile);
167178

168-
results.emplace_back(xbeName, xbePath);
179+
results.emplace_back(xbeName, xbePath, saveImageInfo.first, saveImageInfo.second);
169180
#endif // #ifdef NXDK
170181
}
182+
183+
#ifdef NXDK
184+
// Retrieves the FileAddress and FileSize members of the "$$XTIMAGE" section, which points
185+
// to an XPR0 compressed icon for save games.
186+
//
187+
// NOTE: This will seek within the given file, if it is important to maintain the current
188+
// read position it should be saved before calling this function.
189+
static std::pair<DWORD, DWORD> getSaveImageFileOffset(FILE* file,
190+
DWORD imageBase,
191+
PXBE_SECTION_HEADER firstSectionHeader,
192+
DWORD numberOfSections) {
193+
static const char SAVE_IMAGE_SECTION_NAME[] = "$$XTIMAGE";
194+
static const int SECTION_NAME_SIZE = sizeof(SAVE_IMAGE_SECTION_NAME);
195+
196+
char nameBuffer[SECTION_NAME_SIZE] = { 0 };
197+
for (DWORD i = 0; i < numberOfSections; ++i) {
198+
PXBE_SECTION_HEADER header = firstSectionHeader + i;
199+
long nameOffset = reinterpret_cast<long>(header->SectionName) - imageBase;
200+
fseek(file, nameOffset, SEEK_SET);
201+
size_t read_bytes = fread(nameBuffer, 1, SECTION_NAME_SIZE, file);
202+
if (read_bytes != SECTION_NAME_SIZE) {
203+
return std::make_pair(-1, -1);
204+
}
205+
206+
if (nameBuffer[SECTION_NAME_SIZE - 1] == 0
207+
&& !strcmp(nameBuffer, SAVE_IMAGE_SECTION_NAME)) {
208+
return std::make_pair(header->FileAddress, header->FileSize);
209+
}
210+
}
211+
212+
return std::make_pair(-1, -1);
213+
}
214+
#endif // #ifdef NXDK

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
#ifdef NXDK
1415
#include <windows.h>
@@ -24,12 +25,6 @@
2425
// direct subdirectories containing XBE files.
2526
class XBEScanner {
2627
public:
27-
struct XBEInfo {
28-
XBEInfo(std::string n, std::string p) : name(std::move(n)), path(std::move(p)) {}
29-
std::string name;
30-
std::string path;
31-
};
32-
3328
// (bool succeeded, std::list<XBEInfo> const& xbes, long long scanDuration)
3429
typedef std::function<void(bool, std::list<XBEInfo> const&, long long)> Callback;
3530

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+
}

0 commit comments

Comments
 (0)