From 3e8c48c1162b004f160e727ea8704885dc0d1dd5 Mon Sep 17 00:00:00 2001 From: rozlette Date: Sun, 3 Apr 2022 14:47:24 -0500 Subject: [PATCH] Add save editor --- .../Lib/Fast3D/gfx_direct3d11.cpp | 4 +- libultraship/libultraship/SohImGuiImpl.cpp | 67 ++- libultraship/libultraship/SohImGuiImpl.h | 14 +- soh/soh.vcxproj | 4 + soh/soh.vcxproj.filters | 18 + .../Enhancements/debugger/debugSaveEditor.cpp | 527 ++++++++++++++++++ .../Enhancements/debugger/debugSaveEditor.h | 3 + soh/soh/Enhancements/debugger/debugger.cpp | 6 + soh/soh/Enhancements/debugger/debugger.h | 3 + soh/soh/OTRGlobals.cpp | 2 + 10 files changed, 641 insertions(+), 7 deletions(-) create mode 100644 soh/soh/Enhancements/debugger/debugSaveEditor.cpp create mode 100644 soh/soh/Enhancements/debugger/debugSaveEditor.h create mode 100644 soh/soh/Enhancements/debugger/debugger.cpp create mode 100644 soh/soh/Enhancements/debugger/debugger.h diff --git a/libultraship/libultraship/Lib/Fast3D/gfx_direct3d11.cpp b/libultraship/libultraship/Lib/Fast3D/gfx_direct3d11.cpp index 3481a4db2..9c2f46422 100644 --- a/libultraship/libultraship/Lib/Fast3D/gfx_direct3d11.cpp +++ b/libultraship/libultraship/Lib/Fast3D/gfx_direct3d11.cpp @@ -977,8 +977,8 @@ uint16_t gfx_d3d11_get_pixel_depth_old(float x, float y) { } // namespace -void* SohImGui::GetTextureByID(int id) { - return d3d.textures[id].resource_view.Get(); +ImTextureID SohImGui::GetTextureByID(int id) { + return impl.backend == Backend::DX11 ? d3d.textures[id].resource_view.Get() : reinterpret_cast(id); } struct GfxRenderingAPI gfx_direct3d11_api = { diff --git a/libultraship/libultraship/SohImGuiImpl.cpp b/libultraship/libultraship/SohImGuiImpl.cpp index bdf9c267b..7489d14a2 100644 --- a/libultraship/libultraship/SohImGuiImpl.cpp +++ b/libultraship/libultraship/SohImGuiImpl.cpp @@ -1,6 +1,7 @@ #include "SohImGuiImpl.h" #include +#include #include #include "Archive.h" @@ -14,9 +15,11 @@ #include "TextureMod.h" #include "Window.h" #include "Cvar.h" +#include "Texture.h" #include "../Fast3D/gfx_pc.h" #include "Lib/stb/stb_image.h" #include "Lib/Fast3D/gfx_rendering_api.h" +#include "Lib/spdlog/include/spdlog/common.h" #include "Utils/StringHelper.h" #ifdef ENABLE_OPENGL @@ -51,6 +54,9 @@ namespace SohImGui { bool p_open = false; bool needs_save = false; + std::map> windowCategories; + std::map customWindows; + void ImGuiWMInit() { switch (impl.backend) { case Backend::SDL: @@ -153,7 +159,7 @@ namespace SohImGui { } } - void LoadTexture(std::string name, std::string path) { + void LoadTexture(const std::string& name, const std::string& path) { GfxRenderingAPI* api = gfx_get_current_rendering_api(); const auto res = GlobalCtx2::GetInstance()->GetResourceManager()->LoadFile(normalize(path)); @@ -173,6 +179,25 @@ namespace SohImGui { stbi_image_free(img_data); } + void LoadResource(const std::string& name, const std::string& path) { + GfxRenderingAPI* api = gfx_get_current_rendering_api(); + const auto res = static_cast(GlobalCtx2::GetInstance()->GetResourceManager()->LoadResource(normalize(path)).get()); + + if (res->texType != Ship::TextureType::RGBA32bpp) { + // TODO convert other image types + SPDLOG_WARN("SohImGui::LoadResource: Attempting to load unsupporting image type %s", path.c_str()); + return; + } + + const auto asset = new GameAsset{ api->new_texture() }; + + api->select_texture(0, asset->textureId); + api->set_sampler_parameters(0, false, 0, 0); + api->upload_texture(res->imageData, res->width, res->height); + + DefaultAssets[name] = asset; + } + void Init(WindowImpl window_impl) { impl = window_impl; Game::LoadSettings(); @@ -219,7 +244,7 @@ namespace SohImGui { ImGuiProcessEvent(event); } -#define BindButton(btn, status) ImGui::Image(impl.backend == Backend::DX11 ? GetTextureByID(DefaultAssets[btn]->textureId) : (ImTextureID)(DefaultAssets[btn]->textureId), ImVec2(16.0f * scale, 16.0f * scale), ImVec2(0, 0), ImVec2(1.0f, 1.0f), ImVec4(255, 255, 255, (status) ? 255 : 0)); +#define BindButton(btn, status) ImGui::Image(GetTextureByID(DefaultAssets[btn]->textureId), ImVec2(16.0f * scale, 16.0f * scale), ImVec2(0, 0), ImVec2(1.0f, 1.0f), ImVec4(255, 255, 255, (status) ? 255 : 0)); void BindAudioSlider(const char* name, const char* key, float* value, SeqPlayers playerId) { ImGui::Text(name, static_cast(100 * *(value))); @@ -278,7 +303,7 @@ namespace SohImGui { if (ImGui::BeginMenuBar()) { if (DefaultAssets.contains("Game_Icon")) { ImGui::SetCursorPos(ImVec2(5, 2.5f)); - ImGui::Image(impl.backend == Backend::DX11 ? GetTextureByID(DefaultAssets["Game_Icon"]->textureId) : reinterpret_cast(DefaultAssets["Game_Icon"]->textureId), ImVec2(16.0f, 16.0f)); + ImGui::Image(GetTextureByID(DefaultAssets["Game_Icon"]->textureId), ImVec2(16.0f, 16.0f)); ImGui::SameLine(); ImGui::SetCursorPos(ImVec2(25, 0)); } @@ -425,6 +450,15 @@ namespace SohImGui { ImGui::EndMenu(); } + for (const auto& category : windowCategories) { + if (ImGui::BeginMenu(category.first.c_str())) { + for (const std::string& name : category.second) { + HOOK(ImGui::MenuItem(name.c_str(), nullptr, &customWindows[name].enabled)); + } + ImGui::EndMenu(); + } + } + ImGui::EndMenuBar(); } @@ -527,6 +561,13 @@ namespace SohImGui { console->Draw(); + for (auto& windowIter : customWindows) { + CustomWindow& window = windowIter.second; + if (window.drawFunc != nullptr) { + window.drawFunc(window.enabled); + } + } + ImGui::Render(); ImGuiRenderDrawData(ImGui::GetDrawData()); if (UseViewports()) { @@ -538,4 +579,22 @@ namespace SohImGui { void BindCmd(const std::string& cmd, CommandEntry entry) { console->Commands[cmd] = std::move(entry); } -} + + void AddWindow(const std::string& category, const std::string& name, WindowDrawFunc drawFunc) { + if (customWindows.contains(name)) { + SPDLOG_ERROR("SohImGui::AddWindow: Attempting to add duplicate window name %s", name.c_str()); + return; + } + + customWindows[name] = { + .enabled = false, + .drawFunc = drawFunc + }; + + windowCategories[category].emplace_back(name); + } + + ImTextureID GetTextureByName(const std::string& name) { + return GetTextureByID(DefaultAssets[name]->textureId); + } +} \ No newline at end of file diff --git a/libultraship/libultraship/SohImGuiImpl.h b/libultraship/libultraship/SohImGuiImpl.h index b7c946eff..8275e4735 100644 --- a/libultraship/libultraship/SohImGuiImpl.h +++ b/libultraship/libultraship/SohImGuiImpl.h @@ -47,11 +47,23 @@ namespace SohImGui { } sdl; } EventImpl; + extern WindowImpl impl; + + using WindowDrawFunc = void(*)(bool& enabled); + + typedef struct { + bool enabled; + WindowDrawFunc drawFunc; + } CustomWindow; + extern Console* console; void Init(WindowImpl window_impl); void Update(EventImpl event); void Draw(void); void ShowCursor(bool hide, Dialogues w); void BindCmd(const std::string& cmd, CommandEntry entry); - void* GetTextureByID(int id); + void AddWindow(const std::string& category, const std::string& name, WindowDrawFunc drawFunc); + void LoadResource(const std::string& name, const std::string& path); + ImTextureID GetTextureByID(int id); + ImTextureID GetTextureByName(const std::string& name); } diff --git a/soh/soh.vcxproj b/soh/soh.vcxproj index aa6dee304..5560bde76 100644 --- a/soh/soh.vcxproj +++ b/soh/soh.vcxproj @@ -178,6 +178,8 @@ + + @@ -922,6 +924,8 @@ + + diff --git a/soh/soh.vcxproj.filters b/soh/soh.vcxproj.filters index 30ca9d4ed..de9ad6f96 100644 --- a/soh/soh.vcxproj.filters +++ b/soh/soh.vcxproj.filters @@ -76,6 +76,12 @@ {18b9727f-30de-4ab8-a317-916090d4a110} + + {9a4378ec-e30f-47b6-9ad6-5ce738b4cf99} + + + {04fc1c52-49ff-48e2-ae23-2c00867374f8} + @@ -2169,6 +2175,12 @@ Source Files\src\overlays\actors + + Source Files\soh\Enhancements\debugger + + + Source Files\soh\Enhancements\debugger + @@ -3710,6 +3722,12 @@ Header Files + + Header Files\soh\Enhancements\debugger + + + Header Files\soh\Enhancements\debugger + diff --git a/soh/soh/Enhancements/debugger/debugSaveEditor.cpp b/soh/soh/Enhancements/debugger/debugSaveEditor.cpp new file mode 100644 index 000000000..95e2f912c --- /dev/null +++ b/soh/soh/Enhancements/debugger/debugSaveEditor.cpp @@ -0,0 +1,527 @@ +#include "debugSaveEditor.h" +#include "../libultraship/SohImGuiImpl.h" + +#include +#include +#include + +extern "C" { +#include +#include "variables.h" +#include "functions.h" +#include "macros.h" +extern GlobalContext* gGlobalCtx; + +#include "textures/icon_item_static/icon_item_static.h" +} + +typedef struct { + uint32_t id; + std::string name; + std::string texturePath; +} ItemMapEntry; + +#define ITEM_MAP_ENTRY(id) \ + { \ + id, { \ + id, #id, static_cast(gItemIcons[id]) \ + } \ + } + +// Maps items ids to info for use in ImGui +std::map itemMapping = { + ITEM_MAP_ENTRY(ITEM_STICK), + ITEM_MAP_ENTRY(ITEM_NUT), + ITEM_MAP_ENTRY(ITEM_BOMB), + ITEM_MAP_ENTRY(ITEM_BOW), + ITEM_MAP_ENTRY(ITEM_ARROW_FIRE), + ITEM_MAP_ENTRY(ITEM_DINS_FIRE), + ITEM_MAP_ENTRY(ITEM_SLINGSHOT), + ITEM_MAP_ENTRY(ITEM_OCARINA_FAIRY), + ITEM_MAP_ENTRY(ITEM_OCARINA_TIME), + ITEM_MAP_ENTRY(ITEM_BOMBCHU), + ITEM_MAP_ENTRY(ITEM_HOOKSHOT), + ITEM_MAP_ENTRY(ITEM_LONGSHOT), + ITEM_MAP_ENTRY(ITEM_ARROW_ICE), + ITEM_MAP_ENTRY(ITEM_FARORES_WIND), + ITEM_MAP_ENTRY(ITEM_BOOMERANG), + ITEM_MAP_ENTRY(ITEM_LENS), + ITEM_MAP_ENTRY(ITEM_BEAN), + ITEM_MAP_ENTRY(ITEM_HAMMER), + ITEM_MAP_ENTRY(ITEM_ARROW_LIGHT), + ITEM_MAP_ENTRY(ITEM_NAYRUS_LOVE), + ITEM_MAP_ENTRY(ITEM_BOTTLE), + ITEM_MAP_ENTRY(ITEM_POTION_RED), + ITEM_MAP_ENTRY(ITEM_POTION_GREEN), + ITEM_MAP_ENTRY(ITEM_POTION_BLUE), + ITEM_MAP_ENTRY(ITEM_FAIRY), + ITEM_MAP_ENTRY(ITEM_FISH), + ITEM_MAP_ENTRY(ITEM_MILK_BOTTLE), + ITEM_MAP_ENTRY(ITEM_LETTER_RUTO), + ITEM_MAP_ENTRY(ITEM_BLUE_FIRE), + ITEM_MAP_ENTRY(ITEM_BUG), + ITEM_MAP_ENTRY(ITEM_BIG_POE), + ITEM_MAP_ENTRY(ITEM_MILK_HALF), + ITEM_MAP_ENTRY(ITEM_POE), + ITEM_MAP_ENTRY(ITEM_WEIRD_EGG), + ITEM_MAP_ENTRY(ITEM_CHICKEN), + ITEM_MAP_ENTRY(ITEM_LETTER_ZELDA), + ITEM_MAP_ENTRY(ITEM_MASK_KEATON), + ITEM_MAP_ENTRY(ITEM_MASK_SKULL), + ITEM_MAP_ENTRY(ITEM_MASK_SPOOKY), + ITEM_MAP_ENTRY(ITEM_MASK_BUNNY), + ITEM_MAP_ENTRY(ITEM_MASK_GORON), + ITEM_MAP_ENTRY(ITEM_MASK_ZORA), + ITEM_MAP_ENTRY(ITEM_MASK_GERUDO), + ITEM_MAP_ENTRY(ITEM_MASK_TRUTH), + ITEM_MAP_ENTRY(ITEM_SOLD_OUT), + ITEM_MAP_ENTRY(ITEM_POCKET_EGG), + ITEM_MAP_ENTRY(ITEM_POCKET_CUCCO), + ITEM_MAP_ENTRY(ITEM_COJIRO), + ITEM_MAP_ENTRY(ITEM_ODD_MUSHROOM), + ITEM_MAP_ENTRY(ITEM_ODD_POTION), + ITEM_MAP_ENTRY(ITEM_SAW), + ITEM_MAP_ENTRY(ITEM_SWORD_BROKEN), + ITEM_MAP_ENTRY(ITEM_PRESCRIPTION), + ITEM_MAP_ENTRY(ITEM_FROG), + ITEM_MAP_ENTRY(ITEM_EYEDROPS), + ITEM_MAP_ENTRY(ITEM_CLAIM_CHECK), + ITEM_MAP_ENTRY(ITEM_BOW_ARROW_FIRE), + ITEM_MAP_ENTRY(ITEM_BOW_ARROW_ICE), + ITEM_MAP_ENTRY(ITEM_BOW_ARROW_LIGHT), + ITEM_MAP_ENTRY(ITEM_SWORD_KOKIRI), + ITEM_MAP_ENTRY(ITEM_SWORD_MASTER), + ITEM_MAP_ENTRY(ITEM_SWORD_BGS), + ITEM_MAP_ENTRY(ITEM_SHIELD_DEKU), + ITEM_MAP_ENTRY(ITEM_SHIELD_HYLIAN), + ITEM_MAP_ENTRY(ITEM_SHIELD_MIRROR), + ITEM_MAP_ENTRY(ITEM_TUNIC_KOKIRI), + ITEM_MAP_ENTRY(ITEM_TUNIC_GORON), + ITEM_MAP_ENTRY(ITEM_TUNIC_ZORA), + ITEM_MAP_ENTRY(ITEM_BOOTS_KOKIRI), + ITEM_MAP_ENTRY(ITEM_BOOTS_IRON), + ITEM_MAP_ENTRY(ITEM_BOOTS_HOVER), + ITEM_MAP_ENTRY(ITEM_BULLET_BAG_30), + ITEM_MAP_ENTRY(ITEM_BULLET_BAG_40), + ITEM_MAP_ENTRY(ITEM_BULLET_BAG_50), + ITEM_MAP_ENTRY(ITEM_QUIVER_30), + ITEM_MAP_ENTRY(ITEM_QUIVER_40), + ITEM_MAP_ENTRY(ITEM_QUIVER_50), + ITEM_MAP_ENTRY(ITEM_BOMB_BAG_20), + ITEM_MAP_ENTRY(ITEM_BOMB_BAG_30), + ITEM_MAP_ENTRY(ITEM_BOMB_BAG_40), + ITEM_MAP_ENTRY(ITEM_BRACELET), + ITEM_MAP_ENTRY(ITEM_GAUNTLETS_SILVER), + ITEM_MAP_ENTRY(ITEM_GAUNTLETS_GOLD), + ITEM_MAP_ENTRY(ITEM_SCALE_SILVER), + ITEM_MAP_ENTRY(ITEM_SCALE_GOLDEN), + ITEM_MAP_ENTRY(ITEM_SWORD_KNIFE), + ITEM_MAP_ENTRY(ITEM_WALLET_ADULT), + ITEM_MAP_ENTRY(ITEM_WALLET_GIANT), + ITEM_MAP_ENTRY(ITEM_SEEDS), + ITEM_MAP_ENTRY(ITEM_FISHING_POLE), +}; + +// Maps entries in the GS flag array to the area name it represents +std::vector gsMapping = { + "Deku Tree", + "Dodongo's Cavern", + "Inside Jabu-Jabu's Belly", + "Forest Temple", + "Fire Temple", + "Water Temple", + "Spirit Temple", + "Shadow Temple", + "Bottom of the Well", + "Ice Cavern", + "Hyrule Field", + "Lon Lon Ranch", + "Kokiri Forest", + "Lost Woods, Sacred Forest Meadow", + "Castle Town and Ganon's Castle", + "Death Mountain Trail, Goron City", + "Kakariko Village", + "Zora Fountain, River", + "Lake Hylia", + "Gerudo Valley", + "Gerudo Fortress", + "Desert Colossus, Haunted Wasteland", +}; +extern "C" u8 gAreaGsFlags[]; + +extern "C" u8 gAmmoItems[]; + +// Modification of gAmmoItems that replaces ITEM_NONE with the item in inventory slot it represents +u8 gAllAmmoItems[] = { + ITEM_STICK, ITEM_NUT, ITEM_BOMB, ITEM_BOW, ITEM_ARROW_FIRE, ITEM_DINS_FIRE, + ITEM_SLINGSHOT, ITEM_OCARINA_TIME, ITEM_BOMBCHU, ITEM_LONGSHOT, ITEM_ARROW_ICE, ITEM_FARORES_WIND, + ITEM_BOOMERANG, ITEM_LENS, ITEM_BEAN, ITEM_HAMMER, +}; + +// Adds a text tooltip for the previous ImGui item +void SetLastItemHoverText(const std::string& text) { + if (ImGui::IsItemHovered()) { + ImGui::BeginTooltip(); + ImGui::Text(text.c_str()); + ImGui::EndTooltip(); + } +} + +// Adds a "?" next to the previous ImGui item with a custom tooltip +void InsertHelpHoverText(const std::string& text) { + ImGui::SameLine(); + ImGui::TextColored(ImVec4(0.7f, 0.7f, 0.7f, 1.0f), "?"); + if (ImGui::IsItemHovered()) { + ImGui::BeginTooltip(); + ImGui::Text(text.c_str()); + ImGui::EndTooltip(); + } +} + +void DrawSaveEditor(bool& open) { + if (!open) { + return; + } + + ImGui::SetNextWindowSize(ImVec2(520, 600), ImGuiCond_FirstUseEver); + if (!ImGui::Begin("Save Editor", &open)) { + ImGui::End(); + return; + } + + // TODO This is the bare minimum to get the player name showing + // There will need to be more effort to get it robust and editable + std::string name; + for (int i = 0; i < 8; i++) { + char letter = gSaveContext.playerName[i] + 0x3D; + if (letter == '{') { + letter = '\0'; + } + name += letter; + } + name += '\0'; + + if (ImGui::BeginTabBar("SaveContextTabBar", ImGuiTabBarFlags_NoCloseWithMiddleMouseButton)) { + if (ImGui::BeginTabItem("Info")) { + ImGui::PushItemWidth(ImGui::GetFontSize() * 6); + + ImGui::Text("Name: %s", name.c_str()); + InsertHelpHoverText("Player Name"); + + // Use an intermediary to keep the health from updating (and potentially killing the player) + // until it is done being edited + int16_t healthIntermediary = gSaveContext.healthCapacity; + ImGui::InputScalar("Max Health", ImGuiDataType_S16, &healthIntermediary); + if (ImGui::IsItemDeactivated()) { + gSaveContext.healthCapacity = healthIntermediary; + } + InsertHelpHoverText("Maximum health. 16 units per full heart"); + if (gSaveContext.health > gSaveContext.healthCapacity) { + gSaveContext.health = gSaveContext.healthCapacity; // Clamp health to new max + } + + const uint16_t healthMin = 0; + const uint16_t healthMax = gSaveContext.healthCapacity; + ImGui::SetNextItemWidth(ImGui::GetFontSize() * 15); + ImGui::SliderScalar("Health", ImGuiDataType_S16, &gSaveContext.health, &healthMin, &healthMax); + InsertHelpHoverText("Current health. 16 units per full heart"); + + bool doubleDefense = gSaveContext.doubleDefense != 0; + if (ImGui::Checkbox("Double Defense", &doubleDefense)) { + gSaveContext.doubleDefense = doubleDefense; + gSaveContext.inventory.defenseHearts = + gSaveContext.doubleDefense ? 20 : 0; // Set to get the border drawn in the UI + } + InsertHelpHoverText("Is double defense unlocked?"); + + std::string magicName; + if (gSaveContext.magicLevel == 2) { + magicName = "Double"; + } else if (gSaveContext.magicLevel == 1) { + magicName = "Single"; + } else { + magicName = "None"; + } + ImGui::SetNextItemWidth(ImGui::GetFontSize() * 6); + if (ImGui::BeginCombo("Magic Level", magicName.c_str())) { + if (ImGui::Selectable("Double")) { + gSaveContext.magicLevel = 2; + gSaveContext.magicAcquired = true; + gSaveContext.doubleMagic = true; + } + if (ImGui::Selectable("Single")) { + gSaveContext.magicLevel = 1; + gSaveContext.magicAcquired = true; + gSaveContext.doubleMagic = false; + } + if (ImGui::Selectable("None")) { + gSaveContext.magicLevel = 0; + gSaveContext.magicAcquired = false; + gSaveContext.doubleMagic = false; + } + + ImGui::EndCombo(); + } + InsertHelpHoverText("Current magic level"); + gSaveContext.unk_13F4 = gSaveContext.magicLevel * 0x30; // Set to get the bar drawn in the UI + if (gSaveContext.magic > gSaveContext.unk_13F4) { + gSaveContext.magic = gSaveContext.unk_13F4; // Clamp magic to new max + } + + const uint8_t magicMin = 0; + const uint8_t magicMax = gSaveContext.unk_13F4; + ImGui::SetNextItemWidth(ImGui::GetFontSize() * 15); + ImGui::SliderScalar("Magic", ImGuiDataType_S8, &gSaveContext.magic, &magicMin, &magicMax); + InsertHelpHoverText("Current magic. 48 units per magic level"); + + ImGui::InputScalar("Rupees", ImGuiDataType_S16, &gSaveContext.rupees); + InsertHelpHoverText("Current rupees"); + + const uint16_t dayTimeMin = 0; + const uint16_t dayTimeMax = 0xFFFF; + ImGui::SetNextItemWidth(ImGui::GetFontSize() * 15); + ImGui::SliderScalar("Time", ImGuiDataType_U16, &gSaveContext.dayTime, &dayTimeMin, &dayTimeMax); + InsertHelpHoverText("Time of day"); + if (ImGui::Button("Dawn")) { + gSaveContext.dayTime = 0x4000; + } + ImGui::SameLine(); + if (ImGui::Button("Noon")) { + gSaveContext.dayTime = 0x8000; + } + ImGui::SameLine(); + if (ImGui::Button("Sunset")) { + gSaveContext.dayTime = 0xC000; + } + ImGui::SameLine(); + if (ImGui::Button("Midnight")) { + gSaveContext.dayTime = 0; + } + + ImGui::InputScalar("Total Days", ImGuiDataType_S32, &gSaveContext.totalDays); + InsertHelpHoverText("Total number of days elapsed since the start of the game"); + + ImGui::InputScalar("Deaths", ImGuiDataType_U16, &gSaveContext.deaths); + InsertHelpHoverText("Total number of deaths"); + + // TODO Move to quest status screen once the page is created + ImGui::InputScalar("GS Count", ImGuiDataType_S16, &gSaveContext.inventory.gsTokens); + InsertHelpHoverText("Number of gold skulltula tokens aquired"); + + bool bgsFlag = gSaveContext.bgsFlag != 0; + if (ImGui::Checkbox("Has BGS", &bgsFlag)) { + gSaveContext.bgsFlag = bgsFlag; + } + InsertHelpHoverText("Is Biggoron sword unlocked? Replaces Giant's knife"); + + ImGui::InputScalar("Sword Health", ImGuiDataType_U16, &gSaveContext.swordHealth); + InsertHelpHoverText("Giant's knife health. Default is 8. Must be >0 for Biggoron sword to work"); + + ImGui::InputScalar("Bgs Day Count", ImGuiDataType_S32, &gSaveContext.bgsDayCount); + InsertHelpHoverText("Total number of days elapsed since giving Biggoron the claim check"); + + // TODO Changing Link's age is more involved than just setting gSaveContext.linkAge + // It might not fit here and instead should be only changable when changing scenes + /* + if (ImGui::BeginCombo("Link Age", LINK_IS_ADULT ? "Adult" : "Child")) { + if (ImGui::Selectable("Adult")) { + gSaveContext.linkAge = 0; + } + if (ImGui::Selectable("Child")) { + gSaveContext.linkAge = 1; + } + + ImGui::EndCombo(); + } + */ + + ImGui::PopItemWidth(); + ImGui::EndTabItem(); + } + + if (ImGui::BeginTabItem("Inventory")) { + static bool restrictToValid = true; + + ImGui::Checkbox("Restrict to valid items", &restrictToValid); + InsertHelpHoverText("Restricts items and ammo to only what is possible to legally acquire in-game"); + + for (int32_t y = 0; y < 4; y++) { + for (int32_t x = 0; x < 6; x++) { + int32_t index = x + y * 6; + static int32_t selectedIndex = -1; + static const char* itemPopupPicker = "itemPopupPicker"; + + ImGui::PushID(index); + + if (x != 0) { + ImGui::SameLine(); + } + + ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(1, 1, 1, 0)); + ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 1.0f); + uint8_t item = gSaveContext.inventory.items[index]; + if (item != ITEM_NONE) { + const ItemMapEntry& slotEntry = itemMapping.find(item)->second; + if (ImGui::ImageButton(SohImGui::GetTextureByName(slotEntry.name), ImVec2(32.0f, 32.0f), + ImVec2(0, 0), ImVec2(1, 1), 0)) { + selectedIndex = index; + ImGui::OpenPopup(itemPopupPicker); + } + } else { + if (ImGui::Button("##itemNone", ImVec2(32.0f, 32.0f))) { + selectedIndex = index; + ImGui::OpenPopup(itemPopupPicker); + } + } + ImGui::PopStyleVar(); + ImGui::PopStyleColor(); + + ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(0, 0)); + if (ImGui::BeginPopup(itemPopupPicker)) { + if (ImGui::Button("##itemNonePicker", ImVec2(32.0f, 32.0f))) { + gSaveContext.inventory.items[selectedIndex] = ITEM_NONE; + ImGui::CloseCurrentPopup(); + } + SetLastItemHoverText("ITEM_NONE"); + + std::vector possibleItems; + if (restrictToValid) { + // Scan gItemSlots to find legal items for this slot. Bottles are a special case + for (int slotIndex = 0; slotIndex < 56; slotIndex++) { + int testIndex = (selectedIndex == SLOT_BOTTLE_1 || selectedIndex == SLOT_BOTTLE_2 || + selectedIndex == SLOT_BOTTLE_3 || selectedIndex == SLOT_BOTTLE_4) + ? SLOT_BOTTLE_1 + : selectedIndex; + if (gItemSlots[slotIndex] == testIndex) { + possibleItems.push_back(itemMapping[slotIndex]); + } + } + } else { + for (const auto& entry : itemMapping) { + possibleItems.push_back(entry.second); + } + } + + for (int32_t pickerIndex = 0; pickerIndex < possibleItems.size(); pickerIndex++) { + if (((pickerIndex + 1) % 8) != 0) { + ImGui::SameLine(); + } + const ItemMapEntry& slotEntry = possibleItems[pickerIndex]; + if (ImGui::ImageButton(SohImGui::GetTextureByName(slotEntry.name), ImVec2(32.0f, 32.0f), + ImVec2(0, 0), ImVec2(1, 1), 0)) { + gSaveContext.inventory.items[selectedIndex] = slotEntry.id; + ImGui::CloseCurrentPopup(); + } + SetLastItemHoverText(slotEntry.name); + } + + ImGui::EndPopup(); + } + ImGui::PopStyleVar(); + + ImGui::PopID(); + } + } + + ImGui::Text("Ammo"); + for (uint32_t ammoIndex = 0, drawnAmmoItems = 0; ammoIndex < 16; ammoIndex++) { + uint8_t item = (restrictToValid) ? gAmmoItems[ammoIndex] : gAllAmmoItems[ammoIndex]; + if (item != ITEM_NONE) { + // For legal items, display as 1 row of 7. For unrestricted items, display rows of 6 to match + // inventory + if ((restrictToValid && (drawnAmmoItems != 0)) || ((drawnAmmoItems % 6) != 0)) { + ImGui::SameLine(); + } + drawnAmmoItems++; + + ImGui::PushID(ammoIndex); + ImGui::PushItemWidth(32.0f); + ImGui::BeginGroup(); + + ImGui::Image(SohImGui::GetTextureByName(itemMapping[item].name), ImVec2(32.0f, 32.0f)); + ImGui::InputScalar("##ammoInput", ImGuiDataType_S8, &AMMO(item)); + + ImGui::EndGroup(); + ImGui::PopItemWidth(); + ImGui::PopID(); + } + } + + ImGui::EndTabItem(); + } + + if (ImGui::BeginTabItem("Flags")) { + static uint32_t selectedGsMap = 0; + ImGui::Text("Gold Skulltulas"); + ImGui::Text("Map"); + ImGui::SameLine(); + if (ImGui::BeginCombo("##Gold Skulltula Map", gsMapping[selectedGsMap].c_str())) { + for (int32_t gsIndex = 0; gsIndex < gsMapping.size(); gsIndex++) { + if (ImGui::Selectable(gsMapping[gsIndex].c_str())) { + selectedGsMap = gsIndex; + } + } + + ImGui::EndCombo(); + } + + // TODO We should write out descriptions for each one... ugh + ImGui::Text("Flags"); + uint32_t currentFlags = GET_GS_FLAGS(selectedGsMap); + uint32_t allFlags = gAreaGsFlags[selectedGsMap]; + uint32_t setMask = 1; + // Iterate over bitfield and create a checkbox for each skulltula + while (allFlags != 0) { + bool isThisSet = (currentFlags & 0x1) == 0x1; + + ImGui::SameLine(); + ImGui::PushID(allFlags); + if (ImGui::Checkbox("##gs", &isThisSet)) { + if (isThisSet) { + SET_GS_FLAGS(selectedGsMap, setMask); + } else { + // Have to do this roundabout method as the macro does not support clearing flags + uint32_t currentFlagsBase = GET_GS_FLAGS(selectedGsMap); + gSaveContext.gsFlags[selectedGsMap >> 2] &= ~gGsFlagsMasks[selectedGsMap & 3]; + SET_GS_FLAGS(selectedGsMap, currentFlagsBase & ~setMask); + } + } + + ImGui::PopID(); + + allFlags >>= 1; + currentFlags >>= 1; + setMask <<= 1; + } + + static bool keepGsCountUpdated = true; + ImGui::Checkbox("Keep GS Count Updated", &keepGsCountUpdated); + InsertHelpHoverText("Automatically adjust the number of gold skulltula tokens acquired based on set flags"); + int32_t gsCount = 0; + if (keepGsCountUpdated) { + for (int32_t gsFlagIndex = 0; gsFlagIndex < 6; gsFlagIndex++) { + gsCount += std::popcount(static_cast(gSaveContext.gsFlags[gsFlagIndex])); + } + gSaveContext.inventory.gsTokens = gsCount; + } + + // TODO other flag types, like switch, clear, etc. + // These flags interact with the actor context, so it's a bit more complicated + + ImGui::EndTabItem(); + } + + ImGui::EndTabBar(); + } + + ImGui::End(); +} + +void InitSaveEditor() { + SohImGui::AddWindow("Debug", "Save Editor", DrawSaveEditor); + + // Load item icons into ImGui + for (const auto& entry : itemMapping) { + SohImGui::LoadResource(entry.second.name, entry.second.texturePath); + } +} diff --git a/soh/soh/Enhancements/debugger/debugSaveEditor.h b/soh/soh/Enhancements/debugger/debugSaveEditor.h new file mode 100644 index 000000000..084f7cadc --- /dev/null +++ b/soh/soh/Enhancements/debugger/debugSaveEditor.h @@ -0,0 +1,3 @@ +#pragma once + +void InitSaveEditor(); diff --git a/soh/soh/Enhancements/debugger/debugger.cpp b/soh/soh/Enhancements/debugger/debugger.cpp new file mode 100644 index 000000000..e493375cd --- /dev/null +++ b/soh/soh/Enhancements/debugger/debugger.cpp @@ -0,0 +1,6 @@ +#include "debugger.h" +#include "debugSaveEditor.h" + +void Debug_Init(void) { + InitSaveEditor(); +} diff --git a/soh/soh/Enhancements/debugger/debugger.h b/soh/soh/Enhancements/debugger/debugger.h new file mode 100644 index 000000000..4bc0f985b --- /dev/null +++ b/soh/soh/Enhancements/debugger/debugger.h @@ -0,0 +1,3 @@ +#pragma once + +void Debug_Init(void); diff --git a/soh/soh/OTRGlobals.cpp b/soh/soh/OTRGlobals.cpp index d10600bce..007afd702 100644 --- a/soh/soh/OTRGlobals.cpp +++ b/soh/soh/OTRGlobals.cpp @@ -22,6 +22,7 @@ #include "Lib/stb/stb_image.h" #include "AudioPlayer.h" #include "../soh/Enhancements/debugconsole.h" +#include "../soh/Enhancements/debugger/debugger.h" #include "Utils/BitConverter.h" OTRGlobals* OTRGlobals::Instance; @@ -54,6 +55,7 @@ extern "C" void InitOTR() { clearMtx = (uintptr_t)&gMtxClear; OTRMessage_Init(); DebugConsole_Init(); + Debug_Init(); } extern "C" uint64_t GetFrequency() {