Merge branch 'options-menu-and-installer' into options-menu

# Conflicts:
#	UnleashedRecomp/CMakeLists.txt
#	UnleashedRecomp/gpu/video.cpp
#	UnleashedRecomp/gpu/video.h
#	UnleashedRecomp/locale/locale.h
#	UnleashedRecomp/ui/message_window.cpp
This commit is contained in:
Skyth 2024-12-05 17:56:55 +03:00
commit 2bd80f29b1
21 changed files with 1827 additions and 187 deletions

View file

@ -111,6 +111,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/button_guide.cpp" "ui/button_guide.cpp"
"ui/message_window.cpp" "ui/message_window.cpp"
"ui/options_menu.cpp" "ui/options_menu.cpp"
@ -199,6 +200,7 @@ find_package(unofficial-concurrentqueue REQUIRED)
find_package(imgui CONFIG REQUIRED) 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_package(nfd CONFIG REQUIRED)
find_path(MINIAUDIO_INCLUDE_DIRS "miniaudio.h") find_path(MINIAUDIO_INCLUDE_DIRS "miniaudio.h")
file(MAKE_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/D3D12) file(MAKE_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/D3D12)
@ -237,6 +239,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
@ -318,6 +321,8 @@ generate_aggregate_header(
set(RESOURCES_SOURCE_PATH "${PROJECT_SOURCE_DIR}/../UnleashedRecompResources") set(RESOURCES_SOURCE_PATH "${PROJECT_SOURCE_DIR}/../UnleashedRecompResources")
set(RESOURCES_OUTPUT_PATH "${PROJECT_SOURCE_DIR}/res") set(RESOURCES_OUTPUT_PATH "${PROJECT_SOURCE_DIR}/res")
BIN2C(TARGET_OBJ UnleashedRecomp SOURCE_FILE "${RESOURCES_SOURCE_PATH}/font/im_font_atlas.bin" DEST_FILE "${RESOURCES_OUTPUT_PATH}/font/im_font_atlas.bin" ARRAY_NAME "g_im_font_atlas" COMPRESSION_TYPE "zstd")
BIN2C(TARGET_OBJ UnleashedRecomp SOURCE_FILE "${RESOURCES_SOURCE_PATH}/font/im_font_atlas.dds" DEST_FILE "${RESOURCES_OUTPUT_PATH}/font/im_font_atlas.dds" ARRAY_NAME "g_im_font_atlas_texture" COMPRESSION_TYPE "zstd")
BIN2C(TARGET_OBJ UnleashedRecomp SOURCE_FILE "${RESOURCES_SOURCE_PATH}/images/achievements_menu/trophy.dds" DEST_FILE "${RESOURCES_OUTPUT_PATH}/images/achievements_menu/trophy.dds" ARRAY_NAME "g_trophy" COMPRESSION_TYPE "zstd") BIN2C(TARGET_OBJ UnleashedRecomp SOURCE_FILE "${RESOURCES_SOURCE_PATH}/images/achievements_menu/trophy.dds" DEST_FILE "${RESOURCES_OUTPUT_PATH}/images/achievements_menu/trophy.dds" ARRAY_NAME "g_trophy" COMPRESSION_TYPE "zstd")
BIN2C(TARGET_OBJ UnleashedRecomp SOURCE_FILE "${RESOURCES_SOURCE_PATH}/images/common/general_window.dds" DEST_FILE "${RESOURCES_OUTPUT_PATH}/images/common/general_window.dds" ARRAY_NAME "g_general_window" COMPRESSION_TYPE "zstd") BIN2C(TARGET_OBJ UnleashedRecomp SOURCE_FILE "${RESOURCES_SOURCE_PATH}/images/common/general_window.dds" DEST_FILE "${RESOURCES_OUTPUT_PATH}/images/common/general_window.dds" ARRAY_NAME "g_general_window" COMPRESSION_TYPE "zstd")
BIN2C(TARGET_OBJ UnleashedRecomp SOURCE_FILE "${RESOURCES_SOURCE_PATH}/images/common/left_mouse_button.dds" DEST_FILE "${RESOURCES_OUTPUT_PATH}/images/common/left_mouse_button.dds" ARRAY_NAME "g_left_mouse_button" COMPRESSION_TYPE "zstd") BIN2C(TARGET_OBJ UnleashedRecomp SOURCE_FILE "${RESOURCES_SOURCE_PATH}/images/common/left_mouse_button.dds" DEST_FILE "${RESOURCES_OUTPUT_PATH}/images/common/left_mouse_button.dds" ARRAY_NAME "g_left_mouse_button" COMPRESSION_TYPE "zstd")
@ -327,5 +332,14 @@ BIN2C(TARGET_OBJ UnleashedRecomp SOURCE_FILE "${RESOURCES_SOURCE_PATH}/images/co
BIN2C(TARGET_OBJ UnleashedRecomp SOURCE_FILE "${RESOURCES_SOURCE_PATH}/images/common/start_back.dds" DEST_FILE "${RESOURCES_OUTPUT_PATH}/images/common/start_back.dds" ARRAY_NAME "g_start_back" COMPRESSION_TYPE "zstd") BIN2C(TARGET_OBJ UnleashedRecomp SOURCE_FILE "${RESOURCES_SOURCE_PATH}/images/common/start_back.dds" DEST_FILE "${RESOURCES_OUTPUT_PATH}/images/common/start_back.dds" ARRAY_NAME "g_start_back" COMPRESSION_TYPE "zstd")
BIN2C(TARGET_OBJ UnleashedRecomp SOURCE_FILE "${RESOURCES_SOURCE_PATH}/images/game_icon.bmp" DEST_FILE "${RESOURCES_OUTPUT_PATH}/images/game_icon.bmp" ARRAY_NAME "g_game_icon") BIN2C(TARGET_OBJ UnleashedRecomp SOURCE_FILE "${RESOURCES_SOURCE_PATH}/images/game_icon.bmp" DEST_FILE "${RESOURCES_OUTPUT_PATH}/images/game_icon.bmp" ARRAY_NAME "g_game_icon")
BIN2C(TARGET_OBJ UnleashedRecomp SOURCE_FILE "${RESOURCES_SOURCE_PATH}/images/game_icon_night.bmp" DEST_FILE "${RESOURCES_OUTPUT_PATH}/images/game_icon_night.bmp" ARRAY_NAME "g_game_icon_night") BIN2C(TARGET_OBJ UnleashedRecomp SOURCE_FILE "${RESOURCES_SOURCE_PATH}/images/game_icon_night.bmp" DEST_FILE "${RESOURCES_OUTPUT_PATH}/images/game_icon_night.bmp" ARRAY_NAME "g_game_icon_night")
BIN2C(TARGET_OBJ UnleashedRecomp SOURCE_FILE "${RESOURCES_SOURCE_PATH}/font/im_font_atlas.bin" DEST_FILE "${RESOURCES_OUTPUT_PATH}/font/im_font_atlas.bin" ARRAY_NAME "g_im_font_atlas" COMPRESSION_TYPE "zstd") BIN2C(TARGET_OBJ UnleashedRecomp SOURCE_FILE "${RESOURCES_SOURCE_PATH}/images/installer/arrow_circle.dds" DEST_FILE "${RESOURCES_OUTPUT_PATH}/images/installer/arrow_circle.dds" ARRAY_NAME "g_arrow_circle" COMPRESSION_TYPE "zstd")
BIN2C(TARGET_OBJ UnleashedRecomp SOURCE_FILE "${RESOURCES_SOURCE_PATH}/font/im_font_atlas.dds" DEST_FILE "${RESOURCES_OUTPUT_PATH}/font/im_font_atlas.dds" ARRAY_NAME "g_im_font_atlas_texture" COMPRESSION_TYPE "zstd") BIN2C(TARGET_OBJ UnleashedRecomp SOURCE_FILE "${RESOURCES_SOURCE_PATH}/images/installer/install_001.dds" DEST_FILE "${RESOURCES_OUTPUT_PATH}/images/installer/install_001.dds" ARRAY_NAME "g_install_001" COMPRESSION_TYPE "zstd")
BIN2C(TARGET_OBJ UnleashedRecomp SOURCE_FILE "${RESOURCES_SOURCE_PATH}/images/installer/install_002.dds" DEST_FILE "${RESOURCES_OUTPUT_PATH}/images/installer/install_002.dds" ARRAY_NAME "g_install_002" COMPRESSION_TYPE "zstd")
BIN2C(TARGET_OBJ UnleashedRecomp SOURCE_FILE "${RESOURCES_SOURCE_PATH}/images/installer/install_003.dds" DEST_FILE "${RESOURCES_OUTPUT_PATH}/images/installer/install_003.dds" ARRAY_NAME "g_install_003" COMPRESSION_TYPE "zstd")
BIN2C(TARGET_OBJ UnleashedRecomp SOURCE_FILE "${RESOURCES_SOURCE_PATH}/images/installer/install_004.dds" DEST_FILE "${RESOURCES_OUTPUT_PATH}/images/installer/install_004.dds" ARRAY_NAME "g_install_004" COMPRESSION_TYPE "zstd")
BIN2C(TARGET_OBJ UnleashedRecomp SOURCE_FILE "${RESOURCES_SOURCE_PATH}/images/installer/install_005.dds" DEST_FILE "${RESOURCES_OUTPUT_PATH}/images/installer/install_005.dds" ARRAY_NAME "g_install_005" COMPRESSION_TYPE "zstd")
BIN2C(TARGET_OBJ UnleashedRecomp SOURCE_FILE "${RESOURCES_SOURCE_PATH}/images/installer/install_006.dds" DEST_FILE "${RESOURCES_OUTPUT_PATH}/images/installer/install_006.dds" ARRAY_NAME "g_install_006" COMPRESSION_TYPE "zstd")
BIN2C(TARGET_OBJ UnleashedRecomp SOURCE_FILE "${RESOURCES_SOURCE_PATH}/images/installer/install_007.dds" DEST_FILE "${RESOURCES_OUTPUT_PATH}/images/installer/install_007.dds" ARRAY_NAME "g_install_007" COMPRESSION_TYPE "zstd")
BIN2C(TARGET_OBJ UnleashedRecomp SOURCE_FILE "${RESOURCES_SOURCE_PATH}/images/installer/install_008.dds" DEST_FILE "${RESOURCES_OUTPUT_PATH}/images/installer/install_008.dds" ARRAY_NAME "g_install_008" COMPRESSION_TYPE "zstd")
BIN2C(TARGET_OBJ UnleashedRecomp SOURCE_FILE "${RESOURCES_SOURCE_PATH}/images/installer/miles_electric_icon.dds" DEST_FILE "${RESOURCES_OUTPUT_PATH}/images/installer/miles_electric_icon.dds" ARRAY_NAME "g_miles_electric_icon" COMPRESSION_TYPE "zstd")
BIN2C(TARGET_OBJ UnleashedRecomp SOURCE_FILE "${RESOURCES_SOURCE_PATH}/images/installer/pulse_install.dds" DEST_FILE "${RESOURCES_OUTPUT_PATH}/images/installer/pulse_install.dds" ARRAY_NAME "g_pulse_install" COMPRESSION_TYPE "zstd")

View file

@ -3,12 +3,14 @@
#include <ui/window.h> #include <ui/window.h>
#include <patches/audio_patches.h> #include <patches/audio_patches.h>
bool g_isGameLoaded = false;
double g_deltaTime; double g_deltaTime;
// CApplication::Update // CApplication::Update
PPC_FUNC_IMPL(__imp__sub_822C1130); PPC_FUNC_IMPL(__imp__sub_822C1130);
PPC_FUNC(sub_822C1130) PPC_FUNC(sub_822C1130)
{ {
g_isGameLoaded = true;
g_deltaTime = ctx.f1.f64; g_deltaTime = ctx.f1.f64;
SDL_PumpEvents(); SDL_PumpEvents();

View file

@ -1,3 +1,4 @@
#pragma once #pragma once
extern bool g_isGameLoaded;
extern double g_deltaTime; extern double g_deltaTime;

View file

@ -2,11 +2,16 @@
#include <kernel/heap.h> #include <kernel/heap.h>
#include <kernel/memory.h> #include <kernel/memory.h>
#include <cpu/guest_stack_var.h> #include <cpu/guest_stack_var.h>
#include <ui/installer_wizard.h>
#include <ui/window.h> #include <ui/window.h>
#include <api/boost/smart_ptr/shared_ptr.h> #include <api/boost/smart_ptr/shared_ptr.h>
SWA_API void Game_PlaySound(const char* pName) SWA_API void Game_PlaySound(const char* pName)
{ {
// TODO: use own sound player.
if (InstallerWizard::s_isVisible)
return;
guest_stack_var<boost::anonymous_shared_ptr> soundPlayer; guest_stack_var<boost::anonymous_shared_ptr> soundPlayer;
GuestToHostFunction<void>(sub_82B4DF50, soundPlayer.get(), ((be<uint32_t>*)g_memory.Translate(0x83367900))->get(), 7, 0, 0); GuestToHostFunction<void>(sub_82B4DF50, soundPlayer.get(), ((be<uint32_t>*)g_memory.Translate(0x83367900))->get(), 7, 0, 0);

View file

@ -10,7 +10,9 @@
enum class ImGuiCallback : int32_t enum class ImGuiCallback : int32_t
{ {
SetGradient = -1, SetGradient = -1,
SetShaderModifier = -2 SetShaderModifier = -2,
SetOrigin = -3,
SetScale = -4,
}; };
union ImGuiCallbackData union ImGuiCallbackData
@ -27,6 +29,16 @@ union ImGuiCallbackData
{ {
uint32_t shaderModifier; uint32_t shaderModifier;
} setShaderModifier; } setShaderModifier;
struct
{
float origin[2];
} setOrigin;
struct
{
float scale[2];
} setScale;
}; };
#endif #endif

View file

@ -9,6 +9,8 @@ struct PushConstants
uint ShaderModifier; uint ShaderModifier;
uint Texture2DDescriptorIndex; uint Texture2DDescriptorIndex;
float2 InverseDisplaySize; float2 InverseDisplaySize;
float2 Origin;
float2 Scale;
}; };
Texture2D<float4> g_Texture2DDescriptorHeap[] : register(t0, space0); Texture2D<float4> g_Texture2DDescriptorHeap[] : register(t0, space0);

View file

@ -2,7 +2,8 @@
void main(in float2 position : POSITION, in float2 uv : TEXCOORD, in float4 color : COLOR, out Interpolators interpolators) void main(in float2 position : POSITION, in float2 uv : TEXCOORD, in float4 color : COLOR, out Interpolators interpolators)
{ {
interpolators.Position = float4(position * g_PushConstants.InverseDisplaySize * float2(2.0, -2.0) + float2(-1.0, 1.0), 0.0, 1.0); float2 correctedPosition = g_PushConstants.Origin + (position - g_PushConstants.Origin) * g_PushConstants.Scale;
interpolators.Position = float4(correctedPosition * g_PushConstants.InverseDisplaySize * float2(2.0, -2.0) + float2(-1.0, 1.0), 0.0, 1.0);
interpolators.UV = uv; interpolators.UV = uv;
interpolators.Color = color; interpolators.Color = color;
} }

View file

@ -14,6 +14,7 @@
#include <ui/button_guide.h> #include <ui/button_guide.h>
#include <ui/message_window.h> #include <ui/message_window.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"
@ -1060,6 +1061,8 @@ struct ImGuiPushConstants
uint32_t shaderModifier{}; uint32_t shaderModifier{};
uint32_t texture2DDescriptorIndex{}; uint32_t texture2DDescriptorIndex{};
ImVec2 inverseDisplaySize{}; ImVec2 inverseDisplaySize{};
ImVec2 origin{ 0.0f, 0.0f };
ImVec2 scale{ 1.0f, 1.0f };
}; };
static void CreateImGuiBackend() static void CreateImGuiBackend()
@ -1082,6 +1085,7 @@ static void CreateImGuiBackend()
ButtonGuide::Init(); ButtonGuide::Init();
MessageWindow::Init(); MessageWindow::Init();
OptionsMenu::Init(); OptionsMenu::Init();
InstallerWizard::Init();
ImGui_ImplSDL2_InitForOther(Window::s_pWindow); ImGui_ImplSDL2_InitForOther(Window::s_pWindow);
@ -1223,7 +1227,9 @@ static void CreateImGuiBackend()
#endif #endif
} }
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;
@ -1413,20 +1419,22 @@ 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() void Video::WaitForGPU()
{ {
if (g_vulkan) if (g_vulkan)
{ {
@ -1447,12 +1455,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()
@ -1469,7 +1476,7 @@ static void BeginCommandList()
if (!g_swapChainValid) if (!g_swapChainValid)
{ {
WaitForGPU(); Video::WaitForGPU();
g_backBuffer->framebuffers.clear(); g_backBuffer->framebuffers.clear();
g_swapChainValid = g_swapChain->resize(); g_swapChainValid = g_swapChain->resize();
g_needsResize = g_swapChainValid; g_needsResize = g_swapChainValid;
@ -1493,7 +1500,7 @@ static void BeginCommandList()
if (g_intermediaryBackBufferTextureDescriptorIndex == NULL) if (g_intermediaryBackBufferTextureDescriptorIndex == NULL)
g_intermediaryBackBufferTextureDescriptorIndex = g_textureDescriptorAllocator.allocate(); g_intermediaryBackBufferTextureDescriptorIndex = g_textureDescriptorAllocator.allocate();
WaitForGPU(); // Fine to wait for GPU, this'll only happen during resize. Video::WaitForGPU(); // Fine to wait for GPU, this'll only happen during resize.
g_intermediaryBackBufferTexture = g_device->createTexture(RenderTextureDesc::Texture2D(width, height, 1, BACKBUFFER_FORMAT, RenderTextureFlag::RENDER_TARGET)); g_intermediaryBackBufferTexture = g_device->createTexture(RenderTextureDesc::Texture2D(width, height, 1, BACKBUFFER_FORMAT, RenderTextureFlag::RENDER_TARGET));
g_textureDescriptorSet->setTexture(g_intermediaryBackBufferTextureDescriptorIndex, g_intermediaryBackBufferTexture.get(), RenderTextureLayout::SHADER_READ); g_textureDescriptorSet->setTexture(g_intermediaryBackBufferTextureDescriptorIndex, g_intermediaryBackBufferTexture.get(), RenderTextureLayout::SHADER_READ);
@ -1540,21 +1547,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));
@ -1788,6 +1791,7 @@ static void DrawImGui()
AchievementMenu::Draw(); AchievementMenu::Draw();
OptionsMenu::Draw(); OptionsMenu::Draw();
AchievementOverlay::Draw(); AchievementOverlay::Draw();
InstallerWizard::Draw();
MessageWindow::Draw(); MessageWindow::Draw();
ButtonGuide::Draw(); ButtonGuide::Draw();
@ -1804,8 +1808,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());
@ -1849,6 +1862,12 @@ static void ProcDrawImGui(const RenderCommand& cmd)
case ImGuiCallback::SetShaderModifier: case ImGuiCallback::SetShaderModifier:
commandList->setGraphicsPushConstants(0, &callbackData->setShaderModifier, offsetof(ImGuiPushConstants, shaderModifier), sizeof(callbackData->setShaderModifier)); commandList->setGraphicsPushConstants(0, &callbackData->setShaderModifier, offsetof(ImGuiPushConstants, shaderModifier), sizeof(callbackData->setShaderModifier));
break; break;
case ImGuiCallback::SetOrigin:
commandList->setGraphicsPushConstants(0, &callbackData->setOrigin, offsetof(ImGuiPushConstants, origin), sizeof(callbackData->setOrigin));
break;
case ImGuiCallback::SetScale:
commandList->setGraphicsPushConstants(0, &callbackData->setScale, offsetof(ImGuiPushConstants, scale), sizeof(callbackData->setScale));
break;
} }
} }
else else
@ -1879,21 +1898,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;
@ -1901,10 +1920,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];
@ -1950,11 +1979,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);
} }
@ -1965,7 +1994,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());
@ -1984,14 +2013,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),
@ -2025,7 +2054,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()
@ -4171,10 +4201,10 @@ static RenderFormat ConvertDXGIFormat(ddspp::DXGIFormat format)
} }
} }
static bool LoadTexture(GuestTexture& texture, uint8_t* data, size_t dataSize, RenderComponentMapping componentMapping) static bool LoadTexture(GuestTexture& texture, const uint8_t* data, size_t dataSize, RenderComponentMapping componentMapping)
{ {
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);
@ -4243,7 +4273,7 @@ static bool LoadTexture(GuestTexture& texture, uint8_t* data, size_t dataSize, R
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)
@ -4336,7 +4366,7 @@ static bool LoadTexture(GuestTexture& texture, uint8_t* data, size_t dataSize, R
return false; return false;
} }
std::unique_ptr<GuestTexture> LoadTexture(uint8_t* data, size_t dataSize, RenderComponentMapping componentMapping) std::unique_ptr<GuestTexture> LoadTexture(const uint8_t* data, size_t dataSize, RenderComponentMapping componentMapping)
{ {
GuestTexture texture(ResourceType::Texture); GuestTexture texture(ResourceType::Texture);
@ -5583,7 +5613,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,14 @@
using namespace plume; using namespace plume;
struct Video
{
static void CreateHostDevice();
static void HostPresent();
static void StartPipelinePrecompilation();
static void WaitForGPU();
};
struct GuestSamplerState struct GuestSamplerState
{ {
be<uint32_t> data[6]; be<uint32_t> data[6];
@ -379,6 +387,6 @@ enum GuestTextureAddress
D3DTADDRESS_BORDER = 6 D3DTADDRESS_BORDER = 6
}; };
extern std::unique_ptr<GuestTexture> LoadTexture(uint8_t* data, size_t dataSize, RenderComponentMapping componentMapping = RenderComponentMapping()); extern std::unique_ptr<GuestTexture> LoadTexture(const uint8_t* data, size_t dataSize, RenderComponentMapping componentMapping = RenderComponentMapping());
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,46 @@ 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::checkDLCInstall(const std::filesystem::path &baseDirectory, DLC dlc)
{
switch (dlc)
{
case DLC::Spagonia:
return std::filesystem::exists(baseDirectory / SpagoniaDirectory / DLCValidationFile);
case DLC::Chunnan:
return std::filesystem::exists(baseDirectory / ChunnanDirectory / DLCValidationFile);
case DLC::Mazuri:
return std::filesystem::exists(baseDirectory / MazuriDirectory / DLCValidationFile);
case DLC::Holoska:
return std::filesystem::exists(baseDirectory / HoloskaDirectory / DLCValidationFile);
case DLC::ApotosShamar:
return std::filesystem::exists(baseDirectory / ApotosShamarDirectory / DLCValidationFile);
case DLC::EmpireCityAdabat:
return std::filesystem::exists(baseDirectory / EmpireCityAdabatDirectory / DLCValidationFile);
default:
return false;
}
}
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 +305,49 @@ 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)
{ {
journal = Journal();
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 +390,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 +452,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 +478,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,30 @@ 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 checkDLCInstall(const std::filesystem::path &baseDirectory, DLC dlc);
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

@ -84,6 +84,148 @@ inline static std::unordered_map<std::string, std::unordered_map<ELanguage, std:
{ ELanguage::English, "Achievement Unlocked!" } { ELanguage::English, "Achievement Unlocked!" }
} }
}, },
{
"Installer_Header_Installer",
{
{ ELanguage::English, "INSTALLER" },
{ ELanguage::Spanish, "INSTALADOR" },
},
},
{
"Installer_Header_Installing",
{
{ ELanguage::English, "INSTALLING" },
{ ELanguage::Spanish, "INSTALANDO" },
}
},
{
"Installer_Page_SelectLanguage",
{
{ ELanguage::English, "Please select a language." }
}
},
{
"Installer_Page_Introduction",
{
{ ELanguage::English, "Welcome to Unleashed Recompiled!\n\nYou'll need an Xbox 360 copy\nof Sonic Unleashed in order to proceed with the installation." }
}
},
{
"Installer_Page_SelectGameAndUpdate",
{
{ ELanguage::English, "Add the files for the game and its title update. You can use digital dumps or pre-extracted folders containing the necessary files." }
}
},
{
"Installer_Page_SelectDLC",
{
{ ELanguage::English, "Add the files for the DLC. You can use digital dumps or pre-extracted folders containing the necessary files." }
}
},
{
"Installer_Page_CheckSpace",
{
{ ELanguage::English, "The content will be installed to the program's folder. Please confirm you have enough free space.\n\n" }
}
},
{
"Installer_Page_Installing",
{
{ ELanguage::English, "Please wait while the content is being installed..." }
}
},
{
"Installer_Page_InstallSucceeded",
{
{ ELanguage::English, "Installation complete.\n\nThis project is brought to you by:\n\n" }
}
},
{
"Installer_Page_InstallFailed",
{
{ ELanguage::English, "Installation failed.\n\nError:\n\n" }
}
},
{
"Installer_Step_Game",
{
{ ELanguage::English, "GAME" }
}
},
{
"Installer_Step_Update",
{
{ ELanguage::English, "UPDATE" }
}
},
{
"Installer_Step_RequiredSpace",
{
{ ELanguage::English, "Required space:" }
}
},
{
"Installer_Step_AvailableSpace",
{
{ ELanguage::English, "Available space:" }
}
},
{
"Installer_Button_Next",
{
{ ELanguage::English, "NEXT" },
{ ELanguage::Spanish, "SIGUIENTE" },
{ ELanguage::German, "WEITER" },
}
},
{
"Installer_Button_Skip",
{
{ ELanguage::English, "SKIP" },
{ ELanguage::Spanish, "SALTAR" },
{ ELanguage::German, "ÜBERSPRINGEN" },
}
},
{
"Installer_Button_AddFiles",
{
{ ELanguage::English, "ADD FILES" },
{ ELanguage::Spanish, "AÑADIR ARCHIVOS" },
{ ELanguage::German, "DATEIEN HINZUFÜGEN" },
}
},
{
"Installer_Button_AddFolder",
{
{ ELanguage::English, "ADD FOLDER" },
{ ELanguage::Spanish, "AÑADIR CARPETA" },
{ ELanguage::German, "ORDNER HINZUFÜGEN" },
}
},
{
"Installer_Message_InvalidFilesList",
{
{ ELanguage::English, "The following selected files are invalid:" }
}
},
{
"Installer_Message_InvalidFiles",
{
{ ELanguage::English, "Some of the files that have\nbeen provided are not valid.\n\nPlease make sure all the\nspecified files are correct\nand try again." }
}
},
{
"Installer_Message_IncompatibleGameData",
{
{ ELanguage::English, "The specified game and\nupdate file are incompatible.\n\nPlease ensure the files are\nfor the same version and\nregion and try again." }
}
},
{
"Installer_Message_DLCWarning",
{
{ ELanguage::English, "It is highly recommended\nthat you install all of the\nDLC, as it includes high\nquality lighting textures\nfor the base game.\n\nAre you sure you want to\nskip this step?" }
}
},
{ {
"Common_Next", "Common_Next",
{ {

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

@ -52,6 +52,20 @@ static void SetShaderModifier(uint32_t shaderModifier)
callbackData->setShaderModifier.shaderModifier = shaderModifier; callbackData->setShaderModifier.shaderModifier = shaderModifier;
} }
static void SetOrigin(ImVec2 origin)
{
auto callbackData = AddCallback(ImGuiCallback::SetOrigin);
callbackData->setOrigin.origin[0] = origin.x;
callbackData->setOrigin.origin[1] = origin.y;
}
static void SetScale(ImVec2 scale)
{
auto callbackData = AddCallback(ImGuiCallback::SetScale);
callbackData->setScale.scale[0] = scale.x;
callbackData->setScale.scale[1] = scale.y;
}
// Aspect ratio aware. // Aspect ratio aware.
static float Scale(float size) static float Scale(float size)
{ {
@ -221,6 +235,32 @@ static float CalcWidestTextSize(const ImFont* font, float fontSize, std::span<st
return result; return result;
} }
static std::string Truncate(const std::string& input, size_t maxLength, bool useEllipsis = true, bool usePrefixEllipsis = false)
{
const std::string ellipsis = "...";
if (input.length() > maxLength)
{
if (useEllipsis && maxLength > ellipsis.length())
{
if (usePrefixEllipsis)
{
return ellipsis + input.substr(0, maxLength - ellipsis.length());
}
else
{
return input.substr(0, maxLength - ellipsis.length()) + ellipsis;
}
}
else
{
return input.substr(0, maxLength);
}
}
return input;
}
static std::vector<std::string> Split(const char* str, char delimiter) static std::vector<std::string> Split(const char* str, char delimiter)
{ {
std::vector<std::string> result; std::vector<std::string> result;
@ -274,11 +314,29 @@ static void DrawCentredParagraph(const ImFont* font, float fontSize, const ImVec
auto paragraphSize = MeasureCentredParagraph(font, fontSize, lineMargin, lines); auto paragraphSize = MeasureCentredParagraph(font, fontSize, lineMargin, lines);
auto offsetY = 0.0f; auto offsetY = 0.0f;
for (auto& str : lines) auto hasList = std::strstr(text, "- ");
auto isList = false;
auto listOffsetX = 0.0f;
for (int i = 0; i < lines.size(); i++)
{ {
auto& str = lines[i];
auto textSize = font->CalcTextSizeA(fontSize, FLT_MAX, 0, str.c_str()); auto textSize = font->CalcTextSizeA(fontSize, FLT_MAX, 0, str.c_str());
drawMethod(str.c_str(), ImVec2(/* X */ centre.x - textSize.x / 2, /* Y */ centre.y - paragraphSize.y / 2 + offsetY)); if (hasList)
{
if (!isList && str.starts_with("- ") && lines.size() > i + 1 && lines[i + 1].starts_with("- "))
{
isList = true;
listOffsetX = centre.x - textSize.x / 2;
}
else if (isList && !str.starts_with("- "))
{
isList = false;
}
}
drawMethod(str.c_str(), ImVec2(/* X */ isList ? listOffsetX : centre.x - textSize.x / 2, /* Y */ centre.y - paragraphSize.y / 2 + offsetY));
offsetY += textSize.y + Scale(lineMargin); offsetY += textSize.y + Scale(lineMargin);
} }

File diff suppressed because it is too large Load diff

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

@ -2,7 +2,9 @@
#include "imgui_utils.h" #include "imgui_utils.h"
#include <api/SWA.h> #include <api/SWA.h>
#include <gpu/video.h> #include <gpu/video.h>
#include <app.h>
#include <exports.h> #include <exports.h>
#include <res/images/common/general_window.dds.h>
#include <decompressor.h> #include <decompressor.h>
#include <res/images/common/select_fade.dds.h> #include <res/images/common/select_fade.dds.h>
#include <gpu/imgui_snapshot.h> #include <gpu/imgui_snapshot.h>
@ -30,6 +32,7 @@ static double g_controlsAppearTime;
static ImFont* g_fntSeurat; static ImFont* g_fntSeurat;
static std::unique_ptr<GuestTexture> g_upSelectionCursor; static std::unique_ptr<GuestTexture> g_upSelectionCursor;
static std::unique_ptr<GuestTexture> g_upWindow;
std::string g_text; std::string g_text;
int g_result; int g_result;
@ -61,8 +64,6 @@ bool DrawContainer(float appearTime, ImVec2 centre, ImVec2 max, bool isForegroun
_max.y = Hermite(centre.y, _max.y, containerMotion); _max.y = Hermite(centre.y, _max.y, containerMotion);
} }
auto vertices = GetPauseContainerVertices(_min, _max);
// Transparency fade animation. // Transparency fade animation.
auto colourMotion = g_isClosing auto colourMotion = g_isClosing
? ComputeMotion(appearTime, OVERLAY_CONTAINER_OUTRO_FADE_START, OVERLAY_CONTAINER_OUTRO_FADE_END) ? ComputeMotion(appearTime, OVERLAY_CONTAINER_OUTRO_FADE_START, OVERLAY_CONTAINER_OUTRO_FADE_END)
@ -78,44 +79,7 @@ bool DrawContainer(float appearTime, ImVec2 centre, ImVec2 max, bool isForegroun
if (isForeground) if (isForeground)
drawList->AddRectFilled({ 0.0f, 0.0f }, ImGui::GetIO().DisplaySize, IM_COL32(0, 0, 0, 223 * (g_foregroundCount ? 1 : alpha))); drawList->AddRectFilled({ 0.0f, 0.0f }, ImGui::GetIO().DisplaySize, IM_COL32(0, 0, 0, 223 * (g_foregroundCount ? 1 : alpha)));
auto colShadow = IM_COL32(0, 0, 0, 156 * alpha); DrawPauseContainer(g_upWindow, _min, _max, alpha);
auto colGradientTop = IM_COL32(197, 194, 197, 200 * alpha);
auto colGradientBottom = IM_COL32(115, 113, 115, 236 * alpha);
// Draw vertices with gradient.
SetGradient(_min, _max, colGradientTop, colGradientBottom);
drawList->AddConvexPolyFilled(vertices.data(), vertices.size(), IM_COL32(255, 255, 255, 255 * alpha));
ResetGradient();
// Draw outline.
drawList->AddPolyline
(
vertices.data(),
vertices.size(),
IM_COL32(247, 247, 247, 255 * alpha),
true,
Scale(2.5f)
);
// Offset vertices to draw 3D effect lines.
for (int i = 0; i < vertices.size(); i++)
{
vertices[i].x -= Scale(0.4f);
vertices[i].y -= Scale(0.2f);
}
auto colLineTop = IM_COL32(165, 170, 165, 230 * alpha);
auto colLineBottom = IM_COL32(190, 190, 190, 230 * alpha);
auto lineThickness = Scale(1.0f);
// Top left corner bottom to top left corner top.
drawList->AddLine(vertices[0], vertices[1], colLineTop, lineThickness * 0.5f);
// Top left corner bottom to bottom left.
drawList->AddRectFilledMultiColor({ vertices[0].x - 0.2f, vertices[0].y }, { vertices[6].x + lineThickness - 0.2f, vertices[6].y }, colLineTop, colLineTop, colLineBottom, colLineBottom);
// Top left corner top to top right.
drawList->AddLine(vertices[1], vertices[2], colLineTop, lineThickness);
drawList->PushClipRect(_min, _max); drawList->PushClipRect(_min, _max);
@ -179,6 +143,7 @@ void MessageWindow::Init()
g_fntSeurat = ImFontAtlasSnapshot::GetFont("FOT-SeuratPro-M.otf", 24.0f * FONT_SCALE); g_fntSeurat = ImFontAtlasSnapshot::GetFont("FOT-SeuratPro-M.otf", 24.0f * FONT_SCALE);
g_upSelectionCursor = LoadTexture(decompressZstd(g_select_fade, g_select_fade_uncompressed_size).get(), g_select_fade_uncompressed_size); g_upSelectionCursor = LoadTexture(decompressZstd(g_select_fade, g_select_fade_uncompressed_size).get(), g_select_fade_uncompressed_size);
g_upWindow = LoadTexture(decompressZstd(g_general_window, g_general_window_uncompressed_size).get(), g_general_window_uncompressed_size);
} }
void MessageWindow::Draw() void MessageWindow::Draw()
@ -186,7 +151,7 @@ void MessageWindow::Draw()
if (!s_isVisible) if (!s_isVisible)
return; return;
auto pInputState = SWA::CInputState::GetInstance(); auto pInputState = g_isGameLoaded ? SWA::CInputState::GetInstance() : nullptr;
auto drawList = ImGui::GetForegroundDrawList(); auto drawList = ImGui::GetForegroundDrawList();
auto& res = ImGui::GetIO().DisplaySize; auto& res = ImGui::GetIO().DisplaySize;
@ -194,8 +159,8 @@ void MessageWindow::Draw()
auto fontSize = Scale(28); auto fontSize = Scale(28);
auto textSize = MeasureCentredParagraph(g_fntSeurat, fontSize, 5, g_text.c_str()); auto textSize = MeasureCentredParagraph(g_fntSeurat, fontSize, 5, g_text.c_str());
auto textMarginX = Scale(32); auto textMarginX = Scale(37);
auto textMarginY = Scale(40); auto textMarginY = Scale(45);
if (DrawContainer(g_appearTime, centre, { textSize.x / 2 + textMarginX, textSize.y / 2 + textMarginY }, !g_isControlsVisible)) if (DrawContainer(g_appearTime, centre, { textSize.x / 2 + textMarginX, textSize.y / 2 + textMarginY }, !g_isControlsVisible))
{ {
@ -215,12 +180,16 @@ void MessageWindow::Draw()
drawList->PopClipRect(); drawList->PopClipRect();
bool isAccepted = pInputState
? pInputState->GetPadState().IsTapped(SWA::eKeyState_A)
: ImGui::IsMouseClicked(ImGuiMouseButton_Left);
if (g_buttons.size()) if (g_buttons.size())
{ {
auto itemWidth = std::max(Scale(162), Scale(CalcWidestTextSize(g_fntSeurat, fontSize, g_buttons))); auto itemWidth = std::max(Scale(162), Scale(CalcWidestTextSize(g_fntSeurat, fontSize, g_buttons)));
auto itemHeight = Scale(57); auto itemHeight = Scale(57);
auto windowMarginX = Scale(18); auto windowMarginX = Scale(23);
auto windowMarginY = Scale(25); auto windowMarginY = Scale(30);
ImVec2 controlsMax = { /* X */ itemWidth / 2 + windowMarginX, /* Y */ itemHeight / 2 * g_buttons.size() + windowMarginY }; ImVec2 controlsMax = { /* X */ itemWidth / 2 + windowMarginX, /* Y */ itemHeight / 2 * g_buttons.size() + windowMarginY };
@ -231,54 +200,74 @@ void MessageWindow::Draw()
for (auto& button : g_buttons) for (auto& button : g_buttons)
DrawButton(rowCount++, windowMarginY, itemWidth, itemHeight, button); DrawButton(rowCount++, windowMarginY, itemWidth, itemHeight, button);
drawList->PopClipRect(); if (pInputState)
bool upIsHeld = pInputState->GetPadState().IsDown(SWA::eKeyState_DpadUp) ||
pInputState->GetPadState().LeftStickVertical > 0.5f;
bool downIsHeld = pInputState->GetPadState().IsDown(SWA::eKeyState_DpadDown) ||
pInputState->GetPadState().LeftStickVertical < -0.5f;
bool scrollUp = !g_upWasHeld && upIsHeld;
bool scrollDown = !g_downWasHeld && downIsHeld;
if (scrollUp)
{ {
--g_selectedRowIndex; bool upIsHeld = pInputState->GetPadState().IsDown(SWA::eKeyState_DpadUp) ||
if (g_selectedRowIndex < 0) pInputState->GetPadState().LeftStickVertical > 0.5f;
g_selectedRowIndex = rowCount - 1;
bool downIsHeld = pInputState->GetPadState().IsDown(SWA::eKeyState_DpadDown) ||
pInputState->GetPadState().LeftStickVertical < -0.5f;
bool scrollUp = !g_upWasHeld && upIsHeld;
bool scrollDown = !g_downWasHeld && downIsHeld;
if (scrollUp)
{
--g_selectedRowIndex;
if (g_selectedRowIndex < 0)
g_selectedRowIndex = rowCount - 1;
}
else if (scrollDown)
{
++g_selectedRowIndex;
if (g_selectedRowIndex >= rowCount)
g_selectedRowIndex = 0;
}
if (scrollUp || scrollDown)
Game_PlaySound("sys_actstg_pausecursor");
g_upWasHeld = upIsHeld;
g_downWasHeld = downIsHeld;
if (pInputState->GetPadState().IsTapped(SWA::eKeyState_B))
{
g_result = -1;
Game_PlaySound("sys_actstg_pausecansel");
MessageWindow::Close();
}
} }
else if (scrollDown) else
{ {
++g_selectedRowIndex; auto clipRectMin = drawList->GetClipRectMin();
if (g_selectedRowIndex >= rowCount) auto clipRectMax = drawList->GetClipRectMax();
g_selectedRowIndex = 0;
g_selectedRowIndex = -1;
for (int i = 0; i < rowCount; i++)
{
ImVec2 itemMin = { clipRectMin.x + windowMarginX, clipRectMin.y + windowMarginY + itemHeight * i };
ImVec2 itemMax = { clipRectMax.x - windowMarginX, clipRectMin.y + windowMarginY + itemHeight * i + itemHeight };
if (ImGui::IsMouseHoveringRect(itemMin, itemMax, false))
g_selectedRowIndex = i;
}
} }
if (scrollUp || scrollDown) if (g_selectedRowIndex != -1 && isAccepted)
Game_PlaySound("sys_actstg_pausecursor");
g_upWasHeld = upIsHeld;
g_downWasHeld = downIsHeld;
if (pInputState->GetPadState().IsTapped(SWA::eKeyState_A))
{ {
g_result = g_selectedRowIndex; g_result = g_selectedRowIndex;
Game_PlaySound("sys_actstg_pausedecide"); Game_PlaySound("sys_actstg_pausedecide");
MessageWindow::Close(); MessageWindow::Close();
} }
else if (pInputState->GetPadState().IsTapped(SWA::eKeyState_B))
{
g_result = -1;
Game_PlaySound("sys_actstg_pausecansel"); drawList->PopClipRect();
MessageWindow::Close();
}
} }
else else
{ {
if (!g_isControlsVisible && pInputState->GetPadState().IsTapped(SWA::eKeyState_A)) if (!g_isControlsVisible && isAccepted)
{ {
g_controlsAppearTime = ImGui::GetTime(); g_controlsAppearTime = ImGui::GetTime();
g_isControlsVisible = true; g_isControlsVisible = true;
@ -289,10 +278,18 @@ void MessageWindow::Draw()
} }
else else
{ {
if (pInputState->GetPadState().IsTapped(SWA::eKeyState_A)) if (isAccepted)
{
g_result = 0;
MessageWindow::Close(); MessageWindow::Close();
}
} }
} }
else if (g_isClosing)
{
s_isVisible = false;
}
} }
bool MessageWindow::Open(std::string text, int* result, std::span<std::string> buttons, int defaultButtonIndex) bool MessageWindow::Open(std::string text, int* result, std::span<std::string> buttons, int defaultButtonIndex)
@ -308,7 +305,7 @@ bool MessageWindow::Open(std::string text, int* result, std::span<std::string> b
g_text = text; g_text = text;
g_buttons = std::vector(buttons.begin(), buttons.end()); g_buttons = std::vector(buttons.begin(), buttons.end());
g_defaultButtonIndex = defaultButtonIndex; g_defaultButtonIndex = g_isGameLoaded ? defaultButtonIndex : -1;
ResetSelection(); ResetSelection();
@ -330,6 +327,7 @@ void MessageWindow::Close()
g_controlsAppearTime = ImGui::GetTime(); g_controlsAppearTime = ImGui::GetTime();
g_isClosing = true; g_isClosing = true;
g_isControlsVisible = false; g_isControlsVisible = false;
g_foregroundCount = 0;
g_isAwaitingResult = false; g_isAwaitingResult = false;
} }

View file

@ -989,12 +989,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

@ -27,6 +27,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)
{ {
@ -99,7 +100,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
{ {
@ -111,6 +112,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;

View file

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