From e16481933965a46dd2f2a9394394193cab27a5bf Mon Sep 17 00:00:00 2001 From: Josh Bodner <30329717+jbodner09@users.noreply.github.com> Date: Tue, 12 Apr 2022 18:38:08 -0700 Subject: [PATCH 1/6] Added DPad support to shops (#139) * Added DPad support to shops * Wrap changes in CVar * Remove unnesseccary const_cast * Fixed transparent texture making framebuffers also transparent in D3D11. (#84) This happened with the Mirror Shield in the inventory screen preview. * Add save editor * Added DPad support to shops * Fixing rebase conflict * Remove unnesseccary const_cast * Added DPad support to file selection and pause screens (#124) * Added DPad support to file selection and pause screens * Wrap changes behind CVar * Fix merge conflict for real * Remove unnecessary const_cast * Fixed transparent texture making framebuffers also transparent in D3D11. (#84) This happened with the Mirror Shield in the inventory screen preview. * Add save editor * Added DPad support to file selection and pause screens * Fixing rebase conflict * Remove unnecessary const_cast Co-authored-by: MaikelChan Co-authored-by: rozlette * Added DPad support to shops * Fixing rebase conflict again * Allocate aligned heaps * Formatting fixes * Added DPad support to ocarina and text prompts (#137) * Added DPad support to ocarina playing and text choice selection. * Wrap changes in CVar * Fix mapping not updating if CVar is changed in-game * Remove unnecessary const_cast * Fixed transparent texture making framebuffers also transparent in D3D11. (#84) This happened with the Mirror Shield in the inventory screen preview. * Add save editor * Added DPad support to ocarina playing and text choice selection. * Fixing rebase conflict * Fix mapping not updating if CVar is changed in-game * Remove unnecessary const_cast * Added DPad support to file selection and pause screens (#124) * Added DPad support to file selection and pause screens * Wrap changes behind CVar * Fix merge conflict for real * Remove unnecessary const_cast * Fixed transparent texture making framebuffers also transparent in D3D11. (#84) This happened with the Mirror Shield in the inventory screen preview. * Add save editor * Added DPad support to file selection and pause screens * Fixing rebase conflict * Remove unnecessary const_cast Co-authored-by: MaikelChan Co-authored-by: rozlette * Added DPad support to ocarina playing and text choice selection. * Fixing rebase conflict again * Fix mapping not updating if CVar is changed in-game Co-authored-by: MaikelChan Co-authored-by: rozlette * Added DPad support to shops * Fixing rebase conflict for the last time * Totally Fixing rebase conflict again I promise * This has to be the last time I fix this rebase conflict Co-authored-by: MaikelChan Co-authored-by: rozlette Co-authored-by: GaryOderNichts <12049776+GaryOderNichts@users.noreply.github.com> --- libultraship/libultraship/GameSettings.cpp | 4 ++ libultraship/libultraship/GameSettings.h | 1 + libultraship/libultraship/SohImGuiImpl.cpp | 5 +++ .../overlays/actors/ovl_En_Ossan/z_en_ossan.c | 38 +++++++++++-------- 4 files changed, 33 insertions(+), 15 deletions(-) diff --git a/libultraship/libultraship/GameSettings.cpp b/libultraship/libultraship/GameSettings.cpp index c3294d9e3..678555c7d 100644 --- a/libultraship/libultraship/GameSettings.cpp +++ b/libultraship/libultraship/GameSettings.cpp @@ -95,6 +95,9 @@ namespace Game { Settings.controller.dpad_ocarina_text = stob(Conf[ControllerSection]["dpad_ocarina_text"]); CVar_SetS32("gDpadOcarinaText", Settings.controller.dpad_ocarina_text); + + Settings.controller.dpad_shop = stob(Conf[ControllerSection]["dpad_shop"]); + CVar_SetS32("gDpadShop", Settings.controller.dpad_shop); // Cheats Settings.cheats.debug_mode = stob(Conf[CheatSection]["debug_mode"]); @@ -157,6 +160,7 @@ namespace Game { Conf[ControllerSection]["input_enabled"] = std::to_string(Settings.controller.input_enabled); Conf[ControllerSection]["dpad_pause_name"] = std::to_string(Settings.controller.dpad_pause_name); Conf[ControllerSection]["dpad_ocarina_text"] = std::to_string(Settings.controller.dpad_ocarina_text); + Conf[ControllerSection]["dpad_shop"] = std::to_string(Settings.controller.dpad_shop); // Cheats Conf[CheatSection]["debug_mode"] = std::to_string(Settings.cheats.debug_mode); diff --git a/libultraship/libultraship/GameSettings.h b/libultraship/libultraship/GameSettings.h index 8e1293c65..17079c9bf 100644 --- a/libultraship/libultraship/GameSettings.h +++ b/libultraship/libultraship/GameSettings.h @@ -36,6 +36,7 @@ struct SoHConfigType { bool input_enabled = false; bool dpad_pause_name = false; bool dpad_ocarina_text = false; + bool dpad_shop = false; } controller; // Cheats diff --git a/libultraship/libultraship/SohImGuiImpl.cpp b/libultraship/libultraship/SohImGuiImpl.cpp index 7bdf4859a..9ba3184d8 100644 --- a/libultraship/libultraship/SohImGuiImpl.cpp +++ b/libultraship/libultraship/SohImGuiImpl.cpp @@ -365,6 +365,11 @@ namespace SohImGui { needs_save = true; } + if (ImGui::Checkbox("DPad Support for Browsing Shop Items", &Game::Settings.controller.dpad_shop)) { + CVar_SetS32("gDpadShop", Game::Settings.controller.dpad_shop); + needs_save = true; + } + ImGui::EndMenu(); } diff --git a/soh/src/overlays/actors/ovl_En_Ossan/z_en_ossan.c b/soh/src/overlays/actors/ovl_En_Ossan/z_en_ossan.c index 04975d910..4d5fefec6 100644 --- a/soh/src/overlays/actors/ovl_En_Ossan/z_en_ossan.c +++ b/soh/src/overlays/actors/ovl_En_Ossan/z_en_ossan.c @@ -948,7 +948,9 @@ s32 EnOssan_FacingShopkeeperDialogResult(EnOssan* this, GlobalContext* globalCtx } void EnOssan_State_FacingShopkeeper(EnOssan* this, GlobalContext* globalCtx, Player* player) { + Input* input = &globalCtx->state.input[0]; u8 nextIndex; + bool dpad = CVar_GetS32("gDpadShop", 0); if ((Message_GetState(&globalCtx->msgCtx) == TEXT_STATE_CHOICE) && !EnOssan_TestEndInteraction(this, globalCtx, &globalCtx->state.input[0])) { @@ -957,7 +959,7 @@ void EnOssan_State_FacingShopkeeper(EnOssan* this, GlobalContext* globalCtx, Pla return; } // Stick Left - if (this->stickAccumX < 0) { + if ((this->stickAccumX < 0) || (dpad && CHECK_BTN_ALL(input->press.button, BTN_DLEFT))) { nextIndex = EnOssan_SetCursorIndexFromNeutral(this, 4); if (nextIndex != CURSOR_INVALID) { this->cursorIndex = nextIndex; @@ -966,7 +968,7 @@ void EnOssan_State_FacingShopkeeper(EnOssan* this, GlobalContext* globalCtx, Pla this->stickLeftPrompt.isEnabled = false; func_80078884(NA_SE_SY_CURSOR); } - } else if (this->stickAccumX > 0) { + } else if ((this->stickAccumX > 0) || (dpad && CHECK_BTN_ALL(input->press.button, BTN_DRIGHT))) { nextIndex = EnOssan_SetCursorIndexFromNeutral(this, 0); if (nextIndex != CURSOR_INVALID) { this->cursorIndex = nextIndex; @@ -1023,11 +1025,13 @@ void EnOssan_State_LookToRightShelf(EnOssan* this, GlobalContext* globalCtx, Pla } } -void EnOssan_CursorUpDown(EnOssan* this) { +void EnOssan_CursorUpDown(EnOssan* this, GlobalContext* globalCtx) { + Input* input = &globalCtx->state.input[0]; u8 curTemp = this->cursorIndex; u8 curScanTemp; + bool dpad = CVar_GetS32("gDpadShop", 0); - if (this->stickAccumY < 0) { + if ((this->stickAccumY < 0) || (dpad && CHECK_BTN_ALL(input->press.button, BTN_DDOWN))) { curTemp &= 0xFE; if (this->shelfSlots[curTemp] != NULL) { this->cursorIndex = curTemp; @@ -1066,7 +1070,7 @@ void EnOssan_CursorUpDown(EnOssan* this) { } } } - } else if (this->stickAccumY > 0) { + } else if ((this->stickAccumY > 0) || (dpad && CHECK_BTN_ALL(input->press.button, BTN_DUP))) { curTemp |= 1; if (this->shelfSlots[curTemp] != NULL) { this->cursorIndex = curTemp; @@ -1172,11 +1176,13 @@ s32 EnOssan_HasPlayerSelectedItem(GlobalContext* globalCtx, EnOssan* this, Input } void EnOssan_State_BrowseLeftShelf(EnOssan* this, GlobalContext* globalCtx, Player* player) { + Input* input = &globalCtx->state.input[0]; s32 a; s32 b; u8 prevIndex = this->cursorIndex; s32 c; s32 d; + bool dpad = CVar_GetS32("gDpadShop", 0); if (!EnOssan_ReturnItemToShelf(this)) { osSyncPrintf("%s[%d]:" VT_FGCOL(GREEN) "ズーム中!!" VT_RST "\n", "../z_en_oB1.c", 2152); @@ -1193,7 +1199,7 @@ void EnOssan_State_BrowseLeftShelf(EnOssan* this, GlobalContext* globalCtx, Play if ((Message_GetState(&globalCtx->msgCtx) == TEXT_STATE_EVENT) && !EnOssan_HasPlayerSelectedItem(globalCtx, this, &globalCtx->state.input[0])) { if (this->moveHorizontal) { - if (this->stickAccumX > 0) { + if ((this->stickAccumX > 0) || (dpad && CHECK_BTN_ALL(input->press.button, BTN_DRIGHT))) { a = EnOssan_CursorRight(this, this->cursorIndex, 4); if (a != CURSOR_INVALID) { this->cursorIndex = a; @@ -1201,14 +1207,14 @@ void EnOssan_State_BrowseLeftShelf(EnOssan* this, GlobalContext* globalCtx, Play EnOssan_SetLookToShopkeeperFromShelf(globalCtx, this); return; } - } else if (this->stickAccumX < 0) { + } else if ((this->stickAccumX < 0) || (dpad && CHECK_BTN_ALL(input->press.button, BTN_DLEFT))) { b = EnOssan_CursorLeft(this, this->cursorIndex, 8); if (b != CURSOR_INVALID) { this->cursorIndex = b; } } } else { - if (this->stickAccumX > 0 && this->stickAccumX > 500) { + if ((this->stickAccumX > 0 && this->stickAccumX > 500) || (dpad && CHECK_BTN_ALL(input->press.button, BTN_DRIGHT))) { c = EnOssan_CursorRight(this, this->cursorIndex, 4); if (c != CURSOR_INVALID) { this->cursorIndex = c; @@ -1216,14 +1222,14 @@ void EnOssan_State_BrowseLeftShelf(EnOssan* this, GlobalContext* globalCtx, Play EnOssan_SetLookToShopkeeperFromShelf(globalCtx, this); return; } - } else if (this->stickAccumX < 0 && this->stickAccumX < -500) { + } else if ((this->stickAccumX < 0 && this->stickAccumX < -500) || (dpad && CHECK_BTN_ALL(input->press.button, BTN_DLEFT))) { d = EnOssan_CursorLeft(this, this->cursorIndex, 8); if (d != CURSOR_INVALID) { this->cursorIndex = d; } } } - EnOssan_CursorUpDown(this); + EnOssan_CursorUpDown(this, globalCtx); if (this->cursorIndex != prevIndex) { Message_ContinueTextbox(globalCtx, this->shelfSlots[this->cursorIndex]->actor.textId); func_80078884(NA_SE_SY_CURSOR); @@ -1232,9 +1238,11 @@ void EnOssan_State_BrowseLeftShelf(EnOssan* this, GlobalContext* globalCtx, Play } void EnOssan_State_BrowseRightShelf(EnOssan* this, GlobalContext* globalCtx, Player* player) { + Input* input = &globalCtx->state.input[0]; s32 pad[2]; u8 prevIndex; u8 nextIndex; + bool dpad = CVar_GetS32("gDpadShop", 0); prevIndex = this->cursorIndex; if (!EnOssan_ReturnItemToShelf(this)) { @@ -1252,7 +1260,7 @@ void EnOssan_State_BrowseRightShelf(EnOssan* this, GlobalContext* globalCtx, Pla if ((Message_GetState(&globalCtx->msgCtx) == TEXT_STATE_EVENT) && !EnOssan_HasPlayerSelectedItem(globalCtx, this, &globalCtx->state.input[0])) { if (this->moveHorizontal) { - if (this->stickAccumX < 0) { + if ((this->stickAccumX < 0) || (dpad && CHECK_BTN_ALL(input->press.button, BTN_DLEFT))) { nextIndex = EnOssan_CursorRight(this, this->cursorIndex, 0); if (nextIndex != CURSOR_INVALID) { this->cursorIndex = nextIndex; @@ -1260,14 +1268,14 @@ void EnOssan_State_BrowseRightShelf(EnOssan* this, GlobalContext* globalCtx, Pla EnOssan_SetLookToShopkeeperFromShelf(globalCtx, this); return; } - } else if (this->stickAccumX > 0) { + } else if ((this->stickAccumX > 0) || (dpad && CHECK_BTN_ALL(input->press.button, BTN_DRIGHT))) { nextIndex = EnOssan_CursorLeft(this, this->cursorIndex, 4); if (nextIndex != CURSOR_INVALID) { this->cursorIndex = nextIndex; } } } else { - if (this->stickAccumX < 0 && this->stickAccumX < -500) { + if ((this->stickAccumX < 0 && this->stickAccumX < -500) || (dpad && CHECK_BTN_ALL(input->press.button, BTN_DLEFT))) { nextIndex = EnOssan_CursorRight(this, this->cursorIndex, 0); if (nextIndex != CURSOR_INVALID) { this->cursorIndex = nextIndex; @@ -1275,14 +1283,14 @@ void EnOssan_State_BrowseRightShelf(EnOssan* this, GlobalContext* globalCtx, Pla EnOssan_SetLookToShopkeeperFromShelf(globalCtx, this); return; } - } else if (this->stickAccumX > 0 && this->stickAccumX > 500) { + } else if ((this->stickAccumX > 0 && this->stickAccumX > 500) || (dpad && CHECK_BTN_ALL(input->press.button, BTN_DRIGHT))) { nextIndex = EnOssan_CursorLeft(this, this->cursorIndex, 4); if (nextIndex != CURSOR_INVALID) { this->cursorIndex = nextIndex; } } } - EnOssan_CursorUpDown(this); + EnOssan_CursorUpDown(this, globalCtx); if (this->cursorIndex != prevIndex) { Message_ContinueTextbox(globalCtx, this->shelfSlots[this->cursorIndex]->actor.textId); func_80078884(NA_SE_SY_CURSOR); From 0ef2d0c750fec74716d9113a68a4c0181640028d Mon Sep 17 00:00:00 2001 From: rozlette Date: Wed, 13 Apr 2022 23:53:04 -0500 Subject: [PATCH 2/6] Remove leftover debug code (fixes #60) --- soh/src/overlays/actors/ovl_Boss_Ganondrof/z_boss_ganondrof.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/soh/src/overlays/actors/ovl_Boss_Ganondrof/z_boss_ganondrof.c b/soh/src/overlays/actors/ovl_Boss_Ganondrof/z_boss_ganondrof.c index 6f0c3dc30..94ea16422 100644 --- a/soh/src/overlays/actors/ovl_Boss_Ganondrof/z_boss_ganondrof.c +++ b/soh/src/overlays/actors/ovl_Boss_Ganondrof/z_boss_ganondrof.c @@ -1274,7 +1274,7 @@ void BossGanondrof_CollisionCheck(BossGanondrof* this, GlobalContext* globalCtx) this->actor.colChkInfo.health -= dmg; } - if ((s8)this->actor.colChkInfo.health <= 0 || 1) { + if ((s8)this->actor.colChkInfo.health <= 0) { BossGanondrof_SetupDeath(this, globalCtx); Enemy_StartFinishingBlow(globalCtx, &this->actor); return; From 1aa08caade5bcb8a840eced6a71a74abddbc81cf Mon Sep 17 00:00:00 2001 From: Sparkling Shampoo Date: Wed, 6 Apr 2022 23:33:32 -0400 Subject: [PATCH 3/6] Prevent cached resource from being acquired while it is being destroyed Fixes #158 --- libultraship/libultraship/ResourceMgr.cpp | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/libultraship/libultraship/ResourceMgr.cpp b/libultraship/libultraship/ResourceMgr.cpp index e6f3c8a0a..b4d7b76a8 100644 --- a/libultraship/libultraship/ResourceMgr.cpp +++ b/libultraship/libultraship/ResourceMgr.cpp @@ -217,9 +217,12 @@ namespace Ship { std::shared_ptr ResourceMgr::GetCachedFile(std::string FilePath) { auto resCacheFind = ResourceCache.find(FilePath); - - if (resCacheFind != ResourceCache.end()) + + if (resCacheFind != ResourceCache.end() && + resCacheFind->second.use_count() > 0) + { return resCacheFind->second; + } else return nullptr; } From 1dcd24e7e2aad2b22d2bfdb398e3a05e8a808eef Mon Sep 17 00:00:00 2001 From: "Sparkling.Shampoo" <102923452+sparklingshampoo@users.noreply.github.com> Date: Sun, 17 Apr 2022 11:19:45 -0400 Subject: [PATCH 4/6] Fix cylinder-tri intersection for GCC due to FLT_EVAL_METHOD (#157) --- soh/src/code/sys_math3d.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/soh/src/code/sys_math3d.c b/soh/src/code/sys_math3d.c index d442b7c4a..39ffea881 100644 --- a/soh/src/code/sys_math3d.c +++ b/soh/src/code/sys_math3d.c @@ -1857,7 +1857,7 @@ s32 Math3D_CylTriVsIntersect(Cylinder16* cyl, TriNorm* tri, Vec3f* intersect) { } } - if (minDistSq != 1.e38f) { + if (minDistSq != (f32)1.e38f) { return true; } From a11038f51555aa57e82ae241fc52e3164e937512 Mon Sep 17 00:00:00 2001 From: Rozelette Date: Sun, 17 Apr 2022 10:24:43 -0500 Subject: [PATCH 5/6] Add flag, equipment, and quest status editors (#164) --- libultraship/libultraship/SohImGuiImpl.cpp | 31 +- libultraship/libultraship/SohImGuiImpl.h | 3 +- soh/soh.vcxproj | 2 + soh/soh.vcxproj.filters | 6 + .../Enhancements/debugger/debugSaveEditor.cpp | 1223 +++++++++++++---- soh/soh/util.cpp | 314 +++++ soh/soh/util.h | 11 + 7 files changed, 1281 insertions(+), 309 deletions(-) create mode 100644 soh/soh/util.cpp create mode 100644 soh/soh/util.h diff --git a/libultraship/libultraship/SohImGuiImpl.cpp b/libultraship/libultraship/SohImGuiImpl.cpp index 9ba3184d8..b7e6e01ab 100644 --- a/libultraship/libultraship/SohImGuiImpl.cpp +++ b/libultraship/libultraship/SohImGuiImpl.cpp @@ -179,21 +179,46 @@ namespace SohImGui { stbi_image_free(img_data); } - void LoadResource(const std::string& name, const std::string& path) { + void LoadResource(const std::string& name, const std::string& path, const ImVec4& tint) { 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) { + std::vector texBuffer; + texBuffer.reserve(res->width * res->height * 4); + + switch (res->texType) { + case Ship::TextureType::RGBA32bpp: + texBuffer.assign(res->imageData, res->imageData + (res->width * res->height * 4)); + break; + case Ship::TextureType::GrayscaleAlpha8bpp: + for (int32_t i = 0; i < res->width * res->height; i++) { + uint8_t ia = res->imageData[i]; + uint8_t color = ((ia >> 4) & 0xF) * 255 / 15; + uint8_t alpha = (ia & 0xF) * 255 / 15; + texBuffer.push_back(color); + texBuffer.push_back(color); + texBuffer.push_back(color); + texBuffer.push_back(alpha); + } + break; + default: // TODO convert other image types SPDLOG_WARN("SohImGui::LoadResource: Attempting to load unsupporting image type %s", path.c_str()); return; } + for (size_t pixel = 0; pixel < texBuffer.size() / 4; pixel++) { + texBuffer[pixel * 4 + 0] *= tint.x; + texBuffer[pixel * 4 + 1] *= tint.y; + texBuffer[pixel * 4 + 2] *= tint.z; + texBuffer[pixel * 4 + 3] *= tint.w; + } + 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); + api->upload_texture(texBuffer.data(), res->width, res->height); DefaultAssets[name] = asset; } diff --git a/libultraship/libultraship/SohImGuiImpl.h b/libultraship/libultraship/SohImGuiImpl.h index 8275e4735..58dac7a1b 100644 --- a/libultraship/libultraship/SohImGuiImpl.h +++ b/libultraship/libultraship/SohImGuiImpl.h @@ -1,5 +1,6 @@ #pragma once +#include "Lib/ImGui/imgui.h" #include "SohConsole.h" struct GameAsset { @@ -63,7 +64,7 @@ namespace SohImGui { void ShowCursor(bool hide, Dialogues w); void BindCmd(const std::string& cmd, CommandEntry entry); void AddWindow(const std::string& category, const std::string& name, WindowDrawFunc drawFunc); - void LoadResource(const std::string& name, const std::string& path); + void LoadResource(const std::string& name, const std::string& path, const ImVec4& tint = ImVec4(1, 1, 1, 1)); ImTextureID GetTextureByID(int id); ImTextureID GetTextureByName(const std::string& name); } diff --git a/soh/soh.vcxproj b/soh/soh.vcxproj index 5560bde76..adf205f14 100644 --- a/soh/soh.vcxproj +++ b/soh/soh.vcxproj @@ -185,6 +185,7 @@ + @@ -928,6 +929,7 @@ + diff --git a/soh/soh.vcxproj.filters b/soh/soh.vcxproj.filters index de9ad6f96..c31a78b19 100644 --- a/soh/soh.vcxproj.filters +++ b/soh/soh.vcxproj.filters @@ -2181,6 +2181,9 @@ Source Files\soh\Enhancements\debugger + + Source Files\soh + @@ -3728,6 +3731,9 @@ Header Files\soh\Enhancements\debugger + + Header Files\soh + diff --git a/soh/soh/Enhancements/debugger/debugSaveEditor.cpp b/soh/soh/Enhancements/debugger/debugSaveEditor.cpp index 95e2f912c..2aaac5a1b 100644 --- a/soh/soh/Enhancements/debugger/debugSaveEditor.cpp +++ b/soh/soh/Enhancements/debugger/debugSaveEditor.cpp @@ -1,6 +1,8 @@ #include "debugSaveEditor.h" +#include "../../util.h" #include "../libultraship/SohImGuiImpl.h" +#include #include #include #include @@ -13,18 +15,20 @@ extern "C" { extern GlobalContext* gGlobalCtx; #include "textures/icon_item_static/icon_item_static.h" +#include "textures/icon_item_24_static/icon_item_24_static.h" } typedef struct { uint32_t id; std::string name; + std::string nameFaded; std::string texturePath; } ItemMapEntry; #define ITEM_MAP_ENTRY(id) \ { \ id, { \ - id, #id, static_cast(gItemIcons[id]) \ + id, #id, #id "_Faded", static_cast(gItemIcons[id]) \ } \ } @@ -120,6 +124,10 @@ std::map itemMapping = { ITEM_MAP_ENTRY(ITEM_WALLET_GIANT), ITEM_MAP_ENTRY(ITEM_SEEDS), ITEM_MAP_ENTRY(ITEM_FISHING_POLE), + ITEM_MAP_ENTRY(ITEM_KEY_BOSS), + ITEM_MAP_ENTRY(ITEM_COMPASS), + ITEM_MAP_ENTRY(ITEM_DUNGEON_MAP), + ITEM_MAP_ENTRY(ITEM_KEY_SMALL), }; // Maps entries in the GS flag array to the area name it represents @@ -147,6 +155,7 @@ std::vector gsMapping = { "Gerudo Fortress", "Desert Colossus, Haunted Wasteland", }; + extern "C" u8 gAreaGsFlags[]; extern "C" u8 gAmmoItems[]; @@ -158,6 +167,63 @@ u8 gAllAmmoItems[] = { ITEM_BOOMERANG, ITEM_LENS, ITEM_BEAN, ITEM_HAMMER, }; +typedef struct { + uint32_t id; + std::string name; + std::string nameFaded; + std::string texturePath; +} QuestMapEntry; + +#define QUEST_MAP_ENTRY(id, tex) \ + { \ + id, { \ + id, #id, #id "_Faded", tex \ + } \ + } + +// Maps quest items ids to info for use in ImGui +std::map questMapping = { + QUEST_MAP_ENTRY(QUEST_MEDALLION_FOREST, gForestMedallionIconTex), + QUEST_MAP_ENTRY(QUEST_MEDALLION_FIRE, gFireMedallionIconTex), + QUEST_MAP_ENTRY(QUEST_MEDALLION_WATER, gWaterMedallionIconTex), + QUEST_MAP_ENTRY(QUEST_MEDALLION_SPIRIT, gSpiritMedallionIconTex), + QUEST_MAP_ENTRY(QUEST_MEDALLION_SHADOW, gShadowMedallionIconTex), + QUEST_MAP_ENTRY(QUEST_MEDALLION_LIGHT, gLightMedallionIconTex), + QUEST_MAP_ENTRY(QUEST_KOKIRI_EMERALD, gKokiriEmeraldIconTex), + QUEST_MAP_ENTRY(QUEST_GORON_RUBY, gGoronRubyIconTex), + QUEST_MAP_ENTRY(QUEST_ZORA_SAPPHIRE, gZoraSapphireIconTex), + QUEST_MAP_ENTRY(QUEST_STONE_OF_AGONY, gStoneOfAgonyIconTex), + QUEST_MAP_ENTRY(QUEST_GERUDO_CARD, gGerudosCardIconTex), +}; + +typedef struct { + uint32_t id; + std::string name; + std::string nameFaded; + ImVec4 color; +} SongMapEntry; + +#define SONG_MAP_ENTRY(id, r, g, b) \ + { \ + id, #id, #id "_Faded", ImVec4(r / 255.0f, g / 255.0f, b / 255.0f, 1.0f) \ + } + +// Maps song ids to info for use in ImGui +std::array songMapping = { { + SONG_MAP_ENTRY(QUEST_SONG_LULLABY, 255, 255, 255), + SONG_MAP_ENTRY(QUEST_SONG_EPONA, 255, 255, 255), + SONG_MAP_ENTRY(QUEST_SONG_SARIA, 255, 255, 255), + SONG_MAP_ENTRY(QUEST_SONG_SUN, 255, 255, 255), + SONG_MAP_ENTRY(QUEST_SONG_TIME, 255, 255, 255), + SONG_MAP_ENTRY(QUEST_SONG_STORMS, 255, 255, 255), + SONG_MAP_ENTRY(QUEST_SONG_MINUET, 150, 255, 100), + SONG_MAP_ENTRY(QUEST_SONG_BOLERO, 255, 80, 40), + SONG_MAP_ENTRY(QUEST_SONG_SERENADE, 100, 150, 255), + SONG_MAP_ENTRY(QUEST_SONG_REQUIEM, 255, 160, 0), + SONG_MAP_ENTRY(QUEST_SONG_NOCTURNE, 255, 100, 255), + SONG_MAP_ENTRY(QUEST_SONG_PRELUDE, 255, 240, 100), +} }; + // Adds a text tooltip for the previous ImGui item void SetLastItemHoverText(const std::string& text) { if (ImGui::IsItemHovered()) { @@ -178,17 +244,33 @@ void InsertHelpHoverText(const std::string& text) { } } -void DrawSaveEditor(bool& open) { - if (!open) { - return; - } +// Encapsulates what is drawn by the passed-in function within a border +template +void DrawGroupWithBorder(T&& drawFunc) { + // First group encapsulates the inner portion and border + ImGui::BeginGroup(); - ImGui::SetNextWindowSize(ImVec2(520, 600), ImGuiCond_FirstUseEver); - if (!ImGui::Begin("Save Editor", &open)) { - ImGui::End(); - return; - } + ImVec2 padding = ImGui::GetStyle().FramePadding; + ImVec2 p0 = ImGui::GetCursorScreenPos(); + ImGui::SetCursorScreenPos(ImVec2(p0.x + padding.x, p0.y + padding.y)); + // Second group encapsulates just the inner portion + ImGui::BeginGroup(); + + drawFunc(); + + ImGui::Dummy(padding); + ImGui::EndGroup(); + + ImVec2 p1 = ImGui::GetItemRectMax(); + p1.x += padding.x; + ImVec4 borderCol = ImGui::GetStyle().Colors[ImGuiCol_Border]; + ImGui::GetWindowDrawList()->AddRect(p0, p1, IM_COL32(borderCol.x * 255, borderCol.y * 255, borderCol.z * 255, borderCol.w * 255)); + + ImGui::EndGroup(); +} + +void DrawInfoTab() { // 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; @@ -201,313 +283,833 @@ void DrawSaveEditor(bool& open) { } name += '\0'; + 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"); + + 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(); +} + +void DrawInventoryTab() { + 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("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(SohUtils::GetItemName(slotEntry.id)); + } + + 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(); + } + } +} + +// Draw a flag bitfield as an grid of checkboxes +void DrawFlagArray(const std::string& name, uint32_t& flags) { + ImGui::PushID(name.c_str()); + for (int32_t flagIndex = 0; flagIndex < 32; flagIndex++) { + if ((flagIndex % 8) != 0) { + ImGui::SameLine(); + } + ImGui::PushID(flagIndex); + uint32_t bitMask = 1 << flagIndex; + bool flag = (flags & bitMask) != 0; + if (ImGui::Checkbox("##check", &flag)) { + if (flag) { + flags |= bitMask; + } else { + flags &= ~bitMask; + } + } + ImGui::PopID(); + } + ImGui::PopID(); +} + +void DrawFlagsTab() { + if (ImGui::TreeNode("Current Scene")) { + if (gGlobalCtx != nullptr) { + ActorContext* act = &gGlobalCtx->actorCtx; + + DrawGroupWithBorder([&]() { + ImGui::Text("Switch"); + InsertHelpHoverText("Permanently-saved switch flags"); + DrawFlagArray("Switch", act->flags.swch); + }); + + ImGui::SameLine(); + + DrawGroupWithBorder([&]() { + ImGui::Text("Temp Switch"); + InsertHelpHoverText("Temporary switch flags. Unset on scene transitions"); + DrawFlagArray("Temp Switch", act->flags.tempSwch); + }); + + DrawGroupWithBorder([&]() { + ImGui::Text("Clear"); + InsertHelpHoverText("Permanently-saved room-clear flags"); + DrawFlagArray("Clear", act->flags.clear); + }); + + ImGui::SameLine(); + + DrawGroupWithBorder([&]() { + ImGui::Text("Temp Clear"); + InsertHelpHoverText("Temporary room-clear flags. Unset on scene transitions"); + DrawFlagArray("Temp Clear", act->flags.tempClear); + }); + + DrawGroupWithBorder([&]() { + ImGui::Text("Collect"); + InsertHelpHoverText("Permanently-saved collect flags"); + DrawFlagArray("Collect", act->flags.collect); + }); + + ImGui::SameLine(); + + DrawGroupWithBorder([&]() { + ImGui::Text("Temp Collect"); + InsertHelpHoverText("Temporary collect flags. Unset on scene transitions"); + DrawFlagArray("Temp Collect", act->flags.tempCollect); + }); + + DrawGroupWithBorder([&]() { + ImGui::Text("Chest"); + InsertHelpHoverText("Permanently-saved chest flags"); + DrawFlagArray("Chest", act->flags.chest); + }); + + ImGui::SameLine(); + + ImGui::BeginGroup(); + + if (ImGui::Button("Reload Flags")) { + act->flags.swch = gSaveContext.sceneFlags[gGlobalCtx->sceneNum].swch; + act->flags.clear = gSaveContext.sceneFlags[gGlobalCtx->sceneNum].clear; + act->flags.collect = gSaveContext.sceneFlags[gGlobalCtx->sceneNum].collect; + act->flags.chest = gSaveContext.sceneFlags[gGlobalCtx->sceneNum].chest; + } + SetLastItemHoverText("Load flags from saved scene flags. Normally happens on scene load"); + + if (ImGui::Button("Save Flags")) { + gSaveContext.sceneFlags[gGlobalCtx->sceneNum].swch = act->flags.swch; + gSaveContext.sceneFlags[gGlobalCtx->sceneNum].clear = act->flags.clear; + gSaveContext.sceneFlags[gGlobalCtx->sceneNum].collect = act->flags.collect; + gSaveContext.sceneFlags[gGlobalCtx->sceneNum].chest = act->flags.chest; + } + SetLastItemHoverText("Save current scene flags. Normally happens on scene exit"); + + ImGui::EndGroup(); + } else { + ImGui::Text("Current game state does not have an active scene"); + } + + ImGui::TreePop(); + } + + if (ImGui::TreeNode("Saved Scene Flags")) { + static uint32_t selectedSceneFlagMap = 0; + ImGui::Text("Map"); + ImGui::SameLine(); + if (ImGui::BeginCombo("##Map", SohUtils::GetSceneName(selectedSceneFlagMap).c_str())) { + for (int32_t sceneIndex = 0; sceneIndex < SCENE_ID_MAX; sceneIndex++) { + if (ImGui::Selectable(SohUtils::GetSceneName(sceneIndex).c_str())) { + selectedSceneFlagMap = sceneIndex; + } + } + + ImGui::EndCombo(); + } + + // Don't show current scene button if there is no current scene + if (gGlobalCtx != nullptr) { + ImGui::SameLine(); + if (ImGui::Button("Current")) { + selectedSceneFlagMap = gGlobalCtx->sceneNum; + } + SetLastItemHoverText("Open flags for current scene"); + } + + DrawGroupWithBorder([&]() { + ImGui::Text("Switch"); + InsertHelpHoverText("Switch flags"); + DrawFlagArray("Switch", gSaveContext.sceneFlags[selectedSceneFlagMap].swch); + }); + + ImGui::SameLine(); + + DrawGroupWithBorder([&]() { + ImGui::Text("Clear"); + InsertHelpHoverText("Room-clear flags"); + DrawFlagArray("Clear", gSaveContext.sceneFlags[selectedSceneFlagMap].clear); + }); + + DrawGroupWithBorder([&]() { + ImGui::Text("Collect"); + InsertHelpHoverText("Collect flags"); + DrawFlagArray("Collect", gSaveContext.sceneFlags[selectedSceneFlagMap].collect); + }); + + ImGui::SameLine(); + + DrawGroupWithBorder([&]() { + ImGui::Text("Chest"); + InsertHelpHoverText("Chest flags"); + DrawFlagArray("Chest", gSaveContext.sceneFlags[selectedSceneFlagMap].chest); + }); + + DrawGroupWithBorder([&]() { + ImGui::Text("Rooms"); + InsertHelpHoverText("Flags for visted rooms"); + DrawFlagArray("Rooms", gSaveContext.sceneFlags[selectedSceneFlagMap].rooms); + }); + + ImGui::SameLine(); + + DrawGroupWithBorder([&]() { + ImGui::Text("Floors"); + InsertHelpHoverText("Flags for visted floors"); + DrawFlagArray("Floors", gSaveContext.sceneFlags[selectedSceneFlagMap].floors); + }); + + ImGui::TreePop(); + } + + DrawGroupWithBorder([&]() { + 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; + } + }); +} + +// Draws a combo that lets you choose and upgrade value from a drop-down of text values +void DrawUpgrade(const std::string& categoryName, int32_t categoryId, const std::vector& names) { + ImGui::Text(categoryName.c_str()); + ImGui::SameLine(); + ImGui::PushID(categoryName.c_str()); + if (ImGui::BeginCombo("##upgrade", names[CUR_UPG_VALUE(categoryId)].c_str())) { + for (int32_t i = 0; i < names.size(); i++) { + if (ImGui::Selectable(names[i].c_str())) { + Inventory_ChangeUpgrade(categoryId, i); + } + } + + ImGui::EndCombo(); + } + ImGui::PopID(); + SetLastItemHoverText(categoryName.c_str()); +} + +// Draws a combo that lets you choose and upgrade value from a popup grid of icons +void DrawUpgradeIcon(const std::string& categoryName, int32_t categoryId, const std::vector& items) { + static const char* upgradePopupPicker = "upgradePopupPicker"; + + ImGui::PushID(categoryName.c_str()); + + ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(1, 1, 1, 0)); + ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 1.0f); + uint8_t item = items[CUR_UPG_VALUE(categoryId)]; + if (item != ITEM_NONE) { + const ItemMapEntry& slotEntry = itemMapping[item]; + if (ImGui::ImageButton(SohImGui::GetTextureByName(slotEntry.name), ImVec2(32.0f, 32.0f), ImVec2(0, 0), + ImVec2(1, 1), 0)) { + ImGui::OpenPopup(upgradePopupPicker); + } + } else { + if (ImGui::Button("##itemNone", ImVec2(32.0f, 32.0f))) { + ImGui::OpenPopup(upgradePopupPicker); + } + } + ImGui::PopStyleVar(); + ImGui::PopStyleColor(); + SetLastItemHoverText(categoryName.c_str()); + + ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(0, 0)); + if (ImGui::BeginPopup(upgradePopupPicker)) { + for (int32_t pickerIndex = 0; pickerIndex < items.size(); pickerIndex++) { + if ((pickerIndex % 8) != 0) { + ImGui::SameLine(); + } + + if (items[pickerIndex] == ITEM_NONE) { + if (ImGui::Button("##upgradePopupPicker", ImVec2(32.0f, 32.0f))) { + Inventory_ChangeUpgrade(categoryId, pickerIndex); + ImGui::CloseCurrentPopup(); + } + SetLastItemHoverText("None"); + } else { + const ItemMapEntry& slotEntry = itemMapping[items[pickerIndex]]; + if (ImGui::ImageButton(SohImGui::GetTextureByName(slotEntry.name), ImVec2(32.0f, 32.0f), ImVec2(0, 0), + ImVec2(1, 1), 0)) { + Inventory_ChangeUpgrade(categoryId, pickerIndex); + ImGui::CloseCurrentPopup(); + } + SetLastItemHoverText(SohUtils::GetItemName(slotEntry.id)); + } + } + + ImGui::EndPopup(); + } + ImGui::PopStyleVar(); + + ImGui::PopID(); +} + +void DrawEquipmentTab() { + const std::vector equipmentValues = { + ITEM_SWORD_KOKIRI, ITEM_SWORD_MASTER, ITEM_SWORD_BGS, ITEM_SWORD_BROKEN, + ITEM_SHIELD_DEKU, ITEM_SHIELD_HYLIAN, ITEM_SHIELD_MIRROR, ITEM_NONE, + ITEM_TUNIC_KOKIRI, ITEM_TUNIC_GORON, ITEM_TUNIC_ZORA, ITEM_NONE, + ITEM_BOOTS_KOKIRI, ITEM_BOOTS_IRON, ITEM_BOOTS_HOVER, ITEM_NONE, + }; + for (int32_t i = 0; i < equipmentValues.size(); i++) { + // Skip over unused 4th slots for shields, boots, and tunics + if (equipmentValues[i] == ITEM_NONE) { + continue; + } + if ((i % 4) != 0) { + ImGui::SameLine(); + } + + ImGui::PushID(i); + uint32_t bitMask = 1 << i; + bool hasEquip = (bitMask & gSaveContext.inventory.equipment) != 0; + const ItemMapEntry& entry = itemMapping[equipmentValues[i]]; + ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0, 0, 0, 0)); + if (ImGui::ImageButton(SohImGui::GetTextureByName(hasEquip ? entry.name : entry.nameFaded), + ImVec2(32.0f, 32.0f), ImVec2(0, 0), ImVec2(1, 1), 0)) { + if (hasEquip) { + gSaveContext.inventory.equipment &= ~bitMask; + } else { + gSaveContext.inventory.equipment |= bitMask; + } + } + ImGui::PopStyleColor(); + ImGui::PopID(); + SetLastItemHoverText(SohUtils::GetItemName(entry.id)); + } + + const std::vector bulletBagValues = { + ITEM_NONE, + ITEM_BULLET_BAG_30, + ITEM_BULLET_BAG_40, + ITEM_BULLET_BAG_50, + }; + DrawUpgradeIcon("Bullet Bag", UPG_BULLET_BAG, bulletBagValues); + + ImGui::SameLine(); + + const std::vector quiverValues = { + ITEM_NONE, + ITEM_QUIVER_30, + ITEM_QUIVER_40, + ITEM_QUIVER_50, + }; + DrawUpgradeIcon("Quiver", UPG_QUIVER, quiverValues); + + ImGui::SameLine(); + + const std::vector bombBagValues = { + ITEM_NONE, + ITEM_BOMB_BAG_20, + ITEM_BOMB_BAG_30, + ITEM_BOMB_BAG_40, + }; + DrawUpgradeIcon("Bomb Bag", UPG_BOMB_BAG, bombBagValues); + + ImGui::SameLine(); + + const std::vector scaleValues = { + ITEM_NONE, + ITEM_SCALE_SILVER, + ITEM_SCALE_GOLDEN, + }; + DrawUpgradeIcon("Scale", UPG_SCALE, scaleValues); + + ImGui::SameLine(); + + const std::vector strengthValues = { + ITEM_NONE, + ITEM_BRACELET, + ITEM_GAUNTLETS_SILVER, + ITEM_GAUNTLETS_GOLD, + }; + DrawUpgradeIcon("Strength", UPG_STRENGTH, strengthValues); + + // There is no icon for child wallet, so default to a text list + const std::vector walletNames = { + "Child (99)", + "Adult (200)", + "Giant (500)", + }; + DrawUpgrade("Wallet", UPG_WALLET, walletNames); + + const std::vector stickNames = { + "None", + "10", + "20", + "30", + }; + DrawUpgrade("Sticks", UPG_STICKS, stickNames); + + const std::vector nutNames = { + "None", + "20", + "30", + "40", + }; + DrawUpgrade("Deku Nuts", UPG_NUTS, nutNames); +} + +// Draws a toggleable icon for a quest item that is faded when disabled +void DrawQuestItemButton(uint32_t item) { + const QuestMapEntry& entry = questMapping[item]; + uint32_t bitMask = 1 << entry.id; + bool hasQuestItem = (bitMask & gSaveContext.inventory.questItems) != 0; + ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0, 0, 0, 0)); + if (ImGui::ImageButton(SohImGui::GetTextureByName(hasQuestItem ? entry.name : entry.nameFaded), + ImVec2(32.0f, 32.0f), ImVec2(0, 0), ImVec2(1, 1), 0)) { + if (hasQuestItem) { + gSaveContext.inventory.questItems &= ~bitMask; + } else { + gSaveContext.inventory.questItems |= bitMask; + } + } + ImGui::PopStyleColor(); + SetLastItemHoverText(SohUtils::GetQuestItemName(entry.id)); +} + +// Draws a toggleable icon for a dungeon item that is faded when disabled +void DrawDungeonItemButton(uint32_t item, uint32_t scene) { + const ItemMapEntry& entry = itemMapping[item]; + uint32_t bitMask = 1 << (entry.id - ITEM_KEY_BOSS); // Bitset starts at ITEM_KEY_BOSS == 0. the rest are sequential + bool hasItem = (bitMask & gSaveContext.inventory.dungeonItems[scene]) != 0; + ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0, 0, 0, 0)); + if (ImGui::ImageButton(SohImGui::GetTextureByName(hasItem ? entry.name : entry.nameFaded), + ImVec2(32.0f, 32.0f), ImVec2(0, 0), ImVec2(1, 1), 0)) { + if (hasItem) { + gSaveContext.inventory.dungeonItems[scene] &= ~bitMask; + } else { + gSaveContext.inventory.dungeonItems[scene] |= bitMask; + } + } + ImGui::PopStyleColor(); + SetLastItemHoverText(SohUtils::GetItemName(entry.id)); +} + +void DrawQuestStatusTab() { + ImGui::PushItemWidth(ImGui::GetFontSize() * 6); + + for (int32_t i = QUEST_MEDALLION_FOREST; i < QUEST_MEDALLION_LIGHT + 1; i++) { + if (i != QUEST_MEDALLION_FOREST) { + ImGui::SameLine(); + } + DrawQuestItemButton(i); + } + + for (int32_t i = QUEST_KOKIRI_EMERALD; i < QUEST_ZORA_SAPPHIRE + 1; i++) { + if (i != QUEST_KOKIRI_EMERALD) { + ImGui::SameLine(); + } + DrawQuestItemButton(i); + } + + // Put Stone of Agony and Gerudo Card on the same line with a little space between them + ImGui::SameLine(); + ImGui::Dummy(ImVec2(20, 0)); + + ImGui::SameLine(); + DrawQuestItemButton(QUEST_STONE_OF_AGONY); + + ImGui::SameLine(); + DrawQuestItemButton(QUEST_GERUDO_CARD); + + for (const SongMapEntry& entry : songMapping) { + if ((entry.id != QUEST_SONG_MINUET) && (entry.id != QUEST_SONG_LULLABY)) { + ImGui::SameLine(); + } + + uint32_t bitMask = 1 << entry.id; + bool hasQuestItem = (bitMask & gSaveContext.inventory.questItems) != 0; + ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0, 0, 0, 0)); + if (ImGui::ImageButton(SohImGui::GetTextureByName(hasQuestItem ? entry.name : entry.nameFaded), + ImVec2(16.0f, 24.0f), ImVec2(0, 0), ImVec2(1, 1), 0)) { + if (hasQuestItem) { + gSaveContext.inventory.questItems &= ~bitMask; + } else { + gSaveContext.inventory.questItems |= bitMask; + } + } + ImGui::PopStyleColor(); + SetLastItemHoverText(SohUtils::GetQuestItemName(entry.id)); + } + + ImGui::InputScalar("GS Count", ImGuiDataType_S16, &gSaveContext.inventory.gsTokens); + InsertHelpHoverText("Number of gold skulltula tokens aquired"); + + uint32_t bitMask = 1 << QUEST_SKULL_TOKEN; + bool gsUnlocked = (bitMask & gSaveContext.inventory.questItems) != 0; + if (ImGui::Checkbox("GS unlocked", &gsUnlocked)) { + if (gsUnlocked) { + gSaveContext.inventory.questItems |= bitMask; + } else { + gSaveContext.inventory.questItems &= ~bitMask; + } + } + InsertHelpHoverText("If unlocked, enables showing the gold skulltula count in the quest status menu"); + + int32_t pohCount = (gSaveContext.inventory.questItems & 0xF0000000) >> 28; + if (ImGui::BeginCombo("PoH count", std::to_string(pohCount).c_str())) { + for (int32_t i = 0; i < 4; i++) { + if (ImGui::Selectable(std::to_string(i).c_str(), pohCount == i)) { + gSaveContext.inventory.questItems &= ~0xF0000000; + gSaveContext.inventory.questItems |= (i << 28); + } + } + ImGui::EndCombo(); + } + InsertHelpHoverText("The number of pieces of heart acquired towards the next heart container"); + + DrawGroupWithBorder([&]() { + ImGui::Text("Dungeon Items"); + + static int32_t dungeonItemsScene = SCENE_YDAN; + ImGui::PushItemWidth(-ImGui::GetWindowWidth() * 0.35f); + if (ImGui::BeginCombo("##DungeonSelect", SohUtils::GetSceneName(dungeonItemsScene).c_str())) { + for (int32_t dungeonIndex = SCENE_YDAN; dungeonIndex < SCENE_BDAN_BOSS + 1; dungeonIndex++) { + if (ImGui::Selectable(SohUtils::GetSceneName(dungeonIndex).c_str(), + dungeonIndex == dungeonItemsScene)) { + dungeonItemsScene = dungeonIndex; + } + } + + ImGui::EndCombo(); + } + ImGui::PopItemWidth(); + + DrawDungeonItemButton(ITEM_KEY_BOSS, dungeonItemsScene); + ImGui::SameLine(); + DrawDungeonItemButton(ITEM_COMPASS, dungeonItemsScene); + ImGui::SameLine(); + DrawDungeonItemButton(ITEM_DUNGEON_MAP, dungeonItemsScene); + + if (dungeonItemsScene != SCENE_BDAN_BOSS) { + float lineHeight = ImGui::GetTextLineHeightWithSpacing(); + ImGui::Image(SohImGui::GetTextureByName(itemMapping[ITEM_KEY_SMALL].name), ImVec2(lineHeight, lineHeight)); + ImGui::SameLine(); + ImGui::InputScalar("##Keys", ImGuiDataType_S8, gSaveContext.inventory.dungeonKeys + dungeonItemsScene); + } else { + // dungeonItems is size 20 but dungeonKeys is size 19, so there are no keys for the last scene (Barinade's Lair) + ImGui::Text("Barinade's Lair does not have small keys"); + } + }); + + ImGui::PopItemWidth(); +} + +void DrawSaveEditor(bool& open) { + if (!open) { + return; + } + + ImGui::SetNextWindowSize(ImVec2(520, 600), ImGuiCond_FirstUseEver); + if (!ImGui::Begin("Save Editor", &open)) { + ImGui::End(); + return; + } + 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(); + DrawInfoTab(); 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(); - } - } - + DrawInventoryTab(); 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; - } - } + DrawFlagsTab(); + ImGui::EndTabItem(); + } - 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 + if (ImGui::BeginTabItem("Equipment")) { + DrawEquipmentTab(); + ImGui::EndTabItem(); + } + if (ImGui::BeginTabItem("Quest Status")) { + DrawQuestStatusTab(); ImGui::EndTabItem(); } @@ -523,5 +1125,16 @@ void InitSaveEditor() { // Load item icons into ImGui for (const auto& entry : itemMapping) { SohImGui::LoadResource(entry.second.name, entry.second.texturePath); + SohImGui::LoadResource(entry.second.nameFaded, entry.second.texturePath, ImVec4(1, 1, 1, 0.3f)); + } + for (const auto& entry : questMapping) { + SohImGui::LoadResource(entry.second.name, entry.second.texturePath); + SohImGui::LoadResource(entry.second.nameFaded, entry.second.texturePath, ImVec4(1, 1, 1, 0.3f)); + } + for (const auto& entry : songMapping) { + SohImGui::LoadResource(entry.name, gSongNoteTex, entry.color); + ImVec4 fadedCol = entry.color; + fadedCol.w = 0.3f; + SohImGui::LoadResource(entry.nameFaded, gSongNoteTex, fadedCol); } } diff --git a/soh/soh/util.cpp b/soh/soh/util.cpp new file mode 100644 index 000000000..7a5d0205d --- /dev/null +++ b/soh/soh/util.cpp @@ -0,0 +1,314 @@ +#include "util.h" + +#include + +std::vector sceneNames = { + "Inside the 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", + "Ganon's Tower", + "Gerudo Training Ground", + "Thieves' Hideout", + "Inside Ganon's Castle", + "Ganon's Tower (Collapsing)", + "Inside Ganon's Castle (Collapsing)", + "Treasure Box Shop", + "Gohma's Lair", + "King Dodongo's Lair", + "Barinade's Lair", + "Phantom Ganon's Lair", + "Volvagia's Lair", + "Morpha's Lair", + "Twinrova's Lair & Nabooru's Mini-Boss Room", + "Bongo Bongo's Lair", + "Ganondorf's Lair", + "Tower Collapse Exterior", + "Market Entrance (Child - Day)", + "Market Entrance (Child - Night)", + "Market Entrance (Ruins)", + "Back Alley (Child - Day)", + "Back Alley (Child - Night)", + "Market (Child - Day)", + "Market (Child - Night)", + "Market (Ruins)", + "Temple of Time Exterior (Child - Day)", + "Temple of Time Exterior (Child - Night)", + "Temple of Time Exterior (Ruins)", + "Know-It-All Brothers' House", + "House of Twins", + "Mido's House", + "Saria's House", + "Carpenter Boss's House", + "Back Alley House (Man in Green)", + "Bazaar", + "Kokiri Shop", + "Goron Shop", + "Zora Shop", + "Kakariko Potion Shop", + "Market Potion Shop", + "Bombchu Shop", + "Happy Mask Shop", + "Link's House", + "Back Alley House (Dog Lady)", + "Stable", + "Impa's House", + "Lakeside Laboratory", + "Carpenters' Tent", + "Gravekeeper's Hut", + "Great Fairy's Fountain (Upgrades)", + "Fairy's Fountain", + "Great Fairy's Fountain (Spells)", + "Grottos", + "Grave (Redead)", + "Grave (Fairy's Fountain)", + "Royal Family's Tomb", + "Shooting Gallery", + "Temple of Time", + "Chamber of the Sages", + "Castle Hedge Maze (Day)", + "Castle Hedge Maze (Night)", + "Cutscene Map", + "Damp's Grave & Windmill", + "Fishing Pond", + "Castle Courtyard", + "Bombchu Bowling Alley", + "Ranch House & Silo", + "Guard House", + "Granny's Potion Shop", + "Ganon's Tower Collapse & Battle Arena", + "House of Skulltula", + "Spot 00 - Hyrule Field", + "Spot 01 - Kakariko Village", + "Spot 02 - Graveyard", + "Spot 03 - Zora's River", + "Spot 04 - Kokiri Forest", + "Spot 05 - Sacred Forest Meadow", + "Spot 06 - Lake Hylia", + "Spot 07 - Zora's Domain", + "Spot 08 - Zora's Fountain", + "Spot 09 - Gerudo Valley", + "Spot 10 - Lost Woods", + "Spot 11 - Desert Colossus", + "Spot 12 - Gerudo's Fortress", + "Spot 13 - Haunted Wasteland", + "Spot 15 - Hyrule Castle", + "Spot 16 - Death Mountain Trail", + "Spot 17 - Death Mountain Crater", + "Spot 18 - Goron City", + "Spot 20 - Lon Lon Ranch", + "Ganon's Castle Exterior", + "Jungle Gym", + "Ganondorf Test Room", + "Depth Test", + "Stalfos Mini-Boss Room", + "Stalfos Boss Room", + "Sutaru", + "Castle Hedge Maze (Early)", + "Sasa Test", + "Treasure Chest Room", +}; + +std::vector itemNames = { + "Deku Stick", + "Deku Nut", + "Bomb", + "Fairy Bow", + "Fire Arrow", + "Din's Fire", + "Fairy Slingshot", + "Fairy Ocarina", + "Ocarina of Time", + "Bombchu", + "Hookshot", + "Longshot", + "Ice Arrow", + "Farore's Wind", + "Boomerang", + "Lens of Truth", + "Magic Bean", + "Megaton Hammer", + "Light Arrow", + "Nayru's Love", + "Empty Bottle", + "Red Potion", + "Green Potion", + "Blue Potion", + "Bottled Fairy", + "Fish", + "Lon Lon Milk & Bottle", + "Ruto's Letter", + "Blue Fire", + "Bug", + "Big Poe", + "Lon Lon Milk (Half)", + "Poe", + "Weird Egg", + "Chicken", + "Zelda's Letter", + "Keaton Mask", + "Skull Mask", + "Spooky Mask", + "Bunny Hood", + "Goron Mask", + "Zora Mask", + "Gerudo Mask", + "Mask of Truth", + "SOLD OUT", + "Pocket Egg", + "Pocket Cucco", + "Cojiro", + "Odd Mushroom", + "Odd Potion", + "Poacher's Saw", + "Goron's Sword (Broken)", + "Prescription", + "Eyeball Frog", + "Eye Drops", + "Claim Check", + "Fairy Bow & Fire Arrow", + "Fairy Bow & Ice Arrow", + "Fairy Bow & Light Arrow", + "Kokiri Sword", + "Master Sword", + "Giant's Knife & Biggoron's Sword", + "Deku Shield", + "Hylian Shield", + "Mirror Shield", + "Kokiri Tunic", + "Goron Tunic", + "Zora Tunic", + "Kokiri Boots", + "Iron Boots", + "Hover Boots", + "Bullet Bag (30)", + "Bullet Bag (40)", + "Bullet Bag (50)", + "Quiver (30)", + "Big Quiver (40)", + "Biggest Quiver (50)", + "Bomb Bag (20)", + "Big Bomb Bag (30)", + "Biggest Bomb Bag (40)", + "Goron's Bracelet", + "Silver Gauntlets", + "Golden Gauntlets", + "Silver Scale", + "Golden Scale", + "Giant's Knife (Broken)", + "Adult's Wallet", + "Giant's Wallet", + "Deku Seeds (5)", + "Fishing Pole", + "Minuet of Forest", + "Bolero of Fire", + "Serenade of Water", + "Requiem of Spirit", + "Nocturne of Shadow", + "Prelude of Light", + "Zelda's Lullaby", + "Epona's Song", + "Saria's Song", + "Sun's Song", + "Song of Time", + "Song of Storms", + "Forest Medallion", + "Fire Medallion", + "Water Medallion", + "Spirit Medallion", + "Shadow Medallion", + "Light Medallion", + "Kokiri's Emerald", + "Goron's Ruby", + "Zora's Sapphire", + "Stone of Agony", + "Gerudo's Card", + "Gold Skulltula Token", + "Heart Container", + "Piece of Heart [?]", + "Big Key", + "Compass", + "Dungeon Map", + "Small Key", + "Small Magic Jar", + "Large Magic Jar", + "Piece of Heart", + "[Removed]", + "[Removed]", + "[Removed]", + "[Removed]", + "[Removed]", + "[Removed]", + "[Removed]", + "Lon Lon Milk", + "Recovery Heart", + "Green Rupee", + "Blue Rupee", + "Red Rupee", + "Purple Rupee", + "Huge Rupee", + "[Removed]", + "Deku Sticks (5)", + "Deku Sticks (10)", + "Deku Nuts (5)", + "Deku Nuts (10)", + "Bombs (5)", + "Bombs (10)", + "Bombs (20)", + "Bombs (30)", + "Arrows (Small)", + "Arrows (Medium)", + "Arrows (Large)", + "Deku Seeds (30)", + "Bombchu (5)", + "Bombchu (20)", + "Deku Stick Upgrade (20)", + "Deku Stick Upgrade (30)", + "Deku Nut Upgrade (30)", + "Deku Nut Upgrade (40)", +}; + +std::vector questItemNames = { + "Forest Medallion", + "Fire Medallion", + "Water Medallion", + "Spirit Medallion", + "Shadow Medallion", + "Light Medallion", + "Minuet of Forest", + "Bolero of Fire", + "Serenade of Water", + "Requiem of Spirit", + "Nocturne of Shadow", + "Prelude of Light", + "Zelda's Lullaby", + "Epona's Song", + "Saria's Song", + "Sun's Song", + "Song of Time", + "Song of Storms", + "Kokiri's Emerald", + "Goron's Ruby", + "Zora's Sapphire", + "Stone of Agony", + "Gerudo's Card", + "Gold Skulltula Token", +}; + +const std::string& SohUtils::GetSceneName(int32_t scene) { + return sceneNames[scene]; +} + +const std::string& SohUtils::GetItemName(int32_t item) { + return itemNames[item]; +} + +const std::string& SohUtils::GetQuestItemName(int32_t item) { + return questItemNames[item]; +} diff --git a/soh/soh/util.h b/soh/soh/util.h new file mode 100644 index 000000000..9fd806f18 --- /dev/null +++ b/soh/soh/util.h @@ -0,0 +1,11 @@ +#pragma once +#include +#include + +namespace SohUtils { + const std::string& GetSceneName(int32_t scene); + + const std::string& GetItemName(int32_t item); + + const std::string& GetQuestItemName(int32_t item); +} // namespace SohUtils From fe6dbd2a5b38e63db5fde84091a68803892cc0f1 Mon Sep 17 00:00:00 2001 From: Sirius902 <10891979+Sirius902@users.noreply.github.com> Date: Sun, 17 Apr 2022 08:26:49 -0700 Subject: [PATCH 6/6] MM Bunny Hood enhancement (#181) Allow bunny hood in boss rooms Use math instead of array Allow other masks with enhancement because why not --- libultraship/libultraship/GameSettings.cpp | 4 +++ libultraship/libultraship/GameSettings.h | 1 + libultraship/libultraship/SohImGuiImpl.cpp | 7 ++++- soh/src/code/z_parameter.c | 6 ++++- .../actors/ovl_player_actor/z_player.c | 27 +++++++++++++++---- 5 files changed, 38 insertions(+), 7 deletions(-) diff --git a/libultraship/libultraship/GameSettings.cpp b/libultraship/libultraship/GameSettings.cpp index 678555c7d..1b8d473b2 100644 --- a/libultraship/libultraship/GameSettings.cpp +++ b/libultraship/libultraship/GameSettings.cpp @@ -61,6 +61,9 @@ namespace Game { Settings.enhancements.minimal_ui = stob(Conf[EnhancementSection]["minimal_ui"]); CVar_SetS32("gMinimalUI", Settings.enhancements.minimal_ui); + Settings.enhancements.mm_bunny_hood = stob(Conf[EnhancementSection]["mm_bunny_hood"]); + CVar_SetS32("gMMBunnyHood", Settings.enhancements.mm_bunny_hood); + // Audio Settings.audio.master = Ship::stof(Conf[AudioSection]["master"]); CVar_SetFloat("gGameMasterVolume", Settings.audio.master); @@ -152,6 +155,7 @@ namespace Game { Conf[EnhancementSection]["disable_lod"] = std::to_string(Settings.enhancements.disable_lod); Conf[EnhancementSection]["animated_pause_menu"] = std::to_string(Settings.enhancements.animated_pause_menu); Conf[EnhancementSection]["minimal_ui"] = std::to_string(Settings.enhancements.minimal_ui); + Conf[EnhancementSection]["mm_bunny_hood"] = std::to_string(Settings.enhancements.mm_bunny_hood); // Controllers Conf[ControllerSection]["gyro_sensitivity"] = std::to_string(Settings.controller.gyro_sensitivity); diff --git a/libultraship/libultraship/GameSettings.h b/libultraship/libultraship/GameSettings.h index 17079c9bf..340e93d26 100644 --- a/libultraship/libultraship/GameSettings.h +++ b/libultraship/libultraship/GameSettings.h @@ -24,6 +24,7 @@ struct SoHConfigType { bool disable_lod = false; bool animated_pause_menu = false; bool minimal_ui = false; + bool mm_bunny_hood = false; } enhancements; // Controller diff --git a/libultraship/libultraship/SohImGuiImpl.cpp b/libultraship/libultraship/SohImGuiImpl.cpp index b7e6e01ab..93dcd2492 100644 --- a/libultraship/libultraship/SohImGuiImpl.cpp +++ b/libultraship/libultraship/SohImGuiImpl.cpp @@ -413,6 +413,11 @@ namespace SohImGui { needs_save = true; } + if (ImGui::Checkbox("MM Bunny Hood", &Game::Settings.enhancements.mm_bunny_hood)) { + CVar_SetS32("gMMBunnyHood", Game::Settings.enhancements.mm_bunny_hood); + needs_save = true; + } + ImGui::Text("Graphics"); ImGui::Separator(); @@ -639,4 +644,4 @@ namespace SohImGui { ImTextureID GetTextureByName(const std::string& name) { return GetTextureByID(DefaultAssets[name]->textureId); } -} \ No newline at end of file +} diff --git a/soh/src/code/z_parameter.c b/soh/src/code/z_parameter.c index 1653cdaaf..dc96ba133 100644 --- a/soh/src/code/z_parameter.c +++ b/soh/src/code/z_parameter.c @@ -919,7 +919,11 @@ void func_80083108(GlobalContext* globalCtx) { if (interfaceCtx->restrictions.tradeItems != 0) { for (i = 1; i < 4; i++) { - if ((gSaveContext.equips.buttonItems[i] >= ITEM_WEIRD_EGG) && + if ((CVar_GetS32("gMMBunnyHood", 0) != 0) + && (gSaveContext.equips.buttonItems[i] >= ITEM_MASK_KEATON) + && (gSaveContext.equips.buttonItems[i] <= ITEM_MASK_TRUTH)) { + gSaveContext.buttonStatus[i] = BTN_ENABLED; + } else if ((gSaveContext.equips.buttonItems[i] >= ITEM_WEIRD_EGG) && (gSaveContext.equips.buttonItems[i] <= ITEM_CLAIM_CHECK)) { if (gSaveContext.buttonStatus[i] == BTN_ENABLED) { sp28 = 1; diff --git a/soh/src/overlays/actors/ovl_player_actor/z_player.c b/soh/src/overlays/actors/ovl_player_actor/z_player.c index a2a810175..734ba015e 100644 --- a/soh/src/overlays/actors/ovl_player_actor/z_player.c +++ b/soh/src/overlays/actors/ovl_player_actor/z_player.c @@ -1875,10 +1875,20 @@ void func_80833DF8(Player* this, GlobalContext* globalCtx) { s32 i; if (this->currentMask != PLAYER_MASK_NONE) { - maskActionParam = this->currentMask - 1 + PLAYER_AP_MASK_KEATON; - if (!func_80833C98(C_BTN_ITEM(0), maskActionParam) && !func_80833C98(C_BTN_ITEM(1), maskActionParam) && - !func_80833C98(C_BTN_ITEM(2), maskActionParam)) { - this->currentMask = PLAYER_MASK_NONE; + if (CVar_GetS32("gMMBunnyHood", 0) != 0) { + s32 maskItem = this->currentMask - PLAYER_MASK_KEATON + ITEM_MASK_KEATON; + + if (gSaveContext.equips.buttonItems[0] != maskItem && gSaveContext.equips.buttonItems[1] != maskItem && + gSaveContext.equips.buttonItems[2] != maskItem && gSaveContext.equips.buttonItems[3] != maskItem) { + this->currentMask = PLAYER_MASK_NONE; + func_808328EC(this, NA_SE_PL_CHANGE_ARMS); + } + } else { + maskActionParam = this->currentMask - 1 + PLAYER_AP_MASK_KEATON; + if (!func_80833C98(C_BTN_ITEM(0), maskActionParam) && !func_80833C98(C_BTN_ITEM(1), maskActionParam) && + !func_80833C98(C_BTN_ITEM(2), maskActionParam)) { + this->currentMask = PLAYER_MASK_NONE; + } } } @@ -5942,7 +5952,11 @@ void func_8083DFE0(Player* this, f32* arg1, s16* arg2) { s16 yawDiff = this->currentYaw - *arg2; if (this->swordState == 0) { - this->linearVelocity = CLAMP(this->linearVelocity, -(R_RUN_SPEED_LIMIT / 100.0f), (R_RUN_SPEED_LIMIT / 100.0f)); + float maxSpeed = R_RUN_SPEED_LIMIT / 100.0f; + if (CVar_GetS32("gMMBunnyHood", 0) != 0 && this->currentMask == PLAYER_MASK_BUNNY) { + maxSpeed *= 1.5f; + } + this->linearVelocity = CLAMP(this->linearVelocity, -maxSpeed, maxSpeed); } if (ABS(yawDiff) > 0x6000) { @@ -7523,6 +7537,9 @@ void func_80842180(Player* this, GlobalContext* globalCtx) { func_80837268(this, &sp2C, &sp2A, 0.018f, globalCtx); if (!func_8083C484(this, &sp2C, &sp2A)) { + if (CVar_GetS32("gMMBunnyHood", 0) != 0 && this->currentMask == PLAYER_MASK_BUNNY) { + sp2C *= 1.5f; + } func_8083DF68(this, sp2C, sp2A); func_8083DDC8(this, globalCtx);