diff --git a/files.cmake b/files.cmake index 2178c1066c..d1e0436625 100644 --- a/files.cmake +++ b/files.cmake @@ -1376,6 +1376,7 @@ set(DUSK_FILES src/dusk/imgui/ImGuiSaveEditor.cpp src/dusk/imgui/ImGuiStateShare.hpp src/dusk/imgui/ImGuiStateShare.cpp + src/dusk/iso_validate.cpp src/dusk/offset_ptr.cpp src/dusk/OSContext.cpp src/dusk/OSThread.cpp diff --git a/src/dusk/imgui/ImGuiConsole.cpp b/src/dusk/imgui/ImGuiConsole.cpp index 612c95d7f3..0cf3906d75 100644 --- a/src/dusk/imgui/ImGuiConsole.cpp +++ b/src/dusk/imgui/ImGuiConsole.cpp @@ -40,9 +40,15 @@ namespace dusk { void ImGuiTextCenter(std::string_view text) { ImGui::NewLine(); - float fontSize = ImGui::CalcTextSize(text.data(), text.data() + text.size()).x; + float fontSize = ImGui::CalcTextSize( + text.data(), + text.data() + text.size(), + false, + ImGui::GetWindowSize().x).x; ImGui::SameLine(ImGui::GetWindowSize().x / 2 - fontSize + fontSize / 2); + ImGui::PushTextWrapPos(ImGui::GetWindowSize().x); ImGuiStringViewText(text); + ImGui::PopTextWrapPos(); } bool ImGuiButtonCenter(std::string_view text) { diff --git a/src/dusk/imgui/ImGuiPreLaunchWindow.cpp b/src/dusk/imgui/ImGuiPreLaunchWindow.cpp index 5925fd8c31..c24dbd8247 100644 --- a/src/dusk/imgui/ImGuiPreLaunchWindow.cpp +++ b/src/dusk/imgui/ImGuiPreLaunchWindow.cpp @@ -7,6 +7,7 @@ #include "ImGuiConsole.hpp" #include "dusk/main.h" #include "dusk/settings.h" +#include "../iso_validate.hpp" #include #include @@ -26,15 +27,42 @@ static constexpr std::array skGameDiscFileFilters{{ {"All Files", "*"}, }}; +static std::string ShowIsoInvalidError(const iso::ValidationError code) { + using namespace std::literals::string_literals; + + switch (code) { + case iso::ValidationError::IOError: + return "Unknown IO error occurred"s; + case iso::ValidationError::InvalidImage: + return "Unable to interpret selected file as a disc image"s; + case iso::ValidationError::WrongGame: + return "Selected disc image is for a different game"s; + case iso::ValidationError::WrongVersion: + return "Selected disc image is for an unsupported version of the game. Only North American GameCube (NTSC/GZ2E01) is supported at this time."s; + case iso::ValidationError::ExecutableMismatch: + return "Selected disc image contains modified executable files."s; + default: + return "Unknown error"s; + } +} + void fileDialogCallback(void* userdata, const char* const* filelist, [[maybe_unused]] int filter) { auto* self = static_cast(userdata); + self->m_errorString.clear(); if (filelist != nullptr) { if (filelist[0] == nullptr) { // Cancelled self->m_selectedIsoPath.clear(); } else { - self->m_selectedIsoPath = filelist[0]; - getSettings().backend.isoPath.setValue(self->m_selectedIsoPath); + const auto path = filelist[0]; + const auto ret = iso::validate(path); + if (ret != iso::ValidationError::Success) { + self->m_selectedIsoPath.clear(); + self->m_errorString = std::move(ShowIsoInvalidError(ret)); + return; + } + self->m_selectedIsoPath = path; + getSettings().backend.isoPath.setValue(path); config::Save(); } } else { @@ -111,6 +139,10 @@ void ImGuiPreLaunchWindow::drawMainMenu() { ImGui::PushFont(ImGuiEngine::fontLarge); if (!isSelectedPathValid()) { + if (!m_errorString.empty()) { + ImGuiTextCenter(m_errorString); + } + if (ImGuiButtonCenter("Select disc image...")) { SDL_ShowOpenFileDialog(&fileDialogCallback, this, aurora::window::get_sdl_window(), skGameDiscFileFilters.data(), int(skGameDiscFileFilters.size()), @@ -148,6 +180,10 @@ void ImGuiPreLaunchWindow::drawOptions() { if (ImGui::BeginChild("OptionsChild", ImVec2(childWidth, endCursorY - cursorY), ImGuiChildFlags_None, ImGuiWindowFlags_NoBackground)) { + if (!m_errorString.empty()) { + ImGuiTextCenter(m_errorString); + } + ImGui::InputText("Game ISO Path", &m_selectedIsoPath, ImGuiInputTextFlags_ReadOnly); ImGui::SameLine(); if (ImGui::Button("Set")) { diff --git a/src/dusk/iso_validate.cpp b/src/dusk/iso_validate.cpp new file mode 100644 index 0000000000..755b4eede0 --- /dev/null +++ b/src/dusk/iso_validate.cpp @@ -0,0 +1,126 @@ +#include "iso_validate.hpp" + +#include +#include + +#include "SDL3/SDL_iostream.h" + +namespace dusk::iso { + +constexpr const char* TP_GAME_IDS[] = { + "GZ2E01", // GCN USA + "GZ2P01", // GCN PAL + "GZ2J01", // GCN JPN + "RZDE01", // Wii USA + "RZDP01", // Wii PAL + "RZDJ01", // Wii JPN + "RZDK01", // Wii KOR +}; + +constexpr const char* SUPPORTED_TP_GAME_IDS[] = { + "GZ2E01", // GCN USA +}; + +template +constexpr bool matches(const char (&id)[6], const char* const (&valid)[N]) { + for (auto elem : valid) { + if (strncmp(id, elem, 6) == 0) { + return true; + } + } + + return false; +} + +struct NodHandleWrapper { + NodHandle* handle; + + NodHandleWrapper() : handle(nullptr) { + } + + ~NodHandleWrapper() { + if (handle != nullptr) { + nod_free(handle); + handle = nullptr; + } + } +}; + +static ValidationError convertNodError(NodResult result) { + switch (result) { + case NOD_RESULT_ERR_IO: + return ValidationError::IOError; + case NOD_RESULT_ERR_FORMAT: + return ValidationError::InvalidImage; + default: + return ValidationError::Unknown; + } +} + +s64 StreamReadAt(void* user_data, u64 offset, void* out, size_t len) { + if (len == 0) { + return 0; + } + + auto io = static_cast(user_data); + + auto ret = SDL_SeekIO(io, static_cast(offset), SDL_IO_SEEK_SET); + if (ret < 0) { + return -1; + } + + auto read = SDL_ReadIO(io, out, len); + if (read == 0) { + if (SDL_GetIOStatus(io) == SDL_IO_STATUS_EOF) { + return 0; + } + + return -1; + } + + return static_cast(read); +} + +s64 StreamLength(void* user_data) { + auto io = static_cast(user_data); + return SDL_GetIOSize(io); +} + +void StreamClose(void* user_data) { + auto io = static_cast(user_data); + SDL_CloseIO(io); +} + +ValidationError validate(const char* path) { + NodHandleWrapper disc; + + const auto sdlStream = SDL_IOFromFile(path, "rb"); + const NodDiscStream nod_stream { + .user_data = sdlStream, + .read_at = StreamReadAt, + .stream_len = StreamLength, + .close = StreamClose, + }; + + auto result = nod_disc_open_stream(&nod_stream, nullptr, &disc.handle); + if (disc.handle == nullptr || result != NOD_RESULT_OK) { + return convertNodError(result); + } + + NodDiscHeader header{}; + result = nod_disc_header(disc.handle, &header); + if (result != NOD_RESULT_OK) { + return convertNodError(result); + } + + if (!matches(header.game_id, TP_GAME_IDS)) { + return ValidationError::WrongGame; + } + + if (!matches(header.game_id, SUPPORTED_TP_GAME_IDS)) { + return ValidationError::WrongVersion; + } + + return ValidationError::Success; +} +} // namespace dusk::iso \ No newline at end of file diff --git a/src/dusk/iso_validate.hpp b/src/dusk/iso_validate.hpp new file mode 100644 index 0000000000..da1ef1f2a6 --- /dev/null +++ b/src/dusk/iso_validate.hpp @@ -0,0 +1,18 @@ +#ifndef DUSK_ISO_VALIDATE_HPP +#define DUSK_ISO_VALIDATE_HPP + +namespace dusk::iso { + enum class ValidationError : u8 { + Success = 0, + IOError, + InvalidImage, + WrongGame, + WrongVersion, + ExecutableMismatch, + Unknown + }; + + ValidationError validate(const char* path); +} + +#endif // DUSK_ISO_VALIDATE_HPP