diff --git a/MarathonRecomp/CMakeLists.txt b/MarathonRecomp/CMakeLists.txt index 262528d6..be146874 100644 --- a/MarathonRecomp/CMakeLists.txt +++ b/MarathonRecomp/CMakeLists.txt @@ -163,6 +163,7 @@ set(MARATHON_RECOMP_UI_CXX_SOURCES "ui/options_menu.cpp" "ui/options_menu_thumbnails.cpp" "ui/tv_static.cpp" + "ui/devtitle_menu.cpp" ) set(MARATHON_RECOMP_INSTALL_CXX_SOURCES diff --git a/MarathonRecomp/api/Marathon.h b/MarathonRecomp/api/Marathon.h index 9e520cad..974bd0b9 100644 --- a/MarathonRecomp/api/Marathon.h +++ b/MarathonRecomp/api/Marathon.h @@ -28,8 +28,10 @@ #include "Sonicteam/CsdManager.h" #include "Sonicteam/CsdObject.h" #include "Sonicteam/CsdResource.h" +#include "Sonicteam/DevTitleMode.h" #include "Sonicteam/DocMarathonImp.h" #include "Sonicteam/DocMarathonState.h" +#include "Sonicteam/DocState.h" #include "Sonicteam/Enemy/EnemyShot.h" #include "Sonicteam/Enemy/EnemyShotNormal.h" #include "Sonicteam/Enemy/EnemyShotPoint.h" @@ -65,10 +67,14 @@ #include "Sonicteam/MyTexture.h" #include "Sonicteam/MyTransforms.h" #include "Sonicteam/NamedActor.h" +#include "Sonicteam/StageSelectMode.h" +#include "Sonicteam/StageMap.h" #include "Sonicteam/NamedTask.h" #include "Sonicteam/NoSyncThread.h" #include "Sonicteam/ObjectVehicle.h" #include "Sonicteam/ObjectVehicleBike.h" +#include "Sonicteam/PicBase.h" +#include "Sonicteam/PictureTitle.h" #include "Sonicteam/Player/IDynamicLink.h" #include "Sonicteam/Player/IExportExternalFlag.h" #include "Sonicteam/Player/IExportPostureRequestFlag.h" @@ -123,12 +129,14 @@ #include "Sonicteam/SoX/IResource.h" #include "Sonicteam/SoX/IResource2.h" #include "Sonicteam/SoX/IResourceMgr.h" +#include "Sonicteam/SoX/Input/Listener.h" #include "Sonicteam/SoX/Input/Manager.h" #include "Sonicteam/SoX/LinkNode.h" #include "Sonicteam/SoX/Math/Matrix.h" #include "Sonicteam/SoX/Math/Quaternion.h" #include "Sonicteam/SoX/Math/Vector.h" #include "Sonicteam/SoX/Message.h" +#include "Sonicteam/SoX/Input/Listener.h" #include "Sonicteam/SoX/MessageReceiver.h" #include "Sonicteam/SoX/Object.h" #include "Sonicteam/SoX/Physics/Entity.h" @@ -147,6 +155,8 @@ #include "Sonicteam/SoX/Scenery/CameraImp.h" #include "Sonicteam/SoX/Scenery/Drawable.h" #include "Sonicteam/SoX/Thread.h" +#include "Sonicteam/StageMap.h" +#include "Sonicteam/StageSelectMode.h" #include "Sonicteam/StdImageFilters/BurnoutBlurFilter.h" #include "Sonicteam/StdImageFilters/SingleTechniqueFilter.h" #include "Sonicteam/System/CreateStatic.h" diff --git a/MarathonRecomp/api/Sonicteam/DevTitleMode.h b/MarathonRecomp/api/Sonicteam/DevTitleMode.h new file mode 100644 index 00000000..d58bbe77 --- /dev/null +++ b/MarathonRecomp/api/Sonicteam/DevTitleMode.h @@ -0,0 +1,33 @@ +#pragma once + +#include +#include +#include +#include + +//anonymous namespace +namespace +{ + class StateDevTitle : public Sonicteam::DocState + { + public: + xpointer m_PictureTitle; + }; +} + +namespace Sonicteam +{ + class DevTitleMode : public SoX::Engine::DocMode + { + struct Mode + { + xpointer m_Name; + MARATHON_INSERT_PADDING(4); + }; + + public: + be m_Selected; + MARATHON_INSERT_PADDING(4); + MARATHON_INSERT_PADDING(10); //stdx::vector m_Mode; + }; +} diff --git a/MarathonRecomp/api/Sonicteam/DocState.h b/MarathonRecomp/api/Sonicteam/DocState.h new file mode 100644 index 00000000..ce286cf0 --- /dev/null +++ b/MarathonRecomp/api/Sonicteam/DocState.h @@ -0,0 +1,12 @@ +#pragma once + +#include + +namespace Sonicteam +{ + class DocState + { + public: + xpointer m_pVftable; + }; +} diff --git a/MarathonRecomp/api/Sonicteam/PicBase.h b/MarathonRecomp/api/Sonicteam/PicBase.h new file mode 100644 index 00000000..25894387 --- /dev/null +++ b/MarathonRecomp/api/Sonicteam/PicBase.h @@ -0,0 +1,16 @@ +#pragma once + +#include + + +namespace Sonicteam +{ + class PicBase + { + public: + xpointer m_pVftable; + be m_State1; + be m_State2; + be m_State3; + }; +} diff --git a/MarathonRecomp/api/Sonicteam/PictureTitle.h b/MarathonRecomp/api/Sonicteam/PictureTitle.h new file mode 100644 index 00000000..753264c6 --- /dev/null +++ b/MarathonRecomp/api/Sonicteam/PictureTitle.h @@ -0,0 +1,18 @@ +#pragma once + +#include +#include +#include +#include +#include + +namespace Sonicteam +{ + class PictureTitle:public PicBase, public SoX::Input::Listener + { + public: + MARATHON_INSERT_PADDING(4); + xpointer m_Doc; + MARATHON_INSERT_PADDING(0x18); + }; +} diff --git a/MarathonRecomp/api/Sonicteam/SoX/Engine/DocMode.h b/MarathonRecomp/api/Sonicteam/SoX/Engine/DocMode.h index c445e83f..82a770c2 100644 --- a/MarathonRecomp/api/Sonicteam/SoX/Engine/DocMode.h +++ b/MarathonRecomp/api/Sonicteam/SoX/Engine/DocMode.h @@ -8,6 +8,6 @@ namespace Sonicteam::SoX::Engine class DocMode : public Task { public: - MARATHON_INSERT_PADDING(4); + be m_type; }; } diff --git a/MarathonRecomp/api/Sonicteam/SoX/Input/Listener.h b/MarathonRecomp/api/Sonicteam/SoX/Input/Listener.h new file mode 100644 index 00000000..8acb3344 --- /dev/null +++ b/MarathonRecomp/api/Sonicteam/SoX/Input/Listener.h @@ -0,0 +1,13 @@ +#pragma once + +#include +#include + +namespace Sonicteam::SoX::Input +{ + class Listener:SoX::Object + { + public: + MARATHON_INSERT_PADDING(0x14); //LinkSoxNode LinkInput, flags + }; +} diff --git a/MarathonRecomp/api/Sonicteam/StageMap.h b/MarathonRecomp/api/Sonicteam/StageMap.h new file mode 100644 index 00000000..15cf1b3c --- /dev/null +++ b/MarathonRecomp/api/Sonicteam/StageMap.h @@ -0,0 +1,20 @@ +#pragma once + +#include +#include +#include +#include + +namespace Sonicteam +{ + + class StageMap : public SoX::Component + { + public: + + stdx::string m_Name; + stdx::string m_Text; + stdx::vector> m_vpStageMap; + + }; +} diff --git a/MarathonRecomp/api/Sonicteam/StageSelectMode.h b/MarathonRecomp/api/Sonicteam/StageSelectMode.h new file mode 100644 index 00000000..1544b6fb --- /dev/null +++ b/MarathonRecomp/api/Sonicteam/StageSelectMode.h @@ -0,0 +1,23 @@ +#pragma once + +#include +#include +#include + +namespace Sonicteam +{ + class StageSelectMode : public SoX::Engine::DocMode + { + public: + xpointer m_StageMap; + xpointer m_CurrentStageMap; + MARATHON_INSERT_PADDING(4); + be m_CurrentStageMapIndex; + be m_Deep; + MARATHON_INSERT_PADDING(0x1C); + xpointer> m_StageMapInfo; + xpointer m_StageMapType; + xpointer m_StageMapName; + MARATHON_INSERT_PADDING(0x14); + }; +} diff --git a/MarathonRecomp/gpu/imgui/imgui_snapshot.cpp b/MarathonRecomp/gpu/imgui/imgui_snapshot.cpp index b6454cd0..7fd9fabf 100644 --- a/MarathonRecomp/gpu/imgui/imgui_snapshot.cpp +++ b/MarathonRecomp/gpu/imgui/imgui_snapshot.cpp @@ -7,6 +7,7 @@ #include #include + template void ImFontAtlasSnapshot::SnapPointer(size_t offset, const T1& value, const T2& ptr, size_t count) { diff --git a/MarathonRecomp/gpu/video.cpp b/MarathonRecomp/gpu/video.cpp index 72845414..3f3fc87c 100644 --- a/MarathonRecomp/gpu/video.cpp +++ b/MarathonRecomp/gpu/video.cpp @@ -30,6 +30,7 @@ #include #include #include +#include #include #include #include @@ -1590,6 +1591,7 @@ static void CreateImGuiBackend() MessageWindow::Init(); OptionsMenu::Init(); InstallerWizard::Init(); + DevTitleMenu::Init(); ImGui_ImplSDL2_InitForOther(GameWindow::s_pWindow); @@ -2853,6 +2855,7 @@ static void DrawImGui() ImGui::End(); #endif + DevTitleMenu::Draw(); AchievementMenu::Draw(); // OptionsMenu::Draw(); AchievementOverlay::Draw(); diff --git a/MarathonRecomp/patches/misc_patches.cpp b/MarathonRecomp/patches/misc_patches.cpp index 0620ba93..d5e0d1e4 100644 --- a/MarathonRecomp/patches/misc_patches.cpp +++ b/MarathonRecomp/patches/misc_patches.cpp @@ -2,6 +2,7 @@ #include #include #include +#include // TODO (Hyper): implement achievements menu. void AchievementManagerUnlockMidAsmHook(PPCRegister& id) @@ -103,6 +104,7 @@ void PedestrianAnimationLOD(PPCRegister& val) val.u32 = 0; } + bool DisableHints() { return !Config::Hints; diff --git a/MarathonRecomp/ui/devtitle_menu.cpp b/MarathonRecomp/ui/devtitle_menu.cpp new file mode 100644 index 00000000..854d6c81 --- /dev/null +++ b/MarathonRecomp/ui/devtitle_menu.cpp @@ -0,0 +1,609 @@ +#include "devtitle_menu.h" +#include "imgui_utils.h" +#include +#include +#include +#include +#include +#include + +static ImFont* g_mrodinFont; +static ImFont* g_mnewRodinFont; +const float ScrollAmount = 0.25f; +bool DevTitleMenu::IsVisible = true; + +const float DevTitleDelayDefault = 1.0; +static float DevTitleDelay = DevTitleDelayDefault; + + +enum CenterFlag { + X = 1, + Y = 2 +}; + +struct DevMessage +{ + be id; + be id2; + uint8_t id3; +}; + +struct DevTitleMenuSTable +{ + struct + { + xpointer m_name; + xpointer m_mode; + } m_state[5]; +}; + +static ImVec2 GetMousePos() +{ + return ImGui::GetIO().MousePos; +} + +static void DrawBackgroundDev() +{ + auto& res = ImGui::GetIO().DisplaySize; + auto drawList = ImGui::GetBackgroundDrawList(); + if (Config::DevTitle == EDevTitleMenu::Custom) + { + const uint32_t TOP = IM_COL32(0, 103, 255, 255); + const uint32_t BOTTOM = IM_COL32(0, 40, 100, 255); + drawList->AddRectFilledMultiColor({ 0.0, 0.0 }, res, TOP, TOP, BOTTOM, BOTTOM); + } + else + { + drawList->AddRectFilledMultiColor({ 0.0, 0.0 }, res, 0, 0, 0, 0); + } +} + +static void DrawTextDev(const char* text,float posX, float posY,float vfontSize= 24, uint32_t alpha = 255, uint32_t center = CenterFlag::X) +{ + auto drawList = ImGui::GetBackgroundDrawList(); + auto fontSize = Scale(vfontSize); + auto textSize = g_mnewRodinFont->CalcTextSizeA(fontSize, FLT_MAX, 0, text); + auto cornerRadius = 23; + + float X = Video::s_viewportWidth * posX; + float Y = Video::s_viewportHeight * posY; + + if (center & CenterFlag::X) + X -= textSize.x * 0.5f; + + if (center & CenterFlag::Y) + Y -= textSize.y * 0.5f; + + DrawTextWithOutline + ( + g_mnewRodinFont, + fontSize, + { X,Y }, + IM_COL32(255, 255, 255, alpha), + text, + 4, + IM_COL32(0, 0, 0, alpha) + ); + + ResetTextSkew(); +} + +//05.08.2025 TODO Kanju Display +//31.08.2025 CLANG Version +static std::string ConvertShiftJISToUTF8(const std::string& sjisStr) +{ + int wlen = MultiByteToWideChar(932, 0, sjisStr.c_str(), -1, nullptr, 0); + std::wstring wstr(wlen, 0); + MultiByteToWideChar(932, 0, sjisStr.c_str(), -1, &wstr[0], wlen); + int ulen = WideCharToMultiByte(CP_UTF8, 0, wstr.c_str(), -1, nullptr, 0, nullptr, nullptr); + std::string utf8Str(ulen, 0); + WideCharToMultiByte(CP_UTF8, 0, wstr.c_str(), -1, &utf8Str[0], ulen, nullptr, nullptr); + return utf8Str; +} + +static void IMAddRectangle(ImDrawList* list,const ImVec2& p1, const ImVec2& p2, const ImVec2& p3, const ImVec2& p4, ImU32 col_upr_left, ImU32 col_upr_right, ImU32 col_bot_left,ImU32 col_bot_right) +{ + if (((col_upr_left | col_upr_right | col_bot_right | col_bot_left) & IM_COL32_A_MASK) == 0) + return; + + const ImVec2 uv = list->_Data->TexUvWhitePixel; + list->PrimReserve(6, 4); + list->PrimWriteIdx((ImDrawIdx)(list->_VtxCurrentIdx)); list->PrimWriteIdx((ImDrawIdx)(list->_VtxCurrentIdx + 1)); list->PrimWriteIdx((ImDrawIdx)(list->_VtxCurrentIdx + 2)); + list->PrimWriteIdx((ImDrawIdx)(list->_VtxCurrentIdx)); list->PrimWriteIdx((ImDrawIdx)(list->_VtxCurrentIdx + 2)); list->PrimWriteIdx((ImDrawIdx)(list->_VtxCurrentIdx + 3)); + list->PrimWriteVtx(p1, uv, col_upr_left); + list->PrimWriteVtx(p2, uv, col_upr_right); + list->PrimWriteVtx(p3, uv, col_bot_right); + list->PrimWriteVtx(p4, uv, col_bot_left); +} + +static bool Sonic06Button(ImVec2 pos, const char* label, bool& hovered, const ImVec2& size_arg = {}, + bool selected = false, uint32_t center_flags = 0) +{ + auto* draw_list = ImGui::GetBackgroundDrawList(); + auto& io = ImGui::GetIO(); + const float font_size = size_arg.y / 2.5; + const float skew_amount = 1.0f; + + const auto label_size = g_mnewRodinFont->CalcTextSizeA(font_size, FLT_MAX, 0, label); + const auto size = ImVec2(size_arg.x ? size_arg.x : label_size.x, size_arg.y ? size_arg.y : label_size.y); + const float skew = size.y * skew_amount; + + const ImRect bb(pos, { pos.x + size.x + skew, pos.y + size.y }); + hovered = bb.Contains(GetMousePos()); + bool pressed = hovered && ImGui::IsMouseClicked(ImGuiMouseButton_Left); + + // Colors + ImU32 col_base = 0; + ImU32 col_hover = 0; + if (Config::DevTitle == EDevTitleMenu::Custom) + { + col_base = IM_COL32(0, 6, 142, 255); + col_hover = IM_COL32(0, 26, 162, 255); + } + + const ImU32 bg[4] = + { + hovered ? col_hover : selected ? col_base : 0, + + hovered ? col_hover : selected ? col_base : 0, + + hovered ? col_hover : selected ? col_base : 0, + + hovered ? col_hover : selected ? col_base : 0, + }; + + + const ImVec2 pts[] = { + pos, // top-left + {pos.x + skew, pos.y + size.y}, // bottom-left + {pos.x + size.x , pos.y + size.y}, // bottom-right + {pos.x + size.x, pos.y} // top-right + }; + IMAddRectangle(draw_list, pts[0], pts[1], pts[2], pts[3], bg[0], bg[1], bg[2], bg[3]); + + // Text + ImVec2 text_pos = { pos.x + (2.0f * skew), pos.y }; + + if (center_flags & CenterFlag::X) + text_pos.x = bb.GetCenter().x - label_size.x * 0.5f; + if (center_flags & CenterFlag::Y) + text_pos.y = bb.GetCenter().y - label_size.y * 0.5f; + + draw_list->AddText(g_mnewRodinFont, font_size, { text_pos.x + 1,text_pos.y + 1 }, IM_COL32(0, 0, 0, 120), label); + draw_list->AddText(g_mnewRodinFont, font_size, text_pos, IM_COL32(255, 255, 255, 255), label); + + return pressed; +} + +//DrawStageMapSelector +struct StageMapSelectorConfig { + // Layout + float windowWidthRatio = 0.8f; + float buttonHeightRatio = 0.05f; + float buttonSpacing = 5.0f; + int defaultVisibleItems = 9; + float itemsScaleHeightRatio = 0.05f; + + // Positioning + ImVec2 basePosRatio = { 0.15f, 0.45f }; + ImVec2 buttonOffset = { 10.0f, 0.0 }; + + // Colors + ImU32 tooltipBgColor = IM_COL32(0, 0, 0, 180); + ImU32 tooltipBorderColor = IM_COL32(50, 150, 255, 200); + ImU32 tooltipNameColor = IM_COL32(255, 255, 255, 255); + ImU32 tooltipTextColor = IM_COL32(200, 200, 255, 255); + + // Tooltip + float tooltipPadding = 15.0f; + float tooltipSeparatorWidth = 5.0f; + float tooltipFontSize = 0.0f; // 0 means use current font size + + // Mouse cursor + float cursorRadius = 7.5f; + ImU32 cursorColor = IM_COL32(0, 150, 255, 220); + ImU32 cursorOutlineColor = IM_COL32(255, 255, 255, 180); +}; + +static StageMapSelectorConfig s_StageMapSelectorConfig; + +static void HandleStageMapSelectorInput(Sonicteam::StageSelectMode* pMode, uintptr_t pMsgRec) +{ + auto& io = ImGui::GetIO(); + + // Mouse wheel scrolling + if (abs(io.MouseWheel) > ScrollAmount) { + guest_stack_var vMsgRec(0x10001, io.MouseWheel > 0 ? 0 : 0xB4, 130); + GuestToHostFunction(sub_824A8FF0, pMsgRec, vMsgRec.get()); + } + + // Right click handling + if (ImGui::IsMouseClicked(ImGuiMouseButton_Right)) { + guest_stack_var vMsgRec(0x10002, 0, 0); + GuestToHostFunction(sub_824A8FF0, pMsgRec, vMsgRec.get()); + } +} + +static void DrawStageMapTooltip(stdx::vector>& stages) +{ + if (stages.empty()) return; + + auto& io = ImGui::GetIO(); + auto& config = s_StageMapSelectorConfig; + ImDrawList* draw_list = ImGui::GetForegroundDrawList(); + + const float font_size = config.tooltipFontSize > 0 ? config.tooltipFontSize : ImGui::GetFontSize(); + const float line_spacing = 2.0f; + + // Calculate tooltip size + float max_name_width = 0.0f; + float max_text_width = 0.0f; + float total_height = 0.0f; + std::vector> entries; + + for (auto& stage : stages) { + if (!stage.ptr.get()) continue; + + std::string nameUtf8 = ConvertShiftJISToUTF8(stage->m_Name.c_str()); + std::string textUtf8 = ConvertShiftJISToUTF8(stage->m_Text.c_str()); + entries.emplace_back(nameUtf8, textUtf8); + + const ImVec2 name_size = g_mnewRodinFont->CalcTextSizeA(font_size, FLT_MAX, 0.0f, nameUtf8.c_str()); + const ImVec2 text_size = g_mnewRodinFont->CalcTextSizeA(font_size, FLT_MAX, 0.0f, textUtf8.c_str()); + + max_name_width = ImMax(max_name_width, name_size.x); + max_text_width = ImMax(max_text_width, text_size.x); + total_height += ImMax(name_size.y, text_size.y) + line_spacing; + } + + const float total_width = max_name_width + config.tooltipSeparatorWidth + max_text_width + + config.tooltipPadding * 2; + total_height += config.tooltipPadding * 2 - line_spacing; + + // Position tooltip + ImVec2 mouse_pos = GetMousePos(); + ImVec2 tooltip_pos = ImVec2( + ImClamp(mouse_pos.x - total_width * 0.5f, 10.0f, io.DisplaySize.x - total_width - 10.0f), + ImClamp(mouse_pos.y - total_height - 20.0f, 10.0f, io.DisplaySize.y - total_height - 10.0f) + ); + + if (tooltip_pos.y <= 10) { + tooltip_pos.x += io.DisplaySize.x * 0.040f; + } + + // Draw background + const ImVec2 tooltip_min = tooltip_pos; + const ImVec2 tooltip_max = ImVec2(tooltip_pos.x + total_width, tooltip_pos.y + total_height); + draw_list->AddRectFilled(tooltip_min, tooltip_max, config.tooltipBgColor, 5.0f); + draw_list->AddRect(tooltip_min, tooltip_max, config.tooltipBorderColor, 5.0f, 0, 1.5f); + + // Draw text entries + float current_y = tooltip_pos.y + config.tooltipPadding; + for (const auto& [name, text] : entries) { + const float name_height = g_mnewRodinFont->CalcTextSizeA(font_size, FLT_MAX, 0.0f, name.c_str()).y; + const float text_height = g_mnewRodinFont->CalcTextSizeA(font_size, FLT_MAX, 0.0f, text.c_str()).y; + const float line_height = ImMax(name_height, text_height); + + // Draw name + draw_list->AddText( + g_mnewRodinFont, + font_size, + ImVec2(tooltip_pos.x + config.tooltipPadding, current_y), + config.tooltipNameColor, + name.c_str() + ); + + // Draw text + draw_list->AddText( + g_mnewRodinFont, + font_size, + ImVec2(tooltip_pos.x + config.tooltipPadding + max_name_width + config.tooltipSeparatorWidth, current_y), + config.tooltipTextColor, + text.c_str() + ); + + current_y += line_height + line_spacing; + } +} + +static void DrawStageMapCursor() +{ + auto& config = s_StageMapSelectorConfig; + ImVec2 mouse_pos = GetMousePos(); + + ImGui::GetBackgroundDrawList()->AddCircleFilled( + mouse_pos, config.cursorRadius, config.cursorColor, 12 + ); + ImGui::GetBackgroundDrawList()->AddCircle( + mouse_pos, config.cursorRadius, config.cursorOutlineColor, 12, 1.5f + ); +} + +static void DrawStageMapSelector() +{ + auto& io = ImGui::GetIO(); + auto& res = io.DisplaySize; + auto& config = s_StageMapSelectorConfig; + + auto* pMode = App::s_pApp->m_pDoc->GetDocMode(); + if (!pMode) return; + + DrawBackgroundDev(); + + // Draw HUD if in custom mode + if (Config::DevTitle == EDevTitleMenu::Custom) { + DrawHUD({ 0, 0 }, res, g_mnewRodinFont, pMode->m_StageMapName.get()); + } + + // Calculate layout + int visibleItems = config.defaultVisibleItems; + ImVec2 base_pos = { + (res.x - config.windowWidthRatio * res.x) * config.basePosRatio.x, + (res.y - (config.buttonHeightRatio * res.y + config.buttonSpacing) * visibleItems + + config.itemsScaleHeightRatio * Video::s_viewportHeight) * config.basePosRatio.y + }; + + // Adjust for dev mode + if (Config::DevTitle == EDevTitleMenu::True) { + visibleItems = 18; + base_pos = { 0, 0 }; + } + + auto& items = pMode->m_CurrentStageMap->m_vpStageMap; + const int itemCount = static_cast(items.size()); + int currentIdx = pMode->m_CurrentStageMapIndex.get(); + + // Handle input + auto pMsgRec = reinterpret_cast(static_cast(pMode)); + HandleStageMapSelectorInput(pMode, pMsgRec); + + // Calculate visible range + const int start_idx = std::max(0, currentIdx - visibleItems / 2); + const int end_idx = std::min(itemCount, start_idx + visibleItems); + + // Draw visible items + for (int i = start_idx; i < end_idx; ++i) { + const auto& item = items[i]; + if (!item) continue; + + const bool isSelected = (i == currentIdx); + const int visible_index = i - start_idx; + bool hovered = false; + + // Calculate button position + const ImVec2 button_pos = { + base_pos.x + config.buttonOffset.x, + base_pos.y + config.buttonOffset.y + visible_index * + (config.buttonHeightRatio * res.y + config.buttonSpacing) + }; + + // Format item name + std::string item_name = item->m_Name.c_str(); + if (item->m_vpStageMap.size() > 1) { + item_name = "[" + item_name + "]"; // Mark as group + } + if (isSelected) { + item_name = ">> " + item_name; + } + + // Draw button + if (Sonic06Button(button_pos, item_name.c_str(), hovered, + ImVec2(config.windowWidthRatio * res.x - 20, config.buttonHeightRatio * res.y), + isSelected, CenterFlag::Y)) { + currentIdx = i; + pMode->m_CurrentStageMapIndex = i; + guest_stack_var vMsgRec(0x10002, 0x5A, 1); + GuestToHostFunction(sub_824A8FF0, pMsgRec, vMsgRec.get()); + } + + // Show tooltip if hovered and in custom mode + if (hovered && item->m_vpStageMap.size() > 0 && Config::DevTitle == EDevTitleMenu::Custom) { + DrawStageMapTooltip(item->m_vpStageMap); + } + } + + // Draw mouse cursor in custom mode + if (Config::DevTitle == EDevTitleMenu::Custom) { + DrawStageMapCursor(); + } +} +//DrawStageMapSelector + +static void DrawDevTitle() { + + auto& io = ImGui::GetIO(); + auto* pMode = App::s_pApp->m_pDoc->GetDocMode(); + if (!pMode) + return; + + auto pDevTitle = App::s_pApp->m_pDoc->GetDocMode(); + auto pIndex = pDevTitle->m_Selected.get(); + auto pDevTitleMenuSTable = (DevTitleMenuSTable*)g_memory.Translate(0x82B10A40); + auto pName = fmt::format(">>[{}]<<", pDevTitleMenuSTable->m_state[pIndex].m_name.get()); + + + DrawTextDev(pName.c_str(), 0.5, 0.95, 24, DevTitleDelay * 255, CenterFlag::X | CenterFlag::Y); + auto pMsgRec = (uintptr_t)static_cast(pMode); + + + if ((abs(io.MouseWheel) > ScrollAmount)) + { + guest_stack_var vMsgRec(0x10001, io.MouseWheel >= 0 ? 0 : 0xB4, 130); + GuestToHostFunction(sub_824A0E38, static_cast(pMsgRec), vMsgRec.get()); + } + + if (ImGui::IsMouseClicked(ImGuiMouseButton_Left)) + { + guest_stack_var vMsgRec(0x10002, 0, 1); + GuestToHostFunction(sub_824A0E38, static_cast(pMsgRec), vMsgRec.get()); + } + +} + +static void DevTitleAudioCommon(DevMessage* message) +{ + switch (message->id) + { + case 0x10001: + Game_PlaySound("move"); + break; + case 0x10002: + if (message->id2 == 0x5A || message->id3) + Game_PlaySound("main_deside"); + else + Game_PlaySound("window_close"); + break; + case 0: + if (!message->id3) + Game_PlaySound("window_close"); + break; + } +} + +//Add audio to MessageReciever(StageSelectMode) +PPC_FUNC_IMPL(__imp__sub_824A8FF0); +PPC_FUNC(sub_824A8FF0) +{ + auto pMessage = (DevMessage*)(base + ctx.r4.u32); + DevTitleAudioCommon(pMessage); + __imp__sub_824A8FF0(ctx, base); +} + +//Add audio to MessageReciever(DevTitleMode) +PPC_FUNC_IMPL(__imp__sub_824A0E38); +PPC_FUNC(sub_824A0E38) +{ + auto pMessage = (DevMessage*)(base + ctx.r4.u32); + auto pTitle = (StateDevTitle*)(base + 0x82D35E6C); + if (pTitle->m_PictureTitle->m_State3 >= 4) + return; + + auto pTitleMode = App::s_pApp->m_pDoc->GetDocMode(); + + if (pMessage->id == 0x10002) + { + if (pMessage->id2 == 0x5A || pMessage->id3) + { + auto pPocture = pTitle->m_PictureTitle; + pTitle->m_PictureTitle->m_State3 = 4; + DevTitleDelay = DevTitleDelayDefault; + } + else + { + return; + } + } + + DevTitleAudioCommon(pMessage); + __imp__sub_824A0E38(ctx, base); +} + +PPC_FUNC_IMPL(__imp__sub_82161AB8); +PPC_FUNC(sub_82161AB8) +{ + if (!Config::DevTitle) + { + __imp__sub_82161AB8(ctx, base); + return; + } + + auto pDoc = (Sonicteam::DocMarathonImp*)(base + ctx.r3.u32); + GuestToHostFunction(sub_825E9E28, ctx.r3.u32, ctx.r4.u32); + + //Initialize MainMode + if (!*(uint32_t*)(base + 0x82D35E48)) + *(be*)(base + 0x82D35E48) = GuestToHostFunction(sub_82161150); + + //Initialize DevMode + if (!*(uint32_t*)(base + 0x82D35E78)) + *(be*)(base + 0x82D35E78) = GuestToHostFunction(sub_82161860); + + auto pDevMode = (*(be*)(base + 0x82D35E78)).get(); + + //nah vft call, sub_825E8D78 should do it + //DocSetMode + GuestToHostFunction(sub_825E8D78, ctx.r3.u32, pDevMode); + +} + +//tateDevTitle, vft + 8 +PPC_FUNC_IMPL(__imp__sub_82162298); +PPC_FUNC(sub_82162298) +{ + auto pDoc = (Sonicteam::DocMarathonState*)(base + ctx.r4.u32); + auto pTitle = (StateDevTitle*)(base + ctx.r3.u32); + + if (ctx.r3.u32 && pTitle->m_PictureTitle && pTitle->m_PictureTitle->m_State3 > 4) + { + DevTitleDelay = std::clamp(DevTitleDelay - 0.016666668f, 0.0f, DevTitleDelay); + } + else + { + DevTitleDelay = DevTitleDelayDefault; + } + + if (!GuestToHostFunction(sub_82581C68, ctx.r4.u32)) + { + //Wait for transistion + printf("pTitle->m_PictureTitle->m_State %d\n", pTitle->m_PictureTitle->m_State3.get()); + GuestToHostFunction(sub_824A0870, pTitle->m_PictureTitle.ptr.get(), 0.016666668); + + //Force to wait transistion + if (pTitle->m_PictureTitle->m_State3 != 6) + { + return; + } + } + __imp__sub_82162298(ctx, base); +} + +void DevTitleMenu::Init() +{ + if (!Config::DevTitle) + return; + + g_mrodinFont = ImFontAtlasSnapshot::GetFont("FOT-RodinPro-DB.otf"); + g_mnewRodinFont = ImFontAtlasSnapshot::GetFont("FOT-NewRodinPro-UB.otf"); + +} +void DevTitleMenu::Draw() +{ + + if (!Config::DevTitle) + return; + + if (!App::s_pApp) + return; + + if (!App::s_pApp->m_pDoc) + return; + + if (!App::s_pApp->m_pDoc->m_pDocMode) + return; + + if (!EmbeddedPlayer::s_isActive && Config::DevTitle == EDevTitleMenu::Custom) + EmbeddedPlayer::Init(); + + auto vft = App::s_pApp->m_pDoc->GetDocMode()->m_pVftable.ptr.get(); + switch (vft) + { + //DevTitleMode + case 0x8202B1E0: + { + DevTitleMenu::IsVisible = true; + DrawDevTitle(); + }; + break; + //StageSelect/StoreSelect + case 0x820336A4: + { + DevTitleMenu::IsVisible = true; + DrawStageMapSelector(); + } + break; + defaut: + if (EmbeddedPlayer::s_isActive && DevTitleMenu::IsVisible) + DevTitleMenu::IsVisible = false; + EmbeddedPlayer::Shutdown(); + } +} diff --git a/MarathonRecomp/ui/devtitle_menu.h b/MarathonRecomp/ui/devtitle_menu.h new file mode 100644 index 00000000..69a901b9 --- /dev/null +++ b/MarathonRecomp/ui/devtitle_menu.h @@ -0,0 +1,16 @@ +#pragma once + +#include + +class DevTitleMenu +{ +public: + + static bool IsVisible; + + static void Init(); + static void Draw(); + + static void Close(); + static bool CanClose(); +}; diff --git a/MarathonRecomp/user/config.cpp b/MarathonRecomp/user/config.cpp index 12ad2b13..32f9937d 100644 --- a/MarathonRecomp/user/config.cpp +++ b/MarathonRecomp/user/config.cpp @@ -373,6 +373,13 @@ CONFIG_DEFINE_ENUM_TEMPLATE(EUIAlignmentMode) { "Center", EUIAlignmentMode::Centre } }; +CONFIG_DEFINE_ENUM_TEMPLATE(EDevTitleMenu) +{ + { "false", EDevTitleMenu::False }, + { "true", EDevTitleMenu::True }, + { "custom", EDevTitleMenu::Custom } +}; + #undef CONFIG_DEFINE #define CONFIG_DEFINE(section, type, name, defaultValue) \ ConfigDef Config::name{section, #name, defaultValue}; @@ -381,6 +388,10 @@ CONFIG_DEFINE_ENUM_TEMPLATE(EUIAlignmentMode) #define CONFIG_DEFINE_HIDDEN(section, type, name, defaultValue) \ ConfigDef Config::name{section, #name, defaultValue}; +#undef CONFIG_DEFINE_ENUM_HIDDEN +#define CONFIG_DEFINE_ENUM_HIDDEN(section, type, name, defaultValue) \ + ConfigDef Config::name{section, #name, defaultValue, &g_##type##_template}; + #undef CONFIG_DEFINE_LOCALISED #define CONFIG_DEFINE_LOCALISED(section, type, name, defaultValue) \ extern CONFIG_LOCALE g_##name##_locale; \ diff --git a/MarathonRecomp/user/config.h b/MarathonRecomp/user/config.h index 4ce76611..eb3fc0a0 100644 --- a/MarathonRecomp/user/config.h +++ b/MarathonRecomp/user/config.h @@ -150,6 +150,17 @@ enum class EPlayerCharacter : uint32_t Rouge, Knuckles }; +enum class EDevTitleMenu : uint32_t +{ + False, + True, + Custom +}; + +inline uint32_t operator!(EDevTitleMenu value) +{ + return (uint32_t)value == 0; +} template class ConfigDef final : public IConfigDef @@ -219,6 +230,7 @@ class ConfigDef final : public IConfigDef #define CONFIG_DEFINE_HIDDEN(section, type, name, defaultValue) CONFIG_DECLARE_HIDDEN(type, name) #define CONFIG_DEFINE_LOCALISED(section, type, name, defaultValue) CONFIG_DECLARE(type, name) #define CONFIG_DEFINE_ENUM(section, type, name, defaultValue) CONFIG_DECLARE(type, name) +#define CONFIG_DEFINE_ENUM_HIDDEN(section, type, name, defaultValue) CONFIG_DECLARE_HIDDEN(type, name) #define CONFIG_DEFINE_ENUM_LOCALISED(section, type, name, defaultValue) CONFIG_DECLARE(type, name) class Config diff --git a/MarathonRecomp/user/config_def.h b/MarathonRecomp/user/config_def.h index a1eda6fc..0bfbf299 100644 --- a/MarathonRecomp/user/config_def.h +++ b/MarathonRecomp/user/config_def.h @@ -91,6 +91,7 @@ CONFIG_DEFINE_HIDDEN("Codes", bool, DisableEdgeGrabLeftover, false); CONFIG_DEFINE_HIDDEN("Codes", bool, TailsGauge, false); CONFIG_DEFINE_HIDDEN("Codes", bool, EnableDebugMode, false); CONFIG_DEFINE_HIDDEN("Codes", bool, RestoreDemoCameraMode, false); +CONFIG_DEFINE_ENUM_HIDDEN("Codes", EDevTitleMenu, DevTitle, EDevTitleMenu::False); CONFIG_DEFINE_HIDDEN("Codes", bool, MidairControlForMachSpeed, false); CONFIG_DEFINE_HIDDEN("Codes", bool, MidairControlForSnowboards, false); CONFIG_DEFINE_HIDDEN("Codes", bool, ControllableTeleportDash, false);