Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

(Blocked on #110) xbescanner: Adds mechanism to load save game icons. #124

Draft
wants to merge 1 commit into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,6 @@
[submodule "3rdparty/NaturalSort"]
path = 3rdparty/NaturalSort
url = https://github.com/scopeInfinity/NaturalSort.git
[submodule "3rdparty/s3tc-dxt-decompression"]
path = 3rdparty/s3tc-dxt-decompression
url = https://github.com/Benjamin-Dobell/s3tc-dxt-decompression.git
1 change: 1 addition & 0 deletions 3rdparty/s3tc-dxt-decompression
Submodule s3tc-dxt-decompression added at 17074c
5 changes: 4 additions & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -35,13 +35,16 @@ add_executable(${PROJECT_NAME}
Includes/sntpClient.cpp
Includes/subAppRouter.cpp
Includes/subsystems.cpp
Includes/timing.cpp
Includes/timeMenu.cpp
Includes/timing.cpp
Includes/videoMenu.cpp
Includes/wipeCache.cpp
Includes/xbeInfo.cpp
Includes/xbeLauncher.cpp
Includes/xbeScanner.cpp
Includes/xpr0Image.cpp
3rdparty/SDL_FontCache/SDL_FontCache.c
3rdparty/s3tc-dxt-decompression/s3tc.cpp
)

set_property(TARGET ${PROJECT_NAME} PROPERTY CXX_STANDARD 17)
Expand Down
24 changes: 16 additions & 8 deletions Includes/menu.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -157,8 +157,9 @@ MenuXbe::MenuXbe(MenuNode* parent, std::string const& label, std::string const&
updateScanningLabel();
XBEScanner::scanPath(
remainingScanPaths.front(),
[this](bool succeeded, std::list<XBEScanner::XBEInfo> const& items,
long long duration) { this->onScanCompleted(succeeded, items, duration); });
[this](bool succeeded, std::list<XBEInfo> const& items, long long duration) {
this->onScanCompleted(succeeded, items, duration);
});
}
}

Expand Down Expand Up @@ -221,7 +222,7 @@ void MenuXbe::updateScanningLabel() {
}

void MenuXbe::onScanCompleted(bool succeeded,
std::list<XBEScanner::XBEInfo> const& items,
std::list<XBEInfo> const& items,
long long duration) {
(void)duration;
std::string path = remainingScanPaths.front();
Expand All @@ -239,8 +240,9 @@ void MenuXbe::onScanCompleted(bool succeeded,
updateScanningLabel();
XBEScanner::scanPath(
remainingScanPaths.front(),
[this](bool succeeded, std::list<XBEScanner::XBEInfo> const& items,
long long duration) { this->onScanCompleted(succeeded, items, duration); });
[this](bool succeeded, std::list<XBEInfo> const& items, long long duration) {
this->onScanCompleted(succeeded, items, duration);
});
return;
}

Expand All @@ -251,7 +253,9 @@ void MenuXbe::createChildren() {
std::vector<std::shared_ptr<MenuItem>> newChildren;

for (auto& info: discoveredItems) {
newChildren.push_back(std::make_shared<MenuLaunch>(info.name, info.path));
XPR0Image saveIcon;
info.loadCompressedSaveGameIcon(saveIcon);
newChildren.push_back(std::make_shared<MenuLaunch>(info.title, info.path, saveIcon));
}

std::sort(begin(newChildren), end(newChildren),
Expand Down Expand Up @@ -288,8 +292,12 @@ void MenuXbe::createChildren() {
/******************************************************************************************
MenuLaunch
******************************************************************************************/
MenuLaunch::MenuLaunch(std::string const& label, std::string const& path) :
MenuItem(label), path(path) {
MenuLaunch::MenuLaunch(std::string const& label, std::string path) :
MenuItem(label), path(std::move(path)), image() {
}

MenuLaunch::MenuLaunch(std::string const& label, std::string path, XPR0Image image) :
MenuItem(label), path(std::move(path)), image(std::move(image)) {
}

MenuLaunch::~MenuLaunch() {
Expand Down
11 changes: 6 additions & 5 deletions Includes/menu.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
#include "config.hpp"
#include "font.h"
#include "subApp.h"
#include "xbeInfo.h"
#include "xbeScanner.h"

class MenuNode;
Expand Down Expand Up @@ -77,14 +78,12 @@ class MenuXbe : public MenuNode {
private:
void superscroll(bool moveToPrevious);
void updateScanningLabel();
void onScanCompleted(bool succeeded,
std::list<XBEScanner::XBEInfo> const& items,
long long duration);
void onScanCompleted(bool succeeded, std::list<XBEInfo> const& items, long long duration);
void createChildren();

std::mutex childNodesLock;
std::list<std::string> remainingScanPaths;
std::vector<XBEScanner::XBEInfo> discoveredItems;
std::vector<XBEInfo> discoveredItems;

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

class MenuLaunch : public MenuItem {
public:
MenuLaunch(std::string const& label, std::string const& path);
MenuLaunch(std::string const& label, std::string path);
MenuLaunch(std::string const& label, std::string path, XPR0Image image);
~MenuLaunch() override;
void execute(Menu*) override;

protected:
std::string path;
XPR0Image image;
};

class MenuExec : public MenuItem {
Expand Down
6 changes: 4 additions & 2 deletions Includes/sntpClient.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,8 @@ void sntpClient::updateTime() const {
message.originTimestamp.fractionalSeconds =
htonl(message.originTimestamp.fractionalSeconds);
if (message.originTimestamp.seconds || message.originTimestamp.fractionalSeconds) {
InfoLog::outputLine(InfoLog::INFO, "SNTP: Origin epoch is not 0: %lld\n", message.originTimestamp);
InfoLog::outputLine(InfoLog::INFO, "SNTP: Origin epoch is not 0: %lld\n",
message.originTimestamp);
}

message.transmitTimestamp.seconds = htonl(message.transmitTimestamp.seconds);
Expand All @@ -95,7 +96,8 @@ void sntpClient::updateTime() const {
}

if (delta > allowedDriftSeconds) {
InfoLog::outputLine(InfoLog::DEBUG, "SNTP: Updating system clock (%llu seconds of drift)\n", delta);
InfoLog::outputLine(InfoLog::DEBUG,
"SNTP: Updating system clock (%llu seconds of drift)\n", delta);
NTSTATUS status = NtSetSystemTime(&serverTime, nullptr);
if (!NT_SUCCESS(status)) {
InfoLog::outputLine(InfoLog::INFO, "SNTP: NtSetSystemTime failed: %X\n", status);
Expand Down
48 changes: 48 additions & 0 deletions Includes/xbeInfo.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
#include "xbeInfo.h"
#include "infoLog.h"

XBEInfo::Icon XBEInfo::loadSaveGameIcon() const {
Icon ret;
if (saveGameXPROffset <= 0 || saveGameXPRSize <= 0) {
return ret;
}

XPR0Image compressedImage;
if (!loadCompressedSaveGameIcon(compressedImage)) {
InfoLog::outputLine(InfoLog::WARNING, "Failed to load save game icon from %s",
path.c_str());
return ret;
}

if (!compressedImage.decompress(ret.imageData)) {
InfoLog::outputLine(InfoLog::WARNING, "Failed to decompress save game icon from %s",
path.c_str());
return ret;
}

ret.width = compressedImage.width;
ret.height = compressedImage.height;

return ret;
}

bool XBEInfo::loadCompressedSaveGameIcon(XPR0Image& image) const {
image.clear();
FILE* xbeFile = fopen(path.c_str(), "rb");
if (!xbeFile) {
return false;
}

fseek(xbeFile, saveGameXPROffset, SEEK_SET);
std::vector<uint8_t> buffer(saveGameXPRSize);
size_t bytesRead = fread(buffer.data(), 1, saveGameXPRSize, xbeFile);
fclose(xbeFile);

if (bytesRead != saveGameXPRSize) {
InfoLog::outputLine(InfoLog::WARNING, "Failed to read save game image from %s",
path.c_str());
return false;
}

return image.parse(buffer);
}
33 changes: 33 additions & 0 deletions Includes/xbeInfo.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
#ifndef NEVOLUTIONX_XBEINFO_H
#define NEVOLUTIONX_XBEINFO_H

#include <string>
#include <vector>
#include "xpr0Image.h"

class XBEInfo {
public:
// TODO: See if the DXT1 compressed image can be used directly by the hardware instead.
struct Icon {
// imageData is always 32-bit color.
std::vector<unsigned char> imageData;
uint32_t width{ 0 };
uint32_t height{ 0 };
};

XBEInfo(std::string xbeTitle, std::string xbePath, long xprOffset, size_t xprSize) :
title(std::move(xbeTitle)), path(std::move(xbePath)), saveGameXPROffset(xprOffset),
saveGameXPRSize(xprSize) {}

Icon loadSaveGameIcon() const;
bool loadCompressedSaveGameIcon(XPR0Image& image) const;

std::string title;
std::string path;

private:
long saveGameXPROffset{ 0 };
size_t saveGameXPRSize{ 0 };
};

#endif // NEVOLUTIONX_XBEINFO_H
48 changes: 47 additions & 1 deletion Includes/xbeScanner.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,13 @@
#define XBE_TYPE_MAGIC (0x48454258)
#define SECTORSIZE 0x1000

#ifdef NXDK
static std::pair<DWORD, DWORD> getSaveImageFileOffset(FILE* file,
DWORD imageBase,
PXBE_SECTION_HEADER firstSectionHeader,
DWORD numberOfSections);
#endif

XBEScanner* XBEScanner::singleton = nullptr;

XBEScanner* XBEScanner::getInstance() {
Expand Down Expand Up @@ -165,8 +172,47 @@ void XBEScanner::QueueItem::processFile(const std::string& xbePath) {
if (!strlen(xbeName)) {
strncpy(xbeName, findData.cFileName, sizeof(xbeName) - 1);
}

auto firstSectionHeader = reinterpret_cast<PXBE_SECTION_HEADER>(
xbeData.data() + (DWORD)xbe->PointerToSectionTable - xbe->ImageBase);
std::pair<int, int> saveImageInfo = getSaveImageFileOffset(
xbeFile, xbe->ImageBase, firstSectionHeader, xbe->NumberOfSections);

fclose(xbeFile);

results.emplace_back(xbeName, xbePath);
results.emplace_back(xbeName, xbePath, saveImageInfo.first, saveImageInfo.second);
#endif // #ifdef NXDK
}

#ifdef NXDK
// Retrieves the FileAddress and FileSize members of the "$$XTIMAGE" section, which points
// to an XPR0 compressed icon for save games.
//
// NOTE: This will seek within the given file, if it is important to maintain the current
// read position it should be saved before calling this function.
static std::pair<DWORD, DWORD> getSaveImageFileOffset(FILE* file,
DWORD imageBase,
PXBE_SECTION_HEADER firstSectionHeader,
DWORD numberOfSections) {
static const char SAVE_IMAGE_SECTION_NAME[] = "$$XTIMAGE";
static const int SECTION_NAME_SIZE = sizeof(SAVE_IMAGE_SECTION_NAME);

char nameBuffer[SECTION_NAME_SIZE] = { 0 };
for (DWORD i = 0; i < numberOfSections; ++i) {
PXBE_SECTION_HEADER header = firstSectionHeader + i;
long nameOffset = reinterpret_cast<long>(header->SectionName) - imageBase;
fseek(file, nameOffset, SEEK_SET);
size_t read_bytes = fread(nameBuffer, 1, SECTION_NAME_SIZE, file);
if (read_bytes != SECTION_NAME_SIZE) {
return std::make_pair(-1, -1);
}

if (nameBuffer[SECTION_NAME_SIZE - 1] == 0
&& !strcmp(nameBuffer, SAVE_IMAGE_SECTION_NAME)) {
return std::make_pair(header->FileAddress, header->FileSize);
}
}

return std::make_pair(-1, -1);
}
#endif // #ifdef NXDK
7 changes: 1 addition & 6 deletions Includes/xbeScanner.h
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
#include <thread>
#include <utility>
#include <vector>
#include "xbeInfo.h"

#ifdef NXDK
#include <windows.h>
Expand All @@ -24,12 +25,6 @@
// direct subdirectories containing XBE files.
class XBEScanner {
public:
struct XBEInfo {
XBEInfo(std::string n, std::string p) : name(std::move(n)), path(std::move(p)) {}
std::string name;
std::string path;
};

// (bool succeeded, std::list<XBEInfo> const& xbes, long long scanDuration)
typedef std::function<void(bool, std::list<XBEInfo> const&, long long)> Callback;

Expand Down
52 changes: 52 additions & 0 deletions Includes/xpr0Image.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
#include "xpr0Image.h"
#include "3rdparty/s3tc-dxt-decompression/s3tc.h"
#include "infoLog.h"

static const uint32_t XPR0_MAGIC = 0x30525058;

bool XPR0Image::parse(const std::vector<uint8_t>& buffer) {
auto& header = *reinterpret_cast<XPRHeader const*>(buffer.data());
if (header.magic != XPR0_MAGIC) {
InfoLog::outputLine(InfoLog::ERROR, "Unexpected magic bytes %X in XPR0", header.magic);
return false;
}

static const uint32_t FORMAT_MASK = 0x0000FF00;
format = header.resourceInfo.format & FORMAT_MASK;

static const uint32_t FORMAT_DXT1 = 0x00000C00;
// TODO: Investigate whether formats other than DXT1 are ever used.
if (format != FORMAT_DXT1) {
InfoLog::outputLine(InfoLog::ERROR, "Unexpected format %X (!=DXT1) in XPR0",
header.resourceInfo.format);
return false;
}

uint32_t dataSize = header.totalSize - header.headerSize;
if (dataSize > buffer.size()) {
InfoLog::outputLine(InfoLog::ERROR, "Buffer size too small (%u < %u) in XPR0",
buffer.size(), dataSize);
}

static const uint32_t UV_SIZE_MASK = 0x0FF00000;
static const uint32_t U_SHIFT = 20;
static const uint32_t V_SHIFT = 24;
const uint32_t sizeInfo = header.resourceInfo.format & UV_SIZE_MASK;
width = 1 << ((sizeInfo >> U_SHIFT) & 0x0F);
height = 1 << ((sizeInfo >> V_SHIFT) & 0x0F);

auto imageDataStart = buffer.cbegin() + static_cast<int>(header.headerSize);
imageData = std::vector<uint8_t>(imageDataStart, buffer.cend());

return true;
}

bool XPR0Image::decompress(std::vector<uint8_t>& output) const {
output.resize(width * height * 4);
return decompress(output.data());
}

bool XPR0Image::decompress(uint8_t* output) const {
BlockDecompressImageDXT1(width, height, imageData.data(), (unsigned long*)output);
return true;
}
Loading