Merge branch 'main' into metal

This commit is contained in:
SamoZ256 2024-11-19 19:23:35 +01:00 committed by GitHub
commit cabf56851e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
26 changed files with 2445 additions and 606 deletions

View File

@ -1,4 +1,4 @@
name: Deploy experimental release
name: Deploy release
on:
workflow_dispatch:
inputs:
@ -54,7 +54,7 @@ jobs:
next_version_major: ${{ needs.calculate-version.outputs.next_version_major }}
next_version_minor: ${{ needs.calculate-version.outputs.next_version_minor }}
deploy:
name: Deploy experimental release
name: Deploy release
runs-on: ubuntu-22.04
needs: [call-release-build, calculate-version]
steps:

View File

@ -1,85 +0,0 @@
name: Create new release
on:
workflow_dispatch:
inputs:
PlaceholderInput:
description: PlaceholderInput
required: false
jobs:
call-release-build:
uses: ./.github/workflows/build.yml
with:
deploymode: release
deploy:
name: Deploy release
runs-on: ubuntu-20.04
needs: call-release-build
steps:
- uses: actions/checkout@v3
- uses: actions/download-artifact@v4
with:
name: cemu-bin-linux-x64
path: cemu-bin-linux-x64
- uses: actions/download-artifact@v4
with:
name: cemu-appimage-x64
path: cemu-appimage-x64
- uses: actions/download-artifact@v4
with:
name: cemu-bin-windows-x64
path: cemu-bin-windows-x64
- uses: actions/download-artifact@v4
with:
name: cemu-bin-macos-x64
path: cemu-bin-macos-x64
- name: Initialize
run: |
mkdir upload
sudo apt update -qq
sudo apt install -y zip
- name: Get Cemu release version
run: |
gcc -o getversion .github/getversion.cpp
echo "Cemu CI version: $(./getversion)"
echo "CEMU_FOLDER_NAME=Cemu_$(./getversion)" >> $GITHUB_ENV
echo "CEMU_VERSION=$(./getversion)" >> $GITHUB_ENV
- name: Create release from windows-bin
run: |
ls ./
ls ./bin/
cp -R ./bin ./${{ env.CEMU_FOLDER_NAME }}
mv cemu-bin-windows-x64/Cemu.exe ./${{ env.CEMU_FOLDER_NAME }}/Cemu.exe
zip -9 -r upload/cemu-${{ env.CEMU_VERSION }}-windows-x64.zip ${{ env.CEMU_FOLDER_NAME }}
rm -r ./${{ env.CEMU_FOLDER_NAME }}
- name: Create appimage
run: |
VERSION=${{ env.CEMU_VERSION }}
echo "Cemu Version is $VERSION"
ls cemu-appimage-x64
mv cemu-appimage-x64/Cemu-*-x86_64.AppImage upload/Cemu-$VERSION-x86_64.AppImage
- name: Create release from ubuntu-bin
run: |
ls ./
ls ./bin/
cp -R ./bin ./${{ env.CEMU_FOLDER_NAME }}
mv cemu-bin-linux-x64/Cemu ./${{ env.CEMU_FOLDER_NAME }}/Cemu
zip -9 -r upload/cemu-${{ env.CEMU_VERSION }}-ubuntu-20.04-x64.zip ${{ env.CEMU_FOLDER_NAME }}
rm -r ./${{ env.CEMU_FOLDER_NAME }}
- name: Create release from macos-bin
run: cp cemu-bin-macos-x64/Cemu.dmg upload/cemu-${{ env.CEMU_VERSION }}-macos-12-x64.dmg
- name: Create release
run: |
wget -O ghr.tar.gz https://github.com/tcnksm/ghr/releases/download/v0.15.0/ghr_v0.15.0_linux_amd64.tar.gz
tar xvzf ghr.tar.gz; rm ghr.tar.gz
ghr_v0.15.0_linux_amd64/ghr -t ${{ secrets.GITHUB_TOKEN }} -n "Cemu ${{ env.CEMU_VERSION }}" -b "Changelog:" v${{ env.CEMU_VERSION }} ./upload

View File

@ -465,6 +465,8 @@ add_library(CemuCafe
OS/libs/nsyshid/BackendLibusb.h
OS/libs/nsyshid/BackendWindowsHID.cpp
OS/libs/nsyshid/BackendWindowsHID.h
OS/libs/nsyshid/Dimensions.cpp
OS/libs/nsyshid/Dimensions.h
OS/libs/nsyshid/Infinity.cpp
OS/libs/nsyshid/Infinity.h
OS/libs/nsyshid/Skylander.cpp

View File

@ -645,40 +645,40 @@ namespace CafeSystem
fsc_unmount("/cemuBossStorage/", FSC_PRIORITY_BASE);
}
STATUS_CODE LoadAndMountForegroundTitle(TitleId titleId)
PREPARE_STATUS_CODE LoadAndMountForegroundTitle(TitleId titleId)
{
cemuLog_log(LogType::Force, "Mounting title {:016x}", (uint64)titleId);
sGameInfo_ForegroundTitle = CafeTitleList::GetGameInfo(titleId);
if (!sGameInfo_ForegroundTitle.IsValid())
{
cemuLog_log(LogType::Force, "Mounting failed: Game meta information is either missing, inaccessible or not valid (missing or invalid .xml files in code and meta folder)");
return STATUS_CODE::UNABLE_TO_MOUNT;
return PREPARE_STATUS_CODE::UNABLE_TO_MOUNT;
}
// check base
TitleInfo& titleBase = sGameInfo_ForegroundTitle.GetBase();
if (!titleBase.IsValid())
return STATUS_CODE::UNABLE_TO_MOUNT;
return PREPARE_STATUS_CODE::UNABLE_TO_MOUNT;
if(!titleBase.ParseXmlInfo())
return STATUS_CODE::UNABLE_TO_MOUNT;
return PREPARE_STATUS_CODE::UNABLE_TO_MOUNT;
cemuLog_log(LogType::Force, "Base: {}", titleBase.GetPrintPath());
// mount base
if (!titleBase.Mount("/vol/content", "content", FSC_PRIORITY_BASE) || !titleBase.Mount(GetInternalVirtualCodeFolder(), "code", FSC_PRIORITY_BASE))
{
cemuLog_log(LogType::Force, "Mounting failed");
return STATUS_CODE::UNABLE_TO_MOUNT;
return PREPARE_STATUS_CODE::UNABLE_TO_MOUNT;
}
// check update
TitleInfo& titleUpdate = sGameInfo_ForegroundTitle.GetUpdate();
if (titleUpdate.IsValid())
{
if (!titleUpdate.ParseXmlInfo())
return STATUS_CODE::UNABLE_TO_MOUNT;
return PREPARE_STATUS_CODE::UNABLE_TO_MOUNT;
cemuLog_log(LogType::Force, "Update: {}", titleUpdate.GetPrintPath());
// mount update
if (!titleUpdate.Mount("/vol/content", "content", FSC_PRIORITY_PATCH) || !titleUpdate.Mount(GetInternalVirtualCodeFolder(), "code", FSC_PRIORITY_PATCH))
{
cemuLog_log(LogType::Force, "Mounting failed");
return STATUS_CODE::UNABLE_TO_MOUNT;
return PREPARE_STATUS_CODE::UNABLE_TO_MOUNT;
}
}
else
@ -690,20 +690,20 @@ namespace CafeSystem
// todo - support for multi-title AOC
TitleInfo& titleAOC = aocList[0];
if (!titleAOC.ParseXmlInfo())
return STATUS_CODE::UNABLE_TO_MOUNT;
return PREPARE_STATUS_CODE::UNABLE_TO_MOUNT;
cemu_assert_debug(titleAOC.IsValid());
cemuLog_log(LogType::Force, "DLC: {}", titleAOC.GetPrintPath());
// mount AOC
if (!titleAOC.Mount(fmt::format("/vol/aoc{:016x}", titleAOC.GetAppTitleId()), "content", FSC_PRIORITY_PATCH))
{
cemuLog_log(LogType::Force, "Mounting failed");
return STATUS_CODE::UNABLE_TO_MOUNT;
return PREPARE_STATUS_CODE::UNABLE_TO_MOUNT;
}
}
else
cemuLog_log(LogType::Force, "DLC: Not present");
sForegroundTitleId = titleId;
return STATUS_CODE::SUCCESS;
return PREPARE_STATUS_CODE::SUCCESS;
}
void UnmountForegroundTitle()
@ -731,7 +731,7 @@ namespace CafeSystem
}
}
STATUS_CODE SetupExecutable()
PREPARE_STATUS_CODE SetupExecutable()
{
// set rpx path from cos.xml if available
_pathToBaseExecutable = _pathToExecutable;
@ -763,7 +763,7 @@ namespace CafeSystem
}
}
LoadMainExecutable();
return STATUS_CODE::SUCCESS;
return PREPARE_STATUS_CODE::SUCCESS;
}
void SetupMemorySpace()
@ -777,7 +777,7 @@ namespace CafeSystem
memory_unmapForCurrentTitle();
}
STATUS_CODE PrepareForegroundTitle(TitleId titleId)
PREPARE_STATUS_CODE PrepareForegroundTitle(TitleId titleId)
{
CafeTitleList::WaitForMandatoryScan();
sLaunchModeIsStandalone = false;
@ -788,21 +788,21 @@ namespace CafeSystem
// mount mlc storage
MountBaseDirectories();
// mount title folders
STATUS_CODE r = LoadAndMountForegroundTitle(titleId);
if (r != STATUS_CODE::SUCCESS)
PREPARE_STATUS_CODE r = LoadAndMountForegroundTitle(titleId);
if (r != PREPARE_STATUS_CODE::SUCCESS)
return r;
gameProfile_load();
// setup memory space and PPC recompiler
SetupMemorySpace();
PPCRecompiler_init();
r = SetupExecutable(); // load RPX
if (r != STATUS_CODE::SUCCESS)
if (r != PREPARE_STATUS_CODE::SUCCESS)
return r;
InitVirtualMlcStorage();
return STATUS_CODE::SUCCESS;
return PREPARE_STATUS_CODE::SUCCESS;
}
STATUS_CODE PrepareForegroundTitleFromStandaloneRPX(const fs::path& path)
PREPARE_STATUS_CODE PrepareForegroundTitleFromStandaloneRPX(const fs::path& path)
{
sLaunchModeIsStandalone = true;
cemuLog_log(LogType::Force, "Launching executable in standalone mode due to incorrect layout or missing meta files");
@ -820,7 +820,7 @@ namespace CafeSystem
if (!r)
{
cemuLog_log(LogType::Force, "Failed to mount {}", _pathToUtf8(contentPath));
return STATUS_CODE::UNABLE_TO_MOUNT;
return PREPARE_STATUS_CODE::UNABLE_TO_MOUNT;
}
}
}
@ -832,7 +832,7 @@ namespace CafeSystem
// since a lot of systems (including save folder location) rely on a TitleId, we derive a placeholder id from the executable hash
auto execData = fsc_extractFile(_pathToExecutable.c_str());
if (!execData)
return STATUS_CODE::INVALID_RPX;
return PREPARE_STATUS_CODE::INVALID_RPX;
uint32 h = generateHashFromRawRPXData(execData->data(), execData->size());
sForegroundTitleId = 0xFFFFFFFF00000000ULL | (uint64)h;
cemuLog_log(LogType::Force, "Generated placeholder TitleId: {:016x}", sForegroundTitleId);
@ -842,7 +842,7 @@ namespace CafeSystem
// load executable
SetupExecutable();
InitVirtualMlcStorage();
return STATUS_CODE::SUCCESS;
return PREPARE_STATUS_CODE::SUCCESS;
}
void _LaunchTitleThread()

View File

@ -15,20 +15,19 @@ namespace CafeSystem
virtual void CafeRecreateCanvas() = 0;
};
enum class STATUS_CODE
enum class PREPARE_STATUS_CODE
{
SUCCESS,
INVALID_RPX,
UNABLE_TO_MOUNT, // failed to mount through TitleInfo (most likely caused by an invalid or outdated path)
//BAD_META_DATA, - the title list only stores titles with valid meta, so this error code is impossible
};
void Initialize();
void SetImplementation(SystemImplementation* impl);
void Shutdown();
STATUS_CODE PrepareForegroundTitle(TitleId titleId);
STATUS_CODE PrepareForegroundTitleFromStandaloneRPX(const fs::path& path);
PREPARE_STATUS_CODE PrepareForegroundTitle(TitleId titleId);
PREPARE_STATUS_CODE PrepareForegroundTitleFromStandaloneRPX(const fs::path& path);
void LaunchForegroundTitle();
bool IsTitleRunning();

View File

@ -3,8 +3,7 @@
#include "Cemu/ncrypto/ncrypto.h"
#include "Cafe/Filesystem/WUD/wud.h"
#include "util/crypto/aes128.h"
#include "openssl/evp.h" /* EVP_Digest */
#include "openssl/sha.h" /* SHA1 / SHA256_DIGEST_LENGTH */
#include "openssl/sha.h" /* SHA1 / SHA256 */
#include "fstUtil.h"
#include "FST.h"
@ -141,7 +140,7 @@ struct DiscPartitionTableHeader
static constexpr uint32 MAGIC_VALUE = 0xCCA6E67B;
/* +0x00 */ uint32be magic;
/* +0x04 */ uint32be sectorSize; // must be 0x8000?
/* +0x04 */ uint32be blockSize; // must be 0x8000?
/* +0x08 */ uint8 partitionTableHash[20]; // hash of the data range at +0x800 to end of sector (0x8000)
/* +0x1C */ uint32be numPartitions;
};
@ -164,10 +163,10 @@ struct DiscPartitionHeader
static constexpr uint32 MAGIC_VALUE = 0xCC93A4F5;
/* +0x00 */ uint32be magic;
/* +0x04 */ uint32be sectorSize; // must match DISC_SECTOR_SIZE
/* +0x04 */ uint32be sectorSize; // must match DISC_SECTOR_SIZE for hashed blocks
/* +0x08 */ uint32be ukn008;
/* +0x0C */ uint32be ukn00C;
/* +0x0C */ uint32be ukn00C; // h3 array size?
/* +0x10 */ uint32be h3HashNum;
/* +0x14 */ uint32be fstSize; // in bytes
/* +0x18 */ uint32be fstSector; // relative to partition start
@ -178,13 +177,15 @@ struct DiscPartitionHeader
/* +0x24 */ uint8 fstHashType;
/* +0x25 */ uint8 fstEncryptionType; // purpose of this isn't really understood. Maybe it controls which key is being used? (1 -> disc key, 2 -> partition key)
/* +0x26 */ uint8 versionA;
/* +0x27 */ uint8 ukn027; // also a version field?
/* +0x26 */ uint8be versionA;
/* +0x27 */ uint8be ukn027; // also a version field?
// there is an array at +0x40 ? Related to H3 list. Also related to value at +0x0C and h3HashNum
/* +0x28 */ uint8be _uknOrPadding028[0x18];
/* +0x40 */ uint8be h3HashArray[32]; // dynamic size. Only present if fstHashType != 0
};
static_assert(sizeof(DiscPartitionHeader) == 0x28);
static_assert(sizeof(DiscPartitionHeader) == 0x40+0x20);
bool FSTVolume::FindDiscKey(const fs::path& path, NCrypto::AesKey& discTitleKey)
{
@ -269,7 +270,7 @@ FSTVolume* FSTVolume::OpenFromDiscImage(const fs::path& path, NCrypto::AesKey& d
cemuLog_log(LogType::Force, "Disc image rejected because decryption failed");
return nullptr;
}
if (partitionHeader->sectorSize != DISC_SECTOR_SIZE)
if (partitionHeader->blockSize != DISC_SECTOR_SIZE)
{
cemuLog_log(LogType::Force, "Disc image rejected because partition sector size is invalid");
return nullptr;
@ -336,6 +337,9 @@ FSTVolume* FSTVolume::OpenFromDiscImage(const fs::path& path, NCrypto::AesKey& d
cemu_assert_debug(partitionHeaderSI.fstEncryptionType == 1);
// todo - check other fields?
if(partitionHeaderSI.fstHashType == 0 && partitionHeaderSI.h3HashNum != 0)
cemuLog_log(LogType::Force, "FST: Partition uses unhashed blocks but stores a non-zero amount of H3 hashes");
// GM partition
DiscPartitionHeader partitionHeaderGM{};
if (!readPartitionHeader(partitionHeaderGM, gmPartitionIndex))
@ -349,9 +353,10 @@ FSTVolume* FSTVolume::OpenFromDiscImage(const fs::path& path, NCrypto::AesKey& d
// if decryption is necessary
// load SI FST
dataSource->SetBaseOffset((uint64)partitionArray[siPartitionIndex].partitionAddress * DISC_SECTOR_SIZE);
auto siFST = OpenFST(dataSource.get(), (uint64)partitionHeaderSI.fstSector * DISC_SECTOR_SIZE, partitionHeaderSI.fstSize, &discTitleKey, static_cast<FSTVolume::ClusterHashMode>(partitionHeaderSI.fstHashType));
auto siFST = OpenFST(dataSource.get(), (uint64)partitionHeaderSI.fstSector * DISC_SECTOR_SIZE, partitionHeaderSI.fstSize, &discTitleKey, static_cast<FSTVolume::ClusterHashMode>(partitionHeaderSI.fstHashType), nullptr);
if (!siFST)
return nullptr;
cemu_assert_debug(!(siFST->HashIsDisabled() && partitionHeaderSI.h3HashNum != 0)); // if hash is disabled, no H3 data may be present
// load ticket file for partition that we want to decrypt
NCrypto::ETicketParser ticketParser;
std::vector<uint8> ticketData = siFST->ExtractFile(fmt::format("{:02x}/title.tik", gmPartitionIndex));
@ -360,16 +365,32 @@ FSTVolume* FSTVolume::OpenFromDiscImage(const fs::path& path, NCrypto::AesKey& d
cemuLog_log(LogType::Force, "Disc image ticket file is invalid");
return nullptr;
}
#if 0
// each SI partition seems to contain a title.tmd that we could parse and which should have information about the associated GM partition
// but the console seems to ignore this file for disc images, at least when mounting, so we shouldn't rely on it either
std::vector<uint8> tmdData = siFST->ExtractFile(fmt::format("{:02x}/title.tmd", gmPartitionIndex));
if (tmdData.empty())
{
cemuLog_log(LogType::Force, "Disc image TMD file is missing");
return nullptr;
}
// parse TMD
NCrypto::TMDParser tmdParser;
if (!tmdParser.parse(tmdData.data(), tmdData.size()))
{
cemuLog_log(LogType::Force, "Disc image TMD file is invalid");
return nullptr;
}
#endif
delete siFST;
NCrypto::AesKey gmTitleKey;
ticketParser.GetTitleKey(gmTitleKey);
// load GM partition
dataSource->SetBaseOffset((uint64)partitionArray[gmPartitionIndex].partitionAddress * DISC_SECTOR_SIZE);
FSTVolume* r = OpenFST(std::move(dataSource), (uint64)partitionHeaderGM.fstSector * DISC_SECTOR_SIZE, partitionHeaderGM.fstSize, &gmTitleKey, static_cast<FSTVolume::ClusterHashMode>(partitionHeaderGM.fstHashType));
FSTVolume* r = OpenFST(std::move(dataSource), (uint64)partitionHeaderGM.fstSector * DISC_SECTOR_SIZE, partitionHeaderGM.fstSize, &gmTitleKey, static_cast<FSTVolume::ClusterHashMode>(partitionHeaderGM.fstHashType), nullptr);
if (r)
SET_FST_ERROR(OK);
cemu_assert_debug(!(r->HashIsDisabled() && partitionHeaderGM.h3HashNum != 0)); // if hash is disabled, no H3 data may be present
return r;
}
@ -426,15 +447,15 @@ FSTVolume* FSTVolume::OpenFromContentFolder(fs::path folderPath, ErrorCode* erro
}
// load FST
// fstSize = size of first cluster?
FSTVolume* fstVolume = FSTVolume::OpenFST(std::move(dataSource), 0, fstSize, &titleKey, fstHashMode);
FSTVolume* fstVolume = FSTVolume::OpenFST(std::move(dataSource), 0, fstSize, &titleKey, fstHashMode, &tmdParser);
if (fstVolume)
SET_FST_ERROR(OK);
return fstVolume;
}
FSTVolume* FSTVolume::OpenFST(FSTDataSource* dataSource, uint64 fstOffset, uint32 fstSize, NCrypto::AesKey* partitionTitleKey, ClusterHashMode fstHashMode)
FSTVolume* FSTVolume::OpenFST(FSTDataSource* dataSource, uint64 fstOffset, uint32 fstSize, NCrypto::AesKey* partitionTitleKey, ClusterHashMode fstHashMode, NCrypto::TMDParser* optionalTMD)
{
cemu_assert_debug(fstHashMode != ClusterHashMode::RAW || fstHashMode != ClusterHashMode::RAW2);
cemu_assert_debug(fstHashMode != ClusterHashMode::RAW || fstHashMode != ClusterHashMode::RAW_STREAM);
if (fstSize < sizeof(FSTHeader))
return nullptr;
constexpr uint64 FST_CLUSTER_OFFSET = 0;
@ -465,6 +486,34 @@ FSTVolume* FSTVolume::OpenFST(FSTDataSource* dataSource, uint64 fstOffset, uint3
clusterTable[i].offset = clusterDataTable[i].offset;
clusterTable[i].size = clusterDataTable[i].size;
clusterTable[i].hashMode = static_cast<FSTVolume::ClusterHashMode>((uint8)clusterDataTable[i].hashMode);
clusterTable[i].hasContentHash = false; // from the TMD file (H4?)
}
// if the TMD is available (when opening .app files) we can use the extra info from it to validate unhashed clusters
// each content entry in the TMD corresponds to one cluster used by the FST
if(optionalTMD)
{
if(numCluster != optionalTMD->GetContentList().size())
{
cemuLog_log(LogType::Force, "FST: Number of clusters does not match TMD content list");
return nullptr;
}
auto& contentList = optionalTMD->GetContentList();
for(size_t i=0; i<contentList.size(); i++)
{
auto& cluster = clusterTable[i];
auto& content = contentList[i];
cluster.hasContentHash = true;
cluster.contentHashIsSHA1 = HAS_FLAG(contentList[i].contentFlags, NCrypto::TMDParser::TMDContentFlags::FLAG_SHA1);
cluster.contentSize = content.size;
static_assert(sizeof(content.hash32) == sizeof(cluster.contentHash32));
memcpy(cluster.contentHash32, content.hash32, sizeof(cluster.contentHash32));
// if unhashed mode, then initialize the hash context
if(cluster.hashMode == ClusterHashMode::RAW || cluster.hashMode == ClusterHashMode::RAW_STREAM)
{
cluster.singleHashCtx.reset(EVP_MD_CTX_new());
EVP_DigestInit_ex(cluster.singleHashCtx.get(), cluster.contentHashIsSHA1 ? EVP_sha1() : EVP_sha256(), nullptr);
}
}
}
// preprocess FST table
FSTHeader_FileEntry* fileTable = (FSTHeader_FileEntry*)(clusterDataTable + numCluster);
@ -491,16 +540,17 @@ FSTVolume* FSTVolume::OpenFST(FSTDataSource* dataSource, uint64 fstOffset, uint3
fstVolume->m_offsetFactor = fstHeader->offsetFactor;
fstVolume->m_sectorSize = DISC_SECTOR_SIZE;
fstVolume->m_partitionTitlekey = *partitionTitleKey;
std::swap(fstVolume->m_cluster, clusterTable);
std::swap(fstVolume->m_entries, fstEntries);
std::swap(fstVolume->m_nameStringTable, nameStringTable);
fstVolume->m_hashIsDisabled = fstHeader->hashIsDisabled != 0;
fstVolume->m_cluster = std::move(clusterTable);
fstVolume->m_entries = std::move(fstEntries);
fstVolume->m_nameStringTable = std::move(nameStringTable);
return fstVolume;
}
FSTVolume* FSTVolume::OpenFST(std::unique_ptr<FSTDataSource> dataSource, uint64 fstOffset, uint32 fstSize, NCrypto::AesKey* partitionTitleKey, ClusterHashMode fstHashMode)
FSTVolume* FSTVolume::OpenFST(std::unique_ptr<FSTDataSource> dataSource, uint64 fstOffset, uint32 fstSize, NCrypto::AesKey* partitionTitleKey, ClusterHashMode fstHashMode, NCrypto::TMDParser* optionalTMD)
{
FSTDataSource* ds = dataSource.release();
FSTVolume* fstVolume = OpenFST(ds, fstOffset, fstSize, partitionTitleKey, fstHashMode);
FSTVolume* fstVolume = OpenFST(ds, fstOffset, fstSize, partitionTitleKey, fstHashMode, optionalTMD);
if (!fstVolume)
{
delete ds;
@ -757,7 +807,7 @@ uint32 FSTVolume::ReadFile(FSTFileHandle& fileHandle, uint32 offset, uint32 size
return 0;
cemu_assert_debug(!HAS_FLAG(entry.GetFlags(), FSTEntry::FLAGS::FLAG_LINK));
FSTCluster& cluster = m_cluster[entry.fileInfo.clusterIndex];
if (cluster.hashMode == ClusterHashMode::RAW || cluster.hashMode == ClusterHashMode::RAW2)
if (cluster.hashMode == ClusterHashMode::RAW || cluster.hashMode == ClusterHashMode::RAW_STREAM)
return ReadFile_HashModeRaw(entry.fileInfo.clusterIndex, entry, offset, size, dataOut);
else if (cluster.hashMode == ClusterHashMode::HASH_INTERLEAVED)
return ReadFile_HashModeHashed(entry.fileInfo.clusterIndex, entry, offset, size, dataOut);
@ -765,87 +815,15 @@ uint32 FSTVolume::ReadFile(FSTFileHandle& fileHandle, uint32 offset, uint32 size
return 0;
}
uint32 FSTVolume::ReadFile_HashModeRaw(uint32 clusterIndex, FSTEntry& entry, uint32 readOffset, uint32 readSize, void* dataOut)
{
const uint32 readSizeInput = readSize;
uint8* dataOutU8 = (uint8*)dataOut;
if (readOffset >= entry.fileInfo.fileSize)
return 0;
else if ((readOffset + readSize) >= entry.fileInfo.fileSize)
readSize = (entry.fileInfo.fileSize - readOffset);
const FSTCluster& cluster = m_cluster[clusterIndex];
uint64 clusterOffset = (uint64)cluster.offset * m_sectorSize;
uint64 absFileOffset = entry.fileInfo.fileOffset * m_offsetFactor + readOffset;
// make sure the raw range we read is aligned to AES block size (16)
uint64 readAddrStart = absFileOffset & ~0xF;
uint64 readAddrEnd = (absFileOffset + readSize + 0xF) & ~0xF;
bool usesInitialIV = readOffset < 16;
if (!usesInitialIV)
readAddrStart -= 16; // read previous AES block since we require it for the IV
uint32 prePadding = (uint32)(absFileOffset - readAddrStart); // number of extra bytes we read before readOffset (for AES alignment and IV calculation)
uint32 postPadding = (uint32)(readAddrEnd - (absFileOffset + readSize));
uint8 readBuffer[64 * 1024];
// read first chunk
// if file read offset (readOffset) is within the first AES-block then use initial IV calculated from cluster index
// otherwise read previous AES-block is the IV (AES-CBC)
uint64 readAddrCurrent = readAddrStart;
uint32 rawBytesToRead = (uint32)std::min((readAddrEnd - readAddrStart), (uint64)sizeof(readBuffer));
if (m_dataSource->readData(clusterIndex, clusterOffset, readAddrCurrent, readBuffer, rawBytesToRead) != rawBytesToRead)
{
cemuLog_log(LogType::Force, "FST read error in raw content");
return 0;
}
readAddrCurrent += rawBytesToRead;
uint8 iv[16]{};
if (usesInitialIV)
{
// for the first AES block, the IV is initialized from cluster index
iv[0] = (uint8)(clusterIndex >> 8);
iv[1] = (uint8)(clusterIndex >> 0);
AES128_CBC_decrypt_updateIV(readBuffer, readBuffer, rawBytesToRead, m_partitionTitlekey.b, iv);
std::memcpy(dataOutU8, readBuffer + prePadding, rawBytesToRead - prePadding - postPadding);
dataOutU8 += (rawBytesToRead - prePadding - postPadding);
readSize -= (rawBytesToRead - prePadding - postPadding);
}
else
{
// IV is initialized from previous AES block (AES-CBC)
std::memcpy(iv, readBuffer, 16);
AES128_CBC_decrypt_updateIV(readBuffer + 16, readBuffer + 16, rawBytesToRead - 16, m_partitionTitlekey.b, iv);
std::memcpy(dataOutU8, readBuffer + prePadding, rawBytesToRead - prePadding - postPadding);
dataOutU8 += (rawBytesToRead - prePadding - postPadding);
readSize -= (rawBytesToRead - prePadding - postPadding);
}
// read remaining chunks
while (readSize > 0)
{
uint32 bytesToRead = (uint32)std::min((uint32)sizeof(readBuffer), readSize);
uint32 alignedBytesToRead = (bytesToRead + 15) & ~0xF;
if (m_dataSource->readData(clusterIndex, clusterOffset, readAddrCurrent, readBuffer, alignedBytesToRead) != alignedBytesToRead)
{
cemuLog_log(LogType::Force, "FST read error in raw content");
return 0;
}
AES128_CBC_decrypt_updateIV(readBuffer, readBuffer, alignedBytesToRead, m_partitionTitlekey.b, iv);
std::memcpy(dataOutU8, readBuffer, bytesToRead);
dataOutU8 += bytesToRead;
readSize -= bytesToRead;
readAddrCurrent += alignedBytesToRead;
}
return readSizeInput - readSize;
}
constexpr size_t BLOCK_SIZE = 0x10000;
constexpr size_t BLOCK_HASH_SIZE = 0x0400;
constexpr size_t BLOCK_FILE_SIZE = 0xFC00;
struct FSTRawBlock
{
std::vector<uint8> rawData; // unhashed block size depends on sector size field in partition header
};
struct FSTHashedBlock
{
uint8 rawData[BLOCK_SIZE];
@ -887,12 +865,160 @@ struct FSTHashedBlock
static_assert(sizeof(FSTHashedBlock) == BLOCK_SIZE);
struct FSTCachedRawBlock
{
FSTRawBlock blockData;
uint8 ivForNextBlock[16];
uint64 lastAccess;
};
struct FSTCachedHashedBlock
{
FSTHashedBlock blockData;
uint64 lastAccess;
};
// Checks cache fill state and if necessary drops least recently accessed block from the cache. Optionally allows to recycle the released cache entry to cut down cost of memory allocation and clearing
void FSTVolume::TrimCacheIfRequired(FSTCachedRawBlock** droppedRawBlock, FSTCachedHashedBlock** droppedHashedBlock)
{
// calculate size used by cache
size_t cacheSize = 0;
for (auto& itr : m_cacheDecryptedRawBlocks)
cacheSize += itr.second->blockData.rawData.size();
for (auto& itr : m_cacheDecryptedHashedBlocks)
cacheSize += sizeof(FSTCachedHashedBlock) + sizeof(FSTHashedBlock);
// only trim if cache is full (larger than 2MB)
if (cacheSize < 2*1024*1024) // 2MB
return;
// scan both cache lists to find least recently accessed block to drop
auto dropRawItr = std::min_element(m_cacheDecryptedRawBlocks.begin(), m_cacheDecryptedRawBlocks.end(), [](const auto& a, const auto& b) -> bool
{ return a.second->lastAccess < b.second->lastAccess; });
auto dropHashedItr = std::min_element(m_cacheDecryptedHashedBlocks.begin(), m_cacheDecryptedHashedBlocks.end(), [](const auto& a, const auto& b) -> bool
{ return a.second->lastAccess < b.second->lastAccess; });
uint64 lastAccess = std::numeric_limits<uint64>::max();
if(dropRawItr != m_cacheDecryptedRawBlocks.end())
lastAccess = dropRawItr->second->lastAccess;
if(dropHashedItr != m_cacheDecryptedHashedBlocks.end())
lastAccess = std::min<uint64>(lastAccess, dropHashedItr->second->lastAccess);
if(dropRawItr != m_cacheDecryptedRawBlocks.end() && dropRawItr->second->lastAccess == lastAccess)
{
if (droppedRawBlock)
*droppedRawBlock = dropRawItr->second;
else
delete dropRawItr->second;
m_cacheDecryptedRawBlocks.erase(dropRawItr);
return;
}
else if(dropHashedItr != m_cacheDecryptedHashedBlocks.end() && dropHashedItr->second->lastAccess == lastAccess)
{
if (droppedHashedBlock)
*droppedHashedBlock = dropHashedItr->second;
else
delete dropHashedItr->second;
m_cacheDecryptedHashedBlocks.erase(dropHashedItr);
}
}
void FSTVolume::DetermineUnhashedBlockIV(uint32 clusterIndex, uint32 blockIndex, uint8 ivOut[16])
{
memset(ivOut, 0, sizeof(ivOut));
if(blockIndex == 0)
{
ivOut[0] = (uint8)(clusterIndex >> 8);
ivOut[1] = (uint8)(clusterIndex >> 0);
}
else
{
// the last 16 encrypted bytes of the previous block are the IV (AES CBC)
// if the previous block is cached we can grab the IV from there. Otherwise we have to read the 16 bytes from the data source
uint32 prevBlockIndex = blockIndex - 1;
uint64 cacheBlockId = ((uint64)clusterIndex << (64 - 16)) | (uint64)prevBlockIndex;
auto itr = m_cacheDecryptedRawBlocks.find(cacheBlockId);
if (itr != m_cacheDecryptedRawBlocks.end())
{
memcpy(ivOut, itr->second->ivForNextBlock, 16);
}
else
{
cemu_assert(m_sectorSize >= 16);
uint64 clusterOffset = (uint64)m_cluster[clusterIndex].offset * m_sectorSize;
uint8 prevIV[16];
if (m_dataSource->readData(clusterIndex, clusterOffset, blockIndex * m_sectorSize - 16, prevIV, 16) != 16)
{
cemuLog_log(LogType::Force, "Failed to read IV for raw FST block");
m_detectedCorruption = true;
return;
}
memcpy(ivOut, prevIV, 16);
}
}
}
FSTCachedRawBlock* FSTVolume::GetDecryptedRawBlock(uint32 clusterIndex, uint32 blockIndex)
{
FSTCluster& cluster = m_cluster[clusterIndex];
uint64 clusterOffset = (uint64)cluster.offset * m_sectorSize;
// generate id for cache
uint64 cacheBlockId = ((uint64)clusterIndex << (64 - 16)) | (uint64)blockIndex;
// lookup block in cache
FSTCachedRawBlock* block = nullptr;
auto itr = m_cacheDecryptedRawBlocks.find(cacheBlockId);
if (itr != m_cacheDecryptedRawBlocks.end())
{
block = itr->second;
block->lastAccess = ++m_cacheAccessCounter;
return block;
}
// if cache already full, drop least recently accessed block and recycle FSTCachedRawBlock object if possible
TrimCacheIfRequired(&block, nullptr);
if (!block)
block = new FSTCachedRawBlock();
block->blockData.rawData.resize(m_sectorSize);
// block not cached, read new
block->lastAccess = ++m_cacheAccessCounter;
if (m_dataSource->readData(clusterIndex, clusterOffset, blockIndex * m_sectorSize, block->blockData.rawData.data(), m_sectorSize) != m_sectorSize)
{
cemuLog_log(LogType::Force, "Failed to read raw FST block");
delete block;
m_detectedCorruption = true;
return nullptr;
}
// decrypt hash data
uint8 iv[16]{};
DetermineUnhashedBlockIV(clusterIndex, blockIndex, iv);
memcpy(block->ivForNextBlock, block->blockData.rawData.data() + m_sectorSize - 16, 16);
AES128_CBC_decrypt(block->blockData.rawData.data(), block->blockData.rawData.data(), m_sectorSize, m_partitionTitlekey.b, iv);
// if this is the next block, then hash it
if(cluster.hasContentHash)
{
if(cluster.singleHashNumBlocksHashed == blockIndex)
{
cemu_assert_debug(!(cluster.contentSize % m_sectorSize)); // size should be multiple of sector size? Regardless, the hashing code below can handle non-aligned sizes
bool isLastBlock = blockIndex == (std::max<uint32>(cluster.contentSize / m_sectorSize, 1) - 1);
uint32 hashSize = m_sectorSize;
if(isLastBlock)
hashSize = cluster.contentSize - (uint64)blockIndex*m_sectorSize;
EVP_DigestUpdate(cluster.singleHashCtx.get(), block->blockData.rawData.data(), hashSize);
cluster.singleHashNumBlocksHashed++;
if(isLastBlock)
{
uint8 hash[32];
EVP_DigestFinal_ex(cluster.singleHashCtx.get(), hash, nullptr);
if(memcmp(hash, cluster.contentHash32, cluster.contentHashIsSHA1 ? 20 : 32) != 0)
{
cemuLog_log(LogType::Force, "FST: Raw section hash mismatch");
delete block;
m_detectedCorruption = true;
return nullptr;
}
}
}
}
// register in cache
m_cacheDecryptedRawBlocks.emplace(cacheBlockId, block);
return block;
}
FSTCachedHashedBlock* FSTVolume::GetDecryptedHashedBlock(uint32 clusterIndex, uint32 blockIndex)
{
const FSTCluster& cluster = m_cluster[clusterIndex];
@ -908,22 +1034,17 @@ FSTCachedHashedBlock* FSTVolume::GetDecryptedHashedBlock(uint32 clusterIndex, ui
block->lastAccess = ++m_cacheAccessCounter;
return block;
}
// if cache already full, drop least recently accessed block (but recycle the FSTHashedBlock* object)
if (m_cacheDecryptedHashedBlocks.size() >= 16)
{
auto dropItr = std::min_element(m_cacheDecryptedHashedBlocks.begin(), m_cacheDecryptedHashedBlocks.end(), [](const auto& a, const auto& b) -> bool
{ return a.second->lastAccess < b.second->lastAccess; });
block = dropItr->second;
m_cacheDecryptedHashedBlocks.erase(dropItr);
}
else
// if cache already full, drop least recently accessed block and recycle FSTCachedHashedBlock object if possible
TrimCacheIfRequired(nullptr, &block);
if (!block)
block = new FSTCachedHashedBlock();
// block not cached, read new
block->lastAccess = ++m_cacheAccessCounter;
if (m_dataSource->readData(clusterIndex, clusterOffset, blockIndex * BLOCK_SIZE, block->blockData.rawData, BLOCK_SIZE) != BLOCK_SIZE)
{
cemuLog_log(LogType::Force, "Failed to read FST block");
cemuLog_log(LogType::Force, "Failed to read hashed FST block");
delete block;
m_detectedCorruption = true;
return nullptr;
}
// decrypt hash data
@ -931,11 +1052,46 @@ FSTCachedHashedBlock* FSTVolume::GetDecryptedHashedBlock(uint32 clusterIndex, ui
AES128_CBC_decrypt(block->blockData.getHashData(), block->blockData.getHashData(), BLOCK_HASH_SIZE, m_partitionTitlekey.b, iv);
// decrypt file data
AES128_CBC_decrypt(block->blockData.getFileData(), block->blockData.getFileData(), BLOCK_FILE_SIZE, m_partitionTitlekey.b, block->blockData.getH0Hash(blockIndex%16));
// compare with H0 to verify data integrity
NCrypto::CHash160 h0;
SHA1(block->blockData.getFileData(), BLOCK_FILE_SIZE, h0.b);
uint32 h0Index = (blockIndex % 4096);
if (memcmp(h0.b, block->blockData.getH0Hash(h0Index & 0xF), sizeof(h0.b)) != 0)
{
cemuLog_log(LogType::Force, "FST: Hash H0 mismatch in hashed block (section {} index {})", clusterIndex, blockIndex);
delete block;
m_detectedCorruption = true;
return nullptr;
}
// register in cache
m_cacheDecryptedHashedBlocks.emplace(cacheBlockId, block);
return block;
}
uint32 FSTVolume::ReadFile_HashModeRaw(uint32 clusterIndex, FSTEntry& entry, uint32 readOffset, uint32 readSize, void* dataOut)
{
uint8* dataOutU8 = (uint8*)dataOut;
if (readOffset >= entry.fileInfo.fileSize)
return 0;
else if ((readOffset + readSize) >= entry.fileInfo.fileSize)
readSize = (entry.fileInfo.fileSize - readOffset);
uint64 absFileOffset = entry.fileInfo.fileOffset * m_offsetFactor + readOffset;
uint32 remainingReadSize = readSize;
while (remainingReadSize > 0)
{
const FSTCachedRawBlock* rawBlock = this->GetDecryptedRawBlock(clusterIndex, absFileOffset/m_sectorSize);
if (!rawBlock)
break;
uint32 blockOffset = (uint32)(absFileOffset % m_sectorSize);
uint32 bytesToRead = std::min<uint32>(remainingReadSize, m_sectorSize - blockOffset);
std::memcpy(dataOutU8, rawBlock->blockData.rawData.data() + blockOffset, bytesToRead);
dataOutU8 += bytesToRead;
remainingReadSize -= bytesToRead;
absFileOffset += bytesToRead;
}
return readSize - remainingReadSize;
}
uint32 FSTVolume::ReadFile_HashModeHashed(uint32 clusterIndex, FSTEntry& entry, uint32 readOffset, uint32 readSize, void* dataOut)
{
/*
@ -966,7 +1122,6 @@ uint32 FSTVolume::ReadFile_HashModeHashed(uint32 clusterIndex, FSTEntry& entry,
*/
const FSTCluster& cluster = m_cluster[clusterIndex];
uint64 clusterBaseOffset = (uint64)cluster.offset * m_sectorSize;
uint64 fileReadOffset = entry.fileInfo.fileOffset * m_offsetFactor + readOffset;
uint32 blockIndex = (uint32)(fileReadOffset / BLOCK_FILE_SIZE);
uint32 bytesRemaining = readSize;
@ -1019,6 +1174,8 @@ bool FSTVolume::Next(FSTDirectoryIterator& directoryIterator, FSTFileHandle& fil
FSTVolume::~FSTVolume()
{
for (auto& itr : m_cacheDecryptedRawBlocks)
delete itr.second;
for (auto& itr : m_cacheDecryptedHashedBlocks)
delete itr.second;
if (m_sourceIsOwned)
@ -1115,4 +1272,4 @@ bool FSTVerifier::VerifyHashedContentFile(FileStream* fileContent, const NCrypto
void FSTVolumeTest()
{
FSTPathUnitTest();
}
}

View File

@ -1,5 +1,6 @@
#pragma once
#include "Cemu/ncrypto/ncrypto.h"
#include "openssl/evp.h"
struct FSTFileHandle
{
@ -45,6 +46,7 @@ public:
~FSTVolume();
uint32 GetFileCount() const;
bool HasCorruption() const { return m_detectedCorruption; }
bool OpenFile(std::string_view path, FSTFileHandle& fileHandleOut, bool openOnlyFiles = false);
@ -86,15 +88,25 @@ private:
enum class ClusterHashMode : uint8
{
RAW = 0, // raw data + encryption, no hashing?
RAW2 = 1, // raw data + encryption, with hash stored in tmd?
RAW_STREAM = 1, // raw data + encryption, with hash stored in tmd?
HASH_INTERLEAVED = 2, // hashes + raw interleaved in 0x10000 blocks (0x400 bytes of hashes at the beginning, followed by 0xFC00 bytes of data)
};
struct FSTCluster
{
FSTCluster() : singleHashCtx(nullptr, &EVP_MD_CTX_free) {}
uint32 offset;
uint32 size;
ClusterHashMode hashMode;
// extra data if TMD is available
bool hasContentHash;
uint8 contentHash32[32];
bool contentHashIsSHA1; // if true then it's SHA1 (with extra bytes zeroed out), otherwise it's SHA256
uint64 contentSize; // size of the content (in blocks)
// hash context for single hash mode (content hash must be available)
std::unique_ptr<EVP_MD_CTX, decltype(&EVP_MD_CTX_free)> singleHashCtx; // unique_ptr to make this move-only
uint32 singleHashNumBlocksHashed{0};
};
struct FSTEntry
@ -164,17 +176,30 @@ private:
bool m_sourceIsOwned{};
uint32 m_sectorSize{}; // for cluster offsets
uint32 m_offsetFactor{}; // for file offsets
bool m_hashIsDisabled{}; // disables hash verification (for all clusters of this volume?)
std::vector<FSTCluster> m_cluster;
std::vector<FSTEntry> m_entries;
std::vector<char> m_nameStringTable;
NCrypto::AesKey m_partitionTitlekey;
bool m_detectedCorruption{false};
/* Cache for decrypted hashed blocks */
bool HashIsDisabled() const
{
return m_hashIsDisabled;
}
/* Cache for decrypted raw and hashed blocks */
std::unordered_map<uint64, struct FSTCachedRawBlock*> m_cacheDecryptedRawBlocks;
std::unordered_map<uint64, struct FSTCachedHashedBlock*> m_cacheDecryptedHashedBlocks;
uint64 m_cacheAccessCounter{};
void DetermineUnhashedBlockIV(uint32 clusterIndex, uint32 blockIndex, uint8 ivOut[16]);
struct FSTCachedRawBlock* GetDecryptedRawBlock(uint32 clusterIndex, uint32 blockIndex);
struct FSTCachedHashedBlock* GetDecryptedHashedBlock(uint32 clusterIndex, uint32 blockIndex);
void TrimCacheIfRequired(struct FSTCachedRawBlock** droppedRawBlock, struct FSTCachedHashedBlock** droppedHashedBlock);
/* File reading */
uint32 ReadFile_HashModeRaw(uint32 clusterIndex, FSTEntry& entry, uint32 readOffset, uint32 readSize, void* dataOut);
uint32 ReadFile_HashModeHashed(uint32 clusterIndex, FSTEntry& entry, uint32 readOffset, uint32 readSize, void* dataOut);
@ -185,7 +210,10 @@ private:
/* +0x00 */ uint32be magic;
/* +0x04 */ uint32be offsetFactor;
/* +0x08 */ uint32be numCluster;
/* +0x0C */ uint32be ukn0C;
/* +0x0C */ uint8be hashIsDisabled;
/* +0x0D */ uint8be ukn0D;
/* +0x0E */ uint8be ukn0E;
/* +0x0F */ uint8be ukn0F;
/* +0x10 */ uint32be ukn10;
/* +0x14 */ uint32be ukn14;
/* +0x18 */ uint32be ukn18;
@ -262,8 +290,8 @@ private:
static_assert(sizeof(FSTHeader_FileEntry) == 0x10);
static FSTVolume* OpenFST(FSTDataSource* dataSource, uint64 fstOffset, uint32 fstSize, NCrypto::AesKey* partitionTitleKey, ClusterHashMode fstHashMode);
static FSTVolume* OpenFST(std::unique_ptr<FSTDataSource> dataSource, uint64 fstOffset, uint32 fstSize, NCrypto::AesKey* partitionTitleKey, ClusterHashMode fstHashMode);
static FSTVolume* OpenFST(FSTDataSource* dataSource, uint64 fstOffset, uint32 fstSize, NCrypto::AesKey* partitionTitleKey, ClusterHashMode fstHashMode, NCrypto::TMDParser* optionalTMD);
static FSTVolume* OpenFST(std::unique_ptr<FSTDataSource> dataSource, uint64 fstOffset, uint32 fstSize, NCrypto::AesKey* partitionTitleKey, ClusterHashMode fstHashMode, NCrypto::TMDParser* optionalTMD);
static bool ProcessFST(FSTHeader_FileEntry* fileTable, uint32 numFileEntries, uint32 numCluster, std::vector<char>& nameStringTable, std::vector<FSTEntry>& fstEntries);
bool MatchFSTEntryName(FSTEntry& entry, std::string_view comparedName)

View File

@ -962,7 +962,7 @@ bool GraphicPack2::Activate()
auto option_upscale = rules.FindOption("upscaleMagFilter");
if(option_upscale && boost::iequals(*option_upscale, "NearestNeighbor"))
m_output_settings.upscale_filter = LatteTextureView::MagFilter::kNearestNeighbor;
auto option_downscale = rules.FindOption("NearestNeighbor");
auto option_downscale = rules.FindOption("downscaleMinFilter");
if (option_downscale && boost::iequals(*option_downscale, "NearestNeighbor"))
m_output_settings.downscale_filter = LatteTextureView::MagFilter::kNearestNeighbor;
}

View File

@ -934,13 +934,6 @@ void LatteRenderTarget_copyToBackbuffer(LatteTextureView* textureView, bool isPa
{
sint32 scaling_filter = downscaling ? GetConfig().downscale_filter : GetConfig().upscale_filter;
if (g_renderer->GetType() == RendererAPI::Vulkan)
{
// force linear or nearest neighbor filter
if(scaling_filter != kLinearFilter && scaling_filter != kNearestNeighborFilter)
scaling_filter = kLinearFilter;
}
if (scaling_filter == kLinearFilter)
{
if(renderUpsideDown)
@ -957,7 +950,7 @@ void LatteRenderTarget_copyToBackbuffer(LatteTextureView* textureView, bool isPa
else
shader = RendererOutputShader::s_bicubic_shader;
filter = LatteTextureView::MagFilter::kNearestNeighbor;
filter = LatteTextureView::MagFilter::kLinear;
}
else if (scaling_filter == kBicubicHermiteFilter)
{

View File

@ -570,13 +570,10 @@ void OpenGLRenderer::DrawBackbufferQuad(LatteTextureView* texView, RendererOutpu
g_renderer->ClearColorbuffer(padView);
}
sint32 effectiveWidth, effectiveHeight;
texView->baseTexture->GetEffectiveSize(effectiveWidth, effectiveHeight, 0);
shader_unbind(RendererShader::ShaderType::kGeometry);
shader_bind(shader->GetVertexShader());
shader_bind(shader->GetFragmentShader());
shader->SetUniformParameters(*texView, { effectiveWidth, effectiveHeight }, { imageWidth, imageHeight });
shader->SetUniformParameters(*texView, {imageWidth, imageHeight});
// set viewport
glViewportIndexedf(0, imageX, imageY, imageWidth, imageHeight);
@ -584,6 +581,12 @@ void OpenGLRenderer::DrawBackbufferQuad(LatteTextureView* texView, RendererOutpu
LatteTextureViewGL* texViewGL = (LatteTextureViewGL*)texView;
texture_bindAndActivate(texView, 0);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
texViewGL->samplerState.clampS = texViewGL->samplerState.clampT = 0xFF;
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, useLinearTexFilter ? GL_LINEAR : GL_NEAREST);
texViewGL->samplerState.filterMin = 0xFFFFFFFF;
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, useLinearTexFilter ? GL_LINEAR : GL_NEAREST);
texViewGL->samplerState.filterMag = 0xFFFFFFFF;

View File

@ -2,18 +2,7 @@
#include "Cafe/HW/Latte/Renderer/OpenGL/OpenGLRenderer.h"
const std::string RendererOutputShader::s_copy_shader_source =
R"(#version 420
#ifdef VULKAN
layout(location = 0) in vec2 passUV;
layout(binding = 0) uniform sampler2D textureSrc;
layout(location = 0) out vec4 colorOut0;
#else
in vec2 passUV;
layout(binding=0) uniform sampler2D textureSrc;
layout(location = 0) out vec4 colorOut0;
#endif
R"(
void main()
{
colorOut0 = vec4(texture(textureSrc, passUV).rgb,1.0);
@ -35,20 +24,6 @@ fragment float4 main0(VertexOut in [[stage_in]], texture2d<float> textureSrc [[t
const std::string RendererOutputShader::s_bicubic_shader_source =
R"(
#version 420
#ifdef VULKAN
layout(location = 0) in vec2 passUV;
layout(binding = 0) uniform sampler2D textureSrc;
layout(binding = 1) uniform vec2 textureSrcResolution;
layout(location = 0) out vec4 colorOut0;
#else
in vec2 passUV;
layout(binding=0) uniform sampler2D textureSrc;
uniform vec2 textureSrcResolution;
layout(location = 0) out vec4 colorOut0;
#endif
vec4 cubic(float x)
{
float x2 = x * x;
@ -61,24 +36,23 @@ vec4 cubic(float x)
return w / 6.0;
}
vec4 bcFilter(vec2 texcoord, vec2 texscale)
vec4 bcFilter(vec2 uv, vec4 texelSize)
{
float fx = fract(texcoord.x);
float fy = fract(texcoord.y);
texcoord.x -= fx;
texcoord.y -= fy;
vec2 pixel = uv*texelSize.zw - 0.5;
vec2 pixelFrac = fract(pixel);
vec2 pixelInt = pixel - pixelFrac;
vec4 xcubic = cubic(fx);
vec4 ycubic = cubic(fy);
vec4 xcubic = cubic(pixelFrac.x);
vec4 ycubic = cubic(pixelFrac.y);
vec4 c = vec4(texcoord.x - 0.5, texcoord.x + 1.5, texcoord.y - 0.5, texcoord.y + 1.5);
vec4 c = vec4(pixelInt.x - 0.5, pixelInt.x + 1.5, pixelInt.y - 0.5, pixelInt.y + 1.5);
vec4 s = vec4(xcubic.x + xcubic.y, xcubic.z + xcubic.w, ycubic.x + ycubic.y, ycubic.z + ycubic.w);
vec4 offset = c + vec4(xcubic.y, xcubic.w, ycubic.y, ycubic.w) / s;
vec4 sample0 = texture(textureSrc, vec2(offset.x, offset.z) * texscale);
vec4 sample1 = texture(textureSrc, vec2(offset.y, offset.z) * texscale);
vec4 sample2 = texture(textureSrc, vec2(offset.x, offset.w) * texscale);
vec4 sample3 = texture(textureSrc, vec2(offset.y, offset.w) * texscale);
vec4 sample0 = texture(textureSrc, vec2(offset.x, offset.z) * texelSize.xy);
vec4 sample1 = texture(textureSrc, vec2(offset.y, offset.z) * texelSize.xy);
vec4 sample2 = texture(textureSrc, vec2(offset.x, offset.w) * texelSize.xy);
vec4 sample3 = texture(textureSrc, vec2(offset.y, offset.w) * texelSize.xy);
float sx = s.x / (s.x + s.y);
float sy = s.z / (s.z + s.w);
@ -89,7 +63,8 @@ vec4 bcFilter(vec2 texcoord, vec2 texscale)
}
void main(){
colorOut0 = vec4(bcFilter(passUV*textureSrcResolution, vec2(1.0,1.0)/textureSrcResolution).rgb,1.0);
vec4 texelSize = vec4( 1.0 / textureSrcResolution.xy, textureSrcResolution.xy);
colorOut0 = vec4(bcFilter(passUV, texelSize).rgb,1.0);
}
)";
@ -145,15 +120,7 @@ fragment float4 main0(VertexOut in [[stage_in]], texture2d<float> textureSrc [[t
)";
const std::string RendererOutputShader::s_hermite_shader_source =
R"(#version 420
in vec4 gl_FragCoord;
in vec2 passUV;
layout(binding=0) uniform sampler2D textureSrc;
uniform vec2 textureSrcResolution;
uniform vec2 outputResolution;
layout(location = 0) out vec4 colorOut0;
R"(
// https://www.shadertoy.com/view/MllSzX
vec3 CubicHermite (vec3 A, vec3 B, vec3 C, vec3 D, float t)
@ -174,8 +141,8 @@ vec3 BicubicHermiteTexture(vec2 uv, vec4 texelSize)
vec2 pixel = uv*texelSize.zw + 0.5;
vec2 frac = fract(pixel);
pixel = floor(pixel) / texelSize.zw - vec2(texelSize.xy/2.0);
vec4 doubleSize = texelSize*texelSize;
vec4 doubleSize = texelSize*2.0;
vec3 C00 = texture(textureSrc, pixel + vec2(-texelSize.x ,-texelSize.y)).rgb;
vec3 C10 = texture(textureSrc, pixel + vec2( 0.0 ,-texelSize.y)).rgb;
@ -206,7 +173,7 @@ vec3 BicubicHermiteTexture(vec2 uv, vec4 texelSize)
}
void main(){
vec4 texelSize = vec4( 1.0 / outputResolution.xy, outputResolution.xy);
vec4 texelSize = vec4( 1.0 / textureSrcResolution.xy, textureSrcResolution.xy);
colorOut0 = vec4(BicubicHermiteTexture(passUV, texelSize), 1.0);
}
)";
@ -277,8 +244,10 @@ fragment float4 main0(VertexOut in [[stage_in]], texture2d<float> textureSrc [[t
RendererOutputShader::RendererOutputShader(const std::string& vertex_source, const std::string& fragment_source)
{
auto finalFragmentSrc = PrependFragmentPreamble(fragment_source);
m_vertex_shader = g_renderer->shader_create(RendererShader::ShaderType::kVertex, 0, 0, vertex_source, false, false);
m_fragment_shader = g_renderer->shader_create(RendererShader::ShaderType::kFragment, 0, 0, fragment_source, false, false);
m_fragment_shader = g_renderer->shader_create(RendererShader::ShaderType::kFragment, 0, 0, finalFragmentSrc, false, false);
m_vertex_shader->PreponeCompilation(true);
m_fragment_shader->PreponeCompilation(true);
@ -291,75 +260,45 @@ RendererOutputShader::RendererOutputShader(const std::string& vertex_source, con
if (g_renderer->GetType() == RendererAPI::OpenGL)
{
m_attributes[0].m_loc_texture_src_resolution = m_vertex_shader->GetUniformLocation("textureSrcResolution");
m_attributes[0].m_loc_input_resolution = m_vertex_shader->GetUniformLocation("inputResolution");
m_attributes[0].m_loc_output_resolution = m_vertex_shader->GetUniformLocation("outputResolution");
m_uniformLocations[0].m_loc_textureSrcResolution = m_vertex_shader->GetUniformLocation("textureSrcResolution");
m_uniformLocations[0].m_loc_nativeResolution = m_vertex_shader->GetUniformLocation("nativeResolution");
m_uniformLocations[0].m_loc_outputResolution = m_vertex_shader->GetUniformLocation("outputResolution");
m_attributes[1].m_loc_texture_src_resolution = m_fragment_shader->GetUniformLocation("textureSrcResolution");
m_attributes[1].m_loc_input_resolution = m_fragment_shader->GetUniformLocation("inputResolution");
m_attributes[1].m_loc_output_resolution = m_fragment_shader->GetUniformLocation("outputResolution");
m_uniformLocations[1].m_loc_textureSrcResolution = m_fragment_shader->GetUniformLocation("textureSrcResolution");
m_uniformLocations[1].m_loc_nativeResolution = m_fragment_shader->GetUniformLocation("nativeResolution");
m_uniformLocations[1].m_loc_outputResolution = m_fragment_shader->GetUniformLocation("outputResolution");
}
else
{
if (g_renderer->GetType() == RendererAPI::Vulkan)
cemuLog_logDebug(LogType::Force, "RendererOutputShader() - todo for Vulkan");
m_attributes[0].m_loc_texture_src_resolution = -1;
m_attributes[0].m_loc_input_resolution = -1;
m_attributes[0].m_loc_output_resolution = -1;
m_attributes[1].m_loc_texture_src_resolution = -1;
m_attributes[1].m_loc_input_resolution = -1;
m_attributes[1].m_loc_output_resolution = -1;
}
}
void RendererOutputShader::SetUniformParameters(const LatteTextureView& texture_view, const Vector2i& input_res, const Vector2i& output_res) const
void RendererOutputShader::SetUniformParameters(const LatteTextureView& texture_view, const Vector2i& output_res) const
{
float res[2];
// vertex shader
if (m_attributes[0].m_loc_texture_src_resolution != -1)
{
res[0] = (float)texture_view.baseTexture->width;
res[1] = (float)texture_view.baseTexture->height;
m_vertex_shader->SetUniform2fv(m_attributes[0].m_loc_texture_src_resolution, res, 1);
}
sint32 effectiveWidth, effectiveHeight;
texture_view.baseTexture->GetEffectiveSize(effectiveWidth, effectiveHeight, 0);
auto setUniforms = [&](RendererShader* shader, const UniformLocations& locations){
float res[2];
if (locations.m_loc_textureSrcResolution != -1)
{
res[0] = (float)effectiveWidth;
res[1] = (float)effectiveHeight;
shader->SetUniform2fv(locations.m_loc_textureSrcResolution, res, 1);
}
if (m_attributes[0].m_loc_input_resolution != -1)
{
res[0] = (float)input_res.x;
res[1] = (float)input_res.y;
m_vertex_shader->SetUniform2fv(m_attributes[0].m_loc_input_resolution, res, 1);
}
if (locations.m_loc_nativeResolution != -1)
{
res[0] = (float)texture_view.baseTexture->width;
res[1] = (float)texture_view.baseTexture->height;
shader->SetUniform2fv(locations.m_loc_nativeResolution, res, 1);
}
if (m_attributes[0].m_loc_output_resolution != -1)
{
res[0] = (float)output_res.x;
res[1] = (float)output_res.y;
m_vertex_shader->SetUniform2fv(m_attributes[0].m_loc_output_resolution, res, 1);
}
// fragment shader
if (m_attributes[1].m_loc_texture_src_resolution != -1)
{
res[0] = (float)texture_view.baseTexture->width;
res[1] = (float)texture_view.baseTexture->height;
m_fragment_shader->SetUniform2fv(m_attributes[1].m_loc_texture_src_resolution, res, 1);
}
if (m_attributes[1].m_loc_input_resolution != -1)
{
res[0] = (float)input_res.x;
res[1] = (float)input_res.y;
m_fragment_shader->SetUniform2fv(m_attributes[1].m_loc_input_resolution, res, 1);
}
if (m_attributes[1].m_loc_output_resolution != -1)
{
res[0] = (float)output_res.x;
res[1] = (float)output_res.y;
m_fragment_shader->SetUniform2fv(m_attributes[1].m_loc_output_resolution, res, 1);
}
if (locations.m_loc_outputResolution != -1)
{
res[0] = (float)output_res.x;
res[1] = (float)output_res.y;
shader->SetUniform2fv(locations.m_loc_outputResolution, res, 1);
}
};
setUniforms(m_vertex_shader, m_uniformLocations[0]);
setUniforms(m_fragment_shader, m_uniformLocations[1]);
}
RendererOutputShader* RendererOutputShader::s_copy_shader;
@ -509,6 +448,26 @@ vertex VertexOut main0(ushort vid [[vertex_id]]) {
return vertex_source.str();
}
std::string RendererOutputShader::PrependFragmentPreamble(const std::string& shaderSrc)
{
return R"(#version 430
#ifdef VULKAN
layout(push_constant) uniform pc {
vec2 textureSrcResolution;
vec2 nativeResolution;
vec2 outputResolution;
};
#else
uniform vec2 textureSrcResolution;
uniform vec2 nativeResolution;
uniform vec2 outputResolution;
#endif
layout(location = 0) in vec2 passUV;
layout(binding = 0) uniform sampler2D textureSrc;
layout(location = 0) out vec4 colorOut0;
)" + shaderSrc;
}
void RendererOutputShader::InitializeStatic()
{
std::string vertex_source, vertex_source_ud;
@ -517,42 +476,18 @@ void RendererOutputShader::InitializeStatic()
{
vertex_source = GetOpenGlVertexSource(false);
vertex_source_ud = GetOpenGlVertexSource(true);
s_copy_shader = new RendererOutputShader(vertex_source, s_copy_shader_source);
s_copy_shader_ud = new RendererOutputShader(vertex_source_ud, s_copy_shader_source);
s_bicubic_shader = new RendererOutputShader(vertex_source, s_bicubic_shader_source);
s_bicubic_shader_ud = new RendererOutputShader(vertex_source_ud, s_bicubic_shader_source);
s_hermit_shader = new RendererOutputShader(vertex_source, s_hermite_shader_source);
s_hermit_shader_ud = new RendererOutputShader(vertex_source_ud, s_hermite_shader_source);
}
else if (g_renderer->GetType() == RendererAPI::Vulkan)
{
vertex_source = GetVulkanVertexSource(false);
vertex_source_ud = GetVulkanVertexSource(true);
s_copy_shader = new RendererOutputShader(vertex_source, s_copy_shader_source);
s_copy_shader_ud = new RendererOutputShader(vertex_source_ud, s_copy_shader_source);
/* s_bicubic_shader = new RendererOutputShader(vertex_source, s_bicubic_shader_source); TODO
s_bicubic_shader_ud = new RendererOutputShader(vertex_source_ud, s_bicubic_shader_source);
s_hermit_shader = new RendererOutputShader(vertex_source, s_hermite_shader_source);
s_hermit_shader_ud = new RendererOutputShader(vertex_source_ud, s_hermite_shader_source);*/
}
else
{
vertex_source = GetMetalVertexSource(false);
vertex_source_ud = GetMetalVertexSource(true);
s_copy_shader = new RendererOutputShader(vertex_source, s_copy_shader_source);
s_copy_shader_ud = new RendererOutputShader(vertex_source_ud, s_copy_shader_source);
s_copy_shader = new RendererOutputShader(vertex_source, s_copy_shader_source_mtl);
s_copy_shader_ud = new RendererOutputShader(vertex_source_ud, s_copy_shader_source_mtl);
s_bicubic_shader = new RendererOutputShader(vertex_source, s_bicubic_shader_source);
s_bicubic_shader_ud = new RendererOutputShader(vertex_source_ud, s_bicubic_shader_source);
s_bicubic_shader = new RendererOutputShader(vertex_source, s_bicubic_shader_source_mtl);
s_bicubic_shader_ud = new RendererOutputShader(vertex_source_ud, s_bicubic_shader_source_mtl);
s_hermit_shader = new RendererOutputShader(vertex_source, s_hermite_shader_source_mtl);
s_hermit_shader_ud = new RendererOutputShader(vertex_source_ud, s_hermite_shader_source_mtl);
}
s_hermit_shader = new RendererOutputShader(vertex_source, s_hermite_shader_source);
s_hermit_shader_ud = new RendererOutputShader(vertex_source_ud, s_hermite_shader_source);
}

View File

@ -17,7 +17,7 @@ public:
RendererOutputShader(const std::string& vertex_source, const std::string& fragment_source);
virtual ~RendererOutputShader() = default;
void SetUniformParameters(const LatteTextureView& texture_view, const Vector2i& input_res, const Vector2i& output_res) const;
void SetUniformParameters(const LatteTextureView& texture_view, const Vector2i& output_res) const;
RendererShader* GetVertexShader() const
{
@ -44,16 +44,18 @@ public:
static std::string GetVulkanVertexSource(bool render_upside_down);
static std::string GetMetalVertexSource(bool render_upside_down);
static std::string PrependFragmentPreamble(const std::string& shaderSrc);
protected:
RendererShader* m_vertex_shader;
RendererShader* m_fragment_shader;
struct
struct UniformLocations
{
sint32 m_loc_texture_src_resolution = -1;
sint32 m_loc_input_resolution = -1;
sint32 m_loc_output_resolution = -1;
} m_attributes[2]{};
sint32 m_loc_textureSrcResolution = -1;
sint32 m_loc_nativeResolution = -1;
sint32 m_loc_outputResolution = -1;
} m_uniformLocations[2]{};
private:
static const std::string s_copy_shader_source;

View File

@ -202,6 +202,13 @@ VkSampler LatteTextureViewVk::GetDefaultTextureSampler(bool useLinearTexFilter)
VkSamplerCreateInfo samplerInfo{};
samplerInfo.sType = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO;
// emulate OpenGL minFilters
// see note under: https://docs.vulkan.org/spec/latest/chapters/samplers.html#VkSamplerCreateInfo
// if maxLod = 0 then magnification is always performed
samplerInfo.mipmapMode = VK_SAMPLER_MIPMAP_MODE_NEAREST;
samplerInfo.minLod = 0.0f;
samplerInfo.maxLod = 0.25f;
if (useLinearTexFilter)
{
samplerInfo.magFilter = VK_FILTER_LINEAR;
@ -212,6 +219,9 @@ VkSampler LatteTextureViewVk::GetDefaultTextureSampler(bool useLinearTexFilter)
samplerInfo.magFilter = VK_FILTER_NEAREST;
samplerInfo.minFilter = VK_FILTER_NEAREST;
}
samplerInfo.addressModeU = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
samplerInfo.addressModeV = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
samplerInfo.addressModeW = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
if (vkCreateSampler(m_device, &samplerInfo, nullptr, &sampler) != VK_SUCCESS)
{

View File

@ -2581,10 +2581,18 @@ VkPipeline VulkanRenderer::backbufferBlit_createGraphicsPipeline(VkDescriptorSet
colorBlending.blendConstants[2] = 0.0f;
colorBlending.blendConstants[3] = 0.0f;
VkPushConstantRange pushConstantRange{
.stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT,
.offset = 0,
.size = 3 * sizeof(float) * 2 // 3 vec2's
};
VkPipelineLayoutCreateInfo pipelineLayoutInfo{};
pipelineLayoutInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO;
pipelineLayoutInfo.setLayoutCount = 1;
pipelineLayoutInfo.pSetLayouts = &descriptorLayout;
pipelineLayoutInfo.pushConstantRangeCount = 1;
pipelineLayoutInfo.pPushConstantRanges = &pushConstantRange;
VkResult result = vkCreatePipelineLayout(m_logicalDevice, &pipelineLayoutInfo, nullptr, &m_pipelineLayout);
if (result != VK_SUCCESS)
@ -2956,6 +2964,25 @@ void VulkanRenderer::DrawBackbufferQuad(LatteTextureView* texView, RendererOutpu
vkCmdBindDescriptorSets(m_state.currentCommandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, m_pipelineLayout, 0, 1, &descriptSet, 0, nullptr);
// update push constants
Vector2f pushData[3];
// textureSrcResolution
sint32 effectiveWidth, effectiveHeight;
texView->baseTexture->GetEffectiveSize(effectiveWidth, effectiveHeight, 0);
pushData[0] = {(float)effectiveWidth, (float)effectiveHeight};
// nativeResolution
pushData[1] = {
(float)texViewVk->baseTexture->width,
(float)texViewVk->baseTexture->height,
};
// outputResolution
pushData[2] = {(float)imageWidth,(float)imageHeight};
vkCmdPushConstants(m_state.currentCommandBuffer, m_pipelineLayout, VK_SHADER_STAGE_FRAGMENT_BIT, 0, sizeof(float) * 2 * 3, &pushData);
vkCmdDraw(m_state.currentCommandBuffer, 6, 1, 0, 0);
vkCmdEndRenderPass(m_state.currentCommandBuffer);

View File

@ -40,7 +40,12 @@ namespace coreinit
inline TimerTicks ConvertNsToTimerTicks(uint64 ns)
{
return ((GetTimerClock() / 31250LL) * ((ns)) / 32000LL);
return ((GetTimerClock() / 31250LL) * ((TimerTicks)ns) / 32000LL);
}
inline TimerTicks ConvertMsToTimerTicks(uint64 ms)
{
return (TimerTicks)ms * GetTimerClock() / 1000LL;
}
};

View File

@ -9,32 +9,45 @@
#include <wx/msgdlg.h>
#include "Cafe/OS/libs/coreinit/coreinit_FS.h"
#include "Cafe/OS/libs/coreinit/coreinit_Time.h"
#include "Cafe/OS/libs/vpad/vpad.h"
namespace nn
{
namespace erreula
{
#define RESULTTYPE_NONE 0
#define RESULTTYPE_FINISH 1
#define RESULTTYPE_NEXT 2
#define RESULTTYPE_JUMP 3
#define RESULTTYPE_PASSWORD 4
#define ERRORTYPE_CODE 0
#define ERRORTYPE_TEXT 1
#define ERRORTYPE_TEXT_ONE_BUTTON 2
#define ERRORTYPE_TEXT_TWO_BUTTON 3
#define ERREULA_STATE_HIDDEN 0
#define ERREULA_STATE_APPEARING 1
#define ERREULA_STATE_VISIBLE 2
#define ERREULA_STATE_DISAPPEARING 3
struct AppearArg_t
enum class ErrorDialogType : uint32
{
AppearArg_t() = default;
AppearArg_t(const AppearArg_t& o)
Code = 0,
Text = 1,
TextOneButton = 2,
TextTwoButton = 3
};
static const sint32 FADE_TIME = 80;
enum class ErrEulaState : uint32
{
Hidden = 0,
Appearing = 1,
Visible = 2,
Disappearing = 3
};
enum class ResultType : uint32
{
None = 0,
Finish = 1,
Next = 2,
Jump = 3,
Password = 4
};
struct AppearError
{
AppearError() = default;
AppearError(const AppearError& o)
{
errorType = o.errorType;
screenType = o.screenType;
@ -49,7 +62,7 @@ namespace erreula
drawCursor = o.drawCursor;
}
uint32be errorType;
betype<ErrorDialogType> errorType;
uint32be screenType;
uint32be controllerType;
uint32be holdType;
@ -63,7 +76,9 @@ namespace erreula
bool drawCursor{};
};
static_assert(sizeof(AppearArg_t) == 0x2C); // maybe larger
using AppearArg = AppearError;
static_assert(sizeof(AppearError) == 0x2C); // maybe larger
struct HomeNixSignArg_t
{
@ -80,6 +95,132 @@ namespace erreula
static_assert(sizeof(ControllerInfo_t) == 0x14); // maybe larger
class ErrEulaInstance
{
public:
enum class BUTTON_SELECTION : uint32
{
NONE = 0xFFFFFFFF,
LEFT = 0,
RIGHT = 1,
};
void Init()
{
m_buttonSelection = BUTTON_SELECTION::NONE;
m_resultCode = -1;
m_resultCodeForLeftButton = 0;
m_resultCodeForRightButton = 0;
SetState(ErrEulaState::Hidden);
}
void DoAppearError(AppearArg* arg)
{
m_buttonSelection = BUTTON_SELECTION::NONE;
m_resultCode = -1;
m_resultCodeForLeftButton = -1;
m_resultCodeForRightButton = -1;
// for standard dialog its 0 and 1?
m_resultCodeForLeftButton = 0;
m_resultCodeForRightButton = 1;
SetState(ErrEulaState::Appearing);
}
void DoDisappearError()
{
if(m_state != ErrEulaState::Visible)
return;
SetState(ErrEulaState::Disappearing);
}
void DoCalc()
{
// appearing and disappearing state will automatically advance after some time
if (m_state == ErrEulaState::Appearing || m_state == ErrEulaState::Disappearing)
{
uint32 elapsedTick = coreinit::OSGetTime() - m_lastStateChange;
if (elapsedTick > coreinit::EspressoTime::ConvertMsToTimerTicks(FADE_TIME))
{
SetState(m_state == ErrEulaState::Appearing ? ErrEulaState::Visible : ErrEulaState::Hidden);
}
}
}
bool IsDecideSelectButtonError() const
{
return m_buttonSelection != BUTTON_SELECTION::NONE;
}
bool IsDecideSelectLeftButtonError() const
{
return m_buttonSelection != BUTTON_SELECTION::LEFT;
}
bool IsDecideSelectRightButtonError() const
{
return m_buttonSelection != BUTTON_SELECTION::RIGHT;
}
void SetButtonSelection(BUTTON_SELECTION selection)
{
cemu_assert_debug(m_buttonSelection == BUTTON_SELECTION::NONE);
m_buttonSelection = selection;
cemu_assert_debug(selection == BUTTON_SELECTION::LEFT || selection == BUTTON_SELECTION::RIGHT);
m_resultCode = selection == BUTTON_SELECTION::LEFT ? m_resultCodeForLeftButton : m_resultCodeForRightButton;
}
ErrEulaState GetState() const
{
return m_state;
}
sint32 GetResultCode() const
{
return m_resultCode;
}
ResultType GetResultType() const
{
if(m_resultCode == -1)
return ResultType::None;
if(m_resultCode < 10)
return ResultType::Finish;
if(m_resultCode >= 9999)
return ResultType::Next;
if(m_resultCode == 40)
return ResultType::Password;
return ResultType::Jump;
}
float GetFadeTransparency() const
{
if(m_state == ErrEulaState::Appearing || m_state == ErrEulaState::Disappearing)
{
uint32 elapsedTick = coreinit::OSGetTime() - m_lastStateChange;
if(m_state == ErrEulaState::Appearing)
return std::min<float>(1.0f, (float)elapsedTick / (float)coreinit::EspressoTime::ConvertMsToTimerTicks(FADE_TIME));
else
return std::max<float>(0.0f, 1.0f - (float)elapsedTick / (float)coreinit::EspressoTime::ConvertMsToTimerTicks(FADE_TIME));
}
return 1.0f;
}
private:
void SetState(ErrEulaState state)
{
m_state = state;
m_lastStateChange = coreinit::OSGetTime();
}
ErrEulaState m_state;
uint32 m_lastStateChange;
/* +0x30 */ betype<sint32> m_resultCode;
/* +0x239C */ betype<BUTTON_SELECTION> m_buttonSelection;
/* +0x23A0 */ betype<sint32> m_resultCodeForLeftButton;
/* +0x23A4 */ betype<sint32> m_resultCodeForRightButton;
};
struct ErrEula_t
{
SysAllocator<coreinit::OSMutex> mutex;
@ -87,17 +228,11 @@ namespace erreula
uint32 langType;
MEMPTR<coreinit::FSClient_t> fsClient;
AppearArg_t currentDialog;
uint32 state;
bool buttonPressed;
bool rightButtonPressed;
std::unique_ptr<ErrEulaInstance> errEulaInstance;
AppearError currentDialog;
bool homeNixSignVisible;
std::chrono::steady_clock::time_point stateTimer{};
} g_errEula = {};
std::wstring GetText(uint16be* text)
{
@ -113,22 +248,61 @@ namespace erreula
}
void export_ErrEulaCreate(PPCInterpreter_t* hCPU)
void ErrEulaCreate(void* workmem, uint32 regionType, uint32 langType, coreinit::FSClient_t* fsClient)
{
ppcDefineParamMEMPTR(thisptr, uint8, 0);
ppcDefineParamU32(regionType, 1);
ppcDefineParamU32(langType, 2);
ppcDefineParamMEMPTR(fsClient, coreinit::FSClient_t, 3);
coreinit::OSLockMutex(&g_errEula.mutex);
g_errEula.regionType = regionType;
g_errEula.langType = langType;
g_errEula.fsClient = fsClient;
cemu_assert_debug(!g_errEula.errEulaInstance);
g_errEula.errEulaInstance = std::make_unique<ErrEulaInstance>();
g_errEula.errEulaInstance->Init();
coreinit::OSUnlockMutex(&g_errEula.mutex);
}
osLib_returnFromFunction(hCPU, 0);
void ErrEulaDestroy()
{
g_errEula.errEulaInstance.reset();
}
// check if any dialog button was selected
bool IsDecideSelectButtonError()
{
if(!g_errEula.errEulaInstance)
return false;
return g_errEula.errEulaInstance->IsDecideSelectButtonError();
}
// check if left dialog button was selected
bool IsDecideSelectLeftButtonError()
{
if(!g_errEula.errEulaInstance)
return false;
return g_errEula.errEulaInstance->IsDecideSelectLeftButtonError();
}
// check if right dialog button was selected
bool IsDecideSelectRightButtonError()
{
if(!g_errEula.errEulaInstance)
return false;
return g_errEula.errEulaInstance->IsDecideSelectRightButtonError();
}
sint32 GetResultCode()
{
if(!g_errEula.errEulaInstance)
return -1;
return g_errEula.errEulaInstance->GetResultCode();
}
ResultType GetResultType()
{
if(!g_errEula.errEulaInstance)
return ResultType::None;
return g_errEula.errEulaInstance->GetResultType();
}
void export_AppearHomeNixSign(PPCInterpreter_t* hCPU)
@ -137,28 +311,24 @@ namespace erreula
osLib_returnFromFunction(hCPU, 0);
}
void export_AppearError(PPCInterpreter_t* hCPU)
void ErrEulaAppearError(AppearArg* arg)
{
ppcDefineParamMEMPTR(arg, AppearArg_t, 0);
g_errEula.currentDialog = *arg.GetPtr();
g_errEula.state = ERREULA_STATE_APPEARING;
g_errEula.buttonPressed = false;
g_errEula.rightButtonPressed = false;
g_errEula.stateTimer = tick_cached();
osLib_returnFromFunction(hCPU, 0);
g_errEula.currentDialog = *arg;
if(g_errEula.errEulaInstance)
g_errEula.errEulaInstance->DoAppearError(arg);
}
void export_GetStateErrorViewer(PPCInterpreter_t* hCPU)
void ErrEulaDisappearError()
{
osLib_returnFromFunction(hCPU, g_errEula.state);
if(g_errEula.errEulaInstance)
g_errEula.errEulaInstance->DoDisappearError();
}
void export_DisappearError(PPCInterpreter_t* hCPU)
ErrEulaState ErrEulaGetStateErrorViewer()
{
g_errEula.state = ERREULA_STATE_HIDDEN;
osLib_returnFromFunction(hCPU, 0);
if(!g_errEula.errEulaInstance)
return ErrEulaState::Hidden;
return g_errEula.errEulaInstance->GetState();
}
void export_ChangeLang(PPCInterpreter_t* hCPU)
@ -168,27 +338,6 @@ namespace erreula
osLib_returnFromFunction(hCPU, 0);
}
void export_IsDecideSelectButtonError(PPCInterpreter_t* hCPU)
{
if (g_errEula.buttonPressed)
cemuLog_logDebug(LogType::Force, "IsDecideSelectButtonError: TRUE");
osLib_returnFromFunction(hCPU, g_errEula.buttonPressed);
}
void export_IsDecideSelectLeftButtonError(PPCInterpreter_t* hCPU)
{
if (g_errEula.buttonPressed)
cemuLog_logDebug(LogType::Force, "IsDecideSelectLeftButtonError: TRUE");
osLib_returnFromFunction(hCPU, g_errEula.buttonPressed);
}
void export_IsDecideSelectRightButtonError(PPCInterpreter_t* hCPU)
{
if (g_errEula.rightButtonPressed)
cemuLog_logDebug(LogType::Force, "IsDecideSelectRightButtonError: TRUE");
osLib_returnFromFunction(hCPU, g_errEula.rightButtonPressed);
}
void export_IsAppearHomeNixSign(PPCInterpreter_t* hCPU)
{
osLib_returnFromFunction(hCPU, g_errEula.homeNixSignVisible);
@ -200,61 +349,19 @@ namespace erreula
osLib_returnFromFunction(hCPU, 0);
}
void export_GetResultType(PPCInterpreter_t* hCPU)
void ErrEulaCalc(ControllerInfo_t* controllerInfo)
{
uint32 result = RESULTTYPE_NONE;
if (g_errEula.buttonPressed || g_errEula.rightButtonPressed)
{
cemuLog_logDebug(LogType::Force, "GetResultType: FINISH");
result = RESULTTYPE_FINISH;
}
osLib_returnFromFunction(hCPU, result);
}
void export_Calc(PPCInterpreter_t* hCPU)
{
ppcDefineParamMEMPTR(controllerInfo, ControllerInfo_t, 0);
// TODO: check controller buttons bla to accept dialog?
osLib_returnFromFunction(hCPU, 0);
if(g_errEula.errEulaInstance)
g_errEula.errEulaInstance->DoCalc();
}
void render(bool mainWindow)
{
if(g_errEula.state == ERREULA_STATE_HIDDEN)
if(!g_errEula.errEulaInstance)
return;
if(g_errEula.state == ERREULA_STATE_APPEARING)
{
if(std::chrono::duration_cast<std::chrono::milliseconds>(tick_cached() - g_errEula.stateTimer).count() <= 1000)
{
return;
}
g_errEula.state = ERREULA_STATE_VISIBLE;
g_errEula.stateTimer = tick_cached();
}
/*else if(g_errEula.state == STATE_VISIBLE)
{
if (std::chrono::duration_cast<std::chrono::milliseconds>(tick_cached() - g_errEula.stateTimer).count() >= 1000)
{
g_errEula.state = STATE_DISAPPEARING;
g_errEula.stateTimer = tick_cached();
return;
}
}*/
else if(g_errEula.state == ERREULA_STATE_DISAPPEARING)
{
if (std::chrono::duration_cast<std::chrono::milliseconds>(tick_cached() - g_errEula.stateTimer).count() >= 2000)
{
g_errEula.state = ERREULA_STATE_HIDDEN;
g_errEula.stateTimer = tick_cached();
}
if(g_errEula.errEulaInstance->GetState() != ErrEulaState::Visible && g_errEula.errEulaInstance->GetState() != ErrEulaState::Appearing && g_errEula.errEulaInstance->GetState() != ErrEulaState::Disappearing)
return;
}
const AppearArg_t& appearArg = g_errEula.currentDialog;
const AppearError& appearArg = g_errEula.currentDialog;
std::string text;
const uint32 errorCode = (uint32)appearArg.errorCode;
if (errorCode != 0)
@ -276,17 +383,28 @@ namespace erreula
ImGui::SetNextWindowPos(position, ImGuiCond_Always, pivot);
ImGui::SetNextWindowBgAlpha(0.9f);
ImGui::PushFont(font);
std::string title;
if (appearArg.title)
title = boost::nowide::narrow(GetText(appearArg.title.GetPtr()));
if(title.empty()) // ImGui doesn't allow empty titles, so set one if appearArg.title is not set or empty
if (title.empty()) // ImGui doesn't allow empty titles, so set one if appearArg.title is not set or empty
title = "ErrEula";
float fadeTransparency = 1.0f;
if (g_errEula.errEulaInstance->GetState() == ErrEulaState::Appearing || g_errEula.errEulaInstance->GetState() == ErrEulaState::Disappearing)
{
fadeTransparency = g_errEula.errEulaInstance->GetFadeTransparency();
}
float originalAlpha = ImGui::GetStyle().Alpha;
ImGui::GetStyle().Alpha = fadeTransparency;
ImGui::SetNextWindowBgAlpha(0.9f * fadeTransparency);
if (ImGui::Begin(title.c_str(), nullptr, kPopupFlags))
{
const float startx = ImGui::GetWindowSize().x / 2.0f;
bool hasLeftButtonPressed = false, hasRightButtonPressed = false;
switch ((uint32)appearArg.errorType)
switch (appearArg.errorType)
{
default:
{
@ -294,11 +412,10 @@ namespace erreula
ImGui::TextUnformatted(text.c_str(), text.c_str() + text.size());
ImGui::Spacing();
ImGui::SetCursorPosX(startx - 50);
g_errEula.buttonPressed |= ImGui::Button("OK", {100, 0});
hasLeftButtonPressed = ImGui::Button("OK", {100, 0});
break;
}
case ERRORTYPE_TEXT:
case ErrorDialogType::Text:
{
std::string txtTmp = "Unknown Error";
if (appearArg.text)
@ -309,10 +426,10 @@ namespace erreula
ImGui::Spacing();
ImGui::SetCursorPosX(startx - 50);
g_errEula.buttonPressed |= ImGui::Button("OK", { 100, 0 });
hasLeftButtonPressed = ImGui::Button("OK", { 100, 0 });
break;
}
case ERRORTYPE_TEXT_ONE_BUTTON:
case ErrorDialogType::TextOneButton:
{
std::string txtTmp = "Unknown Error";
if (appearArg.text)
@ -328,10 +445,10 @@ namespace erreula
float width = std::max(100.0f, ImGui::CalcTextSize(button1.c_str()).x + 10.0f);
ImGui::SetCursorPosX(startx - (width / 2.0f));
g_errEula.buttonPressed |= ImGui::Button(button1.c_str(), { width, 0 });
hasLeftButtonPressed = ImGui::Button(button1.c_str(), { width, 0 });
break;
}
case ERRORTYPE_TEXT_TWO_BUTTON:
case ErrorDialogType::TextTwoButton:
{
std::string txtTmp = "Unknown Error";
if (appearArg.text)
@ -352,42 +469,52 @@ namespace erreula
float width2 = std::max(100.0f, ImGui::CalcTextSize(button2.c_str()).x + 10.0f);
ImGui::SetCursorPosX(startx - (width1 / 2.0f) - (width2 / 2.0f) - 10);
g_errEula.buttonPressed |= ImGui::Button(button1.c_str(), { width1, 0 });
hasLeftButtonPressed = ImGui::Button(button1.c_str(), { width1, 0 });
ImGui::SameLine();
g_errEula.rightButtonPressed |= ImGui::Button(button2.c_str(), { width2, 0 });
hasRightButtonPressed = ImGui::Button(button2.c_str(), { width2, 0 });
break;
}
}
if (!g_errEula.errEulaInstance->IsDecideSelectButtonError())
{
if (hasLeftButtonPressed)
g_errEula.errEulaInstance->SetButtonSelection(ErrEulaInstance::BUTTON_SELECTION::LEFT);
if (hasRightButtonPressed)
g_errEula.errEulaInstance->SetButtonSelection(ErrEulaInstance::BUTTON_SELECTION::RIGHT);
}
}
ImGui::End();
ImGui::PopFont();
if(g_errEula.buttonPressed || g_errEula.rightButtonPressed)
{
g_errEula.state = ERREULA_STATE_DISAPPEARING;
g_errEula.stateTimer = tick_cached();
}
ImGui::GetStyle().Alpha = originalAlpha;
}
void load()
{
g_errEula.errEulaInstance.reset();
OSInitMutexEx(&g_errEula.mutex, nullptr);
//osLib_addFunction("erreula", "ErrEulaCreate__3RplFPUcQ3_2nn7erreula10", export_ErrEulaCreate); // copy ctor?
osLib_addFunction("erreula", "ErrEulaCreate__3RplFPUcQ3_2nn7erreula10RegionTypeQ3_2nn7erreula8LangTypeP8FSClient", export_ErrEulaCreate);
cafeExportRegisterFunc(ErrEulaCreate, "erreula", "ErrEulaCreate__3RplFPUcQ3_2nn7erreula10RegionTypeQ3_2nn7erreula8LangTypeP8FSClient", LogType::Placeholder);
cafeExportRegisterFunc(ErrEulaDestroy, "erreula", "ErrEulaDestroy__3RplFv", LogType::Placeholder);
cafeExportRegisterFunc(IsDecideSelectButtonError, "erreula", "ErrEulaIsDecideSelectButtonError__3RplFv", LogType::Placeholder);
cafeExportRegisterFunc(IsDecideSelectLeftButtonError, "erreula", "ErrEulaIsDecideSelectLeftButtonError__3RplFv", LogType::Placeholder);
cafeExportRegisterFunc(IsDecideSelectRightButtonError, "erreula", "ErrEulaIsDecideSelectRightButtonError__3RplFv", LogType::Placeholder);
cafeExportRegisterFunc(GetResultCode, "erreula", "ErrEulaGetResultCode__3RplFv", LogType::Placeholder);
cafeExportRegisterFunc(GetResultType, "erreula", "ErrEulaGetResultType__3RplFv", LogType::Placeholder);
cafeExportRegisterFunc(ErrEulaAppearError, "erreula", "ErrEulaAppearError__3RplFRCQ3_2nn7erreula9AppearArg", LogType::Placeholder);
cafeExportRegisterFunc(ErrEulaDisappearError, "erreula", "ErrEulaDisappearError__3RplFv", LogType::Placeholder);
cafeExportRegisterFunc(ErrEulaGetStateErrorViewer, "erreula", "ErrEulaGetStateErrorViewer__3RplFv", LogType::Placeholder);
cafeExportRegisterFunc(ErrEulaCalc, "erreula", "ErrEulaCalc__3RplFRCQ3_2nn7erreula14ControllerInfo", LogType::Placeholder);
osLib_addFunction("erreula", "ErrEulaAppearHomeNixSign__3RplFRCQ3_2nn7erreula14HomeNixSignArg", export_AppearHomeNixSign);
osLib_addFunction("erreula", "ErrEulaAppearError__3RplFRCQ3_2nn7erreula9AppearArg", export_AppearError);
osLib_addFunction("erreula", "ErrEulaGetStateErrorViewer__3RplFv", export_GetStateErrorViewer);
osLib_addFunction("erreula", "ErrEulaChangeLang__3RplFQ3_2nn7erreula8LangType", export_ChangeLang);
osLib_addFunction("erreula", "ErrEulaIsDecideSelectButtonError__3RplFv", export_IsDecideSelectButtonError);
osLib_addFunction("erreula", "ErrEulaCalc__3RplFRCQ3_2nn7erreula14ControllerInfo", export_Calc);
osLib_addFunction("erreula", "ErrEulaIsDecideSelectLeftButtonError__3RplFv", export_IsDecideSelectLeftButtonError);
osLib_addFunction("erreula", "ErrEulaIsDecideSelectRightButtonError__3RplFv", export_IsDecideSelectRightButtonError);
osLib_addFunction("erreula", "ErrEulaIsAppearHomeNixSign__3RplFv", export_IsAppearHomeNixSign);
osLib_addFunction("erreula", "ErrEulaDisappearHomeNixSign__3RplFv", export_DisappearHomeNixSign);
osLib_addFunction("erreula", "ErrEulaGetResultType__3RplFv", export_GetResultType);
osLib_addFunction("erreula", "ErrEulaDisappearError__3RplFv", export_DisappearError);
}
}
}

View File

@ -1,4 +1,6 @@
#include "BackendEmulated.h"
#include "Dimensions.h"
#include "Infinity.h"
#include "Skylander.h"
#include "config/CemuConfig.h"
@ -33,5 +35,12 @@ namespace nsyshid::backend::emulated
auto device = std::make_shared<InfinityBaseDevice>();
AttachDevice(device);
}
if (GetConfig().emulated_usb_devices.emulate_dimensions_toypad && !FindDeviceById(0x0E6F, 0x0241))
{
cemuLog_logDebug(LogType::Force, "Attaching Emulated Toypad");
// Add Dimensions Toypad
auto device = std::make_shared<DimensionsToypadDevice>();
AttachDevice(device);
}
}
} // namespace nsyshid::backend::emulated

View File

@ -15,7 +15,7 @@ namespace nsyshid::backend::libusb
if (m_initReturnCode < 0)
{
m_ctx = nullptr;
cemuLog_logDebug(LogType::Force, "nsyshid::BackendLibusb: failed to initialize libusb with return code %i",
cemuLog_logDebug(LogType::Force, "nsyshid::BackendLibusb: failed to initialize libusb, return code: {}",
m_initReturnCode);
return;
}
@ -35,7 +35,7 @@ namespace nsyshid::backend::libusb
if (ret != LIBUSB_SUCCESS)
{
cemuLog_logDebug(LogType::Force,
"nsyshid::BackendLibusb: failed to register hotplug callback with return code %i",
"nsyshid::BackendLibusb: failed to register hotplug callback with return code {}",
ret);
}
else
@ -415,7 +415,7 @@ namespace nsyshid::backend::libusb
if (ret < 0)
{
cemuLog_log(LogType::Force,
"nsyshid::DeviceLibusb::open(): failed to get device descriptor; return code: %i",
"nsyshid::DeviceLibusb::open(): failed to get device descriptor, return code: {}",
ret);
libusb_free_device_list(devices, 1);
return false;
@ -439,8 +439,8 @@ namespace nsyshid::backend::libusb
{
this->m_libusbHandle = nullptr;
cemuLog_log(LogType::Force,
"nsyshid::DeviceLibusb::open(): failed to open device; return code: %i",
ret);
"nsyshid::DeviceLibusb::open(): failed to open device: {}",
libusb_strerror(ret));
libusb_free_device_list(devices, 1);
return false;
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,108 @@
#include <mutex>
#include "nsyshid.h"
#include "Backend.h"
#include "Common/FileStream.h"
namespace nsyshid
{
class DimensionsToypadDevice final : public Device
{
public:
DimensionsToypadDevice();
~DimensionsToypadDevice() = default;
bool Open() override;
void Close() override;
bool IsOpened() override;
ReadResult Read(ReadMessage* message) override;
WriteResult Write(WriteMessage* message) override;
bool GetDescriptor(uint8 descType,
uint8 descIndex,
uint8 lang,
uint8* output,
uint32 outputMaxLength) override;
bool SetProtocol(uint8 ifIndex, uint8 protocol) override;
bool SetReport(ReportMessage* message) override;
private:
bool m_IsOpened;
};
class DimensionsUSB
{
public:
struct DimensionsMini final
{
std::unique_ptr<FileStream> dimFile;
std::array<uint8, 0x2D * 0x04> data{};
uint8 index = 255;
uint8 pad = 255;
uint32 id = 0;
void Save();
};
void SendCommand(std::span<const uint8, 32> buf);
std::array<uint8, 32> GetStatus();
void GenerateRandomNumber(std::span<const uint8, 8> buf, uint8 sequence,
std::array<uint8, 32>& replyBuf);
void InitializeRNG(uint32 seed);
void GetChallengeResponse(std::span<const uint8, 8> buf, uint8 sequence,
std::array<uint8, 32>& replyBuf);
void QueryBlock(uint8 index, uint8 page, std::array<uint8, 32>& replyBuf,
uint8 sequence);
void WriteBlock(uint8 index, uint8 page, std::span<const uint8, 4> toWriteBuf, std::array<uint8, 32>& replyBuf,
uint8 sequence);
void GetModel(std::span<const uint8, 8> buf, uint8 sequence,
std::array<uint8, 32>& replyBuf);
bool RemoveFigure(uint8 pad, uint8 index, bool fullRemove);
bool TempRemove(uint8 index);
bool CancelRemove(uint8 index);
uint32 LoadFigure(const std::array<uint8, 0x2D * 0x04>& buf, std::unique_ptr<FileStream> file, uint8 pad, uint8 index);
bool CreateFigure(fs::path pathName, uint32 id);
bool MoveFigure(uint8 pad, uint8 index, uint8 oldPad, uint8 oldIndex);
static std::map<const uint32, const char*> GetListMinifigs();
static std::map<const uint32, const char*> GetListTokens();
std::string FindFigure(uint32 figNum);
protected:
std::mutex m_dimensionsMutex;
std::array<DimensionsMini, 7> m_figures{};
private:
void RandomUID(std::array<uint8, 0x2D * 0x04>& uidBuffer);
uint8 GenerateChecksum(const std::array<uint8, 32>& data,
int numOfBytes) const;
std::array<uint8, 8> Decrypt(std::span<const uint8, 8> buf, std::optional<std::array<uint8, 16>> key);
std::array<uint8, 8> Encrypt(std::span<const uint8, 8> buf, std::optional<std::array<uint8, 16>> key);
std::array<uint8, 16> GenerateFigureKey(const std::array<uint8, 0x2D * 0x04>& uid);
std::array<uint8, 4> PWDGenerate(const std::array<uint8, 0x2D * 0x04>& uid);
std::array<uint8, 4> DimensionsRandomize(const std::vector<uint8> key, uint8 count);
uint32 GetFigureId(const std::array<uint8, 0x2D * 0x04>& buf);
uint32 Scramble(const std::array<uint8, 7>& uid, uint8 count);
uint32 GetNext();
DimensionsMini& GetFigureByIndex(uint8 index);
uint32 m_randomA;
uint32 m_randomB;
uint32 m_randomC;
uint32 m_randomD;
bool m_isAwake = false;
std::queue<std::array<uint8, 32>> m_figureAddedRemovedResponses;
std::queue<std::array<uint8, 32>> m_queries;
};
extern DimensionsUSB g_dimensionstoypad;
} // namespace nsyshid

View File

@ -427,7 +427,7 @@ namespace proc_ui
}
if(callbackType != ProcUICallbackId::AcquireForeground)
priority = -priority;
AddCallbackInternal(funcPtr, userParam, priority, 0, s_CallbackTables[stdx::to_underlying(callbackType)][coreIndex]);
AddCallbackInternal(funcPtr, userParam, 0, priority, s_CallbackTables[stdx::to_underlying(callbackType)][coreIndex]);
}
void ProcUIRegisterCallback(ProcUICallbackId callbackType, void* funcPtr, void* userParam, sint32 priority)
@ -437,7 +437,7 @@ namespace proc_ui
void ProcUIRegisterBackgroundCallback(void* funcPtr, void* userParam, uint64 tickDelay)
{
AddCallbackInternal(funcPtr, userParam, 0, tickDelay, s_backgroundCallbackList);
AddCallbackInternal(funcPtr, userParam, tickDelay, 0, s_backgroundCallbackList);
}
void FreeCallbackChain(ProcUICallbackList& callbackList)

View File

@ -38,7 +38,7 @@ void CemuConfig::Load(XMLConfigParser& parser)
fullscreen_menubar = parser.get("fullscreen_menubar", false);
feral_gamemode = parser.get("feral_gamemode", false);
check_update = parser.get("check_update", check_update);
receive_untested_updates = parser.get("receive_untested_updates", check_update);
receive_untested_updates = parser.get("receive_untested_updates", receive_untested_updates);
save_screenshot = parser.get("save_screenshot", save_screenshot);
did_show_vulkan_warning = parser.get("vk_warning", did_show_vulkan_warning);
did_show_graphic_pack_download = parser.get("gp_download", did_show_graphic_pack_download);
@ -346,6 +346,7 @@ void CemuConfig::Load(XMLConfigParser& parser)
auto usbdevices = parser.get("EmulatedUsbDevices");
emulated_usb_devices.emulate_skylander_portal = usbdevices.get("EmulateSkylanderPortal", emulated_usb_devices.emulate_skylander_portal);
emulated_usb_devices.emulate_infinity_base = usbdevices.get("EmulateInfinityBase", emulated_usb_devices.emulate_infinity_base);
emulated_usb_devices.emulate_dimensions_toypad = usbdevices.get("EmulateDimensionsToypad", emulated_usb_devices.emulate_dimensions_toypad);
}
void CemuConfig::Save(XMLConfigParser& parser)
@ -545,6 +546,7 @@ void CemuConfig::Save(XMLConfigParser& parser)
auto usbdevices = config.set("EmulatedUsbDevices");
usbdevices.set("EmulateSkylanderPortal", emulated_usb_devices.emulate_skylander_portal.GetValue());
usbdevices.set("EmulateInfinityBase", emulated_usb_devices.emulate_infinity_base.GetValue());
usbdevices.set("EmulateDimensionsToypad", emulated_usb_devices.emulate_dimensions_toypad.GetValue());
}
GameEntry* CemuConfig::GetGameEntryByTitleId(uint64 titleId)

View File

@ -545,6 +545,7 @@ struct CemuConfig
{
ConfigValue<bool> emulate_skylander_portal{false};
ConfigValue<bool> emulate_infinity_base{false};
ConfigValue<bool> emulate_dimensions_toypad{false};
}emulated_usb_devices{};
private:

View File

@ -1,4 +1,4 @@
#include "gui/EmulatedUSBDevices/EmulatedUSBDeviceFrame.h"
#include "EmulatedUSBDeviceFrame.h"
#include <algorithm>
@ -8,14 +8,17 @@
#include "util/helpers/helpers.h"
#include "Cafe/OS/libs/nsyshid/nsyshid.h"
#include "Cafe/OS/libs/nsyshid/Dimensions.h"
#include "Common/FileStream.h"
#include <wx/arrstr.h>
#include <wx/button.h>
#include <wx/combobox.h>
#include <wx/checkbox.h>
#include <wx/combobox.h>
#include <wx/filedlg.h>
#include <wx/log.h>
#include <wx/msgdlg.h>
#include <wx/notebook.h>
#include <wx/panel.h>
@ -29,7 +32,6 @@
#include <wx/wfstream.h>
#include "resource/embedded/resources.h"
#include "EmulatedUSBDeviceFrame.h"
EmulatedUSBDeviceFrame::EmulatedUSBDeviceFrame(wxWindow* parent)
: wxFrame(parent, wxID_ANY, _("Emulated USB Devices"), wxDefaultPosition,
@ -44,6 +46,7 @@ EmulatedUSBDeviceFrame::EmulatedUSBDeviceFrame(wxWindow* parent)
notebook->AddPage(AddSkylanderPage(notebook), _("Skylanders Portal"));
notebook->AddPage(AddInfinityPage(notebook), _("Infinity Base"));
notebook->AddPage(AddDimensionsPage(notebook), _("Dimensions Toypad"));
sizer->Add(notebook, 1, wxEXPAND | wxALL, 2);
@ -120,8 +123,52 @@ wxPanel* EmulatedUSBDeviceFrame::AddInfinityPage(wxNotebook* notebook)
return panel;
}
wxBoxSizer* EmulatedUSBDeviceFrame::AddSkylanderRow(uint8 rowNumber,
wxStaticBox* box)
wxPanel* EmulatedUSBDeviceFrame::AddDimensionsPage(wxNotebook* notebook)
{
auto* panel = new wxPanel(notebook);
auto* panel_sizer = new wxBoxSizer(wxVERTICAL);
auto* box = new wxStaticBox(panel, wxID_ANY, _("Dimensions Manager"));
auto* box_sizer = new wxStaticBoxSizer(box, wxVERTICAL);
auto* row = new wxBoxSizer(wxHORIZONTAL);
m_emulateToypad =
new wxCheckBox(box, wxID_ANY, _("Emulate Dimensions Toypad"));
m_emulateToypad->SetValue(
GetConfig().emulated_usb_devices.emulate_dimensions_toypad);
m_emulateToypad->Bind(wxEVT_CHECKBOX, [this](wxCommandEvent&) {
GetConfig().emulated_usb_devices.emulate_dimensions_toypad =
m_emulateToypad->IsChecked();
g_config.Save();
});
row->Add(m_emulateToypad, 1, wxEXPAND | wxALL, 2);
box_sizer->Add(row, 1, wxEXPAND | wxALL, 2);
auto* top_row = new wxBoxSizer(wxHORIZONTAL);
auto* bottom_row = new wxBoxSizer(wxHORIZONTAL);
auto* dummy = new wxStaticText(box, wxID_ANY, "");
top_row->Add(AddDimensionPanel(2, 0, box), 1, wxEXPAND | wxALL, 2);
top_row->Add(dummy, 1, wxEXPAND | wxLEFT | wxRIGHT, 2);
top_row->Add(AddDimensionPanel(1, 1, box), 1, wxEXPAND | wxALL, 2);
top_row->Add(dummy, 1, wxEXPAND | wxLEFT | wxRIGHT, 2);
top_row->Add(AddDimensionPanel(3, 2, box), 1, wxEXPAND | wxALL, 2);
bottom_row->Add(AddDimensionPanel(2, 3, box), 1, wxEXPAND | wxALL, 2);
bottom_row->Add(AddDimensionPanel(2, 4, box), 1, wxEXPAND | wxALL, 2);
bottom_row->Add(dummy, 1, wxEXPAND | wxLEFT | wxRIGHT, 0);
bottom_row->Add(AddDimensionPanel(3, 5, box), 1, wxEXPAND | wxALL, 2);
bottom_row->Add(AddDimensionPanel(3, 6, box), 1, wxEXPAND | wxALL, 2);
box_sizer->Add(top_row, 1, wxEXPAND | wxALL, 2);
box_sizer->Add(bottom_row, 1, wxEXPAND | wxALL, 2);
panel_sizer->Add(box_sizer, 1, wxEXPAND | wxALL, 2);
panel->SetSizerAndFit(panel_sizer);
return panel;
}
wxBoxSizer* EmulatedUSBDeviceFrame::AddSkylanderRow(uint8 rowNumber, wxStaticBox* box)
{
auto* row = new wxBoxSizer(wxHORIZONTAL);
@ -184,6 +231,44 @@ wxBoxSizer* EmulatedUSBDeviceFrame::AddInfinityRow(wxString name, uint8 rowNumbe
return row;
}
wxBoxSizer* EmulatedUSBDeviceFrame::AddDimensionPanel(uint8 pad, uint8 index, wxStaticBox* box)
{
auto* panel = new wxBoxSizer(wxVERTICAL);
auto* combo_row = new wxBoxSizer(wxHORIZONTAL);
m_dimensionSlots[index] = new wxTextCtrl(box, wxID_ANY, _("None"), wxDefaultPosition, wxDefaultSize,
wxTE_READONLY);
combo_row->Add(m_dimensionSlots[index], 1, wxEXPAND | wxALL, 2);
auto* move_button = new wxButton(box, wxID_ANY, _("Move"));
move_button->Bind(wxEVT_BUTTON, [pad, index, this](wxCommandEvent&) {
MoveMinifig(pad, index);
});
combo_row->Add(move_button, 1, wxEXPAND | wxALL, 2);
auto* button_row = new wxBoxSizer(wxHORIZONTAL);
auto* load_button = new wxButton(box, wxID_ANY, _("Load"));
load_button->Bind(wxEVT_BUTTON, [pad, index, this](wxCommandEvent&) {
LoadMinifig(pad, index);
});
auto* clear_button = new wxButton(box, wxID_ANY, _("Clear"));
clear_button->Bind(wxEVT_BUTTON, [pad, index, this](wxCommandEvent&) {
ClearMinifig(pad, index);
});
auto* create_button = new wxButton(box, wxID_ANY, _("Create"));
create_button->Bind(wxEVT_BUTTON, [pad, index, this](wxCommandEvent&) {
CreateMinifig(pad, index);
});
button_row->Add(clear_button, 1, wxEXPAND | wxALL, 2);
button_row->Add(create_button, 1, wxEXPAND | wxALL, 2);
button_row->Add(load_button, 1, wxEXPAND | wxALL, 2);
panel->Add(combo_row, 1, wxEXPAND | wxALL, 2);
panel->Add(button_row, 1, wxEXPAND | wxALL, 2);
return panel;
}
void EmulatedUSBDeviceFrame::LoadSkylander(uint8 slot)
{
wxFileDialog openFileDialog(this, _("Open Skylander dump"), "", "",
@ -307,8 +392,8 @@ CreateSkylanderDialog::CreateSkylanderDialog(wxWindow* parent, uint8 slot)
return;
m_filePath = saveFileDialog.GetPath();
if(!nsyshid::g_skyportal.CreateSkylander(_utf8ToPath(m_filePath.utf8_string()), skyId, skyVar))
if (!nsyshid::g_skyportal.CreateSkylander(_utf8ToPath(m_filePath.utf8_string()), skyId, skyVar))
{
wxMessageDialog errorMessage(this, "Failed to create file");
errorMessage.ShowModal();
@ -351,6 +436,80 @@ wxString CreateSkylanderDialog::GetFilePath() const
return m_filePath;
}
void EmulatedUSBDeviceFrame::UpdateSkylanderEdits()
{
for (auto i = 0; i < nsyshid::MAX_SKYLANDERS; i++)
{
std::string displayString;
if (auto sd = m_skySlots[i])
{
auto [portalSlot, skyId, skyVar] = sd.value();
displayString = nsyshid::g_skyportal.FindSkylander(skyId, skyVar);
}
else
{
displayString = "None";
}
m_skylanderSlots[i]->ChangeValue(displayString);
}
}
void EmulatedUSBDeviceFrame::LoadFigure(uint8 slot)
{
wxFileDialog openFileDialog(this, _("Open Infinity Figure dump"), "", "",
"BIN files (*.bin)|*.bin",
wxFD_OPEN | wxFD_FILE_MUST_EXIST);
if (openFileDialog.ShowModal() != wxID_OK || openFileDialog.GetPath().empty())
{
wxMessageDialog errorMessage(this, "File Okay Error");
errorMessage.ShowModal();
return;
}
LoadFigurePath(slot, openFileDialog.GetPath());
}
void EmulatedUSBDeviceFrame::LoadFigurePath(uint8 slot, wxString path)
{
std::unique_ptr<FileStream> infFile(FileStream::openFile2(_utf8ToPath(path.utf8_string()), true));
if (!infFile)
{
wxMessageDialog errorMessage(this, "File Open Error");
errorMessage.ShowModal();
return;
}
std::array<uint8, nsyshid::INF_FIGURE_SIZE> fileData;
if (infFile->readData(fileData.data(), fileData.size()) != fileData.size())
{
wxMessageDialog open_error(this, "Failed to read file! File was too small");
open_error.ShowModal();
return;
}
ClearFigure(slot);
uint32 number = nsyshid::g_infinitybase.LoadFigure(fileData, std::move(infFile), slot);
m_infinitySlots[slot]->ChangeValue(nsyshid::g_infinitybase.FindFigure(number).second);
}
void EmulatedUSBDeviceFrame::CreateFigure(uint8 slot)
{
cemuLog_log(LogType::Force, "Create Figure: {}", slot);
CreateInfinityFigureDialog create_dlg(this, slot);
create_dlg.ShowModal();
if (create_dlg.GetReturnCode() == 1)
{
LoadFigurePath(slot, create_dlg.GetFilePath());
}
}
void EmulatedUSBDeviceFrame::ClearFigure(uint8 slot)
{
m_infinitySlots[slot]->ChangeValue("None");
nsyshid::g_infinitybase.RemoveFigure(slot);
}
CreateInfinityFigureDialog::CreateInfinityFigureDialog(wxWindow* parent, uint8 slot)
: wxDialog(parent, wxID_ANY, _("Infinity Figure Creator"), wxDefaultPosition, wxSize(500, 150))
{
@ -447,76 +606,231 @@ wxString CreateInfinityFigureDialog::GetFilePath() const
return m_filePath;
}
void EmulatedUSBDeviceFrame::LoadFigure(uint8 slot)
void EmulatedUSBDeviceFrame::LoadMinifig(uint8 pad, uint8 index)
{
wxFileDialog openFileDialog(this, _("Open Infinity Figure dump"), "", "",
"BIN files (*.bin)|*.bin",
wxFileDialog openFileDialog(this, _("Load Dimensions Figure"), "", "",
"Dimensions files (*.bin)|*.bin",
wxFD_OPEN | wxFD_FILE_MUST_EXIST);
if (openFileDialog.ShowModal() != wxID_OK || openFileDialog.GetPath().empty())
return;
LoadMinifigPath(openFileDialog.GetPath(), pad, index);
}
void EmulatedUSBDeviceFrame::LoadMinifigPath(wxString path_name, uint8 pad, uint8 index)
{
std::unique_ptr<FileStream> dim_file(FileStream::openFile2(_utf8ToPath(path_name.utf8_string()), true));
if (!dim_file)
{
wxMessageDialog errorMessage(this, "File Okay Error");
wxMessageDialog errorMessage(this, "Failed to open minifig file");
errorMessage.ShowModal();
return;
}
LoadFigurePath(slot, openFileDialog.GetPath());
}
std::array<uint8, 0x2D * 0x04> file_data;
void EmulatedUSBDeviceFrame::LoadFigurePath(uint8 slot, wxString path)
{
std::unique_ptr<FileStream> infFile(FileStream::openFile2(_utf8ToPath(path.utf8_string()), true));
if (!infFile)
if (dim_file->readData(file_data.data(), file_data.size()) != file_data.size())
{
wxMessageDialog errorMessage(this, "File Open Error");
wxMessageDialog errorMessage(this, "Failed to read minifig file data");
errorMessage.ShowModal();
return;
}
std::array<uint8, nsyshid::INF_FIGURE_SIZE> fileData;
if (infFile->readData(fileData.data(), fileData.size()) != fileData.size())
{
wxMessageDialog open_error(this, "Failed to read file! File was too small");
open_error.ShowModal();
return;
}
ClearFigure(slot);
ClearMinifig(pad, index);
uint32 number = nsyshid::g_infinitybase.LoadFigure(fileData, std::move(infFile), slot);
m_infinitySlots[slot]->ChangeValue(nsyshid::g_infinitybase.FindFigure(number).second);
uint32 id = nsyshid::g_dimensionstoypad.LoadFigure(file_data, std::move(dim_file), pad, index);
m_dimensionSlots[index]->ChangeValue(nsyshid::g_dimensionstoypad.FindFigure(id));
m_dimSlots[index] = id;
}
void EmulatedUSBDeviceFrame::CreateFigure(uint8 slot)
void EmulatedUSBDeviceFrame::ClearMinifig(uint8 pad, uint8 index)
{
cemuLog_log(LogType::Force, "Create Figure: {}", slot);
CreateInfinityFigureDialog create_dlg(this, slot);
nsyshid::g_dimensionstoypad.RemoveFigure(pad, index, true);
m_dimensionSlots[index]->ChangeValue("None");
m_dimSlots[index] = std::nullopt;
}
void EmulatedUSBDeviceFrame::CreateMinifig(uint8 pad, uint8 index)
{
CreateDimensionFigureDialog create_dlg(this);
create_dlg.ShowModal();
if (create_dlg.GetReturnCode() == 1)
{
LoadFigurePath(slot, create_dlg.GetFilePath());
LoadMinifigPath(create_dlg.GetFilePath(), pad, index);
}
}
void EmulatedUSBDeviceFrame::ClearFigure(uint8 slot)
void EmulatedUSBDeviceFrame::MoveMinifig(uint8 pad, uint8 index)
{
m_infinitySlots[slot]->ChangeValue("None");
nsyshid::g_infinitybase.RemoveFigure(slot);
}
if (!m_dimSlots[index])
return;
void EmulatedUSBDeviceFrame::UpdateSkylanderEdits()
{
for (auto i = 0; i < nsyshid::MAX_SKYLANDERS; i++)
MoveDimensionFigureDialog move_dlg(this, index);
nsyshid::g_dimensionstoypad.TempRemove(index);
move_dlg.ShowModal();
if (move_dlg.GetReturnCode() == 1)
{
std::string displayString;
if (auto sd = m_skySlots[i])
nsyshid::g_dimensionstoypad.MoveFigure(move_dlg.GetNewPad(), move_dlg.GetNewIndex(), pad, index);
if (index != move_dlg.GetNewIndex())
{
auto [portalSlot, skyId, skyVar] = sd.value();
displayString = nsyshid::g_skyportal.FindSkylander(skyId, skyVar);
m_dimSlots[move_dlg.GetNewIndex()] = m_dimSlots[index];
m_dimensionSlots[move_dlg.GetNewIndex()]->ChangeValue(m_dimensionSlots[index]->GetValue());
m_dimSlots[index] = std::nullopt;
m_dimensionSlots[index]->ChangeValue("None");
}
else
{
displayString = "None";
}
m_skylanderSlots[i]->ChangeValue(displayString);
}
else
{
nsyshid::g_dimensionstoypad.CancelRemove(index);
}
}
CreateDimensionFigureDialog::CreateDimensionFigureDialog(wxWindow* parent)
: wxDialog(parent, wxID_ANY, _("Dimensions Figure Creator"), wxDefaultPosition, wxSize(500, 200))
{
auto* sizer = new wxBoxSizer(wxVERTICAL);
auto* comboRow = new wxBoxSizer(wxHORIZONTAL);
auto* comboBox = new wxComboBox(this, wxID_ANY);
comboBox->Append("---Select---", reinterpret_cast<void*>(0xFFFFFFFF));
wxArrayString filterlist;
for (const auto& it : nsyshid::g_dimensionstoypad.GetListMinifigs())
{
const uint32 figure = it.first;
comboBox->Append(it.second, reinterpret_cast<void*>(figure));
filterlist.Add(it.second);
}
comboBox->SetSelection(0);
bool enabled = comboBox->AutoComplete(filterlist);
comboRow->Add(comboBox, 1, wxEXPAND | wxALL, 2);
auto* figNumRow = new wxBoxSizer(wxHORIZONTAL);
wxIntegerValidator<uint32> validator;
auto* labelFigNum = new wxStaticText(this, wxID_ANY, "Figure Number:");
auto* editFigNum = new wxTextCtrl(this, wxID_ANY, _("0"), wxDefaultPosition, wxDefaultSize, 0, validator);
figNumRow->Add(labelFigNum, 1, wxALL, 5);
figNumRow->Add(editFigNum, 1, wxALL, 5);
auto* buttonRow = new wxBoxSizer(wxHORIZONTAL);
auto* createButton = new wxButton(this, wxID_ANY, _("Create"));
createButton->Bind(wxEVT_BUTTON, [editFigNum, this](wxCommandEvent&) {
long longFigNum;
if (!editFigNum->GetValue().ToLong(&longFigNum) || longFigNum > 0xFFFF)
{
wxMessageDialog idError(this, "Error Converting Figure Number!", "Number Entered is Invalid");
idError.ShowModal();
this->EndModal(0);
}
uint16 figNum = longFigNum & 0xFFFF;
auto figure = nsyshid::g_dimensionstoypad.FindFigure(figNum);
wxString predefName = figure + ".bin";
wxFileDialog
saveFileDialog(this, _("Create Dimensions Figure file"), "", predefName,
"BIN files (*.bin)|*.bin", wxFD_SAVE | wxFD_OVERWRITE_PROMPT);
if (saveFileDialog.ShowModal() == wxID_CANCEL)
this->EndModal(0);
m_filePath = saveFileDialog.GetPath();
nsyshid::g_dimensionstoypad.CreateFigure(_utf8ToPath(m_filePath.utf8_string()), figNum);
this->EndModal(1);
});
auto* cancelButton = new wxButton(this, wxID_ANY, _("Cancel"));
cancelButton->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) {
this->EndModal(0);
});
comboBox->Bind(wxEVT_COMBOBOX, [comboBox, editFigNum, this](wxCommandEvent&) {
const uint64 fig_info = reinterpret_cast<uint64>(comboBox->GetClientData(comboBox->GetSelection()));
if (fig_info != 0xFFFF)
{
const uint16 figNum = fig_info & 0xFFFF;
editFigNum->SetValue(wxString::Format(wxT("%i"), figNum));
}
});
buttonRow->Add(createButton, 1, wxALL, 5);
buttonRow->Add(cancelButton, 1, wxALL, 5);
sizer->Add(comboRow, 1, wxEXPAND | wxALL, 2);
sizer->Add(figNumRow, 1, wxEXPAND | wxALL, 2);
sizer->Add(buttonRow, 1, wxEXPAND | wxALL, 2);
this->SetSizer(sizer);
this->Centre(wxBOTH);
}
wxString CreateDimensionFigureDialog::GetFilePath() const
{
return m_filePath;
}
MoveDimensionFigureDialog::MoveDimensionFigureDialog(EmulatedUSBDeviceFrame* parent, uint8 currentIndex)
: wxDialog(parent, wxID_ANY, _("Dimensions Figure Mover"), wxDefaultPosition, wxSize(700, 300))
{
auto* sizer = new wxGridSizer(2, 5, 10, 10);
std::array<std::optional<uint32>, 7> ids = parent->GetCurrentMinifigs();
sizer->Add(AddMinifigSlot(2, 0, currentIndex, ids[0]), 1, wxALL, 5);
sizer->Add(new wxStaticText(this, wxID_ANY, ""), 1, wxALL, 5);
sizer->Add(AddMinifigSlot(1, 1, currentIndex, ids[1]), 1, wxALL, 5);
sizer->Add(new wxStaticText(this, wxID_ANY, ""), 1, wxALL, 5);
sizer->Add(AddMinifigSlot(3, 2, currentIndex, ids[2]), 1, wxALL, 5);
sizer->Add(AddMinifigSlot(2, 3, currentIndex, ids[3]), 1, wxALL, 5);
sizer->Add(AddMinifigSlot(2, 4, currentIndex, ids[4]), 1, wxALL, 5);
sizer->Add(new wxStaticText(this, wxID_ANY, ""), 1, wxALL, 5);
sizer->Add(AddMinifigSlot(3, 5, currentIndex, ids[5]), 1, wxALL, 5);
sizer->Add(AddMinifigSlot(3, 6, currentIndex, ids[6]), 1, wxALL, 5);
this->SetSizer(sizer);
this->Centre(wxBOTH);
}
wxBoxSizer* MoveDimensionFigureDialog::AddMinifigSlot(uint8 pad, uint8 index, uint8 currentIndex, std::optional<uint32> currentId)
{
auto* panel = new wxBoxSizer(wxVERTICAL);
auto* label = new wxStaticText(this, wxID_ANY, "None");
if (currentId)
label->SetLabel(nsyshid::g_dimensionstoypad.FindFigure(currentId.value()));
auto* moveButton = new wxButton(this, wxID_ANY, _("Move Here"));
if (index == currentIndex)
moveButton->SetLabelText("Pick up and Place");
moveButton->Bind(wxEVT_BUTTON, [pad, index, this](wxCommandEvent&) {
m_newPad = pad;
m_newIndex = index;
this->EndModal(1);
});
panel->Add(label, 1, wxALL, 5);
panel->Add(moveButton, 1, wxALL, 5);
return panel;
}
uint8 MoveDimensionFigureDialog::GetNewPad() const
{
return m_newPad;
}
uint8 MoveDimensionFigureDialog::GetNewIndex() const
{
return m_newIndex;
}
std::array<std::optional<uint32>, 7> EmulatedUSBDeviceFrame::GetCurrentMinifigs()
{
return m_dimSlots;
}

View File

@ -17,33 +17,47 @@ class wxStaticBox;
class wxString;
class wxTextCtrl;
class EmulatedUSBDeviceFrame : public wxFrame {
class EmulatedUSBDeviceFrame : public wxFrame
{
public:
EmulatedUSBDeviceFrame(wxWindow* parent);
~EmulatedUSBDeviceFrame();
std::array<std::optional<uint32>, 7> GetCurrentMinifigs();
private:
wxCheckBox* m_emulatePortal;
wxCheckBox* m_emulateBase;
wxCheckBox* m_emulateToypad;
std::array<wxTextCtrl*, nsyshid::MAX_SKYLANDERS> m_skylanderSlots;
std::array<wxTextCtrl*, nsyshid::MAX_FIGURES> m_infinitySlots;
std::array<wxTextCtrl*, 7> m_dimensionSlots;
std::array<std::optional<std::tuple<uint8, uint16, uint16>>, nsyshid::MAX_SKYLANDERS> m_skySlots;
std::array<std::optional<uint32>, 7> m_dimSlots;
wxPanel* AddSkylanderPage(wxNotebook* notebook);
wxPanel* AddInfinityPage(wxNotebook* notebook);
wxPanel* AddDimensionsPage(wxNotebook* notebook);
wxBoxSizer* AddSkylanderRow(uint8 row_number, wxStaticBox* box);
wxBoxSizer* AddInfinityRow(wxString name, uint8 row_number, wxStaticBox* box);
wxBoxSizer* AddDimensionPanel(uint8 pad, uint8 index, wxStaticBox* box);
void LoadSkylander(uint8 slot);
void LoadSkylanderPath(uint8 slot, wxString path);
void CreateSkylander(uint8 slot);
void ClearSkylander(uint8 slot);
void UpdateSkylanderEdits();
void LoadFigure(uint8 slot);
void LoadFigurePath(uint8 slot, wxString path);
void CreateFigure(uint8 slot);
void ClearFigure(uint8 slot);
void UpdateSkylanderEdits();
void LoadMinifig(uint8 pad, uint8 index);
void LoadMinifigPath(wxString path_name, uint8 pad, uint8 index);
void CreateMinifig(uint8 pad, uint8 index);
void ClearMinifig(uint8 pad, uint8 index);
void MoveMinifig(uint8 pad, uint8 index);
};
class CreateSkylanderDialog : public wxDialog {
class CreateSkylanderDialog : public wxDialog
{
public:
explicit CreateSkylanderDialog(wxWindow* parent, uint8 slot);
wxString GetFilePath() const;
@ -52,11 +66,37 @@ class CreateSkylanderDialog : public wxDialog {
wxString m_filePath;
};
class CreateInfinityFigureDialog : public wxDialog {
class CreateInfinityFigureDialog : public wxDialog
{
public:
explicit CreateInfinityFigureDialog(wxWindow* parent, uint8 slot);
wxString GetFilePath() const;
protected:
wxString m_filePath;
};
class CreateDimensionFigureDialog : public wxDialog
{
public:
explicit CreateDimensionFigureDialog(wxWindow* parent);
wxString GetFilePath() const;
protected:
wxString m_filePath;
};
class MoveDimensionFigureDialog : public wxDialog
{
public:
explicit MoveDimensionFigureDialog(EmulatedUSBDeviceFrame* parent, uint8 currentIndex);
uint8 GetNewPad() const;
uint8 GetNewIndex() const;
protected:
uint8 m_newIndex = 0;
uint8 m_newPad = 0;
private:
wxBoxSizer* AddMinifigSlot(uint8 pad, uint8 index, uint8 oldIndex, std::optional<uint32> currentId);
};

View File

@ -484,20 +484,20 @@ bool MainWindow::FileLoad(const fs::path launchPath, wxLaunchGameEvent::INITIATE
wxMessageBox(t, _("Error"), wxOK | wxCENTRE | wxICON_ERROR);
return false;
}
CafeSystem::STATUS_CODE r = CafeSystem::PrepareForegroundTitle(baseTitleId);
if (r == CafeSystem::STATUS_CODE::INVALID_RPX)
CafeSystem::PREPARE_STATUS_CODE r = CafeSystem::PrepareForegroundTitle(baseTitleId);
if (r == CafeSystem::PREPARE_STATUS_CODE::INVALID_RPX)
{
cemu_assert_debug(false);
return false;
}
else if (r == CafeSystem::STATUS_CODE::UNABLE_TO_MOUNT)
else if (r == CafeSystem::PREPARE_STATUS_CODE::UNABLE_TO_MOUNT)
{
wxString t = _("Unable to mount title.\nMake sure the configured game paths are still valid and refresh the game list.\n\nFile which failed to load:\n");
t.append(_pathToUtf8(launchPath));
wxMessageBox(t, _("Error"), wxOK | wxCENTRE | wxICON_ERROR);
return false;
}
else if (r != CafeSystem::STATUS_CODE::SUCCESS)
else if (r != CafeSystem::PREPARE_STATUS_CODE::SUCCESS)
{
wxString t = _("Failed to launch game.");
t.append(_pathToUtf8(launchPath));
@ -512,8 +512,8 @@ bool MainWindow::FileLoad(const fs::path launchPath, wxLaunchGameEvent::INITIATE
CafeTitleFileType fileType = DetermineCafeSystemFileType(launchPath);
if (fileType == CafeTitleFileType::RPX || fileType == CafeTitleFileType::ELF)
{
CafeSystem::STATUS_CODE r = CafeSystem::PrepareForegroundTitleFromStandaloneRPX(launchPath);
if (r != CafeSystem::STATUS_CODE::SUCCESS)
CafeSystem::PREPARE_STATUS_CODE r = CafeSystem::PrepareForegroundTitleFromStandaloneRPX(launchPath);
if (r != CafeSystem::PREPARE_STATUS_CODE::SUCCESS)
{
cemu_assert_debug(false); // todo
wxString t = _("Failed to launch executable. Path: ");