From 27f55e1e117c4aaf65222ffcedca5387f8b6e446 Mon Sep 17 00:00:00 2001 From: Caladius Date: Thu, 8 Jan 2026 20:41:39 -0500 Subject: [PATCH] Implement Modern Menu UI (#66) * Implement Modern Menu UI * Implement missing menu options * Add build.c to gitignore --- .gitignore | 3 +- CMakeLists.txt | 44 + cmake/ghostship-cvars.cmake | 30 + cmake/lus-cvars.cmake | 21 + include/variables.h | 24 + port/fonts/Fipps-Regular.otf | Bin 0 -> 34220 bytes port/fonts/Inconsolata-Regular.ttf | Bin 0 -> 101752 bytes port/fonts/Montserrat-Regular.ttf | Bin 0 -> 197624 bytes port/fonts/PressStart2P-Regular.ttf | Bin 0 -> 116008 bytes port/textures/buttons/ABtn.png | Bin 0 -> 1485 bytes port/textures/buttons/ABtnOutline.png | Bin 0 -> 1818 bytes port/textures/buttons/AnalogStick.png | Bin 0 -> 2206 bytes port/textures/buttons/AnalogStickOutline.png | Bin 0 -> 3677 bytes port/textures/buttons/BBtn.png | Bin 0 -> 1534 bytes port/textures/buttons/BBtnOutline.png | Bin 0 -> 1878 bytes port/textures/buttons/CDown.png | Bin 0 -> 1285 bytes port/textures/buttons/CDownOutline.png | Bin 0 -> 1544 bytes port/textures/buttons/CLeft.png | Bin 0 -> 1291 bytes port/textures/buttons/CLeftOutline.png | Bin 0 -> 1544 bytes port/textures/buttons/CRight.png | Bin 0 -> 1336 bytes port/textures/buttons/CRightOutline.png | Bin 0 -> 1541 bytes port/textures/buttons/CUp.png | Bin 0 -> 1305 bytes port/textures/buttons/CUpOutline.png | Bin 0 -> 1546 bytes port/textures/buttons/DPadDown.png | Bin 0 -> 946 bytes port/textures/buttons/DPadDownOutline.png | Bin 0 -> 984 bytes port/textures/buttons/DPadLeft.png | Bin 0 -> 929 bytes port/textures/buttons/DPadLeftOutline.png | Bin 0 -> 953 bytes port/textures/buttons/DPadRight.png | Bin 0 -> 922 bytes port/textures/buttons/DPadRightOutline.png | Bin 0 -> 954 bytes port/textures/buttons/DPadUp.png | Bin 0 -> 968 bytes port/textures/buttons/DPadUpOutline.png | Bin 0 -> 993 bytes .../buttons/InputViewerBackground.png | Bin 0 -> 941 bytes port/textures/buttons/LBtn.png | Bin 0 -> 1031 bytes port/textures/buttons/LBtnOutline.png | Bin 0 -> 1089 bytes port/textures/buttons/Mod1.png | Bin 0 -> 1166 bytes port/textures/buttons/Mod1Outline.png | Bin 0 -> 1180 bytes port/textures/buttons/Mod2.png | Bin 0 -> 1110 bytes port/textures/buttons/Mod2Outline.png | Bin 0 -> 1192 bytes port/textures/buttons/RBtn.png | Bin 0 -> 992 bytes port/textures/buttons/RBtnOutline.png | Bin 0 -> 1074 bytes port/textures/buttons/RightStick.png | Bin 0 -> 2150 bytes port/textures/buttons/RightStickOutline.png | Bin 0 -> 2407 bytes port/textures/buttons/StartBtn.png | Bin 0 -> 973 bytes port/textures/buttons/StartBtnOutline.png | Bin 0 -> 1031 bytes port/textures/buttons/ZBtn.png | Bin 0 -> 1005 bytes port/textures/buttons/ZBtnOutline.png | Bin 0 -> 1069 bytes port/textures/icons/g2ShipIcon.png | Bin 0 -> 13286 bytes src/audio/load.c | 6 +- src/audio/synthesis.c | 3 +- src/game/debug.c | 30 +- src/game/interaction.c | 2 +- src/game/level_update.c | 10 +- src/game/main.h | 2 +- src/game/rendering_graph_node.c | 2 +- src/menu/star_select.c | 12 +- src/menu/title_screen.c | 18 +- src/port/Engine.cpp | 71 +- src/port/Engine.h | 10 + src/port/ShipInit.hpp | 44 + src/port/build.c.in | 15 + src/port/build.h | 24 + src/port/mods/PortEnhancements.c | 10 +- src/port/ui/GhostshipGui.cpp | 85 + src/port/ui/GhostshipGui.hpp | 15 + src/port/ui/GhostshipInputEditorWindow.cpp | 1497 ++++++++++++++ src/port/ui/GhostshipInputEditorWindow.h | 104 + src/port/ui/GhostshipMenu.cpp | 215 ++ src/port/ui/GhostshipMenu.h | 30 + src/port/ui/GhostshipMenuDevTools.cpp | 90 + src/port/ui/GhostshipMenuEnhancements.cpp | 69 + src/port/ui/GhostshipMenuSettings.cpp | 381 ++++ src/port/ui/GhostshipModals.cpp | 83 + src/port/ui/GhostshipModals.h | 19 + src/port/ui/ImguiUI.cpp | 576 ------ src/port/ui/ImguiUI.h | 16 - src/port/ui/InputViewer.cpp | 740 +++++++ src/port/ui/InputViewer.h | 46 + src/port/ui/Menu.cpp | 941 +++++++++ src/port/ui/Menu.h | 64 + src/port/ui/MenuTypes.h | 336 ++++ src/port/ui/Notification.cpp | 27 +- src/port/ui/Notification.h | 12 +- src/port/ui/ResolutionEditor.cpp | 932 +++++---- src/port/ui/ResolutionEditor.h | 25 +- src/port/ui/SaveEditor.cpp | 78 +- src/port/ui/SaveEditor.h | 4 +- src/port/ui/UIWidgets.cpp | 1763 +++++++++-------- src/port/ui/UIWidgets.h | 210 -- src/port/ui/UIWidgets.hpp | 1055 ++++++++++ src/port/ui/cvar_prefixes.h | 22 + src/port/ui/enhancementTypes.h | 1 + 91 files changed, 7724 insertions(+), 2093 deletions(-) create mode 100644 cmake/ghostship-cvars.cmake create mode 100644 cmake/lus-cvars.cmake create mode 100644 include/variables.h create mode 100644 port/fonts/Fipps-Regular.otf create mode 100644 port/fonts/Inconsolata-Regular.ttf create mode 100644 port/fonts/Montserrat-Regular.ttf create mode 100644 port/fonts/PressStart2P-Regular.ttf create mode 100644 port/textures/buttons/ABtn.png create mode 100644 port/textures/buttons/ABtnOutline.png create mode 100644 port/textures/buttons/AnalogStick.png create mode 100644 port/textures/buttons/AnalogStickOutline.png create mode 100644 port/textures/buttons/BBtn.png create mode 100644 port/textures/buttons/BBtnOutline.png create mode 100644 port/textures/buttons/CDown.png create mode 100644 port/textures/buttons/CDownOutline.png create mode 100644 port/textures/buttons/CLeft.png create mode 100644 port/textures/buttons/CLeftOutline.png create mode 100644 port/textures/buttons/CRight.png create mode 100644 port/textures/buttons/CRightOutline.png create mode 100644 port/textures/buttons/CUp.png create mode 100644 port/textures/buttons/CUpOutline.png create mode 100644 port/textures/buttons/DPadDown.png create mode 100644 port/textures/buttons/DPadDownOutline.png create mode 100644 port/textures/buttons/DPadLeft.png create mode 100644 port/textures/buttons/DPadLeftOutline.png create mode 100644 port/textures/buttons/DPadRight.png create mode 100644 port/textures/buttons/DPadRightOutline.png create mode 100644 port/textures/buttons/DPadUp.png create mode 100644 port/textures/buttons/DPadUpOutline.png create mode 100644 port/textures/buttons/InputViewerBackground.png create mode 100644 port/textures/buttons/LBtn.png create mode 100644 port/textures/buttons/LBtnOutline.png create mode 100644 port/textures/buttons/Mod1.png create mode 100644 port/textures/buttons/Mod1Outline.png create mode 100644 port/textures/buttons/Mod2.png create mode 100644 port/textures/buttons/Mod2Outline.png create mode 100644 port/textures/buttons/RBtn.png create mode 100644 port/textures/buttons/RBtnOutline.png create mode 100644 port/textures/buttons/RightStick.png create mode 100644 port/textures/buttons/RightStickOutline.png create mode 100644 port/textures/buttons/StartBtn.png create mode 100644 port/textures/buttons/StartBtnOutline.png create mode 100644 port/textures/buttons/ZBtn.png create mode 100644 port/textures/buttons/ZBtnOutline.png create mode 100644 port/textures/icons/g2ShipIcon.png create mode 100644 src/port/ShipInit.hpp create mode 100644 src/port/build.c.in create mode 100644 src/port/build.h create mode 100644 src/port/ui/GhostshipGui.cpp create mode 100644 src/port/ui/GhostshipGui.hpp create mode 100644 src/port/ui/GhostshipInputEditorWindow.cpp create mode 100644 src/port/ui/GhostshipInputEditorWindow.h create mode 100644 src/port/ui/GhostshipMenu.cpp create mode 100644 src/port/ui/GhostshipMenu.h create mode 100644 src/port/ui/GhostshipMenuDevTools.cpp create mode 100644 src/port/ui/GhostshipMenuEnhancements.cpp create mode 100644 src/port/ui/GhostshipMenuSettings.cpp create mode 100644 src/port/ui/GhostshipModals.cpp create mode 100644 src/port/ui/GhostshipModals.h delete mode 100644 src/port/ui/ImguiUI.cpp delete mode 100644 src/port/ui/ImguiUI.h create mode 100644 src/port/ui/InputViewer.cpp create mode 100644 src/port/ui/InputViewer.h create mode 100644 src/port/ui/Menu.cpp create mode 100644 src/port/ui/Menu.h create mode 100644 src/port/ui/MenuTypes.h delete mode 100644 src/port/ui/UIWidgets.h create mode 100644 src/port/ui/UIWidgets.hpp create mode 100644 src/port/ui/cvar_prefixes.h create mode 100644 src/port/ui/enhancementTypes.h diff --git a/.gitignore b/.gitignore index f6a99aec..b7e18b9d 100644 --- a/.gitignore +++ b/.gitignore @@ -93,4 +93,5 @@ merge/ modifications/ modding/ *.log -node_modules \ No newline at end of file +node_modules +/src/port/build.c diff --git a/CMakeLists.txt b/CMakeLists.txt index 5e13c4d8..3c052c98 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -4,6 +4,9 @@ cmake_minimum_required(VERSION 3.16.0 FATAL_ERROR) project(Ghostship VERSION 1.0.0 LANGUAGES C CXX ASM) include(FetchContent) +include(CMake/ghostship-cvars.cmake) +include(CMake/lus-cvars.cmake) + set(NATO_PHONETIC_ALPHABET "Alfa" "Bravo" "Charlie" "Delta" "Echo" "Foxtrot" "Golf" "Hotel" "India" "Juliett" "Kilo" "Lima" "Mike" "November" "Oscar" "Papa" @@ -20,6 +23,41 @@ list(GET NATO_PHONETIC_ALPHABET ${PATCH_INDEX} PROJECT_PATCH_WORD) set(PROJECT_BUILD_NAME "Barnard ${PROJECT_PATCH_WORD}" CACHE STRING "" FORCE) set(PROJECT_TEAM "Lywx" CACHE STRING "" FORCE) +# Get Git Information +execute_process( + COMMAND git branch --show-current + WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} + OUTPUT_VARIABLE GIT_BRANCH + OUTPUT_STRIP_TRAILING_WHITESPACE +) + +set(CMAKE_PROJECT_GIT_BRANCH "${GIT_BRANCH}" CACHE STRING "Git branch" FORCE) + +execute_process( + COMMAND git rev-parse HEAD + WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} + OUTPUT_VARIABLE GIT_COMMIT_HASH + OUTPUT_STRIP_TRAILING_WHITESPACE +) + +# Get only the first 7 characters of the hash +string(SUBSTRING "${GIT_COMMIT_HASH}" 0 7 SHORT_COMMIT_HASH) + +set(CMAKE_PROJECT_GIT_COMMIT_HASH "${SHORT_COMMIT_HASH}" CACHE STRING "Git commit hash" FORCE) + +execute_process( + COMMAND git describe --tags --abbrev=0 --exact-match HEAD + WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} + OUTPUT_VARIABLE GIT_COMMIT_TAG + OUTPUT_STRIP_TRAILING_WHITESPACE +) + +if(NOT GIT_COMMIT_TAG) + set(GIT_COMMIT_TAG "" CACHE STRING "Git commit tag" FORCE) +endif() + +set(CMAKE_PROJECT_GIT_COMMIT_TAG "${GIT_COMMIT_TAG}" CACHE STRING "Git commit tag" FORCE) + if(APPLE) enable_language(OBJCXX) endif() @@ -65,6 +103,11 @@ set(CMAKE_XCODE_ATTRIBUTE_DEVELOPMENT_TEAM "YOUR_TEAM_ID") set(PROJECT_ID "dev.net64.game") endif() +################################################################################ +# Sources +################################################################################ +configure_file( ${CMAKE_CURRENT_SOURCE_DIR}/src/port/build.c.in ${CMAKE_CURRENT_SOURCE_DIR}/src/port/build.c @ONLY) + ################################################################################ # Set target arch type if empty. Visual studio solution generator provides it. ################################################################################ @@ -154,6 +197,7 @@ file(GLOB_RECURSE ALL_FILES RELATIVE ${CMAKE_CURRENT_SOURCE_DIR} "lib/src/guScaleF.c" "lib/src/guLookAtF.c" "lib/src/guTranslateF.c" + "include/**/*" "sound/*.c" "src/audio/*.h" "src/audio/*.c" diff --git a/cmake/ghostship-cvars.cmake b/cmake/ghostship-cvars.cmake new file mode 100644 index 00000000..227cfb2b --- /dev/null +++ b/cmake/ghostship-cvars.cmake @@ -0,0 +1,30 @@ +set(CVAR_PREFIX_RANDOMIZER_ENHANCEMENT "gRandoEnhancements") +set(CVAR_PREFIX_RANDOMIZER_SETTING "gRandoSettings") +set(CVAR_PREFIX_COSMETIC "gCosmetics") +set(CVAR_PREFIX_AUDIO "gAudioEditor") +set(CVAR_PREFIX_CHEAT "gCheats") +set(CVAR_PREFIX_ENHANCEMENT "gEnhancements") +set(CVAR_PREFIX_SETTING "gSettings") +set(CVAR_PREFIX_WINDOW "gOpenWindows") +set(CVAR_PREFIX_TRACKER "gTrackers") +set(CVAR_PREFIX_DEVELOPER_TOOLS "gDeveloperTools") +set(CVAR_PREFIX_GENERAL "gGeneral") +set(CVAR_PREFIX_REMOTE "gRemote") +set(CVAR_PREFIX_GAMEPLAY_STATS "gGameplayStats") +set(CVAR_PREFIX_TIME_DISPLAY "gTimeDisplay") +add_compile_definitions( + CVAR_PREFIX_RANDOMIZER_ENHANCEMENT="${CVAR_PREFIX_RANDOMIZER_ENHANCEMENT}" + CVAR_PREFIX_RANDOMIZER_SETTING="${CVAR_PREFIX_RANDOMIZER_SETTING}" + CVAR_PREFIX_COSMETIC="${CVAR_PREFIX_COSMETIC}" + CVAR_PREFIX_AUDIO="${CVAR_PREFIX_AUDIO}" + CVAR_PREFIX_CHEAT="${CVAR_PREFIX_CHEAT}" + CVAR_PREFIX_ENHANCEMENT="${CVAR_PREFIX_ENHANCEMENT}" + CVAR_PREFIX_SETTING="${CVAR_PREFIX_SETTING}" + CVAR_PREFIX_WINDOW="${CVAR_PREFIX_WINDOW}" + CVAR_PREFIX_TRACKER="${CVAR_PREFIX_TRACKER}" + CVAR_PREFIX_DEVELOPER_TOOLS="${CVAR_PREFIX_DEVELOPER_TOOLS}" + CVAR_PREFIX_GENERAL="${CVAR_PREFIX_GENERAL}" + CVAR_PREFIX_REMOTE="${CVAR_PREFIX_REMOTE}" + CVAR_PREFIX_GAMEPLAY_STATS="${CVAR_PREFIX_GAMEPLAY_STATS}" + CVAR_PREFIX_TIME_DISPLAY="${CVAR_PREFIX_TIME_DISPLAY}" +) \ No newline at end of file diff --git a/cmake/lus-cvars.cmake b/cmake/lus-cvars.cmake new file mode 100644 index 00000000..e4c54c4a --- /dev/null +++ b/cmake/lus-cvars.cmake @@ -0,0 +1,21 @@ +set(CVAR_VSYNC_ENABLED "${CVAR_PREFIX_SETTING}.VsyncEnabled") +set(CVAR_Z_FIGHTING_MODE "${CVAR_PREFIX_SETTING}.ZFightingMode") +set(CVAR_INTERNAL_RESOLUTION "${CVAR_PREFIX_SETTING}.InternalResolution") +set(CVAR_MSAA_VALUE "${CVAR_PREFIX_SETTING}.MSAAValue") +set(CVAR_SDL_WINDOWED_FULLSCREEN "${CVAR_PREFIX_SETTING}.SdlWindowedFullscreen") +set(CVAR_TEXTURE_FILTER "${CVAR_PREFIX_SETTING}.TextureFilter") +set(CVAR_IMGUI_CONTROLLER_NAV "${CVAR_PREFIX_SETTING}.ControlNav") +set(CVAR_CONSOLE_WINDOW_OPEN "${CVAR_PREFIX_WINDOW}.Console") +set(CVAR_CONTROLLER_CONFIGURATION_WINDOW_OPEN "${CVAR_PREFIX_WINDOW}.ControllerConfiguration") +set(CVAR_CONTROLLER_DISCONNECTED_WINDOW_OPEN "${CVAR_PREFIX_WINDOW}.ControllerDisconnected") +set(CVAR_CONTROLLER_REORDERING_WINDOW_OPEN "${CVAR_PREFIX_WINDOW}.ControllerReordering") +set(CVAR_GFX_DEBUGGER_WINDOW_OPEN "${CVAR_PREFIX_WINDOW}.GfxDebugger") +set(CVAR_STATS_WINDOW_OPEN "${CVAR_PREFIX_WINDOW}.Stats") +set(CVAR_ENABLE_MULTI_VIEWPORTS "${CVAR_PREFIX_SETTING}.EnableMultiViewports") +set(CVAR_LOW_RES_MODE "${CVAR_PREFIX_SETTING}.LowResMode") +set(CVAR_SIMULATED_INPUT_LAG "${CVAR_PREFIX_SETTING}.SimulatedInputLag") +set(CVAR_GAME_OVERLAY_FONT "${CVAR_PREFIX_SETTING}.OverlayFont") +set(CVAR_MENU_BAR_OPEN "${CVAR_PREFIX_SETTING}.OpenMenuBar") +set(CVAR_PREFIX_CONTROLLERS "${CVAR_PREFIX_SETTING}.Controllers") +set(CVAR_PREFIX_ADVANCED_RESOLUTION "${CVAR_PREFIX_SETTING}.AdvancedResolution") +include("libultraship/cmake/cvars.cmake") diff --git a/include/variables.h b/include/variables.h new file mode 100644 index 00000000..a3494d66 --- /dev/null +++ b/include/variables.h @@ -0,0 +1,24 @@ +#ifndef VARIABLES_H +#define VARIABLES_H + +#define BAD_PI 3.141592654 +#define BAD_DTOR (BAD_PI/ 180.0) +#define M_TAU (2*M_PI) +#define BAD_TAU 6.2831853 +#define M_PI 3.14159265358979323846 + +struct Overlay { + void *start; + void *end; +}; + +extern struct Overlay gOverlayTable[]; + +extern f32 climbPoleBottom[3]; +extern f32 climbPoleTop[3]; + +extern const char gBuildVersion[]; +extern const char gGitBranch[]; +extern const char gGitCommitHash[]; +extern u8 gGitCommitTag[]; +#endif diff --git a/port/fonts/Fipps-Regular.otf b/port/fonts/Fipps-Regular.otf new file mode 100644 index 0000000000000000000000000000000000000000..9334dad594277ba8339786217d75260e58436dc8 GIT binary patch literal 34220 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 SXU=y99^PLyB0t}XG5`Q>oR^&d literal 0 HcmV?d00001 diff --git a/port/fonts/Inconsolata-Regular.ttf b/port/fonts/Inconsolata-Regular.ttf new file mode 100644 index 0000000000000000000000000000000000000000..d1241516bc2357fadd283371e34055d9b1cc89a8 GIT binary patch literal 101752 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 literal 0 HcmV?d00001 diff --git a/port/fonts/PressStart2P-Regular.ttf b/port/fonts/PressStart2P-Regular.ttf new file mode 100644 index 0000000000000000000000000000000000000000..2442affbf6570d23c0b2b270cca8d49535df27bf GIT binary patch literal 116008 zcmZQzWME(rU}RumVPJ4~adl(rVya4a7{qG7M#b~nd}UyU z=(wV;@C~HuO4;K{f3E%OVtxS@2iXD=W?*1pe!)0_ft7)Qsf#Isfq}srLNmrO7%?6K zDPaW>V9eAGrl5p#WRNSv0S1QOOkfikm|rlKDKIe12T3!%1B)pzFf(}XU^o#Jv5}Fn zFLDFpP7VeRhU9{c4D3!@b3$xXSQxZGYzMAbE>|vHE^$s44yZsTTOgYyn-VKnfZJ)W zc!OXAcLN(JJ#lJnWZ-w&%Wwb+Sr|CAHZn7~Y*b2lf#fhnw^D5YX{^1)(tGZ8<`kfHnXO%F}S#ZoKGD7#qf)% zkYNr(EkiA1IYTV-3x-;zcMP?l_yL78GLHd^TILt1s*(9LR}W5e3|tIx4F4FIT-NqOY#`_7~~ijogG6I7`PZ1{{IK3Mpgz625tr(27U%1uoxQyBLfQqI|CO3AA=w$ ze?vtX7?>H@7&sYt83Y)F6BBbv7-k`p$;r6|3{#NFw4%ghhCXC6Co?^fp#z1?PtIXz z$}dXEW2izVi%ScN8A^~za6SU%CME_}1~xE{2~4tsNfs~(DwRN(oAC+b0>&6d7e)?- z0}S&R$`~>jtQaJ~xrmX0gMo>GkAa2ZJHrn!2KkqT!IPny;UL(*AQg-ZaturiA`D#M z(AdGi#Ka9|IY3!V3^oi~;N~$h=7D8F;m*Op$RNc4GJ}bMhk=dZ8v_%=7KX13ObkoF zxqUX09U@?Lj0`MHr3}mr2bqMyDxAUoV`5NY;9_8AU}P|1U4`64uWMs z_6UJ}&H#2P69WrFHK^=i{0<5nB%5>?m>9&sYFL;!7?_zP8GnOAfsKhB!ee3s(?1y) z8JHM)zXl8(X2nuT*u$`drV}zHSAl*W6aVDk;rb@6Kps<95HWPyc11C5hzcVal z_`$@;#KgqR#KOSHz{voK)g=rE8GkVTWc&qoKQF@~P}s3>GB7aQV#0WHKZ(q%!0&6f>kUq%bHjlrrQoq%fp16fr0;lrUs~)dn+oGWakkF!(bRfYlI}{ia z!9H?i2xI`UOBk#e6vzr6)G#e)06{&5DsWh(G8i*hFz7MlF{Cn-Fz7QlGvtFq!iIDIIA?M-C>#R#Uqlt67h1{E;uV&r0+$Ry9SirI*H6N?MW5!M*i zQ)~ikacr;H-Pl)h2yhf~+~f@ByulU5)x!0RJCb`pj|NXM&lX-e-gw^Yd<=Xgd|&x9 z`9BHR31kY)7x*ctESM>{Qt*e6x6mY^^TJ}n!NQ9~cttElN=1H&P7qx$dS6UKEK;mP zY?s()aeeV>@zW9t5@{0qCB8`}O7528l!}$QAnhqVS^Aw!s>~9Zo3cW(NwQbvV&%Tc zx60p82vfMBXsNhO@rDwQQh-vM(nh5(%5KUNm2avjs1&NKSCv<7R6VDrtCpj7Up-NM zjfSX3l*TblSIrYz{#vKCd9*uq#C4|Ve9|q`J*5|;_dq{G|B3;>fvv$jLl#3f!!?F) zjg*Z#jV>4q8v7d`F;Ow8GC60eXqTEn{0dY6rmO}fouo42-6wr}mi>}u_H*$dly**Dnlw14iv?~viJ+~I|z ztYfp|E+PP#g{wz}?c{otnR7UVY7?Vr1Yd%gQ#k9v=5p241r zJ&${S^wRMv@LKD2)mzj%#e0qS2OkrkMxWij8ouqm7yYFDBK0&h`hg6GjMJGsnPHi`vedE)vyNp;WhZ5?%VEh$%2}E7A=fc?V(x`J!MxbK#d){# z4f2!ncjkXDC@DB!C{mbMxS;TJk$=(hVuj+|;`PNhO885RN{UL>ms~7mFAXYPSf*I! zQ&v^Bxa>wbOSwgPVfnK1M-|c)Q56d+o>j_MCRNU>yjdkz6;-vM>PfYK^`;uOn!uW# znp?GcwX^G3>k{iG*6ptQU$0$XT7RX1t-+z8qG5i+&qnRW`o_CWdQI6)=bNpYJDTsd z7`9Alx!-EiI;r($n?hS*+p@MN?RxEH?N2&PI%ah|==k3m+u7YYxpQ9^Q6w|fM7X7=pvW$O*;jp?1+C)JnRx2o@XziNMJ|NRMh6ShuNoES85!Nj|h zv?paxdNo;j^5)6+rnpZzH09e=lc`6i{+VVxEqmJP>73Kerx#4$Im2>B$&4K{zRt9s z**WvkEUQ`bXLHTYn0;oB=bUYGp3IG&+d7YJUiG~D^A+b$o&Re=*n*P_J}p#TShR4% zBECh77Hcg&x`by*^^$2z_APm_lxwN^()6X%mp)zkcUjJ|N6XolD=rUSzH#~a6@n{* zR+O!nwc_8(q?NTRXRO@3^8CuztBO|rTphl8>*|+lWY+|(DPA*e&Gt2y*0QW+S*yOb zcI|_8PV45c`?ubH{f70wHrQ?`+^}OK<3`Sn#v2!Jyt9dUlgXydn~gR{Y_8wDWApbd zmRlmXOxm(@%iS%Xw#sfb-aB$Ya%Lo4*vOE-fC=c921+{A! z7(_s=4+ap;!uExMfngIv0|Og_5d#AQFM~dVB||-<2V){*J>w)MX{J3)$C*wu-DPHE zR%140u41lZ?qcp?p1{12c_s4(=9A3lSfp5#S*%!`S>joeS+ZG5S?01VW!cEGo8=J8 z8J3$Y_gP-D{A877RaFpGkW!FSP*PA+Fi@~ka8W2xSfj9BVT-~Jg}sVmib{%;m6(*+ zl=zhdl|+@Kl;o7Ol{}OZlu}hUscuuf>STQVM^kPh5oWvx>w1?>!(@Cc5%nWchw=nlG_c2doUd+6Tc_Z^F76uk+78Mq2 z78jNzmMoScmI*8iSXQxYV>!Tbn&mvp9hOHd?^vZ+RTP93Bot&66u@q_gt~bhlA9-B zb#n@mn_0nbPF2hL$nfL;Z_ua#4F44V$ultgQv}KWdC$P`R};j-j~|JGz!-EeeJnUg$c-Z!^^3=e`Ics+1>;P^oMf#m~(2PzMk?>}W=xU>4siaU$%EVwi2PRt$VJB)W2ZvPeR z=gegN!T{=BF)*xSU|=}K7{auK=@J72GaCpqb1=ikJ{TAnnE4nOm?c1VgGgp2W-SH= z<`@PB<~HU%%=?%RFdt)n!2F8&2MZGm8w&@E42u$r0gD@p4~riI14{r)2uLRkvw%i% zpd1#g9e5Tqs5EwpnMr_2j7f;ej46yMi%FbGo=Jg;lgW{Zk4c3|m`Rd}mq~)joympC zjY)(llYxbSpFw~@j6sS)g+Y};i$Rycg29TxhQW!!nZc7GgdvO}oFSSafgyzYt9R}60$-ZFe-l4dGol40^;GG;Pj5@a%AvSBi1vS%t{%3*3{%4PV@ zq{igSq{@`X)X7x9%fQXh#30Df!63rW%^=Fq!yv@a$sobd&mhS#fkBC3CW8#a zWCjI>=?uyYvlwIVLpQn!(s+)hD8kK4C@$78P+fuFf3;< zV_3^z%&?R}k6|@~CBp^=Ylck>n;C2wwldf=Y-g}z*v8<%u!F&oVJCwN!yX1VhJ6gK z40{>e8TK;-Fq~lUW;o2?&v2X}nBg=-1j7Y}D27W6kqj3ZVi>M4#4}uHh-0|M5X*3t zA&KD*Ln6a%hCGHB4A~4%8PXUYFyt~kXUJlB!jQx8jG>U>HA5-GdxkQG3WiS%RSaJk zDj7aA++;{*xXaYS)WlTFRL@k$RL<1M)W+1!)Wy`p)WOut)W>*|$$;?`<7vh-jOQ57 zGrncKz<81IGvgP=uZ*u5KQO*we9!oi@g3tQMn*;^MoosljEam3jLM8EjH--sjPi_X zjGT<@jKYjk3_lruG5luu!|DxDhEbMLno)^S zol%1^m@$MglrfAkk}--gmNAYoo-u(ji7}5cpRtg!fU$_Nn6ZqpoUxR#hOv>ci7}cn zhOwD3oUxj*im{flj6JNTw(zPbLr0s2-@$0F7r3AzTY}D~Kk9 zLF38D7@K-zd3v+a-9w4})HI6{ci_?savv@^m^ercE(}tGO%5au!ytKdHnJE@jv6$` zTx5*R%v$CbjOCzwhK8}J#U%!^1D6~=agaU`Mpgsj!!WWqC2Wv+l<0@)1(^fFFg_ug z7_}fZFbp#foevWyl?IuEOAkm6hHTrppy9J~Ugwe(E@j>cA7@r!D95HG@>OdHmT3q5FJ!lwQ=77YA z(esP>1p_h$>BEMR)xgY!iGkFE*dQ8&LGmCzhz9XN7{*7ZL2@t*QV+r~K1?l$4-yAq zkQg!siNWNNX%HKPVe%k02!r@AjE#>h2D1mG9wZLJFgA<^iBSvVGZQ2S!Z5o)>|c=j z1svZXJ_v*4L3Awx!(S@~hQIR|82$<|F#MGPwVN1f8OuQsG_%X_3q+#xKw=EF8yIvH zHZX)nxJD`{c<*4~3-sQ=7916^!9;q4Mr1;oLRVaNNm*e7gR)cN2BxI64UEYf7!x+IYUyrZ*HHjDmD@Ra2Ma@z z0>}p&91{}Tf`V(8av~^R zz~RLp4GPl@3ep=K;GwjESyegF1!4s^4(%IM0wOjrfOJ8HgR(?yGAHV zD=S87De7+E)!E3zQFe;lzzgC9MJR0GRd$Nh z-N3J-ut5MEBnmrN7?Qh`6F2ZVCn$6!C~V+Uc2d^eAfV$N60wmb#U)Z@18=Hwh3*DH z9q+&h?_h|av|^;n24S$ckj_R1e&^7Ljr{!18yFLvHt=hM((DE%Ri}+SAcB`k)oBBx zn6knKX4Qa*ji7|QfkoA+t4jfFt(Kzl2F64fTN)w{(!!*gxPebgF>(Wsv-<{KXZM5+ zOsG+%umNiXZe(B(R(9FI=)8f!ZWALTBRJm(>o9C&U~&zK*ubysq~N-NTiIy?m+}Ud z1Sf@r4UCCf1qB%y_!yj=x)L@Dv1xDU)RtC?)ZHMWvynkqTX%z~&PGNMEvB=P2}FzQ zY-9$}5;_}MK(wUJMph6lrL&O@L`&;zWCzhQIvY7aw5-lXP7tlBqYMl84QvVCArZ<7 zdK(yHK^a7MgBFsc7+kUtBB_lesRoxUf=J5gFl^w}Cc}U7IvW|}v~@Qq=xk&J(TX}7 znLxCX&PHYst*o<=1w^apY-9z|syZ9lK(w09Ms^UbuCtK?L~H151yitljdd6{FuG!v1{)Z)u_iK*ID^<0 z1`u$uQ?}S>#3%~#oQaNtje8zdFrKhZ*VAExxyTKAwiGm(jk-`QRWhanIg$)S-5egd|0wXpvDKINYZ)Q?s zmWp%&6{H-_3SG)z)yi%O8(5st^J9X-21ad2y}{(1kfOYS**P&KAYucPbE5PHX6J;+ z4a}ft6gIG^DZ3?FNGosPaCY0kq`HB_8Ei{}0yayOLAEGwU~x_WMFOj8;s#a> zQ#Y`vC4y=qup?9xQUV}FCBm*q-3?Z-a8OWC*ubg=4F_c#3*8OYaH$O(%1#Oj3U12o z8yK~fL9u58lLLDO8o~@A5qb(X%8Jq;*E2x`6+rHSMFE@Y23A#1P(Faw?vTjCOmPXy z(3nyNrEOc7Z3-JWoS{xsNB}hf&;t?{y>>dv3VNVWblV`{91$QL6d9pl6REdRk5NQh zcY{5OY>;#SimU^QEKD!5dFVPEbv9UNBP)Puf?4RKv%ylkOF_W~l)kzwa4X%w;;gqp zPg{3`GuX?ZebSt%DS*!a@dQ2}m{WMC^_&Nr6%n zsp)nDld2o2h_X?(kXA%WA&QYuH@fOUMg~i5MNlk(90~Oa+|%wl8yG|( z8GI81C@%}^Z7|W&-Qa;k+*4;G1EYxW22(BF4PH7M%(Zkkc!R>tRarq#!L3U<5f)ND zI-n%H!CPB*gRjme1_l>x-3@*^n-~~D41b+Xj9`%folT5jkwB0dS8d%5K_E3CMleVX zh!Fx(17d{gY-V6!aMae_5T>)4ff39K*V)Vn5sT2-%m@*S1gT>Lsfz-s1GAz*>cFfR zkUB6c7G$Thw(f>Fkewh#JjhNEBLQS5h>-|VStPGHSU{)qb9V19x7DyeKl?_q{X61m?fmyja8yT$N5ts+&ZLrhU z-H;Dq+H31>C;$aINZAGlZQTupIvW|Sw83hMAWV>&Vz3&J1V~MZ&PE1nxT&RJ9#}^i zgbC774q<|nSLkeHw1ya231Na1R6&>^1=Ttm8EoL@)qr_m^J*bXkn%bR6QsOeXCtEx z#JmOw6QrOK!UQR3(%Hyh3pcMB%mbU(0%3xbw?dd8mdjm)624c}6itaYhLST}DX; zJw_=8eMV{U?1vn~BL)KoLq>T<1x7_iC5AKxBZdo%$_y77(itv;=2RFaGOTCVz+lYq zmcfLdSHfV%sLAkz;WwiegE^x%qYk4kg9XDCMm>hB3>gg981)$q7!4VX7>yY$ z8LSvh7_1p=7+y1)GVErsWi(@W#%Rvq!jQ>m!H~_6!;r;j$!Nup$B@ggfWei)jo}|7 z14BNeHKPrKJHsnRTSi6(4~7EJ%m--xkkNsmkkOIRiNTY>i{U4uGouTmD}y(q8>2ge z55ska5=IY(GKO-7QbtckFGg=h9|m7WUj{!$KSqBBf5rgDK*k`(V1~yG0StlAxt?%_ z3WgwtO2!C=YK9tyD$v{y!$O8yhK&rt4DT317(&7GJh2R6jByN48U8TFGjuX0FoZKE zGA1!5Gej`dF{UsyFf=mMGo~`8F{U$SFlI7DGDI2fnz05tm(&2BV`^q>VQgh=V{B*aVC-b< zV(ez@VeDn>W9(;~z&Md{660jXDU4GYr!h`voWVGgaTeoj#yO008Rs$1XI#LzkZ}>? zV#XzmOBt6jE@xc9xRP-d<7&n=;5jDHoY6+cO^llvw=iyH+{U<_aR=j0#$AlN8TT;m zW!%TOpYZ_WLB>OjhZ&DB9%Venc%1PB<4Nd@+*$CP9B4-F660mYD~wkeuQ6U{yuo;r z@fPE4#ygC68SgRPXMDi;kns`YW5y?pPZ^&vK4*Nv_>%Dz<7>t@jBgp=F}`Q~!1$5z z6Le1R8{>D_np4I%$Y2hESapBteI?>Y? zJejNRF#2CaGBp4(aq!^?bWSFv(Kj<}V#;R_ zU@Bl($*_uHAH#lzqYPUZwlZvE*v@c-sgS7%G@r-N!myWtovD9Y6d=r4-7jP6d3v$6dB|hrZP-nn9Q)8A(3GT!z6||Of?L%7-ljoWvXSE!N9?A zjH!;Po~ePUkzprO6H_x&3sWmo8&f+|2U90g7gIM=4^uCLG6N?=2SXb}H$xXg4?{CU zJi`lyCWb>yeGHCF{Y(>>CNfQ8n#?qXX)4n+rs)j547V8`GTdXh!O+KWhryZQKEo}B z2Ml)^ZZgean#nYaX*P>rsE-euV^UFSSt`3@Zens#ejdAHetLdhYBslXa%NF-X>M9h zY6X{bN`6UVa&l^337boDVo`n`n@e(0eqsrmD_Ad^D_D@r6|Rik6>6F*)HHX5X*}-9 z`MJ4?5c9a);d)p+k`jx!JrD|+JiXXF!4|N4LXGo;7{}&Wl9`i|%I$?v%;Sw@Dw|JY za%o8_n-ADsJU&QLEIujuC2W2Wc|VA}ACf$?YhF5=KiF1Y|BTYS^u(gl+?>SH5_W%x z8`y#%dV?T(gOK#H1%suygAvYR2}w#UVhaJQWiQRkG%zxBWDA8DABtjpD8zX7P>2^< zLyI!=(%C{G9t}sB$Q_Abvqga|mO9g9U zO9k_|QsMIKsZe85p~j>mjNwT~@;O&JTn|eIC>k>m3fVHjrZZ(`v1dY!%Y+!mmI?7) z7D6#kHj=44+37{8sd+hxc`2F6Y&l@RvgLps$&-VmjwJ^ar+EBTH1pqMQI>ww0r6I&_7l2Q~) zN+Fi8mqL8aS_+QYQi$Kn5$1ANBG_zIU~9Rm;Nc3k!^F@4N*h9HBPeYQp$(w4B}|Rh4XZV=iCYM&9* zJ|n1mjiBx{fT}lwy3+{iP9vy$jG*o_g1XBH<}R4|Q2ULb?lXeA%K)YhDsBKZ*8pY@ z)V&5!^9`W(89?nZfSO|jb05?`1E~AWU^LWxb12^&YQ80uZwcc=?XiTKZwb|B33b0C zG@KkE`b=Tr;Rsdd2vY}jj}uhA6VzNMsJTv1_0CXvXK1)PL;d3d6?cKyW8wnU?*i5D z0=35lYL6>aoh#HIu2A)^Q1z}*b#72|-Jt5-pz7VA>fIpfO`++?)DUWqF;t&1)Sbpq z{l?JrVG5-!Ve(LOjiK>r3e|54lZV=842@4?h`lDp(DYymjdx>cIGMuS0SzBx7$2q% z>OW&>{F^}SGlAM?0(Gwm)SaeK^(Ihvnn2xY0(FlG)O{vUcbUN41v4LNzX{ZRCQx^o z!qh>g=1{&l)E-MH-x9`$ znr{hpmnBr6CDa{`(C~4D=re`oYe%R$N0>UOJDi~EouKAALCtl7s&|H}bB4Ot85&M5 zP<<{CdrVxQ`dy&X=iIk;vkdT9U}>o|v1P2reLvElt>63yL#y^7B}tQcDt< zT}u*KLo!lJ5?MU+OA=YV6AKCw*?banlTs3yd`p@9N|_=u+5K}fL2WgrfK29~jC|JM z%=Fwu=8(iv_E3mX%mEph%+48^%mKxj5NjMQxxn_9f})oid=bHHAOINHgXDX)~NB9lEI;$P;XjC_cUvk_}C*wZD6rR=2; zLzxRQGMSSzGMNjCGgGLk*^w0-1H7>K1oJ>IYk_!+pi^YO5;FfuS3 zXqyWv3Nk7RDvBzKDhnE$D%Uc-`^Upr4#K}`YipUa|4d~pXDqL+{nuH`0NU~jF^8W) zoT1GdG#&1s&&CM8(z~JM}!_LIY z>c#3K?O+Jj!Ntf~2hqY%Lr71gg9HPExR|Jju#lht13v@5AO!GoN^6@68k-uMDvBD5 z8Vf3$DvAm!nkt(riz?UFf}H*9UnhvFtz{~#t*x#7+yCnqWBISzTJR29h+mi)#2Mrn zmO^}E!^OzP%frf6z`()4%f`W*%D~US%*4-}3fcr5%fQI!>CMN;!^0lS$;i&`<1N6= z#LCJP$HK_Oqw7I7A^k0Cx45)=^Rl+|Wb zHZ>LodlM8}Adee^JT9sX_OhrV$d|u<{p$q99mv16pip4WuKmaJuM@o6jSy;SSe54(0z?ykEc={F>rD6aiucw^0LG-u&{V~ zGYAOq#4_;k_;_}BVQ_2oV z=-9F|vhwk=uomz$^0M&p<}vUvaB=c*r3x|&Ff%eT#DY?ar#Ay1A6qN~8yhHCSXhvQ z#U8AmkCnF$VgO4GX(mKE=y7ncu;5EEEbJ`oY^>lkBMeG2rpBVirpBU-Xk~^eEMR`s z{`xBf2@hDw!JG|BF@J?1p#uspP@2IgKQ=?d1Y7{%DnA4mL74`*01-qhKcC!_FqYRc6+$u)C=5Utl7ry+2P6c-$T^6a zL6||BVHG4itXLVDdAOOF3wRm1nRvMK7`PZX*ts}T`4~Ydgn@y<)0=^Z2b_a^yxAC; znAqd^Ihokmz1Tse30N}^Gj|+9Ms*KtTa2Nf zDT1jGTY|ty4&d^X0bIulGDtGWGAJ?}fP{b>HzOx23m0br0}lfe0}oRwJ0mLtBQGl> zsD=dR3{P)n&_O@3f*edNEI!^0yu5s|419cE-VFTw0=PEk%#K^{`qfC5NZh(VA6sVI@rW)xHw1=XmK#DQG4G8>C3i<=sYg7OBq zvVmj{aE)7A`-|z_zs}lUP#&~6sRbSE!T>8}g~2rrtj=X+WaQ&zU@Tx^Wa4M!Wnkh> zW#D1pVCUgTW#Hy!j%8qG1{Wh-u?$>Xpj5)Zz#b<6PbIcs?R<>9b>!%ebdW<7AfRwy z5M~gD2Lq_ig)}fg#Rj;p1?Ly^(zLd=7QHC_Ra^TD)M`Q$q~J6HsdGiaX+)LbFf@%Y z2(YrS3b531GP3Y9v*d~JF>x^PvVxi)LZGur7*n|!+1S`)c^KK*J-r1P`T1F68CY3; zyoI@#n3);kKqajg11OCk8^p@OUqhxzkq#PCQVa~LDv)l%Mp{xrT#P}KK~w}0 zKBAyD2{=R`iNx4c8C1lI8jA`ln;Nr%$`@$MggF~lkbsKM+J8K?j9);!obSK!kP*y7}&TtQW=<;p{-5^P@|Ip z9BS~SVhz&E%*b8G0Mg7*LyGQ52Qf$+6eWT1!mC_GQDso;L)2JQ(NtL!+?EEFp1*$m z0#~D;WB^JLziPoPP^9_;)FKyVaCdM4oeaaq$dn2?4~ChIGnIjvfrW#aC6$4logo%d zRB~{z#&R)&+tCaRu=0$7n}HjgV!1gbwM_-V85QglMnzLaK}AzVrgxw+8q}r-wTAz7 z)-q?;{sLvtFTeiw{{o%z0?U`u41o@Qtc=VYjBHHIY^mIgoIH#ST$~IbH-HOiPj3cB zMsNuGcr$Qu@y78n^74A|f_9KfGr-DPMEc<8l+`u|wa4KVE;u;|LaJe9QE*yd{89^Q zhk-Dp>IJ*D6GVc}zk#`p8L3^yz|X+P$6p{M$i&OS$i&0T1S&DOS$ViYr7IUB2OBE~ zQj48|nVBzEfRT^S$D4tTjWdn`)-D4X!@$p1N0B*^4oWO6EXpj9tb$aag5yL`NKgRO zE(5pRMZvxYwaHA4MU_Db1(aV*VeNQG>m5{Z`~qbVPyp2a0=3@3VFBhs3FJ6aW=M31 zV_;+8W@h6~=Mm z1qHnXL2;*y(y#;-gNQf;xgH#cV0VJ^H`w*YXubi*W$iCeG{P{*wP0Z|0pfxCkdS&( zo%~ z_~jqZFHrD=B~pvsWrkO`{PSp->GQaKozxfwZ`m^srx5x~R5 zn99e@EN$cz^(_^_5yLji~<5)0zQxemqC#M(Z)yhiU=$_!J-1v z??sCUMq^WDun!=)3F<3wkqY%jEmXP|R4Sq7F?oh8hjeyERxU;k7EmT)VPNKFVNT^` z<`WQ&>;rBFVTMVNn!=2MgOQ1oqkw^(nT3tL0Ca#D z8zW0911BfU{m{w+Tr9B0K?_!C2Mdr!Mh>PrkWOZHmO5g!g6jx8#j-J|MJ1|gYzi8x zPzGgpP+tp_;Xy?*xU~R|GjJ|}v=sOmq#4#g{NupD#mvIZRlvZ>%ErN2z{AMM%gDgW z$N;J+*f>DBhMOB)G<$k8u(E=SYaee04i5G>K1OJZ+6iO?GZ#x8$P89awmQ-ciFA-* zK$P^T<1zf4(%R;rnjc&VL42zY^)0AI0#*2+S|6VN7|X$}L`d3XEU*1liyW7t46+Q+ zew7OYFB>}>FMAyW4+keVPXPln10NeRUn&DT0|yT~2e{7UX5&m{1#Rz+1&zmedP7&URM^r0F{d&hx{IJnj1ATXg13Fxp=}?~NG1p~XaDU7 zmEcT;psWQ7bL5l->Z3M7a+LuC2Ln4h2Pj3cu!34!py4g>Kpz7W6MHNdsPX6n%1~^e zQ4B9O&;TY#3j+r`DD@DhC(=QPg$2|Fg|`W~K`m!xQ)6*akZZwMP|?&_6jUhuf;k;D z_zH18sF(vuA+;Ns89?K9(;>bvX5eDrju=Ef1 zB={5*Ncktu&cZ(%juvNG8=f9nSqOoJC28uo7;=qN7}&{q=}K4 z0W=&-tS)dNjxn~zizQ!!V@*_16qLF^BS)|V2l4|b!oV0QH-maHFyAmUGIE0t)MaB} z=U`(8^>h(sH$yBJ3lq5i!ok5A$HNG1uNs3jaWgX1F%Yc_>>E%WjI-MU>ea%00%|&d zYoJ)V4XAlPU8DI|LX68a3`$MjVIAft6fehMl2y?({N^UMrq=7nK zQ0f8oB0-G<@XQ3nb)YWGUm--k2DJo0tud(MkiwanfuF(6!5P%MV`E`WM?1q?Tq z0>%iW2cv+&s|h@Jz`y`1W3V)~ctBnO|Ftahj3Srm?2?G-oBWSV)-2CB; z1@-qpy;bg5UPf+iKTyG71JTVHso;nGMuNW@KPwXQ(692ynrKwU`BWHlYIs;Au%w(6ExCAh^-QR0t}O zpyNc4Q6unR<3AqMF=%!MX@=Dhzgjc!fI5!_4D1YSoa~^PNd`_2h-$p@<=xnucx znYg*Zl|2W%jbOF<)yGa>%B1Pzg}vN9Dga58W(aB`%Ak{COv z-@wBI8(n2%V~yoz1Pv)Nu(NZ;@xmI+R$#q6OssVzYX(o+Vi_~!gLGn{5e%+(vyLMl?7DJ@-Q>=gfg%)FmbUmr802x zGBWaUGJ*!MzzsN0Z*E3rW?1Kliwiuu;05X{@W%=;^7H%g`$#)@fy`p%0FAU$Y8ZHY z8RyspXnYye^@WT^fLNeB3>rv;BzaK30qk{9`2Y$FP{M}@fJqPm3KjIWZIFXMs0v}? zUnn9s4W^xcUIqgr=Y*sVE9goTiF`p#C{e zZ7tI~Xwn2#$$y2wX%XZq)Ut=0LES+G)RSgrWC7CT3oq-KhaeI_2KS2Li!oSZvG+Lxu}~&b>f+&HVBitvf%*;}6+E1h+JZ>d zvYLV;UbGfGJ@BuS@e3%$!&1~QP*M{Dy9KeLMVP_C!4{O-!3`#O?+rYe2Gdz4Dy|7@108sr0>Pms4pD~sh z((?ihxpKm~UZ9azwm1ehHZL|GaIJuQfdi-q1sbyiEpQMAt;`Ws1P2mm^b%AFfCf>Z zZiBWUAahON3XuU3`@#&lko2m?%E-*Z#Kc?xUaG(jP7k0E0?o>U7AtT;$^dBxb+AGf zW~Ms)DkB|0=i(#Jm4SM+kQ4+e$3W$}G1%ph!Tnk=1$G}~Mi4Cwg&DjZJWzWN;P3)< zx} zhlqXylI7r%5|X7rb|VT&RGWnvd_k2DXjF^64!q)>)6FKcLA*F08gaf#xj}#8z42(S-KzRyMr$gHLpghIQ#KFYO zQOC;2$im3P%*d1q>g2;)`5YWr+V%um=HRdZ4VZ%+g=m?BR!u=BFKWS!a!~yR&hFsm z7O2mQNFVGBk__#Te5K3H$O39C7eKN-FEh9?$;Ak2Pw{};Q=qiM5epk}(F3bzU}3Ez zN&~o82Wmq>mcD`N1#T`*4p47dNKk-}Q(PM~ucRodY|0MGaA41YJ!ULw42opXkSb`# z5mbtSr;y3@D=%kP-!A4QMF> zvGkVM14 zL&yxW46spTAFwYt**H0%eqe|D0W^9C@&Rb91>%G9UnuJx zco|F`j6kIxcp44dd}U#QS;5N676Vtu%tT63mEdMk0S1l-cV$4W* zJ9scKGca+07Rholu(EKnrh;2CkU14LHjY>xZYB;6aOuht#||0H;^2VICxB9kkf5NT z055cggdMb!2s}cdED8#3(Ap7DP616Of_iJ<$^gQQJRTl~I8dGf9plY_wfq4s zY+-~u3RKvDoCV4;kj^`(y9zFSAaettW)IA1pjZPr5Y&f-w0A%&24fr|8Q2(@7}=OW zYnWJLc{!NC>ogb{*<%?%OU*&)1|iJJ30@871)6#1j%5IkYBDguS_;T3`S>`&EBT<& z2n#^a3O-R~Q)A|AP+} zJBx5H2V_|Z+_{i3R|W>ISZ+oxEVvPe2TfV4g2v>KA{vzEK~W8EfIu7y zuUSBe5EQ$=;K>jc&yY3=Xsp82!H5~unq^=E4ZuN50Z-7nB-U6^7<#e#Fff4HdGIbW zXtcr{I`9ITQddoIfOHCGVri-@}%-}GqJNV zvhcD)hJ6@gnIS{ET#VpShL@K$4pbwtf;-)yaadSULtrQt*|FfYDT;!M(CJICbHVLT z(5Mus4+UDtf#PJOn1J+W{2aVl7@0UZIGMm58aB}A2?J;~3OK;Q3!K3H1aQH~9tT?M z>Ba5?S#t;)HbI(X0PTB$Bnfbn7_{?66f}6s3`rVbhk;x9wczorT2QqOUZBcU2%6X{ zM+;9-Ith0OVP|CJU}WK9Wl7~_=0Tpt*o!m3XS>-!D?1B#(NZ%HWLgbJay9|w0nj)c zDcZoR`yi13O3{cU17AW88>;~=msSM(64Yon7FAXhR0PkjffEa;9)P8lf1Ri)1$4I} z%$K0Pr7Xi#NGs2ji;;tYlar%>p9h@8`4CAQ;!|E;$nY(=bq-7B(hg=|eGD9&btGww zbdcfT0QE>XBsri}2`C4HYhzHS1k_dqjYuJ73{dL?nvX%56@s5h7VP0{a4!Yo zV^C#N`>zuzZ-8bMg&BMtJRw_aK&=MQkOyp}nv)YYRL{Y|9?K0`$Hc+{S^Ua^IcEzR zsfLU>Kt`(J1twD=q<{pqSwNWrl-dxZFsNybn*p>7D8V6?frp=wmy3rNw7i&`fr}e7 z(8gu zUqNM2RYlOUDbV1jsIs}Ku_#gj4=PzewF)G!fP4W8Wl(s6MtTuB1T^D=NI5KwOl<7z zOa&Z_jGT-Nyo?N~JdA8?%(0-sBk=N1NN9o=AR;Z0G6U;lV`8r(Nh7#B3rablWP@cz z6s)iU)tsP45_q#7xX2QO7Fpnw0g4>ZpbN-z;4%y3?|+@3x(+nM0O{32atf$SwRf-q z&G9pU)_*fHfTM+z5jw-~=?!YqvBz>jk_;rlAQhq9prvZy+zRT1K-{EiET{|$_;o@4os||72B)~%q*z%!ZVHptb{OA``UR64@qDo59+_0yMwD1fAbtWn)PN_lIDM-9h7{ z&^6&Gi44?o0QDgh!4-s}C}=e}sN)K2H-Pd0D5!rimLugzUItJ~@^kQJ;NoOtVB}&* z!X+$1A~~Pn52X_Y@~_--s9uvl+p$jjq(bnN_%Be$1~k(Jiga+v334HFhzK%>Gt7sSk2VZkj7*GNOmz&* z>})K|1q^H~>};T$VwpHWvBJr~&ddp3S_6$01{N09I37^T8oUwJ4p}=pGiZW_g`JJ1 z2DA{L2m>M=#32;}>ed-vP8slQJSdhytwnGP7qlV37`zf6)NASl6_1dKCXnwy3yVSR zNz@r^&@LkAK14UrQCSV}v1 zf(!!9Qh)*s$;YId73mva+Z# zsQ(7)#e#efYD9xF9Jtj7%J9%G9H?|dtrtT< z#K_0MBEZLz#vs7J$tnP!;THrC$};ftLl%I53u^W_XvrY$-~%!XJj2h*$-&9WQ3p1S zt%fY)A{}%}rOB)CZj4=(V+5l{ev z!t56)L_r~j=v7KG$TOUTgqAM@2QzpjG!rK?Qyv2&3wQ$!8v{EpXyq9%12YFNb1DNT z7b6QJCrcUwbbArFngDkRz@s@laRQ7yJbpYr(hdP2^O!*kq(KI9)__c80d1=x*GTXz zIixejAPMWng4!Ms4DKm{hWS8)pO7vrxSI_g7-a@YD1z&cUrg^nON&7x-r%qTr6jNj zs8lbkMIIlPW{7kMV*ssj4+W1-gPOgd>!7*0m{S?J85y`k85nt>W7W`Q?w;NZOiaA7 z#O-(i4b_5jKDdGhtpXAScOf8S*P!x^sSwm62XBG`<$sX7L8Suf3TS2qX@-@MICKCl zz~JBnuVUc=tzzL}VCCguP32?cWnf|f?SMh_=eW5!Vxhes=x`ReKj#QCf`O9*GzP*( zmKl)_QYq&0B!{4@#+^ z5CF9?Agh-k@eHbkLG!pUzr$!qT?ZK%l4g)+NOMRA#TySNN2&lLFDnCYC<7~~5z5Mt z%D~3V$jHRT2wFV~9uESyM!}=>KA^YStO+Nz+PO%Ss|y)!{jj{$50D3-t+NG}X#4`@_H7_?><;$CJhaE%EX zZ)bmjtg$v5yZjG@hs1gGuc2Gr+Sh2tZF8;tXD3J6H zYFAr0n1Z%WF)%UKfyQ82VnJcd>KA?P654X%_s`84TM3{y5NybP))%60z8*e zSPL3HL>iX|uZ#hWsxroc)~|p|cRZ7wpm`tAcsQg42Sqq&suP4klOgb33y_cm^-P!< znAkyM;P7G@5}dFN1<1h(3QSN*3hrrxW<5bYZOFJcw9zc62%a5+q`$xYwV+k=piP|M zeS)BT$oPc;ZBz%;iv_I%V`X6BWQ7ln!PcdKmgc~=1aNVI$C$m?eZZ@9#CgC)JE&0S z;|0$R!OqZOSc?K$$OG9s32r@-wU!;(Ns!Sm z@T@tg7X}&k`T`0eQ1OY>8-uvV%fX$2iHnh$9W=^~mY|@kLQwK23kxVoL1GYeatd_8 z01tG*fT*dls3KC_fl36>pdpTgh1v=S^$vX@V`CxenlE!(tm_Sn& z44j;>F)1-I4h~5FkVBLM+E)Olc2KDcO6{O=C3tc&1+D)x1??U$2Mq^+A`CR{0dgWZ zw7^@c8NYzWGC_q3q{f7_8bISo4i2_Rt9L*%qL6V9A8!U07GhTY!$*){DF{4z1dlDy zxDjexK}s*sn1ndPL`bW}n1P9rgORBKUPpt98rE3wF-YKP2`p<@Ks$24x;W}c(ME7R zI%w_?yyit2T#A9JbLjC6ps|f#;5-0c_5!NeVObSC5rk5_&?2GV*4 z^`t>HFtZ|PTRo^dDQXN#6X0YEu7*J@Fax}75Il?o9qIqRf%#4gY+&qlnLt_{~N6v6E zu(5Kor84mFKu2srInl=()Inm69=VgLz`DJEikA*ghr5zN(N|+hgK??{Ov8jo45MW?H zDtW+dXF+IAfaC&5+Zp5xQ1t^UDnS_3d`4TrirA?L+CI+)TARbn02&`b8khsc3@2#) z1Gszv%~L^^14}y?gEeunvVj(p5veQEL5$G4323Jdw7&q9vmwa~f`0~Z&(Ed&`1MOndzC|e+r4@w50a)sGg zP#KiKA(0JUcL}PAz`6YwcnF*sX&owPpDVOh)#GAhVdrLLDL|c~L<|9dceo<;81=y# z*jc#ih|>YyvBSc`z<@bP3~EF{4--S1*#z~rz%yaWqROHm-++oo@JSeuBLA-tIDCKA zf>JVU{en1bg`zhD8w)cF8|a(@CT4b~I`G;Vjv59I&^atM42+!2jGQ$L44hnC44~!P zur4wuC%6*`o|Q$~nB@yGjTv-!0e<5+8Mr`e!|> z9%2O%7b6oBcPt-drxtjLICM$9w1Wq#L7a6A=q537u+|V^6yaemaY)7zRRk@}2bXf7 zAus6mZE!E`&s6Yu322EmxT6LR2QYzDqRTVvgM@(xe0Yk7nTeUFfPsmZnTfZCftQh) zmk~4}!N8b@NaUc|7tkgq$Uqk#BLf3}9BA_rc%;h<)hJMULNkk*k)eht!-yI10;K?B zQ$4?gTero4-`eg`-H&>0Gt!RYg$0V5HMeZ_ppHT0@FJNUIsx17Y9cMR%Rw^GNc_`K&F7E*(fn4(m?|>lLVS$L#)R@ zbm`$kZi1jyHqhOpsG$OCbAmzzIy3_d7jX3t4jAzG%)d_5utBUS5Co@+6_Ai|0QE08 zIa!dZDF#T-kC6d1VFEhN5LT5iGlM&~;4TzL94{jWhZpF$3DD>*7bh2J=^#5Zv~D8J zj7SGb+^ZZx`-Z`}5R@!HiwOlGokn;rfOs5qs42J!2TBg0*3Um4aP~ky6Bjhjk?xSf zz{$?W&Iy_uX5e9E<4I*;WnkfEWl7~@=0#p3#m){}yoGe&3}lTIsNV<~^+3cPQdc^WiNJ@EH>6pu)lei8)AqMuY@>9Tlie#RwYn!J4zdE2&^9 z7@o6VfELGq{11vtP-7A$96;$8w62z&l?6QW!vZ?}hzq( z7d%!A88rgA5>x^mgT$COKj`>6HV#(MsZ-#2S{4RoE>Nk<#ROiC0ot7b9;*YK6x8?g*j-@LFTg zb|i3z0~#uzB8U}!1OjN03uK6i3AEw^l$=4$e&%dYuz-RG6pf&^0`jOVC`HRMtcQe< z6FVa-4-2S&#mUUX%n7bA`52k_z!M~(l}-#iJm3Y3p56?gs*@Xjt}4<(CuguBJglIj zu*fw9-17&O&!8NRebo~*H-j5Vpdv}s7^6@E`x)$cXjp&+Ky3$Dxr8W@L9=P{urslo z*%((Ee*soB7K>9%xSiG++h~4RB%rjp>2Q zKg5V6(>n%8-sys*3qw!?iix#=laYysk&TgwEe+Jt2F;jpfwuobHgJNL5OTzVLIzwV z8G-bG8ePO|BBBlgwS_^IF97D z_!(>*EJ39iXlD&*5EVR!$N*h14xWS{xL_Q#I_<;@r01YvKn)zrx0tJGP}Az=!C7+LH{#>cM;B7U)zV&}b*9;SSm8{kI=<1P!R@0iTTu z2^&zC6|t)ZF}?#zGfb?Y0|gnl7#UJQ!?G-~pqUSFiOdws!N|mf-Yqu2LdHuOn6sJQF@Q$$ogExNp~(n}0oe9h z1_lPml1U#>(dh-+JppdX`EmMST|o&s=>{}n3t8&|nRo@+4c-L>TD1gOk^>4^v{M8? zdAALkb6FUfxWFfrFfxFrxEZ*(z=IpmjS-;EDtKUrBaRzVHA_1ff%R~KP9`B%Q>23^ z1L*i-l#{M`IAuWnCh(R7P<{o?jDZdW1?O0BixK16yX93&c({b&RWO8$il$K%)*x@z{$kS#K^!88ZiWoZ-Nf42A@aC77Jza@IW??`hd$mJ;W+$PSCUkc+LQP zkQ7TCXuQRX#fOUvk{P)8xj^e}xnb*jc{nBDNy%7H7`%a!5wfNjG{6d)at2j%<+Z;+ zyY@hd3FJWJbOg4s!MtJXL{Wg0UP?n}cts0k!jBrxt;F9pLkP7?@ajn9_K;m@vzJZf?kQ zF1WY??-T%ELt+Ke3$8ABNYzYm+yU$>)O{YXafg1!FQA4CD3n1F3m$g>RrH|h5LA~S zau8@%AJpbgc1U30V&LWA;ssqO!otqSk|qF}S7&GCV291CLxvilqYR)usgNlH(#Hl+ zTn;Mglodr`8=63o19CgKzymuTIyQjpekS-T7m(Y-9D*5G8Tc4k`BE8pLB8Nh<79-6 z1t3?6(AtojnE8h3QnjiLG3-r$Q#&MU}u63@P?&)P+9b=7PPz_)I>r}6QJ}D+IPau zz`?@Jkp^Cw3R=8_HrN2|-}rz!fLyWA({0HaY(USb(7^_9LmM>W0CF>^&Zz|rHh_{b zGNx=9)V0Vj9#G6pgN zf?mdeM?gT?0Ter+LIzs5fp+K9GH&2YWI)3nVE2NvCuHaYBmJPp4Jgf{j(tFH6mnr; zX5waIF5u??mG!7CQ%+9Upa*DEG597BT!S925M#LOC^Cn*(GIZJK@DK=q%mkFOi@&k z5kA@h%Kf1F0FvE7*$LF;1FublGzCC?GEm6{>W9JRULmVKp)0pRhyQ^3f1qh@@SgQJ5TXtzFir6M~6BP%;& zDgy@xEVsfBasbu$;6<09fiTob2gr~sXtlSZpt2}96@ZEkP?Ht3QWm^LzZSeCkg@z1 z(zq9_4g`&{ax?I9a`UEw);aTm4k+YfU}xcDPlK*Th17roJWSv{+$=0!knPe8tgL>J zO}(Ho7^26>A*;+l({&&>gH{-U;t)DU4qlW4ZtR2Nim@EBwHLY>43y14Ckuc|KS=t8 zw1i_EBH0*OxY@Z`z(+#EmzQ&Z7O8V{!$!q~uMeZ&raNe1W%sdXZHdvo96)6P<|ZXgMmS{KF$GPNDT!`OR)40uAv}N13tn96e*w@3OpqY z&k2Z;1#Sji2QA2sKIpnva7Jg2#eF^^WM~I`KH?Y9K9X8ckpfvC1u99vGmFeG7=%Hq zvLNRTfP00k43Mi)z$cx6hWo*TEs&L27-zXbmu`bb>cBe)1(gNCNei+A2~?ti!W_KG z7jz~g!at&*^V66>cM~wPF@SbOg62>-QyHM118-8`h~?&D0?!LEFrW>cf)0iNEt&$g zsUQo%Al8Eqg9LXQAYKNomPc^`6VjRyUdXvqh-2fS!%05gpi^TY#~paFfn%73mj!ib zpA&RiGI%xyeBz=YX!-iDe>~u_6cmq;+6=T%7kbhj(>n%E$my1_yKx{Z*Rh_|3ppcC zQP5Zv+#`jwS3riLttS#>0PXvPHl{#Z*I7Xeqd`>xcqot)w2cv5PJ6NVaBy%4a)1^k zg4fh?frjCgO^rb-$Qi*46qHSo){lc$qkt!*!ATK(<^pK#IIQ*omCM`=q70xuF$*^% zD<^1sEhhs9J0}OMO9GxQ;sGu41s4=vY(6|Z92`8NJfO@1sv&qexH-7NZs7$j%m$s{ z0>Y+>qQY?ZfIJQzk_4U02fCaQv{f9sTpToh1nL>XTm^Lz6C(pRD-$yZC|PkZu(N>< zIE5!GP$k6`%fkr1>;b%#9mz%TrQ%>0fyU)Q>o&nl#X+47aD0FYQqW2r&_E9?b%EAI zgUVKn*a58}W#$BRkYP6+!Y7eHI~F)%p+i-Wb~z}1VE0%eP5y!QOMrI4f(kiC&@n&Y zE#Tmz&imo%4c7VwB@)ziQGyJv4o(bAoY0lw>I*19)6qUZCyr+_C(S zbGZ5VAc4at&IjLez`@G_&W%Wcgfc@4+SMY+ZVK8j4=Qg#tF0jU5wyjm9JGxZJo*E! z9Dae81A~SgsaY~WTE4K7p%&EQg~t@cC7^g>VE_$nAX?#)46zPT4B!P$oOPhXeOMSl zi#<{itC7ItgW#inKsPyn?z-~=9mm2G3%W1TkH<$$jD-cVgdUX3Kvfbb!-LKd&V zVbCqU;OR)vwSBNDH>N_+9b}!Lm6V{02vmfFMkYYh;2<`rrBDkx;0%`2Kyk{=z|SDb z5bO}Z#>fJmC}Ra*u*JaxTkQ|&ka~g_(Ske7UYy{v6f_eCavr!40PQ>gpH+yoY7R8R z3z{GVxlYwsR9P6b*9@Ei8NYz0M?ixR;C3slOaPBVfQorgs|>Vu0~D7GOz*(sMZ64- z4tAi^Orh(6!1H~Or83~AD-#o>r1v9O(!pT#(Hx#FR&j@U#s|R^Z|plCmmmq!%067md@eB1EGXo0~=*kvG28bisIXKuu1wf~Z!O{a_$P{s_CJzsPtRU>D z?;wzoC_bjx&`1X*&~UbdyaZ@OTUtsIV?q=(oQ*3?*rDNs95$f(0DP$?Gy}}>=u=4^j;@$!yBw)eH!~jV;IJDx8chHJJ@F*Vgs&8b!!MBfsqaA6b zH%f~Nc1J3aEh^9e1mv7(28LQ_Ss}<^=U@$*@nT?Og`BMmZbyO6gaBWP=i|-E!NklA zTld1kf?UMHnpEKC3#?%*27X$(P`Lr>(u31)E%*d(&if0GP1KV zfDYgUU7`qTh=M1$U}tSY8jYYD6FiEo169pX2flL=zw$^2anQ5``jiA0=*SUxy8?2i zBdAybk4b=<@SqwCa;zh$Eeq*234(eJte}m>jOjht^%Ff8Z z0P&p{Hs6_>f~qPI26s*%ZFumAGpJtz3RF-734YE1cs@njK?8K?4!A_}fn1*nI;;e| zQj&oIF_r>47!!PZry%GwJP-y?okE9-c|iS15Qdfc@ID7KGqTaFERX|PFmDHeoOKT} z6Ec4Z>n$Q;0@PbBf}~VUP(}frnhWo5FfcPi){^*mgCZZYn*w~&s1`^qTBgFGIMP87 zN4^3b00_dMg>aB5dC+~apxH-IW2F<~SkT%6=y`;Yyd?_G)65KeSp=Ig1Z7V~kheh?d->4*x5Pac%j{UX$L*9de9a%qBP(e7Xl3nfVxT` z46aZ>9uNfW3Iq)Zf#L~tR|LopAU1fs6O?DbJM*FG9K6m2Ji-bY7ePLUk+@|wpb>BQ zvKmks3Ll07b($bcM^HxLL8FAD=P-_*!w4EyfEOy@1I<7^X-G*1>UDwA$>=$Z*iR#c zZpQ!(6O5k22pUUMHZ>mB=P-iHX;6-s18I?%Gq5vqfiB$PrD-aC3sLDkD;Fq=OvlbrPWEKJbA=(3umE`G4^E zH0Xj?&~UY}A=3GcouC>VI@1TL8NgiR)d`?_b-16}2%0ae1+O~<%{YoOfL0!IGO#dk zBCRh1?Z$>KwSsKw0<9MF0-d}L8%{#BlzAXqo#2ZXj734~o5UfDY(dSWPVfN{kPZjv z=rm{qz}s%%wlnPPcdREk`gnsn9eyl6n5*Rs!Iv9@Hn)M+ErSMSAgfS7Eo;z*|6ky~ zAEYM*8b_hSu69XeFsOQGYz`z1NCyEU^-h0s_JU!k{?^ z(6Fcg@+ms(NOprPgR~7HE&wn01$Ej$65x}FAg%zFJUx(-2Xu)nHv@A4=)`#LJO(ZX z4o)r()SIRe_s>G_D3W$CWngCHW@P3Dtu|uhW~d=fA9ySURLP;t1B13vn}b@ppxzO9 zGzGLAMj6uZ1t$aW0$<2FJBaT<({f*sMlG|!>l{J53^^FsIYL46ehh3(pcy6T6d5?) zz^Md$%o4cA@5kbUEm%Rp37QoI&7UJhDYyy#3v@yRXx0<7Wx{VLtoiK<~xX*&1@%fn1rQ{SLP*eZK%~?PaxE;if{(o$F(>CqgYN49?eGWPP6ImSkbxDl7L0?5 zF_eLWlaYyqg9&t_C-{s&=yV2Xj}PQ3No@B8fmYIhA_!h=fd=xx3$!823&9ZtYA}FS zXV!vFrUVtMpmBF_jJ*S$9m-e^ooiudP<2p(-7)M5Dy^7fv7hb-TC$@IIza@q*c;ST zKsntHd;%SK$P;uT4de(d(3wflvz*}#O;G&_Iw=q|>H#WyL8F?`MP0vsf$v}ejom`Z zIPiU2p!?$(*&#c~IYDdMLCeRH&%wfd-xla3Akg?G)Glz_15#guYy>4#@OUohVhgkx zabeINcFX3%UnXo3)Q7??2VG(YG_J81q3bkG-Q zZ9nw%K=2L9py5IA>L%FC9H@P1>tF>LNatjO+~xwh!;ArXnF@IR8N5>$JbDcpk3+O8 zL0jk%hX{f~0Frk>X##XV9O&LU&@4FETF|sI##k(9v=nrhAhovBpbgJFpfNu1+A!F?mguW4 zP>v4-Z<&SeREHcN2wvj~TEzrzDS+B6<)BMvkk1YTt?2@-^8Es7y+cwtXa?5P!3{bC z3q3m!bRR1ls3i(+R)gD!TydnY20_FQD4sz@0QhuHP|^o)D*;WSGnRvHT!Qv%z%$Y8 z41(ZO#ZXs-u&}_!%o*6&;G35qiyqLIVn8R!L3seNA_VRj$jWR`b01t(f##YZk@mMA zWvmh84tEC^@R|^CmjgA~aA8R{1XqQCuloh3A5ck93tAOIMCt*xpuHU2dAXU`8MxTk zxxnM&kU|c+A&7?)l7=}rU};!Hgo6WetvaZL05#-64SM*Y51?{E9Fmy9XRM(d5eVL1 z4LNS5widKv6Fk=hnxcd3e1oJMW(JIvVW6YcKr=MposMjApy?-kE5jh`Kj0AqTK@qG zjbEU60nesEGczc4Kzf0oQc0Ad57L_eT{p%KIwA;`-|5Y82V_xC~dDQF2SXxA{d z(*Qx0HFydZG}Z#zWe&=v4zlY(eOPhO3Mx*}S%4g9X8|&ROE}PC0`Oi= zLsRAD z-weFV3BLRr`}t<5YOtPf20D-pv>*d=z8Uz8KUm!kE!#RlC&`1-FQnH73MXL(D+hBn z(BvX>EH@_?D+@C-Xy5`=x`5W-crikj0K#v0hA#jF-SBJ7&5yCY9t#Ag4$Q0 z<>_B)K`dwr1H~U|Jp{UITAV>1)FOn;*MQnHg8Ty9ES#X)hYhq^oRt+ka^&gF#|Ub; z^Fof8l#mb+L9D@mlq&G?0YL%K8Vu0sjiA{a(Ak*GSlkF|wSglL)U|~<7SxObFEInn z`+~MgfzlddIZ|6dnPC+quiJo+*~4;9A3Gzf03#m*E9mfY(85&EoWGAZ`YC;oWB#z7 z(npK|v^=8^l$B61*#D4y1^l2J4;fe(7}-F5DWu|+jSakf0Fs2k6QW4_7C~q4LDzkQ z>l)CsBsf`sOIMI(kkkokc!AO;q-+JXlf=QT6xd;VTny|iTqw6;fKKV*;f9>)iLrYC zcFr3pCxa^+&|nKFVSrZ8vI>HZ^aSaC9(a)=^o$trYEBjw$YFba+&<7|4aTkJNGmx(XS^U9H(-|{;u_qe!(CJsWTv#?|OQJ)?q=e@C0|I*`b8jV(g4e z?0F1q9E_}NtVs-@%94SZIT19d1n$PBF!)G2=%A=(tih`sTtuNvRC02G4#x+N)`MC* zf~v-*%Am0>#xJ0a4WKphAlHMMF`&{3*|kOv`jA!^c$Eyqk%_QFXc9pyns8VI8I%Xz z!VMbVfXu-`P6GuErXY=S@-v7tKu-YyS;fW4z*xY|$jQLPna9Avz{bkKmde1z1#aI- zdxM6_WWB*hsLOz6b`6;s8M!#Q7&+@8S{P~w=>ab;0?n_0>QSV|2QRy{He_)T$mQV0 z{i2|bCg_M_P&*I2Yy;eihm2Iz{_Xz->WtTd;un^tK=BKi?E}>)pmWAp*ucw^8Nn;` zq#@fDIT%@4WiV&^*d@WYhJnV6L4%Z_!y6$hzd*}LK@}iqYXi8c{0q4c1F}@QP1t8l&7&H$Eo8g4H z0ofhkmGZyfeHZ2z4D1Zvkkl=~zz8}8q=12m3F>pu@Td&vPFYEa6muPhT%-dJ==d4P z-6-th+UB6^!A%tf!J8O6!5iHE3L(wVflm4_f!GILfe2cy$i&RT%v1pGV=^!@N`nru zVU6Vg9q}#e&A`GU!vfAsYz(a6<%(d{Ebz6!*px>)2(Yk#?wN!%4+S{crL{qgLvhfR zPJ*DZW7wIm;B%co18SfVSx`F>HDqKRq(JKgnV1>VAd$huB*Wwby62jOT|%1?bn1+< zpdwQtXrCG57v>kB1AN|r^9cj^oDxuZ1)4W!?Xt zXJ%kw1*Ke2feg9~R2sA?6M7MiEc^}_88#nj2P4qbA^4;ZqBTW2$g!|6FyL54#KSJC z4IY9P1-TuJ1rK)pgx$q9*VP#YT7u488qW(aif1Kpm? z!N#7-&CCVgCngQ@0Jupi3(Ap@L)m0FVXbFj4p_$I=Hf(ZIrFlMf-X1#EvFY{1f5?6 zbswnr{8tDO#*kJs=$d&B_Agu$jb~ax}+Vfpjw&hzo{oGeEA1XJKIG2A#f$b|)MwD|ny|oHQYK!pZQ0mcwJ6d}5ac zoeKo+lY#m?pn*5YNET=ft1{@G15m*V!mw@VpvVIk_MkKd+VzBJDM4mAeOnAkx_ z{c*4|a)2+f0M{?z){Lw-7bg=qsu&nh77}o?%Yv#o@L^ZRp#CA`W)M+OYXEdC9qjT= z&;doD;WE^mDhf{1tPC8?tQ?@jfH~P&I1xLkrM*EjOtPRdmpu-=IaG!nyeACjiJy=( zttiR}8jS*#OCT44YFtrJUK4^j3Y4Tl2^!Qu0oA*;$R|UB`u~y)p#CZoFC#N(43B}E zft8b+6*Rre#=^yxil_}hSJrUIf`(QhhhfREfO8$TlLJ6G4|GN;yEwSGhhCTicAFyH zwcz#uC_#a5E(L`hXcxfWe(;iguxpXSPnZFczL?lp*+8qcc^J4Ec(_v;*q9jE!ayhI zurf2UrZI4Faez)Y=irbAwE@Aw33siCh={ZZbR+<2D2tz67F2_PG68rH%M`SEz*x{2 zG~o$q^?}_9DkzYLut23esDla`umGht@W~8_@(VQkJQq?fTQRWku(Cp~ZQ}x;n+n-R z4ywq&ZB%J*1|A+RkgK`4WI>G__BhZGfegEkw1YKBGY<F1^LFeN`+n%5$_i^lOOw7#Sc8oemAq%J`X2q*A(t#gklLxz$Hh8oEy!KvM z5R~~r-3CxLXZ-SKD(D17lpPMjpnehP@?AzYc1AW>K8GFyNhqI#f(DeYK!qk~j8hPn zrNC*mwzd}3OZoK+G?#@aU$`MV*+GjTAh(7|dxOs*kYNUuZ`=&rpr()@q?R`ZwIsl6 z@Ij4X(AsPa61B}P0GoRaZNVpNOMs1LKfZC zf}7)@ISP~(3;6Cmq!uRV8X6f!ko%!`@3D)6TH9ckGiO8G4Gw%EsEGTPXGEAwgGyHLT^rzI-?$k$c^DbEI2k~T9KdY_ zX~@PQ1|}w1Zw4+d-Z;=Lq=ai6a03<8=L5yQC^&n7mydzc0jS>x$_F3}>EeKL47k_= zr$F$G1h`d?e2*ch69&!#C~bAn>I6km&`JmJErXyTM(|1pSj`Hlv)CAz8QGXq85lXB z7puaiN5QoyWW|>Z=%h)EItzSytSG4Ngp9+2$A&?}nUEn^uy;V!eK{!c{e|uPgN(p} zT9^S2z6@*(%p7dYsh|cWv{M4#`Uh@_$$(m!oUx!`Sb4~1W;{(q$Rr?|gFsV(pavqy zVW17Ip!@}HjeuPR+E0UMBZ6i}r9o#bu!3%Q;AUdxPUT_b;$URp^|yEztT%P!k!P$U%)O zXnhE}mIXBD2F0MDfCL06@q;jUM=kR0EZhvB)ve4746LA&EI}~=x^@%P#(}k~kV`^U z&{zR!;@bn+lyL5>m@A*X3JfKn>bTTRv12ZeA{|X8l1~xWu%LUwanM3rP-z1?85L>u zHF!7zlzTuYC?o9RX5a_!FlAsthWvk;i~o89;uta4-d}dji*|2+uLaaj>v6g9jVf*dQKc<7DIHU}s=u zU=x(Hg8b;@U=KUZM;dgZ2e^$S z3u>@3gRcH#W|jdp17U1X!vUjOHw8_PiyDL5L7?UPwTxdttMP18lo&ycM7Xt})olNG zAagX3w#zTbqIa;($Tc*0e63Te5^GsHSXA=(k3iw{$Y zK`kVC2I#Q`5|9o#xVZ-+puJc~Edz0-vM6XD1c(h&Bg<|qK}nVg0eW$nFQb!o1iiZoZvwPI0FN&F)-*kPvBNp zEokW=sGb3}TtO4q7;7idZkz>G|Da`l3~X$&-k{Y1WQ`Mn`_rIy8R)F3U!ao;K+QAo zN+sBtLZBG{M)1aal9Gu|pFlZyfJ5VzWk$=Fg zf5>@$kOGknvLyvPE(;x_h16604E&(fCI~8=q_rXCAt;rB7LR~VAY&?o9YFRD6iuMj zRwy$&pq3SQq?7?VGQtG9T91hpbV)ixEVPIOo%se{ekKFjVgyR)C@m^pP@@7=R)FV~ zKxqZEq8omWGAKcVQU-{w1(h1Vz$*j62eB~~f=6v3>6@QH6m}o183XKk1x7Y-50e>u z&s!>J#)1(v%fZPB%bB3@TyD^yHF%Q499bh9sK-g1PViO|$mS`eFy#SFbU>O*p#4*z zaWzp;!v%DsDyV{ioD~ELSjO_&KT|6F}PY@>tvRptHt7gA|~1ilA{T3dw|!*aa=!`o{z6Mt}$MkU|u6ny<5i11o5# z5^}#bI|C;NJ7^q=gMopI1KN0I;Nk)wEe`JVfdT<}){uu?1~kSCI#Uo765w)HQB+x3 z)EIGwIB0ATG=m84UxI?;7Xt$ud@Pln0aOc$Gk``@4?so^96-zYSRkXRoS@NE1`bd+ z4z$mJk&!u8h>@9D+FKYDtB|__WWl4VT;NevE-o3+fr?I0BUnJAs+@eBJbZPe8WQPX zASVY}GJ-m~3QFLhab+HU9)8H3-VBhAuqbGN3o>?MDsC*Q3>xZI1eF-dg38LEX)x&E zDl}j~@dDoM4hj)ahYLKu3hG^e3wlsL2R^Wh=<9&ON*J^{mVuLzgOQV?j)8-Lk%OTI zX=)MLJOVdfSmW?dQlad*R1{PMpM(jnAi+)ppKbwa!9dH}TF^Q0;2tfkl`PKC3yCum zPDVx+R+O7|!4txuh z?*_V$0@N}A4Q_*$zT>!k7kqUvXutx+NBj)Q5FaTqFfcMPG8C{eGO;qk=4&A9t-upD z;2AJwh$5ysJgP8O4uU4WzzGGk13?+IFkcXSG6#6+AY?NlDEdLoCeUsKSXlu*Gr|C3 zWdQ>d17vQLkr7fpfG6D`SJr~tjNpS`5G|kuIz;LL&wQhl^#|4bkTf6p69Q^$NHffbq*H4K4t5514u(1g9!4f! zo&wNK$c&6U42-FKj9grh0~x_39QbS+Sy1bmC61dBIz?`StecSsR8TO0ZpS7{d!&Pu zfBtZBt^jM*HKC!u%AKW@E}ivCuTrr zu^OA2gAV$E9CQRqd!Xh#poPu+K}sWj@HxS349pB{%&DMV@}Ld~6BD?# z1@7_7GlEtWfP!0qgIz`&biSggGPtV?>Su#mH=yAIP@4`^YJ=t*At?#kx&z&Z0vY`W z9pA*j%8JW;0XB9iWYa-+uYgTv&IXTCfF`7&W`a(@lxK)@hz8|-CPoGxUWQaIMm82^ zwonE}Mp$O&=7#p|Ksg?Ar3X0QgXdV~!6$JM$o!zGIZ%-W9r*(l0pPt2;08R{S&$(< zaA^kG-v>Gz2)subBm&M%;C>z?=YvmV2K9d+y&iDFkpU-}SOz90c_vUl8F~Bu^`Qu6`4Y8G;1xwJO}^d1LrD?%Hr zn2QlqI=~tmxK!hA`!Iq>s=*~U^yE^A>%rwUxT>lB^$T3vfZ_>0nhs4}pcKo`VCbL+ z8r=uw3iyydxNswwGr)~5ML}rJ-~m+*kepEq3MSCl9;hCJ^yP#>=P`3IvN1D)PR0Z6 zGh>YfPqTxqf|a8T3^EL$)PZuEHfVkpHU|f`5;R(3EC@;|;5rGkNC7lu2K64OAp+`? zgHjF1hmf&oMg|rpMwV3A>LKWH3z)NV=8*0jcy6l}G-L^CErRknczYdaRt^#K)Em!0 zjDa#Ufb%;$BP$;x4-+d-ssQYsO=)jV@I`zapv_LQ-ryUXV+HwnnYp=TxFJggcp(hv-c9f}RZ(zIf^$7M1A>ZK$npVDu?ohZdL5LUKxY|)caVZ= zmtVhNW6z)vrfnKR^n^fjQPQy8Ay%OFBj~ydUPf*v9>~cO>|7kFe2DO30JS9mUZO)R1OE zq=O!0(*i8f!8U^+(j90a2sGshgTn(noCsR7046C8(Od+ z=NNtlHwR}1=qV@g+vC8cJ$R%MvZ?}n(>82T4L=KLNFBPrmVudpS&$3tKG1R+PO0KrJp<5P}jsD2ag9g`uso0*zJUJn>H&bo@KC+40OLEsEF#X1rKaO7SX^Q3Th&Pi*3eor1U#DM(>bIOJUG$ZlD$) z-t`=io%i7BBFL%^8B$htfER0m2L3@MXg_2!3sg{n!V0{R_ZMj06#7a;@J$HVcFutZ zHX$7w@B$t<6SjX&hPeH6DDes!-+<(6utUM|it18m4u-mw1wMWaDqBH!_{BLy^Ybxr zGw`y2ZeQYI;9%w9fZe*p2wp%Bnw0=udkM}gEU{1~^oAfAHXkV|DU|ELL7h(!77ze6 zdZ7muvV+_QaxJLk0Lm_kf}o4u%OUk7_;M()yTN==2-kw9g2B_Aps^aH-N?|h7K9mm z9lStyRC2PjV6Lu)ocRx`6Oc1DF)OQ?!Sy(3?8sOUR4#%}`v9#i0K2Ic+JOAqkJ3|2 zg!G+YBd-jMjPOn)Xb2FtrAHpL`vE@kibqwX10Mqe%4!Yp2qx&T8dK=VCFr>P7oY(- z&?+O)-3`d?E6`2>Q3lYBe5?#yoUB}_Jj~okwI*oI8h8yXTO1z;FEbmP44V(^9uCl8 z4WA&Npa4IXTR2e46i`z#(%zuEQx4BBW4atvrwImj`PvlyWl&BG=KKtoHQz7{M5#X)DEvoJDo zaj-Enuro2FGH`M-#zGgs@PICal>skz5oZ7uz`~%U4#uEyeDK5sI8>m$Z_tt?@X{l2 zl>n_qA-j4&)0W_J1hIn%d|J35=oB6JUC~So9E?nmb9}(}RD$-3gS!KuJG$aP_uYW! zFHu7Tw9o;(U=BR#4J!CSn~6Yu%P-&^Y0&iz5a)mb2i5a{{mBqsl%fP!a)!S~;jr1cec(KMuM* z8|OYJS#R9?oZxZ<_Bk;Xf;JEhwtY^Zc|quOENHP6cqb3YouDuRmxiF+oz1|`U=Jw` z#TXcvnHlmJKvSO#%*^0g7(CD_3Tomvql-sw!?_bG`xhwDg8M2A%rBsMq!40@HUldM z0|RRT0|y7Roy5+<#J~W)!a>Fx%w~)OE!U7|1aJIekueag7A zGH9k=6toge5pusGxbX(rRRtQS0_7r*<)HQ(D0pFI8K{LW$w2(gr{D%Byk!C{YrtcD z#N2#}OE*zBpF($CfyxTd38A1CD|lc>6m&;1u{WQhwr@c7457Pp!8;eAX@P;06LOcX z3>UPk1scug5aj^Xil7n}JPw7_X+-G@fQvNHLMTwb8?<#2RF;8yx8S4nL9H85Oo6H+ zaGMNN%7Hd4m4ily!STiPjzJo9IwojbhmnhcnJX1^t_o8uc%da|4G{RcNM3GcaGM4+ z3djJNWC5M-152u)*#^+|0#K_E9De8nW5_*MkPAWS7S?42kEudZtu%wbgAXSoJ7}aH zbVeu(12ZEFc&3vFx-JX4{|TJ2Nm=6tS$_o~LA_59hMa>2y6*wpDS=c>NF+Q>gU_7> z)dQd$#m2zO!Nv;eOLH=SR#||iiy0VGx!9RN(tqkZCTF|lw4t@@P$Z0(S-0afY z!XQgQ2bY4zWKE4tLFdDQdh?)Vhp>E93u?JQVhN?s204!gboVs}13MS!$|o)cZYD17 zR0i1oNYI*39&qEC7qkfrG;{}^Rs*fd0JRx-;AiAO2X9!wqc?ouETCwL&2iveA)v!L zK$Qb13PIPBLJ}9KG6%UCy%h-BS1ru|O8-oJ(A(j-7&v)AQ?5J=yevGtpbcCohw4gu z^Mjf~5*$_gsK;0}lO?3mtxPpE~K>>3OV9E^+%1?-HFVed3>5zhrW zFbtgJnBzb*Mc|z{njobd42*RI)kZo9p&w@p>ZE`d%!8LUfF{9>LHmcmGm)SK37#us zDg=c+$QQqAYf(}IJNQh|5wcGTT=&BFNrBEbWG!G|Vqsum0^d*sxoHHvo{u38w9HNh zbaa6_11lp7Bj}P?{3@~RlLB3gq70fI0xi-4O{;-cyVip80_cokQ0WC5rGS?U4i2_# zTujWYj0_y0W&0eU1xK8z-0V!CeKRbKjEpjjJ{%kj46s50GPeL-3&jIU&)_u?#-gf7 zMFVJV78LoQYz8Wzz{ggD4gdmAts#|~khxPv#Lg*MJUgdE!E4}56-7b)OVAVzXd4)0 z3>1+gKw~ea4n~*@rN9Hi6zrM;BJ z<{u9@4M9Q=``KCG=myVbLMDi@ZMh)!>?}~~g3NM*W}^nv&M1(pvKc^QqS6fB4jycb zOyDVIM$r0oZsu4}i4Pjo;o@S51x+-AR|r5xMZv2~L6sG#rovc%0UC8ev?xJiQOcrt z_C|s72BemPZH)rQ52R&8-q~&-XEQMH#t8{Bfu|>=9Y{Re4c!1*?u$Zdc|n;-{)fy( zqwk9XHFB`-i$WSt19g+Y6YQXp``0hm%}EvQEg-xCGOH01Az0$BlSY9sbUfkzHh z9hAU(qGY{6eL>>)M8S+>`~o_g7qk)p(gQ{8iISz#o+yM(pzGQpHbKG#x+ea#w7dYZ=oU1}1sbbG z*aND+K>KxIt3J{8MS+VoytNp3%m~!2QU+hI0_y34vL|SfF?gQ|IDsPf4#kRK(xpiGrf zc0qyfFb1XPTF}iKu=))&qG0J@#=y+L!N?3A8-(wL0x!WQxC08ZWCFBf5gewV?jLLi z6sQeS`wvvjLRLpWQVUVHHh}$&uU;kX)&@`>fY>e$+9eCF&)GorIU55zCmVYz11A?F z0|#h_H1zaO@VFmjd{TxRv;qg)SR_i_4q5jP?jeIVtQ3N_Z-CqTkU>9i(gG1kr61TW zptC{PSdhBK42)b@dc{Pz1w6?F>MlVBmOw+bptc6IMF^fG2RQ(SK^FvoZa)T%wY^|K zKgk%pS^&I-L>B3edl_6eAA`D^pmS{?dxY@ZagToExQBx)7t$%$;KlLKAr#Qu0_5Iv z84lq;P(Y5oE&nqw?S*`owHp%Ub8?6*E6`Bb5QP=o@@2Cg@HT(Bd2{XRd&DF@eWpK+Rdu7&zpV zI`GL9pmm9$9wsQ3L9<<;6pU4(VcsCO$--Ft2Y8bhIR8UW&BL|(60))aa=$g`cs1}@ z4)4ItYS0-ykV$e-gC5kX2Dc5s_dmNkxPb2=XXXGM#KXY=+Aj!d&r5@ja$$-Ets?`s zjUhQs9%t?c_qd=PJ5Z+|b>|W2{3}r10$%!uh!B+Z#-QCtt`1HNSoa)(w(7%LsZ`p3 z1eziJ0!kpDD|2BZ58yTpC|yW1KzCQ@ure}&Zqq4XU;_=>v9YIu+dI&B;bjD!DZ|6Z z&CJ0e!{H;}B^59XqI8Q#Lh* zbkaa$o}hFMn$87vqd~X4gVH)Eyg{q*(C2s>AY=QwpuRghX8~w`97i4`-+@mdgl-jr zv>jkug`^$yLF(~tE<)7-UXltL2LO#~!KYh6hv3002|-XJM^F)TRRK6>fcBz+MsdMo zgP^7eq?Z6%766+A0gqx8LwukGT6E9O$WXuls-i#xl8{yBu}~&6Go&*o13D*18?2a_ zfxV8fYOL#iL353exku0t29{I3VY67Ub_6kdhQOma;28k;%r%xhLoij?+y$Py2G_Kp zCE3QNpl*C8XpLqW}18QV}ay$4252kkvg5dr03=FJ{46Lc3i-y7L&Y%GY%3oz2E4COp3w)kW)RfnL^_%XwC(}2@E)`z z2#hfnszSy=!Q)BbFaQlNLb3yd`~{g6fQ$%$&pzh|-O+@ybB3Lf5wvLrbmAj7H+UUA zxCI0$fWUzy&w|g5jNo-`qR9JYKw$>T&fv4jK-m`5Qv-L|KtrLR5fIRD1vu}*2yp!X z2}4kcGzF6K%^BFa*g)5ub22k9a27BybFwjqF@W}+@NhG+vO z4q&Te;ACcC2Hn+2tlmfmF%c1rA__Du4~-4bu13hhEzqJ(QP4vDTJWX?(4Z11+(DbP z3qkP#N-xNL3ee6PNe1W*zJ{Q^F2(mmBHa#W4193gL@&#oLaQ6;r#2s`%S$Qq!z7@!jJ7iBVEH{8=e=+vJ zKt>E8$p~~eAl5xFrjW>2G=;A8?*zB=L1hmpbRc~lNGgVm;X?Pn;4&Nk{ufY>4SE_C zBnOK#I62sZPN)T?=u`$~4n`Ix(D{~(jNr2kz+ntotp?r#DbMDER;GZ14KW%Cj(pgJ zBl!3#P!$9j9WMlRi^0t|Xv+>KVP<4x1MMGy zY@LA}yvoJ~9(k1qZM0GZD`8_~s>7uQYb^qr1^_iJK)x0P?VJFmXi$cI2WmL}ss&Zc z;IV92JtEFf3rXEN;4wr7#sbigAtz)f3uw3sbkZ_tL{Y{YwyQ-RbULgqSUG4{3o+`! zjR>rJdO`aUm67T_aP21wuKz#{AyD}VJ`)1O0AWx|2-Merq(bcb5x@zBU`huq@&!#U zfGZj3`Bi8{4e}YNW(7_7 zf#+yI1u3{7LS8e>&cM$A9-CoeU<_pd?TBXpt>K28a={K-x&dw+fhTT}CT%&PleVC3 zg5X&p(6kAt(gclHFqVTCcY$`efEHDOZfb+?Y~qBRWyc6kf70Hdu^^UMR>(F-1_s!P zZEWlk+R)QFAe~T9b^=v*pd&26H5{zRJ;ZjlfLo*3c1KYoB|+Qm;1!vm+627jnw5o> zou!U}g@KEig$r~BH4`T@6Y?3Mi1=pUu%Pd#~S>P)zIM|_UuHnao zf>Ji>nl{Lw9HhVn&&YxH6o404fbtY*^(Oj!EofCpx_wH&+}#Blw^O4i1oWL3K0ni$g%0ML!TR=oke!oN7VC*PvEA zWTiT|)CYBBP~n`MeaGfX&`ui=2CcnCSsVj7MF`2I$YBUN zQ(T;3Q12E2k4%BqXAR9U98m8FGzSeDTNh>+Y%}8^SAo_WfzG_tApvS=gJ!CrZUXl-2)PL~F9|+7TG|1(o8XFYxCzwn1T73h zauYbM^85k~<$zNHB#c1gkuxBnWXZtD%ndr#otuG~I}fzanu`T|K>!CE7e^`sGc&Y* z$iTn=o+Mxg&89&&+FOJ4GBa}5F@Q8P)R3YZJUjquz@RKi0ySVvp?m5fLj<4=XUd|W zT}z;qv7j~C;Nl9@1qC%>KmiXrgb!LqF};JXkpi_@SQ*%uSlLp!7@2q(L03tEZiE2c z5R=LcK57BHSqRh*fUJ`OuXqEsSuh+0n#+M~8Ul@0LsndX9LH>Istm%QK@0HA4S4Jx z)VT-uu)qXpokAzdJQ=7>MdYnchTfooAQQ-~PDbE^zX;yygsunYtxgp-g+IN0-G7Yf0G7IaK8cybLKCAw(JK^JXtfHMs?_26WW!}rFZ5*BnGlAtPR@?BX} z8C(y8>S)k~BakEy+71RPabPPiz;4%f(1EY9hlc?8ycf_J(cln=PvfyeLmIRh1B5|) z%|NXeq&tZI|A+Tam>9}ImxRFikTGDSb-ad=@V> zI2qVDL8siZFoN?c>_#=BQV{a=BrFA?OizN75I7*gR}6zzRDj0)z^hcjAqibmh7^C0 z9jlmO2ptszPc(o>TR|gwOoiZn3dl@Y%93T+11V`eK(}gwZsOu#3O& z0D*7i1QkxaoQ%An$$N%aP`)<=&6I#0V&cuf#l;;5+K$X^1UhBW3uF{C8&e$vl3Bc* zJfNFI*cq9@ci#|cSfqmtsAy+EIvWqvkOX4^4wNf#Koer3pn?xHM*&_q3O>{rTHt{P z)zbL7F{1a4y^?iNAGY0jW!9q^n6>VI>CXMCC1A;(p+!4AJM1fBZ_ zSsQ4?3R_IX&4GF=7AJ?OwxB8KFdzs9pDkYqI(`h~22gB)F!BgAXw@Fp74?RYt_SF3 zc5psGSr)*_L4|ur3?VD(jgapl;h@4jB!=L7NQ^MknX9uj$mLy*+##m>mW$_c*o zhm(twnX8V0iJOO;iKmVaG<^wbN%pV_?Q&8V~49W1@|VbdY0UzO@y1bzJ0EEG( zz9H%(Q2Q9)4J4ps1-kbFwCKXb8*~GS5oo^z_SPeZtTy-*TX3@$RCI#ffp3*P$Ysz? z6wqb%DCI;Eyha9{&m|5jBiI;OK-XlkFs6dem}ZV;XJuk$HuUCVWMJTq<6-0mRT~Tp zn6q&lpxf6#ty)+JL3ihi8jFHjSuhN0X@i0ZhHIg#Fd)4Nd4^?>oPmDuRvsT{^(O;Y z0qm?U)NljOBE|{uGjVbnfsU-Q1MTnRMn1O-te=q!bW|q~c=;wi1Bgrg;9(HZFgj?d zA!y?;c#$V)ISeHAgOUei?Gh;6LmFTZK4|eB=r%LhS~KPs;9Ys3+rR=GeEGTAm|1uj zA#*#t42%rCjHwJ99FRp1hTeRjt(PX=pz%^81|QIon4rW0Uy~t#bPTxwhcxKa21sWP zlwU=SMO8r;dWkBVvVsrdhW0aH9U;(UDQI^Ts1}1bx)#1F8k9mnqY|K9q3jGCjO^f5 z(M-&YOsSw-eIdK6!I=mgzTlD)G|hxKM1qS$N*lEI7}P3P1fL5I+Q7*C0<;twbR{Op zM#%95pxa0hYusTrgVtlSGjOniY-VL(WME|k^(vvOAq`<6!oYyVR#5#3!r(hzSdB%M znO}gs&R7mUXo~p-=v=^mo#1(QP_)6En_awOsb(b zXaE4_VHOrpdmQN^Q*I8(K3G+7+YY=k(iCJnsKM|K)ER-K2hc`SP-`6AoP^D#fqV_R zeHB!Kfcl`EOst%t9K7tztSpR7+^kHgpe)H4%f`dR$Y=;U#0b1x%ZM3##2={V33Cf% z`yG6ukB>uCTNso8z~gzKM64*PXe_F%sw`*>I_U&Fp9i`ntroO+40OUKs9ginQpfHzbP-akPSP$`)H9I3S=op0pQDHu29w9a+7H%GvQ~^d-R^C`aMqXZM!5AyT z&&17b;tjshGY(X6f<0#g*3H1oSVxBTNCzzy6$SkUY6m)xhXGu$fG+W1U}G}^m#^%xJdEt@W}w4)U_}g4eZ$Kkt!)T$0(eOdq?iW{ zy?`*JY=drN0@ePYz5}RL0NVKTw;#4T4CH(8-4_fj9E=Q{u$gU;mrcAuqrDLCn}F{3 zh3tbd1GSe>hW@xYWVOMaO3+dUP!9?`yP^zUtqYoCfvuncw}=qo&in#=Y7%HhiiLrh zorM|H0s@`v&6CQ&$jQjS4T=fSy;2OUtl%D;38>WNiUX}kG~)7MU;r&vU=RhBOW-~) zh~NV^i@-}hL5=~fkOMmiwC33q6iJ}c2fPpl8Zw~v5h%++Dh5cXKz8V$?+;>U1Z~h^ z1ntWQ@4E$8LMEWmF-W+8+mRUI0vUY-J4h5XW&^@t`@nqn50^K>vz{SAG&Bd4oIx`#ASz%!T2NL+4MI+FCC6H4r z%s|)Hxk1cfsiVxGNCzog=X8M60cgiA$iJYrsUmdX0aPL?gYH5BWkyid20A4Ka;-6_ zo&XKQg6FG1H63Ke1GJ{f!od`DlsPN~a)25D;8Qw{yuoa6448qgyG34A1#SX>+aAWO zU`i3vAcn1~f-I-{+Yf4UA-6j~=i)0f?1F^33oj!#GZPPYfgmG4BLg3Q0RtC<03(+` zD)<^I20qX}eCR2*pu!l@qhR3UEPf1IWh=pz#z2(1|*r#b@9KC~WG9jY9&|bpow~1#iLvRd1j; zF9)@DK&FDn*qGiih%-!rgt!?4D-$CNYXJir7biPg0R!lWRz@z+CQeRvCQi@-KFBpk z;7Wxfj)$9xgTn~4@52J5k%^VD4y2QdjkAtet>6J&tQ{*;&{Pb#{SK-`Kpj6&wW2Hv z>R5w<3RH}O4=n+4K#3RbBjy(jvJ7h=K5}4SVr64yD&SybZa3@j|*AqH^%1+`}w7`Q;Ag;&iP=B~RyIyH)&d3=E^aOs&?Fi&7dLYa12Y2?4>MCL0}lgeV?!FK z4FulkVF(%phK?dJ@bZH9HGoTJ{#XGJ0E%)@ z?F5bz=zbH(92h9ZnchKmCxfRmKmrI1o1FyD>w|AZ!`#jYYG@a&j=SvVuoFk$Wv%95SH0szD_axRVIFPXRQt3~3&L zmwrG>M9`sUkTF(B3g>3vXRvZGXJBRnwcpqnSXkLuKsOnKYb40{0Ayg-3^YxKG0=)= zrzt`<34-b$&_WH+bU!GQf%eBjQyVj+RR-FV$IQUQ21-(F42S0J_2u<;zDi~A)Lvk7~gD^uKB)#i0a4@nk za}%*yOIL_x5E8aK1e6ja8QdLQSQr`DSs6kZ zxVYG2xxt+)&~5aPg8+@Zd3iXPLHqF-n3&AKQ>T*P)-m!aYM`buAUo?o zTWmzZ>*~R-03`;<9!F564Q(ca90d*oNM0cCybu%Qo0P##QWDP#K{o*0E@Dt-KyMl$ z4lo6^iwsd~I1mrqEJf7ohS zOsSyz13-N>1_s7B&_t6FqmPIP7Z+rNl1rEi6dIt_Wr6~rBmk;%L3OPusC5iF#vXJ8 zub{FZ$a+wT0Le@sXOu(B!7tz|HXvtrGQ9(j&_UKjL+>63jcJ4SC_(48K;02gONN63 zd}5^$lMf3EXazi|dnPC(C;%#e8I?_qg+)O}+k$%vqQ-)tiE+?*QK0Y!t!;(gHc$&X zg$X>m54D+{K@fC`9|H?3J1Yx&9rB)iLr@XTaR(i@YeZib9Fp3?(8YhCMO4h7@g>mICwPxNv}OYJr9l-T z{EYru(0RL{?1_GAw=_eHLnI$)Jv|3AQz}0%6DJ!Z3l}H&t`%^dW#|pMJ_@p+9z0LY z8pi;xFhPSZ9MT-1VQ$!<1E@NKPEvx-$m0;#21Ow|(y%6S!3L_@KrJ^=V;?jCPzw!5 za0G+g3R+wV8gB;2EHmgfCD5FyG()UI6gML~4aF zjo^18;B+o@yM-X?C=s~KLoSCw&IEONK-mC%4>>3yLN^6KTnnlOSkdNq`5BZ!Zf0O- zVB=wDO9dT-#Kp+L#|uvite{qjp*KGx=q@M$Mjjp$Z*UKh0o*|p5D-unz$m;xi2;;Z z1VDusEJ27v69y}E6d5_V5lI7-G8n(02Rqnro#4_OoR!f-9#pD8_kkLLyu!v_09vmO z9ri}b-k`b%)TjlIF~Ekur5%hxni$yG>PXNP=^)0!0!m$=Yg$0Q1+9b^;02AWg31P{ zFG1~6a51eYs0d0_pwq#@Z6&Zjz`L%&UIw>H|8;`zIE2h>fzp#8gEWIYLxcn9?hhtV zCJ^Ko;ALUw<79#ybOKt?2p)3-EhRJY25oGG+|p^p>LVi~BqSp*BQGZ_BrOEb2@r=0 z3JM589m=k3YRnAEtl;_-6gi;F7>&VW;vffuDpOF23L1?AWnIu9B*YVtYyin6ouE6J zkn;j)w6Gb{_A+2#1MMybAvfAVYGkHD zkl#Ux5ae-CDFMUaa~mOJcc4>jL>b^C8!Qa0phXApm;*JOAxr8^yxF;#*x0~_jT=GO z;HIh>Xs*T_v?`NRNiw;w#h0q!6$Fo6;%(>n&xN?~>ei;Zk4E)KdZjEoEn%#5HWFf$Vq zb10|{1adqxlP?Pk0|V$hRcPVM#v!V`k&VFxbOJ@Oq#5Qx z(u)-XWcZDNmyv;sw*WHg#)UNM<_R90hBUu?z}@RO&}6>PR7$1Atmo;3gv|TZ8uLfHOVPY5?#CW*$)g4%|lp zuUOM|&|qL<03EUf8VvFTFByfLCN9Q021afsM(!F025uf6(3u^~Y^v<%YNPI%^&1$D|Hxee3_ zgD(A21~=3dMM2pOvTh$#pn~?#gVPR(17e_r0Q{~{PBvCH&H^q*76xW+7Une2d;)hU z10(dTXHY!`^E4}~4`_r6$q5Pw?iI2nO_3+~{8#$DjUPT;w5 zkk>#MoUkAaq}&2JzX4iD=rb^Kv9dB2FfcQ4F*0+daxk*8fxC-o44|7%7+6?5LH_dr zH~73j3vvuVI=C2FL6ZqYX#o$GLdq1R2`C;;X>CWP9H7SJvOIJUrv2edB((MAH@kt+{sd$Te!voXTv!}u7u_&_Jjb1;I6 ze$d6s;L$QqZw7vT@SK>BHv=y(XRH7uwip<2PN)ci90wXB24hfR4_a=g3`*UKqQ=bN zPCPh-K?w`AtO3-w`NjAJJX8PAkJ3=$ACD)Y)BkfGcdAqfEFEcfbKM| zVPNKBVGd>BVqjwDVoGIWWMN=uWMK#S0lan06A}^dbY=t6&CUqEcAlMsfi&&-QW+>W zgS-cd6i_AtPa-LTax$oY1ZpaP8{eRIH>CUp2{V2HuP^+?_yycI=4Q}vP=(cPEQ}0n zjI0c-Nes-)iKvUsxg@oPVa+UXw;NQ@gF+OP0>GsN%tY|%q@eZ?BMWOP8)$(!I|F+X z>>8p(&@Hwp;3cU14E)emIwzMj*mTgaAuIwxZE$dsf>;8IK~SBHuq6``H>wPbOst?C zNT7vR4D5-ZMUCK`p9mSqW?*Ga0X0h1AnHI(G!8~~oC+~kH$t5T8i@nVk%G#8P|5+j z3QT}p1{wndxvY@+1p_BT7{q0=3{0TAS_@!p!W3`NCb<3DC&ens%*+5j zv<7l0GY4pE9n>p4j9k)u+{_I8TnwoU`~r-;g8aOx!i<6pLL!1fsp4WxBBG)qp&(jB zG){t1R7BKILIQLMyP<);p019zmZk>c_Ie3f3GgC)P`M8CF=*I_ixawWip!~qQnd;w=7&|)L-l0S$% zB+EkBprb;-b1BR(7!ctE3MEYjU4|ZTC^bs3isKC>Zbk+HE{0S=MgayPegUCWVMcxt zMm|w~zBC3=US82q22oMoSTROkQC?qA@Sq0`I9w!f28<+ufKdgtios)upuhkH4hW+M z3Mj{edaFDTe}gV2hBQhbd{6-fVu9`_!we757y>9f3>i!r=7Pgyk_;;o6N@x6Gb0-) z#jvq6GH|kiE&%7`X5`@I6!e9p98(ixBW$ThRz^ZvLRv}^XCTSJ0tvKZ!BkNc)Xf5|^ki2y zwPq4k1|6&f3LTtb1=(~6DtbYo06IhpGFF5dWZ57|WDE~B&~C|J%-PJ@4AKm44$hEM zEmg%Ag$&FLat_iAplx%Z;JN^`NS4tT zyb*<2kQro|sj(<1GJk7Tuh)IGN|Fn;^{2_8i4ZxojD6?A!F^c!&}Ip!6eXZCI~~O)IsHtC}?>p zIN^d*4LFWKTu@R1(UA7zFVMgeBf~F-T1HUJGc!m!h`~}bD6Wu_vpG1$)c%63I)U{a z#TlkU+C}CJjLgv1G&2W74WtkPUGK!ez{AMN#lV@yz{CVDc|1YGMclExjNIH_pe=b8 zAf3#RmNZFP!Dq-aFhKG?N`H-yQx@9vR0K`2fj2EEizMO6OD52@l0Kl$8+RPs ze^w}ZnQKVU9OC+ED>LZUOa|~$Voyf!(t4yKlnrDVbkZ7>J(&tY z3tK^$fsx@CQz7FQ@Tjw-gBTNR!<{F%QvkY199$#{8ViET9IzJ1j3%@c;|8xK0Ua61 z3@XK-?Lu&p0ZnLnu|nL!&4TQC4v>35*$y;tB??*}20FJ7w7?cTk_z(SP;Nzo>U9R@ zYzENGq8hm6gSWl`jqn;mE<^|STtMpv!98Bk9qeq(OyFIw?2HVI>o{-?HGyg#P~Q#I5C_#jkOc&Ylnu_!;GI^W zS_3p^4y)5(c@=cN38+r8_XBg_CB2?A|M2JLNTXJp`D2Mr;jM+kJOC%9OFM~$?D z1xO=GoMOf$R;}1afQC^Q&Ap#eIg z8dQn-fR+^7f_1Ypanymeb24&4LW4N{#D@mx!cLHiBNb9~qsTJ&2u&coYm2wKp~91FIV*^>c$*&%V(e5nQBH}k6&w9ge(D}&l(;8rJi)e*=Z zGY4Z7`lKfcg-iErP#5{cqUFAEJY@r z$jr~k!34UCii?|(k&_El!*KF2GVp?~X9CZtf^Se@WMcC12CYT&;`M>dCtyy;@Br%KA&+0Nv#~Xc{Dr}N2hglLxZMs;PN1v? ztCm2gfe9O%Dua54uy_E)02qTEj7)&m*n!Rl1FZ*yt@z;t?`D)`h;j($WMtvtU*N6&2 zfPes}tTt#l7HCzdpfMxJ?ck*-qQ=I`rpDre%AjGNU%x;jKcE&E+_j)Cgb?`b@>=Mn z37|*-wILZGp|8x4?2sTN$i&aUz{Jmx%EQRQ&B)3Qszlkj7&+KLoBtUZ1!5Tl1U$WY z89}GY@qyDk0~;H7OADlrgm)1($m!rQA&{%V7+ku5W*I>ZCJ+YAc`#?g3Qo{r$RKtt zczGGzFE9qEbqRGn@W9S5MxW}rPe#2RhXWH=nlyO22M71HqHVb(DqC=PHr~P@*b#z zK;yOG(G4Ha=4kM`FE3EVZUxfH%*s#)(aT;(yyi#;F&-WsaUSse0BAWID8&eXmJBe0 zRuF^IyrL**xwR-{S(~yc(>w4vM*liNDFwV_fCsc69UQ#S>{$zLdqPef0G%%G;o!>4 z$i=|G!39bp9PEsYOdO1$%AXZ-!Vv>EH#jwTf|kE~flloKrC{{kxsZe)st9WMff58{ zlpoaPRTNcZegVFu4;0j(^C&>c6r3W!&H|MvpxO{tN{d5pm1kgMVrBzv90nbt0vb?Y zXNQI~=(q;R2`b=mOw5gb&^1S})CVfnKnlK3f>s%UMzFwv1gQwYg)=DbVQaXc zwHj#EJ-99gt>FUo>_JUUZ%~sP-?)*YC?j+iQY~nO$}gsO;583_g&?s4sh=jsnO@6rh?2RPTUV05I2p&)0#BB7#Pbx#1-X$`4P_G`WvA zbP*Y@{leff8DcZ2s{rdZ!CVGfBM2%5z-2Iq0J{xT!+;nJ$S3PCNrFa=;q@XngN}nH z0}~@FQz&At13Z|=#>mRV3YsN>A9%+p3EHU&uMohNg2wzoEvH`$oFFrq-ho>%pcw_w z%3RR7F`!c`N*oG>nT41cnLtOQFr+ddrjA8>k^!PFc7FJf4 zQ1GdQ9E_|itiF(`FRW82jHbrmVQyB?xM=yWe>~8%4Ngd)o93Aq7(k^u(>ri`P18Y* zfeAcQlnB}>3eH77SQ~Ai0T9smIdk@(sm$4+btoXy5G#T}qvg^JT-&82_*fVjm<>RE z4$u+;V^FaNx_BFOKndt1eo);BX_0`67|{GUsL}_WPY#+GMuZLcyclRM1dmZmGi-yD z^KJ|*>>QvKy^P#Eps^-)24+rn=2Xy$Domi0SeO`i7??oUFR+5wu7N9la8VDggpigQ zdVmaKXW;-X)8yf1Lz4`=EKwT5#Nh*1m9T6&2xRU^0MC?I6NZSrjy_ z2Rdf}TvUPx&~z3w5J6n<8ZgAzt2hJPEJjEl99;f@%mFn>LCG9c2|(QiT7wO1e1OaZ zrF(F%2i%9{X5eSAaj*pSVHsmVdmVkeLC4p@28h|%*keIE1-;mPcz75XAdPzlSn0>b zDG6Hv1WplPo6AAPA2{cNTEd`Nc94x6ppiMOC84N;kerN^gqVn+03Qz*2P^3OPe}Fy z6;z-Dfk2m)iHbs2uz(6F5Fc_M79{(Dx(Qz(#~y*oUr6o*c@>JmGrQ1I1LR*}1}E@* z1Zagkc+iB4lZ}~`gPnTQ^6=38OV&oU*;|DEX z5@8e+6&3{FJkA&^#li&Y&~h`fva-i=F|xD!cuO#fii*WaGKz_LiTSXx;abatJkkK! zRR~%pAtWd$Ak8TO-v21749bb1zOJY-sGtC~ZeSS1H&q4|As}pO49XIqwmB%@@_@H& zg1ijHpnL~fat;n2(1}c-_75n$LG$krHJ~OEsBnX>7zCY1h8jMgurX)oap>e^WaEH_ z4le@_KQB+Jthfv_znCBsp9nu+ssy8mB%^?khyXNzK#9TAn+qI1ply~u-cq7WLPElE z(u~5wUcx>K3NkY0W{@Rc3i=BAsA~o>24x^cm!NHpr3KB>NK##@)hZCsV4HAXyYyyW8NE4(21Cr zG@cGB2ZUky2Q=dau3e!+Tc90qppXV>s0GhJf?1&Lyr5ZSNWT)Kn-!G%R818H!L9|j zOh7pqbXX9m+JoojR4w30M-xEzzDf#1H=U1rp~~?1J6yUB{`^9pw6JppwA%dAf>0HrJ<^#q#!FJ zDIp538zH4T52SPlZ50+SGk38UL zghmlKaf986PC!De7Tj9kXJFgR4hnO~{0ykq04hBpCuo8-f=edw+%>F}1nGg6(h#+v z4jL$gLBk`U?ijqj#%dERY(c{+AYI^ag=hh31{s2`4_llFGQER34eU(tO)bd!q+w|h zG_wJ&8z6xSZtH>gAiF_Y89{e(f*2qSN>$*53S!hUK=yZoLkiTl7iLgq;C0}V5aH+H zWMyJtfy5j$c<2JW&JxUmq*~C@7f{_KY7B}ka9IRmfGmNGsW5@=1P5`!7#wS$q6d;6 zK)Ferfo(H4C~hD=K+8{}pg01%5}F@j84_d&0}E({0My-}eyT9Y-2yzE9IPyGcZ0f& zpcWZBC>9k#H(7%M6dbIe>y(V8b_ef1ei-983;7O4yraGmVw50K<)*V4a^M6pjJ936R7?Lm9*g9y&%Iu7_{LJ zw5$s}kf{t>FlY+00OWp19D!%JAiMNIW3piLL9*bQ`4`$wWr%NL9iliH*%(-u*;vwe zL2X%PE+*zwa03&x%nQ6W2E2j|yypmVu%Z{YkBA6pVKrnG8fg3;F*61qy@#x*S2Sfe z7F1>w1Ql+^g6zto#*C(lqM(E5!0!Cp4{tHVY|=wMN{5eA3OvLGT3(|F+F>Bd3~DNX&tj5rfJOQ2h@YJ_HY8f@>9U)d(KK`SlAtt_sS5pcU<%;2a3zLDu^-fKS%N zUXM=$_cFUdEe6m=XBI|gF;*rfc4nqj32qK?7Ip?s9(GR9d0*h>G6SC^KNmBbARk+* zkQ9@kFr$Eopg<}Ec&7-1h=^z`gQ$q8FR0}wA_AIxgKZLlBv?e750RB2y>-wy8|Y97 z0clQ2NY7Q#R1w@c1l4eY%7UQgf~uf0`0QO!lLA!8gTxdSMHNAP3rGWo@e63TFtnct z>d=82Gqs?Y2eq{R@jzOApdbU)TOcu54O5FPq(C>J!S;$6GOP!O)DonSl7xno3?GLS zD5Us7A;mAtD8R)p0G`zo1nm?OloR5W=3!+M5oAjhm1hzWV-yw_5l&?gmyi$-We^va zh-Huvm+)0nQ&N(Xlahk0yNB(PP}5Y?)KFJaRZ>+^mQ$2dRFH>Eo(V{TFBu2t1hgPj zz!8MPppGAS2n2K$FKCT62;&P#P^tLq7kHo*6lORA5*&=+N(RIN6(OLM2ReZRG@9@Q zTw{ZJq0FFqUl=qW0M4Mw;1hWueQ)qk68OySa?mIO3uvV)cufH;&ANlb-Ub%-oUGv0 z8PIdX*xA`a89;+T4D4*|;DK9EuM3o+?i(ppCABQ-2u7pPsc$M)IkD|_JSuZ zn8C}nydZ~ZBaL{2R{S!8uEvHK1`2Ob_ZCzufx?0j-s<6I0FNXyfo5PpJ2)T%VW8~| zkXaDefEPPxxF5D+9df$?sL2B=Ex=o+KrKpGNe;eAgq;zzDVY;A?+lvs2Q7DjIM&A- z_X<%^Qwh}d0ymQ&4Ny@~#sgUeDxJWkESNyvuLA0M@-hg5>knB5&|E&300#>zCnGyI zD`+Dq0}n4Z54eBH6wAWT#KhzY+Re%s%g4q9-q;`{#K*?~zYYd+Nj;w+pP&FgY=a#G zczj9-v=;%?QW69gBA_||REP*F3xbX^VOJDY`~{f~1PvX)YCKRv_}9t&q84-yNo{TI zUm?(ufS||#VbEX`XjT@q#{_(~fhjmmYB4Y}urM&Pq%tuwFf%iRf|h15$Fi|9F*7iu zPr`swpfGITzbR-%Ex0)X$perTRG{6Z;1wuDu11EeNGt@UV+IDMzgA4|{+WPNEjt-! zSa3qlumBCxe)>30pDq zSE%+^?Y}P2Y!(9p<6jxZ^#2TuPZ*dPhzvte6Y;MM;}gboa2zl&6*Bx{e!;-X;0JOM zq?IEMnge5G%0~>m#)1Z3eWe}5Kr)~~QbtAw*g^;fM&C#W(0CL$F|u%qX@lG=2=2Fo zR%?NVZ9r)WG*SuHx#{g6(vVzM9(4bo!|C&k6>Y0489YK-WNlRtkc%0V^wbYZ&-mF3ee2 zumzxb8c=TvwAKsUQ-kM!CI-+ltwQJ-G-QN2XjBoD$3Rs!s1T+@nZ*QJr2-FqsGFG> zL5o1ZWg&E#8UrJ?ZE}$C0JUntN4`KCu?&phc@WU91`_hHF{t{Xea>J(OKtoN(DcT^ z%*4RN$jHI~S~CJpZ=kJg;I#rQtiCKPpf(9C%?Sw#aKd)9g6_!xtvUnI#-hrgQ5DGX zW}uD5iUAD>?%g)Hi!05}t0kRO}MQ|qsW+7-A9b_kH-3(}m z8C*ny+fcQL)*=O^8uZL4P<;!{wV+Xo7hr#Y(;2w!3@O$48LYrQ(Fgg2frXigkqxwq ziw(W;i|P>$NN-9Qnz%sa1IQzw)_OT8vcSXPNQsOY-r8gahaT5FgZf>FlgfP?VpR znUl)EpfRy_qOHf_0FhSzVAqLV9@ZTiDJ%TMT)Hj1M7t9fZV`f|(bwG4@$t~(+BxOtAMX9lurM%j)lE~*6|+*8 zJTf$#9W7cN_wcX=o1jZb(z1CT`e_d$7wVN=Unyb-TLliyu^uY?aXzTr5t6vJrp!abm2nUz~r6@nH0V{IheT zU&}1#cW6u7TEi?O!}QqoRNVK9^8Dv_{=LmS`<{I_SJ9C@$`dLX7#P@+yxm&5eP9fwRCPvY3H^TNs2H8D`CqU|?WiFY)wsWq-jaC$7!A>MUOo0|SF(iEBiO zbAE1aYF-J0b5UwyNotBhd1gt5g1e`0KzJjcI0FOo5>FS$kcwMx@5Tm6hYPS>$ZLI* ze1xfIRgS%KSke}u&Z!LAjyBONg(~@te{-z(+z_K_y+mu{L=)xtn;CYLuK)7#wd7wH zhQSBExZh&xJA2%-dgscsKTk8;%KT@^$qe)k^Zjgo3e(A?IAIG}*eE*X_-G8Prqo&Nl za<$3ZELZNbb1>K^a-!p%QI08Wb&34DzB~K6_uin{vs_&)MS8c^j|&QWMl? z|4>nARhF@nxWDzY9n0hUoRQs=eQS!0j-NMm|0UfsxlAgwd~eO8lyi3LkDL2>7PstO zJ^gjh-YqL{?GjOaAP~4C^iV3eR%5vt2*bCT^lB)2jSt zVQcBNo&pwuz7O-=uAG}C*!Q7daI00eo5dl+EqA3j-5+vT*Eu|UQP*&OQMGz`i+?<;k#?`sGGmX_%WF9Jn?eAZ|sOaMUYZ2zFzgE0# z>A6}EAb(Eo{UZDQmp8@O&WW=SeaNx*Li_9fb;eSw7naX6*5~`Vw^Q_@ZF*MEEM#J*!O2kqXP?iUfTCbT;?US@`OwAi6ALYS3j3^P6FgZf>FlgfP?VpR znUl)EpfRy_qOHf_0FhSzVAqLV9@ZTiDJ%TMT)Hj1M7t9fZV`f|(bwG4@$t~(+BxOtAMX9lurM%j)lE~*6|+*8 zJTf$#9W7cN_wcX=o1jZb(z1CT`e_d$7wVN=Unyb-TLliyu^uY?aXzTr5t6vJrp!abm2nUz~r6@nH0V{IheT zU&}1#cW6u7TEi?O!}QqoRNVK9^8Dv_{=LmS`<{I_SJ9C@$`dLX7#P@+yxm&5eP9fwRCPvY3H^TNs2H8D`CqU|?WiFY)wsWq-jaC$7zLYx%Fg3=9mCC9V-A z&iT2ysd*&~&PAz-C8;S2<(VZJ3hti10pX2&;tUKdA3a?hLn>~)z3Uis+e3uy!FQW8 zwmE6?A)*n-=cGJPIpC?FelYU*N~4?nmu%a1q&M15owSmPRQ4rd(PO~?t5UVBY3z+!p&t$hnn)Njt&jWPRE+cj*PU;fsfBA z%G|u|aOTA3_&;5qX?6dslaw?B``&nJOzTcf{Sl?QNHQgk)3Zg{`sI#Q$Cpldy`Af5 z=+u;yIN$Weox62q64h&Le!c!B`t~!o_sKmq5=YWQmlU+rh4;9B-!5qQ?#HId?PrP^ zHDwl-uUaK1H>K$Bodli0JT}W8K}xgcr!QW;N={BE`eTq@-Mo`=|7$P)IbkC@;gDe2 z#-u+>?{oIAu98_AWApVdQ;@|WKi1^V45QC~woX<*KkLay=_Lg%aV(G9e<^K>S+`nj zd3pQt{K~WH>NQ?*e*62^*~mWRxXpR&c-i+dX~nN08tKn|UcdR`Sn#cQ=IsspwEBe& zqF3HqcHccbaFeOb^QAE%p|)S9v?!O{^IQM(ZT=fe=-0$Vv z2OrA~eQc)YxbK{sdFsxyH~$|G>ADtWap;+@n&q+Dl~VuDzCQDLTK_S%v=-%@JC3JS z&TLzCZ0XA!lg!m}Tdy4wG|QKpA69;=a`WGVQ%{~!)Gw2Dw>b3dk^1w!xqILr_wX~M62@XFF{hEvYIb?Yl=Nfq5@mGNuCg&2Wlb$W{(_hKDD1=d2D7OJujrh`oJmf|mP3j@kLEy)O$nMDrit zW;!D`+0Ek6v*MMuqmvKjb*Cec_f`>NVNf8xphE=}NRJmx%PuoF8o`H9zEQs}RZdAlb#$l~35 z`OFvd?PPs7-qhQo|C$H(_B44UvYH>C7Fk*QzNIHe$4;%*ttHte*y7Z^zL?UJ+8Jv|NZ=E zhxxXmL8_G1Y5d-hH@oxsuF6@mpy<;jK@cT|Q6p`ZRN4Mp=}F)BliA+dpfk+!oJqFzh@U zxbC#ZJK_Z3TfcL?--n5O$pEo13@#m!gWOnG_tVdj)Q zZ{9Si>a#9BC0Mq+Y0axb&8Jntzip;Ca%w$D&ePS;Wt~$(69CZ=2|@q> literal 0 HcmV?d00001 diff --git a/port/textures/buttons/AnalogStick.png b/port/textures/buttons/AnalogStick.png new file mode 100644 index 0000000000000000000000000000000000000000..d3fec7d015eb293346187db09fea10407ad7cf2a GIT binary patch literal 2206 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 Y<^LOX!Yb**lq8Ump00i_>zopr002?FU;qFB literal 0 HcmV?d00001 diff --git a/port/textures/buttons/BBtn.png b/port/textures/buttons/BBtn.png new file mode 100644 index 0000000000000000000000000000000000000000..8e2fbb54b5345f3d11afb2d62e2361d982fdfbab GIT binary patch literal 1534 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 literal 0 HcmV?d00001 diff --git a/port/textures/buttons/CDown.png b/port/textures/buttons/CDown.png new file mode 100644 index 0000000000000000000000000000000000000000..f30cec2b3b2342eb58de482a8ae5f6777dc74455 GIT binary patch literal 1285 zcmeAS@N?(olHy`uVBq!ia0y~yV033-U|7$=#=yXkrzvB=z`)p=>FgZf>FlgfP?VpR znUl)EpfRy_qOHf_0FhSzVAqLV9@ZTiDJ%TMT)Hj1M7t9fZV`f|(bwG4@$t~(+BxOtAMX9lurM%j)lE~*6|+*8 zJTf$#9W7cN_wcX=o1jZb(z1CT`e_d$7wVN=Unyb-TLliyu^uY?aXzTr5t6vJrp!abm2nUz~r6@nH0V{IheT zU&}1#cW6u7TEi?O!}QqoRNVK9^8Dv_{=LmS`<{I_SJ9C@$`dLX7#P@+yxm&5eP9fwRCPvY3H^TNs2H8D`CqU|?WiFY)wsWq-jaC$6nuzt(>m0|SF(iEBiO zbAE1aYF-J0b5UwyNotBhd1gt5g1e`0KzJjcI0FOI9Zwg>kcwMx@80#5c9dYfV0};I zkqN^hrUjE67!?8=3t2sOFwF6K6w{u_=@`P#VIk93*Xs$UYfSVchb6)S@N0R&%LX6vp5vCYu?iZD`#GL z@?d6n<;AH!-~BcjxIW~#E!-t8Y3{pjo^;oLo|AgVXDumcS)0+lcvgP?o*n(=e#Yi^ zr%OsQuDO0zXYzKwV=608UOlTLEPPESQMuXB^!fWer_PuA8=2-l=C=AM5UM_k9;ud3N9FbpFhF=gRvw^o3no^KbWo(wN|47Ph-mTiwqJZaL;|;=Aq- zn~D1cFyAccVC|{trT3jL?|Elm(4xFy>aUdhA}z{1UMpYc=PtgV|IN|jkiiv3mSEAo zLxOA>Z{%8(KWx8d%i+%OkmFChq(ip$ti=T_3tzGe_-w7P?k{M0sJr7mf9kZCJnNn^ zuP!N`xqHpW&X4a_maeyCs?X|musEdfRrR?1$6JR4E3Vx06f<3IFSfW zvnfA=ZmFA}oOLGqWcWf2nTLm2o*MnBV|{Aj89iy6|E<7JdtYz*U?N3w5}Wr_UHadQ U#f_3N3qX23UHx3vIVCg!0QRly7XSbN literal 0 HcmV?d00001 diff --git a/port/textures/buttons/CDownOutline.png b/port/textures/buttons/CDownOutline.png new file mode 100644 index 0000000000000000000000000000000000000000..9324c282d4de2e78695b3b7c484f7e8d037a2a6a GIT binary patch literal 1544 zcmeAS@N?(olHy`uVBq!ia0y~yV033-U|7$=#=yXkrzvB=z`)p=>FgZf>FlgfP?VpR znUl)EpfRy_qOHf_0FhSzVAqLV9@ZTiDJ%TMT)Hj1M7t9fZV`f|(bwG4@$t~(+BxOtAMX9lurM%j)lE~*6|+*8 zJTf$#9W7cN_wcX=o1jZb(z1CT`e_d$7wVN=Unyb-TLliyu^uY?aXzTr5t6vJrp!abm2nUz~r6@nH0V{IheT zU&}1#cW6u7TEi?O!}QqoRNVK9^8Dv_{=LmS`<{I_SJ9C@$`dLX7#P@+yxm&5eP9fwRCPvY3H^TNs2H8D`CqU|?WiFY)wsWq-jaC$6nsaiF`Ofq_A?#5JPC zIX^cyHLrxhxhOTUBsE2$JhLQ2!QIn0AiR-JoPmM)zNd?0NX4zUcQ5)r_F!OpP%rdr zM-20wrA$t$+^h{QtPZRm3mJBJafqf&XS;U&xK%O#lQ_Q{Evhnp-+rseooH6}eGz<h??T5&^ug4?T^=>O@*}H4X zx|6k^w&&|c`CWROx4NGdEZbzd?$qyj{&V7%c)qNwNw+ySH{PMRWo}mQ?0d;~ zj$E&Nx9go_R{tzzp7$-*C#J7E{W6y|dD0cL>o0fww~*Wao>%4}$DjPHZNF!n{i5W1 z-7x&*L_zUB1C6Eo?%eXa`7iaIM&+9yHIJqX^cievvv`?N+<*Jux^2n@s|!T>3>@^? zF1zp9d#Tvxp@vLiL0nGp>Dh1D!_FH@+cy`q+%5RPnETGbS)^}+ve)&;rlr3f&0fuz zsyAzKLCeC(QkKi^HgUn$Oxwj)f97zPxX1bZ#>Ho8xA(l?vFGHIt)W!{eFhS*bGhTP z`X|rZo_=r5d)X}R`&{lfbWOu=-?)}gIXm*aT5NBlSf7E%>PeUPHPoDc`t_6A_f1KQ3-{Tdy)#eB^*|T9s$|O)htv_rWzzku zyE$RE*rE>!mKiFnxBH>9-E?(9%ii1xO@S7N)<4gjH+fb}p0Pr#*ZzJ^Nr{HTpYN>q z@;faS#FgZf>FlgfP?VpR znUl)EpfRy_qOHf_0FhSzVAqLV9@ZTiDJ%TMT)Hj1M7t9fZV`f|(bwG4@$t~(+BxOtAMX9lurM%j)lE~*6|+*8 zJTf$#9W7cN_wcX=o1jZb(z1CT`e_d$7wVN=Unyb-TLliyu^uY?aXzTr5t6vJrp!abm2nUz~r6@nH0V{IheT zU&}1#cW6u7TEi?O!}QqoRNVK9^8Dv_{=LmS`<{I_SJ9C@$`dLX7#P@+yxm&5eP9fwRCPvY3H^TNs2H8D`CqU|?WiFY)wsWq-jaC$4RsT0Z+V0|SF(iEBiO zbAE1aYF-J0b5UwyNotBhd1gt5g1e`0KzJjcI0FOIBTpB{kcwMx?_Tu19U#H_p!hN0 zBt3>Cri4k87#)(DCU8zjV7TNdrRK1K;ojn{`SKp2D|G`{Sk!;ob+R96UOWHCi#rw{ zeHmy6D!h+!Jg6)ew|q7EZ=lh-lNS8;o|mRA@tw3TWtx2EcR%?$cZ)-AyLO&&3^rbS z@*ubVD%WtM+t%j}iL@%`_-V{bzhknqDpTX1vC3{A6VHbn(ig?tP4D0Pp)~u=oRqv> zuTxW4K0Nqlba^}9F_o1kuf9!PrnbiNP!wBo*13B>v#f91o;$OpRXOKv&@aQ1 z`7`E5FUqtk^Z5R+Ft{2z*I9SoIc1r|3jIH?-T!^L zTM}VbeZz0N_|Eg}_YCjZ7XHobJ1{>=S)k9rqxNKqbboI_i~3vH^uW!(Vi~%_h%7mQ_EmDjZ1eSBsbM(Bii1c!C%T*`8=rUi5yE^UahHqtlM(@4VRM+!wo^^(G@iPwT z=g*ATeb1Ks47l|_Z<6WE>yy?y1zH@s#`4tYPaW%13(x3D+x%|@7Tqn4+#$_NqZB0l ZM*Z4^ne*o6y0?OC@O1TaS?83{1OTuB?_dA` literal 0 HcmV?d00001 diff --git a/port/textures/buttons/CLeftOutline.png b/port/textures/buttons/CLeftOutline.png new file mode 100644 index 0000000000000000000000000000000000000000..c0d48f659fa4bbcfc0d10a2c3939874112b379d4 GIT binary patch literal 1544 zcmeAS@N?(olHy`uVBq!ia0y~yV033-U|7$=#=yXkrzvB=z`)p=>FgZf>FlgfP?VpR znUl)EpfRy_qOHf_0FhSzVAqLV9@ZTiDJ%TMT)Hj1M7t9fZV`f|(bwG4@$t~(+BxOtAMX9lurM%j)lE~*6|+*8 zJTf$#9W7cN_wcX=o1jZb(z1CT`e_d$7wVN=Unyb-TLliyu^uY?aXzTr5t6vJrp!abm2nUz~r6@nH0V{IheT zU&}1#cW6u7TEi?O!}QqoRNVK9^8Dv_{=LmS`<{I_SJ9C@$`dLX7#P@+yxm&5eP9fwRCPvY3H^TNs2H8D`CqU|?WiFY)wsWq-jaC$4QKee8J{0|SF(iEBiO zbAE1aYF-J0b5UwyNotBhd1gt5g1e`0KzJjcI0FOoeNPw1kcwMx?*{rkc4J_BP%rfE z;uogd&sbU}i|}pmn8RSf7-YflMMc_gLag3~cedAN+D-5ZZS~byR(j~Wr_F(8W#13$ z@5@!IF*48$BzPPZIB;5Sy5(J!#i+(a>S*#pyea`W8bvNnfvw^Oj=_%_j$_n zCWuw|xBjSpMnub*Eo0Y%=uBn*F-uPW>Fe``_634hjAYX1w3y za_Jf0qMReG$~|5y&u`xr)VBZ3Tor!V`uRWAT9kX%DxR~tb?5Mf`B%$W@In9=iKJpyjtU$gs(E2Kbms5-{5zDf8kl5iOWNd>P02Cal!5Un>>%N zDY*zzdv=-fjC+qi&blSIV{61*cZ)+m{(Y3Ux^*Y%#P7b>v(LmuEoFbmv3TR%Z4Y*S zvv^%u{p{+mdkeRHSXlY3$Tqguw{@z_US{!ZDQF42HD%6M)+f<=>#v*7{JK`} z-tU_n?mz6$+NXzJ-n-^T%B=VOr?21FkGY?Aar>?c&#-M?0(}PWv|r2%EuP1f@@}7A z@%HTQnwV#&y?4SnOP^ZCfhu5pY_vq*6~!o?34am z-@Ns3#vn^`Vo!ORnzxq6f2!^d(H)Ei7K&)>&pc{kCgndEO+`o$FQibGts|kXO^#{_uNvz`0FxkKR)` mt5TV9d(9gS4jN@0_BZ|}87zMS%|5IE+2HBw=d#Wzp$PzMeTi%U literal 0 HcmV?d00001 diff --git a/port/textures/buttons/CRight.png b/port/textures/buttons/CRight.png new file mode 100644 index 0000000000000000000000000000000000000000..c2d1afabfa0994a1c73817d8172f7bfba44b0433 GIT binary patch literal 1336 zcmeAS@N?(olHy`uVBq!ia0y~yV033-U|7$=#=yXkrzvB=z`)p=>FgZf>FlgfP?VpR znUl)EpfRy_qOHf_0FhSzVAqLV9@ZTiDJ%TMT)Hj1M7t9fZV`f|(bwG4@$t~(+BxOtAMX9lurM%j)lE~*6|+*8 zJTf$#9W7cN_wcX=o1jZb(z1CT`e_d$7wVN=Unyb-TLliyu^uY?aXzTr5t6vJrp!abm2nUz~r6@nH0V{IheT zU&}1#cW6u7TEi?O!}QqoRNVK9^8Dv_{=LmS`<{I_SJ9C@$`dLX7#P@+yxm&5eP9fwRCPvY3H^TNs2H8D`CqU|?WiFY)wsWq-jaC$1xw|1vO=fq_A?#5JPC zIX^cyHLrxhxhOTUBsE2$JhLQ2!QIn0AiR-JoPmLv-_yl0q~g}wyJvl+10~unmcQ}P z+3dmW$E={mpv5V|AS5ag-oV28AVesbv59R)s#&JO$scKqCzTHUQAztyac<%s2c35Y zPW}5{Z>$k!pdFaDvWUB3|J^yM_4D5FObYaL+gYvZ8LL`4$s|T_3@-P`wWyeFFTd;+rYSx<(hiea!KCyofVlP@;xb&n@eY^92Kmx)8+Hpr(>zb zKHGg}+24CSf2tkqW9O&ud1IjF>8ZEwys?=j(_EfpFTc2*dg{^bGdq+o++O=d>9baK zY;AyY--YD{^U_Qto^3m8`rOy9?4x4ucbSFms}g^|XWe&nmYRFQ(&fkW{QK414NUj# zdT1c7=6>MH4z|mSPYPBge9)U874_=2^5w_ta!oIU%ZvH-o$Rw<(q5{y)-N~JF~{u@ z$C@dZ@@DK?f55au`GZbwTV^urI!Bqt^p(*uuU~V;G+QufKmTj>NAMAcZNOIRt+{K! z3b$15PORFXz4XcuTMA zMaV2ve-_I3YsWSVrv6)YXVxtF8_c*m<=WB3LPrFvmYB@kviE52tarY>k2q>)%k>8@ zKlMejQOEn1-}};!eOseiI!X5*Vrkyp1}b(oa$S>tl$)Ml;!@|!35TvzQ;bAQmk`tut;?_=NN zy?(#(+@x|j=;`NOE`c(Q>duw&`|mH5QC+!u$vpM13OjRVo!RxvM2be~?B7ZKg_q{e T?~3LDm5~gdu6{1-oD!MFgZf>FlgfP?VpR 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 literal 0 HcmV?d00001 diff --git a/port/textures/buttons/DPadDown.png b/port/textures/buttons/DPadDown.png new file mode 100644 index 0000000000000000000000000000000000000000..cec0af1e5575f8e0e6eebacd062afc96b8440f5d GIT binary patch literal 946 zcmeAS@N?(olHy`uVBq!ia0y~yV033-U|7$=#=yXkrzvB=z`)p=>FgZf>FlgfP?VpR znUl)EpfRy_qOHf_0FhSzVAqLV9@ZTiDJ%TMT)Hj1M7t9fZV`f|(bwG4@$t~(+BxOtAMX9lurM%j)lE~*6|+*8 zJTf$#9W7cN_wcX=o1jZb(z1CT`e_d$7wVN=Unyb-TLliyu^uY?aXzTr5t6vJrp!abm2nUz~r6@nH0V{IheT zU&}1#cW6u7TEi?O!}QqoRNVK9^8Dv_{=LmS`<{I_SJ9C@$`dLX7#P@+yxm&5eP9fwRCPvY3H^TNs2H8D`CqU|?WiFY)wsWq-jaC$3>|vqNwJ0|SF(iEBiO zbAE1aYF-J0b5UwyNotBhd1gt5g1e`0KzJjcI0FM?r>Bc!NX4zUH#Q138A!A}w11^= z&he-g-$7Yj)0>WW4+yTA(d@e8M(~M+vX%3!u9n}aRup6yBA`IwB+K#ZrLi}Q>tx*T zvtG5H$>1Qvctr3)xVZ3@^uNn?eD@W)0}@~;WchJuc5>?;!=~BF4HirtxCn(lhDQ-w zZ?Y)#{SiOjzSBWwb%2*-*__%a9;I_?7W*=1JzJKkwDd}z%-*MS;>DUfQkts1)YivN sdnzaR_SN)iy`?;s&$my-5_}1l*>9VvzhPZ^RSYES>FVdQ&MBb@0LaWUO8@`> literal 0 HcmV?d00001 diff --git a/port/textures/buttons/DPadDownOutline.png b/port/textures/buttons/DPadDownOutline.png new file mode 100644 index 0000000000000000000000000000000000000000..e8dca39b69449bf33a532f091d6f9649b21d72fa GIT binary patch literal 984 zcmeAS@N?(olHy`uVBq!ia0y~yV033-U|7$=#=yXkrzvB=z`)p=>FgZf>FlgfP?VpR znUl)EpfRy_qOHf_0FhSzVAqLV9@ZTiDJ%TMT)Hj1M7t9fZV`f|(bwG4@$t~(+BxOtAMX9lurM%j)lE~*6|+*8 zJTf$#9W7cN_wcX=o1jZb(z1CT`e_d$7wVN=Unyb-TLliyu^uY?aXzTr5t6vJrp!abm2nUz~r6@nH0V{IheT zU&}1#cW6u7TEi?O!}QqoRNVK9^8Dv_{=LmS`<{I_SJ9C@$`dLX7#P@+yxm&5eP9fwRCPvY3H^TNs2H8D`CqU|?WiFY)wsWq-jaC$6En_wmjI1_lPn64!_l z=ltB<)VvY~=c3falGGH1^30M91$R&1fbd2>aRvs)^`0({Ar-gY-aeRl*g%Bs!SZd* zDSY#dymmFS?t7N=_CZ>6--XG~KPb7`iaL1-$Glj0?fYDv3onoV@!+>+Vi+QzOrCFo$u^ygYP$E{-P;5$~6S=8Mbo^mu4vFtc}W}{FgZf>FlgfP?VpR znUl)EpfRy_qOHf_0FhSzVAqLV9@ZTiDJ%TMT)Hj1M7t9fZV`f|(bwG4@$t~(+BxOtAMX9lurM%j)lE~*6|+*8 zJTf$#9W7cN_wcX=o1jZb(z1CT`e_d$7wVN=Unyb-TLliyu^uY?aXzTr5t6vJrp!abm2nUz~r6@nH0V{IheT zU&}1#cW6u7TEi?O!}QqoRNVK9^8Dv_{=LmS`<{I_SJ9C@$`dLX7#P@+yxm&5eP9fwRCPvY3H^TNs2H8D`CqU|?WiFY)wsWq-jaC$1^+Rjossfq_A?#5JPC zIX^cyHLrxhxhOTUBsE2$JhLQ2!QIn0AiR-JoPmL{!qdeuq~g}w8wUlO33_}4N=u;8Avh?*Xv!!1;I?7gC zwi}xn@JYsrTC`_Os<(md};q3@op` zQeq0|V>mT&{T^3_{mIWG!Y$W*+PW>7W%A>^DM3}54MizwGRId=`80(^!+jd-O>^Jk g-VRSG;)3Qwzn$hbIn^TVIU*npp00i_>zopr0CSKe;s5{u literal 0 HcmV?d00001 diff --git a/port/textures/buttons/DPadLeftOutline.png b/port/textures/buttons/DPadLeftOutline.png new file mode 100644 index 0000000000000000000000000000000000000000..ba3dbf4e8a15d38b6f7407087d2bd118069aa753 GIT binary patch literal 953 zcmeAS@N?(olHy`uVBq!ia0y~yV033-U|7$=#=yXkrzvB=z`)p=>FgZf>FlgfP?VpR znUl)EpfRy_qOHf_0FhSzVAqLV9@ZTiDJ%TMT)Hj1M7t9fZV`f|(bwG4@$t~(+BxOtAMX9lurM%j)lE~*6|+*8 zJTf$#9W7cN_wcX=o1jZb(z1CT`e_d$7wVN=Unyb-TLliyu^uY?aXzTr5t6vJrp!abm2nUz~r6@nH0V{IheT zU&}1#cW6u7TEi?O!}QqoRNVK9^8Dv_{=LmS`<{I_SJ9C@$`dLX7#P@+yxm&5eP9fwRCPvY3H^TNs2H8D`CqU|?WiFY)wsWq-jaC$7oGHveZZ0|SF(iEBiO zbAE1aYF-J0b5UwyNotBhd1gt5g1e`0KzJjcI0FOY1Wy;okcwMxZ*9yyY#_jTA^C~p zB;P5Xj}(tsmFd2l5V$CC)5)EpFOKyx7}u>=4&nGAVAprZm0^g01NK2K2Gdd_p9P&N zcRAy}H#s)^KCAwFm8QZgQRf9eDsx)go6T`-ri7@nhP&0$Df*fX*{0v0{Z%x!kIuf` zeEs8{`M>MC{uUp2VlnCa--j2ISf(g96xBG^-wpktBV^H6yUp=RRQ=-vcaPat_azj` zxTVD{WjHm_FFW`2nQj#a%cA%R*E46?DJ4w?FgZf>FlgfP?VpR znUl)EpfRy_qOHf_0FhSzVAqLV9@ZTiDJ%TMT)Hj1M7t9fZV`f|(bwG4@$t~(+BxOtAMX9lurM%j)lE~*6|+*8 zJTf$#9W7cN_wcX=o1jZb(z1CT`e_d$7wVN=Unyb-TLliyu^uY?aXzTr5t6vJrp!abm2nUz~r6@nH0V{IheT zU&}1#cW6u7TEi?O!}QqoRNVK9^8Dv_{=LmS`<{I_SJ9C@$`dLX7#P@+yxm&5eP9fwRCPvY3H^TNs2H8D`CqU|?WiFY)wsWq-jaC$7oF;Zm5$z`!6`;u=xn zoS&PUnpeW$T$GwvlA5AWo>`Ki;O^-g5Z=fq&cMJ}=;`7ZQgQ3;?Tvzm3W>x1yqWcX z$K*Flx=#vDQ9dQ;V9E4m-rUE0xj){0REzcL+c0Tnx#j|y#FtN|^Q^o2!L0p#GjXBv a;UTM~lFI+(nV&X;GFgZf>FlgfP?VpR znUl)EpfRy_qOHf_0FhSzVAqLV9@ZTiDJ%TMT)Hj1M7t9fZV`f|(bwG4@$t~(+BxOtAMX9lurM%j)lE~*6|+*8 zJTf$#9W7cN_wcX=o1jZb(z1CT`e_d$7wVN=Unyb-TLliyu^uY?aXzTr5t6vJrp!abm2nUz~r6@nH0V{IheT zU&}1#cW6u7TEi?O!}QqoRNVK9^8Dv_{=LmS`<{I_SJ9C@$`dLX7#P@+yxm&5eP9fwRCPvY3H^TNs2H8D`CqU|?WiFY)wsWq-jaC$3@c{-%YQfq_A?#5JPC zIX^cyHLrxhxhOTUBsE2$JhLQ2!QIn0AiR-JoPmLHqNj^vNX4zUw>RbLicDcQT#wi-{W7`Ou7mk0$5)O?mkgipna*-buugo*Rpqlk zL=JxIdHiF`^_cgb*WUlX{QYlUFgZf>FlgfP?VpR znUl)EpfRy_qOHf_0FhSzVAqLV9@ZTiDJ%TMT)Hj1M7t9fZV`f|(bwG4@$t~(+BxOtAMX9lurM%j)lE~*6|+*8 zJTf$#9W7cN_wcX=o1jZb(z1CT`e_d$7wVN=Unyb-TLliyu^uY?aXzTr5t6vJrp!abm2nUz~r6@nH0V{IheT zU&}1#cW6u7TEi?O!}QqoRNVK9^8Dv_{=LmS`<{I_SJ9C@$`dLX7#P@+yxm&5eP9fwRCPvY3H^TNs2H8D`CqU|?WiFY)wsWq-jaC$6cw*6#=?jwMT6BTAg} zb8}PkN*J7rQWHy3QxwWGOEMJPJ$(bh8~MZ;7#Qb!x;TbZ+tewk@@vbXf( zkLKywYvav4I(`^5vF&S^Ih+05J0G4S8$}N@r&~W}D$H^HzGSmPAHyRKg+7K3FgZf>FlgfP?VpR znUl)EpfRy_qOHf_0FhSzVAqLV9@ZTiDJ%TMT)Hj1M7t9fZV`f|(bwG4@$t~(+BxOtAMX9lurM%j)lE~*6|+*8 zJTf$#9W7cN_wcX=o1jZb(z1CT`e_d$7wVN=Unyb-TLliyu^uY?aXzTr5t6vJrp!abm2nUz~r6@nH0V{IheT zU&}1#cW6u7TEi?O!}QqoRNVK9^8Dv_{=LmS`<{I_SJ9C@$`dLX7#P@+yxm&5eP9fwRCPvY3H^TNs2H8D`CqU|?WiFY)wsWq-jaC$1@0zrns2nw<$mP(^ZHkw*N3IXO=f34*PJ?Yz2gOczns#d^Jgv_@NCSz-JaQa zcH`3Ck8iEhv)B5Yd35YBZeqLFF!Q{(0%O}7E sUEj$_Kj{<9>0@}rq0mQUkp4fXzEeT(CDVnK>p(g@UHx3vIVCg!08IT(8UO$Q literal 0 HcmV?d00001 diff --git a/port/textures/buttons/InputViewerBackground.png b/port/textures/buttons/InputViewerBackground.png new file mode 100644 index 0000000000000000000000000000000000000000..091d686c0006e444615a666f5e297103ff343b5e GIT binary patch literal 941 zcmeAS@N?(olHy`uVBq!ia0y~yV033-U|7$=#=yXkrzvB=z`)p=>FgZf>FlgfP?VpR znUl)EpfRy_qOHf_0FhSzVAqLV9@ZTiDJ%TMT)Hj1M7t9fZV`f|(bwG4@$t~(+BxOtAMX9lurM%j)lE~*6|+*8 zJTf$#9W7cN_wcX=o1jZb(z1CT`e_d$7wVN=Unyb-TLliyu^uY?aXzTr5t6vJrp!abm2nUz~r6@nH0V{IheT zU&}1#cW6u7TEi?O!}QqoRNVK9^8Dv_{=LmS`<{I_SJ9C@$`dLX7#P@+yxm&5eP9fwRCPvY3H^TNs2H8D`CqU|?WiFY)wsWq-jaC$1sIWPGEHfq_A?#5JPC zIX^cyHLrxhxhOTUBsE2$JhLQ2!QIn0AiR-JoPmL{#nZ(xq~g}wD~5~=3Ool44ydtJ z1ROSI{^7#Fx6Q?=|6BDU$JAnd!5=)F3}*xn$S~p|7>Zd8+zAkPw1W*NpdD-jJD g!-((;15LrD%J-Jcr&L&Ut3lRyy85}Sb4q9e05>`-vH$=8 literal 0 HcmV?d00001 diff --git a/port/textures/buttons/LBtn.png b/port/textures/buttons/LBtn.png new file mode 100644 index 0000000000000000000000000000000000000000..351ea383a7d97df9eb27444e5bf5d6a4dd18b64b GIT binary patch literal 1031 zcmeAS@N?(olHy`uVBq!ia0y~yV033-U|7$=#=yXkrzvB=z`)p=>FgZf>FlgfP?VpR znUl)EpfRy_qOHf_0FhSzVAqLV9@ZTiDJ%TMT)Hj1M7t9fZV`f|(bwG4@$t~(+BxOtAMX9lurM%j)lE~*6|+*8 zJTf$#9W7cN_wcX=o1jZb(z1CT`e_d$7wVN=Unyb-TLliyu^uY?aXzTr5t6vJrp!abm2nUz~r6@nH0V{IheT zU&}1#cW6u7TEi?O!}QqoRNVK9^8Dv_{=LmS`<{I_SJ9C@$`dLX7#P@+yxm&5eP9fwRCPvY3H^TNs2H8D`CqU|?WiFY)wsWq-jaC$6O?pLML5fq_A?#5JPC zIX^cyHLrxhxhOTUBsE2$JhLQ2!QIn0AiR-JoPmMyo~MgrNX4zUw=eo0Hjrq&c>kS3 zNpqir#1f^sETuv|Y6;Az*kl!Xf@ZS|@PBZSUJ}CfTu0#L>Z(+4PZkDj;7{PLYt6s+ z#b1wlADtcRweoXtjijciM>KrzBv$UbLKl%Rm z*tK6>e|>&B=e?IEyK2I*|99{3ui0v4n8kMN4wK+x6||gY(%X nE>q@wp8VPbn=glin88`=_pE{Hx|{Q@LGJZ*^>bP0l+XkK7U5y> literal 0 HcmV?d00001 diff --git a/port/textures/buttons/LBtnOutline.png b/port/textures/buttons/LBtnOutline.png new file mode 100644 index 0000000000000000000000000000000000000000..10cca9c8f5ddb17f4caed59687100a57842d368b GIT binary patch literal 1089 zcmeAS@N?(olHy`uVBq!ia0y~yV033-U|7$=#=yXkrzvB=z`)p=>FgZf>FlgfP?VpR znUl)EpfRy_qOHf_0FhSzVAqLV9@ZTiDJ%TMT)Hj1M7t9fZV`f|(bwG4@$t~(+BxOtAMX9lurM%j)lE~*6|+*8 zJTf$#9W7cN_wcX=o1jZb(z1CT`e_d$7wVN=Unyb-TLliyu^uY?aXzTr5t6vJrp!abm2nUz~r6@nH0V{IheT zU&}1#cW6u7TEi?O!}QqoRNVK9^8Dv_{=LmS`<{I_SJ9C@$`dLX7#P@+yxm&5eP9fwRCPvY3H^TNs2H8D`CqU|?WiFY)wsWq-jaC$6O=!OoP$z`!6`;u=xn zoS&PUnpeW$T$GwvlA5AWo>`Ki;O^-g5Z=fq&cMJV;pyTSQgQ3;?YEtG3`AH1JcB*v za4l=GXsvj(GUcY%q7%GJntiNh2+eWQc{@KfaZk%%CiXeSQUWmxkJqxbGhhYsBLA-* zd~8v+^;-Vgt#-$5?Cw%8=1@E$p>KHX_nglY_So3L|$cE1k zE&7t6Z@PAC=af&uLK_}$t=gNk{o1>% xwR7*Y&tHF0v1LxsuZQ0qu?Egi5PVQ8$FOoz^>L1~oSq=}db;|#taD0e0svfcg;oFn literal 0 HcmV?d00001 diff --git a/port/textures/buttons/Mod1.png b/port/textures/buttons/Mod1.png new file mode 100644 index 0000000000000000000000000000000000000000..69496db6a7a9cc4a7fc8506653d4c1dbb55c7aa4 GIT binary patch literal 1166 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 Lu6{1-oD!M<`KZpm literal 0 HcmV?d00001 diff --git a/port/textures/buttons/RightStickOutline.png b/port/textures/buttons/RightStickOutline.png new file mode 100644 index 0000000000000000000000000000000000000000..8fbd54fcc36d766bfb97b99365685f88426c4dbf GIT binary patch literal 2407 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 literal 0 HcmV?d00001 diff --git a/port/textures/buttons/ZBtn.png b/port/textures/buttons/ZBtn.png new file mode 100644 index 0000000000000000000000000000000000000000..4fee52d5762f0763b5fb1a5059937b35c1796370 GIT binary patch literal 1005 zcmeAS@N?(olHy`uVBq!ia0y~yV033-U|7$=#=yXkrzvB=z`)p=>FgZf>FlgfP?VpR znUl)EpfRy_qOHf_0FhSzVAqLV9@ZTiDJ%TMT)Hj1M7t9fZV`f|(bwG4@$t~(+BxOtAMX9lurM%j)lE~*6|+*8 zJTf$#9W7cN_wcX=o1jZb(z1CT`e_d$7wVN=Unyb-TLliyu^uY?aXzTr5t6vJrp!abm2nUz~r6@nH0V{IheT zU&}1#cW6u7TEi?O!}QqoRNVK9^8Dv_{=LmS`<{I_SJ9C@$`dLX7#P@+yxm&5eP9fwRCPvY3H^TNs2H8D`CqU|?WiFY)wsWq-jaC$1%PGfn;=0|SF(iEBiO zbAE1aYF-J0b5UwyNotBhd1gt5g1e`0KzJjcI0FOY5lAS>)lqWRvX@Ga0y{<5`wD(7+x^#KuHXBt2Tvki_4>T@Yn`nVzwW*2K?^aR z=Wj}XvG4!cu|oBr#i8#dm&MFkcfs<_1t Oa)76+pUXO@geCxSSy#va literal 0 HcmV?d00001 diff --git a/port/textures/buttons/ZBtnOutline.png b/port/textures/buttons/ZBtnOutline.png new file mode 100644 index 0000000000000000000000000000000000000000..5832ed339031803e0e64d582697c8052449801dd GIT binary patch literal 1069 zcmeAS@N?(olHy`uVBq!ia0y~yV033-U|7$=#=yXkrzvB=z`)p=>FgZf>FlgfP?VpR znUl)EpfRy_qOHf_0FhSzVAqLV9@ZTiDJ%TMT)Hj1M7t9fZV`f|(bwG4@$t~(+BxOtAMX9lurM%j)lE~*6|+*8 zJTf$#9W7cN_wcX=o1jZb(z1CT`e_d$7wVN=Unyb-TLliyu^uY?aXzTr5t6vJrp!abm2nUz~r6@nH0V{IheT zU&}1#cW6u7TEi?O!}QqoRNVK9^8Dv_{=LmS`<{I_SJ9C@$`dLX7#P@+yxm&5eP9fwRCPvY3H^TNs2H8D`CqU|?WiFY)wsWq-jaC$1~80|SF(iEBiO zbAE1aYF-J0b5UwyNotBhd1gt5g1e`0KzJjcI0FL{i>HfYNX4zUw-4stb`W5_Agk57 zf@!P3E`dw4`7Sjs@H*qs>he+{fYnEF^+{ES6eqSd-VGPt-`#PX|3h+H#55)blE9zs zPAspD`ySgF|L%CP^y8^HGmBm3^dEohD5G(lf6=vFxoh`o9Q`hudcD;DvdP&+59ax? zyk00FzbSX^-j2rYIW8Oc4jWwQ_@ULwer;LD+q~_+OLxZ9c{MNfSBf~vb)j^=@#k+x z=PNx*$o;eW+OD%_YSXUGe*8S>l#1+{tJ+g%{!P*f>fyd@a#pY~!modlqh*~=+^_FX zmDHy^KBa!V^5dVF?6<0)tJdbNeJehB@zhznYl>s~^OsKfw@fBG?OLIX>gVHMt8@i7 z+Wc0Od{=nB(6ue`$2+a_&X>RaEU~inWXk?}D%Whb{>;j0ElEe7oRF=PubF(Gospyv dqQByQJMS;rS{?hY^s_n0a!*%3mvv4FO#o{Ed`JKQ literal 0 HcmV?d00001 diff --git a/port/textures/icons/g2ShipIcon.png b/port/textures/icons/g2ShipIcon.png new file mode 100644 index 0000000000000000000000000000000000000000..49785b681a18951b5611a04a8f594bea3bcab154 GIT binary patch literal 13286 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 & L_JPAD) && (gPlayer1Controller->buttonDown & (L_TRIG | R_TRIG))) { - CVarSetInteger("gDeveloper.DebugInfoPage", sDebugPage + 1); + if (gDebugInfoFlags & DEBUG_INFO_FLAG_DPRINT || CVarGetInteger("gDeveloperTools.DrawDebugInfo", 0)) { + if ((gPlayer1Controller->buttonPressed & L_JPAD) + && (gPlayer1Controller->buttonDown & (L_TRIG | R_TRIG))) { + CVarSetInteger("gDeveloperTools.DebugInfoPage", sDebugPage + 1); } - if ((gPlayer1Controller->buttonPressed & R_JPAD) && (gPlayer1Controller->buttonDown & (L_TRIG | R_TRIG))) { - CVarSetInteger("gDeveloper.DebugInfoPage", sDebugPage - 1); + if ((gPlayer1Controller->buttonPressed & R_JPAD) + && (gPlayer1Controller->buttonDown & (L_TRIG | R_TRIG))) { + CVarSetInteger("gDeveloperTools.DebugInfoPage", sDebugPage - 1); } if (sDebugPage >= (DEBUG_PAGE_MAX + 1)) { - CVarSetInteger("gDeveloper.DebugInfoPage", DEBUG_PAGE_MIN); + CVarSetInteger("gDeveloperTools.DebugInfoPage", DEBUG_PAGE_MIN); } if (sDebugPage < DEBUG_PAGE_MIN) { - CVarSetInteger("gDeveloper.DebugInfoPage", DEBUG_PAGE_MAX); + CVarSetInteger("gDeveloperTools.DebugInfoPage", DEBUG_PAGE_MAX); } } } diff --git a/src/game/interaction.c b/src/game/interaction.c index ec01d88b..a28c7be6 100644 --- a/src/game/interaction.c +++ b/src/game/interaction.c @@ -765,7 +765,7 @@ u32 interact_water_ring(struct MarioState* m, UNUSED u32 interactType, struct Ob u32 interact_star_or_key(struct MarioState* m, UNUSED u32 interactType, struct Object* o) { u32 starIndex; u32 starGrabAction = ACT_STAR_DANCE_EXIT; - u32 noExit = (o->oInteractionSubtype & INT_SUBTYPE_NO_EXIT) != 0 || CVarGetInteger("gStarNoExit", 0); + u32 noExit = (o->oInteractionSubtype & INT_SUBTYPE_NO_EXIT) != 0 || CVarGetInteger("gEnhancements.StarNoExit", 0); u32 grandStar = (o->oInteractionSubtype & INT_SUBTYPE_GRAND_STAR) != 0; if (m->health >= 0x100) { diff --git a/src/game/level_update.c b/src/game/level_update.c index 008c8f7e..9439d47f 100644 --- a/src/game/level_update.c +++ b/src/game/level_update.c @@ -442,8 +442,8 @@ void init_mario_after_warp(void) { play_cap_music(SEQUENCE_ARGS(4, SEQ_EVENT_POWERUP)); } - if (CVarGetInteger("gFixKoopaRaceMusic", 0) == 1 && gCurrLevelNum == LEVEL_BOB && - get_current_background_music() != SEQUENCE_ARGS(4, SEQ_LEVEL_SLIDE) && sTimerRunning) { + if (CVarGetInteger("gEnhancements.FixKoopaRaceMusic", 0) == 1 && gCurrLevelNum == LEVEL_BOB + && get_current_background_music() != SEQUENCE_ARGS(4, SEQ_LEVEL_SLIDE) && sTimerRunning) { play_music(SEQ_PLAYER_LEVEL, SEQUENCE_ARGS(4, SEQ_LEVEL_SLIDE), 0); } @@ -568,7 +568,7 @@ s16 music_changed_through_warp(s16 arg) { struct ObjectWarpNode* warpNode = area_get_warp_node(arg); s16 levelNum = warpNode->node.destLevel & 0x7F; - if (CVarGetInteger("gFixKoopaRaceMusic", 0) == 1) { + if(CVarGetInteger("gEnhancements.FixKoopaRaceMusic", 0) == 1) { s16 destArea = warpNode->node.destArea; s16 val4 = TRUE; s16 sp2C; @@ -1184,7 +1184,7 @@ s32 init_level(void) { if (gMarioState->action != ACT_UNINITIALIZED) { if (save_file_exists(gCurrSaveFileNum - 1)) { set_mario_action(gMarioState, ACT_IDLE, 0); - } else if (CVarGetInteger("gDisablePeachCutscene", 0) == 0) { + } else if (CVarGetInteger("gEnhancements.DisablePeachCutscene", 0) == 0) { set_mario_action(gMarioState, ACT_INTRO_CUTSCENE, 0); val4 = TRUE; } @@ -1250,7 +1250,7 @@ s32 lvl_init_from_save_file(UNUSED s16 arg0, s32 levelNum) { #endif sWarpDest.type = WARP_TYPE_NOT_WARPING; sDelayedWarpOp = WARP_OP_NONE; - gNeverEnteredCastle = !save_file_exists(gCurrSaveFileNum - 1) || CVarGetInteger("gDisablePeachCutscene", 0) == 0; + gNeverEnteredCastle = !save_file_exists(gCurrSaveFileNum - 1) || CVarGetInteger("gEnhancements.DisablePeachCutscene", 0) == 0; gCurrLevelNum = levelNum; gCurrCourseNum = COURSE_NONE; diff --git a/src/game/main.h b/src/game/main.h index c533b6ab..fcb4c25d 100644 --- a/src/game/main.h +++ b/src/game/main.h @@ -67,7 +67,7 @@ extern s8 D_8032C650; extern s8 gShowProfiler; extern s8 gShowDebugText; -#define gDebugLevelSelect (CVarGetInteger("gEnableDebugMode", 0) == 1) +#define gDebugLevelSelect (CVarGetInteger("gDeveloperTools.DebugMode", 0) == 1) void exec_display_list(struct SPTask *spTask); diff --git a/src/game/rendering_graph_node.c b/src/game/rendering_graph_node.c index dee33767..b27b39f1 100644 --- a/src/game/rendering_graph_node.c +++ b/src/game/rendering_graph_node.c @@ -287,7 +287,7 @@ static void geo_process_level_of_detail(struct GraphNodeLevelOfDetail* node) { Mtx* mtx = gMatStackFixed[gMatStackIndex]; s16 distanceFromCam = -GET_HIGH_S16_OF_32(mtx->m[1][3]); // z-component of the translation column #endif - if (CVarGetInteger("gDisableLOD", 0) == 1) { + if(CVarGetInteger("gEnhancements.DisableLOD", 0) == 1) { distanceFromCam = 0; } diff --git a/src/menu/star_select.c b/src/menu/star_select.c index 323699cb..1c435442 100644 --- a/src/menu/star_select.c +++ b/src/menu/star_select.c @@ -119,14 +119,14 @@ void bhv_act_selector_init(void) { s32 selectorModelIDs[10]; u8 stars = save_file_get_star_flags(gCurrSaveFileNum - 1, COURSE_NUM_TO_INDEX(gCurrCourseNum)); - if (CVarGetInteger("gSelectAllStars", 0)) { + if (CVarGetInteger("gEnhancements.SelectAllStars", 0)) { sStarSelectState = false; } else { sStarSelectState = true; } sVisibleStars = 0; - while (i != sObtainedStars || (CVarGetInteger("gSelectAllStars", 0) && sVisibleStars != LEVEL_STARS_MAX)) { + while (i != sObtainedStars || (CVarGetInteger("gEnhancements.SelectAllStars", 0) && sVisibleStars != LEVEL_STARS_MAX)) { if (stars & (1 << sVisibleStars)) { // Star has been collected selectorModelIDs[sVisibleStars] = MODEL_STAR; i++; @@ -184,7 +184,7 @@ void bhv_act_selector_loop(void) { u8 starIndexCounter; u8 stars = save_file_get_star_flags(gCurrSaveFileNum - 1, COURSE_NUM_TO_INDEX(gCurrCourseNum)); - if (!sStarSelectState && !CVarGetInteger("gSelectAllStars", 0)) { + if (!sStarSelectState && !CVarGetInteger("gEnhancements.SelectAllStars", 0)) { // If the option is toggled after stars have been drawn, then we want to set the star index to a valid index starIndexCounter = 0; for (i = 0; i <= sSelectableStarIndex; i++) { @@ -195,12 +195,12 @@ void bhv_act_selector_loop(void) { } sSelectableStarIndex = starIndexCounter - 1; sStarSelectState = true; - } else if (sStarSelectState && CVarGetInteger("gSelectAllStars", 0)) { + } else if (sStarSelectState && CVarGetInteger("gEnhancements.SelectAllStars", 0)) { sSelectableStarIndex = sSelectedActIndex; sStarSelectState = false; } - if (sObtainedStars != LEVEL_STARS_MAX && !CVarGetInteger("gSelectAllStars", 0)) { + if (sObtainedStars != LEVEL_STARS_MAX && !CVarGetInteger("gEnhancements.SelectAllStars", 0)) { // Sometimes, stars are not selectable even if they appear on the screen. // This code filters selectable and non-selectable stars. sSelectedActIndex = 0; @@ -429,7 +429,7 @@ s32 lvl_update_obj_and_load_act_button_actions(UNUSED s32 arg, UNUSED s32 unused queue_rumble_data(60, 70); func_sh_8024C89C(1); #endif - if (sInitSelectedActNum >= sSelectedActIndex + 1 || CVarGetInteger("gSelectAllStars", 0)) { + if (sInitSelectedActNum >= sSelectedActIndex + 1 || CVarGetInteger("gEnhancements.SelectAllStars", 0)) { sLoadedActNum = sSelectedActIndex + 1; } else { sLoadedActNum = sInitSelectedActNum; diff --git a/src/menu/title_screen.c b/src/menu/title_screen.c index 697914c0..a3194633 100644 --- a/src/menu/title_screen.c +++ b/src/menu/title_screen.c @@ -114,22 +114,18 @@ s16 intro_level_select(void) { --gCurrLevelNum, stageChanged = TRUE; direction = -1; } - if (gPlayer1Controller->buttonPressed & U_JPAD || - (CVarGetInteger("gDeveloper.BetterLevelSelect", 0) && gPlayer1Controller->buttonPressed & U_CBUTTONS)) { + if (gPlayer1Controller->buttonPressed & U_JPAD || (CVarGetInteger("gDeveloperTools.BetterLevelSelect", 0) && gPlayer1Controller->buttonPressed & U_CBUTTONS)) { --gCurrLevelNum, stageChanged = TRUE; direction = -1; } - if (gPlayer1Controller->buttonPressed & D_JPAD || - (CVarGetInteger("gDeveloper.BetterLevelSelect", 0) && gPlayer1Controller->buttonPressed & D_CBUTTONS)) { + if (gPlayer1Controller->buttonPressed & D_JPAD || (CVarGetInteger("gDeveloperTools.BetterLevelSelect", 0) && gPlayer1Controller->buttonPressed & D_CBUTTONS)) { ++gCurrLevelNum, stageChanged = TRUE; } - if (gPlayer1Controller->buttonPressed & L_JPAD || - (CVarGetInteger("gDeveloper.BetterLevelSelect", 0) && gPlayer1Controller->buttonPressed & L_CBUTTONS)) { + if (gPlayer1Controller->buttonPressed & L_JPAD || (CVarGetInteger("gDeveloperTools.BetterLevelSelect", 0) && gPlayer1Controller->buttonPressed & L_CBUTTONS)) { gCurrLevelNum -= 10, stageChanged = TRUE; direction = -1; } - if (gPlayer1Controller->buttonPressed & R_JPAD || - (CVarGetInteger("gDeveloper.BetterLevelSelect", 0) && gPlayer1Controller->buttonPressed & R_CBUTTONS)) { + if (gPlayer1Controller->buttonPressed & R_JPAD || (CVarGetInteger("gDeveloperTools.BetterLevelSelect", 0) && gPlayer1Controller->buttonPressed & R_CBUTTONS)) { gCurrLevelNum += 10, stageChanged = TRUE; } @@ -147,7 +143,7 @@ s16 intro_level_select(void) { } // This block will ensure that the level select menu skips over the levels that are not in the game. - if (CVarGetInteger("gDeveloper.BetterLevelSelect", 0) && (stageChanged || gCurrLevelNum == 1)) { + if (CVarGetInteger("gDeveloperTools.BetterLevelSelect", 0) && (stageChanged || gCurrLevelNum == 1)) { while (sBetterLevelSelectStageNames[gCurrLevelNum - 1][0] == '\0') { gCurrLevelNum += direction; if (gCurrLevelNum > LEVEL_MAX) { @@ -165,7 +161,7 @@ s16 intro_level_select(void) { print_text_centered(160, 80, "SELECT STAGE"); print_text_centered(160, 30, "PRESS START BUTTON"); - if (CVarGetInteger("gDeveloper.BetterLevelSelect", 0)) { + if (CVarGetInteger("gDeveloperTools.BetterLevelSelect", 0)) { print_text_fmt_int(240, 80, "%2d", gCurrLevelNum); print_text_centered(160, 60, sBetterLevelSelectStageNames[gCurrLevelNum - 1]); } else { @@ -180,7 +176,7 @@ s16 intro_level_select(void) { // ... the level select quit combo is being pressed, which uses START. If this // is the case, quit the menu instead. if (gPlayer1Controller->buttonDown == QUIT_LEVEL_SELECT_COMBO) { - // CVarSetInteger("gEnableDebugMode", 0); + // CVarSetInteger("gDeveloperTools.DebugMode", 0); return -1; } play_sound(SOUND_MENU_STAR_SOUND, gGlobalSoundSource); diff --git a/src/port/Engine.cpp b/src/port/Engine.cpp index 04592256..8a7ea6cb 100644 --- a/src/port/Engine.cpp +++ b/src/port/Engine.cpp @@ -1,5 +1,5 @@ #include "Engine.h" -#include "ui/ImguiUI.h" +#include "ui/GhostshipGui.hpp" #include "GameExtractor.h" #include "port/importer/AnimationFactory.h" #include "port/importer/AudioBankFactory.h" @@ -26,6 +26,7 @@ #endif #include +#include #include #include #include @@ -44,8 +45,14 @@ #include #endif +const float imguiScaleOptionToValue[4] = { 0.75f, 1.0f, 1.5f, 2.0f }; +const uint32_t defaultImGuiScale = 1; +int32_t previousImGuiScaleIndex = -1; +float previousImGuiScale = defaultImGuiScale; + namespace fs = std::filesystem; + extern "C" { #include "sm64.h" #include "audio/external.h" @@ -193,6 +200,14 @@ GameEngine::GameEngine() : dictionary(nullptr) { static_cast(Ship::ResourceType::Blob), 0); prevAltAssets = CVarGetInteger("gEnhancements.Mods.AlternateAssets", 0); context->GetResourceManager()->SetAltAssetsEnabled(prevAltAssets); + + fontMono = CreateFontWithSize(16.0f, "fonts/Inconsolata-Regular.ttf"); + fontMonoLarger = CreateFontWithSize(20.0f, "fonts/Inconsolata-Regular.ttf"); + fontMonoLargest = CreateFontWithSize(24.0f, "fonts/Inconsolata-Regular.ttf"); + fontStandard = CreateFontWithSize(16.0f, "fonts/Montserrat-Regular.ttf"); + fontStandardLarger = CreateFontWithSize(20.0f, "fonts/Montserrat-Regular.ttf"); + fontStandardLargest = CreateFontWithSize(24.0f, "fonts/Montserrat-Regular.ttf"); + ImGui::GetIO().FontDefault = fontMono; } bool GameEngine::GenAssetFile(bool exitOnFail) { @@ -260,9 +275,59 @@ int GameEngine::ShowYesNoBox(const char* title, const char* box) { return ret; } -void GameEngine::Create() { +ImFont *GameEngine::CreateFontWithSize(float size, std::string fontPath) { + auto mImGuiIo = &ImGui::GetIO(); + ImFont *font; + if (fontPath == "") { + ImFontConfig fontCfg = ImFontConfig(); + fontCfg.OversampleH = fontCfg.OversampleV = 1; + fontCfg.PixelSnapH = true; + fontCfg.SizePixels = size; + font = mImGuiIo->Fonts->AddFontDefault(&fontCfg); + } else { + auto initData = std::make_shared(); + ImFontConfig config; + config.FontDataOwnedByAtlas = false; + + initData->Format = RESOURCE_FORMAT_BINARY; + initData->Type = static_cast(RESOURCE_TYPE_FONT); + initData->ResourceVersion = 0; + initData->Path = fontPath; + std::shared_ptr fontData = std::static_pointer_cast( + Ship::Context::GetInstance()->GetResourceManager()->LoadResource(fontPath, false, + initData)); + font = mImGuiIo->Fonts->AddFontFromMemoryTTF(fontData->Data, fontData->DataSize, size, &config); + } + // FontAwesome fonts need to have their sizes reduced by 2.0f/3.0f in order to align correctly + float iconFontSize = size * 2.0f / 3.0f; + static const ImWchar sIconsRanges[] = { ICON_MIN_FA, ICON_MAX_16_FA, 0 }; + ImFontConfig iconsConfig; + iconsConfig.MergeMode = true; + iconsConfig.PixelSnapH = true; + iconsConfig.GlyphMinAdvanceX = iconFontSize; + mImGuiIo->Fonts->AddFontFromMemoryCompressedBase85TTF(fontawesome_compressed_data_base85, + iconFontSize, &iconsConfig, sIconsRanges); + + return font; +} + +void GameEngine::ScaleImGui() { + int32_t imGuiScaleIndex = CVarGetInteger("gSettings.ImGuiScale", defaultImGuiScale); + if (imGuiScaleIndex == previousImGuiScaleIndex) { + return; + } + + float scale = imguiScaleOptionToValue[imGuiScaleIndex]; + float newScale = scale / previousImGuiScale; + ImGui::GetStyle().ScaleAllSizes(newScale); + ImGui::GetIO().FontGlobalScale = scale; + previousImGuiScale = scale; + previousImGuiScaleIndex = imGuiScaleIndex; +} + +void GameEngine::Create(){ const auto instance = Instance = new GameEngine(); - GameUI::SetupGuiElements(); + GhostshipGui::SetupGuiElements(); instance->AudioInit(); instance->LoadDictionary(); instance->LoadPlayerAnims(); diff --git a/src/port/Engine.h b/src/port/Engine.h index d48ac29c..5aa603de 100644 --- a/src/port/Engine.h +++ b/src/port/Engine.h @@ -31,6 +31,13 @@ class GameEngine { public: static GameEngine* Instance; + ImFont *fontStandard; + ImFont *fontStandardLarger; + ImFont *fontStandardLargest; + ImFont *fontMono; + ImFont *fontMonoLarger; + ImFont *fontMonoLargest; + std::shared_ptr context; std::vector animationsTable; @@ -55,6 +62,8 @@ class GameEngine { static void ProcessGfxCommands(Gfx* commands); static uint8_t GetBankIdByName(const std::string& name); static int ShowYesNoBox(const char* title, const char* box); + static ImFont *CreateFontWithSize(float size, std::string fontPath); + static void ScaleImGui(); static void ShowMessage(const char* title, const char* message, SDL_MessageBoxFlags type = SDL_MESSAGEBOX_ERROR); void LoadDictionary(); void LoadPlayerAnims(); @@ -85,6 +94,7 @@ struct DialogEntry* GameEngine_LoadDialog(uint32_t dialogId); uint8_t* GameEngine_LoadTranslation(const char* key); int GameEngine_OTRSigCheck(const char* imgData); struct Animation* GameEngine_LoadAnimation(uint32_t animId); + #ifdef __cplusplus } #endif \ No newline at end of file diff --git a/src/port/ShipInit.hpp b/src/port/ShipInit.hpp new file mode 100644 index 00000000..df874238 --- /dev/null +++ b/src/port/ShipInit.hpp @@ -0,0 +1,44 @@ +#ifndef SHIP_INIT_HPP +#define SHIP_INIT_HPP + +#ifdef __cplusplus + +#include +#include +#include +#include +#include + +struct ShipInit { + static std::unordered_map>>& GetAll() { + static std::unordered_map>> shipInitFuncs; + return shipInitFuncs; + } + + static void InitAll() { + ShipInit::Init("*"); + } + + static void Init(const std::string& path) { + auto& shipInitFuncs = ShipInit::GetAll(); + for (const auto& initFunc : shipInitFuncs[path]) { + initFunc(); + } + } +}; + +struct RegisterShipInitFunc { + RegisterShipInitFunc(std::function initFunc, const std::set& updatePaths = {}) { + auto& shipInitFuncs = ShipInit::GetAll(); + + shipInitFuncs["*"].push_back(initFunc); + + for (const auto& path : updatePaths) { + shipInitFuncs[path].push_back(initFunc); + } + } +}; + +#endif // __cplusplus + +#endif // SHIP_INIT_HPP diff --git a/src/port/build.c.in b/src/port/build.c.in new file mode 100644 index 00000000..afd8f0b6 --- /dev/null +++ b/src/port/build.c.in @@ -0,0 +1,15 @@ +#include + +// Lighthouse Version information +const char gBuildVersion[] = "@PROJECT_BUILD_NAME@ (@CMAKE_PROJECT_VERSION_MAJOR@.@CMAKE_PROJECT_VERSION_MINOR@.@CMAKE_PROJECT_VERSION_PATCH@)"; +const u16 gBuildVersionMajor = @CMAKE_PROJECT_VERSION_MAJOR@; +const u16 gBuildVersionMinor = @CMAKE_PROJECT_VERSION_MINOR@; +const u16 gBuildVersionPatch = @CMAKE_PROJECT_VERSION_PATCH@; + +const char gGitBranch[] = "@CMAKE_PROJECT_GIT_BRANCH@"; +const char gGitCommitHash[] = "@CMAKE_PROJECT_GIT_COMMIT_HASH@"; +const char gGitCommitTag[] = "@CMAKE_PROJECT_GIT_COMMIT_TAG@"; + +const char gBuildTeam[] = "@PROJECT_TEAM@"; +const char gBuildDate[] = __DATE__ " " __TIME__; +const char gBuildMakeOption[] = ""; diff --git a/src/port/build.h b/src/port/build.h new file mode 100644 index 00000000..a2ea2e86 --- /dev/null +++ b/src/port/build.h @@ -0,0 +1,24 @@ +#ifndef BUILD_H +#define BUILD_H + +#include +#ifdef __cplusplus +extern "C" { +#endif +// Ghostship Version information +extern char gBuildVersion[]; +extern u16 gBuildVersionMajor; +extern u16 gBuildVersionMinor; +extern u16 gBuildVersionPatch; + +extern char gGitBranch[]; +extern char gGitCommitHash[]; +extern char gGitCommitTag[]; +extern char gBuildTeam[]; +extern char gBuildDate[]; +extern char gBuildMakeOption[]; +#ifdef __cplusplus +} +#endif + +#endif diff --git a/src/port/mods/PortEnhancements.c b/src/port/mods/PortEnhancements.c index c9a2e6a6..cd7598da 100644 --- a/src/port/mods/PortEnhancements.c +++ b/src/port/mods/PortEnhancements.c @@ -18,8 +18,8 @@ static const Mtx matrix_patch_fullscreen = { { { 2.0f / SCREEN_WIDTH, 0.0f, 0.0f { -1.0f, -1.0f, -1.0f, 1.0f } } }; void OnLivesChange(IEvent* event) { - PlayerLivesChange* ev = (PlayerLivesChange*)event; - if (CVarGetInteger("gInfiniteLives", 0) == 0 || ev->lives > 0) { + PlayerLivesChange* ev = (PlayerLivesChange*) event; + if (CVarGetInteger("gCheats.InfiniteLives", 0) == 0 || ev->lives > 0) { return; } @@ -27,8 +27,8 @@ void OnLivesChange(IEvent* event) { } void OnHealthChange(IEvent* event) { - PlayerHealthChange* ev = (PlayerHealthChange*)event; - if (CVarGetInteger("gInfiniteHealth", 0) == 0 || ev->health > 0) { + PlayerHealthChange* ev = (PlayerHealthChange*) event; + if (CVarGetInteger("gCheats.InfiniteHealth", 0) == 0 || ev->health > 0) { return; } @@ -36,7 +36,7 @@ void OnHealthChange(IEvent* event) { } void OnRenderPauseCourseOptions(IEvent* event) { - if (CVarGetInteger("gPauseExitWhenever", 0) == 0) { + if (CVarGetInteger("gCheats.PauseExitWhenever", 0) == 0) { return; } diff --git a/src/port/ui/GhostshipGui.cpp b/src/port/ui/GhostshipGui.cpp new file mode 100644 index 00000000..425537f5 --- /dev/null +++ b/src/port/ui/GhostshipGui.cpp @@ -0,0 +1,85 @@ +#include "GhostshipGui.hpp" + +#include +#include +#include +#include "UIWidgets.hpp" + +#ifdef __APPLE__ +#include +#endif + +#ifdef __SWITCH__ +#include +#endif + +#include "Notification.h" +#include "GhostshipMenu.h" +#include "GhostshipInputEditorWindow.h" +#include "SaveEditor.h" + +namespace GhostshipGui { +// MARK: - Delegates +std::shared_ptr mInputEditorWindow; +std::shared_ptr mGhostshipMenu; +std::shared_ptr mSaveEditorWindow; +std::shared_ptr mNotificationWindow; +std::shared_ptr mInputViewer; +std::shared_ptr mInputViewerSettings; +std::shared_ptr mModalWindow; + +UIWidgets::Colors GetMenuThemeColor() { + return mGhostshipMenu->GetMenuThemeColor(); +} + +void SetupGuiElements() { + auto gui = Ship::Context::GetInstance()->GetWindow()->GetGui(); + + auto& style = ImGui::GetStyle(); + style.FramePadding = ImVec2(4.0f, 6.0f); + style.ItemSpacing = ImVec2(8.0f, 6.0f); + style.Colors[ImGuiCol_MenuBarBg] = UIWidgets::ColorValues.at(UIWidgets::Colors::DarkGray); + + mGhostshipMenu = std::make_shared(CVAR_WINDOW("Menu"), "Settings Menu"); + gui->SetMenu(mGhostshipMenu); + + mSaveEditorWindow = std::make_shared(CVAR_WINDOW("SaveEditor"), "Save Editor"); + gui->AddGuiWindow(mSaveEditorWindow); + + mInputEditorWindow = std::make_shared( + CVAR_WINDOW("ControllerConfiguration"), "Configure Controller"); + gui->AddGuiWindow(mInputEditorWindow); + + mNotificationWindow = std::make_shared(CVAR_WINDOW("Notifications"), "Notifications Window"); + gui->AddGuiWindow(mNotificationWindow); + mNotificationWindow->Show(); + + mInputViewer = std::make_shared(CVAR_WINDOW("InputViewer"), "Input Viewer"); + gui->AddGuiWindow(mInputViewer); + mInputViewerSettings = std::make_shared(CVAR_WINDOW("InputViewerSettings"), + "Input Viewer Settings", ImVec2(500, 525)); + gui->AddGuiWindow(mInputViewerSettings); + mModalWindow = std::make_shared(CVAR_WINDOW("ModalWindow"), "Modal Window"); + gui->AddGuiWindow(mModalWindow); + mModalWindow->Show(); +} + +void Destroy() { + auto gui = Ship::Context::GetInstance()->GetWindow()->GetGui(); + + gui->RemoveAllGuiWindows(); + mGhostshipMenu = nullptr; + mModalWindow = nullptr; + mSaveEditorWindow = nullptr; + mInputEditorWindow = nullptr; + mNotificationWindow = nullptr; + mInputViewer = nullptr; + mInputViewerSettings = nullptr; +} + +void RegisterPopup(std::string title, std::string message, std::string button1, std::string button2, + std::function button1callback, std::function button2callback) { + mModalWindow->RegisterPopup(title, message, button1, button2, button1callback, button2callback); +} + +} // namespace GhostshipGui diff --git a/src/port/ui/GhostshipGui.hpp b/src/port/ui/GhostshipGui.hpp new file mode 100644 index 00000000..b880d9b0 --- /dev/null +++ b/src/port/ui/GhostshipGui.hpp @@ -0,0 +1,15 @@ +#pragma once + +#include "UIWidgets.hpp" +#include "InputViewer.h" +#include "GhostshipModals.h" + +namespace GhostshipGui { +void SetupHooks(); +void SetupGuiElements(); +void Draw(); +void Destroy(); +UIWidgets::Colors GetMenuThemeColor(); +} // namespace GhostshipGui + +#define THEME_COLOR GhostshipGui::GetMenuThemeColor() diff --git a/src/port/ui/GhostshipInputEditorWindow.cpp b/src/port/ui/GhostshipInputEditorWindow.cpp new file mode 100644 index 00000000..4c6dae81 --- /dev/null +++ b/src/port/ui/GhostshipInputEditorWindow.cpp @@ -0,0 +1,1497 @@ +#include "GhostshipInputEditorWindow.h" +#include +#include +#include +#include +#include +#ifndef __WIIU__ +#include +#endif + +#define SCALE_IMGUI_SIZE(value) ((value / 13.0f) * ImGui::GetFontSize()) + +GhostshipInputEditorWindow::~GhostshipInputEditorWindow() { +} + +void GhostshipInputEditorWindow::InitElement() { + mGameInputBlockTimer = INT32_MAX; + mMappingInputBlockTimer = INT32_MAX; + mRumbleTimer = INT32_MAX; + mRumbleMappingToTest = nullptr; + mInputEditorPopupOpen = false; + + mButtonsBitmasks = { BTN_A, BTN_B, BTN_START, BTN_L, BTN_R, BTN_Z, BTN_CUP, BTN_CDOWN, BTN_CLEFT, BTN_CRIGHT }; + mDpadBitmasks = { BTN_DUP, BTN_DDOWN, BTN_DLEFT, BTN_DRIGHT }; + + addButtonName(BTN_A, "A"); + addButtonName(BTN_B, "B"); + addButtonName(BTN_CUP, "C Up"); + addButtonName(BTN_CDOWN, "C Down"); + addButtonName(BTN_CLEFT, "C Left"); + addButtonName(BTN_CRIGHT, "C Right"); + addButtonName(BTN_L, "L"); + addButtonName(BTN_Z, "Z"); + addButtonName(BTN_R, "R"); + addButtonName(BTN_START, "Start"); + addButtonName(BTN_DUP, "D-pad up"); + addButtonName(BTN_DDOWN, "D-pad down"); + addButtonName(BTN_DLEFT, "D-pad left"); + addButtonName(BTN_DRIGHT, "D-pad right"); + addButtonName(0, "None"); +} + +#define INPUT_EDITOR_WINDOW_GAME_INPUT_BLOCK_ID 95237929 +void GhostshipInputEditorWindow::UpdateElement() { + if (mRumbleTimer != INT32_MAX) { + mRumbleTimer--; + if (mRumbleMappingToTest != nullptr) { + mRumbleMappingToTest->StartRumble(); + } + if (mRumbleTimer <= 0) { + if (mRumbleMappingToTest != nullptr) { + mRumbleMappingToTest->StopRumble(); + } + mRumbleTimer = INT32_MAX; + mRumbleMappingToTest = nullptr; + } + } + + if (mInputEditorPopupOpen && ImGui::IsPopupOpen("", ImGuiPopupFlags_AnyPopupId)) { + Ship::Context::GetInstance()->GetControlDeck()->BlockGameInput(INPUT_EDITOR_WINDOW_GAME_INPUT_BLOCK_ID); + + // continue to block input for a third of a second after getting the mapping + mGameInputBlockTimer = ImGui::GetIO().Framerate / 3; + + if (mMappingInputBlockTimer != INT32_MAX) { + mMappingInputBlockTimer--; + if (mMappingInputBlockTimer <= 0) { + mMappingInputBlockTimer = INT32_MAX; + } + } + + Ship::Context::GetInstance()->GetWindow()->GetGui()->BlockGamepadNavigation(); + } else { + if (mGameInputBlockTimer != INT32_MAX) { + mGameInputBlockTimer--; + if (mGameInputBlockTimer <= 0) { + Ship::Context::GetInstance()->GetControlDeck()->UnblockGameInput( + INPUT_EDITOR_WINDOW_GAME_INPUT_BLOCK_ID); + mGameInputBlockTimer = INT32_MAX; + } + } + + if (Ship::Context::GetInstance()->GetWindow()->GetGui()->GamepadNavigationEnabled()) { + mMappingInputBlockTimer = ImGui::GetIO().Framerate / 3; + } else { + mMappingInputBlockTimer = INT32_MAX; + } + + Ship::Context::GetInstance()->GetWindow()->GetGui()->UnblockGamepadNavigation(); + } +} + +void GhostshipInputEditorWindow::DrawAnalogPreview(const char* label, ImVec2 stick, float deadzone, bool gyro) { + ImGui::BeginChild(label, ImVec2(gyro ? SCALE_IMGUI_SIZE(78) : SCALE_IMGUI_SIZE(96), SCALE_IMGUI_SIZE(85)), false); + ImGui::SetCursorPos(ImVec2(ImGui::GetCursorPos().x + gyro ? SCALE_IMGUI_SIZE(10) : SCALE_IMGUI_SIZE(18), + ImGui::GetCursorPos().y + gyro ? SCALE_IMGUI_SIZE(10) : 0)); + ImDrawList* drawList = ImGui::GetWindowDrawList(); + + const ImVec2 cursorScreenPosition = ImGui::GetCursorScreenPos(); + + // Draw the border box + float borderSquareLeft = cursorScreenPosition.x + SCALE_IMGUI_SIZE(2.0f); + float borderSquareTop = cursorScreenPosition.y + SCALE_IMGUI_SIZE(2.0f); + float borderSquareSize = SCALE_IMGUI_SIZE(65.0f); + drawList->AddRect(ImVec2(borderSquareLeft, borderSquareTop), + ImVec2(borderSquareLeft + borderSquareSize, borderSquareTop + borderSquareSize), + ImColor(100, 100, 100, 255), 0.0f, 0, 1.5f); + + // Draw the gate background + float cardinalRadius = SCALE_IMGUI_SIZE(22.5f); + float diagonalRadius = SCALE_IMGUI_SIZE(22.5f * (69.0f / 85.0f)); + + ImVec2 joystickCenterpoint = ImVec2(cursorScreenPosition.x + cardinalRadius + SCALE_IMGUI_SIZE(12), + cursorScreenPosition.y + cardinalRadius + SCALE_IMGUI_SIZE(11)); + drawList->AddQuadFilled(joystickCenterpoint, + ImVec2(joystickCenterpoint.x - diagonalRadius, joystickCenterpoint.y + diagonalRadius), + ImVec2(joystickCenterpoint.x, joystickCenterpoint.y + cardinalRadius), + ImVec2(joystickCenterpoint.x + diagonalRadius, joystickCenterpoint.y + diagonalRadius), + ImColor(130, 130, 130, 255)); + drawList->AddQuadFilled(joystickCenterpoint, + ImVec2(joystickCenterpoint.x + diagonalRadius, joystickCenterpoint.y + diagonalRadius), + ImVec2(joystickCenterpoint.x + cardinalRadius, joystickCenterpoint.y), + ImVec2(joystickCenterpoint.x + diagonalRadius, joystickCenterpoint.y - diagonalRadius), + ImColor(130, 130, 130, 255)); + drawList->AddQuadFilled(joystickCenterpoint, + ImVec2(joystickCenterpoint.x + diagonalRadius, joystickCenterpoint.y - diagonalRadius), + ImVec2(joystickCenterpoint.x, joystickCenterpoint.y - cardinalRadius), + ImVec2(joystickCenterpoint.x - diagonalRadius, joystickCenterpoint.y - diagonalRadius), + ImColor(130, 130, 130, 255)); + drawList->AddQuadFilled(joystickCenterpoint, + ImVec2(joystickCenterpoint.x - diagonalRadius, joystickCenterpoint.y - diagonalRadius), + ImVec2(joystickCenterpoint.x - cardinalRadius, joystickCenterpoint.y), + ImVec2(joystickCenterpoint.x - diagonalRadius, joystickCenterpoint.y + diagonalRadius), + ImColor(130, 130, 130, 255)); + + // Draw the joystick position indicator + ImVec2 joystickIndicatorDistanceFromCenter = ImVec2(0, 0); + if ((stick.x * stick.x + stick.y * stick.y) > (deadzone * deadzone)) { + joystickIndicatorDistanceFromCenter = + ImVec2((stick.x * (cardinalRadius / 85.0f)), -(stick.y * (cardinalRadius / 85.0f))); + } + float indicatorRadius = SCALE_IMGUI_SIZE(5.0f); + drawList->AddCircleFilled(ImVec2(joystickCenterpoint.x + joystickIndicatorDistanceFromCenter.x, + joystickCenterpoint.y + joystickIndicatorDistanceFromCenter.y), + indicatorRadius, ImColor(34, 51, 76, 255), 7); + + if (!gyro) { + ImGui::SetCursorPos( + ImVec2(ImGui::GetCursorPos().x - SCALE_IMGUI_SIZE(8), ImGui::GetCursorPos().y + SCALE_IMGUI_SIZE(72))); + ImGui::Text("X:%3d, Y:%3d", static_cast(stick.x), static_cast(stick.y)); + } + ImGui::EndChild(); +} + +#define CHIP_COLOR_N64_GREY ImVec4(0.4f, 0.4f, 0.4f, 1.0f) +#define CHIP_COLOR_N64_BLUE ImVec4(0.176f, 0.176f, 0.5f, 1.0f) +#define CHIP_COLOR_N64_GREEN ImVec4(0.0f, 0.294f, 0.0f, 1.0f) +#define CHIP_COLOR_N64_YELLOW ImVec4(0.5f, 0.314f, 0.0f, 1.0f) +#define CHIP_COLOR_N64_RED ImVec4(0.392f, 0.0f, 0.0f, 1.0f) + +#define BUTTON_COLOR_KEYBOARD_BEIGE ImVec4(0.651f, 0.482f, 0.357f, 0.5f) +#define BUTTON_COLOR_KEYBOARD_BEIGE_HOVERED ImVec4(0.651f, 0.482f, 0.357f, 1.0f) + +#define BUTTON_COLOR_MOUSE_BEIGE ImVec4(0.5f, 0.5f, 0.5f, 0.5f) +#define BUTTON_COLOR_MOUSE_BEIGE_HOVERED ImVec4(0.5f, 0.5f, 0.5f, 1.0f) + +#define BUTTON_COLOR_GAMEPAD_BLUE ImVec4(0.0f, 0.255f, 0.976f, 0.5f) +#define BUTTON_COLOR_GAMEPAD_BLUE_HOVERED ImVec4(0.0f, 0.255f, 0.976f, 1.0f) + +#define BUTTON_COLOR_GAMEPAD_RED ImVec4(0.976f, 0.0f, 0.094f, 0.5f) +#define BUTTON_COLOR_GAMEPAD_RED_HOVERED ImVec4(0.976f, 0.0f, 0.094f, 1.0f) + +#define BUTTON_COLOR_GAMEPAD_ORANGE ImVec4(0.976f, 0.376f, 0.0f, 0.5f) +#define BUTTON_COLOR_GAMEPAD_ORANGE_HOVERED ImVec4(0.976f, 0.376f, 0.0f, 1.0f) + +#define BUTTON_COLOR_GAMEPAD_GREEN ImVec4(0.0f, 0.5f, 0.0f, 0.5f) +#define BUTTON_COLOR_GAMEPAD_GREEN_HOVERED ImVec4(0.0f, 0.5f, 0.0f, 1.0f) + +#define BUTTON_COLOR_GAMEPAD_PURPLE ImVec4(0.431f, 0.369f, 0.706f, 0.5f) +#define BUTTON_COLOR_GAMEPAD_PURPLE_HOVERED ImVec4(0.431f, 0.369f, 0.706f, 1.0f) + +void GhostshipInputEditorWindow::GetButtonColorsForDeviceType(Ship::PhysicalDeviceType lusIndex, ImVec4& buttonColor, + ImVec4& buttonHoveredColor) { + switch (lusIndex) { + case Ship::PhysicalDeviceType::Keyboard: + buttonColor = BUTTON_COLOR_KEYBOARD_BEIGE; + buttonHoveredColor = BUTTON_COLOR_KEYBOARD_BEIGE_HOVERED; + break; + case Ship::PhysicalDeviceType::Mouse: + buttonColor = BUTTON_COLOR_MOUSE_BEIGE; + buttonHoveredColor = BUTTON_COLOR_MOUSE_BEIGE_HOVERED; + break; + case Ship::PhysicalDeviceType::SDLGamepad: + buttonColor = BUTTON_COLOR_GAMEPAD_BLUE; + buttonHoveredColor = BUTTON_COLOR_GAMEPAD_BLUE_HOVERED; + break; + default: + buttonColor = BUTTON_COLOR_GAMEPAD_PURPLE; + buttonHoveredColor = BUTTON_COLOR_GAMEPAD_PURPLE_HOVERED; + } +} + +void GhostshipInputEditorWindow::DrawInputChip(const char* buttonName, ImVec4 color = CHIP_COLOR_N64_GREY) { + ImGui::BeginDisabled(); + ImGui::PushStyleColor(ImGuiCol_Button, color); + ImGui::Button(buttonName, ImVec2(SCALE_IMGUI_SIZE(50.0f), 0)); + ImGui::PopStyleColor(); + ImGui::EndDisabled(); +} + +void GhostshipInputEditorWindow::DrawButtonLineAddMappingButton(uint8_t port, N64ButtonMask bitmask) { + ImGui::PushStyleVar(ImGuiStyleVar_ButtonTextAlign, ImVec2(1.0f, 0.5f)); + auto popupId = StringHelper::Sprintf("addButtonMappingPopup##%d-%d", port, bitmask); + if (ImGui::Button(StringHelper::Sprintf("%s###addButtonMappingButton%d-%d", ICON_FA_PLUS, port, bitmask).c_str(), + ImVec2(SCALE_IMGUI_SIZE(20.0f), 0.0f))) { + ImGui::OpenPopup(popupId.c_str()); + OffsetMappingPopup(); + }; + ImGui::PopStyleVar(); + + if (ImGui::BeginPopup(popupId.c_str())) { + mInputEditorPopupOpen = true; + ImGui::Text("Press any button,\nmove any axis,\nor press any key\nto add mapping"); + if (ImGui::Button("Cancel")) { + mInputEditorPopupOpen = false; + ImGui::CloseCurrentPopup(); + } + // todo: figure out why optional params (using id = "" in the definition) wasn't working + if (mMappingInputBlockTimer == INT32_MAX && Ship::Context::GetInstance() + ->GetControlDeck() + ->GetControllerByPort(port) + ->GetButton(bitmask) + ->AddOrEditButtonMappingFromRawPress(bitmask, "")) { + mInputEditorPopupOpen = false; + ImGui::CloseCurrentPopup(); + } + ImGui::EndPopup(); + } +} + +void GhostshipInputEditorWindow::DrawButtonLineEditMappingButton(uint8_t port, N64ButtonMask bitmask, std::string id) { + auto mapping = Ship::Context::GetInstance() + ->GetControlDeck() + ->GetControllerByPort(port) + ->GetButton(bitmask) + ->GetButtonMappingById(id); + if (mapping == nullptr) { + return; + } + + ImGui::PushStyleVar(ImGuiStyleVar_ButtonTextAlign, ImVec2(0.0f, 0.5f)); + std::string icon = ""; + switch (mapping->GetMappingType()) { + case MAPPING_TYPE_GAMEPAD: + icon = ICON_FA_GAMEPAD; + break; + case MAPPING_TYPE_KEYBOARD: + icon = ICON_FA_KEYBOARD_O; + break; + case MAPPING_TYPE_UNKNOWN: + icon = ICON_FA_BUG; + break; + } + auto buttonColor = ImGui::GetStyleColorVec4(ImGuiCol_Button); + auto buttonHoveredColor = ImGui::GetStyleColorVec4(ImGuiCol_ButtonHovered); + auto physicalInputDisplayName = + StringHelper::Sprintf("%s %s", icon.c_str(), mapping->GetPhysicalInputName().c_str()); + GetButtonColorsForDeviceType(mapping->GetPhysicalDeviceType(), buttonColor, buttonHoveredColor); + ImGui::PushStyleColor(ImGuiCol_Button, buttonColor); + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, buttonHoveredColor); + auto popupId = StringHelper::Sprintf("editButtonMappingPopup##%s", id.c_str()); + if (ImGui::Button( + StringHelper::Sprintf("%s###editButtonMappingButton%s", physicalInputDisplayName.c_str(), id.c_str()) + .c_str(), + ImVec2(ImGui::CalcTextSize(physicalInputDisplayName.c_str()).x + SCALE_IMGUI_SIZE(12.0f), 0.0f))) { + ImGui::OpenPopup(popupId.c_str()); + OffsetMappingPopup(); + } + if (ImGui::IsItemHovered(ImGuiHoveredFlags_DelayNormal | ImGuiHoveredFlags_NoSharedDelay)) { + ImGui::SetTooltip("%s", mapping->GetPhysicalDeviceName().c_str()); + } + ImGui::PopStyleColor(); + ImGui::PopStyleColor(); + + if (ImGui::BeginPopup(popupId.c_str())) { + mInputEditorPopupOpen = true; + ImGui::Text("Press any button,\nmove any axis,\nor press any key\nto edit mapping"); + if (ImGui::Button("Cancel")) { + mInputEditorPopupOpen = false; + ImGui::CloseCurrentPopup(); + } + if (mMappingInputBlockTimer == INT32_MAX && Ship::Context::GetInstance() + ->GetControlDeck() + ->GetControllerByPort(port) + ->GetButton(bitmask) + ->AddOrEditButtonMappingFromRawPress(bitmask, id)) { + mInputEditorPopupOpen = false; + ImGui::CloseCurrentPopup(); + } + ImGui::EndPopup(); + } + + ImGui::PopStyleVar(); + ImGui::SameLine(0, 0); + + auto sdlAxisDirectionToButtonMapping = std::dynamic_pointer_cast(mapping); + if (sdlAxisDirectionToButtonMapping != nullptr) { + ImGui::PushStyleVar(ImGuiStyleVar_ButtonTextAlign, ImVec2(0.0f, 0.5f)); + auto buttonColor = ImGui::GetStyleColorVec4(ImGuiCol_Button); + auto buttonHoveredColor = ImGui::GetStyleColorVec4(ImGuiCol_ButtonHovered); + GetButtonColorsForDeviceType(mapping->GetPhysicalDeviceType(), buttonColor, buttonHoveredColor); + ImGui::PushStyleColor(ImGuiCol_Button, buttonColor); + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, buttonHoveredColor); + ImGui::PushStyleVar(ImGuiStyleVar_ButtonTextAlign, ImVec2(1.0f, 0.5f)); + auto popupId = StringHelper::Sprintf("editAxisThresholdPopup##%s", id.c_str()); + if (ImGui::Button(StringHelper::Sprintf("%s###editAxisThresholdButton%s", ICON_FA_COG, id.c_str()).c_str(), + ImVec2(ImGui::CalcTextSize(ICON_FA_COG).x + SCALE_IMGUI_SIZE(10.0f), 0.0f))) { + ImGui::OpenPopup(popupId.c_str()); + OffsetMappingPopup(); + } + if (ImGui::IsItemHovered(ImGuiHoveredFlags_DelayNormal | ImGuiHoveredFlags_NoSharedDelay)) { + ImGui::SetTooltip("Edit axis threshold"); + } + ImGui::PopStyleColor(); + ImGui::PopStyleColor(); + ImGui::PopStyleVar(); + + if (ImGui::BeginPopup(popupId.c_str())) { + mInputEditorPopupOpen = true; + ImGui::Text("Axis Threshold\n\nThe extent to which the joystick\nmust be moved or the trigger\npressed to " + "initiate the assigned\nbutton action.\n\n"); + + auto globalSettings = Ship::Context::GetInstance()->GetControlDeck()->GetGlobalSDLDeviceSettings(); + + if (sdlAxisDirectionToButtonMapping->AxisIsStick()) { + ImGui::Text("Stick axis threshold:"); + + int32_t stickAxisThreshold = globalSettings->GetStickAxisThresholdPercentage(); + if (stickAxisThreshold == 0) { + ImGui::BeginDisabled(); + } + ImGui::PushButtonRepeat(true); + if (ImGui::Button(StringHelper::Sprintf("-##Stick Axis Threshold%s", id.c_str()).c_str())) { + globalSettings->SetStickAxisThresholdPercentage(stickAxisThreshold - 1); + globalSettings->SaveToConfig(); + } + ImGui::PopButtonRepeat(); + if (stickAxisThreshold == 0) { + ImGui::EndDisabled(); + } + ImGui::SameLine(0.0f, 0.0f); + ImGui::SetNextItemWidth(SCALE_IMGUI_SIZE(160.0f)); + if (ImGui::SliderInt(StringHelper::Sprintf("##Stick Axis Threshold%s", id.c_str()).c_str(), + &stickAxisThreshold, 0, 100, "%d%%", ImGuiSliderFlags_AlwaysClamp)) { + globalSettings->SetStickAxisThresholdPercentage(stickAxisThreshold); + globalSettings->SaveToConfig(); + } + ImGui::SameLine(0.0f, 0.0f); + if (stickAxisThreshold == 100) { + ImGui::BeginDisabled(); + } + ImGui::PushButtonRepeat(true); + if (ImGui::Button(StringHelper::Sprintf("+##Stick Axis Threshold%s", id.c_str()).c_str())) { + globalSettings->SetStickAxisThresholdPercentage(stickAxisThreshold + 1); + globalSettings->SaveToConfig(); + } + ImGui::PopButtonRepeat(); + if (stickAxisThreshold == 100) { + ImGui::EndDisabled(); + } + } + + if (sdlAxisDirectionToButtonMapping->AxisIsTrigger()) { + ImGui::Text("Trigger axis threshold:"); + + int32_t triggerAxisThreshold = globalSettings->GetTriggerAxisThresholdPercentage(); + if (triggerAxisThreshold == 0) { + ImGui::BeginDisabled(); + } + ImGui::PushButtonRepeat(true); + if (ImGui::Button(StringHelper::Sprintf("-##Trigger Axis Threshold%s", id.c_str()).c_str())) { + globalSettings->SetTriggerAxisThresholdPercentage(triggerAxisThreshold - 1); + globalSettings->SaveToConfig(); + } + ImGui::PopButtonRepeat(); + if (triggerAxisThreshold == 0) { + ImGui::EndDisabled(); + } + ImGui::SameLine(0.0f, 0.0f); + ImGui::SetNextItemWidth(SCALE_IMGUI_SIZE(160.0f)); + if (ImGui::SliderInt(StringHelper::Sprintf("##Trigger Axis Threshold%s", id.c_str()).c_str(), + &triggerAxisThreshold, 0, 100, "%d%%", ImGuiSliderFlags_AlwaysClamp)) { + globalSettings->SetTriggerAxisThresholdPercentage(triggerAxisThreshold); + globalSettings->SaveToConfig(); + } + ImGui::SameLine(0.0f, 0.0f); + if (triggerAxisThreshold == 100) { + ImGui::BeginDisabled(); + } + ImGui::PushButtonRepeat(true); + if (ImGui::Button(StringHelper::Sprintf("+##Trigger Axis Threshold%s", id.c_str()).c_str())) { + globalSettings->SetTriggerAxisThresholdPercentage(triggerAxisThreshold + 1); + globalSettings->SaveToConfig(); + } + ImGui::PopButtonRepeat(); + if (triggerAxisThreshold == 100) { + ImGui::EndDisabled(); + } + } + + if (ImGui::Button("Close")) { + mInputEditorPopupOpen = false; + ImGui::CloseCurrentPopup(); + } + + ImGui::EndPopup(); + } + + ImGui::PopStyleVar(); + ImGui::SameLine(0, 0); + } + + ImGui::PushStyleColor(ImGuiCol_Button, buttonColor); + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, buttonHoveredColor); + ImGui::PushStyleVar(ImGuiStyleVar_ButtonTextAlign, ImVec2(1.0f, 0.5f)); + if (ImGui::Button(StringHelper::Sprintf("%s###removeButtonMappingButton%s", ICON_FA_TIMES, id.c_str()).c_str(), + ImVec2(ImGui::CalcTextSize(ICON_FA_TIMES).x + SCALE_IMGUI_SIZE(10.0f), 0.0f))) { + Ship::Context::GetInstance() + ->GetControlDeck() + ->GetControllerByPort(port) + ->GetButton(bitmask) + ->ClearButtonMapping(id); + }; + ImGui::PopStyleColor(); + ImGui::PopStyleColor(); + ImGui::PopStyleVar(); + + ImGui::SameLine(0, SCALE_IMGUI_SIZE(4.0f)); +} + +void GhostshipInputEditorWindow::DrawButtonLine(const char* buttonName, uint8_t port, N64ButtonMask bitmask, + ImVec4 color = CHIP_COLOR_N64_GREY) { + ImGui::NewLine(); + ImGui::SameLine(SCALE_IMGUI_SIZE(32.0f)); + DrawInputChip(buttonName, color); + ImGui::SameLine(SCALE_IMGUI_SIZE(86.0f)); + for (auto id : mBitmaskToMappingIds[port][bitmask]) { + DrawButtonLineEditMappingButton(port, bitmask, id); + } + DrawButtonLineAddMappingButton(port, bitmask); +} + +void GhostshipInputEditorWindow::DrawStickDirectionLineAddMappingButton(uint8_t port, uint8_t stick, + Ship::Direction direction) { + ImGui::PushStyleVar(ImGuiStyleVar_ButtonTextAlign, ImVec2(1.0f, 0.5f)); + auto popupId = StringHelper::Sprintf("addStickDirectionMappingPopup##%d-%d-%d", port, stick, direction); + if (ImGui::Button( + StringHelper::Sprintf("%s###addStickDirectionMappingButton%d-%d-%d", ICON_FA_PLUS, port, stick, direction) + .c_str(), + ImVec2(SCALE_IMGUI_SIZE(20.0f), 0.0f))) { + ImGui::OpenPopup(popupId.c_str()); + OffsetMappingPopup(); + }; + ImGui::PopStyleVar(); + + if (ImGui::BeginPopup(popupId.c_str())) { + mInputEditorPopupOpen = true; + ImGui::Text("Press any button,\nmove any axis,\nor press any key\nto add mapping"); + if (ImGui::Button("Cancel")) { + mInputEditorPopupOpen = false; + ImGui::CloseCurrentPopup(); + } + if (stick == Ship::LEFT) { + if (mMappingInputBlockTimer == INT32_MAX && + Ship::Context::GetInstance() + ->GetControlDeck() + ->GetControllerByPort(port) + ->GetLeftStick() + ->AddOrEditAxisDirectionMappingFromRawPress(direction, "")) { + mInputEditorPopupOpen = false; + ImGui::CloseCurrentPopup(); + } + } else { + if (mMappingInputBlockTimer == INT32_MAX && + Ship::Context::GetInstance() + ->GetControlDeck() + ->GetControllerByPort(port) + ->GetRightStick() + ->AddOrEditAxisDirectionMappingFromRawPress(direction, "")) { + ImGui::CloseCurrentPopup(); + } + } + ImGui::EndPopup(); + } +} + +void GhostshipInputEditorWindow::DrawStickDirectionLineEditMappingButton(uint8_t port, uint8_t stick, + Ship::Direction direction, std::string id) { + std::shared_ptr mapping = nullptr; + if (stick == Ship::LEFT) { + mapping = Ship::Context::GetInstance() + ->GetControlDeck() + ->GetControllerByPort(port) + ->GetLeftStick() + ->GetAxisDirectionMappingById(direction, id); + } else { + mapping = Ship::Context::GetInstance() + ->GetControlDeck() + ->GetControllerByPort(port) + ->GetRightStick() + ->GetAxisDirectionMappingById(direction, id); + } + + if (mapping == nullptr) { + return; + } + + ImGui::PushStyleVar(ImGuiStyleVar_ButtonTextAlign, ImVec2(0.0f, 0.5f)); + std::string icon = ""; + switch (mapping->GetMappingType()) { + case MAPPING_TYPE_GAMEPAD: + icon = ICON_FA_GAMEPAD; + break; + case MAPPING_TYPE_KEYBOARD: + icon = ICON_FA_KEYBOARD_O; + break; + case MAPPING_TYPE_UNKNOWN: + icon = ICON_FA_BUG; + break; + } + auto buttonColor = ImGui::GetStyleColorVec4(ImGuiCol_Button); + auto buttonHoveredColor = ImGui::GetStyleColorVec4(ImGuiCol_ButtonHovered); + auto physicalInputDisplayName = + StringHelper::Sprintf("%s %s", icon.c_str(), mapping->GetPhysicalInputName().c_str()); + GetButtonColorsForDeviceType(mapping->GetPhysicalDeviceType(), buttonColor, buttonHoveredColor); + ImGui::PushStyleColor(ImGuiCol_Button, buttonColor); + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, buttonHoveredColor); + auto popupId = StringHelper::Sprintf("editStickDirectionMappingPopup##%s", id.c_str()); + if (ImGui::Button( + StringHelper::Sprintf("%s###editStickDirectionMappingButton%s", physicalInputDisplayName.c_str(), + id.c_str()) + .c_str(), + ImVec2(ImGui::CalcTextSize(physicalInputDisplayName.c_str()).x + SCALE_IMGUI_SIZE(12.0f), 0.0f))) { + ImGui::OpenPopup(popupId.c_str()); + OffsetMappingPopup(); + } + if (ImGui::IsItemHovered(ImGuiHoveredFlags_DelayNormal | ImGuiHoveredFlags_NoSharedDelay)) { + ImGui::SetTooltip("%s", mapping->GetPhysicalDeviceName().c_str()); + } + ImGui::PopStyleColor(); + ImGui::PopStyleColor(); + + if (ImGui::BeginPopup(popupId.c_str())) { + mInputEditorPopupOpen = true; + ImGui::Text("Press any button,\nmove any axis,\nor press any key\nto edit mapping"); + if (ImGui::Button("Cancel")) { + mInputEditorPopupOpen = false; + ImGui::CloseCurrentPopup(); + } + + if (stick == Ship::LEFT) { + if (mMappingInputBlockTimer == INT32_MAX && + Ship::Context::GetInstance() + ->GetControlDeck() + ->GetControllerByPort(port) + ->GetLeftStick() + ->AddOrEditAxisDirectionMappingFromRawPress(direction, id)) { + mInputEditorPopupOpen = false; + ImGui::CloseCurrentPopup(); + } + } else { + if (mMappingInputBlockTimer == INT32_MAX && + Ship::Context::GetInstance() + ->GetControlDeck() + ->GetControllerByPort(port) + ->GetRightStick() + ->AddOrEditAxisDirectionMappingFromRawPress(direction, id)) { + ImGui::CloseCurrentPopup(); + } + } + ImGui::EndPopup(); + } + + ImGui::PopStyleVar(); + ImGui::SameLine(0, 0); + ImGui::PushStyleColor(ImGuiCol_Button, buttonColor); + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, buttonHoveredColor); + ImGui::PushStyleVar(ImGuiStyleVar_ButtonTextAlign, ImVec2(1.0f, 0.5f)); + if (ImGui::Button( + StringHelper::Sprintf("%s###removeStickDirectionMappingButton%s", ICON_FA_TIMES, id.c_str()).c_str(), + ImVec2(ImGui::CalcTextSize(ICON_FA_TIMES).x + SCALE_IMGUI_SIZE(10.0f), 0.0f))) { + if (stick == Ship::LEFT) { + Ship::Context::GetInstance() + ->GetControlDeck() + ->GetControllerByPort(port) + ->GetLeftStick() + ->ClearAxisDirectionMapping(direction, id); + } else { + Ship::Context::GetInstance() + ->GetControlDeck() + ->GetControllerByPort(port) + ->GetRightStick() + ->ClearAxisDirectionMapping(direction, id); + } + }; + ImGui::PopStyleColor(); + ImGui::PopStyleColor(); + ImGui::PopStyleVar(); + ImGui::SameLine(0, SCALE_IMGUI_SIZE(4.0f)); +} + +void GhostshipInputEditorWindow::DrawStickDirectionLine(const char* axisDirectionName, uint8_t port, uint8_t stick, + Ship::Direction direction, + ImVec4 color = CHIP_COLOR_N64_GREY) { + ImGui::NewLine(); + ImGui::SameLine(); + ImGui::BeginDisabled(); + ImGui::PushStyleColor(ImGuiCol_Button, color); + ImGui::Button(axisDirectionName, ImVec2(SCALE_IMGUI_SIZE(26.0f), 0)); + ImGui::PopStyleColor(); + ImGui::EndDisabled(); + ImGui::SameLine(0.0f, SCALE_IMGUI_SIZE(4.0f)); + for (auto id : mStickDirectionToMappingIds[port][stick][direction]) { + DrawStickDirectionLineEditMappingButton(port, stick, direction, id); + } + DrawStickDirectionLineAddMappingButton(port, stick, direction); +} + +void GhostshipInputEditorWindow::DrawStickSection(uint8_t port, uint8_t stick, ImVec4 color = CHIP_COLOR_N64_GREY) { + static int8_t sX, sY; + std::shared_ptr controllerStick = nullptr; + if (stick == Ship::LEFT) { + controllerStick = Ship::Context::GetInstance()->GetControlDeck()->GetControllerByPort(port)->GetLeftStick(); + } else { + controllerStick = Ship::Context::GetInstance()->GetControlDeck()->GetControllerByPort(port)->GetRightStick(); + } + controllerStick->Process(sX, sY); + DrawAnalogPreview(StringHelper::Sprintf("##AnalogPreview%d", stick).c_str(), ImVec2(sX, sY)); + + ImGui::SameLine(); + ImGui::BeginGroup(); + DrawStickDirectionLine(StringHelper::Sprintf("%s##%d", ICON_FA_ARROW_UP, stick).c_str(), port, stick, Ship::UP, + color); + DrawStickDirectionLine(StringHelper::Sprintf("%s##%d", ICON_FA_ARROW_DOWN, stick).c_str(), port, stick, Ship::DOWN, + color); + DrawStickDirectionLine(StringHelper::Sprintf("%s##%d", ICON_FA_ARROW_LEFT, stick).c_str(), port, stick, Ship::LEFT, + color); + DrawStickDirectionLine(StringHelper::Sprintf("%s##%d", ICON_FA_ARROW_RIGHT, stick).c_str(), port, stick, + Ship::RIGHT, color); + ImGui::EndGroup(); + ImGui::SetNextItemOpen(true, ImGuiCond_Once); + if (ImGui::TreeNode(StringHelper::Sprintf("Analog Stick Options##%d", stick).c_str())) { + ImGui::Text("Sensitivity:"); + + int32_t sensitivityPercentage = controllerStick->GetSensitivityPercentage(); + if (sensitivityPercentage == 0) { + ImGui::BeginDisabled(); + } + ImGui::PushButtonRepeat(true); + if (ImGui::Button(StringHelper::Sprintf("-##Sensitivity%d", stick).c_str())) { + controllerStick->SetSensitivity(sensitivityPercentage - 1); + } + ImGui::PopButtonRepeat(); + if (sensitivityPercentage == 0) { + ImGui::EndDisabled(); + } + ImGui::SameLine(0.0f, 0.0f); + ImGui::SetNextItemWidth(SCALE_IMGUI_SIZE(160.0f)); + if (ImGui::SliderInt(StringHelper::Sprintf("##Sensitivity%d", stick).c_str(), &sensitivityPercentage, 0, 200, + "%d%%", ImGuiSliderFlags_AlwaysClamp)) { + controllerStick->SetSensitivity(sensitivityPercentage); + } + ImGui::SameLine(0.0f, 0.0f); + if (sensitivityPercentage == 200) { + ImGui::BeginDisabled(); + } + ImGui::PushButtonRepeat(true); + if (ImGui::Button(StringHelper::Sprintf("+##Sensitivity%d", stick).c_str())) { + controllerStick->SetSensitivity(sensitivityPercentage + 1); + } + ImGui::PopButtonRepeat(); + if (sensitivityPercentage == 200) { + ImGui::EndDisabled(); + } + if (!controllerStick->SensitivityIsDefault()) { + ImGui::SameLine(); + if (ImGui::Button(StringHelper::Sprintf("Reset to Default###resetStickSensitivity%d", stick).c_str())) { + controllerStick->ResetSensitivityToDefault(); + } + } + + ImGui::Text("Deadzone:"); + + int32_t deadzonePercentage = controllerStick->GetDeadzonePercentage(); + if (deadzonePercentage == 0) { + ImGui::BeginDisabled(); + } + ImGui::PushButtonRepeat(true); + if (ImGui::Button(StringHelper::Sprintf("-##Deadzone%d", stick).c_str())) { + controllerStick->SetDeadzone(deadzonePercentage - 1); + } + ImGui::PopButtonRepeat(); + if (deadzonePercentage == 0) { + ImGui::EndDisabled(); + } + ImGui::SameLine(0.0f, 0.0f); + ImGui::SetNextItemWidth(SCALE_IMGUI_SIZE(160.0f)); + if (ImGui::SliderInt(StringHelper::Sprintf("##Deadzone%d", stick).c_str(), &deadzonePercentage, 0, 100, "%d%%", + ImGuiSliderFlags_AlwaysClamp)) { + controllerStick->SetDeadzone(deadzonePercentage); + } + ImGui::SameLine(0.0f, 0.0f); + if (deadzonePercentage == 100) { + ImGui::BeginDisabled(); + } + ImGui::PushButtonRepeat(true); + if (ImGui::Button(StringHelper::Sprintf("+##Deadzone%d", stick).c_str())) { + controllerStick->SetDeadzone(deadzonePercentage + 1); + } + ImGui::PopButtonRepeat(); + if (deadzonePercentage == 100) { + ImGui::EndDisabled(); + } + if (!controllerStick->DeadzoneIsDefault()) { + ImGui::SameLine(); + if (ImGui::Button(StringHelper::Sprintf("Reset to Default###resetStickDeadzone%d", stick).c_str())) { + controllerStick->ResetDeadzoneToDefault(); + } + } + + ImGui::Text("Notch Snap Angle:"); + int32_t notchSnapAngle = controllerStick->GetNotchSnapAngle(); + if (notchSnapAngle == 0) { + ImGui::BeginDisabled(); + } + ImGui::PushButtonRepeat(true); + if (ImGui::Button(StringHelper::Sprintf("-##NotchProximityThreshold%d", stick).c_str())) { + controllerStick->SetNotchSnapAngle(notchSnapAngle - 1); + } + ImGui::PopButtonRepeat(); + if (notchSnapAngle == 0) { + ImGui::EndDisabled(); + } + ImGui::SameLine(0.0f, 0.0f); + ImGui::SetNextItemWidth(SCALE_IMGUI_SIZE(160.0f)); + if (ImGui::SliderInt(StringHelper::Sprintf("##NotchProximityThreshold%d", stick).c_str(), ¬chSnapAngle, 0, + 45, "%d°", ImGuiSliderFlags_AlwaysClamp)) { + controllerStick->SetNotchSnapAngle(notchSnapAngle); + } + ImGui::SameLine(0.0f, 0.0f); + if (notchSnapAngle == 45) { + ImGui::BeginDisabled(); + } + ImGui::PushButtonRepeat(true); + if (ImGui::Button(StringHelper::Sprintf("+##NotchProximityThreshold%d", stick).c_str())) { + controllerStick->SetNotchSnapAngle(notchSnapAngle + 1); + } + ImGui::PopButtonRepeat(); + if (notchSnapAngle == 45) { + ImGui::EndDisabled(); + } + if (!controllerStick->NotchSnapAngleIsDefault()) { + ImGui::SameLine(); + if (ImGui::Button(StringHelper::Sprintf("Reset to Default###resetStickSnap%d", stick).c_str())) { + controllerStick->ResetNotchSnapAngleToDefault(); + } + } + + ImGui::TreePop(); + } +} + +void GhostshipInputEditorWindow::UpdateBitmaskToMappingIds(uint8_t port) { + // todo: do we need this now that ControllerButton exists? + + for (auto [bitmask, button] : + Ship::Context::GetInstance()->GetControlDeck()->GetControllerByPort(port)->GetAllButtons()) { + for (auto [id, mapping] : button->GetAllButtonMappings()) { + // using a vector here instead of a set because i want newly added mappings + // to go to the end of the list instead of autosorting + if (std::find(mBitmaskToMappingIds[port][bitmask].begin(), mBitmaskToMappingIds[port][bitmask].end(), id) == + mBitmaskToMappingIds[port][bitmask].end()) { + mBitmaskToMappingIds[port][bitmask].push_back(id); + } + } + } +} + +void GhostshipInputEditorWindow::UpdateStickDirectionToMappingIds(uint8_t port) { + // todo: do we need this? + for (auto stick : + { std::make_pair>( + Ship::LEFT, Ship::Context::GetInstance()->GetControlDeck()->GetControllerByPort(port)->GetLeftStick()), + std::make_pair>( + Ship::RIGHT, + Ship::Context::GetInstance()->GetControlDeck()->GetControllerByPort(port)->GetRightStick()) }) { + for (auto direction : { Ship::LEFT, Ship::RIGHT, Ship::UP, Ship::DOWN }) { + for (auto [id, mapping] : stick.second->GetAllAxisDirectionMappingByDirection(direction)) { + // using a vector here instead of a set because i want newly added mappings + // to go to the end of the list instead of autosorting + if (std::find(mStickDirectionToMappingIds[port][stick.first][direction].begin(), + mStickDirectionToMappingIds[port][stick.first][direction].end(), + id) == mStickDirectionToMappingIds[port][stick.first][direction].end()) { + mStickDirectionToMappingIds[port][stick.first][direction].push_back(id); + } + } + } + } +} + +void GhostshipInputEditorWindow::DrawRemoveRumbleMappingButton(uint8_t port, std::string id) { + ImGui::SameLine(); + ImGui::PushStyleVar(ImGuiStyleVar_ButtonTextAlign, ImVec2(1.0f, 0.5f)); + if (ImGui::Button(StringHelper::Sprintf("%s###removeRumbleMapping%s", ICON_FA_TIMES, id.c_str()).c_str(), + ImVec2(SCALE_IMGUI_SIZE(20.0f), SCALE_IMGUI_SIZE(20.0f)))) { + Ship::Context::GetInstance()->GetControlDeck()->GetControllerByPort(port)->GetRumble()->ClearRumbleMapping(id); + } + ImGui::PopStyleVar(); +} + +void GhostshipInputEditorWindow::DrawAddRumbleMappingButton(uint8_t port) { + ImGui::SameLine(); + ImGui::PushStyleVar(ImGuiStyleVar_ButtonTextAlign, ImVec2(1.0f, 0.5f)); + auto popupId = StringHelper::Sprintf("addRumbleMappingPopup##%d", port); + if (ImGui::Button(StringHelper::Sprintf("%s###addRumbleMapping%d", ICON_FA_PLUS, port).c_str(), + ImVec2(SCALE_IMGUI_SIZE(20.0f), SCALE_IMGUI_SIZE(20.0f)))) { + ImGui::OpenPopup(popupId.c_str()); + OffsetMappingPopup(); + } + ImGui::PopStyleVar(); + + if (ImGui::BeginPopup(popupId.c_str())) { + mInputEditorPopupOpen = true; + ImGui::Text("Press any button\nor move any axis\nto add rumble device"); + if (ImGui::Button("Cancel")) { + mInputEditorPopupOpen = false; + ImGui::CloseCurrentPopup(); + } + + if (mMappingInputBlockTimer == INT32_MAX && Ship::Context::GetInstance() + ->GetControlDeck() + ->GetControllerByPort(port) + ->GetRumble() + ->AddRumbleMappingFromRawPress()) { + mInputEditorPopupOpen = false; + ImGui::CloseCurrentPopup(); + } + ImGui::EndPopup(); + } +} + +bool GhostshipInputEditorWindow::TestingRumble() { + return mRumbleTimer != INT32_MAX; +} + +void GhostshipInputEditorWindow::DrawRumbleSection(uint8_t port) { + for (auto [id, mapping] : Ship::Context::GetInstance() + ->GetControlDeck() + ->GetControllerByPort(port) + ->GetRumble() + ->GetAllRumbleMappings()) { + ImGui::AlignTextToFramePadding(); + ImGui::SetNextItemOpen(true, ImGuiCond_Once); + auto buttonColor = ImGui::GetStyleColorVec4(ImGuiCol_Button); + auto buttonHoveredColor = ImGui::GetStyleColorVec4(ImGuiCol_ButtonHovered); + GetButtonColorsForDeviceType(mapping->GetPhysicalDeviceType(), buttonColor, buttonHoveredColor); + // begin hackaround https://github.com/ocornut/imgui/issues/282#issuecomment-123763192 + // spaces to have background color for text in a tree node + std::string spaces = ""; + for (size_t i = 0; i < mapping->GetPhysicalDeviceName().length(); i++) { + spaces += " "; + } + auto open = ImGui::TreeNode(StringHelper::Sprintf("%s###Rumble%s", spaces.c_str(), id.c_str()).c_str()); + ImGui::SameLine(); + ImGui::SetCursorPosX(SCALE_IMGUI_SIZE(30.0f)); + // end hackaround + + ImGui::PushItemFlag(ImGuiItemFlags_Disabled, true); + ImGui::PushStyleColor(ImGuiCol_Button, buttonColor); + ImGui::Button(mapping->GetPhysicalDeviceName().c_str()); + ImGui::PopStyleColor(); + ImGui::PopItemFlag(); + + DrawRemoveRumbleMappingButton(port, id); + ImGui::SameLine(); + if (ImGui::Button( + StringHelper::Sprintf("%s###rumbleTestButton%s", TestingRumble() ? "Stop" : "Test", id.c_str()) + .c_str())) { + if (mRumbleTimer != INT32_MAX) { + mRumbleTimer = INT32_MAX; + mRumbleMappingToTest->StopRumble(); + mRumbleMappingToTest = nullptr; + } else { + mRumbleTimer = ImGui::GetIO().Framerate; + mRumbleMappingToTest = mapping; + } + } + if (open) { + ImGui::Text("Small Motor Intensity:"); + + int32_t smallMotorIntensity = mapping->GetHighFrequencyIntensityPercentage(); + if (smallMotorIntensity == 0) { + ImGui::BeginDisabled(); + } + ImGui::PushButtonRepeat(true); + if (ImGui::Button(StringHelper::Sprintf("-##Small Motor Intensity%s", id.c_str()).c_str())) { + mapping->SetHighFrequencyIntensity(smallMotorIntensity - 1); + mapping->SaveToConfig(); + } + ImGui::PopButtonRepeat(); + if (smallMotorIntensity == 0) { + ImGui::EndDisabled(); + } + ImGui::SameLine(0.0f, 0.0f); + ImGui::SetNextItemWidth(SCALE_IMGUI_SIZE(160.0f)); + if (ImGui::SliderInt(StringHelper::Sprintf("##Small Motor Intensity%s", id.c_str()).c_str(), + &smallMotorIntensity, 0, 100, "%d%%", ImGuiSliderFlags_AlwaysClamp)) { + mapping->SetHighFrequencyIntensity(smallMotorIntensity); + mapping->SaveToConfig(); + } + ImGui::SameLine(0.0f, 0.0f); + if (smallMotorIntensity == 100) { + ImGui::BeginDisabled(); + } + ImGui::PushButtonRepeat(true); + if (ImGui::Button(StringHelper::Sprintf("+##Small Motor Intensity%s", id.c_str()).c_str())) { + mapping->SetHighFrequencyIntensity(smallMotorIntensity + 1); + mapping->SaveToConfig(); + } + ImGui::PopButtonRepeat(); + if (smallMotorIntensity == 100) { + ImGui::EndDisabled(); + } + if (!mapping->HighFrequencyIntensityIsDefault()) { + ImGui::SameLine(); + if (ImGui::Button(StringHelper::Sprintf("Reset to Default###resetHighFrequencyIntensity%s", id.c_str()) + .c_str())) { + mapping->ResetHighFrequencyIntensityToDefault(); + } + } + + ImGui::Text("Large Motor Intensity:"); + + int32_t largeMotorIntensity = mapping->GetLowFrequencyIntensityPercentage(); + if (largeMotorIntensity == 0) { + ImGui::BeginDisabled(); + } + ImGui::PushButtonRepeat(true); + if (ImGui::Button(StringHelper::Sprintf("-##Large Motor Intensity%s", id.c_str()).c_str())) { + mapping->SetLowFrequencyIntensity(largeMotorIntensity - 1); + mapping->SaveToConfig(); + } + ImGui::PopButtonRepeat(); + if (largeMotorIntensity == 0) { + ImGui::EndDisabled(); + } + ImGui::SameLine(0.0f, 0.0f); + ImGui::SetNextItemWidth(SCALE_IMGUI_SIZE(160.0f)); + if (ImGui::SliderInt(StringHelper::Sprintf("##Large Motor Intensity%s", id.c_str()).c_str(), + &largeMotorIntensity, 0, 100, "%d%%", ImGuiSliderFlags_AlwaysClamp)) { + mapping->SetLowFrequencyIntensity(largeMotorIntensity); + mapping->SaveToConfig(); + } + ImGui::SameLine(0.0f, 0.0f); + if (largeMotorIntensity == 100) { + ImGui::BeginDisabled(); + } + ImGui::PushButtonRepeat(true); + if (ImGui::Button(StringHelper::Sprintf("+##Large Motor Intensity%s", id.c_str()).c_str())) { + mapping->SetLowFrequencyIntensity(largeMotorIntensity + 1); + mapping->SaveToConfig(); + } + ImGui::PopButtonRepeat(); + if (largeMotorIntensity == 100) { + ImGui::EndDisabled(); + } + if (!mapping->LowFrequencyIntensityIsDefault()) { + ImGui::SameLine(); + if (ImGui::Button( + StringHelper::Sprintf("Reset to Default###resetLowFrequencyIntensity%s", id.c_str()).c_str())) { + mapping->ResetLowFrequencyIntensityToDefault(); + } + } + ImGui::Dummy(ImVec2(0, SCALE_IMGUI_SIZE(20))); + + ImGui::TreePop(); + } + } + + ImGui::AlignTextToFramePadding(); + ImGui::BulletText("Add rumble device"); + DrawAddRumbleMappingButton(port); +} + +void GhostshipInputEditorWindow::DrawRemoveLEDMappingButton(uint8_t port, std::string id) { + ImGui::SameLine(); + ImGui::PushStyleVar(ImGuiStyleVar_ButtonTextAlign, ImVec2(1.0f, 0.5f)); + if (ImGui::Button(StringHelper::Sprintf("%s###removeLEDMapping%s", ICON_FA_TIMES, id.c_str()).c_str(), + ImVec2(SCALE_IMGUI_SIZE(20.0f), SCALE_IMGUI_SIZE(20.0f)))) { + Ship::Context::GetInstance()->GetControlDeck()->GetControllerByPort(port)->GetLED()->ClearLEDMapping(id); + } + ImGui::PopStyleVar(); +} + +void GhostshipInputEditorWindow::DrawAddLEDMappingButton(uint8_t port) { + ImGui::SameLine(); + ImGui::PushStyleVar(ImGuiStyleVar_ButtonTextAlign, ImVec2(1.0f, 0.5f)); + auto popupId = StringHelper::Sprintf("addLEDMappingPopup##%d", port); + if (ImGui::Button(StringHelper::Sprintf("%s###addLEDMapping%d", ICON_FA_PLUS, port).c_str(), + ImVec2(SCALE_IMGUI_SIZE(20.0f), SCALE_IMGUI_SIZE(20.0f)))) { + ImGui::OpenPopup(popupId.c_str()); + OffsetMappingPopup(); + } + ImGui::PopStyleVar(); + + if (ImGui::BeginPopup(popupId.c_str())) { + mInputEditorPopupOpen = true; + ImGui::Text("Press any button\nor move any axis\nto add LED device"); + if (ImGui::Button("Cancel")) { + mInputEditorPopupOpen = false; + ImGui::CloseCurrentPopup(); + } + + if (mMappingInputBlockTimer == INT32_MAX && Ship::Context::GetInstance() + ->GetControlDeck() + ->GetControllerByPort(port) + ->GetLED() + ->AddLEDMappingFromRawPress()) { + mInputEditorPopupOpen = false; + ImGui::CloseCurrentPopup(); + } + ImGui::EndPopup(); + } +} + +void GhostshipInputEditorWindow::DrawLEDSection(uint8_t port) { + for (auto [id, mapping] : + Ship::Context::GetInstance()->GetControlDeck()->GetControllerByPort(port)->GetLED()->GetAllLEDMappings()) { + ImGui::AlignTextToFramePadding(); + ImGui::SetNextItemOpen(true, ImGuiCond_Once); + auto open = ImGui::TreeNode( + StringHelper::Sprintf("%s##LED%s", mapping->GetPhysicalDeviceName().c_str(), id.c_str()).c_str()); + DrawRemoveLEDMappingButton(port, id); + if (open) { + ImGui::AlignTextToFramePadding(); + ImGui::Text("LED Color:"); + ImGui::SameLine(); + ImGui::SetNextItemWidth(SCALE_IMGUI_SIZE(80.0f)); + int32_t colorSource = mapping->GetColorSource(); + if (ImGui::Combo(StringHelper::Sprintf("###ledColorSource%s", mapping->GetLEDMappingId().c_str()).c_str(), + &colorSource, "Off\0Set\0Game\0\0")) { + mapping->SetColorSource(colorSource); + }; + if (mapping->GetColorSource() == LED_COLOR_SOURCE_SET) { + ImGui::SameLine(); + ImVec4 color = { mapping->GetSavedColor().r / 255.0f, mapping->GetSavedColor().g / 255.0f, + mapping->GetSavedColor().b / 255.0f, 1.0f }; + if (ImGui::ColorEdit3( + StringHelper::Sprintf("###ledSavedColor%s", mapping->GetLEDMappingId().c_str()).c_str(), + (float*)&color, ImGuiColorEditFlags_NoInputs | ImGuiColorEditFlags_NoLabel)) { + mapping->SetSavedColor( + Color_RGB8({ static_cast(color.x * 255.0), static_cast(color.y * 255.0), + static_cast(color.z * 255.0) })); + } + } + ImGui::TreePop(); + } + } + + ImGui::AlignTextToFramePadding(); + ImGui::BulletText("Add LED device"); + DrawAddLEDMappingButton(port); +} + +void GhostshipInputEditorWindow::DrawRemoveGyroMappingButton(uint8_t port, std::string id) { + ImGui::SameLine(); + ImGui::PushStyleVar(ImGuiStyleVar_ButtonTextAlign, ImVec2(1.0f, 0.5f)); + if (ImGui::Button(StringHelper::Sprintf("%s###removeGyroMapping%s", ICON_FA_TIMES, id.c_str()).c_str(), + ImVec2(SCALE_IMGUI_SIZE(20.0f), SCALE_IMGUI_SIZE(20.0f)))) { + Ship::Context::GetInstance()->GetControlDeck()->GetControllerByPort(port)->GetGyro()->ClearGyroMapping(); + } + ImGui::PopStyleVar(); +} + +void GhostshipInputEditorWindow::DrawAddGyroMappingButton(uint8_t port) { + ImGui::SameLine(); + ImGui::PushStyleVar(ImGuiStyleVar_ButtonTextAlign, ImVec2(1.0f, 0.5f)); + auto popupId = StringHelper::Sprintf("addGyroMappingPopup##%d", port); + if (ImGui::Button(StringHelper::Sprintf("%s###addGyroMapping%d", ICON_FA_PLUS, port).c_str(), + ImVec2(SCALE_IMGUI_SIZE(20.0f), SCALE_IMGUI_SIZE(20.0f)))) { + ImGui::OpenPopup(popupId.c_str()); + OffsetMappingPopup(); + } + ImGui::PopStyleVar(); + + if (ImGui::BeginPopup(popupId.c_str())) { + mInputEditorPopupOpen = true; + ImGui::Text("Press any button\nor move any axis\nto add gyro device"); + if (ImGui::Button("Cancel")) { + mInputEditorPopupOpen = false; + ImGui::CloseCurrentPopup(); + } + + if (mMappingInputBlockTimer == INT32_MAX && Ship::Context::GetInstance() + ->GetControlDeck() + ->GetControllerByPort(port) + ->GetGyro() + ->SetGyroMappingFromRawPress()) { + mInputEditorPopupOpen = false; + ImGui::CloseCurrentPopup(); + } + ImGui::EndPopup(); + } +} + +void GhostshipInputEditorWindow::DrawGyroSection(uint8_t port) { + auto mapping = + Ship::Context::GetInstance()->GetControlDeck()->GetControllerByPort(port)->GetGyro()->GetGyroMapping(); + if (mapping != nullptr) { + auto id = mapping->GetGyroMappingId(); + ImGui::AlignTextToFramePadding(); + ImGui::SetNextItemOpen(true, ImGuiCond_Once); + ImGui::BulletText("%s", mapping->GetPhysicalDeviceName().c_str()); + DrawRemoveGyroMappingButton(port, id); + + static float sPitch, sYaw = 0.0f; + mapping->UpdatePad(sPitch, sYaw); + + ImGui::SetCursorPos(ImVec2(ImGui::GetCursorPos().x, ImGui::GetCursorPos().y - SCALE_IMGUI_SIZE(8))); + // to find a reasonable scaling factor gyro values + // I tried to find the maximum value reported by shaking + // a PS5 controller as hard as I could without worrying about breaking it + // the max I found for both pitch and yaw was ~21 + // the preview window expects values in an n64 analog stick range (-85 to 85) + // so I decided to multiply these by 85/21 + DrawAnalogPreview(StringHelper::Sprintf("###GyroPreview%s", id.c_str()).c_str(), + ImVec2(sYaw * (85.0f / 21.0f), sPitch * (85.0f / 21.0f)), 0.0f, true); + ImGui::SameLine(); + ImGui::SetCursorPos( + ImVec2(ImGui::GetCursorPos().x + SCALE_IMGUI_SIZE(8), ImGui::GetCursorPos().y + SCALE_IMGUI_SIZE(8))); + + ImGui::BeginGroup(); + ImGui::Text("Sensitivity:"); + + int32_t sensitivity = mapping->GetSensitivityPercent(); + if (sensitivity == 0) { + ImGui::BeginDisabled(); + } + ImGui::PushButtonRepeat(true); + if (ImGui::Button(StringHelper::Sprintf("-##GyroSensitivity%s", id.c_str()).c_str())) { + mapping->SetSensitivity(sensitivity - 1); + mapping->SaveToConfig(); + } + ImGui::PopButtonRepeat(); + if (sensitivity == 0) { + ImGui::EndDisabled(); + } + ImGui::SameLine(0.0f, 0.0f); + ImGui::SetNextItemWidth(SCALE_IMGUI_SIZE(160.0f)); + if (ImGui::SliderInt(StringHelper::Sprintf("##GyroSensitivity%s", id.c_str()).c_str(), &sensitivity, 0, 100, + "%d%%", ImGuiSliderFlags_AlwaysClamp)) { + mapping->SetSensitivity(sensitivity); + mapping->SaveToConfig(); + } + ImGui::SameLine(0.0f, 0.0f); + if (sensitivity == 100) { + ImGui::BeginDisabled(); + } + ImGui::PushButtonRepeat(true); + if (ImGui::Button(StringHelper::Sprintf("+##GyroSensitivity%s", id.c_str()).c_str())) { + mapping->SetSensitivity(sensitivity + 1); + mapping->SaveToConfig(); + } + ImGui::PopButtonRepeat(); + if (sensitivity == 100) { + ImGui::EndDisabled(); + } + + if (!mapping->SensitivityIsDefault()) { + ImGui::SameLine(); + if (ImGui::Button(StringHelper::Sprintf("Reset to Default###resetGyroSensitivity%s", id.c_str()).c_str())) { + mapping->ResetSensitivityToDefault(); + } + } + + ImGui::SetCursorPos(ImVec2(ImGui::GetCursorPos().x, ImGui::GetCursorPos().y + SCALE_IMGUI_SIZE(8))); + if (ImGui::Button("Recalibrate")) { + mapping->Recalibrate(); + mapping->SaveToConfig(); + } + ImGui::EndGroup(); + ImGui::SetCursorPos(ImVec2(ImGui::GetCursorPos().x, ImGui::GetCursorPos().y - SCALE_IMGUI_SIZE(8))); + } else { + ImGui::AlignTextToFramePadding(); + ImGui::BulletText("Add gyro device"); + DrawAddGyroMappingButton(port); + } +} + +const ImGuiTableFlags PANEL_TABLE_FLAGS = ImGuiTableFlags_BordersH | ImGuiTableFlags_BordersV; +const ImGuiTableColumnFlags PANEL_TABLE_COLUMN_FLAGS = + ImGuiTableColumnFlags_IndentEnable | ImGuiTableColumnFlags_NoSort; + +namespace TableHelper { +void InitHeader(bool has_header = true) { + if (has_header) { + ImGui::TableHeadersRow(); + } + ImGui::TableNextRow(); + ImGui::TableNextColumn(); + ImGui::AlignTextToFramePadding(); // This is to adjust Vertical pos of item in a cell to be normlized. + ImGui::PushItemWidth(ImGui::GetContentRegionAvail().x); +} + +void NextCol() { + ImGui::TableNextColumn(); + ImGui::AlignTextToFramePadding(); + ImGui::PushItemWidth(ImGui::GetContentRegionAvail().x); +} + +void NextLine() { + ImGui::TableNextRow(); + ImGui::TableNextColumn(); + ImGui::AlignTextToFramePadding(); + ImGui::PushItemWidth(ImGui::GetContentRegionAvail().x); +} +} // namespace TableHelper + +void GhostshipInputEditorWindow::addButtonName(N64ButtonMask mask, const char* name) { + buttons.push_back(std::make_pair(mask, name)); + buttonNames[mask] = std::prev(buttons.end()); +} + +// Draw a button mapping setting consisting of a padded label and button dropdown. +// excludedButtons indicates which buttons are unavailable to choose from. +void GhostshipInputEditorWindow::DrawMapping(CustomButtonMap& mapping, float labelWidth, + N64ButtonMask excludedButtons) { + N64ButtonMask currentButton = CVarGetInteger(mapping.cVarName, mapping.defaultBtn); + + const char* preview; + if (buttonNames.contains(currentButton)) { + preview = buttonNames[currentButton]->second; + } else { + preview = "Unknown"; + } + + ImGui::Dummy(ImVec2(0.0f, 0.0f)); + ImVec2 cursorPos = ImGui::GetCursorPos(); + ImVec2 textSize = ImGui::CalcTextSize(mapping.label); + ImGui::SetCursorPosY(cursorPos.y + textSize.y / 4); + ImGui::SetCursorPosX(cursorPos.x + abs(textSize.x - labelWidth)); + ImGui::Text("%s", mapping.label); + ImGui::SameLine(); + ImGui::SetCursorPosY(cursorPos.y); + + ImGui::SetNextItemWidth(ImGui::GetFontSize() * 8); + if (ImGui::BeginCombo(StringHelper::Sprintf("##%s", mapping.cVarName).c_str(), preview)) { + for (auto i = buttons.begin(); i != buttons.end(); i++) { + if ((i->first & excludedButtons) != 0) { + continue; + } + if (ImGui::Selectable(i->second, i->first == currentButton)) { + CVarSetInteger(mapping.cVarName, i->first); + Ship::Context::GetInstance()->GetWindow()->GetGui()->SaveConsoleVariablesNextFrame(); + } + } + ImGui::EndCombo(); + } + ImGui::Dummy(ImVec2(0.0f, 0.0f)); +} + +void GhostshipInputEditorWindow::DrawDeviceToggles(uint8_t portIndex) { + ImGui::PushItemFlag(ImGuiItemFlags_Disabled, true); + + auto keyboardButtonColor = ImGui::GetStyleColorVec4(ImGuiCol_Button); + auto keyboardButtonHoveredColor = ImGui::GetStyleColorVec4(ImGuiCol_ButtonHovered); + GetButtonColorsForDeviceType(Ship::PhysicalDeviceType::Keyboard, keyboardButtonColor, keyboardButtonHoveredColor); + ImGui::PushStyleColor(ImGuiCol_Button, keyboardButtonColor); + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, keyboardButtonHoveredColor); + ImGui::Button(StringHelper::Sprintf("%s Keyboard", ICON_FA_KEYBOARD_O).c_str()); + ImGui::PopStyleColor(); + ImGui::PopStyleColor(); + + auto mouseButtonColor = ImGui::GetStyleColorVec4(ImGuiCol_Button); + auto mouseButtonHoveredColor = ImGui::GetStyleColorVec4(ImGuiCol_ButtonHovered); + GetButtonColorsForDeviceType(Ship::PhysicalDeviceType::Mouse, mouseButtonColor, mouseButtonHoveredColor); + ImGui::PushStyleColor(ImGuiCol_Button, mouseButtonColor); + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, mouseButtonHoveredColor); + ImGui::Button(StringHelper::Sprintf("%s Mouse", ICON_FA_KEYBOARD_O).c_str()); + ImGui::PopStyleColor(); + ImGui::PopStyleColor(); + + ImGui::PopItemFlag(); + + auto connectedDeviceManager = Ship::Context::GetInstance()->GetControlDeck()->GetConnectedPhysicalDeviceManager(); + for (const auto& [instanceId, name] : connectedDeviceManager->GetConnectedSDLGamepadNames()) { + ImGui::PushItemFlag(ImGuiItemFlags_Disabled, true); + auto buttonColor = ImGui::GetStyleColorVec4(ImGuiCol_Button); + auto buttonHoveredColor = ImGui::GetStyleColorVec4(ImGuiCol_ButtonHovered); + GetButtonColorsForDeviceType(Ship::PhysicalDeviceType::SDLGamepad, buttonColor, buttonHoveredColor); + ImGui::PushStyleColor(ImGuiCol_Button, buttonColor); + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, buttonHoveredColor); + auto notIgnored = !connectedDeviceManager->PortIsIgnoringInstanceId(portIndex, instanceId); + ImGui::PopItemFlag(); + if (ImGui::Checkbox(StringHelper::Sprintf("###instanceId_%d", instanceId).c_str(), ¬Ignored)) { + if (notIgnored) { + connectedDeviceManager->UnignoreInstanceIdForPort(portIndex, instanceId); + } else { + connectedDeviceManager->IgnoreInstanceIdForPort(portIndex, instanceId); + } + }; + ImGui::PushItemFlag(ImGuiItemFlags_Disabled, true); + ImGui::SameLine(); + ImGui::Button(StringHelper::Sprintf("%s %s (SDL)", ICON_FA_GAMEPAD, name.c_str()).c_str()); + ImGui::PopStyleColor(); + ImGui::PopStyleColor(); + ImGui::PopItemFlag(); + } +} + +void GhostshipInputEditorWindow::DrawClearAllButton(uint8_t portIndex) { + if (ImGui::Button("Clear All", ImGui::CalcTextSize("Clear All") * 2)) { + ImGui::OpenPopup("Clear All##clearAllPopup"); + } + if (ImGui::BeginPopupModal("Clear All##clearAllPopup", NULL, ImGuiWindowFlags_AlwaysAutoResize)) { + ImGui::Text("This will clear all mappings for port %d.\n\nContinue?", portIndex + 1); + if (ImGui::Button("Cancel")) { + ImGui::CloseCurrentPopup(); + } + if (ImGui::Button("Clear All")) { + Ship::Context::GetInstance()->GetControlDeck()->GetControllerByPort(portIndex)->ClearAllMappings(); + ImGui::CloseCurrentPopup(); + } + ImGui::EndPopup(); + } +} + +void GhostshipInputEditorWindow::DrawPortTab(uint8_t portIndex) { + if (ImGui::BeginTabItem(StringHelper::Sprintf("Port %d###port%d", portIndex + 1, portIndex).c_str())) { + DrawPortTabContents(portIndex); + ImGui::EndTabItem(); + } +} + +void GhostshipInputEditorWindow::DrawPortTabContents(uint8_t portIndex) { + DrawClearAllButton(portIndex); + DrawSetDefaultsButton(portIndex); + DrawDeviceToggles(portIndex); + + UpdateBitmaskToMappingIds(portIndex); + UpdateStickDirectionToMappingIds(portIndex); + + ImGui::PushStyleColor(ImGuiCol_Header, ImVec4(0.133f, 0.133f, 0.133f, 1.0f)); + ImGui::PushStyleColor(ImGuiCol_HeaderHovered, ImVec4(0.0f, 0.0f, 0.0f, 1.0f)); + ImGui::PushStyleColor(ImGuiCol_HeaderActive, ImVec4(0.0f, 0.0f, 0.0f, 1.0f)); + + if (ImGui::CollapsingHeader("Buttons", NULL, ImGuiTreeNodeFlags_DefaultOpen)) { + DrawButtonLine("A", portIndex, BTN_A, CHIP_COLOR_N64_BLUE); + DrawButtonLine("B", portIndex, BTN_B, CHIP_COLOR_N64_GREEN); + DrawButtonLine("Start", portIndex, BTN_START, CHIP_COLOR_N64_RED); + DrawButtonLine("L", portIndex, BTN_L); + DrawButtonLine("R", portIndex, BTN_R); + DrawButtonLine("Z", portIndex, BTN_Z); + DrawButtonLine(StringHelper::Sprintf("C %s", ICON_FA_ARROW_UP).c_str(), portIndex, BTN_CUP, + CHIP_COLOR_N64_YELLOW); + DrawButtonLine(StringHelper::Sprintf("C %s", ICON_FA_ARROW_DOWN).c_str(), portIndex, BTN_CDOWN, + CHIP_COLOR_N64_YELLOW); + DrawButtonLine(StringHelper::Sprintf("C %s", ICON_FA_ARROW_LEFT).c_str(), portIndex, BTN_CLEFT, + CHIP_COLOR_N64_YELLOW); + DrawButtonLine(StringHelper::Sprintf("C %s", ICON_FA_ARROW_RIGHT).c_str(), portIndex, BTN_CRIGHT, + CHIP_COLOR_N64_YELLOW); + } + + if (ImGui::CollapsingHeader("D-Pad", NULL, ImGuiTreeNodeFlags_DefaultOpen)) { + DrawButtonLine(StringHelper::Sprintf("%s##DPad", ICON_FA_ARROW_UP).c_str(), portIndex, BTN_DUP); + DrawButtonLine(StringHelper::Sprintf("%s##DPad", ICON_FA_ARROW_DOWN).c_str(), portIndex, BTN_DDOWN); + DrawButtonLine(StringHelper::Sprintf("%s##DPad", ICON_FA_ARROW_LEFT).c_str(), portIndex, BTN_DLEFT); + DrawButtonLine(StringHelper::Sprintf("%s##DPad", ICON_FA_ARROW_RIGHT).c_str(), portIndex, BTN_DRIGHT); + } + + if (ImGui::CollapsingHeader("Analog Stick", NULL, ImGuiTreeNodeFlags_DefaultOpen)) { + DrawStickSection(portIndex, Ship::LEFT); + } + + if (ImGui::CollapsingHeader("Additional (\"Right\") Stick")) { + DrawStickSection(portIndex, Ship::RIGHT, CHIP_COLOR_N64_YELLOW); + } + + if (ImGui::CollapsingHeader("Rumble")) { + DrawRumbleSection(portIndex); + } + + if (ImGui::CollapsingHeader("Gyro")) { + DrawGyroSection(portIndex); + } + + if (ImGui::CollapsingHeader("LEDs")) { + DrawLEDSection(portIndex); + } + + ImGui::PopStyleColor(); + ImGui::PopStyleColor(); + ImGui::PopStyleColor(); +} + +void GhostshipInputEditorWindow::DrawSetDefaultsButton(uint8_t portIndex) { + ImGui::SameLine(); + auto popupId = StringHelper::Sprintf("setDefaultsPopup##%d", portIndex); + if (ImGui::Button(StringHelper::Sprintf("Set Defaults##%d", portIndex).c_str(), + ImVec2(ImGui::CalcTextSize("Set Defaults") * 2))) { + ImGui::OpenPopup(popupId.c_str()); + } + + if (ImGui::BeginPopup(popupId.c_str())) { + bool shouldClose = false; + ImGui::PushStyleColor(ImGuiCol_Button, BUTTON_COLOR_KEYBOARD_BEIGE); + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, BUTTON_COLOR_KEYBOARD_BEIGE_HOVERED); + if (ImGui::Button(StringHelper::Sprintf("%s Keyboard", ICON_FA_KEYBOARD_O).c_str())) { + ImGui::OpenPopup("Set Defaults for Keyboard"); + } + ImGui::PopStyleColor(); + ImGui::PopStyleColor(); + if (ImGui::BeginPopupModal("Set Defaults for Keyboard", NULL, ImGuiWindowFlags_AlwaysAutoResize)) { + ImGui::Text("This will clear all existing mappings for\nKeyboard on port %d.\n\nContinue?", portIndex + 1); + if (ImGui::Button("Cancel")) { + shouldClose = true; + ImGui::CloseCurrentPopup(); + } + if (ImGui::Button("Set defaults")) { + Ship::Context::GetInstance() + ->GetControlDeck() + ->GetControllerByPort(portIndex) + ->ClearAllMappingsForDeviceType(Ship::PhysicalDeviceType::Keyboard); + Ship::Context::GetInstance()->GetControlDeck()->GetControllerByPort(portIndex)->AddDefaultMappings( + Ship::PhysicalDeviceType::Keyboard); + shouldClose = true; + ImGui::CloseCurrentPopup(); + } + ImGui::EndPopup(); + } + + auto buttonColor = ImGui::GetStyleColorVec4(ImGuiCol_Button); + auto buttonHoveredColor = ImGui::GetStyleColorVec4(ImGuiCol_ButtonHovered); + GetButtonColorsForDeviceType(Ship::PhysicalDeviceType::SDLGamepad, buttonColor, buttonHoveredColor); + ImGui::PushStyleColor(ImGuiCol_Button, buttonColor); + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, buttonHoveredColor); + if (ImGui::Button(StringHelper::Sprintf("%s %s", ICON_FA_GAMEPAD, "Gamepad (SDL)").c_str())) { + ImGui::OpenPopup("Set Defaults for Gamepad (SDL)"); + } + ImGui::PopStyleColor(); + ImGui::PopStyleColor(); + if (ImGui::BeginPopupModal("Set Defaults for Gamepad (SDL)", NULL, ImGuiWindowFlags_AlwaysAutoResize)) { + ImGui::Text("This will clear all existing mappings for\nGamepad (SDL) on port %d.\n\nContinue?", + portIndex + 1); + if (ImGui::Button("Cancel")) { + shouldClose = true; + ImGui::CloseCurrentPopup(); + } + if (ImGui::Button("Set defaults")) { + Ship::Context::GetInstance() + ->GetControlDeck() + ->GetControllerByPort(portIndex) + ->ClearAllMappingsForDeviceType(Ship::PhysicalDeviceType::SDLGamepad); + Ship::Context::GetInstance()->GetControlDeck()->GetControllerByPort(portIndex)->AddDefaultMappings( + Ship::PhysicalDeviceType::SDLGamepad); + shouldClose = true; + ImGui::CloseCurrentPopup(); + } + ImGui::EndPopup(); + } + + if (ImGui::Button("Cancel") || shouldClose) { + ImGui::CloseCurrentPopup(); + } + + ImGui::EndPopup(); + } +} + +void GhostshipInputEditorWindow::DrawFullContents() { + ImGui::BeginTabBar("##ControllerConfigPortTabs"); + for (uint8_t i = 0; i < 4; i++) { + DrawPortTab(i); + } + ImGui::EndTabBar(); +} + +void GhostshipInputEditorWindow::DrawElement() { + DrawFullContents(); +} + +void GhostshipInputEditorWindow::OffsetMappingPopup() { + const float HORIZONTAL_OFFSET = 10.0f; + ImVec2 pos = ImGui::GetMousePos(); + pos.x += HORIZONTAL_OFFSET; + ImGui::SetNextWindowPos(pos); +} diff --git a/src/port/ui/GhostshipInputEditorWindow.h b/src/port/ui/GhostshipInputEditorWindow.h new file mode 100644 index 00000000..d192ac00 --- /dev/null +++ b/src/port/ui/GhostshipInputEditorWindow.h @@ -0,0 +1,104 @@ +#pragma once +#include +#include +#include +#include +#include +#include +#include "stdint.h" +#include +#include +#include + +#include +#include + +namespace Ship { +class ControllerRumbleMapping; +} +typedef CONTROLLERBUTTONS_T N64ButtonMask; + +typedef struct { + const char* label; + const char* cVarName; + N64ButtonMask defaultBtn; +} CustomButtonMap; + +class GhostshipInputEditorWindow : public Ship::GuiWindow { + public: + using GuiWindow::GuiWindow; + ~GhostshipInputEditorWindow(); + + void DrawButton(const char* label, int32_t n64Btn, int32_t currentPort, int32_t* btnReading); + + void DrawInputChip(const char* buttonName, ImVec4 color); + void DrawAnalogPreview(const char* label, ImVec2 stick, float deadzone = 0, bool gyro = false); + void DrawControllerSchema(); + bool TestingRumble(); + void DrawFullContents(); + void DrawPortTabContents(uint8_t portIndex); + + protected: + void InitElement() override; + void DrawElement() override; + void UpdateElement() override; + + private: + void DrawStickDirectionLine(const char* axisDirectionName, uint8_t port, uint8_t stick, Ship::Direction direction, + ImVec4 color); + void DrawButtonLine(const char* buttonName, uint8_t port, N64ButtonMask bitmask, ImVec4 color); + void DrawButtonLineEditMappingButton(uint8_t port, N64ButtonMask bitmask, std::string id); + void DrawButtonLineAddMappingButton(uint8_t port, N64ButtonMask bitmask); + + void DrawStickDirectionLineEditMappingButton(uint8_t port, uint8_t stick, Ship::Direction direction, + std::string id); + void DrawStickDirectionLineAddMappingButton(uint8_t port, uint8_t stick, Ship::Direction direction); + void DrawStickSection(uint8_t port, uint8_t stick, ImVec4 color); + + void DrawRumbleSection(uint8_t port); + void DrawRemoveRumbleMappingButton(uint8_t port, std::string id); + void DrawAddRumbleMappingButton(uint8_t port); + + void DrawLEDSection(uint8_t port); + void DrawRemoveLEDMappingButton(uint8_t port, std::string id); + void DrawAddLEDMappingButton(uint8_t port); + + void DrawGyroSection(uint8_t port); + void DrawRemoveGyroMappingButton(uint8_t port, std::string id); + void DrawAddGyroMappingButton(uint8_t port); + + // Used together for an incomplete linked hash map implementation in order to + // map button masks to their names and original mapping on N64 + std::list> buttons; + std::unordered_map buttonNames; + void addButtonName(N64ButtonMask mask, const char* name); + void DrawMapping(CustomButtonMap& mapping, float labelWidth, N64ButtonMask excludedButtons); + + int32_t mGameInputBlockTimer; + int32_t mMappingInputBlockTimer; + int32_t mRumbleTimer; + std::shared_ptr mRumbleMappingToTest; + + // mBitmaskToMappingIds[port][bitmask] = { id0, id1, ... } + std::unordered_map>> mBitmaskToMappingIds; + + // mStickDirectionToMappingIds[port][stick][direction] = { id0, id1, ... } + std::unordered_map>>> + mStickDirectionToMappingIds; + + void UpdateBitmaskToMappingIds(uint8_t port); + void UpdateStickDirectionToMappingIds(uint8_t port); + + void GetButtonColorsForDeviceType(Ship::PhysicalDeviceType lusIndex, ImVec4& buttonColor, + ImVec4& buttonHoveredColor); + void DrawPortTab(uint8_t portIndex); + std::set mButtonsBitmasks; + std::set mDpadBitmasks; + bool mInputEditorPopupOpen; + void DrawSetDefaultsButton(uint8_t portIndex); + void DrawClearAllButton(uint8_t portIndex); + void OffsetMappingPopup(); + + void DrawDeviceToggles(uint8_t portIndex); +}; diff --git a/src/port/ui/GhostshipMenu.cpp b/src/port/ui/GhostshipMenu.cpp new file mode 100644 index 00000000..45a5b77c --- /dev/null +++ b/src/port/ui/GhostshipMenu.cpp @@ -0,0 +1,215 @@ +#include "GhostshipMenu.h" +#include "GhostshipInputEditorWindow.h" +#include +#include +#include +#include +#include +#include + +static const std::unordered_map menuThemeOptions = { + { UIWidgets::Colors::Red, "Red" }, + { UIWidgets::Colors::DarkRed, "Dark Red" }, + { UIWidgets::Colors::Orange, "Orange" }, + { UIWidgets::Colors::Green, "Green" }, + { UIWidgets::Colors::DarkGreen, "Dark Green" }, + { UIWidgets::Colors::LightBlue, "Light Blue" }, + { UIWidgets::Colors::Blue, "Blue" }, + { UIWidgets::Colors::DarkBlue, "Dark Blue" }, + { UIWidgets::Colors::Indigo, "Indigo" }, + { UIWidgets::Colors::Violet, "Violet" }, + { UIWidgets::Colors::Purple, "Purple" }, + { UIWidgets::Colors::Brown, "Brown" }, + { UIWidgets::Colors::Gray, "Gray" }, + { UIWidgets::Colors::DarkGray, "Dark Gray" }, +}; + +static const std::vector textureFilteringOptions = { + "Three-Point", // Fast::FILTER_THREE_POINT, + "Linear", // Fast::FILTER_LINEAR + "None", // Fast::FILTER_NONE +}; + +#ifdef _DEBUG +DebugLogOption defaultLogLevel = DEBUG_LOG_TRACE; +#else +DebugLogOption defaultLogLevel = DEBUG_LOG_INFO; +#endif + +static const std::vector logLevels = { + "Trace", // DEBUG_LOG_TRACE + "Debug", // DEBUG_LOG_DEBUG + "Info", // DEBUG_LOG_INFO + "Warn", // DEBUG_LOG_WARN + "Error", // DEBUG_LOG_ERROR + "Critical", // DEBUG_LOG_CRITICAL + "Off", // DEBUG_LOG_OFF +}; + +extern std::unordered_map warpPointSceneList; + +namespace GhostshipGui { +extern std::shared_ptr mGhostshipMenu; + +using namespace UIWidgets; + +void GhostshipMenu::AddSidebarEntry(std::string sectionName, std::string sidebarName, uint32_t columnCount) { + assert(!sectionName.empty()); + assert(!sidebarName.empty()); + menuEntries.at(sectionName).sidebars.emplace(sidebarName, SidebarEntry{ .columnCount = columnCount }); + menuEntries.at(sectionName).sidebarOrder.push_back(sidebarName); +} + +WidgetInfo& GhostshipMenu::AddWidget(WidgetPath& pathInfo, std::string widgetName, WidgetType widgetType) { + assert(!widgetName.empty()); // Must be unique + assert(menuEntries.contains(pathInfo.sectionName)); // Section/header must already exist + assert(menuEntries.at(pathInfo.sectionName).sidebars.contains(pathInfo.sidebarName)); // Sidebar must already exist + std::unordered_map& sidebar = menuEntries.at(pathInfo.sectionName).sidebars; + uint8_t column = pathInfo.column; + if (sidebar.contains(pathInfo.sidebarName)) { + while (sidebar.at(pathInfo.sidebarName).columnWidgets.size() < column + 1) { + sidebar.at(pathInfo.sidebarName).columnWidgets.push_back({}); + } + } + SidebarEntry& entry = sidebar.at(pathInfo.sidebarName); + entry.columnWidgets.at(column).push_back({ .name = widgetName, .type = widgetType }); + WidgetInfo& widget = entry.columnWidgets.at(column).back(); + switch (widgetType) { + case WIDGET_CHECKBOX: + case WIDGET_CVAR_CHECKBOX: + widget.options = std::make_shared(); + break; + case WIDGET_SLIDER_FLOAT: + case WIDGET_CVAR_SLIDER_FLOAT: + widget.options = std::make_shared(); + break; + case WIDGET_SLIDER_INT: + case WIDGET_CVAR_SLIDER_INT: + widget.options = std::make_shared(); + break; + case WIDGET_COMBOBOX: + case WIDGET_CVAR_COMBOBOX: + case WIDGET_AUDIO_BACKEND: + case WIDGET_VIDEO_BACKEND: + widget.options = std::make_shared(); + break; + case WIDGET_BUTTON: + widget.options = std::make_shared(); + break; + case WIDGET_WINDOW_BUTTON: + widget.options = std::make_shared(); + break; + case WIDGET_CVAR_COLOR_PICKER: + case WIDGET_COLOR_PICKER: + widget.options = std::make_shared(); + break; + case WIDGET_SEPARATOR_TEXT: + case WIDGET_TEXT: + widget.options = std::make_shared(); + break; + case WIDGET_SEARCH: + case WIDGET_SEPARATOR: + default: + widget.options = std::make_shared(); + } + return widget; +} + +GhostshipMenu::GhostshipMenu(const std::string& consoleVariable, const std::string& name) + : Menu(consoleVariable, name, 0, UIWidgets::Colors::LightBlue) { +} + +void GhostshipMenu::InitElement() { + Ship::Menu::InitElement(); + AddMenuSettings(); + AddMenuEnhancements(); + AddMenuDevTools(); + + if (CVarGetInteger(CVAR_SETTING("Menu.SidebarSearch"), 0)) { + InsertSidebarSearch(); + } + + for (auto& initFunc : MenuInit::GetInitFuncs()) { + initFunc(); + } + + disabledMap = { + { DISABLE_FOR_NO_VSYNC, + { [](disabledInfo& info) -> bool { + return !Ship::Context::GetInstance()->GetWindow()->CanDisableVerticalSync(); + }, + "Disabling VSync not supported" } }, + { DISABLE_FOR_NO_WINDOWED_FULLSCREEN, + { [](disabledInfo& info) -> bool { + return !Ship::Context::GetInstance()->GetWindow()->SupportsWindowedFullscreen(); + }, + "Windowed Fullscreen not supported" } }, + { DISABLE_FOR_NO_MULTI_VIEWPORT, + { [](disabledInfo& info) -> bool { + return !Ship::Context::GetInstance()->GetWindow()->GetGui()->SupportsViewports(); + }, + "Multi-viewports not supported" } }, + { DISABLE_FOR_NOT_DIRECTX, + { [](disabledInfo& info) -> bool { + return Ship::Context::GetInstance()->GetWindow()->GetWindowBackend() != + Ship::WindowBackend::FAST3D_DXGI_DX11; + }, + "Available Only on DirectX" } }, + { DISABLE_FOR_DIRECTX, + { [](disabledInfo& info) -> bool { + return Ship::Context::GetInstance()->GetWindow()->GetWindowBackend() == + Ship::WindowBackend::FAST3D_DXGI_DX11; + }, + "Not Available on DirectX" } }, + { DISABLE_FOR_MATCH_REFRESH_RATE_ON, + { [](disabledInfo& info) -> bool { return CVarGetInteger(CVAR_SETTING("MatchRefreshRate"), 0); }, + "Match Refresh Rate is Enabled" } }, + { DISABLE_FOR_ADVANCED_RESOLUTION_ON, + { [](disabledInfo& info) -> bool { return CVarGetInteger(CVAR_PREFIX_ADVANCED_RESOLUTION ".Enabled", 0); }, + "Advanced Resolution Enabled" } }, + { DISABLE_FOR_VERTICAL_RES_TOGGLE_ON, + { [](disabledInfo& info) -> bool { + return CVarGetInteger(CVAR_PREFIX_ADVANCED_RESOLUTION ".VerticalResolutionToggle", 0); + }, + "Vertical Resolution Toggle Enabled" } }, + { DISABLE_FOR_LOW_RES_MODE_ON, + { [](disabledInfo& info) -> bool { return CVarGetInteger(CVAR_LOW_RES_MODE, 0); }, "N64 Mode Enabled" } }, + { DISABLE_FOR_NULL_PLAY_STATE, + { [](disabledInfo& info) -> bool { return /*gPlayState == NULL*/ "TODO"; }, "Save Not Loaded" } }, + { DISABLE_FOR_DEBUG_MODE_OFF, + { [](disabledInfo& info) -> bool { return !CVarGetInteger(CVAR_DEVELOPER_TOOLS("DebugEnabled"), 0); }, + "Debug Mode is Disabled" } }, + { DISABLE_FOR_FRAME_ADVANCE_OFF, + { [](disabledInfo& info) -> bool { + return /*!(gPlayState != nullptr && gPlayState->frameAdvCtx.enabled)*/ "TODO"; + }, + "Frame Advance is Disabled" } }, + { DISABLE_FOR_ADVANCED_RESOLUTION_OFF, + { [](disabledInfo& info) -> bool { return !CVarGetInteger(CVAR_PREFIX_ADVANCED_RESOLUTION ".Enabled", 0); }, + "Advanced Resolution is Disabled" } }, + { DISABLE_FOR_VERTICAL_RESOLUTION_OFF, + { [](disabledInfo& info) -> bool { + return !CVarGetInteger(CVAR_PREFIX_ADVANCED_RESOLUTION ".VerticalResolutionToggle", 0); + }, + "Vertical Resolution Toggle is Off" } }, + { DISABLE_FOR_BOOT_TO_DEBUG_WARP_SCREEN_ON, + { [](disabledInfo& info) -> bool { + return CVarGetInteger(CVAR_DEVELOPER_TOOLS("DebugEnabled"), 0) && + CVarGetInteger(CVAR_DEVELOPER_TOOLS("BootToDebugWarpScreen"), 0); + }, + "\"Boot To Debug Warp Screen\" Enabled (see Dev Tools -> General)" } }, + }; +} + +void GhostshipMenu::UpdateElement() { + Ship::Menu::UpdateElement(); +} + +void GhostshipMenu::Draw() { + Ship::Menu::Draw(); +} + +void GhostshipMenu::DrawElement() { + Ship::Menu::DrawElement(); +} +} // namespace GhostshipGui diff --git a/src/port/ui/GhostshipMenu.h b/src/port/ui/GhostshipMenu.h new file mode 100644 index 00000000..b4580a1c --- /dev/null +++ b/src/port/ui/GhostshipMenu.h @@ -0,0 +1,30 @@ +#pragma once + +#include +#include "UIWidgets.hpp" +#include "Menu.h" +#include +#include "port/ui/cvar_prefixes.h" +#include "port/ui/enhancementTypes.h" + +namespace GhostshipGui { +class GhostshipMenu : public Ship::Menu { + public: + GhostshipMenu(const std::string& consoleVariable, const std::string& name); + + void InitElement() override; + void DrawElement() override; + void UpdateElement() override; + void Draw() override; + + void AddSidebarEntry(std::string sectionName, std::string sidbarName, uint32_t columnCount); + WidgetInfo& AddWidget(WidgetPath& pathInfo, std::string widgetName, WidgetType widgetType); + void AddMenuSettings(); + void AddMenuEnhancements(); + void AddMenuDevTools(); + + private: + char mGitCommitHashTruncated[8]; + bool mIsTaggedVersion; +}; +} // namespace GhostshipGui diff --git a/src/port/ui/GhostshipMenuDevTools.cpp b/src/port/ui/GhostshipMenuDevTools.cpp new file mode 100644 index 00000000..4c51b476 --- /dev/null +++ b/src/port/ui/GhostshipMenuDevTools.cpp @@ -0,0 +1,90 @@ +#include "GhostshipMenu.h" + +namespace GhostshipGui { + +extern std::shared_ptr mGhostshipMenu; +using namespace UIWidgets; + +static const std::unordered_map logLevels = { + { DEBUG_LOG_TRACE, "Trace" }, { DEBUG_LOG_DEBUG, "Debug" }, { DEBUG_LOG_INFO, "Info" }, + { DEBUG_LOG_WARN, "Warn" }, { DEBUG_LOG_ERROR, "Error" }, { DEBUG_LOG_CRITICAL, "Critical" }, + { DEBUG_LOG_OFF, "Off" }, +}; + +static const std::unordered_map debugInfoPages = { + { DEBUG_PAGE_OBJECTINFO, "Object" }, { DEBUG_PAGE_CHECKSURFACEINFO, "Check Surface" }, + { DEBUG_PAGE_MAPINFO, "Map" }, { DEBUG_PAGE_STAGEINFO, "Stage" }, + { DEBUG_PAGE_EFFECTINFO, "Effect" }, { DEBUG_PAGE_ENEMYINFO, "Enemy" }, +}; + +#ifdef _DEBUG +DebugLogOption defaultLogLevel = DEBUG_LOG_TRACE; +#else +DebugLogOption defaultLogLevel = DEBUG_LOG_INFO; +#endif + +void GhostshipMenu::AddMenuDevTools() { + // Add Dev Tools Menu + AddMenuEntry("Dev Tools", CVAR_SETTING("Menu.DevToolsSidebarSection")); + + // General + AddSidebarEntry("Dev Tools", "General", 3); + WidgetPath path = { "Dev Tools", "General", SECTION_COLUMN_1 }; + + AddWidget(path, "Popout Menu", WIDGET_CVAR_CHECKBOX) + .CVar(CVAR_SETTING("Menu.Popout")) + .Options(CheckboxOptions().Tooltip("Changes the menu display from overlay to windowed.")); + AddWidget(path, "Log Level", WIDGET_CVAR_COMBOBOX) + .CVar(CVAR_DEVELOPER_TOOLS("LogLevel")) + .Options(ComboboxOptions() + .Tooltip("The log level determines which messages are printed to the console." + " This does not affect the log file output") + .ComboMap(logLevels) + .DefaultIndex(defaultLogLevel)) + .Callback([](WidgetInfo &info) { + Ship::Context::GetInstance()->GetLogger()->set_level( + (spdlog::level::level_enum) CVarGetInteger(CVAR_DEVELOPER_TOOLS("LogLevel"), + defaultLogLevel)); + }) + .PreFunc([](WidgetInfo &info) { + info.isHidden = mGhostshipMenu->disabledMap.at(DISABLE_FOR_DEBUG_MODE_OFF).active; + }); +#ifdef USE_GBI_TRACE + AddWidget(path, "GFX Trace Mode", WIDGET_CVAR_CHECKBOX) + .CVar(CVAR_DEVELOPER_TOOLS("GFXTrace")) + .Options(CheckboxOptions().Tooltip("Enables the Gfx trace mode, which will output information about the Gfx commands being run.")); +#endif + AddWidget(path, "Debug Mode", WIDGET_CVAR_CHECKBOX) + .CVar(CVAR_DEVELOPER_TOOLS("DebugMode")) + .Options(CheckboxOptions().Tooltip("Various debug features, including a level selector from the main menu.")); + AddWidget(path, "Better Level Select", WIDGET_CVAR_CHECKBOX) + .CVar(CVAR_DEVELOPER_TOOLS("BetterLevelSelect")) + .Options(CheckboxOptions().Tooltip("Tweaks to the level select screen, like naming and allowing C-buttons to be used.")) + .PreFunc([](WidgetInfo &info) { + info.options->Disabled(!CVarGetInteger(CVAR_DEVELOPER_TOOLS("DebugMode"), 0)); + }); + AddWidget(path, "Draw Debug Info", WIDGET_CVAR_CHECKBOX) + .CVar(CVAR_DEVELOPER_TOOLS("DrawDebugInfo")) + .Options(CheckboxOptions().Tooltip("Draws Debug Related Information")); + AddWidget(path, "Debug Info Mode", WIDGET_CVAR_COMBOBOX) + .CVar(CVAR_DEVELOPER_TOOLS("DebugInfoPage")) + .Options(ComboboxOptions() + .Tooltip("Select Debug Page") + .ComboMap(debugInfoPages) + .DefaultIndex(DEBUG_PAGE_OBJECTINFO)) + .PreFunc([](WidgetInfo &info) { + info.options->Disabled(!CVarGetInteger(CVAR_DEVELOPER_TOOLS("DrawDebugInfo"), 0)); + }); + + // Save Editor + path.sidebarName = "Save Editor"; + AddSidebarEntry("Dev Tools", path.sidebarName, 1); + AddWidget(path, "Popout Save Editor", WIDGET_WINDOW_BUTTON) + .CVar(CVAR_WINDOW("SaveEditor")) + .WindowName("Save Editor") + .HideInSearch(true) + .Options(WindowButtonOptions().Tooltip("Enables the separate Save Editor Window.")); + +} + +} // namespace GhostshipGui diff --git a/src/port/ui/GhostshipMenuEnhancements.cpp b/src/port/ui/GhostshipMenuEnhancements.cpp new file mode 100644 index 00000000..a74e534a --- /dev/null +++ b/src/port/ui/GhostshipMenuEnhancements.cpp @@ -0,0 +1,69 @@ +#include "GhostshipMenu.h" + +#define CVAR_INT_SHIP_INIT(cvar, val) \ + CVarSetInteger(cvar, val); \ + ShipInit::Init(cvar); + +static std::string comboboxTooltip = ""; + +namespace GhostshipGui { + +extern std::shared_ptr mGhostshipMenu; +using namespace UIWidgets; + +void GhostshipMenu::AddMenuEnhancements() { + // Add Enhancements Menu + AddMenuEntry("Enhancements", CVAR_SETTING("Menu.EnhancementsSidebarSection")); + + // Quality of Life + WidgetPath path = { "Enhancements", "Gameplay", SECTION_COLUMN_1 }; + AddSidebarEntry("Enhancements", path.sidebarName, 1); + path.column = SECTION_COLUMN_1; + + AddWidget(path, "Disable LoD", WIDGET_CVAR_CHECKBOX) + .CVar(CVAR_ENHANCEMENT("DisableLOD")) + .RaceDisable(false) + .Options(CheckboxOptions().Tooltip("Disable Level of Detail (LOD) to avoid models using " + "lower poly versions at a distance")); + AddWidget(path, "Select Any Star", WIDGET_CVAR_CHECKBOX) + .CVar(CVAR_ENHANCEMENT("SelectAllStars")) + .RaceDisable(false) + .Options(CheckboxOptions().Tooltip("Let's you select any star from the menu regardless " + "of the courses completion status.")); + AddWidget(path, "Collecting Stars Will Not Exit Level", WIDGET_CVAR_CHECKBOX) + .CVar(CVAR_ENHANCEMENT("StarNoExit")) + .RaceDisable(false) + .Options(CheckboxOptions().Tooltip("Collecting Stars will not take you out of the level.")); + AddWidget(path, "Skip Intro Peach Cutscene", WIDGET_CVAR_CHECKBOX) + .CVar(CVAR_ENHANCEMENT("DisablePeachCutscene")) + .RaceDisable(false) + .Options(CheckboxOptions().Tooltip("Skips the Peach cutscene when starting a new game.")); + + path = { "Enhancements", "Fixes", SECTION_COLUMN_1 }; + AddSidebarEntry("Enhancements", path.sidebarName, 1); + path.column = SECTION_COLUMN_1; + + AddWidget(path, "Fix Koopa Race Music", WIDGET_CVAR_CHECKBOX) + .CVar(CVAR_ENHANCEMENT("FixKoopaRaceMusic")) + .RaceDisable(false) + .Options(CheckboxOptions().Tooltip("Fixes the Koopa race music on Bob-omb Battlefield and Tiny-Huge Island.")); + + path = { "Enhancements", "Cheats", SECTION_COLUMN_1 }; + AddSidebarEntry("Enhancements", path.sidebarName, 1); + path.column = SECTION_COLUMN_1; + + AddWidget(path, "Infinite Health", WIDGET_CVAR_CHECKBOX) + .CVar(CVAR_CHEAT("InfiniteHealth")) + .RaceDisable(false) + .Options(CheckboxOptions().Tooltip("Impervious to Damage.")); + AddWidget(path, "Infinite Lives", WIDGET_CVAR_CHECKBOX) + .CVar(CVAR_CHEAT("InfiniteLives")) + .RaceDisable(false) + .Options(CheckboxOptions().Tooltip("Never run of out Lives.")); + AddWidget(path, "Pause Exit Whenever", WIDGET_CVAR_CHECKBOX) + .CVar(CVAR_CHEAT("PauseExitWhenever")) + .RaceDisable(false) + .Options(CheckboxOptions().Tooltip("Exit from anywhere using the Pause Menu.")); +} + +} // namespace GhostshipGui diff --git a/src/port/ui/GhostshipMenuSettings.cpp b/src/port/ui/GhostshipMenuSettings.cpp new file mode 100644 index 00000000..ab213f75 --- /dev/null +++ b/src/port/ui/GhostshipMenuSettings.cpp @@ -0,0 +1,381 @@ +#include "GhostshipMenu.h" +#include "port/Engine.h" +#include "Notification.h" +#include "GhostshipInputEditorWindow.h" +#include "GhostshipModals.h" +#include "UIWidgets.hpp" +#include + +extern "C" { +#include "variables.h" +#include "audio/external.h" +} + +namespace GhostshipGui { + +extern std::shared_ptr mGhostshipMenu; +extern std::shared_ptr mModalWindow; +using namespace UIWidgets; + +static std::unordered_map imguiScaleOptions = { + { 0, "Small" }, + { 1, "Normal" }, + { 2, "Large" }, + { 3, "X-Large" }, +}; + +static const std::unordered_map menuThemeOptions = { + { UIWidgets::Colors::Red, "Red" }, + { UIWidgets::Colors::DarkRed, "Dark Red" }, + { UIWidgets::Colors::Orange, "Orange" }, + { UIWidgets::Colors::Green, "Green" }, + { UIWidgets::Colors::DarkGreen, "Dark Green" }, + { UIWidgets::Colors::LightBlue, "Light Blue" }, + { UIWidgets::Colors::Blue, "Blue" }, + { UIWidgets::Colors::DarkBlue, "Dark Blue" }, + { UIWidgets::Colors::Indigo, "Indigo" }, + { UIWidgets::Colors::Violet, "Violet" }, + { UIWidgets::Colors::Purple, "Purple" }, + { UIWidgets::Colors::Brown, "Brown" }, + { UIWidgets::Colors::Gray, "Gray" }, + { UIWidgets::Colors::DarkGray, "Dark Gray" }, +}; + +static const std::unordered_map textureFilteringMap = { + { Fast::FILTER_THREE_POINT, "Three-Point" }, + { Fast::FILTER_LINEAR, "Linear" }, + { Fast::FILTER_NONE, "None" }, +}; + +static const std::unordered_map notificationPosition = { + { 0, "Top Left" }, { 1, "Top Right" }, { 2, "Bottom Left" }, { 3, "Bottom Right" }, { 4, "Hidden" }, +}; + +void GhostshipMenu::AddMenuSettings() { + // Add Settings Menu + AddMenuEntry("Settings", CVAR_SETTING("Menu.SettingsSidebarSection")); + AddSidebarEntry("Settings", "General", 2); + WidgetPath path = { "Settings", "General", SECTION_COLUMN_1 }; + + // General - Settings + AddWidget(path, "Menu Settings", WIDGET_SEPARATOR_TEXT); + AddWidget(path, "Menu Theme", WIDGET_CVAR_COMBOBOX) + .CVar(CVAR_SETTING("Menu.Theme")) + .RaceDisable(false) + .Options(ComboboxOptions() + .Tooltip("Changes the Theme of the Menu Widgets.") + .ComboMap(menuThemeOptions) + .DefaultIndex(Colors::LightBlue)); +#if not defined(__SWITCH__) and not defined(__WIIU__) + AddWidget(path, "Menu Controller Navigation", WIDGET_CVAR_CHECKBOX) + .CVar(CVAR_IMGUI_CONTROLLER_NAV) + .RaceDisable(false) + .Options(CheckboxOptions().Tooltip( + "Allows controller navigation of the port menu (Settings, Enhancements,...)\nCAUTION: " + "This will disable game inputs while the menu is visible.\n\nD-pad to move between " + "items, A to select, B to move up in scope.")); + AddWidget(path, "Menu Background Opacity", WIDGET_CVAR_SLIDER_FLOAT) + .CVar(CVAR_SETTING("Menu.BackgroundOpacity")) + .RaceDisable(false) + .Options(FloatSliderOptions().DefaultValue(0.85f).IsPercentage().Tooltip( + "Sets the opacity of the background of the port menu.")); + + AddWidget(path, "General Settings", WIDGET_SEPARATOR_TEXT); + AddWidget(path, "Cursor Always Visible", WIDGET_CVAR_CHECKBOX) + .CVar(CVAR_SETTING("CursorVisibility")) + .RaceDisable(false) + .Callback([](WidgetInfo& info) { + Ship::Context::GetInstance()->GetWindow()->SetForceCursorVisibility( + CVarGetInteger(CVAR_SETTING("CursorVisibility"), 0)); + }) + .Options(CheckboxOptions().Tooltip("Makes the cursor always visible, even in full screen.")); +#endif + AddWidget(path, "Search In Sidebar", WIDGET_CVAR_CHECKBOX) + .CVar(CVAR_SETTING("Menu.SidebarSearch")) + .RaceDisable(false) + .Callback([](WidgetInfo& info) { + if (CVarGetInteger(CVAR_SETTING("Menu.SidebarSearch"), 0)) { + mGhostshipMenu->InsertSidebarSearch(); + } else { + mGhostshipMenu->RemoveSidebarSearch(); + } + }) + .Options(CheckboxOptions().Tooltip( + "Displays the Search menu as a sidebar entry in Settings instead of in the header.")); + AddWidget(path, "Search Input Autofocus", WIDGET_CVAR_CHECKBOX) + .CVar(CVAR_SETTING("Menu.SearchAutofocus")) + .RaceDisable(false) + .Options(CheckboxOptions().Tooltip( + "Search input box gets autofocus when visible. Does not affect using other widgets.")); + AddWidget(path, "Open App Files Folder", WIDGET_BUTTON) + .RaceDisable(false) + .Callback([](WidgetInfo& info) { + std::string filesPath = Ship::Context::GetInstance()->GetAppDirectoryPath(); + SDL_OpenURL(std::string("file:///" + std::filesystem::absolute(filesPath).string()).c_str()); + }) + .Options(ButtonOptions().Tooltip("Opens the folder that contains the save and mods folders, etc.")); + AddWidget(path, "EXPERIMENTAL", WIDGET_SEPARATOR_TEXT).Options(TextOptions().Color(Colors::Orange)); + AddWidget(path, "ImGui Menu Scaling", WIDGET_CVAR_COMBOBOX) + .CVar(CVAR_SETTING("ImGuiScale")) + .RaceDisable(false) + .Options(ComboboxOptions() + .ComboMap(imguiScaleOptions) + .Tooltip("Changes the scaling of the ImGui menu elements.") + .DefaultIndex(1) + .ComponentAlignment(ComponentAlignments::Right) + .LabelPosition(LabelPositions::Far)) + .Callback([](WidgetInfo& info) { GameEngine::Instance->ScaleImGui(); }); + + // General - About + path.column = SECTION_COLUMN_2; + + AddWidget(path, "About", WIDGET_SEPARATOR_TEXT); + AddWidget(path, "Ghostship", WIDGET_TEXT); + if (gGitCommitTag[0] != 0) { + AddWidget(path, gBuildVersion, WIDGET_TEXT); + } else { + AddWidget(path, ("Branch: " + std::string(gGitBranch)), WIDGET_TEXT); + AddWidget(path, ("Commit: " + std::string(gGitCommitHash)), WIDGET_TEXT); + } + // for (uint32_t i = 0; i < ResourceMgr_GetNumGameVersions(); i++) { + // AddWidget(path, GetGameVersionString(i), WIDGET_TEXT); + // } + + // Audio Settings + path.sidebarName = "Audio"; + path.column = SECTION_COLUMN_1; + AddSidebarEntry("Settings", "Audio", 3); + + AddWidget(path, "Master Volume: %d %%", WIDGET_CVAR_SLIDER_INT) + .CVar(CVAR_SETTING("Volume.Master")) + .RaceDisable(false) + .Options(IntSliderOptions().Min(0).Max(100).DefaultValue(50).ShowButtons(true).Format("")); + AddWidget(path, "Main Music Volume: %d %%", WIDGET_CVAR_SLIDER_INT) + .CVar(CVAR_SETTING("Volume.MainMusic")) + .RaceDisable(false) + .Options(IntSliderOptions().Min(0).Max(100).DefaultValue(100).ShowButtons(true).Format("")) + .Callback([](WidgetInfo& info) { + audio_set_player_volume(SEQ_PLAYER_LEVEL, + ((float)CVarGetInteger(CVAR_SETTING("Volume.MainMusic"), 100) / 100.0f)); + }); + AddWidget(path, "Environment Volume: %d %%", WIDGET_CVAR_SLIDER_INT) + .CVar(CVAR_SETTING("Volume.Environment")) + .RaceDisable(false) + .Options(IntSliderOptions().Min(0).Max(100).DefaultValue(100).ShowButtons(true).Format("")) + .Callback([](WidgetInfo& info) { + audio_set_player_volume(SEQ_PLAYER_ENV, + ((float)CVarGetInteger(CVAR_SETTING("Volume.Environment"), 100) / 100.0f)); + }); + AddWidget(path, "Sound Effects Volume: %d %%", WIDGET_CVAR_SLIDER_INT) + .CVar(CVAR_SETTING("Volume.SFX")) + .RaceDisable(false) + .Options(IntSliderOptions().Min(0).Max(100).DefaultValue(100).ShowButtons(true).Format("")) + .Callback([](WidgetInfo& info) { + audio_set_player_volume(SEQ_PLAYER_SFX, + ((float) CVarGetInteger(CVAR_SETTING("Volume.SFX"), 100) / 100.0f)); + }); + AddWidget(path, "Audio API (Needs reload)", WIDGET_AUDIO_BACKEND).RaceDisable(false); + + // Graphics Settings + static int32_t maxFps = 360; + const char* tooltip = "Uses Matrix Interpolation to create extra frames, resulting in smoother graphics. This is " + "purely visual and does not impact game logic, execution of glitches etc.\n\nA higher target " + "FPS than your monitor's refresh rate will waste resources, and might give a worse result."; + path.sidebarName = "Graphics"; + AddSidebarEntry("Settings", "Graphics", 3); + AddWidget(path, "Graphics Options", WIDGET_SEPARATOR_TEXT); + AddWidget(path, "Toggle Fullscreen", WIDGET_BUTTON) + .RaceDisable(false) + .Callback([](WidgetInfo& info) { Ship::Context::GetInstance()->GetWindow()->ToggleFullscreen(); }) + .Options(ButtonOptions().Tooltip("Toggles Fullscreen On/Off.")); + AddWidget(path, "Internal Resolution", WIDGET_CVAR_SLIDER_FLOAT) + .CVar(CVAR_INTERNAL_RESOLUTION) + .RaceDisable(false) + .Callback([](WidgetInfo& info) { + Ship::Context::GetInstance()->GetWindow()->SetResolutionMultiplier( + CVarGetFloat(CVAR_INTERNAL_RESOLUTION, 1)); + }) + .PreFunc([](WidgetInfo& info) { + if (mGhostshipMenu->disabledMap.at(DISABLE_FOR_ADVANCED_RESOLUTION_ON).active && + mGhostshipMenu->disabledMap.at(DISABLE_FOR_VERTICAL_RES_TOGGLE_ON).active) { + info.activeDisables.push_back(DISABLE_FOR_ADVANCED_RESOLUTION_ON); + info.activeDisables.push_back(DISABLE_FOR_VERTICAL_RES_TOGGLE_ON); + } else if (mGhostshipMenu->disabledMap.at(DISABLE_FOR_LOW_RES_MODE_ON).active) { + info.activeDisables.push_back(DISABLE_FOR_LOW_RES_MODE_ON); + } + }) + .Options( + FloatSliderOptions() + .Tooltip("Multiplies your output resolution by the value inputted, as a more intensive but effective " + "form of anti-aliasing.") + .ShowButtons(false) + .IsPercentage() + .Min(0.5f) + .Max(2.0f)); +#ifndef __WIIU__ + AddWidget(path, "Anti-aliasing (MSAA)", WIDGET_CVAR_SLIDER_INT) + .CVar(CVAR_MSAA_VALUE) + .RaceDisable(false) + .Callback([](WidgetInfo& info) { + Ship::Context::GetInstance()->GetWindow()->SetMsaaLevel(CVarGetInteger(CVAR_MSAA_VALUE, 1)); + }) + .Options( + IntSliderOptions() + .Tooltip("Activates MSAA (multi-sample anti-aliasing) from 2x up to 8x, to smooth the edges of " + "rendered geometry.\n" + "Higher sample count will result in smoother edges on models, but may reduce performance.") + .Min(1) + .Max(8) + .DefaultValue(1)); +#endif + auto fps = CVarGetInteger(CVAR_SETTING("InterpolationFPS"), 20); + const char* fpsFormat = fps == 20 ? "Original (%d)" : "%d"; + AddWidget(path, "Current FPS", WIDGET_CVAR_SLIDER_INT) + .CVar(CVAR_SETTING("InterpolationFPS")) + .RaceDisable(false) + .Callback([](WidgetInfo& info) { + auto options = std::static_pointer_cast(info.options); + int32_t defaultValue = options->defaultValue; + if (CVarGetInteger(info.cVar, defaultValue) == defaultValue) { + options->format = "Original (%d)"; + } else { + options->format = "%d"; + } + }) + .PreFunc([](WidgetInfo& info) { + if (mGhostshipMenu->disabledMap.at(DISABLE_FOR_MATCH_REFRESH_RATE_ON).active) + info.activeDisables.push_back(DISABLE_FOR_MATCH_REFRESH_RATE_ON); + }) + .Options(IntSliderOptions().Tooltip(tooltip).Min(20).Max(maxFps).DefaultValue(20).Format(fpsFormat)); + AddWidget(path, "Match Refresh Rate", WIDGET_CVAR_CHECKBOX) + .CVar(CVAR_SETTING("MatchRefreshRate")) + .RaceDisable(false) + .Options(CheckboxOptions().Tooltip("Matches interpolation value to the refresh rate of your display.")); + AddWidget(path, "Renderer API (Needs reload)", WIDGET_VIDEO_BACKEND).RaceDisable(false); + AddWidget(path, "Enable Vsync", WIDGET_CVAR_CHECKBOX) + .CVar(CVAR_VSYNC_ENABLED) + .RaceDisable(false) + .PreFunc([](WidgetInfo& info) { info.isHidden = mGhostshipMenu->disabledMap.at(DISABLE_FOR_NO_VSYNC).active; }) + .Options(CheckboxOptions() + .Tooltip("Removes tearing, but clamps your max FPS to your displays refresh rate.") + .DefaultValue(true)); + AddWidget(path, "Windowed Fullscreen", WIDGET_CVAR_CHECKBOX) + .CVar(CVAR_SDL_WINDOWED_FULLSCREEN) + .RaceDisable(false) + .PreFunc([](WidgetInfo& info) { + info.isHidden = mGhostshipMenu->disabledMap.at(DISABLE_FOR_NO_WINDOWED_FULLSCREEN).active; + }) + .Options(CheckboxOptions().Tooltip("Enables Windowed Fullscreen Mode.")); + AddWidget(path, "Allow multi-windows", WIDGET_CVAR_CHECKBOX) + .CVar(CVAR_ENABLE_MULTI_VIEWPORTS) + .RaceDisable(false) + .PreFunc([](WidgetInfo& info) { + info.isHidden = mGhostshipMenu->disabledMap.at(DISABLE_FOR_NO_MULTI_VIEWPORT).active; + }) + .Options(CheckboxOptions() + .Tooltip("Allows multiple windows to be opened at once. Requires a reload to take effect.") + .DefaultValue(true)); + AddWidget(path, "Texture Filter (Needs reload)", WIDGET_CVAR_COMBOBOX) + .CVar(CVAR_TEXTURE_FILTER) + .RaceDisable(false) + .Options(ComboboxOptions().Tooltip("Sets the applied Texture Filtering.").ComboMap(textureFilteringMap)); + + path.column = SECTION_COLUMN_2; + AddWidget(path, "Advanced Graphics Options", WIDGET_SEPARATOR_TEXT); + + // Controls + path.sidebarName = "Controls"; + path.column = SECTION_COLUMN_1; + AddSidebarEntry("Settings", "Controls", 1); + AddWidget(path, "Clear Devices", WIDGET_BUTTON) + .Callback([](WidgetInfo& info) { + GhostshipGui::mModalWindow->RegisterPopup( + "Clear Config", + "This will completely erase the controls config, including registered devices.\nContinue?", "Clear", + "Cancel", + []() { + Ship::Context::GetInstance()->GetConsoleVariables()->ClearBlock(CVAR_PREFIX_SETTING ".Controllers"); + uint8_t bits = 0; + Ship::Context::GetInstance()->GetControlDeck()->Init(&bits); + }, + nullptr); + }) + .Options(ButtonOptions().Size(Sizes::Inline)); + AddWidget(path, "Controller Bindings", WIDGET_SEPARATOR_TEXT); + AddWidget(path, "Popout Bindings Window", WIDGET_WINDOW_BUTTON) + .CVar(CVAR_WINDOW("ControllerConfiguration")) + .RaceDisable(false) + .WindowName("Configure Controller") + .HideInSearch(true) + .Options(WindowButtonOptions().Tooltip("Enables the separate Bindings Window.")); + + // Input Viewer + path.sidebarName = "Input Viewer"; + AddSidebarEntry("Settings", path.sidebarName, 3); + AddWidget(path, "Input Viewer", WIDGET_SEPARATOR_TEXT); + AddWidget(path, "Toggle Input Viewer", WIDGET_WINDOW_BUTTON) + .CVar(CVAR_WINDOW("InputViewer")) + .RaceDisable(false) + .WindowName("Input Viewer") + .HideInSearch(true) + .Options(WindowButtonOptions().Tooltip("Toggles the Input Viewer.").EmbedWindow(false)); + + AddWidget(path, "Input Viewer Settings", WIDGET_SEPARATOR_TEXT); + AddWidget(path, "Popout Input Viewer Settings", WIDGET_WINDOW_BUTTON) + .CVar(CVAR_WINDOW("InputViewerSettings")) + .RaceDisable(false) + .WindowName("Input Viewer Settings") + .HideInSearch(true) + .Options(WindowButtonOptions().Tooltip("Enables the separate Input Viewer Settings Window.")); + + // Notifications + path.sidebarName = "Notifications"; + path.column = SECTION_COLUMN_1; + AddSidebarEntry("Settings", path.sidebarName, 3); + AddWidget(path, "Position", WIDGET_CVAR_COMBOBOX) + .CVar(CVAR_SETTING("Notifications.Position")) + .RaceDisable(false) + .Options(ComboboxOptions() + .Tooltip("Which corner of the screen notifications appear in.") + .ComboMap(notificationPosition) + .DefaultIndex(3)); + AddWidget(path, "Duration (seconds):", WIDGET_CVAR_SLIDER_FLOAT) + .CVar(CVAR_SETTING("Notifications.Duration")) + .RaceDisable(false) + .Options(FloatSliderOptions() + .Tooltip("How long notifications are displayed for.") + .Format("%.1f") + .Step(0.1f) + .Min(3.0f) + .Max(30.0f) + .DefaultValue(10.0f)); + AddWidget(path, "Background Opacity", WIDGET_CVAR_SLIDER_FLOAT) + .CVar(CVAR_SETTING("Notifications.BgOpacity")) + .RaceDisable(false) + .Options(FloatSliderOptions() + .Tooltip("How opaque the background of notifications is.") + .DefaultValue(0.5f) + .IsPercentage()); + AddWidget(path, "Size:", WIDGET_CVAR_SLIDER_FLOAT) + .CVar(CVAR_SETTING("Notifications.Size")) + .RaceDisable(false) + .Options(FloatSliderOptions() + .Tooltip("How large notifications are.") + .Format("%.1f") + .Step(0.1f) + .Min(1.0f) + .Max(5.0f) + .DefaultValue(1.8f)); + AddWidget(path, "Test Notification", WIDGET_BUTTON) + .RaceDisable(false) + .Callback([](WidgetInfo& info) { + Notification::Emit({ + .prefix = "Welcome", + .message = "to", + .suffix = "Ghostship.", + }); + }) + .Options(ButtonOptions().Tooltip("Displays a test notification.")); +} + +} // namespace GhostshipGui diff --git a/src/port/ui/GhostshipModals.cpp b/src/port/ui/GhostshipModals.cpp new file mode 100644 index 00000000..58d1bc64 --- /dev/null +++ b/src/port/ui/GhostshipModals.cpp @@ -0,0 +1,83 @@ +#include "GhostshipModals.h" +#include +#include +#include +#include "UIWidgets.hpp" +#include "GhostshipGui.hpp" + +struct GhostshipModal { + std::string title_; + std::string message_; + std::string button1_; + std::string button2_; + std::function button1callback_; + std::function button2callback_; +}; +std::vector modals; + +bool closePopup = false; + +void GhostshipModalWindow::Draw() { + if (!IsVisible()) { + return; + } + DrawElement(); + // Sync up the IsVisible flag if it was changed by ImGui + SyncVisibilityConsoleVariable(); +} + +void GhostshipModalWindow::DrawElement() { + if (modals.size() > 0) { + GhostshipModal curModal = modals.at(0); + if (!ImGui::IsPopupOpen(curModal.title_.c_str())) { + ImGui::OpenPopup(curModal.title_.c_str()); + } + if (closePopup) { + ImGui::CloseCurrentPopup(); + modals.erase(modals.begin()); + closePopup = false; + } + if (ImGui::BeginPopupModal(curModal.title_.c_str(), NULL, + ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoResize | + ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoScrollbar | + ImGuiWindowFlags_NoSavedSettings)) { + ImGui::Text("%s", curModal.message_.c_str()); + UIWidgets::PushStyleButton(THEME_COLOR); + if (ImGui::Button(curModal.button1_.c_str())) { + if (curModal.button1callback_ != nullptr) { + curModal.button1callback_(); + } + ImGui::CloseCurrentPopup(); + modals.erase(modals.begin()); + } + UIWidgets::PopStyleButton(); + if (curModal.button2_ != "") { + ImGui::SameLine(); + UIWidgets::PushStyleButton(THEME_COLOR); + if (ImGui::Button(curModal.button2_.c_str())) { + if (curModal.button2callback_ != nullptr) { + curModal.button2callback_(); + } + ImGui::CloseCurrentPopup(); + modals.erase(modals.begin()); + } + UIWidgets::PopStyleButton(); + } + } + ImGui::EndPopup(); + } +} + +void GhostshipModalWindow::RegisterPopup(std::string title, std::string message, std::string button1, + std::string button2, std::function button1callback, + std::function button2callback) { + modals.push_back({ title, message, button1, button2, button1callback, button2callback }); +} + +bool GhostshipModalWindow::IsPopupOpen(std::string title) { + return !modals.empty() && modals.at(0).title_ == title; +} + +void GhostshipModalWindow::DismissPopup() { + closePopup = true; +} diff --git a/src/port/ui/GhostshipModals.h b/src/port/ui/GhostshipModals.h new file mode 100644 index 00000000..8cbcd4d2 --- /dev/null +++ b/src/port/ui/GhostshipModals.h @@ -0,0 +1,19 @@ +#pragma once + +#include +#include + +class GhostshipModalWindow final : public Ship::GuiWindow { + public: + using GuiWindow::GuiWindow; + void Draw() override; + + void InitElement() override{}; + void DrawElement() override; + void UpdateElement() override{}; + void RegisterPopup(std::string title, std::string message, std::string button1 = "OK", std::string button2 = "", + std::function button1callback = nullptr, + std::function button2callback = nullptr); + bool IsPopupOpen(std::string title); + void DismissPopup(); +}; \ No newline at end of file diff --git a/src/port/ui/ImguiUI.cpp b/src/port/ui/ImguiUI.cpp deleted file mode 100644 index adcc09ee..00000000 --- a/src/port/ui/ImguiUI.cpp +++ /dev/null @@ -1,576 +0,0 @@ -#include "ImguiUI.h" -#include "UIWidgets.h" -#include "ResolutionEditor.h" -#include "SaveEditor.h" -#include "Notification.h" - -#include -#include -#define IMGUI_DEFINE_MATH_OPERATORS -#include -#include -#include -#include "port/Engine.h" - -#ifdef __SWITCH__ -#include -#include -#endif - -extern "C" { -#include "audio/external.h" -extern void handle_nmi_request(void); -} - -namespace GameUI { -std::shared_ptr mGameMenuBar; -std::shared_ptr mConsoleWindow; -std::shared_ptr mStatsWindow; -std::shared_ptr mInputEditorWindow; -std::shared_ptr mGfxDebuggerWindow; -std::shared_ptr mSaveEditorWindow; -std::shared_ptr mAdvancedResolutionSettingsWindow; - -void SetupGuiElements() { - auto gui = Ship::Context::GetInstance()->GetWindow()->GetGui(); - - auto& style = ImGui::GetStyle(); - style.FramePadding = ImVec2(4.0f, 6.0f); - style.ItemSpacing = ImVec2(8.0f, 6.0f); - style.Colors[ImGuiCol_MenuBarBg] = UIWidgets::Colors::DarkGray; - - mGameMenuBar = std::make_shared("gOpenMenuBar", CVarGetInteger("gOpenMenuBar", 0)); - gui->SetMenuBar(mGameMenuBar); - - if (gui->GetMenuBar() && !gui->GetMenuBar()->IsVisible()) { -#if defined(__SWITCH__) || defined(__WIIU__) - Notification::Emit({ .message = "Press - to access enhancements menu", .remainingTime = 10.0f }); -#else - Notification::Emit({ .message = "Press F1 to access enhancements menu", .remainingTime = 10.0f }); -#endif - } - - mStatsWindow = gui->GetGuiWindow("Stats"); - if (mStatsWindow == nullptr) { - SPDLOG_ERROR("Could not find stats window"); - } - - mConsoleWindow = gui->GetGuiWindow("Console"); - if (mConsoleWindow == nullptr) { - SPDLOG_ERROR("Could not find console window"); - } - - mInputEditorWindow = gui->GetGuiWindow("Input Editor"); - if (mInputEditorWindow == nullptr) { - SPDLOG_ERROR("Could not find input editor window"); - return; - } - - mGfxDebuggerWindow = gui->GetGuiWindow("GfxDebuggerWindow"); - if (mGfxDebuggerWindow == nullptr) { - SPDLOG_ERROR("Could not find input GfxDebuggerWindow"); - } - - mAdvancedResolutionSettingsWindow = std::make_shared( - "gAdvancedResolutionEditorEnabled", "Advanced Resolution Settings"); - gui->AddGuiWindow(mAdvancedResolutionSettingsWindow); - mSaveEditorWindow = std::make_shared("gSaveEditorEnabled", "Save Editor"); - gui->AddGuiWindow(mSaveEditorWindow); -} - -void Destroy() { - mAdvancedResolutionSettingsWindow = nullptr; - mSaveEditorWindow = nullptr; - mConsoleWindow = nullptr; - mStatsWindow = nullptr; - mInputEditorWindow = nullptr; -} - -std::string GetWindowButtonText(const char* text, bool menuOpen) { - char buttonText[100] = ""; - if (menuOpen) { - strcat(buttonText, ICON_FA_CHEVRON_RIGHT " "); - } - strcat(buttonText, text); - if (!menuOpen) { - strcat(buttonText, " "); - } - return buttonText; -} -} // namespace GameUI - -static const char* filters[3] = { -#ifdef __WIIU__ - "", -#else - "Three-Point", -#endif - "Linear", "None" -}; - -void DrawSettingsMenu() { - if (UIWidgets::BeginMenu("Settings")) { - if (UIWidgets::BeginMenu("Audio")) { - UIWidgets::CVarSliderFloat("Master Volume", "gGameMasterVolume", 0.0f, 1.0f, 1.0f, - { - .format = "%.0f%%", - .isPercentage = true, - }); - if (UIWidgets::CVarSliderFloat("Main Music Volume", "gMainMusicVolume", 0.0f, 1.0f, 1.0f, - { - .format = "%.0f%%", - .isPercentage = true, - })) { - audio_set_player_volume(SEQ_PLAYER_LEVEL, CVarGetFloat("gMainMusicVolume", 1.0f)); - } - if (UIWidgets::CVarSliderFloat("Sound Effects Volume", "gSFXMusicVolume", 0.0f, 1.0f, 1.0f, - { - .format = "%.0f%%", - .isPercentage = true, - })) { - audio_set_player_volume(SEQ_PLAYER_SFX, CVarGetFloat("gSFXMusicVolume", 1.0f)); - } - if (UIWidgets::CVarSliderFloat("Environment Volume", "gEnvironmentVolume", 0.0f, 1.0f, 1.0f, - { - .format = "%.0f%%", - .isPercentage = true, - })) { - audio_set_player_volume(SEQ_PLAYER_ENV, CVarGetFloat("gEnvironmentVolume", 1.0f)); - } - - static std::unordered_map audioBackendNames = { - { Ship::AudioBackend::WASAPI, "Windows Audio Session API" }, - { Ship::AudioBackend::SDL, "SDL" }, - }; - - ImGui::Text("Audio API (Needs reload)"); - auto currentAudioBackend = Ship::Context::GetInstance()->GetAudio()->GetCurrentAudioBackend(); - - if (Ship::Context::GetInstance()->GetAudio()->GetAvailableAudioBackends()->size() <= 1) { - UIWidgets::DisableComponent(ImGui::GetStyle().Alpha * 0.5f); - } - if (ImGui::BeginCombo("##AApi", audioBackendNames[currentAudioBackend])) { - for (uint8_t i = 0; i < Ship::Context::GetInstance()->GetAudio()->GetAvailableAudioBackends()->size(); - i++) { - auto backend = Ship::Context::GetInstance()->GetAudio()->GetAvailableAudioBackends()->data()[i]; - if (ImGui::Selectable(audioBackendNames[backend], backend == currentAudioBackend)) { - Ship::Context::GetInstance()->GetAudio()->SetCurrentAudioBackend(backend); - } - } - ImGui::EndCombo(); - } - if (Ship::Context::GetInstance()->GetAudio()->GetAvailableAudioBackends()->size() <= 1) { - UIWidgets::ReEnableComponent(""); - } - - ImGui::EndMenu(); - } - - UIWidgets::Spacer(0); - - if (UIWidgets::BeginMenu("Controller")) { - UIWidgets::WindowButton("Controller Mapping", "gInputEditorWindow", GameUI::mInputEditorWindow); - - UIWidgets::Spacer(0); - -#ifndef __SWITCH__ - UIWidgets::CVarCheckbox( - "Menubar Controller Navigation", "gControlNav", - { .tooltip = "Allows controller navigation of the SOH menu bar (Settings, Enhancements,...)\nCAUTION: " - "This will disable game inputs while the menubar is visible.\n\nD-pad to move between " - "items, A to select, and X to grab focus on the menu bar" }); -#endif - UIWidgets::CVarCheckbox("Show Inputs", "gInputEnabled", - { .tooltip = "Shows currently pressed inputs on the bottom right of the screen" }); - if (CVarGetInteger("gInputEnabled", 0)) { - UIWidgets::CVarSliderFloat( - "Input Scale", "gInputScale", 1.0f, 3.0f, 1.0f, - { - .tooltip = "Sets the on screen size of the displayed inputs from the Show Inputs setting", - .format = "%.1fx", - }); - } - - ImGui::EndMenu(); - } - - ImGui::EndMenu(); - } - - ImGui::SetCursorPosY(0.0f); - if (UIWidgets::BeginMenu("Graphics")) { - UIWidgets::WindowButton("Resolution Editor", "gAdvancedResolutionEditorEnabled", - GameUI::mAdvancedResolutionSettingsWindow); - - UIWidgets::Spacer(0); - - // Previously was running every frame, and nothing was setting it? Maybe a bad copy/paste? - // Ship::Context::GetInstance()->GetWindow()->SetResolutionMultiplier(CVarGetFloat("gInternalResolution", 1)); - // UIWidgets::Tooltip("Multiplies your output resolution by the value inputted, as a more intensive but - // effective form of anti-aliasing"); -#ifndef __WIIU__ - if (UIWidgets::CVarSliderInt( - "MSAA: %d", "gMSAAValue", 1, 8, 1, - { .tooltip = - "Activates multi-sample anti-aliasing when above 1x up to 8x for 8 samples for every pixel" })) { - Ship::Context::GetInstance()->GetWindow()->SetMsaaLevel(CVarGetInteger("gMSAAValue", 1)); - } -#endif - - { // FPS Slider - const int minFps = 30; - static int maxFps; - if (Ship::Context::GetInstance()->GetWindow()->GetWindowBackend() == - Ship::WindowBackend::FAST3D_DXGI_DX11) { - maxFps = 360; - } else { - maxFps = Ship::Context::GetInstance()->GetWindow()->GetCurrentRefreshRate(); - } - int currentFps = fmax(fmin(GameEngine::GetInterpolationFPS(), maxFps), minFps); -#ifdef __WIIU__ - UIWidgets::Spacer(0); - // only support divisors of 60 on the Wii U - if (currentFps > 60) { - currentFps = 60; - } else { - currentFps = 60 / (60 / currentFps); - } - - int fpsSlider = 1; - if (currentFps == 30) { - ImGui::Text("FPS: Original (30)"); - } else { - ImGui::Text("FPS: %d", currentFps); - if (currentFps == 30) { - fpsSlider = 2; - } else { // currentFps == 60 - fpsSlider = 3; - } - } - if (CVarGetInteger("gMatchRefreshRate", 0)) { - UIWidgets::DisableComponent(ImGui::GetStyle().Alpha * 0.5f); - } - - if (ImGui::Button(" - ##WiiUFPS")) { - fpsSlider--; - } - ImGui::SameLine(); - ImGui::SetCursorPosX(ImGui::GetCursorPosX() - 7.0f); - - UIWidgets::Spacer(0); - - ImGui::PushItemWidth(std::min((ImGui::GetContentRegionAvail().x - 60.0f), 260.0f)); - ImGui::SliderInt("##WiiUFPSSlider", &fpsSlider, 1, 3, "", ImGuiSliderFlags_AlwaysClamp); - ImGui::PopItemWidth(); - - ImGui::SameLine(); - ImGui::SetCursorPosX(ImGui::GetCursorPosX() - 7.0f); - if (ImGui::Button(" + ##WiiUFPS")) { - fpsSlider++; - } - - if (CVarGetInteger("gMatchRefreshRate", 0)) { - UIWidgets::ReEnableComponent(""); - } - if (fpsSlider > 3) { - fpsSlider = 3; - } else if (fpsSlider < 1) { - fpsSlider = 1; - } - - if (fpsSlider == 1) { - currentFps = 20; - } else if (fpsSlider == 2) { - currentFps = 30; - } else if (fpsSlider == 3) { - currentFps = 60; - } - CVarSetInteger("gInterpolationFPS", currentFps); - Ship::Context::GetInstance()->GetWindow()->GetGui()->SaveConsoleVariablesNextFrame(); -#else - bool matchingRefreshRate = - CVarGetInteger("gMatchRefreshRate", 0) && - Ship::Context::GetInstance()->GetWindow()->GetWindowBackend() != Ship::WindowBackend::FAST3D_DXGI_DX11; - UIWidgets::CVarSliderInt((currentFps == 20) ? "FPS: Original (20)" : "FPS: %d", "gInterpolationFPS", minFps, - maxFps, 1, { .disabled = matchingRefreshRate }); -#endif - if (Ship::Context::GetInstance()->GetWindow()->GetWindowBackend() == - Ship::WindowBackend::FAST3D_DXGI_DX11) { - UIWidgets::Tooltip( - "Uses Matrix Interpolation to create extra frames, resulting in smoother graphics. This is purely " - "visual and does not impact game logic, execution of glitches etc.\n\n" - "A higher target FPS than your monitor's refresh rate will waste resources, and might give a worse " - "result."); - } else { - UIWidgets::Tooltip( - "Uses Matrix Interpolation to create extra frames, resulting in smoother graphics. This is purely " - "visual and does not impact game logic, execution of glitches etc."); - } - } // END FPS Slider - - if (Ship::Context::GetInstance()->GetWindow()->GetWindowBackend() == Ship::WindowBackend::FAST3D_DXGI_DX11) { - UIWidgets::Spacer(0); - if (ImGui::Button("Match Refresh Rate")) { - int hz = Ship::Context::GetInstance()->GetWindow()->GetCurrentRefreshRate(); - if (hz >= 30 && hz <= 360) { - CVarSetInteger("gInterpolationFPS", hz); - Ship::Context::GetInstance()->GetWindow()->GetGui()->SaveConsoleVariablesNextFrame(); - } - } - } else { - UIWidgets::PaddedEnhancementCheckbox("Match Refresh Rate", "gMatchRefreshRate", true, false); - } - - UIWidgets::Tooltip("Matches interpolation value to the current game's window refresh rate"); - - if (Ship::Context::GetInstance()->GetWindow()->GetWindowBackend() == Ship::WindowBackend::FAST3D_DXGI_DX11) { - UIWidgets::PaddedEnhancementSliderInt( - CVarGetInteger("gExtraLatencyThreshold", 80) == 0 ? "Jitter fix: Off" : "Jitter fix: >= %d FPS", - "##ExtraLatencyThreshold", "gExtraLatencyThreshold", 0, 360, "", 80, true, true, false); - UIWidgets::Tooltip("When Interpolation FPS setting is at least this threshold, add one frame of input lag " - "(e.g. 16.6 ms for 60 FPS) in order to avoid jitter. This setting allows the CPU to " - "work on one frame while GPU works on the previous frame.\nThis setting should be used " - "when your computer is too slow to do CPU + GPU work in time."); - } - - UIWidgets::PaddedSeparator(true, true, 3.0f, 3.0f); - - static std::unordered_map windowBackendNames = { - { Ship::WindowBackend::FAST3D_DXGI_DX11, "DirectX" }, - { Ship::WindowBackend::FAST3D_SDL_OPENGL, "OpenGL" }, - { Ship::WindowBackend::FAST3D_SDL_METAL, "Metal" }, - }; - - ImGui::Text("Renderer API (Needs reload)"); - Ship::WindowBackend runningWindowBackend = Ship::Context::GetInstance()->GetWindow()->GetWindowBackend(); - Ship::WindowBackend configWindowBackend; - int configWindowBackendId = Ship::Context::GetInstance()->GetConfig()->GetInt("Window.Backend.Id", -1); - if (configWindowBackendId != -1 && - configWindowBackendId < static_cast(Ship::WindowBackend::WINDOW_BACKEND_COUNT)) { - configWindowBackend = static_cast(configWindowBackendId); - } else { - configWindowBackend = runningWindowBackend; - } - - if (Ship::Context::GetInstance()->GetWindow()->GetAvailableWindowBackends()->size() <= 1) { - UIWidgets::DisableComponent(ImGui::GetStyle().Alpha * 0.5f); - } - if (ImGui::BeginCombo("##RApi", windowBackendNames[configWindowBackend])) { - for (size_t i = 0; i < Ship::Context::GetInstance()->GetWindow()->GetAvailableWindowBackends()->size(); - i++) { - auto backend = Ship::Context::GetInstance()->GetWindow()->GetAvailableWindowBackends()->data()[i]; - if (ImGui::Selectable(windowBackendNames[backend], backend == configWindowBackend)) { - Ship::Context::GetInstance()->GetConfig()->SetInt("Window.Backend.Id", static_cast(backend)); - Ship::Context::GetInstance()->GetConfig()->SetString("Window.Backend.Name", - windowBackendNames[backend]); - Ship::Context::GetInstance()->GetConfig()->Save(); - } - } - ImGui::EndCombo(); - } - if (Ship::Context::GetInstance()->GetWindow()->GetAvailableWindowBackends()->size() <= 1) { - UIWidgets::ReEnableComponent(""); - } - - if (Ship::Context::GetInstance()->GetWindow()->CanDisableVerticalSync()) { - UIWidgets::PaddedEnhancementCheckbox("Enable Vsync", "gVsyncEnabled", true, false); - } - - if (Ship::Context::GetInstance()->GetWindow()->SupportsWindowedFullscreen()) { - UIWidgets::PaddedEnhancementCheckbox("Windowed fullscreen", "gSdlWindowedFullscreen", true, false); - } - - if (Ship::Context::GetInstance()->GetWindow()->GetGui()->SupportsViewports()) { - UIWidgets::PaddedEnhancementCheckbox("Allow multi-windows", "gEnableMultiViewports", true, false, false, "", - UIWidgets::CheckboxGraphics::Cross, true); - UIWidgets::Tooltip("Allows windows to be able to be dragged off of the main game window. Requires a reload " - "to take effect."); - } - - // If more filters are added to LUS, make sure to add them to the filters list here - ImGui::Text("Texture Filter (Needs reload)"); - - UIWidgets::EnhancementCombobox("gTextureFilter", filters, 0); - - UIWidgets::Spacer(0); - - Ship::Context::GetInstance()->GetWindow()->GetGui()->GetGameOverlay()->DrawSettings(); - - ImGui::EndMenu(); - } -} - -void DrawMenuBarIcon() { - static bool gameIconLoaded = false; - if (!gameIconLoaded) { - // Ship::Context::GetInstance()->GetWindow()->GetGui()->LoadGuiTexture("Game_Icon", "textures/icons/gIcon.png", - // ImVec4(1.0f, 1.0f, 1.0f, 1.0f)); gameIconLoaded = true; - } - - if (Ship::Context::GetInstance()->GetWindow()->GetGui()->GetTextureByName("Game_Icon")) { -#ifdef __SWITCH__ - ImVec2 iconSize = ImVec2(20.0f, 20.0f); - float posScale = 1.0f; -#elif defined(__WIIU__) - ImVec2 iconSize = ImVec2(16.0f * 2, 16.0f * 2); - float posScale = 2.0f; -#else - ImVec2 iconSize = ImVec2(20.0f, 20.0f); - float posScale = 1.5f; -#endif - ImGui::SetCursorPos(ImVec2(5, 2.5f) * posScale); - ImGui::Image(Ship::Context::GetInstance()->GetWindow()->GetGui()->GetTextureByName("Game_Icon"), iconSize); - ImGui::SameLine(); - ImGui::SetCursorPos(ImVec2(25, 0) * posScale); - } -} - -void DrawGameMenu() { - if (UIWidgets::BeginMenu("Ghostship")) { - if (UIWidgets::MenuItem("Reset", -#ifdef __APPLE__ - "Command-R" -#else - "Ctrl+R" -#endif - )) { - std::reinterpret_pointer_cast( - Ship::Context::GetInstance()->GetWindow()->GetGui()->GetGuiWindow("Console")) - ->Dispatch("reset"); - } -#if !defined(__SWITCH__) && !defined(__WIIU__) - if (UIWidgets::MenuItem("Toggle Fullscreen", "F9")) { - Ship::Context::GetInstance()->GetWindow()->ToggleFullscreen(); - } -#endif - if (UIWidgets::MenuItem("Quit")) { - Ship::Context::GetInstance()->GetWindow()->Close(); - } - ImGui::EndMenu(); - } -} - -void DrawEnhancementsMenu() { - if (UIWidgets::BeginMenu("Enhancements")) { - - if (UIWidgets::BeginMenu("Gameplay")) { - UIWidgets::CVarCheckbox( - "No Level of Detail (LOD)", "gDisableLOD", - { .tooltip = "Disable Level of Detail (LOD) to avoid models using lower poly versions at a distance" }); - UIWidgets::CVarCheckbox( - "Select any star from menu", "gSelectAllStars", - { .tooltip = "Let's you select any star from the menu regardless of the courses completion status." }); - UIWidgets::CVarCheckbox( - "Collecting Stars Will Not Exit Level", "gStarNoExit", - { .tooltip = "Stars act like the 100 coin star and will not take you out of the level" }); - UIWidgets::CVarCheckbox("Avoid playing peach cutscene", "gDisablePeachCutscene", - { .tooltip = "Avoid playing the peach cutscene when starting a new game" }); - ImGui::EndMenu(); - } - - if (UIWidgets::BeginMenu("Fixes")) { - UIWidgets::CVarCheckbox( - "Fix Koopa race music", "gFixKoopaRaceMusic", - { .tooltip = "Fixes the Koopa race music on Bob-omb Battlefield and Tiny-Huge Island" }); - - ImGui::EndMenu(); - } - - ImGui::EndMenu(); - } -} - -void DrawCheatsMenu() { - if (UIWidgets::BeginMenu("Cheats")) { - UIWidgets::CVarCheckbox("Infinite Health", "gInfiniteHealth"); - UIWidgets::CVarCheckbox("Infinite Lives", "gInfiniteLives"); - UIWidgets::CVarCheckbox("Pause Exit Whenever", "gPauseExitWhenever"); - - ImGui::EndMenu(); - } -} - -const char* debugInfoPages[6] = { - "Object", "Check Surface", "Map", "Stage", "Effect", "Enemy", -}; - -static const char* logLevels[] = { - "trace", "debug", "info", "warn", "error", "critical", "off", -}; - -void DrawDebugMenu() { - if (UIWidgets::BeginMenu("Developer")) { - if (UIWidgets::CVarCombobox("Log Level", "gDeveloperTools.LogLevel", logLevels, - { - .tooltip = "The log level determines which messages are printed to the " - "console. This does not affect the log file output", - .defaultIndex = 1, - })) { - Ship::Context::GetInstance()->GetLogger()->set_level( - (spdlog::level::level_enum)CVarGetInteger("gDeveloperTools.LogLevel", 1)); - } - UIWidgets::WindowButton( - "Gfx Debugger", "gGfxDebuggerEnabled", GameUI::mGfxDebuggerWindow, - { .tooltip = - "Enables the Gfx Debugger window, allowing you to input commands, type help for some examples" }); - - UIWidgets::CVarCheckbox("Debug mode", "gEnableDebugMode", - { .tooltip = "Various debug features, including a level selector from the main menu" }); - -#ifdef USE_GBI_TRACE - UIWidgets::CVarCheckbox( - "GFX trace mode", "gEnableGFXTrace", - { .tooltip = - "Enables the Gfx trace mode, which will output information about the Gfx commands being run" }); -#endif - - UIWidgets::CVarCheckbox( - "Better Level Select", "gDeveloper.BetterLevelSelect", - { .tooltip = "Tweaks to the level select screen, like naming and allowing C-buttons to be used" }); - - UIWidgets::CVarCheckbox("Draw DebugInfo", "gDeveloper.DrawDebugInfo"); - if (CVarGetInteger("gDeveloper.DrawDebugInfo", 0)) { - UIWidgets::CVarCombobox("DebugInfo mode", "gDeveloper.DebugInfoPage", debugInfoPages, - { - .defaultIndex = 0, - }); - } - UIWidgets::Spacer(0); - - UIWidgets::WindowButton("Save Editor", "gSaveEditorEnabled", GameUI::mSaveEditorWindow); - UIWidgets::WindowButton( - "Stats", "gStatsEnabled", GameUI::mStatsWindow, - { .tooltip = "Shows the stats window, with your FPS and frametimes, and the OS you're playing on" }); - UIWidgets::WindowButton( - "Console", "gConsoleEnabled", GameUI::mConsoleWindow, - { .tooltip = "Enables the console window, allowing you to input commands, type help for some examples" }); - - ImGui::EndMenu(); - } -} - -void GameMenuBar::DrawElement() { - if (ImGui::BeginMenuBar()) { - DrawMenuBarIcon(); - - DrawGameMenu(); - - ImGui::SetCursorPosY(0.0f); - - DrawSettingsMenu(); - - ImGui::SetCursorPosY(0.0f); - - DrawEnhancementsMenu(); - - ImGui::SetCursorPosY(0.0f); - - DrawCheatsMenu(); - - ImGui::SetCursorPosY(0.0f); - - DrawDebugMenu(); - - ImGui::EndMenuBar(); - } -} \ No newline at end of file diff --git a/src/port/ui/ImguiUI.h b/src/port/ui/ImguiUI.h deleted file mode 100644 index e3fa9a66..00000000 --- a/src/port/ui/ImguiUI.h +++ /dev/null @@ -1,16 +0,0 @@ -#pragma once -#include - -namespace GameUI { - void SetupGuiElements(); - void Destroy(); -} - -class GameMenuBar : public Ship::GuiMenuBar { - public: - using Ship::GuiMenuBar::GuiMenuBar; - protected: - void DrawElement() override; - void InitElement() override {}; - void UpdateElement() override {}; -}; \ No newline at end of file diff --git a/src/port/ui/InputViewer.cpp b/src/port/ui/InputViewer.cpp new file mode 100644 index 00000000..968a608b --- /dev/null +++ b/src/port/ui/InputViewer.cpp @@ -0,0 +1,740 @@ +#include "InputViewer.h" + +#include +#include +#include +#include +#include "port/ui/cvar_prefixes.h" +#include +#include +#include + +#include "UIWidgets.hpp" +#include "UIWidgets.hpp" +#include "GhostshipGui.hpp" + +using namespace UIWidgets; + +// Text colors +static Color_RGBA8 textColorDefault = { 255, 255, 255, 255 }; +static Color_RGBA8 range1ColorDefault = { 255, 178, 0, 255 }; +static Color_RGBA8 range2ColorDefault = { 0, 255, 0, 255 }; + +static std::unordered_map buttonOutlineOptions = { + { BUTTON_OUTLINE_ALWAYS_SHOWN, "Always Shown" }, + { BUTTON_OUTLINE_NOT_PRESSED, "Shown Only While Not Pressed" }, + { BUTTON_OUTLINE_PRESSED, "Shown Only While Pressed" }, + { BUTTON_OUTLINE_ALWAYS_HIDDEN, "Always Hidden" } +}; +static std::unordered_map buttonOutlineOptionsVerbose = { + { BUTTON_OUTLINE_ALWAYS_SHOWN, "Outline Always Shown" }, + { BUTTON_OUTLINE_NOT_PRESSED, "Outline Shown Only While Not Pressed" }, + { BUTTON_OUTLINE_PRESSED, "Outline Shown Only While Pressed" }, + { BUTTON_OUTLINE_ALWAYS_HIDDEN, "Outline Always Hidden" } +}; + +static std::unordered_map stickModeOptions = { { STICK_MODE_ALWAYS_SHOWN, "Always" }, + { STICK_MODE_HIDDEN_IN_DEADZONE, "While In Use" }, + { STICK_MODE_ALWAYS_HIDDEN, "Never" } }; + +InputViewer::~InputViewer() { + SPDLOG_TRACE("destruct input viewer"); +} + +void InputViewer::RenderButton(std::string btnTexture, std::string btnOutlineTexture, int state, ImVec2 size, + int outlineMode) { + const ImVec2 pos = ImGui::GetCursorPos(); + ImGui::SetNextItemAllowOverlap(); + // Render Outline based on settings + if (outlineMode == BUTTON_OUTLINE_ALWAYS_SHOWN || (outlineMode == BUTTON_OUTLINE_NOT_PRESSED && !state) || + (outlineMode == BUTTON_OUTLINE_PRESSED && state)) { + ImGui::Image(Ship::Context::GetInstance()->GetWindow()->GetGui()->GetTextureByName(btnOutlineTexture), size, + ImVec2(0, 0), ImVec2(1.0f, 1.0f)); + } + // Render button if pressed + if (state) { + ImGui::SetCursorPos(pos); + ImGui::SetNextItemAllowOverlap(); + ImGui::Image(Ship::Context::GetInstance()->GetWindow()->GetGui()->GetTextureByName(btnTexture), size, + ImVec2(0, 0), ImVec2(1.0f, 1.0f)); + } +} + +void InputViewer::Draw() { + if (!IsVisible()) { + return; + } + DrawElement(); + // Sync up the IsVisible flag if it was changed by ImGui + SyncVisibilityConsoleVariable(); +} + +void InputViewer::DrawElement() { + if (CVarGetInteger(CVAR_WINDOW("InputViewer"), 0)) { + static bool sButtonTexturesLoaded = false; + if (!sButtonTexturesLoaded) { + Ship::Context::GetInstance()->GetWindow()->GetGui()->LoadTextureFromRawImage( + "Input-Viewer-Background", "textures/buttons/InputViewerBackground.png"); + Ship::Context::GetInstance()->GetWindow()->GetGui()->LoadTextureFromRawImage("A-Btn", + "textures/buttons/ABtn.png"); + Ship::Context::GetInstance()->GetWindow()->GetGui()->LoadTextureFromRawImage("B-Btn", + "textures/buttons/BBtn.png"); + Ship::Context::GetInstance()->GetWindow()->GetGui()->LoadTextureFromRawImage("L-Btn", + "textures/buttons/LBtn.png"); + Ship::Context::GetInstance()->GetWindow()->GetGui()->LoadTextureFromRawImage("R-Btn", + "textures/buttons/RBtn.png"); + Ship::Context::GetInstance()->GetWindow()->GetGui()->LoadTextureFromRawImage("Z-Btn", + "textures/buttons/ZBtn.png"); + Ship::Context::GetInstance()->GetWindow()->GetGui()->LoadTextureFromRawImage( + "Start-Btn", "textures/buttons/StartBtn.png"); + Ship::Context::GetInstance()->GetWindow()->GetGui()->LoadTextureFromRawImage("C-Left", + "textures/buttons/CLeft.png"); + Ship::Context::GetInstance()->GetWindow()->GetGui()->LoadTextureFromRawImage("C-Right", + "textures/buttons/CRight.png"); + Ship::Context::GetInstance()->GetWindow()->GetGui()->LoadTextureFromRawImage("C-Up", + "textures/buttons/CUp.png"); + Ship::Context::GetInstance()->GetWindow()->GetGui()->LoadTextureFromRawImage("C-Down", + "textures/buttons/CDown.png"); + Ship::Context::GetInstance()->GetWindow()->GetGui()->LoadTextureFromRawImage( + "Analog-Stick", "textures/buttons/AnalogStick.png"); + Ship::Context::GetInstance()->GetWindow()->GetGui()->LoadTextureFromRawImage( + "Dpad-Left", "textures/buttons/DPadLeft.png"); + Ship::Context::GetInstance()->GetWindow()->GetGui()->LoadTextureFromRawImage( + "Dpad-Right", "textures/buttons/DPadRight.png"); + Ship::Context::GetInstance()->GetWindow()->GetGui()->LoadTextureFromRawImage("Dpad-Up", + "textures/buttons/DPadUp.png"); + Ship::Context::GetInstance()->GetWindow()->GetGui()->LoadTextureFromRawImage( + "Dpad-Down", "textures/buttons/DPadDown.png"); + Ship::Context::GetInstance()->GetWindow()->GetGui()->LoadTextureFromRawImage("Modifier-1", + "textures/buttons/Mod1.png"); + Ship::Context::GetInstance()->GetWindow()->GetGui()->LoadTextureFromRawImage("Modifier-2", + "textures/buttons/Mod2.png"); + Ship::Context::GetInstance()->GetWindow()->GetGui()->LoadTextureFromRawImage( + "Right-Stick", "textures/buttons/RightStick.png"); + Ship::Context::GetInstance()->GetWindow()->GetGui()->LoadTextureFromRawImage( + "A-Btn Outline", "textures/buttons/ABtnOutline.png"); + Ship::Context::GetInstance()->GetWindow()->GetGui()->LoadTextureFromRawImage( + "B-Btn Outline", "textures/buttons/BBtnOutline.png"); + Ship::Context::GetInstance()->GetWindow()->GetGui()->LoadTextureFromRawImage( + "L-Btn Outline", "textures/buttons/LBtnOutline.png"); + Ship::Context::GetInstance()->GetWindow()->GetGui()->LoadTextureFromRawImage( + "R-Btn Outline", "textures/buttons/RBtnOutline.png"); + Ship::Context::GetInstance()->GetWindow()->GetGui()->LoadTextureFromRawImage( + "Z-Btn Outline", "textures/buttons/ZBtnOutline.png"); + Ship::Context::GetInstance()->GetWindow()->GetGui()->LoadTextureFromRawImage( + "Start-Btn Outline", "textures/buttons/StartBtnOutline.png"); + Ship::Context::GetInstance()->GetWindow()->GetGui()->LoadTextureFromRawImage( + "C-Left Outline", "textures/buttons/CLeftOutline.png"); + Ship::Context::GetInstance()->GetWindow()->GetGui()->LoadTextureFromRawImage( + "C-Right Outline", "textures/buttons/CRightOutline.png"); + Ship::Context::GetInstance()->GetWindow()->GetGui()->LoadTextureFromRawImage( + "C-Up Outline", "textures/buttons/CUpOutline.png"); + Ship::Context::GetInstance()->GetWindow()->GetGui()->LoadTextureFromRawImage( + "C-Down Outline", "textures/buttons/CDownOutline.png"); + Ship::Context::GetInstance()->GetWindow()->GetGui()->LoadTextureFromRawImage( + "Analog-Stick Outline", "textures/buttons/AnalogStickOutline.png"); + Ship::Context::GetInstance()->GetWindow()->GetGui()->LoadTextureFromRawImage( + "Dpad-Left Outline", "textures/buttons/DPadLeftOutline.png"); + Ship::Context::GetInstance()->GetWindow()->GetGui()->LoadTextureFromRawImage( + "Dpad-Right Outline", "textures/buttons/DPadRightOutline.png"); + Ship::Context::GetInstance()->GetWindow()->GetGui()->LoadTextureFromRawImage( + "Dpad-Up Outline", "textures/buttons/DPadUpOutline.png"); + Ship::Context::GetInstance()->GetWindow()->GetGui()->LoadTextureFromRawImage( + "Dpad-Down Outline", "textures/buttons/DPadDownOutline.png"); + Ship::Context::GetInstance()->GetWindow()->GetGui()->LoadTextureFromRawImage( + "Modifier-1 Outline", "textures/buttons/Mod1Outline.png"); + Ship::Context::GetInstance()->GetWindow()->GetGui()->LoadTextureFromRawImage( + "Modifier-2 Outline", "textures/buttons/Mod2Outline.png"); + Ship::Context::GetInstance()->GetWindow()->GetGui()->LoadTextureFromRawImage( + "Right-Stick Outline", "textures/buttons/RightStickOutline.png"); + sButtonTexturesLoaded = true; + } + + ImVec2 mainPos = ImGui::GetWindowPos(); + ImVec2 size = ImGui::GetMainViewport()->WorkSize; + +#ifdef __WIIU__ + const float scale = CVarGetFloat(CVAR_INPUT_VIEWER("Scale"), 1.0f) * 2.0f; +#else + const float scale = CVarGetFloat(CVAR_INPUT_VIEWER("Scale"), 1.0f); +#endif + const int showAnalogAngles = CVarGetInteger(CVAR_INPUT_VIEWER("AnalogAngles.Enabled"), 0); + const int buttonOutlineMode = + CVarGetInteger(CVAR_INPUT_VIEWER("ButtonOutlineMode"), BUTTON_OUTLINE_NOT_PRESSED); + const bool useGlobalOutlineMode = CVarGetInteger(CVAR_INPUT_VIEWER("UseGlobalButtonOutlineMode"), 1); + + ImVec2 bgSize = Ship::Context::GetInstance()->GetWindow()->GetGui()->GetTextureSize("Input-Viewer-Background"); + ImVec2 scaledBGSize = ImVec2(bgSize.x * scale, bgSize.y * scale); + + ImGui::SetNextWindowSize( + ImVec2(scaledBGSize.x + 20, scaledBGSize.y + + (showAnalogAngles ? ImGui::CalcTextSize("X").y : 0) * scale * + CVarGetFloat(CVAR_INPUT_VIEWER("AnalogAngles.Scale"), 1.0f) + + 20)); + ImGui::SetNextWindowContentSize( + ImVec2(scaledBGSize.x, scaledBGSize.y + (showAnalogAngles ? 15 : 0) * scale * + CVarGetFloat(CVAR_INPUT_VIEWER("AnalogAngles.Scale"), 1.0f))); + ImGui::SetNextWindowPos( + ImVec2(mainPos.x + size.x - scaledBGSize.x - 30, mainPos.y + size.y - scaledBGSize.y - 30), + ImGuiCond_FirstUseEver); + + ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0, 0, 0, 0)); + ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(0.0f, 0.0f)); + + OSContPad* pads = + std::dynamic_pointer_cast(Ship::Context::GetInstance()->GetControlDeck())->GetPads(); + + ImGuiWindowFlags windowFlags = ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoScrollbar | + ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoBackground | + ImGuiWindowFlags_NoFocusOnAppearing; + + if (!CVarGetInteger(CVAR_INPUT_VIEWER("EnableDragging"), 1)) { + windowFlags |= ImGuiWindowFlags_NoInputs | ImGuiWindowFlags_NoMove; + } + + if (pads != nullptr && ImGui::Begin("Input Viewer", nullptr, windowFlags)) { + ImGui::SetCursorPos(ImVec2(10, 10)); + const ImVec2 aPos = ImGui::GetCursorPos(); + + if (CVarGetInteger(CVAR_INPUT_VIEWER("ShowBackground"), 1)) { + ImGui::SetNextItemAllowOverlap(); + // Background + ImGui::Image( + Ship::Context::GetInstance()->GetWindow()->GetGui()->GetTextureByName("Input-Viewer-Background"), + scaledBGSize, ImVec2(0, 0), ImVec2(1.0f, 1.0f)); + } + + // A/B + if (CVarGetInteger(CVAR_INPUT_VIEWER("BBtn"), 1)) { + ImGui::SetNextItemAllowOverlap(); + ImGui::SetCursorPos(aPos); + RenderButton("B-Btn", "B-Btn Outline", pads[0].button & BTN_B, scaledBGSize, + useGlobalOutlineMode + ? buttonOutlineMode + : CVarGetInteger(CVAR_INPUT_VIEWER("BBtnOutlineMode"), BUTTON_OUTLINE_NOT_PRESSED)); + } + if (CVarGetInteger(CVAR_INPUT_VIEWER("ABtn"), 1)) { + ImGui::SetNextItemAllowOverlap(); + ImGui::SetCursorPos(aPos); + RenderButton("A-Btn", "A-Btn Outline", pads[0].button & BTN_A, scaledBGSize, + useGlobalOutlineMode + ? buttonOutlineMode + : CVarGetInteger(CVAR_INPUT_VIEWER("ABtnOutlineMode"), BUTTON_OUTLINE_NOT_PRESSED)); + } + + // C buttons + if (CVarGetInteger(CVAR_INPUT_VIEWER("CUp"), 1)) { + ImGui::SetNextItemAllowOverlap(); + ImGui::SetCursorPos(aPos); + RenderButton("C-Up", "C-Up Outline", pads[0].button & BTN_CUP, scaledBGSize, + useGlobalOutlineMode + ? buttonOutlineMode + : CVarGetInteger(CVAR_INPUT_VIEWER("CUpOutlineMode"), BUTTON_OUTLINE_NOT_PRESSED)); + } + if (CVarGetInteger(CVAR_INPUT_VIEWER("CLeft"), 1)) { + ImGui::SetNextItemAllowOverlap(); + ImGui::SetCursorPos(aPos); + RenderButton("C-Left", "C-Left Outline", pads[0].button & BTN_CLEFT, scaledBGSize, + useGlobalOutlineMode + ? buttonOutlineMode + : CVarGetInteger(CVAR_INPUT_VIEWER("CLeftOutlineMode"), BUTTON_OUTLINE_NOT_PRESSED)); + } + if (CVarGetInteger(CVAR_INPUT_VIEWER("CRight"), 1)) { + ImGui::SetNextItemAllowOverlap(); + ImGui::SetCursorPos(aPos); + RenderButton("C-Right", "C-Right Outline", pads[0].button & BTN_CRIGHT, scaledBGSize, + useGlobalOutlineMode + ? buttonOutlineMode + : CVarGetInteger(CVAR_INPUT_VIEWER("CRightOutlineMode"), BUTTON_OUTLINE_NOT_PRESSED)); + } + if (CVarGetInteger(CVAR_INPUT_VIEWER("CDown"), 1)) { + ImGui::SetNextItemAllowOverlap(); + ImGui::SetCursorPos(aPos); + RenderButton("C-Down", "C-Down Outline", pads[0].button & BTN_CDOWN, scaledBGSize, + useGlobalOutlineMode + ? buttonOutlineMode + : CVarGetInteger(CVAR_INPUT_VIEWER("CDownOutlineMode"), BUTTON_OUTLINE_NOT_PRESSED)); + } + + // L/R/Z + if (CVarGetInteger(CVAR_INPUT_VIEWER("LBtn"), 1)) { + ImGui::SetNextItemAllowOverlap(); + ImGui::SetCursorPos(aPos); + RenderButton("L-Btn", "L-Btn Outline", pads[0].button & BTN_L, scaledBGSize, + useGlobalOutlineMode + ? buttonOutlineMode + : CVarGetInteger(CVAR_INPUT_VIEWER("LBtnOutlineMode"), BUTTON_OUTLINE_NOT_PRESSED)); + } + if (CVarGetInteger(CVAR_INPUT_VIEWER("RBtn"), 1)) { + ImGui::SetNextItemAllowOverlap(); + ImGui::SetCursorPos(aPos); + RenderButton("R-Btn", "R-Btn Outline", pads[0].button & BTN_R, scaledBGSize, + useGlobalOutlineMode + ? buttonOutlineMode + : CVarGetInteger(CVAR_INPUT_VIEWER("RBtnOutlineMode"), BUTTON_OUTLINE_NOT_PRESSED)); + } + if (CVarGetInteger(CVAR_INPUT_VIEWER("ZBtn"), 1)) { + ImGui::SetNextItemAllowOverlap(); + ImGui::SetCursorPos(aPos); + RenderButton("Z-Btn", "Z-Btn Outline", pads[0].button & BTN_Z, scaledBGSize, + useGlobalOutlineMode + ? buttonOutlineMode + : CVarGetInteger(CVAR_INPUT_VIEWER("ZBtnOutlineMode"), BUTTON_OUTLINE_NOT_PRESSED)); + } + + // Start + if (CVarGetInteger(CVAR_INPUT_VIEWER("StartBtn"), 1)) { + ImGui::SetNextItemAllowOverlap(); + ImGui::SetCursorPos(aPos); + RenderButton("Start-Btn", "Start-Btn Outline", pads[0].button & BTN_START, scaledBGSize, + useGlobalOutlineMode ? buttonOutlineMode + : CVarGetInteger(CVAR_INPUT_VIEWER("StartBtnOutlineMode"), + BUTTON_OUTLINE_NOT_PRESSED)); + } + + // Dpad + if (CVarGetInteger(CVAR_INPUT_VIEWER("Dpad"), 0)) { + ImGui::SetNextItemAllowOverlap(); + ImGui::SetCursorPos(aPos); + RenderButton("Dpad-Left", "Dpad-Left Outline", pads[0].button & BTN_DLEFT, scaledBGSize, + useGlobalOutlineMode + ? buttonOutlineMode + : CVarGetInteger(CVAR_INPUT_VIEWER("DpadOutlineMode"), BUTTON_OUTLINE_NOT_PRESSED)); + ImGui::SetNextItemAllowOverlap(); + ImGui::SetCursorPos(aPos); + RenderButton("Dpad-Right", "Dpad-Right Outline", pads[0].button & BTN_DRIGHT, scaledBGSize, + useGlobalOutlineMode + ? buttonOutlineMode + : CVarGetInteger(CVAR_INPUT_VIEWER("DpadOutlineMode"), BUTTON_OUTLINE_NOT_PRESSED)); + ImGui::SetNextItemAllowOverlap(); + ImGui::SetCursorPos(aPos); + RenderButton("Dpad-Up", "Dpad-Up Outline", pads[0].button & BTN_DUP, scaledBGSize, + useGlobalOutlineMode + ? buttonOutlineMode + : CVarGetInteger(CVAR_INPUT_VIEWER("DpadOutlineMode"), BUTTON_OUTLINE_NOT_PRESSED)); + ImGui::SetNextItemAllowOverlap(); + ImGui::SetCursorPos(aPos); + RenderButton("Dpad-Down", "Dpad-Down Outline", pads[0].button & BTN_DDOWN, scaledBGSize, + useGlobalOutlineMode + ? buttonOutlineMode + : CVarGetInteger(CVAR_INPUT_VIEWER("DpadOutlineMode"), BUTTON_OUTLINE_NOT_PRESSED)); + } + // Ghostship Unused + // Modifier 1 + // if (CVarGetInteger(CVAR_INPUT_VIEWER("Mod1"), 0)) { + // ImGui::SetNextItemAllowOverlap(); + // ImGui::SetCursorPos(aPos); + // RenderButton("Modifier-1", "Modifier-1 Outline", pads[0].button & BTN_CUSTOM_MODIFIER1, scaledBGSize, + // useGlobalOutlineMode + // ? buttonOutlineMode + // : CVarGetInteger(CVAR_INPUT_VIEWER("Mod1OutlineMode"), BUTTON_OUTLINE_NOT_PRESSED)); + //} + // Modifier 2 + // if (CVarGetInteger(CVAR_INPUT_VIEWER("Mod2"), 0)) { + // ImGui::SetNextItemAllowOverlap(); + // ImGui::SetCursorPos(aPos); + // RenderButton("Modifier-2", "Modifier-2 Outline", pads[0].button & BTN_CUSTOM_MODIFIER2, scaledBGSize, + // useGlobalOutlineMode + // ? buttonOutlineMode + // : CVarGetInteger(CVAR_INPUT_VIEWER("Mod2OutlineMode"), BUTTON_OUTLINE_NOT_PRESSED)); + //} + + const bool analogStickIsInDeadzone = !pads[0].stick_x && !pads[0].stick_y; + const bool rightStickIsInDeadzone = !pads[0].right_stick_x && !pads[0].right_stick_y; + + // Analog Stick + const int analogOutlineMode = + CVarGetInteger(CVAR_INPUT_VIEWER("AnalogStick.OutlineMode"), STICK_MODE_ALWAYS_SHOWN); + const int32_t maxStickDistance = CVarGetInteger(CVAR_INPUT_VIEWER("AnalogStick.Movement"), 12); + if (analogOutlineMode == STICK_MODE_ALWAYS_SHOWN || + (analogOutlineMode == STICK_MODE_HIDDEN_IN_DEADZONE && !analogStickIsInDeadzone)) { + ImGui::SetNextItemAllowOverlap(); + ImGui::SetCursorPos(aPos); + ImGui::Image( + Ship::Context::GetInstance()->GetWindow()->GetGui()->GetTextureByName("Analog-Stick Outline"), + scaledBGSize, ImVec2(0, 0), ImVec2(1.0f, 1.0f)); + } + const int analogStickMode = + CVarGetInteger(CVAR_INPUT_VIEWER("AnalogStick.VisibilityMode"), STICK_MODE_ALWAYS_SHOWN); + if (analogStickMode == STICK_MODE_ALWAYS_SHOWN || + (analogStickMode == STICK_MODE_HIDDEN_IN_DEADZONE && !analogStickIsInDeadzone)) { + ImGui::SetNextItemAllowOverlap(); + ImGui::SetCursorPos( + ImVec2(aPos.x + maxStickDistance * ((float)(pads[0].stick_x) / MAX_AXIS_RANGE) * scale, + aPos.y - maxStickDistance * ((float)(pads[0].stick_y) / MAX_AXIS_RANGE) * scale)); + ImGui::Image(Ship::Context::GetInstance()->GetWindow()->GetGui()->GetTextureByName("Analog-Stick"), + scaledBGSize, ImVec2(0, 0), ImVec2(1.0f, 1.0f)); + } + + // Right Stick + const int32_t maxRightStickDistance = CVarGetInteger(CVAR_INPUT_VIEWER("RightStick.Movement"), 7); + const int rightOutlineMode = + CVarGetInteger(CVAR_INPUT_VIEWER("RightStick.OutlineMode"), STICK_MODE_ALWAYS_HIDDEN); + if (rightOutlineMode == STICK_MODE_ALWAYS_SHOWN || + (rightOutlineMode == STICK_MODE_HIDDEN_IN_DEADZONE && !rightStickIsInDeadzone)) { + ImGui::SetNextItemAllowOverlap(); + ImGui::SetCursorPos(aPos); + ImGui::Image( + Ship::Context::GetInstance()->GetWindow()->GetGui()->GetTextureByName("Right-Stick Outline"), + scaledBGSize, ImVec2(0, 0), ImVec2(1.0f, 1.0f)); + } + const int rightStickMode = + CVarGetInteger(CVAR_INPUT_VIEWER("RightStick.VisibilityMode"), STICK_MODE_ALWAYS_HIDDEN); + if (rightStickMode == STICK_MODE_ALWAYS_SHOWN || + (rightStickMode == STICK_MODE_HIDDEN_IN_DEADZONE && !rightStickIsInDeadzone)) { + ImGui::SetNextItemAllowOverlap(); + ImGui::SetCursorPos( + ImVec2(aPos.x + maxRightStickDistance * ((float)(pads[0].right_stick_x) / MAX_AXIS_RANGE) * scale, + aPos.y - maxRightStickDistance * ((float)(pads[0].right_stick_y) / MAX_AXIS_RANGE) * scale)); + ImGui::Image(Ship::Context::GetInstance()->GetWindow()->GetGui()->GetTextureByName("Right-Stick"), + scaledBGSize, ImVec2(0, 0), ImVec2(1.0f, 1.0f)); + } + + // Analog stick angle text + if (showAnalogAngles) { + ImGui::SetCursorPos( + ImVec2(aPos.x + 10 + CVarGetInteger(CVAR_INPUT_VIEWER("AnalogAngles.Offset"), 0) * scale, + scaledBGSize.y + aPos.y + 10)); + // Scale font with input viewer scale + float oldFontScale = ImGui::GetFont()->Scale; + ImGui::GetFont()->Scale *= scale * CVarGetFloat(CVAR_INPUT_VIEWER("AnalogAngles.Scale"), 1.0f); + ImGui::PushFont(ImGui::GetFont()); + + // Calculate polar R coordinate from X and Y angles, squared to avoid sqrt + const int32_t rSquared = pads[0].stick_x * pads[0].stick_x + pads[0].stick_y * pads[0].stick_y; + + // ESS range + const int range1Min = CVarGetInteger(CVAR_INPUT_VIEWER("AnalogAngles.Range1.Min"), 8); + const int range1Max = CVarGetInteger(CVAR_INPUT_VIEWER("AnalogAngles.Range1.Max"), 27); + // Walking speed range + const int range2Min = CVarGetInteger(CVAR_INPUT_VIEWER("AnalogAngles.Range2.Min"), 27); + const int range2Max = CVarGetInteger(CVAR_INPUT_VIEWER("AnalogAngles.Range2.Max"), 62); + + // Push color based on angle ranges + if (CVarGetInteger(CVAR_INPUT_VIEWER("AnalogAngles.Range1.Enabled"), 0) && + (rSquared >= (range1Min * range1Min)) && (rSquared < (range1Max * range1Max))) { + ImGui::PushStyleColor( + ImGuiCol_Text, VecFromRGBA8(CVarGetColor(CVAR_INPUT_VIEWER("AnalogAngles.Range1.Color.Value"), + range1ColorDefault))); + } else if (CVarGetInteger(CVAR_INPUT_VIEWER("AnalogAngles.Range2.Enabled"), 0) && + (rSquared >= (range2Min * range2Min)) && (rSquared < (range2Max * range2Max))) { + ImGui::PushStyleColor( + ImGuiCol_Text, VecFromRGBA8(CVarGetColor(CVAR_INPUT_VIEWER("AnalogAngles.Range2.Color.Value"), + range2ColorDefault))); + } else { + ImGui::PushStyleColor(ImGuiCol_Text, + VecFromRGBA8(CVarGetColor(CVAR_INPUT_VIEWER("AnalogAngles.TextColor.Value"), + textColorDefault))); + } + + // Render text + ImGui::Text("X: %-3d Y: %-3d", pads[0].stick_x, pads[0].stick_y); + // Restore original color + ImGui::PopStyleColor(); + // Restore original font scale + ImGui::GetFont()->Scale = oldFontScale; + ImGui::PopFont(); + } + + ImGui::End(); + } + + ImGui::PopStyleVar(); + ImGui::PopStyleColor(); + } +} + +InputViewerSettingsWindow::~InputViewerSettingsWindow() { + SPDLOG_TRACE("destruct input viewer settings window"); +} + +void InputViewerSettingsWindow::DrawElement() { + // gInputViewer.Scale + CVarSliderFloat("Input Viewer Scale: %.2f", CVAR_INPUT_VIEWER("Scale"), + FloatSliderOptions() + .Color(THEME_COLOR) + .DefaultValue(1.0f) + .Min(0.1f) + .Max(5.0f) + .ShowButtons(true) + .Tooltip("Sets the on screen size of the input viewer")); + + // gInputViewer.EnableDragging + CVarCheckbox("Enable Dragging", CVAR_INPUT_VIEWER("EnableDragging"), + CheckboxOptions().Color(THEME_COLOR).DefaultValue(true)); + + UIWidgets::PaddedSeparator(true, true); + + // gInputViewer.ShowBackground + CVarCheckbox("Show Background Layer", CVAR_INPUT_VIEWER("ShowBackground"), + CheckboxOptions().Color(THEME_COLOR).DefaultValue(true)); + + UIWidgets::PaddedSeparator(true, true); + + PushStyleHeader(THEME_COLOR); + if (ImGui::CollapsingHeader("Buttons")) { + + // gInputViewer.ButtonOutlineMode + CVarCombobox( + "Button Outlines/Backgrounds", CVAR_INPUT_VIEWER("ButtonOutlineMode"), buttonOutlineOptions, + ComboboxOptions({ { .disabled = !CVarGetInteger(CVAR_INPUT_VIEWER("UseGlobalButtonOutlineMode"), 1), + .disabledTooltip = "Disabled because Global Button Outline is off" } }) + .Color(THEME_COLOR) + .DefaultIndex(BUTTON_OUTLINE_NOT_PRESSED) + .Tooltip("Sets the desired visibility behavior for the button outline/background layers. Useful for " + "custom input viewers.")); + + // gInputViewer.UseGlobalButtonOutlineMode + CVarCheckbox("Use for all buttons", CVAR_INPUT_VIEWER("UseGlobalButtonOutlineMode"), + CheckboxOptions().Color(THEME_COLOR).DefaultValue(true)); + + UIWidgets::PaddedSeparator(); + + bool useIndividualOutlines = !CVarGetInteger(CVAR_INPUT_VIEWER("UseGlobalButtonOutlineMode"), 1); + + // gInputViewer.ABtn + CVarCheckbox("Show A-Button Layers", CVAR_INPUT_VIEWER("ABtn"), + CheckboxOptions().Color(THEME_COLOR).DefaultValue(true)); + if (useIndividualOutlines && CVarGetInteger(CVAR_INPUT_VIEWER("ABtn"), 1)) { + ImGui::Indent(); + CVarCombobox("##ABtnOutline", CVAR_INPUT_VIEWER("ABtnOutlineMode"), buttonOutlineOptionsVerbose, + ComboboxOptions().Color(THEME_COLOR).DefaultIndex(BUTTON_OUTLINE_NOT_PRESSED)); + ImGui::Unindent(); + } + // gInputViewer.BBtn + CVarCheckbox("Show B-Button Layers", CVAR_INPUT_VIEWER("BBtn"), + CheckboxOptions().Color(THEME_COLOR).DefaultValue(true)); + if (useIndividualOutlines && CVarGetInteger(CVAR_INPUT_VIEWER("BBtn"), 1)) { + ImGui::Indent(); + CVarCombobox("##BBtnOutline", CVAR_INPUT_VIEWER("BBtnOutlineMode"), buttonOutlineOptionsVerbose, + ComboboxOptions().Color(THEME_COLOR).DefaultIndex(BUTTON_OUTLINE_NOT_PRESSED)); + ImGui::Unindent(); + } + // gInputViewer.CUp + CVarCheckbox("Show C-Up Layers", CVAR_INPUT_VIEWER("CUp"), + CheckboxOptions().Color(THEME_COLOR).DefaultValue(true)); + if (useIndividualOutlines && CVarGetInteger(CVAR_INPUT_VIEWER("CUp"), 1)) { + ImGui::Indent(); + CVarCombobox("##CUpOutline", CVAR_INPUT_VIEWER("CUpOutlineMode"), buttonOutlineOptionsVerbose, + ComboboxOptions().Color(THEME_COLOR).DefaultIndex(BUTTON_OUTLINE_NOT_PRESSED)); + ImGui::Unindent(); + } + // gInputViewer.CRight + CVarCheckbox("Show C-Right Layers", CVAR_INPUT_VIEWER("CRight"), + CheckboxOptions().Color(THEME_COLOR).DefaultValue(true)); + if (useIndividualOutlines && CVarGetInteger(CVAR_INPUT_VIEWER("CRight"), 1)) { + ImGui::Indent(); + CVarCombobox("##CRightOutline", CVAR_INPUT_VIEWER("CRightOutlineMode"), buttonOutlineOptionsVerbose, + ComboboxOptions().Color(THEME_COLOR).DefaultIndex(BUTTON_OUTLINE_NOT_PRESSED)); + ImGui::Unindent(); + } + // gInputViewer.CDown + CVarCheckbox("Show C-Down Layers", CVAR_INPUT_VIEWER("CDown"), + CheckboxOptions().Color(THEME_COLOR).DefaultValue(true)); + if (useIndividualOutlines && CVarGetInteger(CVAR_INPUT_VIEWER("CDown"), 1)) { + ImGui::Indent(); + CVarCombobox("##CDownOutline", CVAR_INPUT_VIEWER("CDownOutlineMode"), buttonOutlineOptionsVerbose, + ComboboxOptions().Color(THEME_COLOR).DefaultIndex(BUTTON_OUTLINE_NOT_PRESSED)); + ImGui::Unindent(); + } + // gInputViewer.CLeft + CVarCheckbox("Show C-Left Layers", CVAR_INPUT_VIEWER("CLeft"), + CheckboxOptions().Color(THEME_COLOR).DefaultValue(true)); + if (useIndividualOutlines && CVarGetInteger(CVAR_INPUT_VIEWER("CLeft"), 1)) { + ImGui::Indent(); + CVarCombobox("##CLeftOutline", CVAR_INPUT_VIEWER("CLeftOutlineMode"), buttonOutlineOptionsVerbose, + ComboboxOptions().Color(THEME_COLOR).DefaultIndex(BUTTON_OUTLINE_NOT_PRESSED)); + ImGui::Unindent(); + } + // gInputViewer.LBtn + CVarCheckbox("Show L-Button Layers", CVAR_INPUT_VIEWER("LBtn"), + CheckboxOptions().Color(THEME_COLOR).DefaultValue(true)); + if (useIndividualOutlines && CVarGetInteger(CVAR_INPUT_VIEWER("LBtn"), 1)) { + ImGui::Indent(); + CVarCombobox("##LBtnOutline", CVAR_INPUT_VIEWER("LBtnOutlineMode"), buttonOutlineOptionsVerbose, + ComboboxOptions().Color(THEME_COLOR).DefaultIndex(BUTTON_OUTLINE_NOT_PRESSED)); + ImGui::Unindent(); + } + // gInputViewer.RBtn + CVarCheckbox("Show R-Button Layers", CVAR_INPUT_VIEWER("RBtn"), + CheckboxOptions().Color(THEME_COLOR).DefaultValue(true)); + if (useIndividualOutlines && CVarGetInteger(CVAR_INPUT_VIEWER("RBtn"), 1)) { + ImGui::Indent(); + CVarCombobox("##RBtnOutline", CVAR_INPUT_VIEWER("RBtnOutlineMode"), buttonOutlineOptionsVerbose, + ComboboxOptions().Color(THEME_COLOR).DefaultIndex(BUTTON_OUTLINE_NOT_PRESSED)); + ImGui::Unindent(); + } + // gInputViewer.ZBtn + CVarCheckbox("Show Z-Button Layers", CVAR_INPUT_VIEWER("ZBtn"), + CheckboxOptions().Color(THEME_COLOR).DefaultValue(true)); + if (useIndividualOutlines && CVarGetInteger(CVAR_INPUT_VIEWER("ZBtn"), 1)) { + ImGui::Indent(); + CVarCombobox("##ZBtnOutline", CVAR_INPUT_VIEWER("ZBtnOutlineMode"), buttonOutlineOptionsVerbose, + ComboboxOptions().Color(THEME_COLOR).DefaultIndex(BUTTON_OUTLINE_NOT_PRESSED)); + ImGui::Unindent(); + } + // gInputViewer.StartBtn + CVarCheckbox("Show Start Button Layers", CVAR_INPUT_VIEWER("StartBtn"), + CheckboxOptions().Color(THEME_COLOR).DefaultValue(true)); + if (useIndividualOutlines && CVarGetInteger(CVAR_INPUT_VIEWER("StartBtn"), 1)) { + ImGui::Indent(); + CVarCombobox("##StartBtnOutline", CVAR_INPUT_VIEWER("StartBtnOutlineMode"), buttonOutlineOptionsVerbose, + ComboboxOptions().Color(THEME_COLOR).DefaultIndex(BUTTON_OUTLINE_NOT_PRESSED)); + ImGui::Unindent(); + } + // gInputViewer.Dpad + CVarCheckbox("Show D-Pad Layers", CVAR_INPUT_VIEWER("Dpad"), + CheckboxOptions().Color(THEME_COLOR).DefaultValue(false)); + if (useIndividualOutlines && CVarGetInteger(CVAR_INPUT_VIEWER("Dpad"), 0)) { + ImGui::Indent(); + CVarCombobox("##DpadOutline", CVAR_INPUT_VIEWER("DpadOutlineMode"), buttonOutlineOptionsVerbose, + ComboboxOptions().Color(THEME_COLOR).DefaultIndex(BUTTON_OUTLINE_NOT_PRESSED)); + ImGui::Unindent(); + } + // gInputViewer.Mod1 + CVarCheckbox("Show Modifier Button 1 Layers", CVAR_INPUT_VIEWER("Mod1"), + CheckboxOptions().Color(THEME_COLOR).DefaultValue(false)); + if (useIndividualOutlines && CVarGetInteger(CVAR_INPUT_VIEWER("Mod1"), 0)) { + ImGui::Indent(); + CVarCombobox("##Mmod1Outline", CVAR_INPUT_VIEWER("Mod1OutlineMode"), buttonOutlineOptionsVerbose, + ComboboxOptions().Color(THEME_COLOR).DefaultIndex(BUTTON_OUTLINE_NOT_PRESSED)); + ImGui::Unindent(); + } + // gInputViewer.Mod2 + CVarCheckbox("Show Modifier Button 2 Layers", CVAR_INPUT_VIEWER("Mod2"), + CheckboxOptions().Color(THEME_COLOR).DefaultValue(false)); + if (useIndividualOutlines && CVarGetInteger(CVAR_INPUT_VIEWER("Mod2"), 0)) { + ImGui::Indent(); + CVarCombobox("##Mod2Outline", CVAR_INPUT_VIEWER("Mod2OutlineMode"), buttonOutlineOptionsVerbose, + ComboboxOptions().Color(THEME_COLOR).DefaultIndex(BUTTON_OUTLINE_NOT_PRESSED)); + ImGui::Unindent(); + } + + UIWidgets::PaddedSeparator(true, true); + } + + if (ImGui::CollapsingHeader("Analog Stick")) { + // gInputViewer.AnalogStick.VisibilityMode + CVarCombobox( + "Analog Stick Visibility", CVAR_INPUT_VIEWER("AnalogStick.VisibilityMode"), stickModeOptions, + ComboboxOptions() + .Color(THEME_COLOR) + .DefaultIndex(STICK_MODE_ALWAYS_SHOWN) + .Tooltip( + "Determines the conditions under which the moving layer of the analog stick texture is visible.")); + + // gInputViewer.AnalogStick.OutlineMode + CVarCombobox( + "Analog Stick Outline/Background Visibility", CVAR_INPUT_VIEWER("AnalogStick.OutlineMode"), + stickModeOptions, + ComboboxOptions() + .Color(THEME_COLOR) + .DefaultIndex(STICK_MODE_ALWAYS_SHOWN) + .Tooltip( + "Determines the conditions under which the analog stick outline/background texture is visible.")); + + // gInputViewer.AnalogStick.Movement + CVarSliderInt("Analog Stick Movement: %dpx", CVAR_INPUT_VIEWER("AnalogStick.Movement"), + IntSliderOptions() + .Color(THEME_COLOR) + .Min(0) + .Max(200) + .DefaultValue(12) + .ShowButtons(true) + .Tooltip("Sets the distance to move the analog stick in the input viewer. Useful for custom " + "input viewers.")); + UIWidgets::PaddedSeparator(true, true); + } + + if (ImGui::CollapsingHeader("Additional (\"Right\") Stick")) { + // gInputViewer.RightStick.VisibilityMode + CVarCombobox( + "Right Stick Visibility", CVAR_INPUT_VIEWER("RightStick.VisibilityMode"), stickModeOptions, + ComboboxOptions() + .Color(THEME_COLOR) + .DefaultIndex(STICK_MODE_ALWAYS_HIDDEN) + .Tooltip( + "Determines the conditions under which the moving layer of the right stick texture is visible.")); + + // gInputViewer.RightStick.OutlineMode + CVarCombobox( + "Right Stick Outline/Background Visibility", CVAR_INPUT_VIEWER("RightStick.OutlineMode"), stickModeOptions, + ComboboxOptions() + .Color(THEME_COLOR) + .DefaultIndex(STICK_MODE_ALWAYS_HIDDEN) + .Tooltip( + "Determines the conditions under which the right stick outline/background texture is visible.")); + + // gInputViewer.RightStick.Movement + CVarSliderInt( + "Right Stick Movement: %dpx", CVAR_INPUT_VIEWER("RightStick.Movement"), + IntSliderOptions() + .Color(THEME_COLOR) + .Min(0) + .Max(200) + .DefaultValue(7) + .ShowButtons(true) + .Tooltip( + "Sets the distance to move the right stick in the input viewer. Useful for custom input viewers.")); + UIWidgets::PaddedSeparator(true, true); + } + + if (ImGui::CollapsingHeader("Analog Angle Values")) { + // gAnalogAngles + CVarCheckbox( + "Show Analog Stick Angle Values", CVAR_INPUT_VIEWER("AnalogAngles.Enabled"), + CheckboxOptions().Color(THEME_COLOR).Tooltip("Displays analog stick angle values in the input viewer")); + if (CVarGetInteger(CVAR_INPUT_VIEWER("AnalogAngles.Enabled"), 0)) { + // gInputViewer.AnalogAngles.TextColor + CVarColorPicker("Text Color", CVAR_INPUT_VIEWER("AnalogAngles.TextColor"), textColorDefault, true, + ColorPickerRandomButton | ColorPickerResetButton); + // gAnalogAngleScale + CVarSliderFloat("Angle Text Scale: %.2f%%", CVAR_INPUT_VIEWER("AnalogAngles.Scale"), + FloatSliderOptions() + .Color(THEME_COLOR) + .IsPercentage() + .Min(0.1f) + .Max(5.0f) + .DefaultValue(1.0f) + .ShowButtons(true)); + // gInputViewer.AnalogAngles.Offset + CVarSliderInt("Angle Text Offset: %dpx", CVAR_INPUT_VIEWER("AnalogAngles.Offset"), + IntSliderOptions() + .Color(THEME_COLOR) + .Min(0) + .Max(400) + .DefaultValue(0) + .ShowButtons(true) + .Tooltip("Sets the distance to move the right stick in the input viewer. Useful for " + "custom input viewers.")); + UIWidgets::PaddedSeparator(true, true); + // gInputViewer.AnalogAngles.Range1.Enabled + CVarCheckbox( + "Highlight ESS Position", CVAR_INPUT_VIEWER("AnalogAngles.Range1.Enabled"), + CheckboxOptions() + .Color(THEME_COLOR) + .Tooltip( + "Highlights the angle value text when the analog stick is in ESS position (on flat ground)")); + if (CVarGetInteger(CVAR_INPUT_VIEWER("AnalogAngles.Range1.Enabled"), 0)) { + // gInputViewer.AnalogAngles.Range1.Color + CVarColorPicker("ESS Color", CVAR_INPUT_VIEWER("AnalogAngles.Range1.Color"), range1ColorDefault, true, + ColorPickerRandomButton | ColorPickerResetButton); + } + + UIWidgets::PaddedSeparator(true, true); + // gInputViewer.AnalogAngles.Range2.Enabled + CVarCheckbox("Highlight Walking Speed Angles", CVAR_INPUT_VIEWER("AnalogAngles.Range2.Enabled"), + CheckboxOptions() + .Color(THEME_COLOR) + .Tooltip("Highlights the angle value text when the analog stick is at an angle that would " + "produce a walking speed (on flat ground)\n\n" + "Useful for 1.0 Empty Jumpslash Quick Put Away")); + if (CVarGetInteger(CVAR_INPUT_VIEWER("AnalogAngles.Range2.Enabled"), 0)) { + // gInputViewer.AnalogAngles.Range2.Color + CVarColorPicker("Walking Speed Color", CVAR_INPUT_VIEWER("AnalogAngles.Range2.Color"), + range2ColorDefault, true, ColorPickerRandomButton | ColorPickerResetButton); + } + } + } + PopStyleHeader(); +} diff --git a/src/port/ui/InputViewer.h b/src/port/ui/InputViewer.h new file mode 100644 index 00000000..67303b7e --- /dev/null +++ b/src/port/ui/InputViewer.h @@ -0,0 +1,46 @@ +#pragma once + +#include + +#define CVAR_INPUT_VIEWER(var) "gInputViewer." var + +typedef enum { + BUTTON_OUTLINE_ALWAYS_SHOWN, + BUTTON_OUTLINE_NOT_PRESSED, + BUTTON_OUTLINE_PRESSED, + BUTTON_OUTLINE_ALWAYS_HIDDEN +} ButtonOutlineMode; + +typedef enum { + STICK_MODE_ALWAYS_SHOWN, + STICK_MODE_HIDDEN_IN_DEADZONE, + STICK_MODE_ALWAYS_HIDDEN, +} StickMode; + +class InputViewer final : public Ship::GuiWindow { + public: + using GuiWindow::GuiWindow; + + void Draw() override; + void InitElement() override{}; + void DrawElement() override; + void UpdateElement() override{}; + + InputViewer(); + ~InputViewer(); + + private: + void RenderButton(std::string btn, std::string btnOutline, int state, ImVec2 size, int outlineMode); +}; + +class InputViewerSettingsWindow final : public Ship::GuiWindow { + public: + using GuiWindow::GuiWindow; + + void InitElement() override{}; + void DrawElement() override; + void UpdateElement() override{}; + + InputViewerSettingsWindow(); + ~InputViewerSettingsWindow(); +}; diff --git a/src/port/ui/Menu.cpp b/src/port/ui/Menu.cpp new file mode 100644 index 00000000..64602029 --- /dev/null +++ b/src/port/ui/Menu.cpp @@ -0,0 +1,941 @@ +#include "Menu.h" +#include "UIWidgets.hpp" +#include "port/Engine.h" +#include "cvar_prefixes.h" +#include "GhostshipInputEditorWindow.h" +#include +#include +#include "GhostshipModals.h" +#include +#include +#include "variables.h" +#include + +std::vector windowTypeSizes = { {} }; + +extern std::unordered_map warpPointSceneList; +extern void Warp(); + +namespace GhostshipGui { +extern std::shared_ptr mModalWindow; +} +std::vector extraSearchWidgets = {}; + +namespace Ship { +std::string disabledTempTooltip; +const char* disabledTooltip; +bool disabledValue = false; + +bool navigateToWidget = false; +const char* navigateMainEntry = ""; +const char* navigateSidebar = ""; +std::string navigateWidgetName = ""; +bool highlightWidget = false; + +bool operator==(Color_RGB8 const& l, Color_RGB8 const& r) noexcept { + return l.r == r.r && l.g == r.g && l.b == r.b; +} + +bool operator==(Color_RGBA8 const& l, Color_RGBA8 const& r) noexcept { + return l.r == r.r && l.g == r.g && l.b == r.b && l.a == r.a; +} + +bool operator<(Color_RGB8 const& l, Color_RGB8 const& r) noexcept { + return (l.r < r.r && l.g <= r.g && l.b <= r.b) || (l.r <= r.r && l.g < r.g && l.b <= r.b) || + (l.r <= r.r && l.g <= r.g && l.b < r.b); +} + +bool operator<(Color_RGBA8 const& l, Color_RGBA8 const& r) noexcept { + return (l.r < r.r && l.g <= r.g && l.b <= r.b && l.a <= r.a) || + (l.r <= r.r && l.g < r.g && l.b <= r.b && l.a <= r.a) || + (l.r <= r.r && l.g <= r.g && l.b < r.b && l.a <= r.a) || + (l.r <= r.r && l.g <= r.g && l.b <= r.b && l.a < r.a); +} + +bool operator>(Color_RGB8 const& l, Color_RGB8 const& r) noexcept { + return (l.r > r.r && l.g >= r.g && l.b >= r.b) || (l.r >= r.r && l.g > r.g && l.b >= r.b) || + (l.r >= r.r && l.g >= r.g && l.b > r.b); +} + +bool operator>(Color_RGBA8 const& l, Color_RGBA8 const& r) noexcept { + return (l.r > r.r && l.g >= r.g && l.b >= r.b && l.a >= r.a) || + (l.r >= r.r && l.g > r.g && l.b >= r.b && l.a >= r.a) || + (l.r >= r.r && l.g >= r.g && l.b > r.b && l.a >= r.a) || + (l.r >= r.r && l.g >= r.g && l.b >= r.b && l.a > r.a); +} + +uint32_t GetVectorIndexOf(std::vector& vector, std::string value) { + return std::distance(vector.begin(), std::find(vector.begin(), vector.end(), value)); +} + +static bool raceDisableActive = false; + +void Menu::InsertSidebarSearch() { + menuEntries["Settings"].sidebars.emplace("Search", searchSidebarEntry); + uint32_t curIndex = 0; + if (!Ship_IsCStringEmpty(CVarGetString(menuEntries["Settings"].sidebarCvar, ""))) { + curIndex = GetVectorIndexOf(menuEntries["Settings"].sidebarOrder, + CVarGetString(menuEntries["Settings"].sidebarCvar, "")); + } + menuEntries["Settings"].sidebarOrder.insert(menuEntries["Settings"].sidebarOrder.begin() + searchSidebarIndex, + "Search"); + if (curIndex > searchSidebarIndex) { + CVarSetString(menuEntries["Settings"].sidebarCvar, menuEntries["Settings"].sidebarOrder.at(curIndex).c_str()); + } +} + +void Menu::RemoveSidebarSearch() { + uint32_t curIndex = GetVectorIndexOf(menuEntries["Settings"].sidebarOrder, + CVarGetString(menuEntries["Settings"].sidebarCvar, "General")); + menuEntries["Settings"].sidebars.erase("Search"); + std::erase_if(menuEntries["Settings"].sidebarOrder, [](std::string& name) { return name == "Search"; }); + if (curIndex > searchSidebarIndex) { + curIndex--; + } else if (curIndex >= menuEntries["Settings"].sidebarOrder.size()) { + curIndex = menuEntries["Settings"].sidebarOrder.size() - 1; + } + CVarSetString(menuEntries["Settings"].sidebarCvar, menuEntries["Settings"].sidebarOrder.at(curIndex).c_str()); +} + +void Menu::UpdateWindowBackendObjects() { + Ship::WindowBackend runningWindowBackend = Ship::Context::GetInstance()->GetWindow()->GetWindowBackend(); + int32_t configWindowBackendId = Ship::Context::GetInstance()->GetConfig()->GetInt("Window.Backend.Id", -1); + if (Ship::Context::GetInstance()->GetWindow()->IsAvailableWindowBackend(configWindowBackendId)) { + configWindowBackend = static_cast(configWindowBackendId); + } else { + configWindowBackend = runningWindowBackend; + } + + availableWindowBackends = Ship::Context::GetInstance()->GetWindow()->GetAvailableWindowBackends(); + for (auto& backend : *availableWindowBackends) { + availableWindowBackendsMap[backend] = windowBackendsMap.at(backend); + } +} + +bool Menu::IsMenuPopped() { + return popped; +} + +UIWidgets::Colors Menu::GetMenuThemeColor() { + return menuThemeIndex; +} + +Menu::Menu(const std::string& cVar, const std::string& name, uint8_t searchSidebarIndex_, + UIWidgets::Colors defaultThemeIndex_) + : GuiWindow(cVar, name), searchSidebarIndex(searchSidebarIndex_), defaultThemeIndex(defaultThemeIndex_) { +} + +void Menu::InitElement() { + popped = CVarGetInteger(CVAR_SETTING("Menu.Popout"), 0); + poppedSize.x = CVarGetInteger(CVAR_SETTING("Menu.PoppedWidth"), 1280); + poppedSize.y = CVarGetInteger(CVAR_SETTING("Menu.PoppedHeight"), 800); + poppedPos.x = CVarGetInteger(CVAR_SETTING("Menu.PoppedPos.x"), 0); + poppedPos.y = CVarGetInteger(CVAR_SETTING("Menu.PoppedPos.y"), 0); + menuThemeIndex = static_cast(CVarGetInteger(CVAR_SETTING("Menu.Theme"), defaultThemeIndex)); + + UpdateWindowBackendObjects(); +} + +void Menu::UpdateElement() { + menuThemeIndex = static_cast(CVarGetInteger(CVAR_SETTING("Menu.Theme"), defaultThemeIndex)); +} + +bool ModernMenuSidebarEntry(std::string label) { + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + ImGuiStyle& style = ImGui::GetStyle(); + ImVec2 pos = window->DC.CursorPos; + const ImGuiID sidebarId = window->GetID(std::string(label + "##Sidebar").c_str()); + ImVec2 labelSize = ImGui::CalcTextSize(label.c_str(), ImGui::FindRenderedTextEnd(label.c_str()), true); + pos.y += style.FramePadding.y; + pos.x = window->WorkRect.GetCenter().x - labelSize.x / 2; + ImRect bb = { pos - style.FramePadding, pos + labelSize + style.FramePadding }; + ImGui::ItemSize(bb, style.FramePadding.y); + ImGui::ItemAdd(bb, sidebarId); + bool hovered, held; + bool pressed = ImGui::ButtonBehavior(bb, sidebarId, &hovered, &held); + if (pressed) { + ImGui::MarkItemEdited(sidebarId); + } + window->DrawList->AddRectFilled(pos - style.FramePadding, pos + labelSize + style.FramePadding, + ImGui::GetColorU32((held && hovered) ? ImGuiCol_ButtonActive + : hovered ? ImGuiCol_ButtonHovered + : ImGuiCol_Button), + 3.0f); + UIWidgets::RenderText(pos, label.c_str(), ImGui::FindRenderedTextEnd(label.c_str()), true); + return pressed; +} + +bool ModernMenuHeaderEntry(std::string label) { + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + ImGuiStyle& style = ImGui::GetStyle(); + ImVec2 pos = window->DC.CursorPos; + const ImGuiID headerId = window->GetID(std::string(label + "##Header").c_str()); + ImVec2 labelSize = ImGui::CalcTextSize(label.c_str(), ImGui::FindRenderedTextEnd(label.c_str()), true); + ImRect bb = { pos, pos + labelSize + style.FramePadding * 2 }; + ImGui::ItemSize(bb, style.FramePadding.y); + ImGui::ItemAdd(bb, headerId); + bool hovered, held; + bool pressed = ImGui::ButtonBehavior(bb, headerId, &hovered, &held); + window->DrawList->AddRectFilled(bb.Min, bb.Max, + ImGui::GetColorU32((held && hovered) ? ImGuiCol_ButtonActive + : hovered ? ImGuiCol_ButtonHovered + : ImGuiCol_Button), + 3.0f); + pos += style.FramePadding; + UIWidgets::RenderText(pos, label.c_str(), ImGui::FindRenderedTextEnd(label.c_str()), true); + return pressed; +} + +uint32_t Menu::DrawSearchResults(std::string& menuSearchText) { + int searchCount = 0; + std::transform(menuSearchText.begin(), menuSearchText.end(), menuSearchText.begin(), ::tolower); + menuSearchText.erase(std::remove(menuSearchText.begin(), menuSearchText.end(), ' '), menuSearchText.end()); + ImGui::SetNextWindowSizeConstraints({ ImGui::GetContentRegionAvail().x / 2, 0 }, + { ImGui::GetContentRegionAvail().x / 2, ImGui::GetContentRegionAvail().y }); + if (ImGui::BeginChild("Search Results Col 1", { ImGui::GetContentRegionAvail().x / 2, 0 }, + ImGuiChildFlags_AutoResizeY, ImGuiWindowFlags_NoTitleBar)) { + for (auto& menuLabel : menuOrder) { + auto& menuEntry = menuEntries.at(menuLabel); + for (auto& sidebarLabel : menuEntry.sidebarOrder) { + auto& sidebar = menuEntry.sidebars[sidebarLabel]; + for (int i = 0; i < sidebar.columnWidgets.size(); i++) { + auto& column = sidebar.columnWidgets.at(i); + for (auto& info : column) { + if (info.type == WIDGET_SEARCH || info.type == WIDGET_SEPARATOR || + info.type == WIDGET_SEPARATOR_TEXT || info.isHidden || info.hideInSearch) { + continue; + } + const char* tooltip = info.options->tooltip; + std::string widgetStr = std::string(info.name) + std::string(tooltip != NULL ? tooltip : ""); + std::transform(widgetStr.begin(), widgetStr.end(), widgetStr.begin(), ::tolower); + widgetStr.erase(std::remove(widgetStr.begin(), widgetStr.end(), ' '), widgetStr.end()); + if (widgetStr.find(menuSearchText) != std::string::npos) { + UIWidgets::ComponentAlignments backupAlignment; + UIWidgets::LabelPositions backupLabelPos; + if (info.type == WIDGET_COMBOBOX || info.type == WIDGET_CVAR_COMBOBOX) { + backupAlignment = + std::static_pointer_cast(info.options)->alignment; + backupLabelPos = + std::static_pointer_cast(info.options)->labelPosition; + std::static_pointer_cast(info.options)->alignment = + UIWidgets::ComponentAlignments::Left; + std::static_pointer_cast(info.options)->labelPosition = + UIWidgets::LabelPositions::Above; + } + MenuDrawItem(info, 400, menuThemeIndex); + ImGui::PushStyleColor(ImGuiCol_Text, UIWidgets::ColorValues.at(UIWidgets::Colors::Gray)); + std::string origin = + fmt::format(" ({} -> {}, Col {})", menuEntry.label, sidebarLabel, i + 1); + ImGui::Text("%s", origin.c_str()); + ImGui::PopStyleColor(); + searchCount++; + if (info.type == WIDGET_COMBOBOX || info.type == WIDGET_CVAR_COMBOBOX) { + std::static_pointer_cast(info.options)->alignment = + backupAlignment; + std::static_pointer_cast(info.options)->labelPosition = + backupLabelPos; + } + } + } + } + } + } + for (auto& entry : extraSearchWidgets) { + if (entry.info.type == WIDGET_SEARCH || entry.info.type == WIDGET_SEPARATOR || + entry.info.type == WIDGET_SEPARATOR_TEXT || entry.info.isHidden || entry.info.hideInSearch) { + continue; + } + std::string widgetStr = + entry.info.name + entry.info.options->tooltip + entry.extraTerms + entry.sidebarName; + std::transform(widgetStr.begin(), widgetStr.end(), widgetStr.begin(), ::tolower); + widgetStr.erase(std::remove(widgetStr.begin(), widgetStr.end(), ' '), widgetStr.end()); + if (widgetStr.find(menuSearchText) != std::string::npos) { + MenuDrawItem(entry.info, 400, menuThemeIndex); + ImGui::PushStyleColor(ImGuiCol_Text, UIWidgets::ColorValues.at(UIWidgets::Colors::Gray)); + std::string origin = fmt::format(" ({} -> {}, {})", entry.menuName, entry.sidebarName, entry.location); + ImGui::Text("%s", origin.c_str()); + ImGui::PopStyleColor(); + searchCount++; + } + } + ImGui::EndChild(); + } + return searchCount; +} + +void Menu::AddMenuEntry(std::string entryName, const char* entryCvar) { + menuEntries.emplace(entryName, MainMenuEntry{ entryName, entryCvar }); + menuOrder.push_back(entryName); +} + +void Menu::AddSearchWidget(SearchWidget widget) { + extraSearchWidgets.push_back(widget); +} + +std::unordered_map& Menu::GetDisabledMap() { + return disabledMap; +} + +void Menu::MenuDrawItem(WidgetInfo& widget, uint32_t width, UIWidgets::Colors menuThemeIndex) { + disabledTempTooltip = "This setting is disabled because: \n"; + disabledValue = false; + disabledTooltip = " "; + + if (widget.preFunc != nullptr) { + widget.ResetDisables(); + widget.preFunc(widget); + if (widget.isHidden) { + return; + } + if (!widget.activeDisables.empty()) { + widget.options->disabled = true; + for (auto option : widget.activeDisables) { + disabledTempTooltip += std::string("\n- ") + disabledMap.at(option).reason; + } + widget.options->disabledTooltip = disabledTempTooltip.c_str(); + } + } + if (widget.raceDisable && raceDisableActive) { + widget.options->disabled = true; + disabledTempTooltip += std::string("\n- Race Lockout Active"); + widget.options->disabledTooltip = disabledTempTooltip.c_str(); + } + + if (widget.sameLine) { + ImGui::SameLine(); + } + + try { + switch (widget.type) { + case WIDGET_CHECKBOX: { + bool* pointer = std::get(widget.valuePointer); + if (pointer == nullptr) { + SPDLOG_ERROR("Checkbox Widget requires a value pointer, currently nullptr"); + assert(false); + return; + } + auto options = std::static_pointer_cast(widget.options); + options->color = menuThemeIndex; + if (UIWidgets::Checkbox(UIWidgets::WrappedText(widget.name.c_str(), width).c_str(), pointer, + *options)) { + if (widget.callback != nullptr) { + widget.callback(widget); + } + } + } break; + case WIDGET_CVAR_CHECKBOX: { + auto options = std::static_pointer_cast(widget.options); + options->color = menuThemeIndex; + if (UIWidgets::CVarCheckbox(UIWidgets::WrappedText(widget.name.c_str(), width).c_str(), widget.cVar, + *options)) { + if (widget.callback != nullptr) { + widget.callback(widget); + } + }; + } break; + case WIDGET_AUDIO_BACKEND: { + auto currentAudioBackend = Ship::Context::GetInstance()->GetAudio()->GetCurrentAudioBackend(); + UIWidgets::ComboboxOptions options = {}; + options.color = menuThemeIndex; + options.tooltip = "Sets the audio API used by the game. Requires a relaunch to take effect."; + options.disabled = Ship::Context::GetInstance()->GetAudio()->GetAvailableAudioBackends()->size() <= 1; + options.disabledTooltip = "Only one audio API is available on this platform."; + if (UIWidgets::Combobox("Audio API", ¤tAudioBackend, audioBackendsMap, options)) { + Ship::Context::GetInstance()->GetAudio()->SetCurrentAudioBackend(currentAudioBackend); + } + } break; + case WIDGET_VIDEO_BACKEND: { + UIWidgets::ComboboxOptions options = {}; + options.color = menuThemeIndex; + options.tooltip = "Sets the renderer API used by the game."; + options.disabled = availableWindowBackends->size() <= 1; + options.disabledTooltip = "Only one renderer API is available on this platform."; + if (UIWidgets::Combobox("Renderer API (Needs reload)", &configWindowBackend, availableWindowBackendsMap, + options)) { + Ship::Context::GetInstance()->GetConfig()->SetInt("Window.Backend.Id", + (int32_t)(configWindowBackend)); + Ship::Context::GetInstance()->GetConfig()->SetString("Window.Backend.Name", + windowBackendsMap.at(configWindowBackend)); + Ship::Context::GetInstance()->GetConfig()->Save(); + UpdateWindowBackendObjects(); + } + } break; + case WIDGET_SEPARATOR: { + ImGui::Separator(); + } break; + case WIDGET_SEPARATOR_TEXT: { + auto options = std::static_pointer_cast(widget.options); + if (options->color != UIWidgets::Colors::NoColor) { + ImGui::PushStyleColor(ImGuiCol_Text, UIWidgets::ColorValues.at(options->color)); + } + ImGui::SeparatorText(widget.name.c_str()); + if (options->color != UIWidgets::Colors::NoColor) { + ImGui::PopStyleColor(); + } + } break; + case WIDGET_TEXT: { + auto options = std::static_pointer_cast(widget.options); + if (options->color != UIWidgets::Colors::NoColor) { + ImGui::PushStyleColor(ImGuiCol_Text, UIWidgets::ColorValues.at(options->color)); + } + ImGui::AlignTextToFramePadding(); + ImGui::TextWrapped("%s", widget.name.c_str()); + if (options->color != UIWidgets::Colors::NoColor) { + ImGui::PopStyleColor(); + } + } break; + case WIDGET_COMBOBOX: { + int32_t* pointer = std::get(widget.valuePointer); + if (pointer == nullptr) { + SPDLOG_ERROR("Combobox Widget requires a value pointer, currently nullptr"); + assert(false); + return; + } + auto options = std::static_pointer_cast(widget.options); + options->color = menuThemeIndex; + if (UIWidgets::Combobox(widget.name.c_str(), pointer, options->comboMap, *options)) { + if (widget.callback != nullptr) { + widget.callback(widget); + } + }; + } break; + case WIDGET_CVAR_COMBOBOX: { + auto options = std::static_pointer_cast(widget.options); + options->color = menuThemeIndex; + if (UIWidgets::CVarCombobox(widget.name.c_str(), widget.cVar, options->comboMap, *options)) { + if (widget.callback != nullptr) { + widget.callback(widget); + } + } + } break; + case WIDGET_SLIDER_INT: { + int32_t* pointer = std::get(widget.valuePointer); + if (pointer == nullptr) { + SPDLOG_ERROR("int32 Slider Widget requires a value pointer, currently nullptr"); + assert(false); + return; + } + auto options = std::static_pointer_cast(widget.options); + options->color = menuThemeIndex; + if (UIWidgets::SliderInt(widget.name.c_str(), pointer, *options)) { + if (widget.callback != nullptr) { + widget.callback(widget); + } + }; + } break; + case WIDGET_CVAR_SLIDER_INT: { + auto options = std::static_pointer_cast(widget.options); + options->color = menuThemeIndex; + if (UIWidgets::CVarSliderInt(widget.name.c_str(), widget.cVar, *options)) { + if (widget.callback != nullptr) { + widget.callback(widget); + } + }; + } break; + case WIDGET_SLIDER_FLOAT: { + float* pointer = std::get(widget.valuePointer); + + if (pointer == nullptr) { + SPDLOG_ERROR("float Slider Widget requires a value pointer, currently nullptr"); + assert(false); + return; + } + auto options = std::static_pointer_cast(widget.options); + options->color = menuThemeIndex; + if (UIWidgets::SliderFloat(widget.name.c_str(), pointer, *options)) { + if (widget.callback != nullptr) { + widget.callback(widget); + } + } + } break; + case WIDGET_CVAR_SLIDER_FLOAT: { + auto options = std::static_pointer_cast(widget.options); + options->color = menuThemeIndex; + if (UIWidgets::CVarSliderFloat(widget.name.c_str(), widget.cVar, *options)) { + if (widget.callback != nullptr) { + widget.callback(widget); + } + } + } break; + case WIDGET_BUTTON: { + auto options = std::static_pointer_cast(widget.options); + options->color = menuThemeIndex; + if (UIWidgets::Button(widget.name.c_str(), *options)) { + if (widget.callback != nullptr) { + widget.callback(widget); + } + } + } break; + case WIDGET_CUSTOM: { + if (widget.customFunction != nullptr) { + widget.customFunction(widget); + } + } break; + case WIDGET_WINDOW_BUTTON: { + if (widget.windowName == nullptr || widget.windowName[0] == '\0') { + std::string msg = + fmt::format("Error drawing window contents for {}: windowName not defined", widget.name); + SPDLOG_ERROR(msg.c_str()); + break; + } + auto window = Ship::Context::GetInstance()->GetWindow()->GetGui()->GetGuiWindow(widget.windowName); + if (!window) { + std::string msg = + fmt::format("Error drawing window contents: windowName {} does not exist", widget.windowName); + SPDLOG_ERROR(msg.c_str()); + break; + } + auto options = std::static_pointer_cast(widget.options); + options->color = menuThemeIndex; + if (options->showButton) { + UIWidgets::WindowButton(widget.name.c_str(), widget.cVar, window, *options); + } + if (!window->IsVisible() && options->embedWindow) { + window->DrawElement(); + } + } break; + case WIDGET_CVAR_COLOR_PICKER: { + auto options = std::static_pointer_cast(widget.options); + uint32_t modifiers = 0; + if (options->showLock) + modifiers |= UIWidgets::ColorPickerLockCheck; + if (options->showRandom) + modifiers |= UIWidgets::ColorPickerRandomButton; + if (options->showReset) + modifiers |= UIWidgets::ColorPickerResetButton; + if (options->showRainbow) + modifiers |= UIWidgets::ColorPickerRainbowCheck; + UIWidgets::CVarColorPicker(widget.name.c_str(), widget.cVar, options->defaultValue, options->useAlpha, + modifiers, options->color); + } break; + case WIDGET_SEARCH: { + UIWidgets::PushStyleButton(menuThemeIndex); + if (ImGui::Button("Clear")) { + menuSearch.Clear(); + } + ImGui::SameLine(); + if (CVarGetInteger(CVAR_SETTING("Menu.SearchAutofocus"), 0) && + ImGui::IsWindowFocused(ImGuiFocusedFlags_RootAndChildWindows) && !ImGui::IsAnyItemActive() && + !ImGui::IsMouseClicked(0)) { + ImGui::SetKeyboardFocusHere(0); + } + UIWidgets::PushStyleCombobox(menuThemeIndex); + ImGui::PushStyleColor(ImGuiCol_Border, UIWidgets::ColorValues.at(menuThemeIndex)); + menuSearch.Draw(); + ImGui::PopStyleColor(); + UIWidgets::PopStyleCombobox(); + UIWidgets::PopStyleButton(); + std::string menuSearchText(menuSearch.InputBuf); + + if (menuSearchText == "") { + ImGui::Text("Start typing to see results."); + return; + } + DrawSearchResults(menuSearchText); + } break; + default: + break; + } + if (widget.postFunc != nullptr) { + widget.postFunc(widget); + } + } catch (const std::bad_variant_access& e) { + SPDLOG_ERROR("Failed to draw menu item \"{}\" due to: {}", widget.name, e.what()); + assert(false); + } +} + +void Menu::Draw() { + if (!IsVisible()) { + return; + } + DrawElement(); + // Sync up the IsVisible flag if it was changed by ImGui + SyncVisibilityConsoleVariable(); +} + +static bool freshOpen = true; +void Menu::DrawElement() { + for (auto& [reason, info] : disabledMap) { + info.active = info.evaluation(info); + } + const char* headerCvar = CVAR_SETTING("Menu.ActiveHeader"); + + if (navigateToWidget) { + if (menuEntries.contains(navigateMainEntry) && + menuEntries.at(navigateMainEntry).sidebars.contains(navigateSidebar)) { + menuSearch.Clear(); + CVarSetString(headerCvar, navigateMainEntry); + const char* sidebarCvar = menuEntries.at(navigateMainEntry).sidebarCvar; + CVarSetString(sidebarCvar, navigateSidebar); + highlightWidget = true; + } + navigateToWidget = false; + } + + raceDisableActive = CVarGetInteger(CVAR_SETTING("DisableChanges"), 0); + + windowHeight = ImGui::GetMainViewport()->WorkSize.y; + windowWidth = ImGui::GetMainViewport()->WorkSize.x; + auto windowFlags = ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoSavedSettings; + bool popout = CVarGetInteger(CVAR_SETTING("Menu.Popout"), 0) && allowPopout; + if (popout) { + windowFlags = ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoDocking; + } + if (popout != popped) { + if (popout) { + windowHeight = poppedSize.y; + windowWidth = poppedSize.x; + ImGui::SetNextWindowSize({ static_cast(windowWidth), static_cast(windowHeight) }, + ImGuiCond_Always); + ImGui::SetNextWindowPos(poppedPos, ImGuiCond_Always); + } else if (popped) { + CVarSetFloat(CVAR_SETTING("Menu.PoppedWidth"), poppedSize.x); + CVarSetFloat(CVAR_SETTING("Menu.PoppedHeight"), poppedSize.y); + CVarSave(); + } + } + popped = popout; + auto windowCond = ImGuiCond_Always; + if (!popout) { + ImGui::SetNextWindowSize({ static_cast(windowWidth), static_cast(windowHeight) }, windowCond); + ImGui::SetNextWindowPos(ImGui::GetMainViewport()->GetCenter(), windowCond, { 0.5f, 0.5f }); + ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0.0f); + } + + ImGui::PushStyleColor(ImGuiCol_WindowBg, + ImVec4(0, 0, 0, CVarGetFloat(CVAR_SETTING("Menu.BackgroundOpacity"), 0.85f))); + + if (!ImGui::Begin("Main Menu", NULL, windowFlags)) { + if (!popout) { + ImGui::PopStyleVar(); + } + freshOpen = true; + ImGui::PopStyleColor(); + ImGui::End(); + return; + } + + ImGui::PopStyleColor(); + + if (popped != popout) { + if (!popout) { + ImGui::PopStyleVar(); + } + ImGui::PopStyleColor(); + CVarSetInteger(CVAR_SETTING("Menu.Popout"), popped); + CVarSetFloat(CVAR_SETTING("Menu.PoppedWidth"), poppedSize.x); + CVarSetFloat(CVAR_SETTING("Menu.PoppedHeight"), poppedSize.y); + CVarSetFloat(CVAR_SETTING("Menu.PoppedPos.x"), poppedSize.x); + CVarSetFloat(CVAR_SETTING("Menu.PoppedPos.y"), poppedSize.y); + CVarSave(); + ImGui::End(); + return; + } + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + ImGuiStyle& style = ImGui::GetStyle(); + windowHeight = window->WorkRect.GetHeight(); + windowWidth = window->WorkRect.GetWidth(); + + ImGui::PushFont(GameEngine::Instance->fontStandardLargest); + ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(10.0f, 8.0f)); + std::string headerIndex = CVarGetString(headerCvar, "Settings"); + ImVec2 pos = window->DC.CursorPos; + float centerX = pos.x + windowWidth / 2 - (style.ItemSpacing.x * (menuEntries.size() + 1)); + std::vector headerSizes; + float headerWidth = style.ItemSpacing.x + 20; + bool headerSearch = !CVarGetInteger(CVAR_SETTING("Menu.SidebarSearch"), 0); + if (headerSearch) { + headerWidth += 200.0f + style.ItemSpacing.x + style.FramePadding.x; + } + for (auto& label : menuOrder) { + ImVec2 size = ImGui::CalcTextSize(label.c_str()); + headerSizes.push_back(size); + headerWidth += size.x + style.FramePadding.x * 2; + if (label == headerIndex) { + headerWidth += style.ItemSpacing.x; + } + } + + // Full screen menu with widths below 1280, heights below 800. + // 5% of screen width/height padding on both sides above those resolutions. + // Menu width will never exceed a 16:9 aspect ratio. + ImVec2 menuSize = { windowWidth, windowHeight }; + if (windowWidth > 1280) { + menuSize.x = std::fminf(windowWidth * 0.9f, (windowHeight * 1.77f)); + } + if (windowHeight > 800) { + menuSize.y = windowHeight * 0.9f; + } + + pos += window->WorkRect.GetSize() / 2 - menuSize / 2; + ImGui::SetNextWindowPos(pos); + ImGui::BeginChild("Menu Block", menuSize, + ImGuiChildFlags_AutoResizeX | ImGuiChildFlags_AutoResizeY | ImGuiChildFlags_AlwaysAutoResize, + ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoScrollbar); + + std::unordered_map* sidebar; + float headerHeight = headerSizes.at(0).y + style.FramePadding.y * 2; + ImVec2 buttonSize = ImGui::CalcTextSize(ICON_FA_TIMES_CIRCLE) + style.FramePadding * 2; + bool scrollbar = false; + if (headerWidth > menuSize.x - buttonSize.x * 3 - style.ItemSpacing.x * 3) { + headerHeight += style.ScrollbarSize; + scrollbar = true; + } + ImGui::SetNextWindowSizeConstraints({ 0, headerHeight }, { headerWidth, headerHeight }); + ImVec2 headerSelSize = { menuSize.x - buttonSize.x * 3 - style.ItemSpacing.x * 3, headerHeight }; + if (scrollbar) { + headerSelSize.y += style.ScrollbarSize; + } + bool autoFocus = CVarGetInteger(CVAR_SETTING("Menu.SearchAutofocus"), 0); + ImGui::BeginChild("Header Selection", headerSelSize, + ImGuiChildFlags_AutoResizeX | ImGuiChildFlags_AutoResizeY | ImGuiChildFlags_AlwaysAutoResize, + ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_HorizontalScrollbar); + uint8_t curIndex = 0; + for (auto& label : menuOrder) { + if (curIndex != 0) { + ImGui::SameLine(); + } + auto& entry = menuEntries.at(label); + std::string nextIndex = label; + UIWidgets::PushStyleButton(menuThemeIndex); + if (headerIndex != label) { + ImGui::PushStyleColor(ImGuiCol_Button, { 0, 0, 0, 0 }); + } + if (ModernMenuHeaderEntry(entry.label)) { + if (headerSearch) { + menuSearch.Clear(); + } + CVarSetString(headerCvar, label.c_str()); + CVarSave(); + nextIndex = label; + } + if (headerIndex != label) { + ImGui::PopStyleColor(); + } + UIWidgets::PopStyleButton(); + if (headerIndex == label) { + sidebar = &entry.sidebars; + } + if (nextIndex != label) { + headerIndex = nextIndex; + } + curIndex++; + } + std::string menuSearchText = ""; + if (headerSearch) { + ImGui::SameLine(); + if (autoFocus && freshOpen) { + ImGui::SetKeyboardFocusHere(); + } + auto color = UIWidgets::ColorValues.at(menuThemeIndex); + color.w = 0.6f; + ImGui::PushStyleColor(ImGuiCol_FrameBg, color); + ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 3.0f); + menuSearch.Draw("##search", 200.0f); + menuSearchText = menuSearch.InputBuf; + menuSearchText.erase(std::remove(menuSearchText.begin(), menuSearchText.end(), ' '), menuSearchText.end()); + if (menuSearchText.length() < 1) { + ImGui::SameLine(headerWidth - 200.0f + style.ItemSpacing.x); + ImGui::TextColored(ImVec4(1.0f, 1.0f, 1.0f, 0.4f), "Search..."); + } + ImGui::PopStyleVar(); + ImGui::PopStyleColor(); + } + ImGui::EndChild(); + ImGui::SameLine(menuSize.x - (buttonSize.x * 3) - (style.ItemSpacing.x * 2)); + UIWidgets::ButtonOptions options3 = {}; + options3.color = UIWidgets::Colors::Red; + options3.size = UIWidgets::Sizes::Inline; + options3.tooltip = "Quit Ghostship"; + if (UIWidgets::Button(ICON_FA_POWER_OFF, options3)) { + GhostshipGui::mModalWindow->RegisterPopup( + "Quit Ghostship", "Are you sure you want to quit Ghostship?", "Quit", "Cancel", + []() { + std::shared_ptr menu = + static_pointer_cast(Ship::Context::GetInstance()->GetWindow()->GetGui()->GetMenu()); + if (!menu->IsMenuPopped()) { + menu->ToggleVisibility(); + } + Ship::Context::GetInstance()->GetWindow()->Close(); + }, + nullptr); + } + ImGui::PopStyleVar(); + ImGui::SameLine(); + UIWidgets::ButtonOptions options2 = {}; + options2.color = UIWidgets::Colors::Red; + options2.size = UIWidgets::Sizes::Inline; + options2.tooltip = "Reset" +#ifdef __APPLE__ + " (Command-R)" +#elif !defined(__SWITCH__) && !defined(__WIIU__) + " (Ctrl+R)" +#else + "" +#endif + ; + if (UIWidgets::Button(ICON_FA_UNDO, options2)) { + std::reinterpret_pointer_cast( + Ship::Context::GetInstance()->GetWindow()->GetGui()->GetGuiWindow("Console")) + ->Dispatch("reset"); + } + ImGui::SameLine(); + UIWidgets::ButtonOptions options = {}; + options.size = UIWidgets::Sizes::Inline; + options.tooltip = "Close Menu (Esc)"; + if (UIWidgets::Button(ICON_FA_TIMES_CIRCLE, options)) { + ToggleVisibility(); + + // Update gamepad navigation after close based on if other menus are still visible + auto mImGuiIo = &ImGui::GetIO(); + if (CVarGetInteger(CVAR_IMGUI_CONTROLLER_NAV, 0) && + Ship::Context::GetInstance()->GetWindow()->GetGui()->GetMenuOrMenubarVisible()) { + mImGuiIo->ConfigFlags |= ImGuiConfigFlags_NavEnableGamepad; + } else { + mImGuiIo->ConfigFlags &= ~ImGuiConfigFlags_NavEnableGamepad; + } + } + + pos.y += headerHeight + style.ItemSpacing.y; + pos.x = centerX - menuSize.x / 2 + (style.ItemSpacing.x * (menuEntries.size() + 1)); + window->DrawList->AddRectFilled(pos, pos + ImVec2{ menuSize.x, 4 }, ImGui::GetColorU32({ 255, 255, 255, 255 }), + true, style.WindowRounding); + pos.y += style.ItemSpacing.y; + float sectionHeight = menuSize.y - headerHeight - 4 - style.ItemSpacing.y * 2; + float columnHeight = sectionHeight - style.ItemSpacing.y * 4; + ImGui::SetNextWindowPos(pos + style.ItemSpacing * 2); + + // Increase sidebar width on larger screens to accomodate people scaling their menus. + float sidebarWidth = 200 - style.ItemSpacing.x; + if (menuSize.x > 1600) { + sidebarWidth = menuSize.x * 0.15f; + } + + const char* sidebarCvar = menuEntries.at(headerIndex).sidebarCvar; + + std::string sectionIndex = CVarGetString(sidebarCvar, ""); + if (!sidebar->contains(sectionIndex)) { + sectionIndex = menuEntries.at(headerIndex).sidebarOrder.at(0); + } + float sectionCenterX = pos.x + (sidebarWidth / 2); + float topY = pos.y; + ImGui::SetNextWindowSizeConstraints({ sidebarWidth, 0 }, { sidebarWidth, columnHeight }); + ImGui::BeginChild((menuEntries.at(headerIndex).label + " Section").c_str(), { sidebarWidth, columnHeight * 3 }, + ImGuiChildFlags_AutoResizeY | ImGuiChildFlags_AlwaysAutoResize, ImGuiWindowFlags_NoTitleBar); + for (auto& sidebarLabel : menuEntries.at(headerIndex).sidebarOrder) { + std::string nextIndex = ""; + UIWidgets::PushStyleButton(menuThemeIndex); + if (sectionIndex != sidebarLabel) { + ImGui::PushStyleColor(ImGuiCol_Button, { 0, 0, 0, 0 }); + } + if (ModernMenuSidebarEntry(sidebarLabel)) { + if (headerSearch) { + menuSearch.Clear(); + } + CVarSetString(sidebarCvar, sidebarLabel.c_str()); + CVarSave(); + nextIndex = sidebarLabel; + } + if (sectionIndex != sidebarLabel) { + ImGui::PopStyleColor(); + } + UIWidgets::PopStyleButton(); + if (nextIndex != "") { + sectionIndex = nextIndex; + } + } + ImGui::EndChild(); + ImGui::PopFont(); + + pos = ImVec2{ sectionCenterX + (sidebarWidth / 2), topY } + style.ItemSpacing * 2; + window->DrawList->AddRectFilled(pos, pos + ImVec2{ 4, sectionHeight - style.FramePadding.y * 2 }, + ImGui::GetColorU32({ 255, 255, 255, 255 }), true, style.WindowRounding); + pos.x += 4 + style.ItemSpacing.x; + ImGui::SetNextWindowPos(pos + style.ItemSpacing); + float sectionWidth = menuSize.x - sidebarWidth - 4 - style.ItemSpacing.x * 4; + std::string sectionMenuId = sectionIndex + " Settings"; + int columns = sidebar->at(sectionIndex).columnCount; + size_t columnFuncs = sidebar->at(sectionIndex).columnWidgets.size(); + if (windowWidth < 800) { + columns = 1; + } + float columnWidth = (sectionWidth - style.ItemSpacing.x * columns) / columns; + bool useColumns = columns > 1; + if (!useColumns || (headerSearch && menuSearchText.length() > 0)) { + ImGui::SameLine(); + ImGui::SetNextWindowSizeConstraints({ sectionWidth, 0 }, { sectionWidth, columnHeight }); + ImGui::BeginChild(sectionMenuId.c_str(), { sectionWidth, windowHeight * 4 }, ImGuiChildFlags_AutoResizeY, + ImGuiWindowFlags_NoTitleBar); + } + if (headerSearch && menuSearchText.length() > 0) { + ImGui::AlignTextToFramePadding(); + ImGui::PushFont(GameEngine::Instance->fontMonoLargest); + ImGui::Text("Search Results"); + ImGui::PopFont(); + ImGui::SameLine(); + UIWidgets::ButtonOptions clearBtnOpts = {}; + clearBtnOpts.size = UIWidgets::Sizes::Inline; + if (UIWidgets::Button("Clear Search", clearBtnOpts)) { + menuSearch.Clear(); + } + ImGui::BeginChild("searchSeparator", ImVec2(ImGui::GetContentRegionAvail().x / 2, 20), + ImGuiChildFlags_AlwaysAutoResize | ImGuiChildFlags_AutoResizeY); + UIWidgets::Separator(true, true, 0, 10); + ImGui::EndChild(); + uint32_t searchCount = DrawSearchResults(menuSearchText); + if (searchCount == 0) { + ImGui::SetCursorPosX((ImGui::GetWindowWidth() - ImGui::CalcTextSize("No results found").x) / 2); + ImGui::SetCursorPosY(ImGui::GetCursorPosY() + 10.0f); + ImGui::TextColored(ImVec4(1.0f, 1.0f, 1.0f, 0.4f), "No results found"); + } + } else { + std::string menuLabel = menuEntries.at(headerIndex).label; + if (MenuInit::GetUpdateFuncs().contains(menuLabel)) { + if (MenuInit::GetUpdateFuncs()[menuLabel].contains(sectionIndex)) { + for (auto& updateFunc : MenuInit::GetUpdateFuncs()[menuLabel][sectionIndex]) { + updateFunc(); + } + } + } + for (int i = 0; i < columnFuncs; i++) { + std::string sectionId = fmt::format("{} Column {}", sectionMenuId, i); + if (useColumns) { + ImGui::SetNextWindowSizeConstraints({ columnWidth, 0 }, { columnWidth, columnHeight }); + ImGui::BeginChild(sectionId.c_str(), { columnWidth, windowHeight * 4 }, ImGuiChildFlags_AutoResizeY, + ImGuiWindowFlags_NoTitleBar); + } + // for (auto& entryName : sidebar->at(sectionIndex).sidebarOrder) { + for (auto& entry : sidebar->at(sectionIndex).columnWidgets.at(i)) { + MenuDrawItem(entry, 90 / sidebar->at(sectionIndex).columnCount, menuThemeIndex); + } + //} + if (useColumns) { + ImGui::EndChild(); + } + if (i < columns - 1) { + ImGui::SameLine(); + } + } + } + if (!useColumns || menuSearchText.length() > 0) { + ImGui::EndChild(); + } + + if (!popout) { + ImGui::PopStyleVar(); + } + ImGui::EndChild(); + if (popout) { + poppedSize = ImGui::GetWindowSize(); + poppedPos = ImGui::GetWindowPos(); + } + if (freshOpen) { + freshOpen = false; + } + ImGui::End(); +} +} // namespace Ship diff --git a/src/port/ui/Menu.h b/src/port/ui/Menu.h new file mode 100644 index 00000000..db3f4ceb --- /dev/null +++ b/src/port/ui/Menu.h @@ -0,0 +1,64 @@ +#pragma once + +#include +#include +#include "MenuTypes.h" + +namespace Ship { +uint32_t GetVectorIndexOf(std::vector& vector, std::string value); +class Menu : public GuiWindow { + public: + using GuiWindow::GuiWindow; + + Menu(const std::string& cVar, const std::string& name, uint8_t searchSidebarIndex_ = 0, + UIWidgets::Colors menuThemeIndex_ = UIWidgets::Colors::LightBlue); + + void InitElement() override; + void DrawElement() override; + void UpdateElement() override; + void Draw() override; + void InsertSidebarSearch(); + void RemoveSidebarSearch(); + void UpdateWindowBackendObjects(); + bool IsMenuPopped(); + UIWidgets::Colors GetMenuThemeColor(); + + void MenuDrawItem(WidgetInfo& widget, uint32_t width, UIWidgets::Colors menuThemeIndex); + void AddMenuEntry(std::string entryName, const char* entryCvar); + void AddSearchWidget(SearchWidget widget); + std::unordered_map& GetDisabledMap(); + + protected: + ImVec2 mOriginalSize; + std::string mName; + uint32_t mWindowFlags; + std::unordered_map menuEntries; + std::vector menuOrder; + uint32_t DrawSearchResults(std::string& menuSearchText); + ImGuiTextFilter menuSearch; + uint8_t searchSidebarIndex; + UIWidgets::Colors defaultThemeIndex; + std::shared_ptr> availableWindowBackends; + std::unordered_map availableWindowBackendsMap; + Ship::WindowBackend configWindowBackend; + + std::unordered_map disabledMap; + std::vector disabledVector; + const SidebarEntry searchSidebarEntry = { + .columnCount = 1, + .columnWidgets = { { { .name = "Sidebar Search", + .type = WIDGET_SEARCH, + .options = std::make_shared(UIWidgets::WidgetOptions{}.Tooltip( + "Searches all menus for the given text, including tooltips.")) } } } + }; + + private: + bool allowPopout = true; // PortNote: should be set to false on small screen ports + bool popped; + ImVec2 poppedSize; + ImVec2 poppedPos; + float windowHeight; + float windowWidth; + UIWidgets::Colors menuThemeIndex; +}; +} // namespace Ship diff --git a/src/port/ui/MenuTypes.h b/src/port/ui/MenuTypes.h new file mode 100644 index 00000000..52dc1ffc --- /dev/null +++ b/src/port/ui/MenuTypes.h @@ -0,0 +1,336 @@ +#pragma once + +#include +#include "UIWidgets.hpp" + +typedef enum { + DISABLE_FOR_NO_VSYNC, + DISABLE_FOR_NO_WINDOWED_FULLSCREEN, + DISABLE_FOR_NO_MULTI_VIEWPORT, + DISABLE_FOR_NOT_DIRECTX, + DISABLE_FOR_DIRECTX, + DISABLE_FOR_MATCH_REFRESH_RATE_ON, + DISABLE_FOR_ADVANCED_RESOLUTION_ON, + DISABLE_FOR_VERTICAL_RES_TOGGLE_ON, + DISABLE_FOR_LOW_RES_MODE_ON, + DISABLE_FOR_NULL_PLAY_STATE, + DISABLE_FOR_DEBUG_MODE_OFF, + DISABLE_FOR_FRAME_ADVANCE_OFF, + DISABLE_FOR_ADVANCED_RESOLUTION_OFF, + DISABLE_FOR_VERTICAL_RESOLUTION_OFF, + DISABLE_FOR_BOOT_TO_DEBUG_WARP_SCREEN_ON, +} DisableOption; + +struct WidgetInfo; +struct disabledInfo; +using VoidFunc = std::function; +using DisableInfoFunc = std::function; +using DisableVec = std::vector; +using WidgetFunc = std::function; + +typedef enum { + WIDGET_CHECKBOX, + WIDGET_COMBOBOX, + WIDGET_SLIDER_INT, + WIDGET_SLIDER_FLOAT, + WIDGET_CVAR_CHECKBOX, + WIDGET_CVAR_COMBOBOX, + WIDGET_CVAR_SLIDER_INT, + WIDGET_CVAR_SLIDER_FLOAT, + WIDGET_BUTTON, + WIDGET_INPUT, + WIDGET_CVAR_INPUT, + WIDGET_CVAR_COLOR_PICKER, + WIDGET_COLOR_PICKER, + WIDGET_SEARCH, + WIDGET_SEPARATOR, + WIDGET_SEPARATOR_TEXT, + WIDGET_TEXT, + WIDGET_WINDOW_BUTTON, + WIDGET_AUDIO_BACKEND, // needed for special operations that can't be handled easily with the normal combobox widget + WIDGET_VIDEO_BACKEND, // same as above + WIDGET_CUSTOM, +} WidgetType; + +typedef enum { + SECTION_COLUMN_1, + SECTION_COLUMN_2, + SECTION_COLUMN_3, +} SectionColumns; + +typedef enum { + DEBUG_LOG_TRACE, + DEBUG_LOG_DEBUG, + DEBUG_LOG_INFO, + DEBUG_LOG_WARN, + DEBUG_LOG_ERROR, + DEBUG_LOG_CRITICAL, + DEBUG_LOG_OFF, +} DebugLogOption; + +typedef enum { + DEBUG_PAGE_OBJECTINFO, // 0: objectinfo + DEBUG_PAGE_CHECKSURFACEINFO, // 1: checkinfo/surfaceinfo + DEBUG_PAGE_MAPINFO, // 2: mapinfo + DEBUG_PAGE_STAGEINFO, // 3: stageinfo + DEBUG_PAGE_EFFECTINFO, // 4: effectinfo + DEBUG_PAGE_ENEMYINFO // 5: enemyinfo +} DebugLogPage; + +// holds the widget values for a widget, contains all CVar types available from LUS. int32_t is used for boolean +// evaluation +using CVarVariant = std::variant; +using OptionsVariant = + std::variant; + +// All the info needed for display and search of all widgets in the menu. +// `name` is the label displayed, +// `cVar` is the string representation of the CVar used to store the widget value +// `tooltip` is what is displayed when hovering (except when disabled, more on that later) +// `type` is the WidgetType for the widget, which is what determines how the information is used in the draw func +// `options` is a variant that holds the UIWidgetsOptions struct for the widget type +// blank objects need to be initialized with specific typing matching the expected Options struct for the widget +// `callback` is a lambda used for running code on widget change. may need `SohGui::GetMenu()` for specific menu actions +// `preFunc` is a lambda called before drawing code starts. It can be used to determine a widget's status, +// whether disabled or hidden, as well as update pointers for non-CVar widget types. +// `postFunc` is a lambda called after all drawing code is finished, for reacting to states other than +// widgets having been changed, like holding buttons. +// All three lambdas accept a `widgetInfo` reference in case it needs information on the widget for these operations +// `activeDisables` is a vector of DisableOptions for specifying what reasons a widget is disabled, which are displayed +// in the disabledTooltip for the widget. Can display multiple reasons. Handling the reasons is done in `preFunc`. +// It is recommended to utilize `disabledInfo`/`DisableReason` to list out all reasons for disabling and isHidden so +// the info can be shown. +// `windowName` is what is displayed and searched for `windowButton` type and window interactions +// `isHidden` just prevents the widget from being drawn under whatever circumstances you specify in the `preFunc` +// `sameLine` allows for specifying that the widget should be on the same line as the previous widget +struct WidgetInfo { + std::string name; // Used by all widgets + const char* cVar; // Used by all widgets except + WidgetType type; + std::shared_ptr options; + std::variant valuePointer; + WidgetFunc callback = nullptr; + WidgetFunc preFunc = nullptr; + WidgetFunc postFunc = nullptr; + WidgetFunc customFunction = nullptr; + DisableVec activeDisables = {}; + const char* windowName = ""; + bool isHidden = false; + bool sameLine = false; + bool raceDisable = true; + bool hideInSearch = false; + + WidgetInfo& CVar(const char* cVar_) { + cVar = cVar_; + return *this; + } + + WidgetInfo& Options(OptionsVariant options_) { + switch (type) { + case WIDGET_AUDIO_BACKEND: + case WIDGET_VIDEO_BACKEND: + case WIDGET_COMBOBOX: + case WIDGET_CVAR_COMBOBOX: + options = std::make_shared(std::get(options_)); + break; + case WIDGET_CHECKBOX: + case WIDGET_CVAR_CHECKBOX: + options = std::make_shared(std::get(options_)); + break; + case WIDGET_SLIDER_FLOAT: + case WIDGET_CVAR_SLIDER_FLOAT: + options = + std::make_shared(std::get(options_)); + break; + case WIDGET_SLIDER_INT: + case WIDGET_CVAR_SLIDER_INT: + options = + std::make_shared(std::get(options_)); + break; + case WIDGET_COLOR_PICKER: + case WIDGET_CVAR_COLOR_PICKER: + options = + std::make_shared(std::get(options_)); + break; + case WIDGET_BUTTON: + options = std::make_shared(std::get(options_)); + break; + case WIDGET_WINDOW_BUTTON: + options = std::make_shared( + std::get(options_)); + break; + case WIDGET_TEXT: + case WIDGET_SEPARATOR_TEXT: + options = std::make_shared(std::get(options_)); + break; + case WIDGET_SEPARATOR: + default: + options = std::make_shared(std::get(options_)); + } + return *this; + } + + WidgetInfo& Options(std::shared_ptr options_) { + options = options_; + return *this; + } + + WidgetInfo& Callback(WidgetFunc callback_) { + callback = callback_; + return *this; + } + + WidgetInfo& PreFunc(WidgetFunc preFunc_) { + preFunc = preFunc_; + return *this; + } + + WidgetInfo& PostFunc(WidgetFunc postFunc_) { + postFunc = postFunc_; + return *this; + } + + WidgetInfo& WindowName(const char* windowName_) { + windowName = windowName_; + return *this; + } + + WidgetInfo& ValuePointer(std::variant valuePointer_) { + valuePointer = valuePointer_; + return *this; + } + + WidgetInfo& SameLine(bool sameLine_) { + sameLine = sameLine_; + return *this; + } + + WidgetInfo& CustomFunction(WidgetFunc customFunction_) { + customFunction = customFunction_; + return *this; + } + + WidgetInfo& RaceDisable(bool disable) { + raceDisable = disable; + return *this; + } + + WidgetInfo& HideInSearch(bool hide) { + hideInSearch = hide; + return *this; + } + + void ResetDisables() { + isHidden = false; + options->disabled = false; + options->disabledTooltip = ""; + activeDisables.clear(); + } +}; + +struct WidgetPath { + std::string sectionName; + std::string sidebarName; + SectionColumns column; +}; + +// `disabledInfo` holds information on reasons for hiding or disabling a widget, as well as an evaluation lambda that +// is run once per frame to update its status (this is done to prevent dozens of redundant CVarGets in each frame loop) +// `evaluation` returns a bool which can be determined by whatever code you want that changes its status +// `reason` is the text displayed in the disabledTooltip when a widget is disabled by a particular DisableReason +// `active` is what's referenced when determining disabled status for a widget that uses this This can also be used to +// hold reasons to hide widgets so that their evaluations are also only run once per frame +struct disabledInfo { + DisableInfoFunc evaluation; + const char* reason; + bool active = false; + int32_t value = 0; +}; + +// Contains the name displayed in the sidebar (label), the number of columns to use in drawing (columnCount; for visual +// separation, 1-3), and nested vectors of the widgets, grouped by column (columnWidgets). The number of widget vectors +// added to the column groups does not need to match the specified columnCount, e.g. you can have one vector added to +// the sidebar, but still separate the window into 3 columns and display only in the first column +struct SidebarEntry { + uint32_t columnCount; + std::vector> columnWidgets; +}; + +// Contains entries for what's listed in the header at the top, including the name displayed on the top bar (label), +// a vector of the SidebarEntries for that header entry, and the name of the cvar used to track what sidebar entry is +// the last viewed for that header. +struct MainMenuEntry { + std::string label; + const char* sidebarCvar; + std::unordered_map sidebars = {}; + std::vector sidebarOrder = {}; +}; + +static const std::unordered_map audioBackendsMap = { + { Ship::AudioBackend::WASAPI, "Windows Audio Session API" }, + { Ship::AudioBackend::SDL, "SDL" }, + { Ship::AudioBackend::NUL, "Null" }, +}; + +static const std::unordered_map windowBackendsMap = { + { Ship::WindowBackend::FAST3D_DXGI_DX11, "DirectX" }, + { Ship::WindowBackend::FAST3D_SDL_OPENGL, "OpenGL" }, + { Ship::WindowBackend::FAST3D_SDL_METAL, "Metal" }, +}; + +struct MenuInit { + static std::vector>& GetInitFuncs() { + static std::vector> menuInitFuncs; + return menuInitFuncs; + } + + static std::unordered_map>>>& + GetUpdateFuncs() { + static std::unordered_map>>> + menuUpdateFuncs; + return menuUpdateFuncs; + } + + static void InitAll() { + auto& menuInitFuncs = MenuInit::GetInitFuncs(); + for (const auto& initFunc : menuInitFuncs) { + initFunc(); + } + } +}; + +struct SearchEntry { + // First four required + std::string widgetName; + std::string menuName; + std::string sidebarName; + std::string location; + std::string extraTerms = ""; +}; + +struct SearchWidget { + // First four required + WidgetInfo& info; + std::string menuName; + std::string sidebarName; + std::string location; + std::string extraTerms = ""; +}; + +struct RegisterMenuInitFunc { + RegisterMenuInitFunc(std::function initFunc) { + auto& menuInitFuncs = MenuInit::GetInitFuncs(); + + menuInitFuncs.push_back(initFunc); + } +}; + +struct RegisterMenuUpdateFunc { + RegisterMenuUpdateFunc(std::function updateFunc, std::string sectionName, std::string sidebarName) { + auto& menuUpdateFuncs = MenuInit::GetUpdateFuncs(); + + menuUpdateFuncs[sectionName][sidebarName].push_back(updateFunc); + } +}; diff --git a/src/port/ui/Notification.cpp b/src/port/ui/Notification.cpp index e790d754..3d7f8d6a 100644 --- a/src/port/ui/Notification.cpp +++ b/src/port/ui/Notification.cpp @@ -1,21 +1,20 @@ - #include "Notification.h" #include -#define ABS(x) ((x) < 0 ? -(x) : (x)) - namespace Notification { static uint32_t nextId = 0; static std::vector notifications = {}; +#define ABS(x) ((x) >= 0 ? (x) : -(x)) + void Window::Draw() { auto vp = ImGui::GetMainViewport(); const float margin = 30.0f; const float padding = 10.0f; - int position = CVarGetInteger("gNotifications.Position", 0); + int position = CVarGetInteger("gSettings.Notifications.Position", 3); // Top Left ImVec2 basePosition; @@ -36,9 +35,11 @@ void Window::Draw() { return; } - ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0, 0, 0, CVarGetFloat("gNotifications.BgOpacity", 0.5f))); + ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0, 0, 0, CVarGetFloat("gSettings.Notifications.BgOpacity", 0.5f))); ImGui::PushStyleColor(ImGuiCol_Border, ImVec4(0, 0, 0, 0)); ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 4.0f); + ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(8.0f * CVarGetFloat("gSettings.Notifications.Size", 1.8f), 6.0f)); + ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(8.0f * CVarGetFloat("gSettings.Notifications.Size", 1.8f), 8.0f)); for (int index = 0; index < notifications.size(); ++index) { auto& notification = notifications[index]; @@ -57,7 +58,7 @@ void Window::Draw() { ImGuiWindowFlags_NoScrollWithMouse | ImGuiWindowFlags_NoInputs | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoScrollbar); - ImGui::SetWindowFontScale(CVarGetFloat("gNotifications.Size", 1.8f)); // Make this adjustable + ImGui::SetWindowFontScale(CVarGetFloat("gSettings.Notifications.Size", 1.8f)); ImVec2 notificationPos; switch (position) { @@ -80,10 +81,12 @@ void Window::Draw() { } ImGui::SetWindowPos(notificationPos); + ImGui::AlignTextToFramePadding(); if (notification.itemIcon != nullptr) { - ImGui::Image(Ship::Context::GetInstance()->GetWindow()->GetGui()->GetTextureByName(notification.itemIcon), - ImVec2(24, 24)); + ImGui::Image( + Ship::Context::GetInstance()->GetWindow()->GetGui()->GetTextureByName(notification.itemIcon), + ImVec2(24 * CVarGetFloat("gSettings.Notifications.Size", 1.8f), 24 * CVarGetFloat("gSettings.Notifications.Size", 1.8f))); ImGui::SameLine(); } if (!notification.prefix.empty()) { @@ -100,7 +103,7 @@ void Window::Draw() { ImGui::PopStyleVar(); } - ImGui::PopStyleVar(); + ImGui::PopStyleVar(3); ImGui::PopStyleColor(2); } @@ -122,10 +125,12 @@ void Window::UpdateElement() { void Emit(Options notification) { notification.id = nextId++; if (notification.remainingTime == 0.0f) { - notification.remainingTime = CVarGetFloat("gNotifications.Duration", 10.0f); + notification.remainingTime = CVarGetFloat("gSettings.Notifications.Duration", 10.0f); } notifications.push_back(notification); - // Audio_PlaySoundGeneral(NA_SE_SY_METRONOME, &D_801333D4, 4, &D_801333E0, &D_801333E0, &D_801333E8); + if (!notification.mute) { + // TODO: play game notification sound + } } } // namespace Notification diff --git a/src/port/ui/Notification.h b/src/port/ui/Notification.h index dbcece01..0c256c11 100644 --- a/src/port/ui/Notification.h +++ b/src/port/ui/Notification.h @@ -3,8 +3,8 @@ #ifdef __cplusplus #include -#include - +#include +#include namespace Notification { struct Options { @@ -17,14 +17,15 @@ struct Options { std::string suffix = ""; ImVec4 suffixColor = ImVec4(1.0f, 0.5f, 0.5f, 1.0f); float remainingTime = 0.0f; // Seconds + bool mute = false; }; class Window : public Ship::GuiWindow { public: using GuiWindow::GuiWindow; - void InitElement() override {}; - void DrawElement() override {}; + void InitElement() override{}; + void DrawElement() override{}; void Draw() override; void UpdateElement() override; }; @@ -32,4 +33,5 @@ class Window : public Ship::GuiWindow { void Emit(Options notification); } // namespace Notification -#endif // __cplusplus + +#endif // __cplusplus \ No newline at end of file diff --git a/src/port/ui/ResolutionEditor.cpp b/src/port/ui/ResolutionEditor.cpp index e9812858..16b1d2cf 100644 --- a/src/port/ui/ResolutionEditor.cpp +++ b/src/port/ui/ResolutionEditor.cpp @@ -1,426 +1,654 @@ #include "ResolutionEditor.h" -#include "UIWidgets.h" -#include -#include "port/Engine.h" - #include #include +#include "port/Engine.h" +#include "UIWidgets.hpp" +#include #include +#include "GhostshipMenu.h" +#include "GhostshipGui.hpp" -/* Console Variables are grouped under gAdvancedResolution. (e.g. "gAdvancedResolution.Enabled") +/* Console Variables are grouped under gAdvancedResolution. (e.g. CVAR_PREFIX_ADVANCED_RESOLUTION + ".Enabled") - The following CVars are used in Libultraship and can be edited here: + The following cvars are used in Libultraship and can be edited here: - Enabled - Turns Advanced Resolution Mode on. - - AspectRatioX, AspectRatioY - Aspect ratio controls. To toggle off, set either to zero. + - AspectRatioX, AspectRatioY - Aspect ratio controls. To toggle off, set + either to zero. - VerticalPixelCount, VerticalResolutionToggle - Resolution controls. - - PixelPerfectMode, IntegerScale.Factor - Pixel Perfect Mode a.k.a. integer scaling controls. - (Waiting on a second PR merge on LUS for this to fully function.): + - PixelPerfectMode, IntegerScale.Factor - Pixel Perfect Mode a.k.a. integer scaling + controls. - IntegerScale.FitAutomatically - Automatic resizing for Pixel Perfect Mode. - - IntegerScale.NeverExceedBounds - Prevents manual resizing from exceeding screen bounds. + - IntegerScale.NeverExceedBounds - Prevents manual resizing from exceeding screen + bounds. - The following CVars are also implemented in LUS for niche use cases: + The following cvars are also implemented in LUS for niche use cases: - IgnoreAspectCorrection - Stretch framebuffer to fill screen. - This is something of a power-user setting for niche setups that most people won't need or care about, - but may be useful if playing the Switch/Wii U ports on a 4:3 television. + This is something of a power-user setting for niche setups that most people won't need or + care about, but may be useful if playing the Switch/Wii U ports on a 4:3 television. - IntegerScale.ExceedBoundsBy - Offset the max screen bounds, usually by +1. - This isn't that useful at the moment. + This isn't that useful at the moment, so it's + unused here. */ -namespace AdvancedResolutionSettings { +namespace GhostshipGui { +extern std::shared_ptr mGhostshipMenu; enum setting { UPDATE_aspectRatioX, UPDATE_aspectRatioY, UPDATE_verticalPixelCount }; -const char* aspectRatioPresetLabels[] = { - "Off", "Custom", "Original (4:3)", "Widescreen (16:9)", "Nintendo 3DS (5:3)", "16:10 (8:5)", "Ultrawide (21:9)" -}; -const float aspectRatioPresetsX[] = { 0.0f, 12.0f, 4.0f, 16.0f, 5.0f, 16.0f, 21.0f }; +std::unordered_map aspectRatioPresetLabels = { { 0, "Off" }, + { 1, "Custom" }, + { 2, "Original (4:3)" }, + { 3, "Widescreen (16:9)" }, + { 4, "Nintendo 3DS (5:3)" }, + { 5, "16:10 (8:5)" }, + { 6, "Ultrawide (21:9)" } }; +const float aspectRatioPresetsX[] = { 0.0f, 16.0f, 4.0f, 16.0f, 5.0f, 16.0f, 21.0f }; const float aspectRatioPresetsY[] = { 0.0f, 9.0f, 3.0f, 9.0f, 3.0f, 10.0f, 9.0f }; const int default_aspectRatio = 1; // Default combo list option -const char* pixelCountPresetLabels[] = { "Custom", "Native N64 (240p)", "2x (480p)", "3x (720p)", "4x (960p)", - "5x (1200p)", "6x (1440p)", "Full HD (1080p)", "4K (2160p)" }; -const int pixelCountPresets[] = { 480, 240, 480, 720, 960, 1200, 1440, 1080, 2160, 480 }; +const char *pixelCountPresetLabels[] = { "Custom", "Native N64 (240p)", "2x (480p)", + "3x (720p)", "4x (960p)", "5x (1200p)", + "6x (1440p)", "Full HD (1080p)", "4K (2160p)" }; +const int pixelCountPresets[] = { 480, 240, 480, 720, 960, 1200, 1440, 1080, 2160 }; const int default_pixelCount = 0; // Default combo list option -const uint32_t minVerticalPixelCount = 240; // see: Ship::AdvancedResolution() -const uint32_t maxVerticalPixelCount = 4320; +// Resolution clamp values as hardcoded in LUS::Gui::ApplyResolutionChanges() +const uint32_t minVerticalPixelCount = SCREEN_HEIGHT; +const uint32_t maxVerticalPixelCount = 4320; // 18x native, or 8K TV resolution const unsigned short default_maxIntegerScaleFactor = 6; // Default size of Integer scale factor slider. -const float enhancementSpacerHeight = 19.0f; -// This will need to be determined more intelligently when Hi-DPI UI support is added. +enum messageType { MESSAGE_ERROR, MESSAGE_WARNING, MESSAGE_QUESTION, MESSAGE_INFO, MESSAGE_GRAY_75 }; +const ImVec4 messageColor[]{ + { 0.85f, 0.0f, 0.0f, 1.0f }, // MESSAGE_ERROR + { 0.85f, 0.85f, 0.0f, 1.0f }, // MESSAGE_WARNING + { 0.0f, 0.85f, 0.85f, 1.0f }, // MESSAGE_QUESTION + { 0.0f, 0.85f, 0.55f, 1.0f }, // MESSAGE_INFO + { 0.75f, 0.75f, 0.75f, 1.0f } // MESSAGE_GRAY_75 +}; +static const float enhancementSpacerHeight = 19.0f; +// Initialise update flags. +static bool update[3]; -void AdvancedResolutionSettingsWindow::InitElement() { +// Initialise integer scale bounds. +static short max_integerScaleFactor = + default_maxIntegerScaleFactor; // default value, which may or may not get + // overridden depending on viewport res + +static short integerScale_maximumBounds = 1; // can change when window is resized + +// Combo List defaults +static int32_t item_aspectRatio; +static int32_t item_pixelCount; +// Stored Values for non-UIWidgets elements +static float aspectRatioX; +static float aspectRatioY; +static int32_t verticalPixelCount; +// Additional settings +static bool showHorizontalResField; +static int32_t horizontalPixelCount; +// Disabling flags +static bool disabled_everything; +static bool disabled_pixelCount; + +using namespace UIWidgets; + +static std::weak_ptr mInterpreter; + +std::shared_ptr GetInterpreter() { + auto intP = mInterpreter.lock(); + if (!intP) { + assert(false && "Lost reference to Fast::Interpreter"); + } + return intP; } -void AdvancedResolutionSettingsWindow::DrawElement() { - ImGui::SetNextWindowSize(ImVec2(497, 532), ImGuiCond_FirstUseEver); - if (ImGui::Begin("Advanced Resolution Settings", &mIsVisible)) { - // Initialise update flags. - bool update[sizeof(setting)]; - for (unsigned short i = 0; i < sizeof(setting); i++) - update[i] = false; - static short updateCountdown = 0; - auto interpreter = GameEngine_GetInterpreter(); - short countdownStartingValue = CVarGetInteger("gInterpolationFPS", 60) / 2; // half of a second, in frames. +void ResolutionCustomWidget(WidgetInfo &info) { + ImGui::BeginDisabled(disabled_everything); + // Vertical Resolution + UIWidgets::CVarCheckbox("Set fixed vertical resolution (disables resolution slider)", + CVAR_PREFIX_ADVANCED_RESOLUTION ".VerticalResolutionToggle", + UIWidgets::CheckboxOptions({ { .disabled = disabled_everything } }) + .Tooltip("Override the resolution scale slider and use the settings " + "below, irrespective of window size.") + .Color(THEME_COLOR)); + // if (disabled_pixelCount || disabled_everything) { // Hide pixel count controls. + // UIWidgets::DisableComponent(ImGui::GetStyle().Alpha * 0.5f); + // } + UIWidgets::PushStyleCombobox(THEME_COLOR); + if (ImGui::Combo("Pixel Count Presets", &item_pixelCount, pixelCountPresetLabels, + IM_ARRAYSIZE(pixelCountPresetLabels)) + && item_pixelCount != default_pixelCount) { // don't change anything if "Custom" is selected. + verticalPixelCount = pixelCountPresets[item_pixelCount]; - // Initialise integer scale bounds. - short max_integerScaleFactor = default_maxIntegerScaleFactor; // default value, which may or may not get - // overridden depending on viewport res - - short integerScale_maximumBounds = 1; // can change when window is resized - // This is mostly just for UX purposes, as Fit Automatically logic is part of LUS. - if (((float)interpreter->mGameWindowViewport.width / interpreter->mGameWindowViewport.height) > - ((float)interpreter->mCurDimensions.width / interpreter->mCurDimensions.height)) { - // Scale to window height - integerScale_maximumBounds = interpreter->mGameWindowViewport.height / interpreter->mCurDimensions.height; - } else { - // Scale to window width - integerScale_maximumBounds = interpreter->mGameWindowViewport.width / interpreter->mCurDimensions.width; - } - // Lower-clamping maximum bounds value to 1 is no-longer necessary as that's accounted for in LUS. - // Letting it go below 1 in this Editor will even allow for checking if screen bounds are being exceeded. - if (default_maxIntegerScaleFactor < integerScale_maximumBounds) { - max_integerScaleFactor = - integerScale_maximumBounds + CVarGetInteger("gAdvancedResolution.IntegerScale.ExceedBoundsBy", 0); + if (showHorizontalResField) { + horizontalPixelCount = (verticalPixelCount / aspectRatioY) * aspectRatioX; } - // Stored Values for non-UIWidgets elements - static float aspectRatioX = CVarGetFloat("gAdvancedResolution.AspectRatioX", 16.0f); - static float aspectRatioY = CVarGetFloat("gAdvancedResolution.AspectRatioY", 9.0f); - static int verticalPixelCount = CVarGetInteger("gAdvancedResolution.VerticalPixelCount", 480); - // Combo List defaults - static int item_aspectRatio = default_aspectRatio; - static int item_pixelCount = default_pixelCount; - // Additional settings - static bool showHorizontalResField = false; - static int horizontalPixelCount = (verticalPixelCount / aspectRatioY) * aspectRatioX; - -#ifdef __APPLE__ - ImGui::Text("Note: these settings may behave incorrectly on Apple Retina Displays."); - UIWidgets::PaddedSeparator(true, true, 3.0f, 3.0f); -#endif - - // The original resolution slider (for convenience) - if (UIWidgets::EnhancementSliderFloat("Internal Resolution: %d %%", "##IMul", "gInternalResolution", 0.5f, 2.0f, - "", 1.0f, true, true, - (CVarGetInteger("gAdvancedResolution.VerticalResolutionToggle", 0) && - CVarGetInteger("gAdvancedResolution.Enabled", 0)) || - CVarGetInteger("gLowResMode", 0))) { - Ship::Context::GetInstance()->GetWindow()->SetResolutionMultiplier(CVarGetFloat("gInternalResolution", 1)); - } - UIWidgets::Tooltip("Multiplies your output resolution by the value entered, as a more intensive but effective " - "form of anti-aliasing"); // Description pulled from SohMenuBar.cpp - - // N64 Mode toggle (again for convenience) - // UIWidgets::PaddedEnhancementCheckbox("(Enhancements>Graphics) N64 Mode", "gLowResMode", false, false, false, - // "", UIWidgets::CheckboxGraphics::Cross, false); - - UIWidgets::PaddedSeparator(true, true, 3.0f, 3.0f); - // Activator - UIWidgets::PaddedEnhancementCheckbox("Enable advanced settings.", "gAdvancedResolution.Enabled", false, false, - false, "", UIWidgets::CheckboxGraphics::Cross, false); - // Error/Warning display - if (!CVarGetInteger("gLowResMode", 0)) { - if (IsDroppingFrames()) { // Significant frame drop warning - ImGui::TextColored({ 0.85f, 0.85f, 0.0f, 1.0f }, - ICON_FA_EXCLAMATION_TRIANGLE " Significant frame rate (FPS) drops may be occuring."); - UIWidgets::Spacer(2); - } else { // No warnings - UIWidgets::Spacer(enhancementSpacerHeight); - } - } else { // N64 Mode warning - ImGui::TextColored({ 0.0f, 0.85f, 0.85f, 1.0f }, - ICON_FA_QUESTION_CIRCLE " \"N64 Mode\" is overriding these settings."); - ImGui::SameLine(); - if (ImGui::Button("Click to disable")) { - CVarSetInteger("gLowResMode", (int)false); - CVarSave(); - } - } - // Resolution visualiser - ImGui::Text("Viewport dimensions: %d x %d", interpreter->mGameWindowViewport.width, - interpreter->mGameWindowViewport.height); - ImGui::Text("Internal resolution: %d x %d", interpreter->mCurDimensions.width, - interpreter->mCurDimensions.height); - - UIWidgets::PaddedSeparator(true, true, 3.0f, 3.0f); - - // Aspect Ratio - ImGui::Text("Force aspect ratio:"); - ImGui::SameLine(); - ImGui::TextColored({ 0.75f, 0.75f, 0.75f, 1.0f }, "(Select \"Off\" to disable.)"); - if (ImGui::Combo(" ", &item_aspectRatio, aspectRatioPresetLabels, - IM_ARRAYSIZE(aspectRatioPresetLabels)) && - item_aspectRatio != default_aspectRatio) { // don't change anything if "Custom" is selected. - aspectRatioX = aspectRatioPresetsX[item_aspectRatio]; - aspectRatioY = aspectRatioPresetsY[item_aspectRatio]; - update[UPDATE_aspectRatioX] = true; - update[UPDATE_aspectRatioY] = true; - - if (showHorizontalResField) { - horizontalPixelCount = (verticalPixelCount / aspectRatioY) * aspectRatioX; - } - } - if (item_aspectRatio == default_aspectRatio && !showHorizontalResField) { - // Declaring the Y input interaction in particular as a variable beforehand - // will prevent a bug where the Y field would disappear when modifying X. - bool inputX = ImGui::InputFloat("X", &aspectRatioX, 0.1f, 1.0f, "%.3f"); - bool inputY = ImGui::InputFloat("Y", &aspectRatioY, 0.1f, 1.0f, "%.3f"); - if (inputX || inputY) { + CVarSetInteger(CVAR_PREFIX_ADVANCED_RESOLUTION ".VerticalPixelCount", verticalPixelCount); + CVarSetInteger(CVAR_PREFIX_ADVANCED_RESOLUTION ".UIComboItem.PixelCount", item_pixelCount); + Ship::Context::GetInstance()->GetWindow()->GetGui()->SaveConsoleVariablesNextFrame(); + } + UIWidgets::PopStyleCombobox(); + // Horizontal Resolution, if visibility is enabled for it. + if (showHorizontalResField) { + // Only show the field if Aspect Ratio is being enforced. + if ((aspectRatioX > 0.0f) && (aspectRatioY > 0.0f)) { + // So basically we're "faking" this one by setting aspectRatioX instead. + UIWidgets::PushStyleInput(THEME_COLOR); + if (ImGui::InputInt("Horiz. Pixel Count", &horizontalPixelCount, 8, 320)) { item_aspectRatio = default_aspectRatio; + if (horizontalPixelCount < SCREEN_WIDTH) { + horizontalPixelCount = SCREEN_WIDTH; + } + aspectRatioX = horizontalPixelCount; + aspectRatioY = verticalPixelCount; update[UPDATE_aspectRatioX] = true; update[UPDATE_aspectRatioY] = true; } - } else if (showHorizontalResField) { // Show calculated aspect ratio - if (item_aspectRatio) { - UIWidgets::Spacer(2); - float resolvedAspectRatio = - (float)interpreter->mCurDimensions.height / interpreter->mCurDimensions.width; - ImGui::Text("Aspect ratio: %.4f", resolvedAspectRatio); - } else { - UIWidgets::Spacer(enhancementSpacerHeight); - } - } - - UIWidgets::Spacer(0); - // Vertical Resolution - UIWidgets::PaddedEnhancementCheckbox("Set fixed vertical resolution (disables Resolution slider)", - "gAdvancedResolution.VerticalResolutionToggle", true, false, false, "", - UIWidgets::CheckboxGraphics::Cross, false); - UIWidgets::Tooltip( - "Override the resolution scale slider and use the settings below, irrespective of window size."); - if (ImGui::Combo("Pixel Count Presets", &item_pixelCount, pixelCountPresetLabels, - IM_ARRAYSIZE(pixelCountPresetLabels)) && - item_pixelCount != default_pixelCount) { // don't change anything if "Custom" is selected. - verticalPixelCount = pixelCountPresets[item_pixelCount]; - update[UPDATE_verticalPixelCount] = true; - - if (showHorizontalResField) { + UIWidgets::PopStyleInput(); + } else { // Display a notice instead. + ImGui::TextColored(messageColor[MESSAGE_QUESTION], + ICON_FA_QUESTION_CIRCLE " \"Force aspect ratio\" required."); + // ImGui::Text(" "); + ImGui::SameLine(); + if (UIWidgets::Button("Click to resolve", UIWidgets::ButtonOptions().Color(THEME_COLOR))) { + item_aspectRatio = default_aspectRatio; // Set it to Custom + aspectRatioX = aspectRatioPresetsX[2]; // but use the 4:3 defaults + aspectRatioY = aspectRatioPresetsY[2]; + update[UPDATE_aspectRatioX] = true; + update[UPDATE_aspectRatioY] = true; horizontalPixelCount = (verticalPixelCount / aspectRatioY) * aspectRatioX; } } - // Horizontal Resolution, if visibility is enabled for it. - if (showHorizontalResField) { - // Only show the field if Aspect Ratio is being enforced. - if ((aspectRatioX > 0.0f) && (aspectRatioY > 0.0f)) { - // So basically we're "faking" this one by setting aspectRatioX instead. - if (ImGui::InputInt("Horiz. Pixel Count", &horizontalPixelCount, 8, 320)) { - item_aspectRatio = default_aspectRatio; - if (horizontalPixelCount < (minVerticalPixelCount / 3.0f) * 4.0f) { - horizontalPixelCount = (minVerticalPixelCount / 3.0f) * 4.0f; - } - aspectRatioX = aspectRatioY * horizontalPixelCount / verticalPixelCount; - update[UPDATE_aspectRatioX] = true; - } - } else { // Display a notice instead. - ImGui::TextColored({ 0.0f, 0.85f, 0.85f, 1.0f }, - ICON_FA_QUESTION_CIRCLE " \"Force aspect ratio\" required."); - // ImGui::Text(" "); - ImGui::SameLine(); - if (ImGui::Button("Click to resolve")) { - item_aspectRatio = default_aspectRatio; // Set it to Custom - aspectRatioX = aspectRatioPresetsX[2]; // but use the 4:3 defaults - aspectRatioY = aspectRatioPresetsY[2]; - update[UPDATE_aspectRatioX] = true; - update[UPDATE_aspectRatioY] = true; - horizontalPixelCount = (verticalPixelCount / aspectRatioY) * aspectRatioX; - } - } - } - // Vertical Resolution part 2 - if (ImGui::InputInt("Vertical Pixel Count", &verticalPixelCount, 8, 240)) { - item_pixelCount = default_pixelCount; - update[UPDATE_verticalPixelCount] = true; + } + // Vertical Resolution part 2 + UIWidgets::PushStyleInput(THEME_COLOR); + if (ImGui::InputInt("Vertical Pixel Count", &verticalPixelCount, 8, 240)) { + item_pixelCount = default_pixelCount; + update[UPDATE_verticalPixelCount] = true; - // Account for the natural instinct to enter horizontal first. - // Ignore vertical resolutions that are below the lower clamp constant. - if (showHorizontalResField && !(verticalPixelCount < minVerticalPixelCount)) { - aspectRatioX = aspectRatioY * horizontalPixelCount / verticalPixelCount; - update[UPDATE_aspectRatioX] = true; - } + // Account for the natural instinct to enter horizontal first. + // Ignore vertical resolutions that are below the lower clamp constant. + if (showHorizontalResField && !(verticalPixelCount < minVerticalPixelCount)) { + item_aspectRatio = default_aspectRatio; + aspectRatioX = horizontalPixelCount; + aspectRatioY = verticalPixelCount; + update[UPDATE_aspectRatioX] = true; + update[UPDATE_aspectRatioY] = true; + } + } + ImGui::EndDisabled(); + UIWidgets::PopStyleInput(); + + // Integer scaling settings group (Pixel Perfect Mode) + static const ImGuiTreeNodeFlags IntegerScalingResolvedImGuiFlag = + CVarGetInteger(CVAR_PREFIX_ADVANCED_RESOLUTION ".PixelPerfectMode", 0) + ? ImGuiTreeNodeFlags_DefaultOpen + : ImGuiTreeNodeFlags_None; + UIWidgets::PushStyleHeader(THEME_COLOR); + if (ImGui::CollapsingHeader("Integer Scaling Settings", IntegerScalingResolvedImGuiFlag)) { + const bool disabled_pixelPerfectMode = + !CVarGetInteger(CVAR_PREFIX_ADVANCED_RESOLUTION ".PixelPerfectMode", 0) + || disabled_everything; + // Pixel Perfect Mode + UIWidgets::CVarCheckbox( + "Pixel Perfect Mode", CVAR_PREFIX_ADVANCED_RESOLUTION ".PixelPerfectMode", + UIWidgets::CheckboxOptions({ { .disabled = disabled_pixelCount || disabled_everything } }) + .Tooltip("Don't scale image to fill window.") + .Color(THEME_COLOR)); + if (disabled_pixelCount + && CVarGetInteger(CVAR_PREFIX_ADVANCED_RESOLUTION ".PixelPerfectMode", 0)) { + CVarSetInteger(CVAR_PREFIX_ADVANCED_RESOLUTION ".PixelPerfectMode", 0); + Ship::Context::GetInstance()->GetWindow()->GetGui()->SaveConsoleVariablesNextFrame(); } - UIWidgets::Spacer(0); - // UIWidgets::PaddedSeparator(true, true, 3.0f, 3.0f); - - // Integer scaling settings group - if (ImGui::CollapsingHeader("Integer Scaling Settings")) { - // Pixel-perfect Mode - UIWidgets::PaddedEnhancementCheckbox("Pixel-perfect Mode", "gAdvancedResolution.PixelPerfectMode", true, - true, - !CVarGetInteger("gAdvancedResolution.VerticalResolutionToggle", 0), "", - UIWidgets::CheckboxGraphics::Cross, false); - UIWidgets::Tooltip("Don't scale image to fill window."); - if (!CVarGetInteger("gAdvancedResolution.VerticalResolutionToggle", 0)) { - CVarSetInteger("gAdvancedResolution.PixelPerfectMode", (int)false); - CVarSave(); - } - - // Integer Scaling - UIWidgets::EnhancementSliderInt("Integer scale factor: %d", "##ARSIntScale", - "gAdvancedResolution.IntegerScale.Factor", 1, max_integerScaleFactor, "%d", - 1, true, - !CVarGetInteger("gAdvancedResolution.PixelPerfectMode", 0) || - CVarGetInteger("gAdvancedResolution.IntegerScale.FitAutomatically", 0)); - UIWidgets::Tooltip("Integer scales the image. Only available in pixel-perfect mode."); - // Display warning if size is being clamped or if framebuffer is larger than viewport. - if (CVarGetInteger("gAdvancedResolution.PixelPerfectMode", 0) && - (CVarGetInteger("gAdvancedResolution.IntegerScale.NeverExceedBounds", 1) && - CVarGetInteger("gAdvancedResolution.IntegerScale.Factor", 1) > integerScale_maximumBounds)) { - ImGui::SameLine(); - ImGui::TextColored({ 0.85f, 0.85f, 0.0f, 1.0f }, ICON_FA_EXCLAMATION_TRIANGLE " Window exceeded."); - } - - UIWidgets::PaddedEnhancementCheckbox("Automatically scale image to fit viewport", - "gAdvancedResolution.IntegerScale.FitAutomatically", true, true, - !CVarGetInteger("gAdvancedResolution.PixelPerfectMode", 0), "", - UIWidgets::CheckboxGraphics::Cross, false); - UIWidgets::Tooltip("Automatically sets scale factor to fit window. Only available in pixel-perfect mode."); - if (CVarGetInteger("gAdvancedResolution.IntegerScale.FitAutomatically", 0)) { - // This is just here to update the value shown on the slider. - // The function in LUS to handle this setting will ignore IntegerScaleFactor while active. - CVarSetInteger("gAdvancedResolution.IntegerScale.Factor", integerScale_maximumBounds); - // CVarSave(); - } + // Integer Scaling + UIWidgets::CVarSliderInt( + fmt::format("Integer scale factor: {}", max_integerScaleFactor).c_str(), + CVAR_PREFIX_ADVANCED_RESOLUTION ".IntegerScale.Factor", + UIWidgets::IntSliderOptions( + { { .disabled = disabled_pixelPerfectMode + || CVarGetInteger(CVAR_PREFIX_ADVANCED_RESOLUTION + ".IntegerScale.FitAutomatically", + 0) } }) + .Min(1) + .Max(max_integerScaleFactor) + .DefaultValue(1) + .Tooltip("Integer scales the image. Only available in Pixel Perfect Mode.") + .Color(THEME_COLOR)); + // Display warning if size is being clamped or if framebuffer is larger than viewport. + if (!disabled_pixelPerfectMode + && (CVarGetInteger(CVAR_PREFIX_ADVANCED_RESOLUTION ".IntegerScale.NeverExceedBounds", 1) + && CVarGetInteger(CVAR_PREFIX_ADVANCED_RESOLUTION ".IntegerScale.Factor", 1) + > integerScale_maximumBounds)) { + ImGui::SameLine(); + ImGui::TextColored(messageColor[MESSAGE_WARNING], + ICON_FA_EXCLAMATION_TRIANGLE " Window exceeded."); } - UIWidgets::PaddedSeparator(true, true, 3.0f, 3.0f); + UIWidgets::CVarCheckbox( + "Automatically scale image to fit viewport", + CVAR_PREFIX_ADVANCED_RESOLUTION ".IntegerScale.FitAutomatically", + UIWidgets::CheckboxOptions({ { .disabled = disabled_pixelPerfectMode } }) + .DefaultValue(true) + .Color(THEME_COLOR) + .Tooltip("Automatically sets scale factor to fit window. Only available in Pixel " + "Perfect Mode.")); + if (CVarGetInteger(CVAR_PREFIX_ADVANCED_RESOLUTION ".IntegerScale.FitAutomatically", 0)) { + // This is just here to update the value shown on the slider. + // The function in LUS to handle this setting will ignore IntegerScaleFactor while active. + CVarSetInteger(CVAR_PREFIX_ADVANCED_RESOLUTION ".IntegerScale.Factor", + integerScale_maximumBounds); + Ship::Context::GetInstance()->GetWindow()->GetGui()->SaveConsoleVariablesNextFrame(); + } + } // End of integer scaling settings + UIWidgets::PopStyleHeader(); - // Collapsible panel for additional settings - if (ImGui::CollapsingHeader("Additional Settings")) { - UIWidgets::Spacer(0); + // Collapsible panel for additional settings + UIWidgets::PushStyleHeader(THEME_COLOR); + if (ImGui::CollapsingHeader("Additional Settings")) { #if defined(__SWITCH__) || defined(__WIIU__) - // Disable aspect correction, stretching the framebuffer to fill the viewport. - // This option is only really needed on systems limited to 16:9 TV resolutions, such as consoles. - // The associated CVar is still functional on PC platforms if you want to use it though. - UIWidgets::PaddedEnhancementCheckbox("Disable aspect correction and stretch the output image.\n" - "(Might be useful for 4:3 televisions!)\n" - "Not available in Pixel Perfect Mode.", - "gAdvancedResolution.IgnoreAspectCorrection", false, true, - CVarGetInteger("gAdvancedResolution.PixelPerfectMode", 0), "", - UIWidgets::CheckboxGraphics::Cross, false); + // Disable aspect correction, stretching the framebuffer to fill the viewport. + // This option is only really needed on systems limited to 16:9 TV resolutions, such as + // consoles. The associated cvar is still functional on PC platforms if you want to use it + // though. + UIWidgets::CVarCheckbox( + "Disable aspect correction and stretch the output image.\n" + "(Might be useful for 4:3 televisions!)\n" + "Not available in Pixel Perfect Mode.", + CVAR_PREFIX_ADVANCED_RESOLUTION ".IgnoreAspectCorrection", + UIWidgets::CheckboxOptions( + { { .disabled = CVarGetInteger(CVAR_PREFIX_ADVANCED_RESOLUTION ".PixelPerfectMode", 0) + || disabled_everything } }) + .Color(THEME_COLOR)); #else - if (CVarGetInteger("gAdvancedResolution.IgnoreAspectCorrection", 0)) { - // This setting is intentionally not exposed on PC platforms, - // but may be accidentally activated for varying reasons. - // Having this button should hopefully prevent support headaches. - ImGui::TextColored({ 0.0f, 0.85f, 0.85f, 1.0f }, ICON_FA_QUESTION_CIRCLE - " If the image is stretched and you don't know why, click this."); - if (ImGui::Button("Click to reenable aspect correction.")) { - CVarSetInteger("gAdvancedResolution.IgnoreAspectCorrection", (int)false); - CVarSave(); - } - UIWidgets::Spacer(2); + if (CVarGetInteger(CVAR_PREFIX_ADVANCED_RESOLUTION ".IgnoreAspectCorrection", 0)) { + // This setting is intentionally not exposed on PC platforms, + // but may be accidentally activated for varying reasons. + // Having this button should hopefully prevent support headaches. + ImGui::TextColored(messageColor[MESSAGE_QUESTION], ICON_FA_QUESTION_CIRCLE + " If the image is stretched and you don't know why, click this."); + if (ImGui::Button("Click to reenable aspect correction.")) { + CVarSetInteger(CVAR_PREFIX_ADVANCED_RESOLUTION ".IgnoreAspectCorrection", 0); + Ship::Context::GetInstance()->GetWindow()->GetGui()->SaveConsoleVariablesNextFrame(); } + UIWidgets::Spacer(2); + } #endif - if (ImGui::Checkbox("Show a horizontal resolution field.", &showHorizontalResField)) { - if (!showHorizontalResField && (aspectRatioX > 0.0f)) { // when turning this setting off - // Refresh relevant values - aspectRatioX = aspectRatioY * horizontalPixelCount / verticalPixelCount; + // A requested addition; an alternative way of displaying the resolution field. + if (UIWidgets::Checkbox("Show a horizontal resolution field, instead of aspect ratio.", + &showHorizontalResField, + UIWidgets::CheckboxOptions().Color(THEME_COLOR))) { + if (!showHorizontalResField && (aspectRatioX > 0.0f)) { // when turning this setting off + // Refresh relevant values + aspectRatioX = aspectRatioY * horizontalPixelCount / verticalPixelCount; + horizontalPixelCount = (verticalPixelCount / aspectRatioY) * aspectRatioX; + } else { // when turning this setting on + item_aspectRatio = default_aspectRatio; + if (aspectRatioX > 0.0f) { + // Refresh relevant values in the opposite order horizontalPixelCount = (verticalPixelCount / aspectRatioY) * aspectRatioX; - } else { // when turning this setting on - item_aspectRatio = default_aspectRatio; - if (aspectRatioX > 0.0f) { - // Refresh relevant values in the opposite order - horizontalPixelCount = (verticalPixelCount / aspectRatioY) * aspectRatioX; - aspectRatioX = aspectRatioY * horizontalPixelCount / verticalPixelCount; - } + aspectRatioX = aspectRatioY * horizontalPixelCount / verticalPixelCount; + } + } + update[UPDATE_aspectRatioX] = true; + } + + // Beginning of Integer Scaling additional settings. + { + // UIWidgets::PaddedSeparator(true, true, 3.0f, 3.0f); + + // Integer Scaling - Never Exceed Bounds. + const bool disabled_neverExceedBounds = + !CVarGetInteger(CVAR_PREFIX_ADVANCED_RESOLUTION ".PixelPerfectMode", 0) + || CVarGetInteger(CVAR_PREFIX_ADVANCED_RESOLUTION ".IntegerScale.FitAutomatically", 0) + || disabled_everything; + if (UIWidgets::CVarCheckbox( + "Prevent integer scaling from exceeding screen bounds.\n" + "(Makes screen bounds take priority over specified factor.)", + CVAR_PREFIX_ADVANCED_RESOLUTION ".IntegerScale.NeverExceedBounds", + UIWidgets::CheckboxOptions({ { .disabled = disabled_neverExceedBounds } }) + .Tooltip("Prevents integer scaling factor from exceeding screen bounds.\n\n" + "Enabled: Will clamp the scaling factor and display a gentle warning " + "in the " + "resolution editor.\n" + "Disabled: Will allow scaling to exceed screen bounds, for users who " + "want to crop " + "overscan.\n\n" + " " ICON_FA_INFO_CIRCLE " Please note that exceeding screen bounds " + "may show a scroll bar on-screen.") + .Color(THEME_COLOR) + .DefaultValue(true))) { + + // Initialise the (currently unused) "Exceed Bounds By" cvar if it's been changed. + if (CVarGetInteger(CVAR_PREFIX_ADVANCED_RESOLUTION ".IntegerScale.ExceedBoundsBy", 0)) { + CVarSetInteger(CVAR_PREFIX_ADVANCED_RESOLUTION ".IntegerScale.ExceedBoundsBy", 0); + Ship::Context::GetInstance() + ->GetWindow() + ->GetGui() + ->SaveConsoleVariablesNextFrame(); } - update[UPDATE_aspectRatioX] = true; } - UIWidgets::PaddedEnhancementCheckbox( - "Don't allow integer scaling to exceed screen bounds.\n" - "(Makes screen bounds take priority over specified factor.)", - "gAdvancedResolution.IntegerScale.NeverExceedBounds", true, false, - !CVarGetInteger("gAdvancedResolution.PixelPerfectMode", 0) || - CVarGetInteger("gAdvancedResolution.IntegerScale.FitAutomatically", 0), - "", UIWidgets::CheckboxGraphics::Cross, true); + // Integer Scaling - Exceed Bounds By 1x/Offset. + // A popular feature in some retro frontends/upscalers, sometimes called "crop overscan" or + // "1080p 5x". + UIWidgets::CVarCheckbox( + "Allow integer scale factor to go +1 above maximum screen bounds.", + CVAR_PREFIX_ADVANCED_RESOLUTION ".IntegerScale.ExceedBoundsBy", + UIWidgets::CheckboxOptions( + { { .disabled = + !CVarGetInteger(CVAR_PREFIX_ADVANCED_RESOLUTION ".PixelPerfectMode", 0) + || disabled_everything } }) + .Color(THEME_COLOR)); - if (!CVarGetInteger("gAdvancedResolution.IntegerScale.NeverExceedBounds", 1) || - CVarGetInteger("gAdvancedResolution.IntegerScale.ExceedBoundsBy", 0)) { - ImGui::TextColored({ 0.0f, 0.85f, 0.85f, 1.0f }, - " " ICON_FA_QUESTION_CIRCLE + // It does actually function as expected, but exceeding the bottom of the screen shows a + // scroll bar. I've ended up commenting this one out because of the scroll bar, and for + // simplicity. + + // Display an info message about the scroll bar. + if (!CVarGetInteger(CVAR_PREFIX_ADVANCED_RESOLUTION ".IntegerScale.NeverExceedBounds", 1) + || CVarGetInteger(CVAR_PREFIX_ADVANCED_RESOLUTION ".IntegerScale.ExceedBoundsBy", 0)) { + ImGui::TextColored(messageColor[MESSAGE_INFO], + " " ICON_FA_INFO_CIRCLE " A scroll bar may become visible if screen bounds are exceeded."); - // Another helpful button for an unused CVar. - if (CVarGetInteger("gAdvancedResolution.IntegerScale.ExceedBoundsBy", 0)) { - if (ImGui::Button("Click to reset an unused CVar that may be causing this.")) { - CVarSetInteger("gAdvancedResolution.IntegerScale.ExceedBoundsBy", 0); - CVarSave(); + + // Another support helper button, to disable the unused "Exceed Bounds By" cvar. + // (Remove this button if uncommenting the checkbox.) + if (CVarGetInteger(CVAR_PREFIX_ADVANCED_RESOLUTION ".IntegerScale.ExceedBoundsBy", 0)) { + if (UIWidgets::Button("Click to reset a console variable that may be causing this.", + UIWidgets::ButtonOptions().Color(THEME_COLOR))) { + CVarSetInteger(CVAR_PREFIX_ADVANCED_RESOLUTION ".IntegerScale.ExceedBoundsBy", + 0); + Ship::Context::GetInstance() + ->GetWindow() + ->GetGui() + ->SaveConsoleVariablesNextFrame(); } } } else { - UIWidgets::Spacer(enhancementSpacerHeight); + ImGui::Text(" "); } + } // End of Integer Scaling additional settings. - // I've ended up dummying this one out because it doesn't function in a satisfactory way. - // Consider this idea on the table, but I don't deem it an important enough feature to push for. - /* - UIWidgets::PaddedEnhancementCheckbox("Allow integer scale factor to go 1x above maximum screen bounds.", - "gAdvancedResolution.IntegerScale.ExceedBoundsBy", false, false, - !CVarGetInteger("gAdvancedResolution.PixelPerfectMode", 0), "", - UIWidgets::CheckboxGraphics::Cross, false); - if (CVarGetInteger("gAdvancedResolution.IntegerScale.ExceedBoundsBy", 0)) { - ImGui::TextColored({ 0.0f, 0.85f, 0.85f, 1.0f }, - " " ICON_FA_QUESTION_CIRCLE - " A scroll bar may become visible if screen bounds are exceeded."); - }*/ + } // End of additional settings + UIWidgets::PopStyleHeader(); - } // end of Additional Settings - - // Clamp and update the CVars that don't use UIWidgets - if (IsBoolArrayTrue(update)) { - if (update[UPDATE_aspectRatioX]) { - if (aspectRatioX < 0.0f) { - aspectRatioX = 0.0f; - } - CVarSetFloat("gAdvancedResolution.AspectRatioX", aspectRatioX); + // Clamp and update the cvars that don't use UIWidgets + if (update[UPDATE_aspectRatioX] || update[UPDATE_aspectRatioY] + || update[UPDATE_verticalPixelCount]) { + if (update[UPDATE_aspectRatioX]) { + if (aspectRatioX < 0.0f) { + aspectRatioX = 0.0f; } - if (update[UPDATE_aspectRatioY]) { - if (aspectRatioY < 0.0f) { - aspectRatioY = 0.0f; - } - CVarSetFloat("gAdvancedResolution.AspectRatioY", aspectRatioY); - } - if (update[UPDATE_verticalPixelCount]) { - // There's a upper and lower clamp on the Libultraship side too, - // so clamping it here is purely visual, so the vertical resolution field reflects it. - if (verticalPixelCount < minVerticalPixelCount) { - verticalPixelCount = minVerticalPixelCount; - } - if (verticalPixelCount > maxVerticalPixelCount) { - verticalPixelCount = maxVerticalPixelCount; - } - CVarSetInteger("gAdvancedResolution.VerticalPixelCount", verticalPixelCount); - } - // Delay saving this set of CVars by a predetermined length of time, in frames. - updateCountdown = countdownStartingValue; + CVarSetFloat(CVAR_PREFIX_ADVANCED_RESOLUTION ".AspectRatioX", aspectRatioX); } - if (updateCountdown > 0) { - updateCountdown--; - } else { - CVarSave(); + if (update[UPDATE_aspectRatioY]) { + if (aspectRatioY < 0.0f) { + aspectRatioY = 0.0f; + } + CVarSetFloat(CVAR_PREFIX_ADVANCED_RESOLUTION ".AspectRatioY", aspectRatioY); } + if (update[UPDATE_verticalPixelCount]) { + // There's a upper and lower clamp on the Libultraship side too, + // so clamping it here is entirely visual, so the vertical resolution field reflects it. + if (verticalPixelCount < minVerticalPixelCount) { + verticalPixelCount = minVerticalPixelCount; + } + if (verticalPixelCount > maxVerticalPixelCount) { + verticalPixelCount = maxVerticalPixelCount; + } + CVarSetInteger(CVAR_PREFIX_ADVANCED_RESOLUTION ".VerticalPixelCount", verticalPixelCount); + } + CVarSetInteger(CVAR_PREFIX_ADVANCED_RESOLUTION ".UIComboItem.AspectRatio", item_aspectRatio); + CVarSetInteger(CVAR_PREFIX_ADVANCED_RESOLUTION ".UIComboItem.PixelCount", item_pixelCount); + Ship::Context::GetInstance()->GetWindow()->GetGui()->SaveConsoleVariablesNextFrame(); } - ImGui::End(); } -void AdvancedResolutionSettingsWindow::UpdateElement() { +void RegisterResolutionWidgets() { + auto fastWnd = dynamic_pointer_cast(Ship::Context::GetInstance()->GetWindow()); + mInterpreter = fastWnd->GetInterpreterWeak(); + + WidgetPath path = { "Settings", "Graphics", SECTION_COLUMN_2 }; + + // Resolution visualiser + mGhostshipMenu->AddWidget(path, "Viewport dimensions: {} x {}", WIDGET_TEXT) + .RaceDisable(false) + .PreFunc([](WidgetInfo &info) { + auto gfx_current_game_window_viewport = GetInterpreter().get()->mGameWindowViewport; + info.name = + fmt::format("Viewport dimensions: {} x {}", gfx_current_game_window_viewport.width, + gfx_current_game_window_viewport.height); + }); + mGhostshipMenu->AddWidget(path, "Internal resolution: {} x {}", WIDGET_TEXT) + .RaceDisable(false) + .PreFunc([](WidgetInfo &info) { + auto gfx_current_dimensions = GetInterpreter().get()->mCurDimensions; + info.name = fmt::format("Internal resolution: {} x {}", gfx_current_dimensions.width, + gfx_current_dimensions.height); + }); + + // Activator + mGhostshipMenu->AddWidget(path, "Enable advanced settings.", WIDGET_CVAR_CHECKBOX) + .CVar(CVAR_PREFIX_ADVANCED_RESOLUTION ".Enabled") + .RaceDisable(false); + // Error/Warning display + mGhostshipMenu + ->AddWidget(path, + ICON_FA_EXCLAMATION_TRIANGLE " Significant frame rate (FPS) drops may be occuring.", + WIDGET_TEXT) + .RaceDisable(false) + .PreFunc([](WidgetInfo &info) { + info.isHidden = !(!CVarGetInteger(CVAR_LOW_RES_MODE, 0) && IsDroppingFrames()); + }) + .Options(TextOptions().Color(Colors::Orange)); + mGhostshipMenu + ->AddWidget(path, ICON_FA_QUESTION_CIRCLE " \"N64 Mode\" is overriding these settings.", + WIDGET_TEXT) + .RaceDisable(false) + .PreFunc([](WidgetInfo &info) { info.isHidden = !CVarGetInteger(CVAR_LOW_RES_MODE, 0); }) + .Options(TextOptions().Color(Colors::LightBlue)); + mGhostshipMenu->AddWidget(path, "Click to disable N64 mode", WIDGET_BUTTON) + .RaceDisable(false) + .PreFunc([](WidgetInfo &info) { info.isHidden = !CVarGetInteger(CVAR_LOW_RES_MODE, 0); }) + .Callback([](WidgetInfo &info) { + CVarSetInteger(CVAR_LOW_RES_MODE, 0); + Ship::Context::GetInstance()->GetWindow()->GetGui()->SaveConsoleVariablesNextFrame(); + }); + + // Aspect Ratio + mGhostshipMenu->AddWidget(path, "AspectSep", WIDGET_SEPARATOR) + .RaceDisable(false) + .PreFunc([](WidgetInfo &info) { + if (mGhostshipMenu->GetDisabledMap().at(DISABLE_FOR_ADVANCED_RESOLUTION_OFF).active) { + info.activeDisables.push_back(DISABLE_FOR_ADVANCED_RESOLUTION_OFF); + } + }); + mGhostshipMenu->AddWidget(path, "Force aspect ratio:", WIDGET_TEXT) + .RaceDisable(false) + .PreFunc([](WidgetInfo &info) { + if (mGhostshipMenu->GetDisabledMap().at(DISABLE_FOR_ADVANCED_RESOLUTION_OFF).active) { + info.activeDisables.push_back(DISABLE_FOR_ADVANCED_RESOLUTION_OFF); + } + }); + mGhostshipMenu->AddWidget(path, "(Select \"Off\" to disable.)", WIDGET_TEXT) + .RaceDisable(false) + .PreFunc([](WidgetInfo &info) { + if (mGhostshipMenu->GetDisabledMap().at(DISABLE_FOR_ADVANCED_RESOLUTION_OFF).active) { + info.activeDisables.push_back(DISABLE_FOR_ADVANCED_RESOLUTION_OFF); + } + }) + .SameLine(true) + .Options(TextOptions().Color(Colors::Gray)); + // Presets + mGhostshipMenu->AddWidget(path, "Aspect Ratio", WIDGET_COMBOBOX) + .ValuePointer(&item_aspectRatio) + .RaceDisable(false) + .PreFunc([](WidgetInfo &info) { + if (mGhostshipMenu->GetDisabledMap().at(DISABLE_FOR_ADVANCED_RESOLUTION_OFF).active) { + info.activeDisables.push_back(DISABLE_FOR_ADVANCED_RESOLUTION_OFF); + } + }) + .Callback([](WidgetInfo &info) { + if (item_aspectRatio + != default_aspectRatio) { // don't change anything if "Custom" is selected. + aspectRatioX = aspectRatioPresetsX[item_aspectRatio]; + aspectRatioY = aspectRatioPresetsY[item_aspectRatio]; + + if (showHorizontalResField) { + horizontalPixelCount = (verticalPixelCount / aspectRatioY) * aspectRatioX; + } + + CVarSetFloat(CVAR_PREFIX_ADVANCED_RESOLUTION ".AspectRatioX", aspectRatioX); + CVarSetFloat(CVAR_PREFIX_ADVANCED_RESOLUTION ".AspectRatioY", aspectRatioY); + } + CVarSetInteger(CVAR_PREFIX_ADVANCED_RESOLUTION ".UIComboItem.AspectRatio", + item_aspectRatio); + Ship::Context::GetInstance()->GetWindow()->GetGui()->SaveConsoleVariablesNextFrame(); + }) + .Options(ComboboxOptions().ComboMap(aspectRatioPresetLabels)); + mGhostshipMenu->AddWidget(path, "AspectRatioCustom", WIDGET_CUSTOM) + .RaceDisable(false) + .CustomFunction([](WidgetInfo &info) { + // Hide aspect ratio input fields if using one of the presets. + if (item_aspectRatio == default_aspectRatio && !showHorizontalResField) { + // Declare input interaction bools outside of IF statement to prevent Y field from + // disappearing. + const bool input_X = UIWidgets::SliderFloat( + "X", &aspectRatioX, + UIWidgets::FloatSliderOptions({ { .disabled = disabled_everything } }) + .Min(0.1f) + .Max(32.0f) + .Step(0.001f) + .Format("%3f") + .Color(THEME_COLOR) + .LabelPosition(UIWidgets::LabelPositions::Near) + .ComponentAlignment(UIWidgets::ComponentAlignments::Right)); + const bool input_Y = UIWidgets::SliderFloat( + "Y", &aspectRatioY, + UIWidgets::FloatSliderOptions({ { .disabled = disabled_everything } }) + .Min(0.1f) + .Max(24.0f) + .Step(0.001f) + .Format("%3f") + .Color(THEME_COLOR) + .LabelPosition(UIWidgets::LabelPositions::Near) + .ComponentAlignment(UIWidgets::ComponentAlignments::Right)); + if (input_X || input_Y) { + item_aspectRatio = default_aspectRatio; + update[UPDATE_aspectRatioX] = true; + update[UPDATE_aspectRatioY] = true; + } + } else if (showHorizontalResField) { // Show calculated aspect ratio + if (item_aspectRatio) { + auto gfx_current_dimensions = GetInterpreter().get()->mCurDimensions; + ImGui::Dummy({ 0, 2 }); + const float resolvedAspectRatio = + (float) gfx_current_dimensions.width / gfx_current_dimensions.height; + ImGui::Text("Aspect ratio: %.2f:1", resolvedAspectRatio); + } + } + }); + mGhostshipMenu->AddWidget(path, "MoreResolutionSettings", WIDGET_CUSTOM) + .CustomFunction(ResolutionCustomWidget) + .RaceDisable(false); } -bool AdvancedResolutionSettingsWindow::IsDroppingFrames() { +void UpdateResolutionVars() { + // Clamp and update the cvars that don't use UIWidgets + if (update[UPDATE_aspectRatioX] || update[UPDATE_aspectRatioY] + || update[UPDATE_verticalPixelCount]) { + if (update[UPDATE_aspectRatioX]) { + if (aspectRatioX < 0.0f) { + aspectRatioX = 0.0f; + } + CVarSetFloat(CVAR_PREFIX_ADVANCED_RESOLUTION ".AspectRatioX", aspectRatioX); + } + if (update[UPDATE_aspectRatioY]) { + if (aspectRatioY < 0.0f) { + aspectRatioY = 0.0f; + } + CVarSetFloat(CVAR_PREFIX_ADVANCED_RESOLUTION ".AspectRatioY", aspectRatioY); + } + if (update[UPDATE_verticalPixelCount]) { + // There's a upper and lower clamp on the Libultraship side too, + // so clamping it here is entirely visual, so the vertical resolution field reflects it. + if (verticalPixelCount < minVerticalPixelCount) { + verticalPixelCount = minVerticalPixelCount; + } + if (verticalPixelCount > maxVerticalPixelCount) { + verticalPixelCount = maxVerticalPixelCount; + } + CVarSetInteger(CVAR_PREFIX_ADVANCED_RESOLUTION ".VerticalPixelCount", verticalPixelCount); + } + CVarSetInteger(CVAR_PREFIX_ADVANCED_RESOLUTION ".UIComboItem.AspectRatio", item_aspectRatio); + CVarSetInteger(CVAR_PREFIX_ADVANCED_RESOLUTION ".UIComboItem.PixelCount", item_pixelCount); + Ship::Context::GetInstance()->GetWindow()->GetGui()->SaveConsoleVariablesNextFrame(); + } + // Initialise update flags. + for (uint8_t i = 0; i < sizeof(update); i++) { + update[i] = false; + } + + // Initialise integer scale bounds. + short max_integerScaleFactor = + default_maxIntegerScaleFactor; // default value, which may or may not get + // overridden depending on viewport res + + short integerScale_maximumBounds = 1; // can change when window is resized + // This is mostly just for UX purposes, as Fit Automatically logic is part of LUS. + auto gfx_current_game_window_viewport = GetInterpreter().get()->mGameWindowViewport; + auto gfx_current_dimensions = GetInterpreter().get()->mCurDimensions; + if (((float) gfx_current_game_window_viewport.width / gfx_current_game_window_viewport.height) + > ((float) gfx_current_dimensions.width / gfx_current_dimensions.height)) { + // Scale to window height + integerScale_maximumBounds = + gfx_current_game_window_viewport.height / gfx_current_dimensions.height; + } else { + // Scale to window width + integerScale_maximumBounds = + gfx_current_game_window_viewport.width / gfx_current_dimensions.width; + } + // Lower-clamping maximum bounds value to 1 is no-longer necessary as that's accounted for in LUS. + // Letting it go below 1 in this Editor will even allow for checking if screen bounds are being + // exceeded. + if (default_maxIntegerScaleFactor < integerScale_maximumBounds) { + max_integerScaleFactor = + integerScale_maximumBounds + + CVarGetInteger(CVAR_PREFIX_ADVANCED_RESOLUTION ".IntegerScale.ExceedBoundsBy", 0); + } + + // Combo List defaults + item_aspectRatio = CVarGetInteger(CVAR_PREFIX_ADVANCED_RESOLUTION ".UIComboItem.AspectRatio", 3); + item_pixelCount = + CVarGetInteger(CVAR_PREFIX_ADVANCED_RESOLUTION ".UIComboItem.PixelCount", default_pixelCount); + // Stored Values for non-UIWidgets elements + aspectRatioX = CVarGetFloat(CVAR_PREFIX_ADVANCED_RESOLUTION ".AspectRatioX", + aspectRatioPresetsX[item_aspectRatio]); + aspectRatioY = CVarGetFloat(CVAR_PREFIX_ADVANCED_RESOLUTION ".AspectRatioY", + aspectRatioPresetsY[item_aspectRatio]); + verticalPixelCount = CVarGetInteger(CVAR_PREFIX_ADVANCED_RESOLUTION ".VerticalPixelCount", + pixelCountPresets[item_pixelCount]); + // Additional settings + horizontalPixelCount = (verticalPixelCount / aspectRatioY) * aspectRatioX; + // Disabling flags + disabled_everything = !CVarGetInteger(CVAR_PREFIX_ADVANCED_RESOLUTION ".Enabled", 0); + disabled_pixelCount = + !CVarGetInteger(CVAR_PREFIX_ADVANCED_RESOLUTION ".VerticalResolutionToggle", 0); +} + +bool IsDroppingFrames() { // a rather imprecise way of checking for frame drops. // but it's mostly there to inform the player of large drops. - const short targetFPS = CVarGetInteger("gInterpolationFPS", 60); + const short targetFPS = GameEngine::Instance->GetInterpolationFPS(); const float threshold = targetFPS / 20.0f + 4.1f; return ImGui::GetIO().Framerate < targetFPS - threshold; } -bool AdvancedResolutionSettingsWindow::IsBoolArrayTrue(bool* foo) { - for (unsigned short i = 0; i < sizeof(&foo); i++) - if (&foo[i]) - return true; - return false; -} -} // namespace AdvancedResolutionSettings \ No newline at end of file +static RegisterMenuUpdateFunc updateFunc(UpdateResolutionVars, "Settings", "Graphics"); +static RegisterMenuInitFunc menuInitFunc(RegisterResolutionWidgets); + +} // namespace GhostshipGui diff --git a/src/port/ui/ResolutionEditor.h b/src/port/ui/ResolutionEditor.h index 90b10f5b..7fdbd757 100644 --- a/src/port/ui/ResolutionEditor.h +++ b/src/port/ui/ResolutionEditor.h @@ -1,19 +1,10 @@ -#pragma once -#include +#ifndef RESOLUTIONEDITOR_H +#define RESOLUTIONEDITOR_H -namespace AdvancedResolutionSettings { - class AdvancedResolutionSettingsWindow : public Ship::GuiWindow { - private: - bool IsDroppingFrames(); +namespace GhostshipGui { +bool IsDroppingFrames(); +void RegisterResolutionWidgets(); +void UpdateResolutionVars(); +} // namespace GhostshipGui - protected: - bool IsBoolArrayTrue(bool*); - - public: - using Ship::GuiWindow::GuiWindow; - - void InitElement() override; - void DrawElement() override; - void UpdateElement() override; - }; -} // namespace AdvancedResolutionSettings \ No newline at end of file +#endif // RESOLUTIONEDITOR_H diff --git a/src/port/ui/SaveEditor.cpp b/src/port/ui/SaveEditor.cpp index d6fb0fcb..5c3147a0 100644 --- a/src/port/ui/SaveEditor.cpp +++ b/src/port/ui/SaveEditor.cpp @@ -1,9 +1,11 @@ #include "SaveEditor.h" -#include "UIWidgets.h" +#include "UIWidgets.hpp" #include #include #include +#include "GhostshipGui.hpp" +#include "port/ui/cvar_prefixes.h" extern "C" { #include "game/save_file.h" @@ -52,47 +54,45 @@ void DrawFlagTableArray32(const FlagTable& flagTable, uint16_t row, uint32_t& fl ImGui::PopID(); } -void SaveEditorWindow::InitElement() { -} - void SaveEditorWindow::DrawElement() { - ImGui::SetNextWindowSize(ImVec2(497, 532), ImGuiCond_FirstUseEver); - if (ImGui::Begin("Save Editor", &mIsVisible)) { + if (CVarGetInteger(CVAR_WINDOW("SaveEditor"), 0)) { + ImGui::SetNextWindowSize(ImVec2(497, 532), ImGuiCond_FirstUseEver); + if (ImGui::Begin("Save Editor")) { - ImGui::BeginTabBar("##saveEditorTabs"); - if (ImGui::BeginTabItem("Main Save & Mario Flags")) { - ImGui::Text("Mario Flags"); - DrawFlagTableArray32(flagTables[1], 0, gMarioState->flags); - ImGui::Text("Save File Flags"); - DrawFlagTableArray32(flagTables[0], 0, gSaveBuffer.files[gCurrSaveFileNum - 1][0].flags); - ImGui::EndTabItem(); - } - - if (ImGui::BeginTabItem("Course Stars & Coins")) { - for (int i = 0; i < COURSE_COUNT; i++) { - ImGui::Text("%s", courseNames[i]); - std::string invisibleLabelStr = "##courseStars" + std::string(courseNames[i]); - const char* invisibleLabel = invisibleLabelStr.c_str(); - UIWidgets::DrawFlagArray8( - invisibleLabel, gSaveBuffer.files[gCurrSaveFileNum - 1][0].courseStars[COURSE_NUM_TO_INDEX(i)]); - if (i < COURSE_STAGES_COUNT) { - ImGui::SameLine(); - std::string invisibleLabelStr2 = "##courseCoins" + std::string(courseNames[i]); - const char* invisibleLabel2 = invisibleLabelStr2.c_str(); - ImGui::SetNextItemWidth(ImGui::GetFontSize() * 4); - ImGui::InputScalar( - invisibleLabel2, ImGuiDataType_U8, - &gSaveBuffer.files[gCurrSaveFileNum - 1][0].courseCoinScores[COURSE_NUM_TO_INDEX(i)], NULL, - NULL, "%u"); - } + ImGui::BeginTabBar("##saveEditorTabs"); + if (ImGui::BeginTabItem("Main Save & Mario Flags")) { + ImGui::Text("Mario Flags"); + DrawFlagTableArray32(flagTables[1], 0, gMarioState->flags); + ImGui::Text("Save File Flags"); + DrawFlagTableArray32(flagTables[0], 0, + gSaveBuffer.files[gCurrSaveFileNum - 1][0].flags); + ImGui::EndTabItem(); } - ImGui::EndTabItem(); + + if (ImGui::BeginTabItem("Course Stars & Coins")) { + for (int i = 0; i < COURSE_COUNT; i++) { + ImGui::Text("%s", courseNames[i]); + std::string invisibleLabelStr = "##courseStars" + std::string(courseNames[i]); + const char *invisibleLabel = invisibleLabelStr.c_str(); + UIWidgets::DrawFlagArray8( + invisibleLabel, + gSaveBuffer.files[gCurrSaveFileNum - 1][0].courseStars[COURSE_NUM_TO_INDEX(i)]); + if (i < COURSE_STAGES_COUNT) { + ImGui::SameLine(); + std::string invisibleLabelStr2 = "##courseCoins" + std::string(courseNames[i]); + const char *invisibleLabel2 = invisibleLabelStr2.c_str(); + ImGui::SetNextItemWidth(ImGui::GetFontSize() * 4); + ImGui::InputScalar(invisibleLabel2, ImGuiDataType_U8, + &gSaveBuffer.files[gCurrSaveFileNum - 1][0] + .courseCoinScores[COURSE_NUM_TO_INDEX(i)], + NULL, NULL, "%u"); + } + } + ImGui::EndTabItem(); + } + + ImGui::EndTabBar(); } - - ImGui::EndTabBar(); + ImGui::End(); } - ImGui::End(); } - -void SaveEditorWindow::UpdateElement() { -} \ No newline at end of file diff --git a/src/port/ui/SaveEditor.h b/src/port/ui/SaveEditor.h index 7f4d2683..27366983 100644 --- a/src/port/ui/SaveEditor.h +++ b/src/port/ui/SaveEditor.h @@ -88,7 +88,7 @@ class SaveEditorWindow : public Ship::GuiWindow { public: using Ship::GuiWindow::GuiWindow; - void InitElement() override; + void InitElement() override {}; void DrawElement() override; - void UpdateElement() override; + void UpdateElement() override {}; }; \ No newline at end of file diff --git a/src/port/ui/UIWidgets.cpp b/src/port/ui/UIWidgets.cpp index db4499aa..8c5bd4ca 100644 --- a/src/port/ui/UIWidgets.cpp +++ b/src/port/ui/UIWidgets.cpp @@ -1,21 +1,21 @@ -#define NOMINMAX -#include "UIWidgets.h" - -#include - -#include +#include "UIWidgets.hpp" +#define IMGUI_DEFINE_MATH_OPERATORS #include +#include #include +#include +#include +#include +#include #include +#include namespace UIWidgets { -// MARK: - Layout Helper - // Automatically adds newlines to break up text longer than a specified number of characters // Manually included newlines will still be respected and reset the line length // If line is midword when it hits the limit, text should break at the last encountered space -char* WrappedText(const char* text, unsigned int charactersPerLine) { +std::string WrappedText(const char* text, unsigned int charactersPerLine) { std::string newText(text); const size_t tipLength = newText.length(); int lastSpace = -1; @@ -37,63 +37,15 @@ char* WrappedText(const char* text, unsigned int charactersPerLine) { currentLineLength++; } - return strdup(newText.c_str()); + return newText; } -char* WrappedText(const std::string& text, unsigned int charactersPerLine) { +std::string WrappedText(const std::string &text, unsigned int charactersPerLine) { return WrappedText(text.c_str(), charactersPerLine); } -void SetLastItemHoverText(const std::string& text) { - if (ImGui::IsItemHovered()) { - ImGui::BeginTooltip(); - ImGui::Text("%s", WrappedText(text, 60)); - ImGui::EndTooltip(); - } -} - -void SetLastItemHoverText(const char* text) { - if (ImGui::IsItemHovered()) { - ImGui::BeginTooltip(); - ImGui::Text("%s", WrappedText(text, 60)); - ImGui::EndTooltip(); - } -} - -// Adds a "?" next to the previous ImGui item with a custom tooltip -void InsertHelpHoverText(const std::string& text) { - ImGui::SameLine(); - ImGui::TextColored(ImVec4(0.7f, 0.7f, 0.7f, 1.0f), "?"); - if (ImGui::IsItemHovered()) { - ImGui::BeginTooltip(); - ImGui::Text("%s", WrappedText(text, 60)); - ImGui::EndTooltip(); - } -} - -void InsertHelpHoverText(const char* text) { - ImGui::SameLine(); - ImGui::TextColored(ImVec4(0.7f, 0.7f, 0.7f, 1.0f), "?"); - if (ImGui::IsItemHovered()) { - ImGui::BeginTooltip(); - ImGui::Text("%s", WrappedText(text, 60)); - ImGui::EndTooltip(); - } -} - -// MARK: - UI Elements - -void Tooltip(const char* text) { - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("%s", WrappedText(text)); - } -} - -void Spacer(float height) { - ImGui::Dummy(ImVec2(0.0f, height)); -} - -void PaddedSeparator(bool padTop, bool padBottom, float extraVerticalTopPadding, float extraVerticalBottomPadding) { +void PaddedSeparator(bool padTop, bool padBottom, float extraVerticalTopPadding, + float extraVerticalBottomPadding) { if (padTop) { Spacer(extraVerticalTopPadding); } @@ -103,549 +55,34 @@ void PaddedSeparator(bool padTop, bool padBottom, float extraVerticalTopPadding, } } -void RenderCross(ImDrawList* draw_list, ImVec2 pos, ImU32 col, float sz) { - float thickness = ImMax(sz / 5.0f, 1.0f); - sz -= thickness * 0.5f; - pos += ImVec2(thickness * 0.25f, thickness * 0.25f); - - draw_list->PathLineTo(ImVec2(pos.x, pos.y)); - draw_list->PathLineTo(ImVec2(pos.x + sz, pos.y + sz)); - draw_list->PathStroke(col, 0, thickness); - - draw_list->PathLineTo(ImVec2(pos.x + sz, pos.y)); - draw_list->PathLineTo(ImVec2(pos.x, pos.y + sz)); - draw_list->PathStroke(col, 0, thickness); +void Tooltip(const char *text) { + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("%s", WrappedText(text).c_str()); + } } -bool CustomCheckbox(const char* label, bool* v, bool disabled, CheckboxGraphics disabledGraphic) { - ImGuiWindow* window = ImGui::GetCurrentWindow(); - if (window->SkipItems) { - return false; - } - - ImGuiContext& g = *GImGui; - const ImGuiStyle& style = g.Style; - const ImGuiID id = window->GetID(label); - const ImVec2 label_size = ImGui::CalcTextSize(label, NULL, true); - - const float square_sz = ImGui::GetFrameHeight(); - const ImVec2 pos = window->DC.CursorPos; - const ImRect total_bb( - pos, pos + ImVec2(square_sz + (label_size.x > 0.0f ? style.ItemInnerSpacing.x + label_size.x : 0.0f), - label_size.y + style.FramePadding.y * 2.0f)); - ImGui::ItemSize(total_bb, style.FramePadding.y); - if (!ImGui::ItemAdd(total_bb, id)) { - IMGUI_TEST_ENGINE_ITEM_INFO(id, label, - g.LastItemData.StatusFlags | ImGuiItemStatusFlags_Checkable | - (*v ? ImGuiItemStatusFlags_Checked : 0)); - return false; - } - - bool hovered, held; - bool pressed = ImGui::ButtonBehavior(total_bb, id, &hovered, &held); - if (pressed) { - *v = !(*v); - ImGui::MarkItemEdited(id); - } - - const ImRect check_bb(pos, pos + ImVec2(square_sz, square_sz)); - ImGui::RenderNavHighlight(total_bb, id); - ImGui::RenderFrame(check_bb.Min, check_bb.Max, - ImGui::GetColorU32((held && hovered) ? ImGuiCol_FrameBgActive - : hovered ? ImGuiCol_FrameBgHovered - : ImGuiCol_FrameBg), - true, style.FrameRounding); - ImU32 check_col = ImGui::GetColorU32(ImGuiCol_CheckMark); - ImU32 cross_col = ImGui::GetColorU32(ImVec4(0.50f, 0.50f, 0.50f, 1.00f)); - bool mixed_value = (g.LastItemData.ItemFlags & ImGuiItemFlags_MixedValue) != 0; - if (mixed_value) { - // Undocumented tristate/mixed/indeterminate checkbox (#2644) - // This may seem awkwardly designed because the aim is to make ImGuiItemFlags_MixedValue supported by all - // widgets (not just checkbox) - ImVec2 pad(ImMax(1.0f, IM_FLOOR(square_sz / 3.6f)), ImMax(1.0f, IM_FLOOR(square_sz / 3.6f))); - window->DrawList->AddRectFilled(check_bb.Min + pad, check_bb.Max - pad, check_col, style.FrameRounding); - } else if ((!disabled && *v) || (disabled && disabledGraphic == CheckboxGraphics::Checkmark)) { - const float pad = ImMax(1.0f, IM_FLOOR(square_sz / 6.0f)); - ImGui::RenderCheckMark(window->DrawList, check_bb.Min + ImVec2(pad, pad), check_col, square_sz - pad * 2.0f); - } else if (disabled && disabledGraphic == CheckboxGraphics::Cross) { - const float pad = ImMax(1.0f, IM_FLOOR(square_sz / 6.0f)); - RenderCross(window->DrawList, check_bb.Min + ImVec2(pad, pad), cross_col, square_sz - pad * 2.0f); - } - - ImVec2 label_pos = ImVec2(check_bb.Max.x + style.ItemInnerSpacing.x, check_bb.Min.y + style.FramePadding.y); - if (g.LogEnabled) { - ImGui::LogRenderedText(&label_pos, mixed_value ? "[~]" : *v ? "[x]" : "[ ]"); - } - if (label_size.x > 0.0f) { - ImGui::RenderText(label_pos, label); - } - - IMGUI_TEST_ENGINE_ITEM_INFO(id, label, - g.LastItemData.StatusFlags | ImGuiItemStatusFlags_Checkable | - (*v ? ImGuiItemStatusFlags_Checked : 0)); - return pressed; -} - -void ReEnableComponent(const char* disabledTooltipText) { - // End of disable region of previous component - ImGui::PopStyleVar(1); - if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled) && strcmp(disabledTooltipText, "") != 0) { - ImGui::SetTooltip("%s", disabledTooltipText); - } - ImGui::PopItemFlag(); -} - -void DisableComponent(const float alpha) { - ImGui::PushItemFlag(ImGuiItemFlags_Disabled, true); - ImGui::PushStyleVar(ImGuiStyleVar_Alpha, alpha); -} - -bool EnhancementCheckbox(const char* text, const char* cvarName, bool disabled, const char* disabledTooltipText, - CheckboxGraphics disabledGraphic, bool defaultValue) { - bool changed = false; - if (disabled) { - DisableComponent(ImGui::GetStyle().Alpha * 0.5f); - } - - bool val = (bool)CVarGetInteger(cvarName, defaultValue); - if (CustomCheckbox(text, &val, disabled, disabledGraphic)) { - CVarSetInteger(cvarName, val); - Ship::Context::GetInstance()->GetWindow()->GetGui()->SaveConsoleVariablesNextFrame(); - changed = true; - } - - if (disabled) { - ReEnableComponent(disabledTooltipText); - } - return changed; -} - -bool PaddedEnhancementCheckbox(const char* text, const char* cvarName, bool padTop, bool padBottom, bool disabled, - const char* disabledTooltipText, CheckboxGraphics disabledGraphic, bool defaultValue) { - ImGui::BeginGroup(); - if (padTop) - Spacer(0); - - bool changed = EnhancementCheckbox(text, cvarName, disabled, disabledTooltipText, disabledGraphic, defaultValue); - - if (padBottom) - Spacer(0); - ImGui::EndGroup(); - return changed; -} - -bool EnhancementCombobox(const char* cvarName, std::span comboArray, - uint8_t defaultIndex, bool disabled, const char* disabledTooltipText, uint8_t disabledValue) { - bool changed = false; - if (defaultIndex <= 0) { - defaultIndex = 0; - } - - if (disabled) { - DisableComponent(ImGui::GetStyle().Alpha * 0.5f); - } - - uint8_t selected = CVarGetInteger(cvarName, defaultIndex); - std::string comboName = std::string("##") + std::string(cvarName); - if (ImGui::BeginCombo(comboName.c_str(), comboArray[selected])) { - for (uint8_t i = 0; i < comboArray.size(); i++) { - if (strlen(comboArray[i]) > 1) { - if (ImGui::Selectable(comboArray[i], i == selected)) { - CVarSetInteger(cvarName, i); - selected = i; - changed = true; - Ship::Context::GetInstance()->GetWindow()->GetGui()->SaveConsoleVariablesNextFrame(); - } - } - } - ImGui::EndCombo(); - } - - if (disabled) { - ReEnableComponent(disabledTooltipText); - - if (disabledValue >= 0 && selected != disabledValue) { - CVarSetInteger(cvarName, disabledValue); - changed = true; - Ship::Context::GetInstance()->GetWindow()->GetGui()->SaveConsoleVariablesNextFrame(); - } - } - - return changed; -} - -bool LabeledRightAlignedEnhancementCombobox(const char* label, const char* cvarName, - std::span comboArray, - uint8_t defaultIndex, bool disabled, const char* disabledTooltipText, - uint8_t disabledValue) { - ImGui::Text(label); - s32 currentValue = CVarGetInteger(cvarName, defaultIndex); - -#ifdef __WIIU__ - ImGui::SameLine(ImGui::GetContentRegionAvail().x - (ImGui::CalcTextSize(comboArray[currentValue]).x + 40.0f)); - ImGui::PushItemWidth(ImGui::CalcTextSize(comboArray[currentValue]).x + 60.0f); -#else - ImGui::SameLine(ImGui::GetContentRegionAvail().x - (ImGui::CalcTextSize(comboArray[currentValue]).x + 20.0f)); - ImGui::PushItemWidth(ImGui::CalcTextSize(comboArray[currentValue]).x + 30.0f); -#endif - - bool changed = - EnhancementCombobox(cvarName, comboArray, defaultIndex, disabled, disabledTooltipText, disabledValue); - - ImGui::PopItemWidth(); - return changed; -} - -void PaddedText(const char* text, bool padTop, bool padBottom) { - if (padTop) - Spacer(0); - - ImGui::Text("%s", text); - - if (padBottom) - Spacer(0); -} - -bool EnhancementSliderInt(const char* text, const char* id, const char* cvarName, int min, int max, const char* format, - int defaultValue, bool PlusMinusButton, bool disabled, const char* disabledTooltipText) { - bool changed = false; - int val = CVarGetInteger(cvarName, defaultValue); - - if (disabled) { - DisableComponent(ImGui::GetStyle().Alpha * 0.5f); - } - - ImGui::Text(text, val); - Spacer(0); - - ImGui::BeginGroup(); - if (PlusMinusButton) { - std::string MinusBTNName = " - ##" + std::string(cvarName); - if (ImGui::Button(MinusBTNName.c_str())) { - val--; - changed = true; - } - ImGui::SameLine(); - ImGui::SetCursorPosX(ImGui::GetCursorPosX() - 7.0f); - } - - ImGui::PushItemWidth( - std::min((ImGui::GetContentRegionAvail().x - (PlusMinusButton ? sliderButtonWidth : 0.0f)), maxSliderWidth)); - if (ImGui::SliderInt(id, &val, min, max, format, ImGuiSliderFlags_AlwaysClamp)) { - changed = true; - } - ImGui::PopItemWidth(); - - if (PlusMinusButton) { - std::string PlusBTNName = " + ##" + std::string(cvarName); - ImGui::SameLine(); - ImGui::SetCursorPosX(ImGui::GetCursorPosX() - 7.0f); - if (ImGui::Button(PlusBTNName.c_str())) { - val++; - changed = true; - } - } - ImGui::EndGroup(); - - if (disabled) { - ReEnableComponent(disabledTooltipText); - } - - if (val < min) { - val = min; - changed = true; - } - - if (val > max) { - val = max; - changed = true; - } - - if (changed) { - CVarSetInteger(cvarName, val); - Ship::Context::GetInstance()->GetWindow()->GetGui()->SaveConsoleVariablesNextFrame(); - } - - return changed; -} - -bool EnhancementSliderFloat(const char* text, const char* id, const char* cvarName, float min, float max, - const char* format, float defaultValue, bool isPercentage, bool PlusMinusButton, - bool disabled, const char* disabledTooltipText) { - bool changed = false; - float val = CVarGetFloat(cvarName, defaultValue); - - if (disabled) { - DisableComponent(ImGui::GetStyle().Alpha * 0.5f); - } - - if (!isPercentage) { - ImGui::Text(text, val); - } else { - ImGui::Text(text, static_cast(100 * val)); - } - Spacer(0); - - ImGui::BeginGroup(); - if (PlusMinusButton) { - std::string MinusBTNName = " - ##" + std::string(cvarName); - if (ImGui::Button(MinusBTNName.c_str())) { - if (isPercentage) { - val -= 0.01f; - } else { - val -= 0.1f; - } - changed = true; - } - ImGui::SameLine(); - ImGui::SetCursorPosX(ImGui::GetCursorPosX() - 7.0f); - } - - ImGui::PushItemWidth( - std::min((ImGui::GetContentRegionAvail().x - (PlusMinusButton ? sliderButtonWidth : 0.0f)), maxSliderWidth)); - if (ImGui::SliderFloat(id, &val, min, max, format, ImGuiSliderFlags_AlwaysClamp)) { - if (isPercentage) { - val = roundf(val * 100) / 100; - } - changed = true; - } - ImGui::PopItemWidth(); - - if (PlusMinusButton) { - std::string PlusBTNName = " + ##" + std::string(cvarName); - ImGui::SameLine(); - ImGui::SetCursorPosX(ImGui::GetCursorPosX() - 7.0f); - if (ImGui::Button(PlusBTNName.c_str())) { - if (isPercentage) { - val += 0.01f; - } else { - val += 0.1f; - } - changed = true; - } - } - ImGui::EndGroup(); - - if (disabled) { - ReEnableComponent(disabledTooltipText); - } - - if (val < min) { - val = min; - changed = true; - } - - if (val > max) { - val = max; - changed = true; - } - - if (changed) { - CVarSetFloat(cvarName, val); - Ship::Context::GetInstance()->GetWindow()->GetGui()->SaveConsoleVariablesNextFrame(); - } - - return changed; -} - -bool PaddedEnhancementSliderInt(const char* text, const char* id, const char* cvarName, int min, int max, - const char* format, int defaultValue, bool PlusMinusButton, bool padTop, bool padBottom, - bool disabled, const char* disabledTooltipText) { - bool changed = false; - ImGui::BeginGroup(); - if (padTop) - Spacer(0); - - changed = EnhancementSliderInt(text, id, cvarName, min, max, format, defaultValue, PlusMinusButton, disabled, - disabledTooltipText); - - if (padBottom) - Spacer(0); - ImGui::EndGroup(); - return changed; -} - -bool PaddedEnhancementSliderFloat(const char* text, const char* id, const char* cvarName, float min, float max, - const char* format, float defaultValue, bool isPercentage, bool PlusMinusButton, - bool padTop, bool padBottom, bool disabled, const char* disabledTooltipText) { - bool changed = false; - ImGui::BeginGroup(); - if (padTop) - Spacer(0); - - changed = EnhancementSliderFloat(text, id, cvarName, min, max, format, defaultValue, isPercentage, PlusMinusButton, - disabled, disabledTooltipText); - - if (padBottom) - Spacer(0); - ImGui::EndGroup(); - return changed; -} - -bool EnhancementRadioButton(const char* text, const char* cvarName, int id) { - /*Usage : - EnhancementRadioButton("My Visible Name","gMyCVarName", MyID); - First arg is the visible name of the Radio button - Second is the cvar name where MyID will be saved. - Note: the CVar name should be the same to each Buddies. - Example : - EnhancementRadioButton("English", "gLanguages", LANGUAGE_ENG); - EnhancementRadioButton("German", "gLanguages", LANGUAGE_GER); - EnhancementRadioButton("French", "gLanguages", LANGUAGE_FRA); - */ - std::string make_invisible = "##" + std::string(text) + std::string(cvarName); - - bool ret = false; - int val = CVarGetInteger(cvarName, 0); - if (ImGui::RadioButton(make_invisible.c_str(), id == val)) { - CVarSetInteger(cvarName, id); - Ship::Context::GetInstance()->GetWindow()->GetGui()->SaveConsoleVariablesNextFrame(); - ret = true; - } - ImGui::SameLine(); - ImGui::Text("%s", text); - - return ret; -} - -bool DrawResetColorButton(const char* cvarName, ImVec4* colors, ImVec4 defaultcolors, bool has_alpha) { - bool changed = false; - std::string Cvar_RBM = std::string(cvarName) + "RBM"; - std::string MakeInvisible = "Reset##" + std::string(cvarName) + "Reset"; - if (ImGui::Button(MakeInvisible.c_str())) { - colors->x = defaultcolors.x; - colors->y = defaultcolors.y; - colors->z = defaultcolors.z; - colors->w = has_alpha ? defaultcolors.w : 255.0f; - - Color_RGBA8 colorsRGBA; - colorsRGBA.r = defaultcolors.x; - colorsRGBA.g = defaultcolors.y; - colorsRGBA.b = defaultcolors.z; - colorsRGBA.a = has_alpha ? defaultcolors.w : 255.0f; - - CVarSetColor(cvarName, colorsRGBA); - CVarSetInteger(Cvar_RBM.c_str(), 0); // On click disable rainbow mode. - Ship::Context::GetInstance()->GetWindow()->GetGui()->SaveConsoleVariablesNextFrame(); - changed = true; - } - Tooltip("Revert colors to the game's original colors (GameCube version)\nOverwrites previously chosen color"); - return changed; -} - -void DrawLockColorCheckbox(const char* cvarName) { - std::string Cvar_Lock = std::string(cvarName) + "Lock"; - s32 lock = CVarGetInteger(Cvar_Lock.c_str(), 0); - std::string FullName = "Lock##" + Cvar_Lock; - EnhancementCheckbox(FullName.c_str(), Cvar_Lock.c_str()); - Tooltip("Prevents this color from being changed upon selecting \"Randomize all\""); -} - -void RainbowColor(const char* cvarName, ImVec4* colors) { - std::string Cvar_RBM = std::string(cvarName) + "RBM"; - std::string MakeInvisible = "Rainbow##" + std::string(cvarName) + "Rainbow"; - - EnhancementCheckbox(MakeInvisible.c_str(), Cvar_RBM.c_str()); - Tooltip("Cycles through colors on a timer\nOverwrites previously chosen color"); -} - -void LoadPickersColors(ImVec4& ColorArray, const char* cvarname, const ImVec4& default_colors, bool has_alpha) { - Color_RGBA8 defaultColors; - defaultColors.r = default_colors.x; - defaultColors.g = default_colors.y; - defaultColors.b = default_colors.z; - defaultColors.a = default_colors.w; - - Color_RGBA8 cvarColor = CVarGetColor(cvarname, defaultColors); - - ColorArray.x = cvarColor.r / 255.0; - ColorArray.y = cvarColor.g / 255.0; - ColorArray.z = cvarColor.b / 255.0; - ColorArray.w = cvarColor.a / 255.0; -} - -void DrawFlagArray32(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 DrawFlagArray16(const std::string& name, uint16_t& flags) { - ImGui::PushID(name.c_str()); - for (int16_t flagIndex = 0; flagIndex < 16; flagIndex++) { - if ((flagIndex % 8) != 0) { - ImGui::SameLine(); - } - ImGui::PushID(flagIndex); - uint16_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 DrawFlagArray8(const std::string& name, uint8_t& flags) { - ImGui::PushID(name.c_str()); - for (int8_t flagIndex = 0; flagIndex < 8; flagIndex++) { - if ((flagIndex % 8) != 0) { - ImGui::SameLine(); - } - ImGui::PushID(flagIndex); - uint8_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(); -} - -// V2 - -void PushStyleMenu(const ImVec4& color) { +void PushStyleMenu(const ImVec4 &color) { ImGui::PushStyleColor(ImGuiCol_Header, ImVec4(color.x, color.y, color.z, 0.5f)); ImGui::PushStyleColor(ImGuiCol_HeaderHovered, ImVec4(color.x, color.y, color.z, 1.0f)); - ImGui::PushStyleColor(ImGuiCol_PopupBg, UIWidgets::Colors::DarkGray); - ImGui::PushStyleColor(ImGuiCol_Border, UIWidgets::Colors::DarkGray); + ImGui::PushStyleColor(ImGuiCol_PopupBg, ColorValues.at(Colors::DarkGray)); + ImGui::PushStyleColor(ImGuiCol_Border, ColorValues.at(Colors::DarkGray)); ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(8.0f, 15.0f)); ImGui::PushStyleVar(ImGuiStyleVar_PopupBorderSize, 3.0f); } +void PushStyleMenu(Colors color) { + PushStyleMenu(ColorValues.at(color)); +} + void PopStyleMenu() { ImGui::PopStyleVar(2); ImGui::PopStyleColor(4); } -bool BeginMenu(const char* label, const ImVec4& color) { +bool BeginMenu(const char* label, Colors color) { bool dirty = false; PushStyleMenu(color); + ImGui::SetNextWindowSizeConstraints(ImVec2(200.0f, 0.0f), ImVec2(FLT_MAX, FLT_MAX)); if (ImGui::BeginMenu(label)) { dirty = true; } @@ -653,17 +90,21 @@ bool BeginMenu(const char* label, const ImVec4& color) { return dirty; } -void PushStyleMenuItem(const ImVec4& color) { - ImGui::PushStyleColor(ImGuiCol_HeaderHovered, ImVec4(color.x, color.y, color.z, 1.0f)); +void PushStyleMenuItem(const ImVec4 &color) { + ImGui::PushStyleColor(ImGuiCol_HeaderHovered, color); ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(20.0f, 15.0f)); } +void PushStyleMenuItem(Colors color) { + PushStyleMenuItem(ColorValues.at(color)); +} + void PopStyleMenuItem() { ImGui::PopStyleVar(1); ImGui::PopStyleColor(1); } -bool MenuItem(const char* label, const char* shortcut, const ImVec4& color) { +bool MenuItem(const char* label, const char* shortcut, Colors color) { bool dirty = false; PushStyleMenuItem(color); if (ImGui::MenuItem(label, shortcut)) { @@ -673,38 +114,79 @@ bool MenuItem(const char* label, const char* shortcut, const ImVec4& color) { return dirty; } -void PushStyleButton(const ImVec4& color) { +void PushStyleButton(const ImVec4 &color, const ImVec2 padding) { ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(color.x, color.y, color.z, 1.0f)); ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(color.x, color.y, color.z, 0.8f)); ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4(color.x, color.y, color.z, 0.6f)); ImGui::PushStyleColor(ImGuiCol_Border, ImVec4(0.0f, 0.0f, 0.0f, 0.3f)); ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 3.0f); - ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(10.0f, 8.0f)); + ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, padding); ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 5.0f); } +void PushStyleButton(Colors color, ImVec2 padding) { + PushStyleButton(ColorValues.at(color), padding); +} + void PopStyleButton() { ImGui::PopStyleVar(3); ImGui::PopStyleColor(4); } -bool Button(const char* label, const ButtonOptions& options) { +void PushStyleInput(const ImVec4 &color) { + ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(color.x, color.y, color.z, 1.0f)); + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(color.x, color.y, color.z, 0.8f)); + ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4(color.x, color.y, color.z, 0.6f)); + ImGui::PushStyleColor(ImGuiCol_FrameBg, ImVec4(color.x, color.y, color.z, 1.0f)); + ImGui::PushStyleColor(ImGuiCol_FrameBgHovered, ImVec4(color.x, color.y, color.z, 0.8f)); + ImGui::PushStyleColor(ImGuiCol_FrameBgActive, ImVec4(color.x, color.y, color.z, 0.6f)); + ImGui::PushStyleColor(ImGuiCol_Border, ImVec4(0.0f, 0.0f, 0.0f, 0.3f)); + ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 3.0f); + ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(10.0f, 6.0f)); + ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 5.0f); +} + +void PushStyleInput(Colors color) { + PushStyleInput(ColorValues.at(color)); +} + +void PopStyleInput() { + ImGui::PopStyleVar(3); + ImGui::PopStyleColor(7); +} + +void PushStyleHeader(const ImVec4 &color) { + ImGui::PushStyleColor(ImGuiCol_Header, ImVec4(color.x, color.y, color.z, 1.0f)); + ImGui::PushStyleColor(ImGuiCol_HeaderHovered, ImVec4(color.x, color.y, color.z, 0.8f)); + ImGui::PushStyleColor(ImGuiCol_HeaderActive, ImVec4(color.x, color.y, color.z, 0.6f)); +} + +void PushStyleHeader(Colors color) { + PushStyleHeader(ColorValues.at(color)); +} + +void PopStyleHeader() { + ImGui::PopStyleColor(3); +} + +bool Button(const char* label, const ButtonOptions &options) { ImGui::BeginDisabled(options.disabled); - PushStyleButton(options.color); + PushStyleButton(options.color, options.padding); bool dirty = ImGui::Button(label, options.size); PopStyleButton(); ImGui::EndDisabled(); - if (options.disabled && ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled) && - strcmp(options.disabledTooltip, "") != 0) { - ImGui::SetTooltip("%s", WrappedText(options.disabledTooltip)); - } else if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled) && strcmp(options.tooltip, "") != 0) { - ImGui::SetTooltip("%s", WrappedText(options.tooltip)); + if (options.disabled && ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled) + && !Ship_IsCStringEmpty(options.disabledTooltip)) { + ImGui::SetTooltip("%s", WrappedText(options.disabledTooltip).c_str()); + } else if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled) + && !Ship_IsCStringEmpty(options.tooltip)) { + ImGui::SetTooltip("%s", WrappedText(options.tooltip).c_str()); } return dirty; } bool WindowButton(const char* label, const char* cvarName, std::shared_ptr windowPtr, - const ButtonOptions& options) { + const WindowButtonOptions &options) { ImGui::PushStyleVar(ImGuiStyleVar_ButtonTextAlign, ImVec2(0, 0)); std::string buttonText = label; bool dirty = false; @@ -713,7 +195,10 @@ bool WindowButton(const char* label, const char* cvarName, std::shared_ptrToggleVisibility(); dirty = true; } @@ -721,91 +206,263 @@ bool WindowButton(const char* label, const char* cvarName, std::shared_ptrDrawList->AddText(g.Font, g.FontSize, pos, ImGui::GetColorU32(ImGuiCol_Text), text, + text_display_end); + if (g.LogEnabled) + ImGui::LogRenderedText(&pos, text, text_display_end); + } +} + +bool Checkbox(const char *_label, bool *value, const CheckboxOptions &options) { + ImGuiWindow *window = ImGui::GetCurrentWindow(); + if (window->SkipItems) + return false; + + ImGui::BeginDisabled(options.disabled); + + bool above = options.labelPosition == LabelPositions::Above; + bool lpFar = options.labelPosition == LabelPositions::Far; + bool right = options.alignment == ComponentAlignments::Right; + bool none = options.labelPosition == LabelPositions::None; + + std::string labelStr = (none ? "##" : ""); + labelStr.append(_label); + + const char *label = labelStr.c_str(); + + PushStyleCheckbox(options.color, options.padding); + ImGuiContext &g = *GImGui; + const ImGuiStyle &style = g.Style; + const ImGuiID id = window->GetID(label); + const ImVec2 label_size = ImGui::CalcTextSize(label, NULL, true); + const float square_sz = ImGui::GetFrameHeight(); + ImVec2 pos = window->DC.CursorPos; + + if (right) { + float labelOffsetX = (above ? 0 : (style.ItemInnerSpacing.x * 2.0f) + square_sz); + if (!lpFar) { + pos.x += ImGui::GetContentRegionAvail().x - (label_size.x + labelOffsetX); + } + } + float bbAboveX = lpFar + ? ImGui::GetContentRegionAvail().x + : (label_size.x + (above ? 0 : (style.ItemInnerSpacing.x * 2.0f) + square_sz)); + float bbAboveY = label_size.y + (above ? square_sz : 0) + (style.FramePadding.y * 2.0f); + const ImRect total_bb(pos, pos + ImVec2(bbAboveX, bbAboveY)); + + ImGui::ItemSize(total_bb, style.FramePadding.y); + if (!ImGui::ItemAdd(total_bb, id)) { + PopStyleCheckbox(); + ImGui::EndDisabled(); + return false; + } + bool hovered, held, pressed; + pressed = ImGui::ButtonBehavior(total_bb, id, &hovered, &held); + if (pressed) { + *value = !(*value); + ImGui::MarkItemEdited(id); + } + ImVec2 checkPos = pos; + ImVec2 labelPos = pos; + if (options.labelPosition == LabelPositions::Above) { + checkPos.y += label_size.y + (style.ItemInnerSpacing.y * 2.0f); + } else { + // Center with checkbox automatically + labelPos.y += ImGui::GetStyle().FramePadding.y; + } + if (options.alignment == ComponentAlignments::Right) { + checkPos.x = total_bb.Max.x - square_sz; + } else { + float labelFarOffset = ImGui::GetContentRegionAvail().x - label_size.x; + float labelOffsetX = + above ? 0 : (lpFar ? labelFarOffset : (style.ItemInnerSpacing.x * 2.0f) + square_sz); + labelPos.x += labelOffsetX; + } + const ImRect check_bb(checkPos, checkPos + ImVec2(square_sz, square_sz)); + ImGui::RenderNavHighlight(total_bb, id); + ImGui::RenderFrame(check_bb.Min, check_bb.Max, + ImGui::GetColorU32((held && hovered) ? ImGuiCol_FrameBgActive + : hovered ? ImGuiCol_FrameBgHovered + : ImGuiCol_FrameBg), + true, style.FrameRounding); + ImU32 check_col = ImGui::GetColorU32(ImGuiCol_CheckMark); + bool mixed_value = (g.LastItemData.ItemFlags & ImGuiItemFlags_MixedValue) != 0; + if (mixed_value) { + // Undocumented tristate/mixed/indeterminate checkbox (#2644) + // This may seem awkwardly designed because the aim is to make ImGuiItemFlags_MixedValue + // supported by all widgets (not just checkbox) + ImVec2 pad(ImMax(1.0f, IM_TRUNC(square_sz / 3.6f)), ImMax(1.0f, IM_TRUNC(square_sz / 3.6f))); + window->DrawList->AddRectFilled(check_bb.Min + pad, check_bb.Max - pad, check_col, + style.FrameRounding); + } else if (*value) { + const float pad = ImMax(1.0f, IM_TRUNC(square_sz / 6.0f)); + ImGui::RenderCheckMark(window->DrawList, check_bb.Min + ImVec2(pad, pad), check_col, + square_sz - pad * 2.0f); + } + RenderText(labelPos, label, ImGui::FindRenderedTextEnd(label), true); + PopStyleCheckbox(); + ImGui::EndDisabled(); + if (options.disabled && ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled) + && !Ship_IsCStringEmpty(options.disabledTooltip)) { + ImGui::SetTooltip("%s", WrappedText(options.disabledTooltip).c_str()); + } else if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled) + && !Ship_IsCStringEmpty(options.tooltip)) { + ImGui::SetTooltip("%s", WrappedText(options.tooltip).c_str()); + } + return pressed; +} + +bool CVarCheckbox(const char *label, const char *cvarName, const CheckboxOptions &options) { bool dirty = false; - bool value = (bool)CVarGetInteger(cvarName, options.defaultValue); + bool value = (bool) CVarGetInteger(cvarName, options.defaultValue); if (Checkbox(label, &value, options)) { CVarSetInteger(cvarName, value); Ship::Context::GetInstance()->GetWindow()->GetGui()->SaveConsoleVariablesNextFrame(); + ShipInit::Init(cvarName); dirty = true; } return dirty; } -void PushStyleCombobox(const ImVec4& color) { +bool StateButton(const char *str_id, const char *label, ImVec2 size, ButtonOptions options, + ImGuiButtonFlags flags) { + + ImGuiContext &g = *GImGui; + ImGuiWindow *window = ImGui::GetCurrentWindow(); + if (window->SkipItems) { + return false; + } + + const ImGuiStyle &style = g.Style; + const ImVec2 label_size = ImGui::CalcTextSize(label, NULL, true); + + const ImGuiID id = window->GetID(str_id); + const ImRect bb(window->DC.CursorPos, window->DC.CursorPos + size); + const float default_size = ImGui::GetFrameHeight(); + ImGui::ItemSize(size, (size.y >= default_size) ? g.Style.FramePadding.y : -1.0f); + if (!ImGui::ItemAdd(bb, id)) + return false; + + if (g.LastItemData.ItemFlags & ImGuiItemFlags_ButtonRepeat) { + ImGui::PushItemFlag(ImGuiItemFlags_ButtonRepeat, true); + } + + bool hovered, held; + bool pressed = ImGui::ButtonBehavior(bb, id, &hovered, &held, flags); + + if (g.LastItemData.ItemFlags & ImGuiItemFlags_ButtonRepeat) { + ImGui::PopItemFlag(); // ImGuiItemFlags_ButtonRepeat; + } + PushStyleButton(options.color); + // Render + const ImU32 bg_col = ImGui::GetColorU32((held && hovered) ? ImGuiCol_ButtonActive + : hovered ? ImGuiCol_ButtonHovered + : ImGuiCol_Button); + // const ImU32 text_col = ImGui::GetColorU32(ImGuiCol_Text); + ImGui::RenderNavHighlight(bb, id); + ImGui::RenderFrame(bb.Min, bb.Max, bg_col, true, g.Style.FrameRounding); + ImGui::RenderTextClipped(bb.Min + (style.FramePadding * 0.35f), bb.Max - (style.FramePadding / 4), + label, NULL, &label_size, style.ButtonTextAlign, &bb); + PopStyleButton(); + /*ImGui::RenderArrow(window->DrawList, + bb.Min + + ImVec2(ImMax(0.0f, (size.x - g.FontSize) * 0.5f), ImMax(0.0f, (size.y - g.FontSize) * 0.5f)), + text_col, dir);*/ + + IMGUI_TEST_ENGINE_ITEM_INFO(id, str_id, g.LastItemData.StatusFlags); + return pressed; +} + +float CalcComboWidth(const char *preview_value, ImGuiComboFlags flags) { + ImGuiContext &g = *GImGui; + + const ImGuiStyle &style = g.Style; + IM_ASSERT((flags & (ImGuiComboFlags_NoArrowButton | ImGuiComboFlags_NoPreview)) + != (ImGuiComboFlags_NoArrowButton + | ImGuiComboFlags_NoPreview)); // Can't use both flags together + if (flags & ImGuiComboFlags_WidthFitPreview) + IM_ASSERT( + (flags & (ImGuiComboFlags_NoPreview | (ImGuiComboFlags) ImGuiComboFlags_CustomPreview)) + == 0); + + const float arrow_size = (flags & ImGuiComboFlags_NoArrowButton) ? 0.0f : ImGui::GetFrameHeight(); + const float preview_width = ImGui::CalcTextSize(preview_value, NULL, true).x; + float w = arrow_size + preview_width + (style.FramePadding.x * 2.0f); + return w; +} + +void PushStyleCombobox(const ImVec4 &color) { ImGui::PushStyleColor(ImGuiCol_FrameBg, ImVec4(color.x, color.y, color.z, 0.8f)); ImGui::PushStyleColor(ImGuiCol_FrameBgHovered, ImVec4(color.x, color.y, color.z, 0.6f)); ImGui::PushStyleColor(ImGuiCol_FrameBgActive, ImVec4(color.x, color.y, color.z, 0.6f)); @@ -821,100 +478,39 @@ void PushStyleCombobox(const ImVec4& color) { ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(10.0f, 6.0f)); } +void PushStyleCombobox(Colors color) { + PushStyleCombobox(ColorValues.at(color)); +} + void PopStyleCombobox() { ImGui::PopStyleVar(4); ImGui::PopStyleColor(9); } -bool Combobox(const char* label, uint8_t* value, std::span comboArray, - const ComboboxOptions& options) { - bool dirty = false; - float startX = ImGui::GetCursorPosX(); - std::string invisibleLabelStr = "##" + std::string(label); - const char* invisibleLabel = invisibleLabelStr.c_str(); - ImGui::PushID(label); - ImGui::BeginGroup(); - ImGui::BeginDisabled(options.disabled); - PushStyleCombobox(options.color); - if (options.alignment == ComponentAlignment::Left) { - if (options.labelPosition == LabelPosition::Above) { - ImGui::Text(label); - ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x); - } else if (options.labelPosition == LabelPosition::Near) { - ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x - ImGui::CalcTextSize(label).x - - ImGui::GetStyle().ItemSpacing.x * 2); - } else if (options.labelPosition == LabelPosition::Far || options.labelPosition == LabelPosition::None) { - ImGui::SetNextItemWidth(ImGui::CalcTextSize(comboArray[*value]).x + ImGui::GetStyle().FramePadding.x * 4 + - ImGui::GetStyle().ItemSpacing.x); - } - } else if (options.alignment == ComponentAlignment::Right) { - if (options.labelPosition == LabelPosition::Above) { - ImGui::NewLine(); - ImGui::SameLine(ImGui::GetContentRegionAvail().x - ImGui::CalcTextSize(label).x); - ImGui::Text(label); - ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x); - } else if (options.labelPosition == LabelPosition::Near) { - ImGui::SameLine(ImGui::CalcTextSize(label).x + ImGui::GetStyle().ItemSpacing.x * 2); - ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x); - } else if (options.labelPosition == LabelPosition::Far || options.labelPosition == LabelPosition::None) { - float width = ImGui::CalcTextSize(comboArray[*value]).x + ImGui::GetStyle().FramePadding.x * 4; - ImGui::SameLine(ImGui::GetContentRegionAvail().x - width); - ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x); - } - } - if (ImGui::BeginCombo(invisibleLabel, comboArray[*value], options.flags)) { - ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(10.0f, 10.0f)); - for (uint8_t i = 0; i < comboArray.size(); i++) { - if (strlen(comboArray[i]) > 1) { - if (ImGui::Selectable(comboArray[i], i == *value)) { - *value = i; - dirty = true; - } - } - } - ImGui::PopStyleVar(); - ImGui::EndCombo(); - } - if (options.alignment == ComponentAlignment::Left) { - if (options.labelPosition == LabelPosition::Near) { - ImGui::SameLine(); - ImGui::Text(label); - } else if (options.labelPosition == LabelPosition::Far) { - ImGui::SameLine(ImGui::GetContentRegionAvail().x - ImGui::CalcTextSize(label).x); - ImGui::Text(label); - } - } else if (options.alignment == ComponentAlignment::Right) { - if (options.labelPosition == LabelPosition::Near || options.labelPosition == LabelPosition::Far) { - ImGui::SameLine(startX); - ImGui::Text(label); - } - } - PopStyleCombobox(); - ImGui::EndDisabled(); - ImGui::EndGroup(); - if (options.disabled && ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled) && - strcmp(options.disabledTooltip, "") != 0) { - ImGui::SetTooltip("%s", WrappedText(options.disabledTooltip)); - } else if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled) && strcmp(options.tooltip, "") != 0) { - ImGui::SetTooltip("%s", WrappedText(options.tooltip)); - } - ImGui::PopID(); - return dirty; +void PushStyleTabs(const ImVec4 &color) { + ImGui::PushStyleColor(ImGuiCol_Tab, ImVec4(color.x, color.y, color.z, 0.8f)); + ImGui::PushStyleColor(ImGuiCol_TabHovered, ImVec4(color.x, color.y, color.z, 0.6f)); + ImGui::PushStyleColor(ImGuiCol_TabActive, ImVec4(color.x, color.y, color.z, 0.6f)); + ImGui::PushStyleColor(ImGuiCol_Header, ImVec4(color.x, color.y, color.z, 0.5f)); + ImGui::PushStyleColor(ImGuiCol_HeaderHovered, ImVec4(color.x, color.y, color.z, 0.6f)); + ImGui::PushStyleColor(ImGuiCol_HeaderActive, ImVec4(color.x, color.y, color.z, 0.6f)); + ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 3.0f); + ImGui::PushStyleVar(ImGuiStyleVar_PopupRounding, 3.0f); + ImGui::PushStyleVar(ImGuiStyleVar_PopupBorderSize, 0.0f); + ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(10.0f, 6.0f)); } -bool CVarCombobox(const char* label, const char* cvarName, std::span comboArray, - const ComboboxOptions& options) { - bool dirty = false; - uint8_t value = (uint8_t)CVarGetInteger(cvarName, options.defaultIndex); - if (Combobox(label, &value, comboArray, options)) { - CVarSetInteger(cvarName, value); - Ship::Context::GetInstance()->GetWindow()->GetGui()->SaveConsoleVariablesNextFrame(); - dirty = true; - } - return dirty; +void PushStyleTabs(Colors color) { + PushStyleTabs(ColorValues.at(color)); } -void PushStyleSlider(const ImVec4& color) { +void PopStyleTabs() { + ImGui::PopStyleColor(6); + ImGui::PopStyleVar(4); +} + +void PushStyleSlider(Colors color_) { + const ImVec4 &color = ColorValues.at(color_); ImGui::PushStyleColor(ImGuiCol_FrameBg, ImVec4(color.x, color.y, color.z, 1.0f)); ImGui::PushStyleColor(ImGuiCol_FrameBgHovered, ImVec4(color.x, color.y, color.z, 1.0f)); ImGui::PushStyleColor(ImGuiCol_FrameBgActive, ImVec4(color.x, color.y, color.z, 1.0f)); @@ -932,152 +528,695 @@ void PopStyleSlider() { ImGui::PopStyleColor(6); } -bool SliderInt(const char* label, int32_t* value, int32_t min, int32_t max, const IntSliderOptions& options) { +bool SliderInt(const char *label, int32_t *value, const IntSliderOptions &options) { bool dirty = false; std::string invisibleLabelStr = "##" + std::string(label); - const char* invisibleLabel = invisibleLabelStr.c_str(); + const char *invisibleLabel = invisibleLabelStr.c_str(); ImGui::PushID(label); ImGui::BeginGroup(); ImGui::BeginDisabled(options.disabled); PushStyleSlider(options.color); - if (options.alignment == ComponentAlignment::Left) { - if (options.labelPosition == LabelPosition::Above) { - ImGui::Text(label, *value); - } - } else if (options.alignment == ComponentAlignment::Right) { - if (options.labelPosition == LabelPosition::Above) { + float width = (options.size == ImVec2(0, 0)) ? ImGui::GetContentRegionAvail().x : options.size.x; + if (options.labelPosition == LabelPositions::Near || options.labelPosition == LabelPositions::Far) { + width = width - (ImGui::CalcTextSize(label).x + ImGui::GetStyle().FramePadding.x); + } + ImGui::AlignTextToFramePadding(); + if (options.alignment == ComponentAlignments::Right) { + ImGui::Text(label, *value); + if (options.labelPosition == LabelPositions::Above) { ImGui::NewLine(); - ImGui::SameLine(ImGui::GetContentRegionAvail().x - ImGui::CalcTextSize(label).x); + ImGui::SameLine(ImGui::GetContentRegionAvail().x - width); + } else if (options.labelPosition == LabelPositions::Near) { + ImGui::SameLine(); + } else if (options.labelPosition == LabelPositions::Far + || options.labelPosition == LabelPositions::None) { + ImGui::SameLine(ImGui::GetContentRegionAvail().x - width); + } + } else if (options.alignment == ComponentAlignments::Left) { + if (options.labelPosition == LabelPositions::Above) { ImGui::Text(label, *value); } } if (options.showButtons) { - if (Button("-", { .color = options.color, .size = Sizes::Inline }) && *value > min) { + if (Button("-", ButtonOptions{ .color = options.color }.Size(Sizes::Inline)) + && *value > options.min) { *value -= options.step; - if (*value < min) - *value = min; - Ship::Context::GetInstance()->GetWindow()->GetGui()->SaveConsoleVariablesNextFrame(); + if (options.clamp) { + if (*value < options.min) { + *value = options.min; + } + } dirty = true; } ImGui::SameLine(0, 3.0f); - ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x - (ImGui::CalcTextSize("+").x + 20.0f + 3.0f)); + ImGui::SetNextItemWidth( + width - (ImGui::CalcTextSize("+").x + ImGui::GetStyle().FramePadding.x * 2 + 3) * 2); } else { - ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x); + ImGui::SetNextItemWidth(width); } - if (ImGui::SliderScalar(invisibleLabel, ImGuiDataType_S32, value, &min, &max, options.format, options.flags)) { - Ship::Context::GetInstance()->GetWindow()->GetGui()->SaveConsoleVariablesNextFrame(); + if (ImGui::SliderScalar(invisibleLabel, ImGuiDataType_S32, value, &options.min, &options.max, + options.format, options.flags)) { + if (options.clamp) { + if (*value < options.min) { + *value = options.min; + } + if (*value > options.max) + *value = options.max; + } dirty = true; } if (options.showButtons) { ImGui::SameLine(0, 3.0f); ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x); - if (Button("+", { .color = options.color, .size = Sizes::Inline }) && *value < max) { + if (Button("+", ButtonOptions{ .color = options.color }.Size(Sizes::Inline)) + && *value < options.max) { *value += options.step; - if (*value > max) - *value = max; - Ship::Context::GetInstance()->GetWindow()->GetGui()->SaveConsoleVariablesNextFrame(); + if (options.clamp) { + if (*value > options.max) + *value = options.max; + } dirty = true; } } + + if (options.alignment == ComponentAlignments::Left) { + if (options.labelPosition == LabelPositions::Near) { + ImGui::SameLine(); + ImGui::Text(label, *value); + } else if (options.labelPosition == LabelPositions::Far + || options.labelPosition == LabelPositions::None) { + ImGui::SameLine(ImGui::GetContentRegionAvail().x - ImGui::CalcTextSize(label).x + + ImGui::GetStyle().ItemSpacing.x); + ImGui::Text(label, *value); + } + } PopStyleSlider(); ImGui::EndDisabled(); ImGui::EndGroup(); - if (options.disabled && ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled) && - strcmp(options.disabledTooltip, "") != 0) { - ImGui::SetTooltip("%s", WrappedText(options.disabledTooltip)); - } else if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled) && strcmp(options.tooltip, "") != 0) { - ImGui::SetTooltip("%s", WrappedText(options.tooltip)); + if (options.disabled && ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled) + && !Ship_IsCStringEmpty(options.disabledTooltip)) { + ImGui::SetTooltip("%s", WrappedText(options.disabledTooltip).c_str()); + } else if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled) + && !Ship_IsCStringEmpty(options.tooltip)) { + ImGui::SetTooltip("%s", WrappedText(options.tooltip).c_str()); } ImGui::PopID(); return dirty; } -bool CVarSliderInt(const char* label, const char* cvarName, int32_t min, int32_t max, const int32_t defaultValue, - const IntSliderOptions& options) { +bool CVarSliderInt(const char *label, const char *cvarName, const IntSliderOptions &options) { bool dirty = false; - int32_t value = CVarGetInteger(cvarName, defaultValue); - if (SliderInt(label, &value, min, max, options)) { + int32_t value = CVarGetInteger(cvarName, options.defaultValue); + if (SliderInt(label, &value, options)) { CVarSetInteger(cvarName, value); Ship::Context::GetInstance()->GetWindow()->GetGui()->SaveConsoleVariablesNextFrame(); + ShipInit::Init(cvarName); dirty = true; } return dirty; } -bool SliderFloat(const char* label, float* value, float min, float max, const FloatSliderOptions& options) { +void ClampFloat(float *value, float min, float max, float step) { + int factor = 1; + if (step < 1.0f) { + factor *= 10; + } + if (step < 0.1f) { + factor *= 10; + } + if (step < 0.01f) { + factor *= 10; + } + if (step < 0.001f) { + factor *= 10; + } + if (step < 0.0001f) { + factor *= 10; + } + if (step < 0.00001f) { + factor *= 10; + } + if (*value < min) { + *value = min; + } else if (*value > max) { + *value = max; + } else { + int trunc = (int) std::round(*value * factor); + *value = (float) trunc / factor; + } +} + +bool SliderFloat(const char *label, float *value, const FloatSliderOptions &options) { bool dirty = false; std::string invisibleLabelStr = "##" + std::string(label); - const char* invisibleLabel = invisibleLabelStr.c_str(); + const char *invisibleLabel = invisibleLabelStr.c_str(); float valueToDisplay = options.isPercentage ? *value * 100.0f : *value; - float maxToDisplay = options.isPercentage ? max * 100.0f : max; - float minToDisplay = options.isPercentage ? min * 100.0f : min; + float maxToDisplay = options.isPercentage ? options.max * 100.0f : options.max; + float minToDisplay = options.isPercentage ? options.min * 100.0f : options.min; ImGui::PushID(label); ImGui::BeginGroup(); ImGui::BeginDisabled(options.disabled); PushStyleSlider(options.color); - if (options.alignment == ComponentAlignment::Left) { - if (options.labelPosition == LabelPosition::Above) { - ImGui::Text(label, valueToDisplay); - } - } else if (options.alignment == ComponentAlignment::Right) { - if (options.labelPosition == LabelPosition::Above) { + float labelSpacing = ImGui::CalcTextSize(label).x + ImGui::GetStyle().ItemSpacing.x; + float width = (options.size == ImVec2(0, 0)) ? ImGui::GetContentRegionAvail().x : options.size.x; + if (options.labelPosition == LabelPositions::Near || options.labelPosition == LabelPositions::Far) { + width = width - (ImGui::CalcTextSize(label).x + ImGui::GetStyle().FramePadding.x); + } + ImGui::AlignTextToFramePadding(); + if (options.alignment == ComponentAlignments::Right) { + ImGui::Text(label, *value); + if (options.labelPosition == LabelPositions::Above) { ImGui::NewLine(); - ImGui::SameLine(ImGui::GetContentRegionAvail().x - ImGui::CalcTextSize(label).x); - ImGui::Text(label, valueToDisplay); + ImGui::SameLine(ImGui::GetContentRegionAvail().x - width); + } else if (options.labelPosition == LabelPositions::Near) { + width -= labelSpacing; + ImGui::SameLine(); + } else if (options.labelPosition == LabelPositions::Far + || options.labelPosition == LabelPositions::None) { + width -= labelSpacing; + ImGui::SameLine(ImGui::GetContentRegionAvail().x - width); + } + } else if (options.alignment == ComponentAlignments::Left) { + if (options.labelPosition == LabelPositions::Above) { + ImGui::Text(label, *value); } } if (options.showButtons) { - if (Button("-", { .color = options.color, .size = Sizes::Inline }) && *value > min) { + if (Button("-", ButtonOptions{ .color = options.color }.Size(Sizes::Inline)) + && *value > options.min) { *value -= options.step; - if (*value < min) - *value = min; - Ship::Context::GetInstance()->GetWindow()->GetGui()->SaveConsoleVariablesNextFrame(); + if (options.clamp) { + ClampFloat(value, options.min, options.max, options.step); + } dirty = true; } ImGui::SameLine(0, 3.0f); - ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x - (ImGui::CalcTextSize("+").x + 20.0f + 3.0f)); + ImGui::SetNextItemWidth( + width - (ImGui::CalcTextSize("+").x + ImGui::GetStyle().FramePadding.x * 2 + 3) * 2); } else { - ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x); + ImGui::SetNextItemWidth(width); } - if (ImGui::SliderScalar(invisibleLabel, ImGuiDataType_Float, &valueToDisplay, &minToDisplay, &maxToDisplay, - options.format, options.flags)) { + if (ImGui::SliderScalar(invisibleLabel, ImGuiDataType_Float, &valueToDisplay, &minToDisplay, + &maxToDisplay, options.format, options.flags)) { *value = options.isPercentage ? valueToDisplay / 100.0f : valueToDisplay; - Ship::Context::GetInstance()->GetWindow()->GetGui()->SaveConsoleVariablesNextFrame(); + if (options.clamp) { + ClampFloat(value, options.min, options.max, options.step); + } dirty = true; } if (options.showButtons) { ImGui::SameLine(0, 3.0f); ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x); - if (Button("+", { .color = options.color, .size = Sizes::Inline }) && *value < max) { + if (Button("+", ButtonOptions{ .color = options.color }.Size(Sizes::Inline)) + && *value < options.max) { *value += options.step; - if (*value > max) - *value = max; - Ship::Context::GetInstance()->GetWindow()->GetGui()->SaveConsoleVariablesNextFrame(); + if (options.clamp) { + ClampFloat(value, options.min, options.max, options.step); + } dirty = true; } } + + if (options.alignment == ComponentAlignments::Left) { + if (options.labelPosition == LabelPositions::Near) { + ImGui::SameLine(); + ImGui::Text(label, *value); + } else if (options.labelPosition == LabelPositions::Far + || options.labelPosition == LabelPositions::None) { + ImGui::SameLine(ImGui::GetContentRegionAvail().x - labelSpacing); + ImGui::Text(label, *value); + } + } PopStyleSlider(); ImGui::EndDisabled(); ImGui::EndGroup(); - if (options.disabled && ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled) && - strcmp(options.disabledTooltip, "") != 0) { - ImGui::SetTooltip("%s", WrappedText(options.disabledTooltip)); - } else if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled) && strcmp(options.tooltip, "") != 0) { - ImGui::SetTooltip("%s", WrappedText(options.tooltip)); + if (options.disabled && ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled) + && !Ship_IsCStringEmpty(options.disabledTooltip)) { + ImGui::SetTooltip("%s", WrappedText(options.disabledTooltip).c_str()); + } else if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled) + && !Ship_IsCStringEmpty(options.tooltip)) { + ImGui::SetTooltip("%s", WrappedText(options.tooltip).c_str()); } ImGui::PopID(); return dirty; } -bool CVarSliderFloat(const char* label, const char* cvarName, float min, float max, const float defaultValue, - const FloatSliderOptions& options) { +bool CVarSliderFloat(const char *label, const char *cvarName, const FloatSliderOptions &options) { bool dirty = false; - float value = CVarGetFloat(cvarName, defaultValue); - if (SliderFloat(label, &value, min, max, options)) { + float value = CVarGetFloat(cvarName, options.defaultValue); + if (SliderFloat(label, &value, options)) { CVarSetFloat(cvarName, value); Ship::Context::GetInstance()->GetWindow()->GetGui()->SaveConsoleVariablesNextFrame(); + ShipInit::Init(cvarName); dirty = true; } return dirty; } + +int InputTextResizeCallback(ImGuiInputTextCallbackData *data) { + std::string *value = (std::string *) data->UserData; + if (data->EventFlag == ImGuiInputTextFlags_CallbackResize) { + value->resize(data->BufTextLen); + data->Buf = (char *) value->c_str(); + } + return 0; +} + +bool InputString(const char *label, std::string *value, const InputOptions &options) { + bool dirty = false; + ImGui::PushID(label); + ImGui::BeginGroup(); + ImGui::BeginDisabled(options.disabled); + PushStyleInput(options.color); + if (options.hasError) { + ImGui::PushStyleColor(ImGuiCol_Border, ColorValues.at(Colors::Red)); + } + float width = (options.size == ImVec2(0, 0)) ? ImGui::GetContentRegionAvail().x : options.size.x; + ImVec2 labelSize = ImGui::CalcTextSize(label, NULL, true); + if (labelSize.x != 0) { + if (options.alignment == ComponentAlignments::Left) { + if (options.labelPosition == LabelPositions::Above) { + ImGui::Text(label, *value->c_str()); + } + } else if (options.alignment == ComponentAlignments::Right) { + if (options.labelPosition == LabelPositions::Above) { + ImGui::NewLine(); + ImGui::SameLine(width - ImGui::CalcTextSize(label).x); + ImGui::Text(label, *value->c_str()); + } + } + } + ImGui::SetNextItemWidth(width); + ImGuiInputTextFlags flags = ImGuiInputTextFlags_CallbackResize; + if (options.secret) { + flags |= ImGuiInputTextFlags_Password; + } + flags |= options.addedFlags; + if (ImGui::InputText(label, (char *) value->c_str(), value->capacity() + 1, flags, + InputTextResizeCallback, value)) { + dirty = true; + } + if (value->empty() && !options.placeholder.empty()) { + ImGui::SameLine(17.0f); + ImGui::TextColored(ImVec4(1.0f, 1.0f, 1.0f, 0.4f), "%s", options.placeholder.c_str()); + } + if (options.hasError) { + ImGui::PopStyleColor(); + } + PopStyleInput(); + ImGui::EndDisabled(); + ImGui::EndGroup(); + if (options.hasError && ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled) + && !Ship_IsCStringEmpty(options.errorText)) { + ImGui::SetTooltip("%s", WrappedText(options.errorText).c_str()); + } else if (options.disabled && ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled) + && !Ship_IsCStringEmpty(options.disabledTooltip)) { + ImGui::SetTooltip("%s", WrappedText(options.disabledTooltip).c_str()); + } else if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled) + && !Ship_IsCStringEmpty(options.tooltip)) { + ImGui::SetTooltip("%s", WrappedText(options.tooltip).c_str()); + } + ImGui::PopID(); + return dirty; +} + +bool CVarInputString(const char *label, const char *cvarName, const InputOptions &options) { + bool dirty = false; + std::string value = CVarGetString(cvarName, options.defaultValue.c_str()); + if (InputString(label, &value, options)) { + CVarSetString(cvarName, value.c_str()); + Ship::Context::GetInstance()->GetWindow()->GetGui()->SaveConsoleVariablesNextFrame(); + ShipInit::Init(cvarName); + dirty = true; + } + return dirty; +} + +bool InputInt(const char *label, int32_t *value, const InputOptions &options) { + bool dirty = false; + ImGui::PushID(label); + ImGui::BeginGroup(); + ImGui::BeginDisabled(options.disabled); + PushStyleInput(options.color); + float width = (options.size == ImVec2(0, 0)) ? ImGui::GetContentRegionAvail().x : options.size.x; + if (options.alignment == ComponentAlignments::Left) { + if (options.labelPosition == LabelPositions::Above) { + ImGui::Text(label, *value); + } + } else if (options.alignment == ComponentAlignments::Right) { + if (options.labelPosition == LabelPositions::Above) { + ImGui::NewLine(); + ImGui::SameLine(width - ImGui::CalcTextSize(label).x); + ImGui::Text(label, *value); + } + } + ImGui::SetNextItemWidth(width); + if (ImGui::InputScalar(label, ImGuiDataType_S32, value, nullptr, nullptr, nullptr, + options.addedFlags)) { + dirty = true; + } + if ((ImGui::GetItemStatusFlags() & ImGuiItemStatusFlags_Edited) && !options.placeholder.empty()) { + ImGui::SameLine(17.0f); + ImGui::TextColored(ImVec4(1.0f, 1.0f, 1.0f, 0.4f), "%s", options.placeholder.c_str()); + } + PopStyleInput(); + ImGui::EndDisabled(); + ImGui::EndGroup(); + if (options.disabled && ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled) + && !Ship_IsCStringEmpty(options.disabledTooltip)) { + ImGui::SetTooltip("%s", WrappedText(options.disabledTooltip).c_str()); + } else if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled) + && !Ship_IsCStringEmpty(options.tooltip)) { + ImGui::SetTooltip("%s", WrappedText(options.tooltip).c_str()); + } + ImGui::PopID(); + return dirty; +} + +bool CVarInputInt(const char *label, const char *cvarName, const InputOptions &options) { + bool dirty = false; + int32_t defaultValue = std::stoi(options.defaultValue); + int32_t value = CVarGetInteger(cvarName, defaultValue); + if (InputInt(label, &value, options)) { + CVarSetInteger(cvarName, value); + Ship::Context::GetInstance()->GetWindow()->GetGui()->SaveConsoleVariablesNextFrame(); + ShipInit::Init(cvarName); + dirty = true; + } + return dirty; +} + +bool CVarColorPicker(const char *label, const char *cvarName, Color_RGBA8 defaultColor, bool hasAlpha, + uint8_t modifiers, UIWidgets::Colors themeColor) { + std::string valueCVar = std::string(cvarName) + ".Value"; + std::string rainbowCVar = std::string(cvarName) + ".Rainbow"; + std::string lockedCVar = std::string(cvarName) + ".Locked"; + Color_RGBA8 color = CVarGetColor(valueCVar.c_str(), defaultColor); + ImVec4 colorVec = ImVec4(color.r / 255.0f, color.g / 255.0f, color.b / 255.0f, color.a / 255.0f); + bool changed = false; + bool showReset = modifiers & ColorPickerResetButton; + bool showRandom = modifiers & ColorPickerRandomButton; + bool showRainbow = modifiers & ColorPickerRainbowCheck; + bool showLock = modifiers & ColorPickerLockCheck; + bool locked = CVarGetInteger(lockedCVar.c_str(), 0); + ImGuiColorEditFlags flags = ImGuiColorEditFlags_NoInputs; + ImGui::BeginDisabled(locked); + PushStyleCombobox(UIWidgets::Colors::DarkGray); + if (hasAlpha) { + changed = + ImGui::ColorEdit4(label, (float *) &colorVec, + flags | ImGuiColorEditFlags_AlphaBar | ImGuiColorEditFlags_AlphaPreview); + } else { + changed = ImGui::ColorEdit3(label, (float *) &colorVec, flags | ImGuiColorEditFlags_NoAlpha); + } + PopStyleCombobox(); + ImGui::AlignTextToFramePadding(); + if (showReset) { + ImGui::SameLine(); + std::string uniqueTag = "Reset##" + std::string(label); + if (UIWidgets::Button( + uniqueTag.c_str(), + UIWidgets::ButtonOptions({ { .tooltip = "Resets this color to its default value" } }) + .Color(themeColor) + .Size(UIWidgets::Sizes::Inline))) { + // TODO: Remove for next minor or major version, temporary fix for already migrated configs + // to 3 for 9.0.0 + CVarClear((std::string(cvarName) + ".R").c_str()); + CVarClear((std::string(cvarName) + ".G").c_str()); + CVarClear((std::string(cvarName) + ".B").c_str()); + CVarClear((std::string(cvarName) + ".A").c_str()); + CVarClear((std::string(cvarName) + ".Type").c_str()); + CVarClearBlock(valueCVar.c_str()); + Ship::Context::GetInstance()->GetWindow()->GetGui()->SaveConsoleVariablesNextFrame(); + } + } + if (showRandom) { + ImGui::SameLine(); + std::string uniqueTag = "Random##" + std::string(label); + if (UIWidgets::Button( + uniqueTag.c_str(), + UIWidgets::ButtonOptions({ { .tooltip = "Generates a random color value to use" } }) + .Color(themeColor) + .Size(UIWidgets::Sizes::Inline))) { + colorVec = GetRandomValue(); + color.r = fmin(fmax(colorVec.x * 255, 0), 255); + color.g = fmin(fmax(colorVec.y * 255, 0), 255); + color.b = fmin(fmax(colorVec.z * 255, 0), 255); + CVarSetColor(valueCVar.c_str(), color); + CVarSetInteger(rainbowCVar.c_str(), 0); // On click disable rainbow mode. + ShipInit::Init(rainbowCVar.c_str()); + Ship::Context::GetInstance()->GetWindow()->GetGui()->SaveConsoleVariablesNextFrame(); + } + } + if (showRainbow) { + ImGui::SameLine(); + std::string uniqueTag = "Rainbow##" + std::string(cvarName) + "Rainbow"; + + UIWidgets::CVarCheckbox( + uniqueTag.c_str(), rainbowCVar.c_str(), + UIWidgets::CheckboxOptions( + { { .tooltip = + "Cycles through colors on a timer\nOverwrites previously chosen color" } }) + .Color(themeColor)); + } + ImGui::EndDisabled(); + if (showLock) { + ImGui::SameLine(); + std::string uniqueTag = "Lock##" + std::string(cvarName) + "Locked"; + + UIWidgets::CVarCheckbox( + uniqueTag.c_str(), lockedCVar.c_str(), + UIWidgets::CheckboxOptions({ { .tooltip = "Prevents this color from being changed" } }) + .Color(themeColor)); + } + if (changed) { + color.r = (uint8_t) (colorVec.x * 255.0f); + color.g = (uint8_t) (colorVec.y * 255.0f); + color.b = (uint8_t) (colorVec.z * 255.0f); + color.a = (uint8_t) (colorVec.w * 255.0f); + CVarSetColor(valueCVar.c_str(), color); + Ship::Context::GetInstance()->GetWindow()->GetGui()->SaveConsoleVariablesNextFrame(); + ShipInit::Init(valueCVar.c_str()); + changed = true; + } + + return changed; +} + +bool RadioButton(const char *label, bool active, const RadioButtonsOptions &options) { + ImGuiWindow *window = ImGui::GetCurrentWindow(); + if (window->SkipItems) + return false; + + ImGuiContext &g = *GImGui; + const ImGuiStyle &style = g.Style; + const ImGuiID id = window->GetID(label); + const ImVec2 label_size = ImGui::CalcTextSize(label, NULL, true); + + const float square_sz = ImGui::GetFrameHeight(); + const ImVec2 pos = window->DC.CursorPos; + const ImRect check_bb(pos, pos + ImVec2(square_sz, square_sz)); + const ImRect total_bb( + pos, + pos + + ImVec2(square_sz + (label_size.x > 0.0f ? style.ItemInnerSpacing.x + label_size.x : 0.0f), + label_size.y + style.FramePadding.y * 2.0f)); + ImGui::ItemSize(total_bb, style.FramePadding.y); + if (!ImGui::ItemAdd(total_bb, id)) + return false; + + ImVec2 center = check_bb.GetCenter(); + center.x = IM_ROUND(center.x); + center.y = IM_ROUND(center.y); + const float radius = (square_sz - 1.0f) * 0.5f; + + bool hovered, held; + bool pressed = ImGui::ButtonBehavior(total_bb, id, &hovered, &held); + if (pressed) + ImGui::MarkItemEdited(id); + + ImGui::RenderNavCursor(total_bb, id); + const int num_segment = window->DrawList->_CalcCircleAutoSegmentCount(radius); + window->DrawList->AddCircleFilled(center, radius, + ImGui::GetColorU32((held && hovered) ? ImGuiCol_FrameBgActive + : hovered ? ImGuiCol_FrameBgHovered + : ImGuiCol_FrameBg), + num_segment); + if (active) { + const float pad = ImMax(1.0f, IM_TRUNC(square_sz / 6.0f)); + window->DrawList->AddCircleFilled(center, radius - pad, ImGui::GetColorU32(ImGuiCol_CheckMark)); + } + + if (style.FrameBorderSize > 0.0f) { + window->DrawList->AddCircle(center + ImVec2(1, 1), radius, + ImGui::GetColorU32(ImGuiCol_BorderShadow), num_segment, + style.FrameBorderSize); + window->DrawList->AddCircle(center, radius, ImGui::GetColorU32(ImGuiCol_Border), num_segment, + style.FrameBorderSize); + } + + ImVec2 label_pos = + ImVec2(check_bb.Max.x + style.ItemInnerSpacing.x, check_bb.Min.y + style.FramePadding.y); + if (g.LogEnabled) + ImGui::LogRenderedText(&label_pos, active ? "(x)" : "( )"); + if (label_size.x > 0.0f) + RenderText(label_pos, label, ImGui::FindRenderedTextEnd(label), true); + + IMGUI_TEST_ENGINE_ITEM_INFO(id, label, g.LastItemData.StatusFlags); + return pressed; +} + +bool CVarRadioButton(const char *text, const char *cvarName, int32_t id, + const RadioButtonsOptions &options) { + std::string make_invisible = "##" + std::string(text) + std::string(cvarName); + + bool ret = false; + int val = CVarGetInteger(cvarName, options.defaultIndex); + PushStyleCheckbox(options.color); + if (ImGui::RadioButton(make_invisible.c_str(), id == val)) { + CVarSetInteger(cvarName, id); + Ship::Context::GetInstance()->GetWindow()->GetGui()->SaveConsoleVariablesNextFrame(); + ret = true; + } + ImGui::SameLine(); + ImGui::Text("%s", text); + PopStyleCheckbox(); + if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled) + && !Ship_IsCStringEmpty(options.tooltip)) { + ImGui::SetTooltip("%s", WrappedText(options.tooltip).c_str()); + } + + return ret; +} + +void DrawFlagArray32(const std::string &name, uint32_t &flags, Colors color) { + 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; + PushStyleCheckbox(color); + ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(4.0f, 3.0f)); + std::string id = fmt::format("##{}{}", name, flagIndex); + if (ImGui::Checkbox(id.c_str(), &flag)) { + if (flag) { + flags |= bitMask; + } else { + flags &= ~bitMask; + } + } + ImGui::PopStyleVar(); + PopStyleCheckbox(); + ImGui::PopID(); + } + ImGui::PopID(); +} + +void DrawFlagArray16(const std::string &name, uint16_t &flags, Colors color) { + ImGui::PushID(name.c_str()); + for (int16_t flagIndex = 0; flagIndex < 16; flagIndex++) { + if ((flagIndex % 8) != 0) { + ImGui::SameLine(); + } + ImGui::PushID(flagIndex); + uint16_t bitMask = 1 << flagIndex; + bool flag = (flags & bitMask) != 0; + PushStyleCheckbox(color); + ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(4.0f, 3.0f)); + std::string id = fmt::format("##{}{}", name, flagIndex); + if (ImGui::Checkbox(id.c_str(), &flag)) { + if (flag) { + flags |= bitMask; + } else { + flags &= ~bitMask; + } + } + ImGui::PopStyleVar(); + PopStyleCheckbox(); + ImGui::PopID(); + } + ImGui::PopID(); +} + +void DrawFlagArray8(const std::string &name, uint8_t &flags, Colors color) { + ImGui::PushID(name.c_str()); + for (int8_t flagIndex = 0; flagIndex < 8; flagIndex++) { + if ((flagIndex % 8) != 0) { + ImGui::SameLine(); + } + ImGui::PushID(flagIndex); + uint8_t bitMask = 1 << flagIndex; + bool flag = (flags & bitMask) != 0; + PushStyleCheckbox(color); + ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(4.0f, 3.0f)); + std::string id = fmt::format("##{}{}", name, flagIndex); + if (ImGui::Checkbox(id.c_str(), &flag)) { + if (flag) { + flags |= bitMask; + } else { + flags &= ~bitMask; + } + } + ImGui::PopStyleVar(); + PopStyleCheckbox(); + ImGui::PopID(); + } + ImGui::PopID(); +} + +void DrawFlagArray8Mask(const std::string &name, uint8_t &flags, Colors color) { + ImGui::PushID(name.c_str()); + for (int8_t flagIndex = 0; flagIndex < 8; flagIndex++) { + if ((flagIndex % 8) != 0) { + ImGui::SameLine(); + } + ImGui::PushID(flagIndex); + uint8_t bitMask = 1 << flagIndex; + bool flag = (flags & bitMask) != 0; + PushStyleCheckbox(color); + ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(4.0f, 3.0f)); + std::string id = fmt::format("##{}{}", name, flagIndex); + if (ImGui::Checkbox(id.c_str(), &flag)) { + if (flag) { + flags |= bitMask; + } else { + flags &= ~bitMask; + } + } + ImGui::PopStyleVar(); + PopStyleCheckbox(); + ImGui::PopID(); + } + ImGui::PopID(); +} } // namespace UIWidgets + +ImVec4 GetRandomValue() { +#if !defined(__SWITCH__) && !defined(__WIIU__) + std::random_device rd; + std::mt19937 rng(rd()); +#else + size_t seed = std::hash{}(std::to_string(rand())); + std::mt19937_64 rng(seed); +#endif + std::uniform_int_distribution dist(0, 255 - 1); + + ImVec4 NewColor; + NewColor.x = (float) (dist(rng)) / 255.0f; + NewColor.y = (float) (dist(rng)) / 255.0f; + NewColor.z = (float) (dist(rng)) / 255.0f; + return NewColor; +} + +Color_RGBA8 RGBA8FromVec(ImVec4 vec) { + Color_RGBA8 color = { vec.x * 255, vec.y * 255, vec.z * 255, vec.w * 255 }; + return color; +} + +ImVec4 VecFromRGBA8(Color_RGBA8 color) { + ImVec4 vec = { color.r / 255.0f, color.g / 255.0f, color.b / 255.0f, color.a / 255.0f }; + return vec; +} diff --git a/src/port/ui/UIWidgets.h b/src/port/ui/UIWidgets.h deleted file mode 100644 index ef860db2..00000000 --- a/src/port/ui/UIWidgets.h +++ /dev/null @@ -1,210 +0,0 @@ -#pragma once - -#include -#include -#include -#include -#define IMGUI_DEFINE_MATH_OPERATORS -#include -#include - -namespace UIWidgets { - - struct TextFilters { - static int FilterNumbers(ImGuiInputTextCallbackData* data) { - if (data->EventChar < 256 && strchr("1234567890", (char)data->EventChar)) { - return 0; - } - return 1; - } - - static int FilterAlphaNum(ImGuiInputTextCallbackData* data) { - const char* alphanum = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWYZ0123456789"; - if (data->EventChar < 256 && strchr(alphanum, (char)data->EventChar)) { - return 0; - } - return 1; - } - - }; - - // MARK: - Enums - - enum class CheckboxGraphics { - Cross, - Checkmark, - None - }; - constexpr float maxSliderWidth = 260.0f; -#ifdef __SWITCH__ - constexpr float sliderButtonWidth = 42.0f; -#elif defined(__WIIU__) - constexpr float sliderButtonWidth = 60.0f; -#else - constexpr float sliderButtonWidth = 30.0f; -#endif - - char* WrappedText(const char* text, unsigned int charactersPerLine = 60); - char* WrappedText(const std::string& text, unsigned int charactersPerLine); - - void SetLastItemHoverText(const std::string& text); - void SetLastItemHoverText(const char* text); - - void InsertHelpHoverText(const std::string& text); - void InsertHelpHoverText(const char* text); - - void Tooltip(const char* text); - void Spacer(float height); - void PaddedSeparator(bool padTop = true, bool padBottom = true, float extraVerticalTopPadding = 0.0f, float extraVerticalBottomPadding = 0.0f); - - void RenderCross(ImDrawList* draw_list, ImVec2 pos, ImU32 col, float sz); - bool CustomCheckbox(const char* label, bool* v, bool disabled, CheckboxGraphics disabledGraphic); - - void ReEnableComponent(const char* disabledTooltipText); - void DisableComponent(const float alpha); - - bool EnhancementCheckbox(const char* text, const char* cvarName, bool disabled = false, const char* disabledTooltipText = "", CheckboxGraphics disabledGraphic = CheckboxGraphics::Cross, bool defaultValue = false); - bool PaddedEnhancementCheckbox(const char* text, const char* cvarName, bool padTop = true, bool padBottom = true, bool disabled = false, const char* disabledTooltipText = "", CheckboxGraphics disabledGraphic = CheckboxGraphics::Cross, bool defaultValue = false); - - bool EnhancementCombobox(const char* cvarName, std::span comboArray, uint8_t defaultIndex, bool disabled = false, const char* disabledTooltipText = "", uint8_t disabledValue = -1); - bool LabeledRightAlignedEnhancementCombobox(const char* label, const char* cvarName, std::span comboArray, uint8_t defaultIndex, bool disabled = false, const char* disabledTooltipText = "", uint8_t disabledValue = -1); - - void PaddedText(const char* text, bool padTop = true, bool padBottom = true); - - bool EnhancementSliderInt(const char* text, const char* id, const char* cvarName, int min, int max, const char* format, int defaultValue = 0, bool PlusMinusButton = true, bool disabled = false, const char* disabledTooltipText = ""); - bool PaddedEnhancementSliderInt(const char* text, const char* id, const char* cvarName, int min, int max, const char* format, int defaultValue = 0, bool PlusMinusButton = true, bool padTop = true, bool padBottom = true, bool disabled = false, const char* disabledTooltipText = ""); - bool EnhancementSliderFloat(const char* text, const char* id, const char* cvarName, float min, float max, const char* format, float defaultValue, bool isPercentage, bool PlusMinusButton = true, bool disabled = false, const char* disabledTooltipText = ""); - bool PaddedEnhancementSliderFloat(const char* text, const char* id, const char* cvarName, float min, float max, const char* format, float defaultValue, bool isPercentage, bool PlusMinusButton = true, bool padTop = true, bool padBottom = true, bool disabled = false, const char* disabledTooltipText = ""); - - bool EnhancementRadioButton(const char* text, const char* cvarName, int id); - - bool DrawResetColorButton(const char* cvarName, ImVec4* colors, ImVec4 defaultcolors, bool has_alpha); - bool DrawRandomizeColorButton(const char* cvarName, ImVec4* colors); - void DrawLockColorCheckbox(const char* cvarName); - void RainbowColor(const char* cvarName, ImVec4* colors); - - void LoadPickersColors(ImVec4& ColorArray, const char* cvarname, const ImVec4& default_colors, bool has_alpha); - bool EnhancementColor(const char* text, const char* cvarName, ImVec4 ColorRGBA, ImVec4 default_colors, bool allow_rainbow = true, bool has_alpha = false, bool TitleSameLine = false); - - void DrawFlagArray32(const std::string& name, uint32_t& flags); - void DrawFlagArray16(const std::string& name, uint16_t& flags); - void DrawFlagArray8(const std::string& name, uint8_t& flags); - - // V2 - namespace Colors { - const ImVec4 White = ImVec4(1.0f, 1.0f, 1.0f, 1.0f); - const ImVec4 Gray = ImVec4(0.4f, 0.4f, 0.4f, 1.0f); - const ImVec4 DarkGray = ImVec4(0.1f, 0.1f, 0.1f, 1.0f); - const ImVec4 Indigo = ImVec4(0.24f, 0.31f, 0.71f, 1.0f); - const ImVec4 Red = ImVec4(0.5f, 0.0f, 0.0f, 1.0f); - const ImVec4 DarkRed = ImVec4(0.3f, 0.0f, 0.0f, 1.0f); - const ImVec4 LightGreen = ImVec4(0.0f, 0.7f, 0.0f, 1.0f); - const ImVec4 Green = ImVec4(0.0f, 0.5f, 0.0f, 1.0f); - const ImVec4 DarkGreen = ImVec4(0.0f, 0.3f, 0.0f, 1.0f); - const ImVec4 Yellow = ImVec4(1.0f, 0.627f, 0.0f, 1.0f); - }; - - namespace Sizes { - const ImVec2 Inline = ImVec2(0.0f, 0.0f); - const ImVec2 Fill = ImVec2(-1.0f, 0.0f); - } - - enum LabelPosition { - Near, - Far, - Above, - None, - Within, - }; - - enum ComponentAlignment { - Left, - Right, - }; - - void PushStyleMenu(const ImVec4& color = Colors::Indigo); - void PopStyleMenu(); - bool BeginMenu(const char* label, const ImVec4& color = Colors::Indigo); - - void PushStyleMenuItem(const ImVec4& color = Colors::Indigo); - void PopStyleMenuItem(); - bool MenuItem(const char* label, const char* shortcut = NULL, const ImVec4& color = Colors::Indigo); - - struct ButtonOptions { - const ImVec4 color = Colors::Gray; - const ImVec2 size = Sizes::Fill; - const char* tooltip = ""; - bool disabled = false; - const char* disabledTooltip = ""; - }; - - void PushStyleButton(const ImVec4& color = Colors::Gray); - void PopStyleButton(); - bool Button(const char* label, const ButtonOptions& options = {}); - bool WindowButton(const char* label, const char* cvarName, std::shared_ptr windowPtr, const ButtonOptions& options = {}); - - struct CheckboxOptions { - const ImVec4 color = Colors::Indigo; - const char* tooltip = ""; - bool disabled = false; - const char* disabledTooltip = ""; - bool defaultValue = false; // Only applicable to CVarCheckbox - ComponentAlignment alignment = ComponentAlignment::Left; - LabelPosition labelPosition = LabelPosition::Near; - }; - - void PushStyleCheckbox(const ImVec4& color = Colors::Indigo); - void PopStyleCheckbox(); - bool Checkbox(const char* label, bool* v, const CheckboxOptions& options = {}); - bool CVarCheckbox(const char* label, const char* cvarName, const CheckboxOptions& options = {}); - - struct ComboboxOptions { - const ImVec4 color = Colors::Indigo; - const char* tooltip = ""; - bool disabled = false; - const char* disabledTooltip = ""; - uint32_t defaultIndex = 0; // Only applicable to CVarCombobox - ComponentAlignment alignment = ComponentAlignment::Left; - LabelPosition labelPosition = LabelPosition::Above; - ImGuiComboFlags flags = 0; - }; - - void PushStyleCombobox(const ImVec4& color = Colors::Indigo); - void PopStyleCombobox(); - bool Combobox(const char* label, uint8_t* value, std::span comboArray, const ComboboxOptions& options = {}); - bool CVarCombobox(const char* label, const char* cvarName, std::span comboArray, const ComboboxOptions& options = {}); - - struct IntSliderOptions { - const ImVec4 color = Colors::Gray; - const char* tooltip = ""; - bool disabled = false; - const char* disabledTooltip = ""; - bool showButtons = true; - ImGuiSliderFlags flags = 0; - const char* format = "%d"; - const uint32_t step = 1; - ComponentAlignment alignment = ComponentAlignment::Left; - LabelPosition labelPosition = LabelPosition::Above; - }; - - struct FloatSliderOptions { - const ImVec4 color = Colors::Gray; - const char* tooltip = ""; - bool disabled = false; - const char* disabledTooltip = ""; - bool showButtons = true; - ImGuiSliderFlags flags = 0; - const char* format = "%f"; - const float step = 0.01f; - bool isPercentage = false; // Multiplies visual value by 100 - ComponentAlignment alignment = ComponentAlignment::Left; - LabelPosition labelPosition = LabelPosition::Above; - }; - - void PushStyleSlider(const ImVec4& color = Colors::Indigo); - void PopStyleSlider(); - bool SliderInt(const char* label, int32_t* value, int32_t min, int32_t max, const IntSliderOptions& options = {}); - bool CVarSliderInt(const char* label, const char* cvarName, int32_t min, int32_t max, const int32_t defaultValue, const IntSliderOptions& options = {}); - bool SliderFloat(const char* label, float* value, float min, float max, const FloatSliderOptions& options = {}); - bool CVarSliderFloat(const char* label, const char* cvarName, float min, float max, const float defaultValue, const FloatSliderOptions& options = {}); -} \ No newline at end of file diff --git a/src/port/ui/UIWidgets.hpp b/src/port/ui/UIWidgets.hpp new file mode 100644 index 00000000..da57ccb7 --- /dev/null +++ b/src/port/ui/UIWidgets.hpp @@ -0,0 +1,1055 @@ +#ifndef UIWidgets2_hpp +#define UIWidgets2_hpp + +#include +#include +#include +#include +#define IMGUI_DEFINE_MATH_OPERATORS +#include +#include +#include +#include "src/port/ShipUtils.h" +#include "src/port/ShipInit.hpp" + +namespace UIWidgets { + +using SectionFunc = void (*)(); + +struct TextFilters { + static int FilterNumbers(ImGuiInputTextCallbackData* data) { + if (data->EventChar < 256 && strchr("1234567890", (char)data->EventChar)) { + return 0; + } + return 1; + } + + static int FilterAlphaNum(ImGuiInputTextCallbackData* data) { + const char* alphanum = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWYZ0123456789"; + if (data->EventChar < 256 && strchr(alphanum, (char)data->EventChar)) { + return 0; + } + return 1; + } +}; + +std::string WrappedText(const char* text, unsigned int charactersPerLine = 80); +std::string WrappedText(const std::string& text, unsigned int charactersPerLine = 80); +void PaddedSeparator(bool padTop = true, bool padBottom = true, float extraVerticalTopPadding = 0.0f, + float extraVerticalBottomPadding = 0.0f); +void Tooltip(const char* text); + +typedef enum ColorPickerModifiers { + ColorPickerResetButton = 1, + ColorPickerRandomButton = 2, + ColorPickerRainbowCheck = 4, + ColorPickerLockCheck = 8, +} ColorPickerModifiers; + +// mostly in order for colors usable by the menu without custom text color +enum Colors { + Red, + DarkRed, + Orange, + Green, + DarkGreen, + LightBlue, + Blue, + DarkBlue, + Indigo, + Violet, + Purple, + Brown, + Gray, + DarkGray, + // not suitable for menu theme use + Pink, + Yellow, + Cyan, + Black, + LightGray, + White, + NoColor +}; + +enum InputTypes { String, Scalar }; + +const std::unordered_map ColorValues = { + { Colors::Pink, ImVec4(0.87f, 0.3f, 0.87f, 1.0f) }, { Colors::Red, ImVec4(0.55f, 0.0f, 0.0f, 1.0f) }, + { Colors::DarkRed, ImVec4(0.3f, 0.0f, 0.0f, 1.0f) }, { Colors::Orange, ImVec4(0.85f, 0.55f, 0.0f, 1.0f) }, + { Colors::Yellow, ImVec4(0.95f, 0.95f, 0.0f, 1.0f) }, { Colors::Green, ImVec4(0.0f, 0.55f, 0.0f, 1.0f) }, + { Colors::DarkGreen, ImVec4(0.0f, 0.3f, 0.0f, 1.0f) }, { Colors::Cyan, ImVec4(0.0f, 0.9f, 0.9f, 1.0f) }, + { Colors::LightBlue, ImVec4(0.0f, 0.24f, 0.8f, 1.0f) }, { Colors::Blue, ImVec4(0.08f, 0.03f, 0.65f, 1.0f) }, + { Colors::DarkBlue, ImVec4(0.03f, 0.0f, 0.5f, 1.0f) }, { Colors::Indigo, ImVec4(0.35f, 0.0f, 0.87f, 1.0f) }, + { Colors::Violet, ImVec4(0.5f, 0.0f, 0.9f, 1.0f) }, { Colors::Purple, ImVec4(0.31f, 0.0f, 0.67f, 1.0f) }, + { Colors::Brown, ImVec4(0.37f, 0.18f, 0.0f, 1.0f) }, { Colors::LightGray, ImVec4(0.75f, 0.75f, 0.75f, 1.0f) }, + { Colors::Gray, ImVec4(0.45f, 0.45f, 0.45f, 1.0f) }, { Colors::DarkGray, ImVec4(0.15f, 0.15f, 0.15f, 1.0f) }, + { Colors::Black, ImVec4(0.0f, 0.0f, 0.0f, 1.0f) }, { Colors::White, ImVec4(1.0f, 1.0f, 1.0f, 1.0f) }, + { Colors::NoColor, ImVec4(0.0f, 0.0f, 0.0f, 0.0f) }, +}; + +namespace Sizes { +const ImVec2 Inline = ImVec2(0.0f, 0.0f); +const ImVec2 Fill = ImVec2(-1.0f, 0.0f); +} // namespace Sizes + +enum LabelPositions { + Near, + Far, + Above, + None, + Within, +}; + +enum ComponentAlignments { + Left, + Right, +}; + +struct WidgetOptions { + const char* tooltip = ""; + bool disabled = false; + const char* disabledTooltip = ""; + + WidgetOptions& Tooltip(const char* tooltip_) { + tooltip = tooltip_; + return *this; + } + + WidgetOptions& Disabled(bool disabled_) { + disabled = disabled_; + return *this; + } + + WidgetOptions& DisabledTooltip(const char* disabledTooltip_) { + disabledTooltip = disabledTooltip_; + return *this; + } +}; + +struct TextOptions : WidgetOptions { + Colors color = Colors::NoColor; + + TextOptions& Color(Colors color_) { + color = color_; + return *this; + } +}; + +struct ButtonOptions : WidgetOptions { + ImVec2 size = Sizes::Fill; + ImVec2 padding = ImVec2(10.0f, 8.0f); + Colors color = Colors::Gray; + + ButtonOptions& Size(ImVec2 size_) { + size = size_; + return *this; + } + + ButtonOptions& Padding(ImVec2 padding_) { + padding = padding_; + return *this; + } + + ButtonOptions& Tooltip(const char* tooltip_) { + WidgetOptions::tooltip = tooltip_; + return *this; + } + + ButtonOptions& Color(Colors color_) { + color = color_; + return *this; + } +}; + +struct ColorPickerOptions : WidgetOptions { + ImVec2 size = Sizes::Fill; + ImVec2 padding = ImVec2(10.0f, 8.0f); + Colors color = Colors::Gray; + Color_RGBA8 defaultValue = { 255, 255, 255, 255 }; + bool useAlpha, showReset, showRandom, showRainbow, showLock; + + ColorPickerOptions& Size(ImVec2 size_) { + size = size_; + return *this; + } + + ColorPickerOptions& Padding(ImVec2 padding_) { + padding = padding_; + return *this; + } + + ColorPickerOptions& Tooltip(const char* tooltip_) { + WidgetOptions::tooltip = tooltip_; + return *this; + } + + ColorPickerOptions& ShowReset(bool showReset_ = true) { + showReset = showReset_; + return *this; + } + + ColorPickerOptions& ShowRandom(bool showRandom_ = true) { + showRandom = showRandom_; + return *this; + } + + ColorPickerOptions& ShowRainbow(bool showRainbow_ = true) { + showRainbow = showRainbow_; + return *this; + } + + ColorPickerOptions& ShowLock(bool showLock_ = true) { + showLock = showLock_; + return *this; + } + + ColorPickerOptions& UseAlpha(bool useAlpha_ = true) { + useAlpha = useAlpha_; + return *this; + } + + ColorPickerOptions& Color(Colors color_) { + color = color_; + return *this; + } + + ColorPickerOptions& DefaultValue(Color_RGBA8 defaultValue_) { + defaultValue = defaultValue_; + return *this; + } +}; + +struct WindowButtonOptions : WidgetOptions { + ImVec2 size = Sizes::Inline; + ImVec2 padding = ImVec2(10.0f, 8.0f); + Colors color = Colors::Gray; + bool showButton = true; + bool embedWindow = true; + + WindowButtonOptions& Size(ImVec2 size_) { + size = size_; + return *this; + } + + WindowButtonOptions& Padding(ImVec2 padding_) { + padding = padding_; + return *this; + } + + WindowButtonOptions& Tooltip(const char* tooltip_) { + WidgetOptions::tooltip = tooltip_; + return *this; + } + + WindowButtonOptions& Color(Colors color_) { + color = color_; + return *this; + } + + WindowButtonOptions& ShowButton(bool showButton_) { + showButton = showButton_; + return *this; + } + + WindowButtonOptions& EmbedWindow(bool embedWindow_) { + embedWindow = embedWindow_; + return *this; + } +}; + +struct CheckboxOptions : WidgetOptions { + bool defaultValue = false; // Only applicable to CVarCheckbox + ComponentAlignments alignment = ComponentAlignments::Left; + LabelPositions labelPosition = LabelPositions::Near; + ImVec2 padding = ImVec2(10.0f, 8.0f); + Colors color = Colors::LightBlue; + + CheckboxOptions& DefaultValue(bool defaultValue_) { + defaultValue = defaultValue_; + return *this; + } + + CheckboxOptions& ComponentAlignment(ComponentAlignments alignment_) { + alignment = alignment_; + return *this; + } + + CheckboxOptions& LabelPosition(LabelPositions labelPosition_) { + labelPosition = labelPosition_; + return *this; + } + + CheckboxOptions& Tooltip(const char* tooltip_) { + WidgetOptions::tooltip = tooltip_; + return *this; + } + + CheckboxOptions& Color(Colors color_) { + color = color_; + return *this; + } + + CheckboxOptions& DisabledTooltip(const char* disabledTooltip_) { + WidgetOptions::disabledTooltip = disabledTooltip_; + return *this; + } + + CheckboxOptions& Padding(ImVec2 padding_) { + padding = padding_; + return *this; + } +}; + +struct ComboboxOptions : WidgetOptions { + std::unordered_map comboMap = {}; + uint32_t defaultIndex = 0; // Only applicable to CVarCombobox + ComponentAlignments alignment = ComponentAlignments::Left; + LabelPositions labelPosition = LabelPositions::Above; + ImGuiComboFlags flags = 0; + Colors color = Colors::LightBlue; + + ComboboxOptions& ComboMap(std::unordered_map comboMap_) { + comboMap = comboMap_; + return *this; + } + + ComboboxOptions& DefaultIndex(uint32_t defaultIndex_) { + defaultIndex = defaultIndex_; + return *this; + } + + ComboboxOptions& ComponentAlignment(ComponentAlignments alignment_) { + alignment = alignment_; + return *this; + } + + ComboboxOptions& LabelPosition(LabelPositions labelPosition_) { + labelPosition = labelPosition_; + return *this; + } + + ComboboxOptions& Tooltip(const char* tooltip_) { + WidgetOptions::tooltip = tooltip_; + return *this; + } + + ComboboxOptions& Color(Colors color_) { + color = color_; + return *this; + } +}; + +struct IntSliderOptions : WidgetOptions { + bool showButtons = true; + const char* format = "%d"; + int32_t step = 1; + int32_t min = 1; + int32_t max = 10; + int32_t defaultValue = 1; + bool clamp = true; + ComponentAlignments alignment = ComponentAlignments::Left; + LabelPositions labelPosition = LabelPositions::Above; + Colors color = Colors::Gray; + ImGuiSliderFlags flags = 0; + ImVec2 size = { 0, 0 }; + + IntSliderOptions& ShowButtons(bool showButtons_) { + showButtons = showButtons_; + return *this; + } + + IntSliderOptions& Format(const char* format_) { + format = format_; + return *this; + } + + IntSliderOptions& Step(int32_t step_) { + step = step_; + return *this; + } + + IntSliderOptions& Min(int32_t min_) { + min = min_; + return *this; + } + + IntSliderOptions& Max(int32_t max_) { + max = max_; + return *this; + } + + IntSliderOptions& DefaultValue(int32_t defaultValue_) { + defaultValue = defaultValue_; + return *this; + } + + IntSliderOptions& ComponentAlignment(ComponentAlignments alignment_) { + alignment = alignment_; + return *this; + } + + IntSliderOptions& LabelPosition(LabelPositions labelPosition_) { + labelPosition = labelPosition_; + return *this; + } + + IntSliderOptions& Tooltip(const char* tooltip_) { + WidgetOptions::tooltip = tooltip_; + return *this; + } + + IntSliderOptions& Color(Colors color_) { + color = color_; + return *this; + } + + IntSliderOptions& Size(ImVec2 size_) { + size = size_; + return *this; + } + + IntSliderOptions& Clamp(bool clamp_) { + clamp = clamp_; + return *this; + } +}; + +struct FloatSliderOptions : WidgetOptions { + bool showButtons = true; + const char* format = "%f"; + float step = 0.01f; + float min = 0.01f; + float max = 10.0f; + float defaultValue = 1.0f; + bool clamp = true; + bool isPercentage = false; // Multiplies visual value by 100 + ComponentAlignments alignment = ComponentAlignments::Left; + LabelPositions labelPosition = LabelPositions::Above; + Colors color = Colors::Gray; + ImGuiSliderFlags flags = 0; + ImVec2 size = { 0, 0 }; + + FloatSliderOptions& ShowButtons(bool showButtons_) { + showButtons = showButtons_; + return *this; + } + + FloatSliderOptions& Format(const char* format_) { + format = format_; + return *this; + } + + FloatSliderOptions& Step(float step_) { + step = step_; + return *this; + } + + FloatSliderOptions& Min(float min_) { + min = min_; + return *this; + } + + FloatSliderOptions& Max(float max_) { + max = max_; + return *this; + } + + FloatSliderOptions& DefaultValue(float defaultValue_) { + defaultValue = defaultValue_; + return *this; + } + + FloatSliderOptions& ComponentAlignment(ComponentAlignments alignment_) { + alignment = alignment_; + return *this; + } + + FloatSliderOptions& LabelPosition(LabelPositions labelPosition_) { + labelPosition = labelPosition_; + return *this; + } + + FloatSliderOptions& IsPercentage(bool isPercentage_ = true) { + isPercentage = isPercentage_; + format = "%.0f%%"; + min = 0.0f; + max = 1.0f; + return *this; + } + + FloatSliderOptions& Tooltip(const char* tooltip_) { + WidgetOptions::tooltip = tooltip_; + return *this; + } + + FloatSliderOptions& Color(Colors color_) { + color = color_; + return *this; + } + + FloatSliderOptions& Size(ImVec2 size_) { + size = size_; + return *this; + } + + FloatSliderOptions& Clamp(bool clamp_) { + clamp = clamp_; + return *this; + } +}; + +struct RadioButtonsOptions : WidgetOptions { + std::unordered_map buttonMap; + int32_t defaultIndex = 0; + Colors color = Colors::LightBlue; + + RadioButtonsOptions& ButtonMap(std::unordered_map buttonMap_) { + buttonMap = buttonMap_; + return *this; + } + + RadioButtonsOptions& Tooltip(const char* tooltip_) { + WidgetOptions::tooltip = tooltip_; + return *this; + } + + RadioButtonsOptions& Color(Colors color_) { + color = color_; + return *this; + } + + RadioButtonsOptions& DefaultIndex(int32_t defaultIndex_) { + defaultIndex = defaultIndex_; + return *this; + } +}; + +struct InputOptions : WidgetOptions { + ComponentAlignments alignment = ComponentAlignments::Left; + LabelPositions labelPosition = LabelPositions::Above; + Colors color = Colors::Gray; + ImVec2 size = { 0, 0 }; + std::string placeholder = ""; + InputTypes type = InputTypes::String; + std::string defaultValue = ""; + bool secret = false; + ImGuiInputFlags addedFlags = 0; + bool hasError = false; + const char* errorText = ""; + + InputOptions& Tooltip(const char* tooltip_) { + WidgetOptions::tooltip = tooltip_; + return *this; + } + + InputOptions& Color(Colors color_) { + color = color_; + return *this; + } + + InputOptions& Size(ImVec2 size_) { + size = size_; + return *this; + } + + InputOptions& LabelPosition(LabelPositions labelPosition_) { + labelPosition = labelPosition_; + return *this; + } + + InputOptions& PlaceholderText(std::string&& placeholder_) { + placeholder = std::move(placeholder_); + return *this; + } + + InputOptions& PlaceholderText(std::string& placeholder_) { + placeholder = placeholder_; + return *this; + } + + InputOptions& InputType(InputTypes type_) { + type = type_; + return *this; + } + + InputOptions& ComponentAlignment(ComponentAlignments alignment_) { + alignment = alignment_; + return *this; + } + + InputOptions& DefaultValue(std::string defaultValue_) { + defaultValue = defaultValue_; + return *this; + } + + InputOptions& IsSecret(bool secret_ = false) { + secret = secret_; + return *this; + } + + InputOptions& HasError(bool error_ = false) { + hasError = error_; + return *this; + } + + InputOptions& ErrorText(const char* errorText_) { + errorText = errorText_; + return *this; + } +}; + +void PushStyleMenu(const ImVec4& color); +void PushStyleMenu(Colors color = Colors::LightBlue); +void PopStyleMenu(); +bool BeginMenu(const char* label, Colors color = Colors::LightBlue); + +void PushStyleMenuItem(const ImVec4& color); +void PushStyleMenuItem(Colors color = Colors::LightBlue); +void PopStyleMenuItem(); +bool MenuItem(const char* label, const char* shortcut = NULL, Colors color = Colors::LightBlue); + +void PushStyleButton(const ImVec4& color, ImVec2 padding = ImVec2(10.0f, 8.0f)); +void PushStyleButton(Colors color = Colors::Gray, ImVec2 padding = ImVec2(10.0f, 8.0f)); +void PopStyleButton(); +bool Button(const char* label, const ButtonOptions& options = {}); +bool WindowButton(const char* label, const char* cvarName, std::shared_ptr windowPtr, + const WindowButtonOptions& options = {}); + +void PushStyleCheckbox(const ImVec4& color, ImVec2 padding = ImVec2(10.0f, 6.0f)); +void PushStyleCheckbox(Colors color = Colors::LightBlue, ImVec2 padding = ImVec2(10.0f, 6.0f)); +void PopStyleCheckbox(); +void RenderText(ImVec2 pos, const char* text, const char* text_end, bool hide_text_after_hash); +bool Checkbox(const char* label, bool* v, const CheckboxOptions& options = {}); +bool CVarCheckbox(const char* label, const char* cvarName, const CheckboxOptions& options = {}); + +void PushStyleCombobox(const ImVec4& color); +void PushStyleCombobox(Colors color = Colors::LightBlue); +void PopStyleCombobox(); + +void PushStyleTabs(const ImVec4& color); +void PushStyleTabs(Colors color = Colors::LightBlue); +void PopStyleTabs(); + +void PushStyleInput(const ImVec4& color); +void PushStyleInput(Colors color = Colors::LightBlue); +void PopStyleInput(); + +void PushStyleHeader(const ImVec4& color); +void PushStyleHeader(Colors color = Colors::LightBlue); +void PopStyleHeader(); + +void Spacer(float height = 0.0f); +void Separator(bool padTop = true, bool padBottom = true, float extraVerticalTopPadding = 0.0f, + float extraVerticalBottomPadding = 0.0f); + +float CalcComboWidth(const char* preview_value, ImGuiComboFlags flags); + +template +bool Combobox(const char* label, T* value, const std::unordered_map& comboMap, + const ComboboxOptions& options = {}) { + bool dirty = false; + float startX = ImGui::GetCursorPosX(); + std::string invisibleLabelStr = "##" + std::string(label); + const char* invisibleLabel = invisibleLabelStr.c_str(); + ImGui::PushID(label); + ImGui::BeginGroup(); + ImGui::BeginDisabled(options.disabled); + PushStyleCombobox(options.color); + + const char* longest; + size_t length = 0; + for (auto& [index, string] : comboMap) { + size_t len = strlen(string); + if (len > length) { + longest = string; + length = len; + } + } + float comboWidth = CalcComboWidth(longest, options.flags); + + ImGui::AlignTextToFramePadding(); + if (options.labelPosition != LabelPositions::None) { + if (options.alignment == ComponentAlignments::Right) { + ImGui::Text("%s", label); + if (options.labelPosition == LabelPositions::Above) { + ImGui::NewLine(); + ImGui::SameLine(ImGui::GetContentRegionAvail().x - comboWidth); + } else if (options.labelPosition == LabelPositions::Near) { + ImGui::SameLine(); + } else if (options.labelPosition == LabelPositions::Far) { + ImGui::SameLine(ImGui::GetContentRegionAvail().x - comboWidth); + } + } else if (options.alignment == ComponentAlignments::Left) { + if (options.labelPosition == LabelPositions::Above) { + ImGui::Text("%s", label); + } + } + } + + ImGui::SetNextItemWidth(comboWidth); + if (ImGui::BeginCombo(invisibleLabel, comboMap.at(*value), options.flags)) { + ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(10.0f, 10.0f)); + for (const auto& pair : comboMap) { + if (strlen(pair.second) > 1) { + if (ImGui::Selectable(pair.second, pair.first == *value)) { + *value = pair.first; + dirty = true; + } + } + } + ImGui::PopStyleVar(); + ImGui::EndCombo(); + } + + if (options.labelPosition != LabelPositions::None) { + if (options.alignment == ComponentAlignments::Left) { + if (options.labelPosition == LabelPositions::Near) { + ImGui::SameLine(); + ImGui::Text("%s", label); + } else if (options.labelPosition == LabelPositions::Far) { + float width = ImGui::CalcTextSize(comboMap.at(*value)).x + ImGui::GetStyle().FramePadding.x * 2; + ImGui::SameLine(ImGui::GetContentRegionAvail().x - width); + ImGui::Text("%s", label); + } + } + } + PopStyleCombobox(); + ImGui::EndDisabled(); + ImGui::EndGroup(); + if (options.disabled && ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled) && + !Ship_IsCStringEmpty(options.disabledTooltip)) { + ImGui::SetTooltip("%s", WrappedText(options.disabledTooltip).c_str()); + } else if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled) && !Ship_IsCStringEmpty(options.tooltip)) { + ImGui::SetTooltip("%s", WrappedText(options.tooltip).c_str()); + } + ImGui::PopID(); + return dirty; +} + +template +bool Combobox(const char* label, T* value, const std::vector& comboVector, + const ComboboxOptions& options = {}) { + bool dirty = false; + size_t currentValueIndex = static_cast(*value); + std::string invisibleLabelStr = "##" + std::string(label); + const char* invisibleLabel = invisibleLabelStr.c_str(); + ImGui::PushID(label); + ImGui::BeginGroup(); + ImGui::BeginDisabled(options.disabled); + PushStyleCombobox(options.color); + + const char* longest; + size_t length = 0; + for (auto& string : comboVector) { + size_t len = strlen(string); + if (len > length) { + longest = string; + length = len; + } + } + float comboWidth = CalcComboWidth(longest, options.flags); + + ImGui::AlignTextToFramePadding(); + if (options.labelPosition != LabelPositions::None) { + if (options.alignment == ComponentAlignments::Right) { + ImGui::Text("%s", label); + if (options.labelPosition == LabelPositions::Above) { + ImGui::NewLine(); + ImGui::SameLine(ImGui::GetContentRegionAvail().x - comboWidth); + } else if (options.labelPosition == LabelPositions::Near) { + ImGui::SameLine(); + } else if (options.labelPosition == LabelPositions::Far) { + ImGui::SameLine(ImGui::GetContentRegionAvail().x - comboWidth); + } + } else if (options.alignment == ComponentAlignments::Left) { + if (options.labelPosition == LabelPositions::Above) { + ImGui::Text("%s", label); + } + } + } + + ImGui::SetNextItemWidth(comboWidth); + if (ImGui::BeginCombo(invisibleLabel, comboVector.at(currentValueIndex), options.flags)) { + ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(10.0f, 10.0f)); + for (size_t i = 0; i < comboVector.size(); ++i) { + auto newValue = static_cast(i); + if (strlen(comboVector.at(i)) > 1) { + if (ImGui::Selectable(comboVector.at(i), newValue == *value)) { + *value = newValue; + dirty = true; + } + } + } + ImGui::PopStyleVar(); + ImGui::EndCombo(); + } + + if (options.labelPosition != LabelPositions::None) { + if (options.alignment == ComponentAlignments::Left) { + if (options.labelPosition == LabelPositions::Near) { + ImGui::SameLine(); + ImGui::Text("%s", label); + } else if (options.labelPosition == LabelPositions::Far) { + float width = ImGui::CalcTextSize(comboVector.at(*value)).x + ImGui::GetStyle().FramePadding.x * 2; + ImGui::SameLine(ImGui::GetContentRegionAvail().x - width); + ImGui::Text("%s", label); + } + } + } + + PopStyleCombobox(); + ImGui::EndDisabled(); + ImGui::EndGroup(); + if (options.disabled && ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled) && + !Ship_IsCStringEmpty(options.disabledTooltip)) { + ImGui::SetTooltip("%s", WrappedText(options.disabledTooltip).c_str()); + } else if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled) && !Ship_IsCStringEmpty(options.tooltip)) { + ImGui::SetTooltip("%s", WrappedText(options.tooltip).c_str()); + } + ImGui::PopID(); + return dirty; +} + +template +bool Combobox(const char* label, T* value, const std::vector& comboVector, + const ComboboxOptions& options = {}) { + bool dirty = false; + size_t currentValueIndex = static_cast(*value); + std::string invisibleLabelStr = "##" + std::string(label); + const char* invisibleLabel = invisibleLabelStr.c_str(); + ImGui::PushID(label); + ImGui::BeginGroup(); + ImGui::BeginDisabled(options.disabled); + PushStyleCombobox(options.color); + + const char* longest; + size_t length = 0; + for (auto& string : comboVector) { + size_t len = string.length(); + if (len > length) { + longest = string.c_str(); + length = len; + } + } + float comboWidth = CalcComboWidth(longest, options.flags); + + ImGui::AlignTextToFramePadding(); + if (options.labelPosition != LabelPositions::None) { + if (options.alignment == ComponentAlignments::Right) { + ImGui::Text("%s", label); + if (options.labelPosition == LabelPositions::Above) { + ImGui::NewLine(); + ImGui::SameLine(ImGui::GetContentRegionAvail().x - comboWidth); + } else if (options.labelPosition == LabelPositions::Near) { + ImGui::SameLine(); + } else if (options.labelPosition == LabelPositions::Far) { + ImGui::SameLine(ImGui::GetContentRegionAvail().x - comboWidth); + } + } else if (options.alignment == ComponentAlignments::Left) { + if (options.labelPosition == LabelPositions::Above) { + ImGui::Text("%s", label); + } + } + } + + ImGui::SetNextItemWidth(comboWidth); + if (ImGui::BeginCombo(invisibleLabel, comboVector.at(currentValueIndex).c_str(), options.flags)) { + ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(10.0f, 10.0f)); + for (size_t i = 0; i < comboVector.size(); ++i) { + auto newValue = static_cast(i); + if (comboVector.at(i).length() > 1) { + if (ImGui::Selectable(comboVector.at(i).c_str(), newValue == *value)) { + *value = newValue; + dirty = true; + } + } + } + ImGui::PopStyleVar(); + ImGui::EndCombo(); + } + + if (options.labelPosition != LabelPositions::None) { + if (options.alignment == ComponentAlignments::Left) { + if (options.labelPosition == LabelPositions::Near) { + ImGui::SameLine(); + ImGui::Text("%s", label); + } else if (options.labelPosition == LabelPositions::Far) { + float width = + ImGui::CalcTextSize(comboVector.at(*value).c_str()).x + ImGui::GetStyle().FramePadding.x * 2; + ImGui::SameLine(ImGui::GetContentRegionAvail().x - width); + ImGui::Text("%s", label); + } + } + } + + PopStyleCombobox(); + ImGui::EndDisabled(); + ImGui::EndGroup(); + if (options.disabled && ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled) && + !Ship_IsCStringEmpty(options.disabledTooltip)) { + ImGui::SetTooltip("%s", WrappedText(options.disabledTooltip).c_str()); + } else if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled) && !Ship_IsCStringEmpty(options.tooltip)) { + ImGui::SetTooltip("%s", WrappedText(options.tooltip).c_str()); + } + ImGui::PopID(); + return dirty; +} + +template +bool Combobox(const char* label, T* value, const char* (&comboArray)[N], const ComboboxOptions& options = {}) { + bool dirty = false; + size_t currentValueIndex = static_cast(*value); + if (currentValueIndex >= N) { + currentValueIndex = 0; + } + std::string invisibleLabelStr = "##" + std::string(label); + const char* invisibleLabel = invisibleLabelStr.c_str(); + ImGui::PushID(label); + ImGui::BeginGroup(); + ImGui::BeginDisabled(options.disabled); + PushStyleCombobox(options.color); + + const char* longest; + size_t length = 0; + for (size_t i = 0; i < N; i++) { + size_t len = strlen(comboArray[i]); + if (len > length) { + longest = comboArray[i]; + length = len; + } + } + float comboWidth = CalcComboWidth(longest, options.flags); + + ImGui::AlignTextToFramePadding(); + if (options.labelPosition != LabelPositions::None) { + if (options.alignment == ComponentAlignments::Right) { + ImGui::Text("%s", label); + if (options.labelPosition == LabelPositions::Above) { + ImGui::NewLine(); + ImGui::SameLine(ImGui::GetContentRegionAvail().x - comboWidth); + } else if (options.labelPosition == LabelPositions::Near) { + ImGui::SameLine(); + } else if (options.labelPosition == LabelPositions::Far) { + ImGui::SameLine(ImGui::GetContentRegionAvail().x - comboWidth); + } + } else if (options.alignment == ComponentAlignments::Left) { + if (options.labelPosition == LabelPositions::Above) { + ImGui::Text("%s", label); + } + } + } + + ImGui::SetNextItemWidth(comboWidth); + if (ImGui::BeginCombo(invisibleLabel, comboArray[currentValueIndex], options.flags)) { + ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(10.0f, 10.0f)); + for (size_t i = 0; i < N; ++i) { + auto newValue = static_cast(i); + if (strlen(comboArray[i]) > 1) { + if (ImGui::Selectable(comboArray[i], newValue == *value)) { + *value = newValue; + dirty = true; + } + } + } + ImGui::PopStyleVar(); + ImGui::EndCombo(); + } + + if (options.labelPosition != LabelPositions::None) { + if (options.alignment == ComponentAlignments::Left) { + if (options.labelPosition == LabelPositions::Near) { + ImGui::SameLine(); + ImGui::Text("%s", label); + } else if (options.labelPosition == LabelPositions::Far) { + float width = ImGui::CalcTextSize(comboArray[*value]).x + ImGui::GetStyle().FramePadding.x * 2; + ImGui::SameLine(ImGui::GetContentRegionAvail().x - width); + ImGui::Text("%s", label); + } + } + } + PopStyleCombobox(); + ImGui::EndDisabled(); + ImGui::EndGroup(); + if (options.disabled && ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled) && + !Ship_IsCStringEmpty(options.disabledTooltip)) { + ImGui::SetTooltip("%s", WrappedText(options.disabledTooltip).c_str()); + } else if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled) && !Ship_IsCStringEmpty(options.tooltip)) { + ImGui::SetTooltip("%s", WrappedText(options.tooltip).c_str()); + } + ImGui::PopID(); + return dirty; +} + +template +bool CVarCombobox(const char* label, const char* cvarName, const std::unordered_map& comboMap, + const ComboboxOptions& options = {}) { + bool dirty = false; + int32_t value = CVarGetInteger(cvarName, options.defaultIndex); + if (Combobox(label, &value, comboMap, options)) { + CVarSetInteger(cvarName, value); + Ship::Context::GetInstance()->GetWindow()->GetGui()->SaveConsoleVariablesNextFrame(); + ShipInit::Init(cvarName); + dirty = true; + } + return dirty; +} + +template +bool CVarCombobox(const char* label, const char* cvarName, const std::vector& comboVector, + const ComboboxOptions& options = {}) { + bool dirty = false; + int32_t value = CVarGetInteger(cvarName, options.defaultIndex); + if (Combobox(label, &value, comboVector, options)) { + CVarSetInteger(cvarName, value); + Ship::Context::GetInstance()->GetWindow()->GetGui()->SaveConsoleVariablesNextFrame(); + ShipInit::Init(cvarName); + dirty = true; + } + return dirty; +} + +template +bool CVarCombobox(const char* label, const char* cvarName, const char* (&comboArray)[N], + const ComboboxOptions& options = {}) { + bool dirty = false; + int32_t value = CVarGetInteger(cvarName, options.defaultIndex); + if (Combobox(label, &value, comboArray, options)) { + CVarSetInteger(cvarName, value); + Ship::Context::GetInstance()->GetWindow()->GetGui()->SaveConsoleVariablesNextFrame(); + ShipInit::Init(cvarName); + dirty = true; + } + return dirty; +} + +void PushStyleSlider(Colors color = Colors::LightBlue); +void PopStyleSlider(); +bool SliderInt(const char* label, int32_t* value, const IntSliderOptions& options = {}); +bool CVarSliderInt(const char* label, const char* cvarName, const IntSliderOptions& options = {}); +bool SliderFloat(const char* label, float* value, const FloatSliderOptions& options = {}); +bool CVarSliderFloat(const char* label, const char* cvarName, const FloatSliderOptions& options = {}); +bool InputString(const char* label, std::string* value, const InputOptions& options = {}); +bool CVarInputString(const char* label, const char* cvarName, const InputOptions& options = {}); +bool InputInt(const char* label, int32_t* value, const InputOptions& options = {}); +bool CVarInputInt(const char* label, const char* cvarName, const InputOptions& options = {}); +bool CVarColorPicker(const char* label, const char* cvarName, Color_RGBA8 defaultColor, bool hasAlpha = false, + uint8_t modifiers = 0, UIWidgets::Colors themeColor = UIWidgets::Colors::LightBlue); +bool RadioButton(const char* label, bool active); +bool CVarRadioButton(const char* text, const char* cvarName, int32_t id, const RadioButtonsOptions& options); +bool StateButton(const char* str_id, const char* label, ImVec2 size, UIWidgets::ButtonOptions options, + ImGuiButtonFlags flags = ImGuiButtonFlags_None); +void DrawFlagArray32(const std::string& name, uint32_t& flags, Colors color = Colors::LightBlue); +void DrawFlagArray16(const std::string& name, uint16_t& flags, Colors color = Colors::LightBlue); +void DrawFlagArray8(const std::string& name, uint8_t& flags, Colors color = Colors::LightBlue); +void DrawFlagArray8Mask(const std::string& name, uint8_t& flags, Colors color = Colors::LightBlue); + +void InsertHelpHoverText(const std::string& text); +void InsertHelpHoverText(const char* text); +} // namespace UIWidgets +ImVec4 GetRandomValue(); + +Color_RGBA8 RGBA8FromVec(ImVec4 vec); +ImVec4 VecFromRGBA8(Color_RGBA8 color); + +#endif /* UIWidgets_hpp */ diff --git a/src/port/ui/cvar_prefixes.h b/src/port/ui/cvar_prefixes.h new file mode 100644 index 00000000..760361e5 --- /dev/null +++ b/src/port/ui/cvar_prefixes.h @@ -0,0 +1,22 @@ +#pragma once + +#define CVAR_RANDOMIZER_ENHANCEMENT(var) CVAR_PREFIX_RANDOMIZER_ENHANCEMENT "." var +#define CVAR_RANDOMIZER_SETTING(var) CVAR_PREFIX_RANDOMIZER_SETTING "." var +#define CVAR_COSMETIC(var) CVAR_PREFIX_COSMETIC "." var +#define CVAR_AUDIO(var) CVAR_PREFIX_AUDIO "." var +#define CVAR_CHEAT(var) CVAR_PREFIX_CHEAT "." var +#define CVAR_ENHANCEMENT(var) CVAR_PREFIX_ENHANCEMENT "." var +#define CVAR_SETTING(var) CVAR_PREFIX_SETTING "." var +#define CVAR_WINDOW(var) CVAR_PREFIX_WINDOW "." var +#define CVAR_TRACKER(var) CVAR_PREFIX_TRACKER "." var +#define CVAR_TRACKER_ITEM(var) CVAR_TRACKER("ItemTracker." var) +#define CVAR_TRACKER_CHECK(var) CVAR_TRACKER("CheckTracker." var) +#define CVAR_TRACKER_ENTRANCE(var) CVAR_TRACKER("EntranceTracker." var) +#define CVAR_DEVELOPER_TOOLS(var) CVAR_PREFIX_DEVELOPER_TOOLS "." var +#define CVAR_GENERAL(var) CVAR_PREFIX_GENERAL "." var +#define CVAR_REMOTE(var) CVAR_PREFIX_REMOTE "." var +#define CVAR_REMOTE_CROWD_CONTROL(var) CVAR_REMOTE("CrowdControl." var) +#define CVAR_REMOTE_SAIL(var) CVAR_REMOTE("Sail." var) +#define CVAR_REMOTE_ANCHOR(var) CVAR_REMOTE("Anchor." var) +#define CVAR_GAMEPLAY_STATS(var) CVAR_PREFIX_GAMEPLAY_STATS "." var +#define CVAR_TIME_DISPLAY(var) CVAR_PREFIX_TIME_DISPLAY "." var \ No newline at end of file diff --git a/src/port/ui/enhancementTypes.h b/src/port/ui/enhancementTypes.h new file mode 100644 index 00000000..6f70f09b --- /dev/null +++ b/src/port/ui/enhancementTypes.h @@ -0,0 +1 @@ +#pragma once