diff --git a/CMakeLists.txt b/CMakeLists.txt index d3faf7fca7..c740479b07 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -220,6 +220,8 @@ set(GAME_INCLUDE_DIRS set(GAME_LIBS aurora::core aurora::gx aurora::gd aurora::si aurora::vi aurora::pad aurora::mtx aurora::os aurora::dvd aurora::card freeverb cxxopts::cxxopts absl::flat_hash_map nlohmann_json::nlohmann_json TracyClient) +list(APPEND GAME_LIBS libzstd_static) + if (DUSK_MOVIE_SUPPORT) if (TARGET libjpeg-turbo::turbojpeg-static) list(APPEND GAME_LIBS libjpeg-turbo::turbojpeg-static) diff --git a/files.cmake b/files.cmake index 7507fb6742..5ba33086f1 100644 --- a/files.cmake +++ b/files.cmake @@ -1373,6 +1373,8 @@ set(DUSK_FILES src/dusk/imgui/ImGuiStubLog.cpp src/dusk/imgui/ImGuiMapLoader.cpp src/dusk/imgui/ImGuiSaveEditor.cpp + src/dusk/imgui/ImGuiStateShare.hpp + src/dusk/imgui/ImGuiStateShare.cpp src/dusk/offset_ptr.cpp src/dusk/OSContext.cpp src/dusk/OSThread.cpp diff --git a/include/dusk/hotkeys.h b/include/dusk/hotkeys.h index 6e98d62521..4879b2ddce 100644 --- a/include/dusk/hotkeys.h +++ b/include/dusk/hotkeys.h @@ -17,6 +17,7 @@ constexpr const char* SHOW_HEAP_VIEWER = "F4"; constexpr const char* SHOW_STUB_LOG = "F5"; constexpr const char* SHOW_CAMERA_DEBUG = "F6"; constexpr const char* SHOW_AUDIO_DEBUG = "F7"; +constexpr const char* SHOW_STATE_SHARE = "F8"; constexpr const char* TURBO = "Tab"; diff --git a/src/dusk/imgui/ImGuiConsole.cpp b/src/dusk/imgui/ImGuiConsole.cpp index 1bff5da458..612c95d7f3 100644 --- a/src/dusk/imgui/ImGuiConsole.cpp +++ b/src/dusk/imgui/ImGuiConsole.cpp @@ -269,6 +269,7 @@ namespace dusk { m_menuTools.ShowAudioDebug(); m_menuTools.ShowSaveEditor(); } + m_menuTools.ShowStateShare(); DuskDebugPad(); // temporary, remove later // Only show cursor when menu or any windows are open diff --git a/src/dusk/imgui/ImGuiMenuTools.cpp b/src/dusk/imgui/ImGuiMenuTools.cpp index f6c29c0a16..3a9935353f 100644 --- a/src/dusk/imgui/ImGuiMenuTools.cpp +++ b/src/dusk/imgui/ImGuiMenuTools.cpp @@ -53,6 +53,7 @@ namespace dusk { ImGui::MenuItem("Map Loader", nullptr, &m_showMapLoader); ImGui::MenuItem("Player Info", nullptr, &m_showPlayerInfo); ImGui::MenuItem("Save Editor", nullptr, &m_showSaveEditor); + ImGui::MenuItem("State Share", hotkeys::SHOW_STATE_SHARE, &m_showStateShare); ImGui::MenuItem("Audio Debug", hotkeys::SHOW_AUDIO_DEBUG, &m_showAudioDebug); if (!dusk::IsGameLaunched) { diff --git a/src/dusk/imgui/ImGuiMenuTools.hpp b/src/dusk/imgui/ImGuiMenuTools.hpp index 37409ddb06..05bdd9364b 100644 --- a/src/dusk/imgui/ImGuiMenuTools.hpp +++ b/src/dusk/imgui/ImGuiMenuTools.hpp @@ -6,6 +6,7 @@ #include "imgui.h" #include "ImGuiSaveEditor.hpp" +#include "ImGuiStateShare.hpp" namespace dusk { class ImGuiMenuTools { @@ -23,6 +24,7 @@ namespace dusk { void ShowPlayerInfo(); void ShowAudioDebug(); void ShowSaveEditor(); + void ShowStateShare(); private: bool m_showDebugOverlay = false; @@ -57,6 +59,9 @@ namespace dusk { bool m_showSaveEditor = false; ImGuiSaveEditor m_saveEditor; + + bool m_showStateShare = false; + ImGuiStateShare m_stateShare; }; } diff --git a/src/dusk/imgui/ImGuiStateShare.cpp b/src/dusk/imgui/ImGuiStateShare.cpp new file mode 100644 index 0000000000..feff4f10ca --- /dev/null +++ b/src/dusk/imgui/ImGuiStateShare.cpp @@ -0,0 +1,129 @@ +#include "ImGuiStateShare.hpp" +#include "ImGuiMenuTools.hpp" +#include "ImGuiConsole.hpp" + +#include "imgui.h" +#include "fmt/format.h" +#include "absl/strings/escaping.h" + +#include "d/d_com_inf_game.h" +#include "dusk/main.h" + +#include + +namespace dusk { + +#pragma pack(push, 1) +struct StateSharePacket { + char stageName[8]; + int8_t roomNo; + int8_t layer; + int16_t startPoint; + // followed by raw dSv_info_c bytes +}; +#pragma pack(pop) + +static constexpr size_t PACKET_TOTAL = sizeof(StateSharePacket) + sizeof(dSv_info_c); + +void ImGuiStateShare::copyState() { + dSv_restart_c& restart = g_dComIfG_gameInfo.info.getRestart(); + + StateSharePacket pkt = {}; + if (const char* s = g_dComIfG_gameInfo.play.getLastPlayStageName()) + strncpy(pkt.stageName, s, 7); + pkt.roomNo = restart.getRoomNo(); + pkt.layer = dComIfGp_getStartStageLayer(); + pkt.startPoint = restart.getStartPoint(); + + std::string raw(PACKET_TOTAL, '\0'); + memcpy(raw.data(), &pkt, sizeof(pkt)); + memcpy(raw.data() + sizeof(pkt), &g_dComIfG_gameInfo.info, sizeof(dSv_info_c)); + + size_t bound = ZSTD_compressBound(raw.size()); + std::string compressed(bound, '\0'); + compressed.resize(ZSTD_compress(compressed.data(), bound, raw.data(), raw.size(), 1)); + + std::string encoded = absl::Base64Escape(compressed); + ImGui::SetClipboardText(encoded.c_str()); + m_statusMsg = "Copied to clipboard."; +} + +bool ImGuiStateShare::pasteState() { + const char* clip = ImGui::GetClipboardText(); + if (!clip || clip[0] == '\0') { + m_statusMsg = "Clipboard is empty."; + return false; + } + + std::string decoded; + if (!absl::Base64Unescape(clip, &decoded)) { + m_statusMsg = "Invalid base64."; + return false; + } + + unsigned long long dSize = ZSTD_getFrameContentSize(decoded.data(), decoded.size()); + if (dSize == ZSTD_CONTENTSIZE_ERROR || dSize == ZSTD_CONTENTSIZE_UNKNOWN || dSize < PACKET_TOTAL) { + m_statusMsg = "Not a valid state string."; + return false; + } + + std::string raw(static_cast(dSize), '\0'); + size_t result = ZSTD_decompress(raw.data(), raw.size(), decoded.data(), decoded.size()); + if (ZSTD_isError(result)) { + m_statusMsg = fmt::format("Decompression failed: {}", ZSTD_getErrorName(result)); + return false; + } + + StateSharePacket pkt; + memcpy(&pkt, raw.data(), sizeof(pkt)); + pkt.stageName[7] = '\0'; + + memcpy(&g_dComIfG_gameInfo.info, raw.data() + sizeof(pkt), sizeof(dSv_info_c)); + dComIfGp_setNextStage(pkt.stageName, pkt.startPoint, pkt.roomNo, pkt.layer); + m_pendingInfo = g_dComIfG_gameInfo.info; + + m_statusMsg = fmt::format("Warping to {} room {} layer {}.", pkt.stageName, (int)pkt.roomNo, (int)pkt.layer); + return true; +} + +void ImGuiStateShare::tickPendingApply() { + if (!m_pendingInfo.has_value() || dComIfGp_isEnableNextStage()) + return; + g_dComIfG_gameInfo.info = *m_pendingInfo; + m_pendingInfo.reset(); +} + +void ImGuiStateShare::draw(bool& open) { + if (dusk::IsGameLaunched) + tickPendingApply(); + + if (!open) + return; + + if (!ImGui::Begin("State Share", &open, ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoFocusOnAppearing | ImGuiWindowFlags_NoNav)) { + ImGui::End(); + return; + } + + if (!dusk::IsGameLaunched) ImGui::BeginDisabled(); + if (ImGui::Button("Copy State")) copyState(); + ImGui::SameLine(); + if (ImGui::Button("Import State")) pasteState(); + if (!dusk::IsGameLaunched) ImGui::EndDisabled(); + + if (!m_statusMsg.empty()) { + ImGui::Spacing(); + ImGui::Separator(); + ImGui::TextWrapped("%s", m_statusMsg.c_str()); + } + + ImGui::End(); +} + +void ImGuiMenuTools::ShowStateShare() { + if (!ImGuiConsole::CheckMenuViewToggle(ImGuiKey_F8, m_showStateShare)) + return; + m_stateShare.draw(m_showStateShare); +} + +} diff --git a/src/dusk/imgui/ImGuiStateShare.hpp b/src/dusk/imgui/ImGuiStateShare.hpp new file mode 100644 index 0000000000..7e21d8f3b1 --- /dev/null +++ b/src/dusk/imgui/ImGuiStateShare.hpp @@ -0,0 +1,23 @@ +#ifndef DUSK_IMGUI_STATESHARE_HPP +#define DUSK_IMGUI_STATESHARE_HPP + +#include "d/d_save.h" +#include +#include + +namespace dusk { + class ImGuiStateShare { + public: + void draw(bool& open); + + private: + void copyState(); + bool pasteState(); + void tickPendingApply(); + + std::string m_statusMsg; + std::optional m_pendingInfo; + }; +} + +#endif