Add installer wizard.

This commit is contained in:
Dario 2024-11-30 16:49:20 -03:00
parent ca7e170f4d
commit aeadcfcf90
13 changed files with 1158 additions and 100 deletions

View file

@ -85,6 +85,7 @@ set(SWA_PATCHES_CXX_SOURCES
set(SWA_UI_CXX_SOURCES set(SWA_UI_CXX_SOURCES
"ui/achievement_menu.cpp" "ui/achievement_menu.cpp"
"ui/achievement_overlay.cpp" "ui/achievement_overlay.cpp"
"ui/installer_wizard.cpp"
"ui/options_menu.cpp" "ui/options_menu.cpp"
"ui/sdl_listener.cpp" "ui/sdl_listener.cpp"
"ui/window.cpp" "ui/window.cpp"
@ -106,6 +107,16 @@ set(SWA_INSTALL_CXX_SOURCES
"install/hashes/update.cpp" "install/hashes/update.cpp"
) )
set(INSTALLER_IMAGES_DIR "../UnleashedRecompResources/images/installer")
BIN2H(SOURCE_FILE "${INSTALLER_IMAGES_DIR}/install_001.dds" HEADER_FILE "res/install_001_dds.h" ARRAY_TYPE "unsigned char" VARIABLE_NAME "g_install001DDS")
BIN2H(SOURCE_FILE "${INSTALLER_IMAGES_DIR}/install_002.dds" HEADER_FILE "res/install_002_dds.h" ARRAY_TYPE "unsigned char" VARIABLE_NAME "g_install002DDS")
BIN2H(SOURCE_FILE "${INSTALLER_IMAGES_DIR}/install_003.dds" HEADER_FILE "res/install_003_dds.h" ARRAY_TYPE "unsigned char" VARIABLE_NAME "g_install003DDS")
BIN2H(SOURCE_FILE "${INSTALLER_IMAGES_DIR}/install_004.dds" HEADER_FILE "res/install_004_dds.h" ARRAY_TYPE "unsigned char" VARIABLE_NAME "g_install004DDS")
BIN2H(SOURCE_FILE "${INSTALLER_IMAGES_DIR}/install_005.dds" HEADER_FILE "res/install_005_dds.h" ARRAY_TYPE "unsigned char" VARIABLE_NAME "g_install005DDS")
BIN2H(SOURCE_FILE "${INSTALLER_IMAGES_DIR}/install_006.dds" HEADER_FILE "res/install_006_dds.h" ARRAY_TYPE "unsigned char" VARIABLE_NAME "g_install006DDS")
BIN2H(SOURCE_FILE "${INSTALLER_IMAGES_DIR}/install_007.dds" HEADER_FILE "res/install_007_dds.h" ARRAY_TYPE "unsigned char" VARIABLE_NAME "g_install007DDS")
BIN2H(SOURCE_FILE "${INSTALLER_IMAGES_DIR}/install_008.dds" HEADER_FILE "res/install_008_dds.h" ARRAY_TYPE "unsigned char" VARIABLE_NAME "g_install008DDS")
set(LIBMSPACK_PATH ${SWA_THIRDPARTY_ROOT}/libmspack/libmspack/mspack) set(LIBMSPACK_PATH ${SWA_THIRDPARTY_ROOT}/libmspack/libmspack/mspack)
set(LIBMSPACK_C_SOURCES set(LIBMSPACK_C_SOURCES
@ -172,6 +183,7 @@ find_package(imgui CONFIG REQUIRED)
find_package(magic_enum CONFIG REQUIRED) find_package(magic_enum CONFIG REQUIRED)
find_package(unofficial-tiny-aes-c CONFIG REQUIRED) find_package(unofficial-tiny-aes-c CONFIG REQUIRED)
find_path(READERWRITERQUEUE_INCLUDE_DIRS "readerwriterqueue/atomicops.h") find_path(READERWRITERQUEUE_INCLUDE_DIRS "readerwriterqueue/atomicops.h")
find_package(nfd CONFIG REQUIRED)
file(MAKE_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/D3D12) file(MAKE_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/D3D12)
add_custom_command(TARGET UnleashedRecomp POST_BUILD add_custom_command(TARGET UnleashedRecomp POST_BUILD
@ -209,6 +221,7 @@ target_link_libraries(UnleashedRecomp PRIVATE
imgui::imgui imgui::imgui
magic_enum::magic_enum magic_enum::magic_enum
unofficial::tiny-aes-c::tiny-aes-c unofficial::tiny-aes-c::tiny-aes-c
nfd::nfd
) )
target_include_directories(UnleashedRecomp PRIVATE target_include_directories(UnleashedRecomp PRIVATE

View file

@ -12,6 +12,7 @@
#include <ui/achievement_menu.h> #include <ui/achievement_menu.h>
#include <ui/achievement_overlay.h> #include <ui/achievement_overlay.h>
#include <ui/options_menu.h> #include <ui/options_menu.h>
#include <ui/installer_wizard.h>
#include "imgui_snapshot.h" #include "imgui_snapshot.h"
#include "imgui_common.h" #include "imgui_common.h"
@ -1066,6 +1067,7 @@ static void CreateImGuiBackend()
AchievementMenu::Init(); AchievementMenu::Init();
AchievementOverlay::Init(); AchievementOverlay::Init();
OptionsMenu::Init(); OptionsMenu::Init();
InstallerWizard::Init();
ImGui_ImplSDL2_InitForOther(Window::s_pWindow); ImGui_ImplSDL2_InitForOther(Window::s_pWindow);
@ -1174,7 +1176,9 @@ static void CreateImGuiBackend()
g_imPipeline = g_device->createGraphicsPipeline(pipelineDesc); g_imPipeline = g_device->createGraphicsPipeline(pipelineDesc);
} }
static void CreateHostDevice() static void BeginCommandList();
void Video::CreateHostDevice()
{ {
for (uint32_t i = 0; i < 16; i++) for (uint32_t i = 0; i < 16; i++)
g_inputSlots[i].index = i; g_inputSlots[i].index = i;
@ -1364,17 +1368,19 @@ static void CreateHostDevice()
desc.renderTargetCount = 1; desc.renderTargetCount = 1;
g_gammaCorrectionPipeline = g_device->createGraphicsPipeline(desc); g_gammaCorrectionPipeline = g_device->createGraphicsPipeline(desc);
g_xdbfTextureCache = std::unordered_map<uint16_t, GuestTexture*>(); g_backBuffer = g_userHeap.AllocPhysical<GuestSurface>(ResourceType::RenderTarget);
g_backBuffer->width = 1280;
g_backBuffer->height = 720;
g_backBuffer->format = BACKBUFFER_FORMAT;
g_backBuffer->textureHolder = g_device->createTexture(RenderTextureDesc::Texture2D(1, 1, 1, BACKBUFFER_FORMAT, RenderTextureFlag::RENDER_TARGET));
for (auto& achievement : g_xdbfWrapper.GetAchievements(XDBF_LANGUAGE_ENGLISH)) BeginCommandList();
{
// huh?
if (!achievement.pImageBuffer || !achievement.ImageBufferSize)
continue;
g_xdbfTextureCache[achievement.ID] = RenderTextureBarrier blankTextureBarriers[TEXTURE_DESCRIPTOR_NULL_COUNT];
LoadTexture((uint8_t*)achievement.pImageBuffer, achievement.ImageBufferSize).release(); for (size_t i = 0; i < TEXTURE_DESCRIPTOR_NULL_COUNT; i++)
} blankTextureBarriers[i] = RenderTextureBarrier(g_blankTextures[i].get(), RenderTextureLayout::SHADER_READ);
g_commandLists[g_frame]->barriers(RenderBarrierStage::NONE, blankTextureBarriers, std::size(blankTextureBarriers));
} }
static void WaitForGPU() static void WaitForGPU()
@ -1398,12 +1404,11 @@ static void WaitForGPU()
} }
} }
static bool g_pendingRenderThread; static std::atomic<bool> g_pendingRenderThread;
static void WaitForRenderThread() static void WaitForRenderThread()
{ {
while (g_pendingRenderThread) g_pendingRenderThread.wait(true);
Sleep(0);
} }
static void BeginCommandList() static void BeginCommandList()
@ -1491,21 +1496,17 @@ static void BeginCommandList()
static uint32_t CreateDevice(uint32_t a1, uint32_t a2, uint32_t a3, uint32_t a4, uint32_t a5, be<uint32_t>* a6) static uint32_t CreateDevice(uint32_t a1, uint32_t a2, uint32_t a3, uint32_t a4, uint32_t a5, be<uint32_t>* a6)
{ {
CreateHostDevice(); g_xdbfTextureCache = std::unordered_map<uint16_t, GuestTexture *>();
g_backBuffer = g_userHeap.AllocPhysical<GuestSurface>(ResourceType::RenderTarget); for (auto &achievement : g_xdbfWrapper.GetAchievements(XDBF_LANGUAGE_ENGLISH))
g_backBuffer->width = 1280; {
g_backBuffer->height = 720; // huh?
g_backBuffer->format = BACKBUFFER_FORMAT; if (!achievement.pImageBuffer || !achievement.ImageBufferSize)
g_backBuffer->textureHolder = g_device->createTexture(RenderTextureDesc::Texture2D(1, 1, 1, BACKBUFFER_FORMAT, RenderTextureFlag::RENDER_TARGET)); continue;
BeginCommandList(); g_xdbfTextureCache[achievement.ID] =
LoadTexture((uint8_t *)achievement.pImageBuffer, achievement.ImageBufferSize).release();
RenderTextureBarrier blankTextureBarriers[TEXTURE_DESCRIPTOR_NULL_COUNT]; }
for (size_t i = 0; i < TEXTURE_DESCRIPTOR_NULL_COUNT; i++)
blankTextureBarriers[i] = RenderTextureBarrier(g_blankTextures[i].get(), RenderTextureLayout::SHADER_READ);
g_commandLists[g_frame]->barriers(RenderBarrierStage::NONE, blankTextureBarriers, std::size(blankTextureBarriers));
auto device = g_userHeap.AllocPhysical<GuestDevice>(); auto device = g_userHeap.AllocPhysical<GuestDevice>();
memset(device, 0, sizeof(*device)); memset(device, 0, sizeof(*device));
@ -1739,6 +1740,7 @@ static void DrawImGui()
AchievementMenu::Draw(); AchievementMenu::Draw();
OptionsMenu::Draw(); OptionsMenu::Draw();
AchievementOverlay::Draw(); AchievementOverlay::Draw();
InstallerWizard::Draw();
ImGui::Render(); ImGui::Render();
@ -1753,8 +1755,17 @@ static void DrawImGui()
} }
} }
static void SetFramebuffer(GuestSurface *renderTarget, GuestSurface *depthStencil, bool settingForClear);
static void FlushViewport();
static void ProcDrawImGui(const RenderCommand& cmd) static void ProcDrawImGui(const RenderCommand& cmd)
{ {
// Make sure the backbuffer is the current target.
AddBarrier(g_backBuffer, RenderTextureLayout::COLOR_WRITE);
FlushBarriers();
SetFramebuffer(g_backBuffer, nullptr, false);
FlushViewport();
auto& commandList = g_commandLists[g_frame]; auto& commandList = g_commandLists[g_frame];
commandList->setGraphicsPipelineLayout(g_imPipelineLayout.get()); commandList->setGraphicsPipelineLayout(g_imPipelineLayout.get());
@ -1828,21 +1839,21 @@ static void ProcDrawImGui(const RenderCommand& cmd)
} }
} }
static bool g_precompiledPipelineStateCache = false; static bool g_shouldPrecompilePipelines = false;
static void Present() void Video::HostPresent()
{ {
DrawImGui();
WaitForRenderThread(); WaitForRenderThread();
DrawImGui();
g_pendingRenderThread = true; g_pendingRenderThread.store(true);
RenderCommand cmd; RenderCommand cmd;
cmd.type = RenderCommandType::Present; cmd.type = RenderCommandType::Present;
g_renderQueue.enqueue(cmd); g_renderQueue.enqueue(cmd);
// All the shaders are available at this point. We can precompile embedded PSOs then. // All the shaders are available at this point. We can precompile embedded PSOs then.
if (!g_precompiledPipelineStateCache) if (g_shouldPrecompilePipelines)
{ {
// This is all the model consumer thread needs to see. // This is all the model consumer thread needs to see.
++g_compilingDataCount; ++g_compilingDataCount;
@ -1850,10 +1861,20 @@ static void Present()
if ((++g_pendingDataCount) == 1) if ((++g_pendingDataCount) == 1)
g_pendingDataCount.notify_all(); g_pendingDataCount.notify_all();
g_precompiledPipelineStateCache = true; g_shouldPrecompilePipelines = false;
} }
} }
void Video::StartPipelinePrecompilation()
{
g_shouldPrecompilePipelines = true;
}
static void GuestPresent()
{
Video::HostPresent();
}
static void SetRootDescriptor(const UploadAllocation& allocation, size_t index) static void SetRootDescriptor(const UploadAllocation& allocation, size_t index)
{ {
auto& commandList = g_commandLists[g_frame]; auto& commandList = g_commandLists[g_frame];
@ -1899,11 +1920,11 @@ static void ProcPresent(const RenderCommand& cmd)
constants.gammaB = 1.0f / std::clamp(constants.gammaB + offset, 0.1f, 4.0f); constants.gammaB = 1.0f / std::clamp(constants.gammaB + offset, 0.1f, 4.0f);
constants.textureDescriptorIndex = g_intermediaryBackBufferTextureDescriptorIndex; constants.textureDescriptorIndex = g_intermediaryBackBufferTextureDescriptorIndex;
auto& framebuffer = g_backBuffer->framebuffers[swapChainTexture]; auto &framebuffer = g_backBuffer->framebuffers[swapChainTexture];
if (!framebuffer) if (!framebuffer)
{ {
RenderFramebufferDesc desc; RenderFramebufferDesc desc;
desc.colorAttachments = const_cast<const RenderTexture**>(&swapChainTexture); desc.colorAttachments = const_cast<const RenderTexture **>(&swapChainTexture);
desc.colorAttachmentsCount = 1; desc.colorAttachmentsCount = 1;
framebuffer = g_device->createFramebuffer(desc); framebuffer = g_device->createFramebuffer(desc);
} }
@ -1914,7 +1935,7 @@ static void ProcPresent(const RenderCommand& cmd)
RenderTextureBarrier(swapChainTexture, RenderTextureLayout::COLOR_WRITE) RenderTextureBarrier(swapChainTexture, RenderTextureLayout::COLOR_WRITE)
}; };
auto& commandList = g_commandLists[g_frame]; auto &commandList = g_commandLists[g_frame];
commandList->barriers(RenderBarrierStage::GRAPHICS, srcBarriers, std::size(srcBarriers)); commandList->barriers(RenderBarrierStage::GRAPHICS, srcBarriers, std::size(srcBarriers));
commandList->setGraphicsPipelineLayout(g_pipelineLayout.get()); commandList->setGraphicsPipelineLayout(g_pipelineLayout.get());
commandList->setPipeline(g_gammaCorrectionPipeline.get()); commandList->setPipeline(g_gammaCorrectionPipeline.get());
@ -1933,14 +1954,14 @@ static void ProcPresent(const RenderCommand& cmd)
} }
} }
auto& commandList = g_commandLists[g_frame]; auto &commandList = g_commandLists[g_frame];
commandList->end(); commandList->end();
if (g_swapChainValid) if (g_swapChainValid)
{ {
const RenderCommandList* commandLists[] = { commandList.get() }; const RenderCommandList *commandLists[] = { commandList.get() };
RenderCommandSemaphore* waitSemaphores[] = { g_acquireSemaphores[g_frame].get() }; RenderCommandSemaphore *waitSemaphores[] = { g_acquireSemaphores[g_frame].get() };
RenderCommandSemaphore* signalSemaphores[] = { g_renderSemaphores[g_frame].get() }; RenderCommandSemaphore *signalSemaphores[] = { g_renderSemaphores[g_frame].get() };
g_queue->executeCommandLists( g_queue->executeCommandLists(
commandLists, std::size(commandLists), commandLists, std::size(commandLists),
@ -1974,7 +1995,8 @@ static void ProcPresent(const RenderCommand& cmd)
BeginCommandList(); BeginCommandList();
g_pendingRenderThread = false; g_pendingRenderThread.store(false);
g_pendingRenderThread.notify_all();
} }
static GuestSurface* GetBackBuffer() static GuestSurface* GetBackBuffer()
@ -4120,10 +4142,10 @@ static RenderFormat ConvertDXGIFormat(ddspp::DXGIFormat format)
} }
} }
static bool LoadTexture(GuestTexture& texture, uint8_t* data, size_t dataSize) static bool LoadTexture(GuestTexture& texture, const uint8_t* data, size_t dataSize)
{ {
ddspp::Descriptor ddsDesc; ddspp::Descriptor ddsDesc;
if (ddspp::decode_header(data, ddsDesc) != ddspp::Error) if (ddspp::decode_header((unsigned char *)(data), ddsDesc) != ddspp::Error)
{ {
RenderTextureDesc desc; RenderTextureDesc desc;
desc.dimension = ConvertTextureDimension(ddsDesc.type); desc.dimension = ConvertTextureDimension(ddsDesc.type);
@ -4191,7 +4213,7 @@ static bool LoadTexture(GuestTexture& texture, uint8_t* data, size_t dataSize)
for (auto& slice : slices) for (auto& slice : slices)
{ {
uint8_t* srcData = data + ddsDesc.headerSize + slice.srcOffset; const uint8_t* srcData = data + ddsDesc.headerSize + slice.srcOffset;
uint8_t* dstData = mappedMemory + slice.dstOffset; uint8_t* dstData = mappedMemory + slice.dstOffset;
if (slice.srcRowPitch == slice.dstRowPitch) if (slice.srcRowPitch == slice.dstRowPitch)
@ -4284,7 +4306,7 @@ static bool LoadTexture(GuestTexture& texture, uint8_t* data, size_t dataSize)
return false; return false;
} }
std::unique_ptr<GuestTexture> LoadTexture(uint8_t* data, size_t dataSize) std::unique_ptr<GuestTexture> LoadTexture(const uint8_t* data, size_t dataSize)
{ {
GuestTexture texture(ResourceType::Texture); GuestTexture texture(ResourceType::Texture);
@ -5531,7 +5553,7 @@ GUEST_FUNCTION_HOOK(sub_82BE96F0, GetSurfaceDesc);
GUEST_FUNCTION_HOOK(sub_82BE04B0, GetVertexDeclaration); GUEST_FUNCTION_HOOK(sub_82BE04B0, GetVertexDeclaration);
GUEST_FUNCTION_HOOK(sub_82BE0530, HashVertexDeclaration); GUEST_FUNCTION_HOOK(sub_82BE0530, HashVertexDeclaration);
GUEST_FUNCTION_HOOK(sub_82BDA8C0, Present); GUEST_FUNCTION_HOOK(sub_82BDA8C0, GuestPresent);
GUEST_FUNCTION_HOOK(sub_82BDD330, GetBackBuffer); GUEST_FUNCTION_HOOK(sub_82BDD330, GetBackBuffer);
GUEST_FUNCTION_HOOK(sub_82BE9498, CreateTexture); GUEST_FUNCTION_HOOK(sub_82BE9498, CreateTexture);

View file

@ -10,6 +10,13 @@
using namespace plume; using namespace plume;
struct Video
{
static void CreateHostDevice();
static void HostPresent();
static void StartPipelinePrecompilation();
};
struct GuestSamplerState struct GuestSamplerState
{ {
be<uint32_t> data[6]; be<uint32_t> data[6];
@ -379,6 +386,6 @@ enum GuestTextureAddress
D3DTADDRESS_BORDER = 6 D3DTADDRESS_BORDER = 6
}; };
extern std::unique_ptr<GuestTexture> LoadTexture(uint8_t* data, size_t dataSize); extern std::unique_ptr<GuestTexture> LoadTexture(const uint8_t* data, size_t dataSize);
extern void VideoConfigValueChangedCallback(class IConfigDef* config); extern void VideoConfigValueChangedCallback(class IConfigDef* config);

View file

@ -66,7 +66,7 @@ static std::unique_ptr<VirtualFileSystem> createFileSystemFromPath(const std::fi
} }
} }
static bool copyFile(const FilePair &pair, const uint64_t *fileHashes, VirtualFileSystem &sourceVfs, const std::filesystem::path &targetDirectory, bool skipHashChecks, std::vector<uint8_t> &fileData, Journal &journal, const std::function<void(uint32_t)> &progressCallback) { static bool copyFile(const FilePair &pair, const uint64_t *fileHashes, VirtualFileSystem &sourceVfs, const std::filesystem::path &targetDirectory, bool skipHashChecks, std::vector<uint8_t> &fileData, Journal &journal, const std::function<void()> &progressCallback) {
const std::string filename(pair.first); const std::string filename(pair.first);
const uint32_t hashCount = pair.second; const uint32_t hashCount = pair.second;
if (!sourceVfs.exists(filename)) if (!sourceVfs.exists(filename))
@ -136,7 +136,8 @@ static bool copyFile(const FilePair &pair, const uint64_t *fileHashes, VirtualFi
return false; return false;
} }
progressCallback(++journal.progressCounter); journal.progressCounter += fileData.size();
progressCallback();
return true; return true;
} }
@ -200,7 +201,25 @@ bool Installer::checkGameInstall(const std::filesystem::path &baseDirectory)
return std::filesystem::exists(baseDirectory / GameDirectory / GameExecutableFile); return std::filesystem::exists(baseDirectory / GameDirectory / GameExecutableFile);
} }
bool Installer::copyFiles(std::span<const FilePair> filePairs, const uint64_t *fileHashes, VirtualFileSystem &sourceVfs, const std::filesystem::path &targetDirectory, const std::string &validationFile, bool skipHashChecks, Journal &journal, const std::function<void(uint32_t)> &progressCallback) bool Installer::computeTotalSize(std::span<const FilePair> filePairs, const uint64_t *fileHashes, VirtualFileSystem &sourceVfs, Journal &journal, uint64_t &totalSize)
{
for (FilePair pair : filePairs)
{
const std::string filename(pair.first);
if (!sourceVfs.exists(filename))
{
journal.lastResult = Journal::Result::FileMissing;
journal.lastErrorMessage = std::format("File {} does not exist in the file system.", filename);
return false;
}
totalSize += sourceVfs.getSize(filename);
}
return true;
}
bool Installer::copyFiles(std::span<const FilePair> filePairs, const uint64_t *fileHashes, VirtualFileSystem &sourceVfs, const std::filesystem::path &targetDirectory, const std::string &validationFile, bool skipHashChecks, Journal &journal, const std::function<void()> &progressCallback)
{ {
if (!std::filesystem::exists(targetDirectory) && !std::filesystem::create_directories(targetDirectory)) if (!std::filesystem::exists(targetDirectory) && !std::filesystem::create_directories(targetDirectory))
{ {
@ -265,45 +284,48 @@ bool Installer::parseContent(const std::filesystem::path &sourcePath, std::uniqu
} }
} }
bool Installer::install(const Input &input, const std::filesystem::path &targetDirectory, Journal &journal, const std::function<void(uint32_t)> &progressCallback) constexpr uint32_t PatcherContribution = 512 * 1024 * 1024;
bool Installer::parseSources(const Input &input, Journal &journal, Sources &sources)
{ {
sources = Sources();
// Parse the contents of the base game. // Parse the contents of the base game.
std::unique_ptr<VirtualFileSystem> gameSource;
if (!input.gameSource.empty()) if (!input.gameSource.empty())
{ {
if (!parseContent(input.gameSource, gameSource, journal)) if (!parseContent(input.gameSource, sources.game, journal))
{ {
return false; return false;
} }
journal.progressTotal += GameFilesSize; if (!computeTotalSize({ GameFiles, GameFilesSize }, GameHashes, *sources.game, journal, sources.totalSize))
{
return false;
}
} }
// Parse the contents of Update. // Parse the contents of Update.
std::unique_ptr<VirtualFileSystem> updateSource;
if (!input.updateSource.empty()) if (!input.updateSource.empty())
{ {
if (!parseContent(input.updateSource, updateSource, journal)) // Add an arbitrary progress size for the patching process.
journal.progressTotal += PatcherContribution;
if (!parseContent(input.updateSource, sources.update, journal))
{ {
return false; return false;
} }
journal.progressTotal += UpdateFilesSize; if (!computeTotalSize({ UpdateFiles, UpdateFilesSize }, UpdateHashes, *sources.update, journal, sources.totalSize))
{
return false;
}
} }
// Parse the contents of the DLC Packs. // Parse the contents of the DLC Packs.
struct DLCSource {
std::unique_ptr<VirtualFileSystem> sourceVfs;
std::span<const FilePair> filePairs;
const uint64_t *fileHashes = nullptr;
std::string targetSubDirectory;
};
std::vector<DLCSource> dlcSources;
for (const auto &path : input.dlcSources) for (const auto &path : input.dlcSources)
{ {
dlcSources.emplace_back(); sources.dlc.emplace_back();
DLCSource &dlcSource = dlcSources.back(); DLCSource &dlcSource = sources.dlc.back();
if (!parseContent(path, dlcSource.sourceVfs, journal)) if (!parseContent(path, dlcSource.sourceVfs, journal))
{ {
return false; return false;
@ -346,17 +368,51 @@ bool Installer::install(const Input &input, const std::filesystem::path &targetD
return false; return false;
} }
journal.progressTotal += dlcSource.filePairs.size(); if (!computeTotalSize(dlcSource.filePairs, dlcSource.fileHashes, *dlcSource.sourceVfs, journal, sources.totalSize))
{
return false;
}
} }
// Install the base game. // Add the total size in bytes as the journal progress.
if (!copyFiles({ GameFiles, GameFilesSize }, GameHashes, *gameSource, targetDirectory / GameDirectory, GameExecutableFile, input.skipHashChecks, journal, progressCallback)) journal.progressTotal += sources.totalSize;
return true;
}
bool Installer::install(const Sources &sources, const std::filesystem::path &targetDirectory, bool skipHashChecks, Journal &journal, const std::function<void()> &progressCallback)
{
// Install files in reverse order of importance. In case of a process crash or power outage, this will increase the likelihood of the installation
// missing critical files required for the game to run. These files are used as the way to detect if the game is installed.
// Install the DLC.
if (!sources.dlc.empty())
{
journal.createdDirectories.insert(targetDirectory / DLCDirectory);
}
for (const DLCSource &dlcSource : sources.dlc)
{
if (!copyFiles(dlcSource.filePairs, dlcSource.fileHashes, *dlcSource.sourceVfs, targetDirectory / dlcSource.targetSubDirectory, DLCValidationFile, skipHashChecks, journal, progressCallback))
{
return false;
}
}
// If no game or update was specified, we're finished. This means the user was only installing the DLC.
if ((sources.game == nullptr) && (sources.update == nullptr))
{
return true;
}
// Install the update.
if (!copyFiles({ UpdateFiles, UpdateFilesSize }, UpdateHashes, *sources.update, targetDirectory / UpdateDirectory, UpdateExecutablePatchFile, skipHashChecks, journal, progressCallback))
{ {
return false; return false;
} }
// Install the update. // Install the base game.
if (!copyFiles({ UpdateFiles, UpdateFilesSize }, UpdateHashes, *updateSource, targetDirectory / UpdateDirectory, UpdateExecutablePatchFile, input.skipHashChecks, journal, progressCallback)) if (!copyFiles({ GameFiles, GameFilesSize }, GameHashes, *sources.game, targetDirectory / GameDirectory, GameExecutableFile, skipHashChecks, journal, progressCallback))
{ {
return false; return false;
} }
@ -374,6 +430,10 @@ bool Installer::install(const Input &input, const std::filesystem::path &targetD
return false; return false;
} }
// Update the progress with the artificial amount attributed to the patching.
journal.progressCounter += PatcherContribution;
progressCallback();
// Replace the executable by renaming and deleting in a safe way. // Replace the executable by renaming and deleting in a safe way.
std::error_code ec; std::error_code ec;
std::filesystem::path oldXexPath = targetDirectory / GameDirectory / (GameExecutableFile + OldExtension); std::filesystem::path oldXexPath = targetDirectory / GameDirectory / (GameExecutableFile + OldExtension);
@ -396,20 +456,6 @@ bool Installer::install(const Input &input, const std::filesystem::path &targetD
std::filesystem::remove(oldXexPath); std::filesystem::remove(oldXexPath);
// Install the DLC.
if (!dlcSources.empty())
{
journal.createdDirectories.insert(targetDirectory / DLCDirectory);
}
for (const DLCSource &dlcSource : dlcSources)
{
if (!copyFiles(dlcSource.filePairs, dlcSource.fileHashes, *dlcSource.sourceVfs, targetDirectory / dlcSource.targetSubDirectory, DLCValidationFile, input.skipHashChecks, journal, progressCallback))
{
return false;
}
}
return true; return true;
} }

View file

@ -13,7 +13,8 @@ enum class DLC {
Mazuri, Mazuri,
Holoska, Holoska,
ApotosShamar, ApotosShamar,
EmpireCityAdabat EmpireCityAdabat,
Count = EmpireCityAdabat
}; };
struct Journal struct Journal
@ -35,8 +36,8 @@ struct Journal
UnknownDLCType UnknownDLCType
}; };
uint32_t progressCounter = 0; uint64_t progressCounter = 0;
uint32_t progressTotal = 0; uint64_t progressTotal = 0;
std::list<std::filesystem::path> createdFiles; std::list<std::filesystem::path> createdFiles;
std::set<std::filesystem::path> createdDirectories; std::set<std::filesystem::path> createdDirectories;
Result lastResult = Result::Success; Result lastResult = Result::Success;
@ -53,13 +54,29 @@ struct Installer
std::filesystem::path gameSource; std::filesystem::path gameSource;
std::filesystem::path updateSource; std::filesystem::path updateSource;
std::list<std::filesystem::path> dlcSources; std::list<std::filesystem::path> dlcSources;
bool skipHashChecks = false; };
struct DLCSource {
std::unique_ptr<VirtualFileSystem> sourceVfs;
std::span<const FilePair> filePairs;
const uint64_t *fileHashes = nullptr;
std::string targetSubDirectory;
};
struct Sources
{
std::unique_ptr<VirtualFileSystem> game;
std::unique_ptr<VirtualFileSystem> update;
std::vector<DLCSource> dlc;
uint64_t totalSize = 0;
}; };
static bool checkGameInstall(const std::filesystem::path &baseDirectory); static bool checkGameInstall(const std::filesystem::path &baseDirectory);
static bool copyFiles(std::span<const FilePair> filePairs, const uint64_t *fileHashes, VirtualFileSystem &sourceVfs, const std::filesystem::path &targetDirectory, const std::string &validationFile, bool skipHashChecks, Journal &journal, const std::function<void(uint32_t)> &progressCallback); static bool computeTotalSize(std::span<const FilePair> filePairs, const uint64_t *fileHashes, VirtualFileSystem &sourceVfs, Journal &journal, uint64_t &totalSize);
static bool copyFiles(std::span<const FilePair> filePairs, const uint64_t *fileHashes, VirtualFileSystem &sourceVfs, const std::filesystem::path &targetDirectory, const std::string &validationFile, bool skipHashChecks, Journal &journal, const std::function<void()> &progressCallback);
static bool parseContent(const std::filesystem::path &sourcePath, std::unique_ptr<VirtualFileSystem> &targetVfs, Journal &journal); static bool parseContent(const std::filesystem::path &sourcePath, std::unique_ptr<VirtualFileSystem> &targetVfs, Journal &journal);
static bool install(const Input &input, const std::filesystem::path &targetDirectory, Journal &journal, const std::function<void(uint32_t)> &progressCallback); static bool parseSources(const Input &input, Journal &journal, Sources &sources);
static bool install(const Sources &sources, const std::filesystem::path &targetDirectory, bool skipHashChecks, Journal &journal, const std::function<void()> &progressCallback);
static void rollback(Journal &journal); static void rollback(Journal &journal);
// Convenience method for checking if the specified file contains the game. This should be used when the user selects the file. // Convenience method for checking if the specified file contains the game. This should be used when the user selects the file.

View file

@ -15,6 +15,8 @@
#include <user/config.h> #include <user/config.h>
#include <user/paths.h> #include <user/paths.h>
#include <kernel/xdbf.h> #include <kernel/xdbf.h>
#include <install/installer.h>
#include <ui/installer_wizard.h>
#define GAME_XEX_PATH "game:\\default.xex" #define GAME_XEX_PATH "game:\\default.xex"
@ -27,8 +29,7 @@ CodeCache g_codeCache;
XDBFWrapper g_xdbfWrapper; XDBFWrapper g_xdbfWrapper;
std::unordered_map<uint16_t, GuestTexture*> g_xdbfTextureCache; std::unordered_map<uint16_t, GuestTexture*> g_xdbfTextureCache;
// Name inspired from nt's entry point void HostStartup()
void KiSystemStartup()
{ {
#ifdef _WIN32 #ifdef _WIN32
CoInitializeEx(nullptr, COINIT_MULTITHREADED); CoInitializeEx(nullptr, COINIT_MULTITHREADED);
@ -39,7 +40,11 @@ void KiSystemStartup()
g_codeCache.Init(); g_codeCache.Init();
g_memory.Alloc(XMAIOBegin, 0xFFFF, MEM_COMMIT); g_memory.Alloc(XMAIOBegin, 0xFFFF, MEM_COMMIT);
}
// Name inspired from nt's entry point
void KiSystemStartup()
{
const auto gameContent = XamMakeContent(XCONTENTTYPE_RESERVED, "Game"); const auto gameContent = XamMakeContent(XCONTENTTYPE_RESERVED, "Game");
const auto updateContent = XamMakeContent(XCONTENTTYPE_RESERVED, "Update"); const auto updateContent = XamMakeContent(XCONTENTTYPE_RESERVED, "Update");
XamRegisterContent(gameContent, DirectoryExists(".\\game") ? ".\\game" : "."); XamRegisterContent(gameContent, DirectoryExists(".\\game") ? ".\\game" : ".");
@ -136,15 +141,39 @@ uint32_t LdrLoadModule(const char* path)
return entry; return entry;
} }
int main() int main(int argc, char *argv[])
{ {
bool forceInstaller = false;
bool forceDLCInstaller = false;
for (uint32_t i = 1; i < argc; i++)
{
forceInstaller = forceInstaller || (strcmp(argv[i], "--install") == 0);
forceDLCInstaller = forceDLCInstaller || (strcmp(argv[i], "--install-dlc") == 0);
}
Config::Load(); Config::Load();
HostStartup();
Video::CreateHostDevice();
bool isGameInstalled = Installer::checkGameInstall(".");
if (forceInstaller || forceDLCInstaller || !isGameInstalled)
{
if (!InstallerWizard::Run(isGameInstalled && forceDLCInstaller))
{
return 1;
}
}
AchievementData::Load(); AchievementData::Load();
KiSystemStartup(); KiSystemStartup();
uint32_t entry = LdrLoadModule(FileSystem::TransformPath(GAME_XEX_PATH)); uint32_t entry = LdrLoadModule(FileSystem::TransformPath(GAME_XEX_PATH));
Video::StartPipelinePrecompilation();
GuestThread::Start(entry); GuestThread::Start(entry);
return 0; return 0;

View file

@ -0,0 +1,902 @@
#include "installer_wizard.h"
#include <nfd.h>
#include <install/installer.h>
#include <gpu/video.h>
#include <ui/imgui_utils.h>
#include <ui/window.h>
#include <res/install_001_dds.h>
#include <res/install_002_dds.h>
#include <res/install_003_dds.h>
#include <res/install_004_dds.h>
#include <res/install_005_dds.h>
#include <res/install_006_dds.h>
#include <res/install_007_dds.h>
#include <res/install_008_dds.h>
#define SKIP_SOURCE_CHECKS 0
static constexpr double SCANLINES_ANIMATION_DURATION = 8.0;
static constexpr double TITLE_ANIMATION_TIME = SCANLINES_ANIMATION_DURATION + 8.0; // 8 frame delay
static constexpr double TITLE_ANIMATION_DURATION = 8.0;
static constexpr double CONTAINER_LINE_ANIMATION_DURATION = 8.0;
static constexpr double CONTAINER_OUTER_TIME = CONTAINER_LINE_ANIMATION_DURATION + 8.0; // 8 frame delay
static constexpr double CONTAINER_OUTER_DURATION = 8.0;
static constexpr double CONTAINER_INNER_TIME = CONTAINER_OUTER_TIME + CONTAINER_OUTER_DURATION + 8.0; // 8 frame delay
static constexpr double CONTAINER_INNER_DURATION = 8.0;
static constexpr double CONTAINER_BACKGROUND_TIME = CONTAINER_INNER_TIME + CONTAINER_INNER_DURATION + 8.0; // 8 frame delay
static constexpr double CONTAINER_BACKGROUND_DURATION = 16.0;
static constexpr double ALL_ANIMATIONS_FULL_DURATION = CONTAINER_BACKGROUND_TIME + CONTAINER_BACKGROUND_DURATION;
constexpr float IMAGE_X = 140.0f;
constexpr float IMAGE_Y = 106.0f;
constexpr float IMAGE_WIDTH = 512.0f;
constexpr float IMAGE_HEIGHT = 512.0f;
constexpr float CONTAINER_X = 510.0f;
constexpr float CONTAINER_Y = 225.0f;
constexpr float CONTAINER_WIDTH = 528.0f;
constexpr float CONTAINER_HEIGHT = 245.0f;
constexpr float SIDE_CONTAINER_WIDTH = CONTAINER_WIDTH / 2.0f;
constexpr float BOTTOM_X_GAP = 4.0f;
constexpr float BOTTOM_Y_GAP = 4.0f;
constexpr float SOURCE_BUTTON_WIDTH = 250.0f;
constexpr float SOURCE_BUTTON_GAP = 9.0f;
constexpr float BUTTON_HEIGHT = 22.0f;
constexpr float BUTTON_TEXT_GAP = 28.0f;
constexpr float BORDER_SIZE = 1.0f;
constexpr float BORDER_OVERSHOOT = 36.0f;
constexpr float FAKE_PROGRESS_RATIO = 0.25f;
static constexpr size_t GRID_SIZE = 9;
static ImFont *g_seuratFont;
static ImFont *g_dfsogeistdFont;
static ImFont *g_newRodinFont;
static double g_appearTime = 0.0;
static double g_disappearTime = DBL_MAX;
static bool g_isDisappearing = false;
static std::filesystem::path g_gameSourcePath;
static std::filesystem::path g_updateSourcePath;
static std::array<std::filesystem::path, int(DLC::Count)> g_dlcSourcePaths;
static std::array<std::unique_ptr<GuestTexture>, 8> g_installTextures;
static Journal g_installerJournal;
static Installer::Sources g_installerSources;
static uint64_t g_installerAvailableSize = 0;
static std::unique_ptr<std::thread> g_installerThread;
static double g_installerStartTime = 0.0;
static float g_installerProgressRatioCurrent = 0.0f;
static std::atomic<float> g_installerProgressRatioTarget = 0.0f;
static std::atomic<bool> g_installerFinished = false;
static bool g_installerFailed = false;
static std::string g_installerErrorMessage;
enum class WizardPage
{
Introduction,
SelectGameAndUpdate,
SelectDLC,
CheckSpace,
Installing,
InstallSucceeded,
InstallFailed,
};
static WizardPage g_currentPage = WizardPage::Introduction;
const char INSTALLER_TEXT[] = "INSTALLER";
const char INSTALLING_TEXT[] = "INSTALLING";
const char CREDITS_TEXT[] = "Sajid (RIP)";
const char *WIZARD_TEXT[] =
{
"Welcome to Unleashed Recompiled!\n\nMake sure you have a copy of Sonic Unleashed's files for Xbox 360 before proceeding with the installation.",
"Select the files for the Game and the Update. You can use digital dumps (PIRS), a folder with the game's contents or a disc image (ISO).",
"Select the files for the DLC Packs. These can be digital dumps (PIRS) or a folder with their contents.",
"The content will be installed to the program's folder. Please confirm you have enough space available.\n\n",
"Please wait while the content is being installed...",
"Installation is complete.\n\nThis project's been brought to you by:\n\n",
"Installation has failed.\n\nError:\n\n"
};
static const int WIZARD_INSTALL_TEXTURE_INDEX[] =
{
0,
1,
2,
3,
4,
7, // Force Werehog on InstallSucceeded.
5 // Force Eggman on InstallFailed.
};
const char GAME_SOURCE_TEXT[] = "FULL GAME";
const char UPDATE_SOURCE_TEXT[] = "UPDATE";
const char *DLC_SOURCE_TEXT[] =
{
"SPAGONIA",
"CHUN-NAN",
"MAZURI",
"HOLOSKA",
"APOTOS & SHAMAR",
"EMPIRE CITY & ADABAT",
};
const char FILES_BUTTON_TEXT[] = "ADD FILES";
const char FOLDER_BUTTON_TEXT[] = "ADD FOLDER";
const char NEXT_BUTTON_TEXT[] = "NEXT";
const char SKIP_BUTTON_TEXT[] = "SKIP";
const char REQUIRED_SPACE_TEXT[] = "Required space";
const char AVAILABLE_SPACE_TEXT[] = "Available space";
static int DLCIndex(DLC dlc)
{
assert(dlc != DLC::Unknown);
return (int)(dlc) - 1;
}
static double ComputeMotionInstaller(double timeAppear, double timeDisappear, double offset, double total)
{
return ComputeMotion(timeAppear, offset, total) * (1.0 - ComputeMotion(timeDisappear, ALL_ANIMATIONS_FULL_DURATION - offset - total, total));
}
static void DrawBackground()
{
auto &res = ImGui::GetIO().DisplaySize;
auto drawList = ImGui::GetForegroundDrawList();
drawList->AddRectFilled({ 0.0, 0.0 }, res, IM_COL32_BLACK);
}
static void DrawLeftImage()
{
int installTextureIndex = WIZARD_INSTALL_TEXTURE_INDEX[int(g_currentPage)];
if (g_currentPage == WizardPage::Installing)
{
// Cycle through the available images while time passes during installation.
constexpr double InstallationSpeed = 1.0 / 15.0;
double installationTime = (ImGui::GetTime() - g_installerStartTime) * InstallationSpeed;
installTextureIndex += int(installationTime);
}
double imageAlpha = ComputeMotionInstaller(g_appearTime, g_disappearTime, CONTAINER_BACKGROUND_TIME, CONTAINER_BACKGROUND_DURATION);
int a = std::lround(255.0 * imageAlpha);
GuestTexture *guestTexture = g_installTextures[installTextureIndex % g_installTextures.size()].get();
auto &res = ImGui::GetIO().DisplaySize;
auto drawList = ImGui::GetForegroundDrawList();
ImVec2 min = { Scale(IMAGE_X), Scale(IMAGE_Y) };
ImVec2 max = { Scale(IMAGE_X + IMAGE_WIDTH), Scale(IMAGE_Y + IMAGE_HEIGHT) };
drawList->AddImage(guestTexture, min, max, ImVec2(0, 0), ImVec2(1, 1), IM_COL32(255, 255, 255, a));
min.y = (min.y + max.y) / 2.0f;
drawList->AddRectFilledMultiColor(min, max, IM_COL32_BLACK_TRANS, IM_COL32_BLACK_TRANS, IM_COL32(0, 0, 0, 255), IM_COL32(0, 0, 0, 255));
}
static void DrawScanlineBars()
{
constexpr uint32_t COLOR0 = IM_COL32(203, 255, 0, 0);
constexpr uint32_t COLOR1 = IM_COL32(203, 255, 0, 55);
constexpr uint32_t FADE_COLOR0 = IM_COL32(0, 0, 0, 255);
constexpr uint32_t FADE_COLOR1 = IM_COL32(0, 0, 0, 0);
constexpr uint32_t OUTLINE_COLOR = IM_COL32(115, 178, 104, 255);
float height = Scale(105.0f) * ComputeMotionInstaller(g_appearTime, g_disappearTime, 0.0, SCANLINES_ANIMATION_DURATION);
auto &res = ImGui::GetIO().DisplaySize;
auto drawList = ImGui::GetForegroundDrawList();
SetShaderModifier(IMGUI_SHADER_MODIFIER_SCANLINE);
// Top bar
drawList->AddRectFilledMultiColor
(
{ 0.0f, 0.0f },
{ res.x, height },
COLOR0,
COLOR0,
COLOR1,
COLOR1
);
// Bottom bar
drawList->AddRectFilledMultiColor
(
{ res.x, res.y },
{ 0.0f, res.y - height },
COLOR0,
COLOR0,
COLOR1,
COLOR1
);
SetShaderModifier(IMGUI_SHADER_MODIFIER_NONE);
// Installer text
// TODO: localise this.
const char *headerText = g_currentPage == WizardPage::Installing ? INSTALLING_TEXT : INSTALLER_TEXT;
int textAlpha = std::lround(255.0f * ComputeMotionInstaller(g_appearTime, g_disappearTime, TITLE_ANIMATION_TIME, TITLE_ANIMATION_DURATION));
DrawTextWithOutline<int>(g_dfsogeistdFont, Scale(48.0f), { Scale(122.0f), Scale(56.0f) }, IM_COL32(255, 195, 0, textAlpha), headerText, 4, IM_COL32(0, 0, 0, textAlpha));
// Top bar line
drawList->AddLine
(
{ 0.0f, height },
{ res.x, height },
OUTLINE_COLOR,
Scale(1)
);
// Bottom bar line
drawList->AddLine
(
{ 0.0f, res.y - height },
{ res.x, res.y - height },
OUTLINE_COLOR,
Scale(1)
);
}
static float AlignToNextGrid(float value)
{
return floor(value / GRID_SIZE) * GRID_SIZE;
}
static void DrawContainer(ImVec2 min, ImVec2 max, bool useInnerColor)
{
double containerHeight = ComputeMotionInstaller(g_appearTime, g_disappearTime, 0.0, CONTAINER_LINE_ANIMATION_DURATION);
float center = (min.y + max.y) / 2.0f;
min.y = Lerp(center, min.y, containerHeight);
max.y = Lerp(center, max.y, containerHeight);
auto &res = ImGui::GetIO().DisplaySize;
auto drawList = ImGui::GetForegroundDrawList();
double outerAlpha = ComputeMotionInstaller(g_appearTime, g_disappearTime, CONTAINER_OUTER_TIME, CONTAINER_OUTER_DURATION);
double innerAlpha = ComputeMotionInstaller(g_appearTime, g_disappearTime, CONTAINER_INNER_TIME, CONTAINER_INNER_DURATION);
double backgroundAlpha = ComputeMotionInstaller(g_appearTime, g_disappearTime, CONTAINER_BACKGROUND_TIME, CONTAINER_BACKGROUND_DURATION);
const uint32_t outerColor = IM_COL32(0, 60, 0, 128 * outerAlpha);
const uint32_t innerColor = IM_COL32(0, 40, 0, 96 * innerAlpha);
const uint32_t backgroundColor = IM_COL32(0, 60, 0, 96 * backgroundAlpha);
float gridSize = Scale(GRID_SIZE);
drawList->AddRectFilled(min, max, backgroundColor);
SetShaderModifier(IMGUI_SHADER_MODIFIER_CHECKERBOARD);
drawList->AddRectFilled(min, max, useInnerColor ? innerColor: outerColor);
SetShaderModifier(IMGUI_SHADER_MODIFIER_NONE);
// The draw area
drawList->PushClipRect({ min.x + gridSize * 2.0f, min.y + gridSize * 2.0f }, { max.x - gridSize * 2.0f + 1.0f, max.y - gridSize * 2.0f + 1.0f });
}
static void DrawDescriptionContainer()
{
auto &res = ImGui::GetIO().DisplaySize;
auto drawList = ImGui::GetForegroundDrawList();
ImVec2 descriptionMin = { Scale(AlignToNextGrid(CONTAINER_X)), Scale(AlignToNextGrid(CONTAINER_Y)) };
ImVec2 descriptionMax = { Scale(AlignToNextGrid(CONTAINER_X + CONTAINER_WIDTH)), Scale(AlignToNextGrid(CONTAINER_Y + CONTAINER_HEIGHT)) };
DrawContainer(descriptionMin, descriptionMax, false);
char descriptionText[512];
strncpy(descriptionText, WIZARD_TEXT[int(g_currentPage)], sizeof(descriptionText) - 1);
if (g_currentPage == WizardPage::CheckSpace)
{
constexpr double DivisorGiB = (1024.0 * 1024.0 * 1024.0);
double requiredGiB = double(g_installerSources.totalSize) / DivisorGiB;
double availableGiB = double(g_installerAvailableSize) / DivisorGiB;
snprintf(descriptionText, sizeof(descriptionText), "%s%s: %2.2f GiB\n\n%s: %2.2f GiB", WIZARD_TEXT[int(g_currentPage)], REQUIRED_SPACE_TEXT, requiredGiB, AVAILABLE_SPACE_TEXT, availableGiB);
}
else if (g_currentPage == WizardPage::InstallSucceeded)
{
strncat(descriptionText, CREDITS_TEXT, sizeof(descriptionText) - 1);
}
else if (g_currentPage == WizardPage::InstallFailed)
{
strncat(descriptionText, g_installerErrorMessage.c_str(), sizeof(descriptionText) - 1);
}
double textAlpha = ComputeMotionInstaller(g_appearTime, g_disappearTime, CONTAINER_BACKGROUND_TIME, CONTAINER_BACKGROUND_DURATION);
auto clipRectMin = drawList->GetClipRectMin();
auto clipRectMax = drawList->GetClipRectMax();
auto size = Scale(26.0f);
drawList->AddText
(
g_seuratFont,
size,
{ clipRectMin.x, clipRectMin.y },
IM_COL32(255, 255, 255, 255 * textAlpha),
descriptionText,
0,
clipRectMax.x - clipRectMin.x
);
drawList->PopClipRect();
ImVec2 sideMin = { descriptionMax.x, descriptionMin.y };
ImVec2 sideMax = { Scale(AlignToNextGrid(CONTAINER_X + CONTAINER_WIDTH + SIDE_CONTAINER_WIDTH)), descriptionMax.y };
DrawContainer(sideMin, sideMax, true);
drawList->PopClipRect();
}
static void DrawButtonContainer(ImVec2 min, ImVec2 max, int baser, int baseg, float alpha)
{
auto &res = ImGui::GetIO().DisplaySize;
auto drawList = ImGui::GetForegroundDrawList();
SetShaderModifier(IMGUI_SHADER_MODIFIER_SCANLINE_BUTTON);
drawList->AddRectFilledMultiColor(min, max, IM_COL32(baser, baseg + 130, 0, 223 * alpha), IM_COL32(baser, baseg + 130, 0, 178 * alpha), IM_COL32(baser, baseg + 130, 0, 223 * alpha), IM_COL32(baser, baseg + 130, 0, 178 * alpha));
drawList->AddRectFilledMultiColor(min, max, IM_COL32(baser, baseg, 0, 13 * alpha), IM_COL32(baser, baseg, 0, 0), IM_COL32(baser, baseg, 0, 55 * alpha), IM_COL32(baser, baseg, 0, 6 * alpha));
drawList->AddRectFilledMultiColor(min, max, IM_COL32(baser, baseg + 130, 0, 13 * alpha), IM_COL32(baser, baseg + 130, 0, 111 * alpha), IM_COL32(baser, baseg + 130, 0, 0), IM_COL32(baser, baseg + 130, 0, 55 * alpha));
SetShaderModifier(IMGUI_SHADER_MODIFIER_NONE);
}
static void DrawButton(ImVec2 min, ImVec2 max, const char *buttonText, bool sourceButton, bool buttonEnabled, bool &buttonPressed)
{
buttonPressed = false;
auto &res = ImGui::GetIO().DisplaySize;
auto drawList = ImGui::GetForegroundDrawList();
float alpha = ComputeMotionInstaller(g_appearTime, g_disappearTime, CONTAINER_BACKGROUND_TIME, CONTAINER_BACKGROUND_DURATION);
if (!buttonEnabled)
{
alpha *= 0.5f;
}
int baser = 0;
int baseg = 0;
if (!sourceButton && buttonEnabled && (alpha >= 1.0f) && ImGui::IsMouseHoveringRect(min, max, false))
{
baser = 48;
baseg = 32;
if (ImGui::IsMouseClicked(ImGuiMouseButton_Left))
{
buttonPressed = true;
}
}
DrawButtonContainer(min, max, baser, baseg, alpha);
float size = Scale(sourceButton ? 15.0f : 20.0f);
ImFont *font = sourceButton ? g_newRodinFont : g_dfsogeistdFont;
ImVec2 textSize = font->CalcTextSizeA(size, FLT_MAX, 0.0f, buttonText);
min.x += ((max.x - min.x) - textSize.x) / 2.0f;
min.y += ((max.y - min.y) - textSize.y) / 2.0f;
if (!sourceButton)
{
// Fixes slight misalignment caused by this particular font.
min.y -= Scale(1.0f);
}
SetGradient
(
min,
{ min.x + textSize.x, min.y + textSize.y },
IM_COL32(baser + 192, 255, 0, 255),
IM_COL32(baser + 128, baseg + 170, 0, 255)
);
DrawTextWithOutline<float>
(
font,
size,
min,
IM_COL32(255, 255, 255, 255 * alpha),
buttonText,
sourceButton ? 1.0f : 1.5f,
IM_COL32(baser, baseg, 0, 255 * alpha)
);
ResetGradient();
}
enum SourceColumn
{
SourceColumnLeft,
SourceColumnMiddle,
SourceColumnRight
};
static void DrawSourceButton(SourceColumn sourceColumn, float yRatio, const char *sourceText, bool sourceSet)
{
bool buttonPressed;
float minX, maxX;
switch (sourceColumn)
{
case SourceColumnLeft:
minX = Scale(AlignToNextGrid(CONTAINER_X) + SOURCE_BUTTON_GAP);
maxX = Scale(AlignToNextGrid(CONTAINER_X) + SOURCE_BUTTON_GAP + SOURCE_BUTTON_WIDTH);
break;
case SourceColumnMiddle:
minX = Scale(AlignToNextGrid(CONTAINER_X + CONTAINER_WIDTH / 2.0f) - SOURCE_BUTTON_WIDTH / 2.0f);
maxX = Scale(AlignToNextGrid(CONTAINER_X + CONTAINER_WIDTH / 2.0f) + SOURCE_BUTTON_WIDTH / 2.0f);
break;
case SourceColumnRight:
minX = Scale(AlignToNextGrid(CONTAINER_X + CONTAINER_WIDTH) - SOURCE_BUTTON_GAP - SOURCE_BUTTON_WIDTH);
maxX = Scale(AlignToNextGrid(CONTAINER_X + CONTAINER_WIDTH) - SOURCE_BUTTON_GAP);
break;
}
float minusY = (SOURCE_BUTTON_GAP + BUTTON_HEIGHT) * yRatio;
ImVec2 min = { minX, Scale(AlignToNextGrid(CONTAINER_Y + CONTAINER_HEIGHT) - SOURCE_BUTTON_GAP - BUTTON_HEIGHT - minusY) };
ImVec2 max = { maxX, Scale(AlignToNextGrid(CONTAINER_Y + CONTAINER_HEIGHT) - SOURCE_BUTTON_GAP - minusY) };
DrawButton(min, max, sourceText, true, sourceSet, buttonPressed);
}
static void DrawProgressBar(float progressRatio)
{
auto &res = ImGui::GetIO().DisplaySize;
auto drawList = ImGui::GetForegroundDrawList();
float alpha = 1.0;
const uint32_t innerColor0 = IM_COL32(0, 65, 0, 255 * alpha);
const uint32_t innerColor1 = IM_COL32(0, 32, 0, 255 * alpha);
float xPadding = Scale(6.0f);
float yPadding = Scale(3.0f);
ImVec2 min = { Scale(AlignToNextGrid(CONTAINER_X) + BOTTOM_X_GAP), Scale(AlignToNextGrid(CONTAINER_Y + CONTAINER_HEIGHT) + BOTTOM_Y_GAP) };
ImVec2 max = { Scale(AlignToNextGrid(CONTAINER_X + CONTAINER_WIDTH) - BOTTOM_X_GAP), Scale(AlignToNextGrid(CONTAINER_Y + CONTAINER_HEIGHT) + BOTTOM_Y_GAP + BUTTON_HEIGHT) };
DrawButtonContainer(min, max, 0, 0, alpha);
drawList->AddRectFilledMultiColor
(
{ min.x + xPadding, min.y + yPadding },
{ max.x - xPadding, max.y - yPadding },
innerColor0,
innerColor0,
innerColor1,
innerColor1
);
const uint32_t sliderColor0 = IM_COL32(57, 241, 0, 255 * alpha);
const uint32_t sliderColor1 = IM_COL32(2, 106, 0, 255 * alpha);
xPadding += Scale(1.0f);
yPadding += Scale(1.0f);
ImVec2 sliderMin = { min.x + xPadding, min.y + yPadding };
ImVec2 sliderMax = { max.x - xPadding, max.y - yPadding };
sliderMax.x = sliderMin.x + (sliderMax.x - sliderMin.x) * progressRatio;
drawList->AddRectFilledMultiColor(sliderMin, sliderMax, sliderColor0, sliderColor0, sliderColor1, sliderColor1);
}
static bool ConvertPathSet(const nfdpathset_t *pathSet, std::list<std::filesystem::path> &filePaths)
{
nfdpathsetsize_t pathSetCount = 0;
if (NFD_PathSet_GetCount(pathSet, &pathSetCount) != NFD_OKAY)
{
return false;
}
for (nfdpathsetsize_t i = 0; i < pathSetCount; i++)
{
char *pathSetPath = nullptr;
if (NFD_PathSet_GetPathU8(pathSet, i, &pathSetPath) != NFD_OKAY)
{
filePaths.clear();
return false;
}
filePaths.emplace_back(std::filesystem::path(std::u8string_view((const char8_t *)(pathSetPath))));
NFD_PathSet_FreePathU8(pathSetPath);
}
return true;
}
static bool ShowFilesPicker(std::list<std::filesystem::path> &filePaths)
{
filePaths.clear();
const nfdpathset_t *pathSet;
nfdresult_t result = NFD_OpenDialogMultipleU8(&pathSet, nullptr, 0, nullptr);
if (result == NFD_OKAY)
{
bool pathsConverted = ConvertPathSet(pathSet, filePaths);
NFD_PathSet_Free(pathSet);
return pathsConverted;
}
else
{
return false;
}
}
static bool ShowFoldersPicker(std::list<std::filesystem::path> &folderPaths)
{
folderPaths.clear();
const nfdpathset_t *pathSet;
nfdresult_t result = NFD_PickFolderMultipleU8(&pathSet, nullptr);
if (result == NFD_OKAY)
{
bool pathsConverted = ConvertPathSet(pathSet, folderPaths);
NFD_PathSet_Free(pathSet);
return pathsConverted;
}
else
{
return false;
}
}
static void ParseSourcePaths(std::list<std::filesystem::path> &paths)
{
assert((g_currentPage == WizardPage::SelectGameAndUpdate) || (g_currentPage == WizardPage::SelectDLC));
std::list<std::filesystem::path> failedPaths;
if (g_currentPage == WizardPage::SelectGameAndUpdate)
{
for (const std::filesystem::path &path : paths)
{
if (Installer::parseGame(path))
{
g_gameSourcePath = path;
}
else if (Installer::parseUpdate(path))
{
g_updateSourcePath = path;
}
else
{
failedPaths.push_back(path);
}
}
}
else if(g_currentPage == WizardPage::SelectDLC)
{
for (const std::filesystem::path &path : paths)
{
DLC dlc = Installer::parseDLC(path);
if (dlc != DLC::Unknown)
{
g_dlcSourcePaths[DLCIndex(dlc)] = path;
}
else
{
failedPaths.push_back(path);
}
}
}
if (!failedPaths.empty())
{
// TODO: Show list of content that failed to parse with a prompt.
}
}
static void DrawSourcePickers()
{
bool buttonPressed = false;
std::list<std::filesystem::path> paths;
if (g_currentPage == WizardPage::SelectGameAndUpdate || g_currentPage == WizardPage::SelectDLC)
{
ImVec2 textSize = g_dfsogeistdFont->CalcTextSizeA(20.0f, FLT_MAX, 0.0f, FILES_BUTTON_TEXT);
textSize.x += BUTTON_TEXT_GAP;
ImVec2 min = { Scale(AlignToNextGrid(CONTAINER_X) + BOTTOM_X_GAP), Scale(AlignToNextGrid(CONTAINER_Y + CONTAINER_HEIGHT) + BOTTOM_Y_GAP) };
ImVec2 max = { Scale(AlignToNextGrid(CONTAINER_X) + BOTTOM_X_GAP + textSize.x), Scale(AlignToNextGrid(CONTAINER_Y + CONTAINER_HEIGHT) + BOTTOM_Y_GAP + BUTTON_HEIGHT) };
DrawButton(min, max, FILES_BUTTON_TEXT, false, true, buttonPressed);
if (buttonPressed && ShowFilesPicker(paths))
{
ParseSourcePaths(paths);
}
min.x += Scale(BOTTOM_X_GAP + textSize.x);
textSize = g_dfsogeistdFont->CalcTextSizeA(20.0f, FLT_MAX, 0.0f, FOLDER_BUTTON_TEXT);
textSize.x += BUTTON_TEXT_GAP;
max.x = min.x + Scale(textSize.x);
DrawButton(min, max, FOLDER_BUTTON_TEXT, false, true, buttonPressed);
if (buttonPressed && ShowFoldersPicker(paths))
{
ParseSourcePaths(paths);
}
}
}
static void DrawSources()
{
if (g_currentPage == WizardPage::SelectGameAndUpdate)
{
DrawSourceButton(SourceColumnMiddle, 1.5f, GAME_SOURCE_TEXT, !g_gameSourcePath.empty());
DrawSourceButton(SourceColumnMiddle, 0.5f, UPDATE_SOURCE_TEXT, !g_updateSourcePath.empty());
}
if (g_currentPage == WizardPage::SelectDLC)
{
for (int i = 0; i < 6; i++)
{
DrawSourceButton((i < 3) ? SourceColumnLeft : SourceColumnRight, float(i % 3), DLC_SOURCE_TEXT[i], !g_dlcSourcePaths[i].empty());
}
}
}
static void DrawInstallingProgress()
{
if (g_currentPage == WizardPage::Installing)
{
float ratioTarget = g_installerProgressRatioTarget.load();
g_installerProgressRatioCurrent += (4.0f * ImGui::GetIO().DeltaTime * (ratioTarget - g_installerProgressRatioCurrent));
DrawProgressBar(g_installerProgressRatioCurrent);
if (g_installerFinished)
{
g_installerThread->join();
g_installerThread.reset();
g_currentPage = g_installerFailed ? WizardPage::InstallFailed : WizardPage::InstallSucceeded;
}
}
}
static void InstallerThread()
{
if (!Installer::install(g_installerSources, ".", false, g_installerJournal, [&]() {
g_installerProgressRatioTarget = float(double(g_installerJournal.progressCounter) / double(g_installerJournal.progressTotal));
}))
{
g_installerFailed = true;
g_installerErrorMessage = g_installerJournal.lastErrorMessage;
// Delete all files that were copied.
Installer::rollback(g_installerJournal);
}
g_installerFinished = true;
}
static void InstallerStart()
{
g_installerStartTime = ImGui::GetTime();
g_installerProgressRatioCurrent = 0.0f;
g_installerProgressRatioTarget = 0.0f;
g_installerFailed = false;
g_installerFinished = false;
g_installerThread = std::make_unique<std::thread>(InstallerThread);
}
static bool InstallerParseSources()
{
std::filesystem::space_info spaceInfo = std::filesystem::space(".");
g_installerAvailableSize = spaceInfo.available;
Installer::Input installerInput;
installerInput.gameSource = g_gameSourcePath;
installerInput.updateSource = g_updateSourcePath;
for (std::filesystem::path &path : g_dlcSourcePaths) {
if (!path.empty())
{
installerInput.dlcSources.push_back(path);
}
}
return Installer::parseSources(installerInput, g_installerJournal, g_installerSources);
}
static void DrawNextButton()
{
if (g_currentPage != WizardPage::Installing) {
bool nextButtonEnabled = !g_isDisappearing;
#if !SKIP_SOURCE_CHECKS
if (nextButtonEnabled && g_currentPage == WizardPage::SelectGameAndUpdate)
{
nextButtonEnabled = !g_gameSourcePath.empty() && !g_updateSourcePath.empty();
}
#endif
bool skipButton = false;
if (g_currentPage == WizardPage::SelectDLC)
{
skipButton = std::all_of(g_dlcSourcePaths.begin(), g_dlcSourcePaths.end(), [](const std::filesystem::path &path) { return path.empty(); });
}
const char *buttonText = skipButton ? SKIP_BUTTON_TEXT : NEXT_BUTTON_TEXT;
ImVec2 textSize = g_newRodinFont->CalcTextSizeA(20.0f, FLT_MAX, 0.0f, buttonText);
textSize.x += BUTTON_TEXT_GAP;
ImVec2 min = { Scale(AlignToNextGrid(CONTAINER_X + CONTAINER_WIDTH) - textSize.x - BOTTOM_X_GAP), Scale(AlignToNextGrid(CONTAINER_Y + CONTAINER_HEIGHT) + BOTTOM_Y_GAP) };
ImVec2 max = { Scale(AlignToNextGrid(CONTAINER_X + CONTAINER_WIDTH) - BOTTOM_X_GAP), Scale(AlignToNextGrid(CONTAINER_Y + CONTAINER_HEIGHT) + BOTTOM_Y_GAP + BUTTON_HEIGHT) };
bool buttonPressed = false;
DrawButton(min, max, buttonText, false, nextButtonEnabled, buttonPressed);
if (buttonPressed)
{
XexPatcher::Result patcherResult;
if (g_currentPage == WizardPage::SelectGameAndUpdate && (patcherResult = Installer::checkGameUpdateCompatibility(g_gameSourcePath, g_updateSourcePath), patcherResult != XexPatcher::Result::Success))
{
// TODO: Show error prompt for compatibility failure check. Tell user to try with different files.
}
else if (g_currentPage == WizardPage::SelectDLC && !InstallerParseSources())
{
// TODO: Show an error that the sources were unable to be parsed. Ask to try again.
}
else if (g_currentPage == WizardPage::InstallSucceeded)
{
g_isDisappearing = true;
g_disappearTime = ImGui::GetTime();
}
else if (g_currentPage == WizardPage::InstallFailed)
{
g_currentPage = WizardPage::Introduction;
}
else
{
g_currentPage = WizardPage(int(g_currentPage) + 1);
if (g_currentPage == WizardPage::Installing)
{
InstallerStart();
}
}
}
}
}
static void DrawHorizontalBorder(bool bottomBorder)
{
const uint32_t FADE_COLOR_LEFT = IM_COL32(155, 155, 155, 0);
const uint32_t SOLID_COLOR = IM_COL32(155, 200, 155, 255);
const uint32_t FADE_COLOR_RIGHT = IM_COL32(155, 225, 155, 0);
auto drawList = ImGui::GetForegroundDrawList();
double borderScale = 1.0 - ComputeMotionInstaller(g_appearTime, g_disappearTime, 0.0, CONTAINER_LINE_ANIMATION_DURATION);
float midX = Scale(AlignToNextGrid(CONTAINER_X + CONTAINER_WIDTH / 5));
float minX = std::lerp(Scale(AlignToNextGrid(CONTAINER_X) - BORDER_SIZE - BORDER_OVERSHOOT), midX, borderScale);
float maxX = std::lerp(Scale(AlignToNextGrid(CONTAINER_X + CONTAINER_WIDTH + SIDE_CONTAINER_WIDTH) + BORDER_OVERSHOOT), midX, borderScale);
float minY = bottomBorder ? Scale(AlignToNextGrid(CONTAINER_Y + CONTAINER_HEIGHT)) : Scale(AlignToNextGrid(CONTAINER_Y) - BORDER_SIZE);
float maxY = minY + Scale(BORDER_SIZE);
drawList->AddRectFilledMultiColor
(
{ minX, minY },
{ midX, maxY },
FADE_COLOR_LEFT,
SOLID_COLOR,
SOLID_COLOR,
FADE_COLOR_LEFT
);
drawList->AddRectFilledMultiColor
(
{ midX, minY },
{ maxX, maxY },
SOLID_COLOR,
FADE_COLOR_RIGHT,
FADE_COLOR_RIGHT,
SOLID_COLOR
);
}
static void DrawVerticalBorder(bool rightBorder)
{
const uint32_t SOLID_COLOR = IM_COL32(155, rightBorder ? 225 : 155, 155, 255);
const uint32_t FADE_COLOR = IM_COL32(155, rightBorder ? 225 : 155, 155, 0);
auto drawList = ImGui::GetForegroundDrawList();
double borderScale = 1.0 - ComputeMotionInstaller(g_appearTime, g_disappearTime, 0.0, CONTAINER_LINE_ANIMATION_DURATION);
float minX = rightBorder ? Scale(AlignToNextGrid(CONTAINER_X + CONTAINER_WIDTH)) : Scale(AlignToNextGrid(CONTAINER_X) - BORDER_SIZE);
float maxX = minX + Scale(BORDER_SIZE);
float midY = Scale(AlignToNextGrid(CONTAINER_Y + CONTAINER_HEIGHT / 2));
float minY = std::lerp(Scale(AlignToNextGrid(CONTAINER_Y) - BORDER_OVERSHOOT), midY, borderScale);
float maxY = std::lerp(Scale(AlignToNextGrid(CONTAINER_Y + CONTAINER_HEIGHT) + BORDER_OVERSHOOT), midY, borderScale);
drawList->AddRectFilledMultiColor
(
{ minX, minY },
{ maxX, midY },
FADE_COLOR,
FADE_COLOR,
SOLID_COLOR,
SOLID_COLOR
);
drawList->AddRectFilledMultiColor
(
{ minX, midY },
{ maxX, maxY },
SOLID_COLOR,
SOLID_COLOR,
FADE_COLOR,
FADE_COLOR
);
}
static void DrawBorders()
{
DrawHorizontalBorder(false);
DrawHorizontalBorder(true);
DrawVerticalBorder(false);
DrawVerticalBorder(true);
}
void InstallerWizard::Init()
{
auto &io = ImGui::GetIO();
constexpr float FONT_SCALE = 2.0f;
g_seuratFont = io.Fonts->AddFontFromFileTTF("FOT-SeuratPro-M.otf", 26.0f * FONT_SCALE);
g_dfsogeistdFont = io.Fonts->AddFontFromFileTTF("DFSoGeiStd-W7.otf", 48.0f * FONT_SCALE);
g_newRodinFont = io.Fonts->AddFontFromFileTTF("FOT-NewRodinPro-DB.otf", 20.0f * FONT_SCALE);
g_installTextures[0] = LoadTexture(g_install001DDS, g_install001DDS_size);
g_installTextures[1] = LoadTexture(g_install002DDS, g_install002DDS_size);
g_installTextures[2] = LoadTexture(g_install003DDS, g_install003DDS_size);
g_installTextures[3] = LoadTexture(g_install004DDS, g_install004DDS_size);
g_installTextures[4] = LoadTexture(g_install005DDS, g_install005DDS_size);
g_installTextures[5] = LoadTexture(g_install006DDS, g_install006DDS_size);
g_installTextures[6] = LoadTexture(g_install007DDS, g_install007DDS_size);
g_installTextures[7] = LoadTexture(g_install008DDS, g_install008DDS_size);
}
void InstallerWizard::Draw()
{
if (!s_isVisible)
{
return;
}
DrawBackground();
DrawLeftImage();
DrawScanlineBars();
DrawDescriptionContainer();
DrawSourcePickers();
DrawSources();
DrawInstallingProgress();
DrawNextButton();
DrawBorders();
if (g_isDisappearing)
{
const double disappearDuration = ALL_ANIMATIONS_FULL_DURATION / 60.0;
if (ImGui::GetTime() > (g_disappearTime + disappearDuration))
{
s_isVisible = false;
}
}
}
void InstallerWizard::Shutdown()
{
if (g_installerThread != nullptr)
{
g_installerThread->join();
g_installerThread.reset();
}
g_installerSources.game.reset();
g_installerSources.update.reset();
g_installerSources.dlc.clear();
}
bool InstallerWizard::Run(bool skipGame)
{
NFD_Init();
if (skipGame)
{
g_currentPage = WizardPage::SelectDLC;
}
Window::SetCursorAllowed(true);
s_isVisible = true;
while (s_isVisible)
{
SDL_PumpEvents();
SDL_FlushEvents(SDL_FIRSTEVENT, SDL_LASTEVENT);
Window::Update();
Video::HostPresent();
}
Window::SetCursorAllowed(false);
NFD_Quit();
return true;
}

View file

@ -0,0 +1,13 @@
#pragma once
#include <api/SWA.h>
struct InstallerWizard
{
inline static bool s_isVisible = false;
static void Init();
static void Draw();
static void Shutdown();
static bool Run(bool skipGame);
};

View file

@ -982,12 +982,11 @@ void OptionsMenu::Init()
void OptionsMenu::Draw() void OptionsMenu::Draw()
{ {
auto pInputState = SWA::CInputState::GetInstance();
if (!s_isVisible) if (!s_isVisible)
return; return;
// We've entered the menu now, no need to check this. // We've entered the menu now, no need to check this.
auto pInputState = SWA::CInputState::GetInstance();
if (pInputState->GetPadState().IsReleased(SWA::eKeyState_A)) if (pInputState->GetPadState().IsReleased(SWA::eKeyState_A))
g_isEnterKeyBuffered = false; g_isEnterKeyBuffered = false;

View file

@ -85,7 +85,7 @@ int Window_OnSDLEvent(void*, SDL_Event* event)
case SDL_WINDOWEVENT_FOCUS_GAINED: case SDL_WINDOWEVENT_FOCUS_GAINED:
Window::s_isFocused = true; Window::s_isFocused = true;
SDL_ShowCursor(Window::IsFullscreen() ? SDL_DISABLE : SDL_ENABLE); SDL_ShowCursor(Window::IsFullscreen() && !Window::s_cursorAllowed ? SDL_DISABLE : SDL_ENABLE);
break; break;
case SDL_WINDOWEVENT_RESTORED: case SDL_WINDOWEVENT_RESTORED:

View file

@ -21,6 +21,7 @@ public:
inline static bool s_isFocused; inline static bool s_isFocused;
inline static bool s_isIconNight; inline static bool s_isIconNight;
inline static bool s_cursorAllowed = false;
static SDL_Surface* GetIconSurface(void* pIconBmp, size_t iconSize) static SDL_Surface* GetIconSurface(void* pIconBmp, size_t iconSize)
{ {
@ -76,7 +77,7 @@ public:
if (isEnabled) if (isEnabled)
{ {
SDL_SetWindowFullscreen(s_pWindow, SDL_WINDOW_FULLSCREEN_DESKTOP); SDL_SetWindowFullscreen(s_pWindow, SDL_WINDOW_FULLSCREEN_DESKTOP);
SDL_ShowCursor(SDL_DISABLE); SDL_ShowCursor(s_cursorAllowed ? SDL_ENABLE : SDL_DISABLE);
} }
else else
{ {
@ -88,6 +89,14 @@ public:
return isEnabled; return isEnabled;
} }
static void SetCursorAllowed(bool isCursorAllowed)
{
s_cursorAllowed = isCursorAllowed;
// Refresh fullscreen state to enable the right cursor behavior.
SetFullscreen(IsFullscreen());
}
static bool IsMaximised() static bool IsMaximised()
{ {
return SDL_GetWindowFlags(s_pWindow) & SDL_WINDOW_MAXIMIZED; return SDL_GetWindowFlags(s_pWindow) & SDL_WINDOW_MAXIMIZED;

@ -1 +1 @@
Subproject commit 7183c6e0e90480fe0f2c270c87cea5cd5df7d5c3 Subproject commit d9063dd234b92fd7ab35e72d1839d0fcfdc83456

View file

@ -22,6 +22,7 @@
"features": [ "sdl2-binding" ] "features": [ "sdl2-binding" ]
}, },
"magic-enum", "magic-enum",
"readerwriterqueue" "readerwriterqueue",
"nativefiledialog-extended"
] ]
} }