#include "ImGuiConsole.hpp" #include "../version.h" #include "MP1/MP1.hpp" #include "Runtime/CStateManager.hpp" #include "Runtime/GameGlobalObjects.hpp" #include "Runtime/ImGuiEntitySupport.hpp" #include "Runtime/World/CPlayer.hpp" #include "ImGuiEngine.hpp" #include "magic_enum.hpp" #ifdef NATIVEFILEDIALOG_SUPPORTED #include "nfd.h" #endif #include #include namespace ImGui { // Internal functions void ClearIniSettings(); } // namespace ImGui namespace aurora::gfx { extern std::atomic_uint32_t queuedPipelines; extern std::atomic_uint32_t createdPipelines; } // namespace aurora::gfx #include "TCastTo.hpp" // Generated file, do not modify include path namespace metaforce { static logvisor::Module Log{"Console"}; std::array ImGuiConsole::entities; std::set ImGuiConsole::inspectingEntities; ImGuiPlayerLoadouts ImGuiConsole::loadouts; void ImGuiStringViewText(std::string_view text) { // begin()/end() do not work on MSVC ImGui::TextUnformatted(text.data(), text.data() + text.size()); } void ImGuiTextCenter(std::string_view text) { ImGui::NewLine(); float fontSize = ImGui::CalcTextSize(text.data(), text.data() + text.size()).x; ImGui::SameLine(ImGui::GetWindowSize().x / 2 - fontSize + fontSize / 2); ImGuiStringViewText(text); } bool ImGuiButtonCenter(std::string_view text) { ImGui::NewLine(); float fontSize = ImGui::CalcTextSize(text.data(), text.data() + text.size()).x; fontSize += ImGui::GetStyle().FramePadding.x; ImGui::SameLine(ImGui::GetWindowSize().x / 2 - fontSize + fontSize / 2); return ImGui::Button(text.data()); } static std::unordered_map> dummyWorlds; static std::unordered_map> stringTables; std::string ImGuiLoadStringTable(CAssetId stringId, int idx) { if (!stringId.IsValid()) { return ""s; } if (!stringTables.contains(stringId)) { stringTables[stringId] = g_SimplePool->GetObj(SObjectTag{SBIG('STRG'), stringId}); } return CStringExtras::ConvertToUTF8(stringTables[stringId].GetObj()->GetString(idx)); } static bool ContainsCaseInsensitive(std::string_view str, std::string_view val) { return std::search(str.begin(), str.end(), val.begin(), val.end(), [](char ch1, char ch2) { return std::toupper(ch1) == std::toupper(ch2); }) != str.end(); } static std::vector> ListWorlds() { std::vector> worlds; for (const auto& pak : g_ResFactory->GetResLoader()->GetPaks()) { if (!pak->IsWorldPak()) { continue; } CAssetId worldId = pak->GetMLVLId(); if (!dummyWorlds.contains(worldId)) { dummyWorlds[worldId] = std::make_unique(worldId, false); } auto& world = dummyWorlds[worldId]; bool complete = world->ICheckWorldComplete(); if (!complete) { continue; } CAssetId stringId = world->IGetStringTableAssetId(); if (!stringId.IsValid()) { continue; } worlds.emplace_back(ImGuiLoadStringTable(stringId, 0), worldId); } return worlds; } static std::vector> ListAreas(CAssetId worldId) { std::vector> areas; const auto& world = dummyWorlds[worldId]; for (TAreaId i = 0; i < world->IGetAreaCount(); ++i) { const auto* area = world->IGetAreaAlways(i); if (area == nullptr) { continue; } CAssetId stringId = area->IGetStringTableAssetId(); if (!stringId.IsValid()) { continue; } areas.emplace_back(ImGuiLoadStringTable(stringId, 0), i); } return areas; } static void Warp(const CAssetId worldId, TAreaId aId) { g_GameState->SetCurrentWorldId(worldId); g_GameState->GetWorldTransitionManager()->DisableTransition(); if (aId >= g_GameState->CurrentWorldState().GetLayerState()->GetAreaCount()) { aId = 0; } g_GameState->CurrentWorldState().SetAreaId(aId); g_Main->SetFlowState(EClientFlowStates::None); if (g_StateManager != nullptr) { g_StateManager->SetWarping(true); g_StateManager->SetShouldQuitGame(true); } else { // TODO(encounter): warp from menu? } } void ImGuiConsole::ShowMenuGame() { m_paused = g_Main->IsPaused(); if (ImGui::MenuItem("Paused", "F5", &m_paused)) { g_Main->SetPaused(m_paused); } if (ImGui::MenuItem("Step Frame", "F6", &m_stepFrame, m_paused)) { g_Main->SetPaused(false); } if (ImGui::BeginMenu("Warp", g_StateManager != nullptr && g_ResFactory != nullptr && g_ResFactory->GetResLoader() != nullptr)) { for (const auto& world : ListWorlds()) { if (ImGui::BeginMenu(world.first.c_str())) { for (const auto& area : ListAreas(world.second)) { if (ImGui::MenuItem(area.first.c_str())) { Warp(world.second, area.second); } } ImGui::EndMenu(); } } ImGui::EndMenu(); } if (ImGui::MenuItem("Quit", "Alt+F4")) { g_Main->Quit(); } } void ImGuiConsole::LerpDebugColor(CActor* act) { if (!act->m_debugSelected && !act->m_debugHovered) { act->m_debugAddColorTime = 0.f; act->m_debugAddColor = zeus::skClear; return; } act->m_debugAddColorTime += ImGui::GetIO().DeltaTime; float lerp = act->m_debugAddColorTime; if (lerp > 2.f) { lerp = 0.f; act->m_debugAddColorTime = 0.f; } else if (lerp > 1.f) { lerp = 2.f - lerp; } act->m_debugAddColor = zeus::CColor::lerp(zeus::skClear, zeus::skBlue, lerp); } void ImGuiConsole::UpdateEntityEntries() { CObjectList& list = g_StateManager->GetAllObjectList(); s16 uid = list.GetFirstObjectIndex(); while (uid != -1) { ImGuiEntityEntry& entry = ImGuiConsole::entities[uid]; if (entry.uid == kInvalidUniqueId || entry.ent == nullptr) { CEntity* ent = list.GetObjectByIndex(uid); entry.uid = ent->GetUniqueId(); entry.ent = ent; entry.type = ent->ImGuiType(); entry.name = ent->GetName(); entry.isActor = TCastToPtr(ent).IsValid(); } else { entry.active = entry.ent->GetActive(); } if (entry.isActor) { LerpDebugColor(entry.AsActor()); } entry.ent->m_debugHovered = false; uid = list.GetNextObjectIndex(uid); } } void ImGuiConsole::BeginEntityRow(const ImGuiEntityEntry& entry) { ImGui::PushID(entry.uid.Value()); ImGui::TableNextRow(); bool isActive = entry.active; ImVec4 textColor = ImGui::GetStyleColorVec4(ImGuiCol_Text); if (!isActive) { textColor.w = 0.5f; } ImGui::PushStyleColor(ImGuiCol_Text, textColor); if (ImGui::TableNextColumn()) { auto text = fmt::format(FMT_STRING("0x{:04X}"), entry.uid.Value()); ImGui::Selectable(text.c_str(), &entry.ent->m_debugSelected, ImGuiSelectableFlags_SpanAllColumns | ImGuiSelectableFlags_AllowItemOverlap); if (ImGui::IsItemHovered()) { entry.ent->m_debugHovered = true; } if (ImGui::BeginPopupContextItem(text.c_str())) { ImGui::PopStyleColor(); if (ImGui::MenuItem(isActive ? "Deactivate" : "Activate")) { entry.ent->SetActive(!isActive); } if (ImGui::MenuItem("Highlight", nullptr, &entry.ent->m_debugSelected)) { entry.ent->SetActive(!isActive); } // Only allow deletion if none of the objects are player related // The player objects will always be in the first 6 slots if (entry.uid.Value() > 6) { ImGui::Separator(); ImGui::PushStyleColor(ImGuiCol_Text, ImVec4{0.77f, 0.12f, 0.23f, 1.f}); if (ImGui::MenuItem("Delete")) { g_StateManager->FreeScriptObject(entry.uid); } ImGui::PopStyleColor(); } ImGui::EndPopup(); ImGui::PushStyleColor(ImGuiCol_Text, textColor); } } } void ImGuiConsole::EndEntityRow(const ImGuiEntityEntry& entry) { ImGui::PopStyleColor(); if (ImGui::TableNextColumn()) { if (ImGui::SmallButton("View")) { ImGuiConsole::inspectingEntities.insert(entry.uid); } } ImGui::PopID(); } static void RenderEntityColumns(const ImGuiEntityEntry& entry) { ImGuiConsole::BeginEntityRow(entry); if (ImGui::TableNextColumn()) { ImGuiStringViewText(entry.type); } if (ImGui::TableNextColumn()) { ImGuiStringViewText(entry.name); } ImGuiConsole::EndEntityRow(entry); } void ImGuiConsole::ShowInspectWindow(bool* isOpen) { float initialWindowSize = 400.f * aurora::get_window_size().scale; ImGui::SetNextWindowSize(ImVec2{initialWindowSize, initialWindowSize * 1.5f}, ImGuiCond_FirstUseEver); if (ImGui::Begin("Inspect", isOpen)) { CObjectList& list = g_StateManager->GetAllObjectList(); ImGui::Text("Objects: %d / %d", list.size(), kMaxEntities); ImGui::SameLine(); if (ImGui::SmallButton("Deselect all")) { for (auto* const ent : list) { ent->m_debugSelected = false; } } if (ImGui::Button("Clear")) { m_inspectFilterText.clear(); } ImGui::SameLine(); ImGui::InputText("Filter", &m_inspectFilterText); ImGui::Checkbox("Active", &m_inspectActiveOnly); ImGui::SameLine(); ImGui::Checkbox("Current area", &m_inspectCurrentAreaOnly); if (ImGui::BeginTable("Entities", 4, ImGuiTableFlags_Resizable | ImGuiTableFlags_Sortable | ImGuiTableFlags_RowBg | ImGuiTableFlags_BordersOuter | ImGuiTableFlags_BordersV | ImGuiTableFlags_ScrollY)) { ImGui::TableSetupColumn("ID", ImGuiTableColumnFlags_PreferSortAscending | ImGuiTableColumnFlags_DefaultSort | ImGuiTableColumnFlags_WidthFixed, 0, 'id'); ImGui::TableSetupColumn("Type", ImGuiTableColumnFlags_WidthFixed, 0, 'type'); ImGui::TableSetupColumn("Name", ImGuiTableColumnFlags_WidthStretch, 0, 'name'); ImGui::TableSetupColumn("", ImGuiTableColumnFlags_NoSort | ImGuiTableColumnFlags_WidthFixed | ImGuiTableColumnFlags_NoResize); ImGui::TableSetupScrollFreeze(0, 1); ImGui::TableHeadersRow(); ImGuiTableSortSpecs* sortSpecs = ImGui::TableGetSortSpecs(); bool hasSortSpec = sortSpecs != nullptr && // no multi-sort sortSpecs->SpecsCount == 1 && // We can skip sorting if we just want uid ascending, // since that's how we iterate over CObjectList (sortSpecs->Specs[0].ColumnUserID != 'id' || sortSpecs->Specs[0].SortDirection != ImGuiSortDirection_Ascending); if (!m_inspectFilterText.empty() || m_inspectActiveOnly || m_inspectCurrentAreaOnly || hasSortSpec) { std::vector sortedList; sortedList.reserve(list.size()); s16 uid = list.GetFirstObjectIndex(); auto currAreaId = kInvalidAreaId; CPlayer* player = nullptr; if (m_inspectCurrentAreaOnly && (player = g_StateManager->Player()) != nullptr) { currAreaId = player->GetAreaIdAlways(); } while (uid != -1) { ImGuiEntityEntry& entry = ImGuiConsole::entities[uid]; if ((!m_inspectActiveOnly || entry.active) && (!m_inspectCurrentAreaOnly || entry.ent->x4_areaId == currAreaId) && (m_inspectFilterText.empty() || ContainsCaseInsensitive(entry.type, m_inspectFilterText) || ContainsCaseInsensitive(entry.name, m_inspectFilterText))) { sortedList.push_back(uid); } uid = list.GetNextObjectIndex(uid); } if (hasSortSpec) { const auto& spec = sortSpecs->Specs[0]; if (spec.ColumnUserID == 'id') { if (spec.SortDirection == ImGuiSortDirection_Ascending) { // no-op } else { std::sort(sortedList.begin(), sortedList.end(), [&](s16 a, s16 b) { return a < b; }); } } else if (spec.ColumnUserID == 'name') { std::sort(sortedList.begin(), sortedList.end(), [&](s16 a, s16 b) { int compare = ImGuiConsole::entities[a].name.compare(ImGuiConsole::entities[b].name); return spec.SortDirection == ImGuiSortDirection_Ascending ? compare < 0 : compare > 0; }); } else if (spec.ColumnUserID == 'type') { std::sort(sortedList.begin(), sortedList.end(), [&](s16 a, s16 b) { int compare = ImGuiConsole::entities[a].type.compare(ImGuiConsole::entities[b].type); return spec.SortDirection == ImGuiSortDirection_Ascending ? compare < 0 : compare > 0; }); } } for (const auto& item : sortedList) { RenderEntityColumns(ImGuiConsole::entities[item]); } } else { // Render uid ascending s16 uid = list.GetFirstObjectIndex(); while (uid != -1) { RenderEntityColumns(ImGuiConsole::entities[uid]); uid = list.GetNextObjectIndex(uid); } } ImGui::EndTable(); } } ImGui::End(); } bool ImGuiConsole::ShowEntityInfoWindow(TUniqueId uid) { bool open = true; ImGuiEntityEntry& entry = ImGuiConsole::entities[uid.Value()]; if (entry.ent == nullptr) { return false; } auto name = fmt::format(FMT_STRING("{}##0x{:04X}"), !entry.name.empty() ? entry.name : entry.type, uid.Value()); if (ImGui::Begin(name.c_str(), &open, ImGuiWindowFlags_AlwaysAutoResize)) { ImGui::PushID(uid.Value()); entry.ent->ImGuiInspect(); ImGui::PopID(); } ImGui::End(); return open; } void ImGuiConsole::ShowConsoleVariablesWindow() { // For some reason the window shows up tiny without this float initialWindowSize = 350.f * aurora::get_window_size().scale; ImGui::SetNextWindowSize(ImVec2{initialWindowSize, initialWindowSize}, ImGuiCond_FirstUseEver); if (ImGui::Begin("Console Variables", &m_showConsoleVariablesWindow)) { if (ImGui::Button("Clear")) { m_cvarFiltersText.clear(); } ImGui::SameLine(); ImGui::InputText("Filter", &m_cvarFiltersText); auto cvars = m_cvarMgr.cvars(CVar::EFlags::Any & ~CVar::EFlags::Hidden); if (ImGui::Button("Reset to defaults")) { for (auto* cv : cvars) { if (cv->name() == "developer" || cv->name() == "cheats") { // don't reset developer or cheats to default continue; } CVarUnlocker l(cv); cv->fromLiteralToType(cv->defaultValue()); } } if (ImGui::BeginTable("ConsoleVariables", 2, ImGuiTableFlags_Resizable | ImGuiTableFlags_Sortable | ImGuiTableFlags_RowBg | ImGuiTableFlags_BordersOuter | ImGuiTableFlags_BordersV | ImGuiTableFlags_ScrollY)) { ImGui::TableSetupColumn("Name", ImGuiTableColumnFlags_PreferSortAscending | ImGuiTableColumnFlags_DefaultSort | ImGuiTableColumnFlags_WidthFixed, 0, 'name'); ImGui::TableSetupColumn("Value", ImGuiTableColumnFlags_WidthStretch, 0, 'val'); ImGui::TableSetupScrollFreeze(0, 1); ImGui::TableHeadersRow(); ImGuiTableSortSpecs* sortSpecs = ImGui::TableGetSortSpecs(); bool hasSortSpec = sortSpecs != nullptr && // no multi-sort sortSpecs->SpecsCount == 1; std::vector sortedList; sortedList.reserve(cvars.size()); for (auto* cvar : cvars) { if (!m_cvarFiltersText.empty()) { if (ContainsCaseInsensitive(magic_enum::enum_name(cvar->type()), m_cvarFiltersText) || ContainsCaseInsensitive(cvar->name(), m_cvarFiltersText)) { sortedList.push_back(cvar); } } else { sortedList.push_back(cvar); } } if (hasSortSpec) { const auto& spec = sortSpecs->Specs[0]; if (spec.ColumnUserID == 'name') { std::sort(sortedList.begin(), sortedList.end(), [&](CVar* a, CVar* b) { int compare = a->name().compare(b->name()); return spec.SortDirection == ImGuiSortDirection_Ascending ? compare < 0 : compare > 0; }); } else if (spec.ColumnUserID == 'val') { std::sort(sortedList.begin(), sortedList.end(), [&](CVar* a, CVar* b) { int compare = a->value().compare(b->value()); return spec.SortDirection == ImGuiSortDirection_Ascending ? compare < 0 : compare > 0; }); } for (auto* cv : sortedList) { ImGui::PushID(cv); ImGui::TableNextRow(); // Name if (ImGui::TableNextColumn()) { ImGuiStringViewText(cv->name()); if (ImGui::IsItemHovered() && !cv->rawHelp().empty()) { std::string sv(cv->rawHelp()); ImGui::SetTooltip("%s", sv.c_str()); } } // Value if (ImGui::TableNextColumn()) { switch (cv->type()) { case CVar::EType::Boolean: { bool b = cv->toBoolean(); if (ImGui::Checkbox("", &b)) { cv->fromBoolean(b); } break; } case CVar::EType::Real: { float f = cv->toReal(); if (ImGui::DragFloat("", &f)) { cv->fromReal(f); } break; } case CVar::EType::Signed: { std::array i{cv->toSigned()}; if (ImGui::DragScalar("", ImGuiDataType_S32, i.data(), i.size())) { cv->fromInteger(i[0]); } break; } case CVar::EType::Unsigned: { std::array i{cv->toUnsigned()}; if (ImGui::DragScalar("", ImGuiDataType_U32, i.data(), i.size())) { cv->fromInteger(i[0]); } break; } case CVar::EType::Literal: { char buf[4096]; strcpy(buf, cv->value().c_str()); if (ImGui::InputText("", buf, 4096, ImGuiInputTextFlags_EnterReturnsTrue)) { cv->fromLiteral(buf); } break; } case CVar::EType::Vec2f: { auto vec = cv->toVec2f(); std::array scalars = {vec.x(), vec.y()}; if (ImGui::DragScalarN("", ImGuiDataType_Float, scalars.data(), scalars.size(), 0.1f)) { vec.x() = scalars[0]; vec.y() = scalars[1]; cv->fromVec2f(vec); } break; } case CVar::EType::Vec2d: { auto vec = cv->toVec2d(); std::array scalars = {vec.x(), vec.y()}; if (ImGui::DragScalarN("", ImGuiDataType_Double, scalars.data(), scalars.size(), 0.1f)) { vec.x() = scalars[0]; vec.y() = scalars[1]; cv->fromVec2d(vec); } break; } case CVar::EType::Vec3f: { auto vec = cv->toVec3f(); std::array scalars = {vec.x(), vec.y(), vec.z()}; if (cv->isColor()) { if (ImGui::ColorEdit3("", scalars.data())) { vec.x() = scalars[0]; vec.y() = scalars[1]; vec.z() = scalars[2]; cv->fromVec3f(vec); } } else if (ImGui::DragScalarN("", ImGuiDataType_Float, scalars.data(), scalars.size(), 0.1f)) { vec.x() = scalars[0]; vec.y() = scalars[1]; vec.z() = scalars[2]; cv->fromVec3f(vec); } break; } case CVar::EType::Vec3d: { auto vec = cv->toVec3d(); std::array scalars = {vec.x(), vec.y(), vec.z()}; if (cv->isColor()) { std::array color{static_cast(scalars[0]), static_cast(scalars[1]), static_cast(scalars[2])}; if (ImGui::ColorEdit3("", color.data())) { vec.x() = scalars[0]; vec.y() = scalars[1]; vec.z() = scalars[2]; cv->fromVec3d(vec); } } else if (ImGui::DragScalarN("", ImGuiDataType_Double, scalars.data(), scalars.size(), 0.1f)) { vec.x() = scalars[0]; vec.y() = scalars[1]; vec.z() = scalars[2]; cv->fromVec3d(vec); } break; } case CVar::EType::Vec4f: { auto vec = cv->toVec4f(); std::array scalars = {vec.x(), vec.y(), vec.z(), vec.w()}; if (cv->isColor()) { if (ImGui::ColorEdit4("", scalars.data())) { vec.x() = scalars[0]; vec.y() = scalars[1]; vec.z() = scalars[2]; vec.w() = scalars[2]; cv->fromVec4f(vec); } } else if (ImGui::DragScalarN("", ImGuiDataType_Float, scalars.data(), scalars.size(), 0.1f)) { vec.x() = scalars[0]; vec.y() = scalars[1]; vec.z() = scalars[2]; vec.w() = scalars[2]; cv->fromVec4f(vec); } break; } case CVar::EType::Vec4d: { auto vec = cv->toVec4d(); std::array scalars = {vec.x(), vec.y(), vec.z(), vec.w()}; if (cv->isColor()) { std::array color{static_cast(scalars[0]), static_cast(scalars[1]), static_cast(scalars[2]), static_cast(scalars[3])}; if (ImGui::ColorEdit4("", color.data())) { vec.x() = scalars[0]; vec.y() = scalars[1]; vec.z() = scalars[2]; vec.w() = scalars[2]; cv->fromVec4d(vec); } } else if (ImGui::DragScalarN("", ImGuiDataType_Double, scalars.data(), scalars.size(), 0.1f)) { vec.x() = scalars[0]; vec.y() = scalars[1]; vec.z() = scalars[2]; vec.w() = scalars[2]; cv->fromVec4d(vec); } break; } default: ImGui::Text("lawl wut? Please contact a developer, your copy of Metaforce is cursed!"); break; } if (ImGui::IsItemHovered()) { std::string sv(cv->defaultValue()); ImGui::SetTooltip("Default: %s", sv.c_str()); } } ImGui::PopID(); } } ImGui::EndTable(); } } ImGui::End(); } std::optional ImGuiConsole::ShowAboutWindow(bool canClose, std::string_view errorString, bool preLaunch) { std::optional result{}; // Center window ImVec2 center = ImGui::GetMainViewport()->GetCenter(); ImGui::SetNextWindowPos(center, canClose ? ImGuiCond_Appearing : ImGuiCond_Always, ImVec2(0.5f, 0.5f)); ImVec4& windowBg = ImGui::GetStyle().Colors[ImGuiCol_WindowBg]; ImGui::PushStyleColor(ImGuiCol_TitleBg, windowBg); ImGui::PushStyleColor(ImGuiCol_TitleBgActive, windowBg); bool* open = nullptr; ImGuiWindowFlags flags = ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoNav | ImGuiWindowFlags_NoSavedSettings; if (canClose) { open = &m_showAboutWindow; } else { flags |= ImGuiWindowFlags_NoBackground | ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoMove; } if (ImGui::Begin("About", open, flags)) { float iconSize = 128.f * aurora::get_window_size().scale; ImGui::SameLine(ImGui::GetWindowSize().x / 2 - iconSize + (iconSize / 2)); ImGui::Image(ImGuiEngine::metaforceIcon, ImVec2{iconSize, iconSize}); ImGui::PushFont(ImGuiEngine::fontLarge); ImGuiTextCenter("Metaforce"); ImGui::PopFont(); ImGuiTextCenter(METAFORCE_WC_DESCRIBE); const ImVec2& padding = ImGui::GetStyle().WindowPadding; ImGui::Dummy(padding); if (preLaunch) { #ifdef NATIVEFILEDIALOG_SUPPORTED if (ImGuiButtonCenter("Select Game Disc")) { nfdchar_t* outPath = nullptr; nfdresult_t nfdResult = NFD_OpenDialog(nullptr, nullptr, &outPath); if (nfdResult == NFD_OKAY) { result = outPath; free(outPath); } else if (nfdResult != NFD_CANCEL) { Log.report(logvisor::Error, FMT_STRING("nativefiledialog error: {}"), NFD_GetError()); } } ImGui::Dummy(padding); #endif } if (!errorString.empty()) { ImGui::PushStyleColor(ImGuiCol_Text, ImVec4{0.77f, 0.12f, 0.23f, 1.f}); ImGuiTextCenter(errorString); ImGui::PopStyleColor(); ImGui::Dummy(padding); } ImGuiTextCenter("2015-2022"); ImGui::BeginGroup(); ImGui::PushStyleColor(ImGuiCol_Text, IM_COL32(255, 255, 255, 200)); ImGuiStringViewText("Development & Research"); ImGui::PopStyleColor(); ImGuiStringViewText("Phillip Stephens (Antidote)"); ImGuiStringViewText("Jack Andersen (jackoalan)"); ImGuiStringViewText("Luke Street (encounter)"); ImGuiStringViewText("Lioncache"); ImGui::EndGroup(); ImGui::SameLine(); ImGui::BeginGroup(); ImGui::PushStyleColor(ImGuiCol_Text, IM_COL32(255, 255, 255, 200)); ImGuiStringViewText("Testing"); ImGui::PopStyleColor(); ImGuiStringViewText("Tom Lube"); ImGui::PushStyleColor(ImGuiCol_Text, IM_COL32(255, 255, 255, 200)); ImGuiStringViewText("Contributions"); ImGui::PopStyleColor(); ImGuiStringViewText("Darkszero (Profiling)"); ImGuiStringViewText("shio (Flamethrower)"); ImGui::EndGroup(); ImGui::Dummy(padding); ImGui::Separator(); if (ImGui::BeginTable("Version Info", 2, ImGuiTableFlags_BordersInnerV)) { ImGui::TableNextRow(); if (ImGui::TableNextColumn()) { ImGuiStringViewText("Branch"); } if (ImGui::TableNextColumn()) { ImGuiStringViewText(METAFORCE_WC_BRANCH); } ImGui::TableNextRow(); if (ImGui::TableNextColumn()) { ImGuiStringViewText("Revision"); } if (ImGui::TableNextColumn()) { ImGuiStringViewText(METAFORCE_WC_REVISION); } ImGui::TableNextRow(); if (ImGui::TableNextColumn()) { ImGuiStringViewText("Build"); } if (ImGui::TableNextColumn()) { ImGuiStringViewText(METAFORCE_DLPACKAGE); } ImGui::TableNextRow(); if (ImGui::TableNextColumn()) { ImGuiStringViewText("Date"); } if (ImGui::TableNextColumn()) { ImGuiStringViewText(METAFORCE_WC_DATE); } ImGui::TableNextRow(); if (ImGui::TableNextColumn()) { ImGuiStringViewText("Type"); } if (ImGui::TableNextColumn()) { ImGuiStringViewText(METAFORCE_BUILD_TYPE); } if (g_Main != nullptr) { ImGui::TableNextRow(); if (ImGui::TableNextColumn()) { ImGuiStringViewText("Game"); } if (ImGui::TableNextColumn()) { ImGuiStringViewText(g_Main->GetVersionString()); } } ImGui::EndTable(); } } ImGui::End(); ImGui::PopStyleColor(2); return result; } void ImGuiConsole::ShowDebugOverlay() { if (!m_frameCounter && !m_frameRate && !m_inGameTime && !m_roomTimer && !m_playerInfo && !m_areaInfo && !m_worldInfo && !m_randomStats && !m_resourceStats && !m_pipelineInfo) { return; } ImGuiIO& io = ImGui::GetIO(); ImGuiWindowFlags windowFlags = ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_NoFocusOnAppearing | ImGuiWindowFlags_NoNav; if (m_debugOverlayCorner != -1) { SetOverlayWindowLocation(m_debugOverlayCorner); windowFlags |= ImGuiWindowFlags_NoMove; } ImGui::SetNextWindowBgAlpha(0.65f); if (ImGui::Begin("Debug Overlay", nullptr, windowFlags)) { bool hasPrevious = false; if (m_frameCounter && g_StateManager != nullptr) { ImGuiStringViewText(fmt::format(FMT_STRING("Frame: {}\n"), g_StateManager->GetUpdateFrameIndex())); hasPrevious = true; } if (m_frameRate) { if (hasPrevious) { ImGui::Separator(); } hasPrevious = true; ImGuiStringViewText(fmt::format(FMT_STRING("FPS: {}\n"), metaforce::CGraphics::GetFPS())); } if (m_inGameTime && g_GameState != nullptr) { if (hasPrevious) { ImGui::Separator(); } hasPrevious = true; double igt = g_GameState->GetTotalPlayTime(); u32 ms = u64(igt * 1000) % 1000; auto pt = std::div(int(igt), 3600); ImGuiStringViewText( fmt::format(FMT_STRING("Play Time: {:02d}:{:02d}:{:02d}.{:03d}\n"), pt.quot, pt.rem / 60, pt.rem % 60, ms)); } if (m_roomTimer && g_StateManager != nullptr) { if (hasPrevious) { ImGui::Separator(); } hasPrevious = true; double igt = g_GameState->GetTotalPlayTime(); double currentRoomTime = igt - m_currentRoomStart; u32 curFrames = u32(std::round(u32(currentRoomTime * 60))); u32 lastFrames = u32(std::round(u32(m_lastRoomTime * 60))); ImGuiStringViewText(fmt::format(FMT_STRING("Room Time: {:7.3f} / {:5d} | Last Room:{:7.3f} / {:5d}\n"), currentRoomTime, curFrames, m_lastRoomTime, lastFrames)); } if (m_playerInfo && g_StateManager != nullptr && g_StateManager->Player() != nullptr) { if (hasPrevious) { ImGui::Separator(); } hasPrevious = true; const CPlayer& pl = g_StateManager->GetPlayer(); const zeus::CQuaternion plQ = zeus::CQuaternion(pl.GetTransform().getRotation().buildMatrix3f()); const zeus::CTransform camXf = g_StateManager->GetCameraManager()->GetCurrentCameraTransform(*g_StateManager); const zeus::CQuaternion camQ = zeus::CQuaternion(camXf.getRotation().buildMatrix3f()); ImGuiStringViewText( fmt::format(FMT_STRING("Player Position x: {: .2f}, y: {: .2f}, z: {: .2f}\n" " Roll: {: .2f}, Pitch: {: .2f}, Yaw: {: .2f}\n" " Momentum x: {: .2f}, y: {: .2f}, z: {: .2f}\n" " Velocity x: {: .2f}, y: {: .2f}, z: {: .2f}\n" "Camera Position x: {: .2f}, y: {: .2f}, z {: .2f}\n" " Roll: {: .2f}, Pitch: {: .2f}, Yaw: {: .2f}\n"), pl.GetTranslation().x(), pl.GetTranslation().y(), pl.GetTranslation().z(), zeus::radToDeg(plQ.roll()), zeus::radToDeg(plQ.pitch()), zeus::radToDeg(plQ.yaw()), pl.GetMomentum().x(), pl.GetMomentum().y(), pl.GetMomentum().z(), pl.GetVelocity().x(), pl.GetVelocity().y(), pl.GetVelocity().z(), camXf.origin.x(), camXf.origin.y(), camXf.origin.z(), zeus::radToDeg(camQ.roll()), zeus::radToDeg(camQ.pitch()), zeus::radToDeg(camQ.yaw()))); } if (m_worldInfo && g_StateManager != nullptr) { if (hasPrevious) { ImGui::Separator(); } hasPrevious = true; const std::string name = ImGuiLoadStringTable(g_StateManager->GetWorld()->IGetStringTableAssetId(), 0); ImGuiStringViewText( fmt::format(FMT_STRING("World Asset ID: 0x{}, Name: {}\n"), g_GameState->CurrentWorldAssetId(), name)); } if (m_areaInfo && g_StateManager != nullptr) { const metaforce::TAreaId aId = g_GameState->CurrentWorldState().GetCurrentAreaId(); if (g_StateManager->GetWorld() != nullptr && g_StateManager->GetWorld()->DoesAreaExist(aId)) { if (hasPrevious) { ImGui::Separator(); } hasPrevious = true; const auto& layerStates = g_GameState->CurrentWorldState().GetLayerState(); std::string layerBits; u32 totalActive = 0; for (int i = 0; i < layerStates->GetAreaLayerCount(aId); ++i) { if (layerStates->IsLayerActive(aId, i)) { ++totalActive; layerBits += "1"; } else { layerBits += "0"; } } CGameArea* pArea = g_StateManager->GetWorld()->GetArea(aId); CAssetId stringId = pArea->IGetStringTableAssetId(); ImGuiStringViewText( fmt::format(FMT_STRING("Area Asset ID: 0x{}, Name: {}\nArea ID: {}, Active Layer bits: {}\n"), pArea->GetAreaAssetId(), ImGuiLoadStringTable(stringId, 0), pArea->GetAreaId(), layerBits)); } } if (m_layerInfo && g_StateManager != nullptr) { const metaforce::TAreaId aId = g_GameState->CurrentWorldState().GetCurrentAreaId(); const auto* world = g_StateManager->GetWorld(); if (world != nullptr && world->DoesAreaExist(aId) && world->GetWorldLayers()) { if (hasPrevious) { ImGui::Separator(); } hasPrevious = true; ImGuiStringViewText("Area Layers:"); ImVec4 activeColor = ImGui::GetStyleColorVec4(ImGuiCol_Text); ImVec4 inactiveColor = activeColor; inactiveColor.w = 0.5f; const CWorldLayers& layers = world->GetWorldLayers().value(); const auto& layerStates = g_GameState->CurrentWorldState().GetLayerState(); int layerCount = int(layerStates->GetAreaLayerCount(aId)); u32 startNameIdx = layers.m_areas[aId].m_startNameIdx; if (startNameIdx + layerCount > layers.m_names.size()) { ImGui::Text("Broken layer data, please re-package"); } else { for (int i = 0; i < layerCount; ++i) { ImGui::PushStyleColor(ImGuiCol_Text, layerStates->IsLayerActive(aId, i) ? activeColor : inactiveColor); ImGuiStringViewText(" " + layers.m_names[startNameIdx + i]); ImGui::PopStyleColor(); } } } } if (m_randomStats) { if (hasPrevious) { ImGui::Separator(); } hasPrevious = true; ImGuiStringViewText( fmt::format(FMT_STRING("CRandom16::Next calls: {}\n"), metaforce::CRandom16::GetNumNextCalls())); } if (m_resourceStats) { if (hasPrevious) { ImGui::Separator(); } hasPrevious = true; ImGuiStringViewText(fmt::format(FMT_STRING("Resource Objects: {}\n"), g_SimplePool->GetLiveObjects())); } if (m_pipelineInfo) { if (hasPrevious) { ImGui::Separator(); } hasPrevious = true; ImGuiStringViewText(fmt::format(FMT_STRING("Queued pipelines: {}\n"), aurora::gfx::queuedPipelines)); ImGuiStringViewText(fmt::format(FMT_STRING("Done pipelines: {}\n"), aurora::gfx::createdPipelines)); } ShowCornerContextMenu(m_debugOverlayCorner, m_inputOverlayCorner); } ImGui::End(); } void TextCenter(const std::string& text) { float font_size = ImGui::GetFontSize() * text.size() / 2; ImGui::SameLine(ImGui::GetWindowSize().x / 2 - font_size + (font_size / 2)); ImGui::TextUnformatted(text.c_str()); } void ImGuiConsole::ShowInputViewer() { if (!m_showInput || g_InputGenerator == nullptr) { return; } auto input = g_InputGenerator->GetLastInput(); if (input.x4_controllerIdx != 0) { return; } if (m_whichController != input.m_which) { m_controllerName = static_cast(aurora::get_controller_name(input.m_which)); m_whichController = input.m_which; } // Code -stolen- borrowed from Practice Mod ImGuiIO& io = ImGui::GetIO(); ImGuiWindowFlags windowFlags = ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_NoFocusOnAppearing | ImGuiWindowFlags_NoNav; if (m_inputOverlayCorner != -1) { SetOverlayWindowLocation(m_inputOverlayCorner); windowFlags |= ImGuiWindowFlags_NoMove; } ImGui::SetNextWindowBgAlpha(0.65f); if (ImGui::Begin("Input Overlay", nullptr, windowFlags)) { float scale = aurora::get_window_size().scale; if (!m_controllerName.empty()) { TextCenter(m_controllerName); ImGui::Separator(); } ImDrawList* dl = ImGui::GetWindowDrawList(); zeus::CVector2f p = ImGui::GetCursorScreenPos(); float leftStickRadius = 30 * scale; p = p + zeus::CVector2f{20, 20} * scale; // Pad p so we don't clip outside our rect zeus::CVector2f leftStickCenter = p + zeus::CVector2f(30, 45) * scale; float dpadRadius = 15 * scale; float dpadWidth = 8 * scale; zeus::CVector2f dpadCenter = p + zeus::CVector2f(80, 90) * scale; float rightStickRadius = 20 * scale; zeus::CVector2f rightStickCenter = p + zeus::CVector2f(160, 90) * scale; float startButtonRadius = 8 * scale; zeus::CVector2f startButtonCenter = p + zeus::CVector2f(120, 55) * scale; float aButtonRadius = 16 * scale; zeus::CVector2f aButtonCenter = p + zeus::CVector2f(210, 48) * scale; float bButtonRadius = 8 * scale; zeus::CVector2f bButtonCenter = aButtonCenter + zeus::CVector2f(-24, 16) * scale; float xButtonRadius = 8 * scale; zeus::CVector2f xButtonCenter = aButtonCenter + zeus::CVector2f(24, -16) * scale; float yButtonRadius = 8 * scale; zeus::CVector2f yButtonCenter = aButtonCenter + zeus::CVector2f(-12, -24) * scale; float triggerWidth = leftStickRadius * 2; float triggerHeight = 8 * scale; zeus::CVector2f lCenter = leftStickCenter + zeus::CVector2f(0, -60) * scale; zeus::CVector2f rCenter = zeus::CVector2f(aButtonCenter.x(), lCenter.y()); const auto zButtonCenter = rCenter + zeus::CVector2f{0, 24 * scale}; const float zButtonHalfWidth = triggerWidth / 2; const float zButtonHalfHeight = 4 * scale; constexpr ImU32 stickGray = IM_COL32(150, 150, 150, 255); constexpr ImU32 darkGray = IM_COL32(60, 60, 60, 255); constexpr ImU32 red = IM_COL32(255, 0, 0, 255); constexpr ImU32 green = IM_COL32(0, 255, 0, 255); // left stick { float x = input.ALeftX(); float y = -input.ALeftY(); dl->AddCircleFilled(leftStickCenter, leftStickRadius, stickGray, 8); dl->AddLine(leftStickCenter, leftStickCenter + zeus::CVector2f(x * leftStickRadius, y * leftStickRadius), IM_COL32(255, 244, 0, 255), 1.5f); dl->AddCircleFilled(leftStickCenter + (zeus::CVector2f{x, y} * leftStickRadius), leftStickRadius / 3, red); } // right stick { float x = input.ARightX(); float y = -input.ARightY(); dl->AddCircleFilled(rightStickCenter, rightStickRadius, stickGray, 8); dl->AddLine(rightStickCenter, rightStickCenter + zeus::CVector2f(x * rightStickRadius, y * rightStickRadius), IM_COL32(255, 244, 0, 255), 1.5f); dl->AddCircleFilled(rightStickCenter + (zeus::CVector2f{x, y} * rightStickRadius), rightStickRadius / 3, red); } // dpad { float halfWidth = dpadWidth / 2; dl->AddRectFilled(dpadCenter + zeus::CVector2f(-halfWidth, -dpadRadius), dpadCenter + zeus::CVector2f(halfWidth, dpadRadius), stickGray); dl->AddRectFilled(dpadCenter + zeus::CVector2f(-dpadRadius, -halfWidth), dpadCenter + zeus::CVector2f(dpadRadius, halfWidth), stickGray); if (input.DDPUp()) { dl->AddRectFilled(dpadCenter + zeus::CVector2f(-halfWidth, -dpadRadius), dpadCenter + zeus::CVector2f(halfWidth, -dpadRadius / 2), red); } if (input.DDPDown()) { dl->AddRectFilled(dpadCenter + zeus::CVector2f(-halfWidth, dpadRadius), dpadCenter + zeus::CVector2f(halfWidth, dpadRadius / 2), red); } if (input.DDPLeft()) { dl->AddRectFilled(dpadCenter + zeus::CVector2f(-dpadRadius, -halfWidth), dpadCenter + zeus::CVector2f(-dpadRadius / 2, halfWidth), red); } if (input.DDPRight()) { dl->AddRectFilled(dpadCenter + zeus::CVector2f(dpadRadius, -halfWidth), dpadCenter + zeus::CVector2f(dpadRadius / 2, halfWidth), red); } } // buttons { // start dl->AddCircleFilled(startButtonCenter, startButtonRadius, input.DStart() ? red : stickGray); // a dl->AddCircleFilled(aButtonCenter, aButtonRadius, input.DA() ? green : stickGray); // b dl->AddCircleFilled(bButtonCenter, bButtonRadius, input.DB() ? red : stickGray); // x dl->AddCircleFilled(xButtonCenter, xButtonRadius, input.DX() ? red : stickGray); // y dl->AddCircleFilled(yButtonCenter, yButtonRadius, input.DY() ? red : stickGray); // z dl->AddRectFilled(zButtonCenter - zeus::CVector2f{zButtonHalfWidth, zButtonHalfHeight}, zButtonCenter + zeus::CVector2f{zButtonHalfWidth, zButtonHalfHeight}, input.DZ() ? IM_COL32(128, 0, 128, 255) : stickGray, 16); } // triggers { float halfTriggerWidth = triggerWidth / 2; zeus::CVector2f lStart = lCenter - zeus::CVector2f(halfTriggerWidth, 0); zeus::CVector2f lEnd = lCenter + zeus::CVector2f(halfTriggerWidth, triggerHeight); float lValue = triggerWidth * std::min(1.f, input.ALTrigger()); dl->AddRectFilled(lStart, lStart + zeus::CVector2f(lValue, triggerHeight), input.DL() ? red : stickGray); dl->AddRectFilled(lStart + zeus::CVector2f(lValue, 0), lEnd, darkGray); zeus::CVector2f rStart = rCenter - zeus::CVector2f(halfTriggerWidth, 0); zeus::CVector2f rEnd = rCenter + zeus::CVector2f(halfTriggerWidth, triggerHeight); float rValue = triggerWidth * std::min(1.f, input.ARTrigger()); dl->AddRectFilled(rEnd - zeus::CVector2f(rValue, triggerHeight), rEnd, input.DR() ? red : stickGray); dl->AddRectFilled(rStart, rEnd - zeus::CVector2f(rValue, 0), darkGray); } ImGui::Dummy(zeus::CVector2f(270, 130) * scale); ShowCornerContextMenu(m_inputOverlayCorner, m_debugOverlayCorner); } ImGui::End(); } void ImGuiConsole::ShowCornerContextMenu(int& corner, int avoidCorner) const { if (ImGui::BeginPopupContextWindow()) { if (ImGui::MenuItem("Custom", nullptr, corner == -1)) { corner = -1; } if (ImGui::MenuItem("Top-left", nullptr, corner == 0, avoidCorner != 0)) { corner = 0; } if (ImGui::MenuItem("Top-right", nullptr, corner == 1, avoidCorner != 1)) { corner = 1; } if (ImGui::MenuItem("Bottom-left", nullptr, corner == 2, avoidCorner != 2)) { corner = 2; } if (ImGui::MenuItem("Bottom-right", nullptr, corner == 3, avoidCorner != 3)) { corner = 3; } ImGui::EndPopup(); } } void ImGuiConsole::SetOverlayWindowLocation(int corner) const { const ImGuiViewport* viewport = ImGui::GetMainViewport(); ImVec2 workPos = viewport->WorkPos; // Use work area to avoid menu-bar/task-bar, if any! ImVec2 workSize = viewport->WorkSize; ImVec2 windowPos; ImVec2 windowPosPivot; constexpr float padding = 10.0f; windowPos.x = (corner & 1) != 0 ? (workPos.x + workSize.x - padding) : (workPos.x + padding); windowPos.y = (corner & 2) != 0 ? (workPos.y + workSize.y - padding) : (workPos.y + padding); windowPosPivot.x = (corner & 1) != 0 ? 1.0f : 0.0f; windowPosPivot.y = (corner & 2) != 0 ? 1.0f : 0.0f; ImGui::SetNextWindowPos(windowPos, ImGuiCond_Always, windowPosPivot); } void ImGuiConsole::ShowAppMainMenuBar(bool canInspect) { if (ImGui::BeginMainMenuBar()) { if (ImGui::BeginMenu("Game")) { ShowMenuGame(); ImGui::EndMenu(); } if (ImGui::BeginMenu("Tools")) { ImGui::MenuItem("Player Transform", nullptr, &m_showPlayerTransformEditor, canInspect && m_developer); ImGui::MenuItem("Inspect", nullptr, &m_showInspectWindow, canInspect && m_developer); ImGui::MenuItem("Items", nullptr, &m_showItemsWindow, canInspect && m_developer && m_cheats); ImGui::MenuItem("Layers", nullptr, &m_showLayersWindow, canInspect && m_developer); ImGui::MenuItem("Console Variables", nullptr, &m_showConsoleVariablesWindow); ImGui::EndMenu(); } if (ImGui::BeginMenu("Debug")) { if (ImGui::MenuItem("Frame Counter", nullptr, &m_frameCounter)) { m_cvarCommons.m_debugOverlayShowFrameCounter->fromBoolean(m_frameCounter); } if (ImGui::MenuItem("Frame Rate", nullptr, &m_frameRate)) { m_cvarCommons.m_debugOverlayShowFramerate->fromBoolean(m_frameRate); } if (ImGui::MenuItem("In-Game Time", nullptr, &m_inGameTime)) { m_cvarCommons.m_debugOverlayShowInGameTime->fromBoolean(m_inGameTime); } if (ImGui::MenuItem("Room Timer", nullptr, &m_roomTimer)) { m_cvarCommons.m_debugOverlayShowRoomTimer->fromBoolean(m_roomTimer); } if (ImGui::MenuItem("Player Info", nullptr, &m_playerInfo)) { m_cvarCommons.m_debugOverlayPlayerInfo->fromBoolean(m_playerInfo); } if (ImGui::MenuItem("World Info", nullptr, &m_worldInfo)) { m_cvarCommons.m_debugOverlayWorldInfo->fromBoolean(m_worldInfo); } if (ImGui::MenuItem("Area Info", nullptr, &m_areaInfo)) { m_cvarCommons.m_debugOverlayAreaInfo->fromBoolean(m_areaInfo); } if (ImGui::MenuItem("Layer Info", nullptr, &m_layerInfo)) { m_cvarCommons.m_debugOverlayLayerInfo->fromBoolean(m_layerInfo); } if (ImGui::MenuItem("Random Stats", nullptr, &m_randomStats)) { m_cvarCommons.m_debugOverlayShowRandomStats->fromBoolean(m_randomStats); } if (ImGui::MenuItem("Resource Stats", nullptr, &m_resourceStats)) { m_cvarCommons.m_debugOverlayShowResourceStats->fromBoolean(m_resourceStats); } if (ImGui::MenuItem("Show Input", nullptr, &m_showInput)) { m_cvarCommons.m_debugOverlayShowInput->fromBoolean(m_showInput); } ImGui::EndMenu(); } ImGui::Spacing(); if (ImGui::BeginMenu("Help")) { ImGui::MenuItem("About", nullptr, &m_showAboutWindow); ImGui::Separator(); if (ImGui::BeginMenu("ImGui")) { if (ImGui::MenuItem("Clear Settings")) { ImGui::ClearIniSettings(); } #ifndef NDEBUG ImGui::MenuItem("Show Demo", nullptr, &m_showDemoWindow); #endif ImGui::EndMenu(); } ImGui::EndMenu(); } ImGui::EndMainMenuBar(); } } void ImGuiConsole::PreUpdate() { OPTICK_EVENT(); if (!m_isInitialized) { m_isInitialized = true; m_cvarCommons.m_debugOverlayShowFrameCounter->addListener([this](CVar* c) { m_frameCounter = c->toBoolean(); }); m_cvarCommons.m_debugOverlayShowFramerate->addListener([this](CVar* c) { m_frameRate = c->toBoolean(); }); m_cvarCommons.m_debugOverlayShowInGameTime->addListener([this](CVar* c) { m_inGameTime = c->toBoolean(); }); m_cvarCommons.m_debugOverlayShowRoomTimer->addListener([this](CVar* c) { m_roomTimer = c->toBoolean(); }); m_cvarCommons.m_debugOverlayPlayerInfo->addListener([this](CVar* c) { m_playerInfo = c->toBoolean(); }); m_cvarCommons.m_debugOverlayWorldInfo->addListener([this](CVar* c) { m_worldInfo = c->toBoolean(); }); m_cvarCommons.m_debugOverlayAreaInfo->addListener([this](CVar* c) { m_areaInfo = c->toBoolean(); }); m_cvarCommons.m_debugOverlayLayerInfo->addListener([this](CVar* c) { m_layerInfo = c->toBoolean(); }); m_cvarCommons.m_debugOverlayShowRandomStats->addListener([this](CVar* c) { m_randomStats = c->toBoolean(); }); m_cvarCommons.m_debugOverlayShowResourceStats->addListener([this](CVar* c) { m_resourceStats = c->toBoolean(); }); m_cvarCommons.m_debugOverlayShowInput->addListener([this](CVar* c) { m_showInput = c->toBoolean(); }); m_cvarMgr.findCVar("developer")->addListener([this](CVar* c) { m_developer = c->toBoolean(); }); m_cvarMgr.findCVar("cheats")->addListener([this](CVar* c) { m_cheats = c->toBoolean(); }); } // We ned to make sure we have a valid CRandom16 at all times, so lets do that here if (g_StateManager != nullptr && g_StateManager->GetActiveRandom() == nullptr) { g_StateManager->SetActiveRandomToDefault(); } if (ImGui::IsKeyReleased(ImGuiKey_GraveAccent)) { m_isVisible ^= 1; } if (m_stepFrame) { g_Main->SetPaused(true); m_stepFrame = false; } if (m_paused && !m_stepFrame && ImGui::IsKeyPressed(ImGuiKey_F6)) { g_Main->SetPaused(false); m_stepFrame = true; } if (ImGui::IsKeyReleased(ImGuiKey_F5)) { m_paused ^= 1; g_Main->SetPaused(m_paused); } bool canInspect = g_StateManager != nullptr && g_StateManager->GetObjectList(); if (m_isVisible) { ShowAppMainMenuBar(canInspect); } else if (m_developer) { ShowMenuHint(); } if (canInspect && (m_showInspectWindow || !inspectingEntities.empty())) { UpdateEntityEntries(); if (m_showInspectWindow) { ShowInspectWindow(&m_showInspectWindow); } auto iter = inspectingEntities.begin(); while (iter != inspectingEntities.end()) { if (!ShowEntityInfoWindow(*iter)) { iter = inspectingEntities.erase(iter); } else { iter++; } } } if (canInspect && m_showItemsWindow && m_cvarMgr.findCVar("cheats")->toBoolean()) { ShowItemsWindow(); } if (canInspect && m_showLayersWindow) { ShowLayersWindow(); } if (m_showAboutWindow) { ShowAboutWindow(true); } if (m_showDemoWindow) { ImGui::ShowDemoWindow(&m_showDemoWindow); } if (m_showConsoleVariablesWindow) { ShowConsoleVariablesWindow(); } ShowDebugOverlay(); ShowInputViewer(); ShowPlayerTransformEditor(); } void ImGuiConsole::PostUpdate() { OPTICK_EVENT(); if (g_StateManager != nullptr && g_StateManager->GetObjectList()) { // Clear deleted objects CObjectList& list = g_StateManager->GetAllObjectList(); for (s16 uid = 0; uid < s16(entities.size()); uid++) { ImGuiEntityEntry& item = entities[uid]; if (item.uid == kInvalidUniqueId) { continue; // already cleared } CEntity* ent = list.GetObjectByIndex(uid); if (ent == nullptr || ent != item.ent) { // Remove inspect windows for deleted entities inspectingEntities.erase(item.uid); item.uid = kInvalidUniqueId; item.ent = nullptr; // for safety } } } else { entities.fill(ImGuiEntityEntry{}); inspectingEntities.clear(); } // Always calculate room time regardless of if the overlay is displayed, this allows us have an accurate display if // the user chooses to display it later on during gameplay if (g_StateManager && m_currentRoom != g_StateManager->GetCurrentArea()) { const double igt = g_GameState->GetTotalPlayTime(); m_currentRoom = static_cast(g_StateManager->GetCurrentArea()); m_lastRoomTime = igt - m_currentRoomStart; m_currentRoomStart = igt; } } void ImGuiConsole::Shutdown() { dummyWorlds.clear(); stringTables.clear(); } static constexpr std::array GeneralItems{ CPlayerState::EItemType::EnergyTanks, CPlayerState::EItemType::CombatVisor, CPlayerState::EItemType::ScanVisor, CPlayerState::EItemType::ThermalVisor, CPlayerState::EItemType::XRayVisor, CPlayerState::EItemType::GrappleBeam, CPlayerState::EItemType::SpaceJumpBoots, CPlayerState::EItemType::PowerSuit, CPlayerState::EItemType::VariaSuit, CPlayerState::EItemType::GravitySuit, CPlayerState::EItemType::PhazonSuit, }; static constexpr std::array WeaponItems{ CPlayerState::EItemType::Missiles, CPlayerState::EItemType::PowerBeam, CPlayerState::EItemType::IceBeam, CPlayerState::EItemType::WaveBeam, CPlayerState::EItemType::PlasmaBeam, CPlayerState::EItemType::SuperMissile, CPlayerState::EItemType::Flamethrower, CPlayerState::EItemType::IceSpreader, CPlayerState::EItemType::Wavebuster, CPlayerState::EItemType::ChargeBeam, }; static constexpr std::array MorphBallItems{ CPlayerState::EItemType::PowerBombs, CPlayerState::EItemType::MorphBall, CPlayerState::EItemType::MorphBallBombs, CPlayerState::EItemType::BoostBall, CPlayerState::EItemType::SpiderBall, }; static constexpr std::array ArtifactItems{ CPlayerState::EItemType::Truth, CPlayerState::EItemType::Strength, CPlayerState::EItemType::Elder, CPlayerState::EItemType::Wild, CPlayerState::EItemType::Lifegiver, CPlayerState::EItemType::Warrior, CPlayerState::EItemType::Chozo, CPlayerState::EItemType::Nature, CPlayerState::EItemType::Sun, CPlayerState::EItemType::World, CPlayerState::EItemType::Spirit, CPlayerState::EItemType::Newborn, }; static constexpr std::array ItemLoadout21Percent{ std::make_pair(CPlayerState::EItemType::PowerSuit, 1), std::make_pair(CPlayerState::EItemType::CombatVisor, 1), std::make_pair(CPlayerState::EItemType::ScanVisor, 1), std::make_pair(CPlayerState::EItemType::PowerBeam, 1), std::make_pair(CPlayerState::EItemType::WaveBeam, 1), std::make_pair(CPlayerState::EItemType::IceBeam, 1), std::make_pair(CPlayerState::EItemType::PlasmaBeam, 1), std::make_pair(CPlayerState::EItemType::XRayVisor, 1), std::make_pair(CPlayerState::EItemType::Missiles, 5), std::make_pair(CPlayerState::EItemType::VariaSuit, 1), std::make_pair(CPlayerState::EItemType::PhazonSuit, 1), std::make_pair(CPlayerState::EItemType::MorphBall, 1), std::make_pair(CPlayerState::EItemType::MorphBallBombs, 1), std::make_pair(CPlayerState::EItemType::PowerBombs, 4), }; static constexpr std::array ItemLoadoutAnyPercent{ std::make_pair(CPlayerState::EItemType::PowerSuit, 1), std::make_pair(CPlayerState::EItemType::CombatVisor, 1), std::make_pair(CPlayerState::EItemType::ScanVisor, 1), std::make_pair(CPlayerState::EItemType::EnergyTanks, 3), std::make_pair(CPlayerState::EItemType::PowerBeam, 1), std::make_pair(CPlayerState::EItemType::WaveBeam, 1), std::make_pair(CPlayerState::EItemType::IceBeam, 1), std::make_pair(CPlayerState::EItemType::PlasmaBeam, 1), std::make_pair(CPlayerState::EItemType::ChargeBeam, 1), std::make_pair(CPlayerState::EItemType::XRayVisor, 1), std::make_pair(CPlayerState::EItemType::ThermalVisor, 1), std::make_pair(CPlayerState::EItemType::Missiles, 25), std::make_pair(CPlayerState::EItemType::VariaSuit, 1), std::make_pair(CPlayerState::EItemType::PhazonSuit, 1), std::make_pair(CPlayerState::EItemType::MorphBall, 1), std::make_pair(CPlayerState::EItemType::BoostBall, 1), std::make_pair(CPlayerState::EItemType::MorphBallBombs, 1), std::make_pair(CPlayerState::EItemType::PowerBombs, 4), std::make_pair(CPlayerState::EItemType::SpaceJumpBoots, 1), }; int roundMultiple(int value, int multiple) { if (multiple == 0) { return value; } return static_cast(std::round(static_cast(value) / static_cast(multiple)) * static_cast(multiple)); } static void RenderItemType(CPlayerState& pState, CPlayerState::EItemType itemType) { u32 maxValue = CPlayerState::GetPowerUpMaxValue(itemType); std::string name{CPlayerState::ItemTypeToName(itemType)}; if (maxValue == 1) { bool enabled = pState.GetItemCapacity(itemType) == 1; if (ImGui::Checkbox(name.c_str(), &enabled)) { if (enabled) { pState.ReInitializePowerUp(itemType, 1); pState.ResetAndIncrPickUp(itemType, 1); } else { pState.ReInitializePowerUp(itemType, 0); } if (itemType == CPlayerState::EItemType::VariaSuit || itemType == CPlayerState::EItemType::PowerSuit || itemType == CPlayerState::EItemType::GravitySuit || itemType == CPlayerState::EItemType::PhazonSuit) { g_StateManager->Player()->AsyncLoadSuit(*g_StateManager); } } } else if (maxValue > 1) { int capacity = int(pState.GetItemCapacity(itemType)); int amount = int(pState.GetItemAmount(itemType)); if (ImGui::SliderInt((name + " (Capacity)").c_str(), &capacity, 0, int(maxValue), "%d", ImGuiSliderFlags_AlwaysClamp)) { if (itemType == CPlayerState::EItemType::Missiles) { capacity = roundMultiple(capacity, 5); } pState.ReInitializePowerUp(itemType, u32(capacity)); pState.ResetAndIncrPickUp(itemType, u32(capacity)); } if (capacity > 0) { if (ImGui::SliderInt((name + " (Amount)").c_str(), &amount, 0, capacity, "%d", ImGuiSliderFlags_AlwaysClamp)) { if (itemType == CPlayerState::EItemType::Missiles) { amount = roundMultiple(amount, 5); } pState.ResetAndIncrPickUp(itemType, u32(amount)); } } else { ImGui::Dummy(ImGui::GetItemRectSize()); } } } template static inline void RenderItemsDualColumn(CPlayerState& pState, const std::array& items, int start) { ImGui::BeginGroup(); // Render left group for (int i = start; i < items.size(); i += 2) { RenderItemType(pState, items[i]); } ImGui::EndGroup(); ImGui::SameLine(); ImGui::BeginGroup(); // Render right group for (int i = start + 1; i < items.size(); i += 2) { RenderItemType(pState, items[i]); } ImGui::EndGroup(); } void ImGuiConsole::ShowItemsWindow() { CPlayerState& pState = *g_StateManager->GetPlayerState(); if (ImGui::Begin("Items", &m_showItemsWindow, ImGuiWindowFlags_AlwaysAutoResize)) { if (ImGui::Button("Refill")) { for (int i = 0; i < int(CPlayerState::EItemType::Max); ++i) { auto itemType = static_cast(i); u32 maxValue = CPlayerState::GetPowerUpMaxValue(itemType); pState.ResetAndIncrPickUp(itemType, maxValue); } } auto& mapWorldInfo = *g_GameState->CurrentWorldState().MapWorldInfo(); ImGui::SameLine(); bool mapStationUsed = mapWorldInfo.GetMapStationUsed(); if (ImGui::Checkbox("Area map", &mapStationUsed)) { mapWorldInfo.SetMapStationUsed(mapStationUsed); } if (ImGui::Button("All")) { for (int i = 0; i < int(CPlayerState::EItemType::Max); ++i) { auto itemType = static_cast(i); u32 maxValue = CPlayerState::GetPowerUpMaxValue(itemType); pState.ReInitializePowerUp(itemType, maxValue); pState.ResetAndIncrPickUp(itemType, maxValue); } mapWorldInfo.SetMapStationUsed(true); } ImGui::SameLine(); if (ImGui::Button("None")) { for (int i = 0; i < int(CPlayerState::EItemType::Max); ++i) { auto itemType = static_cast(i); pState.ReInitializePowerUp(itemType, 0); } mapWorldInfo.SetMapStationUsed(false); } ImGui::SameLine(); if (ImGui::Button("21%")) { for (int i = 0; i < int(CPlayerState::EItemType::Max); ++i) { auto itemType = static_cast(i); pState.ReInitializePowerUp(itemType, 0); } mapWorldInfo.SetMapStationUsed(false); for (const auto& [item, count] : ItemLoadout21Percent) { pState.ReInitializePowerUp(item, count); pState.IncrPickup(item, count); } for (const auto& item : ArtifactItems) { pState.ReInitializePowerUp(item, 1); } } ImGui::SameLine(); if (ImGui::Button("Any%")) { for (int i = 0; i < int(CPlayerState::EItemType::Max); ++i) { auto itemType = static_cast(i); pState.ReInitializePowerUp(itemType, 0); } mapWorldInfo.SetMapStationUsed(false); for (const auto& [item, count] : ItemLoadoutAnyPercent) { pState.ReInitializePowerUp(item, count); pState.IncrPickup(item, count); } for (const auto& item : ArtifactItems) { pState.ReInitializePowerUp(item, 1); } } if (ImGui::BeginTabBar("Items")) { if (ImGui::BeginTabItem("General")) { RenderItemType(pState, GeneralItems[0]); // full width RenderItemsDualColumn(pState, GeneralItems, 1); ImGui::EndTabItem(); } if (ImGui::BeginTabItem("Weapons")) { RenderItemType(pState, WeaponItems[0]); // full width RenderItemsDualColumn(pState, WeaponItems, 1); ImGui::EndTabItem(); } if (ImGui::BeginTabItem("Morph Ball")) { RenderItemType(pState, MorphBallItems[0]); // full width RenderItemsDualColumn(pState, MorphBallItems, 1); ImGui::EndTabItem(); } if (ImGui::BeginTabItem("Artifacts")) { ImGui::Text("NOTE: This doesn't affect Artifact Temple layers"); ImGui::Text("Use the Layers window to set them for progression"); RenderItemsDualColumn(pState, ArtifactItems, 0); ImGui::EndTabItem(); } ImGui::EndTabBar(); } } ImGui::End(); } void ImGuiConsole::ShowLayersWindow() { // For some reason the window shows up tiny without this float initialWindowSize = 350.f * aurora::get_window_size().scale; ImGui::SetNextWindowSize(ImVec2{initialWindowSize, initialWindowSize}, ImGuiCond_FirstUseEver); if (ImGui::Begin("Layers", &m_showLayersWindow)) { if (ImGui::Button("Clear")) { m_layersFilterText.clear(); } ImGui::SameLine(); ImGui::InputText("Filter", &m_layersFilterText); bool hasSearch = !m_layersFilterText.empty(); if (hasSearch) { // kinda hacky way reset the tree state when search changes ImGui::PushID(m_layersFilterText.c_str()); } for (const auto& world : ListWorlds()) { const auto& layers = dummyWorlds[world.second]->GetWorldLayers(); if (!layers) { continue; } auto worldLayerState = g_GameState->StateForWorld(world.second).GetLayerState(); auto areas = ListAreas(world.second); auto iter = areas.begin(); while (iter != areas.end()) { if (hasSearch && !ContainsCaseInsensitive(iter->first, m_layersFilterText)) { iter = areas.erase(iter); } else { iter++; } } if (areas.empty()) { continue; } if (ImGui::TreeNodeEx(world.first.c_str(), hasSearch ? ImGuiTreeNodeFlags_DefaultOpen : 0)) { for (const auto& area : areas) { u32 layerCount = worldLayerState->GetAreaLayerCount(area.second); if (layerCount == 0) { continue; } if (ImGui::TreeNode(area.first.c_str())) { if (ImGui::Button("Warp here")) { Warp(world.second, area.second); } u32 startNameIdx = layers->m_areas[area.second].m_startNameIdx; if (startNameIdx + layerCount > layers->m_names.size()) { ImGui::Text("Broken layer data, please re-package"); } else { for (int layer = 0; layer < layerCount; ++layer) { bool active = worldLayerState->IsLayerActive(area.second, layer); if (ImGui::Checkbox(layers->m_names[startNameIdx + layer].c_str(), &active)) { worldLayerState->SetLayerActive(area.second, layer, active); } } } ImGui::TreePop(); } } ImGui::TreePop(); } } if (hasSearch) { ImGui::PopID(); } } ImGui::End(); } void ImGuiConsole::ShowMenuHint() { if (m_menuHintTime <= 0.f) { return; } m_menuHintTime -= ImGui::GetIO().DeltaTime; const ImGuiViewport* viewport = ImGui::GetMainViewport(); const ImVec2 workPos = viewport->WorkPos; const ImVec2 workSize = viewport->WorkSize; constexpr float padding = 10.0f; const ImVec2 windowPos{workPos.x + workSize.x / 2, workPos.y + workSize.y - padding}; ImGui::SetNextWindowPos(windowPos, ImGuiCond_Always, ImVec2{0.5f, 1.f}); const float alpha = std::min(m_menuHintTime, 1.f); ImGui::SetNextWindowBgAlpha(alpha * 0.65f); ImVec4 textColor = ImGui::GetStyleColorVec4(ImGuiCol_Text); textColor.w *= alpha; ImVec4 borderColor = ImGui::GetStyleColorVec4(ImGuiCol_Border); borderColor.w *= alpha; ImGui::PushStyleColor(ImGuiCol_Text, textColor); ImGui::PushStyleColor(ImGuiCol_Border, borderColor); if (ImGui::Begin("Menu Hint", nullptr, ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_NoFocusOnAppearing | ImGuiWindowFlags_NoNav | ImGuiWindowFlags_NoMove)) { ImGuiStringViewText("Press ` to toggle menu"sv); } ImGui::End(); ImGui::PopStyleColor(2); } void ImGuiConsole::ShowPlayerTransformEditor() { if (!m_showPlayerTransformEditor) { return; } if (ImGui::Begin("Player Transform", &m_showPlayerTransformEditor, ImGuiWindowFlags_AlwaysAutoResize)) { if (ImGui::CollapsingHeader("Position")) { ImGui::PushID("player_position"); zeus::CVector3f vec = g_StateManager->GetPlayer().GetTranslation(); if (ImGuiVector3fInput("Position", vec)) { g_StateManager->GetPlayer().SetTranslation(vec); } if (ImGui::Button("Save")) { m_savedLocation.emplace(vec); } ImGui::SameLine(); if (ImGui::Button("Load") && m_savedLocation) { g_StateManager->GetPlayer().SetTranslation(*m_savedLocation); } ImGui::SameLine(); if (ImGui::Button("Clear") && m_savedLocation) { m_savedLocation.reset(); } if (m_savedLocation) { ImGui::Text("Saved: %g, %g, %g", float(m_savedLocation->x()), float(m_savedLocation->y()), float(m_savedLocation->z())); } ImGui::PopID(); } if (ImGui::CollapsingHeader("Rotation")) { ImGui::PushID("player_rotation"); zeus::CEulerAngles angles(g_StateManager->GetPlayer().GetTransform()); angles = zeus::CEulerAngles(angles * zeus::skRadToDegVec); if (ImGuiVector3fInput("Rotation", angles)) { angles.x() = zeus::clamp(-179.999f, float(angles.x()), 179.999f); angles.y() = zeus::clamp(-89.999f, float(angles.y()), 89.999f); angles.z() = zeus::clamp(-179.999f, float(angles.z()), 179.999f); auto xf = g_StateManager->GetPlayer().GetTransform(); xf.setRotation(zeus::CQuaternion(angles * zeus::skDegToRadVec).toTransform().buildMatrix3f()); g_StateManager->GetPlayer().SetTransform(xf); } if (ImGui::Button("Save")) { m_savedRotation.emplace(angles); } ImGui::SameLine(); if (ImGui::Button("Load") && m_savedRotation) { auto xf = g_StateManager->GetPlayer().GetTransform(); xf.setRotation(zeus::CQuaternion((*m_savedRotation) * zeus::skDegToRadVec).toTransform().buildMatrix3f()); g_StateManager->GetPlayer().SetTransform(xf); } ImGui::SameLine(); if (ImGui::Button("Clear") && m_savedRotation) { m_savedRotation.reset(); } if (m_savedRotation) { ImGui::Text("Saved: %g, %g, %g", float(m_savedRotation->x()), float(m_savedRotation->y()), float(m_savedRotation->z())); } ImGui::PopID(); } } ImGui::End(); } } // namespace metaforce