From 708e4588ae25b164e015fa44578a732d2f378289 Mon Sep 17 00:00:00 2001 From: DefKorns Date: Thu, 10 Sep 2026 16:25:06 +0000 Subject: [PATCH 001/104] Add support for TTF font rendering and improve text handling - Integrated SDL_ttf for advanced text rendering capabilities. - Modified Texture class to support TTF fonts, allowing for proportional text rendering. - Implemented functions to measure text width and line height for TTF fonts. - Updated existing text rendering functions to utilize TTF where applicable. - Added a mechanism to set the TTF font path, with a fallback for missing fonts. - Enhanced text truncation logic to accommodate variable-width fonts. - Updated main application to utilize new TTF rendering features for UI elements. --- Makefile.docker | 27 +- README.md | 2 +- font-build/kanji_codepoints.txt | 2965 +++++++++++++++++ font-build/unicodes-latin.txt | 1 + font-build/unicodes.txt | 1 + .../fonts/NotoSansJP-Latin-subset.ttf | Bin 0 -> 42916 bytes mod/etc/options_menu/fonts/OFL.txt | 93 + .../language/commands/c0010_English | 2 +- .../commands/{c0040_French => c0020_French} | 2 +- .../commands/{c0050_German => c0030_German} | 2 +- .../commands/{c0060_Spanish => c0040_Spanish} | 2 +- .../commands/{c0070_Italian => c0050_Italian} | 2 +- .../commands/{c0080_Dutch => c0060_Dutch} | 2 +- .../{c0020_Portuguese => c0070_Portuguese} | 2 +- .../language/commands/c0080_Russian | 5 + .../{c0030_Japanese => c0090_Japanese} | 2 +- mod/etc/options_menu/language/de-DE.lang | 8 - mod/etc/options_menu/language/en-US.lang | 8 - mod/etc/options_menu/language/es-ES.lang | 8 - mod/etc/options_menu/language/fr-FR.lang | 8 - mod/etc/options_menu/language/it-IT.lang | 8 - mod/etc/options_menu/language/ja-JP.lang | 8 - mod/etc/options_menu/language/nl-NL.lang | 8 - mod/etc/options_menu/language/pt-PT.lang | 8 - mod/etc/options_menu/language/ru-RU.lang | 52 + mod/etc/options_menu/scripts/gen_splash | Bin 49796 -> 0 bytes src/framework/font8x8.h | 1 + src/framework/font8x8_cyrillic.h | 270 ++ src/framework/sdl_context.cpp | 3 + src/framework/texture.cpp | 119 +- src/framework/texture.h | 15 +- src/main.cpp | 67 +- 32 files changed, 3601 insertions(+), 100 deletions(-) create mode 100644 font-build/kanji_codepoints.txt create mode 100644 font-build/unicodes-latin.txt create mode 100644 font-build/unicodes.txt create mode 100644 mod/etc/options_menu/fonts/NotoSansJP-Latin-subset.ttf create mode 100644 mod/etc/options_menu/fonts/OFL.txt rename mod/etc/options_menu/language/commands/{c0040_French => c0020_French} (91%) rename mod/etc/options_menu/language/commands/{c0050_German => c0030_German} (91%) rename mod/etc/options_menu/language/commands/{c0060_Spanish => c0040_Spanish} (91%) rename mod/etc/options_menu/language/commands/{c0070_Italian => c0050_Italian} (91%) rename mod/etc/options_menu/language/commands/{c0080_Dutch => c0060_Dutch} (92%) rename mod/etc/options_menu/language/commands/{c0020_Portuguese => c0070_Portuguese} (89%) create mode 100644 mod/etc/options_menu/language/commands/c0080_Russian rename mod/etc/options_menu/language/commands/{c0030_Japanese => c0090_Japanese} (91%) create mode 100644 mod/etc/options_menu/language/ru-RU.lang delete mode 100644 mod/etc/options_menu/scripts/gen_splash create mode 100644 src/framework/font8x8_cyrillic.h diff --git a/Makefile.docker b/Makefile.docker index f933975..4a12cec 100644 --- a/Makefile.docker +++ b/Makefile.docker @@ -11,15 +11,17 @@ CXX = g++ STRIP = strip ifdef CROSS_PREFIX PKG_CONFIG_LIBDIR = /usr/lib/arm-linux-gnueabihf/pkgconfig -SDL_CFLAGS = -I/usr/include/arm-linux-gnueabihf $(shell PKG_CONFIG_LIBDIR=$(PKG_CONFIG_LIBDIR) pkg-config --cflags sdl2 libpng) -SDL_LIBS = $(shell PKG_CONFIG_LIBDIR=$(PKG_CONFIG_LIBDIR) pkg-config --libs sdl2 libpng) +SDL_CFLAGS = -I/usr/include/arm-linux-gnueabihf $(shell PKG_CONFIG_LIBDIR=$(PKG_CONFIG_LIBDIR) pkg-config --cflags sdl2 SDL2_ttf libpng) +SDL_LIBS = $(shell PKG_CONFIG_LIBDIR=$(PKG_CONFIG_LIBDIR) pkg-config --libs sdl2 SDL2_ttf libpng) PNG_LIBS = $(shell PKG_CONFIG_LIBDIR=$(PKG_CONFIG_LIBDIR) pkg-config --libs libpng) LDFLAGS = -Wl,--allow-shlib-undefined +VENDORED_TTF_LIBS = mod/etc/options_menu/libSDL2_ttf-2.0.so.0 mod/etc/options_menu/libfreetype.so.2.8 else -SDL_CFLAGS = $(shell sdl2-config --cflags) -SDL_LIBS = $(shell sdl2-config --libs) -lpng +SDL_CFLAGS = $(shell sdl2-config --cflags) $(shell pkg-config --cflags SDL2_ttf) +SDL_LIBS = $(shell sdl2-config --libs) $(shell pkg-config --libs SDL2_ttf) -lpng PNG_LIBS = -lpng LDFLAGS = +VENDORED_TTF_LIBS = #FIX_SDL2_SONAME = true endif CXXFLAGS = -std=c++11 -Os $(SDL_CFLAGS) -DMOD_VERSION=\"$(MOD_VER)\" @@ -57,7 +59,7 @@ $(OUT)/$(TARGET_NAME)_b_down.hmod: $(OUT)/$(TARGET_NAME).hmod bump-build-number cd mod/; tar -czvf "../out/$(TARGET_NAME)_b_down.hmod" * cp temp/readme.md mod/readme.md -$(OUT)/$(TARGET_NAME).hmod: mod/etc/options_menu/options mod/etc/options_menu/optiond mod/etc/options_menu/mod_uninstall/mod_uninstall mod/bin/standby_watchdog mod/etc/options_menu/scripts/gen_splash bump-build-number +$(OUT)/$(TARGET_NAME).hmod: mod/etc/options_menu/options mod/etc/options_menu/optiond mod/etc/options_menu/mod_uninstall/mod_uninstall mod/bin/standby_watchdog mod/etc/options_menu/scripts/gen_splash $(VENDORED_TTF_LIBS) bump-build-number $(CROSS_PREFIX)$(STRIP) mod/etc/options_menu/options $(CROSS_PREFIX)$(STRIP) mod/etc/options_menu/optiond $(CROSS_PREFIX)$(STRIP) mod/etc/options_menu/mod_uninstall/mod_uninstall @@ -74,14 +76,23 @@ $(OUT)/$(TARGET_NAME).hmod: mod/etc/options_menu/options mod/etc/options_menu/op find -name "*.o" -type f -delete rm -rf $(TMP) +# SDL2_ttf/freetype aren't on the console's own rootfs (unlike SDL2/libpng); +# they're shipped as mod/etc/options_menu/lib*.so and found via rpath instead. mod/etc/options_menu/options: $(OBJECTS) - $(CROSS_PREFIX)$(CXX) $(OBJECTS) $(LDLIBS) $(LDFLAGS) -o mod/etc/options_menu/options + $(CROSS_PREFIX)$(CXX) $(OBJECTS) $(LDLIBS) $(LDFLAGS) -Wl,-rpath,'$$ORIGIN' -o mod/etc/options_menu/options + +# not ours to redistribute, pulled from the toolchain image each build +mod/etc/options_menu/libSDL2_ttf-2.0.so.0: + cp /usr/lib/arm-linux-gnueabihf/libSDL2_ttf.so mod/etc/options_menu/libSDL2_ttf-2.0.so.0 + +mod/etc/options_menu/libfreetype.so.2.8: + cp /usr/lib/arm-linux-gnueabihf/libfreetype.so.2.8.1 mod/etc/options_menu/libfreetype.so.2.8 mod/etc/options_menu/optiond: src/daemon.o src/framework/controller.o $(CROSS_PREFIX)$(CXX) $(LDFLAGS) src/daemon.o src/framework/controller.o -o mod/etc/options_menu/optiond mod/etc/options_menu/mod_uninstall/mod_uninstall: src/mod_uninstall.o - $(CROSS_PREFIX)$(CXX) src/mod_uninstall.o src/framework/*.o $(LDLIBS) $(LDFLAGS) -o mod/etc/options_menu/mod_uninstall/mod_uninstall + $(CROSS_PREFIX)$(CXX) src/mod_uninstall.o src/framework/*.o $(LDLIBS) $(LDFLAGS) -Wl,-rpath,'$$ORIGIN/..' -o mod/etc/options_menu/mod_uninstall/mod_uninstall mod/bin/standby_watchdog: src/standby_watchdog.o src/framework/controller.o src/framework/powerwatch.o $(CROSS_PREFIX)$(CXX) src/standby_watchdog.o src/framework/controller.o src/framework/powerwatch.o -o mod/bin/standby_watchdog @@ -94,7 +105,7 @@ mod/etc/options_menu/scripts/gen_splash: src/gen_splash.o src/localization.o clean: find -name "*.o" -type f -delete - rm -f mod/etc/options_menu/options mod/etc/options_menu/optiond mod/etc/options_menu/mod_uninstall/mod_uninstall mod/bin/standby_watchdog mod/etc/options_menu/scripts/gen_splash mod/etc/options_menu/button.cfg $(TARGET_NAME).hmod $(TARGET_NAME)_b_down.hmod + rm -f mod/etc/options_menu/options mod/etc/options_menu/optiond mod/etc/options_menu/mod_uninstall/mod_uninstall mod/bin/standby_watchdog mod/etc/options_menu/scripts/gen_splash mod/etc/options_menu/button.cfg mod/etc/options_menu/libSDL2_ttf-2.0.so.0 mod/etc/options_menu/libfreetype.so.2.8 $(TARGET_NAME).hmod $(TARGET_NAME)_b_down.hmod rm -rf out/ temp/ .PHONY: clean bump-build-number diff --git a/README.md b/README.md index c49ac90..5e43868 100644 --- a/README.md +++ b/README.md @@ -168,7 +168,7 @@ This option will force the kernel to clear the page cache and free up unused mem #### Language Options -This option opens a submenu where you can choose the display language for the Options Menu. Currently available: English, Portuguese, French, German, Spanish, Italian and Dutch. The Options Menu automatically restarts in the selected language. +This option opens a submenu where you can choose the display language for the Options Menu. Currently available: English, Français, Deutsch, Español, Italiano, Nederlands, Português (Europeu), Русский and 日本語. The Options Menu automatically restarts in the selected language. ## Network Options diff --git a/font-build/kanji_codepoints.txt b/font-build/kanji_codepoints.txt new file mode 100644 index 0000000..8b7f31b --- /dev/null +++ b/font-build/kanji_codepoints.txt @@ -0,0 +1,2965 @@ +0x4E00 +0x4E01 +0x4E03 +0x4E07 +0x4E08 +0x4E09 +0x4E0A +0x4E0B +0x4E0D +0x4E0E +0x4E11 +0x4E14 +0x4E16 +0x4E18 +0x4E19 +0x4E1E +0x4E21 +0x4E26 +0x4E2D +0x4E32 +0x4E38 +0x4E39 +0x4E3B +0x4E43 +0x4E45 +0x4E4B +0x4E4D +0x4E4E +0x4E4F +0x4E57 +0x4E59 +0x4E5D +0x4E5E +0x4E5F +0x4E71 +0x4E73 +0x4E7E +0x4E80 +0x4E86 +0x4E88 +0x4E89 +0x4E8B +0x4E8C +0x4E91 +0x4E92 +0x4E94 +0x4E95 +0x4E98 +0x4E99 +0x4E9B +0x4E9C +0x4EA1 +0x4EA4 +0x4EA5 +0x4EA6 +0x4EA8 +0x4EAB +0x4EAC +0x4EAD +0x4EAE +0x4EBA +0x4EC0 +0x4EC1 +0x4EC7 +0x4ECA +0x4ECB +0x4ECF +0x4ED4 +0x4ED5 +0x4ED6 +0x4ED8 +0x4ED9 +0x4EE3 +0x4EE4 +0x4EE5 +0x4EEE +0x4EF0 +0x4EF2 +0x4EF6 +0x4EFB +0x4F01 +0x4F0A +0x4F0D +0x4F0E +0x4F0F +0x4F10 +0x4F11 +0x4F1A +0x4F1D +0x4F2F +0x4F34 +0x4F36 +0x4F38 +0x4F3A +0x4F3C +0x4F3D +0x4F43 +0x4F46 +0x4F4D +0x4F4E +0x4F4F +0x4F50 +0x4F51 +0x4F53 +0x4F55 +0x4F59 +0x4F5C +0x4F73 +0x4F75 +0x4F7C +0x4F7F +0x4F83 +0x4F8B +0x4F8D +0x4F9B +0x4F9D +0x4FA0 +0x4FA1 +0x4FAD +0x4FAE +0x4FAF +0x4FB5 +0x4FB6 +0x4FBF +0x4FC2 +0x4FC3 +0x4FC4 +0x4FCA +0x4FD7 +0x4FDD +0x4FE1 +0x4FE3 +0x4FEE +0x4FF3 +0x4FF5 +0x4FF8 +0x4FFA +0x5009 +0x500B +0x500D +0x5012 +0x5016 +0x5019 +0x501F +0x5023 +0x5024 +0x5026 +0x502B +0x502D +0x5036 +0x5039 +0x5049 +0x504F +0x505C +0x5065 +0x5072 +0x5074 +0x5075 +0x5076 +0x507D +0x508D +0x5091 +0x5098 +0x5099 +0x50AC +0x50AD +0x50B5 +0x50B7 +0x50BE +0x50C5 +0x50CD +0x50CF +0x50D1 +0x50D5 +0x50DA +0x50E7 +0x50FB +0x5100 +0x5104 +0x5112 +0x511F +0x512A +0x5132 +0x5141 +0x5143 +0x5144 +0x5145 +0x5146 +0x5147 +0x5148 +0x5149 +0x514B +0x514D +0x514E +0x5150 +0x515A +0x515C +0x5165 +0x5168 +0x516B +0x516C +0x516D +0x5171 +0x5175 +0x5176 +0x5177 +0x5178 +0x517C +0x5185 +0x5186 +0x518A +0x518D +0x5192 +0x5197 +0x5199 +0x51A0 +0x51A5 +0x51A8 +0x51AC +0x51B4 +0x51B6 +0x51B7 +0x51C4 +0x51C6 +0x51CB +0x51CC +0x51CD +0x51DD +0x51E1 +0x51E6 +0x51E7 +0x51EA +0x51F1 +0x51F6 +0x51F8 +0x51F9 +0x51FA +0x51FD +0x5200 +0x5203 +0x5206 +0x5207 +0x5208 +0x520A +0x5211 +0x5217 +0x521D +0x5224 +0x5225 +0x5229 +0x5230 +0x5236 +0x5237 +0x5238 +0x523A +0x523B +0x5243 +0x5247 +0x524A +0x524D +0x5256 +0x525B +0x5263 +0x5264 +0x5265 +0x526F +0x5270 +0x5272 +0x5275 +0x5283 +0x5287 +0x5289 +0x529B +0x529F +0x52A0 +0x52A3 +0x52A9 +0x52AA +0x52AB +0x52B1 +0x52B4 +0x52B9 +0x52BE +0x52C3 +0x52C5 +0x52C7 +0x52C9 +0x52D5 +0x52D8 +0x52D9 +0x52DD +0x52DF +0x52E2 +0x52E4 +0x52E7 +0x52F2 +0x52FA +0x52FE +0x52FF +0x5301 +0x5302 +0x5305 +0x5316 +0x5317 +0x5319 +0x531D +0x5320 +0x5321 +0x532A +0x5339 +0x533A +0x533B +0x533F +0x5341 +0x5343 +0x5347 +0x5348 +0x534A +0x5351 +0x5352 +0x5353 +0x5354 +0x5357 +0x5358 +0x535A +0x535C +0x5360 +0x5366 +0x536F +0x5370 +0x5371 +0x5373 +0x5374 +0x5375 +0x5378 +0x537F +0x5384 +0x5398 +0x539A +0x539F +0x53A8 +0x53A9 +0x53AD +0x53B3 +0x53BB +0x53C2 +0x53C8 +0x53C9 +0x53CA +0x53CB +0x53CC +0x53CD +0x53CE +0x53D4 +0x53D6 +0x53D7 +0x53D9 +0x53DB +0x53E1 +0x53E2 +0x53E3 +0x53E4 +0x53E5 +0x53E9 +0x53EA +0x53EB +0x53EC +0x53EF +0x53F0 +0x53F1 +0x53F2 +0x53F3 +0x53F6 +0x53F7 +0x53F8 +0x5403 +0x5404 +0x5408 +0x5409 +0x540A +0x540B +0x540C +0x540D +0x540E +0x540F +0x5410 +0x5411 +0x541B +0x541F +0x5420 +0x5426 +0x542B +0x5438 +0x5439 +0x543B +0x543E +0x5442 +0x5446 +0x5448 +0x5449 +0x544A +0x5451 +0x5468 +0x546A +0x5473 +0x547C +0x547D +0x548B +0x548C +0x54B2 +0x54B3 +0x54BD +0x54C0 +0x54C1 +0x54C9 +0x54E1 +0x54E8 +0x54E9 +0x54F2 +0x5504 +0x5506 +0x5507 +0x5510 +0x5516 +0x552F +0x5531 +0x553E +0x5544 +0x5546 +0x554F +0x5553 +0x5584 +0x5589 +0x558B +0x559A +0x559C +0x559D +0x55A7 +0x55AA +0x55AB +0x55AC +0x55B0 +0x55B6 +0x55E3 +0x5606 +0x5609 +0x5617 +0x5618 +0x5629 +0x5631 +0x5642 +0x564C +0x565B +0x5668 +0x5674 +0x5678 +0x567A +0x5687 +0x56A2 +0x56DA +0x56DB +0x56DE +0x56E0 +0x56E3 +0x56F0 +0x56F2 +0x56F3 +0x56FA +0x56FD +0x5703 +0x570F +0x5712 +0x571F +0x5727 +0x5728 +0x572D +0x5730 +0x5742 +0x5747 +0x574A +0x5750 +0x5751 +0x5764 +0x5766 +0x576A +0x5782 +0x578B +0x57A2 +0x57A3 +0x57CB +0x57CE +0x57DC +0x57DF +0x57E0 +0x57F4 +0x57F7 +0x57F9 +0x57FA +0x57FC +0x5800 +0x5802 +0x5805 +0x5806 +0x5815 +0x5824 +0x582A +0x5830 +0x5831 +0x5834 +0x5835 +0x583A +0x5840 +0x5841 +0x584A +0x5851 +0x5854 +0x5857 +0x5858 +0x5859 +0x585A +0x585E +0x5869 +0x586B +0x5875 +0x587E +0x5883 +0x5893 +0x5897 +0x589C +0x58A8 +0x58B3 +0x58BE +0x58C1 +0x58C7 +0x58CA +0x58CC +0x58D5 +0x58EB +0x58EC +0x58EE +0x58F0 +0x58F1 +0x58F2 +0x58F7 +0x5909 +0x590F +0x5915 +0x5916 +0x5919 +0x591A +0x591C +0x5922 +0x5927 +0x5929 +0x592A +0x592B +0x592E +0x5931 +0x5937 +0x5944 +0x5947 +0x5948 +0x5949 +0x594F +0x5951 +0x5954 +0x5957 +0x5965 +0x5968 +0x596A +0x596E +0x5973 +0x5974 +0x597D +0x5982 +0x5983 +0x5984 +0x598A +0x5993 +0x5996 +0x5999 +0x59A5 +0x59A8 +0x59AC +0x59B9 +0x59BB +0x59BE +0x59C9 +0x59CB +0x59D0 +0x59D1 +0x59D3 +0x59D4 +0x59E5 +0x59E6 +0x59EA +0x59EB +0x59F6 +0x59FB +0x59FF +0x5A01 +0x5A03 +0x5A18 +0x5A20 +0x5A29 +0x5A2F +0x5A3C +0x5A41 +0x5A46 +0x5A5A +0x5A66 +0x5A7F +0x5A92 +0x5A9B +0x5AC1 +0x5AC9 +0x5ACC +0x5AE1 +0x5B09 +0x5B22 +0x5B2C +0x5B30 +0x5B50 +0x5B54 +0x5B57 +0x5B58 +0x5B5C +0x5B5D +0x5B5F +0x5B63 +0x5B64 +0x5B66 +0x5B6B +0x5B85 +0x5B87 +0x5B88 +0x5B89 +0x5B8B +0x5B8C +0x5B8D +0x5B8F +0x5B95 +0x5B97 +0x5B98 +0x5B99 +0x5B9A +0x5B9B +0x5B9C +0x5B9D +0x5B9F +0x5BA2 +0x5BA3 +0x5BA4 +0x5BA5 +0x5BAE +0x5BB0 +0x5BB3 +0x5BB4 +0x5BB5 +0x5BB6 +0x5BB9 +0x5BBF +0x5BC2 +0x5BC4 +0x5BC5 +0x5BC6 +0x5BCC +0x5BD2 +0x5BD3 +0x5BDB +0x5BDD +0x5BDF +0x5BE1 +0x5BE7 +0x5BE9 +0x5BEE +0x5BF5 +0x5BF8 +0x5BFA +0x5BFE +0x5BFF +0x5C01 +0x5C02 +0x5C04 +0x5C06 +0x5C09 +0x5C0A +0x5C0B +0x5C0E +0x5C0F +0x5C11 +0x5C16 +0x5C1A +0x5C24 +0x5C2D +0x5C31 +0x5C3A +0x5C3B +0x5C3C +0x5C3D +0x5C3E +0x5C3F +0x5C40 +0x5C45 +0x5C48 +0x5C4A +0x5C4B +0x5C4D +0x5C51 +0x5C55 +0x5C5E +0x5C60 +0x5C61 +0x5C64 +0x5C65 +0x5C6F +0x5C71 +0x5C90 +0x5CA1 +0x5CA8 +0x5CA9 +0x5CAC +0x5CB1 +0x5CB3 +0x5CB8 +0x5CE0 +0x5CE1 +0x5CE8 +0x5CEF +0x5CF0 +0x5CF6 +0x5CFB +0x5D07 +0x5D0E +0x5D16 +0x5D29 +0x5D50 +0x5D69 +0x5D6F +0x5D8B +0x5DBA +0x5DCC +0x5DDD +0x5DDE +0x5DE1 +0x5DE3 +0x5DE5 +0x5DE6 +0x5DE7 +0x5DE8 +0x5DEE +0x5DF1 +0x5DF3 +0x5DF4 +0x5DF7 +0x5DFB +0x5DFD +0x5DFE +0x5E02 +0x5E03 +0x5E06 +0x5E0C +0x5E16 +0x5E1D +0x5E25 +0x5E2B +0x5E2D +0x5E2F +0x5E30 +0x5E33 +0x5E38 +0x5E3D +0x5E45 +0x5E4C +0x5E55 +0x5E61 +0x5E63 +0x5E72 +0x5E73 +0x5E74 +0x5E78 +0x5E79 +0x5E7B +0x5E7C +0x5E7D +0x5E7E +0x5E81 +0x5E83 +0x5E84 +0x5E87 +0x5E8A +0x5E8F +0x5E95 +0x5E96 +0x5E97 +0x5E9A +0x5E9C +0x5EA6 +0x5EA7 +0x5EAB +0x5EAD +0x5EB5 +0x5EB6 +0x5EB7 +0x5EB8 +0x5EC3 +0x5EC9 +0x5ECA +0x5ED3 +0x5EDF +0x5EE0 +0x5EF6 +0x5EF7 +0x5EFA +0x5EFB +0x5EFC +0x5EFF +0x5F01 +0x5F04 +0x5F0A +0x5F0F +0x5F10 +0x5F13 +0x5F14 +0x5F15 +0x5F17 +0x5F18 +0x5F1B +0x5F1F +0x5F25 +0x5F26 +0x5F27 +0x5F31 +0x5F35 +0x5F37 +0x5F3C +0x5F3E +0x5F4A +0x5F53 +0x5F62 +0x5F66 +0x5F69 +0x5F6A +0x5F6B +0x5F6C +0x5F70 +0x5F71 +0x5F79 +0x5F7C +0x5F80 +0x5F81 +0x5F84 +0x5F85 +0x5F8B +0x5F8C +0x5F90 +0x5F92 +0x5F93 +0x5F97 +0x5FA1 +0x5FA9 +0x5FAA +0x5FAE +0x5FB3 +0x5FB4 +0x5FB9 +0x5FBD +0x5FC3 +0x5FC5 +0x5FCC +0x5FCD +0x5FD7 +0x5FD8 +0x5FD9 +0x5FDC +0x5FE0 +0x5FEB +0x5FF5 +0x5FFD +0x6012 +0x6016 +0x601C +0x601D +0x6020 +0x6025 +0x6027 +0x6028 +0x602A +0x602F +0x604B +0x6050 +0x6052 +0x6055 +0x6062 +0x6065 +0x6068 +0x6069 +0x606D +0x606F +0x6070 +0x6075 +0x6089 +0x608C +0x6094 +0x609F +0x60A0 +0x60A3 +0x60A6 +0x60A9 +0x60AA +0x60B2 +0x60B6 +0x60BC +0x60C5 +0x60C7 +0x60D1 +0x60DA +0x60DC +0x60DF +0x60E3 +0x60E8 +0x60F0 +0x60F3 +0x60F9 +0x6101 +0x6108 +0x6109 +0x610F +0x611A +0x611B +0x611F +0x6148 +0x614B +0x614C +0x614E +0x6155 +0x6162 +0x6163 +0x6167 +0x6168 +0x616E +0x6170 +0x6176 +0x617E +0x6182 +0x618E +0x6190 +0x61A4 +0x61A7 +0x61A9 +0x61B2 +0x61B6 +0x61BE +0x61C7 +0x61D0 +0x61F2 +0x61F8 +0x620A +0x620E +0x6210 +0x6211 +0x6212 +0x6216 +0x621A +0x621F +0x6226 +0x622F +0x6234 +0x6238 +0x623B +0x623F +0x6240 +0x6247 +0x6249 +0x624B +0x624D +0x6253 +0x6255 +0x6258 +0x626E +0x6271 +0x6276 +0x6279 +0x627F +0x6280 +0x6284 +0x628A +0x6291 +0x6295 +0x6297 +0x6298 +0x629C +0x629E +0x62AB +0x62B1 +0x62B5 +0x62B9 +0x62BC +0x62BD +0x62C5 +0x62CD +0x62D0 +0x62D2 +0x62D3 +0x62D8 +0x62D9 +0x62DB +0x62DD +0x62E0 +0x62E1 +0x62EC +0x62ED +0x62F3 +0x62F6 +0x62F7 +0x62FE +0x6301 +0x6307 +0x6309 +0x6311 +0x6319 +0x631F +0x6328 +0x632B +0x632F +0x633A +0x633D +0x633F +0x6349 +0x634C +0x6355 +0x6357 +0x635C +0x6367 +0x6368 +0x636E +0x6372 +0x6377 +0x637A +0x637B +0x6383 +0x6388 +0x638C +0x6392 +0x6398 +0x639B +0x63A0 +0x63A1 +0x63A2 +0x63A5 +0x63A7 +0x63A8 +0x63A9 +0x63AA +0x63AC +0x63B2 +0x63B4 +0x63BB +0x63C3 +0x63CF +0x63D0 +0x63D6 +0x63DA +0x63DB +0x63E1 +0x63EE +0x63F4 +0x63FA +0x640D +0x642C +0x642D +0x643A +0x643E +0x6442 +0x6458 +0x6469 +0x6478 +0x647A +0x6483 +0x6492 +0x649A +0x649E +0x64A4 +0x64AB +0x64AD +0x64AE +0x64B0 +0x64B2 +0x64B9 +0x64C1 +0x64CD +0x64E2 +0x64E6 +0x64EC +0x64FE +0x652F +0x6539 +0x653B +0x653E +0x653F +0x6545 +0x654F +0x6551 +0x6557 +0x6559 +0x6562 +0x6563 +0x6566 +0x656C +0x6570 +0x6574 +0x6575 +0x6577 +0x6587 +0x6589 +0x658C +0x658E +0x6590 +0x6591 +0x6597 +0x6599 +0x659C +0x65A1 +0x65A4 +0x65A5 +0x65A7 +0x65AC +0x65AD +0x65AF +0x65B0 +0x65B9 +0x65BC +0x65BD +0x65C5 +0x65CB +0x65CF +0x65D7 +0x65E2 +0x65E5 +0x65E6 +0x65E7 +0x65E8 +0x65E9 +0x65EC +0x65ED +0x65FA +0x6602 +0x6606 +0x6607 +0x660C +0x660E +0x660F +0x6613 +0x6614 +0x661F +0x6620 +0x6625 +0x6627 +0x6628 +0x662D +0x662F +0x663C +0x6642 +0x6643 +0x664B +0x6652 +0x6666 +0x6669 +0x666E +0x666F +0x6674 +0x6676 +0x667A +0x6681 +0x6687 +0x6691 +0x6696 +0x6697 +0x66A2 +0x66A6 +0x66AB +0x66AE +0x66B4 +0x66C7 +0x66D9 +0x66DC +0x66DD +0x66F2 +0x66F3 +0x66F4 +0x66F8 +0x66F9 +0x66FD +0x66FE +0x66FF +0x6700 +0x6708 +0x6709 +0x670B +0x670D +0x6714 +0x6715 +0x6717 +0x671B +0x671D +0x671F +0x6728 +0x672A +0x672B +0x672C +0x672D +0x6731 +0x6734 +0x673A +0x673D +0x6749 +0x674E +0x674F +0x6750 +0x6751 +0x6753 +0x6756 +0x675C +0x675F +0x6761 +0x6762 +0x6765 +0x676D +0x676F +0x6771 +0x6775 +0x6777 +0x677E +0x677F +0x6787 +0x6790 +0x6795 +0x6797 +0x679A +0x679C +0x679D +0x67A0 +0x67A2 +0x67AF +0x67B6 +0x67C1 +0x67C4 +0x67CA +0x67CF +0x67D0 +0x67D1 +0x67D3 +0x67D4 +0x67D8 +0x67DA +0x67F1 +0x67F3 +0x67F4 +0x67F5 +0x67FB +0x67FE +0x67FF +0x6802 +0x6803 +0x6804 +0x6813 +0x6816 +0x6817 +0x6821 +0x6822 +0x682A +0x6834 +0x6838 +0x6839 +0x683C +0x683D +0x6841 +0x6842 +0x6843 +0x6848 +0x6850 +0x6851 +0x6853 +0x6854 +0x685C +0x685D +0x685F +0x6867 +0x6876 +0x6881 +0x6885 +0x6893 +0x6897 +0x68A2 +0x68A7 +0x68A8 +0x68AF +0x68B0 +0x68B1 +0x68B6 +0x68BC +0x68C4 +0x68C9 +0x68CB +0x68D2 +0x68DA +0x68DF +0x68EE +0x68F2 +0x68FA +0x6900 +0x6905 +0x690B +0x690D +0x690E +0x6919 +0x691B +0x691C +0x6934 +0x693F +0x694A +0x6953 +0x6955 +0x695A +0x6960 +0x6962 +0x696D +0x696F +0x6973 +0x6975 +0x697C +0x697D +0x6982 +0x698A +0x698E +0x6994 +0x699B +0x69CB +0x69CC +0x69CD +0x69D8 +0x69D9 +0x69FB +0x69FD +0x6A0B +0x6A17 +0x6A19 +0x6A1F +0x6A21 +0x6A29 +0x6A2A +0x6A2B +0x6A35 +0x6A39 +0x6A3A +0x6A3D +0x6A4B +0x6A58 +0x6A5F +0x6A61 +0x6A7F +0x6A80 +0x6A8E +0x6AD3 +0x6ADB +0x6AE8 +0x6B04 +0x6B1D +0x6B20 +0x6B21 +0x6B23 +0x6B27 +0x6B32 +0x6B3A +0x6B3D +0x6B3E +0x6B4C +0x6B4E +0x6B53 +0x6B62 +0x6B63 +0x6B64 +0x6B66 +0x6B69 +0x6B6A +0x6B6F +0x6B73 +0x6B74 +0x6B7B +0x6B86 +0x6B89 +0x6B8A +0x6B8B +0x6B96 +0x6BB4 +0x6BB5 +0x6BBA +0x6BBB +0x6BBF +0x6BC5 +0x6BCD +0x6BCE +0x6BD2 +0x6BD4 +0x6BD8 +0x6BDB +0x6C0F +0x6C11 +0x6C17 +0x6C34 +0x6C37 +0x6C38 +0x6C3E +0x6C40 +0x6C41 +0x6C42 +0x6C4E +0x6C50 +0x6C57 +0x6C5A +0x6C5D +0x6C5F +0x6C60 +0x6C70 +0x6C72 +0x6C7A +0x6C7D +0x6C83 +0x6C88 +0x6C8C +0x6C93 +0x6C96 +0x6C99 +0x6CA1 +0x6CA2 +0x6CAB +0x6CB3 +0x6CB8 +0x6CB9 +0x6CBB +0x6CBC +0x6CBF +0x6CC1 +0x6CC9 +0x6CCA +0x6CCC +0x6CD5 +0x6CE1 +0x6CE2 +0x6CE3 +0x6CE5 +0x6CE8 +0x6CF0 +0x6CF3 +0x6D0B +0x6D17 +0x6D1B +0x6D1E +0x6D25 +0x6D29 +0x6D2A +0x6D32 +0x6D3B +0x6D3E +0x6D41 +0x6D44 +0x6D45 +0x6D5C +0x6D66 +0x6D69 +0x6D6A +0x6D6C +0x6D6E +0x6D74 +0x6D77 +0x6D78 +0x6D88 +0x6D8C +0x6D99 +0x6D9B +0x6D9C +0x6DAF +0x6DB2 +0x6DBC +0x6DC0 +0x6DCB +0x6DD1 +0x6DD8 +0x6DE1 +0x6DEB +0x6DF1 +0x6DF3 +0x6DF5 +0x6DF7 +0x6DFB +0x6E05 +0x6E07 +0x6E08 +0x6E09 +0x6E0B +0x6E13 +0x6E1A +0x6E1B +0x6E20 +0x6E21 +0x6E25 +0x6E26 +0x6E29 +0x6E2C +0x6E2F +0x6E4A +0x6E56 +0x6E58 +0x6E5B +0x6E67 +0x6E6F +0x6E7E +0x6E7F +0x6E80 +0x6E8C +0x6E90 +0x6E96 +0x6E9C +0x6E9D +0x6EA2 +0x6EB6 +0x6EBA +0x6EC5 +0x6ECB +0x6ED1 +0x6EDD +0x6EDE +0x6EF4 +0x6F01 +0x6F02 +0x6F06 +0x6F09 +0x6F0F +0x6F14 +0x6F15 +0x6F20 +0x6F22 +0x6F23 +0x6F2B +0x6F2C +0x6F38 +0x6F45 +0x6F54 +0x6F5C +0x6F5F +0x6F64 +0x6F6E +0x6F70 +0x6F84 +0x6F97 +0x6FB1 +0x6FC0 +0x6FC1 +0x6FC3 +0x6FE0 +0x6FE1 +0x6FEB +0x6FEF +0x7015 +0x701E +0x7026 +0x7027 +0x702C +0x7058 +0x706B +0x706F +0x7070 +0x7078 +0x707C +0x707D +0x7089 +0x708A +0x708E +0x70AD +0x70B9 +0x70BA +0x70C8 +0x70CF +0x70F9 +0x7114 +0x711A +0x7121 +0x7126 +0x7136 +0x713C +0x7149 +0x714E +0x7159 +0x7164 +0x7167 +0x7169 +0x716E +0x717D +0x718A +0x7194 +0x719F +0x71B1 +0x71C3 +0x71C8 +0x71D0 +0x71D5 +0x71E5 +0x71E6 +0x71ED +0x7206 +0x722A +0x7235 +0x7236 +0x723A +0x723D +0x723E +0x7247 +0x7248 +0x724C +0x7252 +0x7259 +0x725B +0x725D +0x725F +0x7261 +0x7262 +0x7267 +0x7269 +0x7272 +0x7279 +0x727D +0x7280 +0x72A0 +0x72AC +0x72AF +0x72B6 +0x72C2 +0x72D0 +0x72D7 +0x72D9 +0x72DB +0x72E9 +0x72EC +0x72ED +0x72F8 +0x72FC +0x72FD +0x731B +0x731F +0x732A +0x732B +0x732E +0x7336 +0x7337 +0x733F +0x7344 +0x7345 +0x7363 +0x7372 +0x7384 +0x7387 +0x7389 +0x738B +0x7396 +0x73A9 +0x73B2 +0x73C2 +0x73CA +0x73CD +0x73E0 +0x73EA +0x73ED +0x73FE +0x7403 +0x7406 +0x7409 +0x7422 +0x7433 +0x7434 +0x7435 +0x7436 +0x745A +0x745B +0x745E +0x7460 +0x7473 +0x7483 +0x74B0 +0x74BD +0x74DC +0x74E2 +0x74E6 +0x74F6 +0x7511 +0x7518 +0x751A +0x751C +0x751F +0x7523 +0x7525 +0x7528 +0x752B +0x7530 +0x7531 +0x7532 +0x7533 +0x7537 +0x753A +0x753B +0x754C +0x754F +0x7551 +0x7554 +0x7559 +0x755C +0x755D +0x7560 +0x7562 +0x7565 +0x7566 +0x756A +0x7570 +0x7573 +0x7577 +0x757F +0x758B +0x758E +0x758F +0x7591 +0x75AB +0x75B2 +0x75B9 +0x75BE +0x75C5 +0x75C7 +0x75D4 +0x75D5 +0x75D8 +0x75DB +0x75E2 +0x75E9 +0x75F4 +0x7642 +0x764C +0x7652 +0x7656 +0x767A +0x767B +0x767D +0x767E +0x7684 +0x7686 +0x7687 +0x7690 +0x76AE +0x76BF +0x76C3 +0x76C6 +0x76C8 +0x76CA +0x76D7 +0x76DB +0x76DF +0x76E3 +0x76E4 +0x76EE +0x76F2 +0x76F4 +0x76F8 +0x76FE +0x7701 +0x7709 +0x770B +0x770C +0x771F +0x7720 +0x773A +0x773C +0x7740 +0x7761 +0x7763 +0x7766 +0x77A5 +0x77AC +0x77AD +0x77B3 +0x77DB +0x77E2 +0x77E5 +0x77E7 +0x77E9 +0x77ED +0x77EF +0x77F3 +0x7802 +0x7814 +0x7815 +0x7825 +0x7826 +0x7827 +0x7832 +0x7834 +0x783A +0x783F +0x785D +0x786B +0x786C +0x786F +0x7872 +0x7881 +0x7887 +0x788D +0x7891 +0x7893 +0x7895 +0x7897 +0x78A7 +0x78A9 +0x78BA +0x78C1 +0x78D0 +0x78E8 +0x78EF +0x7901 +0x790E +0x793A +0x793C +0x793E +0x7941 +0x7947 +0x7948 +0x7949 +0x7950 +0x7956 +0x795D +0x795E +0x7962 +0x7965 +0x7968 +0x796D +0x7977 +0x7981 +0x7984 +0x7985 +0x798D +0x798E +0x798F +0x79A6 +0x79B0 +0x79BD +0x79BE +0x79BF +0x79C0 +0x79C1 +0x79CB +0x79D1 +0x79D2 +0x79D8 +0x79DF +0x79E4 +0x79E6 +0x79E9 +0x79F0 +0x79FB +0x7A00 +0x7A0B +0x7A0E +0x7A14 +0x7A17 +0x7A1A +0x7A1C +0x7A2E +0x7A32 +0x7A3C +0x7A3D +0x7A3F +0x7A40 +0x7A42 +0x7A46 +0x7A4D +0x7A4E +0x7A4F +0x7A50 +0x7A63 +0x7A6B +0x7A74 +0x7A76 +0x7A7A +0x7A7F +0x7A81 +0x7A83 +0x7A84 +0x7A92 +0x7A93 +0x7A9F +0x7AAA +0x7AAE +0x7AAF +0x7ABA +0x7AC3 +0x7ACB +0x7ADC +0x7AE0 +0x7AE3 +0x7AE5 +0x7AEA +0x7AEF +0x7AF6 +0x7AF9 +0x7AFA +0x7AFF +0x7B08 +0x7B11 +0x7B1B +0x7B20 +0x7B25 +0x7B26 +0x7B2C +0x7B39 +0x7B46 +0x7B48 +0x7B49 +0x7B4B +0x7B4F +0x7B51 +0x7B52 +0x7B54 +0x7B56 +0x7B86 +0x7B87 +0x7B94 +0x7B95 +0x7B97 +0x7BA1 +0x7BAA +0x7BAD +0x7BB1 +0x7BB8 +0x7BC0 +0x7BC4 +0x7BC7 +0x7BC9 +0x7BE0 +0x7BE4 +0x7BED +0x7C21 +0x7C38 +0x7C3E +0x7C3F +0x7C4D +0x7C73 +0x7C7E +0x7C81 +0x7C82 +0x7C89 +0x7C8B +0x7C8D +0x7C92 +0x7C95 +0x7C97 +0x7C98 +0x7C9B +0x7C9F +0x7CA5 +0x7CA7 +0x7CBE +0x7CCA +0x7CCE +0x7CD6 +0x7CDE +0x7CDF +0x7CE0 +0x7CE7 +0x7CF8 +0x7CFB +0x7CFE +0x7D00 +0x7D04 +0x7D05 +0x7D0B +0x7D0D +0x7D10 +0x7D14 +0x7D17 +0x7D18 +0x7D19 +0x7D1A +0x7D1B +0x7D20 +0x7D21 +0x7D22 +0x7D2B +0x7D2C +0x7D2F +0x7D30 +0x7D33 +0x7D39 +0x7D3A +0x7D42 +0x7D43 +0x7D44 +0x7D4C +0x7D50 +0x7D5E +0x7D61 +0x7D62 +0x7D66 +0x7D71 +0x7D75 +0x7D76 +0x7D79 +0x7D99 +0x7D9A +0x7D9C +0x7DAC +0x7DAD +0x7DB1 +0x7DB2 +0x7DB4 +0x7DBB +0x7DBE +0x7DBF +0x7DCA +0x7DCB +0x7DCF +0x7DD1 +0x7DD2 +0x7DDA +0x7DE0 +0x7DE8 +0x7DE9 +0x7DEC +0x7DEF +0x7DF4 +0x7E01 +0x7E04 +0x7E1B +0x7E1E +0x7E26 +0x7E2B +0x7E2E +0x7E3E +0x7E41 +0x7E4A +0x7E4B +0x7E4D +0x7E54 +0x7E55 +0x7E6D +0x7E70 +0x7E82 +0x7E8F +0x7F36 +0x7F6A +0x7F6B +0x7F6E +0x7F70 +0x7F72 +0x7F75 +0x7F77 +0x7F85 +0x7F8A +0x7F8E +0x7FA4 +0x7FA8 +0x7FA9 +0x7FBD +0x7FC1 +0x7FCC +0x7FD2 +0x7FE0 +0x7FEB +0x7FF0 +0x7FFB +0x7FFC +0x8000 +0x8001 +0x8003 +0x8005 +0x800C +0x8010 +0x8015 +0x8017 +0x8033 +0x8036 +0x803D +0x8056 +0x805E +0x8061 +0x806F +0x8074 +0x8077 +0x807E +0x8087 +0x8089 +0x808B +0x808C +0x8096 +0x8098 +0x809D +0x80A1 +0x80A2 +0x80A5 +0x80A9 +0x80AA +0x80AF +0x80B1 +0x80B2 +0x80B4 +0x80BA +0x80C3 +0x80C6 +0x80CC +0x80CE +0x80DE +0x80E1 +0x80E4 +0x80F4 +0x80F8 +0x80FD +0x8102 +0x8105 +0x8106 +0x8107 +0x8108 +0x810A +0x811A +0x8131 +0x8133 +0x8139 +0x814E +0x8150 +0x8154 +0x8155 +0x816B +0x8170 +0x8178 +0x8179 +0x817A +0x817F +0x818F +0x819A +0x819C +0x819D +0x81A8 +0x81B3 +0x81BF +0x81C6 +0x81D3 +0x81E3 +0x81E5 +0x81E8 +0x81EA +0x81ED +0x81F3 +0x81F4 +0x81FC +0x8208 +0x820C +0x820E +0x8217 +0x8218 +0x821B +0x821C +0x821E +0x821F +0x822A +0x822C +0x8235 +0x8236 +0x8237 +0x8239 +0x8247 +0x8266 +0x826E +0x826F +0x8272 +0x8276 +0x828B +0x8299 +0x829D +0x82A5 +0x82A6 +0x82AD +0x82AF +0x82B1 +0x82B3 +0x82B8 +0x82B9 +0x82BD +0x82C5 +0x82D1 +0x82D3 +0x82D4 +0x82D7 +0x82DB +0x82E5 +0x82E6 +0x82E7 +0x82EB +0x82F1 +0x8302 +0x8304 +0x8305 +0x830E +0x831C +0x8328 +0x8336 +0x8338 +0x8349 +0x834A +0x834F +0x8352 +0x8358 +0x8377 +0x837B +0x839E +0x83AB +0x83B1 +0x83C5 +0x83CA +0x83CC +0x83D3 +0x83D6 +0x83DC +0x83DF +0x83E9 +0x83EF +0x83F0 +0x83F1 +0x8404 +0x840C +0x840E +0x8429 +0x8431 +0x843D +0x8449 +0x844E +0x8457 +0x845B +0x8461 +0x8463 +0x8466 +0x846C +0x8471 +0x8475 +0x847A +0x848B +0x8490 +0x8494 +0x8499 +0x849C +0x84B2 +0x84B8 +0x84BC +0x84C4 +0x84C9 +0x84CB +0x84D1 +0x84EC +0x84EE +0x8500 +0x8511 +0x8513 +0x851A +0x8526 +0x852D +0x8535 +0x853D +0x8543 +0x8549 +0x854A +0x854E +0x8557 +0x8568 +0x8569 +0x856A +0x8584 +0x8597 +0x8599 +0x85A6 +0x85A9 +0x85AA +0x85AB +0x85AC +0x85AE +0x85AF +0x85C1 +0x85CD +0x85E4 +0x85E9 +0x85F7 +0x85FB +0x8607 +0x862D +0x864E +0x8650 +0x865A +0x865C +0x865E +0x866B +0x8679 +0x867B +0x868A +0x8695 +0x86A4 +0x86C7 +0x86CB +0x86CD +0x86CE +0x86D9 +0x86E4 +0x86ED +0x86EE +0x86F8 +0x86FE +0x8702 +0x8718 +0x871C +0x8749 +0x874B +0x8755 +0x8766 +0x8776 +0x877F +0x878D +0x87BA +0x87F9 +0x87FB +0x8840 +0x8846 +0x884C +0x8853 +0x8857 +0x885B +0x885D +0x8861 +0x8863 +0x8868 +0x8870 +0x8877 +0x887F +0x8888 +0x888B +0x8896 +0x88AB +0x88B4 +0x88B7 +0x88C1 +0x88C2 +0x88C5 +0x88CF +0x88D5 +0x88DC +0x88DF +0x88E1 +0x88F3 +0x88F8 +0x88FD +0x88FE +0x8907 +0x8910 +0x8912 +0x8956 +0x895F +0x8972 +0x897F +0x8981 +0x8986 +0x8987 +0x898B +0x898F +0x8996 +0x8997 +0x899A +0x89A7 +0x89AA +0x89B3 +0x89D2 +0x89E3 +0x89E6 +0x8A00 +0x8A02 +0x8A08 +0x8A0A +0x8A0E +0x8A13 +0x8A17 +0x8A18 +0x8A1F +0x8A23 +0x8A2A +0x8A2D +0x8A31 +0x8A33 +0x8A34 +0x8A3A +0x8A3B +0x8A3C +0x8A50 +0x8A51 +0x8A54 +0x8A55 +0x8A5E +0x8A60 +0x8A63 +0x8A66 +0x8A69 +0x8A6B +0x8A6E +0x8A70 +0x8A71 +0x8A72 +0x8A73 +0x8A87 +0x8A89 +0x8A8C +0x8A8D +0x8A93 +0x8A95 +0x8A98 +0x8A9E +0x8AA0 +0x8AA4 +0x8AAC +0x8AAD +0x8AB0 +0x8AB2 +0x8AB9 +0x8ABC +0x8ABF +0x8AC7 +0x8ACB +0x8ACC +0x8ACF +0x8AD2 +0x8AD6 +0x8ADC +0x8AE6 +0x8AED +0x8AEE +0x8AF8 +0x8AFA +0x8AFE +0x8B00 +0x8B01 +0x8B02 +0x8B04 +0x8B0E +0x8B19 +0x8B1B +0x8B1D +0x8B21 +0x8B2C +0x8B39 +0x8B58 +0x8B5C +0x8B66 +0x8B70 +0x8B72 +0x8B77 +0x8B83 +0x8B90 +0x8C37 +0x8C46 +0x8C4A +0x8C5A +0x8C61 +0x8C6A +0x8C79 +0x8C8C +0x8C9D +0x8C9E +0x8CA0 +0x8CA1 +0x8CA2 +0x8CA7 +0x8CA8 +0x8CA9 +0x8CAB +0x8CAC +0x8CAF +0x8CB0 +0x8CB4 +0x8CB7 +0x8CB8 +0x8CBB +0x8CBC +0x8CBF +0x8CC0 +0x8CC2 +0x8CC3 +0x8CC4 +0x8CC7 +0x8CCA +0x8CCE +0x8CD1 +0x8CD3 +0x8CDB +0x8CDC +0x8CDE +0x8CE0 +0x8CE2 +0x8CE6 +0x8CEA +0x8CED +0x8CFC +0x8D08 +0x8D0B +0x8D64 +0x8D66 +0x8D6B +0x8D70 +0x8D74 +0x8D77 +0x8D85 +0x8D8A +0x8DA3 +0x8DA8 +0x8DB3 +0x8DDD +0x8DE1 +0x8DE8 +0x8DEF +0x8DF3 +0x8DF5 +0x8E0A +0x8E0F +0x8E44 +0x8E5F +0x8E74 +0x8E8D +0x8EAB +0x8EAF +0x8ECA +0x8ECC +0x8ECD +0x8ED2 +0x8EDF +0x8EE2 +0x8EF8 +0x8EFD +0x8F03 +0x8F09 +0x8F14 +0x8F1D +0x8F29 +0x8F2A +0x8F2F +0x8F38 +0x8F3F +0x8F44 +0x8F4D +0x8F5F +0x8F61 +0x8F9B +0x8F9E +0x8FB0 +0x8FB1 +0x8FB2 +0x8FBA +0x8FBB +0x8FBC +0x8FBF +0x8FC2 +0x8FC4 +0x8FC5 +0x8FCE +0x8FD1 +0x8FD4 +0x8FE6 +0x8FE9 +0x8FEB +0x8FED +0x8FF0 +0x8FF7 +0x8FFD +0x9000 +0x9001 +0x9003 +0x9006 +0x900F +0x9010 +0x9013 +0x9014 +0x9017 +0x9019 +0x901A +0x901D +0x901F +0x9020 +0x9022 +0x9023 +0x902E +0x9031 +0x9032 +0x9038 +0x903C +0x9041 +0x9042 +0x9045 +0x9047 +0x904A +0x904B +0x904D +0x904E +0x9053 +0x9054 +0x9055 +0x905C +0x9060 +0x9061 +0x9063 +0x9065 +0x9069 +0x906D +0x906E +0x9075 +0x9077 +0x9078 +0x907A +0x907C +0x907F +0x9084 +0x9091 +0x90A3 +0x90A6 +0x90AA +0x90B8 +0x90C1 +0x90CA +0x90CE +0x90E1 +0x90E8 +0x90ED +0x90F5 +0x90F7 +0x90FD +0x912D +0x9149 +0x914B +0x914C +0x914D +0x914E +0x9152 +0x9154 +0x9162 +0x916A +0x916C +0x9175 +0x9177 +0x9178 +0x9187 +0x918D +0x9190 +0x9192 +0x9197 +0x919C +0x91A4 +0x91B8 +0x91C6 +0x91C7 +0x91C8 +0x91CC +0x91CD +0x91CE +0x91CF +0x91D1 +0x91D8 +0x91DC +0x91DD +0x91E3 +0x91E6 +0x91E7 +0x920D +0x920E +0x9234 +0x9237 +0x9244 +0x925B +0x9262 +0x9266 +0x9271 +0x927E +0x9280 +0x9283 +0x9285 +0x9291 +0x9298 +0x929A +0x92AD +0x92D2 +0x92E4 +0x92EA +0x92ED +0x92F2 +0x92F3 +0x92F8 +0x92FC +0x9306 +0x9310 +0x9318 +0x9320 +0x9326 +0x9328 +0x932B +0x932C +0x932F +0x9332 +0x934B +0x934D +0x9354 +0x935B +0x936C +0x9375 +0x937E +0x938C +0x9396 +0x9397 +0x939A +0x93A7 +0x93AE +0x93D1 +0x93E1 +0x9418 +0x9419 +0x9438 +0x9451 +0x9453 +0x9577 +0x9580 +0x9583 +0x9589 +0x958B +0x958F +0x9591 +0x9593 +0x95A2 +0x95A3 +0x95A4 +0x95A5 +0x95B2 +0x95C7 +0x95D8 +0x961C +0x962A +0x9632 +0x963B +0x963F +0x9640 +0x9644 +0x964D +0x9650 +0x965B +0x9662 +0x9663 +0x9664 +0x9665 +0x966A +0x9670 +0x9673 +0x9675 +0x9676 +0x9678 +0x967A +0x967D +0x9685 +0x9686 +0x9688 +0x968A +0x968E +0x968F +0x9694 +0x9699 +0x969B +0x969C +0x96A0 +0x96A3 +0x96B7 +0x96BB +0x96BC +0x96C0 +0x96C1 +0x96C4 +0x96C5 +0x96C6 +0x96C7 +0x96CC +0x96D1 +0x96DB +0x96E2 +0x96E3 +0x96E8 +0x96EA +0x96EB +0x96F0 +0x96F2 +0x96F6 +0x96F7 +0x96FB +0x9700 +0x9707 +0x970A +0x971C +0x971E +0x9727 +0x9732 +0x9752 +0x9756 +0x9759 +0x975E +0x9762 +0x9769 +0x976D +0x9774 +0x9784 +0x978D +0x9798 +0x97A0 +0x97AD +0x97D3 +0x97EE +0x97F3 +0x97FB +0x97FF +0x9801 +0x9802 +0x9803 +0x9805 +0x9806 +0x9808 +0x9810 +0x9811 +0x9812 +0x9813 +0x9817 +0x9818 +0x981A +0x982C +0x982D +0x9834 +0x983B +0x983C +0x984C +0x984D +0x984E +0x9854 +0x9855 +0x9858 +0x985B +0x985E +0x9867 +0x98A8 +0x98DB +0x98DF +0x98E2 +0x98EF +0x98F2 +0x98F4 +0x98FC +0x98FD +0x98FE +0x9905 +0x990A +0x990C +0x9910 +0x9913 +0x9928 +0x9957 +0x9996 +0x9999 +0x99A8 +0x99AC +0x99B3 +0x99B4 +0x99C1 +0x99C4 +0x99C5 +0x99C6 +0x99C8 +0x99D0 +0x99D2 +0x99D5 +0x99FF +0x9A0E +0x9A12 +0x9A13 +0x9A28 +0x9A30 +0x9A5A +0x9AA8 +0x9AB8 +0x9AC4 +0x9AD8 +0x9AEA +0x9AED +0x9B3C +0x9B41 +0x9B42 +0x9B45 +0x9B54 +0x9B5A +0x9B6F +0x9B8E +0x9B92 +0x9BAA +0x9BAB +0x9BAD +0x9BAE +0x9BC9 +0x9BD6 +0x9BDB +0x9BE8 +0x9BF5 +0x9C0D +0x9C10 +0x9C2D +0x9C2F +0x9C39 +0x9C3B +0x9C48 +0x9C52 +0x9C57 +0x9CE5 +0x9CE9 +0x9CF3 +0x9CF4 +0x9CF6 +0x9D07 +0x9D0E +0x9D1B +0x9D28 +0x9D2B +0x9D2C +0x9D3B +0x9D5C +0x9D60 +0x9D61 +0x9D6C +0x9D8F +0x9DB4 +0x9DF2 +0x9DF9 +0x9DFA +0x9E78 +0x9E7F +0x9E93 +0x9E97 +0x9E9F +0x9EA6 +0x9EB9 +0x9EBA +0x9EBB +0x9EBF +0x9EC4 +0x9ECD +0x9ED2 +0x9ED9 +0x9EDB +0x9F0E +0x9F13 +0x9F20 +0x9F3B +0x9F62 +0x9F8D diff --git a/font-build/unicodes-latin.txt b/font-build/unicodes-latin.txt new file mode 100644 index 0000000..caac114 --- /dev/null +++ b/font-build/unicodes-latin.txt @@ -0,0 +1 @@ +U+0020-007E,U+00A0-00FF,U+0401,U+0410-044F,U+0451 \ No newline at end of file diff --git a/font-build/unicodes.txt b/font-build/unicodes.txt new file mode 100644 index 0000000..f446c0c --- /dev/null +++ b/font-build/unicodes.txt @@ -0,0 +1 @@ +U+0020-007E,U+00A0-00FF,U+3040-309F,U+30A0-30FF,0x4E00,0x4E01,0x4E03,0x4E07,0x4E08,0x4E09,0x4E0A,0x4E0B,0x4E0D,0x4E0E,0x4E11,0x4E14,0x4E16,0x4E18,0x4E19,0x4E1E,0x4E21,0x4E26,0x4E2D,0x4E32,0x4E38,0x4E39,0x4E3B,0x4E43,0x4E45,0x4E4B,0x4E4D,0x4E4E,0x4E4F,0x4E57,0x4E59,0x4E5D,0x4E5E,0x4E5F,0x4E71,0x4E73,0x4E7E,0x4E80,0x4E86,0x4E88,0x4E89,0x4E8B,0x4E8C,0x4E91,0x4E92,0x4E94,0x4E95,0x4E98,0x4E99,0x4E9B,0x4E9C,0x4EA1,0x4EA4,0x4EA5,0x4EA6,0x4EA8,0x4EAB,0x4EAC,0x4EAD,0x4EAE,0x4EBA,0x4EC0,0x4EC1,0x4EC7,0x4ECA,0x4ECB,0x4ECF,0x4ED4,0x4ED5,0x4ED6,0x4ED8,0x4ED9,0x4EE3,0x4EE4,0x4EE5,0x4EEE,0x4EF0,0x4EF2,0x4EF6,0x4EFB,0x4F01,0x4F0A,0x4F0D,0x4F0E,0x4F0F,0x4F10,0x4F11,0x4F1A,0x4F1D,0x4F2F,0x4F34,0x4F36,0x4F38,0x4F3A,0x4F3C,0x4F3D,0x4F43,0x4F46,0x4F4D,0x4F4E,0x4F4F,0x4F50,0x4F51,0x4F53,0x4F55,0x4F59,0x4F5C,0x4F73,0x4F75,0x4F7C,0x4F7F,0x4F83,0x4F8B,0x4F8D,0x4F9B,0x4F9D,0x4FA0,0x4FA1,0x4FAD,0x4FAE,0x4FAF,0x4FB5,0x4FB6,0x4FBF,0x4FC2,0x4FC3,0x4FC4,0x4FCA,0x4FD7,0x4FDD,0x4FE1,0x4FE3,0x4FEE,0x4FF3,0x4FF5,0x4FF8,0x4FFA,0x5009,0x500B,0x500D,0x5012,0x5016,0x5019,0x501F,0x5023,0x5024,0x5026,0x502B,0x502D,0x5036,0x5039,0x5049,0x504F,0x505C,0x5065,0x5072,0x5074,0x5075,0x5076,0x507D,0x508D,0x5091,0x5098,0x5099,0x50AC,0x50AD,0x50B5,0x50B7,0x50BE,0x50C5,0x50CD,0x50CF,0x50D1,0x50D5,0x50DA,0x50E7,0x50FB,0x5100,0x5104,0x5112,0x511F,0x512A,0x5132,0x5141,0x5143,0x5144,0x5145,0x5146,0x5147,0x5148,0x5149,0x514B,0x514D,0x514E,0x5150,0x515A,0x515C,0x5165,0x5168,0x516B,0x516C,0x516D,0x5171,0x5175,0x5176,0x5177,0x5178,0x517C,0x5185,0x5186,0x518A,0x518D,0x5192,0x5197,0x5199,0x51A0,0x51A5,0x51A8,0x51AC,0x51B4,0x51B6,0x51B7,0x51C4,0x51C6,0x51CB,0x51CC,0x51CD,0x51DD,0x51E1,0x51E6,0x51E7,0x51EA,0x51F1,0x51F6,0x51F8,0x51F9,0x51FA,0x51FD,0x5200,0x5203,0x5206,0x5207,0x5208,0x520A,0x5211,0x5217,0x521D,0x5224,0x5225,0x5229,0x5230,0x5236,0x5237,0x5238,0x523A,0x523B,0x5243,0x5247,0x524A,0x524D,0x5256,0x525B,0x5263,0x5264,0x5265,0x526F,0x5270,0x5272,0x5275,0x5283,0x5287,0x5289,0x529B,0x529F,0x52A0,0x52A3,0x52A9,0x52AA,0x52AB,0x52B1,0x52B4,0x52B9,0x52BE,0x52C3,0x52C5,0x52C7,0x52C9,0x52D5,0x52D8,0x52D9,0x52DD,0x52DF,0x52E2,0x52E4,0x52E7,0x52F2,0x52FA,0x52FE,0x52FF,0x5301,0x5302,0x5305,0x5316,0x5317,0x5319,0x531D,0x5320,0x5321,0x532A,0x5339,0x533A,0x533B,0x533F,0x5341,0x5343,0x5347,0x5348,0x534A,0x5351,0x5352,0x5353,0x5354,0x5357,0x5358,0x535A,0x535C,0x5360,0x5366,0x536F,0x5370,0x5371,0x5373,0x5374,0x5375,0x5378,0x537F,0x5384,0x5398,0x539A,0x539F,0x53A8,0x53A9,0x53AD,0x53B3,0x53BB,0x53C2,0x53C8,0x53C9,0x53CA,0x53CB,0x53CC,0x53CD,0x53CE,0x53D4,0x53D6,0x53D7,0x53D9,0x53DB,0x53E1,0x53E2,0x53E3,0x53E4,0x53E5,0x53E9,0x53EA,0x53EB,0x53EC,0x53EF,0x53F0,0x53F1,0x53F2,0x53F3,0x53F6,0x53F7,0x53F8,0x5403,0x5404,0x5408,0x5409,0x540A,0x540B,0x540C,0x540D,0x540E,0x540F,0x5410,0x5411,0x541B,0x541F,0x5420,0x5426,0x542B,0x5438,0x5439,0x543B,0x543E,0x5442,0x5446,0x5448,0x5449,0x544A,0x5451,0x5468,0x546A,0x5473,0x547C,0x547D,0x548B,0x548C,0x54B2,0x54B3,0x54BD,0x54C0,0x54C1,0x54C9,0x54E1,0x54E8,0x54E9,0x54F2,0x5504,0x5506,0x5507,0x5510,0x5516,0x552F,0x5531,0x553E,0x5544,0x5546,0x554F,0x5553,0x5584,0x5589,0x558B,0x559A,0x559C,0x559D,0x55A7,0x55AA,0x55AB,0x55AC,0x55B0,0x55B6,0x55E3,0x5606,0x5609,0x5617,0x5618,0x5629,0x5631,0x5642,0x564C,0x565B,0x5668,0x5674,0x5678,0x567A,0x5687,0x56A2,0x56DA,0x56DB,0x56DE,0x56E0,0x56E3,0x56F0,0x56F2,0x56F3,0x56FA,0x56FD,0x5703,0x570F,0x5712,0x571F,0x5727,0x5728,0x572D,0x5730,0x5742,0x5747,0x574A,0x5750,0x5751,0x5764,0x5766,0x576A,0x5782,0x578B,0x57A2,0x57A3,0x57CB,0x57CE,0x57DC,0x57DF,0x57E0,0x57F4,0x57F7,0x57F9,0x57FA,0x57FC,0x5800,0x5802,0x5805,0x5806,0x5815,0x5824,0x582A,0x5830,0x5831,0x5834,0x5835,0x583A,0x5840,0x5841,0x584A,0x5851,0x5854,0x5857,0x5858,0x5859,0x585A,0x585E,0x5869,0x586B,0x5875,0x587E,0x5883,0x5893,0x5897,0x589C,0x58A8,0x58B3,0x58BE,0x58C1,0x58C7,0x58CA,0x58CC,0x58D5,0x58EB,0x58EC,0x58EE,0x58F0,0x58F1,0x58F2,0x58F7,0x5909,0x590F,0x5915,0x5916,0x5919,0x591A,0x591C,0x5922,0x5927,0x5929,0x592A,0x592B,0x592E,0x5931,0x5937,0x5944,0x5947,0x5948,0x5949,0x594F,0x5951,0x5954,0x5957,0x5965,0x5968,0x596A,0x596E,0x5973,0x5974,0x597D,0x5982,0x5983,0x5984,0x598A,0x5993,0x5996,0x5999,0x59A5,0x59A8,0x59AC,0x59B9,0x59BB,0x59BE,0x59C9,0x59CB,0x59D0,0x59D1,0x59D3,0x59D4,0x59E5,0x59E6,0x59EA,0x59EB,0x59F6,0x59FB,0x59FF,0x5A01,0x5A03,0x5A18,0x5A20,0x5A29,0x5A2F,0x5A3C,0x5A41,0x5A46,0x5A5A,0x5A66,0x5A7F,0x5A92,0x5A9B,0x5AC1,0x5AC9,0x5ACC,0x5AE1,0x5B09,0x5B22,0x5B2C,0x5B30,0x5B50,0x5B54,0x5B57,0x5B58,0x5B5C,0x5B5D,0x5B5F,0x5B63,0x5B64,0x5B66,0x5B6B,0x5B85,0x5B87,0x5B88,0x5B89,0x5B8B,0x5B8C,0x5B8D,0x5B8F,0x5B95,0x5B97,0x5B98,0x5B99,0x5B9A,0x5B9B,0x5B9C,0x5B9D,0x5B9F,0x5BA2,0x5BA3,0x5BA4,0x5BA5,0x5BAE,0x5BB0,0x5BB3,0x5BB4,0x5BB5,0x5BB6,0x5BB9,0x5BBF,0x5BC2,0x5BC4,0x5BC5,0x5BC6,0x5BCC,0x5BD2,0x5BD3,0x5BDB,0x5BDD,0x5BDF,0x5BE1,0x5BE7,0x5BE9,0x5BEE,0x5BF5,0x5BF8,0x5BFA,0x5BFE,0x5BFF,0x5C01,0x5C02,0x5C04,0x5C06,0x5C09,0x5C0A,0x5C0B,0x5C0E,0x5C0F,0x5C11,0x5C16,0x5C1A,0x5C24,0x5C2D,0x5C31,0x5C3A,0x5C3B,0x5C3C,0x5C3D,0x5C3E,0x5C3F,0x5C40,0x5C45,0x5C48,0x5C4A,0x5C4B,0x5C4D,0x5C51,0x5C55,0x5C5E,0x5C60,0x5C61,0x5C64,0x5C65,0x5C6F,0x5C71,0x5C90,0x5CA1,0x5CA8,0x5CA9,0x5CAC,0x5CB1,0x5CB3,0x5CB8,0x5CE0,0x5CE1,0x5CE8,0x5CEF,0x5CF0,0x5CF6,0x5CFB,0x5D07,0x5D0E,0x5D16,0x5D29,0x5D50,0x5D69,0x5D6F,0x5D8B,0x5DBA,0x5DCC,0x5DDD,0x5DDE,0x5DE1,0x5DE3,0x5DE5,0x5DE6,0x5DE7,0x5DE8,0x5DEE,0x5DF1,0x5DF3,0x5DF4,0x5DF7,0x5DFB,0x5DFD,0x5DFE,0x5E02,0x5E03,0x5E06,0x5E0C,0x5E16,0x5E1D,0x5E25,0x5E2B,0x5E2D,0x5E2F,0x5E30,0x5E33,0x5E38,0x5E3D,0x5E45,0x5E4C,0x5E55,0x5E61,0x5E63,0x5E72,0x5E73,0x5E74,0x5E78,0x5E79,0x5E7B,0x5E7C,0x5E7D,0x5E7E,0x5E81,0x5E83,0x5E84,0x5E87,0x5E8A,0x5E8F,0x5E95,0x5E96,0x5E97,0x5E9A,0x5E9C,0x5EA6,0x5EA7,0x5EAB,0x5EAD,0x5EB5,0x5EB6,0x5EB7,0x5EB8,0x5EC3,0x5EC9,0x5ECA,0x5ED3,0x5EDF,0x5EE0,0x5EF6,0x5EF7,0x5EFA,0x5EFB,0x5EFC,0x5EFF,0x5F01,0x5F04,0x5F0A,0x5F0F,0x5F10,0x5F13,0x5F14,0x5F15,0x5F17,0x5F18,0x5F1B,0x5F1F,0x5F25,0x5F26,0x5F27,0x5F31,0x5F35,0x5F37,0x5F3C,0x5F3E,0x5F4A,0x5F53,0x5F62,0x5F66,0x5F69,0x5F6A,0x5F6B,0x5F6C,0x5F70,0x5F71,0x5F79,0x5F7C,0x5F80,0x5F81,0x5F84,0x5F85,0x5F8B,0x5F8C,0x5F90,0x5F92,0x5F93,0x5F97,0x5FA1,0x5FA9,0x5FAA,0x5FAE,0x5FB3,0x5FB4,0x5FB9,0x5FBD,0x5FC3,0x5FC5,0x5FCC,0x5FCD,0x5FD7,0x5FD8,0x5FD9,0x5FDC,0x5FE0,0x5FEB,0x5FF5,0x5FFD,0x6012,0x6016,0x601C,0x601D,0x6020,0x6025,0x6027,0x6028,0x602A,0x602F,0x604B,0x6050,0x6052,0x6055,0x6062,0x6065,0x6068,0x6069,0x606D,0x606F,0x6070,0x6075,0x6089,0x608C,0x6094,0x609F,0x60A0,0x60A3,0x60A6,0x60A9,0x60AA,0x60B2,0x60B6,0x60BC,0x60C5,0x60C7,0x60D1,0x60DA,0x60DC,0x60DF,0x60E3,0x60E8,0x60F0,0x60F3,0x60F9,0x6101,0x6108,0x6109,0x610F,0x611A,0x611B,0x611F,0x6148,0x614B,0x614C,0x614E,0x6155,0x6162,0x6163,0x6167,0x6168,0x616E,0x6170,0x6176,0x617E,0x6182,0x618E,0x6190,0x61A4,0x61A7,0x61A9,0x61B2,0x61B6,0x61BE,0x61C7,0x61D0,0x61F2,0x61F8,0x620A,0x620E,0x6210,0x6211,0x6212,0x6216,0x621A,0x621F,0x6226,0x622F,0x6234,0x6238,0x623B,0x623F,0x6240,0x6247,0x6249,0x624B,0x624D,0x6253,0x6255,0x6258,0x626E,0x6271,0x6276,0x6279,0x627F,0x6280,0x6284,0x628A,0x6291,0x6295,0x6297,0x6298,0x629C,0x629E,0x62AB,0x62B1,0x62B5,0x62B9,0x62BC,0x62BD,0x62C5,0x62CD,0x62D0,0x62D2,0x62D3,0x62D8,0x62D9,0x62DB,0x62DD,0x62E0,0x62E1,0x62EC,0x62ED,0x62F3,0x62F6,0x62F7,0x62FE,0x6301,0x6307,0x6309,0x6311,0x6319,0x631F,0x6328,0x632B,0x632F,0x633A,0x633D,0x633F,0x6349,0x634C,0x6355,0x6357,0x635C,0x6367,0x6368,0x636E,0x6372,0x6377,0x637A,0x637B,0x6383,0x6388,0x638C,0x6392,0x6398,0x639B,0x63A0,0x63A1,0x63A2,0x63A5,0x63A7,0x63A8,0x63A9,0x63AA,0x63AC,0x63B2,0x63B4,0x63BB,0x63C3,0x63CF,0x63D0,0x63D6,0x63DA,0x63DB,0x63E1,0x63EE,0x63F4,0x63FA,0x640D,0x642C,0x642D,0x643A,0x643E,0x6442,0x6458,0x6469,0x6478,0x647A,0x6483,0x6492,0x649A,0x649E,0x64A4,0x64AB,0x64AD,0x64AE,0x64B0,0x64B2,0x64B9,0x64C1,0x64CD,0x64E2,0x64E6,0x64EC,0x64FE,0x652F,0x6539,0x653B,0x653E,0x653F,0x6545,0x654F,0x6551,0x6557,0x6559,0x6562,0x6563,0x6566,0x656C,0x6570,0x6574,0x6575,0x6577,0x6587,0x6589,0x658C,0x658E,0x6590,0x6591,0x6597,0x6599,0x659C,0x65A1,0x65A4,0x65A5,0x65A7,0x65AC,0x65AD,0x65AF,0x65B0,0x65B9,0x65BC,0x65BD,0x65C5,0x65CB,0x65CF,0x65D7,0x65E2,0x65E5,0x65E6,0x65E7,0x65E8,0x65E9,0x65EC,0x65ED,0x65FA,0x6602,0x6606,0x6607,0x660C,0x660E,0x660F,0x6613,0x6614,0x661F,0x6620,0x6625,0x6627,0x6628,0x662D,0x662F,0x663C,0x6642,0x6643,0x664B,0x6652,0x6666,0x6669,0x666E,0x666F,0x6674,0x6676,0x667A,0x6681,0x6687,0x6691,0x6696,0x6697,0x66A2,0x66A6,0x66AB,0x66AE,0x66B4,0x66C7,0x66D9,0x66DC,0x66DD,0x66F2,0x66F3,0x66F4,0x66F8,0x66F9,0x66FD,0x66FE,0x66FF,0x6700,0x6708,0x6709,0x670B,0x670D,0x6714,0x6715,0x6717,0x671B,0x671D,0x671F,0x6728,0x672A,0x672B,0x672C,0x672D,0x6731,0x6734,0x673A,0x673D,0x6749,0x674E,0x674F,0x6750,0x6751,0x6753,0x6756,0x675C,0x675F,0x6761,0x6762,0x6765,0x676D,0x676F,0x6771,0x6775,0x6777,0x677E,0x677F,0x6787,0x6790,0x6795,0x6797,0x679A,0x679C,0x679D,0x67A0,0x67A2,0x67AF,0x67B6,0x67C1,0x67C4,0x67CA,0x67CF,0x67D0,0x67D1,0x67D3,0x67D4,0x67D8,0x67DA,0x67F1,0x67F3,0x67F4,0x67F5,0x67FB,0x67FE,0x67FF,0x6802,0x6803,0x6804,0x6813,0x6816,0x6817,0x6821,0x6822,0x682A,0x6834,0x6838,0x6839,0x683C,0x683D,0x6841,0x6842,0x6843,0x6848,0x6850,0x6851,0x6853,0x6854,0x685C,0x685D,0x685F,0x6867,0x6876,0x6881,0x6885,0x6893,0x6897,0x68A2,0x68A7,0x68A8,0x68AF,0x68B0,0x68B1,0x68B6,0x68BC,0x68C4,0x68C9,0x68CB,0x68D2,0x68DA,0x68DF,0x68EE,0x68F2,0x68FA,0x6900,0x6905,0x690B,0x690D,0x690E,0x6919,0x691B,0x691C,0x6934,0x693F,0x694A,0x6953,0x6955,0x695A,0x6960,0x6962,0x696D,0x696F,0x6973,0x6975,0x697C,0x697D,0x6982,0x698A,0x698E,0x6994,0x699B,0x69CB,0x69CC,0x69CD,0x69D8,0x69D9,0x69FB,0x69FD,0x6A0B,0x6A17,0x6A19,0x6A1F,0x6A21,0x6A29,0x6A2A,0x6A2B,0x6A35,0x6A39,0x6A3A,0x6A3D,0x6A4B,0x6A58,0x6A5F,0x6A61,0x6A7F,0x6A80,0x6A8E,0x6AD3,0x6ADB,0x6AE8,0x6B04,0x6B1D,0x6B20,0x6B21,0x6B23,0x6B27,0x6B32,0x6B3A,0x6B3D,0x6B3E,0x6B4C,0x6B4E,0x6B53,0x6B62,0x6B63,0x6B64,0x6B66,0x6B69,0x6B6A,0x6B6F,0x6B73,0x6B74,0x6B7B,0x6B86,0x6B89,0x6B8A,0x6B8B,0x6B96,0x6BB4,0x6BB5,0x6BBA,0x6BBB,0x6BBF,0x6BC5,0x6BCD,0x6BCE,0x6BD2,0x6BD4,0x6BD8,0x6BDB,0x6C0F,0x6C11,0x6C17,0x6C34,0x6C37,0x6C38,0x6C3E,0x6C40,0x6C41,0x6C42,0x6C4E,0x6C50,0x6C57,0x6C5A,0x6C5D,0x6C5F,0x6C60,0x6C70,0x6C72,0x6C7A,0x6C7D,0x6C83,0x6C88,0x6C8C,0x6C93,0x6C96,0x6C99,0x6CA1,0x6CA2,0x6CAB,0x6CB3,0x6CB8,0x6CB9,0x6CBB,0x6CBC,0x6CBF,0x6CC1,0x6CC9,0x6CCA,0x6CCC,0x6CD5,0x6CE1,0x6CE2,0x6CE3,0x6CE5,0x6CE8,0x6CF0,0x6CF3,0x6D0B,0x6D17,0x6D1B,0x6D1E,0x6D25,0x6D29,0x6D2A,0x6D32,0x6D3B,0x6D3E,0x6D41,0x6D44,0x6D45,0x6D5C,0x6D66,0x6D69,0x6D6A,0x6D6C,0x6D6E,0x6D74,0x6D77,0x6D78,0x6D88,0x6D8C,0x6D99,0x6D9B,0x6D9C,0x6DAF,0x6DB2,0x6DBC,0x6DC0,0x6DCB,0x6DD1,0x6DD8,0x6DE1,0x6DEB,0x6DF1,0x6DF3,0x6DF5,0x6DF7,0x6DFB,0x6E05,0x6E07,0x6E08,0x6E09,0x6E0B,0x6E13,0x6E1A,0x6E1B,0x6E20,0x6E21,0x6E25,0x6E26,0x6E29,0x6E2C,0x6E2F,0x6E4A,0x6E56,0x6E58,0x6E5B,0x6E67,0x6E6F,0x6E7E,0x6E7F,0x6E80,0x6E8C,0x6E90,0x6E96,0x6E9C,0x6E9D,0x6EA2,0x6EB6,0x6EBA,0x6EC5,0x6ECB,0x6ED1,0x6EDD,0x6EDE,0x6EF4,0x6F01,0x6F02,0x6F06,0x6F09,0x6F0F,0x6F14,0x6F15,0x6F20,0x6F22,0x6F23,0x6F2B,0x6F2C,0x6F38,0x6F45,0x6F54,0x6F5C,0x6F5F,0x6F64,0x6F6E,0x6F70,0x6F84,0x6F97,0x6FB1,0x6FC0,0x6FC1,0x6FC3,0x6FE0,0x6FE1,0x6FEB,0x6FEF,0x7015,0x701E,0x7026,0x7027,0x702C,0x7058,0x706B,0x706F,0x7070,0x7078,0x707C,0x707D,0x7089,0x708A,0x708E,0x70AD,0x70B9,0x70BA,0x70C8,0x70CF,0x70F9,0x7114,0x711A,0x7121,0x7126,0x7136,0x713C,0x7149,0x714E,0x7159,0x7164,0x7167,0x7169,0x716E,0x717D,0x718A,0x7194,0x719F,0x71B1,0x71C3,0x71C8,0x71D0,0x71D5,0x71E5,0x71E6,0x71ED,0x7206,0x722A,0x7235,0x7236,0x723A,0x723D,0x723E,0x7247,0x7248,0x724C,0x7252,0x7259,0x725B,0x725D,0x725F,0x7261,0x7262,0x7267,0x7269,0x7272,0x7279,0x727D,0x7280,0x72A0,0x72AC,0x72AF,0x72B6,0x72C2,0x72D0,0x72D7,0x72D9,0x72DB,0x72E9,0x72EC,0x72ED,0x72F8,0x72FC,0x72FD,0x731B,0x731F,0x732A,0x732B,0x732E,0x7336,0x7337,0x733F,0x7344,0x7345,0x7363,0x7372,0x7384,0x7387,0x7389,0x738B,0x7396,0x73A9,0x73B2,0x73C2,0x73CA,0x73CD,0x73E0,0x73EA,0x73ED,0x73FE,0x7403,0x7406,0x7409,0x7422,0x7433,0x7434,0x7435,0x7436,0x745A,0x745B,0x745E,0x7460,0x7473,0x7483,0x74B0,0x74BD,0x74DC,0x74E2,0x74E6,0x74F6,0x7511,0x7518,0x751A,0x751C,0x751F,0x7523,0x7525,0x7528,0x752B,0x7530,0x7531,0x7532,0x7533,0x7537,0x753A,0x753B,0x754C,0x754F,0x7551,0x7554,0x7559,0x755C,0x755D,0x7560,0x7562,0x7565,0x7566,0x756A,0x7570,0x7573,0x7577,0x757F,0x758B,0x758E,0x758F,0x7591,0x75AB,0x75B2,0x75B9,0x75BE,0x75C5,0x75C7,0x75D4,0x75D5,0x75D8,0x75DB,0x75E2,0x75E9,0x75F4,0x7642,0x764C,0x7652,0x7656,0x767A,0x767B,0x767D,0x767E,0x7684,0x7686,0x7687,0x7690,0x76AE,0x76BF,0x76C3,0x76C6,0x76C8,0x76CA,0x76D7,0x76DB,0x76DF,0x76E3,0x76E4,0x76EE,0x76F2,0x76F4,0x76F8,0x76FE,0x7701,0x7709,0x770B,0x770C,0x771F,0x7720,0x773A,0x773C,0x7740,0x7761,0x7763,0x7766,0x77A5,0x77AC,0x77AD,0x77B3,0x77DB,0x77E2,0x77E5,0x77E7,0x77E9,0x77ED,0x77EF,0x77F3,0x7802,0x7814,0x7815,0x7825,0x7826,0x7827,0x7832,0x7834,0x783A,0x783F,0x785D,0x786B,0x786C,0x786F,0x7872,0x7881,0x7887,0x788D,0x7891,0x7893,0x7895,0x7897,0x78A7,0x78A9,0x78BA,0x78C1,0x78D0,0x78E8,0x78EF,0x7901,0x790E,0x793A,0x793C,0x793E,0x7941,0x7947,0x7948,0x7949,0x7950,0x7956,0x795D,0x795E,0x7962,0x7965,0x7968,0x796D,0x7977,0x7981,0x7984,0x7985,0x798D,0x798E,0x798F,0x79A6,0x79B0,0x79BD,0x79BE,0x79BF,0x79C0,0x79C1,0x79CB,0x79D1,0x79D2,0x79D8,0x79DF,0x79E4,0x79E6,0x79E9,0x79F0,0x79FB,0x7A00,0x7A0B,0x7A0E,0x7A14,0x7A17,0x7A1A,0x7A1C,0x7A2E,0x7A32,0x7A3C,0x7A3D,0x7A3F,0x7A40,0x7A42,0x7A46,0x7A4D,0x7A4E,0x7A4F,0x7A50,0x7A63,0x7A6B,0x7A74,0x7A76,0x7A7A,0x7A7F,0x7A81,0x7A83,0x7A84,0x7A92,0x7A93,0x7A9F,0x7AAA,0x7AAE,0x7AAF,0x7ABA,0x7AC3,0x7ACB,0x7ADC,0x7AE0,0x7AE3,0x7AE5,0x7AEA,0x7AEF,0x7AF6,0x7AF9,0x7AFA,0x7AFF,0x7B08,0x7B11,0x7B1B,0x7B20,0x7B25,0x7B26,0x7B2C,0x7B39,0x7B46,0x7B48,0x7B49,0x7B4B,0x7B4F,0x7B51,0x7B52,0x7B54,0x7B56,0x7B86,0x7B87,0x7B94,0x7B95,0x7B97,0x7BA1,0x7BAA,0x7BAD,0x7BB1,0x7BB8,0x7BC0,0x7BC4,0x7BC7,0x7BC9,0x7BE0,0x7BE4,0x7BED,0x7C21,0x7C38,0x7C3E,0x7C3F,0x7C4D,0x7C73,0x7C7E,0x7C81,0x7C82,0x7C89,0x7C8B,0x7C8D,0x7C92,0x7C95,0x7C97,0x7C98,0x7C9B,0x7C9F,0x7CA5,0x7CA7,0x7CBE,0x7CCA,0x7CCE,0x7CD6,0x7CDE,0x7CDF,0x7CE0,0x7CE7,0x7CF8,0x7CFB,0x7CFE,0x7D00,0x7D04,0x7D05,0x7D0B,0x7D0D,0x7D10,0x7D14,0x7D17,0x7D18,0x7D19,0x7D1A,0x7D1B,0x7D20,0x7D21,0x7D22,0x7D2B,0x7D2C,0x7D2F,0x7D30,0x7D33,0x7D39,0x7D3A,0x7D42,0x7D43,0x7D44,0x7D4C,0x7D50,0x7D5E,0x7D61,0x7D62,0x7D66,0x7D71,0x7D75,0x7D76,0x7D79,0x7D99,0x7D9A,0x7D9C,0x7DAC,0x7DAD,0x7DB1,0x7DB2,0x7DB4,0x7DBB,0x7DBE,0x7DBF,0x7DCA,0x7DCB,0x7DCF,0x7DD1,0x7DD2,0x7DDA,0x7DE0,0x7DE8,0x7DE9,0x7DEC,0x7DEF,0x7DF4,0x7E01,0x7E04,0x7E1B,0x7E1E,0x7E26,0x7E2B,0x7E2E,0x7E3E,0x7E41,0x7E4A,0x7E4B,0x7E4D,0x7E54,0x7E55,0x7E6D,0x7E70,0x7E82,0x7E8F,0x7F36,0x7F6A,0x7F6B,0x7F6E,0x7F70,0x7F72,0x7F75,0x7F77,0x7F85,0x7F8A,0x7F8E,0x7FA4,0x7FA8,0x7FA9,0x7FBD,0x7FC1,0x7FCC,0x7FD2,0x7FE0,0x7FEB,0x7FF0,0x7FFB,0x7FFC,0x8000,0x8001,0x8003,0x8005,0x800C,0x8010,0x8015,0x8017,0x8033,0x8036,0x803D,0x8056,0x805E,0x8061,0x806F,0x8074,0x8077,0x807E,0x8087,0x8089,0x808B,0x808C,0x8096,0x8098,0x809D,0x80A1,0x80A2,0x80A5,0x80A9,0x80AA,0x80AF,0x80B1,0x80B2,0x80B4,0x80BA,0x80C3,0x80C6,0x80CC,0x80CE,0x80DE,0x80E1,0x80E4,0x80F4,0x80F8,0x80FD,0x8102,0x8105,0x8106,0x8107,0x8108,0x810A,0x811A,0x8131,0x8133,0x8139,0x814E,0x8150,0x8154,0x8155,0x816B,0x8170,0x8178,0x8179,0x817A,0x817F,0x818F,0x819A,0x819C,0x819D,0x81A8,0x81B3,0x81BF,0x81C6,0x81D3,0x81E3,0x81E5,0x81E8,0x81EA,0x81ED,0x81F3,0x81F4,0x81FC,0x8208,0x820C,0x820E,0x8217,0x8218,0x821B,0x821C,0x821E,0x821F,0x822A,0x822C,0x8235,0x8236,0x8237,0x8239,0x8247,0x8266,0x826E,0x826F,0x8272,0x8276,0x828B,0x8299,0x829D,0x82A5,0x82A6,0x82AD,0x82AF,0x82B1,0x82B3,0x82B8,0x82B9,0x82BD,0x82C5,0x82D1,0x82D3,0x82D4,0x82D7,0x82DB,0x82E5,0x82E6,0x82E7,0x82EB,0x82F1,0x8302,0x8304,0x8305,0x830E,0x831C,0x8328,0x8336,0x8338,0x8349,0x834A,0x834F,0x8352,0x8358,0x8377,0x837B,0x839E,0x83AB,0x83B1,0x83C5,0x83CA,0x83CC,0x83D3,0x83D6,0x83DC,0x83DF,0x83E9,0x83EF,0x83F0,0x83F1,0x8404,0x840C,0x840E,0x8429,0x8431,0x843D,0x8449,0x844E,0x8457,0x845B,0x8461,0x8463,0x8466,0x846C,0x8471,0x8475,0x847A,0x848B,0x8490,0x8494,0x8499,0x849C,0x84B2,0x84B8,0x84BC,0x84C4,0x84C9,0x84CB,0x84D1,0x84EC,0x84EE,0x8500,0x8511,0x8513,0x851A,0x8526,0x852D,0x8535,0x853D,0x8543,0x8549,0x854A,0x854E,0x8557,0x8568,0x8569,0x856A,0x8584,0x8597,0x8599,0x85A6,0x85A9,0x85AA,0x85AB,0x85AC,0x85AE,0x85AF,0x85C1,0x85CD,0x85E4,0x85E9,0x85F7,0x85FB,0x8607,0x862D,0x864E,0x8650,0x865A,0x865C,0x865E,0x866B,0x8679,0x867B,0x868A,0x8695,0x86A4,0x86C7,0x86CB,0x86CD,0x86CE,0x86D9,0x86E4,0x86ED,0x86EE,0x86F8,0x86FE,0x8702,0x8718,0x871C,0x8749,0x874B,0x8755,0x8766,0x8776,0x877F,0x878D,0x87BA,0x87F9,0x87FB,0x8840,0x8846,0x884C,0x8853,0x8857,0x885B,0x885D,0x8861,0x8863,0x8868,0x8870,0x8877,0x887F,0x8888,0x888B,0x8896,0x88AB,0x88B4,0x88B7,0x88C1,0x88C2,0x88C5,0x88CF,0x88D5,0x88DC,0x88DF,0x88E1,0x88F3,0x88F8,0x88FD,0x88FE,0x8907,0x8910,0x8912,0x8956,0x895F,0x8972,0x897F,0x8981,0x8986,0x8987,0x898B,0x898F,0x8996,0x8997,0x899A,0x89A7,0x89AA,0x89B3,0x89D2,0x89E3,0x89E6,0x8A00,0x8A02,0x8A08,0x8A0A,0x8A0E,0x8A13,0x8A17,0x8A18,0x8A1F,0x8A23,0x8A2A,0x8A2D,0x8A31,0x8A33,0x8A34,0x8A3A,0x8A3B,0x8A3C,0x8A50,0x8A51,0x8A54,0x8A55,0x8A5E,0x8A60,0x8A63,0x8A66,0x8A69,0x8A6B,0x8A6E,0x8A70,0x8A71,0x8A72,0x8A73,0x8A87,0x8A89,0x8A8C,0x8A8D,0x8A93,0x8A95,0x8A98,0x8A9E,0x8AA0,0x8AA4,0x8AAC,0x8AAD,0x8AB0,0x8AB2,0x8AB9,0x8ABC,0x8ABF,0x8AC7,0x8ACB,0x8ACC,0x8ACF,0x8AD2,0x8AD6,0x8ADC,0x8AE6,0x8AED,0x8AEE,0x8AF8,0x8AFA,0x8AFE,0x8B00,0x8B01,0x8B02,0x8B04,0x8B0E,0x8B19,0x8B1B,0x8B1D,0x8B21,0x8B2C,0x8B39,0x8B58,0x8B5C,0x8B66,0x8B70,0x8B72,0x8B77,0x8B83,0x8B90,0x8C37,0x8C46,0x8C4A,0x8C5A,0x8C61,0x8C6A,0x8C79,0x8C8C,0x8C9D,0x8C9E,0x8CA0,0x8CA1,0x8CA2,0x8CA7,0x8CA8,0x8CA9,0x8CAB,0x8CAC,0x8CAF,0x8CB0,0x8CB4,0x8CB7,0x8CB8,0x8CBB,0x8CBC,0x8CBF,0x8CC0,0x8CC2,0x8CC3,0x8CC4,0x8CC7,0x8CCA,0x8CCE,0x8CD1,0x8CD3,0x8CDB,0x8CDC,0x8CDE,0x8CE0,0x8CE2,0x8CE6,0x8CEA,0x8CED,0x8CFC,0x8D08,0x8D0B,0x8D64,0x8D66,0x8D6B,0x8D70,0x8D74,0x8D77,0x8D85,0x8D8A,0x8DA3,0x8DA8,0x8DB3,0x8DDD,0x8DE1,0x8DE8,0x8DEF,0x8DF3,0x8DF5,0x8E0A,0x8E0F,0x8E44,0x8E5F,0x8E74,0x8E8D,0x8EAB,0x8EAF,0x8ECA,0x8ECC,0x8ECD,0x8ED2,0x8EDF,0x8EE2,0x8EF8,0x8EFD,0x8F03,0x8F09,0x8F14,0x8F1D,0x8F29,0x8F2A,0x8F2F,0x8F38,0x8F3F,0x8F44,0x8F4D,0x8F5F,0x8F61,0x8F9B,0x8F9E,0x8FB0,0x8FB1,0x8FB2,0x8FBA,0x8FBB,0x8FBC,0x8FBF,0x8FC2,0x8FC4,0x8FC5,0x8FCE,0x8FD1,0x8FD4,0x8FE6,0x8FE9,0x8FEB,0x8FED,0x8FF0,0x8FF7,0x8FFD,0x9000,0x9001,0x9003,0x9006,0x900F,0x9010,0x9013,0x9014,0x9017,0x9019,0x901A,0x901D,0x901F,0x9020,0x9022,0x9023,0x902E,0x9031,0x9032,0x9038,0x903C,0x9041,0x9042,0x9045,0x9047,0x904A,0x904B,0x904D,0x904E,0x9053,0x9054,0x9055,0x905C,0x9060,0x9061,0x9063,0x9065,0x9069,0x906D,0x906E,0x9075,0x9077,0x9078,0x907A,0x907C,0x907F,0x9084,0x9091,0x90A3,0x90A6,0x90AA,0x90B8,0x90C1,0x90CA,0x90CE,0x90E1,0x90E8,0x90ED,0x90F5,0x90F7,0x90FD,0x912D,0x9149,0x914B,0x914C,0x914D,0x914E,0x9152,0x9154,0x9162,0x916A,0x916C,0x9175,0x9177,0x9178,0x9187,0x918D,0x9190,0x9192,0x9197,0x919C,0x91A4,0x91B8,0x91C6,0x91C7,0x91C8,0x91CC,0x91CD,0x91CE,0x91CF,0x91D1,0x91D8,0x91DC,0x91DD,0x91E3,0x91E6,0x91E7,0x920D,0x920E,0x9234,0x9237,0x9244,0x925B,0x9262,0x9266,0x9271,0x927E,0x9280,0x9283,0x9285,0x9291,0x9298,0x929A,0x92AD,0x92D2,0x92E4,0x92EA,0x92ED,0x92F2,0x92F3,0x92F8,0x92FC,0x9306,0x9310,0x9318,0x9320,0x9326,0x9328,0x932B,0x932C,0x932F,0x9332,0x934B,0x934D,0x9354,0x935B,0x936C,0x9375,0x937E,0x938C,0x9396,0x9397,0x939A,0x93A7,0x93AE,0x93D1,0x93E1,0x9418,0x9419,0x9438,0x9451,0x9453,0x9577,0x9580,0x9583,0x9589,0x958B,0x958F,0x9591,0x9593,0x95A2,0x95A3,0x95A4,0x95A5,0x95B2,0x95C7,0x95D8,0x961C,0x962A,0x9632,0x963B,0x963F,0x9640,0x9644,0x964D,0x9650,0x965B,0x9662,0x9663,0x9664,0x9665,0x966A,0x9670,0x9673,0x9675,0x9676,0x9678,0x967A,0x967D,0x9685,0x9686,0x9688,0x968A,0x968E,0x968F,0x9694,0x9699,0x969B,0x969C,0x96A0,0x96A3,0x96B7,0x96BB,0x96BC,0x96C0,0x96C1,0x96C4,0x96C5,0x96C6,0x96C7,0x96CC,0x96D1,0x96DB,0x96E2,0x96E3,0x96E8,0x96EA,0x96EB,0x96F0,0x96F2,0x96F6,0x96F7,0x96FB,0x9700,0x9707,0x970A,0x971C,0x971E,0x9727,0x9732,0x9752,0x9756,0x9759,0x975E,0x9762,0x9769,0x976D,0x9774,0x9784,0x978D,0x9798,0x97A0,0x97AD,0x97D3,0x97EE,0x97F3,0x97FB,0x97FF,0x9801,0x9802,0x9803,0x9805,0x9806,0x9808,0x9810,0x9811,0x9812,0x9813,0x9817,0x9818,0x981A,0x982C,0x982D,0x9834,0x983B,0x983C,0x984C,0x984D,0x984E,0x9854,0x9855,0x9858,0x985B,0x985E,0x9867,0x98A8,0x98DB,0x98DF,0x98E2,0x98EF,0x98F2,0x98F4,0x98FC,0x98FD,0x98FE,0x9905,0x990A,0x990C,0x9910,0x9913,0x9928,0x9957,0x9996,0x9999,0x99A8,0x99AC,0x99B3,0x99B4,0x99C1,0x99C4,0x99C5,0x99C6,0x99C8,0x99D0,0x99D2,0x99D5,0x99FF,0x9A0E,0x9A12,0x9A13,0x9A28,0x9A30,0x9A5A,0x9AA8,0x9AB8,0x9AC4,0x9AD8,0x9AEA,0x9AED,0x9B3C,0x9B41,0x9B42,0x9B45,0x9B54,0x9B5A,0x9B6F,0x9B8E,0x9B92,0x9BAA,0x9BAB,0x9BAD,0x9BAE,0x9BC9,0x9BD6,0x9BDB,0x9BE8,0x9BF5,0x9C0D,0x9C10,0x9C2D,0x9C2F,0x9C39,0x9C3B,0x9C48,0x9C52,0x9C57,0x9CE5,0x9CE9,0x9CF3,0x9CF4,0x9CF6,0x9D07,0x9D0E,0x9D1B,0x9D28,0x9D2B,0x9D2C,0x9D3B,0x9D5C,0x9D60,0x9D61,0x9D6C,0x9D8F,0x9DB4,0x9DF2,0x9DF9,0x9DFA,0x9E78,0x9E7F,0x9E93,0x9E97,0x9E9F,0x9EA6,0x9EB9,0x9EBA,0x9EBB,0x9EBF,0x9EC4,0x9ECD,0x9ED2,0x9ED9,0x9EDB,0x9F0E,0x9F13,0x9F20,0x9F3B,0x9F62,0x9F8D \ No newline at end of file diff --git a/mod/etc/options_menu/fonts/NotoSansJP-Latin-subset.ttf b/mod/etc/options_menu/fonts/NotoSansJP-Latin-subset.ttf new file mode 100644 index 0000000000000000000000000000000000000000..479414c3e5bacff34870f8bb09d6a70e7a9d249f GIT binary patch literal 42916 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 RFHGUP12T6KqsRyQe*o6h2j>6) literal 0 HcmV?d00001 diff --git a/mod/etc/options_menu/fonts/OFL.txt b/mod/etc/options_menu/fonts/OFL.txt new file mode 100644 index 0000000..1c9f432 --- /dev/null +++ b/mod/etc/options_menu/fonts/OFL.txt @@ -0,0 +1,93 @@ +Copyright 2014-2021 Adobe (http://www.adobe.com/), with Reserved Font Name 'Source' + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/mod/etc/options_menu/language/commands/c0010_English b/mod/etc/options_menu/language/commands/c0010_English index 9aa218e..852e729 100644 --- a/mod/etc/options_menu/language/commands/c0010_English +++ b/mod/etc/options_menu/language/commands/c0010_English @@ -1,4 +1,4 @@ -COMMAND_NAME=LANG_ENGLISH +COMMAND_NAME=English COMMAND_TYPE=EXTERNAL RESTART_UI=FALSE COMMAND_STR=echo en-US > /etc/options_menu/language.cfg && usleep 50000 && %options_path%/options --commandPath %options_path%/language/commands/ --title "LANGUAGE_OPTIONS" & diff --git a/mod/etc/options_menu/language/commands/c0040_French b/mod/etc/options_menu/language/commands/c0020_French similarity index 91% rename from mod/etc/options_menu/language/commands/c0040_French rename to mod/etc/options_menu/language/commands/c0020_French index f149dea..c3f0363 100644 --- a/mod/etc/options_menu/language/commands/c0040_French +++ b/mod/etc/options_menu/language/commands/c0020_French @@ -1,4 +1,4 @@ -COMMAND_NAME=LANG_FRENCH +COMMAND_NAME=Français COMMAND_TYPE=EXTERNAL RESTART_UI=FALSE COMMAND_STR=echo fr-FR > /etc/options_menu/language.cfg && usleep 50000 && %options_path%/options --commandPath %options_path%/language/commands/ --title "LANGUAGE_OPTIONS" & diff --git a/mod/etc/options_menu/language/commands/c0050_German b/mod/etc/options_menu/language/commands/c0030_German similarity index 91% rename from mod/etc/options_menu/language/commands/c0050_German rename to mod/etc/options_menu/language/commands/c0030_German index c87c2ed..d10440e 100644 --- a/mod/etc/options_menu/language/commands/c0050_German +++ b/mod/etc/options_menu/language/commands/c0030_German @@ -1,4 +1,4 @@ -COMMAND_NAME=LANG_GERMAN +COMMAND_NAME=Deutsch COMMAND_TYPE=EXTERNAL RESTART_UI=FALSE COMMAND_STR=echo de-DE > /etc/options_menu/language.cfg && usleep 50000 && %options_path%/options --commandPath %options_path%/language/commands/ --title "LANGUAGE_OPTIONS" & diff --git a/mod/etc/options_menu/language/commands/c0060_Spanish b/mod/etc/options_menu/language/commands/c0040_Spanish similarity index 91% rename from mod/etc/options_menu/language/commands/c0060_Spanish rename to mod/etc/options_menu/language/commands/c0040_Spanish index cd4dc01..1e83d7f 100644 --- a/mod/etc/options_menu/language/commands/c0060_Spanish +++ b/mod/etc/options_menu/language/commands/c0040_Spanish @@ -1,4 +1,4 @@ -COMMAND_NAME=LANG_SPANISH +COMMAND_NAME=Español COMMAND_TYPE=EXTERNAL RESTART_UI=FALSE COMMAND_STR=echo es-ES > /etc/options_menu/language.cfg && usleep 50000 && %options_path%/options --commandPath %options_path%/language/commands/ --title "LANGUAGE_OPTIONS" & diff --git a/mod/etc/options_menu/language/commands/c0070_Italian b/mod/etc/options_menu/language/commands/c0050_Italian similarity index 91% rename from mod/etc/options_menu/language/commands/c0070_Italian rename to mod/etc/options_menu/language/commands/c0050_Italian index 91f305e..c20b15d 100644 --- a/mod/etc/options_menu/language/commands/c0070_Italian +++ b/mod/etc/options_menu/language/commands/c0050_Italian @@ -1,4 +1,4 @@ -COMMAND_NAME=LANG_ITALIAN +COMMAND_NAME=Italiano COMMAND_TYPE=EXTERNAL RESTART_UI=FALSE COMMAND_STR=echo it-IT > /etc/options_menu/language.cfg && usleep 50000 && %options_path%/options --commandPath %options_path%/language/commands/ --title "LANGUAGE_OPTIONS" & diff --git a/mod/etc/options_menu/language/commands/c0080_Dutch b/mod/etc/options_menu/language/commands/c0060_Dutch similarity index 92% rename from mod/etc/options_menu/language/commands/c0080_Dutch rename to mod/etc/options_menu/language/commands/c0060_Dutch index 9883fba..577f3e7 100644 --- a/mod/etc/options_menu/language/commands/c0080_Dutch +++ b/mod/etc/options_menu/language/commands/c0060_Dutch @@ -1,4 +1,4 @@ -COMMAND_NAME=LANG_DUTCH +COMMAND_NAME=Nederlands COMMAND_TYPE=EXTERNAL RESTART_UI=FALSE COMMAND_STR=echo nl-NL > /etc/options_menu/language.cfg && usleep 50000 && %options_path%/options --commandPath %options_path%/language/commands/ --title "LANGUAGE_OPTIONS" & diff --git a/mod/etc/options_menu/language/commands/c0020_Portuguese b/mod/etc/options_menu/language/commands/c0070_Portuguese similarity index 89% rename from mod/etc/options_menu/language/commands/c0020_Portuguese rename to mod/etc/options_menu/language/commands/c0070_Portuguese index d63f751..977562a 100644 --- a/mod/etc/options_menu/language/commands/c0020_Portuguese +++ b/mod/etc/options_menu/language/commands/c0070_Portuguese @@ -1,4 +1,4 @@ -COMMAND_NAME=LANG_PORTUGUESE +COMMAND_NAME=Português (Europeu) COMMAND_TYPE=EXTERNAL RESTART_UI=FALSE COMMAND_STR=echo pt-PT > /etc/options_menu/language.cfg && usleep 50000 && %options_path%/options --commandPath %options_path%/language/commands/ --title "LANGUAGE_OPTIONS" & diff --git a/mod/etc/options_menu/language/commands/c0080_Russian b/mod/etc/options_menu/language/commands/c0080_Russian new file mode 100644 index 0000000..ccd0b49 --- /dev/null +++ b/mod/etc/options_menu/language/commands/c0080_Russian @@ -0,0 +1,5 @@ +COMMAND_NAME=Русский +COMMAND_TYPE=EXTERNAL +RESTART_UI=FALSE +COMMAND_STR=echo ru-RU > /etc/options_menu/language.cfg && usleep 50000 && %options_path%/options --commandPath %options_path%/language/commands/ --title "LANGUAGE_OPTIONS" & +PREVIEW_IMAGE=/etc/options_menu/images/preview_placeholder.png diff --git a/mod/etc/options_menu/language/commands/c0030_Japanese b/mod/etc/options_menu/language/commands/c0090_Japanese similarity index 91% rename from mod/etc/options_menu/language/commands/c0030_Japanese rename to mod/etc/options_menu/language/commands/c0090_Japanese index 269f52e..280b3f2 100644 --- a/mod/etc/options_menu/language/commands/c0030_Japanese +++ b/mod/etc/options_menu/language/commands/c0090_Japanese @@ -1,4 +1,4 @@ -COMMAND_NAME=LANG_JAPANESE +COMMAND_NAME=日本語 COMMAND_TYPE=EXTERNAL RESTART_UI=FALSE COMMAND_STR=echo ja-JP > /etc/options_menu/language.cfg && usleep 50000 && %options_path%/options --commandPath %options_path%/language/commands/ --title "LANGUAGE_OPTIONS" & diff --git a/mod/etc/options_menu/language/de-DE.lang b/mod/etc/options_menu/language/de-DE.lang index 5edf242..2420c4f 100644 --- a/mod/etc/options_menu/language/de-DE.lang +++ b/mod/etc/options_menu/language/de-DE.lang @@ -39,14 +39,6 @@ RA_TOGGLE_LOAD_SCREENS=RA- und Canoe-Ladebildschirme umschalten RESTORE_WIFI_CONFIG=Wifi-Konfiguration wiederherstellen OPTIONS_TITLE=Optionen PRESS_B_EXIT=B drücken zum Beenden. -LANG_ENGLISH=Englisch -LANG_PORTUGUESE=Portugiesisch -LANG_JAPANESE=Japanisch -LANG_FRENCH=Französisch -LANG_GERMAN=Deutsch -LANG_SPANISH=Spanisch -LANG_ITALIAN=Italienisch -LANG_DUTCH=Niederländisch WIFI_BACKUP_SPLASH_LINE1=Wifi-Konfigurationsdatei wird gesichert WIFI_BACKUP_SPLASH_LINE2=Bitte warten . . . DELETE_BACKUP_CONFIRM=Diese Sicherung löschen? diff --git a/mod/etc/options_menu/language/en-US.lang b/mod/etc/options_menu/language/en-US.lang index 17168f5..8d0305f 100644 --- a/mod/etc/options_menu/language/en-US.lang +++ b/mod/etc/options_menu/language/en-US.lang @@ -39,14 +39,6 @@ RA_TOGGLE_LOAD_SCREENS=Toggle RA and Canoe load screens RESTORE_WIFI_CONFIG=Restore WIFI Config OPTIONS_TITLE=Options PRESS_B_EXIT=Press B to exit. -LANG_ENGLISH=English -LANG_PORTUGUESE=Portuguese -LANG_JAPANESE=Japanese -LANG_FRENCH=French -LANG_GERMAN=German -LANG_SPANISH=Spanish -LANG_ITALIAN=Italian -LANG_DUTCH=Dutch WIFI_BACKUP_SPLASH_LINE1=Backup Wifi Config File WIFI_BACKUP_SPLASH_LINE2=Please Wait . . . DELETE_BACKUP_CONFIRM=Delete this backup? diff --git a/mod/etc/options_menu/language/es-ES.lang b/mod/etc/options_menu/language/es-ES.lang index a789d2b..f90cefd 100644 --- a/mod/etc/options_menu/language/es-ES.lang +++ b/mod/etc/options_menu/language/es-ES.lang @@ -39,14 +39,6 @@ RA_TOGGLE_LOAD_SCREENS=Alternar pantallas de carga de RA y Canoe RESTORE_WIFI_CONFIG=Restaurar configuración Wifi OPTIONS_TITLE=Opciones PRESS_B_EXIT=Pulsa B para salir. -LANG_ENGLISH=Inglés -LANG_PORTUGUESE=Portugués -LANG_JAPANESE=Japonés -LANG_FRENCH=Francés -LANG_GERMAN=Alemán -LANG_SPANISH=Español -LANG_ITALIAN=Italiano -LANG_DUTCH=Neerlandés WIFI_BACKUP_SPLASH_LINE1=Copiando archivo de configuración Wifi WIFI_BACKUP_SPLASH_LINE2=Espere por favor . . . DELETE_BACKUP_CONFIRM=¿Borrar esta copia de seguridad? diff --git a/mod/etc/options_menu/language/fr-FR.lang b/mod/etc/options_menu/language/fr-FR.lang index e6a8244..86dbfed 100644 --- a/mod/etc/options_menu/language/fr-FR.lang +++ b/mod/etc/options_menu/language/fr-FR.lang @@ -39,14 +39,6 @@ RA_TOGGLE_LOAD_SCREENS=Activer/désactiver les écrans de chargement RA et Canoe RESTORE_WIFI_CONFIG=Restaurer la config Wifi OPTIONS_TITLE=Options PRESS_B_EXIT=Appuyez sur B pour quitter. -LANG_ENGLISH=Anglais -LANG_PORTUGUESE=Portugais -LANG_JAPANESE=Japonais -LANG_FRENCH=Français -LANG_GERMAN=Allemand -LANG_SPANISH=Espagnol -LANG_ITALIAN=Italien -LANG_DUTCH=Néerlandais WIFI_BACKUP_SPLASH_LINE1=Sauvegarde du fichier de config Wifi WIFI_BACKUP_SPLASH_LINE2=Veuillez patienter . . . DELETE_BACKUP_CONFIRM=Supprimer cette sauvegarde? diff --git a/mod/etc/options_menu/language/it-IT.lang b/mod/etc/options_menu/language/it-IT.lang index 6fbe850..0114df2 100644 --- a/mod/etc/options_menu/language/it-IT.lang +++ b/mod/etc/options_menu/language/it-IT.lang @@ -39,14 +39,6 @@ RA_TOGGLE_LOAD_SCREENS=Attiva/disattiva schermate di caricamento RA e Canoe RESTORE_WIFI_CONFIG=Ripristina configurazione Wifi OPTIONS_TITLE=Opzioni PRESS_B_EXIT=Premi B per uscire. -LANG_ENGLISH=Inglese -LANG_PORTUGUESE=Portoghese -LANG_JAPANESE=Giapponese -LANG_FRENCH=Francese -LANG_GERMAN=Tedesco -LANG_SPANISH=Spagnolo -LANG_ITALIAN=Italiano -LANG_DUTCH=Olandese WIFI_BACKUP_SPLASH_LINE1=Backup del file di configurazione Wifi WIFI_BACKUP_SPLASH_LINE2=Attendere prego . . . DELETE_BACKUP_CONFIRM=Eliminare questo backup? diff --git a/mod/etc/options_menu/language/ja-JP.lang b/mod/etc/options_menu/language/ja-JP.lang index 906243d..e3aee92 100644 --- a/mod/etc/options_menu/language/ja-JP.lang +++ b/mod/etc/options_menu/language/ja-JP.lang @@ -39,14 +39,6 @@ RA_TOGGLE_LOAD_SCREENS=RAとCanoeのLoadingがめんをきりかえ RESTORE_WIFI_CONFIG=Wifiせっていをもどす OPTIONS_TITLE=せってい PRESS_B_EXIT=Bでおわる -LANG_ENGLISH=英語 -LANG_PORTUGUESE=ポルトガル語 -LANG_JAPANESE=日本語 -LANG_FRENCH=フランス語 -LANG_GERMAN=ドイツ語 -LANG_SPANISH=スペイン語 -LANG_ITALIAN=イタリア語 -LANG_DUTCH=オランダ語 WIFI_BACKUP_SPLASH_LINE1=Wifiせってい Fileの Backup WIFI_BACKUP_SPLASH_LINE2=おまちください . . . DELETE_BACKUP_CONFIRM=このBackupをさくじょしますか? diff --git a/mod/etc/options_menu/language/nl-NL.lang b/mod/etc/options_menu/language/nl-NL.lang index d49e1ab..fd8befe 100644 --- a/mod/etc/options_menu/language/nl-NL.lang +++ b/mod/etc/options_menu/language/nl-NL.lang @@ -39,14 +39,6 @@ RA_TOGGLE_LOAD_SCREENS=RA- en Canoe-laadschermen schakelen RESTORE_WIFI_CONFIG=Wifi-configuratie herstellen OPTIONS_TITLE=Opties PRESS_B_EXIT=Druk op B om af te sluiten. -LANG_ENGLISH=Engels -LANG_PORTUGUESE=Portugees -LANG_JAPANESE=Japans -LANG_FRENCH=Frans -LANG_GERMAN=Duits -LANG_SPANISH=Spaans -LANG_ITALIAN=Italiaans -LANG_DUTCH=Nederlands WIFI_BACKUP_SPLASH_LINE1=Wifi-configuratiebestand back-uppen WIFI_BACKUP_SPLASH_LINE2=Even geduld . . . DELETE_BACKUP_CONFIRM=Deze back-up verwijderen? diff --git a/mod/etc/options_menu/language/pt-PT.lang b/mod/etc/options_menu/language/pt-PT.lang index 6d1bde8..c40cc26 100644 --- a/mod/etc/options_menu/language/pt-PT.lang +++ b/mod/etc/options_menu/language/pt-PT.lang @@ -39,14 +39,6 @@ RA_TOGGLE_LOAD_SCREENS=Alternar Ecrãs de Carregamento RA e Canoe RESTORE_WIFI_CONFIG=Restaurar Configuração Wifi OPTIONS_TITLE=Opções PRESS_B_EXIT=Pressione B para sair. -LANG_ENGLISH=Inglês -LANG_PORTUGUESE=Português -LANG_JAPANESE=Japonês -LANG_FRENCH=Francês -LANG_GERMAN=Alemão -LANG_SPANISH=Espanhol -LANG_ITALIAN=Italiano -LANG_DUTCH=Neerlandês WIFI_BACKUP_SPLASH_LINE1=Cópia de Segurança Wifi WIFI_BACKUP_SPLASH_LINE2=Aguarde Por Favor . . . DELETE_BACKUP_CONFIRM=Apagar esta cópia de segurança? diff --git a/mod/etc/options_menu/language/ru-RU.lang b/mod/etc/options_menu/language/ru-RU.lang new file mode 100644 index 0000000..6f7aa40 --- /dev/null +++ b/mod/etc/options_menu/language/ru-RU.lang @@ -0,0 +1,52 @@ +HIBERNATE=Спящий режим +SHUTDOWN_DEVICE=Выключить устройство +REBOOT_DEVICE=Перезагрузить устройство +RETROARCH_OPTIONS=Настройки RetroArch +ADVANCED_OPTIONS=Расширенные настройки +NETWORK_OPTIONS=Настройки сети +EXIT=Выход +MODULE_UNINSTALLER=Удаление модулей +DISPLAY_TEMP=Показать температуру +RUN_TOP=Запустить Top +BENCHMARK_TOOL=Инструмент Benchmark +DUMP_FILE_STRUCTURE=Сохранить структуру файлов (на USB) +TOGGLE_USB_WRITE=Переключить запись на USB +TOGGLE_MODERN_UI=Modern UI +RA_DEBUGGER=Отладчик RA (логи на USB) +CHANGE_BUTTON_COMBO=Изменить комбинацию кнопок +CLEAR_CACHE=Очистить кэш +BACK=Назад +LANGUAGE_OPTIONS=Настройки языка +DISPLAY_IP=Показать IP-адрес +RECONNECT=Переподключиться +SEARCH_SSIDS=Поиск SSID +WIFI_BACKUP_NAND=Резервная копия Wifi (NAND) +WIFI_BACKUP_USB=Резервная копия Wifi (USB) +WIFI_RESTORE_NAND=Восстановить Wifi (NAND) +WIFI_RESTORE_USB=Восстановить Wifi (USB) +RA_RESTORE_DEFAULTS_ALL=Восстановить настройки по умолчанию (все) +RA_RESTORE_DEFAULTS=Восстановить настройки по умолчанию +RA_BACKUP_NAND=Резервная копия RA (NAND) +RA_BACKUP_USB=Резервная копия RA (USB) +RA_RESTORE_NAND=Восстановить RA (NAND) +RA_RESTORE_USB=Восстановить RA (USB) +RA_DELETE_BACKUPS=Удалить все резервные копии +RA_TRANSFER_BIOS=Передать файл(ы) BIOS +RA_DELETE_OVERRIDES=Удалить Overrides игр и ядер +RA_DELETE_REMAPS=Удалить файлы Remap +RA_DELETE_BIOS=Удалить файл(ы) BIOS (NAND) +RA_TOGGLE_LOAD_SCREENS=Переключить экраны загрузки RA и Canoe +RESTORE_WIFI_CONFIG=Восстановить настройки Wifi +OPTIONS_TITLE=Настройки +PRESS_B_EXIT=Нажмите B для выхода. +WIFI_BACKUP_SPLASH_LINE1=Резервное копирование Wifi +WIFI_BACKUP_SPLASH_LINE2=Пожалуйста, подождите . . . +DELETE_BACKUP_CONFIRM=Удалить эту резервную копию? +DELETE_CONFIRM_HINT=A = Да B = Нет +DELETE_CONFIRM_GENERIC=Удалить этот элемент? +WIFI_DELETE_HINT_FOOTER=X = Удалить A = Восстановить +HINT_SELECT=Выбрать +HINT_BACK=Назад +HINT_DELETE=Удалить +NO_NET_ERR=Ошибка подключения к серверу, проверьте подключение к интернету и попробуйте снова. +NO_PKG_ERR=Запрошенный пакет сейчас недоступен, попробуйте позже. diff --git a/mod/etc/options_menu/scripts/gen_splash b/mod/etc/options_menu/scripts/gen_splash deleted file mode 100644 index d4d9c9bcbf6e7b83d694fc87822c236a1229984f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 49796 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 #include #include +#include SDL_Context::SDL_Context(std::chrono::milliseconds fpsTime, bool powerButtonExit) : fpsTime(fpsTime), powerButtonExit(powerButtonExit) { @@ -24,11 +25,13 @@ SDL_Context::SDL_Context(std::chrono::milliseconds fpsTime, bool powerButtonExit SDL_DestroyWindow(window); exit(1); } + TTF_Init(); nextFrameTime = std::chrono::system_clock::now()+fpsTime; } SDL_Context::~SDL_Context() { + TTF_Quit(); delete powerwatch; SDL_DestroyRenderer(renderer); SDL_DestroyWindow(window); diff --git a/src/framework/texture.cpp b/src/framework/texture.cpp index f7f711e..a2c861a 100644 --- a/src/framework/texture.cpp +++ b/src/framework/texture.cpp @@ -11,10 +11,13 @@ #include "font8x8.h" #include +#include #include #include +#include #include #include +#include #if SDL_BYTEORDER == SDL_BIG_ENDIAN #define rmask 0xff000000 @@ -30,9 +33,10 @@ Texture::Texture() {} -Texture::Texture(const std::string & text, int fontSize, SDL_Renderer* renderer, int x, int y, bool centerText, const int color) +Texture::Texture(const std::string & text, int fontSize, SDL_Renderer* renderer, int x, int y, bool centerText, const int color, bool useTTF) { - texture = std::shared_ptr(WriteText(text, fontSize, renderer, rect.w, rect.h, color), SDL_DestroyTexture); + auto writer = (useTTF && CanRenderWithTTF(text, fontSize)) ? WriteTextTTF : WriteText; + texture = std::shared_ptr(writer(text, fontSize, renderer, rect.w, rect.h, color), SDL_DestroyTexture); rect.x = (centerText) ? x-rect.w/2 : x; rect.y = (centerText) ? y-rect.h/2 : y; } @@ -173,6 +177,8 @@ SDL_Texture * WriteText(const std::string & text, int fontSize, SDL_Renderer* re bitmap = font8x8_basic[codepoint]; else if (codepoint >= 0xA0 && codepoint <= 0xFF) bitmap = font8x8_ext_latin[codepoint - 0xA0]; + else if (codepoint >= 0x0400 && codepoint <= 0x04FF) + bitmap = font8x8_cyrillic[codepoint - 0x0400]; else if (codepoint >= 0x3040 && codepoint <= 0x309F) bitmap = font8x8_hiragana[codepoint - 0x3040]; else if (codepoint >= 0x30A0 && codepoint <= 0x30FF) @@ -198,3 +204,112 @@ SDL_Texture * WriteText(const std::string & text, int fontSize, SDL_Renderer* re SDL_FreeSurface(surface); return texture; } + +static std::string ttfFontPath; + +// CJK font comes from the optional addon hmod if installed, else the bundled Latin-only one +void SetTTFFontPath(const std::string & optionsLocation) +{ + static const char * const cjkFontPath = "/etc/options_menu/fonts/NotoSansJP-CJK.ttf"; + ttfFontPath = std::ifstream(cjkFontPath).good() ? cjkFontPath : optionsLocation + "fonts/NotoSansJP-Latin-subset.ttf"; +} + +// one TTF_Font per point size actually used, opened once and kept for the process lifetime +static TTF_Font * GetTTFFont(int pointSize) +{ + static std::map fonts; + auto it = fonts.find(pointSize); + if(it != fonts.end()) + return it->second; + + TTF_Font * font = TTF_OpenFont(ttfFontPath.c_str(), pointSize); + fonts[pointSize] = font; + return font; +} + +SDL_Texture * WriteTextTTF(const std::string & text, int fontSize, SDL_Renderer* renderer, int & textureWidth, int & textureHeight, const int color) +{ + if(text.empty()) + return nullptr; + + TTF_Font * font = GetTTFFont(fontSize); + if(!font) + { + std::cerr << "Cannot open TTF font: " << ttfFontPath << " (" << TTF_GetError() << ")\n"; + return nullptr; + } + + // color is packed 0xAABBGGRR, same convention as WriteText/the rest of the codebase + SDL_Color sdlColor{ + static_cast(color & 0xFF), + static_cast((color >> 8) & 0xFF), + static_cast((color >> 16) & 0xFF), + static_cast((color >> 24) & 0xFF) + }; + + SDL_Surface * surface = TTF_RenderUTF8_Blended(font, text.c_str(), sdlColor); + if(!surface) + { + std::cerr << "TTF_RenderUTF8_Blended failed: " << TTF_GetError() << "\n"; + return nullptr; + } + + textureWidth = surface->w; + textureHeight = surface->h; + auto texture = SDL_CreateTextureFromSurface(renderer, surface); + SDL_SetTextureBlendMode(texture, SDL_BLENDMODE_BLEND); + SDL_FreeSurface(surface); + return texture; +} + +bool CanRenderWithTTF(const std::string & text, int fontSize) +{ + TTF_Font * font = GetTTFFont(fontSize); + if(!font) + return false; + + for(size_t i = 0; i < text.size();) + { + unsigned char firstByte = static_cast(text[i]); + unsigned int codepoint = 0xFFFD; + size_t sequenceLength = 1; + + if(firstByte < 0x80) + { + codepoint = firstByte; + } + else if(firstByte >= 0xC2 && firstByte <= 0xDF && i + 1 < text.size()) + { + codepoint = ((firstByte & 0x1F) << 6) | (static_cast(text[i+1]) & 0x3F); + sequenceLength = 2; + } + else if(firstByte >= 0xE0 && firstByte <= 0xEF && i + 2 < text.size()) + { + codepoint = ((firstByte & 0x0F) << 12) | ((static_cast(text[i+1]) & 0x3F) << 6) | (static_cast(text[i+2]) & 0x3F); + sequenceLength = 3; + } + + // TTF_GlyphIsProvided only takes a 16-bit code + if(codepoint > 0xFFFF || !TTF_GlyphIsProvided(font, static_cast(codepoint))) + return false; + + i += sequenceLength; + } + return true; +} + +int MeasureTTFWidth(const std::string & text, int fontSize) +{ + TTF_Font * font = GetTTFFont(fontSize); + if(!font || text.empty()) + return 0; + int w = 0, h = 0; + TTF_SizeUTF8(font, text.c_str(), &w, &h); + return w; +} + +int GetTTFLineHeight(int fontSize) +{ + TTF_Font * font = GetTTFFont(fontSize); + return font ? TTF_FontLineSkip(font) : fontSize; +} diff --git a/src/framework/texture.h b/src/framework/texture.h index e5ed82d..de3e42c 100644 --- a/src/framework/texture.h +++ b/src/framework/texture.h @@ -18,7 +18,7 @@ struct Texture std::shared_ptr texture; SDL_Rect rect{0, 0, 0, 0}; // LoadTexturePNG leaves this untouched on failure, so it must default to zero, not garbage Texture(); - Texture(const std::string & text, int fontSize, SDL_Renderer* renderer, int x = 0, int y = 0, bool centerText = false, const int color = 0xFFFFFFFF); + Texture(const std::string & text, int fontSize, SDL_Renderer* renderer, int x = 0, int y = 0, bool centerText = false, const int color = 0xFFFFFFFF, bool useTTF = false); Texture(const std::string & pngFilePath, SDL_Renderer* renderer, int x = 0, int y = 0, bool centerImg = false); void Draw(SDL_Renderer* renderer); void Draw(SDL_Renderer* renderer, SDL_RendererFlip flip_enum); @@ -33,5 +33,18 @@ struct Sprite SDL_Texture * LoadTexturePNG(SDL_Renderer *renderer, std::string file, SDL_Rect * rect = nullptr); SDL_Texture * WriteText(const std::string & text, int fontSize, SDL_Renderer* renderer, int & textureWidth, int & textureHeight, const int color = 0xFFFFFF); +SDL_Texture * WriteTextTTF(const std::string & text, int fontSize, SDL_Renderer* renderer, int & textureWidth, int & textureHeight, const int color = 0xFFFFFF); + +// call before any useTTF=true Texture, once at startup +void SetTTFFontPath(const std::string & optionsLocation); + +// false if the loaded font is missing a glyph in text - fall back to font8x8 +bool CanRenderWithTTF(const std::string & text, int fontSize); + +// proportional width, unlike font8x8's fixed fontSize/char +int MeasureTTFWidth(const std::string & text, int fontSize); + +// line-to-line spacing, unlike font8x8's fixed-height rows +int GetTTFLineHeight(int fontSize); #endif diff --git a/src/main.cpp b/src/main.cpp index b63bf2c..ac3195a 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -48,6 +48,18 @@ std::string TruncateUtf8(const std::string & text, int maxCodepoints) return text.substr(0, i); } +int Utf8Length(const std::string & text) +{ + int count = 0; + for(size_t i = 0; i < text.size();) + { + unsigned char c = static_cast(text[i]); + i += (c >= 0xF0) ? 4 : (c >= 0xE0) ? 3 : (c >= 0xC0) ? 2 : 1; + ++count; + } + return count; +} + static struct ExitManager { std::string exitCommand; @@ -78,6 +90,7 @@ int main(int argc, char * argv[]) if(langCode.empty()) langCode = "en-US"; LoadLanguage(optionsLocation, langCode); + SetTTFFontPath(optionsLocation); std::string uiStyle; in.open("/etc/options_menu/ui_style.cfg"); @@ -199,18 +212,19 @@ int main(int argc, char * argv[]) Texture badgeInner(optionsLocation + UiTheme::AssetBadgeInner, renderer); //Create Textures for Strings - Texture appTitleText("OptionsMenu", UiTheme::TitleFontSize, renderer, UiTheme::TitleX, UiTheme::TitleY, false, 0xFFFFFFFF); + // appTitleText/appVersionText only ever draw in the modernUI branch (see the render loop below) + Texture appTitleText("OptionsMenu", UiTheme::TitleFontSize, renderer, UiTheme::TitleX, UiTheme::TitleY, false, 0xFFFFFFFF, true); appTitleText.rect.y -= appTitleText.rect.h / 2; // vertically center on the gear (Texture only supports centering both axes together) - Texture appVersionText(MOD_VERSION, UiTheme::VersionFontSize, renderer, appTitleText.rect.x + appTitleText.rect.w + UiTheme::VersionGap, UiTheme::TitleY, false, 0xFFFFFFFF); + Texture appVersionText(MOD_VERSION, UiTheme::VersionFontSize, renderer, appTitleText.rect.x + appTitleText.rect.w + UiTheme::VersionGap, UiTheme::TitleY, false, 0xFFFFFFFF, true); appVersionText.rect.y -= appVersionText.rect.h / 2; Texture titleText = modernUI - ? Texture(titleString, UiTheme::SectionTitleFontSize, renderer, UiTheme::SectionTitleCenterX, UiTheme::SectionTitleY, true) + ? Texture(titleString, UiTheme::SectionTitleFontSize, renderer, UiTheme::SectionTitleCenterX, UiTheme::SectionTitleY, true, 0xFFFFFFFF, true) : Texture(titleString, 36, renderer, 640, 185, true); SDL_Rect highlightRect{ UiTheme::HighlightX, UiTheme::RowFirstY - 2, UiTheme::HighlightW, UiTheme::HighlightH }; Texture pointerText("->", 16, renderer, 20, UiTheme::RowFirstY, false, 0xFF00FF00); SDL_Rect & pointerRect = pointerText.rect; Texture CompComText = modernUI - ? Texture("created by CompCom", 16, renderer) + ? Texture("created by CompCom", 16, renderer, 0, 0, false, 0xFFFFFFFF, true) : Texture("created by CompCom", 16, renderer, 1100, 620, true); if(modernUI) { @@ -218,15 +232,18 @@ int main(int argc, char * argv[]) CompComText.rect.y = UiTheme::CreditY; } Texture deleteHint(Translate("WIFI_DELETE_HINT_FOOTER"), 16, renderer, 30, 612); - Texture scrollUp = modernUI ? Texture("^", 16, renderer, UiTheme::ScrollX, UiTheme::ScrollUpY) : Texture("^", 16, renderer, 30, 248); + Texture scrollUp = modernUI + ? Texture("^", 16, renderer, UiTheme::ScrollX, UiTheme::ScrollUpY, false, 0xFFFFFFFF, true) + : Texture("^", 16, renderer, 30, 248); Texture scrollDown = scrollUp; scrollDown.rect.y = modernUI ? UiTheme::ScrollDownY : (252+DisplayItemCount*18); //Badge cluster (top-right hints): A/B always shown, X only if the row has a delete action + // badges only ever exist in modernUI, so their textures are unconditionally TTF struct Badge { Texture letter; Texture label; UiTheme::BadgeColor rim; UiTheme::BadgeColor fill; }; - Badge badgeA{ Texture("A", 16, renderer, 0, 0, false, UiTheme::BadgeLetterColor), Texture(Translate("HINT_SELECT"), 16, renderer), UiTheme::BadgeADark, UiTheme::BadgeA }; - Badge badgeB{ Texture("B", 16, renderer, 0, 0, false, UiTheme::BadgeLetterColor), Texture(Translate("HINT_BACK"), 16, renderer), UiTheme::BadgeBDark, UiTheme::BadgeB }; - Badge badgeX{ Texture("X", 16, renderer, 0, 0, false, UiTheme::BadgeLetterColor), Texture(Translate("HINT_DELETE"), 16, renderer), UiTheme::BadgeXDark, UiTheme::BadgeX }; + Badge badgeA{ Texture("A", 16, renderer, 0, 0, false, UiTheme::BadgeLetterColor, true), Texture(Translate("HINT_SELECT"), 16, renderer, 0, 0, false, 0xFFFFFFFF, true), UiTheme::BadgeADark, UiTheme::BadgeA }; + Badge badgeB{ Texture("B", 16, renderer, 0, 0, false, UiTheme::BadgeLetterColor, true), Texture(Translate("HINT_BACK"), 16, renderer, 0, 0, false, 0xFFFFFFFF, true), UiTheme::BadgeBDark, UiTheme::BadgeB }; + Badge badgeX{ Texture("X", 16, renderer, 0, 0, false, UiTheme::BadgeLetterColor, true), Texture(Translate("HINT_DELETE"), 16, renderer, 0, 0, false, 0xFFFFFFFF, true), UiTheme::BadgeXDark, UiTheme::BadgeX }; auto DrawBadge = [&](Badge & badge, int rightEdgeX) -> int { int groupW = UiTheme::BadgeOuterSize + UiTheme::BadgeLabelGap + badge.label.rect.w; @@ -268,14 +285,30 @@ int main(int argc, char * argv[]) maxRight = (maxRight < 0) ? switchBound : std::min(maxRight, switchBound); } + bool rowUsesTTF = modernUI && CanRenderWithTTF(label, RowGlyphSize); if(maxRight >= 0) { - int maxChars = std::max(0, (maxRight - textX) / RowGlyphSize); - if(TruncateUtf8(label, maxChars).size() != label.size()) - label = TruncateUtf8(label, std::max(0, maxChars - 3)) + "..."; + int available = std::max(0, maxRight - textX); + if(rowUsesTTF) + { + // proportional font: shrink by measured width, not a fixed px/char guess + if(MeasureTTFWidth(label, RowGlyphSize) > available) + { + int n = Utf8Length(label); + while(n > 0 && MeasureTTFWidth(TruncateUtf8(label, n) + "...", RowGlyphSize) > available) + --n; + label = TruncateUtf8(label, n) + "..."; + } + } + else + { + int maxChars = available / RowGlyphSize; + if(TruncateUtf8(label, maxChars).size() != label.size()) + label = TruncateUtf8(label, std::max(0, maxChars - 3)) + "..."; + } } - c.texture = Texture(label, RowGlyphSize, renderer, textX, 0); + c.texture = Texture(label, RowGlyphSize, renderer, textX, 0, false, 0xFFFFFFFF, modernUI); } int topListItemNumber = 1; @@ -299,14 +332,16 @@ int main(int argc, char * argv[]) if(updateCommandYPos) { int y = UiTheme::RowFirstY; + int rowPitch = modernUI ? std::max(UiTheme::RowPitch, GetTTFLineHeight(RowGlyphSize)) : 18; for(int i = 0, count = std::min(DisplayItemCount,static_cast(commands.size())); i < count; ++i) { commands[i+topListItemNumber].texture.rect.y = y; - y += 18; + y += rowPitch; } } highlightRect.y = currentCommand.texture.rect.y - 2; + highlightRect.h = currentCommand.texture.rect.h + 4; // wrap the row's actual text height (16px font8x8 or variable-height TTF), not a fixed guess highlightRect.w = UiTheme::HighlightW; pointerRect.y = currentCommand.texture.rect.y; if(currentCommand.previewImage.size()) @@ -403,8 +438,8 @@ int main(int argc, char * argv[]) else if(controller.GetButtonStatus(X) && !commands[currentCommandId].deleteCommand.empty()) { const std::string & confirmKey = commands[currentCommandId].deleteConfirmKey; - Texture confirmTitle(Translate(confirmKey.empty() ? "DELETE_CONFIRM_GENERIC" : confirmKey), 24, renderer, 640, 320, true); - Texture confirmHint(Translate("DELETE_CONFIRM_HINT"), 16, renderer, 640, 360, true); + Texture confirmTitle(Translate(confirmKey.empty() ? "DELETE_CONFIRM_GENERIC" : confirmKey), 24, renderer, 640, 320, true, 0xFFFFFFFF, modernUI); + Texture confirmHint(Translate("DELETE_CONFIRM_HINT"), 16, renderer, 640, 360, true, 0xFFFFFFFF, modernUI); bool confirmed = false; for(;;) { @@ -466,7 +501,7 @@ int main(int argc, char * argv[]) { Texture & rowSwitch = rowCommand.stateOn ? switchOn : switchOff; rowSwitch.rect.x = UiTheme::SwitchRightX - UiTheme::SwitchW; - rowSwitch.rect.y = rowCommand.texture.rect.y; + rowSwitch.rect.y = rowCommand.texture.rect.y + (rowCommand.texture.rect.h - UiTheme::SwitchH) / 2; rowSwitch.Draw(renderer); } } From ba96884bd0ca1db1fc79d3026441b2e3390c8535 Mon Sep 17 00:00:00 2001 From: DefKorns Date: Fri, 11 Sep 2026 01:42:17 +0000 Subject: [PATCH 002/104] Add Modern UI drawing helpers and enhance SDL context management - Introduced draw_helpers.h with functions for stroke and fill rectangle drawing, including rounded rectangles and lines for a modern UI aesthetic. - Updated sdl_context.cpp to improve renderer creation with vsync and added a shutdown method for better resource management. - Modified sdl_context.h to include a shutdown method and a shutdown flag. - Refined uitheme.h to adjust color schemes and layout constants for the modern UI, including badge and footer configurations. - Enhanced main.cpp to integrate new drawing functions, streamline command handling, and improve UI rendering logic, including a new method for drawing rows and managing the display of badges. - Implemented a confirmation dialog for delete actions with improved user feedback and interaction handling. --- Makefile.docker | 2 +- .../advanced_commands/c9998_separator | 0 .../options_menu/advanced_commands/c9999_Back | 5 - .../options_menu/advanced_commands/c9999_Exit | 5 - mod/etc/options_menu/commands/c9998_separator | 0 mod/etc/options_menu/commands/c9999_Exit | 5 - .../language/commands/c9998_separator | 0 .../options_menu/language/commands/c9999_Back | 5 - .../options_menu/language/commands/c9999_Exit | 5 - mod/etc/options_menu/language/de-DE.lang | 2 +- mod/etc/options_menu/language/en-US.lang | 2 +- mod/etc/options_menu/language/es-ES.lang | 2 +- mod/etc/options_menu/language/fr-FR.lang | 2 +- mod/etc/options_menu/language/it-IT.lang | 2 +- mod/etc/options_menu/language/ja-JP.lang | 2 +- mod/etc/options_menu/language/nl-NL.lang | 2 +- mod/etc/options_menu/language/pt-PT.lang | 2 +- mod/etc/options_menu/language/ru-RU.lang | 2 +- .../network/commands/c9998_separator | 0 .../options_menu/network/commands/c9999_Back | 5 - .../options_menu/network/commands/c9999_Exit | 5 - .../network/restore/commands/c9998_separator | 0 .../network/restore/commands/c9999_Back | 5 - .../retroarch/commands/c9998_separator | 0 .../retroarch/commands/c9999_Back | 5 - .../retroarch/commands/c9999_Exit | 5 - src/command.cpp | 34 +- src/command.h | 12 +- src/framework/draw_helpers.cpp | 124 ++++++ src/framework/draw_helpers.h | 30 ++ src/framework/sdl_context.cpp | 27 +- src/framework/sdl_context.h | 5 + src/framework/uitheme.h | 154 ++++--- src/main.cpp | 392 ++++++++++++------ 34 files changed, 592 insertions(+), 256 deletions(-) delete mode 100644 mod/etc/options_menu/advanced_commands/c9998_separator delete mode 100644 mod/etc/options_menu/advanced_commands/c9999_Back delete mode 100644 mod/etc/options_menu/advanced_commands/c9999_Exit delete mode 100644 mod/etc/options_menu/commands/c9998_separator delete mode 100644 mod/etc/options_menu/commands/c9999_Exit delete mode 100644 mod/etc/options_menu/language/commands/c9998_separator delete mode 100644 mod/etc/options_menu/language/commands/c9999_Back delete mode 100644 mod/etc/options_menu/language/commands/c9999_Exit delete mode 100644 mod/etc/options_menu/network/commands/c9998_separator delete mode 100644 mod/etc/options_menu/network/commands/c9999_Back delete mode 100644 mod/etc/options_menu/network/commands/c9999_Exit delete mode 100644 mod/etc/options_menu/network/restore/commands/c9998_separator delete mode 100644 mod/etc/options_menu/network/restore/commands/c9999_Back delete mode 100644 mod/etc/options_menu/retroarch/commands/c9998_separator delete mode 100644 mod/etc/options_menu/retroarch/commands/c9999_Back delete mode 100644 mod/etc/options_menu/retroarch/commands/c9999_Exit create mode 100644 src/framework/draw_helpers.cpp create mode 100644 src/framework/draw_helpers.h diff --git a/Makefile.docker b/Makefile.docker index 4a12cec..1ff1367 100644 --- a/Makefile.docker +++ b/Makefile.docker @@ -26,7 +26,7 @@ VENDORED_TTF_LIBS = endif CXXFLAGS = -std=c++11 -Os $(SDL_CFLAGS) -DMOD_VERSION=\"$(MOD_VER)\" LDLIBS = $(SDL_LIBS) -SOURCES = src/main.cpp src/command.cpp src/localization.cpp src/framework/sdl_context.cpp src/framework/texture.cpp src/framework/nineslice.cpp src/framework/controller.cpp src/framework/powerwatch.cpp +SOURCES = src/main.cpp src/command.cpp src/localization.cpp src/framework/sdl_context.cpp src/framework/texture.cpp src/framework/nineslice.cpp src/framework/controller.cpp src/framework/powerwatch.cpp src/framework/draw_helpers.cpp OBJECTS = $(SOURCES:.cpp=.o) DEV_DIR = $(shell realpath .) OUT = $(DEV_DIR)/out diff --git a/mod/etc/options_menu/advanced_commands/c9998_separator b/mod/etc/options_menu/advanced_commands/c9998_separator deleted file mode 100644 index e69de29..0000000 diff --git a/mod/etc/options_menu/advanced_commands/c9999_Back b/mod/etc/options_menu/advanced_commands/c9999_Back deleted file mode 100644 index c605f4b..0000000 --- a/mod/etc/options_menu/advanced_commands/c9999_Back +++ /dev/null @@ -1,5 +0,0 @@ -COMMAND_NAME=BACK -COMMAND_TYPE=EXTERNAL -RESTART_UI=FALSE -COMMAND_STR=usleep 50000 && %options_path%/options & -PREVIEW_IMAGE=/etc/options_menu/images/preview_placeholder.png diff --git a/mod/etc/options_menu/advanced_commands/c9999_Exit b/mod/etc/options_menu/advanced_commands/c9999_Exit deleted file mode 100644 index 804d7ff..0000000 --- a/mod/etc/options_menu/advanced_commands/c9999_Exit +++ /dev/null @@ -1,5 +0,0 @@ -COMMAND_NAME=EXIT -COMMAND_TYPE=EXTERNAL -RESTART_UI=TRUE -COMMAND_STR=echo "Closing Options Menu" -PREVIEW_IMAGE=/etc/options_menu/images/preview_placeholder.png diff --git a/mod/etc/options_menu/commands/c9998_separator b/mod/etc/options_menu/commands/c9998_separator deleted file mode 100644 index e69de29..0000000 diff --git a/mod/etc/options_menu/commands/c9999_Exit b/mod/etc/options_menu/commands/c9999_Exit deleted file mode 100644 index 804d7ff..0000000 --- a/mod/etc/options_menu/commands/c9999_Exit +++ /dev/null @@ -1,5 +0,0 @@ -COMMAND_NAME=EXIT -COMMAND_TYPE=EXTERNAL -RESTART_UI=TRUE -COMMAND_STR=echo "Closing Options Menu" -PREVIEW_IMAGE=/etc/options_menu/images/preview_placeholder.png diff --git a/mod/etc/options_menu/language/commands/c9998_separator b/mod/etc/options_menu/language/commands/c9998_separator deleted file mode 100644 index e69de29..0000000 diff --git a/mod/etc/options_menu/language/commands/c9999_Back b/mod/etc/options_menu/language/commands/c9999_Back deleted file mode 100644 index 606b62d..0000000 --- a/mod/etc/options_menu/language/commands/c9999_Back +++ /dev/null @@ -1,5 +0,0 @@ -COMMAND_NAME=BACK -COMMAND_TYPE=EXTERNAL -RESTART_UI=FALSE -COMMAND_STR=usleep 50000 && %options_path%/options --commandPath %options_path%/advanced_commands/ --title "ADVANCED_OPTIONS" & -PREVIEW_IMAGE=/etc/options_menu/images/preview_placeholder.png diff --git a/mod/etc/options_menu/language/commands/c9999_Exit b/mod/etc/options_menu/language/commands/c9999_Exit deleted file mode 100644 index 804d7ff..0000000 --- a/mod/etc/options_menu/language/commands/c9999_Exit +++ /dev/null @@ -1,5 +0,0 @@ -COMMAND_NAME=EXIT -COMMAND_TYPE=EXTERNAL -RESTART_UI=TRUE -COMMAND_STR=echo "Closing Options Menu" -PREVIEW_IMAGE=/etc/options_menu/images/preview_placeholder.png diff --git a/mod/etc/options_menu/language/de-DE.lang b/mod/etc/options_menu/language/de-DE.lang index 2420c4f..3be5aee 100644 --- a/mod/etc/options_menu/language/de-DE.lang +++ b/mod/etc/options_menu/language/de-DE.lang @@ -47,6 +47,6 @@ DELETE_CONFIRM_GENERIC=Dieses Element löschen? WIFI_DELETE_HINT_FOOTER=X = Löschen A = Wiederherstellen HINT_SELECT=Wählen HINT_BACK=Zurück -HINT_DELETE=Löschen +HINT_DELETE=Löschen (Halten) NO_NET_ERR=Verbindung zum Server fehlgeschlagen. Bitte Internetverbindung prüfen und erneut versuchen. NO_PKG_ERR=Das angeforderte Paket ist derzeit nicht verfügbar. Bitte später erneut versuchen. diff --git a/mod/etc/options_menu/language/en-US.lang b/mod/etc/options_menu/language/en-US.lang index 8d0305f..ed896e8 100644 --- a/mod/etc/options_menu/language/en-US.lang +++ b/mod/etc/options_menu/language/en-US.lang @@ -47,6 +47,6 @@ DELETE_CONFIRM_GENERIC=Delete this item? WIFI_DELETE_HINT_FOOTER=X = Delete A = Restore HINT_SELECT=Select HINT_BACK=Back -HINT_DELETE=Delete +HINT_DELETE=Delete (Hold) NO_NET_ERR=Error while connecting to server, please check your internet connection and try again. NO_PKG_ERR=The package you requested isn't available at the moment, please try again later. diff --git a/mod/etc/options_menu/language/es-ES.lang b/mod/etc/options_menu/language/es-ES.lang index f90cefd..c609984 100644 --- a/mod/etc/options_menu/language/es-ES.lang +++ b/mod/etc/options_menu/language/es-ES.lang @@ -47,6 +47,6 @@ DELETE_CONFIRM_GENERIC=¿Borrar este elemento? WIFI_DELETE_HINT_FOOTER=X = Borrar A = Restaurar HINT_SELECT=Seleccionar HINT_BACK=Atrás -HINT_DELETE=Borrar +HINT_DELETE=Borrar (mantener) NO_NET_ERR=Error al conectar con el servidor, comprueba tu conexión a internet e inténtalo de nuevo. NO_PKG_ERR=El paquete solicitado no está disponible en este momento, inténtalo de nuevo más tarde. diff --git a/mod/etc/options_menu/language/fr-FR.lang b/mod/etc/options_menu/language/fr-FR.lang index 86dbfed..aa41da0 100644 --- a/mod/etc/options_menu/language/fr-FR.lang +++ b/mod/etc/options_menu/language/fr-FR.lang @@ -47,6 +47,6 @@ DELETE_CONFIRM_GENERIC=Supprimer cet élément? WIFI_DELETE_HINT_FOOTER=X = Supprimer A = Restaurer HINT_SELECT=Sélect. HINT_BACK=Retour -HINT_DELETE=Suppr. +HINT_DELETE=Suppr. (Maintenir) NO_NET_ERR=Erreur de connexion au serveur, vérifiez votre connexion internet et réessayez. NO_PKG_ERR=Le paquet demandé n'est pas disponible pour le moment, veuillez réessayer plus tard. diff --git a/mod/etc/options_menu/language/it-IT.lang b/mod/etc/options_menu/language/it-IT.lang index 0114df2..6aadfd8 100644 --- a/mod/etc/options_menu/language/it-IT.lang +++ b/mod/etc/options_menu/language/it-IT.lang @@ -47,6 +47,6 @@ DELETE_CONFIRM_GENERIC=Eliminare questo elemento? WIFI_DELETE_HINT_FOOTER=X = Elimina A = Ripristina HINT_SELECT=Seleziona HINT_BACK=Indietro -HINT_DELETE=Elimina +HINT_DELETE=Elimina (tieni premuto) NO_NET_ERR=Errore di connessione al server, controlla la tua connessione internet e riprova. NO_PKG_ERR=Il pacchetto richiesto non è disponibile al momento, riprova più tardi. diff --git a/mod/etc/options_menu/language/ja-JP.lang b/mod/etc/options_menu/language/ja-JP.lang index e3aee92..34cb131 100644 --- a/mod/etc/options_menu/language/ja-JP.lang +++ b/mod/etc/options_menu/language/ja-JP.lang @@ -47,6 +47,6 @@ DELETE_CONFIRM_GENERIC=これをさくじょしますか? WIFI_DELETE_HINT_FOOTER=X = さくじょ A = もどす HINT_SELECT=けってい HINT_BACK=もどる -HINT_DELETE=けす +HINT_DELETE=けす (ながおし) NO_NET_ERR=Serverへのせつぞくにしっぱいしました. Internetせつぞくをかくにんして, もういちどおためしください. NO_PKG_ERR=ようきゅうされたPackageは, いま, りようできません. しばらくしてから, もういちどおためしください. diff --git a/mod/etc/options_menu/language/nl-NL.lang b/mod/etc/options_menu/language/nl-NL.lang index fd8befe..76d7439 100644 --- a/mod/etc/options_menu/language/nl-NL.lang +++ b/mod/etc/options_menu/language/nl-NL.lang @@ -47,6 +47,6 @@ DELETE_CONFIRM_GENERIC=Dit item verwijderen? WIFI_DELETE_HINT_FOOTER=X = Verwijderen A = Herstellen HINT_SELECT=Kiezen HINT_BACK=Terug -HINT_DELETE=Wissen +HINT_DELETE=Wissen (vasthouden) NO_NET_ERR=Verbinding met de server mislukt, controleer je internetverbinding en probeer het opnieuw. NO_PKG_ERR=Het gevraagde pakket is momenteel niet beschikbaar, probeer het later opnieuw. diff --git a/mod/etc/options_menu/language/pt-PT.lang b/mod/etc/options_menu/language/pt-PT.lang index c40cc26..ece3e8e 100644 --- a/mod/etc/options_menu/language/pt-PT.lang +++ b/mod/etc/options_menu/language/pt-PT.lang @@ -47,6 +47,6 @@ DELETE_CONFIRM_GENERIC=Apagar este item? WIFI_DELETE_HINT_FOOTER=X = Apagar A = Restaurar HINT_SELECT=Selecionar HINT_BACK=Voltar -HINT_DELETE=Apagar +HINT_DELETE=Apagar (manter premido) NO_NET_ERR=Erro ao ligar ao servidor, verifica a tua ligação à internet e tenta novamente. NO_PKG_ERR=O pacote pedido não está disponível de momento, tenta novamente mais tarde. diff --git a/mod/etc/options_menu/language/ru-RU.lang b/mod/etc/options_menu/language/ru-RU.lang index 6f7aa40..95957f5 100644 --- a/mod/etc/options_menu/language/ru-RU.lang +++ b/mod/etc/options_menu/language/ru-RU.lang @@ -47,6 +47,6 @@ DELETE_CONFIRM_GENERIC=Удалить этот элемент? WIFI_DELETE_HINT_FOOTER=X = Удалить A = Восстановить HINT_SELECT=Выбрать HINT_BACK=Назад -HINT_DELETE=Удалить +HINT_DELETE=Удалить (удерж.) NO_NET_ERR=Ошибка подключения к серверу, проверьте подключение к интернету и попробуйте снова. NO_PKG_ERR=Запрошенный пакет сейчас недоступен, попробуйте позже. diff --git a/mod/etc/options_menu/network/commands/c9998_separator b/mod/etc/options_menu/network/commands/c9998_separator deleted file mode 100644 index e69de29..0000000 diff --git a/mod/etc/options_menu/network/commands/c9999_Back b/mod/etc/options_menu/network/commands/c9999_Back deleted file mode 100644 index c605f4b..0000000 --- a/mod/etc/options_menu/network/commands/c9999_Back +++ /dev/null @@ -1,5 +0,0 @@ -COMMAND_NAME=BACK -COMMAND_TYPE=EXTERNAL -RESTART_UI=FALSE -COMMAND_STR=usleep 50000 && %options_path%/options & -PREVIEW_IMAGE=/etc/options_menu/images/preview_placeholder.png diff --git a/mod/etc/options_menu/network/commands/c9999_Exit b/mod/etc/options_menu/network/commands/c9999_Exit deleted file mode 100644 index 804d7ff..0000000 --- a/mod/etc/options_menu/network/commands/c9999_Exit +++ /dev/null @@ -1,5 +0,0 @@ -COMMAND_NAME=EXIT -COMMAND_TYPE=EXTERNAL -RESTART_UI=TRUE -COMMAND_STR=echo "Closing Options Menu" -PREVIEW_IMAGE=/etc/options_menu/images/preview_placeholder.png diff --git a/mod/etc/options_menu/network/restore/commands/c9998_separator b/mod/etc/options_menu/network/restore/commands/c9998_separator deleted file mode 100644 index e69de29..0000000 diff --git a/mod/etc/options_menu/network/restore/commands/c9999_Back b/mod/etc/options_menu/network/restore/commands/c9999_Back deleted file mode 100644 index cf28a50..0000000 --- a/mod/etc/options_menu/network/restore/commands/c9999_Back +++ /dev/null @@ -1,5 +0,0 @@ -COMMAND_NAME=BACK -COMMAND_TYPE=EXTERNAL -RESTART_UI=FALSE -COMMAND_STR=usleep 50000 && %options_path%/options --commandPath %options_path%/network/commands/ --scriptPath %options_path%/network/scripts --title "NETWORK_OPTIONS" & -PREVIEW_IMAGE=/etc/options_menu/images/preview_placeholder.png diff --git a/mod/etc/options_menu/retroarch/commands/c9998_separator b/mod/etc/options_menu/retroarch/commands/c9998_separator deleted file mode 100644 index e69de29..0000000 diff --git a/mod/etc/options_menu/retroarch/commands/c9999_Back b/mod/etc/options_menu/retroarch/commands/c9999_Back deleted file mode 100644 index c605f4b..0000000 --- a/mod/etc/options_menu/retroarch/commands/c9999_Back +++ /dev/null @@ -1,5 +0,0 @@ -COMMAND_NAME=BACK -COMMAND_TYPE=EXTERNAL -RESTART_UI=FALSE -COMMAND_STR=usleep 50000 && %options_path%/options & -PREVIEW_IMAGE=/etc/options_menu/images/preview_placeholder.png diff --git a/mod/etc/options_menu/retroarch/commands/c9999_Exit b/mod/etc/options_menu/retroarch/commands/c9999_Exit deleted file mode 100644 index 804d7ff..0000000 --- a/mod/etc/options_menu/retroarch/commands/c9999_Exit +++ /dev/null @@ -1,5 +0,0 @@ -COMMAND_NAME=EXIT -COMMAND_TYPE=EXTERNAL -RESTART_UI=TRUE -COMMAND_STR=echo "Closing Options Menu" -PREVIEW_IMAGE=/etc/options_menu/images/preview_placeholder.png diff --git a/src/command.cpp b/src/command.cpp index 359b78a..50bd998 100644 --- a/src/command.cpp +++ b/src/command.cpp @@ -9,6 +9,7 @@ #include "command.h" #include "framework/controller.h" +#include "framework/draw_helpers.h" #include "localization.h" #include @@ -78,10 +79,11 @@ Command::Command(std::ifstream & in) else if(param.compare("PREVIEW_IMAGE_HEIGHT")==0) previewImageHeight = SafeStoi(value, -1); } + hasSubmenu = !isToggle && command.find("--commandPath") != std::string::npos; in.close(); } -void Command::RunCommand(SDL_Context & sdl_context, Controller * controller, Sprite & menuL, Sprite & menuU, bool modernUI, const NineSlice & frame, Uint8 bgR, Uint8 bgG, Uint8 bgB) const +void Command::RunCommand(SDL_Context & sdl_context, Controller * controller, Sprite & menuL, Sprite & menuU, bool modernUI, const ModernChrome & chrome, Uint8 bgR, Uint8 bgG, Uint8 bgB) const { std::list textList; FILE* pipe = popen(command.c_str(), "r"); @@ -96,11 +98,32 @@ void Command::RunCommand(SDL_Context & sdl_context, Controller * controller, Spr const int textX = modernUI ? UiTheme::FrameRect.x + UiTheme::FrameInset : 30; const int textFirstY = modernUI ? UiTheme::HeaderDividerY + 24 : 100; + // same app header/footer chrome as the main screen, plus a persistent B/Exit badge + Texture exitLetter("B", 16, renderer, 0, 0, false, UiTheme::BadgeLetterColor, true); + Texture exitLabel(Translate("EXIT"), 16, renderer, 0, 0, false, 0xFFFFFFFF, true); + int badgeGroupW = UiTheme::BadgeOuterSize + UiTheme::BadgeLabelGap + exitLabel.rect.w; + SDL_Rect exitBadge{ UiTheme::BadgeClusterRightX - badgeGroupW, UiTheme::BadgeBandY, UiTheme::BadgeOuterSize, UiTheme::BadgeOuterSize }; + exitLetter.rect.x = exitBadge.x + (exitBadge.w - exitLetter.rect.w) / 2; + exitLetter.rect.y = exitBadge.y + (exitBadge.h - exitLetter.rect.h) / 2; + exitLabel.rect.x = exitBadge.x + exitBadge.w + UiTheme::BadgeLabelGap; + exitLabel.rect.y = exitBadge.y + (exitBadge.h - exitLabel.rect.h) / 2; + auto render = [&](Texture * closeText = nullptr) { sdl_context.StartFrame(); if(modernUI) - frame.Draw(renderer, UiTheme::FrameRect); + { + DrawStrokeRect(renderer, UiTheme::OuterRect, UiTheme::BorderR, UiTheme::BorderG, UiTheme::BorderB, UiTheme::BorderWidth, UiTheme::BorderRadius); + chrome.gearIcon.Draw(renderer); + chrome.appTitleText.Draw(renderer); + chrome.appVersionText.Draw(renderer); + DrawHLine(renderer, UiTheme::HeaderDividerX, UiTheme::HeaderDividerX + UiTheme::HeaderDividerW, UiTheme::HeaderDividerY, UiTheme::BorderR, UiTheme::BorderG, UiTheme::BorderB, UiTheme::BorderWidth); + DrawHLine(renderer, UiTheme::OuterRect.x, UiTheme::OuterRect.x + UiTheme::OuterRect.w, UiTheme::FooterDividerY, UiTheme::BorderR, UiTheme::BorderG, UiTheme::BorderB, UiTheme::BorderWidth); + chrome.creditText.Draw(renderer); + DrawRoundedFillRect(renderer, exitBadge, UiTheme::BadgeB.r, UiTheme::BadgeB.g, UiTheme::BadgeB.b, exitBadge.w/2); + exitLetter.Draw(renderer); + exitLabel.Draw(renderer); + } else { menuU.Draw(renderer); @@ -115,6 +138,8 @@ void Command::RunCommand(SDL_Context & sdl_context, Controller * controller, Spr } if(closeText) closeText->Draw(renderer); + if(modernUI) + SDL_SetRenderDrawColor(renderer, UiTheme::BgR, UiTheme::BgG, UiTheme::BgB, 0xFF); // must be the LAST color-setting call, or it leaks into next frame's clear sdl_context.EndFrame(); }; @@ -145,12 +170,13 @@ void Command::RunCommand(SDL_Context & sdl_context, Controller * controller, Spr render(); } pclose(pipe); - Texture closeText(Translate("PRESS_B_EXIT"), 12, renderer, textX, modernUI ? UiTheme::CreditY : 610); + // modern UI's badge already says "B: Back" - the old text hint is classic UI only + Texture closeText(Translate("PRESS_B_EXIT"), 12, renderer, textX, 610); while (!controller->GetButtonStatus(B)) { controller->Update(); - render(&closeText); + render(modernUI ? nullptr : &closeText); } SDL_SetRenderDrawColor(renderer, bgR, bgG, bgB, 0xFF); } diff --git a/src/command.h b/src/command.h index 1204a7b..2c89bc7 100644 --- a/src/command.h +++ b/src/command.h @@ -14,11 +14,20 @@ class Controller; +// header/footer chrome shared with the main screen, reused as-is for pixel-identical INTERNAL output screens +struct ModernChrome +{ + Texture & gearIcon; + Texture & appTitleText; + Texture & appVersionText; + Texture & creditText; +}; + struct Command { Command(); Command(std::ifstream & in); - void RunCommand(SDL_Context & sdl_context, Controller * controller, Sprite & menuL, Sprite & menuU, bool modernUI, const NineSlice & frame, Uint8 bgR = 0x6e, Uint8 bgG = 0x6e, Uint8 bgB = 0x6e) const; + void RunCommand(SDL_Context & sdl_context, Controller * controller, Sprite & menuL, Sprite & menuU, bool modernUI, const ModernChrome & chrome, Uint8 bgR = 0x6e, Uint8 bgG = 0x6e, Uint8 bgB = 0x6e) const; void UpdateState(); std::string name; @@ -33,6 +42,7 @@ struct Command bool child = false; bool isToggle = false; bool stateOn = false; + bool hasSubmenu = false; // COMMAND_STR relaunches options with a different --commandPath Texture texture; std::string previewImage; int previewImageX = 920; // default position when a command sets PREVIEW_IMAGE without _X/_Y diff --git a/src/framework/draw_helpers.cpp b/src/framework/draw_helpers.cpp new file mode 100644 index 0000000..3ba74c1 --- /dev/null +++ b/src/framework/draw_helpers.cpp @@ -0,0 +1,124 @@ +/** + * Copyright (c) 2026 DefKorns (https://defkorns.github.io/LICENSE) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "draw_helpers.h" + +#include +#include +#include + +// corner arc points within [radius-width, radius], cached per (radius,width) +static const std::vector & RoundedCornerArc(int radius, int width) +{ + static std::map, std::vector> cache; + auto key = std::make_pair(radius, width); + auto it = cache.find(key); + if(it != cache.end()) + return it->second; + std::vector pts; + for(int dy = 0; dy <= radius; ++dy) + for(int dx = 0; dx <= radius; ++dx) + { + double dist = std::sqrt(static_cast(dx*dx + dy*dy)); + if(dist <= radius + 0.5 && dist > radius - width + 0.5) + pts.push_back({dx, dy}); + } + return cache.emplace(key, std::move(pts)).first->second; +} + +void DrawStrokeRect(SDL_Renderer * r, SDL_Rect rect, Uint8 cr, Uint8 cg, Uint8 cb, int width, int radius) +{ + SDL_SetRenderDrawColor(r, cr, cg, cb, 0xFF); + int x0 = rect.x, y0 = rect.y, x1 = rect.x+rect.w-1, y1 = rect.y+rect.h-1; + for(int i = 0; i < width; ++i) + { + SDL_RenderDrawLine(r, x0+radius, y0+i, x1-radius, y0+i); // top + SDL_RenderDrawLine(r, x0+radius, y1-i, x1-radius, y1-i); // bottom + SDL_RenderDrawLine(r, x0+i, y0+radius, x0+i, y1-radius); // left + SDL_RenderDrawLine(r, x1-i, y0+radius, x1-i, y1-radius); // right + } + if(radius <= 0) + return; + const std::vector & arc = RoundedCornerArc(radius, width); + std::vector points; + points.reserve(arc.size() * 4); + for(const SDL_Point & p : arc) + { + points.push_back({ x0+radius-p.x, y0+radius-p.y }); // top-left + points.push_back({ x1-radius+p.x, y0+radius-p.y }); // top-right + points.push_back({ x0+radius-p.x, y1-radius+p.y }); // bottom-left + points.push_back({ x1-radius+p.x, y1-radius+p.y }); // bottom-right + } + SDL_RenderDrawPoints(r, points.data(), static_cast(points.size())); +} + +void DrawFillRect(SDL_Renderer * r, SDL_Rect rect, Uint8 cr, Uint8 cg, Uint8 cb) +{ + SDL_SetRenderDrawColor(r, cr, cg, cb, 0xFF); + SDL_RenderFillRect(r, &rect); +} + +// like RoundedCornerArc but the full quarter-disk, for a solid fill +static const std::vector & RoundedCornerDisk(int radius) +{ + static std::map> cache; + auto it = cache.find(radius); + if(it != cache.end()) + return it->second; + std::vector pts; + for(int dy = 0; dy <= radius; ++dy) + for(int dx = 0; dx <= radius; ++dx) + if(std::sqrt(static_cast(dx*dx + dy*dy)) <= radius + 0.5) + pts.push_back({dx, dy}); + return cache.emplace(radius, std::move(pts)).first->second; +} + +void DrawRoundedFillRect(SDL_Renderer * r, SDL_Rect rect, Uint8 cr, Uint8 cg, Uint8 cb, int radius) +{ + SDL_SetRenderDrawColor(r, cr, cg, cb, 0xFF); + if(radius <= 0) + { + SDL_RenderFillRect(r, &rect); + return; + } + int x0 = rect.x, y0 = rect.y, x1 = rect.x+rect.w-1, y1 = rect.y+rect.h-1; + SDL_Rect mid{ x0, y0+radius, rect.w, rect.h-2*radius }; + SDL_Rect midV{ x0+radius, y0, rect.w-2*radius, rect.h }; + SDL_RenderFillRect(r, &mid); + SDL_RenderFillRect(r, &midV); + const std::vector & disk = RoundedCornerDisk(radius); + std::vector points; + points.reserve(disk.size() * 4); + for(const SDL_Point & p : disk) + { + points.push_back({ x0+radius-p.x, y0+radius-p.y }); + points.push_back({ x1-radius+p.x, y0+radius-p.y }); + points.push_back({ x0+radius-p.x, y1-radius+p.y }); + points.push_back({ x1-radius+p.x, y1-radius+p.y }); + } + SDL_RenderDrawPoints(r, points.data(), static_cast(points.size())); +} + +void DrawHLine(SDL_Renderer * r, int x0, int x1, int y, Uint8 cr, Uint8 cg, Uint8 cb, int width) +{ + DrawFillRect(r, { x0, y, x1-x0, width }, cr, cg, cb); +} + +void DrawVLine(SDL_Renderer * r, int x, int y0, int y1, Uint8 cr, Uint8 cg, Uint8 cb, int width) +{ + DrawFillRect(r, { x, y0, width, y1-y0 }, cr, cg, cb); +} diff --git a/src/framework/draw_helpers.h b/src/framework/draw_helpers.h new file mode 100644 index 0000000..cedd589 --- /dev/null +++ b/src/framework/draw_helpers.h @@ -0,0 +1,30 @@ +/** + * Copyright (c) 2026 DefKorns (https://defkorns.github.io/LICENSE) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef DRAW_HELPERS_H_ +#define DRAW_HELPERS_H_ + +#include + +// flat-line drawing for Modern UI - no textured art +void DrawStrokeRect(SDL_Renderer * r, SDL_Rect rect, Uint8 cr, Uint8 cg, Uint8 cb, int width, int radius = 0); +void DrawFillRect(SDL_Renderer * r, SDL_Rect rect, Uint8 cr, Uint8 cg, Uint8 cb); +void DrawRoundedFillRect(SDL_Renderer * r, SDL_Rect rect, Uint8 cr, Uint8 cg, Uint8 cb, int radius); +void DrawHLine(SDL_Renderer * r, int x0, int x1, int y, Uint8 cr, Uint8 cg, Uint8 cb, int width = 1); +void DrawVLine(SDL_Renderer * r, int x, int y0, int y1, Uint8 cr, Uint8 cg, Uint8 cb, int width = 1); + +#endif diff --git a/src/framework/sdl_context.cpp b/src/framework/sdl_context.cpp index eb999d5..aae4f4e 100644 --- a/src/framework/sdl_context.cpp +++ b/src/framework/sdl_context.cpp @@ -10,6 +10,7 @@ #include "sdl_context.h" #include "powerwatch.h" +#include #include #include #include @@ -18,8 +19,15 @@ SDL_Context::SDL_Context(std::chrono::milliseconds fpsTime, bool powerButtonExit) : fpsTime(fpsTime), powerButtonExit(powerButtonExit) { powerwatch = new PowerWatch(); + // SDL defaults to nearest-neighbor texture scaling, which makes any + // upscaled art (e.g. a preview image scaled to fit its box) look + // blocky/jagged instead of the smooth PNG it actually is + SDL_SetHint(SDL_HINT_RENDER_SCALE_QUALITY, "1"); window = SDL_CreateWindow("", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, 1280, 720, SDL_WINDOW_FULLSCREEN); - renderer = SDL_CreateRenderer(window, -1, 0); + // vsync makes SDL_RenderPresent block for the next vertical blank instead + // of returning immediately, so a post-pause frame burst (see EndFrame) + // can't outrun the display and tear/flicker the way it could unthrottled + renderer = SDL_CreateRenderer(window, -1, SDL_RENDERER_PRESENTVSYNC); if(renderer == NULL) { std::cerr << "Cannot create renderer.\n"; SDL_DestroyWindow(window); @@ -31,8 +39,16 @@ SDL_Context::SDL_Context(std::chrono::milliseconds fpsTime, bool powerButtonExit SDL_Context::~SDL_Context() { - TTF_Quit(); + Shutdown(); delete powerwatch; +} + +void SDL_Context::Shutdown() +{ + if(shutDown) + return; + shutDown = true; + TTF_Quit(); SDL_DestroyRenderer(renderer); SDL_DestroyWindow(window); SDL_Quit(); @@ -49,5 +65,10 @@ void SDL_Context::EndFrame() { SDL_RenderPresent(renderer); std::this_thread::sleep_until(nextFrameTime); - nextFrameTime += fpsTime; + // after a long stall (e.g. hakchi's "uipause" SIGSTOPs this process + // while it reads /dev/fb0 for a screenshot), nextFrameTime is left far + // in the past - clamp to now so resuming doesn't burn through a burst + // of back-to-back unthrottled frames trying to catch up, which is what + // caused the flicker after taking a screenshot + nextFrameTime = std::max(nextFrameTime, std::chrono::system_clock::now()) + fpsTime; } diff --git a/src/framework/sdl_context.h b/src/framework/sdl_context.h index ae87d39..0f7e6a5 100644 --- a/src/framework/sdl_context.h +++ b/src/framework/sdl_context.h @@ -25,6 +25,10 @@ struct SDL_Context ~SDL_Context(); void StartFrame(); void EndFrame(); + // releases the renderer/window now instead of waiting for the destructor - + // call before system()-launching a replacement options process, so that + // process isn't racing this one for the display (see main.cpp) + void Shutdown(); SDL_Window* window; SDL_Renderer* renderer; @@ -34,6 +38,7 @@ struct SDL_Context private: std::chrono::time_point nextFrameTime; bool powerButtonExit; + bool shutDown = false; }; #endif diff --git a/src/framework/uitheme.h b/src/framework/uitheme.h index 8e2f5f7..b532ca4 100644 --- a/src/framework/uitheme.h +++ b/src/framework/uitheme.h @@ -20,64 +20,47 @@ #include -// Geometry and palette for the "settings dialog" look. Screen is a fixed -// 1280x720 (sdl_context.cpp), so these are plain constants. +// Geometry and palette for the split-panel "settings dialog" look. Screen +// is a fixed 1280x720 (sdl_context.cpp), so these are plain constants. +// Border/dividers are flat drawn lines (see main.cpp's DrawRect/FillRect/ +// HLine/VLine helpers), not the old frame.png/highlight.png NineSlice art - +// those two assets are unused by Modern UI now. namespace UiTheme { - const Uint8 BgR = 0x0A, BgG = 0x0A, BgB = 0x0C; + const Uint8 BgR = 0x0C, BgG = 0x0F, BgB = 0x16; + const Uint8 BorderR = 0x2A, BorderG = 0x2C, BorderB = 0x33; + const Uint8 AccentR = 0xF0, AccentG = 0xC4, AccentB = 0x3D; // yellow + const Uint8 SelectedRowBgR = 0x14, SelectedRowBgG = 0x16, SelectedRowBgB = 0x1A; // TV overscan safe area - const int SafeMarginX = 48; + const int SafeMarginX = 64; const int SafeMarginY = 36; + const int BorderWidth = 3; + const int BorderRadius = 14; + const int BoxRadius = 8; // selected row / detail icon box / preview box + const int ContentPadding = 16; // border to content start + const int FrameInset = 24; // frame.png's 9-slice corner size (delete-confirm modal only) const SDL_Rect FrameRect{ SafeMarginX, SafeMarginY, 1280 - 2*SafeMarginX, 720 - 2*SafeMarginY }; - const int FrameInset = 24; // frame.png's 9-slice corner size - - // selection highlight bar - const int HighlightInsetLR = 8; - const int HighlightX = FrameRect.x + 16; - const int HighlightW = (FrameRect.x + FrameRect.w - FrameInset) - HighlightX; - const int HighlightH = 20; - const Uint8 HighlightR = 0x3F, HighlightG = 0x59, HighlightB = 0x7F; - - // right edge for switches/highlight on rows with a PREVIEW_IMAGE - fixed, - // not derived from PREVIEW_IMAGE_X, so it doesn't move as that's tuned - const int ControlColumnRightX = 760; - const int HighlightWWithPreview = ControlColumnRightX - HighlightX; - - // row layout - const int RowFirstY = FrameRect.y + 236; - const int RowPitch = 22; - const int RowTextX = FrameRect.x + 40; - const int DisplayItemCount = 16; // rows visible at once, both UI styles - - // badge cluster (A/B always shown, X shown per-row when it has a delete action) - const int BadgeOuterSize = 28; - const int BadgeInnerSize = 22; - const int BadgeBandY = FrameRect.y + 22; - const int BadgeClusterRightX = FrameRect.x + FrameRect.w - 24; - const int BadgeGroupGap = 24; - const int BadgeLabelGap = 6; - const Uint32 BadgeLetterColor = 0xFF16161C; // AABBGGRR - - struct BadgeColor { Uint8 r, g, b; }; - const BadgeColor BadgeA{ 0xE0, 0xA4, 0x58 }; // amber - const BadgeColor BadgeADark{ 0xA0, 0x6D, 0x2E }; - const BadgeColor BadgeB{ 0x4F, 0xB0, 0xA5 }; // teal - const BadgeColor BadgeBDark{ 0x2C, 0x72, 0x68 }; - const BadgeColor BadgeX{ 0x6C, 0x8E, 0xBF }; // slate-blue - const BadgeColor BadgeXDark{ 0x3F, 0x59, 0x7F }; - const BadgeColor BadgeY{ 0xC9, 0x7B, 0x84 }; // dusty rose, unused - const BadgeColor BadgeYDark{ 0x8F, 0x47, 0x50 }; + const int FrameX = FrameRect.x + ContentPadding; + const int FrameY = FrameRect.y + ContentPadding; + const int FrameW = FrameRect.w - 2*ContentPadding; + + const int HeaderH = 64; + const int FooterGap = 16; + const int FooterH = 50; + const int BodyH = FrameRect.h - 2*ContentPadding - FooterGap - FooterH; + const int FooterY = FrameY + BodyH + FooterGap; + const int FooterDividerY = FooterY - FooterGap/2; + const SDL_Rect OuterRect{ FrameRect.x, FrameRect.y, FrameRect.w, (FooterY+FooterH+ContentPadding) - FrameRect.y }; // header/body divider - const int HeaderDividerY = FrameRect.y + 76; - const int HeaderDividerH = 2; - const int HeaderDividerX = FrameRect.x + FrameInset; - const int HeaderDividerW = FrameRect.w - 2 * FrameInset; + const int HeaderDividerY = FrameY + HeaderH; + const int HeaderDividerX = FrameX; + const int HeaderDividerW = FrameW; // gear icon - const int GearX = FrameRect.x + 32, GearY = FrameRect.y + 22; + const int GearX = FrameX, GearY = FrameY + 4; const int GearSize = 32; // "OptionsMenu" + smaller "vX.Y.Z", right of the gear @@ -88,25 +71,74 @@ namespace UiTheme const int TitleX = GearX + GearSize + TitleGap; const int TitleY = GearY + GearSize / 2; - // current screen's title, centered below the divider + // current screen's title, left-aligned next to a small accent bar const int SectionTitleFontSize = 28; - const int SectionTitleY = FrameRect.y + 116; - const int SectionTitleCenterX = FrameRect.x + FrameRect.w / 2; - - // toggle switch, right-aligned to the control column - const int SwitchRightX = ControlColumnRightX; + const int SectionAccentBarW = 3; + const int SectionAccentBarH = 26; + const int SectionTitleY = HeaderDividerY + 22; + const int SectionTitleX = FrameX + 32 + 14 + SectionAccentBarW; // ListX + accent bar + gap + + // row list (left column) + const int ListX = FrameX + 32; + const int ListW = 800; + const int ListContentRightX = ListX + ListW - 40; // row box / divider right edge + const int RowControlRightX = ListContentRightX - 16; // switch/chevron align here - matches RowTextX's 16px left inset + const int RowFirstY = SectionTitleY + 46; // clears the section title's own text height at SectionTitleFontSize + const int RowPitch = 36; + const int RowTextYNudge = -4; // fine-tune vs. pure (slot-h - text-h)/2 centering + const int RowTextX = ListX + 16; + const int DisplayItemCount = 12; // rows visible at once in the scrollable region, both UI styles + const int PinnedBottomMargin = 10; // gap from the footer divider to the last pinned row (e.g. Exit) + + // detail panel (right column), Modern UI only - just the preview image, + // fixed Y independent of scroll/selection (it must not move as the list + // scrolls). No icon box/title - nothing to show without per-command art. + const int DetailX = ListX + ListW + 16; + const int DetailW = (FrameX + FrameW - 16) - DetailX; + const int PreviewBoxH = 200; + // centered between the header and footer dividers, not anchored to the top + const int PreviewBoxY = HeaderDividerY + ((FooterDividerY - HeaderDividerY) - PreviewBoxH) / 2; + + // toggle switch + const int SwitchRightX = RowControlRightX; const int SwitchW = 40, SwitchH = 16; - // "created by CompCom" footer credit, right-aligned inside the frame - const int CreditRightX = (FrameRect.x + FrameRect.w - FrameInset) - 24; - const int CreditY = FrameRect.y + FrameRect.h - 28; + // badge cluster (A/B always shown, X shown per-row when it has a delete + // action), now in the footer instead of the header + const int BadgeOuterSize = 28; + const int BadgeInnerSize = 22; + const int BadgeBandY = FooterY + (FooterH - BadgeOuterSize) / 2; + const int BadgeClusterRightX = FrameX + FrameW - 8; + const int BadgeGroupGap = 24; + const int BadgeLabelGap = 6; + // DrawBadge's returned "next left edge" already subtracts BadgeGroupGap + // (24px) from the last-drawn badge's own left edge; this closes the gap + // to land the divider exactly 40px left of that badge, per hardware test + const int BadgeDividerGapFromCluster = 16; + const Uint32 BadgeLetterColor = 0xFF16161C; // AABBGGRR + const Uint32 TextDimColor = 0xFF938D8D; // AABBGGRR - muted grey for chevrons/secondary text + + struct BadgeColor { Uint8 r, g, b; }; + const BadgeColor BadgeA{ 0x4C, 0xAF, 0x6E }; // green + const BadgeColor BadgeADark{ 0x2E, 0x6B, 0x42 }; + const BadgeColor BadgeB{ 0xE1, 0x55, 0x54 }; // red + const BadgeColor BadgeBDark{ 0x8C, 0x34, 0x33 }; + const BadgeColor BadgeX{ 0x6C, 0x8E, 0xBF }; // slate-blue + const BadgeColor BadgeXDark{ 0x3F, 0x59, 0x7F }; + const BadgeColor BadgeY{ 0xC9, 0x7B, 0x84 }; // dusty rose, unused + const BadgeColor BadgeYDark{ 0x8F, 0x47, 0x50 }; + + // "created by CompCom" footer credit, left-aligned in the footer + const int CreditX = FrameX; + const int CreditY = FooterY + FooterH/2 - 8; - // scroll chevrons - const int ScrollX = SectionTitleCenterX - 8; - const int ScrollUpY = FrameRect.y + 216; - const int ScrollDownY = RowFirstY + DisplayItemCount * RowPitch + 8; + // scroll chevrons - in the empty gutter left of the list, not over it + const int ScrollX = FrameX + (ListX - FrameX) / 2 - 7; + const int ScrollUpY = HeaderDividerY + 90; + const int ScrollDownY = FooterDividerY - 104; - // asset paths, relative to optionsLocation + // asset paths, relative to optionsLocation (frame/highlight unused by + // Modern UI now - kept for Classic UI / potential rollback) const char * const AssetFrame = "/images/ui/frame.png"; const char * const AssetHighlight = "/images/ui/highlight.png"; const char * const AssetBadgeOuter = "/images/ui/badge_outer.png"; diff --git a/src/main.cpp b/src/main.cpp index ac3195a..0385343 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -10,14 +10,18 @@ #include "framework/sdl_helper.h" #include "framework/controller.h" #include "framework/powerwatch.h" +#include "framework/draw_helpers.h" #include "command.h" #include "localization.h" #include +#include #include #include #include #include +#include +#include #include #ifndef MOD_VERSION @@ -99,6 +103,10 @@ int main(int argc, char * argv[]) bool modernUI = (uiStyle != "classic"); std::string titleString(Translate("OPTIONS_TITLE")); + std::string titleKey = "OPTIONS_TITLE"; + // ancestors, root-first: "path,scriptPath,titleKey;..." - inherited via env, no argv needed + const char * backStackEnv = getenv("OM_BACK_STACK"); + std::string backStack = backStackEnv ? backStackEnv : ""; //Check for external drive bool usbReady = false; @@ -123,11 +131,16 @@ int main(int argc, char * argv[]) } else if(strcmp(argv[i], "--title") == 0) { + titleKey = argv[i+1]; titleString = Translate(argv[i+1]); ++i; } } + // what a child screen inherits as its own OM_BACK_STACK + std::string myEntry = commandLocation + "," + scriptLocation + "," + titleKey; + setenv("OM_BACK_STACK", (backStack.empty() ? myEntry : backStack + ";" + myEntry).c_str(), 1); + //Read commands from command folder std::vector commands; if(auto dir = opendir(commandLocation.c_str())) @@ -181,8 +194,54 @@ int main(int argc, char * argv[]) exit(1); } + // synthesized, not read from c9999_Back/c9999_Exit files - Back only if backStack has an entry to pop + static const char * const NavPreviewImage = "/etc/options_menu/images/preview_placeholder.png"; + const int pinnedStartIndex = static_cast(commands.size()); + if(!backStack.empty()) + { + // pop the last entry (our parent), re-join the rest for the parent's own Back chain + std::vector entries; + for(size_t start = 0; start < backStack.size();) + { + size_t sep = backStack.find(';', start); + if(sep == std::string::npos) sep = backStack.size(); + entries.push_back(backStack.substr(start, sep-start)); + start = sep+1; + } + std::string lastEntry = entries.back(); + entries.pop_back(); + std::string remainingStack; + for(size_t i = 0; i < entries.size(); ++i) + { + if(i) remainingStack += ";"; + remainingStack += entries[i]; + } + + size_t c1 = lastEntry.find(','), c2 = lastEntry.find(',', c1+1); + std::string backPath = lastEntry.substr(0, c1); + std::string backScriptPath = lastEntry.substr(c1+1, c2-c1-1); + std::string backTitleKey = lastEntry.substr(c2+1); + + Command back; + back.name = "BACK"; + back.runInternal = false; + back.restartUI = false; + back.previewImage = NavPreviewImage; + // env prefix overrides OM_BACK_STACK for just this launch, to the parent's own remaining ancestors + back.command = "usleep 50000 && OM_BACK_STACK=\"" + remainingStack + "\" " + optionsLocation + "options --commandPath " + backPath + + (backScriptPath.empty() ? "" : " --scriptPath " + backScriptPath) + + " --title \"" + backTitleKey + "\" &"; + commands.push_back(back); + } + Command exitCmd; + exitCmd.name = "EXIT"; + exitCmd.runInternal = false; + exitCmd.restartUI = true; + exitCmd.previewImage = NavPreviewImage; + exitCmd.command = "echo \"Closing Options Menu\""; + commands.push_back(exitCmd); + int currentCommandId = 0; - const int DisplayItemCount = UiTheme::DisplayItemCount; //Create SDL Window/Renderer and Controller Handler SDL_Context sdl_context(std::chrono::milliseconds(33), false); @@ -202,9 +261,8 @@ int main(int argc, char * argv[]) const Uint8 bgB = modernUI ? UiTheme::BgB : 0x6e; SDL_SetRenderDrawColor(renderer, bgR, bgG, bgB, 0xFF); - //Create dialog frame + selection highlight (own art, not console theme sprites) + // only the delete-confirm modal still uses this (NineSlice) NineSlice frame(optionsLocation + UiTheme::AssetFrame, renderer, UiTheme::FrameInset, UiTheme::FrameInset, UiTheme::FrameInset, UiTheme::FrameInset); - NineSlice highlight(optionsLocation + UiTheme::AssetHighlight, renderer, UiTheme::HighlightInsetLR, UiTheme::HighlightInsetLR, 0, 0); Texture gearIcon(optionsLocation + UiTheme::AssetGear, renderer, UiTheme::GearX, UiTheme::GearY); Texture switchOn(optionsLocation + UiTheme::AssetSwitchOn, renderer); Texture switchOff(optionsLocation + UiTheme::AssetSwitchOff, renderer); @@ -212,38 +270,36 @@ int main(int argc, char * argv[]) Texture badgeInner(optionsLocation + UiTheme::AssetBadgeInner, renderer); //Create Textures for Strings - // appTitleText/appVersionText only ever draw in the modernUI branch (see the render loop below) + // only drawn in the modernUI branch Texture appTitleText("OptionsMenu", UiTheme::TitleFontSize, renderer, UiTheme::TitleX, UiTheme::TitleY, false, 0xFFFFFFFF, true); appTitleText.rect.y -= appTitleText.rect.h / 2; // vertically center on the gear (Texture only supports centering both axes together) Texture appVersionText(MOD_VERSION, UiTheme::VersionFontSize, renderer, appTitleText.rect.x + appTitleText.rect.w + UiTheme::VersionGap, UiTheme::TitleY, false, 0xFFFFFFFF, true); appVersionText.rect.y -= appVersionText.rect.h / 2; Texture titleText = modernUI - ? Texture(titleString, UiTheme::SectionTitleFontSize, renderer, UiTheme::SectionTitleCenterX, UiTheme::SectionTitleY, true, 0xFFFFFFFF, true) + ? Texture(titleString, UiTheme::SectionTitleFontSize, renderer, UiTheme::SectionTitleX, UiTheme::SectionTitleY, false, 0xFFFFFFFF, true) : Texture(titleString, 36, renderer, 640, 185, true); - SDL_Rect highlightRect{ UiTheme::HighlightX, UiTheme::RowFirstY - 2, UiTheme::HighlightW, UiTheme::HighlightH }; + SDL_Rect selectedRowRect{ UiTheme::ListX, UiTheme::RowFirstY - 2, UiTheme::ListContentRightX - UiTheme::ListX, 0 }; Texture pointerText("->", 16, renderer, 20, UiTheme::RowFirstY, false, 0xFF00FF00); SDL_Rect & pointerRect = pointerText.rect; Texture CompComText = modernUI - ? Texture("created by CompCom", 16, renderer, 0, 0, false, 0xFFFFFFFF, true) + ? Texture("created by CompCom - Modern UI by DefKorns", 16, renderer, UiTheme::CreditX, UiTheme::CreditY, false, 0xFFFFFFFF, true) : Texture("created by CompCom", 16, renderer, 1100, 620, true); - if(modernUI) - { - CompComText.rect.x = UiTheme::CreditRightX - CompComText.rect.w; - CompComText.rect.y = UiTheme::CreditY; - } Texture deleteHint(Translate("WIFI_DELETE_HINT_FOOTER"), 16, renderer, 30, 612); Texture scrollUp = modernUI ? Texture("^", 16, renderer, UiTheme::ScrollX, UiTheme::ScrollUpY, false, 0xFFFFFFFF, true) : Texture("^", 16, renderer, 30, 248); + if(modernUI) + scrollUp.rect.x -= scrollUp.rect.w / 2; // ScrollX is the gutter's center, not the glyph's left edge Texture scrollDown = scrollUp; - scrollDown.rect.y = modernUI ? UiTheme::ScrollDownY : (252+DisplayItemCount*18); + scrollDown.rect.y = modernUI ? UiTheme::ScrollDownY : (252+UiTheme::DisplayItemCount*18); //Badge cluster (top-right hints): A/B always shown, X only if the row has a delete action // badges only ever exist in modernUI, so their textures are unconditionally TTF struct Badge { Texture letter; Texture label; UiTheme::BadgeColor rim; UiTheme::BadgeColor fill; }; Badge badgeA{ Texture("A", 16, renderer, 0, 0, false, UiTheme::BadgeLetterColor, true), Texture(Translate("HINT_SELECT"), 16, renderer, 0, 0, false, 0xFFFFFFFF, true), UiTheme::BadgeADark, UiTheme::BadgeA }; Badge badgeB{ Texture("B", 16, renderer, 0, 0, false, UiTheme::BadgeLetterColor, true), Texture(Translate("HINT_BACK"), 16, renderer, 0, 0, false, 0xFFFFFFFF, true), UiTheme::BadgeBDark, UiTheme::BadgeB }; - Badge badgeX{ Texture("X", 16, renderer, 0, 0, false, UiTheme::BadgeLetterColor, true), Texture(Translate("HINT_DELETE"), 16, renderer, 0, 0, false, 0xFFFFFFFF, true), UiTheme::BadgeXDark, UiTheme::BadgeX }; + // hold-to-delete hint, different color than the real B badge + Badge badgeHold{ Texture("B", 16, renderer, 0, 0, false, UiTheme::BadgeLetterColor, true), Texture(Translate("HINT_DELETE"), 16, renderer, 0, 0, false, 0xFFFFFFFF, true), UiTheme::BadgeXDark, UiTheme::BadgeX }; auto DrawBadge = [&](Badge & badge, int rightEdgeX) -> int { int groupW = UiTheme::BadgeOuterSize + UiTheme::BadgeLabelGap + badge.label.rect.w; @@ -270,20 +326,34 @@ int main(int argc, char * argv[]) const int rowTextBaseX = modernUI ? UiTheme::RowTextX : 50; const int RowGlyphSize = 16; const int RowTextGapPx = 16; // gap kept between truncated text and preview image/switch + Texture chevronText(">", RowGlyphSize, renderer, 0, 0, false, UiTheme::TextDimColor, modernUI); for(Command & c : commands) { + // self-relaunch into the same dir (language, ModernUI toggle) isn't a submenu + if(c.hasSubmenu) + { + size_t pos = c.command.find("--commandPath"); + size_t pathStart = c.command.find_first_not_of(' ', pos + std::string("--commandPath").size()); + size_t pathEnd = c.command.find(' ', pathStart); + std::string targetPath = c.command.substr(pathStart, pathEnd - pathStart); + if(targetPath == commandLocation) + c.hasSubmenu = false; + } + // Back also relaunches elsewhere - name is the untranslated key, language-independent + if(c.name == "BACK") + c.hasSubmenu = false; + int textX = rowTextBaseX + (c.child ? ChildIndent : 0); std::string label = Translate(c.name); - // rows with a preview image, or (modern UI) a toggle switch, have less width for text + // classic UI: only rows with a preview image get less width for text. + // modern UI: the list column has a fixed width regardless (previews + // now live in the detail panel, not floating on the row). int maxRight = -1; - if(c.previewImage.size()) + if(!modernUI && c.previewImage.size()) maxRight = c.previewImageX - RowTextGapPx; - if(modernUI && c.isToggle) - { - int switchBound = UiTheme::ControlColumnRightX - RowTextGapPx; - maxRight = (maxRight < 0) ? switchBound : std::min(maxRight, switchBound); - } + if(modernUI) + maxRight = UiTheme::RowControlRightX - RowTextGapPx; bool rowUsesTTF = modernUI && CanRenderWithTTF(label, RowGlyphSize); if(maxRight >= 0) @@ -311,6 +381,24 @@ int main(int argc, char * argv[]) c.texture = Texture(label, RowGlyphSize, renderer, textX, 0, false, 0xFFFFFFFF, modernUI); } + const int modernRowPitch = std::max(UiTheme::RowPitch, GetTTFLineHeight(RowGlyphSize)); + int pinnedAreaTop = UiTheme::FooterDividerY - UiTheme::PinnedBottomMargin; + if(modernUI) + { + int slotBottom = UiTheme::FooterDividerY - UiTheme::PinnedBottomMargin; + for(int i = static_cast(commands.size())-1; i >= pinnedStartIndex; --i) + { + int slotTop = slotBottom - modernRowPitch; + commands[i].texture.rect.y = slotTop + (modernRowPitch - commands[i].texture.rect.h) / 2 + UiTheme::RowTextYNudge; + slotBottom = slotTop; + } + pinnedAreaTop = slotBottom; + } + // scrollable rows that fit above the pinned block - varies per screen + const int DisplayItemCount = modernUI + ? std::max(1, (pinnedAreaTop - UiTheme::RowFirstY) / modernRowPitch) + : UiTheme::DisplayItemCount; + int topListItemNumber = 1; std::shared_ptr PreviewImage; auto SetCurrentCommand = [&] (int newCommandId) @@ -318,58 +406,49 @@ int main(int argc, char * argv[]) currentCommandId = newCommandId; const Command & currentCommand = commands[currentCommandId]; + // pinned rows don't affect scroll position + int scrollBound = modernUI ? pinnedStartIndex : static_cast(commands.size()); + int scrollTarget = modernUI ? std::min(currentCommandId, std::max(0, pinnedStartIndex-1)) : currentCommandId; + bool updateCommandYPos = false; - if(currentCommandId < topListItemNumber) + if(scrollTarget < topListItemNumber) { - topListItemNumber = currentCommandId; + topListItemNumber = scrollTarget; updateCommandYPos = true; } - else if(currentCommandId >= topListItemNumber+DisplayItemCount) + else if(scrollTarget >= topListItemNumber+DisplayItemCount) { - topListItemNumber = currentCommandId-DisplayItemCount+1; + topListItemNumber = scrollTarget-DisplayItemCount+1; updateCommandYPos = true; } if(updateCommandYPos) { int y = UiTheme::RowFirstY; - int rowPitch = modernUI ? std::max(UiTheme::RowPitch, GetTTFLineHeight(RowGlyphSize)) : 18; - for(int i = 0, count = std::min(DisplayItemCount,static_cast(commands.size())); i < count; ++i) + int rowPitch = modernUI ? modernRowPitch : 18; + for(int i = 0, count = std::min(DisplayItemCount, scrollBound-topListItemNumber); i < count; ++i) { - commands[i+topListItemNumber].texture.rect.y = y; + Texture & rowTexture = commands[i+topListItemNumber].texture; + // centered in its slot - TTF glyphs have ascender padding, top-align leaves a gap + rowTexture.rect.y = modernUI ? (y + (rowPitch - rowTexture.rect.h) / 2 + UiTheme::RowTextYNudge) : y; y += rowPitch; } } - highlightRect.y = currentCommand.texture.rect.y - 2; - highlightRect.h = currentCommand.texture.rect.h + 4; // wrap the row's actual text height (16px font8x8 or variable-height TTF), not a fixed guess - highlightRect.w = UiTheme::HighlightW; + selectedRowRect.y = currentCommand.texture.rect.y - 2; + selectedRowRect.h = currentCommand.texture.rect.h + 4; // wrap the row's actual text height (16px font8x8 or variable-height TTF), not a fixed guess pointerRect.y = currentCommand.texture.rect.y; - if(currentCommand.previewImage.size()) + + // scaled to fit the detail panel box, ignores the command's own previewImageX/Y/W/H + if(modernUI && currentCommand.previewImage.size()) { - // fixed width so the bar doesn't run under the image or resize per-row - highlightRect.w = UiTheme::HighlightWWithPreview; - PreviewImage = std::make_shared(currentCommand.previewImage, renderer, currentCommand.previewImageX, currentCommand.previewImageY); - // guards against div-by-zero if the PNG failed to load + PreviewImage = std::make_shared(currentCommand.previewImage, renderer, 0, 0); if(PreviewImage->rect.w > 0 && PreviewImage->rect.h > 0) { - double wAspectRatio = (double)currentCommand.previewImageHeight / (double)PreviewImage->rect.h * (double)PreviewImage->rect.w; - double hAspectRatio = (double)currentCommand.previewImageWidth / (double)PreviewImage->rect.w * (double)PreviewImage->rect.h; - if (currentCommand.previewImageWidth > 0) - { - PreviewImage->rect.w = currentCommand.previewImageWidth; - if (currentCommand.previewImageHeight <= 0) - { - PreviewImage->rect.h = hAspectRatio; - } - } - if (currentCommand.previewImageHeight > 0) - { - PreviewImage->rect.h = currentCommand.previewImageHeight; - if (currentCommand.previewImageWidth <= 0) - { - PreviewImage->rect.w = wAspectRatio; - } - } + const int maxW = UiTheme::DetailW - 2*UiTheme::ContentPadding; + const int maxH = UiTheme::PreviewBoxH - 2*UiTheme::ContentPadding; + double scale = std::min((double)maxW / PreviewImage->rect.w, (double)maxH / PreviewImage->rect.h); + PreviewImage->rect.w = static_cast(PreviewImage->rect.w * scale); + PreviewImage->rect.h = static_cast(PreviewImage->rect.h * scale); } } else @@ -377,6 +456,106 @@ int main(int argc, char * argv[]) }; SetCurrentCommand(0); + auto DrawRow = [&](Command & rowCommand, bool isLastOverall) + { + bool selected = &rowCommand == &commands[currentCommandId]; + rowCommand.texture.Draw(renderer); + if(modernUI && rowCommand.isToggle) + { + Texture & rowSwitch = rowCommand.stateOn ? switchOn : switchOff; + rowSwitch.rect.x = UiTheme::SwitchRightX - UiTheme::SwitchW; + rowSwitch.rect.y = rowCommand.texture.rect.y + (rowCommand.texture.rect.h - UiTheme::SwitchH) / 2; + rowSwitch.Draw(renderer); + } + else if(modernUI && rowCommand.hasSubmenu) + { + chevronText.rect.x = UiTheme::RowControlRightX - chevronText.rect.w; + chevronText.rect.y = rowCommand.texture.rect.y + (rowCommand.texture.rect.h - chevronText.rect.h) / 2; + chevronText.Draw(renderer); + } + // skip for separators and the last row - nothing to separate there + if(modernUI && !selected && !rowCommand.command.empty() && !isLastOverall) + DrawHLine(renderer, UiTheme::ListX, UiTheme::ListContentRightX, rowCommand.texture.rect.y + rowCommand.texture.rect.h + 3, UiTheme::BorderR, UiTheme::BorderG, UiTheme::BorderB); + }; + + // modern-UI-only drawing, called early - must run before DrawRow (bg before text) + auto DrawModernUI = [&]() + { + // one continuous border across header+body+footer, plus the footer divider + DrawStrokeRect(renderer, UiTheme::OuterRect, UiTheme::BorderR, UiTheme::BorderG, UiTheme::BorderB, UiTheme::BorderWidth, UiTheme::BorderRadius); + gearIcon.Draw(renderer); + appTitleText.Draw(renderer); + appVersionText.Draw(renderer); + DrawHLine(renderer, UiTheme::HeaderDividerX, UiTheme::HeaderDividerX + UiTheme::HeaderDividerW, UiTheme::HeaderDividerY, UiTheme::BorderR, UiTheme::BorderG, UiTheme::BorderB, UiTheme::BorderWidth); + DrawHLine(renderer, UiTheme::OuterRect.x, UiTheme::OuterRect.x + UiTheme::OuterRect.w, UiTheme::FooterDividerY, UiTheme::BorderR, UiTheme::BorderG, UiTheme::BorderB, UiTheme::BorderWidth); + // vertically centered on titleText's own rendered height, not a fixed guess + int accentBarY = titleText.rect.y + (titleText.rect.h - UiTheme::SectionAccentBarH) / 2; + DrawFillRect(renderer, { UiTheme::SectionTitleX - UiTheme::SectionAccentBarW - 14, accentBarY, UiTheme::SectionAccentBarW, UiTheme::SectionAccentBarH }, UiTheme::AccentR, UiTheme::AccentG, UiTheme::AccentB); + + DrawRoundedFillRect(renderer, selectedRowRect, UiTheme::SelectedRowBgR, UiTheme::SelectedRowBgG, UiTheme::SelectedRowBgB, UiTheme::BoxRadius); + DrawStrokeRect(renderer, selectedRowRect, UiTheme::AccentR, UiTheme::AccentG, UiTheme::AccentB, 2, UiTheme::BoxRadius); + + // just the preview image - icon/title box looked wrong with no per-command art + SDL_Rect previewBox{ UiTheme::DetailX, UiTheme::PreviewBoxY, UiTheme::DetailW, UiTheme::PreviewBoxH }; + DrawStrokeRect(renderer, previewBox, UiTheme::BorderR, UiTheme::BorderG, UiTheme::BorderB, UiTheme::BorderWidth, UiTheme::BoxRadius); + if(PreviewImage.get()) + { + PreviewImage->rect.x = previewBox.x + (previewBox.w - PreviewImage->rect.w) / 2; + PreviewImage->rect.y = previewBox.y + (previewBox.h - PreviewImage->rect.h) / 2; + PreviewImage->Draw(renderer); + } + + // footer: A rightmost, B left of it, hold-to-delete hint further left + int rightEdge = UiTheme::BadgeClusterRightX; + rightEdge = DrawBadge(badgeA, rightEdge); + rightEdge = DrawBadge(badgeB, rightEdge); + if(!commands[currentCommandId].deleteCommand.empty()) + rightEdge = DrawBadge(badgeHold, rightEdge); + int dividerX = rightEdge - UiTheme::BadgeDividerGapFromCluster; + DrawVLine(renderer, dividerX, UiTheme::FooterY + 10, UiTheme::FooterY + UiTheme::FooterH - 10, UiTheme::BorderR, UiTheme::BorderG, UiTheme::BorderB, 2); + }; + + // true if deleted - caller should break out of the main loop + auto ConfirmDelete = [&]() -> bool + { + const std::string & confirmKey = commands[currentCommandId].deleteConfirmKey; + Texture confirmTitle(Translate(confirmKey.empty() ? "DELETE_CONFIRM_GENERIC" : confirmKey), 24, renderer, 640, 320, true, 0xFFFFFFFF, modernUI); + Texture confirmHint(Translate("DELETE_CONFIRM_HINT"), 16, renderer, 640, 360, true, 0xFFFFFFFF, modernUI); + bool confirmed = false; + for(;;) + { + controller.Update(); + if(controller.GetButtonStatus(B)) + break; + if(controller.GetButtonStatus(A) || controller.GetButtonStatus(START)) + { + confirmed = true; + break; + } + sdl_context.StartFrame(); + if(modernUI) + frame.Draw(renderer, UiTheme::FrameRect); + else + { + banner.Draw(renderer); + menuU.Draw(renderer); + menuL.Draw(renderer); + } + confirmTitle.Draw(renderer); + confirmHint.Draw(renderer); + sdl_context.EndFrame(); + } + if(!confirmed) + return false; + system(commands[currentCommandId].deleteCommand.c_str()); + _exitManager.runExitCommand = false; + return true; + }; + + // ~1s at this loop's ~33ms frame time, same tick-count style as daemon.cpp's L+R hold + const short bHoldThreshold = 30; + short bHoldCount = 0; + for(;;) { //Clear Buffer and Update Input State @@ -399,12 +578,14 @@ int main(int argc, char * argv[]) { if(commands[currentCommandId].runInternal) { - commands[currentCommandId].RunCommand(sdl_context, &controller, menuL, menuU, modernUI, frame, bgR, bgG, bgB); + commands[currentCommandId].RunCommand(sdl_context, &controller, menuL, menuU, modernUI, { gearIcon, appTitleText, appVersionText, CompComText }, bgR, bgG, bgB); if(commands[currentCommandId].isToggle) commands[currentCommandId].UpdateState(); } else { + // release the display first - the child's startup delay assumed our teardown was instant + sdl_context.Shutdown(); system(commands[currentCommandId].command.c_str()); _exitManager.runExitCommand = commands[currentCommandId].restartUI; break; @@ -433,57 +614,24 @@ int main(int argc, char * argv[]) } SetCurrentCommand(newCommandId); } - else if(controller.GetButtonStatus(B)) - SetCurrentCommand(commands.size()-1); - else if(controller.GetButtonStatus(X) && !commands[currentCommandId].deleteCommand.empty()) + + // tap = jump to last item, hold ~1s = delete + if(controller.PeekButtonStatus(B)) { - const std::string & confirmKey = commands[currentCommandId].deleteConfirmKey; - Texture confirmTitle(Translate(confirmKey.empty() ? "DELETE_CONFIRM_GENERIC" : confirmKey), 24, renderer, 640, 320, true, 0xFFFFFFFF, modernUI); - Texture confirmHint(Translate("DELETE_CONFIRM_HINT"), 16, renderer, 640, 360, true, 0xFFFFFFFF, modernUI); - bool confirmed = false; - for(;;) - { - controller.Update(); - if(controller.GetButtonStatus(B)) - break; - if(controller.GetButtonStatus(A) || controller.GetButtonStatus(START)) - { - confirmed = true; - break; - } - sdl_context.StartFrame(); - if(modernUI) - frame.Draw(renderer, UiTheme::FrameRect); - else - { - banner.Draw(renderer); - menuU.Draw(renderer); - menuL.Draw(renderer); - } - confirmTitle.Draw(renderer); - confirmHint.Draw(renderer); - sdl_context.EndFrame(); - } - if(confirmed) - { - system(commands[currentCommandId].deleteCommand.c_str()); - _exitManager.runExitCommand = false; + ++bHoldCount; + if(bHoldCount == bHoldThreshold && !commands[currentCommandId].deleteCommand.empty() && ConfirmDelete()) break; - } + } + else + { + if(bHoldCount > 0 && bHoldCount < bHoldThreshold) + SetCurrentCommand(commands.size()-1); + bHoldCount = 0; } //Draw all textures if(modernUI) - { - frame.Draw(renderer, UiTheme::FrameRect); - gearIcon.Draw(renderer); - appTitleText.Draw(renderer); - appVersionText.Draw(renderer); - SDL_Rect dividerRect{ UiTheme::HeaderDividerX, UiTheme::HeaderDividerY, UiTheme::HeaderDividerW, UiTheme::HeaderDividerH }; - SDL_SetRenderDrawColor(renderer, 0xFF, 0xFF, 0xFF, 0xFF); - SDL_RenderFillRect(renderer, ÷rRect); - SDL_SetRenderDrawColor(renderer, bgR, bgG, bgB, 0xFF); - } + DrawModernUI(); else { banner.Draw(renderer); @@ -491,46 +639,36 @@ int main(int argc, char * argv[]) menuL.Draw(renderer); } titleText.Draw(renderer); + + int lastCommandIndex = static_cast(commands.size()) - 1; + int scrollDrawBound = modernUI ? pinnedStartIndex : static_cast(commands.size()); + for(int i = 0, count = std::min(DisplayItemCount, scrollDrawBound-topListItemNumber); i < count; ++i) + DrawRow(commands[i+topListItemNumber], (i+topListItemNumber) == lastCommandIndex); + + // pinned trailing rows (Back/Exit) - always visible near the footer if(modernUI) - highlight.Draw(renderer, highlightRect, UiTheme::HighlightR, UiTheme::HighlightG, UiTheme::HighlightB); - for(int i = 0, count = std::min(DisplayItemCount,static_cast(commands.size())); i < count; ++i) - { - Command & rowCommand = commands[i+topListItemNumber]; - rowCommand.texture.Draw(renderer); - if(modernUI && rowCommand.isToggle) - { - Texture & rowSwitch = rowCommand.stateOn ? switchOn : switchOff; - rowSwitch.rect.x = UiTheme::SwitchRightX - UiTheme::SwitchW; - rowSwitch.rect.y = rowCommand.texture.rect.y + (rowCommand.texture.rect.h - UiTheme::SwitchH) / 2; - rowSwitch.Draw(renderer); - } - } - if(PreviewImage.get()) - PreviewImage->Draw(renderer); + for(int i = pinnedStartIndex; i <= lastCommandIndex; ++i) + DrawRow(commands[i], i == lastCommandIndex); + CompComText.Draw(renderer); - if(modernUI) - { - // A rightmost, B left of it, X further left so A/B don't jitter - int rightEdge = UiTheme::BadgeClusterRightX; - rightEdge = DrawBadge(badgeA, rightEdge); - rightEdge = DrawBadge(badgeB, rightEdge); - if(!commands[currentCommandId].deleteCommand.empty()) - DrawBadge(badgeX, rightEdge); - } - else + if(!modernUI) { pointerText.Draw(renderer); if(commands[currentCommandId].deleteCommand.size()) deleteHint.Draw(renderer); } - // Display Scroll Arrows when needed + // bounded by scrollDrawBound, not commands.size() - pinned rows need no scroll if(topListItemNumber != 0) scrollUp.Draw(renderer); - if((topListItemNumber + DisplayItemCount) < commands.size()) + if((topListItemNumber + DisplayItemCount) < scrollDrawBound) scrollDown.Draw(renderer, SDL_FLIP_VERTICAL); + // flat-line helpers leave the draw color dirty - reset before EndFrame + if(modernUI) + SDL_SetRenderDrawColor(renderer, bgR, bgG, bgB, 0xFF); + //Render Framebuffer and Wait for Next Frame sdl_context.EndFrame(); } From c25ecac9fa9659120554476e979ee78a038d6088 Mon Sep 17 00:00:00 2001 From: DefKorns Date: Fri, 11 Sep 2026 01:59:07 +0000 Subject: [PATCH 003/104] remove ClassicUI --- Makefile.docker | 2 +- .../advanced_commands/c0510_ModernUI | 6 - mod/etc/options_menu/images/banner.png | Bin 5379 -> 0 bytes mod/etc/options_menu/images/ui/frame.png | Bin 901 -> 0 bytes mod/etc/options_menu/images/ui/highlight.png | Bin 223 -> 0 bytes mod/etc/options_menu/language/de-DE.lang | 2 - mod/etc/options_menu/language/en-US.lang | 2 - mod/etc/options_menu/language/es-ES.lang | 2 - mod/etc/options_menu/language/fr-FR.lang | 2 - mod/etc/options_menu/language/it-IT.lang | 2 - mod/etc/options_menu/language/ja-JP.lang | 2 - mod/etc/options_menu/language/nl-NL.lang | 2 - mod/etc/options_menu/language/pt-PT.lang | 2 - mod/etc/options_menu/language/ru-RU.lang | 2 - mod/etc/options_menu/scripts/om_functions | 20 -- src/command.cpp | 50 ++--- src/command.h | 2 +- src/framework/nineslice.cpp | 70 ------- src/framework/nineslice.h | 42 ---- src/framework/sdl_helper.h | 1 - src/framework/texture.cpp | 5 - src/framework/texture.h | 7 - src/framework/uitheme.h | 12 +- src/main.cpp | 179 ++++++------------ 24 files changed, 78 insertions(+), 336 deletions(-) delete mode 100644 mod/etc/options_menu/advanced_commands/c0510_ModernUI delete mode 100644 mod/etc/options_menu/images/banner.png delete mode 100644 mod/etc/options_menu/images/ui/frame.png delete mode 100644 mod/etc/options_menu/images/ui/highlight.png delete mode 100644 src/framework/nineslice.cpp delete mode 100644 src/framework/nineslice.h diff --git a/Makefile.docker b/Makefile.docker index 1ff1367..d792e3e 100644 --- a/Makefile.docker +++ b/Makefile.docker @@ -26,7 +26,7 @@ VENDORED_TTF_LIBS = endif CXXFLAGS = -std=c++11 -Os $(SDL_CFLAGS) -DMOD_VERSION=\"$(MOD_VER)\" LDLIBS = $(SDL_LIBS) -SOURCES = src/main.cpp src/command.cpp src/localization.cpp src/framework/sdl_context.cpp src/framework/texture.cpp src/framework/nineslice.cpp src/framework/controller.cpp src/framework/powerwatch.cpp src/framework/draw_helpers.cpp +SOURCES = src/main.cpp src/command.cpp src/localization.cpp src/framework/sdl_context.cpp src/framework/texture.cpp src/framework/controller.cpp src/framework/powerwatch.cpp src/framework/draw_helpers.cpp OBJECTS = $(SOURCES:.cpp=.o) DEV_DIR = $(shell realpath .) OUT = $(DEV_DIR)/out diff --git a/mod/etc/options_menu/advanced_commands/c0510_ModernUI b/mod/etc/options_menu/advanced_commands/c0510_ModernUI deleted file mode 100644 index 3742d93..0000000 --- a/mod/etc/options_menu/advanced_commands/c0510_ModernUI +++ /dev/null @@ -1,6 +0,0 @@ -COMMAND_NAME=TOGGLE_MODERN_UI -COMMAND_TYPE=EXTERNAL -RESTART_UI=FALSE -COMMAND_STR=[ "$(cat /etc/options_menu/ui_style.cfg 2>/dev/null)" = "classic" ] && echo modern > /etc/options_menu/ui_style.cfg || echo classic > /etc/options_menu/ui_style.cfg; usleep 50000 && %options_path%/options --commandPath %options_path%/advanced_commands/ --title "ADVANCED_OPTIONS" & -STATE_STR=[ "$(cat /etc/options_menu/ui_style.cfg 2>/dev/null)" = "classic" ] && echo off || echo on -PREVIEW_IMAGE=/etc/options_menu/images/preview_placeholder.png diff --git a/mod/etc/options_menu/images/banner.png b/mod/etc/options_menu/images/banner.png deleted file mode 100644 index c0dfb2b043b15eb45c91a211da55a3e2339c24af..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5379 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 diff --git a/mod/etc/options_menu/images/ui/frame.png b/mod/etc/options_menu/images/ui/frame.png deleted file mode 100644 index ded43999f34a5d260bb702a30ce8b5a21a6c82ce..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 901 zcmV;01A6?4P)9r81K=ir zTL5kYxCGz^fL@#r>cj1-<|BYdBJz@? z28`{b1XR48pydTN^NQqsk{8e+s4wP|`c`DYY?3u>+v1n3f}ghCBsp8K@6j%1TRG>? z(X|Uc&6=;?_ocRiK&z}o{0ovJu9AjebOCo+>o$!OCjsc5B zn@-02XjS}+9o-i2OM-MZonKiO|_;};GuyI8oQP+%1p}vql<9qJ!9JH zrd2f`HZI9|_q(~~Ppkqi2y0H~d_Fipa^1E5JwJ$4fjo$tw%iL;^MD?lAgTBJ$`_J@ z-Jbg`#)3i9NYgx_Hx85Tscuz7zU#wATldXff@w*nZPmV3&665{X=nR7pM&Zl1&zwI z24K{7G)h~pTs5+$Emt1Z{Mfgg_(rl3j4I`SCTmiSrd8S39?+L<034wD9Jn6P*Z8Br z9jsi-m*6$B@%R{-gy19uCm}cq!AS^CLU0m-lMtMQ;3NbmAvg)aNeE6ta1w%(5S)bI zBm^fRI0?Z?2u?z9Qt^LsRG(w-L6UnLBsTwDr2r(HL|8LSKdi(!hmw8!eapo8kN=XQ)Cc*l2j6rUjUX}Ud-8?nkuD1UMnzX z`>J)hp*{RWN&cTI%h;2p5q-LRCiH@dOz0&uO87-n5tq$L^o5hs>MCKE&U!*Go`zB7 b<@0|4;dnQ(cTD4>00000NkvXXu0mjfZP2Q> diff --git a/mod/etc/options_menu/images/ui/highlight.png b/mod/etc/options_menu/images/ui/highlight.png deleted file mode 100644 index e96daeaf62f0e0cba9806a8569b1964850500f8d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 223 zcmV<503iQ~P)LQT Z<{fmnfzxPKzAFF#002ovPDHLkV1hY3Tc`j4 diff --git a/mod/etc/options_menu/language/de-DE.lang b/mod/etc/options_menu/language/de-DE.lang index 3be5aee..b01c058 100644 --- a/mod/etc/options_menu/language/de-DE.lang +++ b/mod/etc/options_menu/language/de-DE.lang @@ -11,7 +11,6 @@ RUN_TOP=Top ausführen BENCHMARK_TOOL=Benchmark-Tool DUMP_FILE_STRUCTURE=Dateistruktur exportieren (auf USB) TOGGLE_USB_WRITE=USB-Schreibzugriff umschalten -TOGGLE_MODERN_UI=Moderne Oberfläche RA_DEBUGGER=RetroArch-Debugger (USB-Logs) CHANGE_BUTTON_COMBO=Tastenkombination ändern CLEAR_CACHE=Cache leeren @@ -44,7 +43,6 @@ WIFI_BACKUP_SPLASH_LINE2=Bitte warten . . . DELETE_BACKUP_CONFIRM=Diese Sicherung löschen? DELETE_CONFIRM_HINT=A = Ja B = Nein DELETE_CONFIRM_GENERIC=Dieses Element löschen? -WIFI_DELETE_HINT_FOOTER=X = Löschen A = Wiederherstellen HINT_SELECT=Wählen HINT_BACK=Zurück HINT_DELETE=Löschen (Halten) diff --git a/mod/etc/options_menu/language/en-US.lang b/mod/etc/options_menu/language/en-US.lang index ed896e8..a79abc2 100644 --- a/mod/etc/options_menu/language/en-US.lang +++ b/mod/etc/options_menu/language/en-US.lang @@ -11,7 +11,6 @@ RUN_TOP=Run Top BENCHMARK_TOOL=Benchmark Tool DUMP_FILE_STRUCTURE=Dump File Structure (to USB) TOGGLE_USB_WRITE=Toggle Write Access on USB -TOGGLE_MODERN_UI=Modern UI RA_DEBUGGER=RetroArch Debugger (USB logs) CHANGE_BUTTON_COMBO=Change Options Button Combo CLEAR_CACHE=Clear Cache @@ -44,7 +43,6 @@ WIFI_BACKUP_SPLASH_LINE2=Please Wait . . . DELETE_BACKUP_CONFIRM=Delete this backup? DELETE_CONFIRM_HINT=A = Yes B = No DELETE_CONFIRM_GENERIC=Delete this item? -WIFI_DELETE_HINT_FOOTER=X = Delete A = Restore HINT_SELECT=Select HINT_BACK=Back HINT_DELETE=Delete (Hold) diff --git a/mod/etc/options_menu/language/es-ES.lang b/mod/etc/options_menu/language/es-ES.lang index c609984..beaa3a1 100644 --- a/mod/etc/options_menu/language/es-ES.lang +++ b/mod/etc/options_menu/language/es-ES.lang @@ -11,7 +11,6 @@ RUN_TOP=Ejecutar Top BENCHMARK_TOOL=Herramienta de benchmark DUMP_FILE_STRUCTURE=Exportar estructura de archivos (a USB) TOGGLE_USB_WRITE=Alternar escritura en USB -TOGGLE_MODERN_UI=Interfaz moderna RA_DEBUGGER=Depurador de RetroArch (logs USB) CHANGE_BUTTON_COMBO=Cambiar combinación de botones CLEAR_CACHE=Vaciar caché @@ -44,7 +43,6 @@ WIFI_BACKUP_SPLASH_LINE2=Espere por favor . . . DELETE_BACKUP_CONFIRM=¿Borrar esta copia de seguridad? DELETE_CONFIRM_HINT=A = Sí B = No DELETE_CONFIRM_GENERIC=¿Borrar este elemento? -WIFI_DELETE_HINT_FOOTER=X = Borrar A = Restaurar HINT_SELECT=Seleccionar HINT_BACK=Atrás HINT_DELETE=Borrar (mantener) diff --git a/mod/etc/options_menu/language/fr-FR.lang b/mod/etc/options_menu/language/fr-FR.lang index aa41da0..d8f63f6 100644 --- a/mod/etc/options_menu/language/fr-FR.lang +++ b/mod/etc/options_menu/language/fr-FR.lang @@ -11,7 +11,6 @@ RUN_TOP=Exécuter Top BENCHMARK_TOOL=Outil de benchmark DUMP_FILE_STRUCTURE=Exporter la structure des fichiers (vers USB) TOGGLE_USB_WRITE=Activer/désactiver l'écriture USB -TOGGLE_MODERN_UI=Interface moderne RA_DEBUGGER=Débogueur RetroArch (logs USB) CHANGE_BUTTON_COMBO=Changer la combinaison de boutons CLEAR_CACHE=Vider le cache @@ -44,7 +43,6 @@ WIFI_BACKUP_SPLASH_LINE2=Veuillez patienter . . . DELETE_BACKUP_CONFIRM=Supprimer cette sauvegarde? DELETE_CONFIRM_HINT=A = Oui B = Non DELETE_CONFIRM_GENERIC=Supprimer cet élément? -WIFI_DELETE_HINT_FOOTER=X = Supprimer A = Restaurer HINT_SELECT=Sélect. HINT_BACK=Retour HINT_DELETE=Suppr. (Maintenir) diff --git a/mod/etc/options_menu/language/it-IT.lang b/mod/etc/options_menu/language/it-IT.lang index 6aadfd8..7aa6d38 100644 --- a/mod/etc/options_menu/language/it-IT.lang +++ b/mod/etc/options_menu/language/it-IT.lang @@ -11,7 +11,6 @@ RUN_TOP=Esegui Top BENCHMARK_TOOL=Strumento di benchmark DUMP_FILE_STRUCTURE=Esporta struttura file (su USB) TOGGLE_USB_WRITE=Attiva/disattiva scrittura USB -TOGGLE_MODERN_UI=Interfaccia moderna RA_DEBUGGER=Debugger RetroArch (log USB) CHANGE_BUTTON_COMBO=Cambia combinazione pulsanti CLEAR_CACHE=Svuota cache @@ -44,7 +43,6 @@ WIFI_BACKUP_SPLASH_LINE2=Attendere prego . . . DELETE_BACKUP_CONFIRM=Eliminare questo backup? DELETE_CONFIRM_HINT=A = Sì B = No DELETE_CONFIRM_GENERIC=Eliminare questo elemento? -WIFI_DELETE_HINT_FOOTER=X = Elimina A = Ripristina HINT_SELECT=Seleziona HINT_BACK=Indietro HINT_DELETE=Elimina (tieni premuto) diff --git a/mod/etc/options_menu/language/ja-JP.lang b/mod/etc/options_menu/language/ja-JP.lang index 34cb131..e7b935e 100644 --- a/mod/etc/options_menu/language/ja-JP.lang +++ b/mod/etc/options_menu/language/ja-JP.lang @@ -11,7 +11,6 @@ RUN_TOP=topをじっこう BENCHMARK_TOOL=Benchmarkのどうぐ DUMP_FILE_STRUCTURE=Fileこうぞうをほぞん (USBへ) TOGGLE_USB_WRITE=USBへのかきこみをきりかえ -TOGGLE_MODERN_UI=Modern UI RA_DEBUGGER=RAのDebugger (USB Log) CHANGE_BUTTON_COMBO=Buttonくみあわせをへんこう CLEAR_CACHE=Cacheをけす @@ -44,7 +43,6 @@ WIFI_BACKUP_SPLASH_LINE2=おまちください . . . DELETE_BACKUP_CONFIRM=このBackupをさくじょしますか? DELETE_CONFIRM_HINT=A = はい B = いいえ DELETE_CONFIRM_GENERIC=これをさくじょしますか? -WIFI_DELETE_HINT_FOOTER=X = さくじょ A = もどす HINT_SELECT=けってい HINT_BACK=もどる HINT_DELETE=けす (ながおし) diff --git a/mod/etc/options_menu/language/nl-NL.lang b/mod/etc/options_menu/language/nl-NL.lang index 76d7439..3b525e1 100644 --- a/mod/etc/options_menu/language/nl-NL.lang +++ b/mod/etc/options_menu/language/nl-NL.lang @@ -11,7 +11,6 @@ RUN_TOP=Top uitvoeren BENCHMARK_TOOL=Benchmarktool DUMP_FILE_STRUCTURE=Bestandsstructuur exporteren (naar USB) TOGGLE_USB_WRITE=USB-schrijftoegang schakelen -TOGGLE_MODERN_UI=Moderne interface RA_DEBUGGER=RetroArch-debugger (USB-logs) CHANGE_BUTTON_COMBO=Knoppencombinatie wijzigen CLEAR_CACHE=Cache wissen @@ -44,7 +43,6 @@ WIFI_BACKUP_SPLASH_LINE2=Even geduld . . . DELETE_BACKUP_CONFIRM=Deze back-up verwijderen? DELETE_CONFIRM_HINT=A = Ja B = Nee DELETE_CONFIRM_GENERIC=Dit item verwijderen? -WIFI_DELETE_HINT_FOOTER=X = Verwijderen A = Herstellen HINT_SELECT=Kiezen HINT_BACK=Terug HINT_DELETE=Wissen (vasthouden) diff --git a/mod/etc/options_menu/language/pt-PT.lang b/mod/etc/options_menu/language/pt-PT.lang index ece3e8e..b9b5e4c 100644 --- a/mod/etc/options_menu/language/pt-PT.lang +++ b/mod/etc/options_menu/language/pt-PT.lang @@ -11,7 +11,6 @@ RUN_TOP=Executar Top BENCHMARK_TOOL=Ferramenta de Benchmark DUMP_FILE_STRUCTURE=Exportar Estrutura de Ficheiros (para USB) TOGGLE_USB_WRITE=Alternar Escrita na USB -TOGGLE_MODERN_UI=Interface Moderna RA_DEBUGGER=Depurador do RetroArch (logs USB) CHANGE_BUTTON_COMBO=Mudar Combinação de Botões CLEAR_CACHE=Limpar Cache @@ -44,7 +43,6 @@ WIFI_BACKUP_SPLASH_LINE2=Aguarde Por Favor . . . DELETE_BACKUP_CONFIRM=Apagar esta cópia de segurança? DELETE_CONFIRM_HINT=A = Sim B = Não DELETE_CONFIRM_GENERIC=Apagar este item? -WIFI_DELETE_HINT_FOOTER=X = Apagar A = Restaurar HINT_SELECT=Selecionar HINT_BACK=Voltar HINT_DELETE=Apagar (manter premido) diff --git a/mod/etc/options_menu/language/ru-RU.lang b/mod/etc/options_menu/language/ru-RU.lang index 95957f5..e49362a 100644 --- a/mod/etc/options_menu/language/ru-RU.lang +++ b/mod/etc/options_menu/language/ru-RU.lang @@ -11,7 +11,6 @@ RUN_TOP=Запустить Top BENCHMARK_TOOL=Инструмент Benchmark DUMP_FILE_STRUCTURE=Сохранить структуру файлов (на USB) TOGGLE_USB_WRITE=Переключить запись на USB -TOGGLE_MODERN_UI=Modern UI RA_DEBUGGER=Отладчик RA (логи на USB) CHANGE_BUTTON_COMBO=Изменить комбинацию кнопок CLEAR_CACHE=Очистить кэш @@ -44,7 +43,6 @@ WIFI_BACKUP_SPLASH_LINE2=Пожалуйста, подождите . . . DELETE_BACKUP_CONFIRM=Удалить эту резервную копию? DELETE_CONFIRM_HINT=A = Да B = Нет DELETE_CONFIRM_GENERIC=Удалить этот элемент? -WIFI_DELETE_HINT_FOOTER=X = Удалить A = Восстановить HINT_SELECT=Выбрать HINT_BACK=Назад HINT_DELETE=Удалить (удерж.) diff --git a/mod/etc/options_menu/scripts/om_functions b/mod/etc/options_menu/scripts/om_functions index 0762d35..27d9bfd 100644 --- a/mod/etc/options_menu/scripts/om_functions +++ b/mod/etc/options_menu/scripts/om_functions @@ -140,26 +140,6 @@ toggleDownloads() { fi } -is_classic_ui() { - # [X]-in-title markers only apply in classic UI; modern UI has a real switch - [ "$(cat /etc/options_menu/ui_style.cfg 2>/dev/null)" = "classic" ] -} - -switchOff() { - is_classic_ui || return 0 - for item in "$@"; do - [ -f "$item" ] && sed -r -i 's/ \[X\]//g' "$item" - done - -} - -switchOn() { - is_classic_ui || return 0 - for item in "$@"; do - [ -f "$item" ] && sed -r -i "1 s|$| [X]|" "$item" - done -} - overlayFS() { [ -d "$1" ] && mount -t overlayfs -o lowerdir="$2",upperdir="$1" overlayfs "$2" } diff --git a/src/command.cpp b/src/command.cpp index 50bd998..ef7d3cf 100644 --- a/src/command.cpp +++ b/src/command.cpp @@ -83,20 +83,17 @@ Command::Command(std::ifstream & in) in.close(); } -void Command::RunCommand(SDL_Context & sdl_context, Controller * controller, Sprite & menuL, Sprite & menuU, bool modernUI, const ModernChrome & chrome, Uint8 bgR, Uint8 bgG, Uint8 bgB) const +void Command::RunCommand(SDL_Context & sdl_context, Controller * controller, const ModernChrome & chrome, Uint8 bgR, Uint8 bgG, Uint8 bgB) const { std::list textList; FILE* pipe = popen(command.c_str(), "r"); if(pipe) { auto renderer = sdl_context.renderer; - if(modernUI) - SDL_SetRenderDrawColor(renderer, UiTheme::BgR, UiTheme::BgG, UiTheme::BgB, SDL_ALPHA_OPAQUE); - else - SDL_SetRenderDrawColor(renderer, 0, 0, 0, SDL_ALPHA_OPAQUE); + SDL_SetRenderDrawColor(renderer, UiTheme::BgR, UiTheme::BgG, UiTheme::BgB, SDL_ALPHA_OPAQUE); char buffer[128] = {0}; - const int textX = modernUI ? UiTheme::FrameRect.x + UiTheme::FrameInset : 30; - const int textFirstY = modernUI ? UiTheme::HeaderDividerY + 24 : 100; + const int textX = UiTheme::FrameRect.x + UiTheme::FrameInset; + const int textFirstY = UiTheme::HeaderDividerY + 24; // same app header/footer chrome as the main screen, plus a persistent B/Exit badge Texture exitLetter("B", 16, renderer, 0, 0, false, UiTheme::BadgeLetterColor, true); @@ -108,27 +105,19 @@ void Command::RunCommand(SDL_Context & sdl_context, Controller * controller, Spr exitLabel.rect.x = exitBadge.x + exitBadge.w + UiTheme::BadgeLabelGap; exitLabel.rect.y = exitBadge.y + (exitBadge.h - exitLabel.rect.h) / 2; - auto render = [&](Texture * closeText = nullptr) + auto render = [&]() { sdl_context.StartFrame(); - if(modernUI) - { - DrawStrokeRect(renderer, UiTheme::OuterRect, UiTheme::BorderR, UiTheme::BorderG, UiTheme::BorderB, UiTheme::BorderWidth, UiTheme::BorderRadius); - chrome.gearIcon.Draw(renderer); - chrome.appTitleText.Draw(renderer); - chrome.appVersionText.Draw(renderer); - DrawHLine(renderer, UiTheme::HeaderDividerX, UiTheme::HeaderDividerX + UiTheme::HeaderDividerW, UiTheme::HeaderDividerY, UiTheme::BorderR, UiTheme::BorderG, UiTheme::BorderB, UiTheme::BorderWidth); - DrawHLine(renderer, UiTheme::OuterRect.x, UiTheme::OuterRect.x + UiTheme::OuterRect.w, UiTheme::FooterDividerY, UiTheme::BorderR, UiTheme::BorderG, UiTheme::BorderB, UiTheme::BorderWidth); - chrome.creditText.Draw(renderer); - DrawRoundedFillRect(renderer, exitBadge, UiTheme::BadgeB.r, UiTheme::BadgeB.g, UiTheme::BadgeB.b, exitBadge.w/2); - exitLetter.Draw(renderer); - exitLabel.Draw(renderer); - } - else - { - menuU.Draw(renderer); - menuL.Draw(renderer); - } + DrawStrokeRect(renderer, UiTheme::OuterRect, UiTheme::BorderR, UiTheme::BorderG, UiTheme::BorderB, UiTheme::BorderWidth, UiTheme::BorderRadius); + chrome.gearIcon.Draw(renderer); + chrome.appTitleText.Draw(renderer); + chrome.appVersionText.Draw(renderer); + DrawHLine(renderer, UiTheme::HeaderDividerX, UiTheme::HeaderDividerX + UiTheme::HeaderDividerW, UiTheme::HeaderDividerY, UiTheme::BorderR, UiTheme::BorderG, UiTheme::BorderB, UiTheme::BorderWidth); + DrawHLine(renderer, UiTheme::OuterRect.x, UiTheme::OuterRect.x + UiTheme::OuterRect.w, UiTheme::FooterDividerY, UiTheme::BorderR, UiTheme::BorderG, UiTheme::BorderB, UiTheme::BorderWidth); + chrome.creditText.Draw(renderer); + DrawRoundedFillRect(renderer, exitBadge, UiTheme::BadgeB.r, UiTheme::BadgeB.g, UiTheme::BadgeB.b, exitBadge.w/2); + exitLetter.Draw(renderer); + exitLabel.Draw(renderer); int y = textFirstY; for(auto & t : textList) { @@ -136,10 +125,7 @@ void Command::RunCommand(SDL_Context & sdl_context, Controller * controller, Spr t.Draw(renderer); y+=10; } - if(closeText) - closeText->Draw(renderer); - if(modernUI) - SDL_SetRenderDrawColor(renderer, UiTheme::BgR, UiTheme::BgG, UiTheme::BgB, 0xFF); // must be the LAST color-setting call, or it leaks into next frame's clear + SDL_SetRenderDrawColor(renderer, UiTheme::BgR, UiTheme::BgG, UiTheme::BgB, 0xFF); // must be the LAST color-setting call, or it leaks into next frame's clear sdl_context.EndFrame(); }; @@ -170,13 +156,11 @@ void Command::RunCommand(SDL_Context & sdl_context, Controller * controller, Spr render(); } pclose(pipe); - // modern UI's badge already says "B: Back" - the old text hint is classic UI only - Texture closeText(Translate("PRESS_B_EXIT"), 12, renderer, textX, 610); while (!controller->GetButtonStatus(B)) { controller->Update(); - render(modernUI ? nullptr : &closeText); + render(); } SDL_SetRenderDrawColor(renderer, bgR, bgG, bgB, 0xFF); } diff --git a/src/command.h b/src/command.h index 2c89bc7..4805cb4 100644 --- a/src/command.h +++ b/src/command.h @@ -27,7 +27,7 @@ struct Command { Command(); Command(std::ifstream & in); - void RunCommand(SDL_Context & sdl_context, Controller * controller, Sprite & menuL, Sprite & menuU, bool modernUI, const ModernChrome & chrome, Uint8 bgR = 0x6e, Uint8 bgG = 0x6e, Uint8 bgB = 0x6e) const; + void RunCommand(SDL_Context & sdl_context, Controller * controller, const ModernChrome & chrome, Uint8 bgR = UiTheme::BgR, Uint8 bgG = UiTheme::BgG, Uint8 bgB = UiTheme::BgB) const; void UpdateState(); std::string name; diff --git a/src/framework/nineslice.cpp b/src/framework/nineslice.cpp deleted file mode 100644 index 1e4a550..0000000 --- a/src/framework/nineslice.cpp +++ /dev/null @@ -1,70 +0,0 @@ -/** - * Copyright (c) 2026 DefKorns (https://defkorns.github.io/LICENSE) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -#include "nineslice.h" -#include "texture.h" - -NineSlice::NineSlice() {} - -NineSlice::NineSlice(const std::string & pngFilePath, SDL_Renderer * renderer, int left, int right, int top, int bottom) - : left(left), right(right), top(top), bottom(bottom) -{ - texture = std::shared_ptr(LoadTexturePNG(renderer, pngFilePath, &srcRect), SDL_DestroyTexture); -} - -void NineSlice::Draw(SDL_Renderer * renderer, const SDL_Rect & dest) const -{ - if(!texture.get()) - return; - - const int srcW = srcRect.w, srcH = srcRect.h; - const int colSrcX[3] = { 0, left, srcW - right }; - const int colSrcW[3] = { left, srcW - left - right, right }; - const int colDstX[3] = { dest.x, dest.x + left, dest.x + dest.w - right }; - const int colDstW[3] = { left, dest.w - left - right, right }; - - const int rowSrcY[3] = { 0, top, srcH - bottom }; - const int rowSrcH[3] = { top, srcH - top - bottom, bottom }; - const int rowDstY[3] = { dest.y, dest.y + top, dest.y + dest.h - bottom }; - const int rowDstH[3] = { top, dest.h - top - bottom, bottom }; - - for(int col = 0; col < 3; ++col) - { - if(colSrcW[col] <= 0 || colDstW[col] <= 0) - continue; - for(int row = 0; row < 3; ++row) - { - if(rowSrcH[row] <= 0 || rowDstH[row] <= 0) - continue; - SDL_Rect src{ srcRect.x + colSrcX[col], srcRect.y + rowSrcY[row], colSrcW[col], rowSrcH[row] }; - SDL_Rect dst{ colDstX[col], rowDstY[row], colDstW[col], rowDstH[row] }; - SDL_RenderCopyEx(renderer, texture.get(), &src, &dst, 0, NULL, SDL_FLIP_NONE); - } - } -} - -void NineSlice::Draw(SDL_Renderer * renderer, const SDL_Rect & dest, Uint8 r, Uint8 g, Uint8 b, Uint8 a) const -{ - if(!texture.get()) - return; - - SDL_SetTextureColorMod(texture.get(), r, g, b); - SDL_SetTextureAlphaMod(texture.get(), a); - Draw(renderer, dest); - SDL_SetTextureColorMod(texture.get(), 255, 255, 255); - SDL_SetTextureAlphaMod(texture.get(), 255); -} diff --git a/src/framework/nineslice.h b/src/framework/nineslice.h deleted file mode 100644 index 436f6f9..0000000 --- a/src/framework/nineslice.h +++ /dev/null @@ -1,42 +0,0 @@ -/** - * Copyright (c) 2026 DefKorns (https://defkorns.github.io/LICENSE) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -#ifndef NINESLICE_H_ -#define NINESLICE_H_ - -#include -#include -#include - -// Stretches a small source PNG into an arbitrary dest rect without -// distorting its corners: corners draw at fixed size, edges stretch along -// one axis, the middle stretches on both. top=bottom=0 (or left=right=0) -// degenerates to a 3-slice. -struct NineSlice -{ - std::shared_ptr texture; - SDL_Rect srcRect{0, 0, 0, 0}; - int left = 0, right = 0, top = 0, bottom = 0; - - NineSlice(); - NineSlice(const std::string & pngFilePath, SDL_Renderer * renderer, int left, int right, int top, int bottom); - - void Draw(SDL_Renderer * renderer, const SDL_Rect & dest) const; - void Draw(SDL_Renderer * renderer, const SDL_Rect & dest, Uint8 r, Uint8 g, Uint8 b, Uint8 a = 255) const; -}; - -#endif diff --git a/src/framework/sdl_helper.h b/src/framework/sdl_helper.h index 2c8aa7d..55fd323 100644 --- a/src/framework/sdl_helper.h +++ b/src/framework/sdl_helper.h @@ -12,7 +12,6 @@ #include "sdl_context.h" #include "texture.h" -#include "nineslice.h" #include "uitheme.h" #endif diff --git a/src/framework/texture.cpp b/src/framework/texture.cpp index a2c861a..e4d23f5 100644 --- a/src/framework/texture.cpp +++ b/src/framework/texture.cpp @@ -60,11 +60,6 @@ void Texture::Draw(SDL_Renderer* renderer, SDL_RendererFlip flip_enum) SDL_RenderCopyEx(renderer, texture.get(), NULL, &rect, 0, NULL, flip_enum); } -void Sprite::Draw(SDL_Renderer * renderer) -{ - SDL_RenderCopyEx(renderer, texture.get(), &sRect, &dRect, 0, NULL, SDL_FLIP_NONE); -} - SDL_Texture * LoadTexturePNG(SDL_Renderer *renderer, std::string file, SDL_Rect * rect) { SDL_Texture* texture = nullptr; diff --git a/src/framework/texture.h b/src/framework/texture.h index de3e42c..7c95c94 100644 --- a/src/framework/texture.h +++ b/src/framework/texture.h @@ -24,13 +24,6 @@ struct Texture void Draw(SDL_Renderer* renderer, SDL_RendererFlip flip_enum); }; -struct Sprite -{ - std::shared_ptr texture; - SDL_Rect sRect, dRect; - void Draw(SDL_Renderer * renderer); -}; - SDL_Texture * LoadTexturePNG(SDL_Renderer *renderer, std::string file, SDL_Rect * rect = nullptr); SDL_Texture * WriteText(const std::string & text, int fontSize, SDL_Renderer* renderer, int & textureWidth, int & textureHeight, const int color = 0xFFFFFF); SDL_Texture * WriteTextTTF(const std::string & text, int fontSize, SDL_Renderer* renderer, int & textureWidth, int & textureHeight, const int color = 0xFFFFFF); diff --git a/src/framework/uitheme.h b/src/framework/uitheme.h index b532ca4..1179556 100644 --- a/src/framework/uitheme.h +++ b/src/framework/uitheme.h @@ -22,9 +22,7 @@ // Geometry and palette for the split-panel "settings dialog" look. Screen // is a fixed 1280x720 (sdl_context.cpp), so these are plain constants. -// Border/dividers are flat drawn lines (see main.cpp's DrawRect/FillRect/ -// HLine/VLine helpers), not the old frame.png/highlight.png NineSlice art - -// those two assets are unused by Modern UI now. +// Border/dividers are flat drawn lines (see draw_helpers.h), no textured art. namespace UiTheme { const Uint8 BgR = 0x0C, BgG = 0x0F, BgB = 0x16; @@ -39,7 +37,7 @@ namespace UiTheme const int BorderRadius = 14; const int BoxRadius = 8; // selected row / detail icon box / preview box const int ContentPadding = 16; // border to content start - const int FrameInset = 24; // frame.png's 9-slice corner size (delete-confirm modal only) + const int FrameInset = 24; // text inset for the INTERNAL command output screen const SDL_Rect FrameRect{ SafeMarginX, SafeMarginY, 1280 - 2*SafeMarginX, 720 - 2*SafeMarginY }; const int FrameX = FrameRect.x + ContentPadding; @@ -87,7 +85,6 @@ namespace UiTheme const int RowPitch = 36; const int RowTextYNudge = -4; // fine-tune vs. pure (slot-h - text-h)/2 centering const int RowTextX = ListX + 16; - const int DisplayItemCount = 12; // rows visible at once in the scrollable region, both UI styles const int PinnedBottomMargin = 10; // gap from the footer divider to the last pinned row (e.g. Exit) // detail panel (right column), Modern UI only - just the preview image, @@ -137,10 +134,7 @@ namespace UiTheme const int ScrollUpY = HeaderDividerY + 90; const int ScrollDownY = FooterDividerY - 104; - // asset paths, relative to optionsLocation (frame/highlight unused by - // Modern UI now - kept for Classic UI / potential rollback) - const char * const AssetFrame = "/images/ui/frame.png"; - const char * const AssetHighlight = "/images/ui/highlight.png"; + // asset paths, relative to optionsLocation const char * const AssetBadgeOuter = "/images/ui/badge_outer.png"; const char * const AssetBadgeInner = "/images/ui/badge_inner.png"; const char * const AssetSwitchOn = "/images/ui/switch_on.png"; diff --git a/src/main.cpp b/src/main.cpp index 0385343..40db7ac 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -82,13 +82,9 @@ int main(int argc, char * argv[]) _exitManager.exitCommand = "/bin/sh " + optionsLocation + "/scripts/ResumeUI.sh"; std::string commandLocation(optionsLocation + "commands/"); std::string scriptLocation(optionsLocation + "scripts/"); - std::string spriteSheetLocation; - std::ifstream in("/tmp/spritesheet"); - std::getline(in, spriteSheetLocation); - in.close(); std::string langCode("en-US"); - in.open("/etc/options_menu/language.cfg"); + std::ifstream in("/etc/options_menu/language.cfg"); std::getline(in, langCode); in.close(); if(langCode.empty()) @@ -96,12 +92,6 @@ int main(int argc, char * argv[]) LoadLanguage(optionsLocation, langCode); SetTTFFontPath(optionsLocation); - std::string uiStyle; - in.open("/etc/options_menu/ui_style.cfg"); - std::getline(in, uiStyle); - in.close(); - bool modernUI = (uiStyle != "classic"); - std::string titleString(Translate("OPTIONS_TITLE")); std::string titleKey = "OPTIONS_TITLE"; // ancestors, root-first: "path,scriptPath,titleKey;..." - inherited via env, no argv needed @@ -251,18 +241,11 @@ int main(int argc, char * argv[]) //Create Options Flag to prevent multiple menu launches system("touch /tmp/options.flag"); - //Create Background - auto spriteSheet = std::shared_ptr(LoadTexturePNG(renderer, spriteSheetLocation), SDL_DestroyTexture); - Texture banner(optionsLocation + "/images/banner.png", renderer, 640, 185, true); - Sprite menuU = { spriteSheet, {1,43,432,40}, {-8,-24,1296,120} }; - Sprite menuL = { spriteSheet, {1,1,432,40}, {-8,630,1296,120} }; - const Uint8 bgR = modernUI ? UiTheme::BgR : 0x6e; - const Uint8 bgG = modernUI ? UiTheme::BgG : 0x6e; - const Uint8 bgB = modernUI ? UiTheme::BgB : 0x6e; + const Uint8 bgR = UiTheme::BgR; + const Uint8 bgG = UiTheme::BgG; + const Uint8 bgB = UiTheme::BgB; SDL_SetRenderDrawColor(renderer, bgR, bgG, bgB, 0xFF); - // only the delete-confirm modal still uses this (NineSlice) - NineSlice frame(optionsLocation + UiTheme::AssetFrame, renderer, UiTheme::FrameInset, UiTheme::FrameInset, UiTheme::FrameInset, UiTheme::FrameInset); Texture gearIcon(optionsLocation + UiTheme::AssetGear, renderer, UiTheme::GearX, UiTheme::GearY); Texture switchOn(optionsLocation + UiTheme::AssetSwitchOn, renderer); Texture switchOff(optionsLocation + UiTheme::AssetSwitchOff, renderer); @@ -270,31 +253,19 @@ int main(int argc, char * argv[]) Texture badgeInner(optionsLocation + UiTheme::AssetBadgeInner, renderer); //Create Textures for Strings - // only drawn in the modernUI branch Texture appTitleText("OptionsMenu", UiTheme::TitleFontSize, renderer, UiTheme::TitleX, UiTheme::TitleY, false, 0xFFFFFFFF, true); appTitleText.rect.y -= appTitleText.rect.h / 2; // vertically center on the gear (Texture only supports centering both axes together) Texture appVersionText(MOD_VERSION, UiTheme::VersionFontSize, renderer, appTitleText.rect.x + appTitleText.rect.w + UiTheme::VersionGap, UiTheme::TitleY, false, 0xFFFFFFFF, true); appVersionText.rect.y -= appVersionText.rect.h / 2; - Texture titleText = modernUI - ? Texture(titleString, UiTheme::SectionTitleFontSize, renderer, UiTheme::SectionTitleX, UiTheme::SectionTitleY, false, 0xFFFFFFFF, true) - : Texture(titleString, 36, renderer, 640, 185, true); + Texture titleText(titleString, UiTheme::SectionTitleFontSize, renderer, UiTheme::SectionTitleX, UiTheme::SectionTitleY, false, 0xFFFFFFFF, true); SDL_Rect selectedRowRect{ UiTheme::ListX, UiTheme::RowFirstY - 2, UiTheme::ListContentRightX - UiTheme::ListX, 0 }; - Texture pointerText("->", 16, renderer, 20, UiTheme::RowFirstY, false, 0xFF00FF00); - SDL_Rect & pointerRect = pointerText.rect; - Texture CompComText = modernUI - ? Texture("created by CompCom - Modern UI by DefKorns", 16, renderer, UiTheme::CreditX, UiTheme::CreditY, false, 0xFFFFFFFF, true) - : Texture("created by CompCom", 16, renderer, 1100, 620, true); - Texture deleteHint(Translate("WIFI_DELETE_HINT_FOOTER"), 16, renderer, 30, 612); - Texture scrollUp = modernUI - ? Texture("^", 16, renderer, UiTheme::ScrollX, UiTheme::ScrollUpY, false, 0xFFFFFFFF, true) - : Texture("^", 16, renderer, 30, 248); - if(modernUI) - scrollUp.rect.x -= scrollUp.rect.w / 2; // ScrollX is the gutter's center, not the glyph's left edge + Texture CompComText("created by CompCom - Modern UI by DefKorns", 16, renderer, UiTheme::CreditX, UiTheme::CreditY, false, 0xFFFFFFFF, true); + Texture scrollUp("^", 16, renderer, UiTheme::ScrollX, UiTheme::ScrollUpY, false, 0xFFFFFFFF, true); + scrollUp.rect.x -= scrollUp.rect.w / 2; // ScrollX is the gutter's center, not the glyph's left edge Texture scrollDown = scrollUp; - scrollDown.rect.y = modernUI ? UiTheme::ScrollDownY : (252+UiTheme::DisplayItemCount*18); + scrollDown.rect.y = UiTheme::ScrollDownY; //Badge cluster (top-right hints): A/B always shown, X only if the row has a delete action - // badges only ever exist in modernUI, so their textures are unconditionally TTF struct Badge { Texture letter; Texture label; UiTheme::BadgeColor rim; UiTheme::BadgeColor fill; }; Badge badgeA{ Texture("A", 16, renderer, 0, 0, false, UiTheme::BadgeLetterColor, true), Texture(Translate("HINT_SELECT"), 16, renderer, 0, 0, false, 0xFFFFFFFF, true), UiTheme::BadgeADark, UiTheme::BadgeA }; Badge badgeB{ Texture("B", 16, renderer, 0, 0, false, UiTheme::BadgeLetterColor, true), Texture(Translate("HINT_BACK"), 16, renderer, 0, 0, false, 0xFFFFFFFF, true), UiTheme::BadgeBDark, UiTheme::BadgeB }; @@ -323,13 +294,12 @@ int main(int argc, char * argv[]) //Create Command Texture const int ChildIndent = 4*16; - const int rowTextBaseX = modernUI ? UiTheme::RowTextX : 50; const int RowGlyphSize = 16; const int RowTextGapPx = 16; // gap kept between truncated text and preview image/switch - Texture chevronText(">", RowGlyphSize, renderer, 0, 0, false, UiTheme::TextDimColor, modernUI); + Texture chevronText(">", RowGlyphSize, renderer, 0, 0, false, UiTheme::TextDimColor, true); for(Command & c : commands) { - // self-relaunch into the same dir (language, ModernUI toggle) isn't a submenu + // self-relaunch into the same dir (e.g. language selection) isn't a submenu if(c.hasSubmenu) { size_t pos = c.command.find("--commandPath"); @@ -343,47 +313,36 @@ int main(int argc, char * argv[]) if(c.name == "BACK") c.hasSubmenu = false; - int textX = rowTextBaseX + (c.child ? ChildIndent : 0); + int textX = UiTheme::RowTextX + (c.child ? ChildIndent : 0); std::string label = Translate(c.name); - // classic UI: only rows with a preview image get less width for text. - // modern UI: the list column has a fixed width regardless (previews - // now live in the detail panel, not floating on the row). - int maxRight = -1; - if(!modernUI && c.previewImage.size()) - maxRight = c.previewImageX - RowTextGapPx; - if(modernUI) - maxRight = UiTheme::RowControlRightX - RowTextGapPx; - - bool rowUsesTTF = modernUI && CanRenderWithTTF(label, RowGlyphSize); - if(maxRight >= 0) + // fixed list-column width regardless of preview - previews live in the detail panel, not floating on the row + int maxRight = UiTheme::RowControlRightX - RowTextGapPx; + bool rowUsesTTF = CanRenderWithTTF(label, RowGlyphSize); + int available = std::max(0, maxRight - textX); + if(rowUsesTTF) { - int available = std::max(0, maxRight - textX); - if(rowUsesTTF) - { - // proportional font: shrink by measured width, not a fixed px/char guess - if(MeasureTTFWidth(label, RowGlyphSize) > available) - { - int n = Utf8Length(label); - while(n > 0 && MeasureTTFWidth(TruncateUtf8(label, n) + "...", RowGlyphSize) > available) - --n; - label = TruncateUtf8(label, n) + "..."; - } - } - else + // proportional font: shrink by measured width, not a fixed px/char guess + if(MeasureTTFWidth(label, RowGlyphSize) > available) { - int maxChars = available / RowGlyphSize; - if(TruncateUtf8(label, maxChars).size() != label.size()) - label = TruncateUtf8(label, std::max(0, maxChars - 3)) + "..."; + int n = Utf8Length(label); + while(n > 0 && MeasureTTFWidth(TruncateUtf8(label, n) + "...", RowGlyphSize) > available) + --n; + label = TruncateUtf8(label, n) + "..."; } } + else + { + int maxChars = available / RowGlyphSize; + if(TruncateUtf8(label, maxChars).size() != label.size()) + label = TruncateUtf8(label, std::max(0, maxChars - 3)) + "..."; + } - c.texture = Texture(label, RowGlyphSize, renderer, textX, 0, false, 0xFFFFFFFF, modernUI); + c.texture = Texture(label, RowGlyphSize, renderer, textX, 0, false, 0xFFFFFFFF, true); } const int modernRowPitch = std::max(UiTheme::RowPitch, GetTTFLineHeight(RowGlyphSize)); - int pinnedAreaTop = UiTheme::FooterDividerY - UiTheme::PinnedBottomMargin; - if(modernUI) + int pinnedAreaTop; { int slotBottom = UiTheme::FooterDividerY - UiTheme::PinnedBottomMargin; for(int i = static_cast(commands.size())-1; i >= pinnedStartIndex; --i) @@ -395,9 +354,7 @@ int main(int argc, char * argv[]) pinnedAreaTop = slotBottom; } // scrollable rows that fit above the pinned block - varies per screen - const int DisplayItemCount = modernUI - ? std::max(1, (pinnedAreaTop - UiTheme::RowFirstY) / modernRowPitch) - : UiTheme::DisplayItemCount; + const int DisplayItemCount = std::max(1, (pinnedAreaTop - UiTheme::RowFirstY) / modernRowPitch); int topListItemNumber = 1; std::shared_ptr PreviewImage; @@ -407,8 +364,7 @@ int main(int argc, char * argv[]) const Command & currentCommand = commands[currentCommandId]; // pinned rows don't affect scroll position - int scrollBound = modernUI ? pinnedStartIndex : static_cast(commands.size()); - int scrollTarget = modernUI ? std::min(currentCommandId, std::max(0, pinnedStartIndex-1)) : currentCommandId; + int scrollTarget = std::min(currentCommandId, std::max(0, pinnedStartIndex-1)); bool updateCommandYPos = false; if(scrollTarget < topListItemNumber) @@ -424,22 +380,20 @@ int main(int argc, char * argv[]) if(updateCommandYPos) { int y = UiTheme::RowFirstY; - int rowPitch = modernUI ? modernRowPitch : 18; - for(int i = 0, count = std::min(DisplayItemCount, scrollBound-topListItemNumber); i < count; ++i) + for(int i = 0, count = std::min(DisplayItemCount, pinnedStartIndex-topListItemNumber); i < count; ++i) { Texture & rowTexture = commands[i+topListItemNumber].texture; // centered in its slot - TTF glyphs have ascender padding, top-align leaves a gap - rowTexture.rect.y = modernUI ? (y + (rowPitch - rowTexture.rect.h) / 2 + UiTheme::RowTextYNudge) : y; - y += rowPitch; + rowTexture.rect.y = y + (modernRowPitch - rowTexture.rect.h) / 2 + UiTheme::RowTextYNudge; + y += modernRowPitch; } } selectedRowRect.y = currentCommand.texture.rect.y - 2; selectedRowRect.h = currentCommand.texture.rect.h + 4; // wrap the row's actual text height (16px font8x8 or variable-height TTF), not a fixed guess - pointerRect.y = currentCommand.texture.rect.y; // scaled to fit the detail panel box, ignores the command's own previewImageX/Y/W/H - if(modernUI && currentCommand.previewImage.size()) + if(currentCommand.previewImage.size()) { PreviewImage = std::make_shared(currentCommand.previewImage, renderer, 0, 0); if(PreviewImage->rect.w > 0 && PreviewImage->rect.h > 0) @@ -460,26 +414,26 @@ int main(int argc, char * argv[]) { bool selected = &rowCommand == &commands[currentCommandId]; rowCommand.texture.Draw(renderer); - if(modernUI && rowCommand.isToggle) + if(rowCommand.isToggle) { Texture & rowSwitch = rowCommand.stateOn ? switchOn : switchOff; rowSwitch.rect.x = UiTheme::SwitchRightX - UiTheme::SwitchW; rowSwitch.rect.y = rowCommand.texture.rect.y + (rowCommand.texture.rect.h - UiTheme::SwitchH) / 2; rowSwitch.Draw(renderer); } - else if(modernUI && rowCommand.hasSubmenu) + else if(rowCommand.hasSubmenu) { chevronText.rect.x = UiTheme::RowControlRightX - chevronText.rect.w; chevronText.rect.y = rowCommand.texture.rect.y + (rowCommand.texture.rect.h - chevronText.rect.h) / 2; chevronText.Draw(renderer); } // skip for separators and the last row - nothing to separate there - if(modernUI && !selected && !rowCommand.command.empty() && !isLastOverall) + if(!selected && !rowCommand.command.empty() && !isLastOverall) DrawHLine(renderer, UiTheme::ListX, UiTheme::ListContentRightX, rowCommand.texture.rect.y + rowCommand.texture.rect.h + 3, UiTheme::BorderR, UiTheme::BorderG, UiTheme::BorderB); }; - // modern-UI-only drawing, called early - must run before DrawRow (bg before text) - auto DrawModernUI = [&]() + // called early, before DrawRow - must run first so its background is behind the row text + auto DrawChrome = [&]() { // one continuous border across header+body+footer, plus the footer divider DrawStrokeRect(renderer, UiTheme::OuterRect, UiTheme::BorderR, UiTheme::BorderG, UiTheme::BorderB, UiTheme::BorderWidth, UiTheme::BorderRadius); @@ -519,8 +473,9 @@ int main(int argc, char * argv[]) auto ConfirmDelete = [&]() -> bool { const std::string & confirmKey = commands[currentCommandId].deleteConfirmKey; - Texture confirmTitle(Translate(confirmKey.empty() ? "DELETE_CONFIRM_GENERIC" : confirmKey), 24, renderer, 640, 320, true, 0xFFFFFFFF, modernUI); - Texture confirmHint(Translate("DELETE_CONFIRM_HINT"), 16, renderer, 640, 360, true, 0xFFFFFFFF, modernUI); + Texture confirmTitle(Translate(confirmKey.empty() ? "DELETE_CONFIRM_GENERIC" : confirmKey), 24, renderer, 640, 320, true, 0xFFFFFFFF, true); + Texture confirmHint(Translate("DELETE_CONFIRM_HINT"), 16, renderer, 640, 360, true, 0xFFFFFFFF, true); + controller.GetButtonStatus(B); // B is still physically held from the long-press that triggered this - don't read it as an instant cancel bool confirmed = false; for(;;) { @@ -533,16 +488,11 @@ int main(int argc, char * argv[]) break; } sdl_context.StartFrame(); - if(modernUI) - frame.Draw(renderer, UiTheme::FrameRect); - else - { - banner.Draw(renderer); - menuU.Draw(renderer); - menuL.Draw(renderer); - } + DrawFillRect(renderer, UiTheme::FrameRect, UiTheme::BgR, UiTheme::BgG, UiTheme::BgB); + DrawStrokeRect(renderer, UiTheme::FrameRect, UiTheme::BorderR, UiTheme::BorderG, UiTheme::BorderB, UiTheme::BorderWidth, UiTheme::BorderRadius); confirmTitle.Draw(renderer); confirmHint.Draw(renderer); + SDL_SetRenderDrawColor(renderer, bgR, bgG, bgB, 0xFF); // flat helpers leave the draw color dirty sdl_context.EndFrame(); } if(!confirmed) @@ -578,7 +528,7 @@ int main(int argc, char * argv[]) { if(commands[currentCommandId].runInternal) { - commands[currentCommandId].RunCommand(sdl_context, &controller, menuL, menuU, modernUI, { gearIcon, appTitleText, appVersionText, CompComText }, bgR, bgG, bgB); + commands[currentCommandId].RunCommand(sdl_context, &controller, { gearIcon, appTitleText, appVersionText, CompComText }, bgR, bgG, bgB); if(commands[currentCommandId].isToggle) commands[currentCommandId].UpdateState(); } @@ -630,44 +580,27 @@ int main(int argc, char * argv[]) } //Draw all textures - if(modernUI) - DrawModernUI(); - else - { - banner.Draw(renderer); - menuU.Draw(renderer); - menuL.Draw(renderer); - } + DrawChrome(); titleText.Draw(renderer); int lastCommandIndex = static_cast(commands.size()) - 1; - int scrollDrawBound = modernUI ? pinnedStartIndex : static_cast(commands.size()); - for(int i = 0, count = std::min(DisplayItemCount, scrollDrawBound-topListItemNumber); i < count; ++i) + for(int i = 0, count = std::min(DisplayItemCount, pinnedStartIndex-topListItemNumber); i < count; ++i) DrawRow(commands[i+topListItemNumber], (i+topListItemNumber) == lastCommandIndex); // pinned trailing rows (Back/Exit) - always visible near the footer - if(modernUI) - for(int i = pinnedStartIndex; i <= lastCommandIndex; ++i) - DrawRow(commands[i], i == lastCommandIndex); + for(int i = pinnedStartIndex; i <= lastCommandIndex; ++i) + DrawRow(commands[i], i == lastCommandIndex); CompComText.Draw(renderer); - if(!modernUI) - { - pointerText.Draw(renderer); - if(commands[currentCommandId].deleteCommand.size()) - deleteHint.Draw(renderer); - } - - // bounded by scrollDrawBound, not commands.size() - pinned rows need no scroll + // bounded by pinnedStartIndex, not commands.size() - pinned rows need no scroll if(topListItemNumber != 0) scrollUp.Draw(renderer); - if((topListItemNumber + DisplayItemCount) < scrollDrawBound) + if((topListItemNumber + DisplayItemCount) < pinnedStartIndex) scrollDown.Draw(renderer, SDL_FLIP_VERTICAL); // flat-line helpers leave the draw color dirty - reset before EndFrame - if(modernUI) - SDL_SetRenderDrawColor(renderer, bgR, bgG, bgB, 0xFF); + SDL_SetRenderDrawColor(renderer, bgR, bgG, bgB, 0xFF); //Render Framebuffer and Wait for Next Frame sdl_context.EndFrame(); From aff48158e6729088d884542cf1928ad41e52dfeb Mon Sep 17 00:00:00 2001 From: DefKorns Date: Fri, 11 Sep 2026 02:03:47 +0000 Subject: [PATCH 004/104] Enhance preview image handling in main UI rendering --- src/command.h | 2 +- src/main.cpp | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/command.h b/src/command.h index 4805cb4..a92eb49 100644 --- a/src/command.h +++ b/src/command.h @@ -14,7 +14,7 @@ class Controller; -// header/footer chrome shared with the main screen, reused as-is for pixel-identical INTERNAL output screens +// header/footer textures, shared by reference with the main screen struct ModernChrome { Texture & gearIcon; diff --git a/src/main.cpp b/src/main.cpp index 40db7ac..7bd421a 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -449,11 +449,11 @@ int main(int argc, char * argv[]) DrawRoundedFillRect(renderer, selectedRowRect, UiTheme::SelectedRowBgR, UiTheme::SelectedRowBgG, UiTheme::SelectedRowBgB, UiTheme::BoxRadius); DrawStrokeRect(renderer, selectedRowRect, UiTheme::AccentR, UiTheme::AccentG, UiTheme::AccentB, 2, UiTheme::BoxRadius); - // just the preview image - icon/title box looked wrong with no per-command art - SDL_Rect previewBox{ UiTheme::DetailX, UiTheme::PreviewBoxY, UiTheme::DetailW, UiTheme::PreviewBoxH }; - DrawStrokeRect(renderer, previewBox, UiTheme::BorderR, UiTheme::BorderG, UiTheme::BorderB, UiTheme::BorderWidth, UiTheme::BoxRadius); + // no box when there's no preview - an empty frame looks broken if(PreviewImage.get()) { + SDL_Rect previewBox{ UiTheme::DetailX, UiTheme::PreviewBoxY, UiTheme::DetailW, UiTheme::PreviewBoxH }; + DrawStrokeRect(renderer, previewBox, UiTheme::BorderR, UiTheme::BorderG, UiTheme::BorderB, UiTheme::BorderWidth, UiTheme::BoxRadius); PreviewImage->rect.x = previewBox.x + (previewBox.w - PreviewImage->rect.w) / 2; PreviewImage->rect.y = previewBox.y + (previewBox.h - PreviewImage->rect.h) / 2; PreviewImage->Draw(renderer); @@ -475,7 +475,7 @@ int main(int argc, char * argv[]) const std::string & confirmKey = commands[currentCommandId].deleteConfirmKey; Texture confirmTitle(Translate(confirmKey.empty() ? "DELETE_CONFIRM_GENERIC" : confirmKey), 24, renderer, 640, 320, true, 0xFFFFFFFF, true); Texture confirmHint(Translate("DELETE_CONFIRM_HINT"), 16, renderer, 640, 360, true, 0xFFFFFFFF, true); - controller.GetButtonStatus(B); // B is still physically held from the long-press that triggered this - don't read it as an instant cancel + controller.GetButtonStatus(B); // consume the still-held B from the triggering long-press bool confirmed = false; for(;;) { From 2515483232ad2c797cb60cf820073dee2d89e8b0 Mon Sep 17 00:00:00 2001 From: DefKorns Date: Fri, 11 Sep 2026 02:07:52 +0000 Subject: [PATCH 005/104] Refine README for clarity on backup deletion and remove Modern UI section --- README.md | 35 ++++++++++++++++------------------- 1 file changed, 16 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index 5e43868..b413a12 100644 --- a/README.md +++ b/README.md @@ -142,10 +142,6 @@ This option will toggle read write access on your USB device. By default, if you > **Note:** If you already use external saves. You don't need to use this toggle. -#### Modern UI - -Toggles between the redesigned "settings dialog" look (dialog frame, selection highlight bar, button-hint badges, real toggle switches) and the original plain menu style. Switching restarts the Options Menu so the change applies immediately. - #### RetroArch Debugger (USB logs) This option will run your locally installed RetroArch in full verbose mode. It will also copy the config files for your RetroArch and save them to: @@ -204,9 +200,9 @@ Overwrites your wifi config with a backup located at: and reconnects to the network. -While browsing the list of saved backups, press **X** to delete the -selected backup (you will be asked to confirm with **A**, or cancel with -**B**). The list refreshes automatically after deleting. +While browsing the list of saved backups, hold **B** (~1 second) to delete +the selected backup (you will be asked to confirm with **A**, or cancel by +pressing **B** again). The list refreshes automatically after deleting. #### Restore Wifi Config (USB) @@ -216,9 +212,9 @@ Overwrites your wifi config with a backup located at: and reconnects to the network. -While browsing the list of saved backups, press **X** to delete the -selected backup (you will be asked to confirm with **A**, or cancel with -**B**). The list refreshes automatically after deleting. +While browsing the list of saved backups, hold **B** (~1 second) to delete +the selected backup (you will be asked to confirm with **A**, or cancel by +pressing **B** again). The list refreshes automatically after deleting. ## Developer Information @@ -253,12 +249,13 @@ Command Files should contain the following fields: |COMMAND_STR|Command string to be executed. Commands must be single line only. To execute multi-line scripts use a script file.| |USB_ONLY|If set to `TRUE`, the command is only loaded when a USB/SD card is mounted.| |IGNORE_INTERRUPT|If set to `TRUE`, pressing B will not interrupt an internal command while its output is being displayed.| -|PREVIEW_IMAGE|Specifies the path to a thumbnail/icon to the oprion menu.| -|PREVIEW_IMAGE_X|Position the thumbnail/icon on the X axys| -|PREVIEW_IMAGE_Y|Position the thumbnail/icon on the Y axys.| -|PREVIEW_IMAGE_WIDTH|Sets the width for the thumbnail/icon.| -|PREVIEW_IMAGE_HEIGHT|Sets the height for the thumbnail/icon.| -|DELETE_STR|Optional command string to run when the user presses X on this entry and confirms. Used to make list entries deletable (e.g. the wifi backup restore list).| +|CHILD|If set to `TRUE`, indents the row under the one above it, for visually grouping a related entry without a separate submenu.| +|PREVIEW_IMAGE|Specifies the path to a thumbnail/icon shown in the detail panel when this row is selected. Auto-scaled and centered to fit the panel.| +|PREVIEW_IMAGE_X|Unused - the detail panel always auto-centers the image.| +|PREVIEW_IMAGE_Y|Unused - the detail panel always auto-centers the image.| +|PREVIEW_IMAGE_WIDTH|Unused - the detail panel always auto-scales the image to fit.| +|PREVIEW_IMAGE_HEIGHT|Unused - the detail panel always auto-scales the image to fit.| +|DELETE_STR|Optional command string to run when the user holds B (~1 second) on this entry and confirms. Used to make list entries deletable (e.g. the wifi backup restore list).| |DELETE_CONFIRM_KEY|Optional translation key for the confirmation prompt shown before running DELETE_STR. Falls back to a generic "Delete this item?" if not set.| |STATE_STR|Optional command string that prints the current on/off state of this entry (its first line of output, case-insensitively `1`/`on`/`y`/`yes`/`true` for on, anything else for off). Presence of this field renders the entry as a toggle switch instead of a plain row; it is only read to draw the switch, not to change behavior, so COMMAND_STR is still what actually flips the setting.| @@ -266,7 +263,7 @@ Command Files should contain the following fields: #### Command String Variables -The command string supports the use of the following variables: +COMMAND_STR, DELETE_STR and STATE_STR all support the use of the following variables: |Variable|Description| |---------|------------| @@ -297,10 +294,10 @@ The options menu can also be compiled using the plain `Makefile` provided. To co - Network commands courtesy by Advokaten and DefKorns - Wifi Backup courtesy of DefKorns - Preview image aspect ratio courtesy of DefKorns -- Wifi backup delete feature (press X on a saved backup) courtesy of DefKorns +- Wifi backup delete feature (hold B on a saved backup) courtesy of DefKorns - Multi-language localization system (i18n) and translations courtesy of DefKorns - Docker-based ARM cross-compile toolchain and build tooling courtesy of DefKorns -- Redesigned "Modern UI" (dialog frame, selection highlight, toggle switches, button-hint badges) courtesy of DefKorns +- Redesigned "Modern UI" (flat-drawn dialog frame, selection highlight, toggle switches, button-hint badges) courtesy of DefKorns - Thanks to ThanosRD for assistance with UI Layout/Design ### Testing From 86d92a516860c878750065ac9d35ef594bc82bf0 Mon Sep 17 00:00:00 2001 From: DefKorns Date: Fri, 11 Sep 2026 08:32:30 +0000 Subject: [PATCH 006/104] Refactor contributions section in README for clarity and consistency --- README.md | 20 ++++++++++---------- mod/readme.md | 42 +++++++++++++++++++++++++----------------- 2 files changed, 35 insertions(+), 27 deletions(-) diff --git a/README.md b/README.md index b413a12..3a7618f 100644 --- a/README.md +++ b/README.md @@ -289,16 +289,16 @@ The options menu can also be compiled using the plain `Makefile` provided. To co ### Contributions -- Hibernate Mod and Hakchi-Option-Pack scripts courtesy of Swingflip -- Retroarch Configuration scripts courtesy of BsLeNuL -- Network commands courtesy by Advokaten and DefKorns -- Wifi Backup courtesy of DefKorns -- Preview image aspect ratio courtesy of DefKorns -- Wifi backup delete feature (hold B on a saved backup) courtesy of DefKorns -- Multi-language localization system (i18n) and translations courtesy of DefKorns -- Docker-based ARM cross-compile toolchain and build tooling courtesy of DefKorns -- Redesigned "Modern UI" (flat-drawn dialog frame, selection highlight, toggle switches, button-hint badges) courtesy of DefKorns -- Thanks to ThanosRD for assistance with UI Layout/Design +- **Swingflip** — Hibernate Mod and Hakchi-Option-Pack scripts +- **BsLeNuL** — Retroarch Configuration scripts +- **Advokaten** — Network commands (with DefKorns) +- **DefKorns** + - Network commands (with Advokaten) + - Wifi Backup, preview image aspect ratio, delete feature (hold B on a saved backup) + - Multi-language localization system (i18n) and translations + - Docker-based ARM cross-compile toolchain and build tooling + - Redesigned "Modern UI" (dialog frame, selection highlight, toggle switches, button-hint badges) +- **ThanosRD** — UI Layout/Design assistance (original Classic UI) ### Testing diff --git a/mod/readme.md b/mod/readme.md index d3281cf..9067bb0 100644 --- a/mod/readme.md +++ b/mod/readme.md @@ -1,40 +1,48 @@ - ## What is the options menu? -The Options Menu is a custom menu that can be launched by holding controller button combo (L+R) for more than 1 second at any point during the console’s operation. +The Options Menu is a custom, easily extendable menu for your console, letting other developers add their own commands to it via hmods. -Current features provided by the options menu include: +### Features +- Modern UI - Hmod Uninstaller - Hibernate Mod - Retroarch Settings Management - Wifi Backup -- Latin Font Support +- Multi-language Support + +### How to open + +Hold **L + R** for about 1 second, at any point during the console's operation. ## Documentation -Full documentation for the options menu can be found [here](https://github.com/CompCom/OptionsMenu). +Full documentation for the options menu can be found in the [OptionsMenu README](https://github.com/DefKorns/OptionsMenu). ## Contributions and Thanks ### Contributions -- Hibernate Mod and Hakchi-Option-Pack scripts courtesy of Swingflip -- Retroarch Configuration scripts courtesy of BsLeNuL -- Network commands courtesy by Advokaten and DefKorns -- Wifi Backup courtesy of DefKorns -- Preview image aspect ratio courtesy of DefKorns -- Thanks to ThanosRD for assistance with UI Layout/Design +- **Swingflip** — Hibernate Mod and Hakchi-Option-Pack scripts +- **BsLeNuL** — Retroarch Configuration scripts +- **Advokaten** — Network commands (with DefKorns) +- **DefKorns** + - Network commands (with Advokaten) + - Wifi Backup, preview image aspect ratio, delete feature (hold B on a saved backup) + - Multi-language localization system (i18n) and translations + - Docker-based ARM cross-compile toolchain and build tooling + - Redesigned "Modern UI" (dialog frame, selection highlight, toggle switches, button-hint badges) +- **ThanosRD** — UI Layout/Design assistance (original Classic UI) ### Testing -Extra thanks to DNA64 (viral_dna) and Swingflip for always testing features. +Extra thanks to **DNA64 (viral_dna)** and **Swingflip** for always testing features. Also thanks to the following people for testing the options menu: -- BsLeNuL -- DefKorns -- DR1001 -- Patton Plays -- ThanosRD +- **BsLeNuL** +- **DefKorns** +- **DR1001** +- **Patton Plays** +- **ThanosRD** From e4840fbb36e4e4faf475582cac75eb8f6b7a86af Mon Sep 17 00:00:00 2001 From: DefKorns Date: Fri, 11 Sep 2026 08:35:37 +0000 Subject: [PATCH 007/104] Update build workflow to improve vendor repo handling and toolchain image naming --- .github/workflows/build.yml | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index cb807c5..0be9b66 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -11,19 +11,22 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 + with: + submodules: true # vendor/ holds the console's own SDL2 headers + libSDL2.so, extracted # from the console's firmware - proprietary, so it isn't committed to # this (public) repo. It's too large for a GitHub Secret (2.6MB, the # limit is ~48-64KB), so it's kept in a separate PRIVATE repo instead # and cloned here with a fine-grained PAT (repo secret VENDOR_REPO_PAT, - # read-only access to that one private repo). + # read-only access to that one private repo). Lands in toolchain/vendor - + # the Dockerfile's build context is the toolchain/ submodule, not repo root. - name: Restore vendor/ from private repo run: | - git clone --depth 1 "https://x-access-token:${{ secrets.VENDOR_REPO_PAT }}@github.com/${{ secrets.VENDOR_REPO }}.git" vendor + git clone --depth 1 "https://x-access-token:${{ secrets.VENDOR_REPO_PAT }}@github.com/${{ secrets.VENDOR_REPO }}.git" toolchain/vendor - name: Build cross-compile toolchain image - run: docker build -f Dockerfile.jessie-armhf -t optionsmenu-toolchain:jessie . + run: docker build -f toolchain/Dockerfile.jessie-armhf -t hakchi-toolchain:jessie-armhf toolchain # On a tag push (release build), force MOD_VER to the tag name itself # rather than trusting `git describe` inside the container -- checkout @@ -37,7 +40,7 @@ jobs: if [ "${{ github.ref_type }}" = "tag" ]; then MOD_VER_ARG="MOD_VER=${{ github.ref_name }}" fi - docker run --rm -v "${{ github.workspace }}:/src" optionsmenu-toolchain:jessie make -f Makefile.docker CROSS_PREFIX=arm-linux-gnueabihf- $MOD_VER_ARG + docker run --rm -v "${{ github.workspace }}:/src" hakchi-toolchain:jessie-armhf make -f Makefile.docker CROSS_PREFIX=arm-linux-gnueabihf- $MOD_VER_ARG - name: Upload .hmod build artifact uses: actions/upload-artifact@v4 From 2203fa6e944e6ef9c91d1219473b838ce5947b79 Mon Sep 17 00:00:00 2001 From: DefKorns Date: Fri, 11 Sep 2026 08:46:49 +0000 Subject: [PATCH 008/104] Update GitHub Actions to use latest action versions for checkout, upload, and release --- .github/workflows/build.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 0be9b66..097b54a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -10,7 +10,7 @@ jobs: build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 with: submodules: true @@ -43,7 +43,7 @@ jobs: docker run --rm -v "${{ github.workspace }}:/src" hakchi-toolchain:jessie-armhf make -f Makefile.docker CROSS_PREFIX=arm-linux-gnueabihf- $MOD_VER_ARG - name: Upload .hmod build artifact - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: options-menu-hmod path: out/*.hmod @@ -54,13 +54,13 @@ jobs: runs-on: ubuntu-latest steps: - name: Download build artifact - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v8 with: name: options-menu-hmod path: out - name: Create GitHub Release - uses: softprops/action-gh-release@v2 + uses: softprops/action-gh-release@v3 with: files: out/*.hmod generate_release_notes: true From 40734ba8dc73c4dd50a3d2791c1e47274abd4035 Mon Sep 17 00:00:00 2001 From: DefKorns Date: Fri, 11 Sep 2026 08:50:49 +0000 Subject: [PATCH 009/104] Add build and license badges to README for improved visibility --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index 3a7618f..332a80e 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,8 @@ # Options Menu Documentation +[![Build](https://github.com/DefKorns/OptionsMenu/actions/workflows/build.yml/badge.svg)](https://github.com/DefKorns/OptionsMenu/actions/workflows/build.yml) +[![License: GPL v3](https://img.shields.io/badge/License-GPLv3-blue.svg)](LICENSE) + ## What is the options menu? The Options Menu is a custom menu that can be launched via a controller button combo at any point during the console’s operation. It features a variety of commands to enhance the user experience of the console. From 7486a5bd52a8299a45eda070ed7f63b92d082f5a Mon Sep 17 00:00:00 2001 From: DefKorns Date: Fri, 11 Sep 2026 12:18:38 +0000 Subject: [PATCH 010/104] Add option combo configuration messages in multiple languages --- mod/etc/options_menu/language/de-DE.lang | 11 +++++++++++ mod/etc/options_menu/language/en-US.lang | 11 +++++++++++ mod/etc/options_menu/language/es-ES.lang | 11 +++++++++++ mod/etc/options_menu/language/fr-FR.lang | 11 +++++++++++ mod/etc/options_menu/language/it-IT.lang | 11 +++++++++++ mod/etc/options_menu/language/ja-JP.lang | 11 +++++++++++ mod/etc/options_menu/language/nl-NL.lang | 11 +++++++++++ mod/etc/options_menu/language/pt-PT.lang | 13 ++++++++++++- mod/etc/options_menu/language/ru-RU.lang | 11 +++++++++++ 9 files changed, 100 insertions(+), 1 deletion(-) diff --git a/mod/etc/options_menu/language/de-DE.lang b/mod/etc/options_menu/language/de-DE.lang index b01c058..47ebab1 100644 --- a/mod/etc/options_menu/language/de-DE.lang +++ b/mod/etc/options_menu/language/de-DE.lang @@ -48,3 +48,14 @@ HINT_BACK=Zurück HINT_DELETE=Löschen (Halten) NO_NET_ERR=Verbindung zum Server fehlgeschlagen. Bitte Internetverbindung prüfen und erneut versuchen. NO_PKG_ERR=Das angeforderte Paket ist derzeit nicht verfügbar. Bitte später erneut versuchen. +CHANGE_COMBO_INTRO_1=Wir legen jetzt die Tastenkombination für das Optionsmenü fest. +CHANGE_COMBO_INTRO_2=Du musst die 3 Tasten drücken, die für das Optionsmenü verwendet werden sollen. +CHANGE_COMBO_INTRO_3=Für eine Zwei-Tasten-Kombination einfach die 2. Taste wiederholen. +CHANGE_COMBO_CONTINUE=A zum Fortfahren, B zum Abbrechen. +CHANGE_COMBO_PRESS_1=Jetzt die erste Taste drücken... +CHANGE_COMBO_PRESS_2=Jetzt die zweite Taste drücken... +CHANGE_COMBO_PRESS_3=Jetzt die dritte Taste drücken... +CHANGE_COMBO_CHOSEN=Deine gewählte Kombination ist: +CHANGE_COMBO_CONFIRM=Wenn das stimmt, A drücken. Andernfalls eine beliebige andere Taste zum erneuten Versuch. +CHANGE_COMBO_DONE=Fertig. Bitte neu starten, damit die Änderung wirksam wird. +CHANGE_COMBO_WRITE_ERROR=Die Tastenkombination konnte nicht gespeichert werden. diff --git a/mod/etc/options_menu/language/en-US.lang b/mod/etc/options_menu/language/en-US.lang index a79abc2..6a5f00e 100644 --- a/mod/etc/options_menu/language/en-US.lang +++ b/mod/etc/options_menu/language/en-US.lang @@ -48,3 +48,14 @@ HINT_BACK=Back HINT_DELETE=Delete (Hold) NO_NET_ERR=Error while connecting to server, please check your internet connection and try again. NO_PKG_ERR=The package you requested isn't available at the moment, please try again later. +CHANGE_COMBO_INTRO_1=We will now set your options menu combo buttons. +CHANGE_COMBO_INTRO_2=You will need to press the 3 buttons you want the options menu set to. +CHANGE_COMBO_INTRO_3=For a two button combo just repeat the 2nd button. +CHANGE_COMBO_CONTINUE=Press A to continue, or B to cancel. +CHANGE_COMBO_PRESS_1=Press the first button now... +CHANGE_COMBO_PRESS_2=Press the second button now... +CHANGE_COMBO_PRESS_3=Press the third button now... +CHANGE_COMBO_CHOSEN=Your chosen options menu combo is: +CHANGE_COMBO_CONFIRM=If this is correct press A. If not press any other button to try again. +CHANGE_COMBO_DONE=Done. Please reboot to take effect. +CHANGE_COMBO_WRITE_ERROR=Could not write button combo to file. diff --git a/mod/etc/options_menu/language/es-ES.lang b/mod/etc/options_menu/language/es-ES.lang index beaa3a1..1c467fd 100644 --- a/mod/etc/options_menu/language/es-ES.lang +++ b/mod/etc/options_menu/language/es-ES.lang @@ -48,3 +48,14 @@ HINT_BACK=Atrás HINT_DELETE=Borrar (mantener) NO_NET_ERR=Error al conectar con el servidor, comprueba tu conexión a internet e inténtalo de nuevo. NO_PKG_ERR=El paquete solicitado no está disponible en este momento, inténtalo de nuevo más tarde. +CHANGE_COMBO_INTRO_1=Ahora vamos a configurar la combinación de botones del menú de opciones. +CHANGE_COMBO_INTRO_2=Tendrás que pulsar los 3 botones que quieras usar para el menú de opciones. +CHANGE_COMBO_INTRO_3=Para una combinación de 2 botones, repite el 2º botón. +CHANGE_COMBO_CONTINUE=Pulsa A para continuar, o B para cancelar. +CHANGE_COMBO_PRESS_1=Pulsa el primer botón ahora... +CHANGE_COMBO_PRESS_2=Pulsa el segundo botón ahora... +CHANGE_COMBO_PRESS_3=Pulsa el tercer botón ahora... +CHANGE_COMBO_CHOSEN=La combinación elegida es: +CHANGE_COMBO_CONFIRM=Si es correcta pulsa A. Si no, pulsa cualquier otro botón para intentarlo de nuevo. +CHANGE_COMBO_DONE=Listo. Reinicia la consola para aplicar los cambios. +CHANGE_COMBO_WRITE_ERROR=No se pudo guardar la combinación en el archivo. diff --git a/mod/etc/options_menu/language/fr-FR.lang b/mod/etc/options_menu/language/fr-FR.lang index d8f63f6..b17826a 100644 --- a/mod/etc/options_menu/language/fr-FR.lang +++ b/mod/etc/options_menu/language/fr-FR.lang @@ -48,3 +48,14 @@ HINT_BACK=Retour HINT_DELETE=Suppr. (Maintenir) NO_NET_ERR=Erreur de connexion au serveur, vérifiez votre connexion internet et réessayez. NO_PKG_ERR=Le paquet demandé n'est pas disponible pour le moment, veuillez réessayer plus tard. +CHANGE_COMBO_INTRO_1=Nous allons maintenant définir la combinaison de boutons du menu d'options. +CHANGE_COMBO_INTRO_2=Tu devras appuyer sur les 3 boutons à utiliser pour le menu d'options. +CHANGE_COMBO_INTRO_3=Pour une combinaison à 2 boutons, répète simplement le 2e bouton. +CHANGE_COMBO_CONTINUE=Appuie sur A pour continuer, ou B pour annuler. +CHANGE_COMBO_PRESS_1=Appuie sur le premier bouton maintenant... +CHANGE_COMBO_PRESS_2=Appuie sur le deuxième bouton maintenant... +CHANGE_COMBO_PRESS_3=Appuie sur le troisième bouton maintenant... +CHANGE_COMBO_CHOSEN=La combinaison choisie est : +CHANGE_COMBO_CONFIRM=Si c'est correct, appuie sur A. Sinon, appuie sur un autre bouton pour réessayer. +CHANGE_COMBO_DONE=Terminé. Redémarre la console pour appliquer les changements. +CHANGE_COMBO_WRITE_ERROR=Impossible d'enregistrer la combinaison dans le fichier. diff --git a/mod/etc/options_menu/language/it-IT.lang b/mod/etc/options_menu/language/it-IT.lang index 7aa6d38..b268cef 100644 --- a/mod/etc/options_menu/language/it-IT.lang +++ b/mod/etc/options_menu/language/it-IT.lang @@ -48,3 +48,14 @@ HINT_BACK=Indietro HINT_DELETE=Elimina (tieni premuto) NO_NET_ERR=Errore di connessione al server, controlla la tua connessione internet e riprova. NO_PKG_ERR=Il pacchetto richiesto non è disponibile al momento, riprova più tardi. +CHANGE_COMBO_INTRO_1=Ora imposteremo la combinazione di tasti del menu opzioni. +CHANGE_COMBO_INTRO_2=Dovrai premere i 3 tasti che vuoi impostare per il menu opzioni. +CHANGE_COMBO_INTRO_3=Per una combinazione a 2 tasti, ripeti semplicemente il 2° tasto. +CHANGE_COMBO_CONTINUE=Premi A per continuare, o B per annullare. +CHANGE_COMBO_PRESS_1=Premi il primo tasto ora... +CHANGE_COMBO_PRESS_2=Premi il secondo tasto ora... +CHANGE_COMBO_PRESS_3=Premi il terzo tasto ora... +CHANGE_COMBO_CHOSEN=La combinazione scelta è: +CHANGE_COMBO_CONFIRM=Se è corretta premi A. Altrimenti premi un altro tasto per riprovare. +CHANGE_COMBO_DONE=Fatto. Riavvia la console per applicare le modifiche. +CHANGE_COMBO_WRITE_ERROR=Impossibile salvare la combinazione nel file. diff --git a/mod/etc/options_menu/language/ja-JP.lang b/mod/etc/options_menu/language/ja-JP.lang index e7b935e..6e5f71e 100644 --- a/mod/etc/options_menu/language/ja-JP.lang +++ b/mod/etc/options_menu/language/ja-JP.lang @@ -48,3 +48,14 @@ HINT_BACK=もどる HINT_DELETE=けす (ながおし) NO_NET_ERR=Serverへのせつぞくにしっぱいしました. Internetせつぞくをかくにんして, もういちどおためしください. NO_PKG_ERR=ようきゅうされたPackageは, いま, りようできません. しばらくしてから, もういちどおためしください. +CHANGE_COMBO_INTRO_1=これから Optionsmenuの Buttonくみあわせを せっていします +CHANGE_COMBO_INTRO_2=Optionsmenuにつかう3つのButtonをおしてください +CHANGE_COMBO_INTRO_3=2つのButtonくみあわせにするときは, 2ばんめのButtonをもういちどおしてください +CHANGE_COMBO_CONTINUE=Aでつづける, Bでキャンセル +CHANGE_COMBO_PRESS_1=1ばんめのButtonをいまおしてください... +CHANGE_COMBO_PRESS_2=2ばんめのButtonをいまおしてください... +CHANGE_COMBO_PRESS_3=3ばんめのButtonをいまおしてください... +CHANGE_COMBO_CHOSEN=えらんだくみあわせ: +CHANGE_COMBO_CONFIRM=これでよければA, ちがえばほかのButtonをおしてください +CHANGE_COMBO_DONE=かんりょう. てきようするにはさいきどうしてください +CHANGE_COMBO_WRITE_ERROR=くみあわせをFileにほぞんできませんでした diff --git a/mod/etc/options_menu/language/nl-NL.lang b/mod/etc/options_menu/language/nl-NL.lang index 3b525e1..d533be6 100644 --- a/mod/etc/options_menu/language/nl-NL.lang +++ b/mod/etc/options_menu/language/nl-NL.lang @@ -48,3 +48,14 @@ HINT_BACK=Terug HINT_DELETE=Wissen (vasthouden) NO_NET_ERR=Verbinding met de server mislukt, controleer je internetverbinding en probeer het opnieuw. NO_PKG_ERR=Het gevraagde pakket is momenteel niet beschikbaar, probeer het later opnieuw. +CHANGE_COMBO_INTRO_1=We gaan nu de knoppencombinatie voor het optiemenu instellen. +CHANGE_COMBO_INTRO_2=Je moet de 3 knoppen indrukken die je voor het optiemenu wilt instellen. +CHANGE_COMBO_INTRO_3=Voor een combinatie van 2 knoppen herhaal je gewoon de 2e knop. +CHANGE_COMBO_CONTINUE=Druk op A om door te gaan, of B om te annuleren. +CHANGE_COMBO_PRESS_1=Druk nu op de eerste knop... +CHANGE_COMBO_PRESS_2=Druk nu op de tweede knop... +CHANGE_COMBO_PRESS_3=Druk nu op de derde knop... +CHANGE_COMBO_CHOSEN=Je gekozen combinatie is: +CHANGE_COMBO_CONFIRM=Als dit klopt, druk op A. Zo niet, druk op een andere knop om opnieuw te proberen. +CHANGE_COMBO_DONE=Klaar. Herstart de console om de wijziging toe te passen. +CHANGE_COMBO_WRITE_ERROR=Kon de combinatie niet opslaan in het bestand. diff --git a/mod/etc/options_menu/language/pt-PT.lang b/mod/etc/options_menu/language/pt-PT.lang index b9b5e4c..aba8f0c 100644 --- a/mod/etc/options_menu/language/pt-PT.lang +++ b/mod/etc/options_menu/language/pt-PT.lang @@ -37,7 +37,7 @@ RA_DELETE_BIOS=Apagar Ficheiro(s) de BIOS da NAND RA_TOGGLE_LOAD_SCREENS=Alternar Ecrãs de Carregamento RA e Canoe RESTORE_WIFI_CONFIG=Restaurar Configuração Wifi OPTIONS_TITLE=Opções -PRESS_B_EXIT=Pressione B para sair. +PRESS_B_EXIT=Prime B para sair. WIFI_BACKUP_SPLASH_LINE1=Cópia de Segurança Wifi WIFI_BACKUP_SPLASH_LINE2=Aguarde Por Favor . . . DELETE_BACKUP_CONFIRM=Apagar esta cópia de segurança? @@ -48,3 +48,14 @@ HINT_BACK=Voltar HINT_DELETE=Apagar (manter premido) NO_NET_ERR=Erro ao ligar ao servidor, verifica a tua ligação à internet e tenta novamente. NO_PKG_ERR=O pacote pedido não está disponível de momento, tenta novamente mais tarde. +CHANGE_COMBO_INTRO_1=Vamos agora definir a combinação de botões do menu de opções. +CHANGE_COMBO_INTRO_2=Vais precisar de premir os 3 botões que queres definir para o menu de opções. +CHANGE_COMBO_INTRO_3=Para uma combinação de 2 botões, repete o 2º botão. +CHANGE_COMBO_CONTINUE=Prime A para continuar, ou B para cancelar. +CHANGE_COMBO_PRESS_1=Prime o primeiro botão agora... +CHANGE_COMBO_PRESS_2=Prime o segundo botão agora... +CHANGE_COMBO_PRESS_3=Prime o terceiro botão agora... +CHANGE_COMBO_CHOSEN=A combinação escolhida é: +CHANGE_COMBO_CONFIRM=Se estiver correta prime A. Caso contrário, prime qualquer outro botão para tentar de novo. +CHANGE_COMBO_DONE=Concluído. Reinicia a consola para aplicar. +CHANGE_COMBO_WRITE_ERROR=Não foi possível guardar a combinação no ficheiro. diff --git a/mod/etc/options_menu/language/ru-RU.lang b/mod/etc/options_menu/language/ru-RU.lang index e49362a..c902463 100644 --- a/mod/etc/options_menu/language/ru-RU.lang +++ b/mod/etc/options_menu/language/ru-RU.lang @@ -48,3 +48,14 @@ HINT_BACK=Назад HINT_DELETE=Удалить (удерж.) NO_NET_ERR=Ошибка подключения к серверу, проверьте подключение к интернету и попробуйте снова. NO_PKG_ERR=Запрошенный пакет сейчас недоступен, попробуйте позже. +CHANGE_COMBO_INTRO_1=Сейчас мы настроим комбинацию кнопок для меню опций. +CHANGE_COMBO_INTRO_2=Нажмите 3 кнопки, которые хотите назначить для меню опций. +CHANGE_COMBO_INTRO_3=Для комбинации из 2 кнопок просто повторите 2-ю кнопку. +CHANGE_COMBO_CONTINUE=Нажмите A, чтобы продолжить, или B, чтобы отменить. +CHANGE_COMBO_PRESS_1=Нажмите первую кнопку сейчас... +CHANGE_COMBO_PRESS_2=Нажмите вторую кнопку сейчас... +CHANGE_COMBO_PRESS_3=Нажмите третью кнопку сейчас... +CHANGE_COMBO_CHOSEN=Выбранная комбинация: +CHANGE_COMBO_CONFIRM=Если верно, нажмите A. Если нет, нажмите любую другую кнопку, чтобы попробовать снова. +CHANGE_COMBO_DONE=Готово. Перезагрузите консоль, чтобы применить изменения. +CHANGE_COMBO_WRITE_ERROR=Не удалось сохранить комбинацию в файл. From cf4adac788c6567d114317060a39ce8e49a9be4b Mon Sep 17 00:00:00 2001 From: DefKorns Date: Fri, 11 Sep 2026 12:19:35 +0000 Subject: [PATCH 011/104] Remove unused build target and clean up Makefile --- Makefile.docker | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/Makefile.docker b/Makefile.docker index d792e3e..1669de4 100644 --- a/Makefile.docker +++ b/Makefile.docker @@ -48,17 +48,11 @@ define inject-readme = sed 1d temp/readme.md >> mod/readme.md endef -all: $(OUT)/$(TARGET_NAME).hmod $(OUT)/$(TARGET_NAME)_b_down.hmod +all: $(OUT)/$(TARGET_NAME).hmod bump-build-number: echo $$(( $$(cat $(BUILD_NUM_FILE) 2>/dev/null || echo 0) + 1 )) > $(BUILD_NUM_FILE) -$(OUT)/$(TARGET_NAME)_b_down.hmod: $(OUT)/$(TARGET_NAME).hmod bump-build-number - $(inject-readme) - cp button.cfg mod/etc/options_menu/ - cd mod/; tar -czvf "../out/$(TARGET_NAME)_b_down.hmod" * - cp temp/readme.md mod/readme.md - $(OUT)/$(TARGET_NAME).hmod: mod/etc/options_menu/options mod/etc/options_menu/optiond mod/etc/options_menu/mod_uninstall/mod_uninstall mod/bin/standby_watchdog mod/etc/options_menu/scripts/gen_splash $(VENDORED_TTF_LIBS) bump-build-number $(CROSS_PREFIX)$(STRIP) mod/etc/options_menu/options $(CROSS_PREFIX)$(STRIP) mod/etc/options_menu/optiond @@ -69,7 +63,6 @@ $(OUT)/$(TARGET_NAME).hmod: mod/etc/options_menu/options mod/etc/options_menu/op $(CROSS_PREFIX)$(STRIP) mod/etc/options_menu/scripts/gen_splash $(inject-readme) - rm -f mod/etc/options_menu/button.cfg cd mod/; tar -czvf "../out/$(TARGET_NAME).hmod" * cp temp/readme.md mod/readme.md @@ -105,7 +98,7 @@ mod/etc/options_menu/scripts/gen_splash: src/gen_splash.o src/localization.o clean: find -name "*.o" -type f -delete - rm -f mod/etc/options_menu/options mod/etc/options_menu/optiond mod/etc/options_menu/mod_uninstall/mod_uninstall mod/bin/standby_watchdog mod/etc/options_menu/scripts/gen_splash mod/etc/options_menu/button.cfg mod/etc/options_menu/libSDL2_ttf-2.0.so.0 mod/etc/options_menu/libfreetype.so.2.8 $(TARGET_NAME).hmod $(TARGET_NAME)_b_down.hmod + rm -f mod/etc/options_menu/options mod/etc/options_menu/optiond mod/etc/options_menu/mod_uninstall/mod_uninstall mod/bin/standby_watchdog mod/etc/options_menu/scripts/gen_splash mod/etc/options_menu/libSDL2_ttf-2.0.so.0 mod/etc/options_menu/libfreetype.so.2.8 $(TARGET_NAME).hmod rm -rf out/ temp/ .PHONY: clean bump-build-number From f00489bd33aa66df88808623a5538b5149a01535 Mon Sep 17 00:00:00 2001 From: DefKorns Date: Fri, 11 Sep 2026 12:21:11 +0000 Subject: [PATCH 012/104] Fix power-off hang, Back navigation, and auto-detect the launch combo - Auto-detect L+R vs B+Down (no shoulder buttons) via the console's own boardtype file, replacing the old hardcoded default and unused GetSpriteSheet.sh output. - Fix RunCommand's pipe reader relying on poll() alone, which left already-buffered lines stuck until unrelated new data arrived. - Stop polling the controller in RunCommand while a command reads input itself, and don't hang the power-off path behind SDL teardown. - Collapse duplicate slashes in path handling so self-relaunches (e.g. after changing language) don't push a stale Back-stack entry pointing at themselves. - Extend mod/uninstall to clean up runtime-generated files hakchi's own package tracking never covered (language.cfg, ui_style.cfg, generated wifi-restore commands, disabled command files, hibernate_mod). --- README.md | 2 + button.cfg | 1 - .../options_menu/scripts/GetSpriteSheet.sh | 34 -------------- mod/readme.md | 2 +- mod/uninstall | 8 ++++ src/command.cpp | 38 +++++++++++++--- src/command.h | 15 +++++++ src/daemon.cpp | 39 +++++++++++----- src/framework/sdl_context.cpp | 4 -- src/main.cpp | 44 ++++++++++++++----- 10 files changed, 118 insertions(+), 69 deletions(-) delete mode 100644 button.cfg delete mode 100644 mod/etc/options_menu/scripts/GetSpriteSheet.sh diff --git a/README.md b/README.md index 332a80e..a61f9e3 100644 --- a/README.md +++ b/README.md @@ -161,6 +161,8 @@ The full verbose log will be located here: This option will allow you to change the button combo used to launch the options menu. You will need to restart the console after setting a new combo. +> **Note:** The default combo is **L + R**, or **B + Down** on consoles with no shoulder buttons (NES Classic, Famicom Classic and its variants, e.g. the Famicom Shonen Jump 50th Anniversary edition), detected automatically. Setting a custom combo here overrides that detection. + #### Clear Cache This option will force the kernel to clear the page cache and free up unused memory. You should not need to call this as the system should manage memory fine on it's own. diff --git a/button.cfg b/button.cfg deleted file mode 100644 index 5b1929e..0000000 --- a/button.cfg +++ /dev/null @@ -1 +0,0 @@ -305 707 707 \ No newline at end of file diff --git a/mod/etc/options_menu/scripts/GetSpriteSheet.sh b/mod/etc/options_menu/scripts/GetSpriteSheet.sh deleted file mode 100644 index 5210df3..0000000 --- a/mod/etc/options_menu/scripts/GetSpriteSheet.sh +++ /dev/null @@ -1,34 +0,0 @@ -#!/bin/sh - -# Get Sprite Sheet code based on External UI hmod by DanTheMan827 - -source /etc/preinit -script_init - -region="$sfregion" -# For backwards compatibility with previous hakchi versions -[ "$(cat "$mountpoint/etc/clover/REGION")" = "EUR" ] && region="eur" - -case "$sftype-$region" in - nes-usa) - echo -ne "$mountpoint/usr/share/clover-ui/resources/sprites/nes.png" - ;; - nes-eur) - echo -ne "$mountpoint/usr/share/clover-ui/resources/sprites/nes.png" - ;; - nes-jpn) - echo -ne "$mountpoint/usr/share/clover-ui/resources/sprites/hvc.png" - ;; - hvcj-jpn) - echo -ne "$mountpoint/usr/share/ui/hvc/resources/sprites/packed.png" - ;; - snes-usa) - echo -ne "$mountpoint/usr/share/ui/snes-usa/resources/sprites/packed.png" - ;; - snes-eur) - echo -ne "$mountpoint/usr/share/ui/snes-eur/resources/sprites/packed.png" - ;; - snes-jpn) - echo -ne "$mountpoint/usr/share/ui/shvc/resources/sprites/packed.png" - ;; -esac diff --git a/mod/readme.md b/mod/readme.md index 9067bb0..ece4a05 100644 --- a/mod/readme.md +++ b/mod/readme.md @@ -14,7 +14,7 @@ The Options Menu is a custom, easily extendable menu for your console, letting o ### How to open -Hold **L + R** for about 1 second, at any point during the console's operation. +Hold **L + R** for about 1 second, at any point during the console's operation. On consoles with no shoulder buttons (NES Classic, Famicom Classic and its variants, e.g. the Famicom Shonen Jump 50th Anniversary edition), hold **B + Down** instead. ## Documentation diff --git a/mod/uninstall b/mod/uninstall index 0fe4ab4..224deaf 100644 --- a/mod/uninstall +++ b/mod/uninstall @@ -1 +1,9 @@ +#!/bin/sh +# shellcheck disable=SC2154 rm -f "$rootfs/etc/options_menu/button.cfg" +rm -f "$rootfs/etc/options_menu/language.cfg" +rm -f "$rootfs/etc/options_menu/ui_style.cfg" +rm -f "$rootfs/etc/options_menu/network/restore/commands"/c0001_* +rm -f "$rootfs/etc/options_menu/network/commands"/_* +find "$rootfs/etc/options_menu" -depth -type d -empty -delete 2>/dev/null +find "$rootfs/etc/hibernate_mod" -depth -type d -empty -delete 2>/dev/null diff --git a/src/command.cpp b/src/command.cpp index ef7d3cf..7581c14 100644 --- a/src/command.cpp +++ b/src/command.cpp @@ -10,6 +10,7 @@ #include "command.h" #include "framework/controller.h" #include "framework/draw_helpers.h" +#include "framework/powerwatch.h" #include "localization.h" #include @@ -17,6 +18,8 @@ #include #include #include +#include +#include // std::stoi throws on non-numeric input; avoid crashing on a bad command file static int SafeStoi(const std::string & value, int fallback = 0) @@ -130,12 +133,16 @@ void Command::RunCommand(SDL_Context & sdl_context, Controller * controller, con }; int fd = fileno(pipe); + fcntl(fd, F_SETFL, O_NONBLOCK); // so a drained-but-still-buffered fgets() below never blocks while(!feof(pipe)) { - // poll with a short timeout instead of blocking fgets, so a script - // that stalls without output doesn't also freeze input/rendering + // poll just paces the loop now (avoids busy-spinning) - it only reflects + // kernel-level readiness, so a burst of lines that all arrive in one + // read() would otherwise pile up in stdio's own buffer, invisible to + // poll() until some later line's arrival makes it true again struct pollfd pfd{fd, POLLIN, 0}; - if(poll(&pfd, 1, 100) > 0 && fgets(buffer, 128, pipe) != nullptr) + poll(&pfd, 1, 100); + while(fgets(buffer, 128, pipe) != nullptr) { std::string sBuffer(buffer); int pos = sBuffer.find('\n'); @@ -148,10 +155,24 @@ void Command::RunCommand(SDL_Context & sdl_context, Controller * controller, con textList.erase(textList.begin()); } } + if(!feof(pipe)) + clearerr(pipe); // last fgets() failed because nothing's ready yet, not EOF - stay readable - controller->Update(); - if(!ignoreInterrupt && controller->GetButtonStatus(B)) - break; + // power press must exit even mid-command - skip SDL teardown, same as main.cpp's loop + if(sdl_context.powerwatch->buttonPress()) + { + system(_exitManager.exitCommand.c_str()); + _exit(0); + } + + // ignoreInterrupt commands (e.g. ChangeCombo) read the controller themselves - + // stop polling it here too, or we race them for the same button events + if(!ignoreInterrupt) + { + controller->Update(); + if(controller->GetButtonStatus(B)) + break; + } render(); } @@ -159,6 +180,11 @@ void Command::RunCommand(SDL_Context & sdl_context, Controller * controller, con while (!controller->GetButtonStatus(B)) { + if(sdl_context.powerwatch->buttonPress()) + { + system(_exitManager.exitCommand.c_str()); + _exit(0); + } controller->Update(); render(); } diff --git a/src/command.h b/src/command.h index a92eb49..0e88683 100644 --- a/src/command.h +++ b/src/command.h @@ -12,6 +12,8 @@ #include "framework/sdl_helper.h" +#include + class Controller; // header/footer textures, shared by reference with the main screen @@ -23,6 +25,19 @@ struct ModernChrome Texture & creditText; }; +// runs the paused-UI resume script on exit, unless something (delete, relaunch) disables it +struct ExitManager +{ + std::string exitCommand; + bool runExitCommand = true; + ~ExitManager() + { + if(runExitCommand) + system(exitCommand.c_str()); + } +}; +extern ExitManager _exitManager; + struct Command { Command(); diff --git a/src/daemon.cpp b/src/daemon.cpp index 3e5e30c..abfa616 100644 --- a/src/daemon.cpp +++ b/src/daemon.cpp @@ -12,27 +12,44 @@ #include #include #include +#include int main() { Controller c(1); GameButton button1 = L, button2 = R, button3 = R; - + bool haveCustomCombo = false; + std::ifstream in("/etc/options_menu/button.cfg"); if(in.is_open()) { - int temp; - in >> temp; - button1 = (GameButton)temp; - in >> temp; - button2 = (GameButton)temp; - in >> temp; - button3 = (GameButton)temp; + int b1, b2, b3; + // all 3 must parse - a truncated/empty file falls through to auto-detect below + if(in >> b1 >> b2 >> b3) + { + button1 = (GameButton)b1; + button2 = (GameButton)b2; + button3 = (GameButton)b3; + haveCustomCombo = true; + } in.close(); } - - system("sh /etc/options_menu/scripts/GetSpriteSheet.sh > /tmp/spritesheet"); - + + if(!haveCustomCombo) + { + // no valid user-set combo - NES/Famicom pads have no L/R, fall back to B+Down. + // matches softwareCheck()'s own dp-shvc check in /etc/preinit.d/b0010_functions + std::ifstream boardtypeFile("/var/squashfs/etc/clover/boardtype"); + std::string boardtype; + std::getline(boardtypeFile, boardtype); + if(boardtype != "dp-shvc") + { + button1 = B; + button2 = DOWN; + button3 = DOWN; + } + } + const auto waitTime = std::chrono::milliseconds(100); const short holdThreshold = 10; short holdCount = 0; diff --git a/src/framework/sdl_context.cpp b/src/framework/sdl_context.cpp index aae4f4e..a68d8b3 100644 --- a/src/framework/sdl_context.cpp +++ b/src/framework/sdl_context.cpp @@ -19,10 +19,6 @@ SDL_Context::SDL_Context(std::chrono::milliseconds fpsTime, bool powerButtonExit) : fpsTime(fpsTime), powerButtonExit(powerButtonExit) { powerwatch = new PowerWatch(); - // SDL defaults to nearest-neighbor texture scaling, which makes any - // upscaled art (e.g. a preview image scaled to fit its box) look - // blocky/jagged instead of the smooth PNG it actually is - SDL_SetHint(SDL_HINT_RENDER_SCALE_QUALITY, "1"); window = SDL_CreateWindow("", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, 1280, 720, SDL_WINDOW_FULLSCREEN); // vsync makes SDL_RenderPresent block for the next vertical blank instead // of returning immediately, so a post-pause frame burst (see EndFrame) diff --git a/src/main.cpp b/src/main.cpp index 7bd421a..8c22483 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -23,6 +23,7 @@ #include #include #include +#include #ifndef MOD_VERSION #define MOD_VERSION "dev" @@ -64,21 +65,24 @@ int Utf8Length(const std::string & text) return count; } -static struct ExitManager +// "%options_path%/xxx" command templates always produce a double slash (optionsLocation +// already ends in one) - collapsing here stops it from growing an extra slash with every +// relaunch, which otherwise breaks string-equality checks like the self-relaunch one below +std::string CollapseSlashes(const std::string & path) { - std::string exitCommand; - bool runExitCommand = true; - ~ExitManager() - { - if(runExitCommand) - system(exitCommand.c_str()); - } -} _exitManager; + std::string result; + for(char ch : path) + if(ch != '/' || result.empty() || result.back() != '/') + result += ch; + return result; +} + +ExitManager _exitManager; int main(int argc, char * argv[]) { std::string optionsLocation(argv[0]); - optionsLocation = optionsLocation.substr(0, optionsLocation.find_last_of('/')+1); + optionsLocation = CollapseSlashes(optionsLocation.substr(0, optionsLocation.find_last_of('/')+1)); _exitManager.exitCommand = "/bin/sh " + optionsLocation + "/scripts/ResumeUI.sh"; std::string commandLocation(optionsLocation + "commands/"); std::string scriptLocation(optionsLocation + "scripts/"); @@ -126,9 +130,19 @@ int main(int argc, char * argv[]) ++i; } } + commandLocation = CollapseSlashes(commandLocation); + scriptLocation = CollapseSlashes(scriptLocation); // what a child screen inherits as its own OM_BACK_STACK std::string myEntry = commandLocation + "," + scriptLocation + "," + titleKey; + // a self-relaunch (e.g. after changing language) re-enters the same screen - the + // stack we received already ends in an entry for "this screen" from the ORIGINAL + // navigation into it. Strip that stale self-reference so both the Back row below + // and what we export to our own children point at our real parent, not ourselves + size_t lastSep = backStack.find_last_of(';'); + std::string lastEntry = backStack.empty() ? "" : (lastSep == std::string::npos ? backStack : backStack.substr(lastSep + 1)); + if(lastEntry == myEntry) + backStack = (lastSep == std::string::npos) ? "" : backStack.substr(0, lastSep); setenv("OM_BACK_STACK", (backStack.empty() ? myEntry : backStack + ";" + myEntry).c_str(), 1); //Read commands from command folder @@ -305,7 +319,7 @@ int main(int argc, char * argv[]) size_t pos = c.command.find("--commandPath"); size_t pathStart = c.command.find_first_not_of(' ', pos + std::string("--commandPath").size()); size_t pathEnd = c.command.find(' ', pathStart); - std::string targetPath = c.command.substr(pathStart, pathEnd - pathStart); + std::string targetPath = CollapseSlashes(c.command.substr(pathStart, pathEnd - pathStart)); if(targetPath == commandLocation) c.hasSubmenu = false; } @@ -395,7 +409,10 @@ int main(int argc, char * argv[]) // scaled to fit the detail panel box, ignores the command's own previewImageX/Y/W/H if(currentCommand.previewImage.size()) { + // linear only for this one texture - baked in at creation, so toggle around it + SDL_SetHint(SDL_HINT_RENDER_SCALE_QUALITY, "1"); PreviewImage = std::make_shared(currentCommand.previewImage, renderer, 0, 0); + SDL_SetHint(SDL_HINT_RENDER_SCALE_QUALITY, "0"); if(PreviewImage->rect.w > 0 && PreviewImage->rect.h > 0) { const int maxW = UiTheme::DetailW - 2*UiTheme::ContentPadding; @@ -522,7 +539,10 @@ int main(int argc, char * argv[]) if(sdl_context.powerwatch->buttonPress()) { - break; + // skip SDL teardown - vsync can block on a vblank the display + // never delivers once the console starts powering off + system(_exitManager.exitCommand.c_str()); + _exit(0); } if(controller.GetButtonStatus(A) || controller.GetButtonStatus(START)) { From 07791dd864eb89e3fb86e75f90984fc455cde658 Mon Sep 17 00:00:00 2001 From: DefKorns Date: Fri, 11 Sep 2026 12:31:46 +0000 Subject: [PATCH 013/104] Reorder menu commands into logical groups, sync docs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Main menu: separator after Reboot, Advanced Options moved after Network Options (was between Retroarch and Network). - advanced_commands/: grouped into Preferences (Language, Button Combo, USB Write) → Maintenance (Clear Cache, Module Uninstaller) → Diagnostics (Display Temp, Run Top, Benchmark, RA Debugger, Dump File), replacing the old add-order numbering. - README.md: reordered sections/entries to match, and added the missing Network Options bullet under Default Options. --- README.md | 130 +++++++++--------- ..._LanguageOptions => c0010_LanguageOptions} | 0 .../{c0890_ChangeCombo => c0020_ChangeCombo} | 0 .../{c0500_USB_RW => c0030_USB_RW} | 0 .../{c0900_ClearCache => c0040_ClearCache} | 0 ...{c0020_ModUninstall => c0050_ModUninstall} | 0 .../{c0030_DisplayTemp => c0060_DisplayTemp} | 0 .../{c0040_DisplayTop => c0070_DisplayTop} | 0 .../{c0050_BenchMark => c0080_BenchMark} | 0 .../{c0700_RADebug => c0090_RADebug} | 0 .../{c0060_DumpFile => c0100_DumpFile} | 0 mod/etc/options_menu/commands/c0025_separator | 0 ...80_NetworkOptions => c0070_NetworkOptions} | 0 ..._AdvancedOptions => c0080_AdvancedOptions} | 0 14 files changed, 67 insertions(+), 63 deletions(-) rename mod/etc/options_menu/advanced_commands/{c0910_LanguageOptions => c0010_LanguageOptions} (100%) rename mod/etc/options_menu/advanced_commands/{c0890_ChangeCombo => c0020_ChangeCombo} (100%) rename mod/etc/options_menu/advanced_commands/{c0500_USB_RW => c0030_USB_RW} (100%) rename mod/etc/options_menu/advanced_commands/{c0900_ClearCache => c0040_ClearCache} (100%) rename mod/etc/options_menu/advanced_commands/{c0020_ModUninstall => c0050_ModUninstall} (100%) rename mod/etc/options_menu/advanced_commands/{c0030_DisplayTemp => c0060_DisplayTemp} (100%) rename mod/etc/options_menu/advanced_commands/{c0040_DisplayTop => c0070_DisplayTop} (100%) rename mod/etc/options_menu/advanced_commands/{c0050_BenchMark => c0080_BenchMark} (100%) rename mod/etc/options_menu/advanced_commands/{c0700_RADebug => c0090_RADebug} (100%) rename mod/etc/options_menu/advanced_commands/{c0060_DumpFile => c0100_DumpFile} (100%) create mode 100644 mod/etc/options_menu/commands/c0025_separator rename mod/etc/options_menu/commands/{c0080_NetworkOptions => c0070_NetworkOptions} (100%) rename mod/etc/options_menu/commands/{c0070_AdvancedOptions => c0080_AdvancedOptions} (100%) diff --git a/README.md b/README.md index a61f9e3..332dc86 100644 --- a/README.md +++ b/README.md @@ -43,6 +43,10 @@ This option will safely restart the console.\* This option will open a menu for managing retroarch configurations (see below). +#### Network Options + +This option will open a menu for managing your network and wifi configuration (see below). + #### Advanced Options This option will open a menu containing more advanced features (see below). @@ -99,63 +103,61 @@ Removes all bios files from NAND only. Toggle the RetroArch and Canoe loading screens on or off. -## Advanced Options - -#### Module Uninstaller +## Network Options -This option launches the module uninstaller program. This program allows you to remove modules from your device. Press the up and down buttons to select a module. Press A to add a module to the uninstall list. Press B to remove a module from the uninstall list. Pressing start will exit if no modules are added to the uninstall list. If there are modules on the uninstall this you will be prompted to ensure you want to remove them press START+SELECT to remove selected modules or B to cancel. +#### Display IP Address ->**Caution:** The uninstaller will list all modules on the device including the default hakchi modules. Only delete modules you have added yourself. Deleting the default hakchi modules can have unintended effects on your console. +This option will display both your public and internal ip addresses if you have a network adapter connected to your device. -#### Display Temp +#### Reconnect -This option will display the current temperature of the CPU. +This option will restart the network service on your device if you are not connected to the internet already. -#### Run Top +#### Search for SSIDs -This option runs and displays the output of the top binary. This shows the current CPU, memory usage and list of running processes. +This option will scan and display a list of available SSIDs. -#### Benchmark Tool +#### Backup Wifi Config (NAND) -This option will go through and echo out useful information regarding your console. -It will also include information on your: +Copies your current wifi config (wpa_supplicant.conf) to a backup folder on your NAND, named after your SSID: -- Current disk mounting information -- CPU analysis information -- RAM analysis information + /etc/wifi_backup/your_ssid_name -> **Note:** The information is displayed in 4 parts and has a 2 second wait between each part +#### Backup Wifi Config (USB) -#### Dump File Structure (to USB) +Copies your current wifi config (wpa_supplicant.conf) to a backup folder on your mounted USB/SD card, named after your SSID: -This option will map and echo out your complete file and directory structure for your console and any mounted disks. The log file will be saved to your mounted USB/SD card at: + /media/data/wifi_backup/your_ssid_name - /media/data/log/Hakchi_file_structure.log +#### Restore Wifi Config (NAND) - > **Note:** If these folders do not exist, they will be created when the option is ran. +Overwrites your wifi config with a backup located at: -The log file will also include information on: + /etc/wifi_backup/ -- Symbolic Links (Overmounts) -- File and directory permissions +and reconnects to the network. -#### Toggle Write Access on USB +While browsing the list of saved backups, hold **B** (~1 second) to delete +the selected backup (you will be asked to confirm with **A**, or cancel by +pressing **B** again). The list refreshes automatically after deleting. -This option will toggle read write access on your USB device. By default, if you don't have a saves folder located on your USB/SD, write access is disabled. If you wish to write logs or use your external mounted storage and **not** use external saves... This is the toggle for you. +#### Restore Wifi Config (USB) - > **Note:** If you already use external saves. You don't need to use this toggle. +Overwrites your wifi config with a backup located at: -#### RetroArch Debugger (USB logs) + /media/data/wifi_backup/ -This option will run your locally installed RetroArch in full verbose mode. It will also copy the config files for your RetroArch and save them to: +and reconnects to the network. - /media/data/log/RetroArchConfig +While browsing the list of saved backups, hold **B** (~1 second) to delete +the selected backup (you will be asked to confirm with **A**, or cancel by +pressing **B** again). The list refreshes automatically after deleting. -The full verbose log will be located here: +## Advanced Options - /media/data/log/Hakchi_retroarch_debug.log +#### Language Options -> **Note:** Depending on what you are trying to do, you might have limited functionality within RetroArch when running it via the debugger. This app is primarily designed to debug cores, custom configs and themes. +This option opens a submenu where you can choose the display language for the Options Menu. Currently available: English, Français, Deutsch, Español, Italiano, Nederlands, Português (Europeu), Русский and 日本語. The Options Menu automatically restarts in the selected language. #### Change Options Button Combo @@ -163,63 +165,65 @@ This option will allow you to change the button combo used to launch the options > **Note:** The default combo is **L + R**, or **B + Down** on consoles with no shoulder buttons (NES Classic, Famicom Classic and its variants, e.g. the Famicom Shonen Jump 50th Anniversary edition), detected automatically. Setting a custom combo here overrides that detection. -#### Clear Cache +#### Toggle Write Access on USB -This option will force the kernel to clear the page cache and free up unused memory. You should not need to call this as the system should manage memory fine on it's own. +This option will toggle read write access on your USB device. By default, if you don't have a saves folder located on your USB/SD, write access is disabled. If you wish to write logs or use your external mounted storage and **not** use external saves... This is the toggle for you. -#### Language Options + > **Note:** If you already use external saves. You don't need to use this toggle. -This option opens a submenu where you can choose the display language for the Options Menu. Currently available: English, Français, Deutsch, Español, Italiano, Nederlands, Português (Europeu), Русский and 日本語. The Options Menu automatically restarts in the selected language. +#### Clear Cache -## Network Options +This option will force the kernel to clear the page cache and free up unused memory. You should not need to call this as the system should manage memory fine on it's own. -#### Display IP Address +#### Module Uninstaller -This option will display both your public and internal ip addresses if you have a network adapter connected to your device. +This option launches the module uninstaller program. This program allows you to remove modules from your device. Press the up and down buttons to select a module. Press A to add a module to the uninstall list. Press B to remove a module from the uninstall list. Pressing start will exit if no modules are added to the uninstall list. If there are modules on the uninstall this you will be prompted to ensure you want to remove them press START+SELECT to remove selected modules or B to cancel. -#### Reconnect +>**Caution:** The uninstaller will list all modules on the device including the default hakchi modules. Only delete modules you have added yourself. Deleting the default hakchi modules can have unintended effects on your console. -This option will restart the network service on your device if you are not connected to the internet already. +#### Display Temp -#### Search for SSIDs +This option will display the current temperature of the CPU. -This option will scan and display a list of available SSIDs. +#### Run Top -#### Backup Wifi Config (NAND) +This option runs and displays the output of the top binary. This shows the current CPU, memory usage and list of running processes. -Copies your current wifi config (wpa_supplicant.conf) to a backup folder on your NAND, named after your SSID: +#### Benchmark Tool - /etc/wifi_backup/your_ssid_name +This option will go through and echo out useful information regarding your console. +It will also include information on your: -#### Backup Wifi Config (USB) +- Current disk mounting information +- CPU analysis information +- RAM analysis information -Copies your current wifi config (wpa_supplicant.conf) to a backup folder on your mounted USB/SD card, named after your SSID: +> **Note:** The information is displayed in 4 parts and has a 2 second wait between each part - /media/data/wifi_backup/your_ssid_name +#### RetroArch Debugger (USB logs) -#### Restore Wifi Config (NAND) +This option will run your locally installed RetroArch in full verbose mode. It will also copy the config files for your RetroArch and save them to: -Overwrites your wifi config with a backup located at: + /media/data/log/RetroArchConfig - /etc/wifi_backup/ +The full verbose log will be located here: -and reconnects to the network. + /media/data/log/Hakchi_retroarch_debug.log -While browsing the list of saved backups, hold **B** (~1 second) to delete -the selected backup (you will be asked to confirm with **A**, or cancel by -pressing **B** again). The list refreshes automatically after deleting. +> **Note:** Depending on what you are trying to do, you might have limited functionality within RetroArch when running it via the debugger. This app is primarily designed to debug cores, custom configs and themes. -#### Restore Wifi Config (USB) +#### Dump File Structure (to USB) -Overwrites your wifi config with a backup located at: +This option will map and echo out your complete file and directory structure for your console and any mounted disks. The log file will be saved to your mounted USB/SD card at: - /media/data/wifi_backup/ + /media/data/log/Hakchi_file_structure.log -and reconnects to the network. + > **Note:** If these folders do not exist, they will be created when the option is ran. -While browsing the list of saved backups, hold **B** (~1 second) to delete -the selected backup (you will be asked to confirm with **A**, or cancel by -pressing **B** again). The list refreshes automatically after deleting. +The log file will also include information on: + +- Symbolic Links (Overmounts) +- File and directory permissions ## Developer Information diff --git a/mod/etc/options_menu/advanced_commands/c0910_LanguageOptions b/mod/etc/options_menu/advanced_commands/c0010_LanguageOptions similarity index 100% rename from mod/etc/options_menu/advanced_commands/c0910_LanguageOptions rename to mod/etc/options_menu/advanced_commands/c0010_LanguageOptions diff --git a/mod/etc/options_menu/advanced_commands/c0890_ChangeCombo b/mod/etc/options_menu/advanced_commands/c0020_ChangeCombo similarity index 100% rename from mod/etc/options_menu/advanced_commands/c0890_ChangeCombo rename to mod/etc/options_menu/advanced_commands/c0020_ChangeCombo diff --git a/mod/etc/options_menu/advanced_commands/c0500_USB_RW b/mod/etc/options_menu/advanced_commands/c0030_USB_RW similarity index 100% rename from mod/etc/options_menu/advanced_commands/c0500_USB_RW rename to mod/etc/options_menu/advanced_commands/c0030_USB_RW diff --git a/mod/etc/options_menu/advanced_commands/c0900_ClearCache b/mod/etc/options_menu/advanced_commands/c0040_ClearCache similarity index 100% rename from mod/etc/options_menu/advanced_commands/c0900_ClearCache rename to mod/etc/options_menu/advanced_commands/c0040_ClearCache diff --git a/mod/etc/options_menu/advanced_commands/c0020_ModUninstall b/mod/etc/options_menu/advanced_commands/c0050_ModUninstall similarity index 100% rename from mod/etc/options_menu/advanced_commands/c0020_ModUninstall rename to mod/etc/options_menu/advanced_commands/c0050_ModUninstall diff --git a/mod/etc/options_menu/advanced_commands/c0030_DisplayTemp b/mod/etc/options_menu/advanced_commands/c0060_DisplayTemp similarity index 100% rename from mod/etc/options_menu/advanced_commands/c0030_DisplayTemp rename to mod/etc/options_menu/advanced_commands/c0060_DisplayTemp diff --git a/mod/etc/options_menu/advanced_commands/c0040_DisplayTop b/mod/etc/options_menu/advanced_commands/c0070_DisplayTop similarity index 100% rename from mod/etc/options_menu/advanced_commands/c0040_DisplayTop rename to mod/etc/options_menu/advanced_commands/c0070_DisplayTop diff --git a/mod/etc/options_menu/advanced_commands/c0050_BenchMark b/mod/etc/options_menu/advanced_commands/c0080_BenchMark similarity index 100% rename from mod/etc/options_menu/advanced_commands/c0050_BenchMark rename to mod/etc/options_menu/advanced_commands/c0080_BenchMark diff --git a/mod/etc/options_menu/advanced_commands/c0700_RADebug b/mod/etc/options_menu/advanced_commands/c0090_RADebug similarity index 100% rename from mod/etc/options_menu/advanced_commands/c0700_RADebug rename to mod/etc/options_menu/advanced_commands/c0090_RADebug diff --git a/mod/etc/options_menu/advanced_commands/c0060_DumpFile b/mod/etc/options_menu/advanced_commands/c0100_DumpFile similarity index 100% rename from mod/etc/options_menu/advanced_commands/c0060_DumpFile rename to mod/etc/options_menu/advanced_commands/c0100_DumpFile diff --git a/mod/etc/options_menu/commands/c0025_separator b/mod/etc/options_menu/commands/c0025_separator new file mode 100644 index 0000000..e69de29 diff --git a/mod/etc/options_menu/commands/c0080_NetworkOptions b/mod/etc/options_menu/commands/c0070_NetworkOptions similarity index 100% rename from mod/etc/options_menu/commands/c0080_NetworkOptions rename to mod/etc/options_menu/commands/c0070_NetworkOptions diff --git a/mod/etc/options_menu/commands/c0070_AdvancedOptions b/mod/etc/options_menu/commands/c0080_AdvancedOptions similarity index 100% rename from mod/etc/options_menu/commands/c0070_AdvancedOptions rename to mod/etc/options_menu/commands/c0080_AdvancedOptions From c62bdecec3e72ee845c9e0e1b1b5da71277716bc Mon Sep 17 00:00:00 2001 From: DefKorns Date: Fri, 11 Sep 2026 14:21:08 +0000 Subject: [PATCH 014/104] Support both RetroArch builds (Neo, discontinued, and _km Ozone) RetroArch's per-core config/overrides moved from .config/retroarch/config to a plain config/ folder in the newer _km Ozone builds, and the old build's loading-screen swap assets are gone entirely. - New ra_vars: detects which layout is installed, exposes $raConfig. - 7 scripts (backup/restore/delete-overrides/delete-remaps/ default-settings-all) updated to use $raConfig instead of the old hardcoded path. - New om_retroarch wrapper: hides "Toggle RA and Canoe load screens" entirely when the installed build has nothing to toggle, instead of showing an option that always fails. - Replaced stale "install the latest RetroArch 'Neo'" messages, which now point at a discontinued build. --- .../commands/c0060_RetroarchOptions | 2 +- .../retroarch/scripts/backup-nand.sh | 3 +- .../retroarch/scripts/backup-usb.sh | 4 ++- .../retroarch/scripts/default-settings-all.sh | 8 +++-- .../retroarch/scripts/default-settings.sh | 2 +- .../retroarch/scripts/delete-overrides.sh | 8 +++-- .../retroarch/scripts/delete-remaps.sh | 4 ++- .../options_menu/retroarch/scripts/ra_vars | 28 ++++++++++++++++ .../retroarch/scripts/restore-nand.sh | 6 ++-- .../retroarch/scripts/restore-usb.sh | 6 ++-- .../retroarch/scripts/toggle-loadscreens.sh | 2 +- mod/etc/options_menu/scripts/om_retroarch | 32 +++++++++++++++++++ 12 files changed, 89 insertions(+), 16 deletions(-) create mode 100644 mod/etc/options_menu/retroarch/scripts/ra_vars create mode 100644 mod/etc/options_menu/scripts/om_retroarch diff --git a/mod/etc/options_menu/commands/c0060_RetroarchOptions b/mod/etc/options_menu/commands/c0060_RetroarchOptions index 0fdbff3..1627765 100644 --- a/mod/etc/options_menu/commands/c0060_RetroarchOptions +++ b/mod/etc/options_menu/commands/c0060_RetroarchOptions @@ -1,5 +1,5 @@ COMMAND_NAME=RETROARCH_OPTIONS COMMAND_TYPE=EXTERNAL RESTART_UI=FALSE -COMMAND_STR=usleep 50000 && %options_path%/options --commandPath %options_path%/retroarch/commands/ --scriptPath %options_path%/retroarch/scripts --title "RETROARCH_OPTIONS" & +COMMAND_STR=sh %script_dir%/om_retroarch PREVIEW_IMAGE=/etc/options_menu/images/preview_retroarch.png diff --git a/mod/etc/options_menu/retroarch/scripts/backup-nand.sh b/mod/etc/options_menu/retroarch/scripts/backup-nand.sh index 7a1d97d..6198fda 100644 --- a/mod/etc/options_menu/retroarch/scripts/backup-nand.sh +++ b/mod/etc/options_menu/retroarch/scripts/backup-nand.sh @@ -2,6 +2,7 @@ source /etc/preinit script_init +source $mountpoint/etc/options_menu/retroarch/scripts/ra_vars Backup_To_NAND() { @@ -12,7 +13,7 @@ Backup_To_NAND() mkdir -pm 777 "$rootfs/etc/ra_backup" /etc/options_menu/retroarch/scripts/RASettingsBackup "Backup" "/etc/options_menu/retroarch/default_files/retroarch.cfg" "/etc/libretro/retroarch.cfg" "$rootfs/etc/ra_backup/retroarch.cfg" /etc/options_menu/retroarch/scripts/RASettingsBackup "Backup" "/etc/options_menu/retroarch/default_files/retroarch-core-options.cfg" "/etc/libretro/retroarch-core-options.cfg" "$rootfs/etc/ra_backup/retroarch-core-options.cfg" - cp -r /etc/libretro/.config/retroarch/config "$rootfs/etc/ra_backup" + cp -r "$raConfig" "$rootfs/etc/ra_backup/config" } Backup_To_NAND && echo "Retroarch config backed up to /etc/ra_backup" diff --git a/mod/etc/options_menu/retroarch/scripts/backup-usb.sh b/mod/etc/options_menu/retroarch/scripts/backup-usb.sh index 022009b..af10482 100644 --- a/mod/etc/options_menu/retroarch/scripts/backup-usb.sh +++ b/mod/etc/options_menu/retroarch/scripts/backup-usb.sh @@ -1,5 +1,7 @@ #!/bin/sh +source $mountpoint/etc/options_menu/retroarch/scripts/ra_vars + Backup_To_USB() { if [ -d /media/data/ra_backup ]; then @@ -9,7 +11,7 @@ Backup_To_USB() mkdir -pm 777 /media/data/ra_backup /etc/options_menu/retroarch/scripts/RASettingsBackup "Backup" "/etc/options_menu/retroarch/default_files/retroarch.cfg" "/etc/libretro/retroarch.cfg" "/media/data/ra_backup/retroarch.cfg" /etc/options_menu/retroarch/scripts/RASettingsBackup "Backup" "/etc/options_menu/retroarch/default_files/retroarch-core-options.cfg" "/etc/libretro/retroarch-core-options.cfg" "/media/data/ra_backup/retroarch-core-options.cfg" - cp -r /etc/libretro/.config/retroarch/config /media/data/ra_backup + cp -r "$raConfig" /media/data/ra_backup/config } Backup_To_USB && echo "Retroarch config backed up to USB:/data/ra_backup" diff --git a/mod/etc/options_menu/retroarch/scripts/default-settings-all.sh b/mod/etc/options_menu/retroarch/scripts/default-settings-all.sh index 4fd4813..bb4dd9a 100644 --- a/mod/etc/options_menu/retroarch/scripts/default-settings-all.sh +++ b/mod/etc/options_menu/retroarch/scripts/default-settings-all.sh @@ -1,12 +1,14 @@ #!/bin/sh +source $mountpoint/etc/options_menu/retroarch/scripts/ra_vars + if [ -f "/etc/options_menu/retroarch/default_files/retroarch.cfg" ]; then cp /etc/options_menu/retroarch/default_files/retroarch.cfg /etc/libretro/ cp /etc/options_menu/retroarch/default_files/retroarch-core-options.cfg /etc/libretro/ - rm -r /etc/libretro/.config/retroarch/config/* - mkdir -m 777 /etc/libretro/.config/retroarch/config/remaps + rm -r "${raConfig:?}"/* + mkdir -m 777 "$raConfig/remaps" echo "Restored Default Settings." else echo "Cannot locate default configuration files." - echo "Please ensure you have the latest RetroArch 'Neo' installed" + echo "Please ensure RetroArch is installed." fi diff --git a/mod/etc/options_menu/retroarch/scripts/default-settings.sh b/mod/etc/options_menu/retroarch/scripts/default-settings.sh index cefda0c..e13a645 100644 --- a/mod/etc/options_menu/retroarch/scripts/default-settings.sh +++ b/mod/etc/options_menu/retroarch/scripts/default-settings.sh @@ -6,5 +6,5 @@ if [ -f "/etc/options_menu/retroarch/default_files/retroarch.cfg" ]; then echo "Restored Default Settings." else echo "Cannot locate default configuration files." - echo "Please ensure you have the latest RetroArch 'Neo' installed" + echo "Please ensure RetroArch is installed." fi diff --git a/mod/etc/options_menu/retroarch/scripts/delete-overrides.sh b/mod/etc/options_menu/retroarch/scripts/delete-overrides.sh index f2b84dc..08b6614 100644 --- a/mod/etc/options_menu/retroarch/scripts/delete-overrides.sh +++ b/mod/etc/options_menu/retroarch/scripts/delete-overrides.sh @@ -1,6 +1,8 @@ #!/bin/sh -mv /etc/libretro/.config/retroarch/config/remaps /etc/libretro/.config/retroarch/ -rm -r /etc/libretro/.config/retroarch/config/* -mv /etc/libretro/.config/retroarch/remaps /etc/libretro/.config/retroarch/config/ +source $mountpoint/etc/options_menu/retroarch/scripts/ra_vars + +mv "$raConfig/remaps" /tmp/ra_remaps +rm -r "${raConfig:?}"/* +mv /tmp/ra_remaps "$raConfig/remaps" echo "Overrides deleted." diff --git a/mod/etc/options_menu/retroarch/scripts/delete-remaps.sh b/mod/etc/options_menu/retroarch/scripts/delete-remaps.sh index ba44872..dd7a496 100644 --- a/mod/etc/options_menu/retroarch/scripts/delete-remaps.sh +++ b/mod/etc/options_menu/retroarch/scripts/delete-remaps.sh @@ -1,4 +1,6 @@ #!/bin/sh -rm -r /etc/libretro/.config/retroarch/config/remaps/* +source $mountpoint/etc/options_menu/retroarch/scripts/ra_vars + +rm -r "${raConfig:?}/remaps"/* echo "Remaps deleted." diff --git a/mod/etc/options_menu/retroarch/scripts/ra_vars b/mod/etc/options_menu/retroarch/scripts/ra_vars new file mode 100644 index 0000000..c073b8f --- /dev/null +++ b/mod/etc/options_menu/retroarch/scripts/ra_vars @@ -0,0 +1,28 @@ +#!/bin/sh +# shellcheck disable=SC2154,SC1091,SC1090,SC2039 +# Copyright (c) 2026 DefKorns (https://defkorns.github.io/LICENSE) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +# shellcheck disable=SC2086 +source $mountpoint/etc/options_menu/scripts/om_functions +script_init + +# per-core config/overrides moved from .config/retroarch/config (Neo, discontinued) +# to a plain config/ folder (_km Ozone builds) - detect which is actually installed +if [ -d "$rootfs/etc/libretro/.config/retroarch/config" ]; then + raConfig="$rootfs/etc/libretro/.config/retroarch/config" +else + raConfig="$rootfs/etc/libretro/config" +fi diff --git a/mod/etc/options_menu/retroarch/scripts/restore-nand.sh b/mod/etc/options_menu/retroarch/scripts/restore-nand.sh index 06dc219..fd7e630 100644 --- a/mod/etc/options_menu/retroarch/scripts/restore-nand.sh +++ b/mod/etc/options_menu/retroarch/scripts/restore-nand.sh @@ -1,11 +1,13 @@ #!/bin/sh +source $mountpoint/etc/options_menu/retroarch/scripts/ra_vars + if [ ! -f "/etc/ra_backup/retroarch.cfg" ]; then echo "Backup file does not exist." else /etc/options_menu/retroarch/scripts/RASettingsBackup "Restore" "/etc/options_menu/retroarch/default_files/retroarch.cfg" "/etc/libretro/retroarch.cfg" "$rootfs/etc/ra_backup/retroarch.cfg" /etc/options_menu/retroarch/scripts/RASettingsBackup "Restore" "/etc/options_menu/retroarch/default_files/retroarch-core-options.cfg" "/etc/libretro/retroarch-core-options.cfg" "$rootfs/etc/ra_backup/retroarch-core-options.cfg" - rm -rf /etc/libretro/.config/retroarch/config/ - cp -r /etc/ra_backup/config /etc/libretro/.config/retroarch + rm -rf "${raConfig:?}" + cp -r /etc/ra_backup/config "$raConfig" echo "Retroarch configs restored." fi diff --git a/mod/etc/options_menu/retroarch/scripts/restore-usb.sh b/mod/etc/options_menu/retroarch/scripts/restore-usb.sh index 70115d3..ba4dc5a 100644 --- a/mod/etc/options_menu/retroarch/scripts/restore-usb.sh +++ b/mod/etc/options_menu/retroarch/scripts/restore-usb.sh @@ -1,11 +1,13 @@ #!/bin/sh +source $mountpoint/etc/options_menu/retroarch/scripts/ra_vars + if [ ! -f "/media/data/ra_backup/retroarch.cfg" ]; then echo "Backup file does not exist." else /etc/options_menu/retroarch/scripts/RASettingsBackup "Restore" "/etc/options_menu/retroarch/default_files/retroarch.cfg" "/etc/libretro/retroarch.cfg" "/media/data/ra_backup/retroarch.cfg" /etc/options_menu/retroarch/scripts/RASettingsBackup "Restore" "/etc/options_menu/retroarch/default_files/retroarch-core-options.cfg" "/etc/libretro/retroarch-core-options.cfg" "/media/data/ra_backup/retroarch-core-options.cfg" - rm -rf /etc/libretro/.config/retroarch/config/ - cp -r /media/data/ra_backup/config /etc/libretro/.config/retroarch + rm -rf "${raConfig:?}" + cp -r /media/data/ra_backup/config "$raConfig" echo "Retroarch configs restored." fi diff --git a/mod/etc/options_menu/retroarch/scripts/toggle-loadscreens.sh b/mod/etc/options_menu/retroarch/scripts/toggle-loadscreens.sh index 738f171..321bd4e 100644 --- a/mod/etc/options_menu/retroarch/scripts/toggle-loadscreens.sh +++ b/mod/etc/options_menu/retroarch/scripts/toggle-loadscreens.sh @@ -26,5 +26,5 @@ fi if [ "$ok" = 1 ]; then echo "RetroArch load screen toggle complete!" else - echo "Failed to locate load screens... Please ensure you are using the latest RetroArch 'Neo' Build!" + echo "Failed to locate load screens - this RetroArch build doesn't support this option." fi diff --git a/mod/etc/options_menu/scripts/om_retroarch b/mod/etc/options_menu/scripts/om_retroarch new file mode 100644 index 0000000..3f07c7a --- /dev/null +++ b/mod/etc/options_menu/scripts/om_retroarch @@ -0,0 +1,32 @@ +#!/bin/sh +# shellcheck disable=SC2154,SC1091,SC1090,SC2039 +# Copyright (c) 2026 DefKorns (https://defkorns.github.io/LICENSE) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +# shellcheck disable=SC2086 +source $mountpoint/etc/options_menu/retroarch/scripts/ra_vars +script_init + +omRetroarchCmds="$optionsMenu/retroarch/commands" +omRetroarchScripts="$optionsMenu/retroarch/scripts" +disableToggleLoadScreens="$omRetroarchCmds/_ToggleLoadScreens" +enableToggleLoadScreens="$omRetroarchCmds/c0090_ToggleLoadScreens" + +# retroarch-clover-child (Ozone builds) dropped the loading-screen swap entirely - +# nothing left for this option to toggle, so hide it instead of failing when picked +enable_cmds "ToggleLoadScreens" +[ ! -f "$rootfs/share/retroarch/assets/RAloading-min.png" ] && disable_cmds "ToggleLoadScreens" + +usleep 50000 && $optionsMenu/options --commandPath $omRetroarchCmds/ --scriptPath $omRetroarchScripts --title "RETROARCH_OPTIONS" & From 162cb20eeadb27cbda81bcda4496ea00e97b685b Mon Sep 17 00:00:00 2001 From: DefKorns Date: Fri, 11 Sep 2026 15:14:00 +0000 Subject: [PATCH 015/104] Revert vsync/Shutdown/power-button _exit bypass Was causing the console to become permanently unresponsive to the combo after a screenshot (options.flag never cleared, needs a reboot to recover) - worse than the flicker it was meant to fix. Back to the original teardown-via-destructor behavior. --- src/command.cpp | 14 -------------- src/command.h | 15 --------------- src/framework/sdl_context.cpp | 23 +++-------------------- src/framework/sdl_context.h | 5 ----- src/main.cpp | 19 +++++++++++-------- 5 files changed, 14 insertions(+), 62 deletions(-) diff --git a/src/command.cpp b/src/command.cpp index 7581c14..7f6f9ef 100644 --- a/src/command.cpp +++ b/src/command.cpp @@ -10,7 +10,6 @@ #include "command.h" #include "framework/controller.h" #include "framework/draw_helpers.h" -#include "framework/powerwatch.h" #include "localization.h" #include @@ -19,7 +18,6 @@ #include #include #include -#include // std::stoi throws on non-numeric input; avoid crashing on a bad command file static int SafeStoi(const std::string & value, int fallback = 0) @@ -158,13 +156,6 @@ void Command::RunCommand(SDL_Context & sdl_context, Controller * controller, con if(!feof(pipe)) clearerr(pipe); // last fgets() failed because nothing's ready yet, not EOF - stay readable - // power press must exit even mid-command - skip SDL teardown, same as main.cpp's loop - if(sdl_context.powerwatch->buttonPress()) - { - system(_exitManager.exitCommand.c_str()); - _exit(0); - } - // ignoreInterrupt commands (e.g. ChangeCombo) read the controller themselves - // stop polling it here too, or we race them for the same button events if(!ignoreInterrupt) @@ -180,11 +171,6 @@ void Command::RunCommand(SDL_Context & sdl_context, Controller * controller, con while (!controller->GetButtonStatus(B)) { - if(sdl_context.powerwatch->buttonPress()) - { - system(_exitManager.exitCommand.c_str()); - _exit(0); - } controller->Update(); render(); } diff --git a/src/command.h b/src/command.h index 0e88683..a92eb49 100644 --- a/src/command.h +++ b/src/command.h @@ -12,8 +12,6 @@ #include "framework/sdl_helper.h" -#include - class Controller; // header/footer textures, shared by reference with the main screen @@ -25,19 +23,6 @@ struct ModernChrome Texture & creditText; }; -// runs the paused-UI resume script on exit, unless something (delete, relaunch) disables it -struct ExitManager -{ - std::string exitCommand; - bool runExitCommand = true; - ~ExitManager() - { - if(runExitCommand) - system(exitCommand.c_str()); - } -}; -extern ExitManager _exitManager; - struct Command { Command(); diff --git a/src/framework/sdl_context.cpp b/src/framework/sdl_context.cpp index a68d8b3..cc34260 100644 --- a/src/framework/sdl_context.cpp +++ b/src/framework/sdl_context.cpp @@ -10,7 +10,6 @@ #include "sdl_context.h" #include "powerwatch.h" -#include #include #include #include @@ -20,10 +19,7 @@ SDL_Context::SDL_Context(std::chrono::milliseconds fpsTime, bool powerButtonExit { powerwatch = new PowerWatch(); window = SDL_CreateWindow("", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, 1280, 720, SDL_WINDOW_FULLSCREEN); - // vsync makes SDL_RenderPresent block for the next vertical blank instead - // of returning immediately, so a post-pause frame burst (see EndFrame) - // can't outrun the display and tear/flicker the way it could unthrottled - renderer = SDL_CreateRenderer(window, -1, SDL_RENDERER_PRESENTVSYNC); + renderer = SDL_CreateRenderer(window, -1, 0); if(renderer == NULL) { std::cerr << "Cannot create renderer.\n"; SDL_DestroyWindow(window); @@ -35,19 +31,11 @@ SDL_Context::SDL_Context(std::chrono::milliseconds fpsTime, bool powerButtonExit SDL_Context::~SDL_Context() { - Shutdown(); - delete powerwatch; -} - -void SDL_Context::Shutdown() -{ - if(shutDown) - return; - shutDown = true; TTF_Quit(); SDL_DestroyRenderer(renderer); SDL_DestroyWindow(window); SDL_Quit(); + delete powerwatch; } void SDL_Context::StartFrame() @@ -61,10 +49,5 @@ void SDL_Context::EndFrame() { SDL_RenderPresent(renderer); std::this_thread::sleep_until(nextFrameTime); - // after a long stall (e.g. hakchi's "uipause" SIGSTOPs this process - // while it reads /dev/fb0 for a screenshot), nextFrameTime is left far - // in the past - clamp to now so resuming doesn't burn through a burst - // of back-to-back unthrottled frames trying to catch up, which is what - // caused the flicker after taking a screenshot - nextFrameTime = std::max(nextFrameTime, std::chrono::system_clock::now()) + fpsTime; + nextFrameTime += fpsTime; } diff --git a/src/framework/sdl_context.h b/src/framework/sdl_context.h index 0f7e6a5..ae87d39 100644 --- a/src/framework/sdl_context.h +++ b/src/framework/sdl_context.h @@ -25,10 +25,6 @@ struct SDL_Context ~SDL_Context(); void StartFrame(); void EndFrame(); - // releases the renderer/window now instead of waiting for the destructor - - // call before system()-launching a replacement options process, so that - // process isn't racing this one for the display (see main.cpp) - void Shutdown(); SDL_Window* window; SDL_Renderer* renderer; @@ -38,7 +34,6 @@ struct SDL_Context private: std::chrono::time_point nextFrameTime; bool powerButtonExit; - bool shutDown = false; }; #endif diff --git a/src/main.cpp b/src/main.cpp index 8c22483..d7929e0 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -23,7 +23,6 @@ #include #include #include -#include #ifndef MOD_VERSION #define MOD_VERSION "dev" @@ -77,7 +76,16 @@ std::string CollapseSlashes(const std::string & path) return result; } -ExitManager _exitManager; +static struct ExitManager +{ + std::string exitCommand; + bool runExitCommand = true; + ~ExitManager() + { + if(runExitCommand) + system(exitCommand.c_str()); + } +} _exitManager; int main(int argc, char * argv[]) { @@ -539,10 +547,7 @@ int main(int argc, char * argv[]) if(sdl_context.powerwatch->buttonPress()) { - // skip SDL teardown - vsync can block on a vblank the display - // never delivers once the console starts powering off - system(_exitManager.exitCommand.c_str()); - _exit(0); + break; } if(controller.GetButtonStatus(A) || controller.GetButtonStatus(START)) { @@ -554,8 +559,6 @@ int main(int argc, char * argv[]) } else { - // release the display first - the child's startup delay assumed our teardown was instant - sdl_context.Shutdown(); system(commands[currentCommandId].command.c_str()); _exitManager.runExitCommand = commands[currentCommandId].restartUI; break; From 026d1b3446887f6ff64dbf160f39edb16f76f1c8 Mon Sep 17 00:00:00 2001 From: DefKorns Date: Fri, 11 Sep 2026 15:16:44 +0000 Subject: [PATCH 016/104] Extract LoadLanguageFromConfig, dedupe ChangeCombo intro lines, upx it - LoadLanguageFromConfig() in localization.cpp replaces the identical language.cfg-reading boilerplate duplicated in main.cpp and change_combo.cpp. - ChangeCombo's 4 intro lines now loop over their translation keys, matching the existing prompts[] pattern below them. - ChangeCombo is upx-compressed like mod_uninstall/standby_watchdog - same occasional-use profile as options/optiond, which stay uncompressed. - Untrack the compiled ChangeCombo binary now that its source exists - it was only vendored before because there was nothing to build it from. --- Makefile.docker | 7 +- mod/etc/options_menu/scripts/ChangeCombo | Bin 9900 -> 0 bytes src/change_combo.cpp | 130 +++++++++++++++++++++++ src/localization.cpp | 11 ++ src/localization.h | 3 + src/main.cpp | 9 +- 6 files changed, 152 insertions(+), 8 deletions(-) delete mode 100755 mod/etc/options_menu/scripts/ChangeCombo create mode 100644 src/change_combo.cpp diff --git a/Makefile.docker b/Makefile.docker index 1669de4..371ce1d 100644 --- a/Makefile.docker +++ b/Makefile.docker @@ -53,7 +53,7 @@ all: $(OUT)/$(TARGET_NAME).hmod bump-build-number: echo $$(( $$(cat $(BUILD_NUM_FILE) 2>/dev/null || echo 0) + 1 )) > $(BUILD_NUM_FILE) -$(OUT)/$(TARGET_NAME).hmod: mod/etc/options_menu/options mod/etc/options_menu/optiond mod/etc/options_menu/mod_uninstall/mod_uninstall mod/bin/standby_watchdog mod/etc/options_menu/scripts/gen_splash $(VENDORED_TTF_LIBS) bump-build-number +$(OUT)/$(TARGET_NAME).hmod: mod/etc/options_menu/options mod/etc/options_menu/optiond mod/etc/options_menu/mod_uninstall/mod_uninstall mod/bin/standby_watchdog mod/etc/options_menu/scripts/gen_splash mod/etc/options_menu/scripts/ChangeCombo $(VENDORED_TTF_LIBS) bump-build-number $(CROSS_PREFIX)$(STRIP) mod/etc/options_menu/options $(CROSS_PREFIX)$(STRIP) mod/etc/options_menu/optiond $(CROSS_PREFIX)$(STRIP) mod/etc/options_menu/mod_uninstall/mod_uninstall @@ -61,6 +61,8 @@ $(OUT)/$(TARGET_NAME).hmod: mod/etc/options_menu/options mod/etc/options_menu/op $(CROSS_PREFIX)$(STRIP) mod/bin/standby_watchdog upx --lzma mod/bin/standby_watchdog $(CROSS_PREFIX)$(STRIP) mod/etc/options_menu/scripts/gen_splash + $(CROSS_PREFIX)$(STRIP) mod/etc/options_menu/scripts/ChangeCombo + upx --lzma mod/etc/options_menu/scripts/ChangeCombo $(inject-readme) cd mod/; tar -czvf "../out/$(TARGET_NAME).hmod" * @@ -93,6 +95,9 @@ mod/bin/standby_watchdog: src/standby_watchdog.o src/framework/controller.o src/ mod/etc/options_menu/scripts/gen_splash: src/gen_splash.o src/localization.o $(CROSS_PREFIX)$(CXX) src/gen_splash.o src/localization.o $(PNG_LIBS) $(LDFLAGS) -o mod/etc/options_menu/scripts/gen_splash +mod/etc/options_menu/scripts/ChangeCombo: src/change_combo.o src/framework/controller.o src/localization.o + $(CROSS_PREFIX)$(CXX) src/change_combo.o src/framework/controller.o src/localization.o $(LDFLAGS) -o mod/etc/options_menu/scripts/ChangeCombo + %.o: %.cpp $(CROSS_PREFIX)$(CXX) $(CXXFLAGS) -c $< -o $@ diff --git a/mod/etc/options_menu/scripts/ChangeCombo b/mod/etc/options_menu/scripts/ChangeCombo deleted file mode 100755 index b28801b27c7f33505dfdb015e918002a5d21cad2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 9900 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 KCx#J9L;NoUO7xci diff --git a/src/change_combo.cpp b/src/change_combo.cpp new file mode 100644 index 0000000..c38d487 --- /dev/null +++ b/src/change_combo.cpp @@ -0,0 +1,130 @@ +/** + * Copyright (c) 2026 DefKorns (https://defkorns.github.io/LICENSE) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "framework/controller.h" +#include "localization.h" + +#include +#include +#include +#include +#include + +static const GameButton AllButtons[] = { A, B, X, Y, L, R, SELECT, START, LEFT, RIGHT, UP, DOWN }; + +static const char * ButtonName(GameButton button) +{ + switch(button) + { + case A: return "A"; + case B: return "B"; + case X: return "X"; + case Y: return "Y"; + case L: return "L"; + case R: return "R"; + case SELECT: return "SELECT"; + case START: return "START"; + case LEFT: return "LEFT"; + case RIGHT: return "RIGHT"; + case UP: return "UP"; + default: return "DOWN"; + } +} + +// drops any button already held, so the next WaitForPress doesn't catch the +// tail of a press that was meant for the previous step +static void ConsumeHeldButtons(Controller & c) +{ + c.Update(); + for(GameButton b : AllButtons) + c.GetButtonStatus(b); +} + +static GameButton WaitForPress(Controller & c) +{ + for(;;) + { + c.Update(); + for(GameButton b : AllButtons) + if(c.GetButtonStatus(b)) + return b; + usleep(50000); + } +} + +int main() +{ + // RunCommand's popen pipe isn't a tty - stdio defaults to fully buffered + // there, so lines can sit unflushed instead of appearing as they're printed + setvbuf(stdout, nullptr, _IONBF, 0); + + LoadLanguageFromConfig("/etc/options_menu/"); + + Controller c(1); + + static const char * const intro[] = { + "CHANGE_COMBO_INTRO_1", "CHANGE_COMBO_INTRO_2", "CHANGE_COMBO_INTRO_3", "CHANGE_COMBO_CONTINUE" + }; + for(const char * key : intro) + std::cout << Translate(key) << std::endl; + ConsumeHeldButtons(c); + GameButton gate; + do { gate = WaitForPress(c); } while(gate != A && gate != B); + if(gate == B) + return 0; + + static const char * const prompts[3] = { + "CHANGE_COMBO_PRESS_1", + "CHANGE_COMBO_PRESS_2", + "CHANGE_COMBO_PRESS_3" + }; + + for(;;) + { + GameButton chosen[3]; + for(int i = 0; i < 3; ++i) + { + std::cout << Translate(prompts[i]) << std::endl; + ConsumeHeldButtons(c); + chosen[i] = WaitForPress(c); + } + + // button names (A, B, X...) stay untranslated, same as the footer badges + std::cout << Translate("CHANGE_COMBO_CHOSEN") << " " << ButtonName(chosen[0]) << " + " << ButtonName(chosen[1]) << " + " << ButtonName(chosen[2]) << std::endl; + std::cout << Translate("CHANGE_COMBO_CONFIRM") << std::endl; + ConsumeHeldButtons(c); + if(WaitForPress(c) == A) + { + // write to a temp file and rename into place - an interrupted write + // must never leave button.cfg truncated/empty for daemon.cpp to trip over + const char * const path = "/etc/options_menu/button.cfg"; + const std::string tmpPath = std::string(path) + ".tmp"; + std::ofstream out(tmpPath); + if(out << (int)chosen[0] << " " << (int)chosen[1] << " " << (int)chosen[2]) + { + out.close(); + std::rename(tmpPath.c_str(), path); + std::cout << Translate("CHANGE_COMBO_DONE") << std::endl; + } + else + std::cerr << Translate("CHANGE_COMBO_WRITE_ERROR") << std::endl; + break; + } + } + + return 0; +} diff --git a/src/localization.cpp b/src/localization.cpp index f159d82..5c40e26 100644 --- a/src/localization.cpp +++ b/src/localization.cpp @@ -64,3 +64,14 @@ std::string Translate(const std::string & key) auto it = strings.find(key); return it == strings.end() ? key : it->second; } + +void LoadLanguageFromConfig(const std::string & optionsRoot) +{ + std::string langCode("en-US"); + std::ifstream in("/etc/options_menu/language.cfg"); + std::getline(in, langCode); + in.close(); + if(langCode.empty()) + langCode = "en-US"; + LoadLanguage(optionsRoot, langCode); +} diff --git a/src/localization.h b/src/localization.h index 57ebb84..6baccfd 100644 --- a/src/localization.h +++ b/src/localization.h @@ -18,4 +18,7 @@ void LoadLanguage(const std::string & optionsRoot, const std::string & langCode); std::string Translate(const std::string & key); +// reads /etc/options_menu/language.cfg (defaulting to en-US) and loads it +void LoadLanguageFromConfig(const std::string & optionsRoot); + #endif diff --git a/src/main.cpp b/src/main.cpp index d7929e0..07cec0e 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -95,13 +95,7 @@ int main(int argc, char * argv[]) std::string commandLocation(optionsLocation + "commands/"); std::string scriptLocation(optionsLocation + "scripts/"); - std::string langCode("en-US"); - std::ifstream in("/etc/options_menu/language.cfg"); - std::getline(in, langCode); - in.close(); - if(langCode.empty()) - langCode = "en-US"; - LoadLanguage(optionsLocation, langCode); + LoadLanguageFromConfig(optionsLocation); SetTTFFontPath(optionsLocation); std::string titleString(Translate("OPTIONS_TITLE")); @@ -155,6 +149,7 @@ int main(int argc, char * argv[]) //Read commands from command folder std::vector commands; + std::ifstream in; if(auto dir = opendir(commandLocation.c_str())) { //Find all commands in folder From 49228efc949ca5c364888ea2985e037ec7914dbe Mon Sep 17 00:00:00 2001 From: DefKorns Date: Fri, 11 Sep 2026 15:50:17 +0000 Subject: [PATCH 017/104] Deduplicate UTF-8 decode and font8x8 glyph lookup texture.cpp, gen_splash.cpp and main.cpp each had their own copy of UTF-8 decoding and font8x8 glyph lookup, so they'd already drifted: gen_splash.cpp's GlyphFor was missing the Cyrillic range texture.cpp had, silently falling back to '?' for Cyrillic splash text. Moved both into shared src/framework/utf8.{h,cpp} and font8x8_lookup.{h,cpp}. --- Makefile.docker | 6 +- src/framework/font8x8_lookup.cpp | 51 +++++++++++++++++ src/framework/font8x8_lookup.h | 24 ++++++++ src/framework/texture.cpp | 94 ++------------------------------ src/framework/utf8.cpp | 84 ++++++++++++++++++++++++++++ src/framework/utf8.h | 33 +++++++++++ src/gen_splash.cpp | 66 +--------------------- src/main.cpp | 28 +--------- 8 files changed, 204 insertions(+), 182 deletions(-) create mode 100644 src/framework/font8x8_lookup.cpp create mode 100644 src/framework/font8x8_lookup.h create mode 100644 src/framework/utf8.cpp create mode 100644 src/framework/utf8.h diff --git a/Makefile.docker b/Makefile.docker index 371ce1d..c222f94 100644 --- a/Makefile.docker +++ b/Makefile.docker @@ -26,7 +26,7 @@ VENDORED_TTF_LIBS = endif CXXFLAGS = -std=c++11 -Os $(SDL_CFLAGS) -DMOD_VERSION=\"$(MOD_VER)\" LDLIBS = $(SDL_LIBS) -SOURCES = src/main.cpp src/command.cpp src/localization.cpp src/framework/sdl_context.cpp src/framework/texture.cpp src/framework/controller.cpp src/framework/powerwatch.cpp src/framework/draw_helpers.cpp +SOURCES = src/main.cpp src/command.cpp src/localization.cpp src/framework/sdl_context.cpp src/framework/texture.cpp src/framework/controller.cpp src/framework/powerwatch.cpp src/framework/draw_helpers.cpp src/framework/utf8.cpp src/framework/font8x8_lookup.cpp OBJECTS = $(SOURCES:.cpp=.o) DEV_DIR = $(shell realpath .) OUT = $(DEV_DIR)/out @@ -92,8 +92,8 @@ mod/etc/options_menu/mod_uninstall/mod_uninstall: src/mod_uninstall.o mod/bin/standby_watchdog: src/standby_watchdog.o src/framework/controller.o src/framework/powerwatch.o $(CROSS_PREFIX)$(CXX) src/standby_watchdog.o src/framework/controller.o src/framework/powerwatch.o -o mod/bin/standby_watchdog -mod/etc/options_menu/scripts/gen_splash: src/gen_splash.o src/localization.o - $(CROSS_PREFIX)$(CXX) src/gen_splash.o src/localization.o $(PNG_LIBS) $(LDFLAGS) -o mod/etc/options_menu/scripts/gen_splash +mod/etc/options_menu/scripts/gen_splash: src/gen_splash.o src/localization.o src/framework/utf8.o src/framework/font8x8_lookup.o + $(CROSS_PREFIX)$(CXX) src/gen_splash.o src/localization.o src/framework/utf8.o src/framework/font8x8_lookup.o $(PNG_LIBS) $(LDFLAGS) -o mod/etc/options_menu/scripts/gen_splash mod/etc/options_menu/scripts/ChangeCombo: src/change_combo.o src/framework/controller.o src/localization.o $(CROSS_PREFIX)$(CXX) src/change_combo.o src/framework/controller.o src/localization.o $(LDFLAGS) -o mod/etc/options_menu/scripts/ChangeCombo diff --git a/src/framework/font8x8_lookup.cpp b/src/framework/font8x8_lookup.cpp new file mode 100644 index 0000000..2c5e9cf --- /dev/null +++ b/src/framework/font8x8_lookup.cpp @@ -0,0 +1,51 @@ +/** + * Copyright (c) 2026 DefKorns (https://defkorns.github.io/LICENSE) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "font8x8_lookup.h" +#include "font8x8.h" + +#include + +static const char * LookupKanjiGlyph(unsigned int codepoint) +{ + auto it = std::lower_bound(std::begin(font8x8_kanji), std::end(font8x8_kanji), codepoint, + [](const Font8x8KanjiEntry & entry, unsigned int cp) { return entry.codepoint < cp; }); + if(it != std::end(font8x8_kanji) && it->codepoint == codepoint) + return reinterpret_cast(it->glyph); + return nullptr; +} + +const char * Font8x8Glyph(unsigned int codepoint) +{ + if(codepoint < 0x80) + return font8x8_basic[codepoint]; + if(codepoint >= 0xA0 && codepoint <= 0xFF) + return font8x8_ext_latin[codepoint - 0xA0]; + if(codepoint >= 0x0400 && codepoint <= 0x04FF) + return font8x8_cyrillic[codepoint - 0x0400]; + if(codepoint >= 0x3040 && codepoint <= 0x309F) + return font8x8_hiragana[codepoint - 0x3040]; + if(codepoint >= 0x30A0 && codepoint <= 0x30FF) + return font8x8_katakana[codepoint - 0x30A0]; + if(codepoint >= 0x4E00 && codepoint <= 0x9FFF) + { + const char * kanjiGlyph = LookupKanjiGlyph(codepoint); + if(kanjiGlyph) + return kanjiGlyph; + } + return font8x8_basic[0x3F]; +} diff --git a/src/framework/font8x8_lookup.h b/src/framework/font8x8_lookup.h new file mode 100644 index 0000000..69bb87c --- /dev/null +++ b/src/framework/font8x8_lookup.h @@ -0,0 +1,24 @@ +/** + * Copyright (c) 2026 DefKorns (https://defkorns.github.io/LICENSE) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef FONT8X8_LOOKUP_H_ +#define FONT8X8_LOOKUP_H_ + +// 8x8 bitmap glyph for a Unicode codepoint, falls back to '?' if unsupported +const char * Font8x8Glyph(unsigned int codepoint); + +#endif diff --git a/src/framework/texture.cpp b/src/framework/texture.cpp index e4d23f5..56a32dc 100644 --- a/src/framework/texture.cpp +++ b/src/framework/texture.cpp @@ -8,9 +8,9 @@ */ #include "texture.h" -#include "font8x8.h" +#include "font8x8_lookup.h" +#include "utf8.h" -#include #include #include #include @@ -109,55 +109,12 @@ SDL_Texture * LoadTexturePNG(SDL_Renderer *renderer, std::string file, SDL_Rect return texture; } -static const char * LookupKanjiGlyph(unsigned int codepoint) -{ - auto it = std::lower_bound(std::begin(font8x8_kanji), std::end(font8x8_kanji), codepoint, - [](const Font8x8KanjiEntry & entry, unsigned int cp) { return entry.codepoint < cp; }); - if (it != std::end(font8x8_kanji) && it->codepoint == codepoint) - return reinterpret_cast(it->glyph); - return nullptr; -} - SDL_Texture * WriteText(const std::string & text, int fontSize, SDL_Renderer* renderer, int & textureWidth, int & textureHeight, const int color) { if(text.size()==0) return nullptr; - std::vector codepoints; - for (size_t i = 0; i < text.size();) - { - unsigned char firstByte = static_cast(text[i]); - unsigned int codepoint = 0xFFFD; - size_t sequenceLength = 1; - - if (firstByte < 0x80) - { - codepoint = firstByte; - } - else if (firstByte >= 0xC2 && firstByte <= 0xDF && i + 1 < text.size()) - { - unsigned char secondByte = static_cast(text[i + 1]); - if ((secondByte & 0xC0) == 0x80) - { - codepoint = ((firstByte & 0x1F) << 6) | (secondByte & 0x3F); - sequenceLength = 2; - } - } - else if (firstByte >= 0xE0 && firstByte <= 0xEF && i + 2 < text.size()) - { - unsigned char secondByte = static_cast(text[i + 1]); - unsigned char thirdByte = static_cast(text[i + 2]); - if ((secondByte & 0xC0) == 0x80 && (thirdByte & 0xC0) == 0x80) - { - codepoint = ((firstByte & 0x0F) << 12) | - ((secondByte & 0x3F) << 6) | (thirdByte & 0x3F); - sequenceLength = 3; - } - } - - codepoints.push_back(codepoint); - i += sequenceLength; - } + std::vector codepoints = Utf8ToCodepoints(text); textureWidth = fontSize*codepoints.size(); textureHeight = fontSize; @@ -166,25 +123,7 @@ SDL_Texture * WriteText(const std::string & text, int fontSize, SDL_Renderer* re int pitch = 8*codepoints.size(); for (unsigned int i = 0; i < codepoints.size(); ++i) { - const char *bitmap = font8x8_basic[0x3F]; - unsigned int codepoint = codepoints[i]; - if (codepoint < 0x80) - bitmap = font8x8_basic[codepoint]; - else if (codepoint >= 0xA0 && codepoint <= 0xFF) - bitmap = font8x8_ext_latin[codepoint - 0xA0]; - else if (codepoint >= 0x0400 && codepoint <= 0x04FF) - bitmap = font8x8_cyrillic[codepoint - 0x0400]; - else if (codepoint >= 0x3040 && codepoint <= 0x309F) - bitmap = font8x8_hiragana[codepoint - 0x3040]; - else if (codepoint >= 0x30A0 && codepoint <= 0x30FF) - bitmap = font8x8_katakana[codepoint - 0x30A0]; - else if (codepoint >= 0x4E00 && codepoint <= 0x9FFF) - { - const char * kanjiGlyph = LookupKanjiGlyph(codepoint); - if (kanjiGlyph) - bitmap = kanjiGlyph; - } - + const char * bitmap = Font8x8Glyph(codepoints[i]); int offset = i*8; for(int y = 0; y < 8; ++y) { @@ -263,33 +202,10 @@ bool CanRenderWithTTF(const std::string & text, int fontSize) if(!font) return false; - for(size_t i = 0; i < text.size();) - { - unsigned char firstByte = static_cast(text[i]); - unsigned int codepoint = 0xFFFD; - size_t sequenceLength = 1; - - if(firstByte < 0x80) - { - codepoint = firstByte; - } - else if(firstByte >= 0xC2 && firstByte <= 0xDF && i + 1 < text.size()) - { - codepoint = ((firstByte & 0x1F) << 6) | (static_cast(text[i+1]) & 0x3F); - sequenceLength = 2; - } - else if(firstByte >= 0xE0 && firstByte <= 0xEF && i + 2 < text.size()) - { - codepoint = ((firstByte & 0x0F) << 12) | ((static_cast(text[i+1]) & 0x3F) << 6) | (static_cast(text[i+2]) & 0x3F); - sequenceLength = 3; - } - + for(unsigned int codepoint : Utf8ToCodepoints(text)) // TTF_GlyphIsProvided only takes a 16-bit code if(codepoint > 0xFFFF || !TTF_GlyphIsProvided(font, static_cast(codepoint))) return false; - - i += sequenceLength; - } return true; } diff --git a/src/framework/utf8.cpp b/src/framework/utf8.cpp new file mode 100644 index 0000000..1a2f784 --- /dev/null +++ b/src/framework/utf8.cpp @@ -0,0 +1,84 @@ +/** + * Copyright (c) 2026 DefKorns (https://defkorns.github.io/LICENSE) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "utf8.h" + +std::vector Utf8ToCodepoints(const std::string & text) +{ + std::vector codepoints; + for(size_t i = 0; i < text.size();) + { + unsigned char firstByte = static_cast(text[i]); + unsigned int codepoint = 0xFFFD; + size_t sequenceLength = 1; + + if(firstByte < 0x80) + { + codepoint = firstByte; + } + else if(firstByte >= 0xC2 && firstByte <= 0xDF && i + 1 < text.size()) + { + unsigned char secondByte = static_cast(text[i + 1]); + if((secondByte & 0xC0) == 0x80) + { + codepoint = ((firstByte & 0x1F) << 6) | (secondByte & 0x3F); + sequenceLength = 2; + } + } + else if(firstByte >= 0xE0 && firstByte <= 0xEF && i + 2 < text.size()) + { + unsigned char secondByte = static_cast(text[i + 1]); + unsigned char thirdByte = static_cast(text[i + 2]); + if((secondByte & 0xC0) == 0x80 && (thirdByte & 0xC0) == 0x80) + { + codepoint = ((firstByte & 0x0F) << 12) | + ((secondByte & 0x3F) << 6) | (thirdByte & 0x3F); + sequenceLength = 3; + } + } + + codepoints.push_back(codepoint); + i += sequenceLength; + } + return codepoints; +} + +int Utf8Length(const std::string & text) +{ + int count = 0; + for(size_t i = 0; i < text.size();) + { + unsigned char c = static_cast(text[i]); + i += (c >= 0xF0) ? 4 : (c >= 0xE0) ? 3 : (c >= 0xC0) ? 2 : 1; + ++count; + } + return count; +} + +std::string TruncateUtf8(const std::string & text, int maxCodepoints) +{ + size_t i = 0; + int count = 0; + while(i < text.size() && count < maxCodepoints) + { + unsigned char c = static_cast(text[i]); + size_t len = (c >= 0xF0) ? 4 : (c >= 0xE0) ? 3 : (c >= 0xC0) ? 2 : 1; + i += len; + ++count; + } + return text.substr(0, i); +} diff --git a/src/framework/utf8.h b/src/framework/utf8.h new file mode 100644 index 0000000..41220bd --- /dev/null +++ b/src/framework/utf8.h @@ -0,0 +1,33 @@ +/** + * Copyright (c) 2026 DefKorns (https://defkorns.github.io/LICENSE) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef UTF8_H_ +#define UTF8_H_ + +#include +#include + +// decodes UTF-8 into Unicode codepoints - malformed sequences become U+FFFD +std::vector Utf8ToCodepoints(const std::string & text); + +// number of codepoints, not bytes +int Utf8Length(const std::string & text); + +// truncates by codepoint, not byte, so multi-byte glyphs (e.g. Japanese) aren't split +std::string TruncateUtf8(const std::string & text, int maxCodepoints); + +#endif diff --git a/src/gen_splash.cpp b/src/gen_splash.cpp index 9a9baf6..df19ca1 100644 --- a/src/gen_splash.cpp +++ b/src/gen_splash.cpp @@ -8,9 +8,9 @@ */ #include "localization.h" -#include "framework/font8x8.h" +#include "framework/font8x8_lookup.h" +#include "framework/utf8.h" -#include #include #include #include @@ -26,66 +26,6 @@ namespace const int Cell = 8 * Scale; const int LineSpacing = Cell + Cell/2; - std::vector Utf8ToCodepoints(const std::string & text) - { - std::vector codepoints; - for (size_t i = 0; i < text.size();) - { - unsigned char firstByte = static_cast(text[i]); - unsigned int codepoint = 0xFFFD; - size_t sequenceLength = 1; - - if (firstByte < 0x80) - { - codepoint = firstByte; - } - else if (firstByte >= 0xC2 && firstByte <= 0xDF && i + 1 < text.size()) - { - unsigned char secondByte = static_cast(text[i + 1]); - if ((secondByte & 0xC0) == 0x80) - { - codepoint = ((firstByte & 0x1F) << 6) | (secondByte & 0x3F); - sequenceLength = 2; - } - } - else if (firstByte >= 0xE0 && firstByte <= 0xEF && i + 2 < text.size()) - { - unsigned char secondByte = static_cast(text[i + 1]); - unsigned char thirdByte = static_cast(text[i + 2]); - if ((secondByte & 0xC0) == 0x80 && (thirdByte & 0xC0) == 0x80) - { - codepoint = ((firstByte & 0x0F) << 12) | - ((secondByte & 0x3F) << 6) | (thirdByte & 0x3F); - sequenceLength = 3; - } - } - - codepoints.push_back(codepoint); - i += sequenceLength; - } - return codepoints; - } - - const char * GlyphFor(unsigned int codepoint) - { - if (codepoint < 0x80) - return font8x8_basic[codepoint]; - if (codepoint >= 0xA0 && codepoint <= 0xFF) - return font8x8_ext_latin[codepoint - 0xA0]; - if (codepoint >= 0x3040 && codepoint <= 0x309F) - return font8x8_hiragana[codepoint - 0x3040]; - if (codepoint >= 0x30A0 && codepoint <= 0x30FF) - return font8x8_katakana[codepoint - 0x30A0]; - if (codepoint >= 0x4E00 && codepoint <= 0x9FFF) - { - auto it = std::lower_bound(std::begin(font8x8_kanji), std::end(font8x8_kanji), codepoint, - [](const Font8x8KanjiEntry & entry, unsigned int cp) { return entry.codepoint < cp; }); - if (it != std::end(font8x8_kanji) && it->codepoint == codepoint) - return reinterpret_cast(it->glyph); - } - return font8x8_basic[0x3F]; - } - void DrawLine(std::vector & pixels, const std::string & text, int topY) { auto codepoints = Utf8ToCodepoints(text); @@ -94,7 +34,7 @@ namespace for (size_t i = 0; i < codepoints.size(); ++i) { - const char * bitmap = GlyphFor(codepoints[i]); + const char * bitmap = Font8x8Glyph(codepoints[i]); int ox = x0 + static_cast(i) * Cell; for (int y = 0; y < 8; ++y) { diff --git a/src/main.cpp b/src/main.cpp index 07cec0e..665267b 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -11,6 +11,7 @@ #include "framework/controller.h" #include "framework/powerwatch.h" #include "framework/draw_helpers.h" +#include "framework/utf8.h" #include "command.h" #include "localization.h" @@ -37,33 +38,6 @@ void sReplace(std::string & command, std::string oldString, std::string newStrin } } -// truncates by UTF-8 codepoint, not byte, so multi-byte glyphs (e.g. Japanese) aren't split -std::string TruncateUtf8(const std::string & text, int maxCodepoints) -{ - size_t i = 0; - int count = 0; - while(i < text.size() && count < maxCodepoints) - { - unsigned char c = static_cast(text[i]); - size_t len = (c >= 0xF0) ? 4 : (c >= 0xE0) ? 3 : (c >= 0xC0) ? 2 : 1; - i += len; - ++count; - } - return text.substr(0, i); -} - -int Utf8Length(const std::string & text) -{ - int count = 0; - for(size_t i = 0; i < text.size();) - { - unsigned char c = static_cast(text[i]); - i += (c >= 0xF0) ? 4 : (c >= 0xE0) ? 3 : (c >= 0xC0) ? 2 : 1; - ++count; - } - return count; -} - // "%options_path%/xxx" command templates always produce a double slash (optionsLocation // already ends in one) - collapsing here stops it from growing an extra slash with every // relaunch, which otherwise breaks string-equality checks like the self-relaunch one below From 3fb9ba80246bd370aaa64fd40fd237e435da4d0f Mon Sep 17 00:00:00 2001 From: DefKorns Date: Fri, 11 Sep 2026 15:51:41 +0000 Subject: [PATCH 018/104] Fix mod_uninstall failing to find libSDL2_ttf/libfreetype Its rpath was $ORIGIN/.. (mod_uninstall/mod_uninstall -> /etc/options_menu), but this console's dynamic linker doesn't resolve the ".." component, so the binary silently failed to launch and left the last options screen frozen on-screen. Hardcode the rpath to /etc/options_menu instead - the install location is already fixed throughout this codebase, so an absolute path costs nothing. --- Makefile.docker | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile.docker b/Makefile.docker index c222f94..00987cc 100644 --- a/Makefile.docker +++ b/Makefile.docker @@ -87,7 +87,7 @@ mod/etc/options_menu/optiond: src/daemon.o src/framework/controller.o $(CROSS_PREFIX)$(CXX) $(LDFLAGS) src/daemon.o src/framework/controller.o -o mod/etc/options_menu/optiond mod/etc/options_menu/mod_uninstall/mod_uninstall: src/mod_uninstall.o - $(CROSS_PREFIX)$(CXX) src/mod_uninstall.o src/framework/*.o $(LDLIBS) $(LDFLAGS) -Wl,-rpath,'$$ORIGIN/..' -o mod/etc/options_menu/mod_uninstall/mod_uninstall + $(CROSS_PREFIX)$(CXX) src/mod_uninstall.o src/framework/*.o $(LDLIBS) $(LDFLAGS) -Wl,-rpath,/etc/options_menu -o mod/etc/options_menu/mod_uninstall/mod_uninstall mod/bin/standby_watchdog: src/standby_watchdog.o src/framework/controller.o src/framework/powerwatch.o $(CROSS_PREFIX)$(CXX) src/standby_watchdog.o src/framework/controller.o src/framework/powerwatch.o -o mod/bin/standby_watchdog From dcf34a85b1200c5483df9f841e820ad50861ef83 Mon Sep 17 00:00:00 2001 From: DefKorns Date: Fri, 11 Sep 2026 21:14:05 +0000 Subject: [PATCH 019/104] Redesign mod_uninstall with Modern UI chrome --- Makefile.docker | 4 +- mod/etc/options_menu/language/de-DE.lang | 7 + mod/etc/options_menu/language/en-US.lang | 7 + mod/etc/options_menu/language/es-ES.lang | 7 + mod/etc/options_menu/language/fr-FR.lang | 7 + mod/etc/options_menu/language/it-IT.lang | 7 + mod/etc/options_menu/language/ja-JP.lang | 7 + mod/etc/options_menu/language/nl-NL.lang | 7 + mod/etc/options_menu/language/pt-PT.lang | 7 + mod/etc/options_menu/language/ru-RU.lang | 7 + src/framework/uitheme.h | 2 + src/mod_uninstall.cpp | 383 ++++++++++++++--------- 12 files changed, 306 insertions(+), 146 deletions(-) diff --git a/Makefile.docker b/Makefile.docker index 00987cc..714a932 100644 --- a/Makefile.docker +++ b/Makefile.docker @@ -86,8 +86,8 @@ mod/etc/options_menu/libfreetype.so.2.8: mod/etc/options_menu/optiond: src/daemon.o src/framework/controller.o $(CROSS_PREFIX)$(CXX) $(LDFLAGS) src/daemon.o src/framework/controller.o -o mod/etc/options_menu/optiond -mod/etc/options_menu/mod_uninstall/mod_uninstall: src/mod_uninstall.o - $(CROSS_PREFIX)$(CXX) src/mod_uninstall.o src/framework/*.o $(LDLIBS) $(LDFLAGS) -Wl,-rpath,/etc/options_menu -o mod/etc/options_menu/mod_uninstall/mod_uninstall +mod/etc/options_menu/mod_uninstall/mod_uninstall: src/mod_uninstall.o src/localization.o + $(CROSS_PREFIX)$(CXX) src/mod_uninstall.o src/localization.o src/framework/*.o $(LDLIBS) $(LDFLAGS) -Wl,-rpath,/etc/options_menu -o mod/etc/options_menu/mod_uninstall/mod_uninstall mod/bin/standby_watchdog: src/standby_watchdog.o src/framework/controller.o src/framework/powerwatch.o $(CROSS_PREFIX)$(CXX) src/standby_watchdog.o src/framework/controller.o src/framework/powerwatch.o -o mod/bin/standby_watchdog diff --git a/mod/etc/options_menu/language/de-DE.lang b/mod/etc/options_menu/language/de-DE.lang index 47ebab1..3c51038 100644 --- a/mod/etc/options_menu/language/de-DE.lang +++ b/mod/etc/options_menu/language/de-DE.lang @@ -59,3 +59,10 @@ CHANGE_COMBO_CHOSEN=Deine gewählte Kombination ist: CHANGE_COMBO_CONFIRM=Wenn das stimmt, A drücken. Andernfalls eine beliebige andere Taste zum erneuten Versuch. CHANGE_COMBO_DONE=Fertig. Bitte neu starten, damit die Änderung wirksam wird. CHANGE_COMBO_WRITE_ERROR=Die Tastenkombination konnte nicht gespeichert werden. +MOD_UNINSTALLER_INSTALLED=Installierte Module +MOD_UNINSTALLER_TO_REMOVE=Zu deinstallieren +HINT_ADD=Hinzufügen +HINT_UNDO=Rückgängig +HINT_UNINSTALL=Deinstallieren +MOD_UNINSTALL_CONFIRM=Diese Module entfernen? +MOD_UNINSTALL_CONFIRM_HINT=Start+Select = Ja B = Nein diff --git a/mod/etc/options_menu/language/en-US.lang b/mod/etc/options_menu/language/en-US.lang index 6a5f00e..179b899 100644 --- a/mod/etc/options_menu/language/en-US.lang +++ b/mod/etc/options_menu/language/en-US.lang @@ -59,3 +59,10 @@ CHANGE_COMBO_CHOSEN=Your chosen options menu combo is: CHANGE_COMBO_CONFIRM=If this is correct press A. If not press any other button to try again. CHANGE_COMBO_DONE=Done. Please reboot to take effect. CHANGE_COMBO_WRITE_ERROR=Could not write button combo to file. +MOD_UNINSTALLER_INSTALLED=Installed Modules +MOD_UNINSTALLER_TO_REMOVE=To Uninstall +HINT_ADD=Add +HINT_UNDO=Undo +HINT_UNINSTALL=Uninstall +MOD_UNINSTALL_CONFIRM=Remove these modules? +MOD_UNINSTALL_CONFIRM_HINT=Start+Select = Yes B = No diff --git a/mod/etc/options_menu/language/es-ES.lang b/mod/etc/options_menu/language/es-ES.lang index 1c467fd..99102f9 100644 --- a/mod/etc/options_menu/language/es-ES.lang +++ b/mod/etc/options_menu/language/es-ES.lang @@ -59,3 +59,10 @@ CHANGE_COMBO_CHOSEN=La combinación elegida es: CHANGE_COMBO_CONFIRM=Si es correcta pulsa A. Si no, pulsa cualquier otro botón para intentarlo de nuevo. CHANGE_COMBO_DONE=Listo. Reinicia la consola para aplicar los cambios. CHANGE_COMBO_WRITE_ERROR=No se pudo guardar la combinación en el archivo. +MOD_UNINSTALLER_INSTALLED=Módulos instalados +MOD_UNINSTALLER_TO_REMOVE=Para desinstalar +HINT_ADD=Añadir +HINT_UNDO=Deshacer +HINT_UNINSTALL=Desinstalar +MOD_UNINSTALL_CONFIRM=¿Eliminar estos módulos? +MOD_UNINSTALL_CONFIRM_HINT=Start+Select = Sí B = No diff --git a/mod/etc/options_menu/language/fr-FR.lang b/mod/etc/options_menu/language/fr-FR.lang index b17826a..e1dbee1 100644 --- a/mod/etc/options_menu/language/fr-FR.lang +++ b/mod/etc/options_menu/language/fr-FR.lang @@ -59,3 +59,10 @@ CHANGE_COMBO_CHOSEN=La combinaison choisie est : CHANGE_COMBO_CONFIRM=Si c'est correct, appuie sur A. Sinon, appuie sur un autre bouton pour réessayer. CHANGE_COMBO_DONE=Terminé. Redémarre la console pour appliquer les changements. CHANGE_COMBO_WRITE_ERROR=Impossible d'enregistrer la combinaison dans le fichier. +MOD_UNINSTALLER_INSTALLED=Modules installés +MOD_UNINSTALLER_TO_REMOVE=À désinstaller +HINT_ADD=Ajouter +HINT_UNDO=Annuler +HINT_UNINSTALL=Désinstaller +MOD_UNINSTALL_CONFIRM=Supprimer ces modules ? +MOD_UNINSTALL_CONFIRM_HINT=Start+Select = Oui B = Non diff --git a/mod/etc/options_menu/language/it-IT.lang b/mod/etc/options_menu/language/it-IT.lang index b268cef..43f451d 100644 --- a/mod/etc/options_menu/language/it-IT.lang +++ b/mod/etc/options_menu/language/it-IT.lang @@ -59,3 +59,10 @@ CHANGE_COMBO_CHOSEN=La combinazione scelta è: CHANGE_COMBO_CONFIRM=Se è corretta premi A. Altrimenti premi un altro tasto per riprovare. CHANGE_COMBO_DONE=Fatto. Riavvia la console per applicare le modifiche. CHANGE_COMBO_WRITE_ERROR=Impossibile salvare la combinazione nel file. +MOD_UNINSTALLER_INSTALLED=Moduli installati +MOD_UNINSTALLER_TO_REMOVE=Da disinstallare +HINT_ADD=Aggiungi +HINT_UNDO=Annulla +HINT_UNINSTALL=Disinstalla +MOD_UNINSTALL_CONFIRM=Rimuovere questi moduli? +MOD_UNINSTALL_CONFIRM_HINT=Start+Select = Sì B = No diff --git a/mod/etc/options_menu/language/ja-JP.lang b/mod/etc/options_menu/language/ja-JP.lang index 6e5f71e..e4d1355 100644 --- a/mod/etc/options_menu/language/ja-JP.lang +++ b/mod/etc/options_menu/language/ja-JP.lang @@ -59,3 +59,10 @@ CHANGE_COMBO_CHOSEN=えらんだくみあわせ: CHANGE_COMBO_CONFIRM=これでよければA, ちがえばほかのButtonをおしてください CHANGE_COMBO_DONE=かんりょう. てきようするにはさいきどうしてください CHANGE_COMBO_WRITE_ERROR=くみあわせをFileにほぞんできませんでした +MOD_UNINSTALLER_INSTALLED=InstallされたModule +MOD_UNINSTALLER_TO_REMOVE=さくじょするModule +HINT_ADD=ついか +HINT_UNDO=とりけし +HINT_UNINSTALL=さくじょ +MOD_UNINSTALL_CONFIRM=これらのModuleをさくじょしますか? +MOD_UNINSTALL_CONFIRM_HINT=Start+Select = はい B = いいえ diff --git a/mod/etc/options_menu/language/nl-NL.lang b/mod/etc/options_menu/language/nl-NL.lang index d533be6..608e68f 100644 --- a/mod/etc/options_menu/language/nl-NL.lang +++ b/mod/etc/options_menu/language/nl-NL.lang @@ -59,3 +59,10 @@ CHANGE_COMBO_CHOSEN=Je gekozen combinatie is: CHANGE_COMBO_CONFIRM=Als dit klopt, druk op A. Zo niet, druk op een andere knop om opnieuw te proberen. CHANGE_COMBO_DONE=Klaar. Herstart de console om de wijziging toe te passen. CHANGE_COMBO_WRITE_ERROR=Kon de combinatie niet opslaan in het bestand. +MOD_UNINSTALLER_INSTALLED=Geïnstalleerde modules +MOD_UNINSTALLER_TO_REMOVE=Te verwijderen +HINT_ADD=Toevoegen +HINT_UNDO=Ongedaan maken +HINT_UNINSTALL=Verwijderen +MOD_UNINSTALL_CONFIRM=Deze modules verwijderen? +MOD_UNINSTALL_CONFIRM_HINT=Start+Select = Ja B = Nee diff --git a/mod/etc/options_menu/language/pt-PT.lang b/mod/etc/options_menu/language/pt-PT.lang index aba8f0c..b0b35d4 100644 --- a/mod/etc/options_menu/language/pt-PT.lang +++ b/mod/etc/options_menu/language/pt-PT.lang @@ -59,3 +59,10 @@ CHANGE_COMBO_CHOSEN=A combinação escolhida é: CHANGE_COMBO_CONFIRM=Se estiver correta prime A. Caso contrário, prime qualquer outro botão para tentar de novo. CHANGE_COMBO_DONE=Concluído. Reinicia a consola para aplicar. CHANGE_COMBO_WRITE_ERROR=Não foi possível guardar a combinação no ficheiro. +MOD_UNINSTALLER_INSTALLED=Módulos instalados +MOD_UNINSTALLER_TO_REMOVE=Para desinstalar +HINT_ADD=Adicionar +HINT_UNDO=Anular +HINT_UNINSTALL=Desinstalar +MOD_UNINSTALL_CONFIRM=Remover estes módulos? +MOD_UNINSTALL_CONFIRM_HINT=Start+Select = Sim B = Não diff --git a/mod/etc/options_menu/language/ru-RU.lang b/mod/etc/options_menu/language/ru-RU.lang index c902463..aded6c8 100644 --- a/mod/etc/options_menu/language/ru-RU.lang +++ b/mod/etc/options_menu/language/ru-RU.lang @@ -59,3 +59,10 @@ CHANGE_COMBO_CHOSEN=Выбранная комбинация: CHANGE_COMBO_CONFIRM=Если верно, нажмите A. Если нет, нажмите любую другую кнопку, чтобы попробовать снова. CHANGE_COMBO_DONE=Готово. Перезагрузите консоль, чтобы применить изменения. CHANGE_COMBO_WRITE_ERROR=Не удалось сохранить комбинацию в файл. +MOD_UNINSTALLER_INSTALLED=Установленные модули +MOD_UNINSTALLER_TO_REMOVE=Для удаления +HINT_ADD=Добавить +HINT_UNDO=Отменить +HINT_UNINSTALL=Удалить +MOD_UNINSTALL_CONFIRM=Удалить эти модули? +MOD_UNINSTALL_CONFIRM_HINT=Start+Select = Да B = Нет diff --git a/src/framework/uitheme.h b/src/framework/uitheme.h index 1179556..b4b225f 100644 --- a/src/framework/uitheme.h +++ b/src/framework/uitheme.h @@ -124,6 +124,8 @@ namespace UiTheme const BadgeColor BadgeXDark{ 0x3F, 0x59, 0x7F }; const BadgeColor BadgeY{ 0xC9, 0x7B, 0x84 }; // dusty rose, unused const BadgeColor BadgeYDark{ 0x8F, 0x47, 0x50 }; + const BadgeColor BadgeStart{ 0xC2, 0xA8, 0x5E }; // muted gold, for a wide "Start" pill instead of a letter circle + const BadgeColor BadgeStartDark{ 0x74, 0x65, 0x38 }; // "created by CompCom" footer credit, left-aligned in the footer const int CreditX = FrameX; diff --git a/src/mod_uninstall.cpp b/src/mod_uninstall.cpp index a66de9f..7dfab40 100644 --- a/src/mod_uninstall.cpp +++ b/src/mod_uninstall.cpp @@ -7,229 +7,324 @@ * of the License, or (at your option) any later version. */ +#include "framework/sdl_helper.h" #include "framework/controller.h" #include "framework/powerwatch.h" -#include "framework/sdl_helper.h" +#include "framework/draw_helpers.h" +#include "framework/utf8.h" +#include "localization.h" +#include #include -#include -#include #include +#include #include -struct Mod +namespace { - std::string name; - Texture texture; - void Draw(SDL_Renderer* renderer) + const std::string OptionsLocation = "/etc/options_menu/"; + const int ListFontSize = 16; + + struct Mod + { + std::string name; + Texture text; + }; + + // "hakchi pack_list" lines keep their trailing newline + std::string TrimTrailingNewline(std::string s) { - texture.Draw(renderer); + while(!s.empty() && (s.back() == '\n' || s.back() == '\r')) + s.pop_back(); + return s; } -}; + + std::string TruncateToWidth(const std::string & text, int maxWidth) + { + if(MeasureTTFWidth(text, ListFontSize) <= maxWidth) + return text; + int n = Utf8Length(text); + while(n > 0 && MeasureTTFWidth(TruncateUtf8(text, n) + "...", ListFontSize) > maxWidth) + --n; + return TruncateUtf8(text, n) + "..."; + } +} int main(int argc, char * argv[]) { std::string folderLocation(argv[0]); folderLocation = folderLocation.substr(0, folderLocation.find_last_of('/')+1); - std::vector installedMods; + std::vector installedMods; { char buf[256]; auto pipe = popen("hakchi pack_list", "r"); if(pipe) { while(!feof(pipe)) - { - if(fgets(buf, 256, pipe) > 0) - installedMods.push_back({buf}); - } + if(fgets(buf, sizeof(buf), pipe)) + installedMods.push_back({TrimTrailingNewline(buf), {}}); pclose(pipe); } } - if(installedMods.size() == 0) + if(installedMods.empty()) { std::cerr << "Cannot find mods"; return 1; } - if(SDL_Init(SDL_INIT_VIDEO) != 0) - { - std::cerr << "Cannot initialise SDL.\n"; - return 1; - } + LoadLanguageFromConfig(OptionsLocation); + SetTTFFontPath(OptionsLocation); + SDL_Context sdl_context(std::chrono::milliseconds(33), false); + auto renderer = sdl_context.renderer; Controller controller(1); - PowerWatch powerWatch; - auto window = SDL_CreateWindow("", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, 1280, 720, SDL_WINDOW_FULLSCREEN); - auto renderer = SDL_CreateRenderer(window, -1, 0); - if(renderer == NULL) { - std::cerr << "Cannot create renderer.\n"; - SDL_DestroyWindow(window); - return 1; - } - SDL_SetRenderDrawColor(renderer, 0, 0, 0, SDL_ALPHA_OPAQUE); - Texture titleText("Module Uninstaller", 36, renderer, 640, 60, true); - Texture modListText("Installed Modules", 18, renderer, 30, 120); - Texture modUninstallText("Modules To Uninstall", 18, renderer, 890, 120); + const Uint8 bgR = UiTheme::BgR, bgG = UiTheme::BgG, bgB = UiTheme::BgB; + SDL_SetRenderDrawColor(renderer, bgR, bgG, bgB, SDL_ALPHA_OPAQUE); + + Texture gearIcon(OptionsLocation + UiTheme::AssetGear, renderer, UiTheme::GearX, UiTheme::GearY); + Texture appTitleText("OptionsMenu", UiTheme::TitleFontSize, renderer, UiTheme::TitleX, UiTheme::TitleY, false, 0xFFFFFFFF, true); + appTitleText.rect.y -= appTitleText.rect.h / 2; + Texture appVersionText(MOD_VERSION, UiTheme::VersionFontSize, renderer, appTitleText.rect.x + appTitleText.rect.w + UiTheme::VersionGap, UiTheme::TitleY, false, 0xFFFFFFFF, true); + appVersionText.rect.y -= appVersionText.rect.h / 2; + Texture titleText(Translate("MODULE_UNINSTALLER"), UiTheme::SectionTitleFontSize, renderer, UiTheme::SectionTitleX, UiTheme::SectionTitleY, false, 0xFFFFFFFF, true); + Texture creditText("created by CompCom - Modern UI by DefKorns", 16, renderer, UiTheme::CreditX, UiTheme::CreditY, false, 0xFFFFFFFF, true); + + // two columns sharing the row list's usual width, split by a center divider + const int ColumnGap = 40; + const int ColumnW = (UiTheme::ListContentRightX - UiTheme::ListX - ColumnGap) / 2; + const int LeftColumnX = UiTheme::ListX; + const int RightColumnX = LeftColumnX + ColumnW + ColumnGap; + const int DividerX = LeftColumnX + ColumnW + ColumnGap/2; + const int ColumnHeaderY = UiTheme::SectionTitleY + 46; + const int ListTopY = ColumnHeaderY + 32; + const int ListBottomY = UiTheme::FooterDividerY - 16; + const int rowPitch = GetTTFLineHeight(ListFontSize); + const int displayCount = std::max(1, (ListBottomY - ListTopY) / rowPitch); + + Texture installedHeader(Translate("MOD_UNINSTALLER_INSTALLED"), 18, renderer, LeftColumnX, ColumnHeaderY, false, UiTheme::TextDimColor, true); + Texture toRemoveHeader(Translate("MOD_UNINSTALLER_TO_REMOVE"), 18, renderer, RightColumnX, ColumnHeaderY, false, UiTheme::TextDimColor, true); for(Mod & mod : installedMods) + mod.text = Texture(TruncateToWidth(mod.name, ColumnW), ListFontSize, renderer, LeftColumnX, 0, false, 0xFFFFFFFF, true); + + // this screen's list spans a different vertical range than the main menu's, so the + // chevrons are anchored to it directly rather than reusing UiTheme::ScrollUpY/DownY + Texture scrollUp("^", 16, renderer, UiTheme::ScrollX, ListTopY + 10, false, 0xFFFFFFFF, true); + scrollUp.rect.x -= scrollUp.rect.w / 2; + Texture scrollDown = scrollUp; + scrollDown.rect.y = ListBottomY - 50; + + Texture badgeOuter(OptionsLocation + UiTheme::AssetBadgeOuter, renderer); + Texture badgeInner(OptionsLocation + UiTheme::AssetBadgeInner, renderer); + struct Badge { Texture letter; Texture label; UiTheme::BadgeColor rim; UiTheme::BadgeColor fill; }; + Badge badgeAdd{ Texture("A", 16, renderer, 0, 0, false, UiTheme::BadgeLetterColor, true), Texture(Translate("HINT_ADD"), 16, renderer, 0, 0, false, 0xFFFFFFFF, true), UiTheme::BadgeADark, UiTheme::BadgeA }; + Badge badgeUndo{ Texture("B", 16, renderer, 0, 0, false, UiTheme::BadgeLetterColor, true), Texture(Translate("HINT_UNDO"), 16, renderer, 0, 0, false, 0xFFFFFFFF, true), UiTheme::BadgeBDark, UiTheme::BadgeB }; + auto DrawBadge = [&](Badge & badge, int rightEdgeX) -> int { - mod.texture = Texture(mod.name, 14, renderer); - mod.texture.rect.x = 60; - } + int groupW = UiTheme::BadgeOuterSize + UiTheme::BadgeLabelGap + badge.label.rect.w; + int x = rightEdgeX - groupW; + int y = UiTheme::BadgeBandY; + badgeOuter.rect = { x, y, UiTheme::BadgeOuterSize, UiTheme::BadgeOuterSize }; + SDL_SetTextureColorMod(badgeOuter.texture.get(), badge.rim.r, badge.rim.g, badge.rim.b); + badgeOuter.Draw(renderer); + int innerOffset = (UiTheme::BadgeOuterSize - UiTheme::BadgeInnerSize) / 2; + badgeInner.rect = { x+innerOffset, y+innerOffset, UiTheme::BadgeInnerSize, UiTheme::BadgeInnerSize }; + SDL_SetTextureColorMod(badgeInner.texture.get(), badge.fill.r, badge.fill.g, badge.fill.b); + badgeInner.Draw(renderer); + badge.letter.rect.x = x + (UiTheme::BadgeOuterSize - badge.letter.rect.w)/2; + badge.letter.rect.y = y + (UiTheme::BadgeOuterSize - badge.letter.rect.h)/2; + badge.letter.Draw(renderer); + badge.label.rect.x = x + UiTheme::BadgeOuterSize + UiTheme::BadgeLabelGap; + badge.label.rect.y = y + (UiTheme::BadgeOuterSize - badge.label.rect.h)/2; + badge.label.Draw(renderer); + return x - UiTheme::BadgeGroupGap; + }; + + // wide "Start" chip instead of a single-letter circle - "St" alone reads ambiguous next to Select + Texture pillText("Start", 14, renderer, 0, 0, false, UiTheme::BadgeLetterColor, true); + Texture uninstallLabel(Translate("HINT_UNINSTALL"), 16, renderer, 0, 0, false, 0xFFFFFFFF, true); + auto DrawPillBadge = [&](Texture & pill, Texture & label, const UiTheme::BadgeColor & rim, const UiTheme::BadgeColor & fill, int rightEdgeX) -> int + { + const int pillPadX = 10; + int pillW = pill.rect.w + pillPadX*2; + int groupW = pillW + UiTheme::BadgeLabelGap + label.rect.w; + int x = rightEdgeX - groupW; + int y = UiTheme::BadgeBandY; + SDL_Rect pillRect{ x, y, pillW, UiTheme::BadgeOuterSize }; + DrawRoundedFillRect(renderer, pillRect, rim.r, rim.g, rim.b, pillRect.h/2); + // same ring thickness as the letter badges' outer/inner circle pair + int border = (UiTheme::BadgeOuterSize - UiTheme::BadgeInnerSize) / 2; + SDL_Rect fillRect{ x+border, y+border, pillW-border*2, UiTheme::BadgeInnerSize }; + DrawRoundedFillRect(renderer, fillRect, fill.r, fill.g, fill.b, fillRect.h/2); + pill.rect.x = x + (pillW - pill.rect.w)/2; + pill.rect.y = y + (UiTheme::BadgeOuterSize - pill.rect.h)/2; + pill.Draw(renderer); + label.rect.x = x + pillW + UiTheme::BadgeLabelGap; + label.rect.y = y + (UiTheme::BadgeOuterSize - label.rect.h)/2; + label.Draw(renderer); + return x - UiTheme::BadgeGroupGap; + }; - Texture pointerTexture("->", 14, renderer, 30, 150, false, 0xFF00FF00); - Texture instructionsText("START - Uninstall Mods/Exit A - Add mod to uninstall list B - Undo", 12, renderer, 640, 648, true); int installListOffset = 0; int currentId = 0; - const int displayCount = 20; std::vector uninstallList; - const auto fpsTime = std::chrono::milliseconds(33); - auto nextFrameTime = std::chrono::system_clock::now()+fpsTime; + + auto DrawChrome = [&]() + { + DrawStrokeRect(renderer, UiTheme::OuterRect, UiTheme::BorderR, UiTheme::BorderG, UiTheme::BorderB, UiTheme::BorderWidth, UiTheme::BorderRadius); + gearIcon.Draw(renderer); + appTitleText.Draw(renderer); + appVersionText.Draw(renderer); + DrawHLine(renderer, UiTheme::HeaderDividerX, UiTheme::HeaderDividerX + UiTheme::HeaderDividerW, UiTheme::HeaderDividerY, UiTheme::BorderR, UiTheme::BorderG, UiTheme::BorderB, UiTheme::BorderWidth); + DrawHLine(renderer, UiTheme::OuterRect.x, UiTheme::OuterRect.x + UiTheme::OuterRect.w, UiTheme::FooterDividerY, UiTheme::BorderR, UiTheme::BorderG, UiTheme::BorderB, UiTheme::BorderWidth); + int accentBarY = titleText.rect.y + (titleText.rect.h - UiTheme::SectionAccentBarH) / 2; + DrawFillRect(renderer, { UiTheme::SectionTitleX - UiTheme::SectionAccentBarW - 14, accentBarY, UiTheme::SectionAccentBarW, UiTheme::SectionAccentBarH }, UiTheme::AccentR, UiTheme::AccentG, UiTheme::AccentB); + titleText.Draw(renderer); + creditText.Draw(renderer); + }; + + auto render = [&]() + { + sdl_context.StartFrame(); + DrawChrome(); + installedHeader.Draw(renderer); + toRemoveHeader.Draw(renderer); + DrawVLine(renderer, DividerX, ColumnHeaderY, ListBottomY, UiTheme::BorderR, UiTheme::BorderG, UiTheme::BorderB, UiTheme::BorderWidth); + + int count = std::min(displayCount, (int)installedMods.size()-installListOffset); + for(int i = 0; i < count; ++i) + { + Texture & rowText = installedMods[installListOffset+i].text; + rowText.rect.x = LeftColumnX; + rowText.rect.y = ListTopY + i*rowPitch + (rowPitch - rowText.rect.h)/2; + if(i == currentId) + { + SDL_Rect rowRect{ LeftColumnX - 8, rowText.rect.y - 2, ColumnW + 8, rowText.rect.h + 4 }; + DrawRoundedFillRect(renderer, rowRect, UiTheme::SelectedRowBgR, UiTheme::SelectedRowBgG, UiTheme::SelectedRowBgB, UiTheme::BoxRadius); + DrawStrokeRect(renderer, rowRect, UiTheme::AccentR, UiTheme::AccentG, UiTheme::AccentB, 2, UiTheme::BoxRadius); + } + rowText.Draw(renderer); + } + if(installListOffset != 0) + scrollUp.Draw(renderer); + if(installListOffset + displayCount < (int)installedMods.size()) + scrollDown.Draw(renderer, SDL_FLIP_VERTICAL); + + int uninstallCount = std::min(displayCount, (int)uninstallList.size()); + for(int i = 0; i < uninstallCount; ++i) + { + Texture & rowText = uninstallList[i].text; + rowText.rect.x = RightColumnX; + rowText.rect.y = ListTopY + i*rowPitch + (rowPitch - rowText.rect.h)/2; + rowText.Draw(renderer); + } + + int rightEdge = UiTheme::BadgeClusterRightX; + rightEdge = DrawBadge(badgeAdd, rightEdge); + rightEdge = DrawBadge(badgeUndo, rightEdge); + DrawPillBadge(pillText, uninstallLabel, UiTheme::BadgeStartDark, UiTheme::BadgeStart, rightEdge); + + SDL_SetRenderDrawColor(renderer, bgR, bgG, bgB, 0xFF); // flat helpers leave the draw color dirty + sdl_context.EndFrame(); + }; + + // true if the uninstall was confirmed and kicked off - caller should break out of the main loop + auto ConfirmUninstall = [&]() -> bool + { + Texture confirmTitle(Translate("MOD_UNINSTALL_CONFIRM"), 24, renderer, 640, 320, true, 0xFFFFFFFF, true); + Texture confirmHint(Translate("MOD_UNINSTALL_CONFIRM_HINT"), 16, renderer, 640, 360, true, 0xFFFFFFFF, true); + controller.GetButtonStatus(B); // consume the still-held B from whatever press opened this dialog + bool confirmed = false; + for(;;) + { + controller.Update(); + if(sdl_context.powerwatch->buttonPress() || controller.GetButtonStatus(B)) + break; + if(controller.PeekButtonStatus(START) && controller.PeekButtonStatus(SELECT)) + { + confirmed = true; + break; + } + sdl_context.StartFrame(); + DrawFillRect(renderer, UiTheme::FrameRect, bgR, bgG, bgB); + DrawStrokeRect(renderer, UiTheme::FrameRect, UiTheme::BorderR, UiTheme::BorderG, UiTheme::BorderB, UiTheme::BorderWidth, UiTheme::BorderRadius); + confirmTitle.Draw(renderer); + confirmHint.Draw(renderer); + SDL_SetRenderDrawColor(renderer, bgR, bgG, bgB, 0xFF); + sdl_context.EndFrame(); + } + if(!confirmed) + return false; + + std::ofstream out("/tmp/uninstall"); + if(out.is_open()) + { + for(auto & mod : uninstallList) + out << mod.name << " "; + out.close(); + system(("/bin/sh " + folderLocation + "/FinishUninstall.sh &").c_str()); + } + return true; + }; + for(;;) { SDL_Event e; - //Poll for quit event (CTRL+C)/Close - while (SDL_PollEvent(&e)) { - if (e.type == SDL_QUIT) { + while(SDL_PollEvent(&e)) + if(e.type == SDL_QUIT) return 0; - } - } controller.Update(); - if(powerWatch.buttonPress()) - { + if(sdl_context.powerwatch->buttonPress()) break; - } + if(controller.GetButtonStatus(UP)) { if(currentId > 0) - { - currentId--; - pointerTexture.rect.y = 150 + 20*currentId; - } + --currentId; else if(installListOffset) - { - installListOffset--; - } + --installListOffset; } else if(controller.GetButtonStatus(DOWN)) { - if(currentId < displayCount-1 && installListOffset + currentId+1 < installedMods.size()) - { - currentId++; - pointerTexture.rect.y = 150 + 20*currentId; - } - else if(installListOffset + displayCount < installedMods.size()) - { - installListOffset++; - } + if(currentId < displayCount-1 && installListOffset+currentId+1 < (int)installedMods.size()) + ++currentId; + else if(installListOffset+displayCount < (int)installedMods.size()) + ++installListOffset; } else if(controller.GetButtonStatus(A)) { - if(currentId != -1 && installedMods.size()) + if(currentId != -1 && !installedMods.empty()) { - installedMods[installListOffset+currentId].texture.rect.x = 890; uninstallList.push_back(installedMods[installListOffset+currentId]); installedMods.erase(installedMods.begin()+installListOffset+currentId); - if(installListOffset && (installListOffset + displayCount) > installedMods.size()) - { + if(installListOffset && installListOffset+displayCount > (int)installedMods.size()) --installListOffset; - } - if(installListOffset==0 && currentId >= installedMods.size()) - { - currentId = installedMods.size()-1; - pointerTexture.rect.y = 150 + 20*currentId; - } + if(installListOffset == 0 && currentId >= (int)installedMods.size()) + currentId = static_cast(installedMods.size()) - 1; // -1 (empty) is a valid sentinel here } } else if(controller.GetButtonStatus(B)) { - if(uninstallList.size()) + if(!uninstallList.empty()) { - uninstallList.back().texture.rect.x = 60; installedMods.push_back(uninstallList.back()); uninstallList.pop_back(); if(currentId == -1) - { currentId = 0; - pointerTexture.rect.y = 150; - } } } else if(controller.GetButtonStatus(START)) { - if(uninstallList.size() == 0) + if(uninstallList.empty() || ConfirmUninstall()) break; - Texture warning("Are you sure you want to remove these modules?", 24, renderer, 640, 248, true); - Texture buttons("Yes - Start+Select No - B", 24, renderer, 640, 300, true); - for(;;) - { - controller.Update(); - if(controller.GetButtonStatus(B) || powerWatch.buttonPress()) - { - uninstallList.clear(); - break; - } - else if(controller.PeekButtonStatus(START) && controller.PeekButtonStatus(SELECT)) - { - break; - } - SDL_RenderClear(renderer); - warning.Draw(renderer); - buttons.Draw(renderer); - SDL_RenderPresent(renderer); - std::this_thread::sleep_until(nextFrameTime); - nextFrameTime += fpsTime; - } - if(uninstallList.size()) - { - std::ofstream out("/tmp/uninstall"); - if(out.is_open()) - { - for(auto & mod : uninstallList) - { - out << mod.name << " "; - } - out.close(); - system(("/bin/sh " + folderLocation + "/FinishUninstall.sh &").c_str()); - } - } - break; } - SDL_RenderClear(renderer); - titleText.Draw(renderer); - modListText.Draw(renderer); - modUninstallText.Draw(renderer); - pointerTexture.Draw(renderer); - instructionsText.Draw(renderer); - int y = 150; - for(int i = installListOffset, len = i+std::min(displayCount,(int)installedMods.size()-i); i < len; ++i) - { - installedMods[i].texture.rect.y = y; - installedMods[i].texture.Draw(renderer); - y+=20; - } - y = 150; - for(auto & mod : uninstallList) - { - mod.texture.rect.y = y; - mod.texture.Draw(renderer); - y+=20; - } - - SDL_RenderPresent(renderer); - - //Simple FPS limiter - std::this_thread::sleep_until(nextFrameTime); - nextFrameTime += fpsTime; + render(); } - SDL_DestroyRenderer(renderer); - SDL_DestroyWindow(window); - SDL_Quit(); return 0; } From d30df3befde78b7801b7e47adf4dd96c0eaf46e7 Mon Sep 17 00:00:00 2001 From: DefKorns Date: Wed, 16 Sep 2026 14:23:13 +0000 Subject: [PATCH 020/104] Add PREVIEW_NEAREST for nearest-neighbor preview scaling Small pixel-art sprites (om_theme-selector's DIY character previews) blur under the default linear filter when scaled up a lot to fit the preview box. Opt-in per command file, same pattern as the other PREVIEW_IMAGE_* fields. --- src/command.cpp | 2 ++ src/command.h | 1 + 2 files changed, 3 insertions(+) diff --git a/src/command.cpp b/src/command.cpp index 7f6f9ef..aacd11e 100644 --- a/src/command.cpp +++ b/src/command.cpp @@ -79,6 +79,8 @@ Command::Command(std::ifstream & in) previewImageWidth = SafeStoi(value, -1); else if(param.compare("PREVIEW_IMAGE_HEIGHT")==0) previewImageHeight = SafeStoi(value, -1); + else if(param.compare("PREVIEW_NEAREST")==0) + previewNearest = (value == "TRUE"); } hasSubmenu = !isToggle && command.find("--commandPath") != std::string::npos; in.close(); diff --git a/src/command.h b/src/command.h index a92eb49..fea4353 100644 --- a/src/command.h +++ b/src/command.h @@ -49,6 +49,7 @@ struct Command int previewImageY = 300; int previewImageWidth = -1; int previewImageHeight = -1; + bool previewNearest = false; // nearest-neighbor scaling instead of linear - for small pixel-art sprites blown up a lot, where linear just blurs them }; #endif From 798db78e0095b1ace3a62cb26ec7ea4bd52e34cd Mon Sep 17 00:00:00 2001 From: DefKorns Date: Wed, 16 Sep 2026 15:53:39 +0000 Subject: [PATCH 021/104] Update switch_on and switch_off images for improved UI --- mod/etc/options_menu/images/ui/switch_off.png | Bin 804 -> 1788 bytes mod/etc/options_menu/images/ui/switch_on.png | Bin 732 -> 1790 bytes 2 files changed, 0 insertions(+), 0 deletions(-) diff --git a/mod/etc/options_menu/images/ui/switch_off.png b/mod/etc/options_menu/images/ui/switch_off.png index c3a5708d61b087b6174da759ce831d7a9a5fbf42..61ba4b5a14131a5bd835642414fcab5a39563d56 100644 GIT binary patch literal 1788 zcmbVNe{2&~9KUX4o7*a=f1t3C^JruU*SlWZt-Z9+u>vbux4Bh*h{|qvZ|%AE?sj*h z-3X2ihA=jMNDM|`Ocz|-Uw{_W7_)$;yCl?vnE$>>iOyRwxLP^MuxP$Xk1! zM370Nyl=U>+*?YsV#vsFVhu1xLJ~wHh+}?4V%R#MqBX$J3r=kO*l`TyIVZNv>LtBW zF$nM_O){uzD)X^Tb*!Dk=FdYN5gHPNfXbkeP*70lh!czR(r~POCNMM(QR|#oktT>P z_g0|Aq6|>G(S)-kX+jI($WlPr?Dn}RMN(#hEFdT=PLVWeqODdm@nBF+=4$Cm_o9Rt zoH?<8s!B9Lgu`KD*lZMKKVh=l?F2~?6oo?st~3fN6Tt;#W|F}T6jtUXl@|q6V`OT? z2Gxl{Qzv`~Nhw-ENyG_8jEFE2VKS1MPjMi}rf^b&9E^)|ECGTb1O!!qSW^ls1w>U; z0^&bVr*QOasGqxs8tffK{Z?0YroW|}f3QShX2jLQ5Qj$`RsQ4TTCLq5d#0U-%~3{Az2E*68L4CSCX z=D4=kOM3)GWds&@+)fNSY2U&-ckU&fY9e=cw2)}c=h6WuF4BZi!p?jkK$ zoTE#7#BJz82kZ(r|=CwBxgr(|wy*>>W!_dAK$d>69Db&~4*aY?6s{|U)3ntMRE z)77)UxBs$k?8E^ut+_ufCu+HHi1&7!8YVAX>Kyo8H|6B_SFUZ~u21dS^}ODhu@S#` zca=V>EB#<-Xz;_^>#GMpx?vdmj68m4@9y0zzc{UrS>BDMeY_(uOX}mj!#B_U)E*tN zZAIsq)?aOWw#D8ZqdLFcmUDZ?mh|DTu3h!lBQ5L7f_HW<$y;~mO8%mZM|HVB|MI!x zVAnS^Wtd7;WM;LsTFbNB(q1Vk?}?r2AJ`FG`l}}+BDaHXBpMy8>oRQUcl%zSb98&l z8YFY^ENgjR?~$!}pSI;**2!ml(?%XUUsg};GF0)W3S`%Um-Ex`i?;M3$9Yz`HGOFB zdmYLoB`4N5?pk-Swdc*g@~jiaC(pt%_rQYc*1rHO-g6oN delta 782 zcmV+p1M&R)4WtH;BYy(xNklF|9LK-E_hx2iwM{Trp;NITL5B!SPZ5FP zf+Q@WhYsGocqszu&?$n*iy%QaFWth6e^xUh0_h^rp_3gMhYPmYtgdw5u3OGQ#j;d!1hUTbSBgdpti7f~+zNTn_yW{YaA zwIU+vI0+mcR)4-UH_Q75kZ;*+?sYQhKK6Y-ARzxonqEOhx z!orfdY}+>0jM?oB8oI7)m&^V{K0ot>`uYZMSQfqk5L#p}YIGf#W`oH-CoUi{FDFL^k^XBO{McEdDjI z6SENOVJ2o1MCgs8h^a+w#_;f9^;fhu0M$$*<5J6#26SMcAMNdJCV^_n6dNIts4}5f z03xk5^AWMKwzf;??7U_sGEjq$&9kNDA~Kl_e7_PqAgf^}W;;Q^P>sMEiDkv^-V-7K z^_z%+wSVS1x3~8PTU*=4jg1nZ*2k7B6gEvZpHre}W)oUtSxU)-iY@2+K4xbZ>IM0@ za&CrUXvS7nzG8j-7hE^~OlFoU6PATi;zbli!4Z&7rBaxmUo@qJt;6wE%U}*Q?ps^? ziK(f#NG6+1f6StfSOR7fi;M4nM3I_w97l4IWPg5!;5s)w{SLdkyY&n{I4EOj=_AI+ zpQBhT!nUn*Db&ouOw87Bs{_EBttgdBluoCS$y|k$0)@ghHa0fkx~{RLiqCpyZgr;| z7OF1gn5$HHe+C@KhT}N(!i|{Z%)>%$&S}T3qXp{6t+-wKzvI?FhL5))=}wfR00000 MNe4wvM6N<$f-(GbL;wH) diff --git a/mod/etc/options_menu/images/ui/switch_on.png b/mod/etc/options_menu/images/ui/switch_on.png index 4330afaf74ec1398970ba79999703d7012701616..22d7c0d6050b9525fb07cbd5a4059bf187104d27 100644 GIT binary patch literal 1790 zcmbVNe@qis96y9Xu@O;%a}KiOG>DG&deS)hD{bQzwq;)O?!9;K z^FE*NkN17oS+(fJDU)YSMi695rQ1~v`vTZ9b*b>p4LUxBUAp96rXYwuOKVBU_O6)- zl5&~%)~R)#S7=rY85vG&1ja~6f@lOOE{#YGy9%giBk=Qr1H0YZhoL;@z-p}?lSe8C z0p8szgPPVwUbb}=OL16f30fSXAwdYJ3>paq1%-|{uqZDL$J%EcL!%INl>>8Xf@qzm z3N069fKo=nz?w`1S_DTH8)>JgLXT2C z1B0fH`w)^6w1N_g6O0%hVI-U|nlzuHK#on|q$W8S73WwS1VIQ0ssgb@0xJbXRa64v zKTs!@{~`cm>+vKsCbT6KN=PVbMKg3G7LWX-g4pE?Pd0@`xPo zYncdTHPZwYN5PZAF)H(KIF6-#q8wtNhkS_f16&gP7@CM1T`mSi8OlL(%u#KRhprS9 zl@VA_>2hGuNh8m5G{M?@tkp*vNDDz4d^QVVV0?C)fgmjuWj9%Tq@^*w-zBn5S}NoF zx&OI;vCP9-VuDZ1qg9x8252|0!0fcdN}vX;j*Wsm8cjRRu-b8UV62u$z+th^{1ZoD zoHiT)kaXhwjw2M&r-m6BIQ=kC|9Nz9Xg99a+_($)-@S{jJyxG_I4pW?n7D4>BynK@ z0p3zFypBG;{CX*ZBz;rqa(dmJ0~Z$K$_N*8=H}vMNAM%eNoyl11u*c#Y z8EJp7zr&rQt9MPEJ}qZXy_0WWY0q<1s?YW|4>Gmi+4juJE#7(ZXYu;OIgkAL4OeW) z_t!r<=be56;di9z22;21s@ne71Q;cc5j*ft1tNW)n{_Vu9W_gExnYVQMRe*_a5D{x64!R+ZNwly8lq$o#5W>qxRp{ zn*XTo8M{y1TQh*|FX-LBSqE+gsv q{_F<#!YyqthsT&M!@iNH*QCuKd?TwgqhqJ`hpMbtqHQ1%lwcQ-5J+h! zrmzrE5G=(YNa`36MX(V>L@ceuh*$(dG+?D-A(&XG5p04aIW?Eu?aq_8vqgx9HhXz+ zJF~O%=eysV_htk@6%j$R`lFF!L~<3?VNwp_6$GJBlf2AUhJT3fh|H1d4LLjeek+q$ zCF^_dc!^5)6p>sak}eva46aZv7fRnGlf#uln|LTYNOhf5?^3T_hJ!on^3qN~L zo6vU>1}{LP22ms#i!rHAm|~+eyv0nug9|oMIC>_3F@F(k%bN}o{yR>23W;wKB7=Evc)P@*x#QwwbCG8wzCKJ^+B>>fn+M0KdbFgZL2v4E+mTonx&oX zO(@Ga^Fv6~#hHc$>nl+EmhZn-OXO{)+}hU=%~_y`8k+&mP7wFtD;XW3B$tEkV+!Ub&{)Tde|`+!;b}c zeH-E~@ZQuTV-lHka&GnmiNC|1as-+b4ryj?o1cQW_wB^Wk2eR+b@=ogUR;LF8b}-x zwXr1rj Date: Wed, 16 Sep 2026 18:53:13 +0000 Subject: [PATCH 022/104] Add previewSquare option for square/near-square preview art --- src/command.cpp | 2 ++ src/command.h | 1 + 2 files changed, 3 insertions(+) diff --git a/src/command.cpp b/src/command.cpp index aacd11e..da3ecf8 100644 --- a/src/command.cpp +++ b/src/command.cpp @@ -81,6 +81,8 @@ Command::Command(std::ifstream & in) previewImageHeight = SafeStoi(value, -1); else if(param.compare("PREVIEW_NEAREST")==0) previewNearest = (value == "TRUE"); + else if(param.compare("PREVIEW_SQUARE")==0) + previewSquare = (value == "TRUE"); } hasSubmenu = !isToggle && command.find("--commandPath") != std::string::npos; in.close(); diff --git a/src/command.h b/src/command.h index fea4353..5fe36bf 100644 --- a/src/command.h +++ b/src/command.h @@ -50,6 +50,7 @@ struct Command int previewImageWidth = -1; int previewImageHeight = -1; bool previewNearest = false; // nearest-neighbor scaling instead of linear - for small pixel-art sprites blown up a lot, where linear just blurs them + bool previewSquare = false; // this screen's preview art is square/near-square - grid tiles should be too, not the default wide tile }; #endif From 432526fd74a54daa4e915054c8dbf946ed51aff3 Mon Sep 17 00:00:00 2001 From: DefKorns Date: Wed, 16 Sep 2026 23:55:52 +0000 Subject: [PATCH 023/104] Implement keyboard fallback for desktop testing in Controller class --- src/framework/controller.cpp | 29 +++++++++++++++++++++++++++++ src/framework/controller.h | 3 +++ 2 files changed, 32 insertions(+) diff --git a/src/framework/controller.cpp b/src/framework/controller.cpp index f4c7db2..77570dd 100644 --- a/src/framework/controller.cpp +++ b/src/framework/controller.cpp @@ -15,6 +15,9 @@ #include #include #include +#ifndef __arm__ +#include +#endif Controller::Controller(int id) { @@ -76,6 +79,32 @@ void Controller::Update() } } } +#else + // keyboard fallback for desktop testing builds - no real controller device. + // only write buttons[] on an actual press/release edge (like the real + // evdev stream does), not every frame - so a still-held key doesn't keep + // re-arming a just-consumed GetButtonStatus() tap, while PeekButtonStatus + // still correctly reports "still held" via the unchanged value in between + const Uint8 * keys = SDL_GetKeyboardState(nullptr); + auto press = [&](SDL_Scancode sc, GameButton button) + { + bool now = keys[sc] != 0; + if(now != (prevKeys[sc] != 0)) + buttons[button] = now; + prevKeys[sc] = now; + }; + press(SDL_SCANCODE_UP, UP); + press(SDL_SCANCODE_DOWN, DOWN); + press(SDL_SCANCODE_LEFT, LEFT); + press(SDL_SCANCODE_RIGHT, RIGHT); + press(SDL_SCANCODE_RETURN, A); + press(SDL_SCANCODE_Z, A); + press(SDL_SCANCODE_BACKSPACE, B); + press(SDL_SCANCODE_X, B); + press(SDL_SCANCODE_SPACE, START); + press(SDL_SCANCODE_TAB, SELECT); + press(SDL_SCANCODE_Q, L); + press(SDL_SCANCODE_E, R); #endif // __arm__ } void Controller::Reset() diff --git a/src/framework/controller.h b/src/framework/controller.h index 4b5089a..6d9125c 100644 --- a/src/framework/controller.h +++ b/src/framework/controller.h @@ -27,6 +27,9 @@ class Controller int fd = -1; ButtonEvent buttonBuffer[10]; std::map buttons; +#ifndef __arm__ + unsigned char prevKeys[512] = {0}; // keyboard fallback for desktop testing builds +#endif public: Controller(int id); ~Controller(); From bca82b5e5b328a83e32a89ae0cb6000699b6b5f4 Mon Sep 17 00:00:00 2001 From: DefKorns Date: Thu, 17 Sep 2026 00:06:56 +0000 Subject: [PATCH 024/104] Refine keyboard fallback for desktop builds in Controller::Update --- src/framework/controller.cpp | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/framework/controller.cpp b/src/framework/controller.cpp index 77570dd..e971129 100644 --- a/src/framework/controller.cpp +++ b/src/framework/controller.cpp @@ -80,11 +80,9 @@ void Controller::Update() } } #else - // keyboard fallback for desktop testing builds - no real controller device. - // only write buttons[] on an actual press/release edge (like the real - // evdev stream does), not every frame - so a still-held key doesn't keep - // re-arming a just-consumed GetButtonStatus() tap, while PeekButtonStatus - // still correctly reports "still held" via the unchanged value in between + // keyboard fallback for desktop builds - only write buttons[] on an edge, + // matching evdev's own press/release-only stream + SDL_PumpEvents(); // nested loops (confirm dialog) don't otherwise pump const Uint8 * keys = SDL_GetKeyboardState(nullptr); auto press = [&](SDL_Scancode sc, GameButton button) { From 3457763f7c7b1064723fe22297924561c929d8ce Mon Sep 17 00:00:00 2001 From: DefKorns Date: Thu, 17 Sep 2026 00:10:45 +0000 Subject: [PATCH 025/104] Implement hold-to-repeat functionality for direction buttons in main loop --- src/main.cpp | 32 ++++++++++++++++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) diff --git a/src/main.cpp b/src/main.cpp index 665267b..475953e 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -229,6 +229,34 @@ int main(int argc, char * argv[]) auto renderer = sdl_context.renderer; Controller controller(1); + // hold-to-repeat for direction buttons + std::map repeatPressedAt, repeatLastFired; + std::map repeatActive; + const Uint32 repeatDelay = 350; + const Uint32 repeatInterval = 90; + auto HeldRepeat = [&](GameButton button) -> bool + { + if(!controller.PeekButtonStatus(button)) + { + repeatActive[button] = false; + return false; + } + Uint32 now = SDL_GetTicks(); + if(!repeatActive[button]) + { + repeatActive[button] = true; + repeatPressedAt[button] = now; + repeatLastFired[button] = now; + return true; + } + if(now - repeatPressedAt[button] >= repeatDelay && now - repeatLastFired[button] >= repeatInterval) + { + repeatLastFired[button] = now; + return true; + } + return false; + }; + //Create Options Flag to prevent multiple menu launches system("touch /tmp/options.flag"); @@ -533,7 +561,7 @@ int main(int argc, char * argv[]) break; } } - else if(controller.GetButtonStatus(UP)) + else if(HeldRepeat(UP)) { // bounded so an all-headers list can't spin forever int newCommandId = currentCommandId; @@ -545,7 +573,7 @@ int main(int argc, char * argv[]) } SetCurrentCommand(newCommandId); } - else if(controller.GetButtonStatus(DOWN)) + else if(HeldRepeat(DOWN)) { int newCommandId = currentCommandId; for(size_t tries = 0; tries < commands.size(); ++tries) From 9fc89262639ccc5e8eddabf6a3b2677a467a08ca Mon Sep 17 00:00:00 2001 From: DefKorns Date: Thu, 17 Sep 2026 18:35:25 +0000 Subject: [PATCH 026/104] Move hold-to-repeat into Controller, add DrawRoundedCornerMask helper --- src/framework/controller.cpp | 48 ++++++++++++++++++++++++++++++ src/framework/controller.h | 8 +++++ src/framework/draw_helpers.cpp | 36 +++++++++++++++++++++++ src/framework/draw_helpers.h | 4 +++ src/main.cpp | 54 ++++++++++------------------------ 5 files changed, 111 insertions(+), 39 deletions(-) diff --git a/src/framework/controller.cpp b/src/framework/controller.cpp index e971129..b09c298 100644 --- a/src/framework/controller.cpp +++ b/src/framework/controller.cpp @@ -15,10 +15,20 @@ #include #include #include +#include #ifndef __arm__ #include #endif +// avoids linking SDL just for a millisecond timer - optiond (Controller's +// other caller) doesn't otherwise need it +static unsigned int MonotonicMillis() +{ + struct timespec ts; + clock_gettime(CLOCK_MONOTONIC, &ts); + return static_cast(ts.tv_sec)*1000 + ts.tv_nsec/1000000; +} + Controller::Controller(int id) { #ifdef __arm__ @@ -105,6 +115,44 @@ void Controller::Update() press(SDL_SCANCODE_E, R); #endif // __arm__ } +bool Controller::HeldRepeat(GameButton button) +{ + const unsigned int repeatDelay = 350, repeatInterval = 90; + if(!PeekButtonStatus(button)) + { + repeatActive[button] = false; + return false; + } + unsigned int now = MonotonicMillis(); + if(!repeatActive[button]) + { + repeatActive[button] = true; + repeatPressedAt[button] = now; + repeatLastFired[button] = now; + return true; + } + if(now - repeatPressedAt[button] >= repeatDelay && now - repeatLastFired[button] >= repeatInterval) + { + repeatLastFired[button] = now; + return true; + } + return false; +} +unsigned int Controller::HeldMillis(GameButton button) +{ + if(!PeekButtonStatus(button)) + { + heldActive[button] = false; + return 0; + } + unsigned int now = MonotonicMillis(); + if(!heldActive[button]) + { + heldActive[button] = true; + heldPressedAt[button] = now; + } + return now - heldPressedAt[button]; +} void Controller::Reset() { buttons[A] = 0; diff --git a/src/framework/controller.h b/src/framework/controller.h index 6d9125c..ee91350 100644 --- a/src/framework/controller.h +++ b/src/framework/controller.h @@ -27,6 +27,10 @@ class Controller int fd = -1; ButtonEvent buttonBuffer[10]; std::map buttons; + std::map repeatPressedAt, repeatLastFired; + std::map repeatActive; + std::map heldPressedAt; + std::map heldActive; #ifndef __arm__ unsigned char prevKeys[512] = {0}; // keyboard fallback for desktop testing builds #endif @@ -35,6 +39,10 @@ class Controller ~Controller(); bool PeekButtonStatus(GameButton button); bool GetButtonStatus(GameButton button); + // fires once on press, then repeats after an initial delay while held + bool HeldRepeat(GameButton button); + // ms this button has been continuously held, 0 if not currently held + unsigned int HeldMillis(GameButton button); void Update(); void Reset(); }; diff --git a/src/framework/draw_helpers.cpp b/src/framework/draw_helpers.cpp index 3ba74c1..69129d8 100644 --- a/src/framework/draw_helpers.cpp +++ b/src/framework/draw_helpers.cpp @@ -113,6 +113,42 @@ void DrawRoundedFillRect(SDL_Renderer * r, SDL_Rect rect, Uint8 cr, Uint8 cg, Ui SDL_RenderDrawPoints(r, points.data(), static_cast(points.size())); } +// complement of RoundedCornerDisk - the corner-square pixels a rounded rect +// of this radius would NOT cover, i.e. what needs painting over to fake +// rounding a rectangularly-clipped image +static const std::vector & RoundedCornerOutside(int radius) +{ + static std::map> cache; + auto it = cache.find(radius); + if(it != cache.end()) + return it->second; + std::vector pts; + for(int dy = 0; dy <= radius; ++dy) + for(int dx = 0; dx <= radius; ++dx) + if(std::sqrt(static_cast(dx*dx + dy*dy)) > radius + 0.5) + pts.push_back({dx, dy}); + return cache.emplace(radius, std::move(pts)).first->second; +} + +void DrawRoundedCornerMask(SDL_Renderer * r, SDL_Rect rect, Uint8 cr, Uint8 cg, Uint8 cb, int radius) +{ + if(radius <= 0) + return; + SDL_SetRenderDrawColor(r, cr, cg, cb, 0xFF); + int x0 = rect.x, y0 = rect.y, x1 = rect.x+rect.w-1, y1 = rect.y+rect.h-1; + const std::vector & outside = RoundedCornerOutside(radius); + std::vector points; + points.reserve(outside.size() * 4); + for(const SDL_Point & p : outside) + { + points.push_back({ x0+radius-p.x, y0+radius-p.y }); + points.push_back({ x1-radius+p.x, y0+radius-p.y }); + points.push_back({ x0+radius-p.x, y1-radius+p.y }); + points.push_back({ x1-radius+p.x, y1-radius+p.y }); + } + SDL_RenderDrawPoints(r, points.data(), static_cast(points.size())); +} + void DrawHLine(SDL_Renderer * r, int x0, int x1, int y, Uint8 cr, Uint8 cg, Uint8 cb, int width) { DrawFillRect(r, { x0, y, x1-x0, width }, cr, cg, cb); diff --git a/src/framework/draw_helpers.h b/src/framework/draw_helpers.h index cedd589..1fd69b7 100644 --- a/src/framework/draw_helpers.h +++ b/src/framework/draw_helpers.h @@ -24,6 +24,10 @@ void DrawStrokeRect(SDL_Renderer * r, SDL_Rect rect, Uint8 cr, Uint8 cg, Uint8 cb, int width, int radius = 0); void DrawFillRect(SDL_Renderer * r, SDL_Rect rect, Uint8 cr, Uint8 cg, Uint8 cb); void DrawRoundedFillRect(SDL_Renderer * r, SDL_Rect rect, Uint8 cr, Uint8 cg, Uint8 cb, int radius); +// paints just the 4 corner-square regions outside the rounded radius with +// a flat color - draw over a rectangularly-clipped image to fake rounding +// it to match a rounded container, without an actual alpha mask/render target +void DrawRoundedCornerMask(SDL_Renderer * r, SDL_Rect rect, Uint8 cr, Uint8 cg, Uint8 cb, int radius); void DrawHLine(SDL_Renderer * r, int x0, int x1, int y, Uint8 cr, Uint8 cg, Uint8 cb, int width = 1); void DrawVLine(SDL_Renderer * r, int x, int y0, int y1, Uint8 cr, Uint8 cg, Uint8 cb, int width = 1); diff --git a/src/main.cpp b/src/main.cpp index 475953e..2a29fb7 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -229,34 +229,6 @@ int main(int argc, char * argv[]) auto renderer = sdl_context.renderer; Controller controller(1); - // hold-to-repeat for direction buttons - std::map repeatPressedAt, repeatLastFired; - std::map repeatActive; - const Uint32 repeatDelay = 350; - const Uint32 repeatInterval = 90; - auto HeldRepeat = [&](GameButton button) -> bool - { - if(!controller.PeekButtonStatus(button)) - { - repeatActive[button] = false; - return false; - } - Uint32 now = SDL_GetTicks(); - if(!repeatActive[button]) - { - repeatActive[button] = true; - repeatPressedAt[button] = now; - repeatLastFired[button] = now; - return true; - } - if(now - repeatPressedAt[button] >= repeatDelay && now - repeatLastFired[button] >= repeatInterval) - { - repeatLastFired[button] = now; - return true; - } - return false; - }; - //Create Options Flag to prevent multiple menu launches system("touch /tmp/options.flag"); @@ -524,9 +496,8 @@ int main(int argc, char * argv[]) return true; }; - // ~1s at this loop's ~33ms frame time, same tick-count style as daemon.cpp's L+R hold - const short bHoldThreshold = 30; - short bHoldCount = 0; + const unsigned int bHoldThresholdMs = 1000; + bool bWasHeld = false, bHoldFired = false; for(;;) { @@ -561,7 +532,7 @@ int main(int argc, char * argv[]) break; } } - else if(HeldRepeat(UP)) + else if(controller.HeldRepeat(UP)) { // bounded so an all-headers list can't spin forever int newCommandId = currentCommandId; @@ -573,7 +544,7 @@ int main(int argc, char * argv[]) } SetCurrentCommand(newCommandId); } - else if(HeldRepeat(DOWN)) + else if(controller.HeldRepeat(DOWN)) { int newCommandId = currentCommandId; for(size_t tries = 0; tries < commands.size(); ++tries) @@ -586,18 +557,23 @@ int main(int argc, char * argv[]) } // tap = jump to last item, hold ~1s = delete - if(controller.PeekButtonStatus(B)) + bool bHeldNow = controller.PeekButtonStatus(B); + if(bHeldNow) { - ++bHoldCount; - if(bHoldCount == bHoldThreshold && !commands[currentCommandId].deleteCommand.empty() && ConfirmDelete()) - break; + if(!bHoldFired && controller.HeldMillis(B) >= bHoldThresholdMs) + { + bHoldFired = true; + if(!commands[currentCommandId].deleteCommand.empty() && ConfirmDelete()) + break; + } } else { - if(bHoldCount > 0 && bHoldCount < bHoldThreshold) + if(bWasHeld && !bHoldFired) SetCurrentCommand(commands.size()-1); - bHoldCount = 0; + bHoldFired = false; } + bWasHeld = bHeldNow; //Draw all textures DrawChrome(); From 2c0247f3f5d1bef7bab5e30cb2c33c70d46a3d01 Mon Sep 17 00:00:00 2001 From: DefKorns Date: Thu, 17 Sep 2026 19:28:24 +0000 Subject: [PATCH 027/104] Add SUBMENU support to various command configurations --- mod/etc/options_menu/commands/c0060_RetroarchOptions | 1 + mod/etc/options_menu/commands/c0070_NetworkOptions | 1 + mod/etc/options_menu/commands/c0080_AdvancedOptions | 1 + mod/etc/options_menu/network/commands/c0050_RestoreNand | 1 + mod/etc/options_menu/network/commands/c0060_RestoreUsb | 1 + src/command.cpp | 5 ++++- src/command.h | 2 +- 7 files changed, 10 insertions(+), 2 deletions(-) diff --git a/mod/etc/options_menu/commands/c0060_RetroarchOptions b/mod/etc/options_menu/commands/c0060_RetroarchOptions index 1627765..b361e00 100644 --- a/mod/etc/options_menu/commands/c0060_RetroarchOptions +++ b/mod/etc/options_menu/commands/c0060_RetroarchOptions @@ -1,5 +1,6 @@ COMMAND_NAME=RETROARCH_OPTIONS COMMAND_TYPE=EXTERNAL RESTART_UI=FALSE +SUBMENU=TRUE COMMAND_STR=sh %script_dir%/om_retroarch PREVIEW_IMAGE=/etc/options_menu/images/preview_retroarch.png diff --git a/mod/etc/options_menu/commands/c0070_NetworkOptions b/mod/etc/options_menu/commands/c0070_NetworkOptions index 8a7de8e..def5f61 100644 --- a/mod/etc/options_menu/commands/c0070_NetworkOptions +++ b/mod/etc/options_menu/commands/c0070_NetworkOptions @@ -1,5 +1,6 @@ COMMAND_NAME=NETWORK_OPTIONS COMMAND_TYPE=EXTERNAL RESTART_UI=FALSE +SUBMENU=TRUE COMMAND_STR=sh %script_dir%/om_network PREVIEW_IMAGE=/etc/options_menu/images/preview_network.png diff --git a/mod/etc/options_menu/commands/c0080_AdvancedOptions b/mod/etc/options_menu/commands/c0080_AdvancedOptions index 2b87622..ab6bd68 100644 --- a/mod/etc/options_menu/commands/c0080_AdvancedOptions +++ b/mod/etc/options_menu/commands/c0080_AdvancedOptions @@ -1,5 +1,6 @@ COMMAND_NAME=ADVANCED_OPTIONS COMMAND_TYPE=EXTERNAL RESTART_UI=FALSE +SUBMENU=TRUE COMMAND_STR=usleep 50000 && %options_path%/options --commandPath %options_path%/advanced_commands/ --title "ADVANCED_OPTIONS" & PREVIEW_IMAGE=/etc/options_menu/images/preview_advanced.png diff --git a/mod/etc/options_menu/network/commands/c0050_RestoreNand b/mod/etc/options_menu/network/commands/c0050_RestoreNand index 675aaf0..1a99f61 100644 --- a/mod/etc/options_menu/network/commands/c0050_RestoreNand +++ b/mod/etc/options_menu/network/commands/c0050_RestoreNand @@ -1,4 +1,5 @@ COMMAND_NAME=WIFI_RESTORE_NAND COMMAND_TYPE=EXTERNAL +SUBMENU=TRUE COMMAND_STR=sh %script_dir%/restore_settings.sh nand cmd PREVIEW_IMAGE=/etc/options_menu/images/preview_placeholder.png diff --git a/mod/etc/options_menu/network/commands/c0060_RestoreUsb b/mod/etc/options_menu/network/commands/c0060_RestoreUsb index 280f89a..645c8e8 100644 --- a/mod/etc/options_menu/network/commands/c0060_RestoreUsb +++ b/mod/etc/options_menu/network/commands/c0060_RestoreUsb @@ -1,5 +1,6 @@ COMMAND_NAME=WIFI_RESTORE_USB COMMAND_TYPE=EXTERNAL USB_ONLY=TRUE +SUBMENU=TRUE COMMAND_STR=sh %script_dir%/restore_settings.sh usb PREVIEW_IMAGE=/etc/options_menu/images/preview_placeholder.png diff --git a/src/command.cpp b/src/command.cpp index da3ecf8..435b014 100644 --- a/src/command.cpp +++ b/src/command.cpp @@ -83,8 +83,11 @@ Command::Command(std::ifstream & in) previewNearest = (value == "TRUE"); else if(param.compare("PREVIEW_SQUARE")==0) previewSquare = (value == "TRUE"); + else if(param.compare("SUBMENU")==0) + hasSubmenu = (value == "TRUE"); } - hasSubmenu = !isToggle && command.find("--commandPath") != std::string::npos; + if(isToggle) + hasSubmenu = false; in.close(); } diff --git a/src/command.h b/src/command.h index 5fe36bf..df1ef3a 100644 --- a/src/command.h +++ b/src/command.h @@ -42,7 +42,7 @@ struct Command bool child = false; bool isToggle = false; bool stateOn = false; - bool hasSubmenu = false; // COMMAND_STR relaunches options with a different --commandPath + bool hasSubmenu = false; Texture texture; std::string previewImage; int previewImageX = 920; // default position when a command sets PREVIEW_IMAGE without _X/_Y From 80e05bd25ebec492d8176abbb57bb0b64026a16b Mon Sep 17 00:00:00 2001 From: DefKorns Date: Fri, 18 Sep 2026 00:14:07 +0000 Subject: [PATCH 028/104] Add PREVIEW_GRID_COLS command field for grid column overrides --- src/command.cpp | 2 ++ src/command.h | 1 + 2 files changed, 3 insertions(+) diff --git a/src/command.cpp b/src/command.cpp index 435b014..f43382f 100644 --- a/src/command.cpp +++ b/src/command.cpp @@ -83,6 +83,8 @@ Command::Command(std::ifstream & in) previewNearest = (value == "TRUE"); else if(param.compare("PREVIEW_SQUARE")==0) previewSquare = (value == "TRUE"); + else if(param.compare("PREVIEW_GRID_COLS")==0) + previewGridCols = SafeStoi(value, -1); else if(param.compare("SUBMENU")==0) hasSubmenu = (value == "TRUE"); } diff --git a/src/command.h b/src/command.h index df1ef3a..d576016 100644 --- a/src/command.h +++ b/src/command.h @@ -51,6 +51,7 @@ struct Command int previewImageHeight = -1; bool previewNearest = false; // nearest-neighbor scaling instead of linear - for small pixel-art sprites blown up a lot, where linear just blurs them bool previewSquare = false; // this screen's preview art is square/near-square - grid tiles should be too, not the default wide tile + int previewGridCols = -1; // explicit grid column count override, unset (-1) defers to the square/wide default }; #endif From 6c3d40e0801189ad967178751d60279340033844 Mon Sep 17 00:00:00 2001 From: DefKorns Date: Fri, 18 Sep 2026 01:15:26 +0000 Subject: [PATCH 029/104] Add theme configuration support and update UI colors --- Makefile.docker | 2 +- mod/etc/options_menu/theme.cfg | 37 +++++++++++ src/command.cpp | 2 +- src/framework/uitheme.cpp | 114 +++++++++++++++++++++++++++++++++ src/framework/uitheme.h | 45 ++++++++----- src/main.cpp | 28 ++++---- src/mod_uninstall.cpp | 23 +++---- 7 files changed, 208 insertions(+), 43 deletions(-) create mode 100644 mod/etc/options_menu/theme.cfg create mode 100644 src/framework/uitheme.cpp diff --git a/Makefile.docker b/Makefile.docker index 714a932..1dc8b08 100644 --- a/Makefile.docker +++ b/Makefile.docker @@ -26,7 +26,7 @@ VENDORED_TTF_LIBS = endif CXXFLAGS = -std=c++11 -Os $(SDL_CFLAGS) -DMOD_VERSION=\"$(MOD_VER)\" LDLIBS = $(SDL_LIBS) -SOURCES = src/main.cpp src/command.cpp src/localization.cpp src/framework/sdl_context.cpp src/framework/texture.cpp src/framework/controller.cpp src/framework/powerwatch.cpp src/framework/draw_helpers.cpp src/framework/utf8.cpp src/framework/font8x8_lookup.cpp +SOURCES = src/main.cpp src/command.cpp src/localization.cpp src/framework/sdl_context.cpp src/framework/texture.cpp src/framework/controller.cpp src/framework/powerwatch.cpp src/framework/draw_helpers.cpp src/framework/utf8.cpp src/framework/font8x8_lookup.cpp src/framework/uitheme.cpp OBJECTS = $(SOURCES:.cpp=.o) DEV_DIR = $(shell realpath .) OUT = $(DEV_DIR)/out diff --git a/mod/etc/options_menu/theme.cfg b/mod/etc/options_menu/theme.cfg new file mode 100644 index 0000000..ffabcdb --- /dev/null +++ b/mod/etc/options_menu/theme.cfg @@ -0,0 +1,37 @@ +# Options Menu / Theme Manager color palette. Each line is "Key=R,G,B" +# (0-255 each). Delete this file (or a line in it) to fall back to the +# built-in default for that color. Read once at startup - restart the app +# to see a change. + +# overall app background, behind everything +Bg=12,15,22 +# divider lines, box outlines, unselected tile border +Border=42,44,51 +# selected row/tile border, section title accent bar, highlights +Accent=240,196,61 +# selected row / grid tile fill - if you change Bg, change this too or the +# rounded tile corners will look like they kept the old background +SelectedRowBg=20,22,26 + +# main text - titles, row labels, badge hints +Text=255,255,255 +# muted secondary text - list chevrons, dimmed labels +TextDim=141,141,147 +# the letter drawn inside a badge circle (A/B/X) +BadgeLetter=28,22,22 + +# A badge (Select) - outer ring, then a darker rim shade +BadgeA=76,175,110 +BadgeADark=46,107,66 +# B badge (Back/Exit) +BadgeB=225,85,84 +BadgeBDark=140,52,51 +# X badge (hold-to-delete hint) +BadgeX=108,142,191 +BadgeXDark=63,89,127 +# Y badge (unused right now, kept for future use) +BadgeY=201,123,132 +BadgeYDark=143,71,80 +# Start pill (wide badge instead of a letter circle) +BadgeStart=194,168,94 +BadgeStartDark=116,101,56 diff --git a/src/command.cpp b/src/command.cpp index f43382f..735377e 100644 --- a/src/command.cpp +++ b/src/command.cpp @@ -107,7 +107,7 @@ void Command::RunCommand(SDL_Context & sdl_context, Controller * controller, con // same app header/footer chrome as the main screen, plus a persistent B/Exit badge Texture exitLetter("B", 16, renderer, 0, 0, false, UiTheme::BadgeLetterColor, true); - Texture exitLabel(Translate("EXIT"), 16, renderer, 0, 0, false, 0xFFFFFFFF, true); + Texture exitLabel(Translate("EXIT"), 16, renderer, 0, 0, false, UiTheme::TextColor, true); int badgeGroupW = UiTheme::BadgeOuterSize + UiTheme::BadgeLabelGap + exitLabel.rect.w; SDL_Rect exitBadge{ UiTheme::BadgeClusterRightX - badgeGroupW, UiTheme::BadgeBandY, UiTheme::BadgeOuterSize, UiTheme::BadgeOuterSize }; exitLetter.rect.x = exitBadge.x + (exitBadge.w - exitLetter.rect.w) / 2; diff --git a/src/framework/uitheme.cpp b/src/framework/uitheme.cpp new file mode 100644 index 0000000..0b532ae --- /dev/null +++ b/src/framework/uitheme.cpp @@ -0,0 +1,114 @@ +/** + * Copyright (C) 2026 DefKorns + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "uitheme.h" + +#include +#include +#include + +namespace UiTheme +{ + Uint8 BgR = 0x0C, BgG = 0x0F, BgB = 0x16; + Uint8 BorderR = 0x2A, BorderG = 0x2C, BorderB = 0x33; + Uint8 AccentR = 0xF0, AccentG = 0xC4, AccentB = 0x3D; // yellow + Uint8 SelectedRowBgR = 0x14, SelectedRowBgG = 0x16, SelectedRowBgB = 0x1A; + + Uint32 BadgeLetterColor = 0xFF16161C; // AABBGGRR + Uint32 TextColor = 0xFFFFFFFF; // AABBGGRR - primary text + Uint32 TextDimColor = 0xFF938D8D; // AABBGGRR - muted grey for chevrons/secondary text + + BadgeColor BadgeA{ 0x4C, 0xAF, 0x6E }; // green + BadgeColor BadgeADark{ 0x2E, 0x6B, 0x42 }; + BadgeColor BadgeB{ 0xE1, 0x55, 0x54 }; // red + BadgeColor BadgeBDark{ 0x8C, 0x34, 0x33 }; + BadgeColor BadgeX{ 0x6C, 0x8E, 0xBF }; // slate-blue + BadgeColor BadgeXDark{ 0x3F, 0x59, 0x7F }; + BadgeColor BadgeY{ 0xC9, 0x7B, 0x84 }; // dusty rose, unused + BadgeColor BadgeYDark{ 0x8F, 0x47, 0x50 }; + BadgeColor BadgeStart{ 0xC2, 0xA8, 0x5E }; // muted gold + BadgeColor BadgeStartDark{ 0x74, 0x65, 0x38 }; + + static bool ParseTriple(const std::string & value, Uint8 & r, Uint8 & g, Uint8 & b) + { + int rv, gv, bv; + if(std::sscanf(value.c_str(), "%d,%d,%d", &rv, &gv, &bv) != 3) + return false; + r = static_cast(rv); + g = static_cast(gv); + b = static_cast(bv); + return true; + } + + static Uint8 ClampedAdd(Uint8 base, int delta) + { + int v = base + delta; + return static_cast(v < 0 ? 0 : (v > 255 ? 255 : v)); + } + + void LoadThemeConfig(const std::string & optionsLocation) + { + std::ifstream in(optionsLocation + "theme.cfg"); + if(!in.is_open()) + return; + + bool bgSet = false, selectedRowBgSet = false; + std::string line; + while(std::getline(in, line)) + { + if(!line.empty() && line.back() == '\r') + line.pop_back(); + size_t eq = line.find('='); + if(eq == std::string::npos || line[0] == '#') + continue; + std::string key = line.substr(0, eq); + std::string value = line.substr(eq + 1); + Uint8 r, g, b; + if(!ParseTriple(value, r, g, b)) + continue; + + if(key == "Bg") { BgR = r; BgG = g; BgB = b; bgSet = true; } + else if(key == "Border") { BorderR = r; BorderG = g; BorderB = b; } + else if(key == "Accent") { AccentR = r; AccentG = g; AccentB = b; } + else if(key == "SelectedRowBg") { SelectedRowBgR = r; SelectedRowBgG = g; SelectedRowBgB = b; selectedRowBgSet = true; } + else if(key == "Text") TextColor = 0xFF000000 | (b << 16) | (g << 8) | r; + else if(key == "TextDim") TextDimColor = 0xFF000000 | (b << 16) | (g << 8) | r; + else if(key == "BadgeLetter") BadgeLetterColor = 0xFF000000 | (b << 16) | (g << 8) | r; + else if(key == "BadgeA") BadgeA = { r, g, b }; + else if(key == "BadgeADark") BadgeADark = { r, g, b }; + else if(key == "BadgeB") BadgeB = { r, g, b }; + else if(key == "BadgeBDark") BadgeBDark = { r, g, b }; + else if(key == "BadgeX") BadgeX = { r, g, b }; + else if(key == "BadgeXDark") BadgeXDark = { r, g, b }; + else if(key == "BadgeY") BadgeY = { r, g, b }; + else if(key == "BadgeYDark") BadgeYDark = { r, g, b }; + else if(key == "BadgeStart") BadgeStart = { r, g, b }; + else if(key == "BadgeStartDark") BadgeStartDark = { r, g, b }; + } + + // SelectedRowBg (tile/row fill - shows at rounded-corner masks, so a + // stale default next to a customized Bg reads as a leftover old + // color) tracks a custom Bg unless set explicitly itself, keeping + // the same +8/+7/+4 offset as the built-in defaults + if(bgSet && !selectedRowBgSet) + { + SelectedRowBgR = ClampedAdd(BgR, 8); + SelectedRowBgG = ClampedAdd(BgG, 7); + SelectedRowBgB = ClampedAdd(BgB, 4); + } + } +} diff --git a/src/framework/uitheme.h b/src/framework/uitheme.h index b4b225f..4446433 100644 --- a/src/framework/uitheme.h +++ b/src/framework/uitheme.h @@ -19,16 +19,24 @@ #define UITHEME_H_ #include +#include // Geometry and palette for the split-panel "settings dialog" look. Screen -// is a fixed 1280x720 (sdl_context.cpp), so these are plain constants. +// is a fixed 1280x720 (sdl_context.cpp), so geometry stays plain constants. // Border/dividers are flat drawn lines (see draw_helpers.h), no textured art. +// Colors are extern, not const: LoadThemeConfig (uitheme.cpp) can override +// them from an optional theme.cfg at startup - defaults live there too. namespace UiTheme { - const Uint8 BgR = 0x0C, BgG = 0x0F, BgB = 0x16; - const Uint8 BorderR = 0x2A, BorderG = 0x2C, BorderB = 0x33; - const Uint8 AccentR = 0xF0, AccentG = 0xC4, AccentB = 0x3D; // yellow - const Uint8 SelectedRowBgR = 0x14, SelectedRowBgG = 0x16, SelectedRowBgB = 0x1A; + // reads /theme.cfg if present ("Key=R,G,B" per line, + // # comments); missing file or bad lines are silently skipped, existing + // defaults stand + void LoadThemeConfig(const std::string & optionsLocation); + + extern Uint8 BgR, BgG, BgB; + extern Uint8 BorderR, BorderG, BorderB; + extern Uint8 AccentR, AccentG, AccentB; + extern Uint8 SelectedRowBgR, SelectedRowBgG, SelectedRowBgB; // TV overscan safe area const int SafeMarginX = 64; @@ -112,20 +120,23 @@ namespace UiTheme // (24px) from the last-drawn badge's own left edge; this closes the gap // to land the divider exactly 40px left of that badge, per hardware test const int BadgeDividerGapFromCluster = 16; - const Uint32 BadgeLetterColor = 0xFF16161C; // AABBGGRR - const Uint32 TextDimColor = 0xFF938D8D; // AABBGGRR - muted grey for chevrons/secondary text + // packed AABBGGRR, kept in sync with the R/G/B triples below by + // LoadThemeConfig - not user-facing keys themselves + extern Uint32 BadgeLetterColor; + extern Uint32 TextColor; // primary text - titles, row labels, badge hints + extern Uint32 TextDimColor; // muted grey for chevrons/secondary text struct BadgeColor { Uint8 r, g, b; }; - const BadgeColor BadgeA{ 0x4C, 0xAF, 0x6E }; // green - const BadgeColor BadgeADark{ 0x2E, 0x6B, 0x42 }; - const BadgeColor BadgeB{ 0xE1, 0x55, 0x54 }; // red - const BadgeColor BadgeBDark{ 0x8C, 0x34, 0x33 }; - const BadgeColor BadgeX{ 0x6C, 0x8E, 0xBF }; // slate-blue - const BadgeColor BadgeXDark{ 0x3F, 0x59, 0x7F }; - const BadgeColor BadgeY{ 0xC9, 0x7B, 0x84 }; // dusty rose, unused - const BadgeColor BadgeYDark{ 0x8F, 0x47, 0x50 }; - const BadgeColor BadgeStart{ 0xC2, 0xA8, 0x5E }; // muted gold, for a wide "Start" pill instead of a letter circle - const BadgeColor BadgeStartDark{ 0x74, 0x65, 0x38 }; + extern BadgeColor BadgeA; // green + extern BadgeColor BadgeADark; + extern BadgeColor BadgeB; // red + extern BadgeColor BadgeBDark; + extern BadgeColor BadgeX; // slate-blue + extern BadgeColor BadgeXDark; + extern BadgeColor BadgeY; // dusty rose, unused + extern BadgeColor BadgeYDark; + extern BadgeColor BadgeStart; // muted gold, for a wide "Start" pill instead of a letter circle + extern BadgeColor BadgeStartDark; // "created by CompCom" footer credit, left-aligned in the footer const int CreditX = FrameX; diff --git a/src/main.cpp b/src/main.cpp index 2a29fb7..a9662a1 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -71,6 +71,7 @@ int main(int argc, char * argv[]) LoadLanguageFromConfig(optionsLocation); SetTTFFontPath(optionsLocation); + UiTheme::LoadThemeConfig(optionsLocation); std::string titleString(Translate("OPTIONS_TITLE")); std::string titleKey = "OPTIONS_TITLE"; @@ -244,24 +245,24 @@ int main(int argc, char * argv[]) Texture badgeInner(optionsLocation + UiTheme::AssetBadgeInner, renderer); //Create Textures for Strings - Texture appTitleText("OptionsMenu", UiTheme::TitleFontSize, renderer, UiTheme::TitleX, UiTheme::TitleY, false, 0xFFFFFFFF, true); + Texture appTitleText("OptionsMenu", UiTheme::TitleFontSize, renderer, UiTheme::TitleX, UiTheme::TitleY, false, UiTheme::TextColor, true); appTitleText.rect.y -= appTitleText.rect.h / 2; // vertically center on the gear (Texture only supports centering both axes together) - Texture appVersionText(MOD_VERSION, UiTheme::VersionFontSize, renderer, appTitleText.rect.x + appTitleText.rect.w + UiTheme::VersionGap, UiTheme::TitleY, false, 0xFFFFFFFF, true); + Texture appVersionText(MOD_VERSION, UiTheme::VersionFontSize, renderer, appTitleText.rect.x + appTitleText.rect.w + UiTheme::VersionGap, UiTheme::TitleY, false, UiTheme::TextColor, true); appVersionText.rect.y -= appVersionText.rect.h / 2; - Texture titleText(titleString, UiTheme::SectionTitleFontSize, renderer, UiTheme::SectionTitleX, UiTheme::SectionTitleY, false, 0xFFFFFFFF, true); + Texture titleText(titleString, UiTheme::SectionTitleFontSize, renderer, UiTheme::SectionTitleX, UiTheme::SectionTitleY, false, UiTheme::TextColor, true); SDL_Rect selectedRowRect{ UiTheme::ListX, UiTheme::RowFirstY - 2, UiTheme::ListContentRightX - UiTheme::ListX, 0 }; - Texture CompComText("created by CompCom - Modern UI by DefKorns", 16, renderer, UiTheme::CreditX, UiTheme::CreditY, false, 0xFFFFFFFF, true); - Texture scrollUp("^", 16, renderer, UiTheme::ScrollX, UiTheme::ScrollUpY, false, 0xFFFFFFFF, true); + Texture CompComText("created by CompCom - Modern UI by DefKorns", 16, renderer, UiTheme::CreditX, UiTheme::CreditY, false, UiTheme::TextColor, true); + Texture scrollUp("^", 16, renderer, UiTheme::ScrollX, UiTheme::ScrollUpY, false, UiTheme::TextColor, true); scrollUp.rect.x -= scrollUp.rect.w / 2; // ScrollX is the gutter's center, not the glyph's left edge Texture scrollDown = scrollUp; scrollDown.rect.y = UiTheme::ScrollDownY; //Badge cluster (top-right hints): A/B always shown, X only if the row has a delete action struct Badge { Texture letter; Texture label; UiTheme::BadgeColor rim; UiTheme::BadgeColor fill; }; - Badge badgeA{ Texture("A", 16, renderer, 0, 0, false, UiTheme::BadgeLetterColor, true), Texture(Translate("HINT_SELECT"), 16, renderer, 0, 0, false, 0xFFFFFFFF, true), UiTheme::BadgeADark, UiTheme::BadgeA }; - Badge badgeB{ Texture("B", 16, renderer, 0, 0, false, UiTheme::BadgeLetterColor, true), Texture(Translate("HINT_BACK"), 16, renderer, 0, 0, false, 0xFFFFFFFF, true), UiTheme::BadgeBDark, UiTheme::BadgeB }; + Badge badgeA{ Texture("A", 16, renderer, 0, 0, false, UiTheme::BadgeLetterColor, true), Texture(Translate("HINT_SELECT"), 16, renderer, 0, 0, false, UiTheme::TextColor, true), UiTheme::BadgeADark, UiTheme::BadgeA }; + Badge badgeB{ Texture("B", 16, renderer, 0, 0, false, UiTheme::BadgeLetterColor, true), Texture(Translate("HINT_BACK"), 16, renderer, 0, 0, false, UiTheme::TextColor, true), UiTheme::BadgeBDark, UiTheme::BadgeB }; // hold-to-delete hint, different color than the real B badge - Badge badgeHold{ Texture("B", 16, renderer, 0, 0, false, UiTheme::BadgeLetterColor, true), Texture(Translate("HINT_DELETE"), 16, renderer, 0, 0, false, 0xFFFFFFFF, true), UiTheme::BadgeXDark, UiTheme::BadgeX }; + Badge badgeHold{ Texture("B", 16, renderer, 0, 0, false, UiTheme::BadgeLetterColor, true), Texture(Translate("HINT_DELETE"), 16, renderer, 0, 0, false, UiTheme::TextColor, true), UiTheme::BadgeXDark, UiTheme::BadgeX }; auto DrawBadge = [&](Badge & badge, int rightEdgeX) -> int { int groupW = UiTheme::BadgeOuterSize + UiTheme::BadgeLabelGap + badge.label.rect.w; @@ -274,8 +275,9 @@ int main(int argc, char * argv[]) badgeInner.rect = { x+innerOffset, y+innerOffset, UiTheme::BadgeInnerSize, UiTheme::BadgeInnerSize }; SDL_SetTextureColorMod(badgeInner.texture.get(), badge.fill.r, badge.fill.g, badge.fill.b); badgeInner.Draw(renderer); - badge.letter.rect.x = x + (UiTheme::BadgeOuterSize - badge.letter.rect.w)/2; - badge.letter.rect.y = y + (UiTheme::BadgeOuterSize - badge.letter.rect.h)/2; + // glyph bearing makes the pure-math center look 1px down/left - nudged + badge.letter.rect.x = x + (UiTheme::BadgeOuterSize - badge.letter.rect.w)/2 + 1; + badge.letter.rect.y = y + (UiTheme::BadgeOuterSize - badge.letter.rect.h)/2 - 1; badge.letter.Draw(renderer); badge.label.rect.x = x + UiTheme::BadgeOuterSize + UiTheme::BadgeLabelGap; badge.label.rect.y = y + (UiTheme::BadgeOuterSize - badge.label.rect.h)/2; @@ -329,7 +331,7 @@ int main(int argc, char * argv[]) label = TruncateUtf8(label, std::max(0, maxChars - 3)) + "..."; } - c.texture = Texture(label, RowGlyphSize, renderer, textX, 0, false, 0xFFFFFFFF, true); + c.texture = Texture(label, RowGlyphSize, renderer, textX, 0, false, UiTheme::TextColor, true); } const int modernRowPitch = std::max(UiTheme::RowPitch, GetTTFLineHeight(RowGlyphSize)); @@ -467,8 +469,8 @@ int main(int argc, char * argv[]) auto ConfirmDelete = [&]() -> bool { const std::string & confirmKey = commands[currentCommandId].deleteConfirmKey; - Texture confirmTitle(Translate(confirmKey.empty() ? "DELETE_CONFIRM_GENERIC" : confirmKey), 24, renderer, 640, 320, true, 0xFFFFFFFF, true); - Texture confirmHint(Translate("DELETE_CONFIRM_HINT"), 16, renderer, 640, 360, true, 0xFFFFFFFF, true); + Texture confirmTitle(Translate(confirmKey.empty() ? "DELETE_CONFIRM_GENERIC" : confirmKey), 24, renderer, 640, 320, true, UiTheme::TextColor, true); + Texture confirmHint(Translate("DELETE_CONFIRM_HINT"), 16, renderer, 640, 360, true, UiTheme::TextColor, true); controller.GetButtonStatus(B); // consume the still-held B from the triggering long-press bool confirmed = false; for(;;) diff --git a/src/mod_uninstall.cpp b/src/mod_uninstall.cpp index 7dfab40..0cd5707 100644 --- a/src/mod_uninstall.cpp +++ b/src/mod_uninstall.cpp @@ -76,6 +76,7 @@ int main(int argc, char * argv[]) LoadLanguageFromConfig(OptionsLocation); SetTTFFontPath(OptionsLocation); + UiTheme::LoadThemeConfig(OptionsLocation); SDL_Context sdl_context(std::chrono::milliseconds(33), false); auto renderer = sdl_context.renderer; @@ -85,12 +86,12 @@ int main(int argc, char * argv[]) SDL_SetRenderDrawColor(renderer, bgR, bgG, bgB, SDL_ALPHA_OPAQUE); Texture gearIcon(OptionsLocation + UiTheme::AssetGear, renderer, UiTheme::GearX, UiTheme::GearY); - Texture appTitleText("OptionsMenu", UiTheme::TitleFontSize, renderer, UiTheme::TitleX, UiTheme::TitleY, false, 0xFFFFFFFF, true); + Texture appTitleText("OptionsMenu", UiTheme::TitleFontSize, renderer, UiTheme::TitleX, UiTheme::TitleY, false, UiTheme::TextColor, true); appTitleText.rect.y -= appTitleText.rect.h / 2; - Texture appVersionText(MOD_VERSION, UiTheme::VersionFontSize, renderer, appTitleText.rect.x + appTitleText.rect.w + UiTheme::VersionGap, UiTheme::TitleY, false, 0xFFFFFFFF, true); + Texture appVersionText(MOD_VERSION, UiTheme::VersionFontSize, renderer, appTitleText.rect.x + appTitleText.rect.w + UiTheme::VersionGap, UiTheme::TitleY, false, UiTheme::TextColor, true); appVersionText.rect.y -= appVersionText.rect.h / 2; - Texture titleText(Translate("MODULE_UNINSTALLER"), UiTheme::SectionTitleFontSize, renderer, UiTheme::SectionTitleX, UiTheme::SectionTitleY, false, 0xFFFFFFFF, true); - Texture creditText("created by CompCom - Modern UI by DefKorns", 16, renderer, UiTheme::CreditX, UiTheme::CreditY, false, 0xFFFFFFFF, true); + Texture titleText(Translate("MODULE_UNINSTALLER"), UiTheme::SectionTitleFontSize, renderer, UiTheme::SectionTitleX, UiTheme::SectionTitleY, false, UiTheme::TextColor, true); + Texture creditText("created by CompCom - Modern UI by DefKorns", 16, renderer, UiTheme::CreditX, UiTheme::CreditY, false, UiTheme::TextColor, true); // two columns sharing the row list's usual width, split by a center divider const int ColumnGap = 40; @@ -108,11 +109,11 @@ int main(int argc, char * argv[]) Texture toRemoveHeader(Translate("MOD_UNINSTALLER_TO_REMOVE"), 18, renderer, RightColumnX, ColumnHeaderY, false, UiTheme::TextDimColor, true); for(Mod & mod : installedMods) - mod.text = Texture(TruncateToWidth(mod.name, ColumnW), ListFontSize, renderer, LeftColumnX, 0, false, 0xFFFFFFFF, true); + mod.text = Texture(TruncateToWidth(mod.name, ColumnW), ListFontSize, renderer, LeftColumnX, 0, false, UiTheme::TextColor, true); // this screen's list spans a different vertical range than the main menu's, so the // chevrons are anchored to it directly rather than reusing UiTheme::ScrollUpY/DownY - Texture scrollUp("^", 16, renderer, UiTheme::ScrollX, ListTopY + 10, false, 0xFFFFFFFF, true); + Texture scrollUp("^", 16, renderer, UiTheme::ScrollX, ListTopY + 10, false, UiTheme::TextColor, true); scrollUp.rect.x -= scrollUp.rect.w / 2; Texture scrollDown = scrollUp; scrollDown.rect.y = ListBottomY - 50; @@ -120,8 +121,8 @@ int main(int argc, char * argv[]) Texture badgeOuter(OptionsLocation + UiTheme::AssetBadgeOuter, renderer); Texture badgeInner(OptionsLocation + UiTheme::AssetBadgeInner, renderer); struct Badge { Texture letter; Texture label; UiTheme::BadgeColor rim; UiTheme::BadgeColor fill; }; - Badge badgeAdd{ Texture("A", 16, renderer, 0, 0, false, UiTheme::BadgeLetterColor, true), Texture(Translate("HINT_ADD"), 16, renderer, 0, 0, false, 0xFFFFFFFF, true), UiTheme::BadgeADark, UiTheme::BadgeA }; - Badge badgeUndo{ Texture("B", 16, renderer, 0, 0, false, UiTheme::BadgeLetterColor, true), Texture(Translate("HINT_UNDO"), 16, renderer, 0, 0, false, 0xFFFFFFFF, true), UiTheme::BadgeBDark, UiTheme::BadgeB }; + Badge badgeAdd{ Texture("A", 16, renderer, 0, 0, false, UiTheme::BadgeLetterColor, true), Texture(Translate("HINT_ADD"), 16, renderer, 0, 0, false, UiTheme::TextColor, true), UiTheme::BadgeADark, UiTheme::BadgeA }; + Badge badgeUndo{ Texture("B", 16, renderer, 0, 0, false, UiTheme::BadgeLetterColor, true), Texture(Translate("HINT_UNDO"), 16, renderer, 0, 0, false, UiTheme::TextColor, true), UiTheme::BadgeBDark, UiTheme::BadgeB }; auto DrawBadge = [&](Badge & badge, int rightEdgeX) -> int { int groupW = UiTheme::BadgeOuterSize + UiTheme::BadgeLabelGap + badge.label.rect.w; @@ -145,7 +146,7 @@ int main(int argc, char * argv[]) // wide "Start" chip instead of a single-letter circle - "St" alone reads ambiguous next to Select Texture pillText("Start", 14, renderer, 0, 0, false, UiTheme::BadgeLetterColor, true); - Texture uninstallLabel(Translate("HINT_UNINSTALL"), 16, renderer, 0, 0, false, 0xFFFFFFFF, true); + Texture uninstallLabel(Translate("HINT_UNINSTALL"), 16, renderer, 0, 0, false, UiTheme::TextColor, true); auto DrawPillBadge = [&](Texture & pill, Texture & label, const UiTheme::BadgeColor & rim, const UiTheme::BadgeColor & fill, int rightEdgeX) -> int { const int pillPadX = 10; @@ -234,8 +235,8 @@ int main(int argc, char * argv[]) // true if the uninstall was confirmed and kicked off - caller should break out of the main loop auto ConfirmUninstall = [&]() -> bool { - Texture confirmTitle(Translate("MOD_UNINSTALL_CONFIRM"), 24, renderer, 640, 320, true, 0xFFFFFFFF, true); - Texture confirmHint(Translate("MOD_UNINSTALL_CONFIRM_HINT"), 16, renderer, 640, 360, true, 0xFFFFFFFF, true); + Texture confirmTitle(Translate("MOD_UNINSTALL_CONFIRM"), 24, renderer, 640, 320, true, UiTheme::TextColor, true); + Texture confirmHint(Translate("MOD_UNINSTALL_CONFIRM_HINT"), 16, renderer, 640, 360, true, UiTheme::TextColor, true); controller.GetButtonStatus(B); // consume the still-held B from whatever press opened this dialog bool confirmed = false; for(;;) From d27cf24558fcd4dedb0e4b458235dd8b2e22843b Mon Sep 17 00:00:00 2001 From: DefKorns Date: Sun, 20 Sep 2026 12:48:45 +0000 Subject: [PATCH 030/104] Add opt-in FIX_LIBSTDCXX build for libstdc++ hmod conflicts --- Makefile.docker | 51 +++++++++++++++++++++++++++++++++---------------- 1 file changed, 35 insertions(+), 16 deletions(-) diff --git a/Makefile.docker b/Makefile.docker index 1dc8b08..61e9406 100644 --- a/Makefile.docker +++ b/Makefile.docker @@ -3,7 +3,13 @@ MOD_BUILDER := DefKorns MOD_CATEGORY := System MOD_VER ?= $(shell git describe --tags --abbrev=0 2>/dev/null || echo v1.3.5) ESC_MOD_VER := $(subst .,_,$(MOD_VER)) +# FIX_LIBSTDCXX=1 ships a private libstdc++.so.6 via rpath, immune to other +# hmods replacing the system copy. Off by default, costs ~225KB. +ifdef FIX_LIBSTDCXX +TARGET_NAME = options_deluxe_$(ESC_MOD_VER)_compat +else TARGET_NAME = options_deluxe_$(ESC_MOD_VER) +endif MOD_NAME := Options Menu $(MOD_VER) GIT_COMMIT := $(shell git describe --always --dirty=.dirty 2>/dev/null) BUILD_NUM_FILE := .build_number @@ -15,13 +21,18 @@ SDL_CFLAGS = -I/usr/include/arm-linux-gnueabihf $(shell PKG_CONFIG_LIBDIR=$(PKG_ SDL_LIBS = $(shell PKG_CONFIG_LIBDIR=$(PKG_CONFIG_LIBDIR) pkg-config --libs sdl2 SDL2_ttf libpng) PNG_LIBS = $(shell PKG_CONFIG_LIBDIR=$(PKG_CONFIG_LIBDIR) pkg-config --libs libpng) LDFLAGS = -Wl,--allow-shlib-undefined -VENDORED_TTF_LIBS = mod/etc/options_menu/libSDL2_ttf-2.0.so.0 mod/etc/options_menu/libfreetype.so.2.8 +VENDORED_LIBS = mod/etc/options_menu/lib/libSDL2_ttf-2.0.so.0 mod/etc/options_menu/lib/libfreetype.so.2.8 +ifdef FIX_LIBSTDCXX +VENDORED_LIBS += mod/etc/options_menu/lib/libstdc++.so.6 +STDCXX_RPATH = -Wl,-rpath,/etc/options_menu/lib +STDCXX_RPATH_ORIGIN = -Wl,-rpath,'$$ORIGIN/lib' +endif else SDL_CFLAGS = $(shell sdl2-config --cflags) $(shell pkg-config --cflags SDL2_ttf) SDL_LIBS = $(shell sdl2-config --libs) $(shell pkg-config --libs SDL2_ttf) -lpng PNG_LIBS = -lpng LDFLAGS = -VENDORED_TTF_LIBS = +VENDORED_LIBS = #FIX_SDL2_SONAME = true endif CXXFLAGS = -std=c++11 -Os $(SDL_CFLAGS) -DMOD_VERSION=\"$(MOD_VER)\" @@ -53,7 +64,7 @@ all: $(OUT)/$(TARGET_NAME).hmod bump-build-number: echo $$(( $$(cat $(BUILD_NUM_FILE) 2>/dev/null || echo 0) + 1 )) > $(BUILD_NUM_FILE) -$(OUT)/$(TARGET_NAME).hmod: mod/etc/options_menu/options mod/etc/options_menu/optiond mod/etc/options_menu/mod_uninstall/mod_uninstall mod/bin/standby_watchdog mod/etc/options_menu/scripts/gen_splash mod/etc/options_menu/scripts/ChangeCombo $(VENDORED_TTF_LIBS) bump-build-number +$(OUT)/$(TARGET_NAME).hmod: mod/etc/options_menu/options mod/etc/options_menu/optiond mod/etc/options_menu/mod_uninstall/mod_uninstall mod/bin/standby_watchdog mod/etc/options_menu/scripts/gen_splash mod/etc/options_menu/scripts/ChangeCombo $(VENDORED_LIBS) bump-build-number $(CROSS_PREFIX)$(STRIP) mod/etc/options_menu/options $(CROSS_PREFIX)$(STRIP) mod/etc/options_menu/optiond $(CROSS_PREFIX)$(STRIP) mod/etc/options_menu/mod_uninstall/mod_uninstall @@ -71,39 +82,47 @@ $(OUT)/$(TARGET_NAME).hmod: mod/etc/options_menu/options mod/etc/options_menu/op find -name "*.o" -type f -delete rm -rf $(TMP) -# SDL2_ttf/freetype aren't on the console's own rootfs (unlike SDL2/libpng); -# they're shipped as mod/etc/options_menu/lib*.so and found via rpath instead. +# SDL2_ttf/freetype aren't on the console's own rootfs; shipped as +# mod/etc/options_menu/lib/*.so and found via rpath instead. mod/etc/options_menu/options: $(OBJECTS) - $(CROSS_PREFIX)$(CXX) $(OBJECTS) $(LDLIBS) $(LDFLAGS) -Wl,-rpath,'$$ORIGIN' -o mod/etc/options_menu/options + $(CROSS_PREFIX)$(CXX) $(OBJECTS) $(LDLIBS) $(LDFLAGS) -Wl,-rpath,'$$ORIGIN/lib' -o mod/etc/options_menu/options # not ours to redistribute, pulled from the toolchain image each build -mod/etc/options_menu/libSDL2_ttf-2.0.so.0: - cp /usr/lib/arm-linux-gnueabihf/libSDL2_ttf.so mod/etc/options_menu/libSDL2_ttf-2.0.so.0 +mod/etc/options_menu/lib/libSDL2_ttf-2.0.so.0: + mkdir -p mod/etc/options_menu/lib + cp /usr/lib/arm-linux-gnueabihf/libSDL2_ttf.so mod/etc/options_menu/lib/libSDL2_ttf-2.0.so.0 + +mod/etc/options_menu/lib/libfreetype.so.2.8: + mkdir -p mod/etc/options_menu/lib + cp /usr/lib/arm-linux-gnueabihf/libfreetype.so.2.8.1 mod/etc/options_menu/lib/libfreetype.so.2.8 -mod/etc/options_menu/libfreetype.so.2.8: - cp /usr/lib/arm-linux-gnueabihf/libfreetype.so.2.8.1 mod/etc/options_menu/libfreetype.so.2.8 +mod/etc/options_menu/lib/libstdc++.so.6: + mkdir -p mod/etc/options_menu/lib + cp $$($(CROSS_PREFIX)$(CXX) -print-file-name=libstdc++.so.6) mod/etc/options_menu/lib/libstdc++.so.6 + $(CROSS_PREFIX)$(STRIP) --strip-unneeded mod/etc/options_menu/lib/libstdc++.so.6 mod/etc/options_menu/optiond: src/daemon.o src/framework/controller.o - $(CROSS_PREFIX)$(CXX) $(LDFLAGS) src/daemon.o src/framework/controller.o -o mod/etc/options_menu/optiond + $(CROSS_PREFIX)$(CXX) $(LDFLAGS) src/daemon.o src/framework/controller.o $(STDCXX_RPATH_ORIGIN) -o mod/etc/options_menu/optiond mod/etc/options_menu/mod_uninstall/mod_uninstall: src/mod_uninstall.o src/localization.o - $(CROSS_PREFIX)$(CXX) src/mod_uninstall.o src/localization.o src/framework/*.o $(LDLIBS) $(LDFLAGS) -Wl,-rpath,/etc/options_menu -o mod/etc/options_menu/mod_uninstall/mod_uninstall + $(CROSS_PREFIX)$(CXX) src/mod_uninstall.o src/localization.o src/framework/*.o $(LDLIBS) $(LDFLAGS) -Wl,-rpath,/etc/options_menu/lib -o mod/etc/options_menu/mod_uninstall/mod_uninstall mod/bin/standby_watchdog: src/standby_watchdog.o src/framework/controller.o src/framework/powerwatch.o - $(CROSS_PREFIX)$(CXX) src/standby_watchdog.o src/framework/controller.o src/framework/powerwatch.o -o mod/bin/standby_watchdog + $(CROSS_PREFIX)$(CXX) $(LDFLAGS) src/standby_watchdog.o src/framework/controller.o src/framework/powerwatch.o $(STDCXX_RPATH) -o mod/bin/standby_watchdog mod/etc/options_menu/scripts/gen_splash: src/gen_splash.o src/localization.o src/framework/utf8.o src/framework/font8x8_lookup.o - $(CROSS_PREFIX)$(CXX) src/gen_splash.o src/localization.o src/framework/utf8.o src/framework/font8x8_lookup.o $(PNG_LIBS) $(LDFLAGS) -o mod/etc/options_menu/scripts/gen_splash + $(CROSS_PREFIX)$(CXX) src/gen_splash.o src/localization.o src/framework/utf8.o src/framework/font8x8_lookup.o $(PNG_LIBS) $(LDFLAGS) $(STDCXX_RPATH) -o mod/etc/options_menu/scripts/gen_splash mod/etc/options_menu/scripts/ChangeCombo: src/change_combo.o src/framework/controller.o src/localization.o - $(CROSS_PREFIX)$(CXX) src/change_combo.o src/framework/controller.o src/localization.o $(LDFLAGS) -o mod/etc/options_menu/scripts/ChangeCombo + $(CROSS_PREFIX)$(CXX) src/change_combo.o src/framework/controller.o src/localization.o $(LDFLAGS) $(STDCXX_RPATH) -o mod/etc/options_menu/scripts/ChangeCombo %.o: %.cpp $(CROSS_PREFIX)$(CXX) $(CXXFLAGS) -c $< -o $@ clean: find -name "*.o" -type f -delete - rm -f mod/etc/options_menu/options mod/etc/options_menu/optiond mod/etc/options_menu/mod_uninstall/mod_uninstall mod/bin/standby_watchdog mod/etc/options_menu/scripts/gen_splash mod/etc/options_menu/libSDL2_ttf-2.0.so.0 mod/etc/options_menu/libfreetype.so.2.8 $(TARGET_NAME).hmod + rm -f mod/etc/options_menu/options mod/etc/options_menu/optiond mod/etc/options_menu/mod_uninstall/mod_uninstall mod/bin/standby_watchdog mod/etc/options_menu/scripts/gen_splash mod/etc/options_menu/scripts/ChangeCombo $(TARGET_NAME).hmod + rm -rf mod/etc/options_menu/lib rm -rf out/ temp/ .PHONY: clean bump-build-number From c87ad1584a3ec8101f25ee6ba6c11d9e3e56c797 Mon Sep 17 00:00:00 2001 From: DefKorns Date: Sun, 20 Sep 2026 12:50:55 +0000 Subject: [PATCH 031/104] Add PREVIEW_FIT_CONTAIN and PREVIEW_HIDE_LABEL command fields Lets a theme tile scale-to-fit instead of cropping, and skip the caption below it, for cases where cropping or a label doesn't suit the art. --- src/command.cpp | 4 ++++ src/command.h | 2 ++ 2 files changed, 6 insertions(+) diff --git a/src/command.cpp b/src/command.cpp index 735377e..1545330 100644 --- a/src/command.cpp +++ b/src/command.cpp @@ -85,6 +85,10 @@ Command::Command(std::ifstream & in) previewSquare = (value == "TRUE"); else if(param.compare("PREVIEW_GRID_COLS")==0) previewGridCols = SafeStoi(value, -1); + else if(param.compare("PREVIEW_FIT_CONTAIN")==0) + previewFitContain = (value == "TRUE"); + else if(param.compare("PREVIEW_HIDE_LABEL")==0) + previewHideLabel = (value == "TRUE"); else if(param.compare("SUBMENU")==0) hasSubmenu = (value == "TRUE"); } diff --git a/src/command.h b/src/command.h index d576016..a940e89 100644 --- a/src/command.h +++ b/src/command.h @@ -52,6 +52,8 @@ struct Command bool previewNearest = false; // nearest-neighbor scaling instead of linear - for small pixel-art sprites blown up a lot, where linear just blurs them bool previewSquare = false; // this screen's preview art is square/near-square - grid tiles should be too, not the default wide tile int previewGridCols = -1; // explicit grid column count override, unset (-1) defers to the square/wide default + bool previewFitContain = false; // scale to fit within the tile instead of cropping to cover it - for portrait art in a wide/square tile + bool previewHideLabel = false; // no caption below this tile - the art speaks for itself }; #endif From 813f39ca101669162e8c4b661b49134ae7659014 Mon Sep 17 00:00:00 2001 From: DefKorns Date: Sun, 20 Sep 2026 12:52:42 +0000 Subject: [PATCH 032/104] Update README: PREVIEW_FIT_CONTAIN/PREVIEW_HIDE_LABEL and FIX_LIBSTDCXX --- README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README.md b/README.md index 332dc86..4c3b2d4 100644 --- a/README.md +++ b/README.md @@ -264,6 +264,8 @@ Command Files should contain the following fields: |PREVIEW_IMAGE_Y|Unused - the detail panel always auto-centers the image.| |PREVIEW_IMAGE_WIDTH|Unused - the detail panel always auto-scales the image to fit.| |PREVIEW_IMAGE_HEIGHT|Unused - the detail panel always auto-scales the image to fit.| +|PREVIEW_FIT_CONTAIN|If set to `TRUE`, this row's grid tile scales the art to fit inside the tile instead of cropping it to fill it. Use for portrait art in a wide/square tile.| +|PREVIEW_HIDE_LABEL|If set to `TRUE`, this row's grid tile shows no caption below it.| |DELETE_STR|Optional command string to run when the user holds B (~1 second) on this entry and confirms. Used to make list entries deletable (e.g. the wifi backup restore list).| |DELETE_CONFIRM_KEY|Optional translation key for the confirmation prompt shown before running DELETE_STR. Falls back to a generic "Delete this item?" if not set.| |STATE_STR|Optional command string that prints the current on/off state of this entry (its first line of output, case-insensitively `1`/`on`/`y`/`yes`/`true` for on, anything else for off). Presence of this field renders the entry as a toggle switch instead of a plain row; it is only read to draw the switch, not to change behavior, so COMMAND_STR is still what actually flips the setting.| @@ -290,6 +292,10 @@ The repo includes a Docker-based ARM cross-compile toolchain, which is the teste This builds the toolchain image (`Dockerfile.jessie-armhf`) if needed, then runs `Makefile.docker` inside it, producing `out/*.hmod`. +Pass `FIX_LIBSTDCXX=1` to also build a `_compat` variant that ships its own `libstdc++.so.6`, for consoles with another hmod that replaces the system one (e.g. RetroArch 1.8.4 Xtreme SC): + + ./build.sh FIX_LIBSTDCXX=1 + #### Native / manual build The options menu can also be compiled using the plain `Makefile` provided. To compile the options menu you must have the SDL2 and libpng libraries as well as GNU Make installed. To cross-compile the options menu call `make` with the `CROSS_PREFIX` set to the prefix of your cross-compiler toolchain eg. `make CROSS_PREFIX=arm-linux-gnueabihf-`. From 76fb00862ba2c5f954089d5007332eaf8ec98d01 Mon Sep 17 00:00:00 2001 From: DefKorns Date: Sun, 20 Sep 2026 18:26:04 +0000 Subject: [PATCH 033/104] Fix language directory path in translate function --- mod/etc/options_menu/scripts/om_functions | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mod/etc/options_menu/scripts/om_functions b/mod/etc/options_menu/scripts/om_functions index 27d9bfd..1a3e601 100644 --- a/mod/etc/options_menu/scripts/om_functions +++ b/mod/etc/options_menu/scripts/om_functions @@ -78,7 +78,7 @@ translate() { [ -z "$langCode" ] && langCode="en-US" value="" - for langDir in "$optionsMenu/lang" "$optionsMenu"/*/lang; do + for langDir in "$optionsMenu/language" "$optionsMenu"/*/lang; do [ -z "$value" ] && [ -f "$langDir/$langCode.lang" ] && value="$(grep "^${key}=" "$langDir/$langCode.lang" 2>/dev/null | tail -1 | cut -d= -f2-)" [ -z "$value" ] && [ -f "$langDir/en-US.lang" ] && value="$(grep "^${key}=" "$langDir/en-US.lang" 2>/dev/null | tail -1 | cut -d= -f2-)" done From 1f65fdd76cc83cd2a4047e5755d276a49b10d510 Mon Sep 17 00:00:00 2001 From: DefKorns Date: Sun, 20 Sep 2026 18:26:42 +0000 Subject: [PATCH 034/104] Add ENABLE_IF command field and implementation for conditional execution --- README.md | 1 + src/command.cpp | 2 ++ src/command.h | 1 + 3 files changed, 4 insertions(+) diff --git a/README.md b/README.md index 4c3b2d4..dbe5d3f 100644 --- a/README.md +++ b/README.md @@ -257,6 +257,7 @@ Command Files should contain the following fields: |RESTART_UI|Specifies if the paused UI should be resumed after running command (external commands only). If you set this value to `FALSE` you must manually resume the UI using `/bin/sh /etc/options_menu/script/ResumeUI.sh` after executing your code.| |COMMAND_STR|Command string to be executed. Commands must be single line only. To execute multi-line scripts use a script file.| |USB_ONLY|If set to `TRUE`, the command is only loaded when a USB/SD card is mounted.| +|ENABLE_IF|Optional shell condition; the row is only loaded if it exits 0. Runs with `/etc/preinit` sourced and `script_init` called first, so it can reference `$rootfs`/`$mountpoint` like any other options_menu script, e.g. `ENABLE_IF=[ -d "$rootfs/etc/wifi_backup" ]`.| |IGNORE_INTERRUPT|If set to `TRUE`, pressing B will not interrupt an internal command while its output is being displayed.| |CHILD|If set to `TRUE`, indents the row under the one above it, for visually grouping a related entry without a separate submenu.| |PREVIEW_IMAGE|Specifies the path to a thumbnail/icon shown in the detail panel when this row is selected. Auto-scaled and centered to fit the panel.| diff --git a/src/command.cpp b/src/command.cpp index 1545330..185f8f5 100644 --- a/src/command.cpp +++ b/src/command.cpp @@ -60,6 +60,8 @@ Command::Command(std::ifstream & in) deleteCommand = value; else if(param.compare("DELETE_CONFIRM_KEY")==0) deleteConfirmKey = value; + else if(param.compare("ENABLE_IF")==0) + enableIfCommand = value; else if(param.compare("STATE_STR")==0) { stateCommand = value; diff --git a/src/command.h b/src/command.h index a940e89..8fae74b 100644 --- a/src/command.h +++ b/src/command.h @@ -35,6 +35,7 @@ struct Command std::string deleteCommand; std::string deleteConfirmKey; std::string stateCommand; + std::string enableIfCommand; // shell condition; row is skipped unless it exits 0 bool runInternal = true; bool restartUI = false; bool ignoreInterrupt = false; From e5b0747f4e4e35855ce0d5dbcd5d96c25254412a Mon Sep 17 00:00:00 2001 From: DefKorns Date: Sun, 20 Sep 2026 18:30:52 +0000 Subject: [PATCH 035/104] Add commands for saving and restoring game data, and update language files --- .../options_menu/commands/c0073_SavedGamesOptions | 7 +++++++ .../c0078_LanguageOptions} | 1 + mod/etc/options_menu/language/de-DE.lang | 11 ++++++++--- mod/etc/options_menu/language/en-US.lang | 12 +++++++++--- mod/etc/options_menu/language/es-ES.lang | 11 ++++++++--- mod/etc/options_menu/language/fr-FR.lang | 11 ++++++++--- mod/etc/options_menu/language/it-IT.lang | 11 ++++++++--- mod/etc/options_menu/language/ja-JP.lang | 11 ++++++++--- mod/etc/options_menu/language/nl-NL.lang | 11 ++++++++--- mod/etc/options_menu/language/pt-PT.lang | 11 ++++++++--- mod/etc/options_menu/language/ru-RU.lang | 11 ++++++++--- .../options_menu/network/commands/c0050_RestoreNand | 1 + .../options_menu/network/commands/c0060_RestoreUsb | 1 + .../retroarch/commands/c0090_ToggleLoadScreens | 1 + .../saved_games/commands/c0010_BackupSavesUsb | 4 ++++ .../saved_games/commands/c0020_RestoreSavesUsb | 4 ++++ mod/etc/options_menu/scripts/BackupSavesUsb.sh | 13 +++++++++++++ mod/etc/options_menu/scripts/RestoreSavesUsb.sh | 11 +++++++++++ mod/etc/options_menu/scripts/om_network | 4 ---- mod/etc/options_menu/scripts/om_retroarch | 12 +----------- 20 files changed, 117 insertions(+), 42 deletions(-) create mode 100644 mod/etc/options_menu/commands/c0073_SavedGamesOptions rename mod/etc/options_menu/{advanced_commands/c0010_LanguageOptions => commands/c0078_LanguageOptions} (95%) create mode 100644 mod/etc/options_menu/saved_games/commands/c0010_BackupSavesUsb create mode 100644 mod/etc/options_menu/saved_games/commands/c0020_RestoreSavesUsb create mode 100644 mod/etc/options_menu/scripts/BackupSavesUsb.sh create mode 100644 mod/etc/options_menu/scripts/RestoreSavesUsb.sh diff --git a/mod/etc/options_menu/commands/c0073_SavedGamesOptions b/mod/etc/options_menu/commands/c0073_SavedGamesOptions new file mode 100644 index 0000000..96ab30f --- /dev/null +++ b/mod/etc/options_menu/commands/c0073_SavedGamesOptions @@ -0,0 +1,7 @@ +COMMAND_NAME=SAVED_GAMES_OPTIONS +COMMAND_TYPE=EXTERNAL +RESTART_UI=FALSE +SUBMENU=TRUE +USB_ONLY=TRUE +COMMAND_STR=usleep 50000 && %options_path%/options --commandPath %options_path%/saved_games/commands/ --title "SAVED_GAMES_OPTIONS" & +PREVIEW_IMAGE=/etc/options_menu/images/preview_placeholder.png diff --git a/mod/etc/options_menu/advanced_commands/c0010_LanguageOptions b/mod/etc/options_menu/commands/c0078_LanguageOptions similarity index 95% rename from mod/etc/options_menu/advanced_commands/c0010_LanguageOptions rename to mod/etc/options_menu/commands/c0078_LanguageOptions index 2f12a54..bb9c302 100644 --- a/mod/etc/options_menu/advanced_commands/c0010_LanguageOptions +++ b/mod/etc/options_menu/commands/c0078_LanguageOptions @@ -1,5 +1,6 @@ COMMAND_NAME=LANGUAGE_OPTIONS COMMAND_TYPE=EXTERNAL RESTART_UI=FALSE +SUBMENU=TRUE COMMAND_STR=usleep 50000 && %options_path%/options --commandPath %options_path%/language/commands/ --title "LANGUAGE_OPTIONS" & PREVIEW_IMAGE=/etc/options_menu/images/preview_placeholder.png diff --git a/mod/etc/options_menu/language/de-DE.lang b/mod/etc/options_menu/language/de-DE.lang index 3c51038..bfa20cf 100644 --- a/mod/etc/options_menu/language/de-DE.lang +++ b/mod/etc/options_menu/language/de-DE.lang @@ -1,21 +1,26 @@ HIBERNATE=Ruhezustand/Standby SHUTDOWN_DEVICE=Gerät ausschalten REBOOT_DEVICE=Gerät neu starten -RETROARCH_OPTIONS=RetroArch-Optionen ADVANCED_OPTIONS=Erweiterte Optionen -NETWORK_OPTIONS=Netzwerkoptionen +NETWORK_OPTIONS=Netzwerk +SAVED_GAMES_OPTIONS=Spielstände EXIT=Beenden MODULE_UNINSTALLER=Modul-Deinstallation DISPLAY_TEMP=Temperatur anzeigen RUN_TOP=Top ausführen BENCHMARK_TOOL=Benchmark-Tool DUMP_FILE_STRUCTURE=Dateistruktur exportieren (auf USB) +BACKUP_SAVES_USB=Spielstände auf USB sichern +RESTORE_SAVES_USB=Spielstände von USB wiederherstellen +BACKUP_SAVES_USB_DONE=Spielstände auf USB:/data/saves_backup gesichert +RESTORE_SAVES_USB_DONE=Spielstände von USB:/data/saves_backup wiederhergestellt +RESTORE_SAVES_USB_NOT_FOUND=Keine Spielstand-Sicherung auf USB gefunden. TOGGLE_USB_WRITE=USB-Schreibzugriff umschalten RA_DEBUGGER=RetroArch-Debugger (USB-Logs) CHANGE_BUTTON_COMBO=Tastenkombination ändern CLEAR_CACHE=Cache leeren BACK=Zurück -LANGUAGE_OPTIONS=Sprachoptionen +LANGUAGE_OPTIONS=Sprache DISPLAY_IP=IP-Adresse anzeigen RECONNECT=Erneut verbinden SEARCH_SSIDS=Nach SSIDs suchen diff --git a/mod/etc/options_menu/language/en-US.lang b/mod/etc/options_menu/language/en-US.lang index 179b899..53849bd 100644 --- a/mod/etc/options_menu/language/en-US.lang +++ b/mod/etc/options_menu/language/en-US.lang @@ -1,21 +1,27 @@ HIBERNATE=Hibernate/Standby SHUTDOWN_DEVICE=Shutdown Device REBOOT_DEVICE=Reboot Device -RETROARCH_OPTIONS=Retroarch Options +RETROARCH_OPTIONS=RetroArch ADVANCED_OPTIONS=Advanced Options -NETWORK_OPTIONS=Network Options +NETWORK_OPTIONS=Network +SAVED_GAMES_OPTIONS=Saved Games EXIT=Exit MODULE_UNINSTALLER=Module Uninstaller DISPLAY_TEMP=Display Temp RUN_TOP=Run Top BENCHMARK_TOOL=Benchmark Tool DUMP_FILE_STRUCTURE=Dump File Structure (to USB) +BACKUP_SAVES_USB=Backup Saved Games to USB +RESTORE_SAVES_USB=Restore Saved Games from USB +BACKUP_SAVES_USB_DONE=Saved games backed up to USB:/data/saves_backup +RESTORE_SAVES_USB_DONE=Saved games restored from USB:/data/saves_backup +RESTORE_SAVES_USB_NOT_FOUND=No saved games backup found on USB. TOGGLE_USB_WRITE=Toggle Write Access on USB RA_DEBUGGER=RetroArch Debugger (USB logs) CHANGE_BUTTON_COMBO=Change Options Button Combo CLEAR_CACHE=Clear Cache BACK=Back -LANGUAGE_OPTIONS=Language Options +LANGUAGE_OPTIONS=Language DISPLAY_IP=Display IP Address RECONNECT=Reconnect SEARCH_SSIDS=Search for SSIDs diff --git a/mod/etc/options_menu/language/es-ES.lang b/mod/etc/options_menu/language/es-ES.lang index 99102f9..9b3bf56 100644 --- a/mod/etc/options_menu/language/es-ES.lang +++ b/mod/etc/options_menu/language/es-ES.lang @@ -1,21 +1,26 @@ HIBERNATE=Hibernar/Reposo SHUTDOWN_DEVICE=Apagar dispositivo REBOOT_DEVICE=Reiniciar dispositivo -RETROARCH_OPTIONS=Opciones de Retroarch ADVANCED_OPTIONS=Opciones avanzadas -NETWORK_OPTIONS=Opciones de red +NETWORK_OPTIONS=Red +SAVED_GAMES_OPTIONS=Partidas Guardadas EXIT=Salir MODULE_UNINSTALLER=Desinstalador de módulos DISPLAY_TEMP=Mostrar temperatura RUN_TOP=Ejecutar Top BENCHMARK_TOOL=Herramienta de benchmark DUMP_FILE_STRUCTURE=Exportar estructura de archivos (a USB) +BACKUP_SAVES_USB=Copiar partidas guardadas a USB +RESTORE_SAVES_USB=Restaurar partidas guardadas desde USB +BACKUP_SAVES_USB_DONE=Partidas guardadas copiadas a USB:/data/saves_backup +RESTORE_SAVES_USB_DONE=Partidas guardadas restauradas desde USB:/data/saves_backup +RESTORE_SAVES_USB_NOT_FOUND=No se encontró ninguna copia de seguridad de partidas guardadas en la USB. TOGGLE_USB_WRITE=Alternar escritura en USB RA_DEBUGGER=Depurador de RetroArch (logs USB) CHANGE_BUTTON_COMBO=Cambiar combinación de botones CLEAR_CACHE=Vaciar caché BACK=Atrás -LANGUAGE_OPTIONS=Opciones de idioma +LANGUAGE_OPTIONS=Idioma DISPLAY_IP=Mostrar dirección IP RECONNECT=Reconectar SEARCH_SSIDS=Buscar SSID diff --git a/mod/etc/options_menu/language/fr-FR.lang b/mod/etc/options_menu/language/fr-FR.lang index e1dbee1..f268404 100644 --- a/mod/etc/options_menu/language/fr-FR.lang +++ b/mod/etc/options_menu/language/fr-FR.lang @@ -1,21 +1,26 @@ HIBERNATE=Hibernation/Veille SHUTDOWN_DEVICE=Éteindre l'appareil REBOOT_DEVICE=Redémarrer l'appareil -RETROARCH_OPTIONS=Options RetroArch ADVANCED_OPTIONS=Options avancées -NETWORK_OPTIONS=Options réseau +NETWORK_OPTIONS=Réseau +SAVED_GAMES_OPTIONS=Sauvegardes de Jeu EXIT=Quitter MODULE_UNINSTALLER=Désinstalleur de modules DISPLAY_TEMP=Afficher la température RUN_TOP=Exécuter Top BENCHMARK_TOOL=Outil de benchmark DUMP_FILE_STRUCTURE=Exporter la structure des fichiers (vers USB) +BACKUP_SAVES_USB=Sauvegarder les sauvegardes de jeu sur USB +RESTORE_SAVES_USB=Restaurer les sauvegardes de jeu depuis l'USB +BACKUP_SAVES_USB_DONE=Sauvegardes de jeu copiées sur USB:/data/saves_backup +RESTORE_SAVES_USB_DONE=Sauvegardes de jeu restaurées depuis USB:/data/saves_backup +RESTORE_SAVES_USB_NOT_FOUND=Aucune sauvegarde de jeu trouvée sur l'USB. TOGGLE_USB_WRITE=Activer/désactiver l'écriture USB RA_DEBUGGER=Débogueur RetroArch (logs USB) CHANGE_BUTTON_COMBO=Changer la combinaison de boutons CLEAR_CACHE=Vider le cache BACK=Retour -LANGUAGE_OPTIONS=Options de langue +LANGUAGE_OPTIONS=Langue DISPLAY_IP=Afficher l'adresse IP RECONNECT=Reconnecter SEARCH_SSIDS=Rechercher des SSID diff --git a/mod/etc/options_menu/language/it-IT.lang b/mod/etc/options_menu/language/it-IT.lang index 43f451d..f8e6301 100644 --- a/mod/etc/options_menu/language/it-IT.lang +++ b/mod/etc/options_menu/language/it-IT.lang @@ -1,21 +1,26 @@ HIBERNATE=Ibernazione/Standby SHUTDOWN_DEVICE=Spegni dispositivo REBOOT_DEVICE=Riavvia dispositivo -RETROARCH_OPTIONS=Opzioni RetroArch ADVANCED_OPTIONS=Opzioni avanzate -NETWORK_OPTIONS=Opzioni di rete +NETWORK_OPTIONS=Rete +SAVED_GAMES_OPTIONS=Salvataggi EXIT=Esci MODULE_UNINSTALLER=Disinstallatore moduli DISPLAY_TEMP=Mostra temperatura RUN_TOP=Esegui Top BENCHMARK_TOOL=Strumento di benchmark DUMP_FILE_STRUCTURE=Esporta struttura file (su USB) +BACKUP_SAVES_USB=Backup salvataggi su USB +RESTORE_SAVES_USB=Ripristina salvataggi da USB +BACKUP_SAVES_USB_DONE=Salvataggi copiati su USB:/data/saves_backup +RESTORE_SAVES_USB_DONE=Salvataggi ripristinati da USB:/data/saves_backup +RESTORE_SAVES_USB_NOT_FOUND=Nessun backup dei salvataggi trovato sulla USB. TOGGLE_USB_WRITE=Attiva/disattiva scrittura USB RA_DEBUGGER=Debugger RetroArch (log USB) CHANGE_BUTTON_COMBO=Cambia combinazione pulsanti CLEAR_CACHE=Svuota cache BACK=Indietro -LANGUAGE_OPTIONS=Opzioni lingua +LANGUAGE_OPTIONS=Lingua DISPLAY_IP=Mostra indirizzo IP RECONNECT=Riconnetti SEARCH_SSIDS=Cerca SSID diff --git a/mod/etc/options_menu/language/ja-JP.lang b/mod/etc/options_menu/language/ja-JP.lang index e4d1355..6f8764a 100644 --- a/mod/etc/options_menu/language/ja-JP.lang +++ b/mod/etc/options_menu/language/ja-JP.lang @@ -1,21 +1,26 @@ HIBERNATE=ねむらせる SHUTDOWN_DEVICE=でんげんをきる REBOOT_DEVICE=さいきどうする -RETROARCH_OPTIONS=RetroArchのせってい ADVANCED_OPTIONS=じょうきゅうせってい -NETWORK_OPTIONS=Networkのせってい +NETWORK_OPTIONS=Network +SAVED_GAMES_OPTIONS=セーブデータ EXIT=おわる MODULE_UNINSTALLER=Moduleのさくじょ DISPLAY_TEMP=おんどをひょうじ RUN_TOP=topをじっこう BENCHMARK_TOOL=Benchmarkのどうぐ DUMP_FILE_STRUCTURE=Fileこうぞうをほぞん (USBへ) +BACKUP_SAVES_USB=セーブデータのBackup (USB) +RESTORE_SAVES_USB=セーブデータをもどす (USB) +BACKUP_SAVES_USB_DONE=セーブデータをUSB:/data/saves_backupにほぞんしました +RESTORE_SAVES_USB_DONE=セーブデータをUSB:/data/saves_backupからもどしました +RESTORE_SAVES_USB_NOT_FOUND=USBにセーブデータのBackupがみつかりません TOGGLE_USB_WRITE=USBへのかきこみをきりかえ RA_DEBUGGER=RAのDebugger (USB Log) CHANGE_BUTTON_COMBO=Buttonくみあわせをへんこう CLEAR_CACHE=Cacheをけす BACK=もどる -LANGUAGE_OPTIONS=げんごせってい +LANGUAGE_OPTIONS=げんご DISPLAY_IP=IP Addressをひょうじ RECONNECT=さいせつぞく SEARCH_SSIDS=SSIDをさがす diff --git a/mod/etc/options_menu/language/nl-NL.lang b/mod/etc/options_menu/language/nl-NL.lang index 608e68f..bee82cc 100644 --- a/mod/etc/options_menu/language/nl-NL.lang +++ b/mod/etc/options_menu/language/nl-NL.lang @@ -1,21 +1,26 @@ HIBERNATE=Slaapstand SHUTDOWN_DEVICE=Apparaat uitschakelen REBOOT_DEVICE=Apparaat herstarten -RETROARCH_OPTIONS=RetroArch-opties ADVANCED_OPTIONS=Geavanceerde opties -NETWORK_OPTIONS=Netwerkopties +NETWORK_OPTIONS=Netwerk +SAVED_GAMES_OPTIONS=Savegames EXIT=Afsluiten MODULE_UNINSTALLER=Module-verwijderaar DISPLAY_TEMP=Temperatuur weergeven RUN_TOP=Top uitvoeren BENCHMARK_TOOL=Benchmarktool DUMP_FILE_STRUCTURE=Bestandsstructuur exporteren (naar USB) +BACKUP_SAVES_USB=Savegames back-uppen naar USB +RESTORE_SAVES_USB=Savegames herstellen vanaf USB +BACKUP_SAVES_USB_DONE=Savegames back-upt naar USB:/data/saves_backup +RESTORE_SAVES_USB_DONE=Savegames hersteld vanaf USB:/data/saves_backup +RESTORE_SAVES_USB_NOT_FOUND=Geen savegame-back-up gevonden op de USB. TOGGLE_USB_WRITE=USB-schrijftoegang schakelen RA_DEBUGGER=RetroArch-debugger (USB-logs) CHANGE_BUTTON_COMBO=Knoppencombinatie wijzigen CLEAR_CACHE=Cache wissen BACK=Terug -LANGUAGE_OPTIONS=Taalopties +LANGUAGE_OPTIONS=Taal DISPLAY_IP=IP-adres weergeven RECONNECT=Opnieuw verbinden SEARCH_SSIDS=SSID's zoeken diff --git a/mod/etc/options_menu/language/pt-PT.lang b/mod/etc/options_menu/language/pt-PT.lang index b0b35d4..6b64dcc 100644 --- a/mod/etc/options_menu/language/pt-PT.lang +++ b/mod/etc/options_menu/language/pt-PT.lang @@ -1,21 +1,26 @@ HIBERNATE=Hibernar/Standby SHUTDOWN_DEVICE=Desligar Dispositivo REBOOT_DEVICE=Reiniciar Dispositivo -RETROARCH_OPTIONS=Opções do Retroarch ADVANCED_OPTIONS=Opções Avançadas -NETWORK_OPTIONS=Opções de Rede +NETWORK_OPTIONS=Rede +SAVED_GAMES_OPTIONS=Jogos Guardados EXIT=Sair MODULE_UNINSTALLER=Desinstalador de Módulos DISPLAY_TEMP=Ver Temperatura RUN_TOP=Executar Top BENCHMARK_TOOL=Ferramenta de Benchmark DUMP_FILE_STRUCTURE=Exportar Estrutura de Ficheiros (para USB) +BACKUP_SAVES_USB=Cópia de Segurança dos Jogos Guardados (USB) +RESTORE_SAVES_USB=Restaurar Jogos Guardados (USB) +BACKUP_SAVES_USB_DONE=Jogos guardados copiados para USB:/data/saves_backup +RESTORE_SAVES_USB_DONE=Jogos guardados restaurados de USB:/data/saves_backup +RESTORE_SAVES_USB_NOT_FOUND=Não foi encontrada nenhuma cópia de segurança de jogos guardados na USB. TOGGLE_USB_WRITE=Alternar Escrita na USB RA_DEBUGGER=Depurador do RetroArch (logs USB) CHANGE_BUTTON_COMBO=Mudar Combinação de Botões CLEAR_CACHE=Limpar Cache BACK=Voltar -LANGUAGE_OPTIONS=Opções de Idioma +LANGUAGE_OPTIONS=Idioma DISPLAY_IP=Mostrar Endereço IP RECONNECT=Reconectar SEARCH_SSIDS=Procurar SSIDs diff --git a/mod/etc/options_menu/language/ru-RU.lang b/mod/etc/options_menu/language/ru-RU.lang index aded6c8..2610293 100644 --- a/mod/etc/options_menu/language/ru-RU.lang +++ b/mod/etc/options_menu/language/ru-RU.lang @@ -1,21 +1,26 @@ HIBERNATE=Спящий режим SHUTDOWN_DEVICE=Выключить устройство REBOOT_DEVICE=Перезагрузить устройство -RETROARCH_OPTIONS=Настройки RetroArch ADVANCED_OPTIONS=Расширенные настройки -NETWORK_OPTIONS=Настройки сети +NETWORK_OPTIONS=Сеть +SAVED_GAMES_OPTIONS=Сохранения EXIT=Выход MODULE_UNINSTALLER=Удаление модулей DISPLAY_TEMP=Показать температуру RUN_TOP=Запустить Top BENCHMARK_TOOL=Инструмент Benchmark DUMP_FILE_STRUCTURE=Сохранить структуру файлов (на USB) +BACKUP_SAVES_USB=Резервная копия сохранений (USB) +RESTORE_SAVES_USB=Восстановить сохранения (USB) +BACKUP_SAVES_USB_DONE=Сохранения скопированы на USB:/data/saves_backup +RESTORE_SAVES_USB_DONE=Сохранения восстановлены с USB:/data/saves_backup +RESTORE_SAVES_USB_NOT_FOUND=Резервная копия сохранений на USB не найдена. TOGGLE_USB_WRITE=Переключить запись на USB RA_DEBUGGER=Отладчик RA (логи на USB) CHANGE_BUTTON_COMBO=Изменить комбинацию кнопок CLEAR_CACHE=Очистить кэш BACK=Назад -LANGUAGE_OPTIONS=Настройки языка +LANGUAGE_OPTIONS=Язык DISPLAY_IP=Показать IP-адрес RECONNECT=Переподключиться SEARCH_SSIDS=Поиск SSID diff --git a/mod/etc/options_menu/network/commands/c0050_RestoreNand b/mod/etc/options_menu/network/commands/c0050_RestoreNand index 1a99f61..52cd569 100644 --- a/mod/etc/options_menu/network/commands/c0050_RestoreNand +++ b/mod/etc/options_menu/network/commands/c0050_RestoreNand @@ -1,5 +1,6 @@ COMMAND_NAME=WIFI_RESTORE_NAND COMMAND_TYPE=EXTERNAL +ENABLE_IF=[ -d "$rootfs/etc/wifi_backup" ] SUBMENU=TRUE COMMAND_STR=sh %script_dir%/restore_settings.sh nand cmd PREVIEW_IMAGE=/etc/options_menu/images/preview_placeholder.png diff --git a/mod/etc/options_menu/network/commands/c0060_RestoreUsb b/mod/etc/options_menu/network/commands/c0060_RestoreUsb index 645c8e8..e10550b 100644 --- a/mod/etc/options_menu/network/commands/c0060_RestoreUsb +++ b/mod/etc/options_menu/network/commands/c0060_RestoreUsb @@ -1,6 +1,7 @@ COMMAND_NAME=WIFI_RESTORE_USB COMMAND_TYPE=EXTERNAL USB_ONLY=TRUE +ENABLE_IF=[ -d /media/data/wifi_backup ] SUBMENU=TRUE COMMAND_STR=sh %script_dir%/restore_settings.sh usb PREVIEW_IMAGE=/etc/options_menu/images/preview_placeholder.png diff --git a/mod/etc/options_menu/retroarch/commands/c0090_ToggleLoadScreens b/mod/etc/options_menu/retroarch/commands/c0090_ToggleLoadScreens index 1318eb0..21d1dae 100644 --- a/mod/etc/options_menu/retroarch/commands/c0090_ToggleLoadScreens +++ b/mod/etc/options_menu/retroarch/commands/c0090_ToggleLoadScreens @@ -1,4 +1,5 @@ COMMAND_NAME=RA_TOGGLE_LOAD_SCREENS COMMAND_TYPE=INTERNAL +ENABLE_IF=[ -f "$rootfs/share/retroarch/assets/RAloading-min.png" ] COMMAND_STR=sh %script_dir%/toggle-loadscreens.sh PREVIEW_IMAGE=/etc/options_menu/images/preview_placeholder.png diff --git a/mod/etc/options_menu/saved_games/commands/c0010_BackupSavesUsb b/mod/etc/options_menu/saved_games/commands/c0010_BackupSavesUsb new file mode 100644 index 0000000..36d8908 --- /dev/null +++ b/mod/etc/options_menu/saved_games/commands/c0010_BackupSavesUsb @@ -0,0 +1,4 @@ +COMMAND_NAME=BACKUP_SAVES_USB +COMMAND_TYPE=INTERNAL +COMMAND_STR=sh %script_dir%/BackupSavesUsb.sh +PREVIEW_IMAGE=/etc/options_menu/images/preview_placeholder.png diff --git a/mod/etc/options_menu/saved_games/commands/c0020_RestoreSavesUsb b/mod/etc/options_menu/saved_games/commands/c0020_RestoreSavesUsb new file mode 100644 index 0000000..3e915f0 --- /dev/null +++ b/mod/etc/options_menu/saved_games/commands/c0020_RestoreSavesUsb @@ -0,0 +1,4 @@ +COMMAND_NAME=RESTORE_SAVES_USB +COMMAND_TYPE=INTERNAL +COMMAND_STR=sh %script_dir%/RestoreSavesUsb.sh +PREVIEW_IMAGE=/etc/options_menu/images/preview_placeholder.png diff --git a/mod/etc/options_menu/scripts/BackupSavesUsb.sh b/mod/etc/options_menu/scripts/BackupSavesUsb.sh new file mode 100644 index 0000000..3f82aa5 --- /dev/null +++ b/mod/etc/options_menu/scripts/BackupSavesUsb.sh @@ -0,0 +1,13 @@ +#!/bin/sh + +source /etc/options_menu/scripts/om_functions + +# /media/hakchi/saves also holds a few non-save housekeeping entries +# (hakchi's own state, home-menu, dimming/lang/mcp-state) - excluded so a +# later restore can't clobber live system state with a stale backup of it +mkdir -pm 777 /media/data/saves_backup +rsync -a --delete \ + --exclude=FOLDER --exclude=hakchi --exclude=home-menu \ + --exclude=dimming.txt --exclude=lang.txt --exclude=mcp-state \ + /media/hakchi/saves/ /media/data/saves_backup/ +translate BACKUP_SAVES_USB_DONE diff --git a/mod/etc/options_menu/scripts/RestoreSavesUsb.sh b/mod/etc/options_menu/scripts/RestoreSavesUsb.sh new file mode 100644 index 0000000..2d3e0d4 --- /dev/null +++ b/mod/etc/options_menu/scripts/RestoreSavesUsb.sh @@ -0,0 +1,11 @@ +#!/bin/sh + +source /etc/options_menu/scripts/om_functions + +if [ ! -d /media/data/saves_backup ]; then + translate RESTORE_SAVES_USB_NOT_FOUND + exit 1 +fi + +rsync -a /media/data/saves_backup/ /media/hakchi/saves/ +translate RESTORE_SAVES_USB_DONE diff --git a/mod/etc/options_menu/scripts/om_network b/mod/etc/options_menu/scripts/om_network index 47801a9..b5f2964 100644 --- a/mod/etc/options_menu/scripts/om_network +++ b/mod/etc/options_menu/scripts/om_network @@ -19,8 +19,4 @@ source $mountpoint/etc/options_menu/network/scripts/om_vars script_init -enable_cmds "RestoreUSB RestoreNAND" -[ ! -d "$usb_backup" ] && disable_cmds "RestoreUSB" -[ ! -d "$nand_backup" ] && disable_cmds "RestoreNAND" - usleep 50000 && $optionsMenu/options --commandPath $omNetworkCmds/ --scriptPath $omNetworkScripts --title "NETWORK_OPTIONS" & diff --git a/mod/etc/options_menu/scripts/om_retroarch b/mod/etc/options_menu/scripts/om_retroarch index 3f07c7a..6e5d5b7 100644 --- a/mod/etc/options_menu/scripts/om_retroarch +++ b/mod/etc/options_menu/scripts/om_retroarch @@ -19,14 +19,4 @@ source $mountpoint/etc/options_menu/retroarch/scripts/ra_vars script_init -omRetroarchCmds="$optionsMenu/retroarch/commands" -omRetroarchScripts="$optionsMenu/retroarch/scripts" -disableToggleLoadScreens="$omRetroarchCmds/_ToggleLoadScreens" -enableToggleLoadScreens="$omRetroarchCmds/c0090_ToggleLoadScreens" - -# retroarch-clover-child (Ozone builds) dropped the loading-screen swap entirely - -# nothing left for this option to toggle, so hide it instead of failing when picked -enable_cmds "ToggleLoadScreens" -[ ! -f "$rootfs/share/retroarch/assets/RAloading-min.png" ] && disable_cmds "ToggleLoadScreens" - -usleep 50000 && $optionsMenu/options --commandPath $omRetroarchCmds/ --scriptPath $omRetroarchScripts --title "RETROARCH_OPTIONS" & +usleep 50000 && $optionsMenu/options --commandPath $optionsMenu/retroarch/commands/ --scriptPath $optionsMenu/retroarch/scripts --title "RETROARCH_OPTIONS" & From 2a0218f343ae4762f009b253a123c4bd124c1167 Mon Sep 17 00:00:00 2001 From: DefKorns Date: Sun, 20 Sep 2026 18:31:14 +0000 Subject: [PATCH 036/104] Refactor Makefile for cleanup and streamline binary stripping; enhance command template expansion in main.cpp --- Makefile.docker | 53 ++++++++++++++++++++++--------------- src/main.cpp | 69 ++++++++++++++++++++++++++++--------------------- 2 files changed, 71 insertions(+), 51 deletions(-) diff --git a/Makefile.docker b/Makefile.docker index 61e9406..ab1de50 100644 --- a/Makefile.docker +++ b/Makefile.docker @@ -26,6 +26,10 @@ ifdef FIX_LIBSTDCXX VENDORED_LIBS += mod/etc/options_menu/lib/libstdc++.so.6 STDCXX_RPATH = -Wl,-rpath,/etc/options_menu/lib STDCXX_RPATH_ORIGIN = -Wl,-rpath,'$$ORIGIN/lib' +else +# a prior FIX_LIBSTDCXX=1 build leaves this behind - without FIX_LIBSTDCXX the +# tar step would silently bundle it into what's meant to be the plain build +STDCXX_CLEANUP = rm -f mod/etc/options_menu/lib/libstdc++.so.6 endif else SDL_CFLAGS = $(shell sdl2-config --cflags) $(shell pkg-config --cflags SDL2_ttf) @@ -59,22 +63,29 @@ define inject-readme = sed 1d temp/readme.md >> mod/readme.md endef +define strip-bin = + $(CROSS_PREFIX)$(STRIP) $(1) +endef + +define strip-and-upx = + $(CROSS_PREFIX)$(STRIP) $(1) + upx --lzma $(1) +endef + all: $(OUT)/$(TARGET_NAME).hmod bump-build-number: echo $$(( $$(cat $(BUILD_NUM_FILE) 2>/dev/null || echo 0) + 1 )) > $(BUILD_NUM_FILE) $(OUT)/$(TARGET_NAME).hmod: mod/etc/options_menu/options mod/etc/options_menu/optiond mod/etc/options_menu/mod_uninstall/mod_uninstall mod/bin/standby_watchdog mod/etc/options_menu/scripts/gen_splash mod/etc/options_menu/scripts/ChangeCombo $(VENDORED_LIBS) bump-build-number - $(CROSS_PREFIX)$(STRIP) mod/etc/options_menu/options - $(CROSS_PREFIX)$(STRIP) mod/etc/options_menu/optiond - $(CROSS_PREFIX)$(STRIP) mod/etc/options_menu/mod_uninstall/mod_uninstall - upx --lzma mod/etc/options_menu/mod_uninstall/mod_uninstall - $(CROSS_PREFIX)$(STRIP) mod/bin/standby_watchdog - upx --lzma mod/bin/standby_watchdog - $(CROSS_PREFIX)$(STRIP) mod/etc/options_menu/scripts/gen_splash - $(CROSS_PREFIX)$(STRIP) mod/etc/options_menu/scripts/ChangeCombo - upx --lzma mod/etc/options_menu/scripts/ChangeCombo + $(call strip-bin,mod/etc/options_menu/options) + $(call strip-bin,mod/etc/options_menu/optiond) + $(call strip-and-upx,mod/etc/options_menu/mod_uninstall/mod_uninstall) + $(call strip-and-upx,mod/bin/standby_watchdog) + $(call strip-bin,mod/etc/options_menu/scripts/gen_splash) + $(call strip-and-upx,mod/etc/options_menu/scripts/ChangeCombo) $(inject-readme) + $(STDCXX_CLEANUP) cd mod/; tar -czvf "../out/$(TARGET_NAME).hmod" * @@ -87,19 +98,19 @@ $(OUT)/$(TARGET_NAME).hmod: mod/etc/options_menu/options mod/etc/options_menu/op mod/etc/options_menu/options: $(OBJECTS) $(CROSS_PREFIX)$(CXX) $(OBJECTS) $(LDLIBS) $(LDFLAGS) -Wl,-rpath,'$$ORIGIN/lib' -o mod/etc/options_menu/options +mod/etc/options_menu/lib: + mkdir -p $@ + # not ours to redistribute, pulled from the toolchain image each build -mod/etc/options_menu/lib/libSDL2_ttf-2.0.so.0: - mkdir -p mod/etc/options_menu/lib - cp /usr/lib/arm-linux-gnueabihf/libSDL2_ttf.so mod/etc/options_menu/lib/libSDL2_ttf-2.0.so.0 - -mod/etc/options_menu/lib/libfreetype.so.2.8: - mkdir -p mod/etc/options_menu/lib - cp /usr/lib/arm-linux-gnueabihf/libfreetype.so.2.8.1 mod/etc/options_menu/lib/libfreetype.so.2.8 - -mod/etc/options_menu/lib/libstdc++.so.6: - mkdir -p mod/etc/options_menu/lib - cp $$($(CROSS_PREFIX)$(CXX) -print-file-name=libstdc++.so.6) mod/etc/options_menu/lib/libstdc++.so.6 - $(CROSS_PREFIX)$(STRIP) --strip-unneeded mod/etc/options_menu/lib/libstdc++.so.6 +mod/etc/options_menu/lib/libSDL2_ttf-2.0.so.0: | mod/etc/options_menu/lib + cp /usr/lib/arm-linux-gnueabihf/libSDL2_ttf.so $@ + +mod/etc/options_menu/lib/libfreetype.so.2.8: | mod/etc/options_menu/lib + cp /usr/lib/arm-linux-gnueabihf/libfreetype.so.2.8.1 $@ + +mod/etc/options_menu/lib/libstdc++.so.6: | mod/etc/options_menu/lib + cp $$($(CROSS_PREFIX)$(CXX) -print-file-name=libstdc++.so.6) $@ + $(CROSS_PREFIX)$(STRIP) --strip-unneeded $@ mod/etc/options_menu/optiond: src/daemon.o src/framework/controller.o $(CROSS_PREFIX)$(CXX) $(LDFLAGS) src/daemon.o src/framework/controller.o $(STDCXX_RPATH_ORIGIN) -o mod/etc/options_menu/optiond diff --git a/src/main.cpp b/src/main.cpp index a9662a1..14587c7 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -122,6 +122,12 @@ int main(int argc, char * argv[]) backStack = (lastSep == std::string::npos) ? "" : backStack.substr(0, lastSep); setenv("OM_BACK_STACK", (backStack.empty() ? myEntry : backStack + ";" + myEntry).c_str(), 1); + auto ExpandTemplates = [&](std::string & cmd) + { + sReplace(cmd, "%options_path%", optionsLocation); + sReplace(cmd, "%script_dir%", scriptLocation); + }; + //Read commands from command folder std::vector commands; std::ifstream in; @@ -147,14 +153,19 @@ int main(int argc, char * argv[]) { if(!c.usbOnly || (c.usbOnly && usbReady)) { - sReplace(c.command, "%options_path%", optionsLocation); - sReplace(c.command, "%script_dir%", scriptLocation); - sReplace(c.deleteCommand, "%options_path%", optionsLocation); - sReplace(c.deleteCommand, "%script_dir%", scriptLocation); + if(!c.enableIfCommand.empty()) + { + ExpandTemplates(c.enableIfCommand); + // ENABLE_IF conditions reference $rootfs/$mountpoint like any + // other options_menu script, so source preinit the same way + if(system(("source /etc/preinit; script_init; " + c.enableIfCommand).c_str()) != 0) + continue; + } + ExpandTemplates(c.command); + ExpandTemplates(c.deleteCommand); if(c.isToggle) { - sReplace(c.stateCommand, "%options_path%", optionsLocation); - sReplace(c.stateCommand, "%script_dir%", scriptLocation); + ExpandTemplates(c.stateCommand); c.UpdateState(); } commands.push_back(c); @@ -259,10 +270,14 @@ int main(int argc, char * argv[]) //Badge cluster (top-right hints): A/B always shown, X only if the row has a delete action struct Badge { Texture letter; Texture label; UiTheme::BadgeColor rim; UiTheme::BadgeColor fill; }; - Badge badgeA{ Texture("A", 16, renderer, 0, 0, false, UiTheme::BadgeLetterColor, true), Texture(Translate("HINT_SELECT"), 16, renderer, 0, 0, false, UiTheme::TextColor, true), UiTheme::BadgeADark, UiTheme::BadgeA }; - Badge badgeB{ Texture("B", 16, renderer, 0, 0, false, UiTheme::BadgeLetterColor, true), Texture(Translate("HINT_BACK"), 16, renderer, 0, 0, false, UiTheme::TextColor, true), UiTheme::BadgeBDark, UiTheme::BadgeB }; + auto MakeBadge = [&](const char * letter, const char * hintKey, UiTheme::BadgeColor rim, UiTheme::BadgeColor fill) -> Badge + { + return { Texture(letter, 16, renderer, 0, 0, false, UiTheme::BadgeLetterColor, true), Texture(Translate(hintKey), 16, renderer, 0, 0, false, UiTheme::TextColor, true), rim, fill }; + }; + Badge badgeA = MakeBadge("A", "HINT_SELECT", UiTheme::BadgeADark, UiTheme::BadgeA); + Badge badgeB = MakeBadge("B", "HINT_BACK", UiTheme::BadgeBDark, UiTheme::BadgeB); // hold-to-delete hint, different color than the real B badge - Badge badgeHold{ Texture("B", 16, renderer, 0, 0, false, UiTheme::BadgeLetterColor, true), Texture(Translate("HINT_DELETE"), 16, renderer, 0, 0, false, UiTheme::TextColor, true), UiTheme::BadgeXDark, UiTheme::BadgeX }; + Badge badgeHold = MakeBadge("B", "HINT_DELETE", UiTheme::BadgeXDark, UiTheme::BadgeX); auto DrawBadge = [&](Badge & badge, int rightEdgeX) -> int { int groupW = UiTheme::BadgeOuterSize + UiTheme::BadgeLabelGap + badge.label.rect.w; @@ -406,6 +421,19 @@ int main(int argc, char * argv[]) }; SetCurrentCommand(0); + // bounded so an all-headers list can't spin forever + auto MoveSelection = [&](int step) + { + int newCommandId = currentCommandId; + for(size_t tries = 0; tries < commands.size(); ++tries) + { + newCommandId = (newCommandId + step + commands.size()) % commands.size(); + if(commands[newCommandId].command.size() != 0) + break; + } + SetCurrentCommand(newCommandId); + }; + auto DrawRow = [&](Command & rowCommand, bool isLastOverall) { bool selected = &rowCommand == &commands[currentCommandId]; @@ -535,28 +563,9 @@ int main(int argc, char * argv[]) } } else if(controller.HeldRepeat(UP)) - { - // bounded so an all-headers list can't spin forever - int newCommandId = currentCommandId; - for(size_t tries = 0; tries < commands.size(); ++tries) - { - newCommandId = (newCommandId-1+commands.size())%commands.size(); - if(commands[newCommandId].command.size() != 0) - break; - } - SetCurrentCommand(newCommandId); - } + MoveSelection(-1); else if(controller.HeldRepeat(DOWN)) - { - int newCommandId = currentCommandId; - for(size_t tries = 0; tries < commands.size(); ++tries) - { - newCommandId = (newCommandId+1)%commands.size(); - if(commands[newCommandId].command.size() != 0) - break; - } - SetCurrentCommand(newCommandId); - } + MoveSelection(1); // tap = jump to last item, hold ~1s = delete bool bHeldNow = controller.PeekButtonStatus(B); From c0f5470d238cb0d40b19dd98ec8d9663005e7300 Mon Sep 17 00:00:00 2001 From: DefKorns Date: Sun, 20 Sep 2026 18:33:20 +0000 Subject: [PATCH 037/104] Add conditional check for badgeB drawing based on backStack state --- src/main.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main.cpp b/src/main.cpp index 14587c7..9321b6f 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -486,7 +486,8 @@ int main(int argc, char * argv[]) // footer: A rightmost, B left of it, hold-to-delete hint further left int rightEdge = UiTheme::BadgeClusterRightX; rightEdge = DrawBadge(badgeA, rightEdge); - rightEdge = DrawBadge(badgeB, rightEdge); + if(!backStack.empty()) + rightEdge = DrawBadge(badgeB, rightEdge); if(!commands[currentCommandId].deleteCommand.empty()) rightEdge = DrawBadge(badgeHold, rightEdge); int dividerX = rightEdge - UiTheme::BadgeDividerGapFromCluster; From 565e0919e7525e729da776af659ccebc9b2cd23b Mon Sep 17 00:00:00 2001 From: DefKorns Date: Sun, 20 Sep 2026 18:51:23 +0000 Subject: [PATCH 038/104] Implement back command logic for root screen; modify badge behavior for menu closure --- src/main.cpp | 44 +++++++++++++++++++++++++------------------- 1 file changed, 25 insertions(+), 19 deletions(-) diff --git a/src/main.cpp b/src/main.cpp index 9321b6f..26ac6ee 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -187,9 +187,11 @@ int main(int argc, char * argv[]) exit(1); } - // synthesized, not read from c9999_Back/c9999_Exit files - Back only if backStack has an entry to pop + // B goes straight back (see the B-handling block below) rather than through a + // visible row, so only the root screen (no ancestors to go back to) gets Exit static const char * const NavPreviewImage = "/etc/options_menu/images/preview_placeholder.png"; const int pinnedStartIndex = static_cast(commands.size()); + std::string backCommand; if(!backStack.empty()) { // pop the last entry (our parent), re-join the rest for the parent's own Back chain @@ -215,24 +217,21 @@ int main(int argc, char * argv[]) std::string backScriptPath = lastEntry.substr(c1+1, c2-c1-1); std::string backTitleKey = lastEntry.substr(c2+1); - Command back; - back.name = "BACK"; - back.runInternal = false; - back.restartUI = false; - back.previewImage = NavPreviewImage; // env prefix overrides OM_BACK_STACK for just this launch, to the parent's own remaining ancestors - back.command = "usleep 50000 && OM_BACK_STACK=\"" + remainingStack + "\" " + optionsLocation + "options --commandPath " + backPath + backCommand = "usleep 50000 && OM_BACK_STACK=\"" + remainingStack + "\" " + optionsLocation + "options --commandPath " + backPath + (backScriptPath.empty() ? "" : " --scriptPath " + backScriptPath) + " --title \"" + backTitleKey + "\" &"; - commands.push_back(back); } - Command exitCmd; - exitCmd.name = "EXIT"; - exitCmd.runInternal = false; - exitCmd.restartUI = true; - exitCmd.previewImage = NavPreviewImage; - exitCmd.command = "echo \"Closing Options Menu\""; - commands.push_back(exitCmd); + else + { + Command exitCmd; + exitCmd.name = "EXIT"; + exitCmd.runInternal = false; + exitCmd.restartUI = true; + exitCmd.previewImage = NavPreviewImage; + exitCmd.command = "echo \"Closing Options Menu\""; + commands.push_back(exitCmd); + } int currentCommandId = 0; @@ -276,6 +275,8 @@ int main(int argc, char * argv[]) }; Badge badgeA = MakeBadge("A", "HINT_SELECT", UiTheme::BadgeADark, UiTheme::BadgeA); Badge badgeB = MakeBadge("B", "HINT_BACK", UiTheme::BadgeBDark, UiTheme::BadgeB); + // root screen: B closes the menu instead of going back + Badge badgeBExit = MakeBadge("B", "EXIT", UiTheme::BadgeBDark, UiTheme::BadgeB); // hold-to-delete hint, different color than the real B badge Badge badgeHold = MakeBadge("B", "HINT_DELETE", UiTheme::BadgeXDark, UiTheme::BadgeX); auto DrawBadge = [&](Badge & badge, int rightEdgeX) -> int @@ -486,8 +487,7 @@ int main(int argc, char * argv[]) // footer: A rightmost, B left of it, hold-to-delete hint further left int rightEdge = UiTheme::BadgeClusterRightX; rightEdge = DrawBadge(badgeA, rightEdge); - if(!backStack.empty()) - rightEdge = DrawBadge(badgeB, rightEdge); + rightEdge = DrawBadge(backStack.empty() ? badgeBExit : badgeB, rightEdge); if(!commands[currentCommandId].deleteCommand.empty()) rightEdge = DrawBadge(badgeHold, rightEdge); int dividerX = rightEdge - UiTheme::BadgeDividerGapFromCluster; @@ -568,7 +568,7 @@ int main(int argc, char * argv[]) else if(controller.HeldRepeat(DOWN)) MoveSelection(1); - // tap = jump to last item, hold ~1s = delete + // tap = go back, or close the menu on the root screen; hold ~1s = delete bool bHeldNow = controller.PeekButtonStatus(B); if(bHeldNow) { @@ -582,7 +582,13 @@ int main(int argc, char * argv[]) else { if(bWasHeld && !bHoldFired) - SetCurrentCommand(commands.size()-1); + { + // root screen: no backCommand, so this closes the menu instead + if(!backCommand.empty()) + system(backCommand.c_str()); + _exitManager.runExitCommand = backCommand.empty(); + break; + } bHoldFired = false; } bWasHeld = bHeldNow; From 89ab97828a5add13c97aaabb41813325d2aeafb8 Mon Sep 17 00:00:00 2001 From: DefKorns Date: Sun, 20 Sep 2026 23:42:00 +0000 Subject: [PATCH 039/104] Update build configuration to use classicmini-cross-toolchain for Docker builds --- .github/workflows/build.yml | 4 ++-- .gitmodules | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 097b54a..b9fc587 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -26,7 +26,7 @@ jobs: git clone --depth 1 "https://x-access-token:${{ secrets.VENDOR_REPO_PAT }}@github.com/${{ secrets.VENDOR_REPO }}.git" toolchain/vendor - name: Build cross-compile toolchain image - run: docker build -f toolchain/Dockerfile.jessie-armhf -t hakchi-toolchain:jessie-armhf toolchain + run: docker build -f toolchain/Dockerfile.jessie-armhf -t classicmini-cross-toolchain:jessie-armhf toolchain # On a tag push (release build), force MOD_VER to the tag name itself # rather than trusting `git describe` inside the container -- checkout @@ -40,7 +40,7 @@ jobs: if [ "${{ github.ref_type }}" = "tag" ]; then MOD_VER_ARG="MOD_VER=${{ github.ref_name }}" fi - docker run --rm -v "${{ github.workspace }}:/src" hakchi-toolchain:jessie-armhf make -f Makefile.docker CROSS_PREFIX=arm-linux-gnueabihf- $MOD_VER_ARG + docker run --rm -v "${{ github.workspace }}:/src" classicmini-cross-toolchain:jessie-armhf make -f Makefile.docker CROSS_PREFIX=arm-linux-gnueabihf- $MOD_VER_ARG - name: Upload .hmod build artifact uses: actions/upload-artifact@v7 diff --git a/.gitmodules b/.gitmodules index a371cc1..0fc596f 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,3 @@ [submodule "toolchain"] path = toolchain - url = https://github.com/DefKorns/hakchi-toolchain.git + url = https://github.com/DefKorns/classicmini-cross-toolchain.git From d291937799bcbcf89b899a4e52f77bf2415e8cd4 Mon Sep 17 00:00:00 2001 From: DefKorns Date: Sun, 20 Sep 2026 23:45:58 +0000 Subject: [PATCH 040/104] Bump toolchain submodule to pick up git/rsync and the classicmini-cross-toolchain rename Co-Authored-By: Claude Sonnet 5 --- toolchain | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/toolchain b/toolchain index 216f5ab..8d9428e 160000 --- a/toolchain +++ b/toolchain @@ -1 +1 @@ -Subproject commit 216f5abcf008e285c3e833c223538518fc9da1ff +Subproject commit 8d9428e2a2e69cb33987390587f7d1d134e0316a From 6baf1bc67fe99c421ee291d2a5788dbe23cf1ca9 Mon Sep 17 00:00:00 2001 From: DefKorns Date: Mon, 21 Sep 2026 00:22:52 +0000 Subject: [PATCH 041/104] Refactor comments for clarity in command and main files --- src/command.cpp | 4 ++-- src/main.cpp | 10 ++++------ 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/src/command.cpp b/src/command.cpp index 185f8f5..026c514 100644 --- a/src/command.cpp +++ b/src/command.cpp @@ -171,8 +171,8 @@ void Command::RunCommand(SDL_Context & sdl_context, Controller * controller, con if(!feof(pipe)) clearerr(pipe); // last fgets() failed because nothing's ready yet, not EOF - stay readable - // ignoreInterrupt commands (e.g. ChangeCombo) read the controller themselves - - // stop polling it here too, or we race them for the same button events + // ignoreInterrupt commands read the controller directly + // stop polling here to avoid racing for the same button events if(!ignoreInterrupt) { controller->Update(); diff --git a/src/main.cpp b/src/main.cpp index 26ac6ee..2580f37 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -110,12 +110,10 @@ int main(int argc, char * argv[]) commandLocation = CollapseSlashes(commandLocation); scriptLocation = CollapseSlashes(scriptLocation); - // what a child screen inherits as its own OM_BACK_STACK + // value inherited by child screens as OM_BACK_STACK std::string myEntry = commandLocation + "," + scriptLocation + "," + titleKey; - // a self-relaunch (e.g. after changing language) re-enters the same screen - the - // stack we received already ends in an entry for "this screen" from the ORIGINAL - // navigation into it. Strip that stale self-reference so both the Back row below - // and what we export to our own children point at our real parent, not ourselves + // a self-relaunch re-enters the same screen with an existing stack entry + // remove it so Back and child screens point to the parent size_t lastSep = backStack.find_last_of(';'); std::string lastEntry = backStack.empty() ? "" : (lastSep == std::string::npos ? backStack : backStack.substr(lastSep + 1)); if(lastEntry == myEntry) @@ -194,7 +192,7 @@ int main(int argc, char * argv[]) std::string backCommand; if(!backStack.empty()) { - // pop the last entry (our parent), re-join the rest for the parent's own Back chain + // pop the last entry (parent), re-join the rest for the parent's Back chain std::vector entries; for(size_t start = 0; start < backStack.size();) { From 2bce3c180def93bf218acaaf61c5e35c19b88789 Mon Sep 17 00:00:00 2001 From: DefKorns Date: Mon, 21 Sep 2026 00:28:48 +0000 Subject: [PATCH 042/104] Refactor comments for clarity and consistency across multiple files --- src/command.cpp | 2 +- src/main.cpp | 8 +++----- src/mod_uninstall.cpp | 5 ++--- 3 files changed, 6 insertions(+), 9 deletions(-) diff --git a/src/command.cpp b/src/command.cpp index 026c514..d3a3470 100644 --- a/src/command.cpp +++ b/src/command.cpp @@ -205,7 +205,7 @@ void Command::UpdateState() char buffer[64] = {0}; std::string result; - // state scripts are expected to be near-instant; don't let a stuck one block menu load + // Prevent stalled state scripts from blocking menu load. struct pollfd pfd{fileno(pipe), POLLIN, 0}; if(poll(&pfd, 1, 1000) > 0 && fgets(buffer, sizeof(buffer), pipe)) result = buffer; diff --git a/src/main.cpp b/src/main.cpp index 2580f37..db69c73 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -38,9 +38,7 @@ void sReplace(std::string & command, std::string oldString, std::string newStrin } } -// "%options_path%/xxx" command templates always produce a double slash (optionsLocation -// already ends in one) - collapsing here stops it from growing an extra slash with every -// relaunch, which otherwise breaks string-equality checks like the self-relaunch one below +// Collapse duplicate slashes for stable path comparisons. std::string CollapseSlashes(const std::string & path) { std::string result; @@ -323,7 +321,7 @@ int main(int argc, char * argv[]) int textX = UiTheme::RowTextX + (c.child ? ChildIndent : 0); std::string label = Translate(c.name); - // fixed list-column width regardless of preview - previews live in the detail panel, not floating on the row + // Keep previews inside the detail panel. int maxRight = UiTheme::RowControlRightX - RowTextGapPx; bool rowUsesTTF = CanRenderWithTTF(label, RowGlyphSize); int available = std::max(0, maxRight - textX); @@ -455,7 +453,7 @@ int main(int argc, char * argv[]) DrawHLine(renderer, UiTheme::ListX, UiTheme::ListContentRightX, rowCommand.texture.rect.y + rowCommand.texture.rect.h + 3, UiTheme::BorderR, UiTheme::BorderG, UiTheme::BorderB); }; - // called early, before DrawRow - must run first so its background is behind the row text + // Draw the background before row text. auto DrawChrome = [&]() { // one continuous border across header+body+footer, plus the footer divider diff --git a/src/mod_uninstall.cpp b/src/mod_uninstall.cpp index 0cd5707..653f454 100644 --- a/src/mod_uninstall.cpp +++ b/src/mod_uninstall.cpp @@ -111,8 +111,7 @@ int main(int argc, char * argv[]) for(Mod & mod : installedMods) mod.text = Texture(TruncateToWidth(mod.name, ColumnW), ListFontSize, renderer, LeftColumnX, 0, false, UiTheme::TextColor, true); - // this screen's list spans a different vertical range than the main menu's, so the - // chevrons are anchored to it directly rather than reusing UiTheme::ScrollUpY/DownY + // Anchor chevrons to the local list range. Texture scrollUp("^", 16, renderer, UiTheme::ScrollX, ListTopY + 10, false, UiTheme::TextColor, true); scrollUp.rect.x -= scrollUp.rect.w / 2; Texture scrollDown = scrollUp; @@ -144,7 +143,7 @@ int main(int argc, char * argv[]) return x - UiTheme::BadgeGroupGap; }; - // wide "Start" chip instead of a single-letter circle - "St" alone reads ambiguous next to Select + // Use a wide chip to distinguish Start from Select. Texture pillText("Start", 14, renderer, 0, 0, false, UiTheme::BadgeLetterColor, true); Texture uninstallLabel(Translate("HINT_UNINSTALL"), 16, renderer, 0, 0, false, UiTheme::TextColor, true); auto DrawPillBadge = [&](Texture & pill, Texture & label, const UiTheme::BadgeColor & rim, const UiTheme::BadgeColor & fill, int rightEdgeX) -> int From 406ad3d617de3df49cb5aaa8aa7e81deb9ad4227 Mon Sep 17 00:00:00 2001 From: DefKorns Date: Wed, 23 Sep 2026 12:06:09 +0000 Subject: [PATCH 043/104] Make point buffers static in draw_helpers to avoid per-frame allocation --- src/framework/draw_helpers.cpp | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/framework/draw_helpers.cpp b/src/framework/draw_helpers.cpp index 69129d8..e5f6418 100644 --- a/src/framework/draw_helpers.cpp +++ b/src/framework/draw_helpers.cpp @@ -54,7 +54,9 @@ void DrawStrokeRect(SDL_Renderer * r, SDL_Rect rect, Uint8 cr, Uint8 cg, Uint8 c if(radius <= 0) return; const std::vector & arc = RoundedCornerArc(radius, width); - std::vector points; + // reused across calls (single render thread) to avoid per-frame heap allocation + static std::vector points; + points.clear(); points.reserve(arc.size() * 4); for(const SDL_Point & p : arc) { @@ -101,7 +103,8 @@ void DrawRoundedFillRect(SDL_Renderer * r, SDL_Rect rect, Uint8 cr, Uint8 cg, Ui SDL_RenderFillRect(r, &mid); SDL_RenderFillRect(r, &midV); const std::vector & disk = RoundedCornerDisk(radius); - std::vector points; + static std::vector points; // reused - see DrawStrokeRect + points.clear(); points.reserve(disk.size() * 4); for(const SDL_Point & p : disk) { @@ -113,9 +116,8 @@ void DrawRoundedFillRect(SDL_Renderer * r, SDL_Rect rect, Uint8 cr, Uint8 cg, Ui SDL_RenderDrawPoints(r, points.data(), static_cast(points.size())); } -// complement of RoundedCornerDisk - the corner-square pixels a rounded rect -// of this radius would NOT cover, i.e. what needs painting over to fake -// rounding a rectangularly-clipped image +// complement of RoundedCornerDisk: corner-square pixels outside the rounded rect, +// for painting over a rectangularly-clipped image to fake rounding static const std::vector & RoundedCornerOutside(int radius) { static std::map> cache; @@ -137,7 +139,8 @@ void DrawRoundedCornerMask(SDL_Renderer * r, SDL_Rect rect, Uint8 cr, Uint8 cg, SDL_SetRenderDrawColor(r, cr, cg, cb, 0xFF); int x0 = rect.x, y0 = rect.y, x1 = rect.x+rect.w-1, y1 = rect.y+rect.h-1; const std::vector & outside = RoundedCornerOutside(radius); - std::vector points; + static std::vector points; // reused - see DrawStrokeRect + points.clear(); points.reserve(outside.size() * 4); for(const SDL_Point & p : outside) { From faf900efa121c0ee0d8db361804597845651c50a Mon Sep 17 00:00:00 2001 From: DefKorns Date: Thu, 24 Sep 2026 23:55:45 +0000 Subject: [PATCH 044/104] Replace hand-drawn preview icons with Bootstrap Icons --- .../options_menu/images/preview_advanced.png | Bin 3183 -> 4547 bytes .../options_menu/images/preview_hibernate.png | Bin 3611 -> 4474 bytes .../options_menu/images/preview_network.png | Bin 5051 -> 4810 bytes .../images/preview_placeholder.png | Bin 3260 -> 3344 bytes .../options_menu/images/preview_reboot.png | Bin 3840 -> 4186 bytes .../options_menu/images/preview_retroarch.png | Bin 4232 -> 4219 bytes .../options_menu/images/preview_shutdown.png | Bin 3418 -> 4035 bytes mod/etc/options_menu/images/ui/gear.png | Bin 1739 -> 772 bytes 8 files changed, 0 insertions(+), 0 deletions(-) diff --git a/mod/etc/options_menu/images/preview_advanced.png b/mod/etc/options_menu/images/preview_advanced.png index f37cd6363270c931b1b36dfaea4b696aedd3d682..2769683c053825eb7dfb6080a8ae1146b01b6a02 100644 GIT binary patch literal 4547 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 literal 3183 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 Tyr=(VDuA(pg?_c3$CLj7qZ?q~ literal 3611 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 diff --git a/mod/etc/options_menu/images/preview_network.png b/mod/etc/options_menu/images/preview_network.png index e6e50b24ada6e86e1528d0eec6ea4061421ecd13..208eac2b01273d65bed2b47938871b8121616ea4 100644 GIT binary patch literal 4810 zcmcIoS5T7;u>BIMpmaf`8hQy$nu-WW6_6Tw4b4I?KOI8vO%OzyR3Xv?0i}c}Ej0N> zLpAgwBq2x_0$l&M`+ny>?C#9Y&d!-R54-227(dpzMt74A0DxI2(Mx?~ z{f$3GPhLzyk8@Pdq`F`HL%r3rr|H)p8bGA=s54?rG>T8qSK({S@(4^iU0gTk2KKn| z$Fu3q`q|@RTqNK3f~|{Qq#3TGYvRsXao1(*u|qhqjS^am1ell$AC(0Gk~A{soVU)H zE#4j{V|FS^ZMCM`dEA{eXq{`fKLb)NZN{d?r zP@b^O@B&bDONu}TMM#Ka-s|jJzu5qtP+SxzqS9Wpj5+7|h*W`m@+t?Y`x+VqG=iGd zHKUa4Ct2kr%@6AU2dbrMP(9`D9e`u=eFxi$GA}xe&xPD0CXUK-VQ4srC*1o2C~O_h zhZ*!TF5LlC#UNZYLxNLPC+#+{2lYrx>eELlXQo+etCF~FmJNNJr4Z9ZC zc2!p9z59k0-XaB~z37J=Cu?CYGO=FFTQ>F_IrnFe;eRU?OW_6{N{#jeURCXrKW39P{V7_FY)P+1^v{RKfM&qQ>*Jo_O$&5 z4ucKc0ok%{BaWAh=!fnXZQd3`XEqa5y8z;zouTJ_jFt1@0vAmY>mLxJ55zt7n^P-5 zB$N#;kY;bJ!Oo0#59)bu<=iO~W0mH7wDfq}pIPp!LA;a(Q!$=@?F#(a!x=b}X$<&Q z6_`pxmx?Yvn~sh4)^$-P4RK@sc=I=Ev~_AR|JQ+k^?5ZCAJSc2IO?39NIQNoUN{s1 zeV72A?STIh5>3s5tWQUn&MI&Rk7SK&>YM>|t}(lmk|j*BmNUaEN)~lczcuyJ)R5{+ zub3AFr|#FI7!f#LgNQB<_KTErC?u<32s$ybrbACdC8yKfa$LH>&ipgC$>42J{`v7`;j5^!@GqqF;|24v2+-gDH7~7uItfeV8wF6@3lj z?Vl`BoZ ziRJ*QMNkI$4E{$6aAeP!2Lc5G{77p?=}CXtsM1gg?P*Ico)ZbvUUH<^jfEHJHGTa3 z!?CtlLgBe(lU1F<_Q}bH0?L|wx(W1=8%@ppN4*pE1g4Dki2>JrxN(u!M%T`bZUTp} zBEFOBcmi|z_cr4uoBSDGbXBVavyZWmj%JTkVdoHpk!{P!6D`rae?+q%rIjNU-$dO; zbO6n=+G43+fn%9Ytxw_d_tAXDfEn~`Q_8l;sl{qcc~^VU+ZbyEJ`rJle-O_0h=MJ7 zwXUghAcPvx0Sb91YtFFYQw5NZP=NsrS$a6zK#^}yJ4E&Re9ldkIpct^gxOAO@ zPN0+bR=TcgG!)Rsffn`&o0_i3uDJqSIW~q}8o-*2v8zLm__wIL?O;CQ;?agT3Z?6|KV5wII>ewZIoA_lazJSo z5rsgOZe1SWDCnw-7NT*R(KV!HQMoCw9>*oeR$1@cYX7|Ju5XbJ1l;?Gv6KP!0RU6E#y@)JezI0L{M&?qO~-LL zL5L4?PEloSp@PV#*5muZEb2z)sLMP)m&yAv&uz(#n=F%oV^Qn6ZaaUnXHgLUf@E@v zEn2mxv1_vxCbL{N-xL0}EI^W)AaMVexJ*pFcVT2^Xq=FDeq=|or>@aR_smlZyvEI` z`1*t4AqCZG|Jn&(TYQiFc*#s>mQEy3vX?vOyf7q7f%~?wz*!ic{cFCk)js$Fir45= zAoaUCv?ODS&1W)rt-XUQZFUFB~dj{eEUIe8JrA}m;U^|f8>&VWw zY9l_1rAD7{n|5zL{oK<#jk^K7ws~$%emZ5(3sDTP$OPoVq;KPy`C;u19q0ZV^Xg%K zzolW=cQCBQCawXAkBnfcvEekn5sE$wgq_>NaJ9&LH{-J<$mV5qj;!X%;+dAb9&d}v zC(N*Yb0fB&8W+eJy^ibqnh%b`gI*$0PUd);L3@5}le*~;jwge`ZRcq&69#aXYO=qM94X6z#(AaPLBw&OY?Y@-g+%A z^=4T8aW`%>-^rWs7GFie=D_^Dz`2^#hQJ)3p8B<$tT7Pb9)pdX?%A&Jw^e(V4UXQ2 zti2maT5AhT(;q(-rN2~5|6(_H8#GPb>Y(>!e%dBoABf;Vb4{vQoJSUkY5+&`RwXxf zc0;{%7xscpe!E<@w-IjTm##yvwRlmjSsMO8o>qL^n6r|wZ|$dW{n;M$DDh=3%qNII)aGXs#Mbi#Vl?)hk5@F%_sf2 znd7y?>$7Oo6mDc+^Y)}WE~p@LS69qkvAyuTOiY7DOX9m=z9_ybjYtH(Dt2%&|4H&A z+`UFeHK{gp0y`{0y`rt_nmiJewqJt2zy$43_gZUumj8GY7`f2LZi*w(H;l;5MU2>F zx$fx$QH@VzD^v=9g&Cf%NbF173P3nA#=8e4%hl$(U=|>0$ymO;e%5>96 zbY^>xrU#qdC{r9UR$o?xvnICB0ud=j+NCbgI3w-B-<4XULwIQH-Z807z5B{5=&*rk zcHzN_W2hA$J~NPO$1G5%V&z-}(F^j0$-d*suU=3b68T_Jara1(qk@xR40i#HM19ikqqc81{AvfLA6^~taAP>a1$qx<>V+AY_D(DY@*rzRp=k{ka2eJtyX4phQ@#K@WXi#oyTm9Qd|z1cz~R?1Ti2*F zXLxU~@NQsi2D8@W{28iv{(d8pbdMVOTA8&cGK3ngg*FBUM1jz0D!cQyDAumbZhf2c zl3LU5u!!u7Gu})lVl9MNPT7`|1}7t36^;2LpNz0!wK+44VO~e)l6<4}``+W}IPn^6 zCjGBMlr^v&JVRN5v~lW;eqa3=(o(%ehEBIyR`8i7fvx%dEeF2PfllrN9%0jBPy9^@ z9M@4-qo7hBzDKks6BTJMKPHnct=Z)sJQ=V7zRY7O+N>vE8v0_dFp0d zfvJ(L!6dVDyCT9*WSHsp%@?6x*j-~{9g8GX5SP31PrrHH6l95r;*o#)*sT93({|qT z6JgY|F%ddUcBYq$hF>`Kq`6oM5!Ivij4Y;N32rP>Q!!)8(RZh%r2E38S#5LvTj&h; zPU$K)s(vC?_-g;j9(B}A2XXklZ$fa1Mcc=w4lU0t`N@f_mhsI~S}Hkdc>A1|e`VA3 z46*gcCp_!*&?M>aZJ!U^fsV7zYz!kyuj2;feGvb|(IMY+oXhd6NBu{>s^ov9xFk-0 z{s$^-G#F&Js?d3-vP1n0<5~Z(IJt`{Iap)~GRgtQXNud`W+#8% z!&MgS^V3sOwd_K+PG9d&u2<=?$*LAE?|nP4XEFRI#)Jc&oU>x3CYntpX!!5IZShg3^nEpQ3mm@>C2d$wWNwN8wDI7WX2-;YA56HXxvT3ht!k ziF1A#{EHC0Z6_~%KWrZFv&lh~tN&L&arm1(Yq3TjpJa!KyQ1{+G{kg9ixr+PTwcsah}}nZEdav&rS%y4{j& z%x{FE&7>CKRn>c=cGO3k=^Emj{rLY&0se0dnSjalXc}uX#26LQ*=w%k4}rLsxNfbc zhNZ8qmnNL}rSYTa(KvvC$~W&a+uD5C%K6Lg35MnqL%a3Lj~j6jd}vc(V!v06NzPld zBsx=x-4-Cw#duQt_6qr$V6RWn?AIh_`>o1zj2=!^1ge;fGJ?pAh)PiLj$uYMSip5H zh42{}rh*7s2#XlKnc}(8^TRersdn2h=b;bts3E4g-`N^v0aE5e%K-%`$wDSCGbM%` zNMK)C#7p497?zbm4mrBtW#?+%+(`)PEZBGqv!n>2LAZeI1R&F=!Df?hs+2&=G-wLo zf*D;9(AX`=mT7{yh_5klFp8)RgCLZ;Vz!F_)ur#eJ&W!!x)HTV575cA4w^+c66|{q z!B>P#jP{C`#=5|hEweS0Eg2vMF0Q* literal 5051 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 E0F*9wy8r+H diff --git a/mod/etc/options_menu/images/preview_placeholder.png b/mod/etc/options_menu/images/preview_placeholder.png index 755b70f1fd3b942f6fc145e9c2d9fbc09845e648..66ae0dc33fc0f6719eb7ef0814de9443f9730a49 100644 GIT binary patch literal 3344 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

8NIk4ktsTYlJ;jHZ*NfnnFmBFw zJVSv@-fKDD&Syfxi&ziou+BUtDUK&P4K*_#K@BL7dL4ChM_!3tB|iP80Olq8`aP&H z%a`PqoloxjkOCA)y?-ejHX`(RCJr(!DTE{A;GAuZ{8lCB6LZF1^T1~8BNb2}orwna zg#RrYHZ?v{IKCgmDn$!w&U81cC=lac0A6Pdq%#yqXQF}K-QgO>(%OEOEQqxxHYGnN z<_A1~aLh9!6;L3ZiO%lNF(ocp^iA)F;+(Uk2dsnH*HsSnY0pjrB*ys+f(ROMhgTKOevWnOt2_miqLFwl{q~ zlNAc2`>49PDaU85NMu%T*9@^5g5hp8)p3vd>4`WNXR^!3{xe0SEo@qk`cNPvOVqX$ zz^}dGl}#%jNJMgnmqi=&5wQ^oCCWW}5{bd2+}q+zcKMhFV;)_*cpJ;o|IC6Vj0Od@eNU*N}{s;bxe zl#gWqo)M9le%{7QQP_;!-FVQ5{4GUadej@-beuoWE8~cR6!)jxIQ5q zO@G8R;6m7^;$w!A=zY#54y%%iqb*AIJ4O!sAPtJ7P!=s$D?OI*#vHB?OuD7kVp`_6 z3ab&8;#-O3pg2Ye1=0XXF`MhW&v2bL>Znne3(Q5q&4I$!sit6Rh3vB)wv?jY=V!rKmL>WZRgF1~WrQVaB@+*SVbya*wm=C zXxApTNT=r)t;JXbx{BmM(?0NdMK21ntJpvJc9m8l5BbdTo5sPxIO)(7w1hV zke=iHgyG_%r-~un_Hh(M|DXjK9|_ilL$c!(4)YR|eGVFvb`Bwvb`Bwvb`B7KhNyTH z*~Xb8mJqC~7~C>pSjn!}08ICdkg1A#w*8r2`|&laeq^=op72`TJNkjS>Z*iHI=rr= zc~G0X4y=6MGtmcm+zZg}-+ri%2FyVIPG|B-$~5KJPxfxl?sF z0AKb2syhLk>0yudv8g~LcB<+p0es%aga2l#@|xYxdX-i}CM>Et8^8_#_64w~$N#Pe zaDj-dNrrqkv;tvTRCNJ>!vO5;)o@pNykuokvzw5K#taw7Y;VU;0k~2`t}nzxs@L{_IBRPhDKi6nV!z5BNT*aCp{ vN!SM=m_S76!ul7_iAcMj(SD-P)>Hlm{L>sMXuq9X00000NkvXXu0mjfGo;$W diff --git a/mod/etc/options_menu/images/preview_retroarch.png b/mod/etc/options_menu/images/preview_retroarch.png index 68571c42a308cc5d962bab54bfe372ee0e67d3e3..f4f126dffeb31b082f001f003b34316ca138b8ba 100644 GIT binary patch literal 4219 zcmbtYX*iS(7k*~QzK$)CWoB%ZWkRT|%`nn}B4n58%`&#^HOMx$Fl5M96V+=;NQ@El zVo0`^l4xYYga~6D`{(=p{rrBMbFSP?mT08yQT=!n z={#IV*&zGZ3;>9jSt3nuMil)i=FM`plSn$$=Dh$h1Ef?r1^CcvJ~ugePZg44&Ay3C z1euDL)2*t5JIc=x%|6xL9=?euJDs;*y?K zvzyBNVP#a$mFsPzhK5@)v-Icz!|s1U1TJcyUs;47549OEmlyMtXzyOXfqCC_cHrT& z`p>+*Kh4k{Sw%wblCl1PpO8BY=qMhL@KmB5W9YF}KzkC@hX58)zdeF*tD`Fnsu1WR zR9WBxDB@ZR6%6nf?qnp)fqcRFqKAhv)RO=XgV}1<{o4t$pz!!4(4tq>bqT-;GUR|Y z;No~%xH|Gum7sg&@$VGi+xdjxq=NqHX6LWwvh|<`0M!Zj1#bG-ckuNKMT(?`#scob z05{k{o*D^UJ3afuUbK4?Fp>&Mzu{uDz1D*9T6V?%$mX~1K(tPHoj*a91g`1Srf zv9?qB=Bu29*&Mp|wIYJD1%4#s_|}c-g=)(Z^VKRXarF*Ho}!8}7K3TN?TsOJr@)Hy z+7cW_ShfBbs|QtuV-J72R&y}^z()f!%6xSNsRMut@tLa--J4QQ0B*(@zLdC=2Kwh9qKJWKC9i~+C6yB&n; z`Kmsso&`s6Qa?Obb`(Zfvzvsc8Hj9SjtJ%0tI1H(+&nxJ#13eid@b^@bD~Pg2WPb0Y#rIlZ4l%U{MkmFA{$OF7p2aPKnW| zL%h=!N0iq)2NT(jiVV6NJnMk3!xt9aQ?8^tSV-{RL9L+`9zL5(o<-&8f!-R&j@kT# z3|(4w0;D3UXY}uqH{D5FXa&I=I;U6d-AjR$e^f&pnf^d4a`ijVnq7`iB4Dyjc3_Xy zK}CWL3DyfG|I{r+!dfU$1C4y$sHHqs>s>o%dL;dq)mFc>NUS>EV%2i-bw@Y=`hb0@cWD3vRRE_l}srbnjbM3;N|h+gtKZv(Dcs8@Pe@z z*qmzI`f=W}Ze%K6b4>#r=;4snbLDNw#zG(4HHO0VZ!%pV@|7`&O0sNngw<9p*w3Zs z7oF~C1i`bo@DeJG&@B-AvcTpQZRxbd+sh>VuYDzWok;R6L4`OSWHxlw8!%2eSRZ>t z5i97Ni*zj2txtPrT;>SR!bq#xj%8*JwrN0@+5^*AirTvsokJHP=y-8#cJin(m@$F~ z8kDH`IymLP!$1O>+V#tqW%o^YO?TiB!j1mLQTQ^KmOS-5?LkNNrwPnvYs8!P6Pvue zWijuRmw=a{QD!e2e6L3xW~Gf-crt`DSs_YFo1*`-tks?yjbG*>27CH#VV4p2Bzeme zgR?U2eBKFGt5ay9djT(>eh^fYs_g#S?j`N0rLkIjPH|g^(0VMq&m}HJ4X%l}X8hz3OuaLtI<5QA&x3-v(!B}CP^Iy=%%a5 za5w3ikzg`?-&Qes2^zQFl@X=f;{~GgF`z&|YD%P2u3Bx7&qaamAEL%8vznhlEmG7C zxTcChcbZ4<{%@n;+Nf#slPAI-iWfBFw0*9=onF~jm{wd^-!ULReX@(*V;N4dVLdly zaG_F;SwEU=<{{p-^UhygY{g%d7!S1l0|Vr$onN$&nF}o@OSiVNO^)~wt?yHV{C#8b^ zbUVSX|C(`-t@+%HWtF_TS(V;4;0hmvZnKYrYnt6ARmgVR1p*KVdOx}DhBxC@qP39n ztP{Z^s{2**h3JbvPEZk}z5f)1Z?5WAl=a#<#nli;t6IWF_Qz9a?zU(lnav&k2{}!6 zg(ak}SpGZi`~8ph1U6HcpVI?pgMGGJj@hK%y{ld5k5Sn@>sQNp@#$u^3T@hmM~`Er z8&7A0C{@BIG0?|Pk|`Z#-QW39aMlfM!!oJW&Y&ogf#xau0uL}+nEcU|tX z;^KK_p2$utURp{-CwEBr>VkCZ`TPAQMW|!6P>buub4E{0O~8(M*TUES&Q6S$?IFG| zVc#E*Jbl)6Hg%@>Tb~@q3Klp_(a90m+}1riU@NyB@s+s|axjm`e?7R+KBK=`+NAHK zl!RV7*${VkG(9CQxU$PEuD!Na|9!^v3%I7>tp$jDEUhT^&l$_+OPH|o#Aj2(Kj;n>bdKeJvJ;+zjx<$6%1hDG&%aZ$%}jd@{z3u`x1~oJ%AQjpJT^|?rK9% zPN?^M0E|pl*MKna3L!B1oLa@{sPmtUfxfi8H-#NwW)bF&tmxs9&*7(fES-3t(%fsC z&cSF=^&tk<4m;_et5oUoE%iRo`f`57OOsvo?6_?Uvkh5%eenR4j5B3!Q7FJXM zo)-J?6mN!_b3C;Z9rB!SfPS$GY1T%xAsLGWG)EkEN4YuL$ zHNDP59^}nhyKpg-O9vD5D|WB@5lZ80viAR8{YE{z0#*ZKjTlR(#$Q~`yMHe@rb`=& z+9$$mV3C|Xf}REXM7P^kG#cV&N-aB@{%GbEh%Q8(y%0@Xc(QxlerB1hKVS^A|Mc=p zXBbyfoD`bD5fs#^))JJ#J!=21E-EyJ>xIWaW_v}%`{i?Lv2QgZ-&LU;p1NF&>E+i2 zR$jB$()N}AKo7)}CJSFp9{zLtL!$RIpL0lqDOk;(*tZx3)Nlm4c)$9AmQvo3KECe7 zFORn=o{=R4q((X3fIVlC;ghIUB7l=vOTUL=yp^f6QG1(Sva8W;*uUsjJY^7%SDx=p z>m=SYtPGC<e?}#SS9age<62*5ib&}GM z^=$Q}RraxaFv4+o3FY1CSPo!M*{BlhkQ=r{bPI?={=S=}KRli*$>S1OzUnk();-g<9exaO#ug4#`Hsl)&RdpUl8$ZLN-km z{+JJf=1ygP6TiIhSO$?S!}t7t!@BrE8;U3v}M4U?NcK-o!2~|qxRftQ^fsViugTvr5t-27tUG>}wtp_1r zu$cFy1(AEG_Iysr$5dY0L{2>rAzj=7+Iu5x_BY=&1gP2Qx50$@+wj`H`2x=}X@-f1 zci;pV5~8|o)u87$6IuM3Kl8_AKK$#9IzBzH4e5o(-M^(Ow{K*a5R62&QU2TLA_^m{ z{k^;IO`cYB8s{*x=#5BfK!~Ak*vd(1HQCdra_b~(_`R^mRyX+z65fcYBcp~od%IEO zlc)9?z0$EZWq!btr`)M*e5-KLAA@sN^jJS~64KAloUP9>Q(fi=3`eQXpJO71&5z## z1^WK9_reDG_amb4oWoVt2X`^8-&eeWAInz*Qt^>XD7D5eA=Gz{Qny7chA1)W<3b@W zyQw@GYR%I~B3X}#4WWMs zD{@JgZ3RUr35wkudP z-u&^ldb`8Xu}BjC*bf{Cy5Ax!kcA~*n&VBqwnTabu{L#U^OFEw$bDhXDObHU!QoGj z(P?;N<^Txu>K}BAeOp1~;ACrA3 z4Co6|cYw!=2A@$7sURyD(W*)kDMnf^ahClI$nv|t+`{S9S5Do9eKogS=G5Y*!hn_r z))Vh?mYFIf-Qme!G`8o?+jY}b)X-leQm?U`EB!pt7bg^}B1p`5_!f5{x{7;J`lQ_~ zh)t=i*q*$KUost*k?`kb=nmC=pN=6~Skxra2+OPX3Rn4ij-+3Z8aAZ-a|lm7gI^O5 zm8yg;B$p21&WJNYvPY6F4&m$>&RJzeGO~#z+mUfb_V(qh zTt+JW`Y(Pjp7G{+`Mh|Zcw-|NBOMPN004{#I28F$2mc|7hUy=yrf+Wo0LudeRLd+V z|DYfu$!wZq;FF89Rk(27l~ko!xUT3NDgx;xDO=pH;o8Vj-K0=9**||Fd^`E7q&WVR zIjO1nb{UWKdwn%m|?6JB@hjmHLDs4Xb(TX!Fo_DwSydQTV|pb(uO!d zNHl;8c`mRz1?Yh!rWa;?Q;<*q`T2;lj-jHuTVf@)jaqWO2$A>=O%nc8nCqvd)L%JPNaWT)+7@y=qV$f4AEQ3|gjH#oDyZ z1LOLx)uQQHPf8|uPt?l3ix+K~DnINZ#)1f@tqsPnZDxI_S)r)Ynf$cw%t4sFWI$U0 zK9zZkAu4U!!<{_TZtOpm(d+>4qj65%U84{5-El=fBJYfeV_K~a+-0Z|<)DI&3L*kF zgRM3xnJx+C^z1KN{8h#VET^d2`kv;~$gvE>uY!F4kA|hZ+kY=JPI5O)Y~!+3lZjI& zm&bYsGuUVF>CTsThUFrr!oRMCCD3nbl1aK7N-22_AtSMI#1VoFBKMnl`O*|g?jddL z2~w+fuu0R53rQ0tksB<5_5%#ajwuT|eAOqn$(_Rmx;4mtb9W~~8!P*7Ic}!K@^&Dz zSo;}(6+n>qK==_{^eGRz7Sw7dL#I?rb*zFGg*L8K>+{>zE8l6K(U=Dvw|bi#^oOl~ONO_r=O6-~&JnkDp(Yxq zjnM)wE4M}yN-dWHve+0^Sq730F*+Cjm88}M9xW#rmm2>d&NA!7GH4Au$%^yYAZ|Lz z3^{}IeB@-o4j#26}O-=B_x3m4ODx&TIl6xg`l!n>h$ zFMgddEd(4c#uMs#9?9^xLLG0NA1uUazeYQJDYe=Jz?|Q*5zHf(IruP6v@Kg>1!w9t zwt)BLpxh-}U(3jJl-XicY;5c?YDV&PJ=D^o#wLD5u7jbXFhr_9h3|&U&X3)x@8dem zl<%b02Q9~$gIWDpvo6K)mM(>}c|xD#TvxyW$^ffKY2EBObm);viBYLBKbNW`7G!BE z!aqp~b;8@@(z>nb{@MmEaoD`;@9$sL8w!y8?QE-GhMbf0F2`(Cy{oAj^TfhyoVNQ3 z$gUw8C^jGOiH%4mE4z}niHg^<7@UFSYsRhNnp zClyay0xt0!2+5LHNt%i~&$0tVXp5L z`{JjUk31jgf=jb9>TxGeY-dNSEY)Cl_em%(KBUAZ$zZr)O2*R1LC)m#FKQu{ua;x~ zlsNm4?#081BJB6QceSzSSW=6?MMjgB5G#@mvvh4YSc=d|EGTn7AfaTp*WU>wSK6`1 z^5L9bm##AN-D#Zca#&GlUzoGH7o=f2ybDmVn<#pLVJiiP_tt+6|Ge2p0bYWY#@Ni9 zLWp%Cr+d+Ky*Qt)cuIOsvw3EB2f0LwM)9JsXFodbM)DS23yYUL{PZ_m{m+jnS2s7> zqJp6H_}0}Z`ln0&vksG`2tJ$2i4vFqaxafCpZ|OkvG!s*{KOf3?{%v*`^R&y++Y@^ zm9O|o@Ev%|sieSUN)8sLNMbUfQ@Y>YqTLp@deH2(5OUzw$suj=@~@{)P9Ya87Ku1v z!;j?1@0gV6P2bJ#@7L?vDGcrLpZgRMF;`Jm7W3=;=xdC&R@E<~uN9Uuv8N@e_jt~H zJUoa`CE5$i&)nc+39Dh&(eV zytz+;GjT0+vORpaEQgAi?mw$2!X80WNlRg?Ks-eacFa45xBfbJstQ!=QLe({D0?UCCTTcI$TF_4FY)^0ZZw%fOZ`!YIW2u} zq1D^O#W_B;05hKMYHE-7Wp

MX9XA5VJ^o`-%2~@dLU;6xH1N@~RUd1Fv!f7^7XxtuE#@un<_qLB_^Ba+ zBJlhI4H6}eof57vH(Zq<4TVn%`&oWIfQIHdH|SFlG^CzXlQ_{4vURd-HHXo#?jds7 z`c|0|U~tN_vrbbg0a`Cgk$&_s_bYa8E22bnd|E&GYY-^L?H-j*F#+OGz((Cl<^(wEWn+h!eb4*}tWgdEX}^@^^Bp z_Jo)Mm8-1J0k8#yHHmH1snygW3mK_;Gb3-scr^HDH&0E_UOHrTtNwi6E<(8p@LRVc z+f>i-Sbddo=vx!b_OEt)yjwIOv3lf0iv-Btg4fXAr=gOFS6(Np*6~aWV86#gcQ`X{ zY9W$c6TNd2aquRE5aPtL>cS~M-Gz^li$A-!Q=?B7I4WgVu+>*MBFRv;RU-UL=DNe1 zY!=>1hn%Q6bXTIhvRAj5YT1%oBmq+849?{YKU*~d!Xw@OW?d`~C zkCeod4msngR%ocE)HXp4W>*;%^5QWPmmK^V^<{Z+=X6K@+geD27o>l3e42n&IHLn|FSNO=zc^G$?)dPP+-cj(6|R9dOGy&#>Cc56h)~KVaM0x#6Y0R1_2?L-7)I zMI~s?g45Kdhm0y6S`92VM_vKf<)B6TUhgaUM!6Gk2vzWXc1je} z=~g^Rg)Lw&%i9^dnuFu9|yeSOj1qvw-K!YNBn z@?=xMR_()|@7fG(_nT4$(VEZ)qYQPU6XlTd+)|07e(YOmdEH>N6eBnr@#KV zT1fGWChd?cicGDj-w^>RzCx3orBz*DO68-Lcxy_H<6XN&L!K!Qj_>K+gL1_*4~G-e z>0G!3w{H!)yRrbuEoT$OXg|zNo>QFaR|4Fwt*=@jf~jgS@8kGE>E>ceN&ZN9tJD)o zI;t@)PvLEwlGrZnk3D1vG8E%@3x(}o<2i{G^@?5-sZ|*#4J!BeF7Yf2O%vI(Fr^0b zNXviF(B&|Otq|u?hRwvCXmiwW`Wp;6D@Zx(hoEP(?(WP@T7~wqPECaSh^wZQLQhx` z$0b{ZjYm&S5-wmbWQJl&Pk@u_uOl*3y%c2E!u|R&hQF`Z+`de5Hm)#{fCIo`G>vLBJEG|!DSpt9jZB}atX&k{lmak58;oWN#+@$m6z7^)i zx%U(uMgh}msq2khN18lm91y|8ven5nT3tz7z(Cd+Si*%A`3)3Le4AB$so4$V5$xFT zS?8MXdu8VHYa6i}{A{BJ^1?AfdA@A%vOR|i)bh0|#p7pz`xkVsRbpUEmMJ%~EmkX! zySYPWM+S0ey=6rI_&m9EUySm$@E*o5`G9OYV_|q)O!=Xh#I(}a_28uUPso`P6(-N6 zV|BOMFTlc!f8WT0`I-0etC700?yhI$d);lS&PI-4%D5+mW4=KOQjQK zv`rH(7;ns%0jp=Z3{5+?f0w5iOuxi%p9isNHCHI*`i*VcCFgzSWE;Ju*hL4CoHz|F z{l^aMdIQWxPmA>VU`qEV{RZC_gem!r#nQH4m;_;1#HHS;S-Oho)zDWz{C!Tdhwv#C z3hmkg0<0;6%(=s<%_ z+}^a?$l7{(6~v8Ne;NMK`BbTD_V*=Wkv;$nOKE}ahbBMW0CS^&irxaf3Aob#6N3I5 zqf%03FLCu3AXBsW=PG!FKPUr55z+6|F+|bLThIQ2(hJnio!;RG)LAYHQog61N#-h~ zML$GVoBvwWpJ17#1tb|PzS+q9({V;{RHt%KcJ;$|1>h14qC@R*0badkvQ#Ivyph9& zpoe=SI^3Vil|?Uz0-3CCEVI12+Qqy%#KeMDTSO*%E#ocqw4eOw?dc*$+A(KYo1>f& zg^cG!Qz@Ot|H(vtA3H$zaf(g4%H@7EV}!Mtn=%cMU*IjCA47p(pxXkpf#vLg*UL*| z^fyd0K<1f+mkDm(p3zZLuxb@K=KIiH#-!v}%bHMyy#^|4X|U@15LUp2Z0s@a+MdJ& z0r_o{RSlskf|h<-`eKxzHGzl!yn;-hnAIM(F=dxVcyjuHhXBE*ONX-)0Kel2 n_TINP)Blw|oD6ff`5hQ^GWDKPS5Sx*bs0ea?|j%DauG$k&IwakH;3 z%P-cgL*NGnP2Wp`U^_n4`*in^b*q8*iPofF-w#LWt8y15JgRp7ASj14<54MI#L1#c`jM|%NQ&@FUT;05xY=Z9h4fbuR4}GdUbTo)ER*KglilCSa zZRc0wH~L$!Q@;R$lo+sOie<`nZQ!B@JX%8~%8P+%c{xWjEeH*J1U&k+~jy7dn>o+vqk?CV}KH}FLK1BGVeCti1 zBH^<0+8PyT<*yRwa3|e-=QuzhAo}q>=lr7|@f+?Me2d&xkOBCn!JumU`sbsUWOuGD zn(0;+al21@wvS0@ca?L{M*KwaJC}TCK3h2xqmU}bWUgAX%Cf2h95tbKvxbK*XUgQB zRv-`h+Y~E;kvCKhl(AC&CPhOma8|=YKdAV3*X(%HF$2bjqFgNzC$pNyI>MR{hbBU? z0q4*6uz6rJp4LANqUkRxAX26lyg@y0Mm1-q+1*q9wFblIUWYiF-EsoS^xBst82OEy zz8YFLufs2qmgs5p4(if^BQ1X(|Fsv36A)h!d`0h)57wl);8l)byoAKEpp(6AhC{^W z1HK*tca(LZ@I`vHv&@LZ`;2YP-G}M`u}7r?G|NG9hg^S(|j0 zgl(^R>o6NDC}KtkX%@9<6;HWC$=|^pOjbNb8d{Ww*c;Y&$=^&?DDK&KMTIU24AwIL zY1avE&n4?-nQOyZ8pm|-t>MSE*7*UtLX`QP{n!3TSe4NYc zQs{ckE423Bks4PchEAVrxLI~C(z{t1+t~YoSmJNugxho~fV8t>0vgKBhNWBAFAY+3`qaJchM7N|W#W zf||BiCe+~vV_rivTK2@8xmnYJihgb1^BxG`rl&r-&Mhg27h(B485avuKL4uf%)pp*H4<^4goeVx00mQ zR^5W0+7cvz3BiPiaiR&VoHo_A>(=g3$OqvVZfD-7rArwJP8KE3g?m)y2u$& zH8>SyCQEg940tY3_JcR-AQj!#LVQkU6>q1Zo|8Qo8DGcQEzm~QSj%esgzu^ekOUx* zV^#6panUoTN?>Dxqas+ln@s@fMPgN}Z9}HH68w_B(BJ%6pv^HwryEUD@M}RS?ujs* zW2ew{J462x?Bnr-TR(NCuu})B)bndS{*V3qw^7-XL4mV#)deqJ51)iJd6cgCP{H1F z#|UUo=Tn4NN?)lBoE`pmv}DLNSGhkN!VIIXr(wC1db2{P z-J|ZlDTDHWh~moRGkYdJ14Y0ilNAY9?ok-ZX2yP@Qi+z}W$#(~3sqb87VL{mcUOL|oGr3(vNQ{;t9VF?K66`ea z53zBVC=6sWbba@ZU&2m(QqGIdLx+BeoT=#JN7l_u!ZMAcj*FfA3`mEN3tIIL$nJTo zZ|Nhs%)K8`%`H+FSA~0~c^JasC3d2w_}OoTzausiAFq2zhlf+zNWnPfzF10qv-1MJ=mXn1g$Lq+d4pT6>3|t>tc-p_I8Y9_dKs_ij&1`?{xK2dwC{54*`C@3 zKDnoygtjXygOJ7n@?M8|S!j}~ Date: Thu, 24 Sep 2026 23:58:12 +0000 Subject: [PATCH 047/104] Use Bootstrap chevrons for submenu and scroll arrows, add ScrollArrow color --- mod/etc/options_menu/images/ui/chevron_right.png | Bin 0 -> 241 bytes mod/etc/options_menu/images/ui/chevron_up.png | Bin 0 -> 252 bytes mod/etc/options_menu/theme.cfg | 6 ++++-- src/framework/uitheme.cpp | 13 +++++++++++-- src/framework/uitheme.h | 5 ++++- src/main.cpp | 12 +++++++----- src/mod_uninstall.cpp | 3 ++- 7 files changed, 28 insertions(+), 11 deletions(-) create mode 100644 mod/etc/options_menu/images/ui/chevron_right.png create mode 100644 mod/etc/options_menu/images/ui/chevron_up.png diff --git a/mod/etc/options_menu/images/ui/chevron_right.png b/mod/etc/options_menu/images/ui/chevron_right.png new file mode 100644 index 0000000000000000000000000000000000000000..8454d9bcf9d0c8515f2082497f838c5530a531f6 GIT binary patch literal 241 zcmVtd&s0djluH0Jv*y5tN{h z1nhM10`!W%()vgxIWX7SYde$=wzaSqo4%K1z|0=(fVF|sO0#;sG4RmYiM_%QI0OB6 zI3StoY#QB$i*M>fECwo?xrh)G7>S5TRyWs%YaUOgL=A@)q{HR_0000> 8) & 0xFF; + ScrollArrowB = (TextColor >> 16) & 0xFF; + } } } diff --git a/src/framework/uitheme.h b/src/framework/uitheme.h index 4446433..360446f 100644 --- a/src/framework/uitheme.h +++ b/src/framework/uitheme.h @@ -37,6 +37,7 @@ namespace UiTheme extern Uint8 BorderR, BorderG, BorderB; extern Uint8 AccentR, AccentG, AccentB; extern Uint8 SelectedRowBgR, SelectedRowBgG, SelectedRowBgB; + extern Uint8 ScrollArrowR, ScrollArrowG, ScrollArrowB; // follows Text unless set // TV overscan safe area const int SafeMarginX = 64; @@ -124,7 +125,7 @@ namespace UiTheme // LoadThemeConfig - not user-facing keys themselves extern Uint32 BadgeLetterColor; extern Uint32 TextColor; // primary text - titles, row labels, badge hints - extern Uint32 TextDimColor; // muted grey for chevrons/secondary text + extern Uint32 TextDimColor; // muted grey for secondary text struct BadgeColor { Uint8 r, g, b; }; extern BadgeColor BadgeA; // green @@ -153,6 +154,8 @@ namespace UiTheme const char * const AssetSwitchOn = "/images/ui/switch_on.png"; const char * const AssetSwitchOff = "/images/ui/switch_off.png"; const char * const AssetGear = "/images/ui/gear.png"; + const char * const AssetChevronRight = "/images/ui/chevron_right.png"; + const char * const AssetChevronUp = "/images/ui/chevron_up.png"; } #endif diff --git a/src/main.cpp b/src/main.cpp index db69c73..c243563 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -258,7 +258,8 @@ int main(int argc, char * argv[]) Texture titleText(titleString, UiTheme::SectionTitleFontSize, renderer, UiTheme::SectionTitleX, UiTheme::SectionTitleY, false, UiTheme::TextColor, true); SDL_Rect selectedRowRect{ UiTheme::ListX, UiTheme::RowFirstY - 2, UiTheme::ListContentRightX - UiTheme::ListX, 0 }; Texture CompComText("created by CompCom - Modern UI by DefKorns", 16, renderer, UiTheme::CreditX, UiTheme::CreditY, false, UiTheme::TextColor, true); - Texture scrollUp("^", 16, renderer, UiTheme::ScrollX, UiTheme::ScrollUpY, false, UiTheme::TextColor, true); + Texture scrollUp(optionsLocation + UiTheme::AssetChevronUp, renderer, UiTheme::ScrollX, UiTheme::ScrollUpY); + SDL_SetTextureColorMod(scrollUp.texture.get(), UiTheme::ScrollArrowR, UiTheme::ScrollArrowG, UiTheme::ScrollArrowB); scrollUp.rect.x -= scrollUp.rect.w / 2; // ScrollX is the gutter's center, not the glyph's left edge Texture scrollDown = scrollUp; scrollDown.rect.y = UiTheme::ScrollDownY; @@ -301,7 +302,8 @@ int main(int argc, char * argv[]) const int ChildIndent = 4*16; const int RowGlyphSize = 16; const int RowTextGapPx = 16; // gap kept between truncated text and preview image/switch - Texture chevronText(">", RowGlyphSize, renderer, 0, 0, false, UiTheme::TextDimColor, true); + Texture chevronIcon(optionsLocation + UiTheme::AssetChevronRight, renderer); + SDL_SetTextureColorMod(chevronIcon.texture.get(), UiTheme::AccentR, UiTheme::AccentG, UiTheme::AccentB); for(Command & c : commands) { // self-relaunch into the same dir (e.g. language selection) isn't a submenu @@ -444,9 +446,9 @@ int main(int argc, char * argv[]) } else if(rowCommand.hasSubmenu) { - chevronText.rect.x = UiTheme::RowControlRightX - chevronText.rect.w; - chevronText.rect.y = rowCommand.texture.rect.y + (rowCommand.texture.rect.h - chevronText.rect.h) / 2; - chevronText.Draw(renderer); + chevronIcon.rect.x = UiTheme::RowControlRightX - chevronIcon.rect.w; + chevronIcon.rect.y = rowCommand.texture.rect.y + (rowCommand.texture.rect.h - chevronIcon.rect.h) / 2; + chevronIcon.Draw(renderer); } // skip for separators and the last row - nothing to separate there if(!selected && !rowCommand.command.empty() && !isLastOverall) diff --git a/src/mod_uninstall.cpp b/src/mod_uninstall.cpp index 653f454..7fd4c0b 100644 --- a/src/mod_uninstall.cpp +++ b/src/mod_uninstall.cpp @@ -112,7 +112,8 @@ int main(int argc, char * argv[]) mod.text = Texture(TruncateToWidth(mod.name, ColumnW), ListFontSize, renderer, LeftColumnX, 0, false, UiTheme::TextColor, true); // Anchor chevrons to the local list range. - Texture scrollUp("^", 16, renderer, UiTheme::ScrollX, ListTopY + 10, false, UiTheme::TextColor, true); + Texture scrollUp(OptionsLocation + UiTheme::AssetChevronUp, renderer, UiTheme::ScrollX, ListTopY + 10); + SDL_SetTextureColorMod(scrollUp.texture.get(), UiTheme::ScrollArrowR, UiTheme::ScrollArrowG, UiTheme::ScrollArrowB); scrollUp.rect.x -= scrollUp.rect.w / 2; Texture scrollDown = scrollUp; scrollDown.rect.y = ListBottomY - 50; From 45b0270e1f3462848bcd0dff76b8cdd7d74de503 Mon Sep 17 00:00:00 2001 From: DefKorns Date: Fri, 25 Sep 2026 00:02:59 +0000 Subject: [PATCH 048/104] Remove redundant Exit row from root screen --- src/framework/uitheme.h | 2 +- src/main.cpp | 58 ++++++++--------------------------------- 2 files changed, 12 insertions(+), 48 deletions(-) diff --git a/src/framework/uitheme.h b/src/framework/uitheme.h index 360446f..8b808ad 100644 --- a/src/framework/uitheme.h +++ b/src/framework/uitheme.h @@ -94,7 +94,7 @@ namespace UiTheme const int RowPitch = 36; const int RowTextYNudge = -4; // fine-tune vs. pure (slot-h - text-h)/2 centering const int RowTextX = ListX + 16; - const int PinnedBottomMargin = 10; // gap from the footer divider to the last pinned row (e.g. Exit) + const int ListBottomMargin = 10; // gap from the footer divider to the last list row // detail panel (right column), Modern UI only - just the preview image, // fixed Y independent of scroll/selection (it must not move as the list diff --git a/src/main.cpp b/src/main.cpp index c243563..0ea6774 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -183,10 +183,7 @@ int main(int argc, char * argv[]) exit(1); } - // B goes straight back (see the B-handling block below) rather than through a - // visible row, so only the root screen (no ancestors to go back to) gets Exit - static const char * const NavPreviewImage = "/etc/options_menu/images/preview_placeholder.png"; - const int pinnedStartIndex = static_cast(commands.size()); + // B goes back to the parent, or closes the menu on the root screen (no ancestors) std::string backCommand; if(!backStack.empty()) { @@ -218,16 +215,6 @@ int main(int argc, char * argv[]) + (backScriptPath.empty() ? "" : " --scriptPath " + backScriptPath) + " --title \"" + backTitleKey + "\" &"; } - else - { - Command exitCmd; - exitCmd.name = "EXIT"; - exitCmd.runInternal = false; - exitCmd.restartUI = true; - exitCmd.previewImage = NavPreviewImage; - exitCmd.command = "echo \"Closing Options Menu\""; - commands.push_back(exitCmd); - } int currentCommandId = 0; @@ -316,9 +303,6 @@ int main(int argc, char * argv[]) if(targetPath == commandLocation) c.hasSubmenu = false; } - // Back also relaunches elsewhere - name is the untranslated key, language-independent - if(c.name == "BACK") - c.hasSubmenu = false; int textX = UiTheme::RowTextX + (c.child ? ChildIndent : 0); std::string label = Translate(c.name); @@ -349,19 +333,8 @@ int main(int argc, char * argv[]) } const int modernRowPitch = std::max(UiTheme::RowPitch, GetTTFLineHeight(RowGlyphSize)); - int pinnedAreaTop; - { - int slotBottom = UiTheme::FooterDividerY - UiTheme::PinnedBottomMargin; - for(int i = static_cast(commands.size())-1; i >= pinnedStartIndex; --i) - { - int slotTop = slotBottom - modernRowPitch; - commands[i].texture.rect.y = slotTop + (modernRowPitch - commands[i].texture.rect.h) / 2 + UiTheme::RowTextYNudge; - slotBottom = slotTop; - } - pinnedAreaTop = slotBottom; - } - // scrollable rows that fit above the pinned block - varies per screen - const int DisplayItemCount = std::max(1, (pinnedAreaTop - UiTheme::RowFirstY) / modernRowPitch); + const int commandCount = static_cast(commands.size()); + const int DisplayItemCount = std::max(1, (UiTheme::FooterDividerY - UiTheme::ListBottomMargin - UiTheme::RowFirstY) / modernRowPitch); int topListItemNumber = 1; std::shared_ptr PreviewImage; @@ -370,24 +343,21 @@ int main(int argc, char * argv[]) currentCommandId = newCommandId; const Command & currentCommand = commands[currentCommandId]; - // pinned rows don't affect scroll position - int scrollTarget = std::min(currentCommandId, std::max(0, pinnedStartIndex-1)); - bool updateCommandYPos = false; - if(scrollTarget < topListItemNumber) + if(currentCommandId < topListItemNumber) { - topListItemNumber = scrollTarget; + topListItemNumber = currentCommandId; updateCommandYPos = true; } - else if(scrollTarget >= topListItemNumber+DisplayItemCount) + else if(currentCommandId >= topListItemNumber+DisplayItemCount) { - topListItemNumber = scrollTarget-DisplayItemCount+1; + topListItemNumber = currentCommandId-DisplayItemCount+1; updateCommandYPos = true; } if(updateCommandYPos) { int y = UiTheme::RowFirstY; - for(int i = 0, count = std::min(DisplayItemCount, pinnedStartIndex-topListItemNumber); i < count; ++i) + for(int i = 0, count = std::min(DisplayItemCount, commandCount-topListItemNumber); i < count; ++i) { Texture & rowTexture = commands[i+topListItemNumber].texture; // centered in its slot - TTF glyphs have ascender padding, top-align leaves a gap @@ -595,20 +565,14 @@ int main(int argc, char * argv[]) DrawChrome(); titleText.Draw(renderer); - int lastCommandIndex = static_cast(commands.size()) - 1; - for(int i = 0, count = std::min(DisplayItemCount, pinnedStartIndex-topListItemNumber); i < count; ++i) - DrawRow(commands[i+topListItemNumber], (i+topListItemNumber) == lastCommandIndex); - - // pinned trailing rows (Back/Exit) - always visible near the footer - for(int i = pinnedStartIndex; i <= lastCommandIndex; ++i) - DrawRow(commands[i], i == lastCommandIndex); + for(int i = 0, count = std::min(DisplayItemCount, commandCount-topListItemNumber); i < count; ++i) + DrawRow(commands[i+topListItemNumber], (i+topListItemNumber) == commandCount-1); CompComText.Draw(renderer); - // bounded by pinnedStartIndex, not commands.size() - pinned rows need no scroll if(topListItemNumber != 0) scrollUp.Draw(renderer); - if((topListItemNumber + DisplayItemCount) < pinnedStartIndex) + if((topListItemNumber + DisplayItemCount) < commandCount) scrollDown.Draw(renderer, SDL_FLIP_VERTICAL); // flat-line helpers leave the draw color dirty - reset before EndFrame From c46e78a8df1c99f78a733747ac29630ee99faadc Mon Sep 17 00:00:00 2001 From: DefKorns Date: Fri, 25 Sep 2026 00:33:07 +0000 Subject: [PATCH 049/104] Apply C++ Core Guidelines to DefKorns-authored sources --- src/change_combo.cpp | 150 +++++++++++++------------ src/framework/draw_helpers.cpp | 185 +++++++++++++------------------ src/framework/draw_helpers.h | 18 +-- src/framework/font8x8_lookup.cpp | 21 ++-- src/framework/uitheme.cpp | 50 +++++---- src/framework/uitheme.h | 3 +- src/framework/utf8.cpp | 103 ++++++++++------- src/gen_splash.cpp | 89 +++++++-------- src/localization.cpp | 98 +++++++++------- src/localization.h | 12 +- 10 files changed, 373 insertions(+), 356 deletions(-) diff --git a/src/change_combo.cpp b/src/change_combo.cpp index c38d487..12f7691 100644 --- a/src/change_combo.cpp +++ b/src/change_combo.cpp @@ -18,51 +18,81 @@ #include "framework/controller.h" #include "localization.h" -#include +#include +#include #include +#include +#include #include -#include #include -static const GameButton AllButtons[] = { A, B, X, Y, L, R, SELECT, START, LEFT, RIGHT, UP, DOWN }; - -static const char * ButtonName(GameButton button) +namespace { - switch(button) + const std::string OptionsRoot = "/etc/options_menu/"; + const std::string ButtonConfigPath = OptionsRoot + "button.cfg"; + constexpr useconds_t PollIntervalUs = 50000; + constexpr size_t ComboLength = 3; + + const std::array AllButtons{ { A, B, X, Y, L, R, SELECT, START, LEFT, RIGHT, UP, DOWN } }; + + const char * ButtonName(GameButton button) { - case A: return "A"; - case B: return "B"; - case X: return "X"; - case Y: return "Y"; - case L: return "L"; - case R: return "R"; - case SELECT: return "SELECT"; - case START: return "START"; - case LEFT: return "LEFT"; - case RIGHT: return "RIGHT"; - case UP: return "UP"; - default: return "DOWN"; + switch(button) + { + case A: return "A"; + case B: return "B"; + case X: return "X"; + case Y: return "Y"; + case L: return "L"; + case R: return "R"; + case SELECT: return "SELECT"; + case START: return "START"; + case LEFT: return "LEFT"; + case RIGHT: return "RIGHT"; + case UP: return "UP"; + case DOWN: return "DOWN"; + } + return "?"; } -} -// drops any button already held, so the next WaitForPress doesn't catch the -// tail of a press that was meant for the previous step -static void ConsumeHeldButtons(Controller & c) -{ - c.Update(); - for(GameButton b : AllButtons) - c.GetButtonStatus(b); -} + // drops any button already held, so the next WaitForPress doesn't catch the + // tail of a press that was meant for the previous step + void ConsumeHeldButtons(Controller & controller) + { + controller.Update(); + for(const GameButton button : AllButtons) + controller.GetButtonStatus(button); + } -static GameButton WaitForPress(Controller & c) -{ - for(;;) + GameButton WaitForPress(Controller & controller) + { + for(;;) + { + controller.Update(); + for(const GameButton button : AllButtons) + if(controller.GetButtonStatus(button)) + return button; + usleep(PollIntervalUs); + } + } + + void PrintLines(const std::initializer_list keys) { - c.Update(); - for(GameButton b : AllButtons) - if(c.GetButtonStatus(b)) - return b; - usleep(50000); + for(const char * key : keys) + std::cout << Translate(key) << std::endl; + } + + // temp file + rename: an interrupted write must never leave button.cfg + // truncated for daemon.cpp to trip over + bool SaveCombo(const std::array & combo) + { + const std::string tmpPath = ButtonConfigPath + ".tmp"; + { + std::ofstream out(tmpPath); + if(!(out << static_cast(combo[0]) << " " << static_cast(combo[1]) << " " << static_cast(combo[2]))) + return false; + } + return std::rename(tmpPath.c_str(), ButtonConfigPath.c_str()) == 0; } } @@ -72,54 +102,38 @@ int main() // there, so lines can sit unflushed instead of appearing as they're printed setvbuf(stdout, nullptr, _IONBF, 0); - LoadLanguageFromConfig("/etc/options_menu/"); + LoadLanguageFromConfig(OptionsRoot); - Controller c(1); + Controller controller(1); - static const char * const intro[] = { - "CHANGE_COMBO_INTRO_1", "CHANGE_COMBO_INTRO_2", "CHANGE_COMBO_INTRO_3", "CHANGE_COMBO_CONTINUE" - }; - for(const char * key : intro) - std::cout << Translate(key) << std::endl; - ConsumeHeldButtons(c); - GameButton gate; - do { gate = WaitForPress(c); } while(gate != A && gate != B); - if(gate == B) + PrintLines({ "CHANGE_COMBO_INTRO_1", "CHANGE_COMBO_INTRO_2", "CHANGE_COMBO_INTRO_3", "CHANGE_COMBO_CONTINUE" }); + ConsumeHeldButtons(controller); + GameButton choice = WaitForPress(controller); + while(choice != A && choice != B) + choice = WaitForPress(controller); + if(choice == B) return 0; - static const char * const prompts[3] = { - "CHANGE_COMBO_PRESS_1", - "CHANGE_COMBO_PRESS_2", - "CHANGE_COMBO_PRESS_3" - }; + const std::array prompts{ { "CHANGE_COMBO_PRESS_1", "CHANGE_COMBO_PRESS_2", "CHANGE_COMBO_PRESS_3" } }; for(;;) { - GameButton chosen[3]; - for(int i = 0; i < 3; ++i) + std::array combo{}; + for(size_t i = 0; i < ComboLength; ++i) { std::cout << Translate(prompts[i]) << std::endl; - ConsumeHeldButtons(c); - chosen[i] = WaitForPress(c); + ConsumeHeldButtons(controller); + combo[i] = WaitForPress(controller); } // button names (A, B, X...) stay untranslated, same as the footer badges - std::cout << Translate("CHANGE_COMBO_CHOSEN") << " " << ButtonName(chosen[0]) << " + " << ButtonName(chosen[1]) << " + " << ButtonName(chosen[2]) << std::endl; + std::cout << Translate("CHANGE_COMBO_CHOSEN") << " " << ButtonName(combo[0]) << " + " << ButtonName(combo[1]) << " + " << ButtonName(combo[2]) << std::endl; std::cout << Translate("CHANGE_COMBO_CONFIRM") << std::endl; - ConsumeHeldButtons(c); - if(WaitForPress(c) == A) + ConsumeHeldButtons(controller); + if(WaitForPress(controller) == A) { - // write to a temp file and rename into place - an interrupted write - // must never leave button.cfg truncated/empty for daemon.cpp to trip over - const char * const path = "/etc/options_menu/button.cfg"; - const std::string tmpPath = std::string(path) + ".tmp"; - std::ofstream out(tmpPath); - if(out << (int)chosen[0] << " " << (int)chosen[1] << " " << (int)chosen[2]) - { - out.close(); - std::rename(tmpPath.c_str(), path); + if(SaveCombo(combo)) std::cout << Translate("CHANGE_COMBO_DONE") << std::endl; - } else std::cerr << Translate("CHANGE_COMBO_WRITE_ERROR") << std::endl; break; diff --git a/src/framework/draw_helpers.cpp b/src/framework/draw_helpers.cpp index e5f6418..b4b0d6b 100644 --- a/src/framework/draw_helpers.cpp +++ b/src/framework/draw_helpers.cpp @@ -19,145 +19,112 @@ #include #include +#include #include -// corner arc points within [radius-width, radius], cached per (radius,width) -static const std::vector & RoundedCornerArc(int radius, int width) +namespace { - static std::map, std::vector> cache; - auto key = std::make_pair(radius, width); - auto it = cache.find(key); - if(it != cache.end()) - return it->second; - std::vector pts; - for(int dy = 0; dy <= radius; ++dy) - for(int dx = 0; dx <= radius; ++dx) - { - double dist = std::sqrt(static_cast(dx*dx + dy*dy)); - if(dist <= radius + 0.5 && dist > radius - width + 0.5) - pts.push_back({dx, dy}); - } - return cache.emplace(key, std::move(pts)).first->second; -} + using CornerPoints = std::vector; -void DrawStrokeRect(SDL_Renderer * r, SDL_Rect rect, Uint8 cr, Uint8 cg, Uint8 cb, int width, int radius) -{ - SDL_SetRenderDrawColor(r, cr, cg, cb, 0xFF); - int x0 = rect.x, y0 = rect.y, x1 = rect.x+rect.w-1, y1 = rect.y+rect.h-1; - for(int i = 0; i < width; ++i) + enum class CornerRegion { Arc, Disk, Outside }; + + // offsets (dx, dy) from a corner's circle center, within one quadrant. + // Arc: ring of the given width on the radius. Disk: full quarter-disk. + // Outside: corner-square pixels beyond the radius. Cached per shape. + const CornerPoints & CornerOffsets(CornerRegion region, int radius, int width = 0) { - SDL_RenderDrawLine(r, x0+radius, y0+i, x1-radius, y0+i); // top - SDL_RenderDrawLine(r, x0+radius, y1-i, x1-radius, y1-i); // bottom - SDL_RenderDrawLine(r, x0+i, y0+radius, x0+i, y1-radius); // left - SDL_RenderDrawLine(r, x1-i, y0+radius, x1-i, y1-radius); // right + static std::map, CornerPoints> cache; + const auto key = std::make_tuple(region, radius, width); + const auto it = cache.find(key); + if(it != cache.end()) + return it->second; + + CornerPoints points; + for(int dy = 0; dy <= radius; ++dy) + for(int dx = 0; dx <= radius; ++dx) + { + const double dist = std::sqrt(static_cast(dx*dx + dy*dy)); + const bool insideRadius = dist <= radius + 0.5; + const bool inRegion = + region == CornerRegion::Arc ? insideRadius && dist > radius - width + 0.5 : + region == CornerRegion::Disk ? insideRadius : + !insideRadius; + if(inRegion) + points.push_back({ dx, dy }); + } + return cache.emplace(key, std::move(points)).first->second; } - if(radius <= 0) - return; - const std::vector & arc = RoundedCornerArc(radius, width); - // reused across calls (single render thread) to avoid per-frame heap allocation - static std::vector points; - points.clear(); - points.reserve(arc.size() * 4); - for(const SDL_Point & p : arc) + + // mirrors quadrant offsets into all 4 corners of rect and plots them + void DrawMirroredCorners(SDL_Renderer * renderer, SDL_Rect rect, int radius, const CornerPoints & offsets) { - points.push_back({ x0+radius-p.x, y0+radius-p.y }); // top-left - points.push_back({ x1-radius+p.x, y0+radius-p.y }); // top-right - points.push_back({ x0+radius-p.x, y1-radius+p.y }); // bottom-left - points.push_back({ x1-radius+p.x, y1-radius+p.y }); // bottom-right + const int x0 = rect.x, y0 = rect.y, x1 = rect.x + rect.w - 1, y1 = rect.y + rect.h - 1; + // reused across calls (single render thread) to avoid per-frame heap allocation + static CornerPoints points; + points.clear(); + points.reserve(offsets.size() * 4); + for(const SDL_Point & p : offsets) + { + points.push_back({ x0 + radius - p.x, y0 + radius - p.y }); // top-left + points.push_back({ x1 - radius + p.x, y0 + radius - p.y }); // top-right + points.push_back({ x0 + radius - p.x, y1 - radius + p.y }); // bottom-left + points.push_back({ x1 - radius + p.x, y1 - radius + p.y }); // bottom-right + } + SDL_RenderDrawPoints(renderer, points.data(), static_cast(points.size())); } - SDL_RenderDrawPoints(r, points.data(), static_cast(points.size())); } -void DrawFillRect(SDL_Renderer * r, SDL_Rect rect, Uint8 cr, Uint8 cg, Uint8 cb) +void DrawStrokeRect(SDL_Renderer * renderer, SDL_Rect rect, Uint8 cr, Uint8 cg, Uint8 cb, int width, int radius) { - SDL_SetRenderDrawColor(r, cr, cg, cb, 0xFF); - SDL_RenderFillRect(r, &rect); + SDL_SetRenderDrawColor(renderer, cr, cg, cb, 0xFF); + const int x0 = rect.x, y0 = rect.y, x1 = rect.x + rect.w - 1, y1 = rect.y + rect.h - 1; + for(int i = 0; i < width; ++i) + { + SDL_RenderDrawLine(renderer, x0 + radius, y0 + i, x1 - radius, y0 + i); // top + SDL_RenderDrawLine(renderer, x0 + radius, y1 - i, x1 - radius, y1 - i); // bottom + SDL_RenderDrawLine(renderer, x0 + i, y0 + radius, x0 + i, y1 - radius); // left + SDL_RenderDrawLine(renderer, x1 - i, y0 + radius, x1 - i, y1 - radius); // right + } + if(radius > 0) + DrawMirroredCorners(renderer, rect, radius, CornerOffsets(CornerRegion::Arc, radius, width)); } -// like RoundedCornerArc but the full quarter-disk, for a solid fill -static const std::vector & RoundedCornerDisk(int radius) +void DrawFillRect(SDL_Renderer * renderer, SDL_Rect rect, Uint8 cr, Uint8 cg, Uint8 cb) { - static std::map> cache; - auto it = cache.find(radius); - if(it != cache.end()) - return it->second; - std::vector pts; - for(int dy = 0; dy <= radius; ++dy) - for(int dx = 0; dx <= radius; ++dx) - if(std::sqrt(static_cast(dx*dx + dy*dy)) <= radius + 0.5) - pts.push_back({dx, dy}); - return cache.emplace(radius, std::move(pts)).first->second; + SDL_SetRenderDrawColor(renderer, cr, cg, cb, 0xFF); + SDL_RenderFillRect(renderer, &rect); } -void DrawRoundedFillRect(SDL_Renderer * r, SDL_Rect rect, Uint8 cr, Uint8 cg, Uint8 cb, int radius) +void DrawRoundedFillRect(SDL_Renderer * renderer, SDL_Rect rect, Uint8 cr, Uint8 cg, Uint8 cb, int radius) { - SDL_SetRenderDrawColor(r, cr, cg, cb, 0xFF); + SDL_SetRenderDrawColor(renderer, cr, cg, cb, 0xFF); if(radius <= 0) { - SDL_RenderFillRect(r, &rect); + SDL_RenderFillRect(renderer, &rect); return; } - int x0 = rect.x, y0 = rect.y, x1 = rect.x+rect.w-1, y1 = rect.y+rect.h-1; - SDL_Rect mid{ x0, y0+radius, rect.w, rect.h-2*radius }; - SDL_Rect midV{ x0+radius, y0, rect.w-2*radius, rect.h }; - SDL_RenderFillRect(r, &mid); - SDL_RenderFillRect(r, &midV); - const std::vector & disk = RoundedCornerDisk(radius); - static std::vector points; // reused - see DrawStrokeRect - points.clear(); - points.reserve(disk.size() * 4); - for(const SDL_Point & p : disk) - { - points.push_back({ x0+radius-p.x, y0+radius-p.y }); - points.push_back({ x1-radius+p.x, y0+radius-p.y }); - points.push_back({ x0+radius-p.x, y1-radius+p.y }); - points.push_back({ x1-radius+p.x, y1-radius+p.y }); - } - SDL_RenderDrawPoints(r, points.data(), static_cast(points.size())); + // cross of two bands, corners filled by the quarter-disks + const SDL_Rect hBand{ rect.x, rect.y + radius, rect.w, rect.h - 2*radius }; + const SDL_Rect vBand{ rect.x + radius, rect.y, rect.w - 2*radius, rect.h }; + SDL_RenderFillRect(renderer, &hBand); + SDL_RenderFillRect(renderer, &vBand); + DrawMirroredCorners(renderer, rect, radius, CornerOffsets(CornerRegion::Disk, radius)); } -// complement of RoundedCornerDisk: corner-square pixels outside the rounded rect, -// for painting over a rectangularly-clipped image to fake rounding -static const std::vector & RoundedCornerOutside(int radius) -{ - static std::map> cache; - auto it = cache.find(radius); - if(it != cache.end()) - return it->second; - std::vector pts; - for(int dy = 0; dy <= radius; ++dy) - for(int dx = 0; dx <= radius; ++dx) - if(std::sqrt(static_cast(dx*dx + dy*dy)) > radius + 0.5) - pts.push_back({dx, dy}); - return cache.emplace(radius, std::move(pts)).first->second; -} - -void DrawRoundedCornerMask(SDL_Renderer * r, SDL_Rect rect, Uint8 cr, Uint8 cg, Uint8 cb, int radius) +void DrawRoundedCornerMask(SDL_Renderer * renderer, SDL_Rect rect, Uint8 cr, Uint8 cg, Uint8 cb, int radius) { if(radius <= 0) return; - SDL_SetRenderDrawColor(r, cr, cg, cb, 0xFF); - int x0 = rect.x, y0 = rect.y, x1 = rect.x+rect.w-1, y1 = rect.y+rect.h-1; - const std::vector & outside = RoundedCornerOutside(radius); - static std::vector points; // reused - see DrawStrokeRect - points.clear(); - points.reserve(outside.size() * 4); - for(const SDL_Point & p : outside) - { - points.push_back({ x0+radius-p.x, y0+radius-p.y }); - points.push_back({ x1-radius+p.x, y0+radius-p.y }); - points.push_back({ x0+radius-p.x, y1-radius+p.y }); - points.push_back({ x1-radius+p.x, y1-radius+p.y }); - } - SDL_RenderDrawPoints(r, points.data(), static_cast(points.size())); + SDL_SetRenderDrawColor(renderer, cr, cg, cb, 0xFF); + DrawMirroredCorners(renderer, rect, radius, CornerOffsets(CornerRegion::Outside, radius)); } -void DrawHLine(SDL_Renderer * r, int x0, int x1, int y, Uint8 cr, Uint8 cg, Uint8 cb, int width) +void DrawHLine(SDL_Renderer * renderer, int x0, int x1, int y, Uint8 cr, Uint8 cg, Uint8 cb, int width) { - DrawFillRect(r, { x0, y, x1-x0, width }, cr, cg, cb); + DrawFillRect(renderer, { x0, y, x1 - x0, width }, cr, cg, cb); } -void DrawVLine(SDL_Renderer * r, int x, int y0, int y1, Uint8 cr, Uint8 cg, Uint8 cb, int width) +void DrawVLine(SDL_Renderer * renderer, int x, int y0, int y1, Uint8 cr, Uint8 cg, Uint8 cb, int width) { - DrawFillRect(r, { x, y0, width, y1-y0 }, cr, cg, cb); + DrawFillRect(renderer, { x, y0, width, y1 - y0 }, cr, cg, cb); } diff --git a/src/framework/draw_helpers.h b/src/framework/draw_helpers.h index 1fd69b7..cf3b00d 100644 --- a/src/framework/draw_helpers.h +++ b/src/framework/draw_helpers.h @@ -21,14 +21,14 @@ #include // flat-line drawing for Modern UI - no textured art -void DrawStrokeRect(SDL_Renderer * r, SDL_Rect rect, Uint8 cr, Uint8 cg, Uint8 cb, int width, int radius = 0); -void DrawFillRect(SDL_Renderer * r, SDL_Rect rect, Uint8 cr, Uint8 cg, Uint8 cb); -void DrawRoundedFillRect(SDL_Renderer * r, SDL_Rect rect, Uint8 cr, Uint8 cg, Uint8 cb, int radius); -// paints just the 4 corner-square regions outside the rounded radius with -// a flat color - draw over a rectangularly-clipped image to fake rounding -// it to match a rounded container, without an actual alpha mask/render target -void DrawRoundedCornerMask(SDL_Renderer * r, SDL_Rect rect, Uint8 cr, Uint8 cg, Uint8 cb, int radius); -void DrawHLine(SDL_Renderer * r, int x0, int x1, int y, Uint8 cr, Uint8 cg, Uint8 cb, int width = 1); -void DrawVLine(SDL_Renderer * r, int x, int y0, int y1, Uint8 cr, Uint8 cg, Uint8 cb, int width = 1); +void DrawStrokeRect(SDL_Renderer * renderer, SDL_Rect rect, Uint8 cr, Uint8 cg, Uint8 cb, int width, int radius = 0); +void DrawFillRect(SDL_Renderer * renderer, SDL_Rect rect, Uint8 cr, Uint8 cg, Uint8 cb); +void DrawRoundedFillRect(SDL_Renderer * renderer, SDL_Rect rect, Uint8 cr, Uint8 cg, Uint8 cb, int radius); +// paints the corner-square pixels outside the rounded radius with a flat +// color - drawn over a rectangularly-clipped image, it fakes rounding it +// without an alpha mask or render target +void DrawRoundedCornerMask(SDL_Renderer * renderer, SDL_Rect rect, Uint8 cr, Uint8 cg, Uint8 cb, int radius); +void DrawHLine(SDL_Renderer * renderer, int x0, int x1, int y, Uint8 cr, Uint8 cg, Uint8 cb, int width = 1); +void DrawVLine(SDL_Renderer * renderer, int x, int y0, int y1, Uint8 cr, Uint8 cg, Uint8 cb, int width = 1); #endif diff --git a/src/framework/font8x8_lookup.cpp b/src/framework/font8x8_lookup.cpp index 2c5e9cf..48acd6e 100644 --- a/src/framework/font8x8_lookup.cpp +++ b/src/framework/font8x8_lookup.cpp @@ -20,13 +20,17 @@ #include -static const char * LookupKanjiGlyph(unsigned int codepoint) +namespace { - auto it = std::lower_bound(std::begin(font8x8_kanji), std::end(font8x8_kanji), codepoint, - [](const Font8x8KanjiEntry & entry, unsigned int cp) { return entry.codepoint < cp; }); - if(it != std::end(font8x8_kanji) && it->codepoint == codepoint) - return reinterpret_cast(it->glyph); - return nullptr; + // font8x8_kanji is sparse (JIS level 1 only), sorted by codepoint + const char * LookupKanjiGlyph(unsigned int codepoint) + { + const auto it = std::lower_bound(std::begin(font8x8_kanji), std::end(font8x8_kanji), codepoint, + [](const Font8x8KanjiEntry & entry, unsigned int cp) { return entry.codepoint < cp; }); + if(it != std::end(font8x8_kanji) && it->codepoint == codepoint) + return reinterpret_cast(it->glyph); + return nullptr; + } } const char * Font8x8Glyph(unsigned int codepoint) @@ -43,9 +47,8 @@ const char * Font8x8Glyph(unsigned int codepoint) return font8x8_katakana[codepoint - 0x30A0]; if(codepoint >= 0x4E00 && codepoint <= 0x9FFF) { - const char * kanjiGlyph = LookupKanjiGlyph(codepoint); - if(kanjiGlyph) + if(const char * kanjiGlyph = LookupKanjiGlyph(codepoint)) return kanjiGlyph; } - return font8x8_basic[0x3F]; + return font8x8_basic['?']; } diff --git a/src/framework/uitheme.cpp b/src/framework/uitheme.cpp index 64b22f0..63bebe9 100644 --- a/src/framework/uitheme.cpp +++ b/src/framework/uitheme.cpp @@ -44,21 +44,30 @@ namespace UiTheme BadgeColor BadgeStart{ 0xC2, 0xA8, 0x5E }; // muted gold BadgeColor BadgeStartDark{ 0x74, 0x65, 0x38 }; - static bool ParseTriple(const std::string & value, Uint8 & r, Uint8 & g, Uint8 & b) + namespace { - int rv, gv, bv; - if(std::sscanf(value.c_str(), "%d,%d,%d", &rv, &gv, &bv) != 3) - return false; - r = static_cast(rv); - g = static_cast(gv); - b = static_cast(bv); - return true; - } + bool ParseRgb(const std::string & value, Uint8 & r, Uint8 & g, Uint8 & b) + { + int rv = 0, gv = 0, bv = 0; + if(std::sscanf(value.c_str(), "%d,%d,%d", &rv, &gv, &bv) != 3) + return false; + r = static_cast(rv); + g = static_cast(gv); + b = static_cast(bv); + return true; + } - static Uint8 ClampedAdd(Uint8 base, int delta) - { - int v = base + delta; - return static_cast(v < 0 ? 0 : (v > 255 ? 255 : v)); + Uint8 ClampedAdd(Uint8 base, int delta) + { + const int v = base + delta; + return static_cast(v < 0 ? 0 : (v > 255 ? 255 : v)); + } + + // opaque AABBGGRR, the layout Texture's text color expects + Uint32 PackAbgr(Uint8 r, Uint8 g, Uint8 b) + { + return 0xFF000000u | (static_cast(b) << 16) | (static_cast(g) << 8) | r; + } } void LoadThemeConfig(const std::string & optionsLocation) @@ -73,13 +82,12 @@ namespace UiTheme { if(!line.empty() && line.back() == '\r') line.pop_back(); - size_t eq = line.find('='); + const size_t eq = line.find('='); if(eq == std::string::npos || line[0] == '#') continue; - std::string key = line.substr(0, eq); - std::string value = line.substr(eq + 1); - Uint8 r, g, b; - if(!ParseTriple(value, r, g, b)) + const std::string key = line.substr(0, eq); + Uint8 r = 0, g = 0, b = 0; + if(!ParseRgb(line.substr(eq + 1), r, g, b)) continue; if(key == "Bg") { BgR = r; BgG = g; BgB = b; bgSet = true; } @@ -87,9 +95,9 @@ namespace UiTheme else if(key == "Accent") { AccentR = r; AccentG = g; AccentB = b; } else if(key == "SelectedRowBg") { SelectedRowBgR = r; SelectedRowBgG = g; SelectedRowBgB = b; selectedRowBgSet = true; } else if(key == "ScrollArrow") { ScrollArrowR = r; ScrollArrowG = g; ScrollArrowB = b; scrollArrowSet = true; } - else if(key == "Text") TextColor = 0xFF000000 | (b << 16) | (g << 8) | r; - else if(key == "TextDim") TextDimColor = 0xFF000000 | (b << 16) | (g << 8) | r; - else if(key == "BadgeLetter") BadgeLetterColor = 0xFF000000 | (b << 16) | (g << 8) | r; + else if(key == "Text") TextColor = PackAbgr(r, g, b); + else if(key == "TextDim") TextDimColor = PackAbgr(r, g, b); + else if(key == "BadgeLetter") BadgeLetterColor = PackAbgr(r, g, b); else if(key == "BadgeA") BadgeA = { r, g, b }; else if(key == "BadgeADark") BadgeADark = { r, g, b }; else if(key == "BadgeB") BadgeB = { r, g, b }; diff --git a/src/framework/uitheme.h b/src/framework/uitheme.h index 8b808ad..a60aece 100644 --- a/src/framework/uitheme.h +++ b/src/framework/uitheme.h @@ -121,8 +121,7 @@ namespace UiTheme // (24px) from the last-drawn badge's own left edge; this closes the gap // to land the divider exactly 40px left of that badge, per hardware test const int BadgeDividerGapFromCluster = 16; - // packed AABBGGRR, kept in sync with the R/G/B triples below by - // LoadThemeConfig - not user-facing keys themselves + // packed AABBGGRR (Texture's text color layout), set by LoadThemeConfig extern Uint32 BadgeLetterColor; extern Uint32 TextColor; // primary text - titles, row labels, badge hints extern Uint32 TextDimColor; // muted grey for secondary text diff --git a/src/framework/utf8.cpp b/src/framework/utf8.cpp index 1a2f784..74feda8 100644 --- a/src/framework/utf8.cpp +++ b/src/framework/utf8.cpp @@ -17,42 +17,71 @@ #include "utf8.h" -std::vector Utf8ToCodepoints(const std::string & text) +namespace { - std::vector codepoints; - for(size_t i = 0; i < text.size();) + constexpr unsigned int ReplacementChar = 0xFFFD; + + bool IsContinuation(unsigned char byte) { - unsigned char firstByte = static_cast(text[i]); - unsigned int codepoint = 0xFFFD; - size_t sequenceLength = 1; + return (byte & 0xC0) == 0x80; + } + + // byte count implied by a lead byte; invalid leads count as 1 so callers + // always make progress + size_t SequenceLength(unsigned char lead) + { + if(lead >= 0xF0 && lead <= 0xF4) return 4; + if(lead >= 0xE0) return lead <= 0xEF ? 3 : 1; + if(lead >= 0xC2) return 2; + return 1; + } - if(firstByte < 0x80) + struct DecodeResult + { + unsigned int codepoint; + bool valid; // false if the sequence is malformed or cut short + }; + + DecodeResult DecodeAt(const std::string & text, size_t pos, size_t length) + { + const unsigned char lead = static_cast(text[pos]); + if(length == 1) + return { lead, lead < 0x80 }; + if(pos + length > text.size()) + return { ReplacementChar, false }; + + static const unsigned char LeadMask[] = { 0, 0, 0x1F, 0x0F, 0x07 }; + unsigned int codepoint = lead & LeadMask[length]; + for(size_t i = 1; i < length; ++i) { - codepoint = firstByte; + const unsigned char byte = static_cast(text[pos + i]); + if(!IsContinuation(byte)) + return { ReplacementChar, false }; + codepoint = (codepoint << 6) | (byte & 0x3F); } - else if(firstByte >= 0xC2 && firstByte <= 0xDF && i + 1 < text.size()) + return { codepoint, true }; + } +} + +std::vector Utf8ToCodepoints(const std::string & text) +{ + std::vector codepoints; + for(size_t pos = 0; pos < text.size();) + { + const size_t length = SequenceLength(static_cast(text[pos])); + const DecodeResult decoded = DecodeAt(text, pos, length); + if(decoded.valid) { - unsigned char secondByte = static_cast(text[i + 1]); - if((secondByte & 0xC0) == 0x80) - { - codepoint = ((firstByte & 0x1F) << 6) | (secondByte & 0x3F); - sequenceLength = 2; - } + codepoints.push_back(decoded.codepoint); + pos += length; } - else if(firstByte >= 0xE0 && firstByte <= 0xEF && i + 2 < text.size()) + else { - unsigned char secondByte = static_cast(text[i + 1]); - unsigned char thirdByte = static_cast(text[i + 2]); - if((secondByte & 0xC0) == 0x80 && (thirdByte & 0xC0) == 0x80) - { - codepoint = ((firstByte & 0x0F) << 12) | - ((secondByte & 0x3F) << 6) | (thirdByte & 0x3F); - sequenceLength = 3; - } + // only the lead byte is consumed, so a stray lead can't swallow + // the valid characters after it + codepoints.push_back(ReplacementChar); + ++pos; } - - codepoints.push_back(codepoint); - i += sequenceLength; } return codepoints; } @@ -60,25 +89,15 @@ std::vector Utf8ToCodepoints(const std::string & text) int Utf8Length(const std::string & text) { int count = 0; - for(size_t i = 0; i < text.size();) - { - unsigned char c = static_cast(text[i]); - i += (c >= 0xF0) ? 4 : (c >= 0xE0) ? 3 : (c >= 0xC0) ? 2 : 1; + for(size_t pos = 0; pos < text.size(); pos += SequenceLength(static_cast(text[pos]))) ++count; - } return count; } std::string TruncateUtf8(const std::string & text, int maxCodepoints) { - size_t i = 0; - int count = 0; - while(i < text.size() && count < maxCodepoints) - { - unsigned char c = static_cast(text[i]); - size_t len = (c >= 0xF0) ? 4 : (c >= 0xE0) ? 3 : (c >= 0xC0) ? 2 : 1; - i += len; - ++count; - } - return text.substr(0, i); + size_t pos = 0; + for(int count = 0; pos < text.size() && count < maxCodepoints; ++count) + pos += SequenceLength(static_cast(text[pos])); + return text.substr(0, pos); } diff --git a/src/gen_splash.cpp b/src/gen_splash.cpp index df19ca1..08dec28 100644 --- a/src/gen_splash.cpp +++ b/src/gen_splash.cpp @@ -11,8 +11,6 @@ #include "framework/font8x8_lookup.h" #include "framework/utf8.h" -#include -#include #include #include #include @@ -20,78 +18,73 @@ namespace { - const int Width = 1280; - const int Height = 720; - const int Scale = 3; - const int Cell = 8 * Scale; - const int LineSpacing = Cell + Cell/2; + const std::string OptionsRoot = "/etc/options_menu/"; + constexpr int ScreenW = 1280; + constexpr int ScreenH = 720; + constexpr int BytesPerPixel = 3; + constexpr int GlyphBits = 8; // font8x8 glyphs are 8x8 bitmaps + constexpr int Scale = 3; + constexpr int GlyphPx = GlyphBits * Scale; + constexpr int LineGap = GlyphPx / 2; + constexpr int LineSpacing = GlyphPx + LineGap; + constexpr unsigned char White = 255; - void DrawLine(std::vector & pixels, const std::string & text, int topY) + // one line of text, horizontally centered, top edge at topY + void DrawTextLine(std::vector & pixels, const std::string & text, int topY) { - auto codepoints = Utf8ToCodepoints(text); - int totalWidth = static_cast(codepoints.size()) * Cell; - int x0 = (Width - totalWidth) / 2; + const std::vector codepoints = Utf8ToCodepoints(text); + const int lineW = static_cast(codepoints.size()) * GlyphPx; + const int lineX = (ScreenW - lineW) / 2; - for (size_t i = 0; i < codepoints.size(); ++i) + for(size_t i = 0; i < codepoints.size(); ++i) { - const char * bitmap = Font8x8Glyph(codepoints[i]); - int ox = x0 + static_cast(i) * Cell; - for (int y = 0; y < 8; ++y) - { - for (int x = 0; x < 8; ++x) + const char * glyph = Font8x8Glyph(codepoints[i]); + const int glyphX = lineX + static_cast(i) * GlyphPx; + for(int row = 0; row < GlyphBits; ++row) + for(int col = 0; col < GlyphBits; ++col) { - if (!(bitmap[y] & (1 << x))) + if(!(glyph[row] & (1 << col))) continue; - for (int sy = 0; sy < Scale; ++sy) - { - for (int sx = 0; sx < Scale; ++sx) + for(int sy = 0; sy < Scale; ++sy) + for(int sx = 0; sx < Scale; ++sx) { - int px = ox + x*Scale + sx; - int py = topY + y*Scale + sy; - if (px < 0 || px >= Width || py < 0 || py >= Height) + const int px = glyphX + col * Scale + sx; + const int py = topY + row * Scale + sy; + if(px < 0 || px >= ScreenW || py < 0 || py >= ScreenH) continue; - size_t idx = (static_cast(py) * Width + px) * 3; - pixels[idx] = pixels[idx+1] = pixels[idx+2] = 255; + const size_t offset = (static_cast(py) * ScreenW + px) * BytesPerPixel; + pixels[offset] = pixels[offset + 1] = pixels[offset + 2] = White; } - } } - } } } } int main(int argc, char * argv[]) { - if (argc < 3) + if(argc < 3) { std::cerr << "Usage: " << argv[0] << " [line-key...]\n"; return 1; } - std::string outputPath = argv[1]; - int lineCount = argc - 2; + const std::string outputPath = argv[1]; + const int lineCount = argc - 2; - std::string langCode("en-US"); - std::ifstream in("/etc/options_menu/language.cfg"); - std::getline(in, langCode); - in.close(); - if (langCode.empty()) - langCode = "en-US"; - LoadLanguage("/etc/options_menu/", langCode); + LoadLanguageFromConfig(OptionsRoot); - std::vector pixels(Width * Height * 3, 0); - int blockHeight = lineCount * Cell + (lineCount - 1) * (Cell/2); - int startY = (Height - blockHeight) / 2; - for (int i = 0; i < lineCount; ++i) - DrawLine(pixels, Translate(argv[2 + i]), startY + i * LineSpacing); + std::vector pixels(static_cast(ScreenW) * ScreenH * BytesPerPixel, 0); + const int blockH = lineCount * LineSpacing - LineGap; + const int firstLineY = (ScreenH - blockH) / 2; + for(int i = 0; i < lineCount; ++i) + DrawTextLine(pixels, Translate(argv[2 + i]), firstLineY + i * LineSpacing); - png_image png; - memset(&png, 0, sizeof(png)); + png_image png{}; png.version = PNG_IMAGE_VERSION; - png.width = Width; - png.height = Height; + png.width = ScreenW; + png.height = ScreenH; png.format = PNG_FORMAT_RGB; - if (!png_image_write_to_file(&png, outputPath.c_str(), 0, pixels.data(), 0, nullptr)) + if(!png_image_write_to_file(&png, outputPath.c_str(), 0, pixels.data(), 0, nullptr)) { std::cerr << "Cannot write png file: " << outputPath << "\n"; return 1; diff --git a/src/localization.cpp b/src/localization.cpp index 5c40e26..4d2d9eb 100644 --- a/src/localization.cpp +++ b/src/localization.cpp @@ -12,66 +12,78 @@ #include #include #include +#include -static std::map strings; - -static void LoadLanguageFile(const std::string & path) +namespace { - std::ifstream in(path); - std::string line; - while(std::getline(in, line)) + const std::string DefaultLangCode = "en-US"; + + // I.2 exception: Translate() is a free function called from every screen + std::map translations; + + using DirHandle = std::unique_ptr; + + bool FileExists(const std::string & path) { - if(!line.empty() && line.back() == '\r') - line.pop_back(); - if(line.empty() || line[0] == '#') - continue; - size_t index = line.find('='); - if(index == std::string::npos) - continue; - strings[line.substr(0, index)] = line.substr(index+1); + return std::ifstream(path).good(); } -} -static void LoadLanguageDir(const std::string & langDir, const std::string & langCode) -{ - LoadLanguageFile(langDir + "en-US.lang"); - if(langCode != "en-US") - LoadLanguageFile(langDir + langCode + ".lang"); + void LoadLanguageFile(const std::string & path) + { + std::ifstream in(path); + std::string line; + while(std::getline(in, line)) + { + if(!line.empty() && line.back() == '\r') + line.pop_back(); + if(line.empty() || line[0] == '#') + continue; + const size_t eq = line.find('='); + if(eq == std::string::npos) + continue; + translations[line.substr(0, eq)] = line.substr(eq + 1); + } + } + + // en-US first so keys missing from a partial translation still resolve + void LoadLanguageDir(const std::string & langDir, const std::string & langCode) + { + LoadLanguageFile(langDir + DefaultLangCode + ".lang"); + if(langCode != DefaultLangCode) + LoadLanguageFile(langDir + langCode + ".lang"); + } } void LoadLanguage(const std::string & optionsRoot, const std::string & langCode) { - strings.clear(); + translations.clear(); LoadLanguageDir(optionsRoot + "language/", langCode); - if(auto dir = opendir(optionsRoot.c_str())) + const DirHandle dir(opendir(optionsRoot.c_str()), &closedir); + if(!dir) + return; + while(const dirent * entry = readdir(dir.get())) { - while(auto entry = readdir(dir)) - { - if(entry->d_type != DT_DIR || entry->d_name[0] == '.') - continue; - std::string pluginLangDir = optionsRoot + entry->d_name + "/lang/"; - std::ifstream check(pluginLangDir + "en-US.lang"); - if(check.good()) - LoadLanguageDir(pluginLangDir, langCode); - } - closedir(dir); + if(entry->d_type != DT_DIR || entry->d_name[0] == '.') + continue; + const std::string pluginLangDir = optionsRoot + entry->d_name + "/lang/"; + if(FileExists(pluginLangDir + DefaultLangCode + ".lang")) + LoadLanguageDir(pluginLangDir, langCode); } } -std::string Translate(const std::string & key) +void LoadLanguageFromConfig(const std::string & optionsRoot) { - auto it = strings.find(key); - return it == strings.end() ? key : it->second; + std::string langCode; + std::ifstream in(optionsRoot + "language.cfg"); + std::getline(in, langCode); + if(!langCode.empty() && langCode.back() == '\r') + langCode.pop_back(); + LoadLanguage(optionsRoot, langCode.empty() ? DefaultLangCode : langCode); } -void LoadLanguageFromConfig(const std::string & optionsRoot) +std::string Translate(const std::string & key) { - std::string langCode("en-US"); - std::ifstream in("/etc/options_menu/language.cfg"); - std::getline(in, langCode); - in.close(); - if(langCode.empty()) - langCode = "en-US"; - LoadLanguage(optionsRoot, langCode); + const auto it = translations.find(key); + return it == translations.end() ? key : it->second; } diff --git a/src/localization.h b/src/localization.h index 6baccfd..d9a84ee 100644 --- a/src/localization.h +++ b/src/localization.h @@ -12,13 +12,15 @@ #include -//optionsRoot is the options_menu install directory (e.g. "/etc/options_menu/"). -//Loads optionsRoot/lang/, then merges in //lang/ for any -//subfolder that has one (e.g. a plugin installed alongside options_menu). +// optionsRoot is the options_menu install directory, with a trailing slash +// (e.g. "/etc/options_menu/"). Loads /language/, then merges in +// //lang/ for every plugin subfolder that has one. void LoadLanguage(const std::string & optionsRoot, const std::string & langCode); -std::string Translate(const std::string & key); -// reads /etc/options_menu/language.cfg (defaulting to en-US) and loads it +// reads /language.cfg (en-US if missing or empty) and loads it void LoadLanguageFromConfig(const std::string & optionsRoot); +// key itself when there is no translation for it +std::string Translate(const std::string & key); + #endif From e32b11be2b33dde3ac7d0387f1708fb3a65592a7 Mon Sep 17 00:00:00 2001 From: DefKorns Date: Fri, 25 Sep 2026 00:33:18 +0000 Subject: [PATCH 050/104] Replace RGB triples and packed colors with a Color type --- src/command.cpp | 20 +++--- src/command.h | 2 +- src/framework/color.h | 27 ++++++++ src/framework/draw_helpers.cpp | 34 ++++++---- src/framework/draw_helpers.h | 18 +++-- src/framework/uitheme.cpp | 119 +++++++++++++++------------------ src/framework/uitheme.h | 42 ++++++------ src/main.cpp | 62 +++++++++-------- src/mod_uninstall.cpp | 66 +++++++++--------- 9 files changed, 209 insertions(+), 181 deletions(-) create mode 100644 src/framework/color.h diff --git a/src/command.cpp b/src/command.cpp index d3a3470..51e0544 100644 --- a/src/command.cpp +++ b/src/command.cpp @@ -99,21 +99,21 @@ Command::Command(std::ifstream & in) in.close(); } -void Command::RunCommand(SDL_Context & sdl_context, Controller * controller, const ModernChrome & chrome, Uint8 bgR, Uint8 bgG, Uint8 bgB) const +void Command::RunCommand(SDL_Context & sdl_context, Controller * controller, const ModernChrome & chrome, Color bg) const { std::list textList; FILE* pipe = popen(command.c_str(), "r"); if(pipe) { auto renderer = sdl_context.renderer; - SDL_SetRenderDrawColor(renderer, UiTheme::BgR, UiTheme::BgG, UiTheme::BgB, SDL_ALPHA_OPAQUE); + SetDrawColor(renderer, UiTheme::Bg); char buffer[128] = {0}; const int textX = UiTheme::FrameRect.x + UiTheme::FrameInset; const int textFirstY = UiTheme::HeaderDividerY + 24; // same app header/footer chrome as the main screen, plus a persistent B/Exit badge - Texture exitLetter("B", 16, renderer, 0, 0, false, UiTheme::BadgeLetterColor, true); - Texture exitLabel(Translate("EXIT"), 16, renderer, 0, 0, false, UiTheme::TextColor, true); + Texture exitLetter("B", 16, renderer, 0, 0, false, ToAbgr(UiTheme::BadgeLetter), true); + Texture exitLabel(Translate("EXIT"), 16, renderer, 0, 0, false, ToAbgr(UiTheme::Text), true); int badgeGroupW = UiTheme::BadgeOuterSize + UiTheme::BadgeLabelGap + exitLabel.rect.w; SDL_Rect exitBadge{ UiTheme::BadgeClusterRightX - badgeGroupW, UiTheme::BadgeBandY, UiTheme::BadgeOuterSize, UiTheme::BadgeOuterSize }; exitLetter.rect.x = exitBadge.x + (exitBadge.w - exitLetter.rect.w) / 2; @@ -124,14 +124,14 @@ void Command::RunCommand(SDL_Context & sdl_context, Controller * controller, con auto render = [&]() { sdl_context.StartFrame(); - DrawStrokeRect(renderer, UiTheme::OuterRect, UiTheme::BorderR, UiTheme::BorderG, UiTheme::BorderB, UiTheme::BorderWidth, UiTheme::BorderRadius); + DrawStrokeRect(renderer, UiTheme::OuterRect, UiTheme::Border, UiTheme::BorderWidth, UiTheme::BorderRadius); chrome.gearIcon.Draw(renderer); chrome.appTitleText.Draw(renderer); chrome.appVersionText.Draw(renderer); - DrawHLine(renderer, UiTheme::HeaderDividerX, UiTheme::HeaderDividerX + UiTheme::HeaderDividerW, UiTheme::HeaderDividerY, UiTheme::BorderR, UiTheme::BorderG, UiTheme::BorderB, UiTheme::BorderWidth); - DrawHLine(renderer, UiTheme::OuterRect.x, UiTheme::OuterRect.x + UiTheme::OuterRect.w, UiTheme::FooterDividerY, UiTheme::BorderR, UiTheme::BorderG, UiTheme::BorderB, UiTheme::BorderWidth); + DrawHLine(renderer, UiTheme::HeaderDividerX, UiTheme::HeaderDividerX + UiTheme::HeaderDividerW, UiTheme::HeaderDividerY, UiTheme::Border, UiTheme::BorderWidth); + DrawHLine(renderer, UiTheme::OuterRect.x, UiTheme::OuterRect.x + UiTheme::OuterRect.w, UiTheme::FooterDividerY, UiTheme::Border, UiTheme::BorderWidth); chrome.creditText.Draw(renderer); - DrawRoundedFillRect(renderer, exitBadge, UiTheme::BadgeB.r, UiTheme::BadgeB.g, UiTheme::BadgeB.b, exitBadge.w/2); + DrawRoundedFillRect(renderer, exitBadge, UiTheme::BadgeB, exitBadge.w/2); exitLetter.Draw(renderer); exitLabel.Draw(renderer); int y = textFirstY; @@ -141,7 +141,7 @@ void Command::RunCommand(SDL_Context & sdl_context, Controller * controller, con t.Draw(renderer); y+=10; } - SDL_SetRenderDrawColor(renderer, UiTheme::BgR, UiTheme::BgG, UiTheme::BgB, 0xFF); // must be the LAST color-setting call, or it leaks into next frame's clear + SetDrawColor(renderer, UiTheme::Bg); // must be the LAST color-setting call, or it leaks into next frame's clear sdl_context.EndFrame(); }; @@ -189,7 +189,7 @@ void Command::RunCommand(SDL_Context & sdl_context, Controller * controller, con controller->Update(); render(); } - SDL_SetRenderDrawColor(renderer, bgR, bgG, bgB, 0xFF); + SetDrawColor(renderer, bg); } } diff --git a/src/command.h b/src/command.h index 8fae74b..46f2b13 100644 --- a/src/command.h +++ b/src/command.h @@ -27,7 +27,7 @@ struct Command { Command(); Command(std::ifstream & in); - void RunCommand(SDL_Context & sdl_context, Controller * controller, const ModernChrome & chrome, Uint8 bgR = UiTheme::BgR, Uint8 bgG = UiTheme::BgG, Uint8 bgB = UiTheme::BgB) const; + void RunCommand(SDL_Context & sdl_context, Controller * controller, const ModernChrome & chrome, Color bg = UiTheme::Bg) const; void UpdateState(); std::string name; diff --git a/src/framework/color.h b/src/framework/color.h new file mode 100644 index 0000000..2a618ef --- /dev/null +++ b/src/framework/color.h @@ -0,0 +1,27 @@ +/** + * Copyright (C) 2026 DefKorns + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 3 + * of the License, or (at your option) any later version. + */ + +#ifndef COLOR_H_ +#define COLOR_H_ + +#include + +// opaque RGB; alpha is always 0xFF wherever it's drawn +struct Color +{ + Uint8 r, g, b; +}; + +// packed AABBGGRR, the layout Texture's text color parameter takes +constexpr Uint32 ToAbgr(Color color) +{ + return 0xFF000000u | (static_cast(color.b) << 16) | (static_cast(color.g) << 8) | color.r; +} + +#endif diff --git a/src/framework/draw_helpers.cpp b/src/framework/draw_helpers.cpp index b4b0d6b..2b195ba 100644 --- a/src/framework/draw_helpers.cpp +++ b/src/framework/draw_helpers.cpp @@ -74,9 +74,19 @@ namespace } } -void DrawStrokeRect(SDL_Renderer * renderer, SDL_Rect rect, Uint8 cr, Uint8 cg, Uint8 cb, int width, int radius) +void SetDrawColor(SDL_Renderer * renderer, Color color) { - SDL_SetRenderDrawColor(renderer, cr, cg, cb, 0xFF); + SDL_SetRenderDrawColor(renderer, color.r, color.g, color.b, SDL_ALPHA_OPAQUE); +} + +void SetColorMod(SDL_Texture * texture, Color color) +{ + SDL_SetTextureColorMod(texture, color.r, color.g, color.b); +} + +void DrawStrokeRect(SDL_Renderer * renderer, SDL_Rect rect, Color color, int width, int radius) +{ + SetDrawColor(renderer, color); const int x0 = rect.x, y0 = rect.y, x1 = rect.x + rect.w - 1, y1 = rect.y + rect.h - 1; for(int i = 0; i < width; ++i) { @@ -89,15 +99,15 @@ void DrawStrokeRect(SDL_Renderer * renderer, SDL_Rect rect, Uint8 cr, Uint8 cg, DrawMirroredCorners(renderer, rect, radius, CornerOffsets(CornerRegion::Arc, radius, width)); } -void DrawFillRect(SDL_Renderer * renderer, SDL_Rect rect, Uint8 cr, Uint8 cg, Uint8 cb) +void DrawFillRect(SDL_Renderer * renderer, SDL_Rect rect, Color color) { - SDL_SetRenderDrawColor(renderer, cr, cg, cb, 0xFF); + SetDrawColor(renderer, color); SDL_RenderFillRect(renderer, &rect); } -void DrawRoundedFillRect(SDL_Renderer * renderer, SDL_Rect rect, Uint8 cr, Uint8 cg, Uint8 cb, int radius) +void DrawRoundedFillRect(SDL_Renderer * renderer, SDL_Rect rect, Color color, int radius) { - SDL_SetRenderDrawColor(renderer, cr, cg, cb, 0xFF); + SetDrawColor(renderer, color); if(radius <= 0) { SDL_RenderFillRect(renderer, &rect); @@ -111,20 +121,20 @@ void DrawRoundedFillRect(SDL_Renderer * renderer, SDL_Rect rect, Uint8 cr, Uint8 DrawMirroredCorners(renderer, rect, radius, CornerOffsets(CornerRegion::Disk, radius)); } -void DrawRoundedCornerMask(SDL_Renderer * renderer, SDL_Rect rect, Uint8 cr, Uint8 cg, Uint8 cb, int radius) +void DrawRoundedCornerMask(SDL_Renderer * renderer, SDL_Rect rect, Color color, int radius) { if(radius <= 0) return; - SDL_SetRenderDrawColor(renderer, cr, cg, cb, 0xFF); + SetDrawColor(renderer, color); DrawMirroredCorners(renderer, rect, radius, CornerOffsets(CornerRegion::Outside, radius)); } -void DrawHLine(SDL_Renderer * renderer, int x0, int x1, int y, Uint8 cr, Uint8 cg, Uint8 cb, int width) +void DrawHLine(SDL_Renderer * renderer, int x0, int x1, int y, Color color, int width) { - DrawFillRect(renderer, { x0, y, x1 - x0, width }, cr, cg, cb); + DrawFillRect(renderer, { x0, y, x1 - x0, width }, color); } -void DrawVLine(SDL_Renderer * renderer, int x, int y0, int y1, Uint8 cr, Uint8 cg, Uint8 cb, int width) +void DrawVLine(SDL_Renderer * renderer, int x, int y0, int y1, Color color, int width) { - DrawFillRect(renderer, { x, y0, width, y1 - y0 }, cr, cg, cb); + DrawFillRect(renderer, { x, y0, width, y1 - y0 }, color); } diff --git a/src/framework/draw_helpers.h b/src/framework/draw_helpers.h index cf3b00d..a8585de 100644 --- a/src/framework/draw_helpers.h +++ b/src/framework/draw_helpers.h @@ -18,17 +18,23 @@ #ifndef DRAW_HELPERS_H_ #define DRAW_HELPERS_H_ +#include "color.h" + #include +void SetDrawColor(SDL_Renderer * renderer, Color color); +// tints a white asset texture +void SetColorMod(SDL_Texture * texture, Color color); + // flat-line drawing for Modern UI - no textured art -void DrawStrokeRect(SDL_Renderer * renderer, SDL_Rect rect, Uint8 cr, Uint8 cg, Uint8 cb, int width, int radius = 0); -void DrawFillRect(SDL_Renderer * renderer, SDL_Rect rect, Uint8 cr, Uint8 cg, Uint8 cb); -void DrawRoundedFillRect(SDL_Renderer * renderer, SDL_Rect rect, Uint8 cr, Uint8 cg, Uint8 cb, int radius); +void DrawStrokeRect(SDL_Renderer * renderer, SDL_Rect rect, Color color, int width, int radius = 0); +void DrawFillRect(SDL_Renderer * renderer, SDL_Rect rect, Color color); +void DrawRoundedFillRect(SDL_Renderer * renderer, SDL_Rect rect, Color color, int radius); // paints the corner-square pixels outside the rounded radius with a flat // color - drawn over a rectangularly-clipped image, it fakes rounding it // without an alpha mask or render target -void DrawRoundedCornerMask(SDL_Renderer * renderer, SDL_Rect rect, Uint8 cr, Uint8 cg, Uint8 cb, int radius); -void DrawHLine(SDL_Renderer * renderer, int x0, int x1, int y, Uint8 cr, Uint8 cg, Uint8 cb, int width = 1); -void DrawVLine(SDL_Renderer * renderer, int x, int y0, int y1, Uint8 cr, Uint8 cg, Uint8 cb, int width = 1); +void DrawRoundedCornerMask(SDL_Renderer * renderer, SDL_Rect rect, Color color, int radius); +void DrawHLine(SDL_Renderer * renderer, int x0, int x1, int y, Color color, int width = 1); +void DrawVLine(SDL_Renderer * renderer, int x, int y0, int y1, Color color, int width = 1); #endif diff --git a/src/framework/uitheme.cpp b/src/framework/uitheme.cpp index 63bebe9..b2b3ee9 100644 --- a/src/framework/uitheme.cpp +++ b/src/framework/uitheme.cpp @@ -19,42 +19,45 @@ #include #include +#include #include namespace UiTheme { - Uint8 BgR = 0x0C, BgG = 0x0F, BgB = 0x16; - Uint8 BorderR = 0x2A, BorderG = 0x2C, BorderB = 0x33; - Uint8 AccentR = 0xF0, AccentG = 0xC4, AccentB = 0x3D; // yellow - Uint8 SelectedRowBgR = 0x14, SelectedRowBgG = 0x16, SelectedRowBgB = 0x1A; - Uint8 ScrollArrowR = 0xFF, ScrollArrowG = 0xFF, ScrollArrowB = 0xFF; - - Uint32 BadgeLetterColor = 0xFF16161C; // AABBGGRR - Uint32 TextColor = 0xFFFFFFFF; // AABBGGRR - primary text - Uint32 TextDimColor = 0xFF938D8D; // AABBGGRR - muted grey for secondary text - - BadgeColor BadgeA{ 0x4C, 0xAF, 0x6E }; // green - BadgeColor BadgeADark{ 0x2E, 0x6B, 0x42 }; - BadgeColor BadgeB{ 0xE1, 0x55, 0x54 }; // red - BadgeColor BadgeBDark{ 0x8C, 0x34, 0x33 }; - BadgeColor BadgeX{ 0x6C, 0x8E, 0xBF }; // slate-blue - BadgeColor BadgeXDark{ 0x3F, 0x59, 0x7F }; - BadgeColor BadgeY{ 0xC9, 0x7B, 0x84 }; // dusty rose, unused - BadgeColor BadgeYDark{ 0x8F, 0x47, 0x50 }; - BadgeColor BadgeStart{ 0xC2, 0xA8, 0x5E }; // muted gold - BadgeColor BadgeStartDark{ 0x74, 0x65, 0x38 }; + Color Bg{ 0x0C, 0x0F, 0x16 }; + Color Border{ 0x2A, 0x2C, 0x33 }; + Color Accent{ 0xF0, 0xC4, 0x3D }; // yellow + Color SelectedRowBg{ 0x14, 0x16, 0x1A }; + Color Text{ 0xFF, 0xFF, 0xFF }; + Color TextDim{ 0x8D, 0x8D, 0x93 }; + Color ScrollArrow{ 0xFF, 0xFF, 0xFF }; + Color BadgeLetter{ 0x1C, 0x16, 0x16 }; + Color BadgeA{ 0x4C, 0xAF, 0x6E }; + Color BadgeADark{ 0x2E, 0x6B, 0x42 }; + Color BadgeB{ 0xE1, 0x55, 0x54 }; + Color BadgeBDark{ 0x8C, 0x34, 0x33 }; + Color BadgeX{ 0x6C, 0x8E, 0xBF }; + Color BadgeXDark{ 0x3F, 0x59, 0x7F }; + Color BadgeY{ 0xC9, 0x7B, 0x84 }; + Color BadgeYDark{ 0x8F, 0x47, 0x50 }; + Color BadgeStart{ 0xC2, 0xA8, 0x5E }; + Color BadgeStartDark{ 0x74, 0x65, 0x38 }; namespace { - bool ParseRgb(const std::string & value, Uint8 & r, Uint8 & g, Uint8 & b) + struct ParsedColor { - int rv = 0, gv = 0, bv = 0; - if(std::sscanf(value.c_str(), "%d,%d,%d", &rv, &gv, &bv) != 3) - return false; - r = static_cast(rv); - g = static_cast(gv); - b = static_cast(bv); - return true; + Color color; + bool valid; + }; + + // "R,G,B", each 0-255 + ParsedColor ParseRgb(const std::string & value) + { + int r = 0, g = 0, b = 0; + if(std::sscanf(value.c_str(), "%d,%d,%d", &r, &g, &b) != 3) + return { {}, false }; + return { { static_cast(r), static_cast(g), static_cast(b) }, true }; } Uint8 ClampedAdd(Uint8 base, int delta) @@ -63,10 +66,19 @@ namespace UiTheme return static_cast(v < 0 ? 0 : (v > 255 ? 255 : v)); } - // opaque AABBGGRR, the layout Texture's text color expects - Uint32 PackAbgr(Uint8 r, Uint8 g, Uint8 b) + const std::map & ColorKeys() { - return 0xFF000000u | (static_cast(b) << 16) | (static_cast(g) << 8) | r; + static const std::map keys{ + { "Bg", &Bg }, { "Border", &Border }, { "Accent", &Accent }, + { "SelectedRowBg", &SelectedRowBg }, { "Text", &Text }, { "TextDim", &TextDim }, + { "ScrollArrow", &ScrollArrow }, { "BadgeLetter", &BadgeLetter }, + { "BadgeA", &BadgeA }, { "BadgeADark", &BadgeADark }, + { "BadgeB", &BadgeB }, { "BadgeBDark", &BadgeBDark }, + { "BadgeX", &BadgeX }, { "BadgeXDark", &BadgeXDark }, + { "BadgeY", &BadgeY }, { "BadgeYDark", &BadgeYDark }, + { "BadgeStart", &BadgeStart }, { "BadgeStartDark", &BadgeStartDark }, + }; + return keys; } } @@ -76,7 +88,7 @@ namespace UiTheme if(!in.is_open()) return; - bool bgSet = false, selectedRowBgSet = false, scrollArrowSet = false; + std::map isSet; std::string line; while(std::getline(in, line)) { @@ -85,47 +97,22 @@ namespace UiTheme const size_t eq = line.find('='); if(eq == std::string::npos || line[0] == '#') continue; - const std::string key = line.substr(0, eq); - Uint8 r = 0, g = 0, b = 0; - if(!ParseRgb(line.substr(eq + 1), r, g, b)) + const auto key = ColorKeys().find(line.substr(0, eq)); + const ParsedColor parsed = ParseRgb(line.substr(eq + 1)); + if(key == ColorKeys().end() || !parsed.valid) continue; - - if(key == "Bg") { BgR = r; BgG = g; BgB = b; bgSet = true; } - else if(key == "Border") { BorderR = r; BorderG = g; BorderB = b; } - else if(key == "Accent") { AccentR = r; AccentG = g; AccentB = b; } - else if(key == "SelectedRowBg") { SelectedRowBgR = r; SelectedRowBgG = g; SelectedRowBgB = b; selectedRowBgSet = true; } - else if(key == "ScrollArrow") { ScrollArrowR = r; ScrollArrowG = g; ScrollArrowB = b; scrollArrowSet = true; } - else if(key == "Text") TextColor = PackAbgr(r, g, b); - else if(key == "TextDim") TextDimColor = PackAbgr(r, g, b); - else if(key == "BadgeLetter") BadgeLetterColor = PackAbgr(r, g, b); - else if(key == "BadgeA") BadgeA = { r, g, b }; - else if(key == "BadgeADark") BadgeADark = { r, g, b }; - else if(key == "BadgeB") BadgeB = { r, g, b }; - else if(key == "BadgeBDark") BadgeBDark = { r, g, b }; - else if(key == "BadgeX") BadgeX = { r, g, b }; - else if(key == "BadgeXDark") BadgeXDark = { r, g, b }; - else if(key == "BadgeY") BadgeY = { r, g, b }; - else if(key == "BadgeYDark") BadgeYDark = { r, g, b }; - else if(key == "BadgeStart") BadgeStart = { r, g, b }; - else if(key == "BadgeStartDark") BadgeStartDark = { r, g, b }; + *key->second = parsed.color; + isSet[key->first] = true; } // SelectedRowBg (tile/row fill - shows at rounded-corner masks, so a // stale default next to a customized Bg reads as a leftover old // color) tracks a custom Bg unless set explicitly itself, keeping // the same +8/+7/+4 offset as the built-in defaults - if(bgSet && !selectedRowBgSet) - { - SelectedRowBgR = ClampedAdd(BgR, 8); - SelectedRowBgG = ClampedAdd(BgG, 7); - SelectedRowBgB = ClampedAdd(BgB, 4); - } + if(isSet["Bg"] && !isSet["SelectedRowBg"]) + SelectedRowBg = { ClampedAdd(Bg.r, 8), ClampedAdd(Bg.g, 7), ClampedAdd(Bg.b, 4) }; - if(!scrollArrowSet) - { - ScrollArrowR = TextColor & 0xFF; - ScrollArrowG = (TextColor >> 8) & 0xFF; - ScrollArrowB = (TextColor >> 16) & 0xFF; - } + if(!isSet["ScrollArrow"]) + ScrollArrow = Text; } } diff --git a/src/framework/uitheme.h b/src/framework/uitheme.h index a60aece..be43a0d 100644 --- a/src/framework/uitheme.h +++ b/src/framework/uitheme.h @@ -18,6 +18,8 @@ #ifndef UITHEME_H_ #define UITHEME_H_ +#include "color.h" + #include #include @@ -33,11 +35,25 @@ namespace UiTheme // defaults stand void LoadThemeConfig(const std::string & optionsLocation); - extern Uint8 BgR, BgG, BgB; - extern Uint8 BorderR, BorderG, BorderB; - extern Uint8 AccentR, AccentG, AccentB; - extern Uint8 SelectedRowBgR, SelectedRowBgG, SelectedRowBgB; - extern Uint8 ScrollArrowR, ScrollArrowG, ScrollArrowB; // follows Text unless set + // palette - each one is a theme.cfg key of the same name + extern Color Bg; + extern Color Border; // dividers, box outlines + extern Color Accent; // selected row border, section title bar, submenu chevrons + extern Color SelectedRowBg; // follows Bg unless set + extern Color Text; // titles, row labels, badge hints + extern Color TextDim; // secondary text + extern Color ScrollArrow; // follows Text unless set + extern Color BadgeLetter; // letter inside a badge circle + extern Color BadgeA; // green + extern Color BadgeADark; + extern Color BadgeB; // red + extern Color BadgeBDark; + extern Color BadgeX; // slate-blue + extern Color BadgeXDark; + extern Color BadgeY; // dusty rose, unused + extern Color BadgeYDark; + extern Color BadgeStart; // muted gold, for a wide "Start" pill instead of a letter circle + extern Color BadgeStartDark; // TV overscan safe area const int SafeMarginX = 64; @@ -121,22 +137,6 @@ namespace UiTheme // (24px) from the last-drawn badge's own left edge; this closes the gap // to land the divider exactly 40px left of that badge, per hardware test const int BadgeDividerGapFromCluster = 16; - // packed AABBGGRR (Texture's text color layout), set by LoadThemeConfig - extern Uint32 BadgeLetterColor; - extern Uint32 TextColor; // primary text - titles, row labels, badge hints - extern Uint32 TextDimColor; // muted grey for secondary text - - struct BadgeColor { Uint8 r, g, b; }; - extern BadgeColor BadgeA; // green - extern BadgeColor BadgeADark; - extern BadgeColor BadgeB; // red - extern BadgeColor BadgeBDark; - extern BadgeColor BadgeX; // slate-blue - extern BadgeColor BadgeXDark; - extern BadgeColor BadgeY; // dusty rose, unused - extern BadgeColor BadgeYDark; - extern BadgeColor BadgeStart; // muted gold, for a wide "Start" pill instead of a letter circle - extern BadgeColor BadgeStartDark; // "created by CompCom" footer credit, left-aligned in the footer const int CreditX = FrameX; diff --git a/src/main.cpp b/src/main.cpp index 0ea6774..c0f05e7 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -226,10 +226,8 @@ int main(int argc, char * argv[]) //Create Options Flag to prevent multiple menu launches system("touch /tmp/options.flag"); - const Uint8 bgR = UiTheme::BgR; - const Uint8 bgG = UiTheme::BgG; - const Uint8 bgB = UiTheme::BgB; - SDL_SetRenderDrawColor(renderer, bgR, bgG, bgB, 0xFF); + const Color bg = UiTheme::Bg; + SetDrawColor(renderer, bg); Texture gearIcon(optionsLocation + UiTheme::AssetGear, renderer, UiTheme::GearX, UiTheme::GearY); Texture switchOn(optionsLocation + UiTheme::AssetSwitchOn, renderer); @@ -238,24 +236,24 @@ int main(int argc, char * argv[]) Texture badgeInner(optionsLocation + UiTheme::AssetBadgeInner, renderer); //Create Textures for Strings - Texture appTitleText("OptionsMenu", UiTheme::TitleFontSize, renderer, UiTheme::TitleX, UiTheme::TitleY, false, UiTheme::TextColor, true); + Texture appTitleText("OptionsMenu", UiTheme::TitleFontSize, renderer, UiTheme::TitleX, UiTheme::TitleY, false, ToAbgr(UiTheme::Text), true); appTitleText.rect.y -= appTitleText.rect.h / 2; // vertically center on the gear (Texture only supports centering both axes together) - Texture appVersionText(MOD_VERSION, UiTheme::VersionFontSize, renderer, appTitleText.rect.x + appTitleText.rect.w + UiTheme::VersionGap, UiTheme::TitleY, false, UiTheme::TextColor, true); + Texture appVersionText(MOD_VERSION, UiTheme::VersionFontSize, renderer, appTitleText.rect.x + appTitleText.rect.w + UiTheme::VersionGap, UiTheme::TitleY, false, ToAbgr(UiTheme::Text), true); appVersionText.rect.y -= appVersionText.rect.h / 2; - Texture titleText(titleString, UiTheme::SectionTitleFontSize, renderer, UiTheme::SectionTitleX, UiTheme::SectionTitleY, false, UiTheme::TextColor, true); + Texture titleText(titleString, UiTheme::SectionTitleFontSize, renderer, UiTheme::SectionTitleX, UiTheme::SectionTitleY, false, ToAbgr(UiTheme::Text), true); SDL_Rect selectedRowRect{ UiTheme::ListX, UiTheme::RowFirstY - 2, UiTheme::ListContentRightX - UiTheme::ListX, 0 }; - Texture CompComText("created by CompCom - Modern UI by DefKorns", 16, renderer, UiTheme::CreditX, UiTheme::CreditY, false, UiTheme::TextColor, true); + Texture CompComText("created by CompCom - Modern UI by DefKorns", 16, renderer, UiTheme::CreditX, UiTheme::CreditY, false, ToAbgr(UiTheme::Text), true); Texture scrollUp(optionsLocation + UiTheme::AssetChevronUp, renderer, UiTheme::ScrollX, UiTheme::ScrollUpY); - SDL_SetTextureColorMod(scrollUp.texture.get(), UiTheme::ScrollArrowR, UiTheme::ScrollArrowG, UiTheme::ScrollArrowB); + SetColorMod(scrollUp.texture.get(), UiTheme::ScrollArrow); scrollUp.rect.x -= scrollUp.rect.w / 2; // ScrollX is the gutter's center, not the glyph's left edge Texture scrollDown = scrollUp; scrollDown.rect.y = UiTheme::ScrollDownY; //Badge cluster (top-right hints): A/B always shown, X only if the row has a delete action - struct Badge { Texture letter; Texture label; UiTheme::BadgeColor rim; UiTheme::BadgeColor fill; }; - auto MakeBadge = [&](const char * letter, const char * hintKey, UiTheme::BadgeColor rim, UiTheme::BadgeColor fill) -> Badge + struct Badge { Texture letter; Texture label; Color rim; Color fill; }; + auto MakeBadge = [&](const char * letter, const char * hintKey, Color rim, Color fill) -> Badge { - return { Texture(letter, 16, renderer, 0, 0, false, UiTheme::BadgeLetterColor, true), Texture(Translate(hintKey), 16, renderer, 0, 0, false, UiTheme::TextColor, true), rim, fill }; + return { Texture(letter, 16, renderer, 0, 0, false, ToAbgr(UiTheme::BadgeLetter), true), Texture(Translate(hintKey), 16, renderer, 0, 0, false, ToAbgr(UiTheme::Text), true), rim, fill }; }; Badge badgeA = MakeBadge("A", "HINT_SELECT", UiTheme::BadgeADark, UiTheme::BadgeA); Badge badgeB = MakeBadge("B", "HINT_BACK", UiTheme::BadgeBDark, UiTheme::BadgeB); @@ -269,11 +267,11 @@ int main(int argc, char * argv[]) int x = rightEdgeX - groupW; int y = UiTheme::BadgeBandY; badgeOuter.rect = { x, y, UiTheme::BadgeOuterSize, UiTheme::BadgeOuterSize }; - SDL_SetTextureColorMod(badgeOuter.texture.get(), badge.rim.r, badge.rim.g, badge.rim.b); + SetColorMod(badgeOuter.texture.get(), badge.rim); badgeOuter.Draw(renderer); int innerOffset = (UiTheme::BadgeOuterSize - UiTheme::BadgeInnerSize) / 2; badgeInner.rect = { x+innerOffset, y+innerOffset, UiTheme::BadgeInnerSize, UiTheme::BadgeInnerSize }; - SDL_SetTextureColorMod(badgeInner.texture.get(), badge.fill.r, badge.fill.g, badge.fill.b); + SetColorMod(badgeInner.texture.get(), badge.fill); badgeInner.Draw(renderer); // glyph bearing makes the pure-math center look 1px down/left - nudged badge.letter.rect.x = x + (UiTheme::BadgeOuterSize - badge.letter.rect.w)/2 + 1; @@ -290,7 +288,7 @@ int main(int argc, char * argv[]) const int RowGlyphSize = 16; const int RowTextGapPx = 16; // gap kept between truncated text and preview image/switch Texture chevronIcon(optionsLocation + UiTheme::AssetChevronRight, renderer); - SDL_SetTextureColorMod(chevronIcon.texture.get(), UiTheme::AccentR, UiTheme::AccentG, UiTheme::AccentB); + SetColorMod(chevronIcon.texture.get(), UiTheme::Accent); for(Command & c : commands) { // self-relaunch into the same dir (e.g. language selection) isn't a submenu @@ -329,7 +327,7 @@ int main(int argc, char * argv[]) label = TruncateUtf8(label, std::max(0, maxChars - 3)) + "..."; } - c.texture = Texture(label, RowGlyphSize, renderer, textX, 0, false, UiTheme::TextColor, true); + c.texture = Texture(label, RowGlyphSize, renderer, textX, 0, false, ToAbgr(UiTheme::Text), true); } const int modernRowPitch = std::max(UiTheme::RowPitch, GetTTFLineHeight(RowGlyphSize)); @@ -422,31 +420,31 @@ int main(int argc, char * argv[]) } // skip for separators and the last row - nothing to separate there if(!selected && !rowCommand.command.empty() && !isLastOverall) - DrawHLine(renderer, UiTheme::ListX, UiTheme::ListContentRightX, rowCommand.texture.rect.y + rowCommand.texture.rect.h + 3, UiTheme::BorderR, UiTheme::BorderG, UiTheme::BorderB); + DrawHLine(renderer, UiTheme::ListX, UiTheme::ListContentRightX, rowCommand.texture.rect.y + rowCommand.texture.rect.h + 3, UiTheme::Border); }; // Draw the background before row text. auto DrawChrome = [&]() { // one continuous border across header+body+footer, plus the footer divider - DrawStrokeRect(renderer, UiTheme::OuterRect, UiTheme::BorderR, UiTheme::BorderG, UiTheme::BorderB, UiTheme::BorderWidth, UiTheme::BorderRadius); + DrawStrokeRect(renderer, UiTheme::OuterRect, UiTheme::Border, UiTheme::BorderWidth, UiTheme::BorderRadius); gearIcon.Draw(renderer); appTitleText.Draw(renderer); appVersionText.Draw(renderer); - DrawHLine(renderer, UiTheme::HeaderDividerX, UiTheme::HeaderDividerX + UiTheme::HeaderDividerW, UiTheme::HeaderDividerY, UiTheme::BorderR, UiTheme::BorderG, UiTheme::BorderB, UiTheme::BorderWidth); - DrawHLine(renderer, UiTheme::OuterRect.x, UiTheme::OuterRect.x + UiTheme::OuterRect.w, UiTheme::FooterDividerY, UiTheme::BorderR, UiTheme::BorderG, UiTheme::BorderB, UiTheme::BorderWidth); + DrawHLine(renderer, UiTheme::HeaderDividerX, UiTheme::HeaderDividerX + UiTheme::HeaderDividerW, UiTheme::HeaderDividerY, UiTheme::Border, UiTheme::BorderWidth); + DrawHLine(renderer, UiTheme::OuterRect.x, UiTheme::OuterRect.x + UiTheme::OuterRect.w, UiTheme::FooterDividerY, UiTheme::Border, UiTheme::BorderWidth); // vertically centered on titleText's own rendered height, not a fixed guess int accentBarY = titleText.rect.y + (titleText.rect.h - UiTheme::SectionAccentBarH) / 2; - DrawFillRect(renderer, { UiTheme::SectionTitleX - UiTheme::SectionAccentBarW - 14, accentBarY, UiTheme::SectionAccentBarW, UiTheme::SectionAccentBarH }, UiTheme::AccentR, UiTheme::AccentG, UiTheme::AccentB); + DrawFillRect(renderer, { UiTheme::SectionTitleX - UiTheme::SectionAccentBarW - 14, accentBarY, UiTheme::SectionAccentBarW, UiTheme::SectionAccentBarH }, UiTheme::Accent); - DrawRoundedFillRect(renderer, selectedRowRect, UiTheme::SelectedRowBgR, UiTheme::SelectedRowBgG, UiTheme::SelectedRowBgB, UiTheme::BoxRadius); - DrawStrokeRect(renderer, selectedRowRect, UiTheme::AccentR, UiTheme::AccentG, UiTheme::AccentB, 2, UiTheme::BoxRadius); + DrawRoundedFillRect(renderer, selectedRowRect, UiTheme::SelectedRowBg, UiTheme::BoxRadius); + DrawStrokeRect(renderer, selectedRowRect, UiTheme::Accent, 2, UiTheme::BoxRadius); // no box when there's no preview - an empty frame looks broken if(PreviewImage.get()) { SDL_Rect previewBox{ UiTheme::DetailX, UiTheme::PreviewBoxY, UiTheme::DetailW, UiTheme::PreviewBoxH }; - DrawStrokeRect(renderer, previewBox, UiTheme::BorderR, UiTheme::BorderG, UiTheme::BorderB, UiTheme::BorderWidth, UiTheme::BoxRadius); + DrawStrokeRect(renderer, previewBox, UiTheme::Border, UiTheme::BorderWidth, UiTheme::BoxRadius); PreviewImage->rect.x = previewBox.x + (previewBox.w - PreviewImage->rect.w) / 2; PreviewImage->rect.y = previewBox.y + (previewBox.h - PreviewImage->rect.h) / 2; PreviewImage->Draw(renderer); @@ -459,15 +457,15 @@ int main(int argc, char * argv[]) if(!commands[currentCommandId].deleteCommand.empty()) rightEdge = DrawBadge(badgeHold, rightEdge); int dividerX = rightEdge - UiTheme::BadgeDividerGapFromCluster; - DrawVLine(renderer, dividerX, UiTheme::FooterY + 10, UiTheme::FooterY + UiTheme::FooterH - 10, UiTheme::BorderR, UiTheme::BorderG, UiTheme::BorderB, 2); + DrawVLine(renderer, dividerX, UiTheme::FooterY + 10, UiTheme::FooterY + UiTheme::FooterH - 10, UiTheme::Border, 2); }; // true if deleted - caller should break out of the main loop auto ConfirmDelete = [&]() -> bool { const std::string & confirmKey = commands[currentCommandId].deleteConfirmKey; - Texture confirmTitle(Translate(confirmKey.empty() ? "DELETE_CONFIRM_GENERIC" : confirmKey), 24, renderer, 640, 320, true, UiTheme::TextColor, true); - Texture confirmHint(Translate("DELETE_CONFIRM_HINT"), 16, renderer, 640, 360, true, UiTheme::TextColor, true); + Texture confirmTitle(Translate(confirmKey.empty() ? "DELETE_CONFIRM_GENERIC" : confirmKey), 24, renderer, 640, 320, true, ToAbgr(UiTheme::Text), true); + Texture confirmHint(Translate("DELETE_CONFIRM_HINT"), 16, renderer, 640, 360, true, ToAbgr(UiTheme::Text), true); controller.GetButtonStatus(B); // consume the still-held B from the triggering long-press bool confirmed = false; for(;;) @@ -481,11 +479,11 @@ int main(int argc, char * argv[]) break; } sdl_context.StartFrame(); - DrawFillRect(renderer, UiTheme::FrameRect, UiTheme::BgR, UiTheme::BgG, UiTheme::BgB); - DrawStrokeRect(renderer, UiTheme::FrameRect, UiTheme::BorderR, UiTheme::BorderG, UiTheme::BorderB, UiTheme::BorderWidth, UiTheme::BorderRadius); + DrawFillRect(renderer, UiTheme::FrameRect, UiTheme::Bg); + DrawStrokeRect(renderer, UiTheme::FrameRect, UiTheme::Border, UiTheme::BorderWidth, UiTheme::BorderRadius); confirmTitle.Draw(renderer); confirmHint.Draw(renderer); - SDL_SetRenderDrawColor(renderer, bgR, bgG, bgB, 0xFF); // flat helpers leave the draw color dirty + SetDrawColor(renderer, bg); // flat helpers leave the draw color dirty sdl_context.EndFrame(); } if(!confirmed) @@ -520,7 +518,7 @@ int main(int argc, char * argv[]) { if(commands[currentCommandId].runInternal) { - commands[currentCommandId].RunCommand(sdl_context, &controller, { gearIcon, appTitleText, appVersionText, CompComText }, bgR, bgG, bgB); + commands[currentCommandId].RunCommand(sdl_context, &controller, { gearIcon, appTitleText, appVersionText, CompComText }, bg); if(commands[currentCommandId].isToggle) commands[currentCommandId].UpdateState(); } @@ -576,7 +574,7 @@ int main(int argc, char * argv[]) scrollDown.Draw(renderer, SDL_FLIP_VERTICAL); // flat-line helpers leave the draw color dirty - reset before EndFrame - SDL_SetRenderDrawColor(renderer, bgR, bgG, bgB, 0xFF); + SetDrawColor(renderer, bg); //Render Framebuffer and Wait for Next Frame sdl_context.EndFrame(); diff --git a/src/mod_uninstall.cpp b/src/mod_uninstall.cpp index 7fd4c0b..8cded30 100644 --- a/src/mod_uninstall.cpp +++ b/src/mod_uninstall.cpp @@ -82,16 +82,16 @@ int main(int argc, char * argv[]) auto renderer = sdl_context.renderer; Controller controller(1); - const Uint8 bgR = UiTheme::BgR, bgG = UiTheme::BgG, bgB = UiTheme::BgB; - SDL_SetRenderDrawColor(renderer, bgR, bgG, bgB, SDL_ALPHA_OPAQUE); + const Color bg = UiTheme::Bg; + SetDrawColor(renderer, bg); Texture gearIcon(OptionsLocation + UiTheme::AssetGear, renderer, UiTheme::GearX, UiTheme::GearY); - Texture appTitleText("OptionsMenu", UiTheme::TitleFontSize, renderer, UiTheme::TitleX, UiTheme::TitleY, false, UiTheme::TextColor, true); + Texture appTitleText("OptionsMenu", UiTheme::TitleFontSize, renderer, UiTheme::TitleX, UiTheme::TitleY, false, ToAbgr(UiTheme::Text), true); appTitleText.rect.y -= appTitleText.rect.h / 2; - Texture appVersionText(MOD_VERSION, UiTheme::VersionFontSize, renderer, appTitleText.rect.x + appTitleText.rect.w + UiTheme::VersionGap, UiTheme::TitleY, false, UiTheme::TextColor, true); + Texture appVersionText(MOD_VERSION, UiTheme::VersionFontSize, renderer, appTitleText.rect.x + appTitleText.rect.w + UiTheme::VersionGap, UiTheme::TitleY, false, ToAbgr(UiTheme::Text), true); appVersionText.rect.y -= appVersionText.rect.h / 2; - Texture titleText(Translate("MODULE_UNINSTALLER"), UiTheme::SectionTitleFontSize, renderer, UiTheme::SectionTitleX, UiTheme::SectionTitleY, false, UiTheme::TextColor, true); - Texture creditText("created by CompCom - Modern UI by DefKorns", 16, renderer, UiTheme::CreditX, UiTheme::CreditY, false, UiTheme::TextColor, true); + Texture titleText(Translate("MODULE_UNINSTALLER"), UiTheme::SectionTitleFontSize, renderer, UiTheme::SectionTitleX, UiTheme::SectionTitleY, false, ToAbgr(UiTheme::Text), true); + Texture creditText("created by CompCom - Modern UI by DefKorns", 16, renderer, UiTheme::CreditX, UiTheme::CreditY, false, ToAbgr(UiTheme::Text), true); // two columns sharing the row list's usual width, split by a center divider const int ColumnGap = 40; @@ -105,35 +105,35 @@ int main(int argc, char * argv[]) const int rowPitch = GetTTFLineHeight(ListFontSize); const int displayCount = std::max(1, (ListBottomY - ListTopY) / rowPitch); - Texture installedHeader(Translate("MOD_UNINSTALLER_INSTALLED"), 18, renderer, LeftColumnX, ColumnHeaderY, false, UiTheme::TextDimColor, true); - Texture toRemoveHeader(Translate("MOD_UNINSTALLER_TO_REMOVE"), 18, renderer, RightColumnX, ColumnHeaderY, false, UiTheme::TextDimColor, true); + Texture installedHeader(Translate("MOD_UNINSTALLER_INSTALLED"), 18, renderer, LeftColumnX, ColumnHeaderY, false, ToAbgr(UiTheme::TextDim), true); + Texture toRemoveHeader(Translate("MOD_UNINSTALLER_TO_REMOVE"), 18, renderer, RightColumnX, ColumnHeaderY, false, ToAbgr(UiTheme::TextDim), true); for(Mod & mod : installedMods) - mod.text = Texture(TruncateToWidth(mod.name, ColumnW), ListFontSize, renderer, LeftColumnX, 0, false, UiTheme::TextColor, true); + mod.text = Texture(TruncateToWidth(mod.name, ColumnW), ListFontSize, renderer, LeftColumnX, 0, false, ToAbgr(UiTheme::Text), true); // Anchor chevrons to the local list range. Texture scrollUp(OptionsLocation + UiTheme::AssetChevronUp, renderer, UiTheme::ScrollX, ListTopY + 10); - SDL_SetTextureColorMod(scrollUp.texture.get(), UiTheme::ScrollArrowR, UiTheme::ScrollArrowG, UiTheme::ScrollArrowB); + SetColorMod(scrollUp.texture.get(), UiTheme::ScrollArrow); scrollUp.rect.x -= scrollUp.rect.w / 2; Texture scrollDown = scrollUp; scrollDown.rect.y = ListBottomY - 50; Texture badgeOuter(OptionsLocation + UiTheme::AssetBadgeOuter, renderer); Texture badgeInner(OptionsLocation + UiTheme::AssetBadgeInner, renderer); - struct Badge { Texture letter; Texture label; UiTheme::BadgeColor rim; UiTheme::BadgeColor fill; }; - Badge badgeAdd{ Texture("A", 16, renderer, 0, 0, false, UiTheme::BadgeLetterColor, true), Texture(Translate("HINT_ADD"), 16, renderer, 0, 0, false, UiTheme::TextColor, true), UiTheme::BadgeADark, UiTheme::BadgeA }; - Badge badgeUndo{ Texture("B", 16, renderer, 0, 0, false, UiTheme::BadgeLetterColor, true), Texture(Translate("HINT_UNDO"), 16, renderer, 0, 0, false, UiTheme::TextColor, true), UiTheme::BadgeBDark, UiTheme::BadgeB }; + struct Badge { Texture letter; Texture label; Color rim; Color fill; }; + Badge badgeAdd{ Texture("A", 16, renderer, 0, 0, false, ToAbgr(UiTheme::BadgeLetter), true), Texture(Translate("HINT_ADD"), 16, renderer, 0, 0, false, ToAbgr(UiTheme::Text), true), UiTheme::BadgeADark, UiTheme::BadgeA }; + Badge badgeUndo{ Texture("B", 16, renderer, 0, 0, false, ToAbgr(UiTheme::BadgeLetter), true), Texture(Translate("HINT_UNDO"), 16, renderer, 0, 0, false, ToAbgr(UiTheme::Text), true), UiTheme::BadgeBDark, UiTheme::BadgeB }; auto DrawBadge = [&](Badge & badge, int rightEdgeX) -> int { int groupW = UiTheme::BadgeOuterSize + UiTheme::BadgeLabelGap + badge.label.rect.w; int x = rightEdgeX - groupW; int y = UiTheme::BadgeBandY; badgeOuter.rect = { x, y, UiTheme::BadgeOuterSize, UiTheme::BadgeOuterSize }; - SDL_SetTextureColorMod(badgeOuter.texture.get(), badge.rim.r, badge.rim.g, badge.rim.b); + SetColorMod(badgeOuter.texture.get(), badge.rim); badgeOuter.Draw(renderer); int innerOffset = (UiTheme::BadgeOuterSize - UiTheme::BadgeInnerSize) / 2; badgeInner.rect = { x+innerOffset, y+innerOffset, UiTheme::BadgeInnerSize, UiTheme::BadgeInnerSize }; - SDL_SetTextureColorMod(badgeInner.texture.get(), badge.fill.r, badge.fill.g, badge.fill.b); + SetColorMod(badgeInner.texture.get(), badge.fill); badgeInner.Draw(renderer); badge.letter.rect.x = x + (UiTheme::BadgeOuterSize - badge.letter.rect.w)/2; badge.letter.rect.y = y + (UiTheme::BadgeOuterSize - badge.letter.rect.h)/2; @@ -145,9 +145,9 @@ int main(int argc, char * argv[]) }; // Use a wide chip to distinguish Start from Select. - Texture pillText("Start", 14, renderer, 0, 0, false, UiTheme::BadgeLetterColor, true); - Texture uninstallLabel(Translate("HINT_UNINSTALL"), 16, renderer, 0, 0, false, UiTheme::TextColor, true); - auto DrawPillBadge = [&](Texture & pill, Texture & label, const UiTheme::BadgeColor & rim, const UiTheme::BadgeColor & fill, int rightEdgeX) -> int + Texture pillText("Start", 14, renderer, 0, 0, false, ToAbgr(UiTheme::BadgeLetter), true); + Texture uninstallLabel(Translate("HINT_UNINSTALL"), 16, renderer, 0, 0, false, ToAbgr(UiTheme::Text), true); + auto DrawPillBadge = [&](Texture & pill, Texture & label, const Color & rim, const Color & fill, int rightEdgeX) -> int { const int pillPadX = 10; int pillW = pill.rect.w + pillPadX*2; @@ -155,11 +155,11 @@ int main(int argc, char * argv[]) int x = rightEdgeX - groupW; int y = UiTheme::BadgeBandY; SDL_Rect pillRect{ x, y, pillW, UiTheme::BadgeOuterSize }; - DrawRoundedFillRect(renderer, pillRect, rim.r, rim.g, rim.b, pillRect.h/2); + DrawRoundedFillRect(renderer, pillRect, rim, pillRect.h/2); // same ring thickness as the letter badges' outer/inner circle pair int border = (UiTheme::BadgeOuterSize - UiTheme::BadgeInnerSize) / 2; SDL_Rect fillRect{ x+border, y+border, pillW-border*2, UiTheme::BadgeInnerSize }; - DrawRoundedFillRect(renderer, fillRect, fill.r, fill.g, fill.b, fillRect.h/2); + DrawRoundedFillRect(renderer, fillRect, fill, fillRect.h/2); pill.rect.x = x + (pillW - pill.rect.w)/2; pill.rect.y = y + (UiTheme::BadgeOuterSize - pill.rect.h)/2; pill.Draw(renderer); @@ -175,14 +175,14 @@ int main(int argc, char * argv[]) auto DrawChrome = [&]() { - DrawStrokeRect(renderer, UiTheme::OuterRect, UiTheme::BorderR, UiTheme::BorderG, UiTheme::BorderB, UiTheme::BorderWidth, UiTheme::BorderRadius); + DrawStrokeRect(renderer, UiTheme::OuterRect, UiTheme::Border, UiTheme::BorderWidth, UiTheme::BorderRadius); gearIcon.Draw(renderer); appTitleText.Draw(renderer); appVersionText.Draw(renderer); - DrawHLine(renderer, UiTheme::HeaderDividerX, UiTheme::HeaderDividerX + UiTheme::HeaderDividerW, UiTheme::HeaderDividerY, UiTheme::BorderR, UiTheme::BorderG, UiTheme::BorderB, UiTheme::BorderWidth); - DrawHLine(renderer, UiTheme::OuterRect.x, UiTheme::OuterRect.x + UiTheme::OuterRect.w, UiTheme::FooterDividerY, UiTheme::BorderR, UiTheme::BorderG, UiTheme::BorderB, UiTheme::BorderWidth); + DrawHLine(renderer, UiTheme::HeaderDividerX, UiTheme::HeaderDividerX + UiTheme::HeaderDividerW, UiTheme::HeaderDividerY, UiTheme::Border, UiTheme::BorderWidth); + DrawHLine(renderer, UiTheme::OuterRect.x, UiTheme::OuterRect.x + UiTheme::OuterRect.w, UiTheme::FooterDividerY, UiTheme::Border, UiTheme::BorderWidth); int accentBarY = titleText.rect.y + (titleText.rect.h - UiTheme::SectionAccentBarH) / 2; - DrawFillRect(renderer, { UiTheme::SectionTitleX - UiTheme::SectionAccentBarW - 14, accentBarY, UiTheme::SectionAccentBarW, UiTheme::SectionAccentBarH }, UiTheme::AccentR, UiTheme::AccentG, UiTheme::AccentB); + DrawFillRect(renderer, { UiTheme::SectionTitleX - UiTheme::SectionAccentBarW - 14, accentBarY, UiTheme::SectionAccentBarW, UiTheme::SectionAccentBarH }, UiTheme::Accent); titleText.Draw(renderer); creditText.Draw(renderer); }; @@ -193,7 +193,7 @@ int main(int argc, char * argv[]) DrawChrome(); installedHeader.Draw(renderer); toRemoveHeader.Draw(renderer); - DrawVLine(renderer, DividerX, ColumnHeaderY, ListBottomY, UiTheme::BorderR, UiTheme::BorderG, UiTheme::BorderB, UiTheme::BorderWidth); + DrawVLine(renderer, DividerX, ColumnHeaderY, ListBottomY, UiTheme::Border, UiTheme::BorderWidth); int count = std::min(displayCount, (int)installedMods.size()-installListOffset); for(int i = 0; i < count; ++i) @@ -204,8 +204,8 @@ int main(int argc, char * argv[]) if(i == currentId) { SDL_Rect rowRect{ LeftColumnX - 8, rowText.rect.y - 2, ColumnW + 8, rowText.rect.h + 4 }; - DrawRoundedFillRect(renderer, rowRect, UiTheme::SelectedRowBgR, UiTheme::SelectedRowBgG, UiTheme::SelectedRowBgB, UiTheme::BoxRadius); - DrawStrokeRect(renderer, rowRect, UiTheme::AccentR, UiTheme::AccentG, UiTheme::AccentB, 2, UiTheme::BoxRadius); + DrawRoundedFillRect(renderer, rowRect, UiTheme::SelectedRowBg, UiTheme::BoxRadius); + DrawStrokeRect(renderer, rowRect, UiTheme::Accent, 2, UiTheme::BoxRadius); } rowText.Draw(renderer); } @@ -228,15 +228,15 @@ int main(int argc, char * argv[]) rightEdge = DrawBadge(badgeUndo, rightEdge); DrawPillBadge(pillText, uninstallLabel, UiTheme::BadgeStartDark, UiTheme::BadgeStart, rightEdge); - SDL_SetRenderDrawColor(renderer, bgR, bgG, bgB, 0xFF); // flat helpers leave the draw color dirty + SetDrawColor(renderer, bg); // flat helpers leave the draw color dirty sdl_context.EndFrame(); }; // true if the uninstall was confirmed and kicked off - caller should break out of the main loop auto ConfirmUninstall = [&]() -> bool { - Texture confirmTitle(Translate("MOD_UNINSTALL_CONFIRM"), 24, renderer, 640, 320, true, UiTheme::TextColor, true); - Texture confirmHint(Translate("MOD_UNINSTALL_CONFIRM_HINT"), 16, renderer, 640, 360, true, UiTheme::TextColor, true); + Texture confirmTitle(Translate("MOD_UNINSTALL_CONFIRM"), 24, renderer, 640, 320, true, ToAbgr(UiTheme::Text), true); + Texture confirmHint(Translate("MOD_UNINSTALL_CONFIRM_HINT"), 16, renderer, 640, 360, true, ToAbgr(UiTheme::Text), true); controller.GetButtonStatus(B); // consume the still-held B from whatever press opened this dialog bool confirmed = false; for(;;) @@ -250,11 +250,11 @@ int main(int argc, char * argv[]) break; } sdl_context.StartFrame(); - DrawFillRect(renderer, UiTheme::FrameRect, bgR, bgG, bgB); - DrawStrokeRect(renderer, UiTheme::FrameRect, UiTheme::BorderR, UiTheme::BorderG, UiTheme::BorderB, UiTheme::BorderWidth, UiTheme::BorderRadius); + DrawFillRect(renderer, UiTheme::FrameRect, bg); + DrawStrokeRect(renderer, UiTheme::FrameRect, UiTheme::Border, UiTheme::BorderWidth, UiTheme::BorderRadius); confirmTitle.Draw(renderer); confirmHint.Draw(renderer); - SDL_SetRenderDrawColor(renderer, bgR, bgG, bgB, 0xFF); + SetDrawColor(renderer, bg); sdl_context.EndFrame(); } if(!confirmed) From a4762af3ebc31a92ce325d0c3e66eb037ed0b195 Mon Sep 17 00:00:00 2001 From: DefKorns Date: Fri, 25 Sep 2026 15:03:30 +0000 Subject: [PATCH 051/104] Mark hyphenated version tags as prereleases --- .github/workflows/build.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b9fc587..2c217fd 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -64,3 +64,5 @@ jobs: with: files: out/*.hmod generate_release_notes: true + # -alpha/-beta/-rc tags must not take over "Latest" + prerelease: ${{ contains(github.ref_name, '-') }} From 5fc3f8b122e29fe3cda2c2c4bc1e60e1008e7685 Mon Sep 17 00:00:00 2001 From: DefKorns Date: Fri, 25 Sep 2026 15:09:22 +0000 Subject: [PATCH 052/104] Update README for 2.0.0-alpha --- README.md | 112 ++++++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 104 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index dbe5d3f..549dab4 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,17 @@ The Options Menu is a custom menu that can be launched via a controller button c The options menu is easily extendible, allowing for other developers to add their own custom commands to the menu via hmods. +## Navigation + +|Button|Action| +|------|------| +|UP / DOWN|Move the selection (hold to repeat).| +|A or START|Run the selected option or open its submenu.| +|B|Go back to the previous menu, or close the Options Menu on the main screen.| +|B (hold ~1 second)|Delete the selected entry, on lists that support it (the footer shows a **Delete (Hold)** hint). You are asked to confirm with **A**.| + +Rows that open a submenu show a **›** on the right. The button hints for the current screen are always shown in the footer. + ## Options Explained Below is a detailed guide on what specific options actually do and what their purpose is. @@ -39,14 +50,26 @@ This option will safely shut down the console.\* This option will safely restart the console.\* -#### Retroarch Options +#### RetroArch This option will open a menu for managing retroarch configurations (see below). -#### Network Options +#### Network This option will open a menu for managing your network and wifi configuration (see below). +#### Saved Games + +This option will open a menu for backing up and restoring your saved games (see below). + +#### Language + +This option opens a submenu where you can choose the display language for the Options Menu. Currently available: English, Français, Deutsch, Español, Italiano, Nederlands, Português (Europeu), Русский and 日本語. The Options Menu automatically restarts in the selected language. + +#### Theme Options + +Only shown when the [Theme Selector](https://github.com/DefKorns/om_theme-selector) hmod is installed. Opens its theme manager. + #### Advanced Options This option will open a menu containing more advanced features (see below). @@ -55,6 +78,8 @@ This option will open a menu containing more advanced features (see below). ## Retroarch Options +Both RetroArch builds are supported: RetroArch Neo (discontinued) and the `_km` RetroArch Xtreme (Ozone). + #### Restore Default Settings (All) Replaces your current retroarch config files with the default files included in the module. This option deletes all game/core overrides and remap files. @@ -153,11 +178,21 @@ While browsing the list of saved backups, hold **B** (~1 second) to delete the selected backup (you will be asked to confirm with **A**, or cancel by pressing **B** again). The list refreshes automatically after deleting. -## Advanced Options +## Saved Games Options -#### Language Options +#### Backup Saved Games to USB -This option opens a submenu where you can choose the display language for the Options Menu. Currently available: English, Français, Deutsch, Español, Italiano, Nederlands, Português (Europeu), Русский and 日本語. The Options Menu automatically restarts in the selected language. +Copies your saved games from the saves folder on your USB/SD card to a backup folder on the same drive: + + /media/data/saves_backup + +The backup mirrors the saves folder, so saves deleted since the last backup are removed from it too. System folders (`FOLDER`, `hakchi`, `home-menu`, `mcp-state`) and system files are not included. + +#### Restore Saved Games from USB + +Copies the saved games from `/media/data/saves_backup` back into your saves folder, overwriting saves with the same name. Saves that are not in the backup are left untouched. + +## Advanced Options #### Change Options Button Combo @@ -177,7 +212,12 @@ This option will force the kernel to clear the page cache and free up unused mem #### Module Uninstaller -This option launches the module uninstaller program. This program allows you to remove modules from your device. Press the up and down buttons to select a module. Press A to add a module to the uninstall list. Press B to remove a module from the uninstall list. Pressing start will exit if no modules are added to the uninstall list. If there are modules on the uninstall this you will be prompted to ensure you want to remove them press START+SELECT to remove selected modules or B to cancel. +This option launches the module uninstaller program. This program allows you to remove modules from your device. The installed modules are listed on the left and the ones queued for removal on the right. + +- **UP / DOWN** - select a module. +- **A** - add the selected module to the uninstall list. +- **B** - remove the last module from the uninstall list. +- **START** - exit if the uninstall list is empty; otherwise you are asked to confirm. Press **START + SELECT** to remove the queued modules, or **B** to cancel. >**Caution:** The uninstaller will list all modules on the device including the default hakchi modules. Only delete modules you have added yourself. Deleting the default hakchi modules can have unintended effects on your console. @@ -260,19 +300,25 @@ Command Files should contain the following fields: |ENABLE_IF|Optional shell condition; the row is only loaded if it exits 0. Runs with `/etc/preinit` sourced and `script_init` called first, so it can reference `$rootfs`/`$mountpoint` like any other options_menu script, e.g. `ENABLE_IF=[ -d "$rootfs/etc/wifi_backup" ]`.| |IGNORE_INTERRUPT|If set to `TRUE`, pressing B will not interrupt an internal command while its output is being displayed.| |CHILD|If set to `TRUE`, indents the row under the one above it, for visually grouping a related entry without a separate submenu.| +|SUBMENU|If set to `TRUE`, marks the row as opening another menu and draws a **›** on its right. Ignored on toggle rows and on rows that relaunch the current menu.| |PREVIEW_IMAGE|Specifies the path to a thumbnail/icon shown in the detail panel when this row is selected. Auto-scaled and centered to fit the panel.| |PREVIEW_IMAGE_X|Unused - the detail panel always auto-centers the image.| |PREVIEW_IMAGE_Y|Unused - the detail panel always auto-centers the image.| |PREVIEW_IMAGE_WIDTH|Unused - the detail panel always auto-scales the image to fit.| |PREVIEW_IMAGE_HEIGHT|Unused - the detail panel always auto-scales the image to fit.| -|PREVIEW_FIT_CONTAIN|If set to `TRUE`, this row's grid tile scales the art to fit inside the tile instead of cropping it to fill it. Use for portrait art in a wide/square tile.| -|PREVIEW_HIDE_LABEL|If set to `TRUE`, this row's grid tile shows no caption below it.| +|PREVIEW_NEAREST|Grid layout only\*. If set to `TRUE`, the preview is scaled with nearest-neighbor instead of linear filtering, for small pixel-art images.| +|PREVIEW_SQUARE|Grid layout only\*. If set to `TRUE` on the first grid item, the screen uses square tiles instead of the default wide ones.| +|PREVIEW_GRID_COLS|Grid layout only\*. Set on the first grid item to force the number of tile columns.| +|PREVIEW_FIT_CONTAIN|Grid layout only\*. If set to `TRUE`, this row's tile scales the art to fit inside the tile instead of cropping it to fill it. Use for portrait art in a wide/square tile.| +|PREVIEW_HIDE_LABEL|Grid layout only\*. If set to `TRUE`, this row's tile shows no caption below it.| |DELETE_STR|Optional command string to run when the user holds B (~1 second) on this entry and confirms. Used to make list entries deletable (e.g. the wifi backup restore list).| |DELETE_CONFIRM_KEY|Optional translation key for the confirmation prompt shown before running DELETE_STR. Falls back to a generic "Delete this item?" if not set.| |STATE_STR|Optional command string that prints the current on/off state of this entry (its first line of output, case-insensitively `1`/`on`/`y`/`yes`/`true` for on, anything else for off). Presence of this field renders the entry as a toggle switch instead of a plain row; it is only read to draw the switch, not to change behavior, so COMMAND_STR is still what actually flips the setting.| >Note: Fields and values are case sensitive. Values should be separated from fields using '=' without spaces. +\*The grid layout fields are parsed by the shared framework but only used by the Theme Selector's `theme_manager` grid screens; the Options Menu itself always shows a list. + #### Command String Variables COMMAND_STR, DELETE_STR and STATE_STR all support the use of the following variables: @@ -282,6 +328,36 @@ COMMAND_STR, DELETE_STR and STATE_STR all support the use of the following varia |%options_path%|Path to the folder containing the options binary.| |%script_dir%|Path to the current script folder set in the options binary.| +### Theme Colors + +The UI colors are read at startup from `/etc/options_menu/theme.cfg`, one `Key=R,G,B` per line (0-255 each, `#` for comments). A missing file, line or key falls back to the built-in default. + +|Key|Used for| +|---|--------| +|Bg|App background.| +|Border|Divider lines and box outlines.| +|Accent|Selected row border, section title bar and submenu chevrons.| +|SelectedRowBg|Selected row fill. Follows `Bg` unless set.| +|Text|Titles, row labels and button hints.| +|TextDim|Secondary text, e.g. the Module Uninstaller column headers.| +|ScrollArrow|List scroll arrows. Follows `Text` unless set.| +|BadgeLetter|Letter inside a button-hint badge.| +|BadgeA / BadgeADark|A badge fill / rim.| +|BadgeB / BadgeBDark|B badge fill / rim.| +|BadgeX / BadgeXDark|Hold-to-delete badge fill / rim.| +|BadgeY / BadgeYDark|Reserved, currently unused.| +|BadgeStart / BadgeStartDark|Start pill fill / rim.| + +### Translations + +UI strings live in `/etc/options_menu/language/.lang` as `KEY=Text` lines; the selected language code is stored in `/etc/options_menu/language.cfg`. `en-US` is always loaded first, so a partial translation falls back to English for missing keys. + +A plugin hmod can ship its own strings in `/etc/options_menu//lang/.lang`. Every such folder is merged in at startup, as long as it has an `en-US.lang`. + +### Preview Icons + +The per-command preview images in `/etc/options_menu/images/` (200x140 PNG) and the UI assets in `/etc/options_menu/images/ui/` are rendered from [Bootstrap Icons](https://icons.getbootstrap.com/) glyphs. The assets in `images/ui/` are white and tinted at runtime with the theme colors. A plugin can point `PREVIEW_IMAGE` at any PNG; it is scaled to fit the detail panel. + ### Compiling Options Menu #### Docker toolchain (recommended) @@ -297,6 +373,17 @@ Pass `FIX_LIBSTDCXX=1` to also build a `_compat` variant that ships its own `lib ./build.sh FIX_LIBSTDCXX=1 +The version shown in the app and in the `.hmod` name comes from the latest git tag. Pass `MOD_VER` to override it, e.g. for a test build: + + ./build.sh MOD_VER=test-dev + +#### Releases + +Pushing a `v*` tag makes the GitHub Actions workflow build the `.hmod` with that tag as its version and publish it as a GitHub Release. Tags with a hyphen (`-alpha`, `-beta`, `-rc`) are published as prereleases. + + git tag -a v2.0.0-alpha -m "Options Menu v2.0.0-alpha" + git push origin v2.0.0-alpha + #### Native / manual build The options menu can also be compiled using the plain `Makefile` provided. To compile the options menu you must have the SDL2 and libpng libraries as well as GNU Make installed. To cross-compile the options menu call `make` with the `CROSS_PREFIX` set to the prefix of your cross-compiler toolchain eg. `make CROSS_PREFIX=arm-linux-gnueabihf-`. @@ -314,7 +401,15 @@ The options menu can also be compiled using the plain `Makefile` provided. To co - Multi-language localization system (i18n) and translations - Docker-based ARM cross-compile toolchain and build tooling - Redesigned "Modern UI" (dialog frame, selection highlight, toggle switches, button-hint badges) + - TTF text rendering, theme colors (`theme.cfg`), preview icons, Saved Games options - **ThanosRD** — UI Layout/Design assistance (original Classic UI) +- **[MadFranko008](https://www.reddit.com/user/MadFranko008/)** — Original idea for the Saved Games backup/restore options + +### Third-party assets + +- [Bootstrap Icons](https://icons.getbootstrap.com/) — source glyphs for the preview icons and UI chevrons (MIT License) +- [Noto Sans JP](https://fonts.google.com/noto/specimen/Noto+Sans+JP) — UI font, subset (SIL Open Font License, see `fonts/OFL.txt`) +- [Misaki font](http://littlelimit.net/misaki.htm) by Num Kadoma — 8x8 Katakana and Kanji glyphs ### Testing @@ -322,6 +417,7 @@ Extra thanks to DNA64 (viral_dna) and Swingflip for always testing features. Also thanks to the following people for testing the options menu: +- Aranthys - BsLeNuL - DefKorns - DR1001 From fbd9dcc47a44229b086f28f293ce2ff68c72e5d5 Mon Sep 17 00:00:00 2001 From: DefKorns Date: Fri, 25 Sep 2026 15:12:52 +0000 Subject: [PATCH 053/104] Build the _compat hmod in CI alongside the plain one --- .github/workflows/build.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 2c217fd..4063152 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -41,6 +41,9 @@ jobs: MOD_VER_ARG="MOD_VER=${{ github.ref_name }}" fi docker run --rm -v "${{ github.workspace }}:/src" classicmini-cross-toolchain:jessie-armhf make -f Makefile.docker CROSS_PREFIX=arm-linux-gnueabihf- $MOD_VER_ARG + # _compat variant (private libstdc++.so.6) - must run after the plain + # build, which strips a leftover libstdc++ out of its own package + docker run --rm -v "${{ github.workspace }}:/src" classicmini-cross-toolchain:jessie-armhf make -f Makefile.docker CROSS_PREFIX=arm-linux-gnueabihf- $MOD_VER_ARG FIX_LIBSTDCXX=1 - name: Upload .hmod build artifact uses: actions/upload-artifact@v7 From cfb7e3f624f33e3b73cac9cbf9bf76e9572c82ca Mon Sep 17 00:00:00 2001 From: DefKorns Date: Fri, 25 Sep 2026 16:32:45 +0000 Subject: [PATCH 054/104] Add numero sign to the Latin font and CJK punctuation to the 8x8 font --- font-build/unicodes-latin.txt | 2 +- .../fonts/NotoSansJP-Latin-subset.ttf | Bin 42916 -> 43040 bytes src/framework/font8x8.h | 1 + src/framework/font8x8_cjk_punct.h | 85 ++++++++++++++++++ src/framework/font8x8_lookup.cpp | 2 + 5 files changed, 89 insertions(+), 1 deletion(-) create mode 100644 src/framework/font8x8_cjk_punct.h diff --git a/font-build/unicodes-latin.txt b/font-build/unicodes-latin.txt index caac114..86ff2ee 100644 --- a/font-build/unicodes-latin.txt +++ b/font-build/unicodes-latin.txt @@ -1 +1 @@ -U+0020-007E,U+00A0-00FF,U+0401,U+0410-044F,U+0451 \ No newline at end of file +U+0020-007E,U+00A0-00FF,U+0401,U+0410-044F,U+0451,U+2116 \ No newline at end of file diff --git a/mod/etc/options_menu/fonts/NotoSansJP-Latin-subset.ttf b/mod/etc/options_menu/fonts/NotoSansJP-Latin-subset.ttf index 479414c3e5bacff34870f8bb09d6a70e7a9d249f..376b7f092e7add478b0b7b2ecee6c470f2c02581 100644 GIT binary patch delta 2413 zcmXw43slqB8UOB0$OA|qZ(a!r|GX3OBriZBDH1>sC=ZcZHM|mPKonG%wrPFUWA$t{ z&Z_GaE7s#mwUbS|!`7~6JDZN3wx{#4ZfmtYb?3~Tx$dZK*YVgFu=(eH_kQ2~eb4)v z_<)XoNE1K+K#YV9>QH6NivG}!5WuGAQTkE1sjmG;2 zgkdGX>LR_t;PQGzZn<2caNDTIQ{r{Ia9qTZi5NZ3GAb&QNjW6BS(l&SdNi`W^mHCi zD(Q3eID+i7G-|S@r}Mc|ai6>Uv49@4H`;8CcH1f&{;ee?R;$O;Me+rd)=2W{+Tpvy zFNX)ZJU{h&2@5526T^QWJ{%s1crSTHWOfPCY#xEh4 zFhIez@PK?vZzdhthJ-xa=kxW2m#8_&FhR3vIm0X$w7)*OeWJB>V*BXp?R6Dr-eWlKM7CdnIaC?;Ptwb`#C~?{OBrWxvjWC<+1ivht_-Q_p}x)FZjT_@yza0Q;=~n zZdYl^y5JKBURgua8quH<6L^3Lyw6#0QkNMmK_<*B)0HeUsY~^4b911wRmRS6)Vh{; zIuR|!U6DzzCEL0BoZI=$1-Z`rhqdM8>YNWJ^U>5-IC+HItTEQOR<R1YY zP+76o71$s!x4+Q7@s;%!i#m{}DnB^dF;-!yw%e<8HeZd(s&IDIRz(Bo!-g) z!bT=gR8eB_8|F`|{aVJQEzBxd<yO4I5Bj*G-O`IOiG0sKKH0NW^ zLoSQ!<92cnaxZef;c0m>-Wcx`Z<_arU&LR|Z|6t(+xRc?f6PD6zmniTOt+>#m7YwW zNxz@bld&)3wTurl{*lSeEYDn*c{uZ$fCw~#ZovV;q~NNM3hl!6!V%$R;cv3oS=OwE ztle3^%w}cVvP0QF%l=s87i|)~B>Iu)p6F{aBMyoC#b?D+;{Qmb5{slpvR1NRG9mes zR3z<}PDtOEej_u;63@scW%uQB`8xSd`3dma*onVeIXP8^eeQiLyLwi#D z7ad3E()H<{)m_xRr~5)L)9dw(`n~!~`WgLw1KXf8tTb#j95ejU$TND3Ta3Rj{@XZj zGMOT#)27eOeDg~4R`W^o4f8#V(qgwnEk`ZqEVrzFYp?ZL>nZEctv9XzE7lcnOHy(2&1ECsE*?X~8$A1Mey&UKsh{#S>`*eDRXJC`0&3 zYcxbP^5js|UAqBIOYwzd;d}fXjKd_n2@7n(Ze%xWo7SA4X8m7v82nxo&#T_oPY?vvrW(*#b9y82^VJ;Jxo=dbyM95RQ>Kl?{EUn QVN20ezchyA^XIhx2R8&(-~a#s delta 2604 zcmb7GeQZNUMaPsxXGv!8A$>6)lA>L8uZf6RfbR>!xn&R>U?f#QWW6r_lX3 zJ>UDzJ?GqWf9H44dGh2XVQNm0i4l<*lSrnf#_n$qHa*cq6nY8T%gr6_-8H*De3~d6 zCTdvG+`X>p%BBl{Cz?72`@Z(>+PZa38|*|r2i9A=dm4Mj_64pHO|Qn<)gN?+jYHxa zu;B!Y1hfIh+fw$HdcuXER`D&M+h!qlT8S#7Fo|@bfUl8@3g2R1&DD-CCb_CF3 zUFHu)$F|5TEwJALVoM;@?>>0xB&g6 z!E7@#W@F5poIEONAX$ z`$?cJi7)97!e1zUKl48t-@f@(K?4%-ON$-w7Rh$9(;D(njIOh9un9KBPO}f$Z6RH7 z3J(bF!e(Jym=Qiu<*V*jg;cMr=0v+VBEBGA6mN*P(sXI7(@vy)q&BG=)gJW|>gUvN ztFNj*)rgv6jZ4#~`Jv{7=Bl<-+pdjir?eNepXwxCt8Pg5ly0ByW!-Vzd0F>??q+&% z`iAsf>8I0gXIL|y%lJjcZ!`X*x9IEiKK&m3Dg7;j!?4!ytl@RT6~mXA&dlD-A7>uS zypj1=W36$6am;ws_(zk=^c_>s^n0^t?l-?^{+0QitemWpthTIR)5mQFc6gE_=c9pk=}`XZd%IT$M6AF-0P(;BfJvHq?=SMXrLQw1{x7nkHN*|cPG$)5{b3J(>jiULK)i~g~+ zZt3==vrBI-yimR`p4BL3MR?PxapF_iGF_4K<@RlQr+*UUTdZ+k5T%>}Twk9fG6Z zG443*xZwEM$($9=4(EvTkn@JiYH)d62VEc4T51Psf338rz|{5S18>DY_c=t1@HE+k zjTn7oQ$3F{Nw!!=pcK^;4KxX2XY6F4e$_@)bQZczg?jq|P10+0gc2eX+r`dA0yYV{ zm!Uffor&ubHxll|n;56(-=i>}Ak2YY4G(DQ5p;h3l+Eu{-JDOtxR zgoc!y&eB!qQ*s71h?i5cft94GQ*tIXsH;-4iSpHtrerf|)zOq}C5!sWq|6xU)#DUD z5u9LhH-%^$jZi;%k-#8DFuO6L(7DkuUJBwpLPOAnXaI8~v?1zC`420;er2_eh7lnO z*#m7iB)76k#vF#W7CT3jNKxW@Mr17d6$r!1k%Lf1$aT*lUGy02Ll_dZ(IW`^Fy>xb zhlXw>X^*_IH+X7O2dq`_asz^!99Z>XM;IfBF`(?OfOb$xDMUUcy)ems99K=>gsB4? zw^0DnY5}-xdpI_;=$c;SW12o z0N^>eK`OU%##Y15Sz4p)mcS4P$m@k46oxfK-@~dOv3N!C3iGF`gX{QNJ_^VP9fm*>IXeGzuSHmPtaBhj?%>518~!Qr`-n2nH`eUK9K>8>t6RJ9VLY zB%fjYv!VXIzob=n(SAKpGV}i}7Y>xub}wtyv5$jxIjSV$MTPe$d2;R^Qo!@1_(cu? z&US#|wX@i1$$H`Z^YnSwEV9VEB-tZ;_aop}0{Aezc*ot1yjH0=ubc|OX9M*>jy8waf4wu_T9Z|36Ob9^Taui4}r%{MUK*Py#2660ZWcxS&Juv%iz5b zhMG4rhY`S=_q`AHc7#~_)wcg= 0x0400 && codepoint <= 0x04FF) return font8x8_cyrillic[codepoint - 0x0400]; + if(codepoint >= 0x3000 && codepoint <= 0x303F) + return font8x8_cjk_punct[codepoint - 0x3000]; if(codepoint >= 0x3040 && codepoint <= 0x309F) return font8x8_hiragana[codepoint - 0x3040]; if(codepoint >= 0x30A0 && codepoint <= 0x30FF) From 7609718a6a52812ecf1d2774892f20cf55296f2c Mon Sep 17 00:00:00 2001 From: DefKorns Date: Fri, 25 Sep 2026 16:43:54 +0000 Subject: [PATCH 055/104] Tag the compat hmod's Name with (compat) --- Makefile.docker | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Makefile.docker b/Makefile.docker index ab1de50..2a19822 100644 --- a/Makefile.docker +++ b/Makefile.docker @@ -7,10 +7,11 @@ ESC_MOD_VER := $(subst .,_,$(MOD_VER)) # hmods replacing the system copy. Off by default, costs ~225KB. ifdef FIX_LIBSTDCXX TARGET_NAME = options_deluxe_$(ESC_MOD_VER)_compat +MOD_NAME := Options Menu $(MOD_VER) (compat) else TARGET_NAME = options_deluxe_$(ESC_MOD_VER) -endif MOD_NAME := Options Menu $(MOD_VER) +endif GIT_COMMIT := $(shell git describe --always --dirty=.dirty 2>/dev/null) BUILD_NUM_FILE := .build_number CXX = g++ From bf58623013a2c2126d7c33c30be8802b49ed525e Mon Sep 17 00:00:00 2001 From: DefKorns Date: Tue, 6 Oct 2026 11:08:38 +0000 Subject: [PATCH 056/104] Add S8104CleanExtraCovers script to remove unnecessary cover images --- mod/etc/init.d/S8104CleanExtraCovers | 39 ++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 mod/etc/init.d/S8104CleanExtraCovers diff --git a/mod/etc/init.d/S8104CleanExtraCovers b/mod/etc/init.d/S8104CleanExtraCovers new file mode 100644 index 0000000..cf6d341 --- /dev/null +++ b/mod/etc/init.d/S8104CleanExtraCovers @@ -0,0 +1,39 @@ +#!/bin/sh +# Copyright (C) 2026 DefKorns (https://defkorns.github.io/LICENSE) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +# Removes the Mega Drive Mini / original-size covers hakchi syncs into every +# game folder; the NES/SNES Mini home menu only reads .png and _small.png. + +clean() { + source /etc/preinit + script_init + + gStorage="$(findGameStorage)" + [ -d "$gStorage" ] || return 0 + find "$gStorage" -type f \( -name "*_mdmini.png" -o -name "*_original.png" \) -exec rm -f {} + +} + +case "$1" in +start) + clean & +;; +stop|restart) +;; +*) + echo "CleanExtraCovers: Please use start." + exit 1 +;; +esac From b673707de0e20a1d93bc21a6f882982bc260e8f7 Mon Sep 17 00:00:00 2001 From: DefKorns Date: Tue, 6 Oct 2026 11:09:06 +0000 Subject: [PATCH 057/104] Refactor install script to improve cleanup process and ensure a clean installation --- mod/install | 29 +++++++++++++++++++++++++---- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/mod/install b/mod/install index c8c18b4..a20b77c 100644 --- a/mod/install +++ b/mod/install @@ -1,4 +1,29 @@ +#!/bin/sh +# shellcheck disable=SC2154 +omRoot="$rootfs/etc/options_menu" +omSettings="button.cfg language.cfg" +omBackup="$temppath/om_settings" + +# Uninstall any previous or legacy install first to ensure a clean installation +rm -rf "$omBackup" +mkdir -p "$omBackup" +for omFile in $omSettings; do + [ -f "$omRoot/$omFile" ] && cp -p "$omRoot/$omFile" "$omBackup/" +done +for omUnfile in "$installpath/hmod/uninstall-options_deluxe"* \ + "$installpath/hmod/uninstall-options_menu_"* \ + "$installpath/hmod/uninstall-hibernate_mod"* \ + "$installpath/hmod/uninstall-Hakchi_Options_Loadscreen_toggle"; do + [ -f "$omUnfile" ] && pack_uninstall "${omUnfile##*/uninstall-}" +done +cd "$transferpath" || return 1 + transfer_default +for omFile in $omSettings; do + [ -f "$omBackup/$omFile" ] && cp -p "$omBackup/$omFile" "$omRoot/" +done +rm -rf "$omBackup" + chmod +x "$rootfs/etc/options_menu/options" chmod +x "$rootfs/etc/options_menu/optiond" chmod +x "$rootfs/etc/options_menu/mod_uninstall/mod_uninstall" @@ -8,8 +33,4 @@ chmod +x "$rootfs/bin/standby" chmod +x "$rootfs/bin/standby_watchdog" rm -f "$rootfs/etc/init.d/S83standby_watchdog" rm -f "$rootfs/etc/options_menu/retroarch/commands/c0025_ToggleLoadScreens" -rm -f "$installpath/hmod/uninstall-hibernate_mod"* -rm -f "$installpath/hmod/uninstall-options_deluxe_"* -rm -f "$installpath/hmod/uninstall-options_menu_"* -rm -f "$installpath/hmod/uninstall-Hakchi_Options_Loadscreen_toggle" return 1 From 6d8053bd34de2179a12655ea7ea48cecb6811c8f Mon Sep 17 00:00:00 2001 From: DefKorns Date: Tue, 6 Oct 2026 15:04:52 +0000 Subject: [PATCH 058/104] Redesign the Module Uninstaller as a checklist that returns to Advanced Options --- .../advanced_commands/c0050_ModUninstall | 2 +- .../options_menu/images/ui/checkbox_off.png | Bin 0 -> 222 bytes .../options_menu/images/ui/checkbox_on.png | Bin 0 -> 373 bytes mod/etc/options_menu/language/de-DE.lang | 12 +- mod/etc/options_menu/language/en-US.lang | 12 +- mod/etc/options_menu/language/es-ES.lang | 12 +- mod/etc/options_menu/language/fr-FR.lang | 12 +- mod/etc/options_menu/language/it-IT.lang | 12 +- mod/etc/options_menu/language/ja-JP.lang | 12 +- mod/etc/options_menu/language/nl-NL.lang | 12 +- mod/etc/options_menu/language/pt-PT.lang | 12 +- mod/etc/options_menu/language/ru-RU.lang | 12 +- .../mod_uninstall/launch_uninstaller.sh | 31 ++ mod/etc/options_menu/theme.cfg | 3 + src/framework/uitheme.cpp | 7 + src/framework/uitheme.h | 4 + src/mod_uninstall.cpp | 454 +++++++++++++----- 17 files changed, 459 insertions(+), 150 deletions(-) create mode 100644 mod/etc/options_menu/images/ui/checkbox_off.png create mode 100644 mod/etc/options_menu/images/ui/checkbox_on.png create mode 100644 mod/etc/options_menu/mod_uninstall/launch_uninstaller.sh diff --git a/mod/etc/options_menu/advanced_commands/c0050_ModUninstall b/mod/etc/options_menu/advanced_commands/c0050_ModUninstall index 0f31d3a..4a7aa44 100644 --- a/mod/etc/options_menu/advanced_commands/c0050_ModUninstall +++ b/mod/etc/options_menu/advanced_commands/c0050_ModUninstall @@ -1,5 +1,5 @@ COMMAND_NAME=MODULE_UNINSTALLER COMMAND_TYPE=EXTERNAL RESTART_UI=FALSE -COMMAND_STR=%options_path%/mod_uninstall/mod_uninstall && sh %script_dir%/ResumeUI.sh & +COMMAND_STR=sh %options_path%/mod_uninstall/launch_uninstaller.sh %options_path% & PREVIEW_IMAGE=/etc/options_menu/images/preview_mod_uninstall.png diff --git a/mod/etc/options_menu/images/ui/checkbox_off.png b/mod/etc/options_menu/images/ui/checkbox_off.png new file mode 100644 index 0000000000000000000000000000000000000000..ba6f48ab0a28fb136466781dc8859629cbc24a09 GIT binary patch literal 222 zcmeAS@N?(olHy`uVBq!ia0vp^A|TAc1|)ksWqE^YH_^gMPknLl6Sy=KP)?~YMFK;9G5?;X4%GVL>d;n$Htpo+}0W1VNZQRW#6XxJ}%w;$J z5Rwf1-^|IGIT0`h?tukRvAQbv8e^vbrjiaNA*s+X;og}6?WK}b>VK+?VGstw%okO& z%7M#FOC*aI<-nyG?7gX&vZI1LyJ_y8OG(?3?mWKuadykvA?eHS*X~;boll|b~!@NfDPcb?Yl*#S#Ik^g(e;?GxJ3U zFoKI^50J8DB7P-Z;Q%2W4wk(S+NXNP4LI}ceb7FbF{ZZ5_6wMVDOpv?XN>VT9e~84 TO5h+B00000NkvXXu0mjfbZn*R literal 0 HcmV?d00001 diff --git a/mod/etc/options_menu/language/de-DE.lang b/mod/etc/options_menu/language/de-DE.lang index bfa20cf..f1d4da0 100644 --- a/mod/etc/options_menu/language/de-DE.lang +++ b/mod/etc/options_menu/language/de-DE.lang @@ -64,10 +64,14 @@ CHANGE_COMBO_CHOSEN=Deine gewählte Kombination ist: CHANGE_COMBO_CONFIRM=Wenn das stimmt, A drücken. Andernfalls eine beliebige andere Taste zum erneuten Versuch. CHANGE_COMBO_DONE=Fertig. Bitte neu starten, damit die Änderung wirksam wird. CHANGE_COMBO_WRITE_ERROR=Die Tastenkombination konnte nicht gespeichert werden. -MOD_UNINSTALLER_INSTALLED=Installierte Module MOD_UNINSTALLER_TO_REMOVE=Zu deinstallieren -HINT_ADD=Hinzufügen -HINT_UNDO=Rückgängig HINT_UNINSTALL=Deinstallieren MOD_UNINSTALL_CONFIRM=Diese Module entfernen? -MOD_UNINSTALL_CONFIRM_HINT=Start+Select = Ja B = Nein +HINT_MARK=Markieren +HINT_UNMARK=Abwählen +HINT_CONFIRM=Bestätigen +HINT_CANCEL=Abbrechen +MOD_UNINSTALL_NONE_MARKED=Kein Modul markiert. +MOD_UNINSTALL_NONE_HINT=Drücke A auf einem Modul, um es zu markieren. +MOD_UNINSTALL_EMPTY=Keine Module installiert. +MOD_UNINSTALL_REBOOT_NOTE=Die Konsole startet zum Abschluss neu. diff --git a/mod/etc/options_menu/language/en-US.lang b/mod/etc/options_menu/language/en-US.lang index 53849bd..781c9be 100644 --- a/mod/etc/options_menu/language/en-US.lang +++ b/mod/etc/options_menu/language/en-US.lang @@ -65,10 +65,14 @@ CHANGE_COMBO_CHOSEN=Your chosen options menu combo is: CHANGE_COMBO_CONFIRM=If this is correct press A. If not press any other button to try again. CHANGE_COMBO_DONE=Done. Please reboot to take effect. CHANGE_COMBO_WRITE_ERROR=Could not write button combo to file. -MOD_UNINSTALLER_INSTALLED=Installed Modules MOD_UNINSTALLER_TO_REMOVE=To Uninstall -HINT_ADD=Add -HINT_UNDO=Undo HINT_UNINSTALL=Uninstall MOD_UNINSTALL_CONFIRM=Remove these modules? -MOD_UNINSTALL_CONFIRM_HINT=Start+Select = Yes B = No +HINT_MARK=Mark +HINT_UNMARK=Unmark +HINT_CONFIRM=Confirm +HINT_CANCEL=Cancel +MOD_UNINSTALL_NONE_MARKED=No modules marked. +MOD_UNINSTALL_NONE_HINT=Press A on a module to mark it. +MOD_UNINSTALL_EMPTY=No modules installed. +MOD_UNINSTALL_REBOOT_NOTE=The console reboots to finish. diff --git a/mod/etc/options_menu/language/es-ES.lang b/mod/etc/options_menu/language/es-ES.lang index 9b3bf56..85e880b 100644 --- a/mod/etc/options_menu/language/es-ES.lang +++ b/mod/etc/options_menu/language/es-ES.lang @@ -64,10 +64,14 @@ CHANGE_COMBO_CHOSEN=La combinación elegida es: CHANGE_COMBO_CONFIRM=Si es correcta pulsa A. Si no, pulsa cualquier otro botón para intentarlo de nuevo. CHANGE_COMBO_DONE=Listo. Reinicia la consola para aplicar los cambios. CHANGE_COMBO_WRITE_ERROR=No se pudo guardar la combinación en el archivo. -MOD_UNINSTALLER_INSTALLED=Módulos instalados MOD_UNINSTALLER_TO_REMOVE=Para desinstalar -HINT_ADD=Añadir -HINT_UNDO=Deshacer HINT_UNINSTALL=Desinstalar MOD_UNINSTALL_CONFIRM=¿Eliminar estos módulos? -MOD_UNINSTALL_CONFIRM_HINT=Start+Select = Sí B = No +HINT_MARK=Marcar +HINT_UNMARK=Desmarcar +HINT_CONFIRM=Confirmar +HINT_CANCEL=Cancelar +MOD_UNINSTALL_NONE_MARKED=Ningún módulo marcado. +MOD_UNINSTALL_NONE_HINT=Pulsa A en un módulo para marcarlo. +MOD_UNINSTALL_EMPTY=No hay módulos instalados. +MOD_UNINSTALL_REBOOT_NOTE=La consola se reinicia para terminar. diff --git a/mod/etc/options_menu/language/fr-FR.lang b/mod/etc/options_menu/language/fr-FR.lang index f268404..557bd66 100644 --- a/mod/etc/options_menu/language/fr-FR.lang +++ b/mod/etc/options_menu/language/fr-FR.lang @@ -64,10 +64,14 @@ CHANGE_COMBO_CHOSEN=La combinaison choisie est : CHANGE_COMBO_CONFIRM=Si c'est correct, appuie sur A. Sinon, appuie sur un autre bouton pour réessayer. CHANGE_COMBO_DONE=Terminé. Redémarre la console pour appliquer les changements. CHANGE_COMBO_WRITE_ERROR=Impossible d'enregistrer la combinaison dans le fichier. -MOD_UNINSTALLER_INSTALLED=Modules installés MOD_UNINSTALLER_TO_REMOVE=À désinstaller -HINT_ADD=Ajouter -HINT_UNDO=Annuler HINT_UNINSTALL=Désinstaller MOD_UNINSTALL_CONFIRM=Supprimer ces modules ? -MOD_UNINSTALL_CONFIRM_HINT=Start+Select = Oui B = Non +HINT_MARK=Cocher +HINT_UNMARK=Décocher +HINT_CONFIRM=Confirmer +HINT_CANCEL=Annuler +MOD_UNINSTALL_NONE_MARKED=Aucun module coché. +MOD_UNINSTALL_NONE_HINT=Appuie sur A sur un module pour le cocher. +MOD_UNINSTALL_EMPTY=Aucun module installé. +MOD_UNINSTALL_REBOOT_NOTE=La console redémarre pour terminer. diff --git a/mod/etc/options_menu/language/it-IT.lang b/mod/etc/options_menu/language/it-IT.lang index f8e6301..92065d1 100644 --- a/mod/etc/options_menu/language/it-IT.lang +++ b/mod/etc/options_menu/language/it-IT.lang @@ -64,10 +64,14 @@ CHANGE_COMBO_CHOSEN=La combinazione scelta è: CHANGE_COMBO_CONFIRM=Se è corretta premi A. Altrimenti premi un altro tasto per riprovare. CHANGE_COMBO_DONE=Fatto. Riavvia la console per applicare le modifiche. CHANGE_COMBO_WRITE_ERROR=Impossibile salvare la combinazione nel file. -MOD_UNINSTALLER_INSTALLED=Moduli installati MOD_UNINSTALLER_TO_REMOVE=Da disinstallare -HINT_ADD=Aggiungi -HINT_UNDO=Annulla HINT_UNINSTALL=Disinstalla MOD_UNINSTALL_CONFIRM=Rimuovere questi moduli? -MOD_UNINSTALL_CONFIRM_HINT=Start+Select = Sì B = No +HINT_MARK=Seleziona +HINT_UNMARK=Deseleziona +HINT_CONFIRM=Conferma +HINT_CANCEL=Annulla +MOD_UNINSTALL_NONE_MARKED=Nessun modulo selezionato. +MOD_UNINSTALL_NONE_HINT=Premi A su un modulo per selezionarlo. +MOD_UNINSTALL_EMPTY=Nessun modulo installato. +MOD_UNINSTALL_REBOOT_NOTE=La console si riavvia per completare. diff --git a/mod/etc/options_menu/language/ja-JP.lang b/mod/etc/options_menu/language/ja-JP.lang index 6f8764a..b537c65 100644 --- a/mod/etc/options_menu/language/ja-JP.lang +++ b/mod/etc/options_menu/language/ja-JP.lang @@ -64,10 +64,14 @@ CHANGE_COMBO_CHOSEN=えらんだくみあわせ: CHANGE_COMBO_CONFIRM=これでよければA, ちがえばほかのButtonをおしてください CHANGE_COMBO_DONE=かんりょう. てきようするにはさいきどうしてください CHANGE_COMBO_WRITE_ERROR=くみあわせをFileにほぞんできませんでした -MOD_UNINSTALLER_INSTALLED=InstallされたModule MOD_UNINSTALLER_TO_REMOVE=さくじょするModule -HINT_ADD=ついか -HINT_UNDO=とりけし HINT_UNINSTALL=さくじょ MOD_UNINSTALL_CONFIRM=これらのModuleをさくじょしますか? -MOD_UNINSTALL_CONFIRM_HINT=Start+Select = はい B = いいえ +HINT_MARK=マーク +HINT_UNMARK=マークをはずす +HINT_CONFIRM=けってい +HINT_CANCEL=キャンセル +MOD_UNINSTALL_NONE_MARKED=マークしたModuleはありません +MOD_UNINSTALL_NONE_HINT=ModuleでAをおすとマークします +MOD_UNINSTALL_EMPTY=さくじょできるModuleはありません +MOD_UNINSTALL_REBOOT_NOTE=かんりょうするとさいきどうします diff --git a/mod/etc/options_menu/language/nl-NL.lang b/mod/etc/options_menu/language/nl-NL.lang index bee82cc..f996878 100644 --- a/mod/etc/options_menu/language/nl-NL.lang +++ b/mod/etc/options_menu/language/nl-NL.lang @@ -64,10 +64,14 @@ CHANGE_COMBO_CHOSEN=Je gekozen combinatie is: CHANGE_COMBO_CONFIRM=Als dit klopt, druk op A. Zo niet, druk op een andere knop om opnieuw te proberen. CHANGE_COMBO_DONE=Klaar. Herstart de console om de wijziging toe te passen. CHANGE_COMBO_WRITE_ERROR=Kon de combinatie niet opslaan in het bestand. -MOD_UNINSTALLER_INSTALLED=Geïnstalleerde modules MOD_UNINSTALLER_TO_REMOVE=Te verwijderen -HINT_ADD=Toevoegen -HINT_UNDO=Ongedaan maken HINT_UNINSTALL=Verwijderen MOD_UNINSTALL_CONFIRM=Deze modules verwijderen? -MOD_UNINSTALL_CONFIRM_HINT=Start+Select = Ja B = Nee +HINT_MARK=Markeren +HINT_UNMARK=Demarkeren +HINT_CONFIRM=Bevestigen +HINT_CANCEL=Annuleren +MOD_UNINSTALL_NONE_MARKED=Geen modules gemarkeerd. +MOD_UNINSTALL_NONE_HINT=Druk op A bij een module om deze te markeren. +MOD_UNINSTALL_EMPTY=Geen modules geïnstalleerd. +MOD_UNINSTALL_REBOOT_NOTE=De console herstart om af te ronden. diff --git a/mod/etc/options_menu/language/pt-PT.lang b/mod/etc/options_menu/language/pt-PT.lang index 6b64dcc..20937f0 100644 --- a/mod/etc/options_menu/language/pt-PT.lang +++ b/mod/etc/options_menu/language/pt-PT.lang @@ -64,10 +64,14 @@ CHANGE_COMBO_CHOSEN=A combinação escolhida é: CHANGE_COMBO_CONFIRM=Se estiver correta prime A. Caso contrário, prime qualquer outro botão para tentar de novo. CHANGE_COMBO_DONE=Concluído. Reinicia a consola para aplicar. CHANGE_COMBO_WRITE_ERROR=Não foi possível guardar a combinação no ficheiro. -MOD_UNINSTALLER_INSTALLED=Módulos instalados MOD_UNINSTALLER_TO_REMOVE=Para desinstalar -HINT_ADD=Adicionar -HINT_UNDO=Anular HINT_UNINSTALL=Desinstalar MOD_UNINSTALL_CONFIRM=Remover estes módulos? -MOD_UNINSTALL_CONFIRM_HINT=Start+Select = Sim B = Não +HINT_MARK=Marcar +HINT_UNMARK=Desmarcar +HINT_CONFIRM=Confirmar +HINT_CANCEL=Cancelar +MOD_UNINSTALL_NONE_MARKED=Nenhum módulo marcado. +MOD_UNINSTALL_NONE_HINT=Prime A num módulo para o marcar. +MOD_UNINSTALL_EMPTY=Nenhum módulo instalado. +MOD_UNINSTALL_REBOOT_NOTE=A consola reinicia para concluir. diff --git a/mod/etc/options_menu/language/ru-RU.lang b/mod/etc/options_menu/language/ru-RU.lang index 2610293..36eecf7 100644 --- a/mod/etc/options_menu/language/ru-RU.lang +++ b/mod/etc/options_menu/language/ru-RU.lang @@ -64,10 +64,14 @@ CHANGE_COMBO_CHOSEN=Выбранная комбинация: CHANGE_COMBO_CONFIRM=Если верно, нажмите A. Если нет, нажмите любую другую кнопку, чтобы попробовать снова. CHANGE_COMBO_DONE=Готово. Перезагрузите консоль, чтобы применить изменения. CHANGE_COMBO_WRITE_ERROR=Не удалось сохранить комбинацию в файл. -MOD_UNINSTALLER_INSTALLED=Установленные модули MOD_UNINSTALLER_TO_REMOVE=Для удаления -HINT_ADD=Добавить -HINT_UNDO=Отменить HINT_UNINSTALL=Удалить MOD_UNINSTALL_CONFIRM=Удалить эти модули? -MOD_UNINSTALL_CONFIRM_HINT=Start+Select = Да B = Нет +HINT_MARK=Отметить +HINT_UNMARK=Снять +HINT_CONFIRM=Подтвердить +HINT_CANCEL=Отмена +MOD_UNINSTALL_NONE_MARKED=Нет отмеченных модулей. +MOD_UNINSTALL_NONE_HINT=Нажмите A на модуле, чтобы отметить его. +MOD_UNINSTALL_EMPTY=Нет установленных модулей. +MOD_UNINSTALL_REBOOT_NOTE=Консоль перезагрузится для завершения. diff --git a/mod/etc/options_menu/mod_uninstall/launch_uninstaller.sh b/mod/etc/options_menu/mod_uninstall/launch_uninstaller.sh new file mode 100644 index 0000000..59012a7 --- /dev/null +++ b/mod/etc/options_menu/mod_uninstall/launch_uninstaller.sh @@ -0,0 +1,31 @@ +#!/bin/sh +# Copyright (C) 2026 DefKorns (https://defkorns.github.io/LICENSE) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +# mod_uninstall exit codes: 0 = back to Advanced Options, 2 = uninstall +# started (FinishUninstall.sh reboots), anything else = leave the menu. +optionsPath="${1%/}" + +"$optionsPath/mod_uninstall/mod_uninstall" +case $? in +0) + usleep 50000 + "$optionsPath/options" --commandPath "$optionsPath/advanced_commands/" --title "ADVANCED_OPTIONS" + ;; +2) ;; +*) + sh "$optionsPath/scripts/ResumeUI.sh" + ;; +esac diff --git a/mod/etc/options_menu/theme.cfg b/mod/etc/options_menu/theme.cfg index 13054e5..5b26969 100644 --- a/mod/etc/options_menu/theme.cfg +++ b/mod/etc/options_menu/theme.cfg @@ -37,3 +37,6 @@ BadgeYDark=143,71,80 # Start pill (wide badge instead of a letter circle) BadgeStart=194,168,94 BadgeStartDark=116,101,56 +# Module Uninstaller checkboxes (marked / unmarked) +CheckboxOn=225,85,84 +CheckboxOff=141,141,147 diff --git a/src/framework/uitheme.cpp b/src/framework/uitheme.cpp index b2b3ee9..b2b3bcc 100644 --- a/src/framework/uitheme.cpp +++ b/src/framework/uitheme.cpp @@ -42,6 +42,8 @@ namespace UiTheme Color BadgeYDark{ 0x8F, 0x47, 0x50 }; Color BadgeStart{ 0xC2, 0xA8, 0x5E }; Color BadgeStartDark{ 0x74, 0x65, 0x38 }; + Color CheckboxOn{ 0xE1, 0x55, 0x54 }; + Color CheckboxOff{ 0x8D, 0x8D, 0x93 }; namespace { @@ -77,6 +79,7 @@ namespace UiTheme { "BadgeX", &BadgeX }, { "BadgeXDark", &BadgeXDark }, { "BadgeY", &BadgeY }, { "BadgeYDark", &BadgeYDark }, { "BadgeStart", &BadgeStart }, { "BadgeStartDark", &BadgeStartDark }, + { "CheckboxOn", &CheckboxOn }, { "CheckboxOff", &CheckboxOff }, }; return keys; } @@ -114,5 +117,9 @@ namespace UiTheme if(!isSet["ScrollArrow"]) ScrollArrow = Text; + if(!isSet["CheckboxOn"]) + CheckboxOn = BadgeB; + if(!isSet["CheckboxOff"]) + CheckboxOff = TextDim; } } diff --git a/src/framework/uitheme.h b/src/framework/uitheme.h index be43a0d..7e3a7de 100644 --- a/src/framework/uitheme.h +++ b/src/framework/uitheme.h @@ -54,6 +54,8 @@ namespace UiTheme extern Color BadgeYDark; extern Color BadgeStart; // muted gold, for a wide "Start" pill instead of a letter circle extern Color BadgeStartDark; + extern Color CheckboxOn; // follows BadgeB unless set + extern Color CheckboxOff; // follows TextDim unless set // TV overscan safe area const int SafeMarginX = 64; @@ -155,6 +157,8 @@ namespace UiTheme const char * const AssetGear = "/images/ui/gear.png"; const char * const AssetChevronRight = "/images/ui/chevron_right.png"; const char * const AssetChevronUp = "/images/ui/chevron_up.png"; + const char * const AssetCheckboxOn = "/images/ui/checkbox_on.png"; + const char * const AssetCheckboxOff = "/images/ui/checkbox_off.png"; } #endif diff --git a/src/mod_uninstall.cpp b/src/mod_uninstall.cpp index 8cded30..916e4cf 100644 --- a/src/mod_uninstall.cpp +++ b/src/mod_uninstall.cpp @@ -23,12 +23,24 @@ namespace { const std::string OptionsLocation = "/etc/options_menu/"; + // never list the menu itself + const std::string HiddenModPrefix = "options_deluxe"; const int ListFontSize = 16; + const int PanelFontSize = 16; + const int PanelHeaderFontSize = 18; + const int NoteFontSize = 15; + + const int ExitBack = 0; + const int ExitLeaveMenu = 1; + const int ExitUninstallStarted = 2; + + const Color White{ 255, 255, 255 }; struct Mod { std::string name; Texture text; + bool marked = false; }; // "hakchi pack_list" lines keep their trailing newline @@ -39,15 +51,72 @@ namespace return s; } - std::string TruncateToWidth(const std::string & text, int maxWidth) + // Texture falls back to font8x8 (fontSize px per codepoint) when TTF lacks a glyph + int TextWidth(const std::string & text, int fontSize) + { + if(CanRenderWithTTF(text, fontSize)) + return MeasureTTFWidth(text, fontSize); + return Utf8Length(text) * fontSize; + } + + std::string TruncateToWidth(const std::string & text, int fontSize, int maxWidth) { - if(MeasureTTFWidth(text, ListFontSize) <= maxWidth) + if(TextWidth(text, fontSize) <= maxWidth) return text; int n = Utf8Length(text); - while(n > 0 && MeasureTTFWidth(TruncateUtf8(text, n) + "...", ListFontSize) > maxWidth) + while(n > 0 && TextWidth(TruncateUtf8(text, n) + "...", fontSize) > maxWidth) --n; return TruncateUtf8(text, n) + "..."; } + + // unspaced (CJK) or overlong words break per codepoint + std::vector WrapToWidth(const std::string & text, int fontSize, int maxWidth) + { + std::vector lines; + std::string line; + size_t pos = 0; + while(pos < text.size()) + { + size_t end = text.find(' ', pos); + std::string word = text.substr(pos, end == std::string::npos ? std::string::npos : end - pos); + pos = (end == std::string::npos) ? text.size() : end + 1; + + std::string candidate = line.empty() ? word : line + " " + word; + if(TextWidth(candidate, fontSize) <= maxWidth) + { + line = candidate; + continue; + } + if(!line.empty()) + lines.push_back(line); + line = word; + while(TextWidth(line, fontSize) > maxWidth && Utf8Length(line) > 1) + { + int n = Utf8Length(line) - 1; + while(n > 1 && TextWidth(TruncateUtf8(line, n), fontSize) > maxWidth) + --n; + std::string head = TruncateUtf8(line, n); + lines.push_back(head); + line = line.substr(head.size()); + } + } + if(!line.empty()) + lines.push_back(line); + return lines; + } + + std::vector MakeLines(const std::vector & lines, int fontSize, SDL_Renderer * renderer, Color color) + { + std::vector textures; + for(const std::string & line : lines) + textures.emplace_back(line, fontSize, renderer, 0, 0, false, ToAbgr(color), true); + return textures; + } + + void DrawDot(SDL_Renderer * renderer, int x, int centerY) + { + DrawRoundedFillRect(renderer, { x, centerY - 4, 8, 8 }, UiTheme::CheckboxOn, 4); + } } int main(int argc, char * argv[]) @@ -55,7 +124,7 @@ int main(int argc, char * argv[]) std::string folderLocation(argv[0]); folderLocation = folderLocation.substr(0, folderLocation.find_last_of('/')+1); - std::vector installedMods; + std::vector mods; { char buf[256]; auto pipe = popen("hakchi pack_list", "r"); @@ -63,17 +132,19 @@ int main(int argc, char * argv[]) { while(!feof(pipe)) if(fgets(buf, sizeof(buf), pipe)) - installedMods.push_back({TrimTrailingNewline(buf), {}}); + { + std::string name = TrimTrailingNewline(buf); + if(!name.empty() && name.compare(0, HiddenModPrefix.size(), HiddenModPrefix) != 0) + { + Mod mod; + mod.name = name; + mods.push_back(mod); + } + } pclose(pipe); } } - if(installedMods.empty()) - { - std::cerr << "Cannot find mods"; - return 1; - } - LoadLanguageFromConfig(OptionsLocation); SetTTFFontPath(OptionsLocation); UiTheme::LoadThemeConfig(OptionsLocation); @@ -93,36 +164,58 @@ int main(int argc, char * argv[]) Texture titleText(Translate("MODULE_UNINSTALLER"), UiTheme::SectionTitleFontSize, renderer, UiTheme::SectionTitleX, UiTheme::SectionTitleY, false, ToAbgr(UiTheme::Text), true); Texture creditText("created by CompCom - Modern UI by DefKorns", 16, renderer, UiTheme::CreditX, UiTheme::CreditY, false, ToAbgr(UiTheme::Text), true); - // two columns sharing the row list's usual width, split by a center divider - const int ColumnGap = 40; - const int ColumnW = (UiTheme::ListContentRightX - UiTheme::ListX - ColumnGap) / 2; - const int LeftColumnX = UiTheme::ListX; - const int RightColumnX = LeftColumnX + ColumnW + ColumnGap; - const int DividerX = LeftColumnX + ColumnW + ColumnGap/2; - const int ColumnHeaderY = UiTheme::SectionTitleY + 46; - const int ListTopY = ColumnHeaderY + 32; - const int ListBottomY = UiTheme::FooterDividerY - 16; - const int rowPitch = GetTTFLineHeight(ListFontSize); - const int displayCount = std::max(1, (ListBottomY - ListTopY) / rowPitch); - - Texture installedHeader(Translate("MOD_UNINSTALLER_INSTALLED"), 18, renderer, LeftColumnX, ColumnHeaderY, false, ToAbgr(UiTheme::TextDim), true); - Texture toRemoveHeader(Translate("MOD_UNINSTALLER_TO_REMOVE"), 18, renderer, RightColumnX, ColumnHeaderY, false, ToAbgr(UiTheme::TextDim), true); - - for(Mod & mod : installedMods) - mod.text = Texture(TruncateToWidth(mod.name, ColumnW), ListFontSize, renderer, LeftColumnX, 0, false, ToAbgr(UiTheme::Text), true); - - // Anchor chevrons to the local list range. - Texture scrollUp(OptionsLocation + UiTheme::AssetChevronUp, renderer, UiTheme::ScrollX, ListTopY + 10); + const int RowBoxH = UiTheme::RowPitch - 6; + Texture checkboxOn(OptionsLocation + UiTheme::AssetCheckboxOn, renderer); + SetColorMod(checkboxOn.texture.get(), UiTheme::CheckboxOn); + Texture checkboxOff(OptionsLocation + UiTheme::AssetCheckboxOff, renderer); + SetColorMod(checkboxOff.texture.get(), UiTheme::CheckboxOff); + const int CheckboxSize = 20; + const int CheckboxX = UiTheme::RowControlRightX - CheckboxSize; + const int RowTextMaxW = CheckboxX - 16 - UiTheme::RowTextX; + const int displayCount = std::max(1, (UiTheme::FooterDividerY - UiTheme::ListBottomMargin - UiTheme::RowFirstY) / UiTheme::RowPitch); + + // white, tinted per mark state at draw time + for(Mod & mod : mods) + mod.text = Texture(TruncateToWidth(mod.name, ListFontSize, RowTextMaxW), ListFontSize, renderer, UiTheme::RowTextX, 0, false, ToAbgr(White), true); + Texture emptyText(Translate("MOD_UNINSTALL_EMPTY"), ListFontSize, renderer, UiTheme::RowTextX, UiTheme::RowFirstY, false, ToAbgr(UiTheme::TextDim), true); + + Texture scrollUp(OptionsLocation + UiTheme::AssetChevronUp, renderer, UiTheme::ScrollX, UiTheme::ScrollUpY); SetColorMod(scrollUp.texture.get(), UiTheme::ScrollArrow); - scrollUp.rect.x -= scrollUp.rect.w / 2; Texture scrollDown = scrollUp; - scrollDown.rect.y = ListBottomY - 50; + scrollDown.rect.y = UiTheme::ScrollDownY; + + const int PanelPad = 16; + const int PanelInnerX = UiTheme::DetailX + PanelPad; + const int PanelInnerW = UiTheme::DetailW - 2*PanelPad; + const SDL_Rect summaryBox{ UiTheme::DetailX, UiTheme::RowFirstY, UiTheme::DetailW, 260 }; + const int SummaryDividerY = summaryBox.y + 46; + const int SummaryListY = summaryBox.y + 62; + const int SummaryRowPitch = 28; + const int SummaryMaxRows = (summaryBox.y + summaryBox.h - PanelPad - SummaryListY) / SummaryRowPitch; + const int BulletTextGap = 18; + const SDL_Rect noteBox{ UiTheme::DetailX, summaryBox.y + summaryBox.h + 16, UiTheme::DetailW, 72 }; + const int NoteIconCX = noteBox.x + 26; + const int NoteTextX = noteBox.x + 46; + + Texture summaryHeader(Translate("MOD_UNINSTALLER_TO_REMOVE"), PanelHeaderFontSize, renderer, PanelInnerX, summaryBox.y + 14, false, ToAbgr(UiTheme::TextDim), true); + Texture summaryCount; + Texture noneMarkedText(Translate("MOD_UNINSTALL_NONE_MARKED"), PanelFontSize, renderer, PanelInnerX, SummaryListY, false, ToAbgr(UiTheme::Text), true); + std::vector noneHintLines = MakeLines(WrapToWidth(Translate("MOD_UNINSTALL_NONE_HINT"), PanelFontSize, PanelInnerW), PanelFontSize, renderer, UiTheme::TextDim); + std::vector rebootNoteLines = MakeLines(WrapToWidth(Translate("MOD_UNINSTALL_REBOOT_NOTE"), NoteFontSize, noteBox.x + noteBox.w - PanelPad - NoteTextX), NoteFontSize, renderer, UiTheme::TextDim); + Texture moreText; + std::vector summaryNames; Texture badgeOuter(OptionsLocation + UiTheme::AssetBadgeOuter, renderer); Texture badgeInner(OptionsLocation + UiTheme::AssetBadgeInner, renderer); struct Badge { Texture letter; Texture label; Color rim; Color fill; }; - Badge badgeAdd{ Texture("A", 16, renderer, 0, 0, false, ToAbgr(UiTheme::BadgeLetter), true), Texture(Translate("HINT_ADD"), 16, renderer, 0, 0, false, ToAbgr(UiTheme::Text), true), UiTheme::BadgeADark, UiTheme::BadgeA }; - Badge badgeUndo{ Texture("B", 16, renderer, 0, 0, false, ToAbgr(UiTheme::BadgeLetter), true), Texture(Translate("HINT_UNDO"), 16, renderer, 0, 0, false, ToAbgr(UiTheme::Text), true), UiTheme::BadgeBDark, UiTheme::BadgeB }; + auto MakeBadge = [&](const std::string & letter, const std::string & labelKey, Color rim, Color fill) -> Badge + { + return { Texture(letter, 16, renderer, 0, 0, false, ToAbgr(UiTheme::BadgeLetter), true), Texture(Translate(labelKey), 16, renderer, 0, 0, false, ToAbgr(UiTheme::Text), true), rim, fill }; + }; + Badge badgeMark = MakeBadge("A", "HINT_MARK", UiTheme::BadgeADark, UiTheme::BadgeA); + Badge badgeUnmark = MakeBadge("A", "HINT_UNMARK", UiTheme::BadgeADark, UiTheme::BadgeA); + Badge badgeBack = MakeBadge("B", "HINT_BACK", UiTheme::BadgeBDark, UiTheme::BadgeB); + Badge badgeCancel = MakeBadge("B", "HINT_CANCEL", UiTheme::BadgeBDark, UiTheme::BadgeB); auto DrawBadge = [&](Badge & badge, int rightEdgeX) -> int { int groupW = UiTheme::BadgeOuterSize + UiTheme::BadgeLabelGap + badge.label.rect.w; @@ -145,9 +238,11 @@ int main(int argc, char * argv[]) }; // Use a wide chip to distinguish Start from Select. - Texture pillText("Start", 14, renderer, 0, 0, false, ToAbgr(UiTheme::BadgeLetter), true); - Texture uninstallLabel(Translate("HINT_UNINSTALL"), 16, renderer, 0, 0, false, ToAbgr(UiTheme::Text), true); - auto DrawPillBadge = [&](Texture & pill, Texture & label, const Color & rim, const Color & fill, int rightEdgeX) -> int + Texture startPill("Start", 14, renderer, 0, 0, false, ToAbgr(UiTheme::BadgeLetter), true); + Texture startSelectPill("Start + Select", 14, renderer, 0, 0, false, ToAbgr(UiTheme::BadgeLetter), true); + Texture uninstallLabel; + Texture confirmLabel(Translate("HINT_CONFIRM"), 16, renderer, 0, 0, false, ToAbgr(UiTheme::Text), true); + auto DrawPillBadge = [&](Texture & pill, Texture & label, int rightEdgeX) -> int { const int pillPadX = 10; int pillW = pill.rect.w + pillPadX*2; @@ -155,11 +250,11 @@ int main(int argc, char * argv[]) int x = rightEdgeX - groupW; int y = UiTheme::BadgeBandY; SDL_Rect pillRect{ x, y, pillW, UiTheme::BadgeOuterSize }; - DrawRoundedFillRect(renderer, pillRect, rim, pillRect.h/2); + DrawRoundedFillRect(renderer, pillRect, UiTheme::BadgeStartDark, pillRect.h/2); // same ring thickness as the letter badges' outer/inner circle pair int border = (UiTheme::BadgeOuterSize - UiTheme::BadgeInnerSize) / 2; SDL_Rect fillRect{ x+border, y+border, pillW-border*2, UiTheme::BadgeInnerSize }; - DrawRoundedFillRect(renderer, fillRect, fill, fillRect.h/2); + DrawRoundedFillRect(renderer, fillRect, UiTheme::BadgeStart, fillRect.h/2); pill.rect.x = x + (pillW - pill.rect.w)/2; pill.rect.y = y + (UiTheme::BadgeOuterSize - pill.rect.h)/2; pill.Draw(renderer); @@ -168,10 +263,45 @@ int main(int argc, char * argv[]) label.Draw(renderer); return x - UiTheme::BadgeGroupGap; }; + auto DrawFooterDivider = [&](int rightEdge) + { + int dividerX = rightEdge - UiTheme::BadgeDividerGapFromCluster; + DrawVLine(renderer, dividerX, UiTheme::FooterY + 10, UiTheme::FooterY + UiTheme::FooterH - 10, UiTheme::Border, 2); + }; - int installListOffset = 0; + int listOffset = 0; int currentId = 0; - std::vector uninstallList; + int markedCount = 0; + const int modCount = static_cast(mods.size()); + + auto MarkedMods = [&]() + { + std::vector marked; + for(const Mod & mod : mods) + if(mod.marked) + marked.push_back(&mod); + return marked; + }; + + auto RefreshSummary = [&]() + { + auto marked = MarkedMods(); + const int count = static_cast(marked.size()); + markedCount = count; + summaryCount = Texture(std::to_string(count), PanelHeaderFontSize, renderer, 0, summaryBox.y + 14, false, ToAbgr(count ? UiTheme::CheckboxOn : UiTheme::TextDim), true); + summaryCount.rect.x = summaryBox.x + summaryBox.w - PanelPad - summaryCount.rect.w; + uninstallLabel = Texture(Translate("HINT_UNINSTALL") + " (" + std::to_string(count) + ")", 16, renderer, 0, 0, false, ToAbgr(UiTheme::Text), true); + + // keep the last row for "+N" when the list overflows the panel + int shown = (count > SummaryMaxRows) ? SummaryMaxRows - 1 : count; + summaryNames.clear(); + for(int i = 0; i < shown; ++i) + summaryNames.emplace_back(TruncateToWidth(marked[i]->name, PanelFontSize, PanelInnerW - BulletTextGap), PanelFontSize, renderer, PanelInnerX + BulletTextGap, 0, false, ToAbgr(UiTheme::Text), true); + moreText = (shown < count) + ? Texture("+" + std::to_string(count - shown), PanelFontSize, renderer, PanelInnerX + BulletTextGap, 0, false, ToAbgr(UiTheme::TextDim), true) + : Texture(); + }; + RefreshSummary(); auto DrawChrome = [&]() { @@ -187,60 +317,150 @@ int main(int argc, char * argv[]) creditText.Draw(renderer); }; - auto render = [&]() + auto DrawList = [&]() { - sdl_context.StartFrame(); - DrawChrome(); - installedHeader.Draw(renderer); - toRemoveHeader.Draw(renderer); - DrawVLine(renderer, DividerX, ColumnHeaderY, ListBottomY, UiTheme::Border, UiTheme::BorderWidth); - - int count = std::min(displayCount, (int)installedMods.size()-installListOffset); + if(mods.empty()) + { + emptyText.rect.y = UiTheme::RowFirstY + (RowBoxH - emptyText.rect.h)/2; + emptyText.Draw(renderer); + return; + } + int count = std::min(displayCount, modCount - listOffset); for(int i = 0; i < count; ++i) { - Texture & rowText = installedMods[installListOffset+i].text; - rowText.rect.x = LeftColumnX; - rowText.rect.y = ListTopY + i*rowPitch + (rowPitch - rowText.rect.h)/2; + Mod & mod = mods[listOffset + i]; + SDL_Rect rowRect{ UiTheme::ListX, UiTheme::RowFirstY + i*UiTheme::RowPitch, UiTheme::ListContentRightX - UiTheme::ListX, RowBoxH }; + int centerY = rowRect.y + rowRect.h/2; if(i == currentId) { - SDL_Rect rowRect{ LeftColumnX - 8, rowText.rect.y - 2, ColumnW + 8, rowText.rect.h + 4 }; DrawRoundedFillRect(renderer, rowRect, UiTheme::SelectedRowBg, UiTheme::BoxRadius); DrawStrokeRect(renderer, rowRect, UiTheme::Accent, 2, UiTheme::BoxRadius); } - rowText.Draw(renderer); + else + DrawHLine(renderer, rowRect.x, rowRect.x + rowRect.w, rowRect.y + rowRect.h + 3, UiTheme::Border, 1); + + mod.text.rect.y = centerY - mod.text.rect.h/2; + SetColorMod(mod.text.texture.get(), mod.marked ? UiTheme::TextDim : UiTheme::Text); + mod.text.Draw(renderer); + if(mod.marked) + DrawHLine(renderer, mod.text.rect.x, mod.text.rect.x + mod.text.rect.w, centerY + 1, UiTheme::TextDim, 1); + Texture & checkbox = mod.marked ? checkboxOn : checkboxOff; + checkbox.rect.x = CheckboxX; + checkbox.rect.y = centerY - checkbox.rect.h/2; + checkbox.Draw(renderer); } - if(installListOffset != 0) + if(listOffset != 0) scrollUp.Draw(renderer); - if(installListOffset + displayCount < (int)installedMods.size()) + if(listOffset + displayCount < modCount) scrollDown.Draw(renderer, SDL_FLIP_VERTICAL); + }; + + auto DrawSummary = [&]() + { + DrawStrokeRect(renderer, summaryBox, UiTheme::Border, UiTheme::BorderWidth, UiTheme::BoxRadius); + summaryHeader.Draw(renderer); + summaryCount.Draw(renderer); + DrawHLine(renderer, PanelInnerX, PanelInnerX + PanelInnerW, SummaryDividerY, UiTheme::Border, 1); + if(markedCount == 0) + { + noneMarkedText.Draw(renderer); + int y = SummaryListY + 28; + for(Texture & line : noneHintLines) + { + line.rect.x = PanelInnerX; + line.rect.y = y; + line.Draw(renderer); + y += 22; + } + } + else + { + int y = SummaryListY; + for(Texture & name : summaryNames) + { + int centerY = y + SummaryRowPitch/2 - 4; + DrawDot(renderer, PanelInnerX, centerY); + name.rect.y = centerY - name.rect.h/2; + name.Draw(renderer); + y += SummaryRowPitch; + } + if(moreText.texture) + { + moreText.rect.y = y + SummaryRowPitch/2 - 4 - moreText.rect.h/2; + moreText.Draw(renderer); + } + } - int uninstallCount = std::min(displayCount, (int)uninstallList.size()); - for(int i = 0; i < uninstallCount; ++i) + DrawStrokeRect(renderer, noteBox, UiTheme::Border, UiTheme::BorderWidth, UiTheme::BoxRadius); + int noteCenterY = noteBox.y + noteBox.h/2; + DrawStrokeRect(renderer, { NoteIconCX - 9, noteCenterY - 9, 18, 18 }, UiTheme::TextDim, 2, 9); + DrawFillRect(renderer, { NoteIconCX - 1, noteCenterY - 5, 2, 2 }, UiTheme::TextDim); + DrawFillRect(renderer, { NoteIconCX - 1, noteCenterY - 1, 2, 6 }, UiTheme::TextDim); + const int noteLinePitch = 20; + int y = noteCenterY - (static_cast(rebootNoteLines.size()) * noteLinePitch)/2; + for(Texture & line : rebootNoteLines) { - Texture & rowText = uninstallList[i].text; - rowText.rect.x = RightColumnX; - rowText.rect.y = ListTopY + i*rowPitch + (rowPitch - rowText.rect.h)/2; - rowText.Draw(renderer); + line.rect.x = NoteTextX; + line.rect.y = y + (noteLinePitch - line.rect.h)/2; + line.Draw(renderer); + y += noteLinePitch; } + }; + + auto render = [&]() + { + sdl_context.StartFrame(); + DrawChrome(); + DrawList(); + DrawSummary(); int rightEdge = UiTheme::BadgeClusterRightX; - rightEdge = DrawBadge(badgeAdd, rightEdge); - rightEdge = DrawBadge(badgeUndo, rightEdge); - DrawPillBadge(pillText, uninstallLabel, UiTheme::BadgeStartDark, UiTheme::BadgeStart, rightEdge); + if(!mods.empty()) + rightEdge = DrawBadge(mods[listOffset + currentId].marked ? badgeUnmark : badgeMark, rightEdge); + rightEdge = DrawBadge(badgeBack, rightEdge); + if(markedCount > 0) + rightEdge = DrawPillBadge(startPill, uninstallLabel, rightEdge); + DrawFooterDivider(rightEdge); SetDrawColor(renderer, bg); // flat helpers leave the draw color dirty sdl_context.EndFrame(); }; - // true if the uninstall was confirmed and kicked off - caller should break out of the main loop auto ConfirmUninstall = [&]() -> bool { - Texture confirmTitle(Translate("MOD_UNINSTALL_CONFIRM"), 24, renderer, 640, 320, true, ToAbgr(UiTheme::Text), true); - Texture confirmHint(Translate("MOD_UNINSTALL_CONFIRM_HINT"), 16, renderer, 640, 360, true, ToAbgr(UiTheme::Text), true); - controller.GetButtonStatus(B); // consume the still-held B from whatever press opened this dialog + auto marked = MarkedMods(); + const int maxNames = 8; + const int namePitch = 30; + const int shown = std::min(maxNames, static_cast(marked.size())); + const bool overflow = static_cast(marked.size()) > shown; + const int listRows = shown + (overflow ? 1 : 0); + + const int boxW = 560; + const int boxH = 150 + listRows*namePitch; + const int bodyCenterY = (UiTheme::HeaderDividerY + UiTheme::FooterDividerY) / 2; + const SDL_Rect box{ 640 - boxW/2, bodyCenterY - boxH/2, boxW, boxH }; + + Texture confirmTitle(Translate("MOD_UNINSTALL_CONFIRM"), 24, renderer, 640, box.y + 36, true, ToAbgr(UiTheme::Text), true); + std::vector names; + int nameW = 0; + for(int i = 0; i < shown; ++i) + { + names.emplace_back(TruncateToWidth(marked[i]->name, PanelFontSize, boxW - 80), PanelFontSize, renderer, 0, 0, false, ToAbgr(UiTheme::Text), true); + nameW = std::max(nameW, names.back().rect.w); + } + if(overflow) + names.emplace_back("+" + std::to_string(marked.size() - shown), PanelFontSize, renderer, 0, 0, false, ToAbgr(UiTheme::TextDim), true); + const int listX = 640 - (nameW + BulletTextGap)/2; + std::vector noteLines = MakeLines(WrapToWidth(Translate("MOD_UNINSTALL_REBOOT_NOTE"), NoteFontSize, boxW - 48), NoteFontSize, renderer, UiTheme::TextDim); + + controller.GetButtonStatus(B); // drop a stale B press bool confirmed = false; for(;;) { + SDL_Event e; + while(SDL_PollEvent(&e)) + if(e.type == SDL_QUIT) + return false; controller.Update(); if(sdl_context.powerwatch->buttonPress() || controller.GetButtonStatus(B)) break; @@ -249,11 +469,36 @@ int main(int argc, char * argv[]) confirmed = true; break; } + sdl_context.StartFrame(); - DrawFillRect(renderer, UiTheme::FrameRect, bg); - DrawStrokeRect(renderer, UiTheme::FrameRect, UiTheme::Border, UiTheme::BorderWidth, UiTheme::BorderRadius); + DrawChrome(); + DrawRoundedFillRect(renderer, box, UiTheme::SelectedRowBg, UiTheme::BorderRadius); + DrawStrokeRect(renderer, box, UiTheme::Accent, 2, UiTheme::BorderRadius); confirmTitle.Draw(renderer); - confirmHint.Draw(renderer); + int y = box.y + 78; + for(int i = 0; i < static_cast(names.size()); ++i) + { + if(i < shown) + DrawDot(renderer, listX, y); + names[i].rect.x = listX + BulletTextGap; + names[i].rect.y = y - names[i].rect.h/2; + names[i].Draw(renderer); + y += namePitch; + } + int noteY = box.y + box.h - 34 - (static_cast(noteLines.size()) - 1) * 10; + for(Texture & line : noteLines) + { + line.rect.x = 640 - line.rect.w/2; + line.rect.y = noteY - line.rect.h/2; + line.Draw(renderer); + noteY += 20; + } + + int rightEdge = UiTheme::BadgeClusterRightX; + rightEdge = DrawBadge(badgeCancel, rightEdge); + rightEdge = DrawPillBadge(startSelectPill, confirmLabel, rightEdge); + DrawFooterDivider(rightEdge); + SetDrawColor(renderer, bg); sdl_context.EndFrame(); } @@ -261,13 +506,12 @@ int main(int argc, char * argv[]) return false; std::ofstream out("/tmp/uninstall"); - if(out.is_open()) - { - for(auto & mod : uninstallList) - out << mod.name << " "; - out.close(); - system(("/bin/sh " + folderLocation + "/FinishUninstall.sh &").c_str()); - } + if(!out.is_open()) + return false; + for(const Mod * mod : marked) + out << mod->name << " "; + out.close(); + system(("/bin/sh " + folderLocation + "/FinishUninstall.sh &").c_str()); return true; }; @@ -276,56 +520,40 @@ int main(int argc, char * argv[]) SDL_Event e; while(SDL_PollEvent(&e)) if(e.type == SDL_QUIT) - return 0; + return ExitLeaveMenu; controller.Update(); if(sdl_context.powerwatch->buttonPress()) - break; + return ExitLeaveMenu; - if(controller.GetButtonStatus(UP)) + if(controller.HeldRepeat(UP) && !mods.empty()) { if(currentId > 0) --currentId; - else if(installListOffset) - --installListOffset; + else if(listOffset > 0) + --listOffset; } - else if(controller.GetButtonStatus(DOWN)) + else if(controller.HeldRepeat(DOWN) && !mods.empty()) { - if(currentId < displayCount-1 && installListOffset+currentId+1 < (int)installedMods.size()) + if(currentId < displayCount-1 && listOffset+currentId+1 < modCount) ++currentId; - else if(installListOffset+displayCount < (int)installedMods.size()) - ++installListOffset; + else if(listOffset+displayCount < modCount) + ++listOffset; } - else if(controller.GetButtonStatus(A)) + else if(controller.GetButtonStatus(A) && !mods.empty()) { - if(currentId != -1 && !installedMods.empty()) - { - uninstallList.push_back(installedMods[installListOffset+currentId]); - installedMods.erase(installedMods.begin()+installListOffset+currentId); - if(installListOffset && installListOffset+displayCount > (int)installedMods.size()) - --installListOffset; - if(installListOffset == 0 && currentId >= (int)installedMods.size()) - currentId = static_cast(installedMods.size()) - 1; // -1 (empty) is a valid sentinel here - } + Mod & mod = mods[listOffset + currentId]; + mod.marked = !mod.marked; + RefreshSummary(); } else if(controller.GetButtonStatus(B)) + return ExitBack; + else if(controller.GetButtonStatus(START) && markedCount > 0) { - if(!uninstallList.empty()) - { - installedMods.push_back(uninstallList.back()); - uninstallList.pop_back(); - if(currentId == -1) - currentId = 0; - } - } - else if(controller.GetButtonStatus(START)) - { - if(uninstallList.empty() || ConfirmUninstall()) - break; + if(ConfirmUninstall()) + return ExitUninstallStarted; } render(); } - - return 0; } From 79576d55cc1bcd1b482dcd71f7f4b82a8b4b29cd Mon Sep 17 00:00:00 2001 From: DefKorns Date: Tue, 6 Oct 2026 17:01:25 +0000 Subject: [PATCH 059/104] Skip the output screen for silent commands and refresh all toggle states --- src/command.cpp | 9 ++++++++- src/main.cpp | 5 ++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/src/command.cpp b/src/command.cpp index 51e0544..1a3af3d 100644 --- a/src/command.cpp +++ b/src/command.cpp @@ -180,10 +180,17 @@ void Command::RunCommand(SDL_Context & sdl_context, Controller * controller, con break; } - render(); + // no output: skip the output screen + if(!textList.empty()) + render(); } pclose(pipe); + if(textList.empty()) + { + SetDrawColor(renderer, bg); + return; + } while (!controller->GetButtonStatus(B)) { controller->Update(); diff --git a/src/main.cpp b/src/main.cpp index c0f05e7..a609ed3 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -519,8 +519,11 @@ int main(int argc, char * argv[]) if(commands[currentCommandId].runInternal) { commands[currentCommandId].RunCommand(sdl_context, &controller, { gearIcon, appTitleText, appVersionText, CompComText }, bg); + // refresh all: preset rows form a radio group if(commands[currentCommandId].isToggle) - commands[currentCommandId].UpdateState(); + for(Command & c : commands) + if(c.isToggle) + c.UpdateState(); } else { From 5fbf6cfcdb58f5e382f2aa0901d4b2926932da02 Mon Sep 17 00:00:00 2001 From: DefKorns Date: Tue, 6 Oct 2026 17:01:52 +0000 Subject: [PATCH 060/104] Move controller settings and diagnostics into Advanced Options submenus --- .../advanced_commands/c0010_ControllerOptions | 6 ++ .../c0015_DiagnosticsOptions | 6 ++ .../advanced_commands/c0018_separator | 0 .../c0035_EpilepsyProtection | 6 ++ .../commands/c0005_ChangeCombo} | 2 +- .../controller/commands/c0008_separator | 0 .../controller/commands/c0010_Autofire | 5 ++ .../controller/commands/c0011_AutofireFast | 6 ++ .../controller/commands/c0012_AutofireNormal | 6 ++ .../controller/commands/c0013_AutofireSlow | 6 ++ .../controller/commands/c0020_AutofireXY | 5 ++ .../controller/commands/c0025_separator | 0 .../controller/commands/c0030_HomeCombo | 5 ++ .../controller/commands/c0040_FcStart | 6 ++ .../controller/scripts/clovercon.sh | 78 ++++++++++++++++++ .../diagnostics/commands/c0010_SystemInfo | 4 + .../commands/c0020_DisplayTemp} | 0 .../commands/c0030_DisplayTop} | 0 .../commands/c0040_BenchMark} | 0 .../diagnostics/commands/c0045_separator | 0 .../commands/c0050_RADebug} | 0 .../commands/c0060_DumpFile} | 0 .../diagnostics/commands/c0070_SaveDmesg | 5 ++ .../options_menu/images/preview_autofire.png | Bin 0 -> 3089 bytes .../images/preview_autofire_speed.png | Bin 0 -> 4561 bytes .../images/preview_autofire_xy.png | Bin 0 -> 5389 bytes .../images/preview_controller.png | Bin 0 -> 6009 bytes .../images/preview_diagnostics.png | Bin 0 -> 3395 bytes mod/etc/options_menu/images/preview_dmesg.png | Bin 0 -> 2569 bytes .../options_menu/images/preview_epilepsy.png | Bin 0 -> 4560 bytes .../options_menu/images/preview_fc_start.png | Bin 0 -> 5613 bytes .../images/preview_home_combo.png | Bin 0 -> 5215 bytes .../images/preview_system_info.png | Bin 0 -> 3863 bytes mod/etc/options_menu/language/de-DE.lang | 17 ++++ mod/etc/options_menu/language/en-US.lang | 17 ++++ mod/etc/options_menu/language/es-ES.lang | 17 ++++ mod/etc/options_menu/language/fr-FR.lang | 17 ++++ mod/etc/options_menu/language/it-IT.lang | 17 ++++ mod/etc/options_menu/language/ja-JP.lang | 17 ++++ mod/etc/options_menu/language/nl-NL.lang | 17 ++++ mod/etc/options_menu/language/pt-PT.lang | 17 ++++ mod/etc/options_menu/language/ru-RU.lang | 17 ++++ .../mod_uninstall/launch_uninstaller.sh | 3 +- mod/etc/options_menu/scripts/armet.sh | 30 +++++++ mod/etc/options_menu/scripts/save_dmesg.sh | 25 ++++++ mod/etc/options_menu/scripts/system_info.sh | 37 +++++++++ mod/install | 6 +- mod/uninstall | 1 + 48 files changed, 395 insertions(+), 6 deletions(-) create mode 100644 mod/etc/options_menu/advanced_commands/c0010_ControllerOptions create mode 100644 mod/etc/options_menu/advanced_commands/c0015_DiagnosticsOptions create mode 100644 mod/etc/options_menu/advanced_commands/c0018_separator create mode 100644 mod/etc/options_menu/advanced_commands/c0035_EpilepsyProtection rename mod/etc/options_menu/{advanced_commands/c0020_ChangeCombo => controller/commands/c0005_ChangeCombo} (73%) create mode 100644 mod/etc/options_menu/controller/commands/c0008_separator create mode 100644 mod/etc/options_menu/controller/commands/c0010_Autofire create mode 100644 mod/etc/options_menu/controller/commands/c0011_AutofireFast create mode 100644 mod/etc/options_menu/controller/commands/c0012_AutofireNormal create mode 100644 mod/etc/options_menu/controller/commands/c0013_AutofireSlow create mode 100644 mod/etc/options_menu/controller/commands/c0020_AutofireXY create mode 100644 mod/etc/options_menu/controller/commands/c0025_separator create mode 100644 mod/etc/options_menu/controller/commands/c0030_HomeCombo create mode 100644 mod/etc/options_menu/controller/commands/c0040_FcStart create mode 100644 mod/etc/options_menu/controller/scripts/clovercon.sh create mode 100644 mod/etc/options_menu/diagnostics/commands/c0010_SystemInfo rename mod/etc/options_menu/{advanced_commands/c0060_DisplayTemp => diagnostics/commands/c0020_DisplayTemp} (100%) rename mod/etc/options_menu/{advanced_commands/c0070_DisplayTop => diagnostics/commands/c0030_DisplayTop} (100%) rename mod/etc/options_menu/{advanced_commands/c0080_BenchMark => diagnostics/commands/c0040_BenchMark} (100%) create mode 100644 mod/etc/options_menu/diagnostics/commands/c0045_separator rename mod/etc/options_menu/{advanced_commands/c0090_RADebug => diagnostics/commands/c0050_RADebug} (100%) rename mod/etc/options_menu/{advanced_commands/c0100_DumpFile => diagnostics/commands/c0060_DumpFile} (100%) create mode 100644 mod/etc/options_menu/diagnostics/commands/c0070_SaveDmesg create mode 100644 mod/etc/options_menu/images/preview_autofire.png create mode 100644 mod/etc/options_menu/images/preview_autofire_speed.png create mode 100644 mod/etc/options_menu/images/preview_autofire_xy.png create mode 100644 mod/etc/options_menu/images/preview_controller.png create mode 100644 mod/etc/options_menu/images/preview_diagnostics.png create mode 100644 mod/etc/options_menu/images/preview_dmesg.png create mode 100644 mod/etc/options_menu/images/preview_epilepsy.png create mode 100644 mod/etc/options_menu/images/preview_fc_start.png create mode 100644 mod/etc/options_menu/images/preview_home_combo.png create mode 100644 mod/etc/options_menu/images/preview_system_info.png create mode 100644 mod/etc/options_menu/scripts/armet.sh create mode 100644 mod/etc/options_menu/scripts/save_dmesg.sh create mode 100644 mod/etc/options_menu/scripts/system_info.sh diff --git a/mod/etc/options_menu/advanced_commands/c0010_ControllerOptions b/mod/etc/options_menu/advanced_commands/c0010_ControllerOptions new file mode 100644 index 0000000..4ed2114 --- /dev/null +++ b/mod/etc/options_menu/advanced_commands/c0010_ControllerOptions @@ -0,0 +1,6 @@ +COMMAND_NAME=CONTROLLER_OPTIONS +COMMAND_TYPE=EXTERNAL +RESTART_UI=FALSE +SUBMENU=TRUE +COMMAND_STR=usleep 50000 && %options_path%/options --commandPath %options_path%/controller/commands/ --scriptPath %options_path%/controller/scripts/ --title "CONTROLLER_OPTIONS" & +PREVIEW_IMAGE=/etc/options_menu/images/preview_controller.png diff --git a/mod/etc/options_menu/advanced_commands/c0015_DiagnosticsOptions b/mod/etc/options_menu/advanced_commands/c0015_DiagnosticsOptions new file mode 100644 index 0000000..e2f9f35 --- /dev/null +++ b/mod/etc/options_menu/advanced_commands/c0015_DiagnosticsOptions @@ -0,0 +1,6 @@ +COMMAND_NAME=DIAGNOSTICS_OPTIONS +COMMAND_TYPE=EXTERNAL +RESTART_UI=FALSE +SUBMENU=TRUE +COMMAND_STR=usleep 50000 && %options_path%/options --commandPath %options_path%/diagnostics/commands/ --scriptPath %options_path%/scripts/ --title "DIAGNOSTICS_OPTIONS" & +PREVIEW_IMAGE=/etc/options_menu/images/preview_diagnostics.png diff --git a/mod/etc/options_menu/advanced_commands/c0018_separator b/mod/etc/options_menu/advanced_commands/c0018_separator new file mode 100644 index 0000000..e69de29 diff --git a/mod/etc/options_menu/advanced_commands/c0035_EpilepsyProtection b/mod/etc/options_menu/advanced_commands/c0035_EpilepsyProtection new file mode 100644 index 0000000..6fa471e --- /dev/null +++ b/mod/etc/options_menu/advanced_commands/c0035_EpilepsyProtection @@ -0,0 +1,6 @@ +COMMAND_NAME=EPILEPSY_PROTECTION +COMMAND_TYPE=INTERNAL +ENABLE_IF=[ "$sftype" = "nes" ] || [ "$sftype" = "hvcj" ] +COMMAND_STR=sh %script_dir%/armet.sh toggle +STATE_STR=sh %script_dir%/armet.sh state +PREVIEW_IMAGE=/etc/options_menu/images/preview_epilepsy.png diff --git a/mod/etc/options_menu/advanced_commands/c0020_ChangeCombo b/mod/etc/options_menu/controller/commands/c0005_ChangeCombo similarity index 73% rename from mod/etc/options_menu/advanced_commands/c0020_ChangeCombo rename to mod/etc/options_menu/controller/commands/c0005_ChangeCombo index 9cd87f7..1378b36 100644 --- a/mod/etc/options_menu/advanced_commands/c0020_ChangeCombo +++ b/mod/etc/options_menu/controller/commands/c0005_ChangeCombo @@ -1,5 +1,5 @@ COMMAND_NAME=CHANGE_BUTTON_COMBO COMMAND_TYPE=INTERNAL IGNORE_INTERRUPT=TRUE -COMMAND_STR=exec %script_dir%/ChangeCombo +COMMAND_STR=exec %options_path%/scripts/ChangeCombo PREVIEW_IMAGE=/etc/options_menu/images/preview_change_combo.png diff --git a/mod/etc/options_menu/controller/commands/c0008_separator b/mod/etc/options_menu/controller/commands/c0008_separator new file mode 100644 index 0000000..e69de29 diff --git a/mod/etc/options_menu/controller/commands/c0010_Autofire b/mod/etc/options_menu/controller/commands/c0010_Autofire new file mode 100644 index 0000000..f30ed69 --- /dev/null +++ b/mod/etc/options_menu/controller/commands/c0010_Autofire @@ -0,0 +1,5 @@ +COMMAND_NAME=AUTOFIRE +COMMAND_TYPE=INTERNAL +COMMAND_STR=sh %script_dir%/clovercon.sh toggle autofire +STATE_STR=sh %script_dir%/clovercon.sh state autofire +PREVIEW_IMAGE=/etc/options_menu/images/preview_autofire.png diff --git a/mod/etc/options_menu/controller/commands/c0011_AutofireFast b/mod/etc/options_menu/controller/commands/c0011_AutofireFast new file mode 100644 index 0000000..57d76b5 --- /dev/null +++ b/mod/etc/options_menu/controller/commands/c0011_AutofireFast @@ -0,0 +1,6 @@ +COMMAND_NAME=AUTOFIRE_SPEED_FAST +COMMAND_TYPE=INTERNAL +CHILD=TRUE +COMMAND_STR=sh %script_dir%/clovercon.sh set autofire_interval 4 +STATE_STR=sh %script_dir%/clovercon.sh is autofire_interval 4 +PREVIEW_IMAGE=/etc/options_menu/images/preview_autofire_speed.png diff --git a/mod/etc/options_menu/controller/commands/c0012_AutofireNormal b/mod/etc/options_menu/controller/commands/c0012_AutofireNormal new file mode 100644 index 0000000..f5402b9 --- /dev/null +++ b/mod/etc/options_menu/controller/commands/c0012_AutofireNormal @@ -0,0 +1,6 @@ +COMMAND_NAME=AUTOFIRE_SPEED_NORMAL +COMMAND_TYPE=INTERNAL +CHILD=TRUE +COMMAND_STR=sh %script_dir%/clovercon.sh set autofire_interval 8 +STATE_STR=sh %script_dir%/clovercon.sh is autofire_interval 8 +PREVIEW_IMAGE=/etc/options_menu/images/preview_autofire_speed.png diff --git a/mod/etc/options_menu/controller/commands/c0013_AutofireSlow b/mod/etc/options_menu/controller/commands/c0013_AutofireSlow new file mode 100644 index 0000000..c492205 --- /dev/null +++ b/mod/etc/options_menu/controller/commands/c0013_AutofireSlow @@ -0,0 +1,6 @@ +COMMAND_NAME=AUTOFIRE_SPEED_SLOW +COMMAND_TYPE=INTERNAL +CHILD=TRUE +COMMAND_STR=sh %script_dir%/clovercon.sh set autofire_interval 16 +STATE_STR=sh %script_dir%/clovercon.sh is autofire_interval 16 +PREVIEW_IMAGE=/etc/options_menu/images/preview_autofire_speed.png diff --git a/mod/etc/options_menu/controller/commands/c0020_AutofireXY b/mod/etc/options_menu/controller/commands/c0020_AutofireXY new file mode 100644 index 0000000..88cad77 --- /dev/null +++ b/mod/etc/options_menu/controller/commands/c0020_AutofireXY @@ -0,0 +1,5 @@ +COMMAND_NAME=AUTOFIRE_XY +COMMAND_TYPE=INTERNAL +COMMAND_STR=sh %script_dir%/clovercon.sh toggle autofire_xy +STATE_STR=sh %script_dir%/clovercon.sh state autofire_xy +PREVIEW_IMAGE=/etc/options_menu/images/preview_autofire_xy.png diff --git a/mod/etc/options_menu/controller/commands/c0025_separator b/mod/etc/options_menu/controller/commands/c0025_separator new file mode 100644 index 0000000..e69de29 diff --git a/mod/etc/options_menu/controller/commands/c0030_HomeCombo b/mod/etc/options_menu/controller/commands/c0030_HomeCombo new file mode 100644 index 0000000..3181d4b --- /dev/null +++ b/mod/etc/options_menu/controller/commands/c0030_HomeCombo @@ -0,0 +1,5 @@ +COMMAND_NAME=HOME_COMBO +COMMAND_TYPE=INTERNAL +COMMAND_STR=sh %script_dir%/clovercon.sh home toggle +STATE_STR=sh %script_dir%/clovercon.sh home state +PREVIEW_IMAGE=/etc/options_menu/images/preview_home_combo.png diff --git a/mod/etc/options_menu/controller/commands/c0040_FcStart b/mod/etc/options_menu/controller/commands/c0040_FcStart new file mode 100644 index 0000000..59b8df5 --- /dev/null +++ b/mod/etc/options_menu/controller/commands/c0040_FcStart @@ -0,0 +1,6 @@ +COMMAND_NAME=FC_START +COMMAND_TYPE=INTERNAL +ENABLE_IF=[ "$sftype" = "nes" ] && [ "$sfregion" = "jpn" ] +COMMAND_STR=sh %script_dir%/clovercon.sh toggle fc_start +STATE_STR=sh %script_dir%/clovercon.sh state fc_start +PREVIEW_IMAGE=/etc/options_menu/images/preview_fc_start.png diff --git a/mod/etc/options_menu/controller/scripts/clovercon.sh b/mod/etc/options_menu/controller/scripts/clovercon.sh new file mode 100644 index 0000000..da41e65 --- /dev/null +++ b/mod/etc/options_menu/controller/scripts/clovercon.sh @@ -0,0 +1,78 @@ +#!/bin/sh +# Copyright (C) 2026 DefKorns (https://defkorns.github.io/LICENSE) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +# clovercon.sh state|toggle +# clovercon.sh is|set +# clovercon.sh home state|toggle +# Saves cfg_clovercon_ and applies it live through sysfs. +source /etc/preinit +script_init + +home_disabled="0x7FFF" +home_default="0x0024" +home_prev_file="$mountpoint/etc/options_menu/controller/home_combination.prev" + +get_cfg() { + eval "echo \"\$cfg_clovercon_$1\"" +} + +apply() { + eval "cfg_clovercon_$1='$2'" + save_config + param_file="/sys/module/clovercon/parameters/$1" + [ -w "$param_file" ] && echo "$2" > "$param_file" +} + +on_off() { + if [ "$1" = "y" ]; then echo "on"; else echo "off"; fi +} + +home_enabled() { + current="$(get_cfg home_combination)" + [ -n "$current" ] && [ "$(printf '%d' "$current")" != "$(printf '%d' "$home_disabled")" ] +} + +case "$1" in +state) + [ "$(get_cfg "$2")" = "1" ] && on_off y || on_off n + ;; +toggle) + if [ "$(get_cfg "$2")" = "1" ]; then apply "$2" 0; else apply "$2" 1; fi + ;; +is) + [ "$(get_cfg "$2")" = "$3" ] && on_off y || on_off n + ;; +set) + apply "$2" "$3" + ;; +home) + case "$2" in + state) + home_enabled && on_off y || on_off n + ;; + toggle) + if home_enabled; then + mkdir -p "$(dirname "$home_prev_file")" + get_cfg home_combination > "$home_prev_file" + apply home_combination "$home_disabled" + else + prev="$(cat "$home_prev_file" 2>/dev/null)" + apply home_combination "${prev:-$home_default}" + fi + ;; + esac + ;; +esac diff --git a/mod/etc/options_menu/diagnostics/commands/c0010_SystemInfo b/mod/etc/options_menu/diagnostics/commands/c0010_SystemInfo new file mode 100644 index 0000000..89dd329 --- /dev/null +++ b/mod/etc/options_menu/diagnostics/commands/c0010_SystemInfo @@ -0,0 +1,4 @@ +COMMAND_NAME=SYSTEM_INFO +COMMAND_TYPE=INTERNAL +COMMAND_STR=sh %script_dir%/system_info.sh +PREVIEW_IMAGE=/etc/options_menu/images/preview_system_info.png diff --git a/mod/etc/options_menu/advanced_commands/c0060_DisplayTemp b/mod/etc/options_menu/diagnostics/commands/c0020_DisplayTemp similarity index 100% rename from mod/etc/options_menu/advanced_commands/c0060_DisplayTemp rename to mod/etc/options_menu/diagnostics/commands/c0020_DisplayTemp diff --git a/mod/etc/options_menu/advanced_commands/c0070_DisplayTop b/mod/etc/options_menu/diagnostics/commands/c0030_DisplayTop similarity index 100% rename from mod/etc/options_menu/advanced_commands/c0070_DisplayTop rename to mod/etc/options_menu/diagnostics/commands/c0030_DisplayTop diff --git a/mod/etc/options_menu/advanced_commands/c0080_BenchMark b/mod/etc/options_menu/diagnostics/commands/c0040_BenchMark similarity index 100% rename from mod/etc/options_menu/advanced_commands/c0080_BenchMark rename to mod/etc/options_menu/diagnostics/commands/c0040_BenchMark diff --git a/mod/etc/options_menu/diagnostics/commands/c0045_separator b/mod/etc/options_menu/diagnostics/commands/c0045_separator new file mode 100644 index 0000000..e69de29 diff --git a/mod/etc/options_menu/advanced_commands/c0090_RADebug b/mod/etc/options_menu/diagnostics/commands/c0050_RADebug similarity index 100% rename from mod/etc/options_menu/advanced_commands/c0090_RADebug rename to mod/etc/options_menu/diagnostics/commands/c0050_RADebug diff --git a/mod/etc/options_menu/advanced_commands/c0100_DumpFile b/mod/etc/options_menu/diagnostics/commands/c0060_DumpFile similarity index 100% rename from mod/etc/options_menu/advanced_commands/c0100_DumpFile rename to mod/etc/options_menu/diagnostics/commands/c0060_DumpFile diff --git a/mod/etc/options_menu/diagnostics/commands/c0070_SaveDmesg b/mod/etc/options_menu/diagnostics/commands/c0070_SaveDmesg new file mode 100644 index 0000000..d838fdc --- /dev/null +++ b/mod/etc/options_menu/diagnostics/commands/c0070_SaveDmesg @@ -0,0 +1,5 @@ +COMMAND_NAME=SAVE_DMESG +COMMAND_TYPE=INTERNAL +USB_ONLY=TRUE +COMMAND_STR=sh %script_dir%/save_dmesg.sh +PREVIEW_IMAGE=/etc/options_menu/images/preview_dmesg.png diff --git a/mod/etc/options_menu/images/preview_autofire.png b/mod/etc/options_menu/images/preview_autofire.png new file mode 100644 index 0000000000000000000000000000000000000000..4d92f23c9cdbe5325ec929ac715d21faa9bbbd05 GIT binary patch literal 3089 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 MIk;fT(IobN0GnUQbpQYW literal 0 HcmV?d00001 diff --git a/mod/etc/options_menu/images/preview_autofire_speed.png b/mod/etc/options_menu/images/preview_autofire_speed.png new file mode 100644 index 0000000000000000000000000000000000000000..5af165a27922864332458dfefb0796cf38a36ed4 GIT binary patch literal 4561 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 U_J?QxzG?s~3mfw$Q_rOT0l5^IEC2ui literal 0 HcmV?d00001 diff --git a/mod/etc/options_menu/images/preview_autofire_xy.png b/mod/etc/options_menu/images/preview_autofire_xy.png new file mode 100644 index 0000000000000000000000000000000000000000..2d1f8e059091b6af1c3fdd662aae0e5cf19e7ca0 GIT binary patch literal 5389 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 literal 0 HcmV?d00001 diff --git a/mod/etc/options_menu/images/preview_controller.png b/mod/etc/options_menu/images/preview_controller.png new file mode 100644 index 0000000000000000000000000000000000000000..6a0bc11d591c24313996606c40150d1cee851f15 GIT binary patch literal 6009 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 literal 0 HcmV?d00001 diff --git a/mod/etc/options_menu/images/preview_fc_start.png b/mod/etc/options_menu/images/preview_fc_start.png new file mode 100644 index 0000000000000000000000000000000000000000..68868d3543927d7f9a22e8976fc7ba671de8cd14 GIT binary patch literal 5613 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 EAF4m2A^-pY literal 0 HcmV?d00001 diff --git a/mod/etc/options_menu/images/preview_home_combo.png b/mod/etc/options_menu/images/preview_home_combo.png new file mode 100644 index 0000000000000000000000000000000000000000..04a98ec0a5710f6be1335de9b8ba223b71f926b0 GIT binary patch literal 5215 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 TM|u7;4gt8XsZN8|llcDub@vO6 literal 0 HcmV?d00001 diff --git a/mod/etc/options_menu/images/preview_system_info.png b/mod/etc/options_menu/images/preview_system_info.png new file mode 100644 index 0000000000000000000000000000000000000000..588fca0011b141c11c579261c0cf68cf7cee18f4 GIT binary patch literal 3863 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 literal 0 HcmV?d00001 diff --git a/mod/etc/options_menu/language/de-DE.lang b/mod/etc/options_menu/language/de-DE.lang index f1d4da0..58977e6 100644 --- a/mod/etc/options_menu/language/de-DE.lang +++ b/mod/etc/options_menu/language/de-DE.lang @@ -75,3 +75,20 @@ MOD_UNINSTALL_NONE_MARKED=Kein Modul markiert. MOD_UNINSTALL_NONE_HINT=Drücke A auf einem Modul, um es zu markieren. MOD_UNINSTALL_EMPTY=Keine Module installiert. MOD_UNINSTALL_REBOOT_NOTE=Die Konsole startet zum Abschluss neu. +CONTROLLER_OPTIONS=Controller +AUTOFIRE=Dauerfeuer +AUTOFIRE_SPEED_FAST=Schnell +AUTOFIRE_SPEED_NORMAL=Normal +AUTOFIRE_SPEED_SLOW=Langsam +AUTOFIRE_XY=X/Y als Turbo-A/B verwenden +HOME_COMBO=Tastenkombination fürs Hauptmenü +FC_START=Start am 2. Controller +EPILEPSY_PROTECTION=Epilepsieschutz (NES) +SYSTEM_INFO=Systeminformationen +SAVE_DMESG=Kernel-Log speichern (auf USB) +SAVE_DMESG_DONE=Kernel-Log gespeichert unter USB:/data/log/dmesg.log +SYSINFO_CONSOLE=Konsole +SYSINFO_FIRMWARE=Firmware +SYSINFO_SAVES=Spielstände +SYSINFO_FREE=frei +DIAGNOSTICS_OPTIONS=Diagnose diff --git a/mod/etc/options_menu/language/en-US.lang b/mod/etc/options_menu/language/en-US.lang index 781c9be..8867aa8 100644 --- a/mod/etc/options_menu/language/en-US.lang +++ b/mod/etc/options_menu/language/en-US.lang @@ -76,3 +76,20 @@ MOD_UNINSTALL_NONE_MARKED=No modules marked. MOD_UNINSTALL_NONE_HINT=Press A on a module to mark it. MOD_UNINSTALL_EMPTY=No modules installed. MOD_UNINSTALL_REBOOT_NOTE=The console reboots to finish. +CONTROLLER_OPTIONS=Controller +AUTOFIRE=Autofire +AUTOFIRE_SPEED_FAST=Fast speed +AUTOFIRE_SPEED_NORMAL=Normal speed +AUTOFIRE_SPEED_SLOW=Slow speed +AUTOFIRE_XY=Use X/Y as turbo A/B +HOME_COMBO=Home menu button combo +FC_START=Start on 2nd controller +EPILEPSY_PROTECTION=Epilepsy protection (NES) +SYSTEM_INFO=System Information +SAVE_DMESG=Save kernel log (to USB) +SAVE_DMESG_DONE=Kernel log saved to USB:/data/log/dmesg.log +SYSINFO_CONSOLE=Console +SYSINFO_FIRMWARE=Firmware +SYSINFO_SAVES=Saves +SYSINFO_FREE=free +DIAGNOSTICS_OPTIONS=Diagnostics diff --git a/mod/etc/options_menu/language/es-ES.lang b/mod/etc/options_menu/language/es-ES.lang index 85e880b..ca66605 100644 --- a/mod/etc/options_menu/language/es-ES.lang +++ b/mod/etc/options_menu/language/es-ES.lang @@ -75,3 +75,20 @@ MOD_UNINSTALL_NONE_MARKED=Ningún módulo marcado. MOD_UNINSTALL_NONE_HINT=Pulsa A en un módulo para marcarlo. MOD_UNINSTALL_EMPTY=No hay módulos instalados. MOD_UNINSTALL_REBOOT_NOTE=La consola se reinicia para terminar. +CONTROLLER_OPTIONS=Mando +AUTOFIRE=Autodisparo +AUTOFIRE_SPEED_FAST=Velocidad rápida +AUTOFIRE_SPEED_NORMAL=Velocidad normal +AUTOFIRE_SPEED_SLOW=Velocidad lenta +AUTOFIRE_XY=Usar X/Y como turbo A/B +HOME_COMBO=Combo para el menú de inicio +FC_START=Start en el 2.º mando +EPILEPSY_PROTECTION=Protección contra epilepsia (NES) +SYSTEM_INFO=Información del sistema +SAVE_DMESG=Guardar log del kernel (en USB) +SAVE_DMESG_DONE=Log del kernel guardado en USB:/data/log/dmesg.log +SYSINFO_CONSOLE=Consola +SYSINFO_FIRMWARE=Firmware +SYSINFO_SAVES=Partidas +SYSINFO_FREE=libres +DIAGNOSTICS_OPTIONS=Diagnóstico diff --git a/mod/etc/options_menu/language/fr-FR.lang b/mod/etc/options_menu/language/fr-FR.lang index 557bd66..3dddcb5 100644 --- a/mod/etc/options_menu/language/fr-FR.lang +++ b/mod/etc/options_menu/language/fr-FR.lang @@ -75,3 +75,20 @@ MOD_UNINSTALL_NONE_MARKED=Aucun module coché. MOD_UNINSTALL_NONE_HINT=Appuie sur A sur un module pour le cocher. MOD_UNINSTALL_EMPTY=Aucun module installé. MOD_UNINSTALL_REBOOT_NOTE=La console redémarre pour terminer. +CONTROLLER_OPTIONS=Manette +AUTOFIRE=Tir automatique +AUTOFIRE_SPEED_FAST=Vitesse rapide +AUTOFIRE_SPEED_NORMAL=Vitesse normale +AUTOFIRE_SPEED_SLOW=Vitesse lente +AUTOFIRE_XY=Utiliser X/Y comme turbo A/B +HOME_COMBO=Combo pour le menu d'accueil +FC_START=Start sur la 2e manette +EPILEPSY_PROTECTION=Protection contre l'épilepsie (NES) +SYSTEM_INFO=Informations système +SAVE_DMESG=Enregistrer le log du noyau (sur USB) +SAVE_DMESG_DONE=Log du noyau enregistré sur USB:/data/log/dmesg.log +SYSINFO_CONSOLE=Console +SYSINFO_FIRMWARE=Firmware +SYSINFO_SAVES=Sauvegardes +SYSINFO_FREE=libres +DIAGNOSTICS_OPTIONS=Diagnostic diff --git a/mod/etc/options_menu/language/it-IT.lang b/mod/etc/options_menu/language/it-IT.lang index 92065d1..62fddd3 100644 --- a/mod/etc/options_menu/language/it-IT.lang +++ b/mod/etc/options_menu/language/it-IT.lang @@ -75,3 +75,20 @@ MOD_UNINSTALL_NONE_MARKED=Nessun modulo selezionato. MOD_UNINSTALL_NONE_HINT=Premi A su un modulo per selezionarlo. MOD_UNINSTALL_EMPTY=Nessun modulo installato. MOD_UNINSTALL_REBOOT_NOTE=La console si riavvia per completare. +CONTROLLER_OPTIONS=Controller +AUTOFIRE=Fuoco automatico +AUTOFIRE_SPEED_FAST=Velocità rapida +AUTOFIRE_SPEED_NORMAL=Velocità normale +AUTOFIRE_SPEED_SLOW=Velocità lenta +AUTOFIRE_XY=Usa X/Y come turbo A/B +HOME_COMBO=Combo per il menu principale +FC_START=Start sul 2° controller +EPILEPSY_PROTECTION=Protezione epilessia (NES) +SYSTEM_INFO=Informazioni di sistema +SAVE_DMESG=Salva log del kernel (su USB) +SAVE_DMESG_DONE=Log del kernel salvato in USB:/data/log/dmesg.log +SYSINFO_CONSOLE=Console +SYSINFO_FIRMWARE=Firmware +SYSINFO_SAVES=Salvataggi +SYSINFO_FREE=liberi +DIAGNOSTICS_OPTIONS=Diagnostica diff --git a/mod/etc/options_menu/language/ja-JP.lang b/mod/etc/options_menu/language/ja-JP.lang index b537c65..a8379ed 100644 --- a/mod/etc/options_menu/language/ja-JP.lang +++ b/mod/etc/options_menu/language/ja-JP.lang @@ -75,3 +75,20 @@ MOD_UNINSTALL_NONE_MARKED=マークしたModuleはありません MOD_UNINSTALL_NONE_HINT=ModuleでAをおすとマークします MOD_UNINSTALL_EMPTY=さくじょできるModuleはありません MOD_UNINSTALL_REBOOT_NOTE=かんりょうするとさいきどうします +CONTROLLER_OPTIONS=コントローラー +AUTOFIRE=れんしゃ +AUTOFIRE_SPEED_FAST=はやい +AUTOFIRE_SPEED_NORMAL=ふつう +AUTOFIRE_SPEED_SLOW=おそい +AUTOFIRE_XY=X/YをA/Bのれんしゃにする +HOME_COMBO=ホームメニューへのButtonくみあわせ +FC_START=2PコントローラーでStart +EPILEPSY_PROTECTION=てんかんたいさく (NES) +SYSTEM_INFO=システムじょうほう +SAVE_DMESG=KernelのLogをほぞん (USBへ) +SAVE_DMESG_DONE=KernelのLogをUSB:/data/log/dmesg.logにほぞんしました +SYSINFO_CONSOLE=ほんたい +SYSINFO_FIRMWARE=Firmware +SYSINFO_SAVES=セーブデータ +SYSINFO_FREE=あき +DIAGNOSTICS_OPTIONS=しんだん diff --git a/mod/etc/options_menu/language/nl-NL.lang b/mod/etc/options_menu/language/nl-NL.lang index f996878..9535c7f 100644 --- a/mod/etc/options_menu/language/nl-NL.lang +++ b/mod/etc/options_menu/language/nl-NL.lang @@ -75,3 +75,20 @@ MOD_UNINSTALL_NONE_MARKED=Geen modules gemarkeerd. MOD_UNINSTALL_NONE_HINT=Druk op A bij een module om deze te markeren. MOD_UNINSTALL_EMPTY=Geen modules geïnstalleerd. MOD_UNINSTALL_REBOOT_NOTE=De console herstart om af te ronden. +CONTROLLER_OPTIONS=Controller +AUTOFIRE=Autofire +AUTOFIRE_SPEED_FAST=Snel +AUTOFIRE_SPEED_NORMAL=Normaal +AUTOFIRE_SPEED_SLOW=Langzaam +AUTOFIRE_XY=X/Y als turbo-A/B gebruiken +HOME_COMBO=Knoppencombinatie voor het hoofdmenu +FC_START=Start op 2e controller +EPILEPSY_PROTECTION=Epilepsiebescherming (NES) +SYSTEM_INFO=Systeeminformatie +SAVE_DMESG=Kernellog opslaan (op USB) +SAVE_DMESG_DONE=Kernellog opgeslagen in USB:/data/log/dmesg.log +SYSINFO_CONSOLE=Console +SYSINFO_FIRMWARE=Firmware +SYSINFO_SAVES=Opslagbestanden +SYSINFO_FREE=vrij +DIAGNOSTICS_OPTIONS=Diagnose diff --git a/mod/etc/options_menu/language/pt-PT.lang b/mod/etc/options_menu/language/pt-PT.lang index 20937f0..ece3316 100644 --- a/mod/etc/options_menu/language/pt-PT.lang +++ b/mod/etc/options_menu/language/pt-PT.lang @@ -75,3 +75,20 @@ MOD_UNINSTALL_NONE_MARKED=Nenhum módulo marcado. MOD_UNINSTALL_NONE_HINT=Prime A num módulo para o marcar. MOD_UNINSTALL_EMPTY=Nenhum módulo instalado. MOD_UNINSTALL_REBOOT_NOTE=A consola reinicia para concluir. +CONTROLLER_OPTIONS=Comando +AUTOFIRE=Autofire +AUTOFIRE_SPEED_FAST=Velocidade rápida +AUTOFIRE_SPEED_NORMAL=Velocidade normal +AUTOFIRE_SPEED_SLOW=Velocidade lenta +AUTOFIRE_XY=Usar X/Y como turbo A/B +HOME_COMBO=Combo para o menu inicial +FC_START=Start no 2.º comando +EPILEPSY_PROTECTION=Proteção contra epilepsia (NES) +SYSTEM_INFO=Informação do sistema +SAVE_DMESG=Guardar log do kernel (no USB) +SAVE_DMESG_DONE=Log do kernel guardado em USB:/data/log/dmesg.log +SYSINFO_CONSOLE=Consola +SYSINFO_FIRMWARE=Firmware +SYSINFO_SAVES=Saves +SYSINFO_FREE=livres +DIAGNOSTICS_OPTIONS=Diagnóstico diff --git a/mod/etc/options_menu/language/ru-RU.lang b/mod/etc/options_menu/language/ru-RU.lang index 36eecf7..e9d63fc 100644 --- a/mod/etc/options_menu/language/ru-RU.lang +++ b/mod/etc/options_menu/language/ru-RU.lang @@ -75,3 +75,20 @@ MOD_UNINSTALL_NONE_MARKED=Нет отмеченных модулей. MOD_UNINSTALL_NONE_HINT=Нажмите A на модуле, чтобы отметить его. MOD_UNINSTALL_EMPTY=Нет установленных модулей. MOD_UNINSTALL_REBOOT_NOTE=Консоль перезагрузится для завершения. +CONTROLLER_OPTIONS=Контроллер +AUTOFIRE=Автоогонь +AUTOFIRE_SPEED_FAST=Быстрая скорость +AUTOFIRE_SPEED_NORMAL=Обычная скорость +AUTOFIRE_SPEED_SLOW=Медленная скорость +AUTOFIRE_XY=X/Y как турбо A/B +HOME_COMBO=Комбинация для главного меню +FC_START=Start на 2-м контроллере +EPILEPSY_PROTECTION=Защита от эпилепсии (NES) +SYSTEM_INFO=Информация о системе +SAVE_DMESG=Сохранить лог ядра (на USB) +SAVE_DMESG_DONE=Лог ядра сохранён в USB:/data/log/dmesg.log +SYSINFO_CONSOLE=Консоль +SYSINFO_FIRMWARE=Прошивка +SYSINFO_SAVES=Сохранения +SYSINFO_FREE=свободно +DIAGNOSTICS_OPTIONS=Диагностика diff --git a/mod/etc/options_menu/mod_uninstall/launch_uninstaller.sh b/mod/etc/options_menu/mod_uninstall/launch_uninstaller.sh index 59012a7..5c575eb 100644 --- a/mod/etc/options_menu/mod_uninstall/launch_uninstaller.sh +++ b/mod/etc/options_menu/mod_uninstall/launch_uninstaller.sh @@ -14,8 +14,7 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . # -# mod_uninstall exit codes: 0 = back to Advanced Options, 2 = uninstall -# started (FinishUninstall.sh reboots), anything else = leave the menu. +# mod_uninstall exit: 0 = back, 2 = uninstalling (reboots), else leave menu optionsPath="${1%/}" "$optionsPath/mod_uninstall/mod_uninstall" diff --git a/mod/etc/options_menu/scripts/armet.sh b/mod/etc/options_menu/scripts/armet.sh new file mode 100644 index 0000000..4282265 --- /dev/null +++ b/mod/etc/options_menu/scripts/armet.sh @@ -0,0 +1,30 @@ +#!/bin/sh +# Copyright (C) 2026 DefKorns (https://defkorns.github.io/LICENSE) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +# armet.sh state|toggle +# protection is on unless cfg_disable_armet=y (clover-kachikachi-wr) +source /etc/preinit +script_init + +case "$1" in +state) + if [ "$cfg_disable_armet" = "y" ]; then echo "off"; else echo "on"; fi + ;; +toggle) + if [ "$cfg_disable_armet" = "y" ]; then cfg_disable_armet='n'; else cfg_disable_armet='y'; fi + save_config + ;; +esac diff --git a/mod/etc/options_menu/scripts/save_dmesg.sh b/mod/etc/options_menu/scripts/save_dmesg.sh new file mode 100644 index 0000000..24b0abe --- /dev/null +++ b/mod/etc/options_menu/scripts/save_dmesg.sh @@ -0,0 +1,25 @@ +#!/bin/sh +# Copyright (C) 2026 DefKorns (https://defkorns.github.io/LICENSE) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +# shellcheck source=/dev/null +source /etc/preinit +script_init +source "$mountpoint/etc/options_menu/scripts/om_functions" + +log_dir="$mountpoint/media/data/log" +mkdir -pm 777 "$log_dir" +dmesg > "$log_dir/dmesg.log" +translate SAVE_DMESG_DONE diff --git a/mod/etc/options_menu/scripts/system_info.sh b/mod/etc/options_menu/scripts/system_info.sh new file mode 100644 index 0000000..9ec8972 --- /dev/null +++ b/mod/etc/options_menu/scripts/system_info.sh @@ -0,0 +1,37 @@ +#!/bin/sh +# Copyright (C) 2026 DefKorns (https://defkorns.github.io/LICENSE) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +# shellcheck source=/dev/null +source /etc/preinit +script_init +source "$mountpoint/etc/options_menu/scripts/om_functions" + +disk_usage() { + df -h "$1" 2>/dev/null | tail -1 | awk -v free="$(translate SYSINFO_FREE)" '{ print $3 " / " $2 " (" $4 " " free ")" }' +} + +[ -f /var/version ] && source /var/version + +echo "$(translate SYSINFO_CONSOLE): $sftype-$sfregion" +echo "hakchi: $hakchiVersion" +echo "Kernel: $kernelVersion" +echo "Boot: $bootVersion" +echo "$(translate SYSINFO_FIRMWARE): $(hakchi currentFirmware 2>/dev/null | sed "s/^_nand_$/NAND/")" +echo " " +echo "NAND: $(disk_usage "$mountpoint/var/lib")" +[ -d "$usb_path" ] && echo "USB/SD: $(disk_usage "$usb_path")" +saves="$(readlink -f /var/saves)" +[ -d "$saves" ] && echo "$(translate SYSINFO_SAVES): $(du -sh "$saves" 2>/dev/null | cut -f1)" diff --git a/mod/install b/mod/install index a20b77c..9a0dc36 100644 --- a/mod/install +++ b/mod/install @@ -1,14 +1,14 @@ #!/bin/sh # shellcheck disable=SC2154 omRoot="$rootfs/etc/options_menu" -omSettings="button.cfg language.cfg" +omSettings="button.cfg language.cfg controller/home_combination.prev" omBackup="$temppath/om_settings" # Uninstall any previous or legacy install first to ensure a clean installation rm -rf "$omBackup" mkdir -p "$omBackup" for omFile in $omSettings; do - [ -f "$omRoot/$omFile" ] && cp -p "$omRoot/$omFile" "$omBackup/" + [ -f "$omRoot/$omFile" ] && mkdir -p "$(dirname "$omBackup/$omFile")" && cp -p "$omRoot/$omFile" "$omBackup/$omFile" done for omUnfile in "$installpath/hmod/uninstall-options_deluxe"* \ "$installpath/hmod/uninstall-options_menu_"* \ @@ -20,7 +20,7 @@ cd "$transferpath" || return 1 transfer_default for omFile in $omSettings; do - [ -f "$omBackup/$omFile" ] && cp -p "$omBackup/$omFile" "$omRoot/" + [ -f "$omBackup/$omFile" ] && mkdir -p "$(dirname "$omRoot/$omFile")" && cp -p "$omBackup/$omFile" "$omRoot/$omFile" done rm -rf "$omBackup" diff --git a/mod/uninstall b/mod/uninstall index 224deaf..5d27e0f 100644 --- a/mod/uninstall +++ b/mod/uninstall @@ -3,6 +3,7 @@ rm -f "$rootfs/etc/options_menu/button.cfg" rm -f "$rootfs/etc/options_menu/language.cfg" rm -f "$rootfs/etc/options_menu/ui_style.cfg" +rm -f "$rootfs/etc/options_menu/controller/home_combination.prev" rm -f "$rootfs/etc/options_menu/network/restore/commands"/c0001_* rm -f "$rootfs/etc/options_menu/network/commands"/_* find "$rootfs/etc/options_menu" -depth -type d -empty -delete 2>/dev/null From ec26204595e651b00ec8375338ef87f6f3505aa0 Mon Sep 17 00:00:00 2001 From: DefKorns Date: Tue, 6 Oct 2026 17:02:12 +0000 Subject: [PATCH 061/104] Hide the RetroArch menu when RetroArch isn't installed --- mod/etc/options_menu/commands/c0060_RetroarchOptions | 1 + 1 file changed, 1 insertion(+) diff --git a/mod/etc/options_menu/commands/c0060_RetroarchOptions b/mod/etc/options_menu/commands/c0060_RetroarchOptions index b361e00..20fc8e5 100644 --- a/mod/etc/options_menu/commands/c0060_RetroarchOptions +++ b/mod/etc/options_menu/commands/c0060_RetroarchOptions @@ -2,5 +2,6 @@ COMMAND_NAME=RETROARCH_OPTIONS COMMAND_TYPE=EXTERNAL RESTART_UI=FALSE SUBMENU=TRUE +ENABLE_IF=[ -e /bin/retroarch ] || [ -e /bin/retroarch-clover ] COMMAND_STR=sh %script_dir%/om_retroarch PREVIEW_IMAGE=/etc/options_menu/images/preview_retroarch.png From dff1bf8ae806b93f07230ecf848ac753030c85f5 Mon Sep 17 00:00:00 2001 From: DefKorns Date: Tue, 6 Oct 2026 17:02:41 +0000 Subject: [PATCH 062/104] Rename the boot cleanup script and remove stale RetroArch defaults --- ...eanExtraCovers => S8104OptionsMenuCleanup} | 21 +++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) rename mod/etc/init.d/{S8104CleanExtraCovers => S8104OptionsMenuCleanup} (71%) diff --git a/mod/etc/init.d/S8104CleanExtraCovers b/mod/etc/init.d/S8104OptionsMenuCleanup similarity index 71% rename from mod/etc/init.d/S8104CleanExtraCovers rename to mod/etc/init.d/S8104OptionsMenuCleanup index cf6d341..2aa5d00 100644 --- a/mod/etc/init.d/S8104CleanExtraCovers +++ b/mod/etc/init.d/S8104OptionsMenuCleanup @@ -14,16 +14,25 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . # -# Removes the Mega Drive Mini / original-size covers hakchi syncs into every -# game folder; the NES/SNES Mini home menu only reads .png and _small.png. +# the home menu only reads .png and _small.png +clean_extra_covers() { + gStorage="$(findGameStorage)" + [ -d "$gStorage" ] || return 0 + find "$gStorage" -type f \( -name "*_mdmini.png" -o -name "*_original.png" \) -exec rm -f {} + +} + +# left by RetroArch hmods' install, missed by their uninstall +clean_retroarch_defaults() { + [ -e /bin/retroarch ] || [ -e /bin/retroarch-clover ] && return 0 + rm -rf "$mountpoint/etc/options_menu/retroarch/default_files" +} clean() { source /etc/preinit script_init - gStorage="$(findGameStorage)" - [ -d "$gStorage" ] || return 0 - find "$gStorage" -type f \( -name "*_mdmini.png" -o -name "*_original.png" \) -exec rm -f {} + + clean_extra_covers + clean_retroarch_defaults } case "$1" in @@ -33,7 +42,7 @@ start) stop|restart) ;; *) - echo "CleanExtraCovers: Please use start." + echo "OptionsMenuCleanup: Please use start." exit 1 ;; esac From fbdc5be95c07d7885fb60c71560b26d27c86425a Mon Sep 17 00:00:00 2001 From: DefKorns Date: Tue, 6 Oct 2026 17:27:55 +0000 Subject: [PATCH 063/104] Apply snake_case/camelCase naming to every OptionsMenu shell script Variables are snake_case and functions camelCase, with no name shared with hakchi's preinit: - om_functions: options_menu, om_commands, om_scripts, om_images, game_storage, mod_state_path (hakchi has a local state_path), and enableCmds/disableCmds toggling $enable_/$disable_; translate/remove/the toggles keep their variables local - standby: hibernate, standby, resume, displayMenu, hibernateReboot, reloadImagePayload, uiKill, uiList (standby_watchdog calls them by name, updated to match) - network: backupWifi/restoreWifi, om_network*, om_wifi_restore*, enable_restore_usb/nand, dst -> backup_dest (clashed with hakchi) - retroarch: backupToNand/backupToUsb, raConfig -> ra_config, RADebug's RA_CONFIG -> ra_base_dir - install, launch_uninstaller.sh, show-my-ip.sh: snake_case variables Plugins must move to the new om_functions names --- mod/bin/standby | 46 +++++------ mod/etc/init.d/S8104OptionsMenuCleanup | 14 ++-- .../controller/scripts/clovercon.sh | 20 ++--- .../mod_uninstall/launch_uninstaller.sh | 8 +- .../network/scripts/backup-wifi.sh | 14 ++-- mod/etc/options_menu/network/scripts/om_vars | 22 +++--- .../options_menu/network/scripts/reconnect.sh | 6 +- .../network/scripts/restore_settings.sh | 12 +-- .../network/scripts/show-my-ip.sh | 4 +- .../retroarch/scripts/backup-nand.sh | 6 +- .../retroarch/scripts/backup-usb.sh | 6 +- .../retroarch/scripts/default-settings-all.sh | 4 +- .../retroarch/scripts/delete-overrides.sh | 6 +- .../retroarch/scripts/delete-remaps.sh | 2 +- .../options_menu/retroarch/scripts/ra_vars | 4 +- .../retroarch/scripts/restore-nand.sh | 4 +- .../retroarch/scripts/restore-usb.sh | 4 +- mod/etc/options_menu/scripts/RADebug.sh | 6 +- mod/etc/options_menu/scripts/om_functions | 78 ++++++++++--------- mod/etc/options_menu/scripts/om_network | 2 +- mod/etc/options_menu/scripts/om_retroarch | 2 +- mod/etc/options_menu/scripts/system_info.sh | 6 +- mod/install | 24 +++--- src/standby_watchdog.cpp | 14 ++-- 24 files changed, 161 insertions(+), 153 deletions(-) diff --git a/mod/bin/standby b/mod/bin/standby index e2baf89..2e26b35 100644 --- a/mod/bin/standby +++ b/mod/bin/standby @@ -3,7 +3,7 @@ source /etc/preinit script_init -Hibernate() +hibernate() { xz -dc "$rootfs/etc/hibernate_mod/hibernating.png.xz" > /dev/fb0 echo "Initiating shutdown followed by hibernate mode..." @@ -13,7 +13,7 @@ Hibernate() echo "Hibernation mode active! Awaiting input..." } -Standby() +standby() { xz -dc "$rootfs/etc/hibernate_mod/hibernating.png.xz" > /dev/fb0 /etc/init.d/S8103OptionsMenu stop @@ -22,51 +22,51 @@ Standby() killall udevd modprobe -r clvcon killall -9 clover-mcp - Uikill TERM + uiKill TERM echo 0 > /sys/devices/virtual/disp/disp/attr/lcd [ -b '/dev/sda1' ] && umount /dev/sda1 echo "Initialising standby mode..." #echo "Core Temperature at the time of standby: $(hakchi hwmon)c" - echo "Standby mode active! Awaiting input..." + echo "standby mode active! Awaiting input..." } #UI Commands from latest hakchi version -Uilist(){ +uiList(){ lsof -n | grep -F "/dev/fb0" | grep -v "standby" | grep -v "busybox" | awk '{print $1}' | sort -u } -Uikill(){ +uiKill(){ [ -z "$1" ] && return 1 - Uilist | xargs -r kill -s "$1" + uiList | xargs -r kill -s "$1" } -DisplayMenu() +displayMenu() { - Uikill STOP + uiKill STOP #xz -dc "$rootfs/etc/hibernate_mod/1.png.xz" > /dev/fb0 } -Resume() +resume() { rm -f /tmp/power_menu_screen - Uikill CONT + uiKill CONT } -HibernateReboot() +hibernateReboot() { echo "Shutting down console out from hibernation mode..." - Resume + resume sync reboot } -ReloadImagePayload() +reloadImagePayload() { - payloadCount=$(find "$rootfs/etc/hibernate_mod/" -type f -name *.png.xz | wc -l) - #echo $payloadCount - payloadCount=$(($payloadCount-1)) #Deduct 1 to account for the hibernate.png.xz - #echo $payloadCount - if [ $payloadCount = 1 ]; then #lite version in play... + payload_count=$(find "$rootfs/etc/hibernate_mod/" -type f -name *.png.xz | wc -l) + #echo $payload_count + payload_count=$(($payload_count-1)) #Deduct 1 to account for the hibernate.png.xz + #echo $payload_count + if [ $payload_count = 1 ]; then #lite version in play... #echo "Lite version in play" if [ ! -f /tmp/power_menu_screen ]; then #echo "File doesn't exist" @@ -74,14 +74,14 @@ ReloadImagePayload() fi else #echo "Deluxe version in play" - DIFF=$(($payloadCount-1+1)) + image_range=$(($payload_count-1+1)) RANDOM=$$ - selectedImage="$rootfs/etc/hibernate_mod/"$(($(($RANDOM%$DIFF))+1))".png.xz" - #echo $selectedImage + selected_image="$rootfs/etc/hibernate_mod/"$(($(($RANDOM%$image_range))+1))".png.xz" + #echo $selected_image rm -f /tmp/power_menu_screen #Just incase - xz -dc $selectedImage > /tmp/power_menu_screen + xz -dc $selected_image > /tmp/power_menu_screen fi } diff --git a/mod/etc/init.d/S8104OptionsMenuCleanup b/mod/etc/init.d/S8104OptionsMenuCleanup index 2aa5d00..9b95950 100644 --- a/mod/etc/init.d/S8104OptionsMenuCleanup +++ b/mod/etc/init.d/S8104OptionsMenuCleanup @@ -15,14 +15,14 @@ # along with this program. If not, see . # # the home menu only reads .png and _small.png -clean_extra_covers() { - gStorage="$(findGameStorage)" - [ -d "$gStorage" ] || return 0 - find "$gStorage" -type f \( -name "*_mdmini.png" -o -name "*_original.png" \) -exec rm -f {} + +cleanExtraCovers() { + game_storage="$(findGameStorage)" + [ -d "$game_storage" ] || return 0 + find "$game_storage" -type f \( -name "*_mdmini.png" -o -name "*_original.png" \) -exec rm -f {} + } # left by RetroArch hmods' install, missed by their uninstall -clean_retroarch_defaults() { +cleanRetroarchDefaults() { [ -e /bin/retroarch ] || [ -e /bin/retroarch-clover ] && return 0 rm -rf "$mountpoint/etc/options_menu/retroarch/default_files" } @@ -31,8 +31,8 @@ clean() { source /etc/preinit script_init - clean_extra_covers - clean_retroarch_defaults + cleanExtraCovers + cleanRetroarchDefaults } case "$1" in diff --git a/mod/etc/options_menu/controller/scripts/clovercon.sh b/mod/etc/options_menu/controller/scripts/clovercon.sh index da41e65..e5b0697 100644 --- a/mod/etc/options_menu/controller/scripts/clovercon.sh +++ b/mod/etc/options_menu/controller/scripts/clovercon.sh @@ -25,7 +25,7 @@ home_disabled="0x7FFF" home_default="0x0024" home_prev_file="$mountpoint/etc/options_menu/controller/home_combination.prev" -get_cfg() { +getCfg() { eval "echo \"\$cfg_clovercon_$1\"" } @@ -36,24 +36,24 @@ apply() { [ -w "$param_file" ] && echo "$2" > "$param_file" } -on_off() { +onOff() { if [ "$1" = "y" ]; then echo "on"; else echo "off"; fi } -home_enabled() { - current="$(get_cfg home_combination)" +homeEnabled() { + current="$(getCfg home_combination)" [ -n "$current" ] && [ "$(printf '%d' "$current")" != "$(printf '%d' "$home_disabled")" ] } case "$1" in state) - [ "$(get_cfg "$2")" = "1" ] && on_off y || on_off n + [ "$(getCfg "$2")" = "1" ] && onOff y || onOff n ;; toggle) - if [ "$(get_cfg "$2")" = "1" ]; then apply "$2" 0; else apply "$2" 1; fi + if [ "$(getCfg "$2")" = "1" ]; then apply "$2" 0; else apply "$2" 1; fi ;; is) - [ "$(get_cfg "$2")" = "$3" ] && on_off y || on_off n + [ "$(getCfg "$2")" = "$3" ] && onOff y || onOff n ;; set) apply "$2" "$3" @@ -61,12 +61,12 @@ set) home) case "$2" in state) - home_enabled && on_off y || on_off n + homeEnabled && onOff y || onOff n ;; toggle) - if home_enabled; then + if homeEnabled; then mkdir -p "$(dirname "$home_prev_file")" - get_cfg home_combination > "$home_prev_file" + getCfg home_combination > "$home_prev_file" apply home_combination "$home_disabled" else prev="$(cat "$home_prev_file" 2>/dev/null)" diff --git a/mod/etc/options_menu/mod_uninstall/launch_uninstaller.sh b/mod/etc/options_menu/mod_uninstall/launch_uninstaller.sh index 5c575eb..ded33d7 100644 --- a/mod/etc/options_menu/mod_uninstall/launch_uninstaller.sh +++ b/mod/etc/options_menu/mod_uninstall/launch_uninstaller.sh @@ -15,16 +15,16 @@ # along with this program. If not, see . # # mod_uninstall exit: 0 = back, 2 = uninstalling (reboots), else leave menu -optionsPath="${1%/}" +options_path="${1%/}" -"$optionsPath/mod_uninstall/mod_uninstall" +"$options_path/mod_uninstall/mod_uninstall" case $? in 0) usleep 50000 - "$optionsPath/options" --commandPath "$optionsPath/advanced_commands/" --title "ADVANCED_OPTIONS" + "$options_path/options" --commandPath "$options_path/advanced_commands/" --title "ADVANCED_OPTIONS" ;; 2) ;; *) - sh "$optionsPath/scripts/ResumeUI.sh" + sh "$options_path/scripts/ResumeUI.sh" ;; esac diff --git a/mod/etc/options_menu/network/scripts/backup-wifi.sh b/mod/etc/options_menu/network/scripts/backup-wifi.sh index 3e536e3..fc9bb19 100644 --- a/mod/etc/options_menu/network/scripts/backup-wifi.sh +++ b/mod/etc/options_menu/network/scripts/backup-wifi.sh @@ -28,7 +28,7 @@ ssid_lower="$(echo "$ssid" | tr '[:upper:]' '[:lower:]' | tr -cd 'a-z0-9_.-')" [ "$1" = "nand" ] && backup_path="$rootfs/etc/wifi_backup" network="$backup_path/$ssid_lower" -Backup_Wifi() { +backupWifi() { if [ -d "$backup_path" ]; then [ -d "$network" ] && rm -rf "$network" fi @@ -38,7 +38,7 @@ Backup_Wifi() { } -Restore_Wifi() { +restoreWifi() { if [ -n "$1" ]; then backup_conf="$backup_path/$1/wpa_supplicant.conf" rm -f "$wpa_supplicant_conf" @@ -55,14 +55,14 @@ Restore_Wifi() { case "$2" in backup) - "$omScripts/gen_splash" /tmp/wifi_backup_splash.png WIFI_BACKUP_SPLASH_LINE1 WIFI_BACKUP_SPLASH_LINE2 && decodepng /tmp/wifi_backup_splash.png >/dev/fb0 - Backup_Wifi + "$om_scripts/gen_splash" /tmp/wifi_backup_splash.png WIFI_BACKUP_SPLASH_LINE1 WIFI_BACKUP_SPLASH_LINE2 && decodepng /tmp/wifi_backup_splash.png >/dev/fb0 + backupWifi echo "WIFI config backed up to $network" sleep 1 - sh "$omScripts/om_network" + sh "$om_scripts/om_network" ;; restore) - Restore_Wifi $3 - sh "$omNetworkScripts/reconnect.sh" + restoreWifi $3 + sh "$om_network_scripts/reconnect.sh" ;; esac diff --git a/mod/etc/options_menu/network/scripts/om_vars b/mod/etc/options_menu/network/scripts/om_vars index f1f84b3..50882c8 100644 --- a/mod/etc/options_menu/network/scripts/om_vars +++ b/mod/etc/options_menu/network/scripts/om_vars @@ -19,21 +19,21 @@ source $mountpoint/etc/options_menu/scripts/om_functions script_init -omNetwork="$optionsMenu/network" -omNetworkCmds="$omNetwork/commands" -omNetworkScripts="$omNetwork/scripts" -omWifiRestore="$omNetwork/restore" -omWifiRestoreCmds="$omWifiRestore/commands" -omWifiRestoreScripts="$omWifiRestore/scripts" +om_network="$options_menu/network" +om_network_cmds="$om_network/commands" +om_network_scripts="$om_network/scripts" +om_wifi_restore="$om_network/restore" +om_wifi_restore_cmds="$om_wifi_restore/commands" +om_wifi_restore_scripts="$om_wifi_restore/scripts" usb_backup="/media/data/wifi_backup" nand_backup="$rootfs/etc/wifi_backup" wpa_supplicant="/etc/wpa_supplicant" -disableRestoreUSB="$omNetworkCmds/_RestoreUsb" -disableRestoreNAND="$omNetworkCmds/_RestoreNand" -enableRestoreUSB="$omNetworkCmds/c0060_RestoreUsb" -enableRestoreNAND="$omNetworkCmds/c0050_RestoreNand" +disable_restore_usb="$om_network_cmds/_RestoreUsb" +disable_restore_nand="$om_network_cmds/_RestoreNand" +enable_restore_usb="$om_network_cmds/c0060_RestoreUsb" +enable_restore_nand="$om_network_cmds/c0050_RestoreNand" backup_path="/media/data/wifi_backup" -dst="usb" +backup_dest="usb" diff --git a/mod/etc/options_menu/network/scripts/reconnect.sh b/mod/etc/options_menu/network/scripts/reconnect.sh index ebe20b1..f1eee96 100644 --- a/mod/etc/options_menu/network/scripts/reconnect.sh +++ b/mod/etc/options_menu/network/scripts/reconnect.sh @@ -1,15 +1,17 @@ #!/bin/bash +# shellcheck source=/dev/null +source $mountpoint/etc/options_menu/scripts/om_functions echo "Testing internet connection..." sleep 1 -if nc -zw1 google.com 443; then +if hasInternet; then echo "You are already connected to the internet." else echo "Restarting network. Please wait." /etc/init.d/S92networking restart echo "Testing internet connection..." sleep 5 - if nc -zw1 google.com 443; then + if hasInternet; then echo "Success! You are now connected to the internet." else echo "Connection failed. Please connect your WiFi adapter." diff --git a/mod/etc/options_menu/network/scripts/restore_settings.sh b/mod/etc/options_menu/network/scripts/restore_settings.sh index c860320..e8e2395 100644 --- a/mod/etc/options_menu/network/scripts/restore_settings.sh +++ b/mod/etc/options_menu/network/scripts/restore_settings.sh @@ -21,10 +21,10 @@ script_init if [ "$1" = "nand" ]; then backup_path="$rootfs/etc/wifi_backup" - dst="$1" + backup_dest="$1" fi -find "$omWifiRestoreCmds" -type f -name 'c0001_*' -exec rm {} \; +find "$om_wifi_restore_cmds" -type f -name 'c0001_*' -exec rm {} \; find "$backup_path/." -mindepth 1 -maxdepth 1 -type d -print0 | xargs -0 -n 1 basename | while IFS= read -r wifi_network; do wpa_supplicant_conf="$backup_path/$wifi_network/wpa_supplicant.conf" @@ -35,9 +35,9 @@ find "$backup_path/." -mindepth 1 -maxdepth 1 -type d -print0 | xargs -0 -n 1 ba echo "COMMAND_NAME=$ssid COMMAND_TYPE=INTERNAL RESTART_UI=FALSE -COMMAND_STR=sh $omNetworkScripts/backup-wifi.sh $dst restore $ssid_lower -DELETE_STR=rm -rf \"$backup_path/$wifi_network\" && sh $omNetworkScripts/restore_settings.sh $1 -DELETE_CONFIRM_KEY=DELETE_BACKUP_CONFIRM" >"$omWifiRestoreCmds/c0001_$ssid_lower" +COMMAND_STR=sh $om_network_scripts/backup-wifi.sh $backup_dest restore $ssid_lower +DELETE_STR=rm -rf \"$backup_path/$wifi_network\" && sh $om_network_scripts/restore_settings.sh $1 +DELETE_CONFIRM_KEY=DELETE_BACKUP_CONFIRM" >"$om_wifi_restore_cmds/c0001_$ssid_lower" done -$optionsMenu/options --commandPath $omWifiRestoreCmds/ --scriptPath $omWifiRestoreScripts --title "RESTORE_WIFI_CONFIG" & +$options_menu/options --commandPath $om_wifi_restore_cmds/ --scriptPath $om_wifi_restore_scripts --title "RESTORE_WIFI_CONFIG" & diff --git a/mod/etc/options_menu/network/scripts/show-my-ip.sh b/mod/etc/options_menu/network/scripts/show-my-ip.sh index b17db34..fa37b3c 100644 --- a/mod/etc/options_menu/network/scripts/show-my-ip.sh +++ b/mod/etc/options_menu/network/scripts/show-my-ip.sh @@ -6,11 +6,11 @@ ifconfig | grep "inet " | grep -v 127.0.0.1|awk 'match($0, /([0-9]+\.[0-9]+\.[0- echo "Grabbing External IP... (Might take a bit)" -PUBLIC_IP=$(wget http://ipecho.net/plain -O - -q) +public_ip=$(wget http://ipecho.net/plain -O - -q) if wget -q --tries=10 --timeout=10 --spider http://google.com; then echo "You are online!" - echo "Your external IP is:" "$PUBLIC_IP" + echo "Your external IP is:" "$public_ip" else echo "You are offline" fi diff --git a/mod/etc/options_menu/retroarch/scripts/backup-nand.sh b/mod/etc/options_menu/retroarch/scripts/backup-nand.sh index 6198fda..62977fd 100644 --- a/mod/etc/options_menu/retroarch/scripts/backup-nand.sh +++ b/mod/etc/options_menu/retroarch/scripts/backup-nand.sh @@ -4,7 +4,7 @@ source /etc/preinit script_init source $mountpoint/etc/options_menu/retroarch/scripts/ra_vars -Backup_To_NAND() +backupToNand() { if [ -d "$rootfs/etc/ra_backup" ]; then rm -rf "$rootfs/etc/ra_backup/" @@ -13,7 +13,7 @@ Backup_To_NAND() mkdir -pm 777 "$rootfs/etc/ra_backup" /etc/options_menu/retroarch/scripts/RASettingsBackup "Backup" "/etc/options_menu/retroarch/default_files/retroarch.cfg" "/etc/libretro/retroarch.cfg" "$rootfs/etc/ra_backup/retroarch.cfg" /etc/options_menu/retroarch/scripts/RASettingsBackup "Backup" "/etc/options_menu/retroarch/default_files/retroarch-core-options.cfg" "/etc/libretro/retroarch-core-options.cfg" "$rootfs/etc/ra_backup/retroarch-core-options.cfg" - cp -r "$raConfig" "$rootfs/etc/ra_backup/config" + cp -r "$ra_config" "$rootfs/etc/ra_backup/config" } -Backup_To_NAND && echo "Retroarch config backed up to /etc/ra_backup" +backupToNand && echo "Retroarch config backed up to /etc/ra_backup" diff --git a/mod/etc/options_menu/retroarch/scripts/backup-usb.sh b/mod/etc/options_menu/retroarch/scripts/backup-usb.sh index af10482..a32bcbf 100644 --- a/mod/etc/options_menu/retroarch/scripts/backup-usb.sh +++ b/mod/etc/options_menu/retroarch/scripts/backup-usb.sh @@ -2,7 +2,7 @@ source $mountpoint/etc/options_menu/retroarch/scripts/ra_vars -Backup_To_USB() +backupToUsb() { if [ -d /media/data/ra_backup ]; then rm -rf /media/data/ra_backup/ @@ -11,7 +11,7 @@ Backup_To_USB() mkdir -pm 777 /media/data/ra_backup /etc/options_menu/retroarch/scripts/RASettingsBackup "Backup" "/etc/options_menu/retroarch/default_files/retroarch.cfg" "/etc/libretro/retroarch.cfg" "/media/data/ra_backup/retroarch.cfg" /etc/options_menu/retroarch/scripts/RASettingsBackup "Backup" "/etc/options_menu/retroarch/default_files/retroarch-core-options.cfg" "/etc/libretro/retroarch-core-options.cfg" "/media/data/ra_backup/retroarch-core-options.cfg" - cp -r "$raConfig" /media/data/ra_backup/config + cp -r "$ra_config" /media/data/ra_backup/config } -Backup_To_USB && echo "Retroarch config backed up to USB:/data/ra_backup" +backupToUsb && echo "Retroarch config backed up to USB:/data/ra_backup" diff --git a/mod/etc/options_menu/retroarch/scripts/default-settings-all.sh b/mod/etc/options_menu/retroarch/scripts/default-settings-all.sh index bb4dd9a..ed5c832 100644 --- a/mod/etc/options_menu/retroarch/scripts/default-settings-all.sh +++ b/mod/etc/options_menu/retroarch/scripts/default-settings-all.sh @@ -5,8 +5,8 @@ source $mountpoint/etc/options_menu/retroarch/scripts/ra_vars if [ -f "/etc/options_menu/retroarch/default_files/retroarch.cfg" ]; then cp /etc/options_menu/retroarch/default_files/retroarch.cfg /etc/libretro/ cp /etc/options_menu/retroarch/default_files/retroarch-core-options.cfg /etc/libretro/ - rm -r "${raConfig:?}"/* - mkdir -m 777 "$raConfig/remaps" + rm -r "${ra_config:?}"/* + mkdir -m 777 "$ra_config/remaps" echo "Restored Default Settings." else echo "Cannot locate default configuration files." diff --git a/mod/etc/options_menu/retroarch/scripts/delete-overrides.sh b/mod/etc/options_menu/retroarch/scripts/delete-overrides.sh index 08b6614..1d2c114 100644 --- a/mod/etc/options_menu/retroarch/scripts/delete-overrides.sh +++ b/mod/etc/options_menu/retroarch/scripts/delete-overrides.sh @@ -2,7 +2,7 @@ source $mountpoint/etc/options_menu/retroarch/scripts/ra_vars -mv "$raConfig/remaps" /tmp/ra_remaps -rm -r "${raConfig:?}"/* -mv /tmp/ra_remaps "$raConfig/remaps" +mv "$ra_config/remaps" /tmp/ra_remaps +rm -r "${ra_config:?}"/* +mv /tmp/ra_remaps "$ra_config/remaps" echo "Overrides deleted." diff --git a/mod/etc/options_menu/retroarch/scripts/delete-remaps.sh b/mod/etc/options_menu/retroarch/scripts/delete-remaps.sh index dd7a496..7b55772 100644 --- a/mod/etc/options_menu/retroarch/scripts/delete-remaps.sh +++ b/mod/etc/options_menu/retroarch/scripts/delete-remaps.sh @@ -2,5 +2,5 @@ source $mountpoint/etc/options_menu/retroarch/scripts/ra_vars -rm -r "${raConfig:?}/remaps"/* +rm -r "${ra_config:?}/remaps"/* echo "Remaps deleted." diff --git a/mod/etc/options_menu/retroarch/scripts/ra_vars b/mod/etc/options_menu/retroarch/scripts/ra_vars index c073b8f..1fce6fc 100644 --- a/mod/etc/options_menu/retroarch/scripts/ra_vars +++ b/mod/etc/options_menu/retroarch/scripts/ra_vars @@ -22,7 +22,7 @@ script_init # per-core config/overrides moved from .config/retroarch/config (Neo, discontinued) # to a plain config/ folder (_km Ozone builds) - detect which is actually installed if [ -d "$rootfs/etc/libretro/.config/retroarch/config" ]; then - raConfig="$rootfs/etc/libretro/.config/retroarch/config" + ra_config="$rootfs/etc/libretro/.config/retroarch/config" else - raConfig="$rootfs/etc/libretro/config" + ra_config="$rootfs/etc/libretro/config" fi diff --git a/mod/etc/options_menu/retroarch/scripts/restore-nand.sh b/mod/etc/options_menu/retroarch/scripts/restore-nand.sh index fd7e630..28c7d92 100644 --- a/mod/etc/options_menu/retroarch/scripts/restore-nand.sh +++ b/mod/etc/options_menu/retroarch/scripts/restore-nand.sh @@ -7,7 +7,7 @@ if [ ! -f "/etc/ra_backup/retroarch.cfg" ]; then else /etc/options_menu/retroarch/scripts/RASettingsBackup "Restore" "/etc/options_menu/retroarch/default_files/retroarch.cfg" "/etc/libretro/retroarch.cfg" "$rootfs/etc/ra_backup/retroarch.cfg" /etc/options_menu/retroarch/scripts/RASettingsBackup "Restore" "/etc/options_menu/retroarch/default_files/retroarch-core-options.cfg" "/etc/libretro/retroarch-core-options.cfg" "$rootfs/etc/ra_backup/retroarch-core-options.cfg" - rm -rf "${raConfig:?}" - cp -r /etc/ra_backup/config "$raConfig" + rm -rf "${ra_config:?}" + cp -r /etc/ra_backup/config "$ra_config" echo "Retroarch configs restored." fi diff --git a/mod/etc/options_menu/retroarch/scripts/restore-usb.sh b/mod/etc/options_menu/retroarch/scripts/restore-usb.sh index ba4dc5a..cdb991f 100644 --- a/mod/etc/options_menu/retroarch/scripts/restore-usb.sh +++ b/mod/etc/options_menu/retroarch/scripts/restore-usb.sh @@ -7,7 +7,7 @@ if [ ! -f "/media/data/ra_backup/retroarch.cfg" ]; then else /etc/options_menu/retroarch/scripts/RASettingsBackup "Restore" "/etc/options_menu/retroarch/default_files/retroarch.cfg" "/etc/libretro/retroarch.cfg" "/media/data/ra_backup/retroarch.cfg" /etc/options_menu/retroarch/scripts/RASettingsBackup "Restore" "/etc/options_menu/retroarch/default_files/retroarch-core-options.cfg" "/etc/libretro/retroarch-core-options.cfg" "/media/data/ra_backup/retroarch-core-options.cfg" - rm -rf "${raConfig:?}" - cp -r /media/data/ra_backup/config "$raConfig" + rm -rf "${ra_config:?}" + cp -r /media/data/ra_backup/config "$ra_config" echo "Retroarch configs restored." fi diff --git a/mod/etc/options_menu/scripts/RADebug.sh b/mod/etc/options_menu/scripts/RADebug.sh index e6030ea..f8087ad 100644 --- a/mod/etc/options_menu/scripts/RADebug.sh +++ b/mod/etc/options_menu/scripts/RADebug.sh @@ -12,9 +12,9 @@ rm -rf /media/data/log/RetroArchConfig/* { chmod -R 777 /etc/libretro #Set full permissions for retroarch echo "Dumping retroarch config to log folder..." - RA_CONFIG=/etc/libretro/.config - [ -d "$RA_CONFIG" ] || RA_CONFIG=/etc/libretro/config - cp -r "$RA_CONFIG" /media/data/log/RetroArchConfig + ra_base_dir=/etc/libretro/.config + [ -d "$ra_base_dir" ] || ra_base_dir=/etc/libretro/config + cp -r "$ra_base_dir" /media/data/log/RetroArchConfig cp -r /etc/libretro/retroarch-core-options.cfg /media/data/log/RetroArchConfig cp -r /etc/libretro/retroarch.cfg /media/data/log/RetroArchConfig } >> /media/data/log/Hakchi_retroarch_debug.log 2>&1 diff --git a/mod/etc/options_menu/scripts/om_functions b/mod/etc/options_menu/scripts/om_functions index 1a3e601..7f15dc8 100644 --- a/mod/etc/options_menu/scripts/om_functions +++ b/mod/etc/options_menu/scripts/om_functions @@ -1,5 +1,5 @@ #!/bin/sh -# shellcheck disable=SC2154,SC1091,SC2034,SC2068,SC2039,SC3051,SC3046 +# shellcheck disable=SC2154,SC1091,SC2034,SC2068,SC2039,SC3051,SC3046,SC3043 # Copyright (c) 2026 DefKorns (https://defkorns.github.io/LICENSE) # # This program is free software: you can redistribute it and/or modify @@ -28,7 +28,7 @@ string_path="resources/strings" root_bin="$rootfs/bin" usb_path="$mountpoint/media/$modname" font_dir="$usb_path/fonts" -state_path="$mountpoint$profilepath/$modname" +mod_state_path="$mountpoint$profilepath/$modname" [ ! -d "$font_dir" ] && font_dir="$nand_path/fonts" @@ -59,36 +59,38 @@ snes) esac # OM PATHS -optionsMenu="$mountpoint/etc/options_menu" -omCommands="$optionsMenu/commands" -omScripts="$optionsMenu/scripts" -omImages="$optionsMenu/images" +options_menu="$mountpoint/etc/options_menu" +om_commands="$options_menu/commands" +om_scripts="$options_menu/scripts" +om_images="$options_menu/images" # FUNCTIONS -gStorage="$(findGameStorage)" +game_storage="$(findGameStorage)" rename() { [ -f "$1" ] && mv "$1" "$2" } translate() { - key="$1" - langCode="en-US" - [ -f "$optionsMenu/language.cfg" ] && langCode="$(cat "$optionsMenu/language.cfg")" - [ -z "$langCode" ] && langCode="en-US" - - value="" - for langDir in "$optionsMenu/language" "$optionsMenu"/*/lang; do - [ -z "$value" ] && [ -f "$langDir/$langCode.lang" ] && value="$(grep "^${key}=" "$langDir/$langCode.lang" 2>/dev/null | tail -1 | cut -d= -f2-)" - [ -z "$value" ] && [ -f "$langDir/en-US.lang" ] && value="$(grep "^${key}=" "$langDir/en-US.lang" 2>/dev/null | tail -1 | cut -d= -f2-)" + local key="$1" + local lang_code="en-US" + local lang_dir + local value="" + [ -f "$options_menu/language.cfg" ] && lang_code="$(cat "$options_menu/language.cfg")" + [ -z "$lang_code" ] && lang_code="en-US" + + for lang_dir in "$options_menu/language" "$options_menu"/*/lang; do + [ -z "$value" ] && [ -f "$lang_dir/$lang_code.lang" ] && value="$(grep "^${key}=" "$lang_dir/$lang_code.lang" 2>/dev/null | tail -1 | cut -d= -f2-)" + [ -z "$value" ] && [ -f "$lang_dir/en-US.lang" ] && value="$(grep "^${key}=" "$lang_dir/en-US.lang" 2>/dev/null | tail -1 | cut -d= -f2-)" done [ -z "$value" ] && value="$key" echo "$value" } remove() { - for f in "$@"; do - [ -f "$f" ] || [ -d "$f" ] && rm -rf "$f" + local target + for target in "$@"; do + [ -f "$target" ] || [ -d "$target" ] && rm -rf "$target" done } @@ -104,26 +106,32 @@ create() { [ ! -f "$1" ] && touch "$1" } -enable_cmds() { - for arg in "$@"; do - in="disable$arg" - out="enable$arg" - # shellcheck disable=SC2086 - eval "rename "\$$in" "\$$out"" +enableCmds() { + local cmd_name + for cmd_name in "$@"; do + eval "rename \"\$disable_$cmd_name\" \"\$enable_$cmd_name\"" done } -disable_cmds() { - for arg in "$@"; do - in="enable$arg" - out="disable$arg" - # shellcheck disable=SC2086 - eval "rename "\$$in" "\$$out"" +disableCmds() { + local cmd_name + for cmd_name in "$@"; do + eval "rename \"\$enable_$cmd_name\" \"\$disable_$cmd_name\"" done } +# two providers, so one being down or blocked isn't read as offline +hasInternet() { + local host + ip route 2>/dev/null | grep -q '^default' || return 1 + for host in connectivitycheck.gstatic.com cloudflare.com; do + nc -z -w 3 "$host" 443 >/dev/null 2>&1 && return 0 + done + return 1 +} + netChecker() { - if ! nc -z 8.8.8.8 53 >/dev/null 2>&1; then + if hasInternet; then translate NO_PKG_ERR else translate NO_NET_ERR @@ -131,12 +139,10 @@ netChecker() { } toggleDownloads() { - nc -z 8.8.8.8 53 >/dev/null 2>&1 - # shellcheck disable=SC2181 - if [ $? -eq 0 ]; then - enable_cmds "Downloads" + if hasInternet; then + enableCmds "downloads" else - disable_cmds "Downloads" + disableCmds "downloads" fi } diff --git a/mod/etc/options_menu/scripts/om_network b/mod/etc/options_menu/scripts/om_network index b5f2964..001a48f 100644 --- a/mod/etc/options_menu/scripts/om_network +++ b/mod/etc/options_menu/scripts/om_network @@ -19,4 +19,4 @@ source $mountpoint/etc/options_menu/network/scripts/om_vars script_init -usleep 50000 && $optionsMenu/options --commandPath $omNetworkCmds/ --scriptPath $omNetworkScripts --title "NETWORK_OPTIONS" & +usleep 50000 && $options_menu/options --commandPath $om_network_cmds/ --scriptPath $om_network_scripts --title "NETWORK_OPTIONS" & diff --git a/mod/etc/options_menu/scripts/om_retroarch b/mod/etc/options_menu/scripts/om_retroarch index 6e5d5b7..1e58b79 100644 --- a/mod/etc/options_menu/scripts/om_retroarch +++ b/mod/etc/options_menu/scripts/om_retroarch @@ -19,4 +19,4 @@ source $mountpoint/etc/options_menu/retroarch/scripts/ra_vars script_init -usleep 50000 && $optionsMenu/options --commandPath $optionsMenu/retroarch/commands/ --scriptPath $optionsMenu/retroarch/scripts --title "RETROARCH_OPTIONS" & +usleep 50000 && $options_menu/options --commandPath $options_menu/retroarch/commands/ --scriptPath $options_menu/retroarch/scripts --title "RETROARCH_OPTIONS" & diff --git a/mod/etc/options_menu/scripts/system_info.sh b/mod/etc/options_menu/scripts/system_info.sh index 9ec8972..9a33105 100644 --- a/mod/etc/options_menu/scripts/system_info.sh +++ b/mod/etc/options_menu/scripts/system_info.sh @@ -19,7 +19,7 @@ source /etc/preinit script_init source "$mountpoint/etc/options_menu/scripts/om_functions" -disk_usage() { +diskUsage() { df -h "$1" 2>/dev/null | tail -1 | awk -v free="$(translate SYSINFO_FREE)" '{ print $3 " / " $2 " (" $4 " " free ")" }' } @@ -31,7 +31,7 @@ echo "Kernel: $kernelVersion" echo "Boot: $bootVersion" echo "$(translate SYSINFO_FIRMWARE): $(hakchi currentFirmware 2>/dev/null | sed "s/^_nand_$/NAND/")" echo " " -echo "NAND: $(disk_usage "$mountpoint/var/lib")" -[ -d "$usb_path" ] && echo "USB/SD: $(disk_usage "$usb_path")" +echo "NAND: $(diskUsage "$mountpoint/var/lib")" +[ -d "$usb_path" ] && echo "USB/SD: $(diskUsage "$usb_path")" saves="$(readlink -f /var/saves)" [ -d "$saves" ] && echo "$(translate SYSINFO_SAVES): $(du -sh "$saves" 2>/dev/null | cut -f1)" diff --git a/mod/install b/mod/install index 9a0dc36..4630eee 100644 --- a/mod/install +++ b/mod/install @@ -1,28 +1,28 @@ #!/bin/sh # shellcheck disable=SC2154 -omRoot="$rootfs/etc/options_menu" -omSettings="button.cfg language.cfg controller/home_combination.prev" -omBackup="$temppath/om_settings" +om_root="$rootfs/etc/options_menu" +om_settings="button.cfg language.cfg controller/home_combination.prev" +om_backup="$temppath/om_settings" # Uninstall any previous or legacy install first to ensure a clean installation -rm -rf "$omBackup" -mkdir -p "$omBackup" -for omFile in $omSettings; do - [ -f "$omRoot/$omFile" ] && mkdir -p "$(dirname "$omBackup/$omFile")" && cp -p "$omRoot/$omFile" "$omBackup/$omFile" +rm -rf "$om_backup" +mkdir -p "$om_backup" +for om_file in $om_settings; do + [ -f "$om_root/$om_file" ] && mkdir -p "$(dirname "$om_backup/$om_file")" && cp -p "$om_root/$om_file" "$om_backup/$om_file" done -for omUnfile in "$installpath/hmod/uninstall-options_deluxe"* \ +for om_unfile in "$installpath/hmod/uninstall-options_deluxe"* \ "$installpath/hmod/uninstall-options_menu_"* \ "$installpath/hmod/uninstall-hibernate_mod"* \ "$installpath/hmod/uninstall-Hakchi_Options_Loadscreen_toggle"; do - [ -f "$omUnfile" ] && pack_uninstall "${omUnfile##*/uninstall-}" + [ -f "$om_unfile" ] && pack_uninstall "${om_unfile##*/uninstall-}" done cd "$transferpath" || return 1 transfer_default -for omFile in $omSettings; do - [ -f "$omBackup/$omFile" ] && mkdir -p "$(dirname "$omRoot/$omFile")" && cp -p "$omBackup/$omFile" "$omRoot/$omFile" +for om_file in $om_settings; do + [ -f "$om_backup/$om_file" ] && mkdir -p "$(dirname "$om_root/$om_file")" && cp -p "$om_backup/$om_file" "$om_root/$om_file" done -rm -rf "$omBackup" +rm -rf "$om_backup" chmod +x "$rootfs/etc/options_menu/options" chmod +x "$rootfs/etc/options_menu/optiond" diff --git a/src/standby_watchdog.cpp b/src/standby_watchdog.cpp index ebbb598..14c7cc1 100644 --- a/src/standby_watchdog.cpp +++ b/src/standby_watchdog.cpp @@ -25,7 +25,7 @@ auto nextUpdateTime = std::chrono::system_clock::now()+fpsTime; int DisplayMenu() { - system("standby ReloadImagePayload"); + system("standby reloadImagePayload"); system("cat /tmp/power_menu_screen > /dev/fb0"); auto fpsTime = std::chrono::milliseconds(33); // Check for input faster //system("echo DEBUG: Displaying hibernate menu..."); @@ -64,7 +64,7 @@ int GetState() c.Update(); if(c.PeekButtonStatus(L) && c.PeekButtonStatus(R) && c.GetButtonStatus(UP)) { - system("standby DisplayMenu"); + system("standby displayMenu"); return DisplayMenu(); } else @@ -77,7 +77,7 @@ int GetState() void Hibernate() { - system("standby Hibernate"); + system("standby hibernate"); for(;;) { @@ -91,12 +91,12 @@ void Hibernate() else if(c.PeekButtonStatus(L) && c.PeekButtonStatus(R) && c.GetButtonStatus(DOWN)) { //system("echo Core Temperature at the time of shutdown out of hibernation: $(hakchi hwmon)c"); - system("standby HibernateReboot &"); + system("standby hibernateReboot &"); exit(0); } else if(pw.buttonPress()) { - system("standby Resume &"); + system("standby resume &"); exit(0); } std::this_thread::sleep_until(nextUpdateTime); @@ -106,7 +106,7 @@ void Hibernate() void Standby() { - system("standby Standby"); + system("standby standby"); for(;;) { @@ -162,7 +162,7 @@ int main(int argc, char * argv[]) Standby(); break; } - system("standby Resume"); + system("standby resume"); } } } From 4f0ba3e743c206dcd3c56d5ceb79380459331b02 Mon Sep 17 00:00:00 2001 From: DefKorns Date: Tue, 6 Oct 2026 23:12:06 +0000 Subject: [PATCH 064/104] Keep CI builds from reporting a dirty tree - Mark the bundled TTF and RASettingsBackup as binary: the toolchain image's git 2.1.4 treated them as text, saw an EOL change and tagged every CI build .dirty - Version in the hmod readme is now just the release tag; the git commit moves to its own line and the local build counter is gone --- .gitattributes | 2 ++ Makefile.docker | 11 ++++------- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/.gitattributes b/.gitattributes index 0316295..6c4a2a6 100644 --- a/.gitattributes +++ b/.gitattributes @@ -8,3 +8,5 @@ *.png binary *.xz binary *.hmod binary +*.ttf binary +mod/etc/options_menu/retroarch/scripts/RASettingsBackup binary diff --git a/Makefile.docker b/Makefile.docker index 2a19822..ce1fce6 100644 --- a/Makefile.docker +++ b/Makefile.docker @@ -13,7 +13,6 @@ TARGET_NAME = options_deluxe_$(ESC_MOD_VER) MOD_NAME := Options Menu $(MOD_VER) endif GIT_COMMIT := $(shell git describe --always --dirty=.dirty 2>/dev/null) -BUILD_NUM_FILE := .build_number CXX = g++ STRIP = strip ifdef CROSS_PREFIX @@ -57,8 +56,9 @@ define inject-readme = "Name: $(MOD_NAME)" \ "Creator: $(MOD_CREATOR)" \ "Category: $(MOD_CATEGORY)" \ - "Version: $(MOD_VER) build #$(shell cat $(BUILD_NUM_FILE))$(if $(GIT_COMMIT), ($(GIT_COMMIT)))" \ + "Version: $(MOD_VER)" \ "Built on: $(shell date +"%A, %d %b %Y - %T") by $(MOD_BUILDER)" \ + "Git commit: $(GIT_COMMIT)" \ "---" > mod/readme.md sed 1d temp/readme.md >> mod/readme.md @@ -75,10 +75,7 @@ endef all: $(OUT)/$(TARGET_NAME).hmod -bump-build-number: - echo $$(( $$(cat $(BUILD_NUM_FILE) 2>/dev/null || echo 0) + 1 )) > $(BUILD_NUM_FILE) - -$(OUT)/$(TARGET_NAME).hmod: mod/etc/options_menu/options mod/etc/options_menu/optiond mod/etc/options_menu/mod_uninstall/mod_uninstall mod/bin/standby_watchdog mod/etc/options_menu/scripts/gen_splash mod/etc/options_menu/scripts/ChangeCombo $(VENDORED_LIBS) bump-build-number +$(OUT)/$(TARGET_NAME).hmod: mod/etc/options_menu/options mod/etc/options_menu/optiond mod/etc/options_menu/mod_uninstall/mod_uninstall mod/bin/standby_watchdog mod/etc/options_menu/scripts/gen_splash mod/etc/options_menu/scripts/ChangeCombo $(VENDORED_LIBS) $(call strip-bin,mod/etc/options_menu/options) $(call strip-bin,mod/etc/options_menu/optiond) $(call strip-and-upx,mod/etc/options_menu/mod_uninstall/mod_uninstall) @@ -137,4 +134,4 @@ clean: rm -rf mod/etc/options_menu/lib rm -rf out/ temp/ -.PHONY: clean bump-build-number +.PHONY: clean From fe8f4569f451041ba6b2e97f61170ebc3d9b5a7c Mon Sep 17 00:00:00 2001 From: DefKorns Date: Tue, 6 Oct 2026 23:19:55 +0000 Subject: [PATCH 065/104] List the current menus and options in the hmod readme --- mod/readme.md | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/mod/readme.md b/mod/readme.md index ece4a05..fb7ae53 100644 --- a/mod/readme.md +++ b/mod/readme.md @@ -5,12 +5,17 @@ The Options Menu is a custom, easily extendable menu for your console, letting o ### Features -- Modern UI -- Hmod Uninstaller -- Hibernate Mod -- Retroarch Settings Management -- Wifi Backup -- Multi-language Support +- **Modern UI** with smooth TTF fonts and preview images +- **Power:** Hibernate/Standby, Shutdown, Restart +- **RetroArch:** back up/restore settings (NAND or USB), restore defaults, transfer BIOS files, clean up overrides/remaps/BIOS, toggle load screens +- **Network:** show IP, reconnect, search for SSIDs, back up/restore your wifi config (NAND or USB) +- **Saved Games:** back up/restore saves to USB +- **Language:** English, Français, Deutsch, Español, Italiano, Nederlands, Português, Русский, 日本語 +- **Advanced:** + - Controller: change the menu button combo, autofire (speed, X/Y as turbo A/B), Home menu combo, Start on the 2nd controller + - Diagnostics: system info, temperature, top, benchmark, RetroArch debugger, file structure and kernel log dumps to USB + - Module Uninstaller, Clear Cache, USB write access toggle, epilepsy protection (NES) +- Extendable: other hmods (e.g. Theme Selector) add their own menus ### How to open @@ -41,6 +46,7 @@ Extra thanks to **DNA64 (viral_dna)** and **Swingflip** for always testing featu Also thanks to the following people for testing the options menu: +- **Aranthys** - **BsLeNuL** - **DefKorns** - **DR1001** From 73c051de2b2e9c2985af306e4ea6bb19eeee96e1 Mon Sep 17 00:00:00 2001 From: DefKorns Date: Wed, 7 Oct 2026 11:07:59 +0000 Subject: [PATCH 066/104] Build and release through hmod-build as options_menu --- .gitmodules | 3 ++ Makefile.docker | 137 ------------------------------------------------ hmod-build | 1 + 3 files changed, 4 insertions(+), 137 deletions(-) delete mode 100644 Makefile.docker create mode 160000 hmod-build diff --git a/.gitmodules b/.gitmodules index 0fc596f..85dbe8c 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,6 @@ [submodule "toolchain"] path = toolchain url = https://github.com/DefKorns/classicmini-cross-toolchain.git +[submodule "hmod-build"] + path = hmod-build + url = https://github.com/DefKorns/hmod-build.git diff --git a/Makefile.docker b/Makefile.docker deleted file mode 100644 index ce1fce6..0000000 --- a/Makefile.docker +++ /dev/null @@ -1,137 +0,0 @@ -MOD_CREATOR := CompCom -MOD_BUILDER := DefKorns -MOD_CATEGORY := System -MOD_VER ?= $(shell git describe --tags --abbrev=0 2>/dev/null || echo v1.3.5) -ESC_MOD_VER := $(subst .,_,$(MOD_VER)) -# FIX_LIBSTDCXX=1 ships a private libstdc++.so.6 via rpath, immune to other -# hmods replacing the system copy. Off by default, costs ~225KB. -ifdef FIX_LIBSTDCXX -TARGET_NAME = options_deluxe_$(ESC_MOD_VER)_compat -MOD_NAME := Options Menu $(MOD_VER) (compat) -else -TARGET_NAME = options_deluxe_$(ESC_MOD_VER) -MOD_NAME := Options Menu $(MOD_VER) -endif -GIT_COMMIT := $(shell git describe --always --dirty=.dirty 2>/dev/null) -CXX = g++ -STRIP = strip -ifdef CROSS_PREFIX -PKG_CONFIG_LIBDIR = /usr/lib/arm-linux-gnueabihf/pkgconfig -SDL_CFLAGS = -I/usr/include/arm-linux-gnueabihf $(shell PKG_CONFIG_LIBDIR=$(PKG_CONFIG_LIBDIR) pkg-config --cflags sdl2 SDL2_ttf libpng) -SDL_LIBS = $(shell PKG_CONFIG_LIBDIR=$(PKG_CONFIG_LIBDIR) pkg-config --libs sdl2 SDL2_ttf libpng) -PNG_LIBS = $(shell PKG_CONFIG_LIBDIR=$(PKG_CONFIG_LIBDIR) pkg-config --libs libpng) -LDFLAGS = -Wl,--allow-shlib-undefined -VENDORED_LIBS = mod/etc/options_menu/lib/libSDL2_ttf-2.0.so.0 mod/etc/options_menu/lib/libfreetype.so.2.8 -ifdef FIX_LIBSTDCXX -VENDORED_LIBS += mod/etc/options_menu/lib/libstdc++.so.6 -STDCXX_RPATH = -Wl,-rpath,/etc/options_menu/lib -STDCXX_RPATH_ORIGIN = -Wl,-rpath,'$$ORIGIN/lib' -else -# a prior FIX_LIBSTDCXX=1 build leaves this behind - without FIX_LIBSTDCXX the -# tar step would silently bundle it into what's meant to be the plain build -STDCXX_CLEANUP = rm -f mod/etc/options_menu/lib/libstdc++.so.6 -endif -else -SDL_CFLAGS = $(shell sdl2-config --cflags) $(shell pkg-config --cflags SDL2_ttf) -SDL_LIBS = $(shell sdl2-config --libs) $(shell pkg-config --libs SDL2_ttf) -lpng -PNG_LIBS = -lpng -LDFLAGS = -VENDORED_LIBS = -#FIX_SDL2_SONAME = true -endif -CXXFLAGS = -std=c++11 -Os $(SDL_CFLAGS) -DMOD_VERSION=\"$(MOD_VER)\" -LDLIBS = $(SDL_LIBS) -SOURCES = src/main.cpp src/command.cpp src/localization.cpp src/framework/sdl_context.cpp src/framework/texture.cpp src/framework/controller.cpp src/framework/powerwatch.cpp src/framework/draw_helpers.cpp src/framework/utf8.cpp src/framework/font8x8_lookup.cpp src/framework/uitheme.cpp -OBJECTS = $(SOURCES:.cpp=.o) -DEV_DIR = $(shell realpath .) -OUT = $(DEV_DIR)/out -TMP = $(DEV_DIR)/temp - -define inject-readme = - mkdir -p $(OUT)/ $(TMP)/ - cp mod/readme.md temp/readme.md - - printf "%s\n" \ - "---" \ - "Name: $(MOD_NAME)" \ - "Creator: $(MOD_CREATOR)" \ - "Category: $(MOD_CATEGORY)" \ - "Version: $(MOD_VER)" \ - "Built on: $(shell date +"%A, %d %b %Y - %T") by $(MOD_BUILDER)" \ - "Git commit: $(GIT_COMMIT)" \ - "---" > mod/readme.md - - sed 1d temp/readme.md >> mod/readme.md -endef - -define strip-bin = - $(CROSS_PREFIX)$(STRIP) $(1) -endef - -define strip-and-upx = - $(CROSS_PREFIX)$(STRIP) $(1) - upx --lzma $(1) -endef - -all: $(OUT)/$(TARGET_NAME).hmod - -$(OUT)/$(TARGET_NAME).hmod: mod/etc/options_menu/options mod/etc/options_menu/optiond mod/etc/options_menu/mod_uninstall/mod_uninstall mod/bin/standby_watchdog mod/etc/options_menu/scripts/gen_splash mod/etc/options_menu/scripts/ChangeCombo $(VENDORED_LIBS) - $(call strip-bin,mod/etc/options_menu/options) - $(call strip-bin,mod/etc/options_menu/optiond) - $(call strip-and-upx,mod/etc/options_menu/mod_uninstall/mod_uninstall) - $(call strip-and-upx,mod/bin/standby_watchdog) - $(call strip-bin,mod/etc/options_menu/scripts/gen_splash) - $(call strip-and-upx,mod/etc/options_menu/scripts/ChangeCombo) - $(inject-readme) - $(STDCXX_CLEANUP) - - cd mod/; tar -czvf "../out/$(TARGET_NAME).hmod" * - - cp temp/readme.md mod/readme.md - find -name "*.o" -type f -delete - rm -rf $(TMP) - -# SDL2_ttf/freetype aren't on the console's own rootfs; shipped as -# mod/etc/options_menu/lib/*.so and found via rpath instead. -mod/etc/options_menu/options: $(OBJECTS) - $(CROSS_PREFIX)$(CXX) $(OBJECTS) $(LDLIBS) $(LDFLAGS) -Wl,-rpath,'$$ORIGIN/lib' -o mod/etc/options_menu/options - -mod/etc/options_menu/lib: - mkdir -p $@ - -# not ours to redistribute, pulled from the toolchain image each build -mod/etc/options_menu/lib/libSDL2_ttf-2.0.so.0: | mod/etc/options_menu/lib - cp /usr/lib/arm-linux-gnueabihf/libSDL2_ttf.so $@ - -mod/etc/options_menu/lib/libfreetype.so.2.8: | mod/etc/options_menu/lib - cp /usr/lib/arm-linux-gnueabihf/libfreetype.so.2.8.1 $@ - -mod/etc/options_menu/lib/libstdc++.so.6: | mod/etc/options_menu/lib - cp $$($(CROSS_PREFIX)$(CXX) -print-file-name=libstdc++.so.6) $@ - $(CROSS_PREFIX)$(STRIP) --strip-unneeded $@ - -mod/etc/options_menu/optiond: src/daemon.o src/framework/controller.o - $(CROSS_PREFIX)$(CXX) $(LDFLAGS) src/daemon.o src/framework/controller.o $(STDCXX_RPATH_ORIGIN) -o mod/etc/options_menu/optiond - -mod/etc/options_menu/mod_uninstall/mod_uninstall: src/mod_uninstall.o src/localization.o - $(CROSS_PREFIX)$(CXX) src/mod_uninstall.o src/localization.o src/framework/*.o $(LDLIBS) $(LDFLAGS) -Wl,-rpath,/etc/options_menu/lib -o mod/etc/options_menu/mod_uninstall/mod_uninstall - -mod/bin/standby_watchdog: src/standby_watchdog.o src/framework/controller.o src/framework/powerwatch.o - $(CROSS_PREFIX)$(CXX) $(LDFLAGS) src/standby_watchdog.o src/framework/controller.o src/framework/powerwatch.o $(STDCXX_RPATH) -o mod/bin/standby_watchdog - -mod/etc/options_menu/scripts/gen_splash: src/gen_splash.o src/localization.o src/framework/utf8.o src/framework/font8x8_lookup.o - $(CROSS_PREFIX)$(CXX) src/gen_splash.o src/localization.o src/framework/utf8.o src/framework/font8x8_lookup.o $(PNG_LIBS) $(LDFLAGS) $(STDCXX_RPATH) -o mod/etc/options_menu/scripts/gen_splash - -mod/etc/options_menu/scripts/ChangeCombo: src/change_combo.o src/framework/controller.o src/localization.o - $(CROSS_PREFIX)$(CXX) src/change_combo.o src/framework/controller.o src/localization.o $(LDFLAGS) $(STDCXX_RPATH) -o mod/etc/options_menu/scripts/ChangeCombo - -%.o: %.cpp - $(CROSS_PREFIX)$(CXX) $(CXXFLAGS) -c $< -o $@ - -clean: - find -name "*.o" -type f -delete - rm -f mod/etc/options_menu/options mod/etc/options_menu/optiond mod/etc/options_menu/mod_uninstall/mod_uninstall mod/bin/standby_watchdog mod/etc/options_menu/scripts/gen_splash mod/etc/options_menu/scripts/ChangeCombo $(TARGET_NAME).hmod - rm -rf mod/etc/options_menu/lib - rm -rf out/ temp/ - -.PHONY: clean diff --git a/hmod-build b/hmod-build new file mode 160000 index 0000000..38654e9 --- /dev/null +++ b/hmod-build @@ -0,0 +1 @@ +Subproject commit 38654e9535a04120c81a9276447d785514001c59 From e90e5766a227a30e612054d5d86a193e9c537469 Mon Sep 17 00:00:00 2001 From: DefKorns Date: Wed, 7 Oct 2026 11:08:25 +0000 Subject: [PATCH 067/104] Build and release through hmod-build as options_menu --- .github/workflows/build.yml | 74 +---- Makefile | 122 +++++++-- README.md | 519 ++++++++++++------------------------ mod/install | 1 + 4 files changed, 278 insertions(+), 438 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 4063152..4cca7fa 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -7,65 +7,15 @@ on: pull_request: jobs: - build: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v7 - with: - submodules: true - - # vendor/ holds the console's own SDL2 headers + libSDL2.so, extracted - # from the console's firmware - proprietary, so it isn't committed to - # this (public) repo. It's too large for a GitHub Secret (2.6MB, the - # limit is ~48-64KB), so it's kept in a separate PRIVATE repo instead - # and cloned here with a fine-grained PAT (repo secret VENDOR_REPO_PAT, - # read-only access to that one private repo). Lands in toolchain/vendor - - # the Dockerfile's build context is the toolchain/ submodule, not repo root. - - name: Restore vendor/ from private repo - run: | - git clone --depth 1 "https://x-access-token:${{ secrets.VENDOR_REPO_PAT }}@github.com/${{ secrets.VENDOR_REPO }}.git" toolchain/vendor - - - name: Build cross-compile toolchain image - run: docker build -f toolchain/Dockerfile.jessie-armhf -t classicmini-cross-toolchain:jessie-armhf toolchain - - # On a tag push (release build), force MOD_VER to the tag name itself - # rather than trusting `git describe` inside the container -- checkout - # is shallow by default, and describe can't reliably resolve tags - # without full history. Branch/PR builds just use the Makefile's own - # git-describe-based default (fine, since the exact string doesn't - # matter for a plain compile check). - - name: Compile - run: | - MOD_VER_ARG="" - if [ "${{ github.ref_type }}" = "tag" ]; then - MOD_VER_ARG="MOD_VER=${{ github.ref_name }}" - fi - docker run --rm -v "${{ github.workspace }}:/src" classicmini-cross-toolchain:jessie-armhf make -f Makefile.docker CROSS_PREFIX=arm-linux-gnueabihf- $MOD_VER_ARG - # _compat variant (private libstdc++.so.6) - must run after the plain - # build, which strips a leftover libstdc++ out of its own package - docker run --rm -v "${{ github.workspace }}:/src" classicmini-cross-toolchain:jessie-armhf make -f Makefile.docker CROSS_PREFIX=arm-linux-gnueabihf- $MOD_VER_ARG FIX_LIBSTDCXX=1 - - - name: Upload .hmod build artifact - uses: actions/upload-artifact@v7 - with: - name: options-menu-hmod - path: out/*.hmod - - release: - needs: build - if: startsWith(github.ref, 'refs/tags/v') - runs-on: ubuntu-latest - steps: - - name: Download build artifact - uses: actions/download-artifact@v8 - with: - name: options-menu-hmod - path: out - - - name: Create GitHub Release - uses: softprops/action-gh-release@v3 - with: - files: out/*.hmod - generate_release_notes: true - # -alpha/-beta/-rc tags must not take over "Latest" - prerelease: ${{ contains(github.ref_name, '-') }} + hmod: + uses: DefKorns/hmod-build/.github/workflows/hmod.yml@master + with: + release-name: Options Menu + docker: true + # the compat build relinks with its own rpath and adds a private libstdc++ + make: | + hmod + hmod FIX_LIBSTDCXX=1 + secrets: inherit + permissions: + contents: write diff --git a/Makefile b/Makefile index 2ad39db..698cb36 100644 --- a/Makefile +++ b/Makefile @@ -1,47 +1,115 @@ -TARGET_NAME = options_deluxe +# FIX_LIBSTDCXX=1 builds the _compat variant: it ships a private libstdc++.so.6 +# via rpath, immune to other hmods replacing the system copy (~225KB more). +ifdef FIX_LIBSTDCXX +MOD_ID := options_menu_compat +MOD_NAME := Options Menu (compat) +VARIANT := compat +else +MOD_ID := options_menu +MOD_NAME := Options Menu +VARIANT := plain +endif +MOD_CREATOR := CompCom, DefKorns +MOD_CATEGORY := System +BINARIES := mod/etc/options_menu/options mod/etc/options_menu/optiond mod/etc/options_menu/mod_uninstall/mod_uninstall mod/bin/standby_watchdog mod/etc/options_menu/scripts/gen_splash mod/etc/options_menu/scripts/ChangeCombo + CXX = g++ STRIP = strip -CXXFLAGS = -std=c++11 -Os `sdl2-config --cflags` -LDLIBS = `sdl2-config --libs` -lpng -LDFLAGS = -SOURCES = src/main.cpp src/command.cpp src/localization.cpp src/framework/sdl_context.cpp src/framework/texture.cpp src/framework/nineslice.cpp src/framework/controller.cpp src/framework/powerwatch.cpp +ifdef CROSS_PREFIX +PKG_CONFIG_LIBDIR = /usr/lib/arm-linux-gnueabihf/pkgconfig +SDL_CFLAGS = -I/usr/include/arm-linux-gnueabihf $(shell PKG_CONFIG_LIBDIR=$(PKG_CONFIG_LIBDIR) pkg-config --cflags sdl2 SDL2_ttf libpng) +SDL_LIBS = $(shell PKG_CONFIG_LIBDIR=$(PKG_CONFIG_LIBDIR) pkg-config --libs sdl2 SDL2_ttf libpng) +PNG_LIBS = $(shell PKG_CONFIG_LIBDIR=$(PKG_CONFIG_LIBDIR) pkg-config --libs libpng) +LDFLAGS = -Wl,--allow-shlib-undefined +VENDORED_LIBS = mod/etc/options_menu/lib/libSDL2_ttf-2.0.so.0 mod/etc/options_menu/lib/libfreetype.so.2.8 +ifdef FIX_LIBSTDCXX +VENDORED_LIBS += mod/etc/options_menu/lib/libstdc++.so.6 +STDCXX_RPATH = -Wl,-rpath,/etc/options_menu/lib +STDCXX_RPATH_ORIGIN = -Wl,-rpath,'$$ORIGIN/lib' +endif +else +SDL_CFLAGS = $(shell sdl2-config --cflags) $(shell pkg-config --cflags SDL2_ttf) +SDL_LIBS = $(shell sdl2-config --libs) $(shell pkg-config --libs SDL2_ttf) -lpng +PNG_LIBS = -lpng +LDFLAGS = +VENDORED_LIBS = +endif +CXXFLAGS = -std=c++11 -Os $(SDL_CFLAGS) -DMOD_VERSION=\"v$(MOD_VER)\" +LDLIBS = $(SDL_LIBS) +SOURCES = src/main.cpp src/command.cpp src/localization.cpp src/framework/sdl_context.cpp src/framework/texture.cpp src/framework/controller.cpp src/framework/powerwatch.cpp src/framework/draw_helpers.cpp src/framework/utf8.cpp src/framework/font8x8_lookup.cpp src/framework/uitheme.cpp OBJECTS = $(SOURCES:.cpp=.o) +ALL_OBJECTS = $(OBJECTS) src/daemon.o src/mod_uninstall.o src/standby_watchdog.o src/gen_splash.o src/change_combo.o + +MOD_DEPS := $(BINARIES) $(VENDORED_LIBS) +ifndef FIX_LIBSTDCXX +# a prior compat build leaves this behind in mod/ +MOD_DEPS += no-private-libstdcxx +endif + +all: hmod -all: $(TARGET_NAME).hmod $(TARGET_NAME)_b_down.hmod +compile: $(BINARIES) $(VENDORED_LIBS) -$(TARGET_NAME)_b_down.hmod: $(TARGET_NAME).hmod - cp button.cfg mod/etc/options_menu/ - cd mod/; tar -czvf "../$(TARGET_NAME)_b_down.hmod" * +# relink everything when switching variant (different rpaths) or version (MOD_VERSION) +.build-flags: FORCE + @[ "$$(cat $@ 2>/dev/null)" = "$(VARIANT) $(MOD_VER)" ] || echo "$(VARIANT) $(MOD_VER)" > $@ -$(TARGET_NAME).hmod: mod/etc/options_menu/options mod/etc/options_menu/optiond mod/etc/options_menu/mod_uninstall/mod_uninstall mod/bin/standby_watchdog mod/etc/options_menu/scripts/gen_splash - $(CROSS_PREFIX)$(STRIP) mod/etc/options_menu/options - $(CROSS_PREFIX)$(STRIP) mod/etc/options_menu/optiond - $(CROSS_PREFIX)$(STRIP) mod/etc/options_menu/mod_uninstall/mod_uninstall - $(CROSS_PREFIX)$(STRIP) mod/bin/standby_watchdog - $(CROSS_PREFIX)$(STRIP) mod/etc/options_menu/scripts/gen_splash - rm -f mod/etc/options_menu/button.cfg - cd mod/; tar -czvf "../$(TARGET_NAME).hmod" * +$(ALL_OBJECTS): .build-flags +no-private-libstdcxx: + rm -f mod/etc/options_menu/lib/libstdc++.so.6 + +# SDL2_ttf/freetype aren't on the console's own rootfs; shipped as +# mod/etc/options_menu/lib/*.so and found via rpath instead. mod/etc/options_menu/options: $(OBJECTS) - $(CROSS_PREFIX)$(CXX) $(OBJECTS) $(LDLIBS) $(LDFLAGS) -o mod/etc/options_menu/options + $(CROSS_PREFIX)$(CXX) $(OBJECTS) $(LDLIBS) $(LDFLAGS) -Wl,-rpath,'$$ORIGIN/lib' -o $@ + $(CROSS_PREFIX)$(STRIP) $@ + +mod/etc/options_menu/lib: + mkdir -p $@ + +# not ours to redistribute, pulled from the toolchain image each build +mod/etc/options_menu/lib/libSDL2_ttf-2.0.so.0: | mod/etc/options_menu/lib + cp /usr/lib/arm-linux-gnueabihf/libSDL2_ttf.so $@ + +mod/etc/options_menu/lib/libfreetype.so.2.8: | mod/etc/options_menu/lib + cp /usr/lib/arm-linux-gnueabihf/libfreetype.so.2.8.1 $@ + +mod/etc/options_menu/lib/libstdc++.so.6: | mod/etc/options_menu/lib + cp $$($(CROSS_PREFIX)$(CXX) -print-file-name=libstdc++.so.6) $@ + $(CROSS_PREFIX)$(STRIP) --strip-unneeded $@ mod/etc/options_menu/optiond: src/daemon.o src/framework/controller.o - $(CROSS_PREFIX)$(CXX) $(LDFLAGS) src/daemon.o src/framework/controller.o -o mod/etc/options_menu/optiond + $(CROSS_PREFIX)$(CXX) $(LDFLAGS) $^ $(STDCXX_RPATH_ORIGIN) -o $@ + $(CROSS_PREFIX)$(STRIP) $@ -mod/etc/options_menu/mod_uninstall/mod_uninstall: src/mod_uninstall.o - $(CROSS_PREFIX)$(CXX) src/mod_uninstall.o src/framework/*.o $(LDLIBS) $(LDFLAGS) -o mod/etc/options_menu/mod_uninstall/mod_uninstall +mod/etc/options_menu/mod_uninstall/mod_uninstall: src/mod_uninstall.o src/localization.o $(filter src/framework/%,$(OBJECTS)) + $(CROSS_PREFIX)$(CXX) $^ $(LDLIBS) $(LDFLAGS) -Wl,-rpath,/etc/options_menu/lib -o $@ + $(CROSS_PREFIX)$(STRIP) $@ + upx --lzma $@ mod/bin/standby_watchdog: src/standby_watchdog.o src/framework/controller.o src/framework/powerwatch.o - $(CROSS_PREFIX)$(CXX) src/standby_watchdog.o src/framework/controller.o src/framework/powerwatch.o -o mod/bin/standby_watchdog + $(CROSS_PREFIX)$(CXX) $(LDFLAGS) $^ $(STDCXX_RPATH) -o $@ + $(CROSS_PREFIX)$(STRIP) $@ + upx --lzma $@ -mod/etc/options_menu/scripts/gen_splash: src/gen_splash.o src/localization.o - $(CROSS_PREFIX)$(CXX) src/gen_splash.o src/localization.o $(LDLIBS) $(LDFLAGS) -o mod/etc/options_menu/scripts/gen_splash +mod/etc/options_menu/scripts/gen_splash: src/gen_splash.o src/localization.o src/framework/utf8.o src/framework/font8x8_lookup.o + $(CROSS_PREFIX)$(CXX) $^ $(PNG_LIBS) $(LDFLAGS) $(STDCXX_RPATH) -o $@ + $(CROSS_PREFIX)$(STRIP) $@ + +mod/etc/options_menu/scripts/ChangeCombo: src/change_combo.o src/framework/controller.o src/localization.o + $(CROSS_PREFIX)$(CXX) $^ $(LDFLAGS) $(STDCXX_RPATH) -o $@ + $(CROSS_PREFIX)$(STRIP) $@ + upx --lzma $@ %.o: %.cpp $(CROSS_PREFIX)$(CXX) $(CXXFLAGS) -c $< -o $@ clean: - find -name "*.o" -type f -delete - rm -f mod/etc/options_menu/options mod/etc/options_menu/optiond mod/etc/options_menu/mod_uninstall/mod_uninstall mod/bin/standby_watchdog mod/etc/options_menu/scripts/gen_splash mod/etc/options_menu/button.cfg $(TARGET_NAME).hmod $(TARGET_NAME)_b_down.hmod + find . -name "*.o" -type f -not -path "./toolchain/*" -delete + rm -f $(BINARIES) .build-flags + rm -rf mod/etc/options_menu/lib out/ + +include hmod-build/hmod.mk -.PHONY: clean +.PHONY: all compile clean no-private-libstdcxx diff --git a/README.md b/README.md index 549dab4..b7a2b67 100644 --- a/README.md +++ b/README.md @@ -1,425 +1,246 @@ -# Options Menu Documentation +# Options Menu [![Build](https://github.com/DefKorns/OptionsMenu/actions/workflows/build.yml/badge.svg)](https://github.com/DefKorns/OptionsMenu/actions/workflows/build.yml) [![License: GPL v3](https://img.shields.io/badge/License-GPLv3-blue.svg)](LICENSE) -## What is the options menu? +A menu you can open at any moment on your NES, SNES, Famicom or Super Famicom Classic — on the Home screen or in the middle of a game — to power off, back up your saves and settings, manage Wi-Fi and RetroArch, tweak the controllers and more. Other hmods plug their own screens into it, like [Theme Selector](https://github.com/DefKorns/om_theme-selector). -The Options Menu is a custom menu that can be launched via a controller button combo at any point during the console’s operation. It features a variety of commands to enhance the user experience of the console. +This is my continuation of [CompCom's Options Menu](https://github.com/CompCom/OptionsMenu) (last release 1.3.4, 2019). Version 2 is a rewrite of the interface and a large part of the scripts: -The options menu is easily extendible, allowing for other developers to add their own custom commands to the menu via hmods. +- **New look:** smooth TTF fonts, preview icons, toggle switches, submenu markers and button hints, with colors you can change (`theme.cfg`) +- **9 languages:** English, Français, Deutsch, Español, Italiano, Nederlands, Português, Русский, 日本語 +- **New menus:** Controller (autofire, Home combo, Start on the 2nd Famicom controller), Diagnostics, Saved Games, Wi-Fi backups, epilepsy protection +- **Safer behaviour:** entries that need a USB/SD drive or a missing component are hidden instead of failing, and lists such as Wi-Fi backups let you delete entries +- **NES/Famicom friendly:** the opening combo is picked automatically for consoles without shoulder buttons +- A **compat** build for setups where another hmod replaces the console's `libstdc++` -## Navigation +## Install -|Button|Action| -|------|------| -|UP / DOWN|Move the selection (hold to repeat).| -|A or START|Run the selected option or open its submenu.| -|B|Go back to the previous menu, or close the Options Menu on the main screen.| -|B (hold ~1 second)|Delete the selected entry, on lists that support it (the footer shows a **Delete (Hold)** hint). You are asked to confirm with **A**.| +**From my Mod Hub (recommended):** in hakchi open **Manage repositories**, add `https://defkorns.github.io/hakchi-repo/` and install **Options Menu** from it. Updates show up there automatically. -Rows that open a submenu show a **›** on the right. The button hints for the current screen are always shown in the footer. +**By hand:** download `options_menu.hmod` from [Releases](https://github.com/DefKorns/OptionsMenu/releases), put it in hakchi's `user_mods` folder (or drag and drop it onto the hakchi window), then install it from hakchi (**Modules → Install extra modules**). -## Options Explained +Installing it replaces CompCom's Options Menu (`options_deluxe`) and the old Hibernate mod, and keeps your button combo, language and Home combo settings. -Below is a detailed guide on what specific options actually do and what their purpose is. +> **Which build?** Use `options_menu`. Pick `options_menu_compat` only if you have an hmod that replaces the console's `libstdc++` (e.g. RetroArch 1.8.4 Xtreme SC) and the menu doesn't open with the normal build. Installing one removes the other. -## Default Options +## Opening it -#### Hibernate/Standby +Hold **L + R** for about a second. On consoles without shoulder buttons (NES Classic, Famicom Classic, Famicom Shonen Jump), hold **B + Down** instead. You can change the combo in **Advanced Options → Controller**. -This option launches Swingflip’s power menu. The power menu will provide the following options: +| Button | Action | +| --- | --- | +| Up / Down | Move (hold to repeat) | +| A or Start | Run the entry or open its submenu (marked **›**) | +| B | Back; closes the menu on the main screen | +| B (hold ~1 second) | Delete the selected entry, on lists that allow it — confirm with A | -- **Hibernate** - Put the console in a low power state, turn the screen off and when woken up the console will resume whatever you were doing before you put the console in hibernate. -- **Standby** - Put the console in lowest power state possible, turn the screen off and when woken up the console will reboot and start the console from a fresh boot. +The footer always shows the buttons for the current screen. -> **Note:** Basically... Standby saves more energy however when you remotely wake the console it will start up as a fresh boot where hibernate will resume where ever you left off when you put it in to hibernate. +## Menus -To wake the console when in hibernate or standby please refer to the below table: +### Main menu -|Button combination|Hibernate Mode |Standby Mode | -|------------------|---------------|-----------------| -| L + R + UP |Resume Console |Reboot Console | -| L + R + DOWN |Reboot Console |Shutdown Console | +| Entry | What it does | +| --- | --- | +| Hibernate/Standby | Opens Swingflip's power menu (see below) | +| Shutdown Device | Safely turns the console off\* | +| Reboot Device | Safely restarts the console\* | +| RetroArch › | RetroArch settings, backups and BIOS files. Shown when RetroArch is installed | +| Network › | IP address, Wi-Fi scan and Wi-Fi config backups | +| Saved Games › | Back up and restore your saves. Needs a USB/SD drive | +| Language › | Menu language; the menu restarts in the new language | +| Theme Options › | Added by [Theme Selector](https://github.com/DefKorns/om_theme-selector) when it's installed | +| Advanced Options › | Controller, diagnostics and system tools | -#### Shutdown Device +\*Don't shut down or restart from the menu while RetroArch is running: exit RetroArch first, or you may lose saves. -This option will safely shut down the console.\* +**Hibernate** turns the screen off and keeps everything in memory, so waking up resumes exactly where you were. **Standby** uses even less power, but waking up boots the console from scratch. To wake it: -#### Restart Device +| Combo | From Hibernate | From Standby | +| --- | --- | --- | +| L + R + Up | Resume | Reboot | +| L + R + Down | Reboot | Shut down | -This option will safely restart the console.\* +### RetroArch -#### RetroArch +Works with RetroArch Neo and with the `_km` RetroArch Xtreme (Ozone). -This option will open a menu for managing retroarch configurations (see below). +| Entry | What it does | +| --- | --- | +| Restore Default Settings (All) | Puts back the default RetroArch config **and** deletes every game/core override and remap | +| Restore Default Settings | Puts back the default RetroArch config, keeping overrides and remaps | +| Backup Settings to NAND / USB | Copies your RetroArch config to `/etc/ra_backup/` or `USB:/data/ra_backup/` | +| Restore Settings from NAND / USB | Restores the config from those backups | +| Transfer BIOS file(s) | Copies BIOS files to `USB:/data/ra_bios` and restores them from there to NAND | +| Delete all settings backups | Removes the NAND and USB backups | +| Delete game and core overrides | Removes only the overrides | +| Delete remap files | Removes only the remaps | +| Delete BIOS file(s) from NAND | Removes the BIOS files from NAND | +| Toggle RA and Canoe load screens | Turns the RetroArch and Canoe loading screens on or off | -#### Network +The USB entries only show up with a USB/SD drive connected. -This option will open a menu for managing your network and wifi configuration (see below). +### Network -#### Saved Games +| Entry | What it does | +| --- | --- | +| Display IP Address | Shows your local and public IP | +| Reconnect | Restarts the network when you're not connected | +| Search for SSIDs | Lists the Wi-Fi networks in range | +| Backup Wifi Config (NAND / USB) | Saves your `wpa_supplicant.conf` to `/etc/wifi_backup/` or `/media/data/wifi_backup/` | +| Restore Wifi Config (NAND / USB) › | Lists your backups; pick one to restore it and reconnect, or hold **B** to delete it | -This option will open a menu for backing up and restoring your saved games (see below). +Restore entries only show up when there's a backup to restore. -#### Language +### Saved Games -This option opens a submenu where you can choose the display language for the Options Menu. Currently available: English, Français, Deutsch, Español, Italiano, Nederlands, Português (Europeu), Русский and 日本語. The Options Menu automatically restarts in the selected language. +Needs a USB/SD drive with your saves on it. -#### Theme Options +| Entry | What it does | +| --- | --- | +| Backup Saved Games to USB | Mirrors your saves folder to `/media/data/saves_backup`. Saves you deleted since the last backup are removed from it too; system folders (`FOLDER`, `hakchi`, `home-menu`, `mcp-state`) are skipped | +| Restore Saved Games from USB | Copies the backup back, overwriting saves with the same name and leaving the others alone | -Only shown when the [Theme Selector](https://github.com/DefKorns/om_theme-selector) hmod is installed. Opens its theme manager. +### Advanced Options -#### Advanced Options +| Entry | What it does | +| --- | --- | +| Controller › | See below | +| Diagnostics › | See below | +| Toggle Write Access on USB | Makes the USB/SD drive writable even if you don't keep your saves on it (it's read-only otherwise). Not needed if you use USB saves | +| Epilepsy protection (NES) | The console's own dimming of fast flashes in NES/Famicom games. On by default; turn it off to see games exactly as they were. NES/Famicom only | +| Clear Cache | Asks the kernel to free cached memory. Rarely needed | +| Module Uninstaller | Remove installed hmods from the console (see below) | -This option will open a menu containing more advanced features (see below). +**Module Uninstaller:** installed mods are on the left, the ones queued for removal on the right. **A** queues the selected mod, **B** takes the last one off the queue, **Start** exits — or, with mods queued, asks you to press **Start + Select** to remove them (**B** cancels). It lists hakchi's own modules too: only remove mods you installed yourself. ->\***Note:** You should **NOT** turn off your console via the options/power menu while running retroarch. Ensure you properly exit retroarch before shutting down/restarting or you may experience a loss of saves. +#### Controller -## Retroarch Options +| Entry | What it does | +| --- | --- | +| Change Options Button Combo | Press the buttons you want to open the menu with. Restart the console afterwards. Overrides the automatic L+R / B+Down choice | +| Autofire | Turbo for the controllers, with **Fast / Normal / Slow** speed | +| Use X/Y as turbo A/B | X and Y act as turbo A and B | +| Home menu button combo | Turns off the combo that sends you from a game back to the Home menu, so you can't press it by accident | +| Start on 2nd controller | Famicom only: the 2nd Famicom controller has no Start button; this gives it one | -Both RetroArch builds are supported: RetroArch Neo (discontinued) and the `_km` RetroArch Xtreme (Ozone). +Changes apply right away, without restarting. -#### Restore Default Settings (All) +#### Diagnostics -Replaces your current retroarch config files with the default files included in the module. This option deletes all game/core overrides and remap files. +| Entry | What it does | +| --- | --- | +| System Information | Console type and region, hakchi/kernel/boot versions, firmware, free space on NAND and USB/SD, size of your saves | +| Display Temp | CPU temperature | +| Run Top | CPU and memory use and the running processes | +| Benchmark Tool | Mounts, CPU and RAM information, in 4 parts | +| RetroArch Debugger (USB logs) | Runs RetroArch in verbose mode and saves its config and log to `/media/data/log/` | +| Dump File Structure (to USB) | Writes every file and folder, with permissions and links, to `/media/data/log/Hakchi_file_structure.log` | +| Save kernel log (to USB) | Writes `dmesg` to `/media/data/log/dmesg.log` | -#### Restore Default Settings +The USB entries only show up with a USB/SD drive connected. They're the logs to attach when reporting a problem. -Replaces your current retroarch config files with the default files included in the module. Does not erase your game/core overrides and remap files. +## For mod developers -#### Backup Settings to NAND +Your hmod can add entries and whole screens to the menu. -Copies your current retroarch config files to a backup file located at /etc/ra_backup/ +### How it works -#### Backup Settings to USB (usb only) +`optiond` waits for the button combo and launches `options`. Everything lives in `/etc/options_menu/`: each screen is a `commands/` folder with one file per entry, and the scripts they run go in a `scripts/` folder. -Copies your current retroarch config files to a backup file located at USB:/data/ra_backup/ +`options` takes these arguments (always call it by its full path, `/etc/options_menu/options`): -#### Restore Settings from NAND +| Argument | Description | +| --- | --- | +| `--commandPath ` | Commands folder for this screen | +| `--scriptPath ` | Scripts folder, available as `%script_dir%` | +| `--title ` | Screen title (a translation key or plain text) | -Overwrites your retroarch configs with the backups located at /etc/ra_backup/ +### Command files -#### Restore Settings from USB (usb only) +One file per entry, named `cNNNN_Name`; entries are sorted by name. Each line is `FIELD=value`, no spaces around `=`, case sensitive. -Overwrites your retroarch configs with the backups located at USB:/data/ra_backup/ +| Field | Description | +| --- | --- | +| `COMMAND_NAME` | Label shown, or a translation key | +| `COMMAND_TYPE` | `INTERNAL` shows the command's output in the menu's console; `EXTERNAL` runs it with the menu paused | +| `COMMAND_STR` | Single-line command to run. Use a script for anything longer | +| `RESTART_UI` | External commands only. `FALSE` leaves the UI paused; resume it yourself with `/bin/sh /etc/options_menu/scripts/ResumeUI.sh` | +| `IGNORE_INTERRUPT` | `TRUE`: B doesn't interrupt an internal command while it runs | +| `USB_ONLY` | `TRUE`: only shown when a USB/SD drive is mounted | +| `ENABLE_IF` | Shell condition; the entry is only shown if it exits 0. `/etc/preinit` is sourced and `script_init` called first, so `$rootfs`, `$mountpoint`, `$sftype`... work, e.g. `ENABLE_IF=[ -d "$rootfs/etc/wifi_backup" ]` | +| `SUBMENU` | `TRUE`: draws **›**, for entries that open another screen. Ignored on toggles | +| `CHILD` | `TRUE`: indents the entry under the one above it | +| `STATE_STR` | Command whose first output line is the entry's state (`1`/`on`/`y`/`yes`/`true` = on). Turns the entry into a toggle switch; `COMMAND_STR` still does the toggling | +| `DELETE_STR` | Command run when the user holds B on the entry and confirms — makes list entries deletable | +| `DELETE_CONFIRM_KEY` | Translation key for that confirmation; defaults to a generic "Delete this item?" | +| `PREVIEW_IMAGE` | PNG shown in the detail panel, scaled to fit | -#### Delete all settings backups +`COMMAND_STR`, `DELETE_STR` and `STATE_STR` can use `%options_path%` (the folder with the `options` binary) and `%script_dir%` (the current `--scriptPath`). -Removes all backup files on both NAND and USB. - -#### Transfer BIOS file(s) - -Copies bios files to USB:/data/ra_bios. Restores bios files from this folder to NAND. - -#### Delete game and core overrides - -Removes only the game and core overrides. Does not modify the Retroarch settings and remap files. - -#### Delete remap files - -Removes only the remap files. Does not modify the Retroarch settings, game and core overrides. - -#### Delete BIOS file(s) from NAND - -Removes all bios files from NAND only. - -#### Toggle RA and Canoe load screens - -Toggle the RetroArch and Canoe loading screens on or off. - -## Network Options - -#### Display IP Address - -This option will display both your public and internal ip addresses if you have a network adapter connected to your device. - -#### Reconnect - -This option will restart the network service on your device if you are not connected to the internet already. - -#### Search for SSIDs - -This option will scan and display a list of available SSIDs. - -#### Backup Wifi Config (NAND) - -Copies your current wifi config (wpa_supplicant.conf) to a backup folder on your NAND, named after your SSID: - - /etc/wifi_backup/your_ssid_name - -#### Backup Wifi Config (USB) - -Copies your current wifi config (wpa_supplicant.conf) to a backup folder on your mounted USB/SD card, named after your SSID: - - /media/data/wifi_backup/your_ssid_name - -#### Restore Wifi Config (NAND) - -Overwrites your wifi config with a backup located at: - - /etc/wifi_backup/ - -and reconnects to the network. - -While browsing the list of saved backups, hold **B** (~1 second) to delete -the selected backup (you will be asked to confirm with **A**, or cancel by -pressing **B** again). The list refreshes automatically after deleting. - -#### Restore Wifi Config (USB) - -Overwrites your wifi config with a backup located at: - - /media/data/wifi_backup/ - -and reconnects to the network. - -While browsing the list of saved backups, hold **B** (~1 second) to delete -the selected backup (you will be asked to confirm with **A**, or cancel by -pressing **B** again). The list refreshes automatically after deleting. - -## Saved Games Options - -#### Backup Saved Games to USB - -Copies your saved games from the saves folder on your USB/SD card to a backup folder on the same drive: - - /media/data/saves_backup - -The backup mirrors the saves folder, so saves deleted since the last backup are removed from it too. System folders (`FOLDER`, `hakchi`, `home-menu`, `mcp-state`) and system files are not included. - -#### Restore Saved Games from USB - -Copies the saved games from `/media/data/saves_backup` back into your saves folder, overwriting saves with the same name. Saves that are not in the backup are left untouched. - -## Advanced Options - -#### Change Options Button Combo - -This option will allow you to change the button combo used to launch the options menu. You will need to restart the console after setting a new combo. - -> **Note:** The default combo is **L + R**, or **B + Down** on consoles with no shoulder buttons (NES Classic, Famicom Classic and its variants, e.g. the Famicom Shonen Jump 50th Anniversary edition), detected automatically. Setting a custom combo here overrides that detection. - -#### Toggle Write Access on USB - -This option will toggle read write access on your USB device. By default, if you don't have a saves folder located on your USB/SD, write access is disabled. If you wish to write logs or use your external mounted storage and **not** use external saves... This is the toggle for you. - - > **Note:** If you already use external saves. You don't need to use this toggle. - -#### Clear Cache - -This option will force the kernel to clear the page cache and free up unused memory. You should not need to call this as the system should manage memory fine on it's own. - -#### Module Uninstaller - -This option launches the module uninstaller program. This program allows you to remove modules from your device. The installed modules are listed on the left and the ones queued for removal on the right. - -- **UP / DOWN** - select a module. -- **A** - add the selected module to the uninstall list. -- **B** - remove the last module from the uninstall list. -- **START** - exit if the uninstall list is empty; otherwise you are asked to confirm. Press **START + SELECT** to remove the queued modules, or **B** to cancel. - ->**Caution:** The uninstaller will list all modules on the device including the default hakchi modules. Only delete modules you have added yourself. Deleting the default hakchi modules can have unintended effects on your console. - -#### Display Temp - -This option will display the current temperature of the CPU. - -#### Run Top - -This option runs and displays the output of the top binary. This shows the current CPU, memory usage and list of running processes. - -#### Benchmark Tool - -This option will go through and echo out useful information regarding your console. -It will also include information on your: - -- Current disk mounting information -- CPU analysis information -- RAM analysis information - -> **Note:** The information is displayed in 4 parts and has a 2 second wait between each part - -#### RetroArch Debugger (USB logs) - -This option will run your locally installed RetroArch in full verbose mode. It will also copy the config files for your RetroArch and save them to: - - /media/data/log/RetroArchConfig - -The full verbose log will be located here: - - /media/data/log/Hakchi_retroarch_debug.log - -> **Note:** Depending on what you are trying to do, you might have limited functionality within RetroArch when running it via the debugger. This app is primarily designed to debug cores, custom configs and themes. - -#### Dump File Structure (to USB) - -This option will map and echo out your complete file and directory structure for your console and any mounted disks. The log file will be saved to your mounted USB/SD card at: - - /media/data/log/Hakchi_file_structure.log - - > **Note:** If these folders do not exist, they will be created when the option is ran. - -The log file will also include information on: - -- Symbolic Links (Overmounts) -- File and directory permissions - -## Developer Information - -### General Information - -The options mod is comprised of two executables. The `optiond` executable acts as a watchdog that waits for the controller combo to be held down and executes the `options` executable. All files are stored with the `/etc/options_menu/` folder. There are two important folders for the usage of the options menu these are the commands and scripts folders. The commands folder stores the options to be displayed on the menu. The scripts folder contains scripts executed by these commands. - -### Command Line Arguments -> ->Note: The options menu should always be called with the full file path. ->/etc/options_menu/options - -|Argument|Description| -|----------|-------------| -|--commandPath <Command Folder Path>|Set a custom command folder.| -|--scriptPath <Script Folder Path>|Set custom script folder.| -|--title <Window Title>|Set custom title for options menu screen.| - -### Command Files - -Command Files must be named in the following format: - - cxxxx_CommandName - -Command Files should contain the following fields: - -|Field|Description| -|-------|-------------| -|COMMAND_NAME|Display Name of command.| -|COMMAND_TYPE|Specifies whether command is run inside the options menu pseudo terminal (internally) or run externally. Possible values `INTERNAL` or `EXTERNAL`| -|RESTART_UI|Specifies if the paused UI should be resumed after running command (external commands only). If you set this value to `FALSE` you must manually resume the UI using `/bin/sh /etc/options_menu/script/ResumeUI.sh` after executing your code.| -|COMMAND_STR|Command string to be executed. Commands must be single line only. To execute multi-line scripts use a script file.| -|USB_ONLY|If set to `TRUE`, the command is only loaded when a USB/SD card is mounted.| -|ENABLE_IF|Optional shell condition; the row is only loaded if it exits 0. Runs with `/etc/preinit` sourced and `script_init` called first, so it can reference `$rootfs`/`$mountpoint` like any other options_menu script, e.g. `ENABLE_IF=[ -d "$rootfs/etc/wifi_backup" ]`.| -|IGNORE_INTERRUPT|If set to `TRUE`, pressing B will not interrupt an internal command while its output is being displayed.| -|CHILD|If set to `TRUE`, indents the row under the one above it, for visually grouping a related entry without a separate submenu.| -|SUBMENU|If set to `TRUE`, marks the row as opening another menu and draws a **›** on its right. Ignored on toggle rows and on rows that relaunch the current menu.| -|PREVIEW_IMAGE|Specifies the path to a thumbnail/icon shown in the detail panel when this row is selected. Auto-scaled and centered to fit the panel.| -|PREVIEW_IMAGE_X|Unused - the detail panel always auto-centers the image.| -|PREVIEW_IMAGE_Y|Unused - the detail panel always auto-centers the image.| -|PREVIEW_IMAGE_WIDTH|Unused - the detail panel always auto-scales the image to fit.| -|PREVIEW_IMAGE_HEIGHT|Unused - the detail panel always auto-scales the image to fit.| -|PREVIEW_NEAREST|Grid layout only\*. If set to `TRUE`, the preview is scaled with nearest-neighbor instead of linear filtering, for small pixel-art images.| -|PREVIEW_SQUARE|Grid layout only\*. If set to `TRUE` on the first grid item, the screen uses square tiles instead of the default wide ones.| -|PREVIEW_GRID_COLS|Grid layout only\*. Set on the first grid item to force the number of tile columns.| -|PREVIEW_FIT_CONTAIN|Grid layout only\*. If set to `TRUE`, this row's tile scales the art to fit inside the tile instead of cropping it to fill it. Use for portrait art in a wide/square tile.| -|PREVIEW_HIDE_LABEL|Grid layout only\*. If set to `TRUE`, this row's tile shows no caption below it.| -|DELETE_STR|Optional command string to run when the user holds B (~1 second) on this entry and confirms. Used to make list entries deletable (e.g. the wifi backup restore list).| -|DELETE_CONFIRM_KEY|Optional translation key for the confirmation prompt shown before running DELETE_STR. Falls back to a generic "Delete this item?" if not set.| -|STATE_STR|Optional command string that prints the current on/off state of this entry (its first line of output, case-insensitively `1`/`on`/`y`/`yes`/`true` for on, anything else for off). Presence of this field renders the entry as a toggle switch instead of a plain row; it is only read to draw the switch, not to change behavior, so COMMAND_STR is still what actually flips the setting.| - ->Note: Fields and values are case sensitive. Values should be separated from fields using '=' without spaces. - -\*The grid layout fields are parsed by the shared framework but only used by the Theme Selector's `theme_manager` grid screens; the Options Menu itself always shows a list. - -#### Command String Variables - -COMMAND_STR, DELETE_STR and STATE_STR all support the use of the following variables: - -|Variable|Description| -|---------|------------| -|%options_path%|Path to the folder containing the options binary.| -|%script_dir%|Path to the current script folder set in the options binary.| - -### Theme Colors - -The UI colors are read at startup from `/etc/options_menu/theme.cfg`, one `Key=R,G,B` per line (0-255 each, `#` for comments). A missing file, line or key falls back to the built-in default. - -|Key|Used for| -|---|--------| -|Bg|App background.| -|Border|Divider lines and box outlines.| -|Accent|Selected row border, section title bar and submenu chevrons.| -|SelectedRowBg|Selected row fill. Follows `Bg` unless set.| -|Text|Titles, row labels and button hints.| -|TextDim|Secondary text, e.g. the Module Uninstaller column headers.| -|ScrollArrow|List scroll arrows. Follows `Text` unless set.| -|BadgeLetter|Letter inside a button-hint badge.| -|BadgeA / BadgeADark|A badge fill / rim.| -|BadgeB / BadgeBDark|B badge fill / rim.| -|BadgeX / BadgeXDark|Hold-to-delete badge fill / rim.| -|BadgeY / BadgeYDark|Reserved, currently unused.| -|BadgeStart / BadgeStartDark|Start pill fill / rim.| +The shared framework also reads a few grid-only fields (`PREVIEW_NEAREST`, `PREVIEW_SQUARE`, `PREVIEW_GRID_COLS`, `PREVIEW_FIT_CONTAIN`, `PREVIEW_HIDE_LABEL`). Only Theme Selector's grid screens use them. `PREVIEW_IMAGE_X/Y/WIDTH/HEIGHT` from the original format are accepted and ignored. ### Translations -UI strings live in `/etc/options_menu/language/.lang` as `KEY=Text` lines; the selected language code is stored in `/etc/options_menu/language.cfg`. `en-US` is always loaded first, so a partial translation falls back to English for missing keys. - -A plugin hmod can ship its own strings in `/etc/options_menu//lang/.lang`. Every such folder is merged in at startup, as long as it has an `en-US.lang`. - -### Preview Icons - -The per-command preview images in `/etc/options_menu/images/` (200x140 PNG) and the UI assets in `/etc/options_menu/images/ui/` are rendered from [Bootstrap Icons](https://icons.getbootstrap.com/) glyphs. The assets in `images/ui/` are white and tinted at runtime with the theme colors. A plugin can point `PREVIEW_IMAGE` at any PNG; it is scaled to fit the detail panel. - -### Compiling Options Menu - -#### Docker toolchain (recommended) - -The repo includes a Docker-based ARM cross-compile toolchain, which is the tested and supported way to build a release-ready `.hmod`. It needs only Docker installed - no local SDL2/libpng/ARM toolchain setup required. - - ./build.sh # Linux/macOS/Git Bash - ./build.ps1 # Windows PowerShell - -This builds the toolchain image (`Dockerfile.jessie-armhf`) if needed, then runs `Makefile.docker` inside it, producing `out/*.hmod`. +Strings live in `/etc/options_menu/language/.lang` as `KEY=Text`; the chosen language is in `/etc/options_menu/language.cfg`. `en-US` is always loaded first, so missing keys fall back to English. -Pass `FIX_LIBSTDCXX=1` to also build a `_compat` variant that ships its own `libstdc++.so.6`, for consoles with another hmod that replaces the system one (e.g. RetroArch 1.8.4 Xtreme SC): +Your hmod can ship its own strings in `/etc/options_menu//lang/.lang`. Every such folder with an `en-US.lang` is merged in at startup. - ./build.sh FIX_LIBSTDCXX=1 +### Colors -The version shown in the app and in the `.hmod` name comes from the latest git tag. Pass `MOD_VER` to override it, e.g. for a test build: +`/etc/options_menu/theme.cfg` holds the UI colors, one `Key=R,G,B` per line (0–255, `#` for comments). Missing keys use the defaults. - ./build.sh MOD_VER=test-dev +| Key | Used for | +| --- | --- | +| `Bg` | Background | +| `Border` | Dividers and outlines | +| `Accent` | Selected entry border, section title bar, **›** | +| `SelectedRowBg` | Selected entry fill (follows `Bg` unless set) | +| `Text` / `TextDim` | Main and secondary text | +| `ScrollArrow` | Scroll arrows (follows `Text` unless set) | +| `BadgeLetter` | Letter inside button-hint badges | +| `BadgeA`, `BadgeB`, `BadgeX`, `BadgeStart` (+ `…Dark` rims) | Button-hint badges; X is the hold-to-delete hint | +| `BadgeY` (+ `BadgeYDark`) | Reserved | -#### Releases +### Icons -Pushing a `v*` tag makes the GitHub Actions workflow build the `.hmod` with that tag as its version and publish it as a GitHub Release. Tags with a hyphen (`-alpha`, `-beta`, `-rc`) are published as prereleases. +Preview images (`/etc/options_menu/images/`, 200×140 PNG) and the UI glyphs in `images/ui/` are rendered from [Bootstrap Icons](https://icons.getbootstrap.com/). The `images/ui/` glyphs are white and tinted with the theme colors at runtime. - git tag -a v2.0.0-alpha -m "Options Menu v2.0.0-alpha" - git push origin v2.0.0-alpha +## Building -#### Native / manual build +```sh +git clone --recursive https://github.com/DefKorns/OptionsMenu.git +cd OptionsMenu +./build.sh # out/options_menu.hmod (./build.ps1 on PowerShell) +./build.sh FIX_LIBSTDCXX=1 # out/options_menu_compat.hmod +./build.sh MOD_VER=test-dev # override the version for a test build +``` -The options menu can also be compiled using the plain `Makefile` provided. To compile the options menu you must have the SDL2 and libpng libraries as well as GNU Make installed. To cross-compile the options menu call `make` with the `CROSS_PREFIX` set to the prefix of your cross-compiler toolchain eg. `make CROSS_PREFIX=arm-linux-gnueabihf-`. +`build.sh` builds the [classicmini-cross-toolchain](https://github.com/DefKorns/classicmini-cross-toolchain) Docker image if needed and runs `make` inside it — no local ARM toolchain required. Outside Docker, the same `Makefile` works with SDL2, SDL2_ttf, libpng and `make CROSS_PREFIX=arm-linux-gnueabihf-`. -## Contributions and Thanks +Packaging and versioning come from [hmod-build](https://github.com/DefKorns/hmod-build): the version is the latest `v*` git tag, and the `.hmod` name has no version, so hakchi sees every release as the same mod. Pushing a tag builds both variants and publishes a GitHub Release (a prerelease for tags with a hyphen, e.g. `v2.0.0-alpha5`). -### Contributions +## Credits +- **[CompCom](https://github.com/CompCom)** — the original Options Menu +- **[DefKorns](https://github.com/DefKorns)** — version 2: the new interface, TTF text, theme colors and icons, translations, Network (with Advokaten), Wi-Fi backups, Saved Games, Controller and Diagnostics menus, build tooling - **Swingflip** — Hibernate Mod and Hakchi-Option-Pack scripts -- **BsLeNuL** — Retroarch Configuration scripts -- **Advokaten** — Network commands (with DefKorns) -- **DefKorns** - - Network commands (with Advokaten) - - Wifi Backup, preview image aspect ratio, delete feature (hold B on a saved backup) - - Multi-language localization system (i18n) and translations - - Docker-based ARM cross-compile toolchain and build tooling - - Redesigned "Modern UI" (dialog frame, selection highlight, toggle switches, button-hint badges) - - TTF text rendering, theme colors (`theme.cfg`), preview icons, Saved Games options -- **ThanosRD** — UI Layout/Design assistance (original Classic UI) -- **[MadFranko008](https://www.reddit.com/user/MadFranko008/)** — Original idea for the Saved Games backup/restore options +- **[BsLeNuL](https://github.com/bslenul)** — RetroArch configuration scripts +- **Advokaten** — Network commands +- **ThanosRD** — layout and design of the original interface +- **[MadFranko008](https://www.reddit.com/user/MadFranko008/)** — idea for the Saved Games backup ### Third-party assets -- [Bootstrap Icons](https://icons.getbootstrap.com/) — source glyphs for the preview icons and UI chevrons (MIT License) +- [Bootstrap Icons](https://icons.getbootstrap.com/) — preview icons and UI glyphs (MIT License) - [Noto Sans JP](https://fonts.google.com/noto/specimen/Noto+Sans+JP) — UI font, subset (SIL Open Font License, see `fonts/OFL.txt`) -- [Misaki font](http://littlelimit.net/misaki.htm) by Num Kadoma — 8x8 Katakana and Kanji glyphs +- [Misaki font](http://littlelimit.net/misaki.htm) by Num Kadoma — 8×8 kana and kanji glyphs ### Testing -Extra thanks to DNA64 (viral_dna) and Swingflip for always testing features. +Thanks to DNA64 (viral_dna) and Swingflip for always testing, and to Aranthys, BsLeNuL, DR1001, Patton Plays and ThanosRD. -Also thanks to the following people for testing the options menu: +## License -- Aranthys -- BsLeNuL -- DefKorns -- DR1001 -- Patton Plays -- ThanosRD +GPLv3 or later — see [LICENSE](LICENSE). diff --git a/mod/install b/mod/install index 4630eee..1da8441 100644 --- a/mod/install +++ b/mod/install @@ -11,6 +11,7 @@ for om_file in $om_settings; do [ -f "$om_root/$om_file" ] && mkdir -p "$(dirname "$om_backup/$om_file")" && cp -p "$om_root/$om_file" "$om_backup/$om_file" done for om_unfile in "$installpath/hmod/uninstall-options_deluxe"* \ + "$installpath/hmod/uninstall-options_menu" \ "$installpath/hmod/uninstall-options_menu_"* \ "$installpath/hmod/uninstall-hibernate_mod"* \ "$installpath/hmod/uninstall-Hakchi_Options_Loadscreen_toggle"; do From 7dc5255412db25fd7ce74c4804c2215a752f5764 Mon Sep 17 00:00:00 2001 From: DefKorns Date: Wed, 7 Oct 2026 11:18:24 +0000 Subject: [PATCH 068/104] Rewrite the readme for Options Menu v2 --- README.md | 37 ++++++++++++++++++++++--------------- mod/readme.md | 4 ++-- 2 files changed, 24 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index b7a2b67..293409c 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ [![Build](https://github.com/DefKorns/OptionsMenu/actions/workflows/build.yml/badge.svg)](https://github.com/DefKorns/OptionsMenu/actions/workflows/build.yml) [![License: GPL v3](https://img.shields.io/badge/License-GPLv3-blue.svg)](LICENSE) -A menu you can open at any moment on your NES, SNES, Famicom or Super Famicom Classic — on the Home screen or in the middle of a game — to power off, back up your saves and settings, manage Wi-Fi and RetroArch, tweak the controllers and more. Other hmods plug their own screens into it, like [Theme Selector](https://github.com/DefKorns/om_theme-selector). +A menu you can open at any moment on your NES, SNES, Famicom or Super Famicom Classic — on the Home screen or in the middle of a game — to power off, back up your saves and settings, manage Wi-Fi and RetroArch, tweak the controllers and more. Other hmods can add their own entries and screens to it. This is my continuation of [CompCom's Options Menu](https://github.com/CompCom/OptionsMenu) (last release 1.3.4, 2019). Version 2 is a rewrite of the interface and a large part of the scripts: @@ -50,7 +50,6 @@ The footer always shows the buttons for the current screen. | Network › | IP address, Wi-Fi scan and Wi-Fi config backups | | Saved Games › | Back up and restore your saves. Needs a USB/SD drive | | Language › | Menu language; the menu restarts in the new language | -| Theme Options › | Added by [Theme Selector](https://github.com/DefKorns/om_theme-selector) when it's installed | | Advanced Options › | Controller, diagnostics and system tools | \*Don't shut down or restart from the menu while RetroArch is running: exit RetroArch first, or you may lose saves. @@ -179,7 +178,7 @@ One file per entry, named `cNNNN_Name`; entries are sorted by name. Each line is `COMMAND_STR`, `DELETE_STR` and `STATE_STR` can use `%options_path%` (the folder with the `options` binary) and `%script_dir%` (the current `--scriptPath`). -The shared framework also reads a few grid-only fields (`PREVIEW_NEAREST`, `PREVIEW_SQUARE`, `PREVIEW_GRID_COLS`, `PREVIEW_FIT_CONTAIN`, `PREVIEW_HIDE_LABEL`). Only Theme Selector's grid screens use them. `PREVIEW_IMAGE_X/Y/WIDTH/HEIGHT` from the original format are accepted and ignored. +The shared framework also reads a few grid-only fields (`PREVIEW_NEAREST`, `PREVIEW_SQUARE`, `PREVIEW_GRID_COLS`, `PREVIEW_FIT_CONTAIN`, `PREVIEW_HIDE_LABEL`). The menu itself always shows a list; they're for apps built on the shared framework that use a grid layout. `PREVIEW_IMAGE_X/Y/WIDTH/HEIGHT` from the original format are accepted and ignored. ### Translations @@ -189,19 +188,27 @@ Your hmod can ship its own strings in `/etc/options_menu//lang/. ### Colors -`/etc/options_menu/theme.cfg` holds the UI colors, one `Key=R,G,B` per line (0–255, `#` for comments). Missing keys use the defaults. +`/etc/options_menu/theme.cfg` holds the UI colors, one `Key=R,G,B` per line (0–255, `#` for comments). It's read once at startup: reopen the menu to see a change. A missing file, line or key uses the default. -| Key | Used for | -| --- | --- | -| `Bg` | Background | -| `Border` | Dividers and outlines | -| `Accent` | Selected entry border, section title bar, **›** | -| `SelectedRowBg` | Selected entry fill (follows `Bg` unless set) | -| `Text` / `TextDim` | Main and secondary text | -| `ScrollArrow` | Scroll arrows (follows `Text` unless set) | -| `BadgeLetter` | Letter inside button-hint badges | -| `BadgeA`, `BadgeB`, `BadgeX`, `BadgeStart` (+ `…Dark` rims) | Button-hint badges; X is the hold-to-delete hint | -| `BadgeY` (+ `BadgeYDark`) | Reserved | +| Key | Default | Used for | +| --- | --- | --- | +| `Bg` | `12,15,22` | Background | +| `Border` | `42,44,51` | Dividers, box outlines, unselected tile border | +| `Accent` | `240,196,61` | Selected entry border, section title bar, **›**, highlights | +| `SelectedRowBg` | `20,22,26` | Selected entry and tile fill. If only `Bg` is set, it follows `Bg` (slightly lighter) so rounded corners don't show the old color | +| `Text` | `255,255,255` | Titles, entry labels, button hints | +| `TextDim` | `141,141,147` | Secondary text, e.g. column headers | +| `ScrollArrow` | follows `Text` | List scroll arrows | +| `BadgeLetter` | `28,22,22` | Letter inside the button-hint badges | +| `BadgeA` / `BadgeADark` | `76,175,110` / `46,107,66` | A badge (Select): fill / rim | +| `BadgeB` / `BadgeBDark` | `225,85,84` / `140,52,51` | B badge (Back): fill / rim | +| `BadgeX` / `BadgeXDark` | `108,142,191` / `63,89,127` | Hold-to-delete hint: fill / rim | +| `BadgeStart` / `BadgeStartDark` | `194,168,94` / `116,101,56` | Start pill: fill / rim | +| `BadgeY` / `BadgeYDark` | `201,123,132` / `143,71,80` | Reserved | +| `CheckboxOn` | follows `BadgeB` | Module Uninstaller: marked checkbox | +| `CheckboxOff` | follows `TextDim` | Module Uninstaller: unmarked checkbox | + +Apps built on the same framework read this file too, so they match the menu. ### Icons diff --git a/mod/readme.md b/mod/readme.md index fb7ae53..aa2489b 100644 --- a/mod/readme.md +++ b/mod/readme.md @@ -15,7 +15,7 @@ The Options Menu is a custom, easily extendable menu for your console, letting o - Controller: change the menu button combo, autofire (speed, X/Y as turbo A/B), Home menu combo, Start on the 2nd controller - Diagnostics: system info, temperature, top, benchmark, RetroArch debugger, file structure and kernel log dumps to USB - Module Uninstaller, Clear Cache, USB write access toggle, epilepsy protection (NES) -- Extendable: other hmods (e.g. Theme Selector) add their own menus +- Extendable: other hmods can add their own entries and menus ### How to open @@ -23,7 +23,7 @@ Hold **L + R** for about 1 second, at any point during the console's operation. ## Documentation -Full documentation for the options menu can be found in the [OptionsMenu README](https://github.com/DefKorns/OptionsMenu). +Every menu, step by step, and how to add your own entries from an hmod: [github.com/DefKorns/OptionsMenu/wiki](https://github.com/DefKorns/OptionsMenu/wiki) ## Contributions and Thanks From 67112cd86866fd962feb25674bdc005b1365d0aa Mon Sep 17 00:00:00 2001 From: DefKorns Date: Wed, 7 Oct 2026 14:32:05 +0000 Subject: [PATCH 069/104] Set the hmod category to its tab in the hub --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 698cb36..81ef6b8 100644 --- a/Makefile +++ b/Makefile @@ -10,7 +10,7 @@ MOD_NAME := Options Menu VARIANT := plain endif MOD_CREATOR := CompCom, DefKorns -MOD_CATEGORY := System +MOD_CATEGORY := Options Menu BINARIES := mod/etc/options_menu/options mod/etc/options_menu/optiond mod/etc/options_menu/mod_uninstall/mod_uninstall mod/bin/standby_watchdog mod/etc/options_menu/scripts/gen_splash mod/etc/options_menu/scripts/ChangeCombo CXX = g++ From 3b9b9f56717b595a69e421fe44b0c4ad88ebd5e1 Mon Sep 17 00:00:00 2001 From: DefKorns Date: Wed, 7 Oct 2026 14:56:52 +0000 Subject: [PATCH 070/104] Stop an INTERNAL command and its children when B aborts it Run it in its own process group so B can SIGTERM everything it started, close the output screen right away instead of waiting for a second B, and poll every 20 ms so a quick tap isn't missed. --- src/command.cpp | 71 ++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 61 insertions(+), 10 deletions(-) diff --git a/src/command.cpp b/src/command.cpp index 1a3af3d..1b0f06f 100644 --- a/src/command.cpp +++ b/src/command.cpp @@ -18,6 +18,51 @@ #include #include #include +#include +#include +#include + +// like popen(), but in its own process group so an aborted command can be +// stopped along with everything it started in the background +struct CommandProcess +{ + FILE * out; + pid_t pid; +}; + +static CommandProcess StartCommand(const std::string & command) +{ + int fds[2]; + if(pipe(fds) != 0) + return { nullptr, -1 }; + const pid_t pid = fork(); + if(pid < 0) + { + close(fds[0]); + close(fds[1]); + return { nullptr, -1 }; + } + if(pid == 0) + { + setpgid(0, 0); + dup2(fds[1], STDOUT_FILENO); + close(fds[0]); + close(fds[1]); + execl("/bin/sh", "sh", "-c", command.c_str(), static_cast(nullptr)); + _exit(127); + } + setpgid(pid, pid); + close(fds[1]); + return { fdopen(fds[0], "r"), pid }; +} + +static void FinishCommand(const CommandProcess & process, bool abort) +{ + if(abort) + kill(-process.pid, SIGTERM); + fclose(process.out); + waitpid(process.pid, nullptr, 0); +} // std::stoi throws on non-numeric input; avoid crashing on a bad command file static int SafeStoi(const std::string & value, int fallback = 0) @@ -102,8 +147,9 @@ Command::Command(std::ifstream & in) void Command::RunCommand(SDL_Context & sdl_context, Controller * controller, const ModernChrome & chrome, Color bg) const { std::list textList; - FILE* pipe = popen(command.c_str(), "r"); - if(pipe) + const CommandProcess process = StartCommand(command); + FILE * out = process.out; + if(out) { auto renderer = sdl_context.renderer; SetDrawColor(renderer, UiTheme::Bg); @@ -145,17 +191,19 @@ void Command::RunCommand(SDL_Context & sdl_context, Controller * controller, con sdl_context.EndFrame(); }; - int fd = fileno(pipe); + int fd = fileno(out); fcntl(fd, F_SETFL, O_NONBLOCK); // so a drained-but-still-buffered fgets() below never blocks - while(!feof(pipe)) + const int PollTimeoutMs = 20; // short enough that a quick B tap isn't lost between Update() calls + bool aborted = false; + while(!feof(out)) { // poll just paces the loop now (avoids busy-spinning) - it only reflects // kernel-level readiness, so a burst of lines that all arrive in one // read() would otherwise pile up in stdio's own buffer, invisible to // poll() until some later line's arrival makes it true again struct pollfd pfd{fd, POLLIN, 0}; - poll(&pfd, 1, 100); - while(fgets(buffer, 128, pipe) != nullptr) + poll(&pfd, 1, PollTimeoutMs); + while(fgets(buffer, 128, out) != nullptr) { std::string sBuffer(buffer); int pos = sBuffer.find('\n'); @@ -168,8 +216,8 @@ void Command::RunCommand(SDL_Context & sdl_context, Controller * controller, con textList.erase(textList.begin()); } } - if(!feof(pipe)) - clearerr(pipe); // last fgets() failed because nothing's ready yet, not EOF - stay readable + if(!feof(out)) + clearerr(out); // last fgets() failed because nothing's ready yet, not EOF - stay readable // ignoreInterrupt commands read the controller directly // stop polling here to avoid racing for the same button events @@ -177,16 +225,19 @@ void Command::RunCommand(SDL_Context & sdl_context, Controller * controller, con { controller->Update(); if(controller->GetButtonStatus(B)) + { + aborted = true; break; + } } // no output: skip the output screen if(!textList.empty()) render(); } - pclose(pipe); + FinishCommand(process, aborted); - if(textList.empty()) + if(textList.empty() || aborted) { SetDrawColor(renderer, bg); return; From 72f2f4b3b42098a2ce99f50ac5b331510ce4d645 Mon Sep 17 00:00:00 2001 From: DefKorns Date: Wed, 7 Oct 2026 15:37:22 +0000 Subject: [PATCH 071/104] Refactor and clean up code across multiple files - Removed unnecessary comments and clarified existing ones in draw_helpers.h, texture.cpp, texture.h, uitheme.cpp, uitheme.h, utf8.cpp, utf8.h, gen_splash.cpp, localization.cpp, localization.h, main.cpp, mod_uninstall.cpp, and standby_watchdog.cpp. - Simplified function documentation and improved readability by eliminating redundant explanations. - Adjusted comments to better reflect the purpose of the code and maintain consistency in style. - Ensured that all changes maintain the functionality of the code while enhancing clarity for future developers. --- src/change_combo.cpp | 8 +--- src/command.cpp | 22 +++------- src/command.h | 15 ++++--- src/daemon.cpp | 4 +- src/framework/color.h | 2 - src/framework/controller.cpp | 18 +++------ src/framework/controller.h | 4 +- src/framework/draw_helpers.cpp | 22 ++++------ src/framework/draw_helpers.h | 5 --- src/framework/texture.cpp | 8 ---- src/framework/texture.h | 6 +-- src/framework/uitheme.cpp | 8 +--- src/framework/uitheme.h | 74 +++++++++++----------------------- src/framework/utf8.cpp | 6 +-- src/framework/utf8.h | 3 -- src/gen_splash.cpp | 3 +- src/localization.cpp | 1 - src/localization.h | 5 --- src/main.cpp | 57 +++----------------------- src/mod_uninstall.cpp | 12 +----- src/standby_watchdog.cpp | 22 ++++------ 21 files changed, 73 insertions(+), 232 deletions(-) diff --git a/src/change_combo.cpp b/src/change_combo.cpp index 12f7691..363a926 100644 --- a/src/change_combo.cpp +++ b/src/change_combo.cpp @@ -55,8 +55,6 @@ namespace return "?"; } - // drops any button already held, so the next WaitForPress doesn't catch the - // tail of a press that was meant for the previous step void ConsumeHeldButtons(Controller & controller) { controller.Update(); @@ -82,8 +80,6 @@ namespace std::cout << Translate(key) << std::endl; } - // temp file + rename: an interrupted write must never leave button.cfg - // truncated for daemon.cpp to trip over bool SaveCombo(const std::array & combo) { const std::string tmpPath = ButtonConfigPath + ".tmp"; @@ -98,8 +94,7 @@ namespace int main() { - // RunCommand's popen pipe isn't a tty - stdio defaults to fully buffered - // there, so lines can sit unflushed instead of appearing as they're printed + // stdout is a pipe here, so it is fully buffered unless flushed setvbuf(stdout, nullptr, _IONBF, 0); LoadLanguageFromConfig(OptionsRoot); @@ -126,7 +121,6 @@ int main() combo[i] = WaitForPress(controller); } - // button names (A, B, X...) stay untranslated, same as the footer badges std::cout << Translate("CHANGE_COMBO_CHOSEN") << " " << ButtonName(combo[0]) << " + " << ButtonName(combo[1]) << " + " << ButtonName(combo[2]) << std::endl; std::cout << Translate("CHANGE_COMBO_CONFIRM") << std::endl; ConsumeHeldButtons(controller); diff --git a/src/command.cpp b/src/command.cpp index 1b0f06f..34f07a7 100644 --- a/src/command.cpp +++ b/src/command.cpp @@ -22,8 +22,6 @@ #include #include -// like popen(), but in its own process group so an aborted command can be -// stopped along with everything it started in the background struct CommandProcess { FILE * out; @@ -64,7 +62,6 @@ static void FinishCommand(const CommandProcess & process, bool abort) waitpid(process.pid, nullptr, 0); } -// std::stoi throws on non-numeric input; avoid crashing on a bad command file static int SafeStoi(const std::string & value, int fallback = 0) { try @@ -157,7 +154,6 @@ void Command::RunCommand(SDL_Context & sdl_context, Controller * controller, con const int textX = UiTheme::FrameRect.x + UiTheme::FrameInset; const int textFirstY = UiTheme::HeaderDividerY + 24; - // same app header/footer chrome as the main screen, plus a persistent B/Exit badge Texture exitLetter("B", 16, renderer, 0, 0, false, ToAbgr(UiTheme::BadgeLetter), true); Texture exitLabel(Translate("EXIT"), 16, renderer, 0, 0, false, ToAbgr(UiTheme::Text), true); int badgeGroupW = UiTheme::BadgeOuterSize + UiTheme::BadgeLabelGap + exitLabel.rect.w; @@ -187,20 +183,17 @@ void Command::RunCommand(SDL_Context & sdl_context, Controller * controller, con t.Draw(renderer); y+=10; } - SetDrawColor(renderer, UiTheme::Bg); // must be the LAST color-setting call, or it leaks into next frame's clear + SetDrawColor(renderer, UiTheme::Bg); // must be the last color call, or it leaks into the next frame's clear sdl_context.EndFrame(); }; int fd = fileno(out); - fcntl(fd, F_SETFL, O_NONBLOCK); // so a drained-but-still-buffered fgets() below never blocks - const int PollTimeoutMs = 20; // short enough that a quick B tap isn't lost between Update() calls + fcntl(fd, F_SETFL, O_NONBLOCK); + const int PollTimeoutMs = 20; bool aborted = false; while(!feof(out)) { - // poll just paces the loop now (avoids busy-spinning) - it only reflects - // kernel-level readiness, so a burst of lines that all arrive in one - // read() would otherwise pile up in stdio's own buffer, invisible to - // poll() until some later line's arrival makes it true again + // stdio can hold lines poll() never sees, so fgets drains every pass struct pollfd pfd{fd, POLLIN, 0}; poll(&pfd, 1, PollTimeoutMs); while(fgets(buffer, 128, out) != nullptr) @@ -217,10 +210,9 @@ void Command::RunCommand(SDL_Context & sdl_context, Controller * controller, con } } if(!feof(out)) - clearerr(out); // last fgets() failed because nothing's ready yet, not EOF - stay readable + clearerr(out); - // ignoreInterrupt commands read the controller directly - // stop polling here to avoid racing for the same button events + // ignoreInterrupt commands read the controller themselves if(!ignoreInterrupt) { controller->Update(); @@ -231,7 +223,6 @@ void Command::RunCommand(SDL_Context & sdl_context, Controller * controller, con } } - // no output: skip the output screen if(!textList.empty()) render(); } @@ -263,7 +254,6 @@ void Command::UpdateState() char buffer[64] = {0}; std::string result; - // Prevent stalled state scripts from blocking menu load. struct pollfd pfd{fileno(pipe), POLLIN, 0}; if(poll(&pfd, 1, 1000) > 0 && fgets(buffer, sizeof(buffer), pipe)) result = buffer; diff --git a/src/command.h b/src/command.h index 46f2b13..b23267b 100644 --- a/src/command.h +++ b/src/command.h @@ -14,7 +14,6 @@ class Controller; -// header/footer textures, shared by reference with the main screen struct ModernChrome { Texture & gearIcon; @@ -35,7 +34,7 @@ struct Command std::string deleteCommand; std::string deleteConfirmKey; std::string stateCommand; - std::string enableIfCommand; // shell condition; row is skipped unless it exits 0 + std::string enableIfCommand; bool runInternal = true; bool restartUI = false; bool ignoreInterrupt = false; @@ -46,15 +45,15 @@ struct Command bool hasSubmenu = false; Texture texture; std::string previewImage; - int previewImageX = 920; // default position when a command sets PREVIEW_IMAGE without _X/_Y + int previewImageX = 920; int previewImageY = 300; int previewImageWidth = -1; int previewImageHeight = -1; - bool previewNearest = false; // nearest-neighbor scaling instead of linear - for small pixel-art sprites blown up a lot, where linear just blurs them - bool previewSquare = false; // this screen's preview art is square/near-square - grid tiles should be too, not the default wide tile - int previewGridCols = -1; // explicit grid column count override, unset (-1) defers to the square/wide default - bool previewFitContain = false; // scale to fit within the tile instead of cropping to cover it - for portrait art in a wide/square tile - bool previewHideLabel = false; // no caption below this tile - the art speaks for itself + bool previewNearest = false; + bool previewSquare = false; + int previewGridCols = -1; + bool previewFitContain = false; + bool previewHideLabel = false; }; #endif diff --git a/src/daemon.cpp b/src/daemon.cpp index abfa616..b512912 100644 --- a/src/daemon.cpp +++ b/src/daemon.cpp @@ -24,7 +24,6 @@ int main() if(in.is_open()) { int b1, b2, b3; - // all 3 must parse - a truncated/empty file falls through to auto-detect below if(in >> b1 >> b2 >> b3) { button1 = (GameButton)b1; @@ -37,8 +36,7 @@ int main() if(!haveCustomCombo) { - // no valid user-set combo - NES/Famicom pads have no L/R, fall back to B+Down. - // matches softwareCheck()'s own dp-shvc check in /etc/preinit.d/b0010_functions + // NES/Famicom pads have no L/R, so fall back to B+Down std::ifstream boardtypeFile("/var/squashfs/etc/clover/boardtype"); std::string boardtype; std::getline(boardtypeFile, boardtype); diff --git a/src/framework/color.h b/src/framework/color.h index 2a618ef..5495c9f 100644 --- a/src/framework/color.h +++ b/src/framework/color.h @@ -12,13 +12,11 @@ #include -// opaque RGB; alpha is always 0xFF wherever it's drawn struct Color { Uint8 r, g, b; }; -// packed AABBGGRR, the layout Texture's text color parameter takes constexpr Uint32 ToAbgr(Color color) { return 0xFF000000u | (static_cast(color.b) << 16) | (static_cast(color.g) << 8) | color.r; diff --git a/src/framework/controller.cpp b/src/framework/controller.cpp index b09c298..d210f15 100644 --- a/src/framework/controller.cpp +++ b/src/framework/controller.cpp @@ -20,8 +20,6 @@ #include #endif -// avoids linking SDL just for a millisecond timer - optiond (Controller's -// other caller) doesn't otherwise need it static unsigned int MonotonicMillis() { struct timespec ts; @@ -35,9 +33,7 @@ Controller::Controller(int id) std::stringstream s; s << "/dev/input/by-path/platform-twi." << id << "-event-joystick"; - //Right after boot this node sometimes doesn't exist yet (input driver - //still enumerating) and optiond, started from init.d, hits it before - //it's there. Retry for ~10s instead of exiting immediately. + // the input node can appear a few seconds after boot const int maxAttempts = 50; const useconds_t retryDelay = 200000; for(int attempt = 0; fd == -1 && attempt < maxAttempts; ++attempt) @@ -51,7 +47,7 @@ Controller::Controller(int id) std::cerr << "Cannot access controller.\n"; exit(1); } -#endif // __arm__ +#endif Reset(); } @@ -60,7 +56,7 @@ Controller::~Controller() #ifdef __arm__ if(fd != -1) close(fd); -#endif // __arm__ +#endif } bool Controller::PeekButtonStatus(GameButton button) { @@ -82,7 +78,7 @@ void Controller::Update() for(int i = 0; i < len; ++i) { auto & buttonEvent = buttonBuffer[i]; - //Unk1 equals 1 when displaying button press status + // unk1 == 1 marks a button event if(buttonEvent.unk1 == 1) { buttons[buttonEvent.button] = buttonEvent.pressed; @@ -90,9 +86,7 @@ void Controller::Update() } } #else - // keyboard fallback for desktop builds - only write buttons[] on an edge, - // matching evdev's own press/release-only stream - SDL_PumpEvents(); // nested loops (confirm dialog) don't otherwise pump + SDL_PumpEvents(); const Uint8 * keys = SDL_GetKeyboardState(nullptr); auto press = [&](SDL_Scancode sc, GameButton button) { @@ -113,7 +107,7 @@ void Controller::Update() press(SDL_SCANCODE_TAB, SELECT); press(SDL_SCANCODE_Q, L); press(SDL_SCANCODE_E, R); -#endif // __arm__ +#endif } bool Controller::HeldRepeat(GameButton button) { diff --git a/src/framework/controller.h b/src/framework/controller.h index ee91350..2e41ce7 100644 --- a/src/framework/controller.h +++ b/src/framework/controller.h @@ -32,16 +32,14 @@ class Controller std::map heldPressedAt; std::map heldActive; #ifndef __arm__ - unsigned char prevKeys[512] = {0}; // keyboard fallback for desktop testing builds + unsigned char prevKeys[512] = {0}; #endif public: Controller(int id); ~Controller(); bool PeekButtonStatus(GameButton button); bool GetButtonStatus(GameButton button); - // fires once on press, then repeats after an initial delay while held bool HeldRepeat(GameButton button); - // ms this button has been continuously held, 0 if not currently held unsigned int HeldMillis(GameButton button); void Update(); void Reset(); diff --git a/src/framework/draw_helpers.cpp b/src/framework/draw_helpers.cpp index 2b195ba..089065c 100644 --- a/src/framework/draw_helpers.cpp +++ b/src/framework/draw_helpers.cpp @@ -28,9 +28,6 @@ namespace enum class CornerRegion { Arc, Disk, Outside }; - // offsets (dx, dy) from a corner's circle center, within one quadrant. - // Arc: ring of the given width on the radius. Disk: full quarter-disk. - // Outside: corner-square pixels beyond the radius. Cached per shape. const CornerPoints & CornerOffsets(CornerRegion region, int radius, int width = 0) { static std::map, CornerPoints> cache; @@ -55,20 +52,18 @@ namespace return cache.emplace(key, std::move(points)).first->second; } - // mirrors quadrant offsets into all 4 corners of rect and plots them void DrawMirroredCorners(SDL_Renderer * renderer, SDL_Rect rect, int radius, const CornerPoints & offsets) { const int x0 = rect.x, y0 = rect.y, x1 = rect.x + rect.w - 1, y1 = rect.y + rect.h - 1; - // reused across calls (single render thread) to avoid per-frame heap allocation static CornerPoints points; points.clear(); points.reserve(offsets.size() * 4); for(const SDL_Point & p : offsets) { - points.push_back({ x0 + radius - p.x, y0 + radius - p.y }); // top-left - points.push_back({ x1 - radius + p.x, y0 + radius - p.y }); // top-right - points.push_back({ x0 + radius - p.x, y1 - radius + p.y }); // bottom-left - points.push_back({ x1 - radius + p.x, y1 - radius + p.y }); // bottom-right + points.push_back({ x0 + radius - p.x, y0 + radius - p.y }); + points.push_back({ x1 - radius + p.x, y0 + radius - p.y }); + points.push_back({ x0 + radius - p.x, y1 - radius + p.y }); + points.push_back({ x1 - radius + p.x, y1 - radius + p.y }); } SDL_RenderDrawPoints(renderer, points.data(), static_cast(points.size())); } @@ -90,10 +85,10 @@ void DrawStrokeRect(SDL_Renderer * renderer, SDL_Rect rect, Color color, int wid const int x0 = rect.x, y0 = rect.y, x1 = rect.x + rect.w - 1, y1 = rect.y + rect.h - 1; for(int i = 0; i < width; ++i) { - SDL_RenderDrawLine(renderer, x0 + radius, y0 + i, x1 - radius, y0 + i); // top - SDL_RenderDrawLine(renderer, x0 + radius, y1 - i, x1 - radius, y1 - i); // bottom - SDL_RenderDrawLine(renderer, x0 + i, y0 + radius, x0 + i, y1 - radius); // left - SDL_RenderDrawLine(renderer, x1 - i, y0 + radius, x1 - i, y1 - radius); // right + SDL_RenderDrawLine(renderer, x0 + radius, y0 + i, x1 - radius, y0 + i); + SDL_RenderDrawLine(renderer, x0 + radius, y1 - i, x1 - radius, y1 - i); + SDL_RenderDrawLine(renderer, x0 + i, y0 + radius, x0 + i, y1 - radius); + SDL_RenderDrawLine(renderer, x1 - i, y0 + radius, x1 - i, y1 - radius); } if(radius > 0) DrawMirroredCorners(renderer, rect, radius, CornerOffsets(CornerRegion::Arc, radius, width)); @@ -113,7 +108,6 @@ void DrawRoundedFillRect(SDL_Renderer * renderer, SDL_Rect rect, Color color, in SDL_RenderFillRect(renderer, &rect); return; } - // cross of two bands, corners filled by the quarter-disks const SDL_Rect hBand{ rect.x, rect.y + radius, rect.w, rect.h - 2*radius }; const SDL_Rect vBand{ rect.x + radius, rect.y, rect.w - 2*radius, rect.h }; SDL_RenderFillRect(renderer, &hBand); diff --git a/src/framework/draw_helpers.h b/src/framework/draw_helpers.h index a8585de..d80e9d3 100644 --- a/src/framework/draw_helpers.h +++ b/src/framework/draw_helpers.h @@ -23,16 +23,11 @@ #include void SetDrawColor(SDL_Renderer * renderer, Color color); -// tints a white asset texture void SetColorMod(SDL_Texture * texture, Color color); -// flat-line drawing for Modern UI - no textured art void DrawStrokeRect(SDL_Renderer * renderer, SDL_Rect rect, Color color, int width, int radius = 0); void DrawFillRect(SDL_Renderer * renderer, SDL_Rect rect, Color color); void DrawRoundedFillRect(SDL_Renderer * renderer, SDL_Rect rect, Color color, int radius); -// paints the corner-square pixels outside the rounded radius with a flat -// color - drawn over a rectangularly-clipped image, it fakes rounding it -// without an alpha mask or render target void DrawRoundedCornerMask(SDL_Renderer * renderer, SDL_Rect rect, Color color, int radius); void DrawHLine(SDL_Renderer * renderer, int x0, int x1, int y, Color color, int width = 1); void DrawVLine(SDL_Renderer * renderer, int x, int y0, int y1, Color color, int width = 1); diff --git a/src/framework/texture.cpp b/src/framework/texture.cpp index 56a32dc..c2b60af 100644 --- a/src/framework/texture.cpp +++ b/src/framework/texture.cpp @@ -74,7 +74,6 @@ SDL_Texture * LoadTexturePNG(SDL_Renderer *renderer, std::string file, SDL_Rect std::cerr << "Cannot open png file: " << file << "\n"; return nullptr; } - //Set PNG Format png.format = PNG_FORMAT_RGBA; if(png.width == 0 || png.height == 0) { @@ -87,15 +86,12 @@ SDL_Texture * LoadTexturePNG(SDL_Renderer *renderer, std::string file, SDL_Rect rect->h = png.height; rect->w = png.width; } - //Create surface auto surface = SDL_CreateRGBSurface(0, png.width, png.height, 32, rmask, gmask, bmask, amask); if(surface) { - //Read png data to surface png_image_finish_read(&png, NULL, surface->pixels, 0, NULL); png_image_free(&png); - //Convert surface to texture texture = SDL_CreateTextureFromSurface(renderer, surface); SDL_FreeSurface(surface); } @@ -141,14 +137,12 @@ SDL_Texture * WriteText(const std::string & text, int fontSize, SDL_Renderer* re static std::string ttfFontPath; -// CJK font comes from the optional addon hmod if installed, else the bundled Latin-only one void SetTTFFontPath(const std::string & optionsLocation) { static const char * const cjkFontPath = "/etc/options_menu/fonts/NotoSansJP-CJK.ttf"; ttfFontPath = std::ifstream(cjkFontPath).good() ? cjkFontPath : optionsLocation + "fonts/NotoSansJP-Latin-subset.ttf"; } -// one TTF_Font per point size actually used, opened once and kept for the process lifetime static TTF_Font * GetTTFFont(int pointSize) { static std::map fonts; @@ -173,7 +167,6 @@ SDL_Texture * WriteTextTTF(const std::string & text, int fontSize, SDL_Renderer* return nullptr; } - // color is packed 0xAABBGGRR, same convention as WriteText/the rest of the codebase SDL_Color sdlColor{ static_cast(color & 0xFF), static_cast((color >> 8) & 0xFF), @@ -203,7 +196,6 @@ bool CanRenderWithTTF(const std::string & text, int fontSize) return false; for(unsigned int codepoint : Utf8ToCodepoints(text)) - // TTF_GlyphIsProvided only takes a 16-bit code if(codepoint > 0xFFFF || !TTF_GlyphIsProvided(font, static_cast(codepoint))) return false; return true; diff --git a/src/framework/texture.h b/src/framework/texture.h index 7c95c94..7656ccc 100644 --- a/src/framework/texture.h +++ b/src/framework/texture.h @@ -16,7 +16,7 @@ struct Texture { std::shared_ptr texture; - SDL_Rect rect{0, 0, 0, 0}; // LoadTexturePNG leaves this untouched on failure, so it must default to zero, not garbage + SDL_Rect rect{0, 0, 0, 0}; // LoadTexturePNG leaves it untouched on failure Texture(); Texture(const std::string & text, int fontSize, SDL_Renderer* renderer, int x = 0, int y = 0, bool centerText = false, const int color = 0xFFFFFFFF, bool useTTF = false); Texture(const std::string & pngFilePath, SDL_Renderer* renderer, int x = 0, int y = 0, bool centerImg = false); @@ -28,16 +28,12 @@ SDL_Texture * LoadTexturePNG(SDL_Renderer *renderer, std::string file, SDL_Rect SDL_Texture * WriteText(const std::string & text, int fontSize, SDL_Renderer* renderer, int & textureWidth, int & textureHeight, const int color = 0xFFFFFF); SDL_Texture * WriteTextTTF(const std::string & text, int fontSize, SDL_Renderer* renderer, int & textureWidth, int & textureHeight, const int color = 0xFFFFFF); -// call before any useTTF=true Texture, once at startup void SetTTFFontPath(const std::string & optionsLocation); -// false if the loaded font is missing a glyph in text - fall back to font8x8 bool CanRenderWithTTF(const std::string & text, int fontSize); -// proportional width, unlike font8x8's fixed fontSize/char int MeasureTTFWidth(const std::string & text, int fontSize); -// line-to-line spacing, unlike font8x8's fixed-height rows int GetTTFLineHeight(int fontSize); #endif diff --git a/src/framework/uitheme.cpp b/src/framework/uitheme.cpp index b2b3bcc..1d83801 100644 --- a/src/framework/uitheme.cpp +++ b/src/framework/uitheme.cpp @@ -26,7 +26,7 @@ namespace UiTheme { Color Bg{ 0x0C, 0x0F, 0x16 }; Color Border{ 0x2A, 0x2C, 0x33 }; - Color Accent{ 0xF0, 0xC4, 0x3D }; // yellow + Color Accent{ 0xF0, 0xC4, 0x3D }; Color SelectedRowBg{ 0x14, 0x16, 0x1A }; Color Text{ 0xFF, 0xFF, 0xFF }; Color TextDim{ 0x8D, 0x8D, 0x93 }; @@ -53,7 +53,6 @@ namespace UiTheme bool valid; }; - // "R,G,B", each 0-255 ParsedColor ParseRgb(const std::string & value) { int r = 0, g = 0, b = 0; @@ -108,10 +107,7 @@ namespace UiTheme isSet[key->first] = true; } - // SelectedRowBg (tile/row fill - shows at rounded-corner masks, so a - // stale default next to a customized Bg reads as a leftover old - // color) tracks a custom Bg unless set explicitly itself, keeping - // the same +8/+7/+4 offset as the built-in defaults + // SelectedRowBg follows a custom Bg unless set itself if(isSet["Bg"] && !isSet["SelectedRowBg"]) SelectedRowBg = { ClampedAdd(Bg.r, 8), ClampedAdd(Bg.g, 7), ClampedAdd(Bg.b, 4) }; diff --git a/src/framework/uitheme.h b/src/framework/uitheme.h index 7e3a7de..6535e0d 100644 --- a/src/framework/uitheme.h +++ b/src/framework/uitheme.h @@ -23,48 +23,38 @@ #include #include -// Geometry and palette for the split-panel "settings dialog" look. Screen -// is a fixed 1280x720 (sdl_context.cpp), so geometry stays plain constants. -// Border/dividers are flat drawn lines (see draw_helpers.h), no textured art. -// Colors are extern, not const: LoadThemeConfig (uitheme.cpp) can override -// them from an optional theme.cfg at startup - defaults live there too. namespace UiTheme { - // reads /theme.cfg if present ("Key=R,G,B" per line, - // # comments); missing file or bad lines are silently skipped, existing - // defaults stand void LoadThemeConfig(const std::string & optionsLocation); - // palette - each one is a theme.cfg key of the same name extern Color Bg; - extern Color Border; // dividers, box outlines - extern Color Accent; // selected row border, section title bar, submenu chevrons - extern Color SelectedRowBg; // follows Bg unless set - extern Color Text; // titles, row labels, badge hints - extern Color TextDim; // secondary text - extern Color ScrollArrow; // follows Text unless set - extern Color BadgeLetter; // letter inside a badge circle - extern Color BadgeA; // green + extern Color Border; + extern Color Accent; + extern Color SelectedRowBg; + extern Color Text; + extern Color TextDim; + extern Color ScrollArrow; + extern Color BadgeLetter; + extern Color BadgeA; extern Color BadgeADark; - extern Color BadgeB; // red + extern Color BadgeB; extern Color BadgeBDark; - extern Color BadgeX; // slate-blue + extern Color BadgeX; extern Color BadgeXDark; - extern Color BadgeY; // dusty rose, unused + extern Color BadgeY; extern Color BadgeYDark; - extern Color BadgeStart; // muted gold, for a wide "Start" pill instead of a letter circle + extern Color BadgeStart; extern Color BadgeStartDark; - extern Color CheckboxOn; // follows BadgeB unless set - extern Color CheckboxOff; // follows TextDim unless set + extern Color CheckboxOn; + extern Color CheckboxOff; - // TV overscan safe area const int SafeMarginX = 64; const int SafeMarginY = 36; const int BorderWidth = 3; const int BorderRadius = 14; - const int BoxRadius = 8; // selected row / detail icon box / preview box - const int ContentPadding = 16; // border to content start - const int FrameInset = 24; // text inset for the INTERNAL command output screen + const int BoxRadius = 8; + const int ContentPadding = 16; + const int FrameInset = 24; const SDL_Rect FrameRect{ SafeMarginX, SafeMarginY, 1280 - 2*SafeMarginX, 720 - 2*SafeMarginY }; const int FrameX = FrameRect.x + ContentPadding; @@ -79,16 +69,13 @@ namespace UiTheme const int FooterDividerY = FooterY - FooterGap/2; const SDL_Rect OuterRect{ FrameRect.x, FrameRect.y, FrameRect.w, (FooterY+FooterH+ContentPadding) - FrameRect.y }; - // header/body divider const int HeaderDividerY = FrameY + HeaderH; const int HeaderDividerX = FrameX; const int HeaderDividerW = FrameW; - // gear icon const int GearX = FrameX, GearY = FrameY + 4; const int GearSize = 32; - // "OptionsMenu" + smaller "vX.Y.Z", right of the gear const int TitleFontSize = 24; const int VersionFontSize = 14; const int TitleGap = 12; @@ -96,60 +83,45 @@ namespace UiTheme const int TitleX = GearX + GearSize + TitleGap; const int TitleY = GearY + GearSize / 2; - // current screen's title, left-aligned next to a small accent bar const int SectionTitleFontSize = 28; const int SectionAccentBarW = 3; const int SectionAccentBarH = 26; const int SectionTitleY = HeaderDividerY + 22; - const int SectionTitleX = FrameX + 32 + 14 + SectionAccentBarW; // ListX + accent bar + gap + const int SectionTitleX = FrameX + 32 + 14 + SectionAccentBarW; - // row list (left column) const int ListX = FrameX + 32; const int ListW = 800; - const int ListContentRightX = ListX + ListW - 40; // row box / divider right edge - const int RowControlRightX = ListContentRightX - 16; // switch/chevron align here - matches RowTextX's 16px left inset - const int RowFirstY = SectionTitleY + 46; // clears the section title's own text height at SectionTitleFontSize + const int ListContentRightX = ListX + ListW - 40; + const int RowControlRightX = ListContentRightX - 16; + const int RowFirstY = SectionTitleY + 46; const int RowPitch = 36; - const int RowTextYNudge = -4; // fine-tune vs. pure (slot-h - text-h)/2 centering + const int RowTextYNudge = -4; const int RowTextX = ListX + 16; - const int ListBottomMargin = 10; // gap from the footer divider to the last list row + const int ListBottomMargin = 10; - // detail panel (right column), Modern UI only - just the preview image, - // fixed Y independent of scroll/selection (it must not move as the list - // scrolls). No icon box/title - nothing to show without per-command art. const int DetailX = ListX + ListW + 16; const int DetailW = (FrameX + FrameW - 16) - DetailX; const int PreviewBoxH = 200; - // centered between the header and footer dividers, not anchored to the top const int PreviewBoxY = HeaderDividerY + ((FooterDividerY - HeaderDividerY) - PreviewBoxH) / 2; - // toggle switch const int SwitchRightX = RowControlRightX; const int SwitchW = 40, SwitchH = 16; - // badge cluster (A/B always shown, X shown per-row when it has a delete - // action), now in the footer instead of the header const int BadgeOuterSize = 28; const int BadgeInnerSize = 22; const int BadgeBandY = FooterY + (FooterH - BadgeOuterSize) / 2; const int BadgeClusterRightX = FrameX + FrameW - 8; const int BadgeGroupGap = 24; const int BadgeLabelGap = 6; - // DrawBadge's returned "next left edge" already subtracts BadgeGroupGap - // (24px) from the last-drawn badge's own left edge; this closes the gap - // to land the divider exactly 40px left of that badge, per hardware test const int BadgeDividerGapFromCluster = 16; - // "created by CompCom" footer credit, left-aligned in the footer const int CreditX = FrameX; const int CreditY = FooterY + FooterH/2 - 8; - // scroll chevrons - in the empty gutter left of the list, not over it const int ScrollX = FrameX + (ListX - FrameX) / 2 - 7; const int ScrollUpY = HeaderDividerY + 90; const int ScrollDownY = FooterDividerY - 104; - // asset paths, relative to optionsLocation const char * const AssetBadgeOuter = "/images/ui/badge_outer.png"; const char * const AssetBadgeInner = "/images/ui/badge_inner.png"; const char * const AssetSwitchOn = "/images/ui/switch_on.png"; diff --git a/src/framework/utf8.cpp b/src/framework/utf8.cpp index 74feda8..aeac092 100644 --- a/src/framework/utf8.cpp +++ b/src/framework/utf8.cpp @@ -26,8 +26,6 @@ namespace return (byte & 0xC0) == 0x80; } - // byte count implied by a lead byte; invalid leads count as 1 so callers - // always make progress size_t SequenceLength(unsigned char lead) { if(lead >= 0xF0 && lead <= 0xF4) return 4; @@ -39,7 +37,7 @@ namespace struct DecodeResult { unsigned int codepoint; - bool valid; // false if the sequence is malformed or cut short + bool valid; }; DecodeResult DecodeAt(const std::string & text, size_t pos, size_t length) @@ -77,8 +75,6 @@ std::vector Utf8ToCodepoints(const std::string & text) } else { - // only the lead byte is consumed, so a stray lead can't swallow - // the valid characters after it codepoints.push_back(ReplacementChar); ++pos; } diff --git a/src/framework/utf8.h b/src/framework/utf8.h index 41220bd..4e7f102 100644 --- a/src/framework/utf8.h +++ b/src/framework/utf8.h @@ -21,13 +21,10 @@ #include #include -// decodes UTF-8 into Unicode codepoints - malformed sequences become U+FFFD std::vector Utf8ToCodepoints(const std::string & text); -// number of codepoints, not bytes int Utf8Length(const std::string & text); -// truncates by codepoint, not byte, so multi-byte glyphs (e.g. Japanese) aren't split std::string TruncateUtf8(const std::string & text, int maxCodepoints); #endif diff --git a/src/gen_splash.cpp b/src/gen_splash.cpp index 08dec28..c34d48f 100644 --- a/src/gen_splash.cpp +++ b/src/gen_splash.cpp @@ -22,14 +22,13 @@ namespace constexpr int ScreenW = 1280; constexpr int ScreenH = 720; constexpr int BytesPerPixel = 3; - constexpr int GlyphBits = 8; // font8x8 glyphs are 8x8 bitmaps + constexpr int GlyphBits = 8; constexpr int Scale = 3; constexpr int GlyphPx = GlyphBits * Scale; constexpr int LineGap = GlyphPx / 2; constexpr int LineSpacing = GlyphPx + LineGap; constexpr unsigned char White = 255; - // one line of text, horizontally centered, top edge at topY void DrawTextLine(std::vector & pixels, const std::string & text, int topY) { const std::vector codepoints = Utf8ToCodepoints(text); diff --git a/src/localization.cpp b/src/localization.cpp index 4d2d9eb..5be775e 100644 --- a/src/localization.cpp +++ b/src/localization.cpp @@ -45,7 +45,6 @@ namespace } } - // en-US first so keys missing from a partial translation still resolve void LoadLanguageDir(const std::string & langDir, const std::string & langCode) { LoadLanguageFile(langDir + DefaultLangCode + ".lang"); diff --git a/src/localization.h b/src/localization.h index d9a84ee..b23c2e0 100644 --- a/src/localization.h +++ b/src/localization.h @@ -12,15 +12,10 @@ #include -// optionsRoot is the options_menu install directory, with a trailing slash -// (e.g. "/etc/options_menu/"). Loads /language/, then merges in -// //lang/ for every plugin subfolder that has one. void LoadLanguage(const std::string & optionsRoot, const std::string & langCode); -// reads /language.cfg (en-US if missing or empty) and loads it void LoadLanguageFromConfig(const std::string & optionsRoot); -// key itself when there is no translation for it std::string Translate(const std::string & key); #endif diff --git a/src/main.cpp b/src/main.cpp index a609ed3..5657db1 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -38,7 +38,6 @@ void sReplace(std::string & command, std::string oldString, std::string newStrin } } -// Collapse duplicate slashes for stable path comparisons. std::string CollapseSlashes(const std::string & path) { std::string result; @@ -73,11 +72,9 @@ int main(int argc, char * argv[]) std::string titleString(Translate("OPTIONS_TITLE")); std::string titleKey = "OPTIONS_TITLE"; - // ancestors, root-first: "path,scriptPath,titleKey;..." - inherited via env, no argv needed const char * backStackEnv = getenv("OM_BACK_STACK"); std::string backStack = backStackEnv ? backStackEnv : ""; - //Check for external drive bool usbReady = false; if(auto dir = opendir("/media/hakchi/")) { @@ -85,7 +82,6 @@ int main(int argc, char * argv[]) usbReady = true; } - //Parse args for(int i = 1; i < argc; ++i) { if(strcmp(argv[i], "--commandPath") == 0) @@ -108,10 +104,7 @@ int main(int argc, char * argv[]) commandLocation = CollapseSlashes(commandLocation); scriptLocation = CollapseSlashes(scriptLocation); - // value inherited by child screens as OM_BACK_STACK std::string myEntry = commandLocation + "," + scriptLocation + "," + titleKey; - // a self-relaunch re-enters the same screen with an existing stack entry - // remove it so Back and child screens point to the parent size_t lastSep = backStack.find_last_of(';'); std::string lastEntry = backStack.empty() ? "" : (lastSep == std::string::npos ? backStack : backStack.substr(lastSep + 1)); if(lastEntry == myEntry) @@ -124,12 +117,10 @@ int main(int argc, char * argv[]) sReplace(cmd, "%script_dir%", scriptLocation); }; - //Read commands from command folder std::vector commands; std::ifstream in; if(auto dir = opendir(commandLocation.c_str())) { - //Find all commands in folder std::list fileList; while(auto entry = readdir(dir)) { @@ -152,8 +143,6 @@ int main(int argc, char * argv[]) if(!c.enableIfCommand.empty()) { ExpandTemplates(c.enableIfCommand); - // ENABLE_IF conditions reference $rootfs/$mountpoint like any - // other options_menu script, so source preinit the same way if(system(("source /etc/preinit; script_init; " + c.enableIfCommand).c_str()) != 0) continue; } @@ -176,18 +165,15 @@ int main(int argc, char * argv[]) exit(1); } - // commands[0] below would be UB on an empty vector if(commands.empty()) { std::cerr << "No usable commands in " << commandLocation << "\n"; exit(1); } - // B goes back to the parent, or closes the menu on the root screen (no ancestors) std::string backCommand; if(!backStack.empty()) { - // pop the last entry (parent), re-join the rest for the parent's Back chain std::vector entries; for(size_t start = 0; start < backStack.size();) { @@ -210,7 +196,6 @@ int main(int argc, char * argv[]) std::string backScriptPath = lastEntry.substr(c1+1, c2-c1-1); std::string backTitleKey = lastEntry.substr(c2+1); - // env prefix overrides OM_BACK_STACK for just this launch, to the parent's own remaining ancestors backCommand = "usleep 50000 && OM_BACK_STACK=\"" + remainingStack + "\" " + optionsLocation + "options --commandPath " + backPath + (backScriptPath.empty() ? "" : " --scriptPath " + backScriptPath) + " --title \"" + backTitleKey + "\" &"; @@ -218,12 +203,10 @@ int main(int argc, char * argv[]) int currentCommandId = 0; - //Create SDL Window/Renderer and Controller Handler SDL_Context sdl_context(std::chrono::milliseconds(33), false); auto renderer = sdl_context.renderer; Controller controller(1); - //Create Options Flag to prevent multiple menu launches system("touch /tmp/options.flag"); const Color bg = UiTheme::Bg; @@ -235,9 +218,8 @@ int main(int argc, char * argv[]) Texture badgeOuter(optionsLocation + UiTheme::AssetBadgeOuter, renderer); Texture badgeInner(optionsLocation + UiTheme::AssetBadgeInner, renderer); - //Create Textures for Strings Texture appTitleText("OptionsMenu", UiTheme::TitleFontSize, renderer, UiTheme::TitleX, UiTheme::TitleY, false, ToAbgr(UiTheme::Text), true); - appTitleText.rect.y -= appTitleText.rect.h / 2; // vertically center on the gear (Texture only supports centering both axes together) + appTitleText.rect.y -= appTitleText.rect.h / 2; Texture appVersionText(MOD_VERSION, UiTheme::VersionFontSize, renderer, appTitleText.rect.x + appTitleText.rect.w + UiTheme::VersionGap, UiTheme::TitleY, false, ToAbgr(UiTheme::Text), true); appVersionText.rect.y -= appVersionText.rect.h / 2; Texture titleText(titleString, UiTheme::SectionTitleFontSize, renderer, UiTheme::SectionTitleX, UiTheme::SectionTitleY, false, ToAbgr(UiTheme::Text), true); @@ -245,11 +227,10 @@ int main(int argc, char * argv[]) Texture CompComText("created by CompCom - Modern UI by DefKorns", 16, renderer, UiTheme::CreditX, UiTheme::CreditY, false, ToAbgr(UiTheme::Text), true); Texture scrollUp(optionsLocation + UiTheme::AssetChevronUp, renderer, UiTheme::ScrollX, UiTheme::ScrollUpY); SetColorMod(scrollUp.texture.get(), UiTheme::ScrollArrow); - scrollUp.rect.x -= scrollUp.rect.w / 2; // ScrollX is the gutter's center, not the glyph's left edge + scrollUp.rect.x -= scrollUp.rect.w / 2; Texture scrollDown = scrollUp; scrollDown.rect.y = UiTheme::ScrollDownY; - //Badge cluster (top-right hints): A/B always shown, X only if the row has a delete action struct Badge { Texture letter; Texture label; Color rim; Color fill; }; auto MakeBadge = [&](const char * letter, const char * hintKey, Color rim, Color fill) -> Badge { @@ -257,9 +238,7 @@ int main(int argc, char * argv[]) }; Badge badgeA = MakeBadge("A", "HINT_SELECT", UiTheme::BadgeADark, UiTheme::BadgeA); Badge badgeB = MakeBadge("B", "HINT_BACK", UiTheme::BadgeBDark, UiTheme::BadgeB); - // root screen: B closes the menu instead of going back Badge badgeBExit = MakeBadge("B", "EXIT", UiTheme::BadgeBDark, UiTheme::BadgeB); - // hold-to-delete hint, different color than the real B badge Badge badgeHold = MakeBadge("B", "HINT_DELETE", UiTheme::BadgeXDark, UiTheme::BadgeX); auto DrawBadge = [&](Badge & badge, int rightEdgeX) -> int { @@ -273,7 +252,6 @@ int main(int argc, char * argv[]) badgeInner.rect = { x+innerOffset, y+innerOffset, UiTheme::BadgeInnerSize, UiTheme::BadgeInnerSize }; SetColorMod(badgeInner.texture.get(), badge.fill); badgeInner.Draw(renderer); - // glyph bearing makes the pure-math center look 1px down/left - nudged badge.letter.rect.x = x + (UiTheme::BadgeOuterSize - badge.letter.rect.w)/2 + 1; badge.letter.rect.y = y + (UiTheme::BadgeOuterSize - badge.letter.rect.h)/2 - 1; badge.letter.Draw(renderer); @@ -283,15 +261,13 @@ int main(int argc, char * argv[]) return x - UiTheme::BadgeGroupGap; }; - //Create Command Texture const int ChildIndent = 4*16; const int RowGlyphSize = 16; - const int RowTextGapPx = 16; // gap kept between truncated text and preview image/switch + const int RowTextGapPx = 16; Texture chevronIcon(optionsLocation + UiTheme::AssetChevronRight, renderer); SetColorMod(chevronIcon.texture.get(), UiTheme::Accent); for(Command & c : commands) { - // self-relaunch into the same dir (e.g. language selection) isn't a submenu if(c.hasSubmenu) { size_t pos = c.command.find("--commandPath"); @@ -305,13 +281,11 @@ int main(int argc, char * argv[]) int textX = UiTheme::RowTextX + (c.child ? ChildIndent : 0); std::string label = Translate(c.name); - // Keep previews inside the detail panel. int maxRight = UiTheme::RowControlRightX - RowTextGapPx; bool rowUsesTTF = CanRenderWithTTF(label, RowGlyphSize); int available = std::max(0, maxRight - textX); if(rowUsesTTF) { - // proportional font: shrink by measured width, not a fixed px/char guess if(MeasureTTFWidth(label, RowGlyphSize) > available) { int n = Utf8Length(label); @@ -358,19 +332,16 @@ int main(int argc, char * argv[]) for(int i = 0, count = std::min(DisplayItemCount, commandCount-topListItemNumber); i < count; ++i) { Texture & rowTexture = commands[i+topListItemNumber].texture; - // centered in its slot - TTF glyphs have ascender padding, top-align leaves a gap rowTexture.rect.y = y + (modernRowPitch - rowTexture.rect.h) / 2 + UiTheme::RowTextYNudge; y += modernRowPitch; } } selectedRowRect.y = currentCommand.texture.rect.y - 2; - selectedRowRect.h = currentCommand.texture.rect.h + 4; // wrap the row's actual text height (16px font8x8 or variable-height TTF), not a fixed guess + selectedRowRect.h = currentCommand.texture.rect.h + 4; - // scaled to fit the detail panel box, ignores the command's own previewImageX/Y/W/H if(currentCommand.previewImage.size()) { - // linear only for this one texture - baked in at creation, so toggle around it SDL_SetHint(SDL_HINT_RENDER_SCALE_QUALITY, "1"); PreviewImage = std::make_shared(currentCommand.previewImage, renderer, 0, 0); SDL_SetHint(SDL_HINT_RENDER_SCALE_QUALITY, "0"); @@ -388,7 +359,6 @@ int main(int argc, char * argv[]) }; SetCurrentCommand(0); - // bounded so an all-headers list can't spin forever auto MoveSelection = [&](int step) { int newCommandId = currentCommandId; @@ -418,29 +388,24 @@ int main(int argc, char * argv[]) chevronIcon.rect.y = rowCommand.texture.rect.y + (rowCommand.texture.rect.h - chevronIcon.rect.h) / 2; chevronIcon.Draw(renderer); } - // skip for separators and the last row - nothing to separate there if(!selected && !rowCommand.command.empty() && !isLastOverall) DrawHLine(renderer, UiTheme::ListX, UiTheme::ListContentRightX, rowCommand.texture.rect.y + rowCommand.texture.rect.h + 3, UiTheme::Border); }; - // Draw the background before row text. auto DrawChrome = [&]() { - // one continuous border across header+body+footer, plus the footer divider DrawStrokeRect(renderer, UiTheme::OuterRect, UiTheme::Border, UiTheme::BorderWidth, UiTheme::BorderRadius); gearIcon.Draw(renderer); appTitleText.Draw(renderer); appVersionText.Draw(renderer); DrawHLine(renderer, UiTheme::HeaderDividerX, UiTheme::HeaderDividerX + UiTheme::HeaderDividerW, UiTheme::HeaderDividerY, UiTheme::Border, UiTheme::BorderWidth); DrawHLine(renderer, UiTheme::OuterRect.x, UiTheme::OuterRect.x + UiTheme::OuterRect.w, UiTheme::FooterDividerY, UiTheme::Border, UiTheme::BorderWidth); - // vertically centered on titleText's own rendered height, not a fixed guess int accentBarY = titleText.rect.y + (titleText.rect.h - UiTheme::SectionAccentBarH) / 2; DrawFillRect(renderer, { UiTheme::SectionTitleX - UiTheme::SectionAccentBarW - 14, accentBarY, UiTheme::SectionAccentBarW, UiTheme::SectionAccentBarH }, UiTheme::Accent); DrawRoundedFillRect(renderer, selectedRowRect, UiTheme::SelectedRowBg, UiTheme::BoxRadius); DrawStrokeRect(renderer, selectedRowRect, UiTheme::Accent, 2, UiTheme::BoxRadius); - // no box when there's no preview - an empty frame looks broken if(PreviewImage.get()) { SDL_Rect previewBox{ UiTheme::DetailX, UiTheme::PreviewBoxY, UiTheme::DetailW, UiTheme::PreviewBoxH }; @@ -450,7 +415,6 @@ int main(int argc, char * argv[]) PreviewImage->Draw(renderer); } - // footer: A rightmost, B left of it, hold-to-delete hint further left int rightEdge = UiTheme::BadgeClusterRightX; rightEdge = DrawBadge(badgeA, rightEdge); rightEdge = DrawBadge(backStack.empty() ? badgeBExit : badgeB, rightEdge); @@ -460,13 +424,12 @@ int main(int argc, char * argv[]) DrawVLine(renderer, dividerX, UiTheme::FooterY + 10, UiTheme::FooterY + UiTheme::FooterH - 10, UiTheme::Border, 2); }; - // true if deleted - caller should break out of the main loop auto ConfirmDelete = [&]() -> bool { const std::string & confirmKey = commands[currentCommandId].deleteConfirmKey; Texture confirmTitle(Translate(confirmKey.empty() ? "DELETE_CONFIRM_GENERIC" : confirmKey), 24, renderer, 640, 320, true, ToAbgr(UiTheme::Text), true); Texture confirmHint(Translate("DELETE_CONFIRM_HINT"), 16, renderer, 640, 360, true, ToAbgr(UiTheme::Text), true); - controller.GetButtonStatus(B); // consume the still-held B from the triggering long-press + controller.GetButtonStatus(B); bool confirmed = false; for(;;) { @@ -483,7 +446,7 @@ int main(int argc, char * argv[]) DrawStrokeRect(renderer, UiTheme::FrameRect, UiTheme::Border, UiTheme::BorderWidth, UiTheme::BorderRadius); confirmTitle.Draw(renderer); confirmHint.Draw(renderer); - SetDrawColor(renderer, bg); // flat helpers leave the draw color dirty + SetDrawColor(renderer, bg); sdl_context.EndFrame(); } if(!confirmed) @@ -498,12 +461,10 @@ int main(int argc, char * argv[]) for(;;) { - //Clear Buffer and Update Input State sdl_context.StartFrame(); controller.Update(); SDL_Event e; - //Poll for quit event (CTRL+C)/Close while (SDL_PollEvent(&e)) { if (e.type == SDL_QUIT) { return 0; @@ -519,7 +480,6 @@ int main(int argc, char * argv[]) if(commands[currentCommandId].runInternal) { commands[currentCommandId].RunCommand(sdl_context, &controller, { gearIcon, appTitleText, appVersionText, CompComText }, bg); - // refresh all: preset rows form a radio group if(commands[currentCommandId].isToggle) for(Command & c : commands) if(c.isToggle) @@ -537,7 +497,6 @@ int main(int argc, char * argv[]) else if(controller.HeldRepeat(DOWN)) MoveSelection(1); - // tap = go back, or close the menu on the root screen; hold ~1s = delete bool bHeldNow = controller.PeekButtonStatus(B); if(bHeldNow) { @@ -552,7 +511,6 @@ int main(int argc, char * argv[]) { if(bWasHeld && !bHoldFired) { - // root screen: no backCommand, so this closes the menu instead if(!backCommand.empty()) system(backCommand.c_str()); _exitManager.runExitCommand = backCommand.empty(); @@ -562,7 +520,6 @@ int main(int argc, char * argv[]) } bWasHeld = bHeldNow; - //Draw all textures DrawChrome(); titleText.Draw(renderer); @@ -576,10 +533,8 @@ int main(int argc, char * argv[]) if((topListItemNumber + DisplayItemCount) < commandCount) scrollDown.Draw(renderer, SDL_FLIP_VERTICAL); - // flat-line helpers leave the draw color dirty - reset before EndFrame SetDrawColor(renderer, bg); - //Render Framebuffer and Wait for Next Frame sdl_context.EndFrame(); } diff --git a/src/mod_uninstall.cpp b/src/mod_uninstall.cpp index 916e4cf..dea74e0 100644 --- a/src/mod_uninstall.cpp +++ b/src/mod_uninstall.cpp @@ -23,7 +23,6 @@ namespace { const std::string OptionsLocation = "/etc/options_menu/"; - // never list the menu itself const std::string HiddenModPrefix = "options_deluxe"; const int ListFontSize = 16; const int PanelFontSize = 16; @@ -43,7 +42,6 @@ namespace bool marked = false; }; - // "hakchi pack_list" lines keep their trailing newline std::string TrimTrailingNewline(std::string s) { while(!s.empty() && (s.back() == '\n' || s.back() == '\r')) @@ -51,7 +49,6 @@ namespace return s; } - // Texture falls back to font8x8 (fontSize px per codepoint) when TTF lacks a glyph int TextWidth(const std::string & text, int fontSize) { if(CanRenderWithTTF(text, fontSize)) @@ -69,7 +66,6 @@ namespace return TruncateUtf8(text, n) + "..."; } - // unspaced (CJK) or overlong words break per codepoint std::vector WrapToWidth(const std::string & text, int fontSize, int maxWidth) { std::vector lines; @@ -174,7 +170,6 @@ int main(int argc, char * argv[]) const int RowTextMaxW = CheckboxX - 16 - UiTheme::RowTextX; const int displayCount = std::max(1, (UiTheme::FooterDividerY - UiTheme::ListBottomMargin - UiTheme::RowFirstY) / UiTheme::RowPitch); - // white, tinted per mark state at draw time for(Mod & mod : mods) mod.text = Texture(TruncateToWidth(mod.name, ListFontSize, RowTextMaxW), ListFontSize, renderer, UiTheme::RowTextX, 0, false, ToAbgr(White), true); Texture emptyText(Translate("MOD_UNINSTALL_EMPTY"), ListFontSize, renderer, UiTheme::RowTextX, UiTheme::RowFirstY, false, ToAbgr(UiTheme::TextDim), true); @@ -237,7 +232,6 @@ int main(int argc, char * argv[]) return x - UiTheme::BadgeGroupGap; }; - // Use a wide chip to distinguish Start from Select. Texture startPill("Start", 14, renderer, 0, 0, false, ToAbgr(UiTheme::BadgeLetter), true); Texture startSelectPill("Start + Select", 14, renderer, 0, 0, false, ToAbgr(UiTheme::BadgeLetter), true); Texture uninstallLabel; @@ -251,7 +245,6 @@ int main(int argc, char * argv[]) int y = UiTheme::BadgeBandY; SDL_Rect pillRect{ x, y, pillW, UiTheme::BadgeOuterSize }; DrawRoundedFillRect(renderer, pillRect, UiTheme::BadgeStartDark, pillRect.h/2); - // same ring thickness as the letter badges' outer/inner circle pair int border = (UiTheme::BadgeOuterSize - UiTheme::BadgeInnerSize) / 2; SDL_Rect fillRect{ x+border, y+border, pillW-border*2, UiTheme::BadgeInnerSize }; DrawRoundedFillRect(renderer, fillRect, UiTheme::BadgeStart, fillRect.h/2); @@ -292,7 +285,6 @@ int main(int argc, char * argv[]) summaryCount.rect.x = summaryBox.x + summaryBox.w - PanelPad - summaryCount.rect.w; uninstallLabel = Texture(Translate("HINT_UNINSTALL") + " (" + std::to_string(count) + ")", 16, renderer, 0, 0, false, ToAbgr(UiTheme::Text), true); - // keep the last row for "+N" when the list overflows the panel int shown = (count > SummaryMaxRows) ? SummaryMaxRows - 1 : count; summaryNames.clear(); for(int i = 0; i < shown; ++i) @@ -422,7 +414,7 @@ int main(int argc, char * argv[]) rightEdge = DrawPillBadge(startPill, uninstallLabel, rightEdge); DrawFooterDivider(rightEdge); - SetDrawColor(renderer, bg); // flat helpers leave the draw color dirty + SetDrawColor(renderer, bg); sdl_context.EndFrame(); }; @@ -453,7 +445,7 @@ int main(int argc, char * argv[]) const int listX = 640 - (nameW + BulletTextGap)/2; std::vector noteLines = MakeLines(WrapToWidth(Translate("MOD_UNINSTALL_REBOOT_NOTE"), NoteFontSize, boxW - 48), NoteFontSize, renderer, UiTheme::TextDim); - controller.GetButtonStatus(B); // drop a stale B press + controller.GetButtonStatus(B); bool confirmed = false; for(;;) { diff --git a/src/standby_watchdog.cpp b/src/standby_watchdog.cpp index 14c7cc1..28c58bc 100644 --- a/src/standby_watchdog.cpp +++ b/src/standby_watchdog.cpp @@ -27,31 +27,27 @@ int DisplayMenu() { system("standby reloadImagePayload"); system("cat /tmp/power_menu_screen > /dev/fb0"); - auto fpsTime = std::chrono::milliseconds(33); // Check for input faster - //system("echo DEBUG: Displaying hibernate menu..."); + auto fpsTime = std::chrono::milliseconds(33); for(;;) { c.Update(); - if(c.PeekButtonStatus(A)) //Accept (Hibernate) + if(c.PeekButtonStatus(A)) { - //system("echo DEBUG: Accepted hibernate..."); return 1; } - else if(c.PeekButtonStatus(X)) //Accept (Standby) + else if(c.PeekButtonStatus(X)) { - //system("echo DEBUG: Accepted standby..."); return 2; } - else if(c.PeekButtonStatus(Y)) //Cancel + else if(c.PeekButtonStatus(Y)) { - //system("echo DEBUG: Cancelled hibernate..."); return 3; } else if(pw.buttonPress()) { return 3; } - system("cat /tmp/power_menu_screen > /dev/fb0"); // This will fix the framebuffer getting overwritten by a stack process + system("cat /tmp/power_menu_screen > /dev/fb0"); // another process can overwrite the framebuffer std::this_thread::sleep_until(nextUpdateTime); nextUpdateTime+=fpsTime; } @@ -82,15 +78,13 @@ void Hibernate() for(;;) { c.Update(); - if(c.PeekButtonStatus(L) && c.PeekButtonStatus(R) && c.GetButtonStatus(UP)) //L+R+SELECT = Hibernate / Reboot from Hibernate + if(c.PeekButtonStatus(L) && c.PeekButtonStatus(R) && c.GetButtonStatus(UP)) { - //system("echo Core Temperature at the time of reboot out of hibernation: $(hakchi hwmon)c"); system("echo 1 > /sys/devices/virtual/disp/disp/attr/lcd"); break; } else if(c.PeekButtonStatus(L) && c.PeekButtonStatus(R) && c.GetButtonStatus(DOWN)) { - //system("echo Core Temperature at the time of shutdown out of hibernation: $(hakchi hwmon)c"); system("standby hibernateReboot &"); exit(0); } @@ -111,9 +105,8 @@ void Standby() for(;;) { c.Update(); - if(c.PeekButtonStatus(L) && c.PeekButtonStatus(R) && c.GetButtonStatus(UP)) //L+R+SELECT = Hibernate / Reboot from Hibernate + if(c.PeekButtonStatus(L) && c.PeekButtonStatus(R) && c.GetButtonStatus(UP)) { - //system("echo Core Temperature at the time of reboot out of standby: $(hakchi hwmon)c"); system("echo Rebooting console out from standby mode..."); sync(); setuid(0); @@ -122,7 +115,6 @@ void Standby() } if(c.PeekButtonStatus(L) && c.PeekButtonStatus(R) && c.GetButtonStatus(DOWN)) { - //system("echo Core Temperature at the time of shutdown out of standby: $(hakchi hwmon)c"); system("echo Shutting down console out from standby mode..."); sync(); setuid(0); From 3f393cfdeda3e49016982649cff07d350a9b1243 Mon Sep 17 00:00:00 2001 From: DefKorns Date: Wed, 7 Oct 2026 17:17:33 +0000 Subject: [PATCH 072/104] Share badges and a dimmed confirm dialog across Options Menu screens --- Makefile | 2 +- src/framework/badge.cpp | 92 ++++++++++++++++++++++++++++++++++++++ src/framework/badge.h | 47 ++++++++++++++++++++ src/framework/dialog.cpp | 95 ++++++++++++++++++++++++++++++++++++++++ src/framework/dialog.h | 56 +++++++++++++++++++++++ src/main.cpp | 68 ++++++++++++---------------- src/mod_uninstall.cpp | 84 ++++++++++------------------------- 7 files changed, 341 insertions(+), 103 deletions(-) create mode 100644 src/framework/badge.cpp create mode 100644 src/framework/badge.h create mode 100644 src/framework/dialog.cpp create mode 100644 src/framework/dialog.h diff --git a/Makefile b/Makefile index 81ef6b8..8935ee4 100644 --- a/Makefile +++ b/Makefile @@ -36,7 +36,7 @@ VENDORED_LIBS = endif CXXFLAGS = -std=c++11 -Os $(SDL_CFLAGS) -DMOD_VERSION=\"v$(MOD_VER)\" LDLIBS = $(SDL_LIBS) -SOURCES = src/main.cpp src/command.cpp src/localization.cpp src/framework/sdl_context.cpp src/framework/texture.cpp src/framework/controller.cpp src/framework/powerwatch.cpp src/framework/draw_helpers.cpp src/framework/utf8.cpp src/framework/font8x8_lookup.cpp src/framework/uitheme.cpp +SOURCES = src/main.cpp src/command.cpp src/localization.cpp src/framework/badge.cpp src/framework/dialog.cpp src/framework/sdl_context.cpp src/framework/texture.cpp src/framework/controller.cpp src/framework/powerwatch.cpp src/framework/draw_helpers.cpp src/framework/utf8.cpp src/framework/font8x8_lookup.cpp src/framework/uitheme.cpp OBJECTS = $(SOURCES:.cpp=.o) ALL_OBJECTS = $(OBJECTS) src/daemon.o src/mod_uninstall.o src/standby_watchdog.o src/gen_splash.o src/change_combo.o diff --git a/src/framework/badge.cpp b/src/framework/badge.cpp new file mode 100644 index 0000000..03abb1a --- /dev/null +++ b/src/framework/badge.cpp @@ -0,0 +1,92 @@ +/** + * Copyright (C) 2026 DefKorns + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 3 + * of the License, or (at your option) any later version. + */ + +#include "badge.h" +#include "draw_helpers.h" +#include "uitheme.h" + +namespace +{ + constexpr int BadgeFontSize = 16; + constexpr int PillFontSize = 14; + constexpr int PillPadX = 10; +} + +BadgePainter::BadgePainter(const std::string & optionsLocation, SDL_Renderer * renderer) + : renderer_(renderer) + , outer_(optionsLocation + UiTheme::AssetBadgeOuter, renderer) + , inner_(optionsLocation + UiTheme::AssetBadgeInner, renderer) +{ +} + +Badge BadgePainter::Make(const std::string & letter, const std::string & label, Color rim, Color fill) const +{ + return Badge{ Texture(letter, BadgeFontSize, renderer_, 0, 0, false, ToAbgr(UiTheme::BadgeLetter), true), + Texture(label, BadgeFontSize, renderer_, 0, 0, false, ToAbgr(UiTheme::Text), true), rim, fill, false }; +} + +Badge BadgePainter::MakePill(const std::string & keys, const std::string & label) const +{ + return Badge{ Texture(keys, PillFontSize, renderer_, 0, 0, false, ToAbgr(UiTheme::BadgeLetter), true), + Texture(label, BadgeFontSize, renderer_, 0, 0, false, ToAbgr(UiTheme::Text), true), UiTheme::BadgeStartDark, UiTheme::BadgeStart, true }; +} + +int BadgePainter::MarkWidth(const Badge & badge) const +{ + return badge.pill ? badge.letter.rect.w + 2 * PillPadX : UiTheme::BadgeOuterSize; +} + +int BadgePainter::Width(const Badge & badge) const +{ + return MarkWidth(badge) + UiTheme::BadgeLabelGap + badge.label.rect.w; +} + +int BadgePainter::Draw(Badge & badge, int rightEdgeX, int y) +{ + const int x = rightEdgeX - Width(badge); + const int markW = MarkWidth(badge); + const int border = (UiTheme::BadgeOuterSize - UiTheme::BadgeInnerSize) / 2; + if(badge.pill) + { + const SDL_Rect pillRect{ x, y, markW, UiTheme::BadgeOuterSize }; + DrawRoundedFillRect(renderer_, pillRect, badge.rim, pillRect.h / 2); + const SDL_Rect fillRect{ x + border, y + border, markW - 2 * border, UiTheme::BadgeInnerSize }; + DrawRoundedFillRect(renderer_, fillRect, badge.fill, fillRect.h / 2); + badge.letter.rect.x = x + (markW - badge.letter.rect.w) / 2; + badge.letter.rect.y = y + (UiTheme::BadgeOuterSize - badge.letter.rect.h) / 2; + } + else + { + outer_.rect = { x, y, UiTheme::BadgeOuterSize, UiTheme::BadgeOuterSize }; + SetColorMod(outer_.texture.get(), badge.rim); + outer_.Draw(renderer_); + inner_.rect = { x + border, y + border, UiTheme::BadgeInnerSize, UiTheme::BadgeInnerSize }; + SetColorMod(inner_.texture.get(), badge.fill); + inner_.Draw(renderer_); + badge.letter.rect.x = x + (UiTheme::BadgeOuterSize - badge.letter.rect.w) / 2 + 1; + badge.letter.rect.y = y + (UiTheme::BadgeOuterSize - badge.letter.rect.h) / 2 - 1; + } + badge.letter.Draw(renderer_); + badge.label.rect.x = x + markW + UiTheme::BadgeLabelGap; + badge.label.rect.y = y + (UiTheme::BadgeOuterSize - badge.label.rect.h) / 2; + badge.label.Draw(renderer_); + return x - UiTheme::BadgeGroupGap; +} + +void BadgePainter::DrawCentered(const std::vector & badges, int centerX, int y) +{ + int total = 0; + for(const Badge * badge : badges) + total += Width(*badge); + if(!badges.empty()) + total += static_cast(badges.size() - 1) * UiTheme::BadgeGroupGap; + int rightEdge = centerX + total / 2; + for(auto it = badges.rbegin(); it != badges.rend(); ++it) + rightEdge = Draw(**it, rightEdge, y); +} diff --git a/src/framework/badge.h b/src/framework/badge.h new file mode 100644 index 0000000..6cc846a --- /dev/null +++ b/src/framework/badge.h @@ -0,0 +1,47 @@ +/** + * Copyright (C) 2026 DefKorns + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 3 + * of the License, or (at your option) any later version. + */ + +#ifndef BADGE_H_ +#define BADGE_H_ + +#include "color.h" +#include "texture.h" + +#include +#include + +struct Badge +{ + Texture letter; + Texture label; + Color rim; + Color fill; + bool pill; +}; + +class BadgePainter +{ +public: + BadgePainter(const std::string & optionsLocation, SDL_Renderer * renderer); + + Badge Make(const std::string & letter, const std::string & label, Color rim, Color fill) const; + Badge MakePill(const std::string & keys, const std::string & label) const; + int Width(const Badge & badge) const; + int Draw(Badge & badge, int rightEdgeX, int y); + void DrawCentered(const std::vector & badges, int centerX, int y); + +private: + int MarkWidth(const Badge & badge) const; + + SDL_Renderer * renderer_; + Texture outer_; + Texture inner_; +}; + +#endif diff --git a/src/framework/dialog.cpp b/src/framework/dialog.cpp new file mode 100644 index 0000000..8cfb2e4 --- /dev/null +++ b/src/framework/dialog.cpp @@ -0,0 +1,95 @@ +/** + * Copyright (C) 2026 DefKorns + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 3 + * of the License, or (at your option) any later version. + */ + +#include "dialog.h" +#include "draw_helpers.h" +#include "uitheme.h" + +#include +#include +#include + +namespace +{ + constexpr Uint8 BackdropAlpha = 0xB0; + constexpr int ScreenW = 1280; + constexpr int ScreenH = 720; + constexpr int MinPanelW = 520; + constexpr int TopPad = 36; + constexpr int PanelBorderW = 2; +} + +void Dialog::DrawBackdrop(SDL_Renderer * renderer) +{ + SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_BLEND); + SDL_SetRenderDrawColor(renderer, 0, 0, 0, BackdropAlpha); + const SDL_Rect screen{ 0, 0, ScreenW, ScreenH }; + SDL_RenderFillRect(renderer, &screen); + SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_NONE); +} + +ScreenSnapshot::ScreenSnapshot(SDL_Renderer * renderer) + : renderer_(renderer) +{ + int width = 0; + int height = 0; + SDL_GetRendererOutputSize(renderer, &width, &height); + if(width <= 0 || height <= 0) + return; + const int pitch = width * 4; + std::vector pixels(static_cast(pitch) * height); + if(SDL_RenderReadPixels(renderer, nullptr, SDL_PIXELFORMAT_ARGB8888, pixels.data(), pitch) != 0) + return; + SDL_Texture * texture = SDL_CreateTexture(renderer, SDL_PIXELFORMAT_ARGB8888, SDL_TEXTUREACCESS_STATIC, width, height); + if(!texture) + return; + SDL_UpdateTexture(texture, nullptr, pixels.data(), pitch); + texture_.reset(texture, SDL_DestroyTexture); +} + +void ScreenSnapshot::Draw() const +{ + if(texture_) + SDL_RenderCopy(renderer_, texture_.get(), nullptr, nullptr); +} + +SDL_Rect Dialog::PanelRect(int width, int height) +{ + const int bodyCenterY = (UiTheme::HeaderDividerY + UiTheme::FooterDividerY) / 2; + return SDL_Rect{ ScreenW / 2 - width / 2, bodyCenterY - height / 2, width, height }; +} + +void Dialog::DrawPanel(SDL_Renderer * renderer, const SDL_Rect & panel) +{ + DrawRoundedFillRect(renderer, panel, UiTheme::Bg, UiTheme::BorderRadius); + DrawStrokeRect(renderer, panel, UiTheme::Accent, PanelBorderW, UiTheme::BorderRadius); +} + +ConfirmDialog::ConfirmDialog(SDL_Renderer * renderer, BadgePainter & painter, const std::string & title, Badge confirm, Badge cancel) + : renderer_(renderer) + , painter_(&painter) + , title_(title, Dialog::TitleFontSize, renderer, 0, 0, false, ToAbgr(UiTheme::Text), true) + , confirm_(std::move(confirm)) + , cancel_(std::move(cancel)) +{ + const int badgesW = painter.Width(cancel_) + UiTheme::BadgeGroupGap + painter.Width(confirm_); + const int width = std::max(MinPanelW, std::max(title_.rect.w, badgesW) + 2 * Dialog::PadX); + const int height = TopPad + title_.rect.h + Dialog::BadgeRowGap + UiTheme::BadgeOuterSize + Dialog::BottomPad; + panel_ = Dialog::PanelRect(width, height); + title_.rect.x = panel_.x + (panel_.w - title_.rect.w) / 2; + title_.rect.y = panel_.y + TopPad; +} + +void ConfirmDialog::Draw() +{ + Dialog::DrawBackdrop(renderer_); + Dialog::DrawPanel(renderer_, panel_); + title_.Draw(renderer_); + painter_->DrawCentered({ &cancel_, &confirm_ }, panel_.x + panel_.w / 2, title_.rect.y + title_.rect.h + Dialog::BadgeRowGap); +} diff --git a/src/framework/dialog.h b/src/framework/dialog.h new file mode 100644 index 0000000..85c88a2 --- /dev/null +++ b/src/framework/dialog.h @@ -0,0 +1,56 @@ +/** + * Copyright (C) 2026 DefKorns + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 3 + * of the License, or (at your option) any later version. + */ + +#ifndef DIALOG_H_ +#define DIALOG_H_ + +#include "badge.h" + +#include +#include + +namespace Dialog +{ + constexpr int PadX = 48; + constexpr int TitleFontSize = 24; + constexpr int BadgeRowGap = 36; + constexpr int BottomPad = 32; + + void DrawBackdrop(SDL_Renderer * renderer); + SDL_Rect PanelRect(int width, int height); + void DrawPanel(SDL_Renderer * renderer, const SDL_Rect & panel); +} + +class ScreenSnapshot +{ +public: + explicit ScreenSnapshot(SDL_Renderer * renderer); + void Draw() const; + +private: + SDL_Renderer * renderer_; + std::shared_ptr texture_; +}; + +class ConfirmDialog +{ +public: + ConfirmDialog(SDL_Renderer * renderer, BadgePainter & painter, const std::string & title, Badge confirm, Badge cancel); + void Draw(); + +private: + SDL_Renderer * renderer_; + BadgePainter * painter_; + Texture title_; + Badge confirm_; + Badge cancel_; + SDL_Rect panel_; +}; + +#endif diff --git a/src/main.cpp b/src/main.cpp index 5657db1..1d75310 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -7,6 +7,8 @@ * of the License, or (at your option) any later version. */ +#include "framework/badge.h" +#include "framework/dialog.h" #include "framework/sdl_helper.h" #include "framework/controller.h" #include "framework/powerwatch.h" @@ -215,8 +217,6 @@ int main(int argc, char * argv[]) Texture gearIcon(optionsLocation + UiTheme::AssetGear, renderer, UiTheme::GearX, UiTheme::GearY); Texture switchOn(optionsLocation + UiTheme::AssetSwitchOn, renderer); Texture switchOff(optionsLocation + UiTheme::AssetSwitchOff, renderer); - Texture badgeOuter(optionsLocation + UiTheme::AssetBadgeOuter, renderer); - Texture badgeInner(optionsLocation + UiTheme::AssetBadgeInner, renderer); Texture appTitleText("OptionsMenu", UiTheme::TitleFontSize, renderer, UiTheme::TitleX, UiTheme::TitleY, false, ToAbgr(UiTheme::Text), true); appTitleText.rect.y -= appTitleText.rect.h / 2; @@ -231,10 +231,10 @@ int main(int argc, char * argv[]) Texture scrollDown = scrollUp; scrollDown.rect.y = UiTheme::ScrollDownY; - struct Badge { Texture letter; Texture label; Color rim; Color fill; }; + BadgePainter badges(optionsLocation, renderer); auto MakeBadge = [&](const char * letter, const char * hintKey, Color rim, Color fill) -> Badge { - return { Texture(letter, 16, renderer, 0, 0, false, ToAbgr(UiTheme::BadgeLetter), true), Texture(Translate(hintKey), 16, renderer, 0, 0, false, ToAbgr(UiTheme::Text), true), rim, fill }; + return badges.Make(letter, Translate(hintKey), rim, fill); }; Badge badgeA = MakeBadge("A", "HINT_SELECT", UiTheme::BadgeADark, UiTheme::BadgeA); Badge badgeB = MakeBadge("B", "HINT_BACK", UiTheme::BadgeBDark, UiTheme::BadgeB); @@ -242,23 +242,7 @@ int main(int argc, char * argv[]) Badge badgeHold = MakeBadge("B", "HINT_DELETE", UiTheme::BadgeXDark, UiTheme::BadgeX); auto DrawBadge = [&](Badge & badge, int rightEdgeX) -> int { - int groupW = UiTheme::BadgeOuterSize + UiTheme::BadgeLabelGap + badge.label.rect.w; - int x = rightEdgeX - groupW; - int y = UiTheme::BadgeBandY; - badgeOuter.rect = { x, y, UiTheme::BadgeOuterSize, UiTheme::BadgeOuterSize }; - SetColorMod(badgeOuter.texture.get(), badge.rim); - badgeOuter.Draw(renderer); - int innerOffset = (UiTheme::BadgeOuterSize - UiTheme::BadgeInnerSize) / 2; - badgeInner.rect = { x+innerOffset, y+innerOffset, UiTheme::BadgeInnerSize, UiTheme::BadgeInnerSize }; - SetColorMod(badgeInner.texture.get(), badge.fill); - badgeInner.Draw(renderer); - badge.letter.rect.x = x + (UiTheme::BadgeOuterSize - badge.letter.rect.w)/2 + 1; - badge.letter.rect.y = y + (UiTheme::BadgeOuterSize - badge.letter.rect.h)/2 - 1; - badge.letter.Draw(renderer); - badge.label.rect.x = x + UiTheme::BadgeOuterSize + UiTheme::BadgeLabelGap; - badge.label.rect.y = y + (UiTheme::BadgeOuterSize - badge.label.rect.h)/2; - badge.label.Draw(renderer); - return x - UiTheme::BadgeGroupGap; + return badges.Draw(badge, rightEdgeX, UiTheme::BadgeBandY); }; const int ChildIndent = 4*16; @@ -424,11 +408,28 @@ int main(int argc, char * argv[]) DrawVLine(renderer, dividerX, UiTheme::FooterY + 10, UiTheme::FooterY + UiTheme::FooterH - 10, UiTheme::Border, 2); }; + auto DrawScreen = [&]() + { + DrawChrome(); + titleText.Draw(renderer); + + for(int i = 0, count = std::min(DisplayItemCount, commandCount-topListItemNumber); i < count; ++i) + DrawRow(commands[i+topListItemNumber], (i+topListItemNumber) == commandCount-1); + + CompComText.Draw(renderer); + + if(topListItemNumber != 0) + scrollUp.Draw(renderer); + if((topListItemNumber + DisplayItemCount) < commandCount) + scrollDown.Draw(renderer, SDL_FLIP_VERTICAL); + }; + auto ConfirmDelete = [&]() -> bool { const std::string & confirmKey = commands[currentCommandId].deleteConfirmKey; - Texture confirmTitle(Translate(confirmKey.empty() ? "DELETE_CONFIRM_GENERIC" : confirmKey), 24, renderer, 640, 320, true, ToAbgr(UiTheme::Text), true); - Texture confirmHint(Translate("DELETE_CONFIRM_HINT"), 16, renderer, 640, 360, true, ToAbgr(UiTheme::Text), true); + ConfirmDialog dialog(renderer, badges, Translate(confirmKey.empty() ? "DELETE_CONFIRM_GENERIC" : confirmKey), + MakeBadge("A", "HINT_CONFIRM", UiTheme::BadgeADark, UiTheme::BadgeA), + MakeBadge("B", "HINT_CANCEL", UiTheme::BadgeBDark, UiTheme::BadgeB)); controller.GetButtonStatus(B); bool confirmed = false; for(;;) @@ -442,10 +443,8 @@ int main(int argc, char * argv[]) break; } sdl_context.StartFrame(); - DrawFillRect(renderer, UiTheme::FrameRect, UiTheme::Bg); - DrawStrokeRect(renderer, UiTheme::FrameRect, UiTheme::Border, UiTheme::BorderWidth, UiTheme::BorderRadius); - confirmTitle.Draw(renderer); - confirmHint.Draw(renderer); + DrawScreen(); + dialog.Draw(); SetDrawColor(renderer, bg); sdl_context.EndFrame(); } @@ -520,21 +519,8 @@ int main(int argc, char * argv[]) } bWasHeld = bHeldNow; - DrawChrome(); - titleText.Draw(renderer); - - for(int i = 0, count = std::min(DisplayItemCount, commandCount-topListItemNumber); i < count; ++i) - DrawRow(commands[i+topListItemNumber], (i+topListItemNumber) == commandCount-1); - - CompComText.Draw(renderer); - - if(topListItemNumber != 0) - scrollUp.Draw(renderer); - if((topListItemNumber + DisplayItemCount) < commandCount) - scrollDown.Draw(renderer, SDL_FLIP_VERTICAL); - + DrawScreen(); SetDrawColor(renderer, bg); - sdl_context.EndFrame(); } diff --git a/src/mod_uninstall.cpp b/src/mod_uninstall.cpp index dea74e0..eab0204 100644 --- a/src/mod_uninstall.cpp +++ b/src/mod_uninstall.cpp @@ -7,6 +7,8 @@ * of the License, or (at your option) any later version. */ +#include "framework/badge.h" +#include "framework/dialog.h" #include "framework/sdl_helper.h" #include "framework/controller.h" #include "framework/powerwatch.h" @@ -200,61 +202,20 @@ int main(int argc, char * argv[]) Texture moreText; std::vector summaryNames; - Texture badgeOuter(OptionsLocation + UiTheme::AssetBadgeOuter, renderer); - Texture badgeInner(OptionsLocation + UiTheme::AssetBadgeInner, renderer); - struct Badge { Texture letter; Texture label; Color rim; Color fill; }; + BadgePainter badges(OptionsLocation, renderer); auto MakeBadge = [&](const std::string & letter, const std::string & labelKey, Color rim, Color fill) -> Badge { - return { Texture(letter, 16, renderer, 0, 0, false, ToAbgr(UiTheme::BadgeLetter), true), Texture(Translate(labelKey), 16, renderer, 0, 0, false, ToAbgr(UiTheme::Text), true), rim, fill }; + return badges.Make(letter, Translate(labelKey), rim, fill); }; Badge badgeMark = MakeBadge("A", "HINT_MARK", UiTheme::BadgeADark, UiTheme::BadgeA); Badge badgeUnmark = MakeBadge("A", "HINT_UNMARK", UiTheme::BadgeADark, UiTheme::BadgeA); Badge badgeBack = MakeBadge("B", "HINT_BACK", UiTheme::BadgeBDark, UiTheme::BadgeB); Badge badgeCancel = MakeBadge("B", "HINT_CANCEL", UiTheme::BadgeBDark, UiTheme::BadgeB); + Badge uninstallPill = badges.MakePill("Start", ""); + Badge confirmPill = badges.MakePill("Start + Select", Translate("HINT_CONFIRM")); auto DrawBadge = [&](Badge & badge, int rightEdgeX) -> int { - int groupW = UiTheme::BadgeOuterSize + UiTheme::BadgeLabelGap + badge.label.rect.w; - int x = rightEdgeX - groupW; - int y = UiTheme::BadgeBandY; - badgeOuter.rect = { x, y, UiTheme::BadgeOuterSize, UiTheme::BadgeOuterSize }; - SetColorMod(badgeOuter.texture.get(), badge.rim); - badgeOuter.Draw(renderer); - int innerOffset = (UiTheme::BadgeOuterSize - UiTheme::BadgeInnerSize) / 2; - badgeInner.rect = { x+innerOffset, y+innerOffset, UiTheme::BadgeInnerSize, UiTheme::BadgeInnerSize }; - SetColorMod(badgeInner.texture.get(), badge.fill); - badgeInner.Draw(renderer); - badge.letter.rect.x = x + (UiTheme::BadgeOuterSize - badge.letter.rect.w)/2; - badge.letter.rect.y = y + (UiTheme::BadgeOuterSize - badge.letter.rect.h)/2; - badge.letter.Draw(renderer); - badge.label.rect.x = x + UiTheme::BadgeOuterSize + UiTheme::BadgeLabelGap; - badge.label.rect.y = y + (UiTheme::BadgeOuterSize - badge.label.rect.h)/2; - badge.label.Draw(renderer); - return x - UiTheme::BadgeGroupGap; - }; - - Texture startPill("Start", 14, renderer, 0, 0, false, ToAbgr(UiTheme::BadgeLetter), true); - Texture startSelectPill("Start + Select", 14, renderer, 0, 0, false, ToAbgr(UiTheme::BadgeLetter), true); - Texture uninstallLabel; - Texture confirmLabel(Translate("HINT_CONFIRM"), 16, renderer, 0, 0, false, ToAbgr(UiTheme::Text), true); - auto DrawPillBadge = [&](Texture & pill, Texture & label, int rightEdgeX) -> int - { - const int pillPadX = 10; - int pillW = pill.rect.w + pillPadX*2; - int groupW = pillW + UiTheme::BadgeLabelGap + label.rect.w; - int x = rightEdgeX - groupW; - int y = UiTheme::BadgeBandY; - SDL_Rect pillRect{ x, y, pillW, UiTheme::BadgeOuterSize }; - DrawRoundedFillRect(renderer, pillRect, UiTheme::BadgeStartDark, pillRect.h/2); - int border = (UiTheme::BadgeOuterSize - UiTheme::BadgeInnerSize) / 2; - SDL_Rect fillRect{ x+border, y+border, pillW-border*2, UiTheme::BadgeInnerSize }; - DrawRoundedFillRect(renderer, fillRect, UiTheme::BadgeStart, fillRect.h/2); - pill.rect.x = x + (pillW - pill.rect.w)/2; - pill.rect.y = y + (UiTheme::BadgeOuterSize - pill.rect.h)/2; - pill.Draw(renderer); - label.rect.x = x + pillW + UiTheme::BadgeLabelGap; - label.rect.y = y + (UiTheme::BadgeOuterSize - label.rect.h)/2; - label.Draw(renderer); - return x - UiTheme::BadgeGroupGap; + return badges.Draw(badge, rightEdgeX, UiTheme::BadgeBandY); }; auto DrawFooterDivider = [&](int rightEdge) { @@ -283,7 +244,7 @@ int main(int argc, char * argv[]) markedCount = count; summaryCount = Texture(std::to_string(count), PanelHeaderFontSize, renderer, 0, summaryBox.y + 14, false, ToAbgr(count ? UiTheme::CheckboxOn : UiTheme::TextDim), true); summaryCount.rect.x = summaryBox.x + summaryBox.w - PanelPad - summaryCount.rect.w; - uninstallLabel = Texture(Translate("HINT_UNINSTALL") + " (" + std::to_string(count) + ")", 16, renderer, 0, 0, false, ToAbgr(UiTheme::Text), true); + uninstallPill.label = Texture(Translate("HINT_UNINSTALL") + " (" + std::to_string(count) + ")", 16, renderer, 0, 0, false, ToAbgr(UiTheme::Text), true); int shown = (count > SummaryMaxRows) ? SummaryMaxRows - 1 : count; summaryNames.clear(); @@ -399,9 +360,8 @@ int main(int argc, char * argv[]) } }; - auto render = [&]() + auto DrawScreen = [&]() { - sdl_context.StartFrame(); DrawChrome(); DrawList(); DrawSummary(); @@ -411,9 +371,14 @@ int main(int argc, char * argv[]) rightEdge = DrawBadge(mods[listOffset + currentId].marked ? badgeUnmark : badgeMark, rightEdge); rightEdge = DrawBadge(badgeBack, rightEdge); if(markedCount > 0) - rightEdge = DrawPillBadge(startPill, uninstallLabel, rightEdge); + rightEdge = DrawBadge(uninstallPill, rightEdge); DrawFooterDivider(rightEdge); + }; + auto render = [&]() + { + sdl_context.StartFrame(); + DrawScreen(); SetDrawColor(renderer, bg); sdl_context.EndFrame(); }; @@ -428,9 +393,9 @@ int main(int argc, char * argv[]) const int listRows = shown + (overflow ? 1 : 0); const int boxW = 560; - const int boxH = 150 + listRows*namePitch; - const int bodyCenterY = (UiTheme::HeaderDividerY + UiTheme::FooterDividerY) / 2; - const SDL_Rect box{ 640 - boxW/2, bodyCenterY - boxH/2, boxW, boxH }; + const int badgeRowH = Dialog::BadgeRowGap + UiTheme::BadgeOuterSize; + const int boxH = 150 + listRows*namePitch + badgeRowH; + const SDL_Rect box = Dialog::PanelRect(boxW, boxH); Texture confirmTitle(Translate("MOD_UNINSTALL_CONFIRM"), 24, renderer, 640, box.y + 36, true, ToAbgr(UiTheme::Text), true); std::vector names; @@ -463,9 +428,9 @@ int main(int argc, char * argv[]) } sdl_context.StartFrame(); - DrawChrome(); - DrawRoundedFillRect(renderer, box, UiTheme::SelectedRowBg, UiTheme::BorderRadius); - DrawStrokeRect(renderer, box, UiTheme::Accent, 2, UiTheme::BorderRadius); + DrawScreen(); + Dialog::DrawBackdrop(renderer); + Dialog::DrawPanel(renderer, box); confirmTitle.Draw(renderer); int y = box.y + 78; for(int i = 0; i < static_cast(names.size()); ++i) @@ -477,7 +442,7 @@ int main(int argc, char * argv[]) names[i].Draw(renderer); y += namePitch; } - int noteY = box.y + box.h - 34 - (static_cast(noteLines.size()) - 1) * 10; + int noteY = box.y + box.h - badgeRowH - 34 - (static_cast(noteLines.size()) - 1) * 10; for(Texture & line : noteLines) { line.rect.x = 640 - line.rect.w/2; @@ -486,10 +451,7 @@ int main(int argc, char * argv[]) noteY += 20; } - int rightEdge = UiTheme::BadgeClusterRightX; - rightEdge = DrawBadge(badgeCancel, rightEdge); - rightEdge = DrawPillBadge(startSelectPill, confirmLabel, rightEdge); - DrawFooterDivider(rightEdge); + badges.DrawCentered({ &badgeCancel, &confirmPill }, box.x + box.w / 2, box.y + box.h - Dialog::BottomPad - UiTheme::BadgeOuterSize); SetDrawColor(renderer, bg); sdl_context.EndFrame(); From f7fc180af603463dab5d83c3257cb1ee14d8ce64 Mon Sep 17 00:00:00 2001 From: DefKorns Date: Wed, 7 Oct 2026 22:16:00 +0000 Subject: [PATCH 073/104] Read every Clovercon pad, including hakchi's virtual USB pads The Controller only opened the front port, so a USB pad (exposed by sntool as a virtual 'Nintendo Clovercon - controllerN') could not open the menu or navigate any screen, and without a front pad optiond exited after 10 s. Open every input device named Nintendo Clovercon, keep waiting until one shows up, rescan once a second for new pads and drop any pad that goes away, front port included. --- src/framework/controller.cpp | 92 +++++++++++++++++++++++++++--------- src/framework/controller.h | 8 +++- 2 files changed, 76 insertions(+), 24 deletions(-) diff --git a/src/framework/controller.cpp b/src/framework/controller.cpp index d210f15..9acb07e 100644 --- a/src/framework/controller.cpp +++ b/src/framework/controller.cpp @@ -12,14 +12,23 @@ #include #include #include +#include #include +#include +#include +#include #include -#include #include #ifndef __arm__ #include #endif +namespace +{ + const std::string CloverconNamePrefix = "Nintendo Clovercon"; + constexpr unsigned int RescanIntervalMs = 1000; +} + static unsigned int MonotonicMillis() { struct timespec ts; @@ -27,26 +36,21 @@ static unsigned int MonotonicMillis() return static_cast(ts.tv_sec)*1000 + ts.tv_nsec/1000000; } -Controller::Controller(int id) +Controller::Controller(int) { #ifdef __arm__ - std::stringstream s; - s << "/dev/input/by-path/platform-twi." << id << "-event-joystick"; - // the input node can appear a few seconds after boot const int maxAttempts = 50; const useconds_t retryDelay = 200000; - for(int attempt = 0; fd == -1 && attempt < maxAttempts; ++attempt) + for(int attempt = 0; attempt < maxAttempts; ++attempt) { - fd = open(s.str().c_str(), O_RDONLY | O_NONBLOCK); - if(fd == -1) - usleep(retryDelay); - } - if(fd == -1) - { - std::cerr << "Cannot access controller.\n"; - exit(1); + ScanCloverconPads(); + if(!fds.empty()) + break; + usleep(retryDelay); } + if(fds.empty()) + std::cerr << "No controller yet, waiting for one.\n"; #endif Reset(); @@ -54,10 +58,39 @@ Controller::Controller(int id) Controller::~Controller() { #ifdef __arm__ - if(fd != -1) + for(int fd : fds) close(fd); #endif } +void Controller::OpenNode(const std::string & node) +{ + if(std::find(openNodes.begin(), openNodes.end(), node) != openNodes.end()) + return; + const int fd = open(("/dev/input/" + node).c_str(), O_RDONLY | O_NONBLOCK); + if(fd == -1) + return; + fds.push_back(fd); + openNodes.push_back(node); +} +void Controller::ScanCloverconPads() +{ + lastScan = MonotonicMillis(); + DIR * dir = opendir("/sys/class/input"); + if(!dir) + return; + while(dirent * entry = readdir(dir)) + { + const std::string node = entry->d_name; + if(node.compare(0, 5, "event") != 0) + continue; + std::ifstream nameFile("/sys/class/input/" + node + "/device/name"); + std::string name; + std::getline(nameFile, name); + if(name.compare(0, CloverconNamePrefix.size(), CloverconNamePrefix) == 0) + OpenNode(node); + } + closedir(dir); +} bool Controller::PeekButtonStatus(GameButton button) { return buttons[button]; @@ -71,20 +104,33 @@ bool Controller::GetButtonStatus(GameButton button) void Controller::Update() { #ifdef __arm__ - int len; - while((len = read(fd, &buttonBuffer, sizeof(ButtonEvent)*10)) > 0) + for(size_t device = 0; device < fds.size();) { - len/=sizeof(ButtonEvent); - for(int i = 0; i < len; ++i) + int len; + while((len = read(fds[device], &buttonBuffer, sizeof(ButtonEvent)*10)) > 0) { - auto & buttonEvent = buttonBuffer[i]; - // unk1 == 1 marks a button event - if(buttonEvent.unk1 == 1) + len/=sizeof(ButtonEvent); + for(int i = 0; i < len; ++i) { - buttons[buttonEvent.button] = buttonEvent.pressed; + auto & buttonEvent = buttonBuffer[i]; + // unk1 == 1 marks a button event + if(buttonEvent.unk1 == 1) + { + buttons[buttonEvent.button] = buttonEvent.pressed; + } } } + if(len == -1 && errno == ENODEV) + { + close(fds[device]); + fds.erase(fds.begin() + device); + openNodes.erase(openNodes.begin() + device); + continue; + } + ++device; } + if(MonotonicMillis() - lastScan >= RescanIntervalMs) + ScanCloverconPads(); #else SDL_PumpEvents(); const Uint8 * keys = SDL_GetKeyboardState(nullptr); diff --git a/src/framework/controller.h b/src/framework/controller.h index 2e41ce7..d407c93 100644 --- a/src/framework/controller.h +++ b/src/framework/controller.h @@ -9,6 +9,8 @@ #ifndef CONTROLLER_H_ #define CONTROLLER_H_ #include +#include +#include enum GameButton : uint16_t { A=304, B=305, X=307, Y=308, L=310, R=311, SELECT=314, START=315, LEFT=704, RIGHT=705, UP=706, DOWN=707 }; @@ -24,7 +26,9 @@ struct ButtonEvent class Controller { private: - int fd = -1; + std::vector fds; + std::vector openNodes; + unsigned int lastScan = 0; ButtonEvent buttonBuffer[10]; std::map buttons; std::map repeatPressedAt, repeatLastFired; @@ -34,6 +38,8 @@ class Controller #ifndef __arm__ unsigned char prevKeys[512] = {0}; #endif + void OpenNode(const std::string & node); + void ScanCloverconPads(); public: Controller(int id); ~Controller(); From a07a5fd588a49b5cf02b3cb4587f69155711f889 Mon Sep 17 00:00:00 2001 From: DefKorns Date: Thu, 8 Oct 2026 12:12:24 +0000 Subject: [PATCH 074/104] Add a loading splash with an animated progress bar to gen_splash gen_splash --loading draws the translated LOADING string straight to the visible page of /dev/fb0 and slides an Accent segment across a Border track below it. It detaches right away and keeps animating until the script that called it exits, so callers need no background job or pid. Adds the shared LOADING string to every language. --- Makefile | 2 +- mod/etc/options_menu/language/de-DE.lang | 1 + mod/etc/options_menu/language/en-US.lang | 1 + mod/etc/options_menu/language/es-ES.lang | 1 + mod/etc/options_menu/language/fr-FR.lang | 1 + mod/etc/options_menu/language/it-IT.lang | 1 + mod/etc/options_menu/language/ja-JP.lang | 1 + mod/etc/options_menu/language/nl-NL.lang | 1 + mod/etc/options_menu/language/pt-PT.lang | 1 + mod/etc/options_menu/language/ru-RU.lang | 1 + src/gen_splash.cpp | 242 +++++++++++++++++++++-- 11 files changed, 231 insertions(+), 22 deletions(-) diff --git a/Makefile b/Makefile index 8935ee4..d1df003 100644 --- a/Makefile +++ b/Makefile @@ -93,7 +93,7 @@ mod/bin/standby_watchdog: src/standby_watchdog.o src/framework/controller.o src/ $(CROSS_PREFIX)$(STRIP) $@ upx --lzma $@ -mod/etc/options_menu/scripts/gen_splash: src/gen_splash.o src/localization.o src/framework/utf8.o src/framework/font8x8_lookup.o +mod/etc/options_menu/scripts/gen_splash: src/gen_splash.o src/localization.o src/framework/utf8.o src/framework/font8x8_lookup.o src/framework/uitheme.o $(CROSS_PREFIX)$(CXX) $^ $(PNG_LIBS) $(LDFLAGS) $(STDCXX_RPATH) -o $@ $(CROSS_PREFIX)$(STRIP) $@ diff --git a/mod/etc/options_menu/language/de-DE.lang b/mod/etc/options_menu/language/de-DE.lang index 58977e6..dfc7ca5 100644 --- a/mod/etc/options_menu/language/de-DE.lang +++ b/mod/etc/options_menu/language/de-DE.lang @@ -43,6 +43,7 @@ RA_TOGGLE_LOAD_SCREENS=RA- und Canoe-Ladebildschirme umschalten RESTORE_WIFI_CONFIG=Wifi-Konfiguration wiederherstellen OPTIONS_TITLE=Optionen PRESS_B_EXIT=B drücken zum Beenden. +LOADING=Wird geladen... WIFI_BACKUP_SPLASH_LINE1=Wifi-Konfigurationsdatei wird gesichert WIFI_BACKUP_SPLASH_LINE2=Bitte warten . . . DELETE_BACKUP_CONFIRM=Diese Sicherung löschen? diff --git a/mod/etc/options_menu/language/en-US.lang b/mod/etc/options_menu/language/en-US.lang index 8867aa8..5dbf0d3 100644 --- a/mod/etc/options_menu/language/en-US.lang +++ b/mod/etc/options_menu/language/en-US.lang @@ -44,6 +44,7 @@ RA_TOGGLE_LOAD_SCREENS=Toggle RA and Canoe load screens RESTORE_WIFI_CONFIG=Restore WIFI Config OPTIONS_TITLE=Options PRESS_B_EXIT=Press B to exit. +LOADING=Loading... WIFI_BACKUP_SPLASH_LINE1=Backup Wifi Config File WIFI_BACKUP_SPLASH_LINE2=Please Wait . . . DELETE_BACKUP_CONFIRM=Delete this backup? diff --git a/mod/etc/options_menu/language/es-ES.lang b/mod/etc/options_menu/language/es-ES.lang index ca66605..2433d8a 100644 --- a/mod/etc/options_menu/language/es-ES.lang +++ b/mod/etc/options_menu/language/es-ES.lang @@ -43,6 +43,7 @@ RA_TOGGLE_LOAD_SCREENS=Alternar pantallas de carga de RA y Canoe RESTORE_WIFI_CONFIG=Restaurar configuración Wifi OPTIONS_TITLE=Opciones PRESS_B_EXIT=Pulsa B para salir. +LOADING=Cargando... WIFI_BACKUP_SPLASH_LINE1=Copiando archivo de configuración Wifi WIFI_BACKUP_SPLASH_LINE2=Espere por favor . . . DELETE_BACKUP_CONFIRM=¿Borrar esta copia de seguridad? diff --git a/mod/etc/options_menu/language/fr-FR.lang b/mod/etc/options_menu/language/fr-FR.lang index 3dddcb5..2f39bb4 100644 --- a/mod/etc/options_menu/language/fr-FR.lang +++ b/mod/etc/options_menu/language/fr-FR.lang @@ -43,6 +43,7 @@ RA_TOGGLE_LOAD_SCREENS=Activer/désactiver les écrans de chargement RA et Canoe RESTORE_WIFI_CONFIG=Restaurer la config Wifi OPTIONS_TITLE=Options PRESS_B_EXIT=Appuyez sur B pour quitter. +LOADING=Chargement... WIFI_BACKUP_SPLASH_LINE1=Sauvegarde du fichier de config Wifi WIFI_BACKUP_SPLASH_LINE2=Veuillez patienter . . . DELETE_BACKUP_CONFIRM=Supprimer cette sauvegarde? diff --git a/mod/etc/options_menu/language/it-IT.lang b/mod/etc/options_menu/language/it-IT.lang index 62fddd3..21f74c3 100644 --- a/mod/etc/options_menu/language/it-IT.lang +++ b/mod/etc/options_menu/language/it-IT.lang @@ -43,6 +43,7 @@ RA_TOGGLE_LOAD_SCREENS=Attiva/disattiva schermate di caricamento RA e Canoe RESTORE_WIFI_CONFIG=Ripristina configurazione Wifi OPTIONS_TITLE=Opzioni PRESS_B_EXIT=Premi B per uscire. +LOADING=Caricamento... WIFI_BACKUP_SPLASH_LINE1=Backup del file di configurazione Wifi WIFI_BACKUP_SPLASH_LINE2=Attendere prego . . . DELETE_BACKUP_CONFIRM=Eliminare questo backup? diff --git a/mod/etc/options_menu/language/ja-JP.lang b/mod/etc/options_menu/language/ja-JP.lang index a8379ed..e20cdfd 100644 --- a/mod/etc/options_menu/language/ja-JP.lang +++ b/mod/etc/options_menu/language/ja-JP.lang @@ -43,6 +43,7 @@ RA_TOGGLE_LOAD_SCREENS=RAとCanoeのLoadingがめんをきりかえ RESTORE_WIFI_CONFIG=Wifiせっていをもどす OPTIONS_TITLE=せってい PRESS_B_EXIT=Bでおわる +LOADING=よみこみちゅう... WIFI_BACKUP_SPLASH_LINE1=Wifiせってい Fileの Backup WIFI_BACKUP_SPLASH_LINE2=おまちください . . . DELETE_BACKUP_CONFIRM=このBackupをさくじょしますか? diff --git a/mod/etc/options_menu/language/nl-NL.lang b/mod/etc/options_menu/language/nl-NL.lang index 9535c7f..156b9a3 100644 --- a/mod/etc/options_menu/language/nl-NL.lang +++ b/mod/etc/options_menu/language/nl-NL.lang @@ -43,6 +43,7 @@ RA_TOGGLE_LOAD_SCREENS=RA- en Canoe-laadschermen schakelen RESTORE_WIFI_CONFIG=Wifi-configuratie herstellen OPTIONS_TITLE=Opties PRESS_B_EXIT=Druk op B om af te sluiten. +LOADING=Laden... WIFI_BACKUP_SPLASH_LINE1=Wifi-configuratiebestand back-uppen WIFI_BACKUP_SPLASH_LINE2=Even geduld . . . DELETE_BACKUP_CONFIRM=Deze back-up verwijderen? diff --git a/mod/etc/options_menu/language/pt-PT.lang b/mod/etc/options_menu/language/pt-PT.lang index ece3316..da7fbb6 100644 --- a/mod/etc/options_menu/language/pt-PT.lang +++ b/mod/etc/options_menu/language/pt-PT.lang @@ -43,6 +43,7 @@ RA_TOGGLE_LOAD_SCREENS=Alternar Ecrãs de Carregamento RA e Canoe RESTORE_WIFI_CONFIG=Restaurar Configuração Wifi OPTIONS_TITLE=Opções PRESS_B_EXIT=Prime B para sair. +LOADING=A carregar... WIFI_BACKUP_SPLASH_LINE1=Cópia de Segurança Wifi WIFI_BACKUP_SPLASH_LINE2=Aguarde Por Favor . . . DELETE_BACKUP_CONFIRM=Apagar esta cópia de segurança? diff --git a/mod/etc/options_menu/language/ru-RU.lang b/mod/etc/options_menu/language/ru-RU.lang index e9d63fc..cdd5790 100644 --- a/mod/etc/options_menu/language/ru-RU.lang +++ b/mod/etc/options_menu/language/ru-RU.lang @@ -43,6 +43,7 @@ RA_TOGGLE_LOAD_SCREENS=Переключить экраны загрузки RA RESTORE_WIFI_CONFIG=Восстановить настройки Wifi OPTIONS_TITLE=Настройки PRESS_B_EXIT=Нажмите B для выхода. +LOADING=Загрузка... WIFI_BACKUP_SPLASH_LINE1=Резервное копирование Wifi WIFI_BACKUP_SPLASH_LINE2=Пожалуйста, подождите . . . DELETE_BACKUP_CONFIRM=Удалить эту резервную копию? diff --git a/src/gen_splash.cpp b/src/gen_splash.cpp index c34d48f..48d2f28 100644 --- a/src/gen_splash.cpp +++ b/src/gen_splash.cpp @@ -9,12 +9,22 @@ #include "localization.h" #include "framework/font8x8_lookup.h" +#include "framework/uitheme.h" #include "framework/utf8.h" +#include +#include +#include #include #include #include +#include +#include #include +#include +#include +#include +#include namespace { @@ -29,7 +39,16 @@ namespace constexpr int LineSpacing = GlyphPx + LineGap; constexpr unsigned char White = 255; - void DrawTextLine(std::vector & pixels, const std::string & text, int topY) + constexpr int BarW = 320; + constexpr int BarH = 8; + constexpr int BarGap = 32; + constexpr int SegmentW = 80; + constexpr int SweepMs = 1400; + constexpr int TickMs = 33; + + using Pixels = std::vector; + + void DrawTextLine(Pixels & pixels, const std::string & text, int topY) { const std::vector codepoints = Utf8ToCodepoints(text); const int lineW = static_cast(codepoints.size()) * GlyphPx; @@ -57,37 +76,218 @@ namespace } } } + + struct TextBlock + { + Pixels pixels; + int bottomY; + }; + + std::string WithoutTrailingEllipsis(std::string text) + { + const std::string ellipsis = "…"; + for(;;) + { + if(!text.empty() && (text.back() == '.' || text.back() == ' ')) + text.pop_back(); + else if(text.size() >= ellipsis.size() && text.compare(text.size() - ellipsis.size(), ellipsis.size(), ellipsis) == 0) + text.resize(text.size() - ellipsis.size()); + else + return text; + } + } + + TextBlock RenderText(const std::vector & keys, bool withBar) + { + TextBlock block{ Pixels(static_cast(ScreenW) * ScreenH * BytesPerPixel, 0), 0 }; + const int lineCount = static_cast(keys.size()); + const int blockH = lineCount * LineSpacing - LineGap; + const int firstLineY = (ScreenH - blockH) / 2; + for(int i = 0; i < lineCount; ++i) + { + const std::string line = Translate(keys[i]); + DrawTextLine(block.pixels, withBar && i == lineCount - 1 ? WithoutTrailingEllipsis(line) : line, firstLineY + i * LineSpacing); + } + block.bottomY = firstLineY + blockH; + return block; + } + + bool WritePng(const Pixels & pixels, const std::string & outputPath) + { + png_image png{}; + png.version = PNG_IMAGE_VERSION; + png.width = ScreenW; + png.height = ScreenH; + png.format = PNG_FORMAT_RGB; + return png_image_write_to_file(&png, outputPath.c_str(), 0, pixels.data(), 0, nullptr) != 0; + } + + class Framebuffer + { + public: + Framebuffer() + { + fd_ = open("/dev/fb0", O_RDWR); + if(fd_ < 0) + return; + fb_fix_screeninfo fix{}; + if(ioctl(fd_, FBIOGET_VSCREENINFO, &var_) < 0 || ioctl(fd_, FBIOGET_FSCREENINFO, &fix) < 0 || + (var_.bits_per_pixel != 16 && var_.bits_per_pixel != 32)) + return; + stride_ = fix.line_length; + size_ = static_cast(fix.smem_len); + void * mem = mmap(nullptr, size_, PROT_READ | PROT_WRITE, MAP_SHARED, fd_, 0); + if(mem != MAP_FAILED) + mem_ = static_cast(mem); + } + ~Framebuffer() + { + if(mem_) + munmap(mem_, size_); + if(fd_ >= 0) + close(fd_); + } + Framebuffer(const Framebuffer &) = delete; + Framebuffer & operator=(const Framebuffer &) = delete; + + bool Ready() const { return mem_ != nullptr; } + + void Put(int x, int y, Color color) + { + if(x < 0 || y < 0 || x >= static_cast(var_.xres) || y >= static_cast(var_.yres)) + return; + const size_t offset = static_cast(y + var_.yoffset) * stride_ + + static_cast(x + var_.xoffset) * (var_.bits_per_pixel / 8); + if(offset + var_.bits_per_pixel / 8 > size_) + return; + const Uint32 value = Channel(color.r, var_.red) | Channel(color.g, var_.green) | + Channel(color.b, var_.blue) | Channel(0xFF, var_.transp); + if(var_.bits_per_pixel == 32) + std::memcpy(mem_ + offset, &value, 4); + else + { + const Uint16 value16 = static_cast(value); + std::memcpy(mem_ + offset, &value16, 2); + } + } + + void Fill(int x, int y, int w, int h, Color color) + { + for(int row = y; row < y + h; ++row) + for(int col = x; col < x + w; ++col) + Put(col, row, color); + } + + private: + static Uint32 Channel(unsigned char value, const fb_bitfield & field) + { + if(field.length == 0) + return 0; + return (static_cast(value) >> (8 - field.length)) << field.offset; + } + + int fd_ = -1; + fb_var_screeninfo var_{}; + size_t stride_ = 0; + size_t size_ = 0; + unsigned char * mem_ = nullptr; + }; + + bool PidAlive(pid_t pid) + { + return kill(pid, 0) == 0 || errno == EPERM; + } + + void DrawBar(Framebuffer & fb, int barY, int elapsedMs) + { + const int barX = (ScreenW - BarW) / 2; + const int segmentX = barX - SegmentW + (BarW + SegmentW) * (elapsedMs % SweepMs) / SweepMs; + const int visibleX = std::max(segmentX, barX); + const int visibleEnd = std::min(segmentX + SegmentW, barX + BarW); + fb.Fill(barX, barY, BarW, BarH, UiTheme::Border); + if(visibleEnd > visibleX) + fb.Fill(visibleX, barY, visibleEnd - visibleX, BarH, UiTheme::Accent); + } + + void DrawText(Framebuffer & fb, const TextBlock & block) + { + for(int y = 0; y < ScreenH; ++y) + for(int x = 0; x < ScreenW; ++x) + { + const size_t offset = (static_cast(y) * ScreenW + x) * BytesPerPixel; + fb.Put(x, y, { block.pixels[offset], block.pixels[offset + 1], block.pixels[offset + 2] }); + } + } + + void DetachFromCaller() + { + setsid(); + const int devNull = open("/dev/null", O_RDWR); + if(devNull < 0) + return; + dup2(devNull, STDIN_FILENO); + dup2(devNull, STDOUT_FILENO); + dup2(devNull, STDERR_FILENO); + if(devNull > STDERR_FILENO) + close(devNull); + } + + int RunLoading() + { + const pid_t caller = getppid(); + const TextBlock block = RenderText({ "LOADING" }, true); + UiTheme::LoadThemeConfig(OptionsRoot); + const int barY = block.bottomY + BarGap; + + Framebuffer fb; + if(!fb.Ready()) + { + std::cerr << "Cannot open /dev/fb0\n"; + return 1; + } + DrawText(fb, block); + DrawBar(fb, barY, 0); + + const pid_t child = fork(); + if(child != 0) + return child < 0 ? 1 : 0; + + DetachFromCaller(); + for(int elapsedMs = TickMs; PidAlive(caller); elapsedMs += TickMs) + { + usleep(TickMs * 1000); + DrawBar(fb, barY, elapsedMs); + } + return 0; + } + + void PrintUsage(const char * self) + { + std::cerr << "Usage: " << self << " [line-key...]\n" + << " " << self << " --loading\n"; + } } int main(int argc, char * argv[]) { - if(argc < 3) + const std::vector args(argv + 1, argv + argc); + if(args.size() == 1 && args[0] == "--loading") + { + LoadLanguageFromConfig(OptionsRoot); + return RunLoading(); + } + if(args.size() < 2) { - std::cerr << "Usage: " << argv[0] << " [line-key...]\n"; + PrintUsage(argv[0]); return 1; } - const std::string outputPath = argv[1]; - const int lineCount = argc - 2; LoadLanguageFromConfig(OptionsRoot); - - std::vector pixels(static_cast(ScreenW) * ScreenH * BytesPerPixel, 0); - const int blockH = lineCount * LineSpacing - LineGap; - const int firstLineY = (ScreenH - blockH) / 2; - for(int i = 0; i < lineCount; ++i) - DrawTextLine(pixels, Translate(argv[2 + i]), firstLineY + i * LineSpacing); - - png_image png{}; - png.version = PNG_IMAGE_VERSION; - png.width = ScreenW; - png.height = ScreenH; - png.format = PNG_FORMAT_RGB; - - if(!png_image_write_to_file(&png, outputPath.c_str(), 0, pixels.data(), 0, nullptr)) + const TextBlock block = RenderText(std::vector(args.begin() + 1, args.end()), false); + if(!WritePng(block.pixels, args[0])) { - std::cerr << "Cannot write png file: " << outputPath << "\n"; + std::cerr << "Cannot write png file: " << args[0] << "\n"; return 1; } - return 0; } From 3850e984a136adec390fec4a28923c17883cbb5b Mon Sep 17 00:00:00 2001 From: DefKorns Date: Thu, 8 Oct 2026 22:22:48 +0000 Subject: [PATCH 075/104] Remove obsolete USB save backup and restore commands --- .../options_menu/saved_games/commands/c0010_BackupSavesUsb | 4 ---- .../options_menu/saved_games/commands/c0020_RestoreSavesUsb | 4 ---- 2 files changed, 8 deletions(-) delete mode 100644 mod/etc/options_menu/saved_games/commands/c0010_BackupSavesUsb delete mode 100644 mod/etc/options_menu/saved_games/commands/c0020_RestoreSavesUsb diff --git a/mod/etc/options_menu/saved_games/commands/c0010_BackupSavesUsb b/mod/etc/options_menu/saved_games/commands/c0010_BackupSavesUsb deleted file mode 100644 index d5f6b94..0000000 --- a/mod/etc/options_menu/saved_games/commands/c0010_BackupSavesUsb +++ /dev/null @@ -1,4 +0,0 @@ -COMMAND_NAME=BACKUP_SAVES_USB -COMMAND_TYPE=INTERNAL -COMMAND_STR=sh %script_dir%/BackupSavesUsb.sh -PREVIEW_IMAGE=/etc/options_menu/images/preview_saves_backup_usb.png diff --git a/mod/etc/options_menu/saved_games/commands/c0020_RestoreSavesUsb b/mod/etc/options_menu/saved_games/commands/c0020_RestoreSavesUsb deleted file mode 100644 index c8821e9..0000000 --- a/mod/etc/options_menu/saved_games/commands/c0020_RestoreSavesUsb +++ /dev/null @@ -1,4 +0,0 @@ -COMMAND_NAME=RESTORE_SAVES_USB -COMMAND_TYPE=INTERNAL -COMMAND_STR=sh %script_dir%/RestoreSavesUsb.sh -PREVIEW_IMAGE=/etc/options_menu/images/preview_saves_restore_usb.png From dec109ce7f5caec792b282404a43faec251eeba0 Mon Sep 17 00:00:00 2001 From: DefKorns Date: Thu, 8 Oct 2026 22:24:27 +0000 Subject: [PATCH 076/104] Replace the Saved Games submenu with a Save Manager screen One screen for USB backup/restore plus two new actions: move the saves of games that are no longer on the console (SRAM/suspend folders and RetroArch saves/states) to USB:/data/deleted_games_saves, and bring them back. Lists start with a select-all row, every action asks for Start + Select, a restore never overwrites a save already on the console, and CLV-S menu folders are never touched. The panel shows the saves on the console, in the USB backup, of removed games and moved aside. Drop the unused SAVED_GAMES_OPTIONS, PRESS_B_EXIT and DELETE_CONFIRM_HINT strings. --- Makefile | 9 +- README.md | 12 +- .../commands/c0073_SavedGamesOptions | 4 +- mod/etc/options_menu/language/de-DE.lang | 32 +- mod/etc/options_menu/language/en-US.lang | 32 +- mod/etc/options_menu/language/es-ES.lang | 32 +- mod/etc/options_menu/language/fr-FR.lang | 32 +- mod/etc/options_menu/language/it-IT.lang | 32 +- mod/etc/options_menu/language/ja-JP.lang | 32 +- mod/etc/options_menu/language/nl-NL.lang | 32 +- mod/etc/options_menu/language/pt-PT.lang | 32 +- mod/etc/options_menu/language/ru-RU.lang | 32 +- .../save_manager/launch_save_manager.sh | 39 + mod/etc/options_menu/scripts/OrphanSaves.sh | 131 +++ mod/readme.md | 2 +- src/save_manager.cpp | 786 ++++++++++++++++++ 16 files changed, 1234 insertions(+), 37 deletions(-) create mode 100644 mod/etc/options_menu/save_manager/launch_save_manager.sh create mode 100644 mod/etc/options_menu/scripts/OrphanSaves.sh create mode 100644 src/save_manager.cpp diff --git a/Makefile b/Makefile index d1df003..c562ad9 100644 --- a/Makefile +++ b/Makefile @@ -11,7 +11,7 @@ VARIANT := plain endif MOD_CREATOR := CompCom, DefKorns MOD_CATEGORY := Options Menu -BINARIES := mod/etc/options_menu/options mod/etc/options_menu/optiond mod/etc/options_menu/mod_uninstall/mod_uninstall mod/bin/standby_watchdog mod/etc/options_menu/scripts/gen_splash mod/etc/options_menu/scripts/ChangeCombo +BINARIES := mod/etc/options_menu/options mod/etc/options_menu/optiond mod/etc/options_menu/mod_uninstall/mod_uninstall mod/etc/options_menu/save_manager/save_manager mod/bin/standby_watchdog mod/etc/options_menu/scripts/gen_splash mod/etc/options_menu/scripts/ChangeCombo CXX = g++ STRIP = strip @@ -38,7 +38,7 @@ CXXFLAGS = -std=c++11 -Os $(SDL_CFLAGS) -DMOD_VERSION=\"v$(MOD_VER)\" LDLIBS = $(SDL_LIBS) SOURCES = src/main.cpp src/command.cpp src/localization.cpp src/framework/badge.cpp src/framework/dialog.cpp src/framework/sdl_context.cpp src/framework/texture.cpp src/framework/controller.cpp src/framework/powerwatch.cpp src/framework/draw_helpers.cpp src/framework/utf8.cpp src/framework/font8x8_lookup.cpp src/framework/uitheme.cpp OBJECTS = $(SOURCES:.cpp=.o) -ALL_OBJECTS = $(OBJECTS) src/daemon.o src/mod_uninstall.o src/standby_watchdog.o src/gen_splash.o src/change_combo.o +ALL_OBJECTS = $(OBJECTS) src/daemon.o src/mod_uninstall.o src/save_manager.o src/standby_watchdog.o src/gen_splash.o src/change_combo.o MOD_DEPS := $(BINARIES) $(VENDORED_LIBS) ifndef FIX_LIBSTDCXX @@ -88,6 +88,11 @@ mod/etc/options_menu/mod_uninstall/mod_uninstall: src/mod_uninstall.o src/locali $(CROSS_PREFIX)$(STRIP) $@ upx --lzma $@ +mod/etc/options_menu/save_manager/save_manager: src/save_manager.o src/localization.o $(filter src/framework/%,$(OBJECTS)) + $(CROSS_PREFIX)$(CXX) $^ $(LDLIBS) $(LDFLAGS) -Wl,-rpath,/etc/options_menu/lib -o $@ + $(CROSS_PREFIX)$(STRIP) $@ + upx --lzma $@ + mod/bin/standby_watchdog: src/standby_watchdog.o src/framework/controller.o src/framework/powerwatch.o $(CROSS_PREFIX)$(CXX) $(LDFLAGS) $^ $(STDCXX_RPATH) -o $@ $(CROSS_PREFIX)$(STRIP) $@ diff --git a/README.md b/README.md index 293409c..70aa572 100644 --- a/README.md +++ b/README.md @@ -48,7 +48,7 @@ The footer always shows the buttons for the current screen. | Reboot Device | Safely restarts the console\* | | RetroArch › | RetroArch settings, backups and BIOS files. Shown when RetroArch is installed | | Network › | IP address, Wi-Fi scan and Wi-Fi config backups | -| Saved Games › | Back up and restore your saves. Needs a USB/SD drive | +| Save Manager › | Back up, restore and clean up your saves. Needs a USB/SD drive | | Language › | Menu language; the menu restarts in the new language | | Advanced Options › | Controller, diagnostics and system tools | @@ -92,14 +92,16 @@ The USB entries only show up with a USB/SD drive connected. Restore entries only show up when there's a backup to restore. -### Saved Games +### Save Manager -Needs a USB/SD drive with your saves on it. +Needs a USB/SD drive with your saves on it. One screen lists the actions on the left; the right panel explains the selected one and shows how many saves are on the console, in the USB backup, left over from removed games and moved aside. Every action asks for confirmation (**Start + Select**). -| Entry | What it does | +| Action | What it does | | --- | --- | | Backup Saved Games to USB | Mirrors your saves folder to `/media/data/saves_backup`. Saves you deleted since the last backup are removed from it too; system folders (`FOLDER`, `hakchi`, `home-menu`, `mcp-state`) are skipped | | Restore Saved Games from USB | Copies the backup back, overwriting saves with the same name and leaving the others alone | +| Clean Saves of Removed Games › | Lists the save folders of games that are no longer in any menu (all marked; **A** unmarks) and moves the marked ones to `/media/data/deleted_games_saves`. Menu folders (`CLV-S-*`) are never touched | +| Restore Saves of Removed Games › | Lists what was moved aside; mark the ones you want back with **A** and press **Start**. A save already on the console is never overwritten | ### Advanced Options @@ -231,7 +233,7 @@ Packaging and versioning come from [hmod-build](https://github.com/DefKorns/hmod ## Credits - **[CompCom](https://github.com/CompCom)** — the original Options Menu -- **[DefKorns](https://github.com/DefKorns)** — version 2: the new interface, TTF text, theme colors and icons, translations, Network (with Advokaten), Wi-Fi backups, Saved Games, Controller and Diagnostics menus, build tooling +- **[DefKorns](https://github.com/DefKorns)** — version 2: the new interface, TTF text, theme colors and icons, translations, Network (with Advokaten), Wi-Fi backups, Save Manager, Controller and Diagnostics menus, build tooling - **Swingflip** — Hibernate Mod and Hakchi-Option-Pack scripts - **[BsLeNuL](https://github.com/bslenul)** — RetroArch configuration scripts - **Advokaten** — Network commands diff --git a/mod/etc/options_menu/commands/c0073_SavedGamesOptions b/mod/etc/options_menu/commands/c0073_SavedGamesOptions index a6f44c9..10da3a0 100644 --- a/mod/etc/options_menu/commands/c0073_SavedGamesOptions +++ b/mod/etc/options_menu/commands/c0073_SavedGamesOptions @@ -1,7 +1,7 @@ -COMMAND_NAME=SAVED_GAMES_OPTIONS +COMMAND_NAME=SAVE_MANAGER COMMAND_TYPE=EXTERNAL RESTART_UI=FALSE SUBMENU=TRUE USB_ONLY=TRUE -COMMAND_STR=usleep 50000 && %options_path%/options --commandPath %options_path%/saved_games/commands/ --title "SAVED_GAMES_OPTIONS" & +COMMAND_STR=sh %options_path%/save_manager/launch_save_manager.sh %options_path% & PREVIEW_IMAGE=/etc/options_menu/images/preview_saved_games.png diff --git a/mod/etc/options_menu/language/de-DE.lang b/mod/etc/options_menu/language/de-DE.lang index dfc7ca5..31f26a9 100644 --- a/mod/etc/options_menu/language/de-DE.lang +++ b/mod/etc/options_menu/language/de-DE.lang @@ -3,7 +3,6 @@ SHUTDOWN_DEVICE=Gerät ausschalten REBOOT_DEVICE=Gerät neu starten ADVANCED_OPTIONS=Erweiterte Optionen NETWORK_OPTIONS=Netzwerk -SAVED_GAMES_OPTIONS=Spielstände EXIT=Beenden MODULE_UNINSTALLER=Modul-Deinstallation DISPLAY_TEMP=Temperatur anzeigen @@ -42,12 +41,10 @@ RA_DELETE_BIOS=BIOS-Datei(en) von NAND löschen RA_TOGGLE_LOAD_SCREENS=RA- und Canoe-Ladebildschirme umschalten RESTORE_WIFI_CONFIG=Wifi-Konfiguration wiederherstellen OPTIONS_TITLE=Optionen -PRESS_B_EXIT=B drücken zum Beenden. LOADING=Wird geladen... WIFI_BACKUP_SPLASH_LINE1=Wifi-Konfigurationsdatei wird gesichert WIFI_BACKUP_SPLASH_LINE2=Bitte warten . . . DELETE_BACKUP_CONFIRM=Diese Sicherung löschen? -DELETE_CONFIRM_HINT=A = Ja B = Nein DELETE_CONFIRM_GENERIC=Dieses Element löschen? HINT_SELECT=Wählen HINT_BACK=Zurück @@ -93,3 +90,32 @@ SYSINFO_FIRMWARE=Firmware SYSINFO_SAVES=Spielstände SYSINFO_FREE=frei DIAGNOSTICS_OPTIONS=Diagnose +SAVE_MANAGER=Spielstand-Verwaltung +SAVE_MANAGER_BACKUP_DESC=Kopiert alle Spielstände nach USB:/data/saves_backup. +SAVE_MANAGER_RESTORE_DESC=Kopiert die USB-Sicherung zurück auf die Konsole. Spielstände mit gleichem Namen werden ersetzt. +SAVE_MANAGER_CLEAN=Spielstände entfernter Spiele aufräumen +SAVE_MANAGER_CLEAN_DESC=Verschiebt Spielstände von Spielen, die nicht mehr auf der Konsole sind, nach USB:/data/deleted_games_saves. +SAVE_MANAGER_RESTORE_DELETED=Spielstände entfernter Spiele wiederherstellen +SAVE_MANAGER_RESTORE_DELETED_DESC=Holt verschobene Spielstände zurück, für wieder installierte Spiele. +SAVE_MANAGER_STAT_SAVES=Spielstände auf der Konsole +SAVE_MANAGER_STAT_BACKUP=Spielstände in der USB-Sicherung +SAVE_MANAGER_STAT_NONE=Keine +SAVE_MANAGER_STAT_ORPHANS=Spielstände entfernter Spiele +SAVE_MANAGER_STAT_KEPT=Beiseitegelegt +SAVE_MANAGER_BACKUP_CONFIRM=Spielstände auf USB sichern? +SAVE_MANAGER_RESTORE_CONFIRM=Spielstände von USB wiederherstellen? +SAVE_MANAGER_CLEAN_CONFIRM=Diese Spielstände beiseitelegen? +SAVE_MANAGER_RESTORE_DELETED_CONFIRM=Diese Spielstände wiederherstellen? +SAVE_MANAGER_TO_MOVE=Zu verschieben +SAVE_MANAGER_TO_RESTORE=Wiederherzustellen +SAVE_MANAGER_NONE_MARKED=Keine Spielstände markiert. +SAVE_MANAGER_NONE_HINT=Drücke A auf einem Spielstand, um ihn zu markieren. +SAVE_MANAGER_NO_ORPHANS=Keine Spielstände entfernter Spiele gefunden. +SAVE_MANAGER_NO_DELETED=Keine beiseitegelegten Spielstände zum Wiederherstellen. +SAVE_MANAGER_CLEAN_NOTE=Aufbewahrt in USB:/data/deleted_games_saves +SAVE_MANAGER_RESTORE_NOTE=Spielstände, die schon auf der Konsole sind, werden nicht ersetzt. +SAVE_MANAGER_WORKING=Wird ausgeführt... +HINT_OK=OK +HINT_MOVE=Verschieben +HINT_RESTORE=Wiederherstellen +SAVE_MANAGER_SELECT_ALL=Alle auswählen diff --git a/mod/etc/options_menu/language/en-US.lang b/mod/etc/options_menu/language/en-US.lang index 5dbf0d3..beae488 100644 --- a/mod/etc/options_menu/language/en-US.lang +++ b/mod/etc/options_menu/language/en-US.lang @@ -4,7 +4,6 @@ REBOOT_DEVICE=Reboot Device RETROARCH_OPTIONS=RetroArch ADVANCED_OPTIONS=Advanced Options NETWORK_OPTIONS=Network -SAVED_GAMES_OPTIONS=Saved Games EXIT=Exit MODULE_UNINSTALLER=Module Uninstaller DISPLAY_TEMP=Display Temp @@ -43,12 +42,10 @@ RA_DELETE_BIOS=Delete BIOS file(s) from NAND RA_TOGGLE_LOAD_SCREENS=Toggle RA and Canoe load screens RESTORE_WIFI_CONFIG=Restore WIFI Config OPTIONS_TITLE=Options -PRESS_B_EXIT=Press B to exit. LOADING=Loading... WIFI_BACKUP_SPLASH_LINE1=Backup Wifi Config File WIFI_BACKUP_SPLASH_LINE2=Please Wait . . . DELETE_BACKUP_CONFIRM=Delete this backup? -DELETE_CONFIRM_HINT=A = Yes B = No DELETE_CONFIRM_GENERIC=Delete this item? HINT_SELECT=Select HINT_BACK=Back @@ -94,3 +91,32 @@ SYSINFO_FIRMWARE=Firmware SYSINFO_SAVES=Saves SYSINFO_FREE=free DIAGNOSTICS_OPTIONS=Diagnostics +SAVE_MANAGER=Save Manager +SAVE_MANAGER_BACKUP_DESC=Copies every saved game to USB:/data/saves_backup. +SAVE_MANAGER_RESTORE_DESC=Copies the USB backup back to the console. Saves with the same name are replaced. +SAVE_MANAGER_CLEAN=Clean Saves of Removed Games +SAVE_MANAGER_CLEAN_DESC=Moves saves of games that are no longer on the console to USB:/data/deleted_games_saves. +SAVE_MANAGER_RESTORE_DELETED=Restore Saves of Removed Games +SAVE_MANAGER_RESTORE_DELETED_DESC=Brings moved saves back, for games you installed again. +SAVE_MANAGER_STAT_SAVES=Saves on console +SAVE_MANAGER_STAT_BACKUP=Saves in USB backup +SAVE_MANAGER_STAT_NONE=None +SAVE_MANAGER_STAT_ORPHANS=Saves of removed games +SAVE_MANAGER_STAT_KEPT=Moved aside +SAVE_MANAGER_BACKUP_CONFIRM=Back up saved games to USB? +SAVE_MANAGER_RESTORE_CONFIRM=Restore saved games from USB? +SAVE_MANAGER_CLEAN_CONFIRM=Move these saves aside? +SAVE_MANAGER_RESTORE_DELETED_CONFIRM=Restore these saves? +SAVE_MANAGER_TO_MOVE=To Move +SAVE_MANAGER_TO_RESTORE=To Restore +SAVE_MANAGER_NONE_MARKED=No saves marked. +SAVE_MANAGER_NONE_HINT=Press A on a save to mark it. +SAVE_MANAGER_NO_ORPHANS=No saves of removed games found. +SAVE_MANAGER_NO_DELETED=No moved saves to restore. +SAVE_MANAGER_CLEAN_NOTE=Kept in USB:/data/deleted_games_saves +SAVE_MANAGER_RESTORE_NOTE=Saves already on the console are not replaced. +SAVE_MANAGER_WORKING=Working... +HINT_OK=OK +HINT_MOVE=Move +HINT_RESTORE=Restore +SAVE_MANAGER_SELECT_ALL=Select all diff --git a/mod/etc/options_menu/language/es-ES.lang b/mod/etc/options_menu/language/es-ES.lang index 2433d8a..01ff2dc 100644 --- a/mod/etc/options_menu/language/es-ES.lang +++ b/mod/etc/options_menu/language/es-ES.lang @@ -3,7 +3,6 @@ SHUTDOWN_DEVICE=Apagar dispositivo REBOOT_DEVICE=Reiniciar dispositivo ADVANCED_OPTIONS=Opciones avanzadas NETWORK_OPTIONS=Red -SAVED_GAMES_OPTIONS=Partidas Guardadas EXIT=Salir MODULE_UNINSTALLER=Desinstalador de módulos DISPLAY_TEMP=Mostrar temperatura @@ -42,12 +41,10 @@ RA_DELETE_BIOS=Borrar archivo(s) BIOS de NAND RA_TOGGLE_LOAD_SCREENS=Alternar pantallas de carga de RA y Canoe RESTORE_WIFI_CONFIG=Restaurar configuración Wifi OPTIONS_TITLE=Opciones -PRESS_B_EXIT=Pulsa B para salir. LOADING=Cargando... WIFI_BACKUP_SPLASH_LINE1=Copiando archivo de configuración Wifi WIFI_BACKUP_SPLASH_LINE2=Espere por favor . . . DELETE_BACKUP_CONFIRM=¿Borrar esta copia de seguridad? -DELETE_CONFIRM_HINT=A = Sí B = No DELETE_CONFIRM_GENERIC=¿Borrar este elemento? HINT_SELECT=Seleccionar HINT_BACK=Atrás @@ -93,3 +90,32 @@ SYSINFO_FIRMWARE=Firmware SYSINFO_SAVES=Partidas SYSINFO_FREE=libres DIAGNOSTICS_OPTIONS=Diagnóstico +SAVE_MANAGER=Gestor de Partidas +SAVE_MANAGER_BACKUP_DESC=Copia todas las partidas guardadas en USB:/data/saves_backup. +SAVE_MANAGER_RESTORE_DESC=Copia la copia de seguridad de la USB de vuelta a la consola. Las partidas con el mismo nombre se reemplazan. +SAVE_MANAGER_CLEAN=Limpiar Partidas de Juegos Eliminados +SAVE_MANAGER_CLEAN_DESC=Mueve las partidas de juegos que ya no están en la consola a USB:/data/deleted_games_saves. +SAVE_MANAGER_RESTORE_DELETED=Restaurar Partidas de Juegos Eliminados +SAVE_MANAGER_RESTORE_DELETED_DESC=Devuelve las partidas movidas, para juegos que has vuelto a instalar. +SAVE_MANAGER_STAT_SAVES=Partidas en la consola +SAVE_MANAGER_STAT_BACKUP=Partidas en la copia USB +SAVE_MANAGER_STAT_NONE=Ninguna +SAVE_MANAGER_STAT_ORPHANS=Partidas de juegos eliminados +SAVE_MANAGER_STAT_KEPT=Apartadas +SAVE_MANAGER_BACKUP_CONFIRM=¿Copiar las partidas guardadas a la USB? +SAVE_MANAGER_RESTORE_CONFIRM=¿Restaurar las partidas guardadas desde la USB? +SAVE_MANAGER_CLEAN_CONFIRM=¿Apartar estas partidas? +SAVE_MANAGER_RESTORE_DELETED_CONFIRM=¿Restaurar estas partidas? +SAVE_MANAGER_TO_MOVE=A Mover +SAVE_MANAGER_TO_RESTORE=A Restaurar +SAVE_MANAGER_NONE_MARKED=Ninguna partida marcada. +SAVE_MANAGER_NONE_HINT=Pulsa A en una partida para marcarla. +SAVE_MANAGER_NO_ORPHANS=No hay partidas de juegos eliminados. +SAVE_MANAGER_NO_DELETED=No hay partidas apartadas para restaurar. +SAVE_MANAGER_CLEAN_NOTE=Guardadas en USB:/data/deleted_games_saves +SAVE_MANAGER_RESTORE_NOTE=Las partidas que ya están en la consola no se reemplazan. +SAVE_MANAGER_WORKING=Procesando... +HINT_OK=OK +HINT_MOVE=Mover +HINT_RESTORE=Restaurar +SAVE_MANAGER_SELECT_ALL=Seleccionar todo diff --git a/mod/etc/options_menu/language/fr-FR.lang b/mod/etc/options_menu/language/fr-FR.lang index 2f39bb4..fc08a3a 100644 --- a/mod/etc/options_menu/language/fr-FR.lang +++ b/mod/etc/options_menu/language/fr-FR.lang @@ -3,7 +3,6 @@ SHUTDOWN_DEVICE=Éteindre l'appareil REBOOT_DEVICE=Redémarrer l'appareil ADVANCED_OPTIONS=Options avancées NETWORK_OPTIONS=Réseau -SAVED_GAMES_OPTIONS=Sauvegardes de Jeu EXIT=Quitter MODULE_UNINSTALLER=Désinstalleur de modules DISPLAY_TEMP=Afficher la température @@ -42,12 +41,10 @@ RA_DELETE_BIOS=Supprimer le(s) fichier(s) BIOS de la NAND RA_TOGGLE_LOAD_SCREENS=Activer/désactiver les écrans de chargement RA et Canoe RESTORE_WIFI_CONFIG=Restaurer la config Wifi OPTIONS_TITLE=Options -PRESS_B_EXIT=Appuyez sur B pour quitter. LOADING=Chargement... WIFI_BACKUP_SPLASH_LINE1=Sauvegarde du fichier de config Wifi WIFI_BACKUP_SPLASH_LINE2=Veuillez patienter . . . DELETE_BACKUP_CONFIRM=Supprimer cette sauvegarde? -DELETE_CONFIRM_HINT=A = Oui B = Non DELETE_CONFIRM_GENERIC=Supprimer cet élément? HINT_SELECT=Sélect. HINT_BACK=Retour @@ -93,3 +90,32 @@ SYSINFO_FIRMWARE=Firmware SYSINFO_SAVES=Sauvegardes SYSINFO_FREE=libres DIAGNOSTICS_OPTIONS=Diagnostic +SAVE_MANAGER=Gestionnaire de Sauvegardes +SAVE_MANAGER_BACKUP_DESC=Copie toutes les sauvegardes vers USB:/data/saves_backup. +SAVE_MANAGER_RESTORE_DESC=Recopie la sauvegarde USB sur la console. Les sauvegardes portant le même nom sont remplacées. +SAVE_MANAGER_CLEAN=Nettoyer les Sauvegardes de Jeux Supprimés +SAVE_MANAGER_CLEAN_DESC=Déplace les sauvegardes des jeux qui ne sont plus sur la console vers USB:/data/deleted_games_saves. +SAVE_MANAGER_RESTORE_DELETED=Restaurer les Sauvegardes de Jeux Supprimés +SAVE_MANAGER_RESTORE_DELETED_DESC=Remet en place les sauvegardes déplacées, pour les jeux réinstallés. +SAVE_MANAGER_STAT_SAVES=Sauvegardes sur la console +SAVE_MANAGER_STAT_BACKUP=Sauvegardes dans la copie USB +SAVE_MANAGER_STAT_NONE=Aucune +SAVE_MANAGER_STAT_ORPHANS=Sauvegardes de jeux supprimés +SAVE_MANAGER_STAT_KEPT=Mises de côté +SAVE_MANAGER_BACKUP_CONFIRM=Copier les sauvegardes sur la USB ? +SAVE_MANAGER_RESTORE_CONFIRM=Restaurer les sauvegardes depuis la USB ? +SAVE_MANAGER_CLEAN_CONFIRM=Mettre ces sauvegardes de côté ? +SAVE_MANAGER_RESTORE_DELETED_CONFIRM=Restaurer ces sauvegardes ? +SAVE_MANAGER_TO_MOVE=À Déplacer +SAVE_MANAGER_TO_RESTORE=À Restaurer +SAVE_MANAGER_NONE_MARKED=Aucune sauvegarde cochée. +SAVE_MANAGER_NONE_HINT=Appuyez sur A sur une sauvegarde pour la cocher. +SAVE_MANAGER_NO_ORPHANS=Aucune sauvegarde de jeu supprimé. +SAVE_MANAGER_NO_DELETED=Aucune sauvegarde mise de côté à restaurer. +SAVE_MANAGER_CLEAN_NOTE=Conservées dans USB:/data/deleted_games_saves +SAVE_MANAGER_RESTORE_NOTE=Les sauvegardes déjà présentes sur la console ne sont pas remplacées. +SAVE_MANAGER_WORKING=En cours... +HINT_OK=OK +HINT_MOVE=Déplacer +HINT_RESTORE=Restaurer +SAVE_MANAGER_SELECT_ALL=Tout sélectionner diff --git a/mod/etc/options_menu/language/it-IT.lang b/mod/etc/options_menu/language/it-IT.lang index 21f74c3..5825c7c 100644 --- a/mod/etc/options_menu/language/it-IT.lang +++ b/mod/etc/options_menu/language/it-IT.lang @@ -3,7 +3,6 @@ SHUTDOWN_DEVICE=Spegni dispositivo REBOOT_DEVICE=Riavvia dispositivo ADVANCED_OPTIONS=Opzioni avanzate NETWORK_OPTIONS=Rete -SAVED_GAMES_OPTIONS=Salvataggi EXIT=Esci MODULE_UNINSTALLER=Disinstallatore moduli DISPLAY_TEMP=Mostra temperatura @@ -42,12 +41,10 @@ RA_DELETE_BIOS=Elimina file BIOS da NAND RA_TOGGLE_LOAD_SCREENS=Attiva/disattiva schermate di caricamento RA e Canoe RESTORE_WIFI_CONFIG=Ripristina configurazione Wifi OPTIONS_TITLE=Opzioni -PRESS_B_EXIT=Premi B per uscire. LOADING=Caricamento... WIFI_BACKUP_SPLASH_LINE1=Backup del file di configurazione Wifi WIFI_BACKUP_SPLASH_LINE2=Attendere prego . . . DELETE_BACKUP_CONFIRM=Eliminare questo backup? -DELETE_CONFIRM_HINT=A = Sì B = No DELETE_CONFIRM_GENERIC=Eliminare questo elemento? HINT_SELECT=Seleziona HINT_BACK=Indietro @@ -93,3 +90,32 @@ SYSINFO_FIRMWARE=Firmware SYSINFO_SAVES=Salvataggi SYSINFO_FREE=liberi DIAGNOSTICS_OPTIONS=Diagnostica +SAVE_MANAGER=Gestione Salvataggi +SAVE_MANAGER_BACKUP_DESC=Copia tutti i salvataggi su USB:/data/saves_backup. +SAVE_MANAGER_RESTORE_DESC=Ricopia il backup della USB sulla console. I salvataggi con lo stesso nome vengono sostituiti. +SAVE_MANAGER_CLEAN=Pulisci Salvataggi di Giochi Rimossi +SAVE_MANAGER_CLEAN_DESC=Sposta i salvataggi dei giochi non più presenti sulla console in USB:/data/deleted_games_saves. +SAVE_MANAGER_RESTORE_DELETED=Ripristina Salvataggi di Giochi Rimossi +SAVE_MANAGER_RESTORE_DELETED_DESC=Riporta i salvataggi spostati, per i giochi reinstallati. +SAVE_MANAGER_STAT_SAVES=Salvataggi sulla console +SAVE_MANAGER_STAT_BACKUP=Salvataggi nel backup USB +SAVE_MANAGER_STAT_NONE=Nessuno +SAVE_MANAGER_STAT_ORPHANS=Salvataggi di giochi rimossi +SAVE_MANAGER_STAT_KEPT=Messi da parte +SAVE_MANAGER_BACKUP_CONFIRM=Copiare i salvataggi sulla USB? +SAVE_MANAGER_RESTORE_CONFIRM=Ripristinare i salvataggi dalla USB? +SAVE_MANAGER_CLEAN_CONFIRM=Mettere da parte questi salvataggi? +SAVE_MANAGER_RESTORE_DELETED_CONFIRM=Ripristinare questi salvataggi? +SAVE_MANAGER_TO_MOVE=Da Spostare +SAVE_MANAGER_TO_RESTORE=Da Ripristinare +SAVE_MANAGER_NONE_MARKED=Nessun salvataggio selezionato. +SAVE_MANAGER_NONE_HINT=Premi A su un salvataggio per selezionarlo. +SAVE_MANAGER_NO_ORPHANS=Nessun salvataggio di giochi rimossi. +SAVE_MANAGER_NO_DELETED=Nessun salvataggio messo da parte da ripristinare. +SAVE_MANAGER_CLEAN_NOTE=Conservati in USB:/data/deleted_games_saves +SAVE_MANAGER_RESTORE_NOTE=I salvataggi già presenti sulla console non vengono sostituiti. +SAVE_MANAGER_WORKING=Elaborazione... +HINT_OK=OK +HINT_MOVE=Sposta +HINT_RESTORE=Ripristina +SAVE_MANAGER_SELECT_ALL=Seleziona tutto diff --git a/mod/etc/options_menu/language/ja-JP.lang b/mod/etc/options_menu/language/ja-JP.lang index e20cdfd..f22a0b5 100644 --- a/mod/etc/options_menu/language/ja-JP.lang +++ b/mod/etc/options_menu/language/ja-JP.lang @@ -3,7 +3,6 @@ SHUTDOWN_DEVICE=でんげんをきる REBOOT_DEVICE=さいきどうする ADVANCED_OPTIONS=じょうきゅうせってい NETWORK_OPTIONS=Network -SAVED_GAMES_OPTIONS=セーブデータ EXIT=おわる MODULE_UNINSTALLER=Moduleのさくじょ DISPLAY_TEMP=おんどをひょうじ @@ -42,12 +41,10 @@ RA_DELETE_BIOS=BIOS Fileをさくじょ (NAND) RA_TOGGLE_LOAD_SCREENS=RAとCanoeのLoadingがめんをきりかえ RESTORE_WIFI_CONFIG=Wifiせっていをもどす OPTIONS_TITLE=せってい -PRESS_B_EXIT=Bでおわる LOADING=よみこみちゅう... WIFI_BACKUP_SPLASH_LINE1=Wifiせってい Fileの Backup WIFI_BACKUP_SPLASH_LINE2=おまちください . . . DELETE_BACKUP_CONFIRM=このBackupをさくじょしますか? -DELETE_CONFIRM_HINT=A = はい B = いいえ DELETE_CONFIRM_GENERIC=これをさくじょしますか? HINT_SELECT=けってい HINT_BACK=もどる @@ -93,3 +90,32 @@ SYSINFO_FIRMWARE=Firmware SYSINFO_SAVES=セーブデータ SYSINFO_FREE=あき DIAGNOSTICS_OPTIONS=しんだん +SAVE_MANAGER=セーブ管理 +SAVE_MANAGER_BACKUP_DESC=すべてのセーブデータを USB:/data/saves_backup にコピーします。 +SAVE_MANAGER_RESTORE_DESC=USBのバックアップを本体に戻します。同じ名前のセーブデータは上書きされます。 +SAVE_MANAGER_CLEAN=削除したゲームのセーブを整理 +SAVE_MANAGER_CLEAN_DESC=本体にないゲームのセーブデータを USB:/data/deleted_games_saves に移動します。 +SAVE_MANAGER_RESTORE_DELETED=削除したゲームのセーブを復元 +SAVE_MANAGER_RESTORE_DELETED_DESC=移動したセーブデータを、再インストールしたゲームのために戻します。 +SAVE_MANAGER_STAT_SAVES=本体のセーブ数 +SAVE_MANAGER_STAT_BACKUP=USBバックアップのセーブ数 +SAVE_MANAGER_STAT_NONE=なし +SAVE_MANAGER_STAT_ORPHANS=削除したゲームのセーブ +SAVE_MANAGER_STAT_KEPT=移動済み +SAVE_MANAGER_BACKUP_CONFIRM=セーブデータをUSBにバックアップしますか? +SAVE_MANAGER_RESTORE_CONFIRM=USBからセーブデータを復元しますか? +SAVE_MANAGER_CLEAN_CONFIRM=これらのセーブを移動しますか? +SAVE_MANAGER_RESTORE_DELETED_CONFIRM=これらのセーブを復元しますか? +SAVE_MANAGER_TO_MOVE=移動する +SAVE_MANAGER_TO_RESTORE=復元する +SAVE_MANAGER_NONE_MARKED=セーブが選択されていません。 +SAVE_MANAGER_NONE_HINT=Aボタンでセーブを選択します。 +SAVE_MANAGER_NO_ORPHANS=削除したゲームのセーブはありません。 +SAVE_MANAGER_NO_DELETED=復元できる移動済みのセーブはありません。 +SAVE_MANAGER_CLEAN_NOTE=USB:/data/deleted_games_saves に保管 +SAVE_MANAGER_RESTORE_NOTE=本体にあるセーブデータは上書きされません。 +SAVE_MANAGER_WORKING=処理中... +HINT_OK=OK +HINT_MOVE=移動 +HINT_RESTORE=復元 +SAVE_MANAGER_SELECT_ALL=すべて選択 diff --git a/mod/etc/options_menu/language/nl-NL.lang b/mod/etc/options_menu/language/nl-NL.lang index 156b9a3..d190000 100644 --- a/mod/etc/options_menu/language/nl-NL.lang +++ b/mod/etc/options_menu/language/nl-NL.lang @@ -3,7 +3,6 @@ SHUTDOWN_DEVICE=Apparaat uitschakelen REBOOT_DEVICE=Apparaat herstarten ADVANCED_OPTIONS=Geavanceerde opties NETWORK_OPTIONS=Netwerk -SAVED_GAMES_OPTIONS=Savegames EXIT=Afsluiten MODULE_UNINSTALLER=Module-verwijderaar DISPLAY_TEMP=Temperatuur weergeven @@ -42,12 +41,10 @@ RA_DELETE_BIOS=BIOS-bestand(en) van NAND verwijderen RA_TOGGLE_LOAD_SCREENS=RA- en Canoe-laadschermen schakelen RESTORE_WIFI_CONFIG=Wifi-configuratie herstellen OPTIONS_TITLE=Opties -PRESS_B_EXIT=Druk op B om af te sluiten. LOADING=Laden... WIFI_BACKUP_SPLASH_LINE1=Wifi-configuratiebestand back-uppen WIFI_BACKUP_SPLASH_LINE2=Even geduld . . . DELETE_BACKUP_CONFIRM=Deze back-up verwijderen? -DELETE_CONFIRM_HINT=A = Ja B = Nee DELETE_CONFIRM_GENERIC=Dit item verwijderen? HINT_SELECT=Kiezen HINT_BACK=Terug @@ -93,3 +90,32 @@ SYSINFO_FIRMWARE=Firmware SYSINFO_SAVES=Opslagbestanden SYSINFO_FREE=vrij DIAGNOSTICS_OPTIONS=Diagnose +SAVE_MANAGER=Opslagbeheer +SAVE_MANAGER_BACKUP_DESC=Kopieert alle opgeslagen spellen naar USB:/data/saves_backup. +SAVE_MANAGER_RESTORE_DESC=Kopieert de USB-back-up terug naar de console. Opslagbestanden met dezelfde naam worden vervangen. +SAVE_MANAGER_CLEAN=Opslag van verwijderde spellen opruimen +SAVE_MANAGER_CLEAN_DESC=Verplaatst opslag van spellen die niet meer op de console staan naar USB:/data/deleted_games_saves. +SAVE_MANAGER_RESTORE_DELETED=Opslag van verwijderde spellen herstellen +SAVE_MANAGER_RESTORE_DELETED_DESC=Zet verplaatste opslag terug, voor spellen die je opnieuw hebt geïnstalleerd. +SAVE_MANAGER_STAT_SAVES=Opslag op de console +SAVE_MANAGER_STAT_BACKUP=Opslag in USB-back-up +SAVE_MANAGER_STAT_NONE=Geen +SAVE_MANAGER_STAT_ORPHANS=Opslag van verwijderde spellen +SAVE_MANAGER_STAT_KEPT=Apart gezet +SAVE_MANAGER_BACKUP_CONFIRM=Opgeslagen spellen naar USB kopiëren? +SAVE_MANAGER_RESTORE_CONFIRM=Opgeslagen spellen van USB herstellen? +SAVE_MANAGER_CLEAN_CONFIRM=Deze opslag apart zetten? +SAVE_MANAGER_RESTORE_DELETED_CONFIRM=Deze opslag herstellen? +SAVE_MANAGER_TO_MOVE=Te verplaatsen +SAVE_MANAGER_TO_RESTORE=Te herstellen +SAVE_MANAGER_NONE_MARKED=Geen opslag gemarkeerd. +SAVE_MANAGER_NONE_HINT=Druk op A bij opslag om deze te markeren. +SAVE_MANAGER_NO_ORPHANS=Geen opslag van verwijderde spellen gevonden. +SAVE_MANAGER_NO_DELETED=Geen apart gezette opslag om te herstellen. +SAVE_MANAGER_CLEAN_NOTE=Bewaard in USB:/data/deleted_games_saves +SAVE_MANAGER_RESTORE_NOTE=Opslag die al op de console staat wordt niet vervangen. +SAVE_MANAGER_WORKING=Bezig... +HINT_OK=OK +HINT_MOVE=Verplaatsen +HINT_RESTORE=Herstellen +SAVE_MANAGER_SELECT_ALL=Alles selecteren diff --git a/mod/etc/options_menu/language/pt-PT.lang b/mod/etc/options_menu/language/pt-PT.lang index da7fbb6..240e4f0 100644 --- a/mod/etc/options_menu/language/pt-PT.lang +++ b/mod/etc/options_menu/language/pt-PT.lang @@ -3,7 +3,6 @@ SHUTDOWN_DEVICE=Desligar Dispositivo REBOOT_DEVICE=Reiniciar Dispositivo ADVANCED_OPTIONS=Opções Avançadas NETWORK_OPTIONS=Rede -SAVED_GAMES_OPTIONS=Jogos Guardados EXIT=Sair MODULE_UNINSTALLER=Desinstalador de Módulos DISPLAY_TEMP=Ver Temperatura @@ -42,12 +41,10 @@ RA_DELETE_BIOS=Apagar Ficheiro(s) de BIOS da NAND RA_TOGGLE_LOAD_SCREENS=Alternar Ecrãs de Carregamento RA e Canoe RESTORE_WIFI_CONFIG=Restaurar Configuração Wifi OPTIONS_TITLE=Opções -PRESS_B_EXIT=Prime B para sair. LOADING=A carregar... WIFI_BACKUP_SPLASH_LINE1=Cópia de Segurança Wifi WIFI_BACKUP_SPLASH_LINE2=Aguarde Por Favor . . . DELETE_BACKUP_CONFIRM=Apagar esta cópia de segurança? -DELETE_CONFIRM_HINT=A = Sim B = Não DELETE_CONFIRM_GENERIC=Apagar este item? HINT_SELECT=Selecionar HINT_BACK=Voltar @@ -93,3 +90,32 @@ SYSINFO_FIRMWARE=Firmware SYSINFO_SAVES=Saves SYSINFO_FREE=livres DIAGNOSTICS_OPTIONS=Diagnóstico +SAVE_MANAGER=Gestor de Jogos Guardados +SAVE_MANAGER_BACKUP_DESC=Copia todos os jogos guardados para USB:/data/saves_backup. +SAVE_MANAGER_RESTORE_DESC=Copia a cópia de segurança da USB de volta para a consola. Os jogos guardados com o mesmo nome são substituídos. +SAVE_MANAGER_CLEAN=Limpar Jogos Guardados de Jogos Removidos +SAVE_MANAGER_CLEAN_DESC=Move os jogos guardados de jogos que já não estão na consola para USB:/data/deleted_games_saves. +SAVE_MANAGER_RESTORE_DELETED=Restaurar Jogos Guardados de Jogos Removidos +SAVE_MANAGER_RESTORE_DELETED_DESC=Devolve os jogos guardados movidos, para jogos que voltaste a instalar. +SAVE_MANAGER_STAT_SAVES=Jogos guardados na consola +SAVE_MANAGER_STAT_BACKUP=Jogos guardados no backup USB +SAVE_MANAGER_STAT_NONE=Nenhum +SAVE_MANAGER_STAT_ORPHANS=Jogos guardados de jogos removidos +SAVE_MANAGER_STAT_KEPT=Movidos à parte +SAVE_MANAGER_BACKUP_CONFIRM=Copiar os jogos guardados para a USB? +SAVE_MANAGER_RESTORE_CONFIRM=Restaurar os jogos guardados da USB? +SAVE_MANAGER_CLEAN_CONFIRM=Mover estes jogos guardados? +SAVE_MANAGER_RESTORE_DELETED_CONFIRM=Restaurar estes jogos guardados? +SAVE_MANAGER_TO_MOVE=A Mover +SAVE_MANAGER_TO_RESTORE=A Restaurar +SAVE_MANAGER_NONE_MARKED=Nenhum jogo guardado marcado. +SAVE_MANAGER_NONE_HINT=Carrega em A num jogo guardado para o marcar. +SAVE_MANAGER_NO_ORPHANS=Não há jogos guardados de jogos removidos. +SAVE_MANAGER_NO_DELETED=Não há jogos guardados movidos para restaurar. +SAVE_MANAGER_CLEAN_NOTE=Guardados em USB:/data/deleted_games_saves +SAVE_MANAGER_RESTORE_NOTE=Os jogos guardados que já estão na consola não são substituídos. +SAVE_MANAGER_WORKING=A trabalhar... +HINT_OK=OK +HINT_MOVE=Mover +HINT_RESTORE=Restaurar +SAVE_MANAGER_SELECT_ALL=Selecionar tudo diff --git a/mod/etc/options_menu/language/ru-RU.lang b/mod/etc/options_menu/language/ru-RU.lang index cdd5790..f758b7c 100644 --- a/mod/etc/options_menu/language/ru-RU.lang +++ b/mod/etc/options_menu/language/ru-RU.lang @@ -3,7 +3,6 @@ SHUTDOWN_DEVICE=Выключить устройство REBOOT_DEVICE=Перезагрузить устройство ADVANCED_OPTIONS=Расширенные настройки NETWORK_OPTIONS=Сеть -SAVED_GAMES_OPTIONS=Сохранения EXIT=Выход MODULE_UNINSTALLER=Удаление модулей DISPLAY_TEMP=Показать температуру @@ -42,12 +41,10 @@ RA_DELETE_BIOS=Удалить файл(ы) BIOS (NAND) RA_TOGGLE_LOAD_SCREENS=Переключить экраны загрузки RA и Canoe RESTORE_WIFI_CONFIG=Восстановить настройки Wifi OPTIONS_TITLE=Настройки -PRESS_B_EXIT=Нажмите B для выхода. LOADING=Загрузка... WIFI_BACKUP_SPLASH_LINE1=Резервное копирование Wifi WIFI_BACKUP_SPLASH_LINE2=Пожалуйста, подождите . . . DELETE_BACKUP_CONFIRM=Удалить эту резервную копию? -DELETE_CONFIRM_HINT=A = Да B = Нет DELETE_CONFIRM_GENERIC=Удалить этот элемент? HINT_SELECT=Выбрать HINT_BACK=Назад @@ -93,3 +90,32 @@ SYSINFO_FIRMWARE=Прошивка SYSINFO_SAVES=Сохранения SYSINFO_FREE=свободно DIAGNOSTICS_OPTIONS=Диагностика +SAVE_MANAGER=Менеджер сохранений +SAVE_MANAGER_BACKUP_DESC=Копирует все сохранения в USB:/data/saves_backup. +SAVE_MANAGER_RESTORE_DESC=Копирует резервную копию с USB обратно на консоль. Сохранения с тем же именем заменяются. +SAVE_MANAGER_CLEAN=Очистить сохранения удалённых игр +SAVE_MANAGER_CLEAN_DESC=Перемещает сохранения игр, которых больше нет на консоли, в USB:/data/deleted_games_saves. +SAVE_MANAGER_RESTORE_DELETED=Восстановить сохранения удалённых игр +SAVE_MANAGER_RESTORE_DELETED_DESC=Возвращает перемещённые сохранения для заново установленных игр. +SAVE_MANAGER_STAT_SAVES=Сохранений на консоли +SAVE_MANAGER_STAT_BACKUP=Сохранений в копии на USB +SAVE_MANAGER_STAT_NONE=Нет +SAVE_MANAGER_STAT_ORPHANS=Сохранений удалённых игр +SAVE_MANAGER_STAT_KEPT=Перемещено +SAVE_MANAGER_BACKUP_CONFIRM=Скопировать сохранения на USB? +SAVE_MANAGER_RESTORE_CONFIRM=Восстановить сохранения с USB? +SAVE_MANAGER_CLEAN_CONFIRM=Переместить эти сохранения? +SAVE_MANAGER_RESTORE_DELETED_CONFIRM=Восстановить эти сохранения? +SAVE_MANAGER_TO_MOVE=К перемещению +SAVE_MANAGER_TO_RESTORE=К восстановлению +SAVE_MANAGER_NONE_MARKED=Сохранения не отмечены. +SAVE_MANAGER_NONE_HINT=Нажмите A на сохранении, чтобы отметить его. +SAVE_MANAGER_NO_ORPHANS=Сохранений удалённых игр не найдено. +SAVE_MANAGER_NO_DELETED=Нет перемещённых сохранений для восстановления. +SAVE_MANAGER_CLEAN_NOTE=Хранятся в USB:/data/deleted_games_saves +SAVE_MANAGER_RESTORE_NOTE=Сохранения, уже имеющиеся на консоли, не заменяются. +SAVE_MANAGER_WORKING=Выполняется... +HINT_OK=OK +HINT_MOVE=Переместить +HINT_RESTORE=Восстановить +SAVE_MANAGER_SELECT_ALL=Выбрать все diff --git a/mod/etc/options_menu/save_manager/launch_save_manager.sh b/mod/etc/options_menu/save_manager/launch_save_manager.sh new file mode 100644 index 0000000..e478bae --- /dev/null +++ b/mod/etc/options_menu/save_manager/launch_save_manager.sh @@ -0,0 +1,39 @@ +#!/bin/sh +# Copyright (C) 2026 DefKorns (https://defkorns.github.io/LICENSE) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +# save_manager exit: 0 = back to the menu that opened it (top of OM_BACK_STACK), else leave the menu +options_path="${1%/}" + +"$options_path/save_manager/save_manager" +if [ $? -ne 0 ]; then + sh "$options_path/scripts/ResumeUI.sh" + exit 0 +fi + +entry="${OM_BACK_STACK##*;}" +remaining="${OM_BACK_STACK%;*}" +[ "$remaining" = "$OM_BACK_STACK" ] && remaining="" +command_path="${entry%%,*}" +rest="${entry#*,}" +script_path="${rest%%,*}" +title_key="${rest#*,}" + +usleep 50000 +if [ -n "$script_path" ]; then + OM_BACK_STACK="$remaining" "$options_path/options" --commandPath "$command_path" --scriptPath "$script_path" --title "$title_key" +else + OM_BACK_STACK="$remaining" "$options_path/options" --commandPath "$command_path" --title "$title_key" +fi diff --git a/mod/etc/options_menu/scripts/OrphanSaves.sh b/mod/etc/options_menu/scripts/OrphanSaves.sh new file mode 100644 index 0000000..f62f265 --- /dev/null +++ b/mod/etc/options_menu/scripts/OrphanSaves.sh @@ -0,0 +1,131 @@ +#!/bin/sh +# Copyright (C) 2026 DefKorns (https://defkorns.github.io/LICENSE) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +# Saves of games that are no longer on the console. An entry is either a game +# code (CLV-..., the folder of its SRAM and suspend points) or retroarch/ +# (every RetroArch save/state file of that content). +# OrphanSaves.sh stats saves, USB backup (-1 = none), orphans, moved aside +# OrphanSaves.sh orphans entries with saves but no game +# OrphanSaves.sh deleted entries already moved aside +# OrphanSaves.sh move FILE move the entries listed in FILE aside +# OrphanSaves.sh restore FILE move them back (never over an existing save) + +source /etc/preinit +script_init + +saves_root=/media/hakchi/saves +backup_root=/media/data/saves_backup +deleted_root=/media/data/deleted_games_saves +ra_save_suffix='\.(srm|rtc|state([0-9]+)?(\.auto)?)(\.png)?$' + +raContent() { + sed -E "s/$ra_save_suffix//" +} + +saveEntries() { + ls "$1" 2>/dev/null | grep "^CLV-" | grep -v "^CLV-S-" + ls "$1/retroarch" 2>/dev/null | raContent | sed 's#^#retroarch/#' +} + +entriesOnConsole() { + local games_root + games_root="$(dirname "$(findGameStorage)")" + { + find "$games_root" -maxdepth 4 -name "*.desktop" -exec sed -n "s/^Code=//p" {} + + ls "$games_root"/*/.storage "$squashfs/usr/share/games" + { + find "$games_root"/*/.storage "$squashfs/usr/share/games" "$rootfs/usr/share/games" -mindepth 2 -maxdepth 2 -type f + find "$games_root" -maxdepth 4 -name "*.desktop" -exec sed -n "s/^Exec=//p" {} + | tr ' ' '\n' | grep / + } | sed 's#.*/##' | while IFS= read -r rom; do + echo "retroarch/$rom" + echo "retroarch/${rom%.*}" + done + } 2>/dev/null | grep -E "^(CLV-|retroarch/)" | sort -u +} + +listOrphans() { + local on_console + on_console="$(mktemp)" + entriesOnConsole > "$on_console" + saveEntries "$saves_root" | sort -u | comm -23 - "$on_console" + rm -f "$on_console" +} + +isEntry() { + case "$1" in + CLV-S-*|*/*/*|retroarch/|retroarch/.|retroarch/..) return 1 ;; + CLV-*) case "$1" in */*) return 1 ;; esac ;; + retroarch/*) ;; + *) return 1 ;; + esac +} + +moveEntry() { + local from="$1" to="$2" entry="$3" content file + case "$entry" in + retroarch/*) + content="${entry#retroarch/}" + mkdir -p "$to/retroarch" + ls "$from/retroarch" 2>/dev/null | while IFS= read -r file; do + [ "$(echo "$file" | raContent)" = "$content" ] || continue + [ -e "$to/retroarch/$file" ] && [ "$to" = "$saves_root" ] && continue + mv -f "$from/retroarch/$file" "$to/retroarch/$file" + done + rmdir "$from/retroarch" 2>/dev/null + ;; + *) + [ -d "$from/$entry" ] || return + [ -e "$to/$entry" ] && [ "$to" = "$saves_root" ] && return + rm -rf "$to/$entry" + mv "$from/$entry" "$to/$entry" + ;; + esac +} + +moveListed() { + local from="$1" to="$2" list="$3" entry + mkdir -pm 777 "$to" + while IFS= read -r entry; do + isEntry "$entry" && moveEntry "$from" "$to" "$entry" + done < "$list" + rmdir "$deleted_root" 2>/dev/null + sync +} + +case "$1" in + stats) + saveEntries "$saves_root" | sort -u | wc -l + if [ -d "$backup_root" ]; then saveEntries "$backup_root" | sort -u | wc -l; else echo -1; fi + listOrphans | wc -l + saveEntries "$deleted_root" | sort -u | wc -l + ;; + orphans) + listOrphans + ;; + deleted) + saveEntries "$deleted_root" | sort -u + ;; + move) + [ -f "$2" ] && moveListed "$saves_root" "$deleted_root" "$2" + ;; + restore) + [ -f "$2" ] && moveListed "$deleted_root" "$saves_root" "$2" + ;; + *) + echo "usage: $0 stats|orphans|deleted|move FILE|restore FILE" >&2 + exit 2 + ;; +esac diff --git a/mod/readme.md b/mod/readme.md index aa2489b..6fc5f08 100644 --- a/mod/readme.md +++ b/mod/readme.md @@ -9,7 +9,7 @@ The Options Menu is a custom, easily extendable menu for your console, letting o - **Power:** Hibernate/Standby, Shutdown, Restart - **RetroArch:** back up/restore settings (NAND or USB), restore defaults, transfer BIOS files, clean up overrides/remaps/BIOS, toggle load screens - **Network:** show IP, reconnect, search for SSIDs, back up/restore your wifi config (NAND or USB) -- **Saved Games:** back up/restore saves to USB +- **Save Manager:** back up/restore saves to USB, and move aside (or bring back) saves of games no longer on the console - **Language:** English, Français, Deutsch, Español, Italiano, Nederlands, Português, Русский, 日本語 - **Advanced:** - Controller: change the menu button combo, autofire (speed, X/Y as turbo A/B), Home menu combo, Start on the 2nd controller diff --git a/src/save_manager.cpp b/src/save_manager.cpp new file mode 100644 index 0000000..5a55d7f --- /dev/null +++ b/src/save_manager.cpp @@ -0,0 +1,786 @@ +/** + * Copyright (C) 2026 DefKorns + * Based on the Module Uninstaller screen by CompCom. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 3 + * of the License, or (at your option) any later version. + */ + +#include "framework/badge.h" +#include "framework/dialog.h" +#include "framework/sdl_helper.h" +#include "framework/controller.h" +#include "framework/powerwatch.h" +#include "framework/draw_helpers.h" +#include "framework/utf8.h" +#include "localization.h" + +#include +#include +#include +#include +#include +#include +#include + +namespace +{ + const std::string OptionsLocation = "/etc/options_menu/"; + const std::string ScriptLocation = OptionsLocation + "scripts/"; + const std::string SelectionFile = "/tmp/save_manager_selection"; + const std::string RetroArchPrefix = "retroarch/"; + + // narrower list than the shared theme: entries are short codes, the detail panels need the room + const int ListW = 600; + const int ListRightX = UiTheme::ListX + ListW - 40; + const int ControlRightX = ListRightX - 16; + const int DetailX = UiTheme::ListX + ListW + 16; + const int DetailW = UiTheme::FrameX + UiTheme::FrameW - 16 - DetailX; + + const int ListFontSize = 16; + const int PanelFontSize = 16; + const int PanelHeaderFontSize = 18; + const int NoteFontSize = 15; + + const int ExitBack = 0; + const int ExitLeaveMenu = 1; + + const Color White{ 255, 255, 255 }; + + enum class Action { Backup, RestoreUsb, Clean, RestoreDeleted }; + + struct ActionInfo + { + Action action; + std::string titleKey; + std::string descriptionKey; + }; + + const std::vector Actions{ + { Action::Backup, "BACKUP_SAVES_USB", "SAVE_MANAGER_BACKUP_DESC" }, + { Action::RestoreUsb, "RESTORE_SAVES_USB", "SAVE_MANAGER_RESTORE_DESC" }, + { Action::Clean, "SAVE_MANAGER_CLEAN", "SAVE_MANAGER_CLEAN_DESC" }, + { Action::RestoreDeleted, "SAVE_MANAGER_RESTORE_DELETED", "SAVE_MANAGER_RESTORE_DELETED_DESC" }, + }; + + struct SaveEntry + { + std::string code; + Texture text; + bool marked = false; + }; + + std::string TrimTrailingNewline(std::string s) + { + while(!s.empty() && (s.back() == '\n' || s.back() == '\r')) + s.pop_back(); + return s; + } + + std::vector ReadLines(const std::string & command) + { + std::vector lines; + FILE * pipe = popen(command.c_str(), "r"); + if(!pipe) + return lines; + char buf[256]; + while(fgets(buf, sizeof(buf), pipe)) + { + std::string line = TrimTrailingNewline(buf); + if(!line.empty()) + lines.push_back(line); + } + pclose(pipe); + return lines; + } + + std::string OrphanSavesCommand(const std::string & args) + { + return "sh " + ScriptLocation + "OrphanSaves.sh " + args; + } + + int TextWidth(const std::string & text, int fontSize) + { + if(CanRenderWithTTF(text, fontSize)) + return MeasureTTFWidth(text, fontSize); + return Utf8Length(text) * fontSize; + } + + std::string TruncateToWidth(const std::string & text, int fontSize, int maxWidth) + { + if(TextWidth(text, fontSize) <= maxWidth) + return text; + int n = Utf8Length(text); + while(n > 0 && TextWidth(TruncateUtf8(text, n) + "...", fontSize) > maxWidth) + --n; + return TruncateUtf8(text, n) + "..."; + } + + std::vector WrapToWidth(const std::string & text, int fontSize, int maxWidth) + { + std::vector lines; + std::string line; + size_t pos = 0; + while(pos < text.size()) + { + size_t end = text.find(' ', pos); + std::string word = text.substr(pos, end == std::string::npos ? std::string::npos : end - pos); + pos = (end == std::string::npos) ? text.size() : end + 1; + + std::string candidate = line.empty() ? word : line + " " + word; + if(TextWidth(candidate, fontSize) <= maxWidth) + { + line = candidate; + continue; + } + if(!line.empty()) + lines.push_back(line); + line = word; + while(TextWidth(line, fontSize) > maxWidth && Utf8Length(line) > 1) + { + int n = Utf8Length(line) - 1; + while(n > 1 && TextWidth(TruncateUtf8(line, n), fontSize) > maxWidth) + --n; + std::string head = TruncateUtf8(line, n); + lines.push_back(head); + line = line.substr(head.size()); + } + } + if(!line.empty()) + lines.push_back(line); + return lines; + } + + std::vector MakeLines(const std::vector & lines, int fontSize, SDL_Renderer * renderer, Color color) + { + std::vector textures; + for(const std::string & line : lines) + textures.emplace_back(line, fontSize, renderer, 0, 0, false, ToAbgr(color), true); + return textures; + } + + std::string EntryLabel(const std::string & entry) + { + if(entry.compare(0, RetroArchPrefix.size(), RetroArchPrefix) == 0) + return "RetroArch: " + entry.substr(RetroArchPrefix.size()); + return entry; + } + + void DrawDot(SDL_Renderer * renderer, int x, int centerY) + { + DrawRoundedFillRect(renderer, { x, centerY - 4, 8, 8 }, UiTheme::Accent, 4); + } + + void DrawLinesCentered(SDL_Renderer * renderer, std::vector & lines, int centerX, int y, int pitch) + { + for(Texture & line : lines) + { + line.rect.x = centerX - line.rect.w/2; + line.rect.y = y; + line.Draw(renderer); + y += pitch; + } + } +} + +int main(int, char **) +{ + LoadLanguageFromConfig(OptionsLocation); + SetTTFFontPath(OptionsLocation); + UiTheme::LoadThemeConfig(OptionsLocation); + + SDL_Context sdl_context(std::chrono::milliseconds(33), false); + auto renderer = sdl_context.renderer; + Controller controller(1); + + const Color bg = UiTheme::Bg; + SetDrawColor(renderer, bg); + + Texture gearIcon(OptionsLocation + UiTheme::AssetGear, renderer, UiTheme::GearX, UiTheme::GearY); + Texture appTitleText("OptionsMenu", UiTheme::TitleFontSize, renderer, UiTheme::TitleX, UiTheme::TitleY, false, ToAbgr(UiTheme::Text), true); + appTitleText.rect.y -= appTitleText.rect.h / 2; + Texture appVersionText(MOD_VERSION, UiTheme::VersionFontSize, renderer, appTitleText.rect.x + appTitleText.rect.w + UiTheme::VersionGap, UiTheme::TitleY, false, ToAbgr(UiTheme::Text), true); + appVersionText.rect.y -= appVersionText.rect.h / 2; + Texture creditText("Save Manager - by DefKorns", 16, renderer, UiTheme::CreditX, UiTheme::CreditY, false, ToAbgr(UiTheme::Text), true); + Texture titleText; + auto SetTitle = [&](const std::string & key) + { + titleText = Texture(Translate(key), UiTheme::SectionTitleFontSize, renderer, UiTheme::SectionTitleX, UiTheme::SectionTitleY, false, ToAbgr(UiTheme::Text), true); + }; + + const int RowBoxH = UiTheme::RowPitch - 6; + Texture checkboxOn(OptionsLocation + UiTheme::AssetCheckboxOn, renderer); + SetColorMod(checkboxOn.texture.get(), UiTheme::Accent); + Texture checkboxOff(OptionsLocation + UiTheme::AssetCheckboxOff, renderer); + SetColorMod(checkboxOff.texture.get(), UiTheme::CheckboxOff); + const int CheckboxSize = 20; + const int CheckboxX = ControlRightX - CheckboxSize; + const int RowTextMaxW = CheckboxX - 16 - UiTheme::RowTextX; + const int displayCount = std::max(1, (UiTheme::FooterDividerY - UiTheme::ListBottomMargin - UiTheme::RowFirstY) / UiTheme::RowPitch); + + Texture scrollUp(OptionsLocation + UiTheme::AssetChevronUp, renderer, UiTheme::ScrollX, UiTheme::ScrollUpY); + SetColorMod(scrollUp.texture.get(), UiTheme::ScrollArrow); + Texture scrollDown = scrollUp; + scrollDown.rect.y = UiTheme::ScrollDownY; + + const int PanelPad = 16; + const int PanelInnerX = DetailX + PanelPad; + const int PanelInnerW = DetailW - 2*PanelPad; + const SDL_Rect summaryBox{ DetailX, UiTheme::RowFirstY, DetailW, 260 }; + const int SummaryDividerY = summaryBox.y + 46; + const int SummaryListY = summaryBox.y + 62; + const int SummaryRowPitch = 28; + const int SummaryMaxRows = (summaryBox.y + summaryBox.h - PanelPad - SummaryListY) / SummaryRowPitch; + const int BulletTextGap = 18; + const SDL_Rect noteBox{ DetailX, summaryBox.y + summaryBox.h + 16, DetailW, 72 }; + const int NoteIconCX = noteBox.x + 26; + const int StatPitch = 26; + const int StatsBoxH = 4*StatPitch + 2*PanelPad - 6; + const int DetailBottomY = UiTheme::FooterDividerY - UiTheme::ListBottomMargin; + const SDL_Rect statsBox{ DetailX, DetailBottomY - StatsBoxH, DetailW, StatsBoxH }; + const SDL_Rect descriptionBox{ DetailX, UiTheme::RowFirstY, DetailW, statsBox.y - 16 - UiTheme::RowFirstY }; + const int NoteTextX = noteBox.x + 46; + + BadgePainter badges(OptionsLocation, renderer); + auto MakeBadge = [&](const std::string & letter, const std::string & labelKey, Color rim, Color fill) -> Badge + { + return badges.Make(letter, Translate(labelKey), rim, fill); + }; + Badge badgeSelect = MakeBadge("A", "HINT_SELECT", UiTheme::BadgeADark, UiTheme::BadgeA); + Badge badgeMark = MakeBadge("A", "HINT_MARK", UiTheme::BadgeADark, UiTheme::BadgeA); + Badge badgeUnmark = MakeBadge("A", "HINT_UNMARK", UiTheme::BadgeADark, UiTheme::BadgeA); + Badge badgeOk = MakeBadge("A", "HINT_OK", UiTheme::BadgeADark, UiTheme::BadgeA); + Badge badgeBack = MakeBadge("B", "HINT_BACK", UiTheme::BadgeBDark, UiTheme::BadgeB); + Badge badgeCancel = MakeBadge("B", "HINT_CANCEL", UiTheme::BadgeBDark, UiTheme::BadgeB); + Badge confirmPill = badges.MakePill("Start + Select", Translate("HINT_CONFIRM")); + auto DrawBadge = [&](Badge & badge, int rightEdgeX) -> int + { + return badges.Draw(badge, rightEdgeX, UiTheme::BadgeBandY); + }; + auto DrawFooterDivider = [&](int rightEdge) + { + int dividerX = rightEdge - UiTheme::BadgeDividerGapFromCluster; + DrawVLine(renderer, dividerX, UiTheme::FooterY + 10, UiTheme::FooterY + UiTheme::FooterH - 10, UiTheme::Border, 2); + }; + + auto DrawChrome = [&]() + { + DrawStrokeRect(renderer, UiTheme::OuterRect, UiTheme::Border, UiTheme::BorderWidth, UiTheme::BorderRadius); + gearIcon.Draw(renderer); + appTitleText.Draw(renderer); + appVersionText.Draw(renderer); + DrawHLine(renderer, UiTheme::HeaderDividerX, UiTheme::HeaderDividerX + UiTheme::HeaderDividerW, UiTheme::HeaderDividerY, UiTheme::Border, UiTheme::BorderWidth); + DrawHLine(renderer, UiTheme::OuterRect.x, UiTheme::OuterRect.x + UiTheme::OuterRect.w, UiTheme::FooterDividerY, UiTheme::Border, UiTheme::BorderWidth); + int accentBarY = titleText.rect.y + (titleText.rect.h - UiTheme::SectionAccentBarH) / 2; + DrawFillRect(renderer, { UiTheme::SectionTitleX - UiTheme::SectionAccentBarW - 14, accentBarY, UiTheme::SectionAccentBarW, UiTheme::SectionAccentBarH }, UiTheme::Accent); + titleText.Draw(renderer); + creditText.Draw(renderer); + }; + + auto DrawRowFrame = [&](int index, bool selected) -> SDL_Rect + { + SDL_Rect rowRect{ UiTheme::ListX, UiTheme::RowFirstY + index*UiTheme::RowPitch, ListRightX - UiTheme::ListX, RowBoxH }; + if(selected) + { + DrawRoundedFillRect(renderer, rowRect, UiTheme::SelectedRowBg, UiTheme::BoxRadius); + DrawStrokeRect(renderer, rowRect, UiTheme::Accent, 2, UiTheme::BoxRadius); + } + else + DrawHLine(renderer, rowRect.x, rowRect.x + rowRect.w, rowRect.y + rowRect.h + 3, UiTheme::Border, 1); + return rowRect; + }; + + auto DrawNote = [&](std::vector & lines) + { + DrawStrokeRect(renderer, noteBox, UiTheme::Border, UiTheme::BorderWidth, UiTheme::BoxRadius); + int noteCenterY = noteBox.y + noteBox.h/2; + DrawStrokeRect(renderer, { NoteIconCX - 9, noteCenterY - 9, 18, 18 }, UiTheme::TextDim, 2, 9); + DrawFillRect(renderer, { NoteIconCX - 1, noteCenterY - 5, 2, 2 }, UiTheme::TextDim); + DrawFillRect(renderer, { NoteIconCX - 1, noteCenterY - 1, 2, 6 }, UiTheme::TextDim); + const int noteLinePitch = 20; + int y = noteCenterY - (static_cast(lines.size()) * noteLinePitch)/2; + for(Texture & line : lines) + { + line.rect.x = NoteTextX; + line.rect.y = y + (noteLinePitch - line.rect.h)/2; + line.Draw(renderer); + y += noteLinePitch; + } + }; + + auto NoteLines = [&](const std::string & key) + { + return MakeLines(WrapToWidth(Translate(key), NoteFontSize, noteBox.x + noteBox.w - PanelPad - NoteTextX), NoteFontSize, renderer, UiTheme::TextDim); + }; + + // Modal panel over the current screen: confirm (Start + Select) or acknowledge (A); B always cancels. + auto RunDialog = [&](const std::function & drawUnder, const std::string & titleKey, const std::vector & bodyLines, + const std::string & noteKey, bool confirm) -> bool + { + const int boxW = 600; + const int bodyPitch = 30; + const int badgeRowH = Dialog::BadgeRowGap + UiTheme::BadgeOuterSize; + std::vector body; + for(const std::string & line : bodyLines) + body.emplace_back(TruncateToWidth(line, PanelFontSize, boxW - 80), PanelFontSize, renderer, 0, 0, false, ToAbgr(UiTheme::Text), true); + std::vector noteLines = noteKey.empty() ? std::vector() + : MakeLines(WrapToWidth(Translate(noteKey), NoteFontSize, boxW - 48), NoteFontSize, renderer, UiTheme::TextDim); + const int boxH = 110 + static_cast(body.size())*bodyPitch + static_cast(noteLines.size())*20 + badgeRowH; + const SDL_Rect box = Dialog::PanelRect(boxW, boxH); + Texture title(Translate(titleKey), Dialog::TitleFontSize, renderer, 640, box.y + 36, true, ToAbgr(UiTheme::Text), true); + + controller.GetButtonStatus(A); + controller.GetButtonStatus(B); + for(;;) + { + SDL_Event e; + while(SDL_PollEvent(&e)) + if(e.type == SDL_QUIT) + return false; + controller.Update(); + if(sdl_context.powerwatch->buttonPress() || controller.GetButtonStatus(B)) + return false; + if(confirm && controller.PeekButtonStatus(START) && controller.PeekButtonStatus(SELECT)) + return true; + if(!confirm && controller.GetButtonStatus(A)) + return true; + + sdl_context.StartFrame(); + drawUnder(); + Dialog::DrawBackdrop(renderer); + Dialog::DrawPanel(renderer, box); + title.Draw(renderer); + int y = box.y + 72; + for(Texture & line : body) + { + line.rect.x = 640 - line.rect.w/2; + line.rect.y = y; + line.Draw(renderer); + y += bodyPitch; + } + DrawLinesCentered(renderer, noteLines, 640, y + 6, 20); + if(confirm) + badges.DrawCentered({ &badgeCancel, &confirmPill }, box.x + box.w / 2, box.y + box.h - Dialog::BottomPad - UiTheme::BadgeOuterSize); + else + badges.DrawCentered({ &badgeOk }, box.x + box.w / 2, box.y + box.h - Dialog::BottomPad - UiTheme::BadgeOuterSize); + SetDrawColor(renderer, bg); + sdl_context.EndFrame(); + } + }; + + // Shows a "working" panel for one frame, then runs the blocking command. + auto RunBusy = [&](const std::function & drawUnder, const std::string & command) + { + const SDL_Rect box = Dialog::PanelRect(420, 110); + Texture working(Translate("SAVE_MANAGER_WORKING"), Dialog::TitleFontSize, renderer, 640, box.y + box.h/2 - 12, true, ToAbgr(UiTheme::Text), true); + sdl_context.StartFrame(); + drawUnder(); + Dialog::DrawBackdrop(renderer); + Dialog::DrawPanel(renderer, box); + working.Draw(renderer); + SetDrawColor(renderer, bg); + sdl_context.EndFrame(); + system(command.c_str()); + controller.Update(); + controller.GetButtonStatus(A); + controller.GetButtonStatus(B); + }; + + // Marked-list screen shared by "clean" (orphans, all pre-marked) and "restore deleted" (none marked). + auto RunSaveList = [&](bool cleaning) -> int + { + SetTitle(cleaning ? "SAVE_MANAGER_CLEAN" : "SAVE_MANAGER_RESTORE_DELETED"); + Texture summaryHeader(Translate(cleaning ? "SAVE_MANAGER_TO_MOVE" : "SAVE_MANAGER_TO_RESTORE"), PanelHeaderFontSize, renderer, PanelInnerX, summaryBox.y + 14, false, ToAbgr(UiTheme::TextDim), true); + Texture noneMarkedText(Translate("SAVE_MANAGER_NONE_MARKED"), PanelFontSize, renderer, PanelInnerX, SummaryListY, false, ToAbgr(UiTheme::Text), true); + std::vector noneHintLines = MakeLines(WrapToWidth(Translate("SAVE_MANAGER_NONE_HINT"), PanelFontSize, PanelInnerW), PanelFontSize, renderer, UiTheme::TextDim); + std::vector noteLines = NoteLines(cleaning ? "SAVE_MANAGER_CLEAN_NOTE" : "SAVE_MANAGER_RESTORE_NOTE"); + Badge actionPill = badges.MakePill("Start", ""); + + std::vector entries; + int listOffset = 0; + int currentId = 0; + int markedCount = 0; + Texture summaryCount; + Texture moreText; + std::vector summaryNames; + + const int selectAllRows = 1; + Texture selectAllText(Translate("SAVE_MANAGER_SELECT_ALL"), ListFontSize, renderer, UiTheme::RowTextX, 0, false, ToAbgr(White), true); + + auto Marked = [&]() + { + std::vector marked; + for(const SaveEntry & entry : entries) + if(entry.marked) + marked.push_back(&entry); + return marked; + }; + + auto RefreshSummary = [&]() + { + auto marked = Marked(); + const int count = static_cast(marked.size()); + markedCount = count; + summaryCount = Texture(std::to_string(count), PanelHeaderFontSize, renderer, 0, summaryBox.y + 14, false, ToAbgr(count ? UiTheme::Accent : UiTheme::TextDim), true); + summaryCount.rect.x = summaryBox.x + summaryBox.w - PanelPad - summaryCount.rect.w; + actionPill.label = Texture(Translate(cleaning ? "HINT_MOVE" : "HINT_RESTORE") + " (" + std::to_string(count) + ")", 16, renderer, 0, 0, false, ToAbgr(UiTheme::Text), true); + + int shown = (count > SummaryMaxRows) ? SummaryMaxRows - 1 : count; + summaryNames.clear(); + for(int i = 0; i < shown; ++i) + summaryNames.emplace_back(TruncateToWidth(EntryLabel(marked[i]->code), PanelFontSize, PanelInnerW - BulletTextGap), PanelFontSize, renderer, PanelInnerX + BulletTextGap, 0, false, ToAbgr(UiTheme::Text), true); + moreText = (shown < count) + ? Texture("+" + std::to_string(count - shown), PanelFontSize, renderer, PanelInnerX + BulletTextGap, 0, false, ToAbgr(UiTheme::TextDim), true) + : Texture(); + }; + + auto Load = [&]() + { + entries.clear(); + for(const std::string & code : ReadLines(OrphanSavesCommand(cleaning ? "orphans" : "deleted"))) + { + SaveEntry entry; + entry.code = code; + entry.text = Texture(TruncateToWidth(EntryLabel(code), ListFontSize, RowTextMaxW), ListFontSize, renderer, UiTheme::RowTextX, 0, false, ToAbgr(White), true); + entry.marked = cleaning; + entries.push_back(entry); + } + listOffset = 0; + currentId = 0; + RefreshSummary(); + }; + + auto DrawScreen = [&]() + { + DrawChrome(); + const int entryCount = static_cast(entries.size()); + const int rowCount = entryCount + selectAllRows; + const int count = std::min(displayCount, rowCount - listOffset); + for(int i = 0; i < count; ++i) + { + const int row = listOffset + i; + SDL_Rect rowRect = DrawRowFrame(i, i == currentId); + int centerY = rowRect.y + rowRect.h/2; + Texture & text = row == 0 ? selectAllText : entries[row - selectAllRows].text; + const bool marked = row == 0 ? markedCount == entryCount : entries[row - selectAllRows].marked; + text.rect.y = centerY - text.rect.h/2; + SetColorMod(text.texture.get(), row != 0 && marked ? UiTheme::TextDim : UiTheme::Text); + text.Draw(renderer); + Texture & checkbox = marked ? checkboxOn : checkboxOff; + checkbox.rect.x = CheckboxX; + checkbox.rect.y = centerY - checkbox.rect.h/2; + checkbox.Draw(renderer); + } + if(listOffset != 0) + scrollUp.Draw(renderer); + if(listOffset + displayCount < rowCount) + scrollDown.Draw(renderer, SDL_FLIP_VERTICAL); + + DrawStrokeRect(renderer, summaryBox, UiTheme::Border, UiTheme::BorderWidth, UiTheme::BoxRadius); + summaryHeader.Draw(renderer); + summaryCount.Draw(renderer); + DrawHLine(renderer, PanelInnerX, PanelInnerX + PanelInnerW, SummaryDividerY, UiTheme::Border, 1); + if(markedCount == 0) + { + noneMarkedText.Draw(renderer); + int y = SummaryListY + 28; + for(Texture & line : noneHintLines) + { + line.rect.x = PanelInnerX; + line.rect.y = y; + line.Draw(renderer); + y += 22; + } + } + else + { + int y = SummaryListY; + for(Texture & name : summaryNames) + { + int centerY = y + SummaryRowPitch/2 - 4; + DrawDot(renderer, PanelInnerX, centerY); + name.rect.y = centerY - name.rect.h/2; + name.Draw(renderer); + y += SummaryRowPitch; + } + if(moreText.texture) + { + moreText.rect.y = y + SummaryRowPitch/2 - 4 - moreText.rect.h/2; + moreText.Draw(renderer); + } + } + DrawNote(noteLines); + + int rightEdge = UiTheme::BadgeClusterRightX; + const int row = listOffset + currentId; + const bool rowMarked = row == 0 ? markedCount == entryCount : entries[row - selectAllRows].marked; + rightEdge = DrawBadge(rowMarked ? badgeUnmark : badgeMark, rightEdge); + rightEdge = DrawBadge(badgeBack, rightEdge); + if(markedCount > 0) + rightEdge = DrawBadge(actionPill, rightEdge); + DrawFooterDivider(rightEdge); + }; + + Load(); + for(;;) + { + SDL_Event e; + while(SDL_PollEvent(&e)) + if(e.type == SDL_QUIT) + return ExitLeaveMenu; + + controller.Update(); + if(sdl_context.powerwatch->buttonPress()) + return ExitLeaveMenu; + + const int entryCount = static_cast(entries.size()); + const int rowCount = entryCount + selectAllRows; + if(controller.HeldRepeat(UP)) + { + if(currentId > 0) + --currentId; + else if(listOffset > 0) + --listOffset; + } + else if(controller.HeldRepeat(DOWN)) + { + if(currentId < displayCount-1 && listOffset+currentId+1 < rowCount) + ++currentId; + else if(listOffset+displayCount < rowCount) + ++listOffset; + } + else if(controller.GetButtonStatus(A) && entryCount > 0) + { + const int row = listOffset + currentId; + if(row == 0) + { + const bool markAll = markedCount != entryCount; + for(SaveEntry & entry : entries) + entry.marked = markAll; + } + else + entries[row - selectAllRows].marked = !entries[row - selectAllRows].marked; + RefreshSummary(); + } + else if(controller.GetButtonStatus(B)) + return ExitBack; + else if(controller.GetButtonStatus(START) && markedCount > 0) + { + auto marked = Marked(); + std::vector lines; + const int maxNames = 6; + for(int i = 0; i < std::min(maxNames, static_cast(marked.size())); ++i) + lines.push_back(EntryLabel(marked[i]->code)); + if(static_cast(marked.size()) > maxNames) + lines.push_back("+" + std::to_string(marked.size() - maxNames)); + if(RunDialog(DrawScreen, cleaning ? "SAVE_MANAGER_CLEAN_CONFIRM" : "SAVE_MANAGER_RESTORE_DELETED_CONFIRM", lines, + cleaning ? "SAVE_MANAGER_CLEAN_NOTE" : "SAVE_MANAGER_RESTORE_NOTE", true)) + { + std::ofstream selection(SelectionFile); + for(const SaveEntry * entry : marked) + selection << entry->code << "\n"; + selection.close(); + RunBusy(DrawScreen, OrphanSavesCommand((cleaning ? "move " : "restore ") + SelectionFile)); + std::remove(SelectionFile.c_str()); + Load(); + } + } + + sdl_context.StartFrame(); + DrawScreen(); + SetDrawColor(renderer, bg); + sdl_context.EndFrame(); + } + }; + + struct Stats + { + int saves = 0; + int orphans = 0; + int deleted = 0; + int backupSaves = -1; + } stats; + + std::vector actionTexts; + for(const ActionInfo & info : Actions) + actionTexts.emplace_back(Translate(info.titleKey), ListFontSize, renderer, UiTheme::RowTextX, 0, false, ToAbgr(White), true); + std::vector actionCounts(Actions.size()); + std::vector statLabels; + std::vector statValues; + std::vector descriptionLines; + Texture detailHeader; + int currentAction = 0; + + auto RefreshStats = [&]() + { + const std::vector values = ReadLines(OrphanSavesCommand("stats")); + auto Value = [&](size_t i) { return i < values.size() ? std::atoi(values[i].c_str()) : 0; }; + stats.saves = Value(0); + stats.backupSaves = values.size() > 1 ? Value(1) : -1; + stats.orphans = Value(2); + stats.deleted = Value(3); + + const int counts[] = { -1, -1, stats.orphans, stats.deleted }; + for(size_t i = 0; i < Actions.size(); ++i) + actionCounts[i] = counts[i] < 0 ? Texture() + : Texture(std::to_string(counts[i]), ListFontSize, renderer, 0, 0, false, ToAbgr(counts[i] ? UiTheme::Accent : UiTheme::TextDim), true); + + const std::vector> rows{ + { "SAVE_MANAGER_STAT_SAVES", std::to_string(stats.saves) }, + { "SAVE_MANAGER_STAT_BACKUP", stats.backupSaves >= 0 ? std::to_string(stats.backupSaves) : Translate("SAVE_MANAGER_STAT_NONE") }, + { "SAVE_MANAGER_STAT_ORPHANS", std::to_string(stats.orphans) }, + { "SAVE_MANAGER_STAT_KEPT", std::to_string(stats.deleted) }, + }; + statLabels.clear(); + statValues.clear(); + for(const auto & row : rows) + { + statValues.emplace_back(row.second, PanelFontSize, renderer, 0, 0, false, ToAbgr(UiTheme::Text), true); + const int labelMaxW = PanelInnerW - statValues.back().rect.w - 12; + statLabels.emplace_back(TruncateToWidth(Translate(row.first), PanelFontSize, labelMaxW), PanelFontSize, renderer, PanelInnerX, 0, false, ToAbgr(UiTheme::TextDim), true); + } + }; + + auto RefreshDetail = [&]() + { + const ActionInfo & info = Actions[currentAction]; + detailHeader = Texture(TruncateToWidth(Translate(info.titleKey), PanelHeaderFontSize, PanelInnerW), PanelHeaderFontSize, renderer, PanelInnerX, summaryBox.y + 14, false, ToAbgr(UiTheme::TextDim), true); + descriptionLines = MakeLines(WrapToWidth(Translate(info.descriptionKey), PanelFontSize, PanelInnerW), PanelFontSize, renderer, UiTheme::Text); + }; + + auto DrawActions = [&]() + { + DrawChrome(); + for(int i = 0; i < static_cast(Actions.size()); ++i) + { + SDL_Rect rowRect = DrawRowFrame(i, i == currentAction); + int centerY = rowRect.y + rowRect.h/2; + actionTexts[i].rect.y = centerY - actionTexts[i].rect.h/2; + actionTexts[i].Draw(renderer); + if(actionCounts[i].texture) + { + actionCounts[i].rect.x = ControlRightX - actionCounts[i].rect.w; + actionCounts[i].rect.y = centerY - actionCounts[i].rect.h/2; + actionCounts[i].Draw(renderer); + } + } + + DrawStrokeRect(renderer, descriptionBox, UiTheme::Border, UiTheme::BorderWidth, UiTheme::BoxRadius); + detailHeader.Draw(renderer); + DrawHLine(renderer, PanelInnerX, PanelInnerX + PanelInnerW, SummaryDividerY, UiTheme::Border, 1); + int y = SummaryListY; + for(Texture & line : descriptionLines) + { + if(y + line.rect.h > descriptionBox.y + descriptionBox.h - 6) + break; + line.rect.x = PanelInnerX; + line.rect.y = y; + line.Draw(renderer); + y += 22; + } + + DrawStrokeRect(renderer, statsBox, UiTheme::Border, UiTheme::BorderWidth, UiTheme::BoxRadius); + for(size_t i = 0; i < statLabels.size(); ++i) + { + const int rowY = statsBox.y + PanelPad - 2 + static_cast(i) * StatPitch; + statLabels[i].rect.y = rowY; + statLabels[i].Draw(renderer); + statValues[i].rect.x = PanelInnerX + PanelInnerW - statValues[i].rect.w; + statValues[i].rect.y = rowY; + statValues[i].Draw(renderer); + } + + int rightEdge = UiTheme::BadgeClusterRightX; + rightEdge = DrawBadge(badgeSelect, rightEdge); + rightEdge = DrawBadge(badgeBack, rightEdge); + DrawFooterDivider(rightEdge); + }; + + auto ShowMessage = [&](const std::string & titleKey, const std::string & noteKey) + { + RunDialog(DrawActions, titleKey, {}, noteKey, false); + }; + + auto RunAction = [&](Action action) -> int + { + switch(action) + { + case Action::Backup: + if(RunDialog(DrawActions, "SAVE_MANAGER_BACKUP_CONFIRM", {}, "SAVE_MANAGER_BACKUP_DESC", true)) + { + RunBusy(DrawActions, "sh " + ScriptLocation + "BackupSavesUsb.sh >/dev/null 2>&1"); + RefreshStats(); + ShowMessage("BACKUP_SAVES_USB_DONE", ""); + } + return -1; + case Action::RestoreUsb: + if(stats.backupSaves < 0) + { + ShowMessage("RESTORE_SAVES_USB_NOT_FOUND", ""); + return -1; + } + if(RunDialog(DrawActions, "SAVE_MANAGER_RESTORE_CONFIRM", { Translate("SAVE_MANAGER_STAT_BACKUP") + ": " + std::to_string(stats.backupSaves) }, "SAVE_MANAGER_RESTORE_DESC", true)) + { + RunBusy(DrawActions, "sh " + ScriptLocation + "RestoreSavesUsb.sh >/dev/null 2>&1"); + RefreshStats(); + ShowMessage("RESTORE_SAVES_USB_DONE", ""); + } + return -1; + case Action::Clean: + case Action::RestoreDeleted: + { + const bool cleaning = action == Action::Clean; + if((cleaning ? stats.orphans : stats.deleted) == 0) + { + ShowMessage(cleaning ? "SAVE_MANAGER_NO_ORPHANS" : "SAVE_MANAGER_NO_DELETED", ""); + return -1; + } + const int result = RunSaveList(cleaning); + SetTitle("SAVE_MANAGER"); + RefreshStats(); + return result == ExitLeaveMenu ? ExitLeaveMenu : -1; + } + } + return -1; + }; + + SetTitle("SAVE_MANAGER"); + RefreshStats(); + RefreshDetail(); + for(;;) + { + SDL_Event e; + while(SDL_PollEvent(&e)) + if(e.type == SDL_QUIT) + return ExitLeaveMenu; + + controller.Update(); + if(sdl_context.powerwatch->buttonPress()) + return ExitLeaveMenu; + + if(controller.HeldRepeat(UP) && currentAction > 0) + { + --currentAction; + RefreshDetail(); + } + else if(controller.HeldRepeat(DOWN) && currentAction + 1 < static_cast(Actions.size())) + { + ++currentAction; + RefreshDetail(); + } + else if(controller.GetButtonStatus(A)) + { + if(RunAction(Actions[currentAction].action) == ExitLeaveMenu) + return ExitLeaveMenu; + } + else if(controller.GetButtonStatus(B)) + return ExitBack; + + sdl_context.StartFrame(); + DrawActions(); + SetDrawColor(renderer, bg); + sdl_context.EndFrame(); + } +} From b6f58e467d1b1e60430fc86bab9c65ac68053505 Mon Sep 17 00:00:00 2001 From: DefKorns Date: Fri, 9 Oct 2026 00:09:56 +0000 Subject: [PATCH 077/104] Pin the hmod-build workflow and submodule to 9f553b5 --- .github/workflows/build.yml | 3 ++- hmod-build | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 4cca7fa..440e120 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -8,7 +8,8 @@ on: jobs: hmod: - uses: DefKorns/hmod-build/.github/workflows/hmod.yml@master + # Keep in sync with the hmod-build submodule commit. + uses: DefKorns/hmod-build/.github/workflows/hmod.yml@9f553b5718ca671cf7bf59f9c714c3e07042ee4d with: release-name: Options Menu docker: true diff --git a/hmod-build b/hmod-build index 38654e9..9f553b5 160000 --- a/hmod-build +++ b/hmod-build @@ -1 +1 @@ -Subproject commit 38654e9535a04120c81a9276447d785514001c59 +Subproject commit 9f553b5718ca671cf7bf59f9c714c3e07042ee4d From a0b34ccd399fce4688a4b5a928e968a5285bd7c9 Mon Sep 17 00:00:00 2001 From: DefKorns Date: Tue, 13 Oct 2026 13:10:27 +0000 Subject: [PATCH 078/104] Harden argument parsing, child process cleanup and save backup error reporting --- mod/etc/options_menu/language/de-DE.lang | 2 + mod/etc/options_menu/language/en-US.lang | 2 + mod/etc/options_menu/language/es-ES.lang | 2 + mod/etc/options_menu/language/fr-FR.lang | 2 + mod/etc/options_menu/language/it-IT.lang | 2 + mod/etc/options_menu/language/ja-JP.lang | 2 + mod/etc/options_menu/language/nl-NL.lang | 2 + mod/etc/options_menu/language/pt-PT.lang | 2 + mod/etc/options_menu/language/ru-RU.lang | 2 + .../options_menu/scripts/BackupSavesUsb.sh | 2 +- .../options_menu/scripts/RestoreSavesUsb.sh | 2 +- src/command.cpp | 15 ++++++- src/framework/draw_helpers.cpp | 2 + src/framework/texture.cpp | 6 ++- src/gen_splash.cpp | 2 + src/localization.cpp | 2 +- src/main.cpp | 40 +++++++++++-------- src/mod_uninstall.cpp | 17 ++++---- src/save_manager.cpp | 14 ++++--- 19 files changed, 82 insertions(+), 38 deletions(-) diff --git a/mod/etc/options_menu/language/de-DE.lang b/mod/etc/options_menu/language/de-DE.lang index 31f26a9..0d7dcd6 100644 --- a/mod/etc/options_menu/language/de-DE.lang +++ b/mod/etc/options_menu/language/de-DE.lang @@ -13,6 +13,8 @@ BACKUP_SAVES_USB=Spielstände auf USB sichern RESTORE_SAVES_USB=Spielstände von USB wiederherstellen BACKUP_SAVES_USB_DONE=Spielstände auf USB:/data/saves_backup gesichert RESTORE_SAVES_USB_DONE=Spielstände von USB:/data/saves_backup wiederhergestellt +BACKUP_SAVES_USB_FAILED=Sicherung der Spielstände auf USB fehlgeschlagen. +RESTORE_SAVES_USB_FAILED=Wiederherstellung der Spielstände von USB fehlgeschlagen. RESTORE_SAVES_USB_NOT_FOUND=Keine Spielstand-Sicherung auf USB gefunden. TOGGLE_USB_WRITE=USB-Schreibzugriff umschalten RA_DEBUGGER=RetroArch-Debugger (USB-Logs) diff --git a/mod/etc/options_menu/language/en-US.lang b/mod/etc/options_menu/language/en-US.lang index beae488..aa3d9c5 100644 --- a/mod/etc/options_menu/language/en-US.lang +++ b/mod/etc/options_menu/language/en-US.lang @@ -14,6 +14,8 @@ BACKUP_SAVES_USB=Backup Saved Games to USB RESTORE_SAVES_USB=Restore Saved Games from USB BACKUP_SAVES_USB_DONE=Saved games backed up to USB:/data/saves_backup RESTORE_SAVES_USB_DONE=Saved games restored from USB:/data/saves_backup +BACKUP_SAVES_USB_FAILED=Saved games backup to USB failed. +RESTORE_SAVES_USB_FAILED=Saved games restore from USB failed. RESTORE_SAVES_USB_NOT_FOUND=No saved games backup found on USB. TOGGLE_USB_WRITE=Toggle Write Access on USB RA_DEBUGGER=RetroArch Debugger (USB logs) diff --git a/mod/etc/options_menu/language/es-ES.lang b/mod/etc/options_menu/language/es-ES.lang index 01ff2dc..22649ac 100644 --- a/mod/etc/options_menu/language/es-ES.lang +++ b/mod/etc/options_menu/language/es-ES.lang @@ -13,6 +13,8 @@ BACKUP_SAVES_USB=Copiar partidas guardadas a USB RESTORE_SAVES_USB=Restaurar partidas guardadas desde USB BACKUP_SAVES_USB_DONE=Partidas guardadas copiadas a USB:/data/saves_backup RESTORE_SAVES_USB_DONE=Partidas guardadas restauradas desde USB:/data/saves_backup +BACKUP_SAVES_USB_FAILED=Error al copiar las partidas guardadas a USB. +RESTORE_SAVES_USB_FAILED=Error al restaurar las partidas guardadas desde USB. RESTORE_SAVES_USB_NOT_FOUND=No se encontró ninguna copia de seguridad de partidas guardadas en la USB. TOGGLE_USB_WRITE=Alternar escritura en USB RA_DEBUGGER=Depurador de RetroArch (logs USB) diff --git a/mod/etc/options_menu/language/fr-FR.lang b/mod/etc/options_menu/language/fr-FR.lang index fc08a3a..262266e 100644 --- a/mod/etc/options_menu/language/fr-FR.lang +++ b/mod/etc/options_menu/language/fr-FR.lang @@ -13,6 +13,8 @@ BACKUP_SAVES_USB=Sauvegarder les sauvegardes de jeu sur USB RESTORE_SAVES_USB=Restaurer les sauvegardes de jeu depuis l'USB BACKUP_SAVES_USB_DONE=Sauvegardes de jeu copiées sur USB:/data/saves_backup RESTORE_SAVES_USB_DONE=Sauvegardes de jeu restaurées depuis USB:/data/saves_backup +BACKUP_SAVES_USB_FAILED=Échec de la copie des sauvegardes sur USB. +RESTORE_SAVES_USB_FAILED=Échec de la restauration des sauvegardes depuis USB. RESTORE_SAVES_USB_NOT_FOUND=Aucune sauvegarde de jeu trouvée sur l'USB. TOGGLE_USB_WRITE=Activer/désactiver l'écriture USB RA_DEBUGGER=Débogueur RetroArch (logs USB) diff --git a/mod/etc/options_menu/language/it-IT.lang b/mod/etc/options_menu/language/it-IT.lang index 5825c7c..c6e5f90 100644 --- a/mod/etc/options_menu/language/it-IT.lang +++ b/mod/etc/options_menu/language/it-IT.lang @@ -13,6 +13,8 @@ BACKUP_SAVES_USB=Backup salvataggi su USB RESTORE_SAVES_USB=Ripristina salvataggi da USB BACKUP_SAVES_USB_DONE=Salvataggi copiati su USB:/data/saves_backup RESTORE_SAVES_USB_DONE=Salvataggi ripristinati da USB:/data/saves_backup +BACKUP_SAVES_USB_FAILED=Copia dei salvataggi su USB non riuscita. +RESTORE_SAVES_USB_FAILED=Ripristino dei salvataggi da USB non riuscito. RESTORE_SAVES_USB_NOT_FOUND=Nessun backup dei salvataggi trovato sulla USB. TOGGLE_USB_WRITE=Attiva/disattiva scrittura USB RA_DEBUGGER=Debugger RetroArch (log USB) diff --git a/mod/etc/options_menu/language/ja-JP.lang b/mod/etc/options_menu/language/ja-JP.lang index f22a0b5..64a0fb0 100644 --- a/mod/etc/options_menu/language/ja-JP.lang +++ b/mod/etc/options_menu/language/ja-JP.lang @@ -13,6 +13,8 @@ BACKUP_SAVES_USB=セーブデータのBackup (USB) RESTORE_SAVES_USB=セーブデータをもどす (USB) BACKUP_SAVES_USB_DONE=セーブデータをUSB:/data/saves_backupにほぞんしました RESTORE_SAVES_USB_DONE=セーブデータをUSB:/data/saves_backupからもどしました +BACKUP_SAVES_USB_FAILED=USBへのセーブデータのほぞんにしっぱいしました +RESTORE_SAVES_USB_FAILED=USBからのセーブデータのふくげんにしっぱいしました RESTORE_SAVES_USB_NOT_FOUND=USBにセーブデータのBackupがみつかりません TOGGLE_USB_WRITE=USBへのかきこみをきりかえ RA_DEBUGGER=RAのDebugger (USB Log) diff --git a/mod/etc/options_menu/language/nl-NL.lang b/mod/etc/options_menu/language/nl-NL.lang index d190000..0de0f4d 100644 --- a/mod/etc/options_menu/language/nl-NL.lang +++ b/mod/etc/options_menu/language/nl-NL.lang @@ -13,6 +13,8 @@ BACKUP_SAVES_USB=Savegames back-uppen naar USB RESTORE_SAVES_USB=Savegames herstellen vanaf USB BACKUP_SAVES_USB_DONE=Savegames back-upt naar USB:/data/saves_backup RESTORE_SAVES_USB_DONE=Savegames hersteld vanaf USB:/data/saves_backup +BACKUP_SAVES_USB_FAILED=Back-up van savegames naar USB mislukt. +RESTORE_SAVES_USB_FAILED=Herstellen van savegames vanaf USB mislukt. RESTORE_SAVES_USB_NOT_FOUND=Geen savegame-back-up gevonden op de USB. TOGGLE_USB_WRITE=USB-schrijftoegang schakelen RA_DEBUGGER=RetroArch-debugger (USB-logs) diff --git a/mod/etc/options_menu/language/pt-PT.lang b/mod/etc/options_menu/language/pt-PT.lang index 240e4f0..765d4f4 100644 --- a/mod/etc/options_menu/language/pt-PT.lang +++ b/mod/etc/options_menu/language/pt-PT.lang @@ -13,6 +13,8 @@ BACKUP_SAVES_USB=Cópia de Segurança dos Jogos Guardados (USB) RESTORE_SAVES_USB=Restaurar Jogos Guardados (USB) BACKUP_SAVES_USB_DONE=Jogos guardados copiados para USB:/data/saves_backup RESTORE_SAVES_USB_DONE=Jogos guardados restaurados de USB:/data/saves_backup +BACKUP_SAVES_USB_FAILED=Falha ao copiar os jogos guardados para USB. +RESTORE_SAVES_USB_FAILED=Falha ao restaurar os jogos guardados de USB. RESTORE_SAVES_USB_NOT_FOUND=Não foi encontrada nenhuma cópia de segurança de jogos guardados na USB. TOGGLE_USB_WRITE=Alternar Escrita na USB RA_DEBUGGER=Depurador do RetroArch (logs USB) diff --git a/mod/etc/options_menu/language/ru-RU.lang b/mod/etc/options_menu/language/ru-RU.lang index f758b7c..0cd3dd7 100644 --- a/mod/etc/options_menu/language/ru-RU.lang +++ b/mod/etc/options_menu/language/ru-RU.lang @@ -13,6 +13,8 @@ BACKUP_SAVES_USB=Резервная копия сохранений (USB) RESTORE_SAVES_USB=Восстановить сохранения (USB) BACKUP_SAVES_USB_DONE=Сохранения скопированы на USB:/data/saves_backup RESTORE_SAVES_USB_DONE=Сохранения восстановлены с USB:/data/saves_backup +BACKUP_SAVES_USB_FAILED=Не удалось скопировать сохранения на USB. +RESTORE_SAVES_USB_FAILED=Не удалось восстановить сохранения с USB. RESTORE_SAVES_USB_NOT_FOUND=Резервная копия сохранений на USB не найдена. TOGGLE_USB_WRITE=Переключить запись на USB RA_DEBUGGER=Отладчик RA (логи на USB) diff --git a/mod/etc/options_menu/scripts/BackupSavesUsb.sh b/mod/etc/options_menu/scripts/BackupSavesUsb.sh index 3f82aa5..3265a5d 100644 --- a/mod/etc/options_menu/scripts/BackupSavesUsb.sh +++ b/mod/etc/options_menu/scripts/BackupSavesUsb.sh @@ -9,5 +9,5 @@ mkdir -pm 777 /media/data/saves_backup rsync -a --delete \ --exclude=FOLDER --exclude=hakchi --exclude=home-menu \ --exclude=dimming.txt --exclude=lang.txt --exclude=mcp-state \ - /media/hakchi/saves/ /media/data/saves_backup/ + /media/hakchi/saves/ /media/data/saves_backup/ || exit 1 translate BACKUP_SAVES_USB_DONE diff --git a/mod/etc/options_menu/scripts/RestoreSavesUsb.sh b/mod/etc/options_menu/scripts/RestoreSavesUsb.sh index 2d3e0d4..0e33e81 100644 --- a/mod/etc/options_menu/scripts/RestoreSavesUsb.sh +++ b/mod/etc/options_menu/scripts/RestoreSavesUsb.sh @@ -7,5 +7,5 @@ if [ ! -d /media/data/saves_backup ]; then exit 1 fi -rsync -a /media/data/saves_backup/ /media/hakchi/saves/ +rsync -a /media/data/saves_backup/ /media/hakchi/saves/ || exit 1 translate RESTORE_SAVES_USB_DONE diff --git a/src/command.cpp b/src/command.cpp index 34f07a7..bce0da5 100644 --- a/src/command.cpp +++ b/src/command.cpp @@ -51,14 +51,25 @@ static CommandProcess StartCommand(const std::string & command) } setpgid(pid, pid); close(fds[1]); - return { fdopen(fds[0], "r"), pid }; + FILE * out = fdopen(fds[0], "r"); + if(!out) + { + close(fds[0]); + kill(-pid, SIGKILL); + waitpid(pid, nullptr, 0); + return { nullptr, -1 }; + } + return { out, pid }; } static void FinishCommand(const CommandProcess & process, bool abort) { + if(process.pid <= 0) + return; if(abort) kill(-process.pid, SIGTERM); - fclose(process.out); + if(process.out) + fclose(process.out); waitpid(process.pid, nullptr, 0); } diff --git a/src/framework/draw_helpers.cpp b/src/framework/draw_helpers.cpp index 089065c..085b5d7 100644 --- a/src/framework/draw_helpers.cpp +++ b/src/framework/draw_helpers.cpp @@ -17,6 +17,7 @@ #include "draw_helpers.h" +#include #include #include #include @@ -81,6 +82,7 @@ void SetColorMod(SDL_Texture * texture, Color color) void DrawStrokeRect(SDL_Renderer * renderer, SDL_Rect rect, Color color, int width, int radius) { + radius = std::min(radius, std::min(rect.w, rect.h) / 2); SetDrawColor(renderer, color); const int x0 = rect.x, y0 = rect.y, x1 = rect.x + rect.w - 1, y1 = rect.y + rect.h - 1; for(int i = 0; i < width; ++i) diff --git a/src/framework/texture.cpp b/src/framework/texture.cpp index c2b60af..26ad69c 100644 --- a/src/framework/texture.cpp +++ b/src/framework/texture.cpp @@ -130,7 +130,8 @@ SDL_Texture * WriteText(const std::string & text, int fontSize, SDL_Renderer* re } } auto texture = SDL_CreateTextureFromSurface(renderer, surface); - SDL_SetTextureBlendMode(texture, SDL_BLENDMODE_BLEND); + if(texture) + SDL_SetTextureBlendMode(texture, SDL_BLENDMODE_BLEND); SDL_FreeSurface(surface); return texture; } @@ -184,7 +185,8 @@ SDL_Texture * WriteTextTTF(const std::string & text, int fontSize, SDL_Renderer* textureWidth = surface->w; textureHeight = surface->h; auto texture = SDL_CreateTextureFromSurface(renderer, surface); - SDL_SetTextureBlendMode(texture, SDL_BLENDMODE_BLEND); + if(texture) + SDL_SetTextureBlendMode(texture, SDL_BLENDMODE_BLEND); SDL_FreeSurface(surface); return texture; } diff --git a/src/gen_splash.cpp b/src/gen_splash.cpp index 48d2f28..07ec751 100644 --- a/src/gen_splash.cpp +++ b/src/gen_splash.cpp @@ -235,6 +235,8 @@ namespace int RunLoading() { const pid_t caller = getppid(); + if(caller <= 1) + return 0; const TextBlock block = RenderText({ "LOADING" }, true); UiTheme::LoadThemeConfig(OptionsRoot); const int barY = block.bottomY + BarGap; diff --git a/src/localization.cpp b/src/localization.cpp index 5be775e..f455915 100644 --- a/src/localization.cpp +++ b/src/localization.cpp @@ -63,7 +63,7 @@ void LoadLanguage(const std::string & optionsRoot, const std::string & langCode) return; while(const dirent * entry = readdir(dir.get())) { - if(entry->d_type != DT_DIR || entry->d_name[0] == '.') + if(entry->d_name[0] == '.' || (entry->d_type != DT_DIR && entry->d_type != DT_UNKNOWN)) continue; const std::string pluginLangDir = optionsRoot + entry->d_name + "/lang/"; if(FileExists(pluginLangDir + DefaultLangCode + ".lang")) diff --git a/src/main.cpp b/src/main.cpp index 1d75310..6cb2f13 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -86,17 +86,17 @@ int main(int argc, char * argv[]) for(int i = 1; i < argc; ++i) { - if(strcmp(argv[i], "--commandPath") == 0) + if(strcmp(argv[i], "--commandPath") == 0 && i + 1 < argc) { commandLocation = argv[i+1]; ++i; } - else if(strcmp(argv[i], "--scriptPath") == 0) + else if(strcmp(argv[i], "--scriptPath") == 0 && i + 1 < argc) { scriptLocation = argv[i+1]; ++i; } - else if(strcmp(argv[i], "--title") == 0) + else if(strcmp(argv[i], "--title") == 0 && i + 1 < argc) { titleKey = argv[i+1]; titleString = Translate(argv[i+1]); @@ -193,14 +193,18 @@ int main(int argc, char * argv[]) remainingStack += entries[i]; } - size_t c1 = lastEntry.find(','), c2 = lastEntry.find(',', c1+1); - std::string backPath = lastEntry.substr(0, c1); - std::string backScriptPath = lastEntry.substr(c1+1, c2-c1-1); - std::string backTitleKey = lastEntry.substr(c2+1); + const size_t c1 = lastEntry.find(','); + const size_t c2 = c1 == std::string::npos ? c1 : lastEntry.find(',', c1+1); + if(c2 != std::string::npos) + { + std::string backPath = lastEntry.substr(0, c1); + std::string backScriptPath = lastEntry.substr(c1+1, c2-c1-1); + std::string backTitleKey = lastEntry.substr(c2+1); - backCommand = "usleep 50000 && OM_BACK_STACK=\"" + remainingStack + "\" " + optionsLocation + "options --commandPath " + backPath - + (backScriptPath.empty() ? "" : " --scriptPath " + backScriptPath) - + " --title \"" + backTitleKey + "\" &"; + backCommand = "usleep 50000 && OM_BACK_STACK=\"" + remainingStack + "\" " + optionsLocation + "options --commandPath " + backPath + + (backScriptPath.empty() ? "" : " --scriptPath " + backScriptPath) + + " --title \"" + backTitleKey + "\" &"; + } } int currentCommandId = 0; @@ -254,12 +258,16 @@ int main(int argc, char * argv[]) { if(c.hasSubmenu) { - size_t pos = c.command.find("--commandPath"); - size_t pathStart = c.command.find_first_not_of(' ', pos + std::string("--commandPath").size()); - size_t pathEnd = c.command.find(' ', pathStart); - std::string targetPath = CollapseSlashes(c.command.substr(pathStart, pathEnd - pathStart)); - if(targetPath == commandLocation) - c.hasSubmenu = false; + const std::string flag = "--commandPath"; + const size_t pos = c.command.find(flag); + const size_t pathStart = pos == std::string::npos ? pos : c.command.find_first_not_of(' ', pos + flag.size()); + if(pathStart != std::string::npos) + { + const size_t pathEnd = c.command.find(' ', pathStart); + const std::string targetPath = CollapseSlashes(c.command.substr(pathStart, pathEnd == std::string::npos ? pathEnd : pathEnd - pathStart)); + if(targetPath == commandLocation) + c.hasSubmenu = false; + } } int textX = UiTheme::RowTextX + (c.child ? ChildIndent : 0); diff --git a/src/mod_uninstall.cpp b/src/mod_uninstall.cpp index eab0204..e6a4af6 100644 --- a/src/mod_uninstall.cpp +++ b/src/mod_uninstall.cpp @@ -128,17 +128,16 @@ int main(int argc, char * argv[]) auto pipe = popen("hakchi pack_list", "r"); if(pipe) { - while(!feof(pipe)) - if(fgets(buf, sizeof(buf), pipe)) + while(fgets(buf, sizeof(buf), pipe)) + { + std::string name = TrimTrailingNewline(buf); + if(!name.empty() && name.compare(0, HiddenModPrefix.size(), HiddenModPrefix) != 0) { - std::string name = TrimTrailingNewline(buf); - if(!name.empty() && name.compare(0, HiddenModPrefix.size(), HiddenModPrefix) != 0) - { - Mod mod; - mod.name = name; - mods.push_back(mod); - } + Mod mod; + mod.name = name; + mods.push_back(mod); } + } pclose(pipe); } } diff --git a/src/save_manager.cpp b/src/save_manager.cpp index 5a55d7f..8046874 100644 --- a/src/save_manager.cpp +++ b/src/save_manager.cpp @@ -24,6 +24,7 @@ #include #include #include +#include namespace { @@ -371,7 +372,7 @@ int main(int, char **) }; // Shows a "working" panel for one frame, then runs the blocking command. - auto RunBusy = [&](const std::function & drawUnder, const std::string & command) + auto RunBusy = [&](const std::function & drawUnder, const std::string & command) -> bool { const SDL_Rect box = Dialog::PanelRect(420, 110); Texture working(Translate("SAVE_MANAGER_WORKING"), Dialog::TitleFontSize, renderer, 640, box.y + box.h/2 - 12, true, ToAbgr(UiTheme::Text), true); @@ -382,10 +383,11 @@ int main(int, char **) working.Draw(renderer); SetDrawColor(renderer, bg); sdl_context.EndFrame(); - system(command.c_str()); + const int status = system(command.c_str()); controller.Update(); controller.GetButtonStatus(A); controller.GetButtonStatus(B); + return status != -1 && WIFEXITED(status) && WEXITSTATUS(status) == 0; }; // Marked-list screen shared by "clean" (orphans, all pre-marked) and "restore deleted" (none marked). @@ -710,9 +712,9 @@ int main(int, char **) case Action::Backup: if(RunDialog(DrawActions, "SAVE_MANAGER_BACKUP_CONFIRM", {}, "SAVE_MANAGER_BACKUP_DESC", true)) { - RunBusy(DrawActions, "sh " + ScriptLocation + "BackupSavesUsb.sh >/dev/null 2>&1"); + const bool ok = RunBusy(DrawActions, "sh " + ScriptLocation + "BackupSavesUsb.sh >/dev/null 2>&1"); RefreshStats(); - ShowMessage("BACKUP_SAVES_USB_DONE", ""); + ShowMessage(ok ? "BACKUP_SAVES_USB_DONE" : "BACKUP_SAVES_USB_FAILED", ""); } return -1; case Action::RestoreUsb: @@ -723,9 +725,9 @@ int main(int, char **) } if(RunDialog(DrawActions, "SAVE_MANAGER_RESTORE_CONFIRM", { Translate("SAVE_MANAGER_STAT_BACKUP") + ": " + std::to_string(stats.backupSaves) }, "SAVE_MANAGER_RESTORE_DESC", true)) { - RunBusy(DrawActions, "sh " + ScriptLocation + "RestoreSavesUsb.sh >/dev/null 2>&1"); + const bool ok = RunBusy(DrawActions, "sh " + ScriptLocation + "RestoreSavesUsb.sh >/dev/null 2>&1"); RefreshStats(); - ShowMessage("RESTORE_SAVES_USB_DONE", ""); + ShowMessage(ok ? "RESTORE_SAVES_USB_DONE" : "RESTORE_SAVES_USB_FAILED", ""); } return -1; case Action::Clean: From bee6692b648600c643078d6a853d0747e359fc1c Mon Sep 17 00:00:00 2001 From: DefKorns Date: Tue, 13 Oct 2026 13:10:40 +0000 Subject: [PATCH 079/104] Wrap long Save Manager dialog titles --- src/save_manager.cpp | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/save_manager.cpp b/src/save_manager.cpp index 8046874..731833e 100644 --- a/src/save_manager.cpp +++ b/src/save_manager.cpp @@ -328,9 +328,15 @@ int main(int, char **) body.emplace_back(TruncateToWidth(line, PanelFontSize, boxW - 80), PanelFontSize, renderer, 0, 0, false, ToAbgr(UiTheme::Text), true); std::vector noteLines = noteKey.empty() ? std::vector() : MakeLines(WrapToWidth(Translate(noteKey), NoteFontSize, boxW - 48), NoteFontSize, renderer, UiTheme::TextDim); - const int boxH = 110 + static_cast(body.size())*bodyPitch + static_cast(noteLines.size())*20 + badgeRowH; + const int titlePitch = 32; + std::vector titleLines; + for(const std::string & line : WrapToWidth(Translate(titleKey), Dialog::TitleFontSize, boxW - 48)) + titleLines.emplace_back(line, Dialog::TitleFontSize, renderer, 640, 0, true, ToAbgr(UiTheme::Text), true); + const int titleExtraH = std::max(0, static_cast(titleLines.size()) - 1) * titlePitch; + const int boxH = 110 + titleExtraH + static_cast(body.size())*bodyPitch + static_cast(noteLines.size())*20 + badgeRowH; const SDL_Rect box = Dialog::PanelRect(boxW, boxH); - Texture title(Translate(titleKey), Dialog::TitleFontSize, renderer, 640, box.y + 36, true, ToAbgr(UiTheme::Text), true); + for(size_t i = 0; i < titleLines.size(); ++i) + titleLines[i].rect.y = box.y + 36 + static_cast(i)*titlePitch - titleLines[i].rect.h/2; controller.GetButtonStatus(A); controller.GetButtonStatus(B); @@ -352,8 +358,9 @@ int main(int, char **) drawUnder(); Dialog::DrawBackdrop(renderer); Dialog::DrawPanel(renderer, box); - title.Draw(renderer); - int y = box.y + 72; + for(Texture & line : titleLines) + line.Draw(renderer); + int y = box.y + 72 + titleExtraH; for(Texture & line : body) { line.rect.x = 640 - line.rect.w/2; From 9e4d30043b5beff6e395148296b4a2378a348e0b Mon Sep 17 00:00:00 2001 From: DefKorns Date: Tue, 13 Oct 2026 13:18:41 +0000 Subject: [PATCH 080/104] Update hmod-build: changelog in pre-release readmes --- hmod-build | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hmod-build b/hmod-build index 9f553b5..14408f7 160000 --- a/hmod-build +++ b/hmod-build @@ -1 +1 @@ -Subproject commit 9f553b5718ca671cf7bf59f9c714c3e07042ee4d +Subproject commit 14408f7b6e4e3aa1488e0d50152ffc9fe1e4e966 From 12e4d54a0cf6f1c1cf0fe2bacf236a5d51ee982f Mon Sep 17 00:00:00 2001 From: DefKorns Date: Tue, 13 Oct 2026 14:28:19 +0000 Subject: [PATCH 081/104] Show Hakchi CE game names in the Save Manager removed-games lists --- README.md | 17 +++ mod/etc/options_menu/language/de-DE.lang | 10 ++ mod/etc/options_menu/language/en-US.lang | 10 ++ mod/etc/options_menu/language/es-ES.lang | 10 ++ mod/etc/options_menu/language/fr-FR.lang | 10 ++ mod/etc/options_menu/language/it-IT.lang | 10 ++ mod/etc/options_menu/language/ja-JP.lang | 10 ++ mod/etc/options_menu/language/nl-NL.lang | 10 ++ mod/etc/options_menu/language/pt-PT.lang | 10 ++ mod/etc/options_menu/language/ru-RU.lang | 10 ++ mod/etc/options_menu/scripts/OrphanSaves.sh | 73 ++++++++++- pc/push_game_names.ps1 | 137 ++++++++++++++++++++ src/save_manager.cpp | 127 +++++++++++++++--- 13 files changed, 428 insertions(+), 16 deletions(-) create mode 100644 pc/push_game_names.ps1 diff --git a/README.md b/README.md index 70aa572..e4fe368 100644 --- a/README.md +++ b/README.md @@ -103,6 +103,23 @@ Needs a USB/SD drive with your saves on it. One screen lists the actions on the | Clean Saves of Removed Games › | Lists the save folders of games that are no longer in any menu (all marked; **A** unmarks) and moves the marked ones to `/media/data/deleted_games_saves`. Menu folders (`CLV-S-*`) are never touched | | Restore Saves of Removed Games › | Lists what was moved aside; mark the ones you want back with **A** and press **Start**. A save already on the console is never overwritten | +#### Game names from Hakchi CE + +The two "Removed Games" lists show game codes (`CLV-H-ABCDE`) because the games are no longer on the console. They show the game names instead once the names have been copied from your Hakchi CE library to `/media/data/hakchi_game_names`. Saves without a match keep showing their code. Getting the names never changes or moves a save. + +The console can't read your PC, so the PC sends the names. With the console on and reachable by Hakchi CE (USB or Wi-Fi), run [`pc/push_game_names.ps1`](pc/push_game_names.ps1) from a PowerShell window: + +```powershell +.\push_game_names.ps1 --ip 192.168.1.50 +``` + +- `--ip`: the console's IP address, shown in Hakchi CE or in Options Menu › Network › Display IP Address. Defaults to `169.254.13.37`, the console connected over USB. +- `--hakchi-path`: the Hakchi CE folder that has the `games` folders. Usually not needed: the script looks in its own folder first, so copying it next to `hakchi.exe` (portable Hakchi CE) is enough. It also finds the library of an open Hakchi CE, and `Documents\hakchi2` for non-portable installs. + +If Windows refuses to run scripts, use `powershell -ExecutionPolicy Bypass -File push_game_names.ps1 --ip 192.168.1.50`. + +Run it again after adding or removing games in Hakchi CE. If a portable Hakchi CE folder is on the USB drive itself, press **Select** (or **Y**) in either list to read the names from there instead. + ### Advanced Options | Entry | What it does | diff --git a/mod/etc/options_menu/language/de-DE.lang b/mod/etc/options_menu/language/de-DE.lang index 0d7dcd6..c4ad47e 100644 --- a/mod/etc/options_menu/language/de-DE.lang +++ b/mod/etc/options_menu/language/de-DE.lang @@ -121,3 +121,13 @@ HINT_OK=OK HINT_MOVE=Verschieben HINT_RESTORE=Wiederherstellen SAVE_MANAGER_SELECT_ALL=Alle auswählen +HINT_GAME_NAMES=Spielnamen +SAVE_MANAGER_HAKCHI_SEARCHING=Suche nach Hakchi... +SAVE_MANAGER_HAKCHI_SCANNING=Spiele werden gelesen... +SAVE_MANAGER_HAKCHI_NOT_FOUND=Hakchi wurde auf dem USB-Laufwerk nicht gefunden. +SAVE_MANAGER_HAKCHI_NOT_FOUND_NOTE=Kopiere einen portablen Hakchi-CE-Ordner auf das USB-Laufwerk und versuche es erneut. +SAVE_MANAGER_HAKCHI_FAILED=Die Hakchi-Spiele konnten nicht gelesen werden. +SAVE_MANAGER_HAKCHI_DONE=Spielnamen aktualisiert +SAVE_MANAGER_HAKCHI_GAMES=Spiele in Hakchi +SAVE_MANAGER_HAKCHI_NAMED=Spielstände mit Namen +SAVE_MANAGER_HAKCHI_PARTIAL_NOTE=Spielstände ohne Treffer werden mit ihrem Spielcode angezeigt. diff --git a/mod/etc/options_menu/language/en-US.lang b/mod/etc/options_menu/language/en-US.lang index aa3d9c5..f212b6b 100644 --- a/mod/etc/options_menu/language/en-US.lang +++ b/mod/etc/options_menu/language/en-US.lang @@ -122,3 +122,13 @@ HINT_OK=OK HINT_MOVE=Move HINT_RESTORE=Restore SAVE_MANAGER_SELECT_ALL=Select all +HINT_GAME_NAMES=Game Names +SAVE_MANAGER_HAKCHI_SEARCHING=Searching for Hakchi... +SAVE_MANAGER_HAKCHI_SCANNING=Scanning games... +SAVE_MANAGER_HAKCHI_NOT_FOUND=Hakchi was not found on the USB drive. +SAVE_MANAGER_HAKCHI_NOT_FOUND_NOTE=Copy a portable Hakchi CE folder to the USB drive and try again. +SAVE_MANAGER_HAKCHI_FAILED=Could not read the Hakchi games. +SAVE_MANAGER_HAKCHI_DONE=Game names updated +SAVE_MANAGER_HAKCHI_GAMES=Games in Hakchi +SAVE_MANAGER_HAKCHI_NAMED=Saves with a name +SAVE_MANAGER_HAKCHI_PARTIAL_NOTE=Saves without a match are shown by their game code. diff --git a/mod/etc/options_menu/language/es-ES.lang b/mod/etc/options_menu/language/es-ES.lang index 22649ac..96f0bff 100644 --- a/mod/etc/options_menu/language/es-ES.lang +++ b/mod/etc/options_menu/language/es-ES.lang @@ -121,3 +121,13 @@ HINT_OK=OK HINT_MOVE=Mover HINT_RESTORE=Restaurar SAVE_MANAGER_SELECT_ALL=Seleccionar todo +HINT_GAME_NAMES=Nombres de juegos +SAVE_MANAGER_HAKCHI_SEARCHING=Buscando Hakchi... +SAVE_MANAGER_HAKCHI_SCANNING=Analizando juegos... +SAVE_MANAGER_HAKCHI_NOT_FOUND=No se encontró Hakchi en la USB. +SAVE_MANAGER_HAKCHI_NOT_FOUND_NOTE=Copia una carpeta portátil de Hakchi CE a la USB e inténtalo de nuevo. +SAVE_MANAGER_HAKCHI_FAILED=No se pudieron leer los juegos de Hakchi. +SAVE_MANAGER_HAKCHI_DONE=Nombres de juegos actualizados +SAVE_MANAGER_HAKCHI_GAMES=Juegos en Hakchi +SAVE_MANAGER_HAKCHI_NAMED=Partidas con nombre +SAVE_MANAGER_HAKCHI_PARTIAL_NOTE=Las partidas sin coincidencia se muestran con el código del juego. diff --git a/mod/etc/options_menu/language/fr-FR.lang b/mod/etc/options_menu/language/fr-FR.lang index 262266e..cfdc6ce 100644 --- a/mod/etc/options_menu/language/fr-FR.lang +++ b/mod/etc/options_menu/language/fr-FR.lang @@ -121,3 +121,13 @@ HINT_OK=OK HINT_MOVE=Déplacer HINT_RESTORE=Restaurer SAVE_MANAGER_SELECT_ALL=Tout sélectionner +HINT_GAME_NAMES=Noms des jeux +SAVE_MANAGER_HAKCHI_SEARCHING=Recherche de Hakchi... +SAVE_MANAGER_HAKCHI_SCANNING=Analyse des jeux... +SAVE_MANAGER_HAKCHI_NOT_FOUND=Hakchi est introuvable sur l'USB. +SAVE_MANAGER_HAKCHI_NOT_FOUND_NOTE=Copiez un dossier portable de Hakchi CE sur l'USB et réessayez. +SAVE_MANAGER_HAKCHI_FAILED=Impossible de lire les jeux de Hakchi. +SAVE_MANAGER_HAKCHI_DONE=Noms des jeux mis à jour +SAVE_MANAGER_HAKCHI_GAMES=Jeux dans Hakchi +SAVE_MANAGER_HAKCHI_NAMED=Sauvegardes nommées +SAVE_MANAGER_HAKCHI_PARTIAL_NOTE=Les sauvegardes sans correspondance gardent le code du jeu. diff --git a/mod/etc/options_menu/language/it-IT.lang b/mod/etc/options_menu/language/it-IT.lang index c6e5f90..886511d 100644 --- a/mod/etc/options_menu/language/it-IT.lang +++ b/mod/etc/options_menu/language/it-IT.lang @@ -121,3 +121,13 @@ HINT_OK=OK HINT_MOVE=Sposta HINT_RESTORE=Ripristina SAVE_MANAGER_SELECT_ALL=Seleziona tutto +HINT_GAME_NAMES=Nomi dei giochi +SAVE_MANAGER_HAKCHI_SEARCHING=Ricerca di Hakchi... +SAVE_MANAGER_HAKCHI_SCANNING=Analisi dei giochi... +SAVE_MANAGER_HAKCHI_NOT_FOUND=Hakchi non trovato sulla USB. +SAVE_MANAGER_HAKCHI_NOT_FOUND_NOTE=Copia una cartella portatile di Hakchi CE sulla USB e riprova. +SAVE_MANAGER_HAKCHI_FAILED=Impossibile leggere i giochi di Hakchi. +SAVE_MANAGER_HAKCHI_DONE=Nomi dei giochi aggiornati +SAVE_MANAGER_HAKCHI_GAMES=Giochi in Hakchi +SAVE_MANAGER_HAKCHI_NAMED=Salvataggi con nome +SAVE_MANAGER_HAKCHI_PARTIAL_NOTE=I salvataggi senza corrispondenza mostrano il codice del gioco. diff --git a/mod/etc/options_menu/language/ja-JP.lang b/mod/etc/options_menu/language/ja-JP.lang index 64a0fb0..5a5c927 100644 --- a/mod/etc/options_menu/language/ja-JP.lang +++ b/mod/etc/options_menu/language/ja-JP.lang @@ -121,3 +121,13 @@ HINT_OK=OK HINT_MOVE=移動 HINT_RESTORE=復元 SAVE_MANAGER_SELECT_ALL=すべて選択 +HINT_GAME_NAMES=ゲーム名 +SAVE_MANAGER_HAKCHI_SEARCHING=Hakchiを検索中... +SAVE_MANAGER_HAKCHI_SCANNING=ゲームを確認中... +SAVE_MANAGER_HAKCHI_NOT_FOUND=USBにHakchiがみつかりません +SAVE_MANAGER_HAKCHI_NOT_FOUND_NOTE=ポータブル版Hakchi CEのフォルダをUSBにコピーしてください。 +SAVE_MANAGER_HAKCHI_FAILED=Hakchiのゲームを読み込めませんでした +SAVE_MANAGER_HAKCHI_DONE=ゲーム名を更新しました +SAVE_MANAGER_HAKCHI_GAMES=Hakchiのゲーム数 +SAVE_MANAGER_HAKCHI_NAMED=名前がわかったセーブ +SAVE_MANAGER_HAKCHI_PARTIAL_NOTE=一致しないセーブはゲームコードで表示されます。 diff --git a/mod/etc/options_menu/language/nl-NL.lang b/mod/etc/options_menu/language/nl-NL.lang index 0de0f4d..bcb537b 100644 --- a/mod/etc/options_menu/language/nl-NL.lang +++ b/mod/etc/options_menu/language/nl-NL.lang @@ -121,3 +121,13 @@ HINT_OK=OK HINT_MOVE=Verplaatsen HINT_RESTORE=Herstellen SAVE_MANAGER_SELECT_ALL=Alles selecteren +HINT_GAME_NAMES=Spelnamen +SAVE_MANAGER_HAKCHI_SEARCHING=Hakchi zoeken... +SAVE_MANAGER_HAKCHI_SCANNING=Spellen scannen... +SAVE_MANAGER_HAKCHI_NOT_FOUND=Hakchi niet gevonden op de USB. +SAVE_MANAGER_HAKCHI_NOT_FOUND_NOTE=Kopieer een draagbare Hakchi CE-map naar de USB en probeer het opnieuw. +SAVE_MANAGER_HAKCHI_FAILED=Kan de Hakchi-spellen niet lezen. +SAVE_MANAGER_HAKCHI_DONE=Spelnamen bijgewerkt +SAVE_MANAGER_HAKCHI_GAMES=Spellen in Hakchi +SAVE_MANAGER_HAKCHI_NAMED=Opslag met naam +SAVE_MANAGER_HAKCHI_PARTIAL_NOTE=Opslag zonder overeenkomst wordt getoond met de spelcode. diff --git a/mod/etc/options_menu/language/pt-PT.lang b/mod/etc/options_menu/language/pt-PT.lang index 765d4f4..3b42328 100644 --- a/mod/etc/options_menu/language/pt-PT.lang +++ b/mod/etc/options_menu/language/pt-PT.lang @@ -121,3 +121,13 @@ HINT_OK=OK HINT_MOVE=Mover HINT_RESTORE=Restaurar SAVE_MANAGER_SELECT_ALL=Selecionar tudo +HINT_GAME_NAMES=Nomes dos Jogos +SAVE_MANAGER_HAKCHI_SEARCHING=A procurar o Hakchi... +SAVE_MANAGER_HAKCHI_SCANNING=A analisar os jogos... +SAVE_MANAGER_HAKCHI_NOT_FOUND=O Hakchi não foi encontrado na USB. +SAVE_MANAGER_HAKCHI_NOT_FOUND_NOTE=Copie uma pasta portátil do Hakchi CE para a USB e tente novamente. +SAVE_MANAGER_HAKCHI_FAILED=Não foi possível ler os jogos do Hakchi. +SAVE_MANAGER_HAKCHI_DONE=Nomes dos jogos atualizados +SAVE_MANAGER_HAKCHI_GAMES=Jogos no Hakchi +SAVE_MANAGER_HAKCHI_NAMED=Jogos guardados com nome +SAVE_MANAGER_HAKCHI_PARTIAL_NOTE=Os jogos guardados sem correspondência são mostrados pelo código do jogo. diff --git a/mod/etc/options_menu/language/ru-RU.lang b/mod/etc/options_menu/language/ru-RU.lang index 0cd3dd7..ef06f85 100644 --- a/mod/etc/options_menu/language/ru-RU.lang +++ b/mod/etc/options_menu/language/ru-RU.lang @@ -121,3 +121,13 @@ HINT_OK=OK HINT_MOVE=Переместить HINT_RESTORE=Восстановить SAVE_MANAGER_SELECT_ALL=Выбрать все +HINT_GAME_NAMES=Названия игр +SAVE_MANAGER_HAKCHI_SEARCHING=Поиск Hakchi... +SAVE_MANAGER_HAKCHI_SCANNING=Сканирование игр... +SAVE_MANAGER_HAKCHI_NOT_FOUND=Hakchi не найден на USB. +SAVE_MANAGER_HAKCHI_NOT_FOUND_NOTE=Скопируйте портативную папку Hakchi CE на USB и повторите попытку. +SAVE_MANAGER_HAKCHI_FAILED=Не удалось прочитать игры Hakchi. +SAVE_MANAGER_HAKCHI_DONE=Названия игр обновлены +SAVE_MANAGER_HAKCHI_GAMES=Игр в Hakchi +SAVE_MANAGER_HAKCHI_NAMED=Сохранений с названием +SAVE_MANAGER_HAKCHI_PARTIAL_NOTE=Сохранения без совпадения показаны по коду игры. diff --git a/mod/etc/options_menu/scripts/OrphanSaves.sh b/mod/etc/options_menu/scripts/OrphanSaves.sh index f62f265..07877f8 100644 --- a/mod/etc/options_menu/scripts/OrphanSaves.sh +++ b/mod/etc/options_menu/scripts/OrphanSaves.sh @@ -22,6 +22,10 @@ # OrphanSaves.sh deleted entries already moved aside # OrphanSaves.sh move FILE move the entries listed in FILE aside # OrphanSaves.sh restore FILE move them back (never over an existing save) +# OrphanSaves.sh hakchi portable Hakchi CE folders found on the USB drive +# OrphanSaves.sh names FILE rebuild the entryname cache from the Hakchi folders listed in FILE +# OrphanSaves.sh cached-names print the name cache +# Name lookups only read Hakchi's .desktop files, saves are never touched. source /etc/preinit script_init @@ -29,6 +33,7 @@ script_init saves_root=/media/hakchi/saves backup_root=/media/data/saves_backup deleted_root=/media/data/deleted_games_saves +names_cache=/media/data/hakchi_game_names ra_save_suffix='\.(srm|rtc|state([0-9]+)?(\.auto)?)(\.png)?$' raContent() { @@ -105,6 +110,63 @@ moveListed() { sync } +# game folders are pruned so the walk stays shallow on big libraries +hakchiRoots() { + find /media -maxdepth 4 -type d \( -name 'games*' -o -name saves -o -name '.*' \) -prune -o -type f -name '[Hh][Aa][Kk][Cc][Hh][Ii].[Ee][Xx][Ee]' -print 2>/dev/null | sed 's#/[^/]*$##' | sort -u +} + +# entryname for every game in games, games_snes, games_md, games_originals, games_cache. +# RetroArch content names come from the files in each game folder (exact, spaces included) +# and from Exec= paths (ROMs kept elsewhere). +hakchiNames() { + local root dir + while IFS= read -r root; do + for dir in "$root"/games*; do + [ -d "$dir" ] || continue + find "$dir" -mindepth 2 -maxdepth 2 -type f -name '*.desktop' -exec awk ' + function flush( n, i, token) { + if(code ~ /^CLV-/ && name != "") { + print "N\t" code "\t" name + n = split(exec, token, " ") + for(i = 1; i <= n; i++) + if(token[i] ~ /\//) { sub(/.*\//, "", token[i]); print "R\t" code "\t" token[i] } + } + code = name = exec = "" + } + FNR == 1 { flush() } + { sub(/\r$/, ""); gsub(/\t/, " ") } + /^Code=/ { code = substr($0, 6) } + /^Name=/ && name == "" { name = substr($0, 6) } + /^Exec=/ { exec = substr($0, 6) } + END { flush() }' {} + 2>/dev/null + find "$dir" -mindepth 2 -maxdepth 2 -type f ! -name '*.desktop' ! -name '*.png' 2>/dev/null \ + | awk -F / '{ print "R\t" $(NF-1) "\t" $NF }' + done + done < "$1" | awk -F '\t' ' + NF != 3 || $3 == "" { next } + $1 == "N" { if(!($2 in name)) { name[$2] = $3; print $2 "\t" $3 } next } + { rom[++n] = $2 "\t" $3 } + END { + for(i = 1; i <= n; i++) { + split(rom[i], r, "\t") + if(!(r[1] in name)) continue + # game.sfc.gz is saved as game.srm + print "retroarch/" r[2] "\t" name[r[1]] + if(sub(/\.[^.]*$/, "", r[2])) print "retroarch/" r[2] "\t" name[r[1]] + if(sub(/\.[^.]*$/, "", r[2])) print "retroarch/" r[2] "\t" name[r[1]] + } + }' | awk -F '\t' '!seen[$1]++' +} + +refreshNames() { + local tmp="$names_cache.tmp" + mkdir -pm 777 "$(dirname "$names_cache")" || return 1 + hakchiNames "$1" > "$tmp" || { rm -f "$tmp"; return 1; } + mv -f "$tmp" "$names_cache" || return 1 + sync + grep -c "^CLV-" "$names_cache" || true +} + case "$1" in stats) saveEntries "$saves_root" | sort -u | wc -l @@ -124,8 +186,17 @@ case "$1" in restore) [ -f "$2" ] && moveListed "$deleted_root" "$saves_root" "$2" ;; + hakchi) + hakchiRoots + ;; + names) + [ -s "$2" ] && refreshNames "$2" + ;; + cached-names) + cat "$names_cache" 2>/dev/null + ;; *) - echo "usage: $0 stats|orphans|deleted|move FILE|restore FILE" >&2 + echo "usage: $0 stats|orphans|deleted|move FILE|restore FILE|hakchi|names FILE|cached-names" >&2 exit 2 ;; esac diff --git a/pc/push_game_names.ps1 b/pc/push_game_names.ps1 new file mode 100644 index 0000000..2f778f6 --- /dev/null +++ b/pc/push_game_names.ps1 @@ -0,0 +1,137 @@ +<# + Copyright (C) 2026 DefKorns + + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License + as published by the Free Software Foundation; either version 3 + of the License, or (at your option) any later version. + + Sends the game names of a Hakchi CE library to the console, so the Save Manager + shows names instead of codes for saves of removed games. Read-only on both sides + except for USB:/data/hakchi_game_names; saves are never touched. + + push_game_names.ps1 [--ip CONSOLE_IP] [--hakchi-path DIR] + --ip defaults to 169.254.13.37, the console over USB. +#> +[CmdletBinding(PositionalBinding = $false)] +param( + [string]$Ip = '169.254.13.37', + [string]$HakchiPath, + # in-session calls pass --ip as a plain value, not as a parameter name + [Parameter(ValueFromRemainingArguments = $true)] + [string[]]$Rest +) + +$ErrorActionPreference = 'Stop' +$CachePath = '/media/data/hakchi_game_names' + +function Stop-WithError([string]$message) { + Write-Host $message -ForegroundColor Red + exit 1 +} + +for ($i = 0; $i -lt $Rest.Count; $i++) { + $option = $Rest[$i] + if ($i + 1 -ge $Rest.Count) { Stop-WithError "Missing value for $option. Usage: push_game_names.ps1 [--ip CONSOLE_IP] [--hakchi-path DIR]" } + switch ($option) { + '--ip' { $Ip = $Rest[++$i] } + '--hakchi-path' { $HakchiPath = $Rest[++$i] } + default { Stop-WithError "Unknown option $option. Usage: push_game_names.ps1 [--ip CONSOLE_IP] [--hakchi-path DIR]" } + } +} + +function Test-Library([string]$dir) { + $dir -and (Test-Path -LiteralPath $dir) -and @(Get-ChildItem -LiteralPath $dir -Directory -Filter 'games*').Count -gt 0 +} + +# portable mode keeps games next to hakchi.exe, non-portable in Documents\hakchi2 +function Find-Library { + $candidates = @($PSScriptRoot) + foreach ($process in @(Get-Process -Name hakchi -ErrorAction SilentlyContinue)) { + if ($process.Path) { $candidates += Split-Path -Parent $process.Path } + } + $candidates += Join-Path ([Environment]::GetFolderPath('MyDocuments')) 'hakchi2' + foreach ($dir in $candidates) { + if (Test-Library $dir) { return $dir } + } + return $null +} + +if (-not $HakchiPath) { $HakchiPath = Find-Library } +if (-not (Test-Library $HakchiPath)) { + Stop-WithError 'Hakchi CE games not found. Put this script next to hakchi.exe, open Hakchi CE first, or pass --hakchi-path with the folder that has the games folders.' +} + +# same rules as OrphanSaves.sh hakchiNames: code -> Name=, RetroArch content -> name of its game +$utf8 = New-Object System.Text.UTF8Encoding($false) +$gameNames = New-Object 'System.Collections.Generic.Dictionary[string,string]' +$seen = New-Object 'System.Collections.Generic.HashSet[string]' +$lines = New-Object 'System.Collections.Generic.List[string]' +$roms = New-Object 'System.Collections.Generic.List[string[]]' +$skipped = 0 + +function Add-Line([string]$key, [string]$name) { + if ($script:seen.Add($key)) { $script:lines.Add("$key`t$name") } +} + +foreach ($gamesDir in Get-ChildItem -LiteralPath $HakchiPath -Directory -Filter 'games*' | Sort-Object Name) { + foreach ($gameDir in Get-ChildItem -LiteralPath $gamesDir.FullName -Directory) { + foreach ($file in Get-ChildItem -LiteralPath $gameDir.FullName -File) { + if ($file.Extension -cne '.desktop') { + if ($file.Extension -cne '.png') { $roms.Add(@($gameDir.Name, $file.Name)) } + continue + } + try { $text = [IO.File]::ReadAllLines($file.FullName, $utf8) } + catch { $skipped++; continue } + $code = ''; $name = ''; $exec = '' + foreach ($raw in $text) { + $line = $raw.Replace("`t", ' ') + if ($line.StartsWith('Code=')) { $code = $line.Substring(5) } + elseif ($line.StartsWith('Name=') -and $name -eq '') { $name = $line.Substring(5) } + elseif ($line.StartsWith('Exec=')) { $exec = $line.Substring(5) } + } + if (-not $code.StartsWith('CLV-') -or $name -eq '') { $skipped++; continue } + if (-not $gameNames.ContainsKey($code)) { + $gameNames[$code] = $name + Add-Line $code $name + } + foreach ($token in $exec.Split(' ')) { + if ($token.Contains('/')) { $roms.Add(@($code, $token.Substring($token.LastIndexOf('/') + 1))) } + } + } + } +} + +foreach ($rom in $roms) { + $name = $null + if ($rom[1] -eq '' -or -not $gameNames.TryGetValue($rom[0], [ref]$name)) { continue } + # game.sfc.gz is saved as game.srm + $content = $rom[1] + Add-Line "retroarch/$content" $name + for ($i = 0; $i -lt 2 -and $content -match '\.[^.]*$'; $i++) { + $content = $content -replace '\.[^.]*$', '' + Add-Line "retroarch/$content" $name + } +} + +if ($gameNames.Count -eq 0) { + Stop-WithError "No games with a name found in $HakchiPath." +} + +$tmp = [IO.Path]::GetTempFileName() +try { + [IO.File]::WriteAllText($tmp, ($lines -join "`n") + "`n", $utf8) + $remote = "`"mkdir -p /media/data && cat > $CachePath.tmp && mv -f $CachePath.tmp $CachePath && sync`"" + Write-Host "Sending $($gameNames.Count) game names from $HakchiPath to $Ip..." + $ssh = Start-Process -FilePath ssh -NoNewWindow -Wait -PassThru -RedirectStandardInput $tmp ` + -ArgumentList @('-o', 'ConnectTimeout=10', '-o', 'StrictHostKeyChecking=accept-new', "root@$Ip", $remote) + if ($ssh.ExitCode -ne 0) { + Stop-WithError "Could not reach the console at $Ip (ssh exit code $($ssh.ExitCode)). Check that it is on and pass its IP with --ip." + } +} +finally { + Remove-Item -LiteralPath $tmp -ErrorAction SilentlyContinue +} + +Write-Host "Done. Open Save Manager > Clean Saves of Removed Games to see the names." +if ($skipped -gt 0) { Write-Host "$skipped game files without a code or name were skipped." } diff --git a/src/save_manager.cpp b/src/save_manager.cpp index 731833e..de5987a 100644 --- a/src/save_manager.cpp +++ b/src/save_manager.cpp @@ -22,6 +22,7 @@ #include #include #include +#include #include #include #include @@ -31,9 +32,10 @@ namespace const std::string OptionsLocation = "/etc/options_menu/"; const std::string ScriptLocation = OptionsLocation + "scripts/"; const std::string SelectionFile = "/tmp/save_manager_selection"; + const std::string HakchiRootsFile = "/tmp/save_manager_hakchi"; const std::string RetroArchPrefix = "retroarch/"; - // narrower list than the shared theme: entries are short codes, the detail panels need the room + // narrower list than the shared theme: the detail panels need the room, long names are truncated const int ListW = 600; const int ListRightX = UiTheme::ListX + ListW - 40; const int ControlRightX = ListRightX - 16; @@ -66,9 +68,13 @@ namespace { Action::RestoreDeleted, "SAVE_MANAGER_RESTORE_DELETED", "SAVE_MANAGER_RESTORE_DELETED_DESC" }, }; + using GameNames = std::map; + struct SaveEntry { std::string code; + std::string label; + bool named = false; Texture text; bool marked = false; }; @@ -86,7 +92,7 @@ namespace FILE * pipe = popen(command.c_str(), "r"); if(!pipe) return lines; - char buf[256]; + char buf[1024]; while(fgets(buf, sizeof(buf), pipe)) { std::string line = TrimTrailingNewline(buf); @@ -162,11 +168,27 @@ namespace return textures; } - std::string EntryLabel(const std::string & entry) + // entryname lines cached from the last Hakchi scan; malformed lines are skipped + GameNames LoadGameNames() + { + GameNames names; + for(const std::string & line : ReadLines(OrphanSavesCommand("cached-names"))) + { + const size_t tab = line.find('\t'); + if(tab == std::string::npos || tab == 0 || tab + 1 == line.size()) + continue; + names.emplace(line.substr(0, tab), line.substr(tab + 1)); + } + return names; + } + + // the entry stays the identifier, a known name only changes what is shown + std::string EntryLabel(const std::string & entry, const GameNames & names) { - if(entry.compare(0, RetroArchPrefix.size(), RetroArchPrefix) == 0) - return "RetroArch: " + entry.substr(RetroArchPrefix.size()); - return entry; + const auto found = names.find(entry); + const bool retroArch = entry.compare(0, RetroArchPrefix.size(), RetroArchPrefix) == 0; + const std::string shown = found != names.end() ? found->second : retroArch ? entry.substr(RetroArchPrefix.size()) : entry; + return retroArch ? "RetroArch: " + shown : shown; } void DrawDot(SDL_Renderer * renderer, int x, int centerY) @@ -350,7 +372,12 @@ int main(int, char **) if(sdl_context.powerwatch->buttonPress() || controller.GetButtonStatus(B)) return false; if(confirm && controller.PeekButtonStatus(START) && controller.PeekButtonStatus(SELECT)) + { + // still held on return; Select alone is the Hakchi names shortcut + controller.GetButtonStatus(START); + controller.GetButtonStatus(SELECT); return true; + } if(!confirm && controller.GetButtonStatus(A)) return true; @@ -378,11 +405,12 @@ int main(int, char **) } }; - // Shows a "working" panel for one frame, then runs the blocking command. - auto RunBusy = [&](const std::function & drawUnder, const std::string & command) -> bool + // Shows a progress panel for one frame; the caller then runs the blocking work. + auto ShowBusy = [&](const std::function & drawUnder, const std::string & messageKey) { - const SDL_Rect box = Dialog::PanelRect(420, 110); - Texture working(Translate("SAVE_MANAGER_WORKING"), Dialog::TitleFontSize, renderer, 640, box.y + box.h/2 - 12, true, ToAbgr(UiTheme::Text), true); + const std::string message = Translate(messageKey); + const SDL_Rect box = Dialog::PanelRect(std::max(420, TextWidth(message, Dialog::TitleFontSize) + 96), 110); + Texture working(message, Dialog::TitleFontSize, renderer, 640, box.y + box.h/2 - 12, true, ToAbgr(UiTheme::Text), true); sdl_context.StartFrame(); drawUnder(); Dialog::DrawBackdrop(renderer); @@ -390,10 +418,20 @@ int main(int, char **) working.Draw(renderer); SetDrawColor(renderer, bg); sdl_context.EndFrame(); - const int status = system(command.c_str()); + }; + + auto DropPendingInput = [&]() + { controller.Update(); controller.GetButtonStatus(A); controller.GetButtonStatus(B); + }; + + auto RunBusy = [&](const std::function & drawUnder, const std::string & command) -> bool + { + ShowBusy(drawUnder, "SAVE_MANAGER_WORKING"); + const int status = system(command.c_str()); + DropPendingInput(); return status != -1 && WIFEXITED(status) && WEXITSTATUS(status) == 0; }; @@ -406,6 +444,8 @@ int main(int, char **) std::vector noneHintLines = MakeLines(WrapToWidth(Translate("SAVE_MANAGER_NONE_HINT"), PanelFontSize, PanelInnerW), PanelFontSize, renderer, UiTheme::TextDim); std::vector noteLines = NoteLines(cleaning ? "SAVE_MANAGER_CLEAN_NOTE" : "SAVE_MANAGER_RESTORE_NOTE"); Badge actionPill = badges.MakePill("Start", ""); + Badge namesPill = badges.MakePill("Select", Translate("HINT_GAME_NAMES")); + GameNames gameNames = LoadGameNames(); std::vector entries; int listOffset = 0; @@ -439,12 +479,30 @@ int main(int, char **) int shown = (count > SummaryMaxRows) ? SummaryMaxRows - 1 : count; summaryNames.clear(); for(int i = 0; i < shown; ++i) - summaryNames.emplace_back(TruncateToWidth(EntryLabel(marked[i]->code), PanelFontSize, PanelInnerW - BulletTextGap), PanelFontSize, renderer, PanelInnerX + BulletTextGap, 0, false, ToAbgr(UiTheme::Text), true); + summaryNames.emplace_back(TruncateToWidth(marked[i]->label, PanelFontSize, PanelInnerW - BulletTextGap), PanelFontSize, renderer, PanelInnerX + BulletTextGap, 0, false, ToAbgr(UiTheme::Text), true); moreText = (shown < count) ? Texture("+" + std::to_string(count - shown), PanelFontSize, renderer, PanelInnerX + BulletTextGap, 0, false, ToAbgr(UiTheme::TextDim), true) : Texture(); }; + // named entries first, alphabetically; the rest keep their code order + auto ApplyNames = [&]() + { + for(SaveEntry & entry : entries) + { + entry.named = gameNames.count(entry.code) != 0; + entry.label = EntryLabel(entry.code, gameNames); + entry.text = Texture(TruncateToWidth(entry.label, ListFontSize, RowTextMaxW), ListFontSize, renderer, UiTheme::RowTextX, 0, false, ToAbgr(White), true); + } + std::stable_sort(entries.begin(), entries.end(), [](const SaveEntry & a, const SaveEntry & b) + { + if(a.named != b.named) + return a.named; + return a.named && a.label < b.label; + }); + RefreshSummary(); + }; + auto Load = [&]() { entries.clear(); @@ -452,13 +510,12 @@ int main(int, char **) { SaveEntry entry; entry.code = code; - entry.text = Texture(TruncateToWidth(EntryLabel(code), ListFontSize, RowTextMaxW), ListFontSize, renderer, UiTheme::RowTextX, 0, false, ToAbgr(White), true); entry.marked = cleaning; entries.push_back(entry); } listOffset = 0; currentId = 0; - RefreshSummary(); + ApplyNames(); }; auto DrawScreen = [&]() @@ -529,9 +586,47 @@ int main(int, char **) rightEdge = DrawBadge(badgeBack, rightEdge); if(markedCount > 0) rightEdge = DrawBadge(actionPill, rightEdge); + rightEdge = DrawBadge(namesPill, rightEdge); DrawFooterDivider(rightEdge); }; + // Read-only: names come from a portable Hakchi CE folder on the USB drive, saves are left as they are. + auto RefreshNamesFromHakchi = [&]() + { + ShowBusy(DrawScreen, "SAVE_MANAGER_HAKCHI_SEARCHING"); + const std::vector roots = ReadLines(OrphanSavesCommand("hakchi")); + if(roots.empty()) + { + DropPendingInput(); + RunDialog(DrawScreen, "SAVE_MANAGER_HAKCHI_NOT_FOUND", {}, "SAVE_MANAGER_HAKCHI_NOT_FOUND_NOTE", false); + return; + } + + ShowBusy(DrawScreen, "SAVE_MANAGER_HAKCHI_SCANNING"); + { + std::ofstream rootsFile(HakchiRootsFile); + for(const std::string & root : roots) + rootsFile << root << "\n"; + } + const std::vector output = ReadLines(OrphanSavesCommand("names " + HakchiRootsFile)); + std::remove(HakchiRootsFile.c_str()); + DropPendingInput(); + if(output.empty()) + { + RunDialog(DrawScreen, "SAVE_MANAGER_HAKCHI_FAILED", {}, "", false); + return; + } + + gameNames = LoadGameNames(); + ApplyNames(); + const int total = static_cast(entries.size()); + const int named = static_cast(std::count_if(entries.begin(), entries.end(), [](const SaveEntry & entry) { return entry.named; })); + RunDialog(DrawScreen, "SAVE_MANAGER_HAKCHI_DONE", + { Translate("SAVE_MANAGER_HAKCHI_GAMES") + ": " + output.back(), + Translate("SAVE_MANAGER_HAKCHI_NAMED") + ": " + std::to_string(named) + " / " + std::to_string(total) }, + named < total ? "SAVE_MANAGER_HAKCHI_PARTIAL_NOTE" : "", false); + }; + Load(); for(;;) { @@ -575,13 +670,15 @@ int main(int, char **) } else if(controller.GetButtonStatus(B)) return ExitBack; + else if((controller.GetButtonStatus(SELECT) || controller.GetButtonStatus(Y)) && !controller.PeekButtonStatus(START)) + RefreshNamesFromHakchi(); else if(controller.GetButtonStatus(START) && markedCount > 0) { auto marked = Marked(); std::vector lines; const int maxNames = 6; for(int i = 0; i < std::min(maxNames, static_cast(marked.size())); ++i) - lines.push_back(EntryLabel(marked[i]->code)); + lines.push_back(marked[i]->label); if(static_cast(marked.size()) > maxNames) lines.push_back("+" + std::to_string(marked.size() - maxNames)); if(RunDialog(DrawScreen, cleaning ? "SAVE_MANAGER_CLEAN_CONFIRM" : "SAVE_MANAGER_RESTORE_DELETED_CONFIRM", lines, From 593204f7704eb27e4ed4c31cefcfb4817352ce4f Mon Sep 17 00:00:00 2001 From: DefKorns Date: Tue, 13 Oct 2026 18:39:24 +0000 Subject: [PATCH 082/104] Remember installed game names and restore single saves from the USB backup --- README.md | 10 +- mod/etc/options_menu/language/de-DE.lang | 3 +- mod/etc/options_menu/language/en-US.lang | 3 +- mod/etc/options_menu/language/es-ES.lang | 3 +- mod/etc/options_menu/language/fr-FR.lang | 3 +- mod/etc/options_menu/language/it-IT.lang | 3 +- mod/etc/options_menu/language/ja-JP.lang | 3 +- mod/etc/options_menu/language/nl-NL.lang | 3 +- mod/etc/options_menu/language/pt-PT.lang | 3 +- mod/etc/options_menu/language/ru-RU.lang | 3 +- mod/etc/options_menu/scripts/OrphanSaves.sh | 140 +++++++++++++++----- pc/push_game_names.ps1 | 6 +- src/save_manager.cpp | 79 ++++++----- 13 files changed, 183 insertions(+), 79 deletions(-) diff --git a/README.md b/README.md index e4fe368..f574f05 100644 --- a/README.md +++ b/README.md @@ -99,15 +99,15 @@ Needs a USB/SD drive with your saves on it. One screen lists the actions on the | Action | What it does | | --- | --- | | Backup Saved Games to USB | Mirrors your saves folder to `/media/data/saves_backup`. Saves you deleted since the last backup are removed from it too; system folders (`FOLDER`, `hakchi`, `home-menu`, `mcp-state`) are skipped | -| Restore Saved Games from USB | Copies the backup back, overwriting saves with the same name and leaving the others alone | +| Restore Saved Games from USB › | Lists the saves in the backup (none marked); mark the ones you want back with **A** and press **Start**. They overwrite saves with the same name and leave the others alone. **Select all** restores the whole backup | | Clean Saves of Removed Games › | Lists the save folders of games that are no longer in any menu (all marked; **A** unmarks) and moves the marked ones to `/media/data/deleted_games_saves`. Menu folders (`CLV-S-*`) are never touched | | Restore Saves of Removed Games › | Lists what was moved aside; mark the ones you want back with **A** and press **Start**. A save already on the console is never overwritten | -#### Game names from Hakchi CE +#### Game names -The two "Removed Games" lists show game codes (`CLV-H-ABCDE`) because the games are no longer on the console. They show the game names instead once the names have been copied from your Hakchi CE library to `/media/data/hakchi_game_names`. Saves without a match keep showing their code. Getting the names never changes or moves a save. +The two "Removed Games" lists show the names of the removed games instead of their codes (`CLV-H-ABCDE`). Every time Save Manager opens, it remembers the names of the games installed at that moment in `/media/data/hakchi_game_names`, and a name stays there after its game is removed. Saves without a known name keep showing their code. Getting the names never changes or moves a save. -The console can't read your PC, so the PC sends the names. With the console on and reachable by Hakchi CE (USB or Wi-Fi), run [`pc/push_game_names.ps1`](pc/push_game_names.ps1) from a PowerShell window: +Games removed before you first opened this version of Save Manager were never seen on the console, so their names can only come from your Hakchi CE library. The console can't read your PC, so the PC sends them. With the console on and reachable by Hakchi CE (USB or Wi-Fi), run [`pc/push_game_names.ps1`](pc/push_game_names.ps1) from a PowerShell window: ```powershell .\push_game_names.ps1 --ip 192.168.1.50 @@ -118,7 +118,7 @@ The console can't read your PC, so the PC sends the names. With the console on a If Windows refuses to run scripts, use `powershell -ExecutionPolicy Bypass -File push_game_names.ps1 --ip 192.168.1.50`. -Run it again after adding or removing games in Hakchi CE. If a portable Hakchi CE folder is on the USB drive itself, press **Select** (or **Y**) in either list to read the names from there instead. +The names it sends are added to the ones already on the console. If a portable Hakchi CE folder is on the USB drive itself, press **Select** (or **Y**) in either list to read the names from there instead. ### Advanced Options diff --git a/mod/etc/options_menu/language/de-DE.lang b/mod/etc/options_menu/language/de-DE.lang index c4ad47e..d7924f9 100644 --- a/mod/etc/options_menu/language/de-DE.lang +++ b/mod/etc/options_menu/language/de-DE.lang @@ -94,7 +94,7 @@ SYSINFO_FREE=frei DIAGNOSTICS_OPTIONS=Diagnose SAVE_MANAGER=Spielstand-Verwaltung SAVE_MANAGER_BACKUP_DESC=Kopiert alle Spielstände nach USB:/data/saves_backup. -SAVE_MANAGER_RESTORE_DESC=Kopiert die USB-Sicherung zurück auf die Konsole. Spielstände mit gleichem Namen werden ersetzt. +SAVE_MANAGER_RESTORE_DESC=Wähle Spielstände aus der USB-Sicherung und kopiere sie zurück auf die Konsole. Spielstände mit gleichem Namen werden ersetzt. SAVE_MANAGER_CLEAN=Spielstände entfernter Spiele aufräumen SAVE_MANAGER_CLEAN_DESC=Verschiebt Spielstände von Spielen, die nicht mehr auf der Konsole sind, nach USB:/data/deleted_games_saves. SAVE_MANAGER_RESTORE_DELETED=Spielstände entfernter Spiele wiederherstellen @@ -116,6 +116,7 @@ SAVE_MANAGER_NO_ORPHANS=Keine Spielstände entfernter Spiele gefunden. SAVE_MANAGER_NO_DELETED=Keine beiseitegelegten Spielstände zum Wiederherstellen. SAVE_MANAGER_CLEAN_NOTE=Aufbewahrt in USB:/data/deleted_games_saves SAVE_MANAGER_RESTORE_NOTE=Spielstände, die schon auf der Konsole sind, werden nicht ersetzt. +SAVE_MANAGER_RESTORE_USB_NOTE=Markierte Spielstände ersetzen die gleichnamigen auf der Konsole. SAVE_MANAGER_WORKING=Wird ausgeführt... HINT_OK=OK HINT_MOVE=Verschieben diff --git a/mod/etc/options_menu/language/en-US.lang b/mod/etc/options_menu/language/en-US.lang index f212b6b..4083d27 100644 --- a/mod/etc/options_menu/language/en-US.lang +++ b/mod/etc/options_menu/language/en-US.lang @@ -95,7 +95,7 @@ SYSINFO_FREE=free DIAGNOSTICS_OPTIONS=Diagnostics SAVE_MANAGER=Save Manager SAVE_MANAGER_BACKUP_DESC=Copies every saved game to USB:/data/saves_backup. -SAVE_MANAGER_RESTORE_DESC=Copies the USB backup back to the console. Saves with the same name are replaced. +SAVE_MANAGER_RESTORE_DESC=Choose saves from the USB backup and copy them back to the console. Saves with the same name are replaced. SAVE_MANAGER_CLEAN=Clean Saves of Removed Games SAVE_MANAGER_CLEAN_DESC=Moves saves of games that are no longer on the console to USB:/data/deleted_games_saves. SAVE_MANAGER_RESTORE_DELETED=Restore Saves of Removed Games @@ -117,6 +117,7 @@ SAVE_MANAGER_NO_ORPHANS=No saves of removed games found. SAVE_MANAGER_NO_DELETED=No moved saves to restore. SAVE_MANAGER_CLEAN_NOTE=Kept in USB:/data/deleted_games_saves SAVE_MANAGER_RESTORE_NOTE=Saves already on the console are not replaced. +SAVE_MANAGER_RESTORE_USB_NOTE=Marked saves replace the ones with the same name on the console. SAVE_MANAGER_WORKING=Working... HINT_OK=OK HINT_MOVE=Move diff --git a/mod/etc/options_menu/language/es-ES.lang b/mod/etc/options_menu/language/es-ES.lang index 96f0bff..13b1e53 100644 --- a/mod/etc/options_menu/language/es-ES.lang +++ b/mod/etc/options_menu/language/es-ES.lang @@ -94,7 +94,7 @@ SYSINFO_FREE=libres DIAGNOSTICS_OPTIONS=Diagnóstico SAVE_MANAGER=Gestor de Partidas SAVE_MANAGER_BACKUP_DESC=Copia todas las partidas guardadas en USB:/data/saves_backup. -SAVE_MANAGER_RESTORE_DESC=Copia la copia de seguridad de la USB de vuelta a la consola. Las partidas con el mismo nombre se reemplazan. +SAVE_MANAGER_RESTORE_DESC=Elige partidas de la copia de seguridad de la USB y cópialas de vuelta a la consola. Las partidas con el mismo nombre se reemplazan. SAVE_MANAGER_CLEAN=Limpiar Partidas de Juegos Eliminados SAVE_MANAGER_CLEAN_DESC=Mueve las partidas de juegos que ya no están en la consola a USB:/data/deleted_games_saves. SAVE_MANAGER_RESTORE_DELETED=Restaurar Partidas de Juegos Eliminados @@ -116,6 +116,7 @@ SAVE_MANAGER_NO_ORPHANS=No hay partidas de juegos eliminados. SAVE_MANAGER_NO_DELETED=No hay partidas apartadas para restaurar. SAVE_MANAGER_CLEAN_NOTE=Guardadas en USB:/data/deleted_games_saves SAVE_MANAGER_RESTORE_NOTE=Las partidas que ya están en la consola no se reemplazan. +SAVE_MANAGER_RESTORE_USB_NOTE=Las partidas marcadas reemplazan a las del mismo nombre en la consola. SAVE_MANAGER_WORKING=Procesando... HINT_OK=OK HINT_MOVE=Mover diff --git a/mod/etc/options_menu/language/fr-FR.lang b/mod/etc/options_menu/language/fr-FR.lang index cfdc6ce..cb2d8fa 100644 --- a/mod/etc/options_menu/language/fr-FR.lang +++ b/mod/etc/options_menu/language/fr-FR.lang @@ -94,7 +94,7 @@ SYSINFO_FREE=libres DIAGNOSTICS_OPTIONS=Diagnostic SAVE_MANAGER=Gestionnaire de Sauvegardes SAVE_MANAGER_BACKUP_DESC=Copie toutes les sauvegardes vers USB:/data/saves_backup. -SAVE_MANAGER_RESTORE_DESC=Recopie la sauvegarde USB sur la console. Les sauvegardes portant le même nom sont remplacées. +SAVE_MANAGER_RESTORE_DESC=Choisissez des sauvegardes dans la sauvegarde USB et recopiez-les sur la console. Les sauvegardes portant le même nom sont remplacées. SAVE_MANAGER_CLEAN=Nettoyer les Sauvegardes de Jeux Supprimés SAVE_MANAGER_CLEAN_DESC=Déplace les sauvegardes des jeux qui ne sont plus sur la console vers USB:/data/deleted_games_saves. SAVE_MANAGER_RESTORE_DELETED=Restaurer les Sauvegardes de Jeux Supprimés @@ -116,6 +116,7 @@ SAVE_MANAGER_NO_ORPHANS=Aucune sauvegarde de jeu supprimé. SAVE_MANAGER_NO_DELETED=Aucune sauvegarde mise de côté à restaurer. SAVE_MANAGER_CLEAN_NOTE=Conservées dans USB:/data/deleted_games_saves SAVE_MANAGER_RESTORE_NOTE=Les sauvegardes déjà présentes sur la console ne sont pas remplacées. +SAVE_MANAGER_RESTORE_USB_NOTE=Les sauvegardes cochées remplacent celles du même nom sur la console. SAVE_MANAGER_WORKING=En cours... HINT_OK=OK HINT_MOVE=Déplacer diff --git a/mod/etc/options_menu/language/it-IT.lang b/mod/etc/options_menu/language/it-IT.lang index 886511d..6c83891 100644 --- a/mod/etc/options_menu/language/it-IT.lang +++ b/mod/etc/options_menu/language/it-IT.lang @@ -94,7 +94,7 @@ SYSINFO_FREE=liberi DIAGNOSTICS_OPTIONS=Diagnostica SAVE_MANAGER=Gestione Salvataggi SAVE_MANAGER_BACKUP_DESC=Copia tutti i salvataggi su USB:/data/saves_backup. -SAVE_MANAGER_RESTORE_DESC=Ricopia il backup della USB sulla console. I salvataggi con lo stesso nome vengono sostituiti. +SAVE_MANAGER_RESTORE_DESC=Scegli i salvataggi dal backup della USB e ricopiali sulla console. I salvataggi con lo stesso nome vengono sostituiti. SAVE_MANAGER_CLEAN=Pulisci Salvataggi di Giochi Rimossi SAVE_MANAGER_CLEAN_DESC=Sposta i salvataggi dei giochi non più presenti sulla console in USB:/data/deleted_games_saves. SAVE_MANAGER_RESTORE_DELETED=Ripristina Salvataggi di Giochi Rimossi @@ -116,6 +116,7 @@ SAVE_MANAGER_NO_ORPHANS=Nessun salvataggio di giochi rimossi. SAVE_MANAGER_NO_DELETED=Nessun salvataggio messo da parte da ripristinare. SAVE_MANAGER_CLEAN_NOTE=Conservati in USB:/data/deleted_games_saves SAVE_MANAGER_RESTORE_NOTE=I salvataggi già presenti sulla console non vengono sostituiti. +SAVE_MANAGER_RESTORE_USB_NOTE=I salvataggi selezionati sostituiscono quelli con lo stesso nome sulla console. SAVE_MANAGER_WORKING=Elaborazione... HINT_OK=OK HINT_MOVE=Sposta diff --git a/mod/etc/options_menu/language/ja-JP.lang b/mod/etc/options_menu/language/ja-JP.lang index 5a5c927..e061e77 100644 --- a/mod/etc/options_menu/language/ja-JP.lang +++ b/mod/etc/options_menu/language/ja-JP.lang @@ -94,7 +94,7 @@ SYSINFO_FREE=あき DIAGNOSTICS_OPTIONS=しんだん SAVE_MANAGER=セーブ管理 SAVE_MANAGER_BACKUP_DESC=すべてのセーブデータを USB:/data/saves_backup にコピーします。 -SAVE_MANAGER_RESTORE_DESC=USBのバックアップを本体に戻します。同じ名前のセーブデータは上書きされます。 +SAVE_MANAGER_RESTORE_DESC=USBのバックアップからセーブデータを選んで本体に戻します。同じ名前のセーブデータは上書きされます。 SAVE_MANAGER_CLEAN=削除したゲームのセーブを整理 SAVE_MANAGER_CLEAN_DESC=本体にないゲームのセーブデータを USB:/data/deleted_games_saves に移動します。 SAVE_MANAGER_RESTORE_DELETED=削除したゲームのセーブを復元 @@ -116,6 +116,7 @@ SAVE_MANAGER_NO_ORPHANS=削除したゲームのセーブはありません。 SAVE_MANAGER_NO_DELETED=復元できる移動済みのセーブはありません。 SAVE_MANAGER_CLEAN_NOTE=USB:/data/deleted_games_saves に保管 SAVE_MANAGER_RESTORE_NOTE=本体にあるセーブデータは上書きされません。 +SAVE_MANAGER_RESTORE_USB_NOTE=選んだセーブデータは本体にある同じ名前のものを上書きします。 SAVE_MANAGER_WORKING=処理中... HINT_OK=OK HINT_MOVE=移動 diff --git a/mod/etc/options_menu/language/nl-NL.lang b/mod/etc/options_menu/language/nl-NL.lang index bcb537b..bab27c1 100644 --- a/mod/etc/options_menu/language/nl-NL.lang +++ b/mod/etc/options_menu/language/nl-NL.lang @@ -94,7 +94,7 @@ SYSINFO_FREE=vrij DIAGNOSTICS_OPTIONS=Diagnose SAVE_MANAGER=Opslagbeheer SAVE_MANAGER_BACKUP_DESC=Kopieert alle opgeslagen spellen naar USB:/data/saves_backup. -SAVE_MANAGER_RESTORE_DESC=Kopieert de USB-back-up terug naar de console. Opslagbestanden met dezelfde naam worden vervangen. +SAVE_MANAGER_RESTORE_DESC=Kies opslagbestanden uit de USB-back-up en kopieer ze terug naar de console. Opslagbestanden met dezelfde naam worden vervangen. SAVE_MANAGER_CLEAN=Opslag van verwijderde spellen opruimen SAVE_MANAGER_CLEAN_DESC=Verplaatst opslag van spellen die niet meer op de console staan naar USB:/data/deleted_games_saves. SAVE_MANAGER_RESTORE_DELETED=Opslag van verwijderde spellen herstellen @@ -116,6 +116,7 @@ SAVE_MANAGER_NO_ORPHANS=Geen opslag van verwijderde spellen gevonden. SAVE_MANAGER_NO_DELETED=Geen apart gezette opslag om te herstellen. SAVE_MANAGER_CLEAN_NOTE=Bewaard in USB:/data/deleted_games_saves SAVE_MANAGER_RESTORE_NOTE=Opslag die al op de console staat wordt niet vervangen. +SAVE_MANAGER_RESTORE_USB_NOTE=Gemarkeerde opslagbestanden vervangen die met dezelfde naam op de console. SAVE_MANAGER_WORKING=Bezig... HINT_OK=OK HINT_MOVE=Verplaatsen diff --git a/mod/etc/options_menu/language/pt-PT.lang b/mod/etc/options_menu/language/pt-PT.lang index 3b42328..51e8539 100644 --- a/mod/etc/options_menu/language/pt-PT.lang +++ b/mod/etc/options_menu/language/pt-PT.lang @@ -94,7 +94,7 @@ SYSINFO_FREE=livres DIAGNOSTICS_OPTIONS=Diagnóstico SAVE_MANAGER=Gestor de Jogos Guardados SAVE_MANAGER_BACKUP_DESC=Copia todos os jogos guardados para USB:/data/saves_backup. -SAVE_MANAGER_RESTORE_DESC=Copia a cópia de segurança da USB de volta para a consola. Os jogos guardados com o mesmo nome são substituídos. +SAVE_MANAGER_RESTORE_DESC=Escolha jogos guardados da cópia de segurança da USB e copie-os de volta para a consola. Os jogos guardados com o mesmo nome são substituídos. SAVE_MANAGER_CLEAN=Limpar Jogos Guardados de Jogos Removidos SAVE_MANAGER_CLEAN_DESC=Move os jogos guardados de jogos que já não estão na consola para USB:/data/deleted_games_saves. SAVE_MANAGER_RESTORE_DELETED=Restaurar Jogos Guardados de Jogos Removidos @@ -116,6 +116,7 @@ SAVE_MANAGER_NO_ORPHANS=Não há jogos guardados de jogos removidos. SAVE_MANAGER_NO_DELETED=Não há jogos guardados movidos para restaurar. SAVE_MANAGER_CLEAN_NOTE=Guardados em USB:/data/deleted_games_saves SAVE_MANAGER_RESTORE_NOTE=Os jogos guardados que já estão na consola não são substituídos. +SAVE_MANAGER_RESTORE_USB_NOTE=Os jogos guardados marcados substituem os que têm o mesmo nome na consola. SAVE_MANAGER_WORKING=A trabalhar... HINT_OK=OK HINT_MOVE=Mover diff --git a/mod/etc/options_menu/language/ru-RU.lang b/mod/etc/options_menu/language/ru-RU.lang index ef06f85..928e788 100644 --- a/mod/etc/options_menu/language/ru-RU.lang +++ b/mod/etc/options_menu/language/ru-RU.lang @@ -94,7 +94,7 @@ SYSINFO_FREE=свободно DIAGNOSTICS_OPTIONS=Диагностика SAVE_MANAGER=Менеджер сохранений SAVE_MANAGER_BACKUP_DESC=Копирует все сохранения в USB:/data/saves_backup. -SAVE_MANAGER_RESTORE_DESC=Копирует резервную копию с USB обратно на консоль. Сохранения с тем же именем заменяются. +SAVE_MANAGER_RESTORE_DESC=Выберите сохранения из резервной копии на USB и скопируйте их обратно на консоль. Сохранения с тем же именем заменяются. SAVE_MANAGER_CLEAN=Очистить сохранения удалённых игр SAVE_MANAGER_CLEAN_DESC=Перемещает сохранения игр, которых больше нет на консоли, в USB:/data/deleted_games_saves. SAVE_MANAGER_RESTORE_DELETED=Восстановить сохранения удалённых игр @@ -116,6 +116,7 @@ SAVE_MANAGER_NO_ORPHANS=Сохранений удалённых игр не на SAVE_MANAGER_NO_DELETED=Нет перемещённых сохранений для восстановления. SAVE_MANAGER_CLEAN_NOTE=Хранятся в USB:/data/deleted_games_saves SAVE_MANAGER_RESTORE_NOTE=Сохранения, уже имеющиеся на консоли, не заменяются. +SAVE_MANAGER_RESTORE_USB_NOTE=Отмеченные сохранения заменяют сохранения с тем же именем на консоли. SAVE_MANAGER_WORKING=Выполняется... HINT_OK=OK HINT_MOVE=Переместить diff --git a/mod/etc/options_menu/scripts/OrphanSaves.sh b/mod/etc/options_menu/scripts/OrphanSaves.sh index 07877f8..f84da7b 100644 --- a/mod/etc/options_menu/scripts/OrphanSaves.sh +++ b/mod/etc/options_menu/scripts/OrphanSaves.sh @@ -22,8 +22,12 @@ # OrphanSaves.sh deleted entries already moved aside # OrphanSaves.sh move FILE move the entries listed in FILE aside # OrphanSaves.sh restore FILE move them back (never over an existing save) +# OrphanSaves.sh backup entries in the USB backup +# OrphanSaves.sh restore-backup FILE copy the entries listed in FILE back from the USB backup (replaces same names) # OrphanSaves.sh hakchi portable Hakchi CE folders found on the USB drive -# OrphanSaves.sh names FILE rebuild the entryname cache from the Hakchi folders listed in FILE +# OrphanSaves.sh names FILE add the games of the Hakchi folders listed in FILE to the entryname cache +# OrphanSaves.sh merge-names FILE add the entryname lines in FILE to the cache +# OrphanSaves.sh sync-names add the games installed on the console to the cache # OrphanSaves.sh cached-names print the name cache # Name lookups only read Hakchi's .desktop files, saves are never touched. @@ -110,39 +114,69 @@ moveListed() { sync } +# like the full restore: files with the same name are replaced, the others are left alone +copyFromBackup() { + local entry="$1" content file + case "$entry" in + retroarch/*) + content="${entry#retroarch/}" + mkdir -p "$saves_root/retroarch" || return 1 + ls "$backup_root/retroarch" 2>/dev/null | while IFS= read -r file; do + [ "$(echo "$file" | raContent)" = "$content" ] || continue + cp -a "$backup_root/retroarch/$file" "$saves_root/retroarch/$file" || exit 1 + done + ;; + *) + [ -d "$backup_root/$entry" ] || return 0 + cp -a "$backup_root/$entry" "$saves_root/" + ;; + esac +} + +restoreFromBackup() { + local entry + while IFS= read -r entry; do + isEntry "$entry" || continue + copyFromBackup "$entry" || return 1 + done < "$1" + sync +} + # game folders are pruned so the walk stays shallow on big libraries hakchiRoots() { find /media -maxdepth 4 -type d \( -name 'games*' -o -name saves -o -name '.*' \) -prune -o -type f -name '[Hh][Aa][Kk][Cc][Hh][Ii].[Ee][Xx][Ee]' -print 2>/dev/null | sed 's#/[^/]*$##' | sort -u } -# entryname for every game in games, games_snes, games_md, games_originals, games_cache. +# entryname for every game folder inside the folders read from stdin. # RetroArch content names come from the files in each game folder (exact, spaces included) # and from Exec= paths (ROMs kept elsewhere). -hakchiNames() { - local root dir - while IFS= read -r root; do - for dir in "$root"/games*; do - [ -d "$dir" ] || continue - find "$dir" -mindepth 2 -maxdepth 2 -type f -name '*.desktop' -exec awk ' - function flush( n, i, token) { - if(code ~ /^CLV-/ && name != "") { - print "N\t" code "\t" name - n = split(exec, token, " ") - for(i = 1; i <= n; i++) - if(token[i] ~ /\//) { sub(/.*\//, "", token[i]); print "R\t" code "\t" token[i] } - } - code = name = exec = "" +gameNames() { + local dir + set -- + while IFS= read -r dir; do + [ -d "$dir" ] && set -- "$@" "$dir" + done + [ $# -gt 0 ] || return 0 + { + find "$@" -mindepth 2 -maxdepth 2 -type f -name '*.desktop' -exec awk ' + function flush( n, i, token) { + if(code ~ /^CLV-/ && name != "") { + print "N\t" code "\t" name + n = split(exec, token, " ") + for(i = 1; i <= n; i++) + if(token[i] ~ /\//) { sub(/.*\//, "", token[i]); print "R\t" code "\t" token[i] } } - FNR == 1 { flush() } - { sub(/\r$/, ""); gsub(/\t/, " ") } - /^Code=/ { code = substr($0, 6) } - /^Name=/ && name == "" { name = substr($0, 6) } - /^Exec=/ { exec = substr($0, 6) } - END { flush() }' {} + 2>/dev/null - find "$dir" -mindepth 2 -maxdepth 2 -type f ! -name '*.desktop' ! -name '*.png' 2>/dev/null \ - | awk -F / '{ print "R\t" $(NF-1) "\t" $NF }' - done - done < "$1" | awk -F '\t' ' + code = name = exec = "" + } + FNR == 1 { flush() } + { sub(/\r$/, ""); gsub(/\t/, " ") } + /^Code=/ { code = substr($0, 6) } + /^Name=/ && name == "" { name = substr($0, 6) } + /^Exec=/ { exec = substr($0, 6) } + END { flush() }' {} + 2>/dev/null + find "$@" -mindepth 2 -maxdepth 2 -type f ! -name '*.desktop' ! -name '*.png' 2>/dev/null \ + | awk -F / '{ print "R\t" $(NF-1) "\t" $NF }' + } | awk -F '\t' ' NF != 3 || $3 == "" { next } $1 == "N" { if(!($2 in name)) { name[$2] = $3; print $2 "\t" $3 } next } { rom[++n] = $2 "\t" $3 } @@ -158,13 +192,45 @@ hakchiNames() { }' | awk -F '\t' '!seen[$1]++' } -refreshNames() { +# games, games_snes, games_md, games_originals, games_cache of each Hakchi folder listed in $1 +hakchiNames() { + local root + while IFS= read -r root; do + printf '%s\n' "$root"/games* + done < "$1" | gameNames +} + +# menu folders, linked storage and the stock games +installedNames() { + local games_root + games_root="$(dirname "$(findGameStorage)")" + printf '%s\n' "$games_root"/*/* "$games_root"/*/.storage "$squashfs/usr/share/games" | gameNames +} + +# New names win and names of games that are gone are kept, so a save stays named after its +# game is removed. The cache is only rewritten when something changed. +mergeNames() { local tmp="$names_cache.tmp" mkdir -pm 777 "$(dirname "$names_cache")" || return 1 - hakchiNames "$1" > "$tmp" || { rm -f "$tmp"; return 1; } + { cat "$1"; cat "$names_cache" 2>/dev/null; } | awk -F '\t' 'NF == 2 && $2 != "" && !seen[$1]++' | sort > "$tmp" || { rm -f "$tmp"; return 1; } + if cmp -s "$tmp" "$names_cache"; then + rm -f "$tmp" + return 0 + fi mv -f "$tmp" "$names_cache" || return 1 sync - grep -c "^CLV-" "$names_cache" || true +} + +# runs a lookup ($@) and merges what it found; for Hakchi folders also prints how many games had a name +addNames() { + local found status + found="$(mktemp)" + "$@" > "$found" + mergeNames "$found" + status=$? + [ $status -eq 0 ] && [ "$1" = hakchiNames ] && { grep -c "^CLV-" "$found" || true; } + rm -f "$found" + return $status } case "$1" in @@ -186,17 +252,29 @@ case "$1" in restore) [ -f "$2" ] && moveListed "$deleted_root" "$saves_root" "$2" ;; + backup) + saveEntries "$backup_root" | sort -u + ;; + restore-backup) + [ -f "$2" ] && restoreFromBackup "$2" + ;; hakchi) hakchiRoots ;; names) - [ -s "$2" ] && refreshNames "$2" + [ -s "$2" ] && addNames hakchiNames "$2" + ;; + merge-names) + [ -f "$2" ] && mergeNames "$2" + ;; + sync-names) + addNames installedNames ;; cached-names) cat "$names_cache" 2>/dev/null ;; *) - echo "usage: $0 stats|orphans|deleted|move FILE|restore FILE|hakchi|names FILE|cached-names" >&2 + echo "usage: $0 stats|orphans|deleted|move FILE|restore FILE|backup|restore-backup FILE|hakchi|names FILE|merge-names FILE|sync-names|cached-names" >&2 exit 2 ;; esac diff --git a/pc/push_game_names.ps1 b/pc/push_game_names.ps1 index 2f778f6..0b13ea9 100644 --- a/pc/push_game_names.ps1 +++ b/pc/push_game_names.ps1 @@ -23,7 +23,8 @@ param( ) $ErrorActionPreference = 'Stop' -$CachePath = '/media/data/hakchi_game_names' +$PushPath = '/tmp/hakchi_game_names.push' +$MergeScript = '/etc/options_menu/scripts/OrphanSaves.sh' function Stop-WithError([string]$message) { Write-Host $message -ForegroundColor Red @@ -121,7 +122,8 @@ if ($gameNames.Count -eq 0) { $tmp = [IO.Path]::GetTempFileName() try { [IO.File]::WriteAllText($tmp, ($lines -join "`n") + "`n", $utf8) - $remote = "`"mkdir -p /media/data && cat > $CachePath.tmp && mv -f $CachePath.tmp $CachePath && sync`"" + # merged on the console, so names of games no longer in this library are kept + $remote = "`"cat > $PushPath && sh $MergeScript merge-names $PushPath; status=`$?; rm -f $PushPath; exit `$status`"" Write-Host "Sending $($gameNames.Count) game names from $HakchiPath to $Ip..." $ssh = Start-Process -FilePath ssh -NoNewWindow -Wait -PassThru -RedirectStandardInput $tmp ` -ArgumentList @('-o', 'ConnectTimeout=10', '-o', 'StrictHostKeyChecking=accept-new', "root@$Ip", $remote) diff --git a/src/save_manager.cpp b/src/save_manager.cpp index de5987a..a3dc231 100644 --- a/src/save_manager.cpp +++ b/src/save_manager.cpp @@ -54,6 +54,23 @@ namespace enum class Action { Backup, RestoreUsb, Clean, RestoreDeleted }; + struct ListInfo + { + std::string titleKey; + std::string headerKey; + std::string noteKey; + std::string confirmKey; + std::string actionHintKey; + std::string listArgs; + std::string actionArgs; + bool preMarked; + }; + + // Clean starts with every orphan marked, the restores with none + const ListInfo CleanList{ "SAVE_MANAGER_CLEAN", "SAVE_MANAGER_TO_MOVE", "SAVE_MANAGER_CLEAN_NOTE", "SAVE_MANAGER_CLEAN_CONFIRM", "HINT_MOVE", "orphans", "move ", true }; + const ListInfo RestoreDeletedList{ "SAVE_MANAGER_RESTORE_DELETED", "SAVE_MANAGER_TO_RESTORE", "SAVE_MANAGER_RESTORE_NOTE", "SAVE_MANAGER_RESTORE_DELETED_CONFIRM", "HINT_RESTORE", "deleted", "restore ", false }; + const ListInfo RestoreBackupList{ "RESTORE_SAVES_USB", "SAVE_MANAGER_TO_RESTORE", "SAVE_MANAGER_RESTORE_USB_NOTE", "SAVE_MANAGER_RESTORE_CONFIRM", "HINT_RESTORE", "backup", "restore-backup ", false }; + struct ActionInfo { Action action; @@ -435,14 +452,13 @@ int main(int, char **) return status != -1 && WIFEXITED(status) && WEXITSTATUS(status) == 0; }; - // Marked-list screen shared by "clean" (orphans, all pre-marked) and "restore deleted" (none marked). - auto RunSaveList = [&](bool cleaning) -> int + auto RunSaveList = [&](const ListInfo & list) -> int { - SetTitle(cleaning ? "SAVE_MANAGER_CLEAN" : "SAVE_MANAGER_RESTORE_DELETED"); - Texture summaryHeader(Translate(cleaning ? "SAVE_MANAGER_TO_MOVE" : "SAVE_MANAGER_TO_RESTORE"), PanelHeaderFontSize, renderer, PanelInnerX, summaryBox.y + 14, false, ToAbgr(UiTheme::TextDim), true); + SetTitle(list.titleKey); + Texture summaryHeader(Translate(list.headerKey), PanelHeaderFontSize, renderer, PanelInnerX, summaryBox.y + 14, false, ToAbgr(UiTheme::TextDim), true); Texture noneMarkedText(Translate("SAVE_MANAGER_NONE_MARKED"), PanelFontSize, renderer, PanelInnerX, SummaryListY, false, ToAbgr(UiTheme::Text), true); std::vector noneHintLines = MakeLines(WrapToWidth(Translate("SAVE_MANAGER_NONE_HINT"), PanelFontSize, PanelInnerW), PanelFontSize, renderer, UiTheme::TextDim); - std::vector noteLines = NoteLines(cleaning ? "SAVE_MANAGER_CLEAN_NOTE" : "SAVE_MANAGER_RESTORE_NOTE"); + std::vector noteLines = NoteLines(list.noteKey); Badge actionPill = badges.MakePill("Start", ""); Badge namesPill = badges.MakePill("Select", Translate("HINT_GAME_NAMES")); GameNames gameNames = LoadGameNames(); @@ -474,7 +490,7 @@ int main(int, char **) markedCount = count; summaryCount = Texture(std::to_string(count), PanelHeaderFontSize, renderer, 0, summaryBox.y + 14, false, ToAbgr(count ? UiTheme::Accent : UiTheme::TextDim), true); summaryCount.rect.x = summaryBox.x + summaryBox.w - PanelPad - summaryCount.rect.w; - actionPill.label = Texture(Translate(cleaning ? "HINT_MOVE" : "HINT_RESTORE") + " (" + std::to_string(count) + ")", 16, renderer, 0, 0, false, ToAbgr(UiTheme::Text), true); + actionPill.label = Texture(Translate(list.actionHintKey) + " (" + std::to_string(count) + ")", 16, renderer, 0, 0, false, ToAbgr(UiTheme::Text), true); int shown = (count > SummaryMaxRows) ? SummaryMaxRows - 1 : count; summaryNames.clear(); @@ -506,11 +522,11 @@ int main(int, char **) auto Load = [&]() { entries.clear(); - for(const std::string & code : ReadLines(OrphanSavesCommand(cleaning ? "orphans" : "deleted"))) + for(const std::string & code : ReadLines(OrphanSavesCommand(list.listArgs))) { SaveEntry entry; entry.code = code; - entry.marked = cleaning; + entry.marked = list.preMarked; entries.push_back(entry); } listOffset = 0; @@ -681,16 +697,25 @@ int main(int, char **) lines.push_back(marked[i]->label); if(static_cast(marked.size()) > maxNames) lines.push_back("+" + std::to_string(marked.size() - maxNames)); - if(RunDialog(DrawScreen, cleaning ? "SAVE_MANAGER_CLEAN_CONFIRM" : "SAVE_MANAGER_RESTORE_DELETED_CONFIRM", lines, - cleaning ? "SAVE_MANAGER_CLEAN_NOTE" : "SAVE_MANAGER_RESTORE_NOTE", true)) + if(RunDialog(DrawScreen, list.confirmKey, lines, list.noteKey, true)) { - std::ofstream selection(SelectionFile); - for(const SaveEntry * entry : marked) - selection << entry->code << "\n"; - selection.close(); - RunBusy(DrawScreen, OrphanSavesCommand((cleaning ? "move " : "restore ") + SelectionFile)); - std::remove(SelectionFile.c_str()); + const bool fromBackup = &list == &RestoreBackupList; + bool ok; + // everything marked is the full restore, which also brings back what the list doesn't show + if(fromBackup && markedCount == entryCount) + ok = RunBusy(DrawScreen, "sh " + ScriptLocation + "RestoreSavesUsb.sh >/dev/null 2>&1"); + else + { + std::ofstream selection(SelectionFile); + for(const SaveEntry * entry : marked) + selection << entry->code << "\n"; + selection.close(); + ok = RunBusy(DrawScreen, OrphanSavesCommand(list.actionArgs + SelectionFile)); + std::remove(SelectionFile.c_str()); + } Load(); + if(fromBackup) + RunDialog(DrawScreen, ok ? "RESTORE_SAVES_USB_DONE" : "RESTORE_SAVES_USB_FAILED", {}, "", false); } } @@ -822,28 +847,16 @@ int main(int, char **) } return -1; case Action::RestoreUsb: - if(stats.backupSaves < 0) - { - ShowMessage("RESTORE_SAVES_USB_NOT_FOUND", ""); - return -1; - } - if(RunDialog(DrawActions, "SAVE_MANAGER_RESTORE_CONFIRM", { Translate("SAVE_MANAGER_STAT_BACKUP") + ": " + std::to_string(stats.backupSaves) }, "SAVE_MANAGER_RESTORE_DESC", true)) - { - const bool ok = RunBusy(DrawActions, "sh " + ScriptLocation + "RestoreSavesUsb.sh >/dev/null 2>&1"); - RefreshStats(); - ShowMessage(ok ? "RESTORE_SAVES_USB_DONE" : "RESTORE_SAVES_USB_FAILED", ""); - } - return -1; case Action::Clean: case Action::RestoreDeleted: { - const bool cleaning = action == Action::Clean; - if((cleaning ? stats.orphans : stats.deleted) == 0) + const int available = action == Action::RestoreUsb ? stats.backupSaves : action == Action::Clean ? stats.orphans : stats.deleted; + if(available <= 0) { - ShowMessage(cleaning ? "SAVE_MANAGER_NO_ORPHANS" : "SAVE_MANAGER_NO_DELETED", ""); + ShowMessage(action == Action::RestoreUsb ? "RESTORE_SAVES_USB_NOT_FOUND" : action == Action::Clean ? "SAVE_MANAGER_NO_ORPHANS" : "SAVE_MANAGER_NO_DELETED", ""); return -1; } - const int result = RunSaveList(cleaning); + const int result = RunSaveList(action == Action::RestoreUsb ? RestoreBackupList : action == Action::Clean ? CleanList : RestoreDeletedList); SetTitle("SAVE_MANAGER"); RefreshStats(); return result == ExitLeaveMenu ? ExitLeaveMenu : -1; @@ -852,6 +865,8 @@ int main(int, char **) return -1; }; + // remembers the names of the installed games, so their saves stay named once a game is removed + system(OrphanSavesCommand("sync-names").c_str()); SetTitle("SAVE_MANAGER"); RefreshStats(); RefreshDetail(); From f63298aa66c7ba82d91b1a41c14ba9d044727d5f Mon Sep 17 00:00:00 2001 From: DefKorns Date: Tue, 13 Oct 2026 19:19:55 +0000 Subject: [PATCH 083/104] Keep game names in the saves folder and record them at boot --- README.md | 4 +-- ...tionsMenuCleanup => S8104OptionsMenuTasks} | 12 +++++-- mod/etc/options_menu/scripts/OrphanSaves.sh | 36 ++++++++++++------- .../options_menu/scripts/RestoreSavesUsb.sh | 3 +- mod/install | 1 + pc/push_game_names.ps1 | 3 +- src/save_manager.cpp | 5 +-- 7 files changed, 39 insertions(+), 25 deletions(-) rename mod/etc/init.d/{S8104OptionsMenuCleanup => S8104OptionsMenuTasks} (83%) diff --git a/README.md b/README.md index f574f05..4dc3edd 100644 --- a/README.md +++ b/README.md @@ -105,9 +105,9 @@ Needs a USB/SD drive with your saves on it. One screen lists the actions on the #### Game names -The two "Removed Games" lists show the names of the removed games instead of their codes (`CLV-H-ABCDE`). Every time Save Manager opens, it remembers the names of the games installed at that moment in `/media/data/hakchi_game_names`, and a name stays there after its game is removed. Saves without a known name keep showing their code. Getting the names never changes or moves a save. +The two "Removed Games" lists show the names of the removed games instead of their codes (`CLV-H-ABCDE`). Every time the console starts, it remembers the names of the games installed at that moment in `game_names.txt` in your saves folder, and a name stays there after its game is removed. The backup keeps the file with your saves, and a restore adds its names without replacing newer ones. Saves without a known name keep showing their code. Getting the names never changes or moves a save. -Games removed before you first opened this version of Save Manager were never seen on the console, so their names can only come from your Hakchi CE library. The console can't read your PC, so the PC sends them. With the console on and reachable by Hakchi CE (USB or Wi-Fi), run [`pc/push_game_names.ps1`](pc/push_game_names.ps1) from a PowerShell window: +Games removed before this version was installed were never seen on the console, so their names can only come from your Hakchi CE library. The console can't read your PC, so the PC sends them. With the console on and reachable by Hakchi CE (USB or Wi-Fi), run [`pc/push_game_names.ps1`](pc/push_game_names.ps1) from a PowerShell window: ```powershell .\push_game_names.ps1 --ip 192.168.1.50 diff --git a/mod/etc/init.d/S8104OptionsMenuCleanup b/mod/etc/init.d/S8104OptionsMenuTasks similarity index 83% rename from mod/etc/init.d/S8104OptionsMenuCleanup rename to mod/etc/init.d/S8104OptionsMenuTasks index 9b95950..6979a51 100644 --- a/mod/etc/init.d/S8104OptionsMenuCleanup +++ b/mod/etc/init.d/S8104OptionsMenuTasks @@ -27,22 +27,28 @@ cleanRetroarchDefaults() { rm -rf "$mountpoint/etc/options_menu/retroarch/default_files" } -clean() { +# the console restarts after every Hakchi sync, so each new game is seen before it can be removed +rememberGameNames() { + sh /etc/options_menu/scripts/OrphanSaves.sh sync-names +} + +tasks() { source /etc/preinit script_init cleanExtraCovers cleanRetroarchDefaults + rememberGameNames } case "$1" in start) - clean & + tasks & ;; stop|restart) ;; *) - echo "OptionsMenuCleanup: Please use start." + echo "OptionsMenuTasks: Please use start." exit 1 ;; esac diff --git a/mod/etc/options_menu/scripts/OrphanSaves.sh b/mod/etc/options_menu/scripts/OrphanSaves.sh index f84da7b..6d07610 100644 --- a/mod/etc/options_menu/scripts/OrphanSaves.sh +++ b/mod/etc/options_menu/scripts/OrphanSaves.sh @@ -27,6 +27,7 @@ # OrphanSaves.sh hakchi portable Hakchi CE folders found on the USB drive # OrphanSaves.sh names FILE add the games of the Hakchi folders listed in FILE to the entryname cache # OrphanSaves.sh merge-names FILE add the entryname lines in FILE to the cache +# OrphanSaves.sh merge-backup-names add the names in the USB backup, keeping newer ones # OrphanSaves.sh sync-names add the games installed on the console to the cache # OrphanSaves.sh cached-names print the name cache # Name lookups only read Hakchi's .desktop files, saves are never touched. @@ -37,7 +38,8 @@ script_init saves_root=/media/hakchi/saves backup_root=/media/data/saves_backup deleted_root=/media/data/deleted_games_saves -names_cache=/media/data/hakchi_game_names +names_cache="$profilepath/game_names.txt" +old_names_cache=/media/data/hakchi_game_names ra_save_suffix='\.(srm|rtc|state([0-9]+)?(\.auto)?)(\.png)?$' raContent() { @@ -114,7 +116,6 @@ moveListed() { sync } -# like the full restore: files with the same name are replaced, the others are left alone copyFromBackup() { local entry="$1" content file case "$entry" in @@ -139,7 +140,12 @@ restoreFromBackup() { isEntry "$entry" || continue copyFromBackup "$entry" || return 1 done < "$1" - sync + mergeBackupNames +} + +mergeBackupNames() { + [ -f "$backup_root/game_names.txt" ] || { sync; return 0; } + mergeNames "$backup_root/game_names.txt" older } # game folders are pruned so the walk stays shallow on big libraries @@ -192,7 +198,6 @@ gameNames() { }' | awk -F '\t' '!seen[$1]++' } -# games, games_snes, games_md, games_originals, games_cache of each Hakchi folder listed in $1 hakchiNames() { local root while IFS= read -r root; do @@ -200,28 +205,30 @@ hakchiNames() { done < "$1" | gameNames } -# menu folders, linked storage and the stock games installedNames() { local games_root games_root="$(dirname "$(findGameStorage)")" printf '%s\n' "$games_root"/*/* "$games_root"/*/.storage "$squashfs/usr/share/games" | gameNames } -# New names win and names of games that are gone are kept, so a save stays named after its -# game is removed. The cache is only rewritten when something changed. +# names are never dropped; FILE wins unless $2 = older. Folds in the alpha10 cache once. mergeNames() { local tmp="$names_cache.tmp" - mkdir -pm 777 "$(dirname "$names_cache")" || return 1 - { cat "$1"; cat "$names_cache" 2>/dev/null; } | awk -F '\t' 'NF == 2 && $2 != "" && !seen[$1]++' | sort > "$tmp" || { rm -f "$tmp"; return 1; } + { + [ "$2" = older ] || cat "$1" + cat "$names_cache" 2>/dev/null + [ "$2" = older ] && cat "$1" + cat "$old_names_cache" 2>/dev/null + } | awk -F '\t' 'NF == 2 && $2 != "" && !seen[$1]++' | sort > "$tmp" || { rm -f "$tmp"; return 1; } if cmp -s "$tmp" "$names_cache"; then rm -f "$tmp" - return 0 + else + mv -f "$tmp" "$names_cache" || return 1 fi - mv -f "$tmp" "$names_cache" || return 1 + [ -f "$old_names_cache" ] && rm -f "$old_names_cache" sync } -# runs a lookup ($@) and merges what it found; for Hakchi folders also prints how many games had a name addNames() { local found status found="$(mktemp)" @@ -267,6 +274,9 @@ case "$1" in merge-names) [ -f "$2" ] && mergeNames "$2" ;; + merge-backup-names) + mergeBackupNames + ;; sync-names) addNames installedNames ;; @@ -274,7 +284,7 @@ case "$1" in cat "$names_cache" 2>/dev/null ;; *) - echo "usage: $0 stats|orphans|deleted|move FILE|restore FILE|backup|restore-backup FILE|hakchi|names FILE|merge-names FILE|sync-names|cached-names" >&2 + echo "usage: $0 stats|orphans|deleted|move FILE|restore FILE|backup|restore-backup FILE|hakchi|names FILE|merge-names FILE|merge-backup-names|sync-names|cached-names" >&2 exit 2 ;; esac diff --git a/mod/etc/options_menu/scripts/RestoreSavesUsb.sh b/mod/etc/options_menu/scripts/RestoreSavesUsb.sh index 0e33e81..c4675a8 100644 --- a/mod/etc/options_menu/scripts/RestoreSavesUsb.sh +++ b/mod/etc/options_menu/scripts/RestoreSavesUsb.sh @@ -7,5 +7,6 @@ if [ ! -d /media/data/saves_backup ]; then exit 1 fi -rsync -a /media/data/saves_backup/ /media/hakchi/saves/ || exit 1 +rsync -a --exclude=game_names.txt /media/data/saves_backup/ /media/hakchi/saves/ || exit 1 +sh /etc/options_menu/scripts/OrphanSaves.sh merge-backup-names translate RESTORE_SAVES_USB_DONE diff --git a/mod/install b/mod/install index 1da8441..6137336 100644 --- a/mod/install +++ b/mod/install @@ -33,5 +33,6 @@ chmod +x "$rootfs/etc/options_menu/retroarch/scripts/RASettingsBackup" chmod +x "$rootfs/bin/standby" chmod +x "$rootfs/bin/standby_watchdog" rm -f "$rootfs/etc/init.d/S83standby_watchdog" +rm -f "$rootfs/etc/init.d/S8104OptionsMenuCleanup" rm -f "$rootfs/etc/options_menu/retroarch/commands/c0025_ToggleLoadScreens" return 1 diff --git a/pc/push_game_names.ps1 b/pc/push_game_names.ps1 index 0b13ea9..bdddaa2 100644 --- a/pc/push_game_names.ps1 +++ b/pc/push_game_names.ps1 @@ -8,7 +8,7 @@ Sends the game names of a Hakchi CE library to the console, so the Save Manager shows names instead of codes for saves of removed games. Read-only on both sides - except for USB:/data/hakchi_game_names; saves are never touched. + except for game_names.txt in the saves folder; saves are never touched. push_game_names.ps1 [--ip CONSOLE_IP] [--hakchi-path DIR] --ip defaults to 169.254.13.37, the console over USB. @@ -122,7 +122,6 @@ if ($gameNames.Count -eq 0) { $tmp = [IO.Path]::GetTempFileName() try { [IO.File]::WriteAllText($tmp, ($lines -join "`n") + "`n", $utf8) - # merged on the console, so names of games no longer in this library are kept $remote = "`"cat > $PushPath && sh $MergeScript merge-names $PushPath; status=`$?; rm -f $PushPath; exit `$status`"" Write-Host "Sending $($gameNames.Count) game names from $HakchiPath to $Ip..." $ssh = Start-Process -FilePath ssh -NoNewWindow -Wait -PassThru -RedirectStandardInput $tmp ` diff --git a/src/save_manager.cpp b/src/save_manager.cpp index a3dc231..54c681d 100644 --- a/src/save_manager.cpp +++ b/src/save_manager.cpp @@ -66,7 +66,6 @@ namespace bool preMarked; }; - // Clean starts with every orphan marked, the restores with none const ListInfo CleanList{ "SAVE_MANAGER_CLEAN", "SAVE_MANAGER_TO_MOVE", "SAVE_MANAGER_CLEAN_NOTE", "SAVE_MANAGER_CLEAN_CONFIRM", "HINT_MOVE", "orphans", "move ", true }; const ListInfo RestoreDeletedList{ "SAVE_MANAGER_RESTORE_DELETED", "SAVE_MANAGER_TO_RESTORE", "SAVE_MANAGER_RESTORE_NOTE", "SAVE_MANAGER_RESTORE_DELETED_CONFIRM", "HINT_RESTORE", "deleted", "restore ", false }; const ListInfo RestoreBackupList{ "RESTORE_SAVES_USB", "SAVE_MANAGER_TO_RESTORE", "SAVE_MANAGER_RESTORE_USB_NOTE", "SAVE_MANAGER_RESTORE_CONFIRM", "HINT_RESTORE", "backup", "restore-backup ", false }; @@ -701,7 +700,7 @@ int main(int, char **) { const bool fromBackup = &list == &RestoreBackupList; bool ok; - // everything marked is the full restore, which also brings back what the list doesn't show + // all marked: full restore, which also covers what the list hides if(fromBackup && markedCount == entryCount) ok = RunBusy(DrawScreen, "sh " + ScriptLocation + "RestoreSavesUsb.sh >/dev/null 2>&1"); else @@ -865,8 +864,6 @@ int main(int, char **) return -1; }; - // remembers the names of the installed games, so their saves stay named once a game is removed - system(OrphanSavesCommand("sync-names").c_str()); SetTitle("SAVE_MANAGER"); RefreshStats(); RefreshDetail(); From c7b40d772473cdf08b0de28b1b0389b4e9ecce98 Mon Sep 17 00:00:00 2001 From: DefKorns Date: Wed, 14 Oct 2026 01:10:36 +0000 Subject: [PATCH 084/104] Fix the Wi-Fi restore menu closing the Options Menu The restore commands folder is empty in the repo, so it never ships in the hmod; create it before listing backups. When no backups are left, remove the empty backup folder and go back to the Network menu instead of opening an empty list. --- mod/etc/options_menu/network/scripts/restore_settings.sh | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/mod/etc/options_menu/network/scripts/restore_settings.sh b/mod/etc/options_menu/network/scripts/restore_settings.sh index e8e2395..8727c8f 100644 --- a/mod/etc/options_menu/network/scripts/restore_settings.sh +++ b/mod/etc/options_menu/network/scripts/restore_settings.sh @@ -24,6 +24,7 @@ if [ "$1" = "nand" ]; then backup_dest="$1" fi +mkdir -p "$om_wifi_restore_cmds" find "$om_wifi_restore_cmds" -type f -name 'c0001_*' -exec rm {} \; find "$backup_path/." -mindepth 1 -maxdepth 1 -type d -print0 | xargs -0 -n 1 basename | while IFS= read -r wifi_network; do @@ -40,4 +41,10 @@ DELETE_STR=rm -rf \"$backup_path/$wifi_network\" && sh $om_network_scripts/resto DELETE_CONFIRM_KEY=DELETE_BACKUP_CONFIRM" >"$om_wifi_restore_cmds/c0001_$ssid_lower" done +if ! ls "$om_wifi_restore_cmds"/c0001_* >/dev/null 2>&1; then + rmdir "$backup_path" 2>/dev/null + OM_BACK_STACK="${OM_BACK_STACK%;*}" sh "$options_menu/scripts/om_network" + exit 0 +fi + $options_menu/options --commandPath $om_wifi_restore_cmds/ --scriptPath $om_wifi_restore_scripts --title "RESTORE_WIFI_CONFIG" & From 5e336b35869c26a72e41f5f7d22135baf823bf7c Mon Sep 17 00:00:00 2001 From: DefKorns Date: Wed, 14 Oct 2026 01:10:56 +0000 Subject: [PATCH 085/104] Remove legacy Options Menu v1 files on install Files from the 2018 Options Menu predate hakchi's install records, so no uninstall removes them and stale Back/Exit rows and old commands kept showing up. Delete them by name, and uninstall the standalone om-wifi-backup mod, now built into the Options Menu. --- mod/install | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/mod/install b/mod/install index 6137336..8968bc6 100644 --- a/mod/install +++ b/mod/install @@ -14,6 +14,7 @@ for om_unfile in "$installpath/hmod/uninstall-options_deluxe"* \ "$installpath/hmod/uninstall-options_menu" \ "$installpath/hmod/uninstall-options_menu_"* \ "$installpath/hmod/uninstall-hibernate_mod"* \ + "$installpath/hmod/uninstall-om-wifi-backup"* \ "$installpath/hmod/uninstall-Hakchi_Options_Loadscreen_toggle"; do [ -f "$om_unfile" ] && pack_uninstall "${om_unfile##*/uninstall-}" done @@ -34,5 +35,16 @@ chmod +x "$rootfs/bin/standby" chmod +x "$rootfs/bin/standby_watchdog" rm -f "$rootfs/etc/init.d/S83standby_watchdog" rm -f "$rootfs/etc/init.d/S8104OptionsMenuCleanup" -rm -f "$rootfs/etc/options_menu/retroarch/commands/c0025_ToggleLoadScreens" +for om_legacy in advanced_commands/c0020_ModUninstall advanced_commands/c0030_DisplayTemp \ + advanced_commands/c0040_DisplayTop advanced_commands/c0050_BenchMark \ + advanced_commands/c0060_DumpFile advanced_commands/c0500_USB_RW \ + advanced_commands/c0700_RADebug advanced_commands/c0890_ChangeCombo \ + advanced_commands/c0900_ClearCache advanced_commands/c9999_Back \ + advanced_commands/c9999_Exit commands/c0070_AdvancedOptions \ + commands/c0080_NetworkOptions commands/c9999_Exit \ + network/commands/c9999_Back network/commands/c9999_Exit \ + retroarch/commands/c0025_ToggleLoadScreens retroarch/commands/c0061_DeleteBackups \ + retroarch/commands/c9999_Back retroarch/commands/c9999_Exit; do + rm -f "$om_root/$om_legacy" +done return 1 From 23b8e499abd96a706dcf2d0e09d0bed7d65ba548 Mon Sep 17 00:00:00 2001 From: DefKorns Date: Wed, 14 Oct 2026 21:40:00 +0000 Subject: [PATCH 086/104] Fix B going unresponsive after a hold on screens without a hold action --- src/main.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main.cpp b/src/main.cpp index 6cb2f13..09fe7d5 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -505,9 +505,11 @@ int main(int argc, char * argv[]) MoveSelection(1); bool bHeldNow = controller.PeekButtonStatus(B); + // must run every frame: it only resets its timer when sampled while released + const unsigned int bHeldMs = controller.HeldMillis(B); if(bHeldNow) { - if(!bHoldFired && controller.HeldMillis(B) >= bHoldThresholdMs) + if(!bHoldFired && bHeldMs >= bHoldThresholdMs) { bHoldFired = true; if(!commands[currentCommandId].deleteCommand.empty() && ConfirmDelete()) From 8a6863c0a68e5bcd0369afe457236061b9188711 Mon Sep 17 00:00:00 2001 From: DefKorns Date: Wed, 14 Oct 2026 21:43:54 +0000 Subject: [PATCH 087/104] Add a shared HTTPS downloader with progress and batch mode --- Makefile | 23 +- mod/etc/options_menu/language/de-DE.lang | 1 + mod/etc/options_menu/language/en-US.lang | 1 + mod/etc/options_menu/language/es-ES.lang | 1 + mod/etc/options_menu/language/fr-FR.lang | 1 + mod/etc/options_menu/language/it-IT.lang | 1 + mod/etc/options_menu/language/ja-JP.lang | 1 + mod/etc/options_menu/language/nl-NL.lang | 1 + mod/etc/options_menu/language/pt-PT.lang | 1 + mod/etc/options_menu/language/ru-RU.lang | 1 + mod/etc/options_menu/scripts/om_functions | 42 +++ src/command.cpp | 8 + src/downloader.c | 314 ++++++++++++++++++++++ 13 files changed, 395 insertions(+), 1 deletion(-) create mode 100644 src/downloader.c diff --git a/Makefile b/Makefile index c562ad9..235dba6 100644 --- a/Makefile +++ b/Makefile @@ -11,7 +11,7 @@ VARIANT := plain endif MOD_CREATOR := CompCom, DefKorns MOD_CATEGORY := Options Menu -BINARIES := mod/etc/options_menu/options mod/etc/options_menu/optiond mod/etc/options_menu/mod_uninstall/mod_uninstall mod/etc/options_menu/save_manager/save_manager mod/bin/standby_watchdog mod/etc/options_menu/scripts/gen_splash mod/etc/options_menu/scripts/ChangeCombo +BINARIES := mod/etc/options_menu/options mod/etc/options_menu/optiond mod/etc/options_menu/mod_uninstall/mod_uninstall mod/etc/options_menu/save_manager/save_manager mod/bin/standby_watchdog mod/etc/options_menu/scripts/gen_splash mod/etc/options_menu/scripts/ChangeCombo mod/etc/options_menu/scripts/downloader CXX = g++ STRIP = strip @@ -107,6 +107,27 @@ mod/etc/options_menu/scripts/ChangeCombo: src/change_combo.o src/framework/contr $(CROSS_PREFIX)$(STRIP) $@ upx --lzma $@ +# plain C against the toolchain's static curl+OpenSSL: no libstdc++, same in both variants +CC = gcc +CURL_PREFIX = /opt/curl-static +CFLAGS = -std=gnu99 -O2 -Wall -I$(CURL_PREFIX)/include +CURL_LDLIBS = -L$(CURL_PREFIX)/lib -L/usr/lib/arm-linux-gnueabihf -lcurl -lssl -lcrypto -lz -ldl -lpthread + +src/cacert.pem: + curl -fL -o $@ https://curl.se/ca/cacert.pem + +# built from inside src/ so the symbols are _binary_cacert_pem_* +src/ca_bundle.o: src/cacert.pem + cd src && $(CROSS_PREFIX)ld -r -b binary -o ca_bundle.o cacert.pem + +mod/etc/options_menu/scripts/downloader: src/downloader.o src/ca_bundle.o + $(CROSS_PREFIX)$(CC) $^ $(CURL_LDLIBS) -o $@ + $(CROSS_PREFIX)$(STRIP) $@ + upx --lzma $@ + +%.o: %.c + $(CROSS_PREFIX)$(CC) $(CFLAGS) -c $< -o $@ + %.o: %.cpp $(CROSS_PREFIX)$(CXX) $(CXXFLAGS) -c $< -o $@ diff --git a/mod/etc/options_menu/language/de-DE.lang b/mod/etc/options_menu/language/de-DE.lang index d7924f9..5298b4f 100644 --- a/mod/etc/options_menu/language/de-DE.lang +++ b/mod/etc/options_menu/language/de-DE.lang @@ -53,6 +53,7 @@ HINT_BACK=Zurück HINT_DELETE=Löschen (Halten) NO_NET_ERR=Verbindung zum Server fehlgeschlagen. Bitte Internetverbindung prüfen und erneut versuchen. NO_PKG_ERR=Das angeforderte Paket ist derzeit nicht verfügbar. Bitte später erneut versuchen. +NOT_ENOUGH_SPACE=Nicht genügend freier Speicher, gib Speicher frei und versuche es erneut. CHANGE_COMBO_INTRO_1=Wir legen jetzt die Tastenkombination für das Optionsmenü fest. CHANGE_COMBO_INTRO_2=Du musst die 3 Tasten drücken, die für das Optionsmenü verwendet werden sollen. CHANGE_COMBO_INTRO_3=Für eine Zwei-Tasten-Kombination einfach die 2. Taste wiederholen. diff --git a/mod/etc/options_menu/language/en-US.lang b/mod/etc/options_menu/language/en-US.lang index 4083d27..6e69c39 100644 --- a/mod/etc/options_menu/language/en-US.lang +++ b/mod/etc/options_menu/language/en-US.lang @@ -54,6 +54,7 @@ HINT_BACK=Back HINT_DELETE=Delete (Hold) NO_NET_ERR=Error while connecting to server, please check your internet connection and try again. NO_PKG_ERR=The package you requested isn't available at the moment, please try again later. +NOT_ENOUGH_SPACE=Not enough free space, free some space and try again. CHANGE_COMBO_INTRO_1=We will now set your options menu combo buttons. CHANGE_COMBO_INTRO_2=You will need to press the 3 buttons you want the options menu set to. CHANGE_COMBO_INTRO_3=For a two button combo just repeat the 2nd button. diff --git a/mod/etc/options_menu/language/es-ES.lang b/mod/etc/options_menu/language/es-ES.lang index 13b1e53..880d658 100644 --- a/mod/etc/options_menu/language/es-ES.lang +++ b/mod/etc/options_menu/language/es-ES.lang @@ -53,6 +53,7 @@ HINT_BACK=Atrás HINT_DELETE=Borrar (mantener) NO_NET_ERR=Error al conectar con el servidor, comprueba tu conexión a internet e inténtalo de nuevo. NO_PKG_ERR=El paquete solicitado no está disponible en este momento, inténtalo de nuevo más tarde. +NOT_ENOUGH_SPACE=No hay suficiente espacio libre, libera espacio e inténtalo de nuevo. CHANGE_COMBO_INTRO_1=Ahora vamos a configurar la combinación de botones del menú de opciones. CHANGE_COMBO_INTRO_2=Tendrás que pulsar los 3 botones que quieras usar para el menú de opciones. CHANGE_COMBO_INTRO_3=Para una combinación de 2 botones, repite el 2º botón. diff --git a/mod/etc/options_menu/language/fr-FR.lang b/mod/etc/options_menu/language/fr-FR.lang index cb2d8fa..09ce7d5 100644 --- a/mod/etc/options_menu/language/fr-FR.lang +++ b/mod/etc/options_menu/language/fr-FR.lang @@ -53,6 +53,7 @@ HINT_BACK=Retour HINT_DELETE=Suppr. (Maintenir) NO_NET_ERR=Erreur de connexion au serveur, vérifiez votre connexion internet et réessayez. NO_PKG_ERR=Le paquet demandé n'est pas disponible pour le moment, veuillez réessayer plus tard. +NOT_ENOUGH_SPACE=Espace libre insuffisant, libérez de l'espace et réessayez. CHANGE_COMBO_INTRO_1=Nous allons maintenant définir la combinaison de boutons du menu d'options. CHANGE_COMBO_INTRO_2=Tu devras appuyer sur les 3 boutons à utiliser pour le menu d'options. CHANGE_COMBO_INTRO_3=Pour une combinaison à 2 boutons, répète simplement le 2e bouton. diff --git a/mod/etc/options_menu/language/it-IT.lang b/mod/etc/options_menu/language/it-IT.lang index 6c83891..01e9c3b 100644 --- a/mod/etc/options_menu/language/it-IT.lang +++ b/mod/etc/options_menu/language/it-IT.lang @@ -53,6 +53,7 @@ HINT_BACK=Indietro HINT_DELETE=Elimina (tieni premuto) NO_NET_ERR=Errore di connessione al server, controlla la tua connessione internet e riprova. NO_PKG_ERR=Il pacchetto richiesto non è disponibile al momento, riprova più tardi. +NOT_ENOUGH_SPACE=Spazio libero insufficiente, libera spazio e riprova. CHANGE_COMBO_INTRO_1=Ora imposteremo la combinazione di tasti del menu opzioni. CHANGE_COMBO_INTRO_2=Dovrai premere i 3 tasti che vuoi impostare per il menu opzioni. CHANGE_COMBO_INTRO_3=Per una combinazione a 2 tasti, ripeti semplicemente il 2° tasto. diff --git a/mod/etc/options_menu/language/ja-JP.lang b/mod/etc/options_menu/language/ja-JP.lang index e061e77..4d3c0fe 100644 --- a/mod/etc/options_menu/language/ja-JP.lang +++ b/mod/etc/options_menu/language/ja-JP.lang @@ -53,6 +53,7 @@ HINT_BACK=もどる HINT_DELETE=けす (ながおし) NO_NET_ERR=Serverへのせつぞくにしっぱいしました. Internetせつぞくをかくにんして, もういちどおためしください. NO_PKG_ERR=ようきゅうされたPackageは, いま, りようできません. しばらくしてから, もういちどおためしください. +NOT_ENOUGH_SPACE=あきようりょうがたりません. ようりょうをあけて, もういちどおためしください. CHANGE_COMBO_INTRO_1=これから Optionsmenuの Buttonくみあわせを せっていします CHANGE_COMBO_INTRO_2=Optionsmenuにつかう3つのButtonをおしてください CHANGE_COMBO_INTRO_3=2つのButtonくみあわせにするときは, 2ばんめのButtonをもういちどおしてください diff --git a/mod/etc/options_menu/language/nl-NL.lang b/mod/etc/options_menu/language/nl-NL.lang index bab27c1..27cdc1f 100644 --- a/mod/etc/options_menu/language/nl-NL.lang +++ b/mod/etc/options_menu/language/nl-NL.lang @@ -53,6 +53,7 @@ HINT_BACK=Terug HINT_DELETE=Wissen (vasthouden) NO_NET_ERR=Verbinding met de server mislukt, controleer je internetverbinding en probeer het opnieuw. NO_PKG_ERR=Het gevraagde pakket is momenteel niet beschikbaar, probeer het later opnieuw. +NOT_ENOUGH_SPACE=Onvoldoende vrije ruimte, maak ruimte vrij en probeer het opnieuw. CHANGE_COMBO_INTRO_1=We gaan nu de knoppencombinatie voor het optiemenu instellen. CHANGE_COMBO_INTRO_2=Je moet de 3 knoppen indrukken die je voor het optiemenu wilt instellen. CHANGE_COMBO_INTRO_3=Voor een combinatie van 2 knoppen herhaal je gewoon de 2e knop. diff --git a/mod/etc/options_menu/language/pt-PT.lang b/mod/etc/options_menu/language/pt-PT.lang index 51e8539..a7eed43 100644 --- a/mod/etc/options_menu/language/pt-PT.lang +++ b/mod/etc/options_menu/language/pt-PT.lang @@ -53,6 +53,7 @@ HINT_BACK=Voltar HINT_DELETE=Apagar (manter premido) NO_NET_ERR=Erro ao ligar ao servidor, verifica a tua ligação à internet e tenta novamente. NO_PKG_ERR=O pacote pedido não está disponível de momento, tenta novamente mais tarde. +NOT_ENOUGH_SPACE=Não há espaço livre suficiente, liberta algum espaço e tenta novamente. CHANGE_COMBO_INTRO_1=Vamos agora definir a combinação de botões do menu de opções. CHANGE_COMBO_INTRO_2=Vais precisar de premir os 3 botões que queres definir para o menu de opções. CHANGE_COMBO_INTRO_3=Para uma combinação de 2 botões, repete o 2º botão. diff --git a/mod/etc/options_menu/language/ru-RU.lang b/mod/etc/options_menu/language/ru-RU.lang index 928e788..f28277f 100644 --- a/mod/etc/options_menu/language/ru-RU.lang +++ b/mod/etc/options_menu/language/ru-RU.lang @@ -53,6 +53,7 @@ HINT_BACK=Назад HINT_DELETE=Удалить (удерж.) NO_NET_ERR=Ошибка подключения к серверу, проверьте подключение к интернету и попробуйте снова. NO_PKG_ERR=Запрошенный пакет сейчас недоступен, попробуйте позже. +NOT_ENOUGH_SPACE=Недостаточно свободного места, освободите место и повторите попытку. CHANGE_COMBO_INTRO_1=Сейчас мы настроим комбинацию кнопок для меню опций. CHANGE_COMBO_INTRO_2=Нажмите 3 кнопки, которые хотите назначить для меню опций. CHANGE_COMBO_INTRO_3=Для комбинации из 2 кнопок просто повторите 2-ю кнопку. diff --git a/mod/etc/options_menu/scripts/om_functions b/mod/etc/options_menu/scripts/om_functions index 7f15dc8..451d367 100644 --- a/mod/etc/options_menu/scripts/om_functions +++ b/mod/etc/options_menu/scripts/om_functions @@ -146,6 +146,48 @@ toggleDownloads() { fi } +# downloader exit codes: 0 ok, 1 network/server, 2 usage, 3 no space, 143 cancelled +downloadError() { + case "$1" in + 0 | 143) ;; + 3) translate NOT_ENOUGH_SPACE ;; + *) netChecker ;; + esac +} + +# omDownload [fallback_url...] +omDownload() { + local rc + "$om_scripts/downloader" --progress "$@" 2>&1 + rc=$? + downloadError "$rc" + return "$rc" +} + +# omDownloadAll [keep] +# list lines: out_pathurl[fallback_url...]; runs " " +# per item. With keep > 0 the handler must delete each file or the batch stalls. +# A handler returning 3 (no space) stops the batch. +omDownloadAll() { + local fifo="/tmp/om_download.$$" dl_pid dl_rc rc=0 item_rc item + rm -f "$fifo" + mkfifo "$fifo" || return 1 + # progress (stderr) to the screen, results (stdout) to the fifo + # shellcheck disable=SC2069 + "$om_scripts/downloader" --progress --keep "${3:-0}" --batch "$1" 2>&1 > "$fifo" & + dl_pid=$! + while read -r item_rc item; do + "$2" "$item_rc" "$item" + [ "$?" = 3 ] && rc=3 && break + done < "$fifo" + kill "$dl_pid" 2> /dev/null + wait "$dl_pid" + dl_rc=$? + [ "$dl_rc" = 3 ] && rc=3 + rm -f "$fifo" + return "$rc" +} + overlayFS() { [ -d "$1" ] && mount -t overlayfs -o lowerdir="$2",upperdir="$1" overlayfs "$2" } diff --git a/src/command.cpp b/src/command.cpp index bce0da5..ac376b3 100644 --- a/src/command.cpp +++ b/src/command.cpp @@ -202,6 +202,7 @@ void Command::RunCommand(SDL_Context & sdl_context, Controller * controller, con fcntl(fd, F_SETFL, O_NONBLOCK); const int PollTimeoutMs = 20; bool aborted = false; + bool lastWasProgress = false; while(!feof(out)) { // stdio can hold lines poll() never sees, so fgets drains every pass @@ -213,6 +214,13 @@ void Command::RunCommand(SDL_Context & sdl_context, Controller * controller, con int pos = sBuffer.find('\n'); if(pos > 0) sBuffer[pos] = '\0'; + // a leading \r rewrites the previous progress line in place + const bool isProgress = !sBuffer.empty() && sBuffer[0] == '\r'; + if(isProgress) + sBuffer.erase(0, 1); + if(isProgress && lastWasProgress && !textList.empty()) + textList.pop_back(); + lastWasProgress = isProgress; textList.push_back(Texture(sBuffer, 8, renderer, textX)); if(textList.size() > 40) diff --git a/src/downloader.c b/src/downloader.c new file mode 100644 index 0000000..91f949e --- /dev/null +++ b/src/downloader.c @@ -0,0 +1,314 @@ +/* + * Copyright (C) 2026 DefKorns + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +/* Shared HTTPS downloader for the Options Menu and its addons: BusyBox wget + * can't do the TLS GitHub requires, so this uses static curl+OpenSSL with an + * embedded CA bundle. Each URL is tried in order until one succeeds. + * + * Batch list lines are tab-separated: \t[\t...] + * Batch results go to stdout as " "; progress always goes to + * stderr as "\rI_@)zpMTtX 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 M>Y3`+>bMa82Q{j!f&c&j literal 3775 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 f&@U=#I8zaIjRjHYT?H-Wq^ZI`P3$`OV diff --git a/mod/etc/options_menu/images/preview_lang_ru.png b/mod/etc/options_menu/images/preview_lang_ru.png index da2b247f5dc8264a93374530d78171d4e8747dd5..bef63da34813b6c3943ea9e7c483903e85e5c35d 100644 GIT binary patch literal 1255 zcmeAS@N?(olHy`uVBq!ia0vp^3xK$SgAGWQF8Z0zz`(N3)5S5QV$Rz;2lGWzC60f* z|M{E)Pm-{W$;2feBerT7x+QZId}vT{*^}{u+2WIXNBVgg%L1339!^d+6%B5@F2W{j zi;hTT?YQFLy2SePjC5<^?ORkbZq@Sr{u{IHjNeS#8sqoB&*dE7!dhlyD=wX)mK|4;gX%~EgEBQLe5l@*&fN!yz4`fVM%_O}YJ%GL8V8V_Fn zniiH6V!Bznw)mQ_z~L=RqVHy3IT^`+du9Hsc}tq_zAmu4zP?PqZs%md2MhC*)9rh_@84P|(IoG2%X*tL^Z%$` z2J^cLY{yc6=(cW@<-L(nA>Oi6ev`!g97ioC7qZc+S^K%%mfiZX*hghmxSY*78#}c* zZ>`HZ)s+=|_kKFQc+cYGM_0o(3%JRJ9rQT*l7A(K0Bup0@OcgQdACAU<>V%>oA=3n zvpuugjN0miX@(M+uf#2xl>+x3lM(2QU{w~q&N#7)fqb-TM}-AXkxTW`Njfio%qX}R zC+V@emBlda;+?AMclPhLJ#@ZxtFWHKXIX~stnP1Kn!lFy1>Ts+o#YcEchXp9UXu-HGev4?YE6Awydv|#wE8aeRdF#)$ zbxD``rSrQ!K5)pHf9>jVM- zyIAeWwvK6wR%U2Rbze)W-V**x{!emfzTUsG$BnbT-`#2x8eZxn5jo?*|5v|XMn&~) zTeLA_t^LQ`9qdLmkGW0npFj8Je)R31&sP=QiH*v=9Wl>jQ-ovjPBwq@Jqrb|Pw~Ba zFR1OHP1dc|tC{WpUVFO~n7RJ`o1J&)=$sAfKTqE|GkEJ%TK&4r_D0X zTs85|4<+U;|JF|Xk<{o7bacS%bt`{Ny*Yo{y5qm+dpvzpr9WHm^Izsj!^!>iDc-ff P0*t}a)z4*}Q$iB}Gl)em literal 4697 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 diff --git a/mod/etc/options_menu/language/commands/c0010_English b/mod/etc/options_menu/language/commands/c0010_English index a62d845..cad52e5 100644 --- a/mod/etc/options_menu/language/commands/c0010_English +++ b/mod/etc/options_menu/language/commands/c0010_English @@ -3,3 +3,4 @@ COMMAND_TYPE=EXTERNAL RESTART_UI=FALSE COMMAND_STR=echo en-US > /etc/options_menu/language.cfg && usleep 50000 && %options_path%/options --commandPath %options_path%/language/commands/ --title "LANGUAGE_OPTIONS" & PREVIEW_IMAGE=/etc/options_menu/images/preview_lang_en.png +ROW_ICON=/etc/options_menu/images/flag_en_small.png diff --git a/mod/etc/options_menu/language/commands/c0020_French b/mod/etc/options_menu/language/commands/c0020_French index e67543f..470d7cc 100644 --- a/mod/etc/options_menu/language/commands/c0020_French +++ b/mod/etc/options_menu/language/commands/c0020_French @@ -3,3 +3,4 @@ COMMAND_TYPE=EXTERNAL RESTART_UI=FALSE COMMAND_STR=echo fr-FR > /etc/options_menu/language.cfg && usleep 50000 && %options_path%/options --commandPath %options_path%/language/commands/ --title "LANGUAGE_OPTIONS" & PREVIEW_IMAGE=/etc/options_menu/images/preview_lang_fr.png +ROW_ICON=/etc/options_menu/images/flag_fr_small.png diff --git a/mod/etc/options_menu/language/commands/c0030_German b/mod/etc/options_menu/language/commands/c0030_German index 1348444..25e206c 100644 --- a/mod/etc/options_menu/language/commands/c0030_German +++ b/mod/etc/options_menu/language/commands/c0030_German @@ -3,3 +3,4 @@ COMMAND_TYPE=EXTERNAL RESTART_UI=FALSE COMMAND_STR=echo de-DE > /etc/options_menu/language.cfg && usleep 50000 && %options_path%/options --commandPath %options_path%/language/commands/ --title "LANGUAGE_OPTIONS" & PREVIEW_IMAGE=/etc/options_menu/images/preview_lang_de.png +ROW_ICON=/etc/options_menu/images/flag_de_small.png diff --git a/mod/etc/options_menu/language/commands/c0040_Spanish b/mod/etc/options_menu/language/commands/c0040_Spanish index fbc3c04..ec93ed0 100644 --- a/mod/etc/options_menu/language/commands/c0040_Spanish +++ b/mod/etc/options_menu/language/commands/c0040_Spanish @@ -3,3 +3,4 @@ COMMAND_TYPE=EXTERNAL RESTART_UI=FALSE COMMAND_STR=echo es-ES > /etc/options_menu/language.cfg && usleep 50000 && %options_path%/options --commandPath %options_path%/language/commands/ --title "LANGUAGE_OPTIONS" & PREVIEW_IMAGE=/etc/options_menu/images/preview_lang_es.png +ROW_ICON=/etc/options_menu/images/flag_es_small.png diff --git a/mod/etc/options_menu/language/commands/c0050_Italian b/mod/etc/options_menu/language/commands/c0050_Italian index 7f2cbfe..21fe205 100644 --- a/mod/etc/options_menu/language/commands/c0050_Italian +++ b/mod/etc/options_menu/language/commands/c0050_Italian @@ -3,3 +3,4 @@ COMMAND_TYPE=EXTERNAL RESTART_UI=FALSE COMMAND_STR=echo it-IT > /etc/options_menu/language.cfg && usleep 50000 && %options_path%/options --commandPath %options_path%/language/commands/ --title "LANGUAGE_OPTIONS" & PREVIEW_IMAGE=/etc/options_menu/images/preview_lang_it.png +ROW_ICON=/etc/options_menu/images/flag_it_small.png diff --git a/mod/etc/options_menu/language/commands/c0060_Dutch b/mod/etc/options_menu/language/commands/c0060_Dutch index dd82dc8..5a784a9 100644 --- a/mod/etc/options_menu/language/commands/c0060_Dutch +++ b/mod/etc/options_menu/language/commands/c0060_Dutch @@ -3,3 +3,4 @@ COMMAND_TYPE=EXTERNAL RESTART_UI=FALSE COMMAND_STR=echo nl-NL > /etc/options_menu/language.cfg && usleep 50000 && %options_path%/options --commandPath %options_path%/language/commands/ --title "LANGUAGE_OPTIONS" & PREVIEW_IMAGE=/etc/options_menu/images/preview_lang_nl.png +ROW_ICON=/etc/options_menu/images/flag_nl_small.png diff --git a/mod/etc/options_menu/language/commands/c0070_Portuguese b/mod/etc/options_menu/language/commands/c0070_Portuguese index fbfd375..e76ee89 100644 --- a/mod/etc/options_menu/language/commands/c0070_Portuguese +++ b/mod/etc/options_menu/language/commands/c0070_Portuguese @@ -3,3 +3,4 @@ COMMAND_TYPE=EXTERNAL RESTART_UI=FALSE COMMAND_STR=echo pt-PT > /etc/options_menu/language.cfg && usleep 50000 && %options_path%/options --commandPath %options_path%/language/commands/ --title "LANGUAGE_OPTIONS" & PREVIEW_IMAGE=/etc/options_menu/images/preview_lang_pt.png +ROW_ICON=/etc/options_menu/images/flag_pt_small.png diff --git a/mod/etc/options_menu/language/commands/c0080_Russian b/mod/etc/options_menu/language/commands/c0080_Russian index 88002e4..3161462 100644 --- a/mod/etc/options_menu/language/commands/c0080_Russian +++ b/mod/etc/options_menu/language/commands/c0080_Russian @@ -3,3 +3,4 @@ COMMAND_TYPE=EXTERNAL RESTART_UI=FALSE COMMAND_STR=echo ru-RU > /etc/options_menu/language.cfg && usleep 50000 && %options_path%/options --commandPath %options_path%/language/commands/ --title "LANGUAGE_OPTIONS" & PREVIEW_IMAGE=/etc/options_menu/images/preview_lang_ru.png +ROW_ICON=/etc/options_menu/images/flag_ru_small.png diff --git a/mod/etc/options_menu/language/commands/c0090_Japanese b/mod/etc/options_menu/language/commands/c0090_Japanese index 2139390..b907b0a 100644 --- a/mod/etc/options_menu/language/commands/c0090_Japanese +++ b/mod/etc/options_menu/language/commands/c0090_Japanese @@ -3,3 +3,4 @@ COMMAND_TYPE=EXTERNAL RESTART_UI=FALSE COMMAND_STR=echo ja-JP > /etc/options_menu/language.cfg && usleep 50000 && %options_path%/options --commandPath %options_path%/language/commands/ --title "LANGUAGE_OPTIONS" & PREVIEW_IMAGE=/etc/options_menu/images/preview_lang_ja.png +ROW_ICON=/etc/options_menu/images/flag_ja_small.png diff --git a/src/command.cpp b/src/command.cpp index 5cf1c11..d6b65e3 100644 --- a/src/command.cpp +++ b/src/command.cpp @@ -126,6 +126,8 @@ Command::Command(std::ifstream & in) usbOnly = (value == "TRUE"); else if(param.compare("CHILD")==0) child = (value == "TRUE"); + else if(param.compare("ROW_ICON")==0) + rowIcon = value; else if(param.compare("PREVIEW_IMAGE")==0) previewImage = value; else if(param.compare("PREVIEW_IMAGE_X")==0) diff --git a/src/command.h b/src/command.h index de69c7b..1af0c69 100644 --- a/src/command.h +++ b/src/command.h @@ -46,6 +46,8 @@ struct Command bool hasSubmenu = false; Texture texture; std::string previewImage; + std::string rowIcon; + Texture rowIconTexture; int previewImageX = 920; int previewImageY = 300; int previewImageWidth = -1; diff --git a/src/main.cpp b/src/main.cpp index 270a256..b1fb820 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -303,6 +303,8 @@ int main(int argc, char * argv[]) } c.texture = Texture(label, RowGlyphSize, renderer, textX, 0, false, ToAbgr(UiTheme::Text), true); + if(!c.rowIcon.empty()) + c.rowIconTexture = Texture(c.rowIcon, renderer, 0, 0); } const int modernRowPitch = std::max(UiTheme::RowPitch, GetTTFLineHeight(RowGlyphSize)); @@ -424,6 +426,13 @@ int main(int argc, char * argv[]) rowSwitch.rect.y = rowCommand.texture.rect.y + (rowCommand.texture.rect.h - UiTheme::SwitchH) / 2; rowSwitch.Draw(renderer); } + else if(rowCommand.rowIconTexture.texture) + { + Texture & icon = rowCommand.rowIconTexture; + icon.rect.x = UiTheme::RowControlRightX - icon.rect.w; + icon.rect.y = rowCommand.texture.rect.y + (rowCommand.texture.rect.h - icon.rect.h) / 2; + icon.Draw(renderer); + } else if(rowCommand.hasSubmenu) { chevronIcon.rect.x = UiTheme::RowControlRightX - chevronIcon.rect.w; From 59af8296a5423fda843c970ead99440be80a842e Mon Sep 17 00:00:00 2001 From: DefKorns Date: Tue, 20 Oct 2026 01:41:24 +0000 Subject: [PATCH 102/104] Take the UI font from the toolchain image instead of tracking it --- .gitignore | 1 + Makefile | 8 + font-build/.gitignore | 8 - font-build/kanji_codepoints.txt | 2965 ----------------- font-build/unicodes-latin.txt | 1 - font-build/unicodes.txt | 1 - .../fonts/NotoSansJP-Latin-subset.ttf | Bin 43040 -> 0 bytes 7 files changed, 9 insertions(+), 2975 deletions(-) delete mode 100644 font-build/.gitignore delete mode 100644 font-build/kanji_codepoints.txt delete mode 100644 font-build/unicodes-latin.txt delete mode 100644 font-build/unicodes.txt delete mode 100644 mod/etc/options_menu/fonts/NotoSansJP-Latin-subset.ttf diff --git a/.gitignore b/.gitignore index 9c03429..31482e3 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,4 @@ mod/etc/options_menu/save_manager/save_manager mod/etc/options_menu/scripts/ChangeCombo mod/etc/options_menu/scripts/gen_splash mod/etc/options_menu/scripts/downloader +mod/etc/options_menu/fonts/NotoSansJP-Latin-subset.ttf diff --git a/Makefile b/Makefile index e496599..df4fb70 100644 --- a/Makefile +++ b/Makefile @@ -46,6 +46,10 @@ ifndef FIX_LIBSTDCXX MOD_DEPS += no-private-libstdcxx endif +# built into the toolchain image from Noto Sans JP (see its README), not tracked +FONT_LATIN := mod/etc/options_menu/fonts/NotoSansJP-Latin-subset.ttf +MOD_DEPS += $(FONT_LATIN) + all: hmod compile: $(BINARIES) $(VENDORED_LIBS) @@ -68,6 +72,10 @@ mod/etc/options_menu/options: $(OBJECTS) mod/etc/options_menu/lib: mkdir -p $@ +# always copied, so a new image can't leave a stale font behind +$(FONT_LATIN): FORCE + cp /opt/options-menu/fonts/NotoSansJP-Latin-subset.ttf $@ + # built from source in the toolchain image, copied in each build mod/etc/options_menu/lib/libSDL2_ttf-2.0.so.0: | mod/etc/options_menu/lib cp /usr/lib/arm-linux-gnueabihf/libSDL2_ttf-2.0.so.0 $@ diff --git a/font-build/.gitignore b/font-build/.gitignore deleted file mode 100644 index 9fb8544..0000000 --- a/font-build/.gitignore +++ /dev/null @@ -1,8 +0,0 @@ -# regenerable font binaries -*.ttf - -# 512_8 reference dump, already mined into font8x8_cyrillic.h -512_8_* - -# console-extracted, not ours to redistribute -lib-staging/ diff --git a/font-build/kanji_codepoints.txt b/font-build/kanji_codepoints.txt deleted file mode 100644 index 8b7f31b..0000000 --- a/font-build/kanji_codepoints.txt +++ /dev/null @@ -1,2965 +0,0 @@ -0x4E00 -0x4E01 -0x4E03 -0x4E07 -0x4E08 -0x4E09 -0x4E0A -0x4E0B -0x4E0D -0x4E0E -0x4E11 -0x4E14 -0x4E16 -0x4E18 -0x4E19 -0x4E1E -0x4E21 -0x4E26 -0x4E2D -0x4E32 -0x4E38 -0x4E39 -0x4E3B -0x4E43 -0x4E45 -0x4E4B -0x4E4D -0x4E4E -0x4E4F -0x4E57 -0x4E59 -0x4E5D -0x4E5E -0x4E5F -0x4E71 -0x4E73 -0x4E7E -0x4E80 -0x4E86 -0x4E88 -0x4E89 -0x4E8B -0x4E8C -0x4E91 -0x4E92 -0x4E94 -0x4E95 -0x4E98 -0x4E99 -0x4E9B -0x4E9C -0x4EA1 -0x4EA4 -0x4EA5 -0x4EA6 -0x4EA8 -0x4EAB -0x4EAC -0x4EAD -0x4EAE -0x4EBA -0x4EC0 -0x4EC1 -0x4EC7 -0x4ECA -0x4ECB -0x4ECF -0x4ED4 -0x4ED5 -0x4ED6 -0x4ED8 -0x4ED9 -0x4EE3 -0x4EE4 -0x4EE5 -0x4EEE -0x4EF0 -0x4EF2 -0x4EF6 -0x4EFB -0x4F01 -0x4F0A -0x4F0D -0x4F0E -0x4F0F -0x4F10 -0x4F11 -0x4F1A -0x4F1D -0x4F2F -0x4F34 -0x4F36 -0x4F38 -0x4F3A -0x4F3C -0x4F3D -0x4F43 -0x4F46 -0x4F4D -0x4F4E -0x4F4F -0x4F50 -0x4F51 -0x4F53 -0x4F55 -0x4F59 -0x4F5C -0x4F73 -0x4F75 -0x4F7C -0x4F7F -0x4F83 -0x4F8B -0x4F8D -0x4F9B -0x4F9D -0x4FA0 -0x4FA1 -0x4FAD -0x4FAE -0x4FAF -0x4FB5 -0x4FB6 -0x4FBF -0x4FC2 -0x4FC3 -0x4FC4 -0x4FCA -0x4FD7 -0x4FDD -0x4FE1 -0x4FE3 -0x4FEE -0x4FF3 -0x4FF5 -0x4FF8 -0x4FFA -0x5009 -0x500B -0x500D -0x5012 -0x5016 -0x5019 -0x501F -0x5023 -0x5024 -0x5026 -0x502B -0x502D -0x5036 -0x5039 -0x5049 -0x504F -0x505C -0x5065 -0x5072 -0x5074 -0x5075 -0x5076 -0x507D -0x508D -0x5091 -0x5098 -0x5099 -0x50AC -0x50AD -0x50B5 -0x50B7 -0x50BE -0x50C5 -0x50CD -0x50CF -0x50D1 -0x50D5 -0x50DA -0x50E7 -0x50FB -0x5100 -0x5104 -0x5112 -0x511F -0x512A -0x5132 -0x5141 -0x5143 -0x5144 -0x5145 -0x5146 -0x5147 -0x5148 -0x5149 -0x514B -0x514D -0x514E -0x5150 -0x515A -0x515C -0x5165 -0x5168 -0x516B -0x516C -0x516D -0x5171 -0x5175 -0x5176 -0x5177 -0x5178 -0x517C -0x5185 -0x5186 -0x518A -0x518D -0x5192 -0x5197 -0x5199 -0x51A0 -0x51A5 -0x51A8 -0x51AC -0x51B4 -0x51B6 -0x51B7 -0x51C4 -0x51C6 -0x51CB -0x51CC -0x51CD -0x51DD -0x51E1 -0x51E6 -0x51E7 -0x51EA -0x51F1 -0x51F6 -0x51F8 -0x51F9 -0x51FA -0x51FD -0x5200 -0x5203 -0x5206 -0x5207 -0x5208 -0x520A -0x5211 -0x5217 -0x521D -0x5224 -0x5225 -0x5229 -0x5230 -0x5236 -0x5237 -0x5238 -0x523A -0x523B -0x5243 -0x5247 -0x524A -0x524D -0x5256 -0x525B -0x5263 -0x5264 -0x5265 -0x526F -0x5270 -0x5272 -0x5275 -0x5283 -0x5287 -0x5289 -0x529B -0x529F -0x52A0 -0x52A3 -0x52A9 -0x52AA -0x52AB -0x52B1 -0x52B4 -0x52B9 -0x52BE -0x52C3 -0x52C5 -0x52C7 -0x52C9 -0x52D5 -0x52D8 -0x52D9 -0x52DD -0x52DF -0x52E2 -0x52E4 -0x52E7 -0x52F2 -0x52FA -0x52FE -0x52FF -0x5301 -0x5302 -0x5305 -0x5316 -0x5317 -0x5319 -0x531D -0x5320 -0x5321 -0x532A -0x5339 -0x533A -0x533B -0x533F -0x5341 -0x5343 -0x5347 -0x5348 -0x534A -0x5351 -0x5352 -0x5353 -0x5354 -0x5357 -0x5358 -0x535A -0x535C -0x5360 -0x5366 -0x536F -0x5370 -0x5371 -0x5373 -0x5374 -0x5375 -0x5378 -0x537F -0x5384 -0x5398 -0x539A -0x539F -0x53A8 -0x53A9 -0x53AD -0x53B3 -0x53BB -0x53C2 -0x53C8 -0x53C9 -0x53CA -0x53CB -0x53CC -0x53CD -0x53CE -0x53D4 -0x53D6 -0x53D7 -0x53D9 -0x53DB -0x53E1 -0x53E2 -0x53E3 -0x53E4 -0x53E5 -0x53E9 -0x53EA -0x53EB -0x53EC -0x53EF -0x53F0 -0x53F1 -0x53F2 -0x53F3 -0x53F6 -0x53F7 -0x53F8 -0x5403 -0x5404 -0x5408 -0x5409 -0x540A -0x540B -0x540C -0x540D -0x540E -0x540F -0x5410 -0x5411 -0x541B -0x541F -0x5420 -0x5426 -0x542B -0x5438 -0x5439 -0x543B -0x543E -0x5442 -0x5446 -0x5448 -0x5449 -0x544A -0x5451 -0x5468 -0x546A -0x5473 -0x547C -0x547D -0x548B -0x548C -0x54B2 -0x54B3 -0x54BD -0x54C0 -0x54C1 -0x54C9 -0x54E1 -0x54E8 -0x54E9 -0x54F2 -0x5504 -0x5506 -0x5507 -0x5510 -0x5516 -0x552F -0x5531 -0x553E -0x5544 -0x5546 -0x554F -0x5553 -0x5584 -0x5589 -0x558B -0x559A -0x559C -0x559D -0x55A7 -0x55AA -0x55AB -0x55AC -0x55B0 -0x55B6 -0x55E3 -0x5606 -0x5609 -0x5617 -0x5618 -0x5629 -0x5631 -0x5642 -0x564C -0x565B -0x5668 -0x5674 -0x5678 -0x567A -0x5687 -0x56A2 -0x56DA -0x56DB -0x56DE -0x56E0 -0x56E3 -0x56F0 -0x56F2 -0x56F3 -0x56FA -0x56FD -0x5703 -0x570F -0x5712 -0x571F -0x5727 -0x5728 -0x572D -0x5730 -0x5742 -0x5747 -0x574A -0x5750 -0x5751 -0x5764 -0x5766 -0x576A -0x5782 -0x578B -0x57A2 -0x57A3 -0x57CB -0x57CE -0x57DC -0x57DF -0x57E0 -0x57F4 -0x57F7 -0x57F9 -0x57FA -0x57FC -0x5800 -0x5802 -0x5805 -0x5806 -0x5815 -0x5824 -0x582A -0x5830 -0x5831 -0x5834 -0x5835 -0x583A -0x5840 -0x5841 -0x584A -0x5851 -0x5854 -0x5857 -0x5858 -0x5859 -0x585A -0x585E -0x5869 -0x586B -0x5875 -0x587E -0x5883 -0x5893 -0x5897 -0x589C -0x58A8 -0x58B3 -0x58BE -0x58C1 -0x58C7 -0x58CA -0x58CC -0x58D5 -0x58EB -0x58EC -0x58EE -0x58F0 -0x58F1 -0x58F2 -0x58F7 -0x5909 -0x590F -0x5915 -0x5916 -0x5919 -0x591A -0x591C -0x5922 -0x5927 -0x5929 -0x592A -0x592B -0x592E -0x5931 -0x5937 -0x5944 -0x5947 -0x5948 -0x5949 -0x594F -0x5951 -0x5954 -0x5957 -0x5965 -0x5968 -0x596A -0x596E -0x5973 -0x5974 -0x597D -0x5982 -0x5983 -0x5984 -0x598A -0x5993 -0x5996 -0x5999 -0x59A5 -0x59A8 -0x59AC -0x59B9 -0x59BB -0x59BE -0x59C9 -0x59CB -0x59D0 -0x59D1 -0x59D3 -0x59D4 -0x59E5 -0x59E6 -0x59EA -0x59EB -0x59F6 -0x59FB -0x59FF -0x5A01 -0x5A03 -0x5A18 -0x5A20 -0x5A29 -0x5A2F -0x5A3C -0x5A41 -0x5A46 -0x5A5A -0x5A66 -0x5A7F -0x5A92 -0x5A9B -0x5AC1 -0x5AC9 -0x5ACC -0x5AE1 -0x5B09 -0x5B22 -0x5B2C -0x5B30 -0x5B50 -0x5B54 -0x5B57 -0x5B58 -0x5B5C -0x5B5D -0x5B5F -0x5B63 -0x5B64 -0x5B66 -0x5B6B -0x5B85 -0x5B87 -0x5B88 -0x5B89 -0x5B8B -0x5B8C -0x5B8D -0x5B8F -0x5B95 -0x5B97 -0x5B98 -0x5B99 -0x5B9A -0x5B9B -0x5B9C -0x5B9D -0x5B9F -0x5BA2 -0x5BA3 -0x5BA4 -0x5BA5 -0x5BAE -0x5BB0 -0x5BB3 -0x5BB4 -0x5BB5 -0x5BB6 -0x5BB9 -0x5BBF -0x5BC2 -0x5BC4 -0x5BC5 -0x5BC6 -0x5BCC -0x5BD2 -0x5BD3 -0x5BDB -0x5BDD -0x5BDF -0x5BE1 -0x5BE7 -0x5BE9 -0x5BEE -0x5BF5 -0x5BF8 -0x5BFA -0x5BFE -0x5BFF -0x5C01 -0x5C02 -0x5C04 -0x5C06 -0x5C09 -0x5C0A -0x5C0B -0x5C0E -0x5C0F -0x5C11 -0x5C16 -0x5C1A -0x5C24 -0x5C2D -0x5C31 -0x5C3A -0x5C3B -0x5C3C -0x5C3D -0x5C3E -0x5C3F -0x5C40 -0x5C45 -0x5C48 -0x5C4A -0x5C4B -0x5C4D -0x5C51 -0x5C55 -0x5C5E -0x5C60 -0x5C61 -0x5C64 -0x5C65 -0x5C6F -0x5C71 -0x5C90 -0x5CA1 -0x5CA8 -0x5CA9 -0x5CAC -0x5CB1 -0x5CB3 -0x5CB8 -0x5CE0 -0x5CE1 -0x5CE8 -0x5CEF -0x5CF0 -0x5CF6 -0x5CFB -0x5D07 -0x5D0E -0x5D16 -0x5D29 -0x5D50 -0x5D69 -0x5D6F -0x5D8B -0x5DBA -0x5DCC -0x5DDD -0x5DDE -0x5DE1 -0x5DE3 -0x5DE5 -0x5DE6 -0x5DE7 -0x5DE8 -0x5DEE -0x5DF1 -0x5DF3 -0x5DF4 -0x5DF7 -0x5DFB -0x5DFD -0x5DFE -0x5E02 -0x5E03 -0x5E06 -0x5E0C -0x5E16 -0x5E1D -0x5E25 -0x5E2B -0x5E2D -0x5E2F -0x5E30 -0x5E33 -0x5E38 -0x5E3D -0x5E45 -0x5E4C -0x5E55 -0x5E61 -0x5E63 -0x5E72 -0x5E73 -0x5E74 -0x5E78 -0x5E79 -0x5E7B -0x5E7C -0x5E7D -0x5E7E -0x5E81 -0x5E83 -0x5E84 -0x5E87 -0x5E8A -0x5E8F -0x5E95 -0x5E96 -0x5E97 -0x5E9A -0x5E9C -0x5EA6 -0x5EA7 -0x5EAB -0x5EAD -0x5EB5 -0x5EB6 -0x5EB7 -0x5EB8 -0x5EC3 -0x5EC9 -0x5ECA -0x5ED3 -0x5EDF -0x5EE0 -0x5EF6 -0x5EF7 -0x5EFA -0x5EFB -0x5EFC -0x5EFF -0x5F01 -0x5F04 -0x5F0A -0x5F0F -0x5F10 -0x5F13 -0x5F14 -0x5F15 -0x5F17 -0x5F18 -0x5F1B -0x5F1F -0x5F25 -0x5F26 -0x5F27 -0x5F31 -0x5F35 -0x5F37 -0x5F3C -0x5F3E -0x5F4A -0x5F53 -0x5F62 -0x5F66 -0x5F69 -0x5F6A -0x5F6B -0x5F6C -0x5F70 -0x5F71 -0x5F79 -0x5F7C -0x5F80 -0x5F81 -0x5F84 -0x5F85 -0x5F8B -0x5F8C -0x5F90 -0x5F92 -0x5F93 -0x5F97 -0x5FA1 -0x5FA9 -0x5FAA -0x5FAE -0x5FB3 -0x5FB4 -0x5FB9 -0x5FBD -0x5FC3 -0x5FC5 -0x5FCC -0x5FCD -0x5FD7 -0x5FD8 -0x5FD9 -0x5FDC -0x5FE0 -0x5FEB -0x5FF5 -0x5FFD -0x6012 -0x6016 -0x601C -0x601D -0x6020 -0x6025 -0x6027 -0x6028 -0x602A -0x602F -0x604B -0x6050 -0x6052 -0x6055 -0x6062 -0x6065 -0x6068 -0x6069 -0x606D -0x606F -0x6070 -0x6075 -0x6089 -0x608C -0x6094 -0x609F -0x60A0 -0x60A3 -0x60A6 -0x60A9 -0x60AA -0x60B2 -0x60B6 -0x60BC -0x60C5 -0x60C7 -0x60D1 -0x60DA -0x60DC -0x60DF -0x60E3 -0x60E8 -0x60F0 -0x60F3 -0x60F9 -0x6101 -0x6108 -0x6109 -0x610F -0x611A -0x611B -0x611F -0x6148 -0x614B -0x614C -0x614E -0x6155 -0x6162 -0x6163 -0x6167 -0x6168 -0x616E -0x6170 -0x6176 -0x617E -0x6182 -0x618E -0x6190 -0x61A4 -0x61A7 -0x61A9 -0x61B2 -0x61B6 -0x61BE -0x61C7 -0x61D0 -0x61F2 -0x61F8 -0x620A -0x620E -0x6210 -0x6211 -0x6212 -0x6216 -0x621A -0x621F -0x6226 -0x622F -0x6234 -0x6238 -0x623B -0x623F -0x6240 -0x6247 -0x6249 -0x624B -0x624D -0x6253 -0x6255 -0x6258 -0x626E -0x6271 -0x6276 -0x6279 -0x627F -0x6280 -0x6284 -0x628A -0x6291 -0x6295 -0x6297 -0x6298 -0x629C -0x629E -0x62AB -0x62B1 -0x62B5 -0x62B9 -0x62BC -0x62BD -0x62C5 -0x62CD -0x62D0 -0x62D2 -0x62D3 -0x62D8 -0x62D9 -0x62DB -0x62DD -0x62E0 -0x62E1 -0x62EC -0x62ED -0x62F3 -0x62F6 -0x62F7 -0x62FE -0x6301 -0x6307 -0x6309 -0x6311 -0x6319 -0x631F -0x6328 -0x632B -0x632F -0x633A -0x633D -0x633F -0x6349 -0x634C -0x6355 -0x6357 -0x635C -0x6367 -0x6368 -0x636E -0x6372 -0x6377 -0x637A -0x637B -0x6383 -0x6388 -0x638C -0x6392 -0x6398 -0x639B -0x63A0 -0x63A1 -0x63A2 -0x63A5 -0x63A7 -0x63A8 -0x63A9 -0x63AA -0x63AC -0x63B2 -0x63B4 -0x63BB -0x63C3 -0x63CF -0x63D0 -0x63D6 -0x63DA -0x63DB -0x63E1 -0x63EE -0x63F4 -0x63FA -0x640D -0x642C -0x642D -0x643A -0x643E -0x6442 -0x6458 -0x6469 -0x6478 -0x647A -0x6483 -0x6492 -0x649A -0x649E -0x64A4 -0x64AB -0x64AD -0x64AE -0x64B0 -0x64B2 -0x64B9 -0x64C1 -0x64CD -0x64E2 -0x64E6 -0x64EC -0x64FE -0x652F -0x6539 -0x653B -0x653E -0x653F -0x6545 -0x654F -0x6551 -0x6557 -0x6559 -0x6562 -0x6563 -0x6566 -0x656C -0x6570 -0x6574 -0x6575 -0x6577 -0x6587 -0x6589 -0x658C -0x658E -0x6590 -0x6591 -0x6597 -0x6599 -0x659C -0x65A1 -0x65A4 -0x65A5 -0x65A7 -0x65AC -0x65AD -0x65AF -0x65B0 -0x65B9 -0x65BC -0x65BD -0x65C5 -0x65CB -0x65CF -0x65D7 -0x65E2 -0x65E5 -0x65E6 -0x65E7 -0x65E8 -0x65E9 -0x65EC -0x65ED -0x65FA -0x6602 -0x6606 -0x6607 -0x660C -0x660E -0x660F -0x6613 -0x6614 -0x661F -0x6620 -0x6625 -0x6627 -0x6628 -0x662D -0x662F -0x663C -0x6642 -0x6643 -0x664B -0x6652 -0x6666 -0x6669 -0x666E -0x666F -0x6674 -0x6676 -0x667A -0x6681 -0x6687 -0x6691 -0x6696 -0x6697 -0x66A2 -0x66A6 -0x66AB -0x66AE -0x66B4 -0x66C7 -0x66D9 -0x66DC -0x66DD -0x66F2 -0x66F3 -0x66F4 -0x66F8 -0x66F9 -0x66FD -0x66FE -0x66FF -0x6700 -0x6708 -0x6709 -0x670B -0x670D -0x6714 -0x6715 -0x6717 -0x671B -0x671D -0x671F -0x6728 -0x672A -0x672B -0x672C -0x672D -0x6731 -0x6734 -0x673A -0x673D -0x6749 -0x674E -0x674F -0x6750 -0x6751 -0x6753 -0x6756 -0x675C -0x675F -0x6761 -0x6762 -0x6765 -0x676D -0x676F -0x6771 -0x6775 -0x6777 -0x677E -0x677F -0x6787 -0x6790 -0x6795 -0x6797 -0x679A -0x679C -0x679D -0x67A0 -0x67A2 -0x67AF -0x67B6 -0x67C1 -0x67C4 -0x67CA -0x67CF -0x67D0 -0x67D1 -0x67D3 -0x67D4 -0x67D8 -0x67DA -0x67F1 -0x67F3 -0x67F4 -0x67F5 -0x67FB -0x67FE -0x67FF -0x6802 -0x6803 -0x6804 -0x6813 -0x6816 -0x6817 -0x6821 -0x6822 -0x682A -0x6834 -0x6838 -0x6839 -0x683C -0x683D -0x6841 -0x6842 -0x6843 -0x6848 -0x6850 -0x6851 -0x6853 -0x6854 -0x685C -0x685D -0x685F -0x6867 -0x6876 -0x6881 -0x6885 -0x6893 -0x6897 -0x68A2 -0x68A7 -0x68A8 -0x68AF -0x68B0 -0x68B1 -0x68B6 -0x68BC -0x68C4 -0x68C9 -0x68CB -0x68D2 -0x68DA -0x68DF -0x68EE -0x68F2 -0x68FA -0x6900 -0x6905 -0x690B -0x690D -0x690E -0x6919 -0x691B -0x691C -0x6934 -0x693F -0x694A -0x6953 -0x6955 -0x695A -0x6960 -0x6962 -0x696D -0x696F -0x6973 -0x6975 -0x697C -0x697D -0x6982 -0x698A -0x698E -0x6994 -0x699B -0x69CB -0x69CC -0x69CD -0x69D8 -0x69D9 -0x69FB -0x69FD -0x6A0B -0x6A17 -0x6A19 -0x6A1F -0x6A21 -0x6A29 -0x6A2A -0x6A2B -0x6A35 -0x6A39 -0x6A3A -0x6A3D -0x6A4B -0x6A58 -0x6A5F -0x6A61 -0x6A7F -0x6A80 -0x6A8E -0x6AD3 -0x6ADB -0x6AE8 -0x6B04 -0x6B1D -0x6B20 -0x6B21 -0x6B23 -0x6B27 -0x6B32 -0x6B3A -0x6B3D -0x6B3E -0x6B4C -0x6B4E -0x6B53 -0x6B62 -0x6B63 -0x6B64 -0x6B66 -0x6B69 -0x6B6A -0x6B6F -0x6B73 -0x6B74 -0x6B7B -0x6B86 -0x6B89 -0x6B8A -0x6B8B -0x6B96 -0x6BB4 -0x6BB5 -0x6BBA -0x6BBB -0x6BBF -0x6BC5 -0x6BCD -0x6BCE -0x6BD2 -0x6BD4 -0x6BD8 -0x6BDB -0x6C0F -0x6C11 -0x6C17 -0x6C34 -0x6C37 -0x6C38 -0x6C3E -0x6C40 -0x6C41 -0x6C42 -0x6C4E -0x6C50 -0x6C57 -0x6C5A -0x6C5D -0x6C5F -0x6C60 -0x6C70 -0x6C72 -0x6C7A -0x6C7D -0x6C83 -0x6C88 -0x6C8C -0x6C93 -0x6C96 -0x6C99 -0x6CA1 -0x6CA2 -0x6CAB -0x6CB3 -0x6CB8 -0x6CB9 -0x6CBB -0x6CBC -0x6CBF -0x6CC1 -0x6CC9 -0x6CCA -0x6CCC -0x6CD5 -0x6CE1 -0x6CE2 -0x6CE3 -0x6CE5 -0x6CE8 -0x6CF0 -0x6CF3 -0x6D0B -0x6D17 -0x6D1B -0x6D1E -0x6D25 -0x6D29 -0x6D2A -0x6D32 -0x6D3B -0x6D3E -0x6D41 -0x6D44 -0x6D45 -0x6D5C -0x6D66 -0x6D69 -0x6D6A -0x6D6C -0x6D6E -0x6D74 -0x6D77 -0x6D78 -0x6D88 -0x6D8C -0x6D99 -0x6D9B -0x6D9C -0x6DAF -0x6DB2 -0x6DBC -0x6DC0 -0x6DCB -0x6DD1 -0x6DD8 -0x6DE1 -0x6DEB -0x6DF1 -0x6DF3 -0x6DF5 -0x6DF7 -0x6DFB -0x6E05 -0x6E07 -0x6E08 -0x6E09 -0x6E0B -0x6E13 -0x6E1A -0x6E1B -0x6E20 -0x6E21 -0x6E25 -0x6E26 -0x6E29 -0x6E2C -0x6E2F -0x6E4A -0x6E56 -0x6E58 -0x6E5B -0x6E67 -0x6E6F -0x6E7E -0x6E7F -0x6E80 -0x6E8C -0x6E90 -0x6E96 -0x6E9C -0x6E9D -0x6EA2 -0x6EB6 -0x6EBA -0x6EC5 -0x6ECB -0x6ED1 -0x6EDD -0x6EDE -0x6EF4 -0x6F01 -0x6F02 -0x6F06 -0x6F09 -0x6F0F -0x6F14 -0x6F15 -0x6F20 -0x6F22 -0x6F23 -0x6F2B -0x6F2C -0x6F38 -0x6F45 -0x6F54 -0x6F5C -0x6F5F -0x6F64 -0x6F6E -0x6F70 -0x6F84 -0x6F97 -0x6FB1 -0x6FC0 -0x6FC1 -0x6FC3 -0x6FE0 -0x6FE1 -0x6FEB -0x6FEF -0x7015 -0x701E -0x7026 -0x7027 -0x702C -0x7058 -0x706B -0x706F -0x7070 -0x7078 -0x707C -0x707D -0x7089 -0x708A -0x708E -0x70AD -0x70B9 -0x70BA -0x70C8 -0x70CF -0x70F9 -0x7114 -0x711A -0x7121 -0x7126 -0x7136 -0x713C -0x7149 -0x714E -0x7159 -0x7164 -0x7167 -0x7169 -0x716E -0x717D -0x718A -0x7194 -0x719F -0x71B1 -0x71C3 -0x71C8 -0x71D0 -0x71D5 -0x71E5 -0x71E6 -0x71ED -0x7206 -0x722A -0x7235 -0x7236 -0x723A -0x723D -0x723E -0x7247 -0x7248 -0x724C -0x7252 -0x7259 -0x725B -0x725D -0x725F -0x7261 -0x7262 -0x7267 -0x7269 -0x7272 -0x7279 -0x727D -0x7280 -0x72A0 -0x72AC -0x72AF -0x72B6 -0x72C2 -0x72D0 -0x72D7 -0x72D9 -0x72DB -0x72E9 -0x72EC -0x72ED -0x72F8 -0x72FC -0x72FD -0x731B -0x731F -0x732A -0x732B -0x732E -0x7336 -0x7337 -0x733F -0x7344 -0x7345 -0x7363 -0x7372 -0x7384 -0x7387 -0x7389 -0x738B -0x7396 -0x73A9 -0x73B2 -0x73C2 -0x73CA -0x73CD -0x73E0 -0x73EA -0x73ED -0x73FE -0x7403 -0x7406 -0x7409 -0x7422 -0x7433 -0x7434 -0x7435 -0x7436 -0x745A -0x745B -0x745E -0x7460 -0x7473 -0x7483 -0x74B0 -0x74BD -0x74DC -0x74E2 -0x74E6 -0x74F6 -0x7511 -0x7518 -0x751A -0x751C -0x751F -0x7523 -0x7525 -0x7528 -0x752B -0x7530 -0x7531 -0x7532 -0x7533 -0x7537 -0x753A -0x753B -0x754C -0x754F -0x7551 -0x7554 -0x7559 -0x755C -0x755D -0x7560 -0x7562 -0x7565 -0x7566 -0x756A -0x7570 -0x7573 -0x7577 -0x757F -0x758B -0x758E -0x758F -0x7591 -0x75AB -0x75B2 -0x75B9 -0x75BE -0x75C5 -0x75C7 -0x75D4 -0x75D5 -0x75D8 -0x75DB -0x75E2 -0x75E9 -0x75F4 -0x7642 -0x764C -0x7652 -0x7656 -0x767A -0x767B -0x767D -0x767E -0x7684 -0x7686 -0x7687 -0x7690 -0x76AE -0x76BF -0x76C3 -0x76C6 -0x76C8 -0x76CA -0x76D7 -0x76DB -0x76DF -0x76E3 -0x76E4 -0x76EE -0x76F2 -0x76F4 -0x76F8 -0x76FE -0x7701 -0x7709 -0x770B -0x770C -0x771F -0x7720 -0x773A -0x773C -0x7740 -0x7761 -0x7763 -0x7766 -0x77A5 -0x77AC -0x77AD -0x77B3 -0x77DB -0x77E2 -0x77E5 -0x77E7 -0x77E9 -0x77ED -0x77EF -0x77F3 -0x7802 -0x7814 -0x7815 -0x7825 -0x7826 -0x7827 -0x7832 -0x7834 -0x783A -0x783F -0x785D -0x786B -0x786C -0x786F -0x7872 -0x7881 -0x7887 -0x788D -0x7891 -0x7893 -0x7895 -0x7897 -0x78A7 -0x78A9 -0x78BA -0x78C1 -0x78D0 -0x78E8 -0x78EF -0x7901 -0x790E -0x793A -0x793C -0x793E -0x7941 -0x7947 -0x7948 -0x7949 -0x7950 -0x7956 -0x795D -0x795E -0x7962 -0x7965 -0x7968 -0x796D -0x7977 -0x7981 -0x7984 -0x7985 -0x798D -0x798E -0x798F -0x79A6 -0x79B0 -0x79BD -0x79BE -0x79BF -0x79C0 -0x79C1 -0x79CB -0x79D1 -0x79D2 -0x79D8 -0x79DF -0x79E4 -0x79E6 -0x79E9 -0x79F0 -0x79FB -0x7A00 -0x7A0B -0x7A0E -0x7A14 -0x7A17 -0x7A1A -0x7A1C -0x7A2E -0x7A32 -0x7A3C -0x7A3D -0x7A3F -0x7A40 -0x7A42 -0x7A46 -0x7A4D -0x7A4E -0x7A4F -0x7A50 -0x7A63 -0x7A6B -0x7A74 -0x7A76 -0x7A7A -0x7A7F -0x7A81 -0x7A83 -0x7A84 -0x7A92 -0x7A93 -0x7A9F -0x7AAA -0x7AAE -0x7AAF -0x7ABA -0x7AC3 -0x7ACB -0x7ADC -0x7AE0 -0x7AE3 -0x7AE5 -0x7AEA -0x7AEF -0x7AF6 -0x7AF9 -0x7AFA -0x7AFF -0x7B08 -0x7B11 -0x7B1B -0x7B20 -0x7B25 -0x7B26 -0x7B2C -0x7B39 -0x7B46 -0x7B48 -0x7B49 -0x7B4B -0x7B4F -0x7B51 -0x7B52 -0x7B54 -0x7B56 -0x7B86 -0x7B87 -0x7B94 -0x7B95 -0x7B97 -0x7BA1 -0x7BAA -0x7BAD -0x7BB1 -0x7BB8 -0x7BC0 -0x7BC4 -0x7BC7 -0x7BC9 -0x7BE0 -0x7BE4 -0x7BED -0x7C21 -0x7C38 -0x7C3E -0x7C3F -0x7C4D -0x7C73 -0x7C7E -0x7C81 -0x7C82 -0x7C89 -0x7C8B -0x7C8D -0x7C92 -0x7C95 -0x7C97 -0x7C98 -0x7C9B -0x7C9F -0x7CA5 -0x7CA7 -0x7CBE -0x7CCA -0x7CCE -0x7CD6 -0x7CDE -0x7CDF -0x7CE0 -0x7CE7 -0x7CF8 -0x7CFB -0x7CFE -0x7D00 -0x7D04 -0x7D05 -0x7D0B -0x7D0D -0x7D10 -0x7D14 -0x7D17 -0x7D18 -0x7D19 -0x7D1A -0x7D1B -0x7D20 -0x7D21 -0x7D22 -0x7D2B -0x7D2C -0x7D2F -0x7D30 -0x7D33 -0x7D39 -0x7D3A -0x7D42 -0x7D43 -0x7D44 -0x7D4C -0x7D50 -0x7D5E -0x7D61 -0x7D62 -0x7D66 -0x7D71 -0x7D75 -0x7D76 -0x7D79 -0x7D99 -0x7D9A -0x7D9C -0x7DAC -0x7DAD -0x7DB1 -0x7DB2 -0x7DB4 -0x7DBB -0x7DBE -0x7DBF -0x7DCA -0x7DCB -0x7DCF -0x7DD1 -0x7DD2 -0x7DDA -0x7DE0 -0x7DE8 -0x7DE9 -0x7DEC -0x7DEF -0x7DF4 -0x7E01 -0x7E04 -0x7E1B -0x7E1E -0x7E26 -0x7E2B -0x7E2E -0x7E3E -0x7E41 -0x7E4A -0x7E4B -0x7E4D -0x7E54 -0x7E55 -0x7E6D -0x7E70 -0x7E82 -0x7E8F -0x7F36 -0x7F6A -0x7F6B -0x7F6E -0x7F70 -0x7F72 -0x7F75 -0x7F77 -0x7F85 -0x7F8A -0x7F8E -0x7FA4 -0x7FA8 -0x7FA9 -0x7FBD -0x7FC1 -0x7FCC -0x7FD2 -0x7FE0 -0x7FEB -0x7FF0 -0x7FFB -0x7FFC -0x8000 -0x8001 -0x8003 -0x8005 -0x800C -0x8010 -0x8015 -0x8017 -0x8033 -0x8036 -0x803D -0x8056 -0x805E -0x8061 -0x806F -0x8074 -0x8077 -0x807E -0x8087 -0x8089 -0x808B -0x808C -0x8096 -0x8098 -0x809D -0x80A1 -0x80A2 -0x80A5 -0x80A9 -0x80AA -0x80AF -0x80B1 -0x80B2 -0x80B4 -0x80BA -0x80C3 -0x80C6 -0x80CC -0x80CE -0x80DE -0x80E1 -0x80E4 -0x80F4 -0x80F8 -0x80FD -0x8102 -0x8105 -0x8106 -0x8107 -0x8108 -0x810A -0x811A -0x8131 -0x8133 -0x8139 -0x814E -0x8150 -0x8154 -0x8155 -0x816B -0x8170 -0x8178 -0x8179 -0x817A -0x817F -0x818F -0x819A -0x819C -0x819D -0x81A8 -0x81B3 -0x81BF -0x81C6 -0x81D3 -0x81E3 -0x81E5 -0x81E8 -0x81EA -0x81ED -0x81F3 -0x81F4 -0x81FC -0x8208 -0x820C -0x820E -0x8217 -0x8218 -0x821B -0x821C -0x821E -0x821F -0x822A -0x822C -0x8235 -0x8236 -0x8237 -0x8239 -0x8247 -0x8266 -0x826E -0x826F -0x8272 -0x8276 -0x828B -0x8299 -0x829D -0x82A5 -0x82A6 -0x82AD -0x82AF -0x82B1 -0x82B3 -0x82B8 -0x82B9 -0x82BD -0x82C5 -0x82D1 -0x82D3 -0x82D4 -0x82D7 -0x82DB -0x82E5 -0x82E6 -0x82E7 -0x82EB -0x82F1 -0x8302 -0x8304 -0x8305 -0x830E -0x831C -0x8328 -0x8336 -0x8338 -0x8349 -0x834A -0x834F -0x8352 -0x8358 -0x8377 -0x837B -0x839E -0x83AB -0x83B1 -0x83C5 -0x83CA -0x83CC -0x83D3 -0x83D6 -0x83DC -0x83DF -0x83E9 -0x83EF -0x83F0 -0x83F1 -0x8404 -0x840C -0x840E -0x8429 -0x8431 -0x843D -0x8449 -0x844E -0x8457 -0x845B -0x8461 -0x8463 -0x8466 -0x846C -0x8471 -0x8475 -0x847A -0x848B -0x8490 -0x8494 -0x8499 -0x849C -0x84B2 -0x84B8 -0x84BC -0x84C4 -0x84C9 -0x84CB -0x84D1 -0x84EC -0x84EE -0x8500 -0x8511 -0x8513 -0x851A -0x8526 -0x852D -0x8535 -0x853D -0x8543 -0x8549 -0x854A -0x854E -0x8557 -0x8568 -0x8569 -0x856A -0x8584 -0x8597 -0x8599 -0x85A6 -0x85A9 -0x85AA -0x85AB -0x85AC -0x85AE -0x85AF -0x85C1 -0x85CD -0x85E4 -0x85E9 -0x85F7 -0x85FB -0x8607 -0x862D -0x864E -0x8650 -0x865A -0x865C -0x865E -0x866B -0x8679 -0x867B -0x868A -0x8695 -0x86A4 -0x86C7 -0x86CB -0x86CD -0x86CE -0x86D9 -0x86E4 -0x86ED -0x86EE -0x86F8 -0x86FE -0x8702 -0x8718 -0x871C -0x8749 -0x874B -0x8755 -0x8766 -0x8776 -0x877F -0x878D -0x87BA -0x87F9 -0x87FB -0x8840 -0x8846 -0x884C -0x8853 -0x8857 -0x885B -0x885D -0x8861 -0x8863 -0x8868 -0x8870 -0x8877 -0x887F -0x8888 -0x888B -0x8896 -0x88AB -0x88B4 -0x88B7 -0x88C1 -0x88C2 -0x88C5 -0x88CF -0x88D5 -0x88DC -0x88DF -0x88E1 -0x88F3 -0x88F8 -0x88FD -0x88FE -0x8907 -0x8910 -0x8912 -0x8956 -0x895F -0x8972 -0x897F -0x8981 -0x8986 -0x8987 -0x898B -0x898F -0x8996 -0x8997 -0x899A -0x89A7 -0x89AA -0x89B3 -0x89D2 -0x89E3 -0x89E6 -0x8A00 -0x8A02 -0x8A08 -0x8A0A -0x8A0E -0x8A13 -0x8A17 -0x8A18 -0x8A1F -0x8A23 -0x8A2A -0x8A2D -0x8A31 -0x8A33 -0x8A34 -0x8A3A -0x8A3B -0x8A3C -0x8A50 -0x8A51 -0x8A54 -0x8A55 -0x8A5E -0x8A60 -0x8A63 -0x8A66 -0x8A69 -0x8A6B -0x8A6E -0x8A70 -0x8A71 -0x8A72 -0x8A73 -0x8A87 -0x8A89 -0x8A8C -0x8A8D -0x8A93 -0x8A95 -0x8A98 -0x8A9E -0x8AA0 -0x8AA4 -0x8AAC -0x8AAD -0x8AB0 -0x8AB2 -0x8AB9 -0x8ABC -0x8ABF -0x8AC7 -0x8ACB -0x8ACC -0x8ACF -0x8AD2 -0x8AD6 -0x8ADC -0x8AE6 -0x8AED -0x8AEE -0x8AF8 -0x8AFA -0x8AFE -0x8B00 -0x8B01 -0x8B02 -0x8B04 -0x8B0E -0x8B19 -0x8B1B -0x8B1D -0x8B21 -0x8B2C -0x8B39 -0x8B58 -0x8B5C -0x8B66 -0x8B70 -0x8B72 -0x8B77 -0x8B83 -0x8B90 -0x8C37 -0x8C46 -0x8C4A -0x8C5A -0x8C61 -0x8C6A -0x8C79 -0x8C8C -0x8C9D -0x8C9E -0x8CA0 -0x8CA1 -0x8CA2 -0x8CA7 -0x8CA8 -0x8CA9 -0x8CAB -0x8CAC -0x8CAF -0x8CB0 -0x8CB4 -0x8CB7 -0x8CB8 -0x8CBB -0x8CBC -0x8CBF -0x8CC0 -0x8CC2 -0x8CC3 -0x8CC4 -0x8CC7 -0x8CCA -0x8CCE -0x8CD1 -0x8CD3 -0x8CDB -0x8CDC -0x8CDE -0x8CE0 -0x8CE2 -0x8CE6 -0x8CEA -0x8CED -0x8CFC -0x8D08 -0x8D0B -0x8D64 -0x8D66 -0x8D6B -0x8D70 -0x8D74 -0x8D77 -0x8D85 -0x8D8A -0x8DA3 -0x8DA8 -0x8DB3 -0x8DDD -0x8DE1 -0x8DE8 -0x8DEF -0x8DF3 -0x8DF5 -0x8E0A -0x8E0F -0x8E44 -0x8E5F -0x8E74 -0x8E8D -0x8EAB -0x8EAF -0x8ECA -0x8ECC -0x8ECD -0x8ED2 -0x8EDF -0x8EE2 -0x8EF8 -0x8EFD -0x8F03 -0x8F09 -0x8F14 -0x8F1D -0x8F29 -0x8F2A -0x8F2F -0x8F38 -0x8F3F -0x8F44 -0x8F4D -0x8F5F -0x8F61 -0x8F9B -0x8F9E -0x8FB0 -0x8FB1 -0x8FB2 -0x8FBA -0x8FBB -0x8FBC -0x8FBF -0x8FC2 -0x8FC4 -0x8FC5 -0x8FCE -0x8FD1 -0x8FD4 -0x8FE6 -0x8FE9 -0x8FEB -0x8FED -0x8FF0 -0x8FF7 -0x8FFD -0x9000 -0x9001 -0x9003 -0x9006 -0x900F -0x9010 -0x9013 -0x9014 -0x9017 -0x9019 -0x901A -0x901D -0x901F -0x9020 -0x9022 -0x9023 -0x902E -0x9031 -0x9032 -0x9038 -0x903C -0x9041 -0x9042 -0x9045 -0x9047 -0x904A -0x904B -0x904D -0x904E -0x9053 -0x9054 -0x9055 -0x905C -0x9060 -0x9061 -0x9063 -0x9065 -0x9069 -0x906D -0x906E -0x9075 -0x9077 -0x9078 -0x907A -0x907C -0x907F -0x9084 -0x9091 -0x90A3 -0x90A6 -0x90AA -0x90B8 -0x90C1 -0x90CA -0x90CE -0x90E1 -0x90E8 -0x90ED -0x90F5 -0x90F7 -0x90FD -0x912D -0x9149 -0x914B -0x914C -0x914D -0x914E -0x9152 -0x9154 -0x9162 -0x916A -0x916C -0x9175 -0x9177 -0x9178 -0x9187 -0x918D -0x9190 -0x9192 -0x9197 -0x919C -0x91A4 -0x91B8 -0x91C6 -0x91C7 -0x91C8 -0x91CC -0x91CD -0x91CE -0x91CF -0x91D1 -0x91D8 -0x91DC -0x91DD -0x91E3 -0x91E6 -0x91E7 -0x920D -0x920E -0x9234 -0x9237 -0x9244 -0x925B -0x9262 -0x9266 -0x9271 -0x927E -0x9280 -0x9283 -0x9285 -0x9291 -0x9298 -0x929A -0x92AD -0x92D2 -0x92E4 -0x92EA -0x92ED -0x92F2 -0x92F3 -0x92F8 -0x92FC -0x9306 -0x9310 -0x9318 -0x9320 -0x9326 -0x9328 -0x932B -0x932C -0x932F -0x9332 -0x934B -0x934D -0x9354 -0x935B -0x936C -0x9375 -0x937E -0x938C -0x9396 -0x9397 -0x939A -0x93A7 -0x93AE -0x93D1 -0x93E1 -0x9418 -0x9419 -0x9438 -0x9451 -0x9453 -0x9577 -0x9580 -0x9583 -0x9589 -0x958B -0x958F -0x9591 -0x9593 -0x95A2 -0x95A3 -0x95A4 -0x95A5 -0x95B2 -0x95C7 -0x95D8 -0x961C -0x962A -0x9632 -0x963B -0x963F -0x9640 -0x9644 -0x964D -0x9650 -0x965B -0x9662 -0x9663 -0x9664 -0x9665 -0x966A -0x9670 -0x9673 -0x9675 -0x9676 -0x9678 -0x967A -0x967D -0x9685 -0x9686 -0x9688 -0x968A -0x968E -0x968F -0x9694 -0x9699 -0x969B -0x969C -0x96A0 -0x96A3 -0x96B7 -0x96BB -0x96BC -0x96C0 -0x96C1 -0x96C4 -0x96C5 -0x96C6 -0x96C7 -0x96CC -0x96D1 -0x96DB -0x96E2 -0x96E3 -0x96E8 -0x96EA -0x96EB -0x96F0 -0x96F2 -0x96F6 -0x96F7 -0x96FB -0x9700 -0x9707 -0x970A -0x971C -0x971E -0x9727 -0x9732 -0x9752 -0x9756 -0x9759 -0x975E -0x9762 -0x9769 -0x976D -0x9774 -0x9784 -0x978D -0x9798 -0x97A0 -0x97AD -0x97D3 -0x97EE -0x97F3 -0x97FB -0x97FF -0x9801 -0x9802 -0x9803 -0x9805 -0x9806 -0x9808 -0x9810 -0x9811 -0x9812 -0x9813 -0x9817 -0x9818 -0x981A -0x982C -0x982D -0x9834 -0x983B -0x983C -0x984C -0x984D -0x984E -0x9854 -0x9855 -0x9858 -0x985B -0x985E -0x9867 -0x98A8 -0x98DB -0x98DF -0x98E2 -0x98EF -0x98F2 -0x98F4 -0x98FC -0x98FD -0x98FE -0x9905 -0x990A -0x990C -0x9910 -0x9913 -0x9928 -0x9957 -0x9996 -0x9999 -0x99A8 -0x99AC -0x99B3 -0x99B4 -0x99C1 -0x99C4 -0x99C5 -0x99C6 -0x99C8 -0x99D0 -0x99D2 -0x99D5 -0x99FF -0x9A0E -0x9A12 -0x9A13 -0x9A28 -0x9A30 -0x9A5A -0x9AA8 -0x9AB8 -0x9AC4 -0x9AD8 -0x9AEA -0x9AED -0x9B3C -0x9B41 -0x9B42 -0x9B45 -0x9B54 -0x9B5A -0x9B6F -0x9B8E -0x9B92 -0x9BAA -0x9BAB -0x9BAD -0x9BAE -0x9BC9 -0x9BD6 -0x9BDB -0x9BE8 -0x9BF5 -0x9C0D -0x9C10 -0x9C2D -0x9C2F -0x9C39 -0x9C3B -0x9C48 -0x9C52 -0x9C57 -0x9CE5 -0x9CE9 -0x9CF3 -0x9CF4 -0x9CF6 -0x9D07 -0x9D0E -0x9D1B -0x9D28 -0x9D2B -0x9D2C -0x9D3B -0x9D5C -0x9D60 -0x9D61 -0x9D6C -0x9D8F -0x9DB4 -0x9DF2 -0x9DF9 -0x9DFA -0x9E78 -0x9E7F -0x9E93 -0x9E97 -0x9E9F -0x9EA6 -0x9EB9 -0x9EBA -0x9EBB -0x9EBF -0x9EC4 -0x9ECD -0x9ED2 -0x9ED9 -0x9EDB -0x9F0E -0x9F13 -0x9F20 -0x9F3B -0x9F62 -0x9F8D diff --git a/font-build/unicodes-latin.txt b/font-build/unicodes-latin.txt deleted file mode 100644 index 86ff2ee..0000000 --- a/font-build/unicodes-latin.txt +++ /dev/null @@ -1 +0,0 @@ -U+0020-007E,U+00A0-00FF,U+0401,U+0410-044F,U+0451,U+2116 \ No newline at end of file diff --git a/font-build/unicodes.txt b/font-build/unicodes.txt deleted file mode 100644 index f446c0c..0000000 --- a/font-build/unicodes.txt +++ /dev/null @@ -1 +0,0 @@ -U+0020-007E,U+00A0-00FF,U+3040-309F,U+30A0-30FF,0x4E00,0x4E01,0x4E03,0x4E07,0x4E08,0x4E09,0x4E0A,0x4E0B,0x4E0D,0x4E0E,0x4E11,0x4E14,0x4E16,0x4E18,0x4E19,0x4E1E,0x4E21,0x4E26,0x4E2D,0x4E32,0x4E38,0x4E39,0x4E3B,0x4E43,0x4E45,0x4E4B,0x4E4D,0x4E4E,0x4E4F,0x4E57,0x4E59,0x4E5D,0x4E5E,0x4E5F,0x4E71,0x4E73,0x4E7E,0x4E80,0x4E86,0x4E88,0x4E89,0x4E8B,0x4E8C,0x4E91,0x4E92,0x4E94,0x4E95,0x4E98,0x4E99,0x4E9B,0x4E9C,0x4EA1,0x4EA4,0x4EA5,0x4EA6,0x4EA8,0x4EAB,0x4EAC,0x4EAD,0x4EAE,0x4EBA,0x4EC0,0x4EC1,0x4EC7,0x4ECA,0x4ECB,0x4ECF,0x4ED4,0x4ED5,0x4ED6,0x4ED8,0x4ED9,0x4EE3,0x4EE4,0x4EE5,0x4EEE,0x4EF0,0x4EF2,0x4EF6,0x4EFB,0x4F01,0x4F0A,0x4F0D,0x4F0E,0x4F0F,0x4F10,0x4F11,0x4F1A,0x4F1D,0x4F2F,0x4F34,0x4F36,0x4F38,0x4F3A,0x4F3C,0x4F3D,0x4F43,0x4F46,0x4F4D,0x4F4E,0x4F4F,0x4F50,0x4F51,0x4F53,0x4F55,0x4F59,0x4F5C,0x4F73,0x4F75,0x4F7C,0x4F7F,0x4F83,0x4F8B,0x4F8D,0x4F9B,0x4F9D,0x4FA0,0x4FA1,0x4FAD,0x4FAE,0x4FAF,0x4FB5,0x4FB6,0x4FBF,0x4FC2,0x4FC3,0x4FC4,0x4FCA,0x4FD7,0x4FDD,0x4FE1,0x4FE3,0x4FEE,0x4FF3,0x4FF5,0x4FF8,0x4FFA,0x5009,0x500B,0x500D,0x5012,0x5016,0x5019,0x501F,0x5023,0x5024,0x5026,0x502B,0x502D,0x5036,0x5039,0x5049,0x504F,0x505C,0x5065,0x5072,0x5074,0x5075,0x5076,0x507D,0x508D,0x5091,0x5098,0x5099,0x50AC,0x50AD,0x50B5,0x50B7,0x50BE,0x50C5,0x50CD,0x50CF,0x50D1,0x50D5,0x50DA,0x50E7,0x50FB,0x5100,0x5104,0x5112,0x511F,0x512A,0x5132,0x5141,0x5143,0x5144,0x5145,0x5146,0x5147,0x5148,0x5149,0x514B,0x514D,0x514E,0x5150,0x515A,0x515C,0x5165,0x5168,0x516B,0x516C,0x516D,0x5171,0x5175,0x5176,0x5177,0x5178,0x517C,0x5185,0x5186,0x518A,0x518D,0x5192,0x5197,0x5199,0x51A0,0x51A5,0x51A8,0x51AC,0x51B4,0x51B6,0x51B7,0x51C4,0x51C6,0x51CB,0x51CC,0x51CD,0x51DD,0x51E1,0x51E6,0x51E7,0x51EA,0x51F1,0x51F6,0x51F8,0x51F9,0x51FA,0x51FD,0x5200,0x5203,0x5206,0x5207,0x5208,0x520A,0x5211,0x5217,0x521D,0x5224,0x5225,0x5229,0x5230,0x5236,0x5237,0x5238,0x523A,0x523B,0x5243,0x5247,0x524A,0x524D,0x5256,0x525B,0x5263,0x5264,0x5265,0x526F,0x5270,0x5272,0x5275,0x5283,0x5287,0x5289,0x529B,0x529F,0x52A0,0x52A3,0x52A9,0x52AA,0x52AB,0x52B1,0x52B4,0x52B9,0x52BE,0x52C3,0x52C5,0x52C7,0x52C9,0x52D5,0x52D8,0x52D9,0x52DD,0x52DF,0x52E2,0x52E4,0x52E7,0x52F2,0x52FA,0x52FE,0x52FF,0x5301,0x5302,0x5305,0x5316,0x5317,0x5319,0x531D,0x5320,0x5321,0x532A,0x5339,0x533A,0x533B,0x533F,0x5341,0x5343,0x5347,0x5348,0x534A,0x5351,0x5352,0x5353,0x5354,0x5357,0x5358,0x535A,0x535C,0x5360,0x5366,0x536F,0x5370,0x5371,0x5373,0x5374,0x5375,0x5378,0x537F,0x5384,0x5398,0x539A,0x539F,0x53A8,0x53A9,0x53AD,0x53B3,0x53BB,0x53C2,0x53C8,0x53C9,0x53CA,0x53CB,0x53CC,0x53CD,0x53CE,0x53D4,0x53D6,0x53D7,0x53D9,0x53DB,0x53E1,0x53E2,0x53E3,0x53E4,0x53E5,0x53E9,0x53EA,0x53EB,0x53EC,0x53EF,0x53F0,0x53F1,0x53F2,0x53F3,0x53F6,0x53F7,0x53F8,0x5403,0x5404,0x5408,0x5409,0x540A,0x540B,0x540C,0x540D,0x540E,0x540F,0x5410,0x5411,0x541B,0x541F,0x5420,0x5426,0x542B,0x5438,0x5439,0x543B,0x543E,0x5442,0x5446,0x5448,0x5449,0x544A,0x5451,0x5468,0x546A,0x5473,0x547C,0x547D,0x548B,0x548C,0x54B2,0x54B3,0x54BD,0x54C0,0x54C1,0x54C9,0x54E1,0x54E8,0x54E9,0x54F2,0x5504,0x5506,0x5507,0x5510,0x5516,0x552F,0x5531,0x553E,0x5544,0x5546,0x554F,0x5553,0x5584,0x5589,0x558B,0x559A,0x559C,0x559D,0x55A7,0x55AA,0x55AB,0x55AC,0x55B0,0x55B6,0x55E3,0x5606,0x5609,0x5617,0x5618,0x5629,0x5631,0x5642,0x564C,0x565B,0x5668,0x5674,0x5678,0x567A,0x5687,0x56A2,0x56DA,0x56DB,0x56DE,0x56E0,0x56E3,0x56F0,0x56F2,0x56F3,0x56FA,0x56FD,0x5703,0x570F,0x5712,0x571F,0x5727,0x5728,0x572D,0x5730,0x5742,0x5747,0x574A,0x5750,0x5751,0x5764,0x5766,0x576A,0x5782,0x578B,0x57A2,0x57A3,0x57CB,0x57CE,0x57DC,0x57DF,0x57E0,0x57F4,0x57F7,0x57F9,0x57FA,0x57FC,0x5800,0x5802,0x5805,0x5806,0x5815,0x5824,0x582A,0x5830,0x5831,0x5834,0x5835,0x583A,0x5840,0x5841,0x584A,0x5851,0x5854,0x5857,0x5858,0x5859,0x585A,0x585E,0x5869,0x586B,0x5875,0x587E,0x5883,0x5893,0x5897,0x589C,0x58A8,0x58B3,0x58BE,0x58C1,0x58C7,0x58CA,0x58CC,0x58D5,0x58EB,0x58EC,0x58EE,0x58F0,0x58F1,0x58F2,0x58F7,0x5909,0x590F,0x5915,0x5916,0x5919,0x591A,0x591C,0x5922,0x5927,0x5929,0x592A,0x592B,0x592E,0x5931,0x5937,0x5944,0x5947,0x5948,0x5949,0x594F,0x5951,0x5954,0x5957,0x5965,0x5968,0x596A,0x596E,0x5973,0x5974,0x597D,0x5982,0x5983,0x5984,0x598A,0x5993,0x5996,0x5999,0x59A5,0x59A8,0x59AC,0x59B9,0x59BB,0x59BE,0x59C9,0x59CB,0x59D0,0x59D1,0x59D3,0x59D4,0x59E5,0x59E6,0x59EA,0x59EB,0x59F6,0x59FB,0x59FF,0x5A01,0x5A03,0x5A18,0x5A20,0x5A29,0x5A2F,0x5A3C,0x5A41,0x5A46,0x5A5A,0x5A66,0x5A7F,0x5A92,0x5A9B,0x5AC1,0x5AC9,0x5ACC,0x5AE1,0x5B09,0x5B22,0x5B2C,0x5B30,0x5B50,0x5B54,0x5B57,0x5B58,0x5B5C,0x5B5D,0x5B5F,0x5B63,0x5B64,0x5B66,0x5B6B,0x5B85,0x5B87,0x5B88,0x5B89,0x5B8B,0x5B8C,0x5B8D,0x5B8F,0x5B95,0x5B97,0x5B98,0x5B99,0x5B9A,0x5B9B,0x5B9C,0x5B9D,0x5B9F,0x5BA2,0x5BA3,0x5BA4,0x5BA5,0x5BAE,0x5BB0,0x5BB3,0x5BB4,0x5BB5,0x5BB6,0x5BB9,0x5BBF,0x5BC2,0x5BC4,0x5BC5,0x5BC6,0x5BCC,0x5BD2,0x5BD3,0x5BDB,0x5BDD,0x5BDF,0x5BE1,0x5BE7,0x5BE9,0x5BEE,0x5BF5,0x5BF8,0x5BFA,0x5BFE,0x5BFF,0x5C01,0x5C02,0x5C04,0x5C06,0x5C09,0x5C0A,0x5C0B,0x5C0E,0x5C0F,0x5C11,0x5C16,0x5C1A,0x5C24,0x5C2D,0x5C31,0x5C3A,0x5C3B,0x5C3C,0x5C3D,0x5C3E,0x5C3F,0x5C40,0x5C45,0x5C48,0x5C4A,0x5C4B,0x5C4D,0x5C51,0x5C55,0x5C5E,0x5C60,0x5C61,0x5C64,0x5C65,0x5C6F,0x5C71,0x5C90,0x5CA1,0x5CA8,0x5CA9,0x5CAC,0x5CB1,0x5CB3,0x5CB8,0x5CE0,0x5CE1,0x5CE8,0x5CEF,0x5CF0,0x5CF6,0x5CFB,0x5D07,0x5D0E,0x5D16,0x5D29,0x5D50,0x5D69,0x5D6F,0x5D8B,0x5DBA,0x5DCC,0x5DDD,0x5DDE,0x5DE1,0x5DE3,0x5DE5,0x5DE6,0x5DE7,0x5DE8,0x5DEE,0x5DF1,0x5DF3,0x5DF4,0x5DF7,0x5DFB,0x5DFD,0x5DFE,0x5E02,0x5E03,0x5E06,0x5E0C,0x5E16,0x5E1D,0x5E25,0x5E2B,0x5E2D,0x5E2F,0x5E30,0x5E33,0x5E38,0x5E3D,0x5E45,0x5E4C,0x5E55,0x5E61,0x5E63,0x5E72,0x5E73,0x5E74,0x5E78,0x5E79,0x5E7B,0x5E7C,0x5E7D,0x5E7E,0x5E81,0x5E83,0x5E84,0x5E87,0x5E8A,0x5E8F,0x5E95,0x5E96,0x5E97,0x5E9A,0x5E9C,0x5EA6,0x5EA7,0x5EAB,0x5EAD,0x5EB5,0x5EB6,0x5EB7,0x5EB8,0x5EC3,0x5EC9,0x5ECA,0x5ED3,0x5EDF,0x5EE0,0x5EF6,0x5EF7,0x5EFA,0x5EFB,0x5EFC,0x5EFF,0x5F01,0x5F04,0x5F0A,0x5F0F,0x5F10,0x5F13,0x5F14,0x5F15,0x5F17,0x5F18,0x5F1B,0x5F1F,0x5F25,0x5F26,0x5F27,0x5F31,0x5F35,0x5F37,0x5F3C,0x5F3E,0x5F4A,0x5F53,0x5F62,0x5F66,0x5F69,0x5F6A,0x5F6B,0x5F6C,0x5F70,0x5F71,0x5F79,0x5F7C,0x5F80,0x5F81,0x5F84,0x5F85,0x5F8B,0x5F8C,0x5F90,0x5F92,0x5F93,0x5F97,0x5FA1,0x5FA9,0x5FAA,0x5FAE,0x5FB3,0x5FB4,0x5FB9,0x5FBD,0x5FC3,0x5FC5,0x5FCC,0x5FCD,0x5FD7,0x5FD8,0x5FD9,0x5FDC,0x5FE0,0x5FEB,0x5FF5,0x5FFD,0x6012,0x6016,0x601C,0x601D,0x6020,0x6025,0x6027,0x6028,0x602A,0x602F,0x604B,0x6050,0x6052,0x6055,0x6062,0x6065,0x6068,0x6069,0x606D,0x606F,0x6070,0x6075,0x6089,0x608C,0x6094,0x609F,0x60A0,0x60A3,0x60A6,0x60A9,0x60AA,0x60B2,0x60B6,0x60BC,0x60C5,0x60C7,0x60D1,0x60DA,0x60DC,0x60DF,0x60E3,0x60E8,0x60F0,0x60F3,0x60F9,0x6101,0x6108,0x6109,0x610F,0x611A,0x611B,0x611F,0x6148,0x614B,0x614C,0x614E,0x6155,0x6162,0x6163,0x6167,0x6168,0x616E,0x6170,0x6176,0x617E,0x6182,0x618E,0x6190,0x61A4,0x61A7,0x61A9,0x61B2,0x61B6,0x61BE,0x61C7,0x61D0,0x61F2,0x61F8,0x620A,0x620E,0x6210,0x6211,0x6212,0x6216,0x621A,0x621F,0x6226,0x622F,0x6234,0x6238,0x623B,0x623F,0x6240,0x6247,0x6249,0x624B,0x624D,0x6253,0x6255,0x6258,0x626E,0x6271,0x6276,0x6279,0x627F,0x6280,0x6284,0x628A,0x6291,0x6295,0x6297,0x6298,0x629C,0x629E,0x62AB,0x62B1,0x62B5,0x62B9,0x62BC,0x62BD,0x62C5,0x62CD,0x62D0,0x62D2,0x62D3,0x62D8,0x62D9,0x62DB,0x62DD,0x62E0,0x62E1,0x62EC,0x62ED,0x62F3,0x62F6,0x62F7,0x62FE,0x6301,0x6307,0x6309,0x6311,0x6319,0x631F,0x6328,0x632B,0x632F,0x633A,0x633D,0x633F,0x6349,0x634C,0x6355,0x6357,0x635C,0x6367,0x6368,0x636E,0x6372,0x6377,0x637A,0x637B,0x6383,0x6388,0x638C,0x6392,0x6398,0x639B,0x63A0,0x63A1,0x63A2,0x63A5,0x63A7,0x63A8,0x63A9,0x63AA,0x63AC,0x63B2,0x63B4,0x63BB,0x63C3,0x63CF,0x63D0,0x63D6,0x63DA,0x63DB,0x63E1,0x63EE,0x63F4,0x63FA,0x640D,0x642C,0x642D,0x643A,0x643E,0x6442,0x6458,0x6469,0x6478,0x647A,0x6483,0x6492,0x649A,0x649E,0x64A4,0x64AB,0x64AD,0x64AE,0x64B0,0x64B2,0x64B9,0x64C1,0x64CD,0x64E2,0x64E6,0x64EC,0x64FE,0x652F,0x6539,0x653B,0x653E,0x653F,0x6545,0x654F,0x6551,0x6557,0x6559,0x6562,0x6563,0x6566,0x656C,0x6570,0x6574,0x6575,0x6577,0x6587,0x6589,0x658C,0x658E,0x6590,0x6591,0x6597,0x6599,0x659C,0x65A1,0x65A4,0x65A5,0x65A7,0x65AC,0x65AD,0x65AF,0x65B0,0x65B9,0x65BC,0x65BD,0x65C5,0x65CB,0x65CF,0x65D7,0x65E2,0x65E5,0x65E6,0x65E7,0x65E8,0x65E9,0x65EC,0x65ED,0x65FA,0x6602,0x6606,0x6607,0x660C,0x660E,0x660F,0x6613,0x6614,0x661F,0x6620,0x6625,0x6627,0x6628,0x662D,0x662F,0x663C,0x6642,0x6643,0x664B,0x6652,0x6666,0x6669,0x666E,0x666F,0x6674,0x6676,0x667A,0x6681,0x6687,0x6691,0x6696,0x6697,0x66A2,0x66A6,0x66AB,0x66AE,0x66B4,0x66C7,0x66D9,0x66DC,0x66DD,0x66F2,0x66F3,0x66F4,0x66F8,0x66F9,0x66FD,0x66FE,0x66FF,0x6700,0x6708,0x6709,0x670B,0x670D,0x6714,0x6715,0x6717,0x671B,0x671D,0x671F,0x6728,0x672A,0x672B,0x672C,0x672D,0x6731,0x6734,0x673A,0x673D,0x6749,0x674E,0x674F,0x6750,0x6751,0x6753,0x6756,0x675C,0x675F,0x6761,0x6762,0x6765,0x676D,0x676F,0x6771,0x6775,0x6777,0x677E,0x677F,0x6787,0x6790,0x6795,0x6797,0x679A,0x679C,0x679D,0x67A0,0x67A2,0x67AF,0x67B6,0x67C1,0x67C4,0x67CA,0x67CF,0x67D0,0x67D1,0x67D3,0x67D4,0x67D8,0x67DA,0x67F1,0x67F3,0x67F4,0x67F5,0x67FB,0x67FE,0x67FF,0x6802,0x6803,0x6804,0x6813,0x6816,0x6817,0x6821,0x6822,0x682A,0x6834,0x6838,0x6839,0x683C,0x683D,0x6841,0x6842,0x6843,0x6848,0x6850,0x6851,0x6853,0x6854,0x685C,0x685D,0x685F,0x6867,0x6876,0x6881,0x6885,0x6893,0x6897,0x68A2,0x68A7,0x68A8,0x68AF,0x68B0,0x68B1,0x68B6,0x68BC,0x68C4,0x68C9,0x68CB,0x68D2,0x68DA,0x68DF,0x68EE,0x68F2,0x68FA,0x6900,0x6905,0x690B,0x690D,0x690E,0x6919,0x691B,0x691C,0x6934,0x693F,0x694A,0x6953,0x6955,0x695A,0x6960,0x6962,0x696D,0x696F,0x6973,0x6975,0x697C,0x697D,0x6982,0x698A,0x698E,0x6994,0x699B,0x69CB,0x69CC,0x69CD,0x69D8,0x69D9,0x69FB,0x69FD,0x6A0B,0x6A17,0x6A19,0x6A1F,0x6A21,0x6A29,0x6A2A,0x6A2B,0x6A35,0x6A39,0x6A3A,0x6A3D,0x6A4B,0x6A58,0x6A5F,0x6A61,0x6A7F,0x6A80,0x6A8E,0x6AD3,0x6ADB,0x6AE8,0x6B04,0x6B1D,0x6B20,0x6B21,0x6B23,0x6B27,0x6B32,0x6B3A,0x6B3D,0x6B3E,0x6B4C,0x6B4E,0x6B53,0x6B62,0x6B63,0x6B64,0x6B66,0x6B69,0x6B6A,0x6B6F,0x6B73,0x6B74,0x6B7B,0x6B86,0x6B89,0x6B8A,0x6B8B,0x6B96,0x6BB4,0x6BB5,0x6BBA,0x6BBB,0x6BBF,0x6BC5,0x6BCD,0x6BCE,0x6BD2,0x6BD4,0x6BD8,0x6BDB,0x6C0F,0x6C11,0x6C17,0x6C34,0x6C37,0x6C38,0x6C3E,0x6C40,0x6C41,0x6C42,0x6C4E,0x6C50,0x6C57,0x6C5A,0x6C5D,0x6C5F,0x6C60,0x6C70,0x6C72,0x6C7A,0x6C7D,0x6C83,0x6C88,0x6C8C,0x6C93,0x6C96,0x6C99,0x6CA1,0x6CA2,0x6CAB,0x6CB3,0x6CB8,0x6CB9,0x6CBB,0x6CBC,0x6CBF,0x6CC1,0x6CC9,0x6CCA,0x6CCC,0x6CD5,0x6CE1,0x6CE2,0x6CE3,0x6CE5,0x6CE8,0x6CF0,0x6CF3,0x6D0B,0x6D17,0x6D1B,0x6D1E,0x6D25,0x6D29,0x6D2A,0x6D32,0x6D3B,0x6D3E,0x6D41,0x6D44,0x6D45,0x6D5C,0x6D66,0x6D69,0x6D6A,0x6D6C,0x6D6E,0x6D74,0x6D77,0x6D78,0x6D88,0x6D8C,0x6D99,0x6D9B,0x6D9C,0x6DAF,0x6DB2,0x6DBC,0x6DC0,0x6DCB,0x6DD1,0x6DD8,0x6DE1,0x6DEB,0x6DF1,0x6DF3,0x6DF5,0x6DF7,0x6DFB,0x6E05,0x6E07,0x6E08,0x6E09,0x6E0B,0x6E13,0x6E1A,0x6E1B,0x6E20,0x6E21,0x6E25,0x6E26,0x6E29,0x6E2C,0x6E2F,0x6E4A,0x6E56,0x6E58,0x6E5B,0x6E67,0x6E6F,0x6E7E,0x6E7F,0x6E80,0x6E8C,0x6E90,0x6E96,0x6E9C,0x6E9D,0x6EA2,0x6EB6,0x6EBA,0x6EC5,0x6ECB,0x6ED1,0x6EDD,0x6EDE,0x6EF4,0x6F01,0x6F02,0x6F06,0x6F09,0x6F0F,0x6F14,0x6F15,0x6F20,0x6F22,0x6F23,0x6F2B,0x6F2C,0x6F38,0x6F45,0x6F54,0x6F5C,0x6F5F,0x6F64,0x6F6E,0x6F70,0x6F84,0x6F97,0x6FB1,0x6FC0,0x6FC1,0x6FC3,0x6FE0,0x6FE1,0x6FEB,0x6FEF,0x7015,0x701E,0x7026,0x7027,0x702C,0x7058,0x706B,0x706F,0x7070,0x7078,0x707C,0x707D,0x7089,0x708A,0x708E,0x70AD,0x70B9,0x70BA,0x70C8,0x70CF,0x70F9,0x7114,0x711A,0x7121,0x7126,0x7136,0x713C,0x7149,0x714E,0x7159,0x7164,0x7167,0x7169,0x716E,0x717D,0x718A,0x7194,0x719F,0x71B1,0x71C3,0x71C8,0x71D0,0x71D5,0x71E5,0x71E6,0x71ED,0x7206,0x722A,0x7235,0x7236,0x723A,0x723D,0x723E,0x7247,0x7248,0x724C,0x7252,0x7259,0x725B,0x725D,0x725F,0x7261,0x7262,0x7267,0x7269,0x7272,0x7279,0x727D,0x7280,0x72A0,0x72AC,0x72AF,0x72B6,0x72C2,0x72D0,0x72D7,0x72D9,0x72DB,0x72E9,0x72EC,0x72ED,0x72F8,0x72FC,0x72FD,0x731B,0x731F,0x732A,0x732B,0x732E,0x7336,0x7337,0x733F,0x7344,0x7345,0x7363,0x7372,0x7384,0x7387,0x7389,0x738B,0x7396,0x73A9,0x73B2,0x73C2,0x73CA,0x73CD,0x73E0,0x73EA,0x73ED,0x73FE,0x7403,0x7406,0x7409,0x7422,0x7433,0x7434,0x7435,0x7436,0x745A,0x745B,0x745E,0x7460,0x7473,0x7483,0x74B0,0x74BD,0x74DC,0x74E2,0x74E6,0x74F6,0x7511,0x7518,0x751A,0x751C,0x751F,0x7523,0x7525,0x7528,0x752B,0x7530,0x7531,0x7532,0x7533,0x7537,0x753A,0x753B,0x754C,0x754F,0x7551,0x7554,0x7559,0x755C,0x755D,0x7560,0x7562,0x7565,0x7566,0x756A,0x7570,0x7573,0x7577,0x757F,0x758B,0x758E,0x758F,0x7591,0x75AB,0x75B2,0x75B9,0x75BE,0x75C5,0x75C7,0x75D4,0x75D5,0x75D8,0x75DB,0x75E2,0x75E9,0x75F4,0x7642,0x764C,0x7652,0x7656,0x767A,0x767B,0x767D,0x767E,0x7684,0x7686,0x7687,0x7690,0x76AE,0x76BF,0x76C3,0x76C6,0x76C8,0x76CA,0x76D7,0x76DB,0x76DF,0x76E3,0x76E4,0x76EE,0x76F2,0x76F4,0x76F8,0x76FE,0x7701,0x7709,0x770B,0x770C,0x771F,0x7720,0x773A,0x773C,0x7740,0x7761,0x7763,0x7766,0x77A5,0x77AC,0x77AD,0x77B3,0x77DB,0x77E2,0x77E5,0x77E7,0x77E9,0x77ED,0x77EF,0x77F3,0x7802,0x7814,0x7815,0x7825,0x7826,0x7827,0x7832,0x7834,0x783A,0x783F,0x785D,0x786B,0x786C,0x786F,0x7872,0x7881,0x7887,0x788D,0x7891,0x7893,0x7895,0x7897,0x78A7,0x78A9,0x78BA,0x78C1,0x78D0,0x78E8,0x78EF,0x7901,0x790E,0x793A,0x793C,0x793E,0x7941,0x7947,0x7948,0x7949,0x7950,0x7956,0x795D,0x795E,0x7962,0x7965,0x7968,0x796D,0x7977,0x7981,0x7984,0x7985,0x798D,0x798E,0x798F,0x79A6,0x79B0,0x79BD,0x79BE,0x79BF,0x79C0,0x79C1,0x79CB,0x79D1,0x79D2,0x79D8,0x79DF,0x79E4,0x79E6,0x79E9,0x79F0,0x79FB,0x7A00,0x7A0B,0x7A0E,0x7A14,0x7A17,0x7A1A,0x7A1C,0x7A2E,0x7A32,0x7A3C,0x7A3D,0x7A3F,0x7A40,0x7A42,0x7A46,0x7A4D,0x7A4E,0x7A4F,0x7A50,0x7A63,0x7A6B,0x7A74,0x7A76,0x7A7A,0x7A7F,0x7A81,0x7A83,0x7A84,0x7A92,0x7A93,0x7A9F,0x7AAA,0x7AAE,0x7AAF,0x7ABA,0x7AC3,0x7ACB,0x7ADC,0x7AE0,0x7AE3,0x7AE5,0x7AEA,0x7AEF,0x7AF6,0x7AF9,0x7AFA,0x7AFF,0x7B08,0x7B11,0x7B1B,0x7B20,0x7B25,0x7B26,0x7B2C,0x7B39,0x7B46,0x7B48,0x7B49,0x7B4B,0x7B4F,0x7B51,0x7B52,0x7B54,0x7B56,0x7B86,0x7B87,0x7B94,0x7B95,0x7B97,0x7BA1,0x7BAA,0x7BAD,0x7BB1,0x7BB8,0x7BC0,0x7BC4,0x7BC7,0x7BC9,0x7BE0,0x7BE4,0x7BED,0x7C21,0x7C38,0x7C3E,0x7C3F,0x7C4D,0x7C73,0x7C7E,0x7C81,0x7C82,0x7C89,0x7C8B,0x7C8D,0x7C92,0x7C95,0x7C97,0x7C98,0x7C9B,0x7C9F,0x7CA5,0x7CA7,0x7CBE,0x7CCA,0x7CCE,0x7CD6,0x7CDE,0x7CDF,0x7CE0,0x7CE7,0x7CF8,0x7CFB,0x7CFE,0x7D00,0x7D04,0x7D05,0x7D0B,0x7D0D,0x7D10,0x7D14,0x7D17,0x7D18,0x7D19,0x7D1A,0x7D1B,0x7D20,0x7D21,0x7D22,0x7D2B,0x7D2C,0x7D2F,0x7D30,0x7D33,0x7D39,0x7D3A,0x7D42,0x7D43,0x7D44,0x7D4C,0x7D50,0x7D5E,0x7D61,0x7D62,0x7D66,0x7D71,0x7D75,0x7D76,0x7D79,0x7D99,0x7D9A,0x7D9C,0x7DAC,0x7DAD,0x7DB1,0x7DB2,0x7DB4,0x7DBB,0x7DBE,0x7DBF,0x7DCA,0x7DCB,0x7DCF,0x7DD1,0x7DD2,0x7DDA,0x7DE0,0x7DE8,0x7DE9,0x7DEC,0x7DEF,0x7DF4,0x7E01,0x7E04,0x7E1B,0x7E1E,0x7E26,0x7E2B,0x7E2E,0x7E3E,0x7E41,0x7E4A,0x7E4B,0x7E4D,0x7E54,0x7E55,0x7E6D,0x7E70,0x7E82,0x7E8F,0x7F36,0x7F6A,0x7F6B,0x7F6E,0x7F70,0x7F72,0x7F75,0x7F77,0x7F85,0x7F8A,0x7F8E,0x7FA4,0x7FA8,0x7FA9,0x7FBD,0x7FC1,0x7FCC,0x7FD2,0x7FE0,0x7FEB,0x7FF0,0x7FFB,0x7FFC,0x8000,0x8001,0x8003,0x8005,0x800C,0x8010,0x8015,0x8017,0x8033,0x8036,0x803D,0x8056,0x805E,0x8061,0x806F,0x8074,0x8077,0x807E,0x8087,0x8089,0x808B,0x808C,0x8096,0x8098,0x809D,0x80A1,0x80A2,0x80A5,0x80A9,0x80AA,0x80AF,0x80B1,0x80B2,0x80B4,0x80BA,0x80C3,0x80C6,0x80CC,0x80CE,0x80DE,0x80E1,0x80E4,0x80F4,0x80F8,0x80FD,0x8102,0x8105,0x8106,0x8107,0x8108,0x810A,0x811A,0x8131,0x8133,0x8139,0x814E,0x8150,0x8154,0x8155,0x816B,0x8170,0x8178,0x8179,0x817A,0x817F,0x818F,0x819A,0x819C,0x819D,0x81A8,0x81B3,0x81BF,0x81C6,0x81D3,0x81E3,0x81E5,0x81E8,0x81EA,0x81ED,0x81F3,0x81F4,0x81FC,0x8208,0x820C,0x820E,0x8217,0x8218,0x821B,0x821C,0x821E,0x821F,0x822A,0x822C,0x8235,0x8236,0x8237,0x8239,0x8247,0x8266,0x826E,0x826F,0x8272,0x8276,0x828B,0x8299,0x829D,0x82A5,0x82A6,0x82AD,0x82AF,0x82B1,0x82B3,0x82B8,0x82B9,0x82BD,0x82C5,0x82D1,0x82D3,0x82D4,0x82D7,0x82DB,0x82E5,0x82E6,0x82E7,0x82EB,0x82F1,0x8302,0x8304,0x8305,0x830E,0x831C,0x8328,0x8336,0x8338,0x8349,0x834A,0x834F,0x8352,0x8358,0x8377,0x837B,0x839E,0x83AB,0x83B1,0x83C5,0x83CA,0x83CC,0x83D3,0x83D6,0x83DC,0x83DF,0x83E9,0x83EF,0x83F0,0x83F1,0x8404,0x840C,0x840E,0x8429,0x8431,0x843D,0x8449,0x844E,0x8457,0x845B,0x8461,0x8463,0x8466,0x846C,0x8471,0x8475,0x847A,0x848B,0x8490,0x8494,0x8499,0x849C,0x84B2,0x84B8,0x84BC,0x84C4,0x84C9,0x84CB,0x84D1,0x84EC,0x84EE,0x8500,0x8511,0x8513,0x851A,0x8526,0x852D,0x8535,0x853D,0x8543,0x8549,0x854A,0x854E,0x8557,0x8568,0x8569,0x856A,0x8584,0x8597,0x8599,0x85A6,0x85A9,0x85AA,0x85AB,0x85AC,0x85AE,0x85AF,0x85C1,0x85CD,0x85E4,0x85E9,0x85F7,0x85FB,0x8607,0x862D,0x864E,0x8650,0x865A,0x865C,0x865E,0x866B,0x8679,0x867B,0x868A,0x8695,0x86A4,0x86C7,0x86CB,0x86CD,0x86CE,0x86D9,0x86E4,0x86ED,0x86EE,0x86F8,0x86FE,0x8702,0x8718,0x871C,0x8749,0x874B,0x8755,0x8766,0x8776,0x877F,0x878D,0x87BA,0x87F9,0x87FB,0x8840,0x8846,0x884C,0x8853,0x8857,0x885B,0x885D,0x8861,0x8863,0x8868,0x8870,0x8877,0x887F,0x8888,0x888B,0x8896,0x88AB,0x88B4,0x88B7,0x88C1,0x88C2,0x88C5,0x88CF,0x88D5,0x88DC,0x88DF,0x88E1,0x88F3,0x88F8,0x88FD,0x88FE,0x8907,0x8910,0x8912,0x8956,0x895F,0x8972,0x897F,0x8981,0x8986,0x8987,0x898B,0x898F,0x8996,0x8997,0x899A,0x89A7,0x89AA,0x89B3,0x89D2,0x89E3,0x89E6,0x8A00,0x8A02,0x8A08,0x8A0A,0x8A0E,0x8A13,0x8A17,0x8A18,0x8A1F,0x8A23,0x8A2A,0x8A2D,0x8A31,0x8A33,0x8A34,0x8A3A,0x8A3B,0x8A3C,0x8A50,0x8A51,0x8A54,0x8A55,0x8A5E,0x8A60,0x8A63,0x8A66,0x8A69,0x8A6B,0x8A6E,0x8A70,0x8A71,0x8A72,0x8A73,0x8A87,0x8A89,0x8A8C,0x8A8D,0x8A93,0x8A95,0x8A98,0x8A9E,0x8AA0,0x8AA4,0x8AAC,0x8AAD,0x8AB0,0x8AB2,0x8AB9,0x8ABC,0x8ABF,0x8AC7,0x8ACB,0x8ACC,0x8ACF,0x8AD2,0x8AD6,0x8ADC,0x8AE6,0x8AED,0x8AEE,0x8AF8,0x8AFA,0x8AFE,0x8B00,0x8B01,0x8B02,0x8B04,0x8B0E,0x8B19,0x8B1B,0x8B1D,0x8B21,0x8B2C,0x8B39,0x8B58,0x8B5C,0x8B66,0x8B70,0x8B72,0x8B77,0x8B83,0x8B90,0x8C37,0x8C46,0x8C4A,0x8C5A,0x8C61,0x8C6A,0x8C79,0x8C8C,0x8C9D,0x8C9E,0x8CA0,0x8CA1,0x8CA2,0x8CA7,0x8CA8,0x8CA9,0x8CAB,0x8CAC,0x8CAF,0x8CB0,0x8CB4,0x8CB7,0x8CB8,0x8CBB,0x8CBC,0x8CBF,0x8CC0,0x8CC2,0x8CC3,0x8CC4,0x8CC7,0x8CCA,0x8CCE,0x8CD1,0x8CD3,0x8CDB,0x8CDC,0x8CDE,0x8CE0,0x8CE2,0x8CE6,0x8CEA,0x8CED,0x8CFC,0x8D08,0x8D0B,0x8D64,0x8D66,0x8D6B,0x8D70,0x8D74,0x8D77,0x8D85,0x8D8A,0x8DA3,0x8DA8,0x8DB3,0x8DDD,0x8DE1,0x8DE8,0x8DEF,0x8DF3,0x8DF5,0x8E0A,0x8E0F,0x8E44,0x8E5F,0x8E74,0x8E8D,0x8EAB,0x8EAF,0x8ECA,0x8ECC,0x8ECD,0x8ED2,0x8EDF,0x8EE2,0x8EF8,0x8EFD,0x8F03,0x8F09,0x8F14,0x8F1D,0x8F29,0x8F2A,0x8F2F,0x8F38,0x8F3F,0x8F44,0x8F4D,0x8F5F,0x8F61,0x8F9B,0x8F9E,0x8FB0,0x8FB1,0x8FB2,0x8FBA,0x8FBB,0x8FBC,0x8FBF,0x8FC2,0x8FC4,0x8FC5,0x8FCE,0x8FD1,0x8FD4,0x8FE6,0x8FE9,0x8FEB,0x8FED,0x8FF0,0x8FF7,0x8FFD,0x9000,0x9001,0x9003,0x9006,0x900F,0x9010,0x9013,0x9014,0x9017,0x9019,0x901A,0x901D,0x901F,0x9020,0x9022,0x9023,0x902E,0x9031,0x9032,0x9038,0x903C,0x9041,0x9042,0x9045,0x9047,0x904A,0x904B,0x904D,0x904E,0x9053,0x9054,0x9055,0x905C,0x9060,0x9061,0x9063,0x9065,0x9069,0x906D,0x906E,0x9075,0x9077,0x9078,0x907A,0x907C,0x907F,0x9084,0x9091,0x90A3,0x90A6,0x90AA,0x90B8,0x90C1,0x90CA,0x90CE,0x90E1,0x90E8,0x90ED,0x90F5,0x90F7,0x90FD,0x912D,0x9149,0x914B,0x914C,0x914D,0x914E,0x9152,0x9154,0x9162,0x916A,0x916C,0x9175,0x9177,0x9178,0x9187,0x918D,0x9190,0x9192,0x9197,0x919C,0x91A4,0x91B8,0x91C6,0x91C7,0x91C8,0x91CC,0x91CD,0x91CE,0x91CF,0x91D1,0x91D8,0x91DC,0x91DD,0x91E3,0x91E6,0x91E7,0x920D,0x920E,0x9234,0x9237,0x9244,0x925B,0x9262,0x9266,0x9271,0x927E,0x9280,0x9283,0x9285,0x9291,0x9298,0x929A,0x92AD,0x92D2,0x92E4,0x92EA,0x92ED,0x92F2,0x92F3,0x92F8,0x92FC,0x9306,0x9310,0x9318,0x9320,0x9326,0x9328,0x932B,0x932C,0x932F,0x9332,0x934B,0x934D,0x9354,0x935B,0x936C,0x9375,0x937E,0x938C,0x9396,0x9397,0x939A,0x93A7,0x93AE,0x93D1,0x93E1,0x9418,0x9419,0x9438,0x9451,0x9453,0x9577,0x9580,0x9583,0x9589,0x958B,0x958F,0x9591,0x9593,0x95A2,0x95A3,0x95A4,0x95A5,0x95B2,0x95C7,0x95D8,0x961C,0x962A,0x9632,0x963B,0x963F,0x9640,0x9644,0x964D,0x9650,0x965B,0x9662,0x9663,0x9664,0x9665,0x966A,0x9670,0x9673,0x9675,0x9676,0x9678,0x967A,0x967D,0x9685,0x9686,0x9688,0x968A,0x968E,0x968F,0x9694,0x9699,0x969B,0x969C,0x96A0,0x96A3,0x96B7,0x96BB,0x96BC,0x96C0,0x96C1,0x96C4,0x96C5,0x96C6,0x96C7,0x96CC,0x96D1,0x96DB,0x96E2,0x96E3,0x96E8,0x96EA,0x96EB,0x96F0,0x96F2,0x96F6,0x96F7,0x96FB,0x9700,0x9707,0x970A,0x971C,0x971E,0x9727,0x9732,0x9752,0x9756,0x9759,0x975E,0x9762,0x9769,0x976D,0x9774,0x9784,0x978D,0x9798,0x97A0,0x97AD,0x97D3,0x97EE,0x97F3,0x97FB,0x97FF,0x9801,0x9802,0x9803,0x9805,0x9806,0x9808,0x9810,0x9811,0x9812,0x9813,0x9817,0x9818,0x981A,0x982C,0x982D,0x9834,0x983B,0x983C,0x984C,0x984D,0x984E,0x9854,0x9855,0x9858,0x985B,0x985E,0x9867,0x98A8,0x98DB,0x98DF,0x98E2,0x98EF,0x98F2,0x98F4,0x98FC,0x98FD,0x98FE,0x9905,0x990A,0x990C,0x9910,0x9913,0x9928,0x9957,0x9996,0x9999,0x99A8,0x99AC,0x99B3,0x99B4,0x99C1,0x99C4,0x99C5,0x99C6,0x99C8,0x99D0,0x99D2,0x99D5,0x99FF,0x9A0E,0x9A12,0x9A13,0x9A28,0x9A30,0x9A5A,0x9AA8,0x9AB8,0x9AC4,0x9AD8,0x9AEA,0x9AED,0x9B3C,0x9B41,0x9B42,0x9B45,0x9B54,0x9B5A,0x9B6F,0x9B8E,0x9B92,0x9BAA,0x9BAB,0x9BAD,0x9BAE,0x9BC9,0x9BD6,0x9BDB,0x9BE8,0x9BF5,0x9C0D,0x9C10,0x9C2D,0x9C2F,0x9C39,0x9C3B,0x9C48,0x9C52,0x9C57,0x9CE5,0x9CE9,0x9CF3,0x9CF4,0x9CF6,0x9D07,0x9D0E,0x9D1B,0x9D28,0x9D2B,0x9D2C,0x9D3B,0x9D5C,0x9D60,0x9D61,0x9D6C,0x9D8F,0x9DB4,0x9DF2,0x9DF9,0x9DFA,0x9E78,0x9E7F,0x9E93,0x9E97,0x9E9F,0x9EA6,0x9EB9,0x9EBA,0x9EBB,0x9EBF,0x9EC4,0x9ECD,0x9ED2,0x9ED9,0x9EDB,0x9F0E,0x9F13,0x9F20,0x9F3B,0x9F62,0x9F8D \ No newline at end of file diff --git a/mod/etc/options_menu/fonts/NotoSansJP-Latin-subset.ttf b/mod/etc/options_menu/fonts/NotoSansJP-Latin-subset.ttf deleted file mode 100644 index 376b7f092e7add478b0b7b2ecee6c470f2c02581..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 43040 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 Nt**$0G=+GO{|DW=IspIx From 4ae9078fd0d79f99954490b3c4b602ebd305bb47 Mon Sep 17 00:00:00 2001 From: DefKorns Date: Tue, 20 Oct 2026 01:50:58 +0000 Subject: [PATCH 103/104] Update hmod-build to v1.3.0 --- .github/workflows/build.yml | 2 +- vendor/hmod-build | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 680bff5..6303cf5 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -9,7 +9,7 @@ on: jobs: hmod: # Keep in sync with the vendor/hmod-build submodule tag. - uses: DefKorns/hmod-build/.github/workflows/hmod.yml@v1.2.0 + uses: DefKorns/hmod-build/.github/workflows/hmod.yml@v1.3.0 with: release-name: Options Menu docker: true diff --git a/vendor/hmod-build b/vendor/hmod-build index 0de820c..472d4bf 160000 --- a/vendor/hmod-build +++ b/vendor/hmod-build @@ -1 +1 @@ -Subproject commit 0de820c3e7e65e5072f98d8a76bcbaab3617f41f +Subproject commit 472d4bf6e16539c3f6ff01ac39bbf68433b34222 From 8b4d8f2f88f56c0db0893c5b761519c64a24c33e Mon Sep 17 00:00:00 2001 From: DefKorns Date: Tue, 20 Oct 2026 02:32:43 +0000 Subject: [PATCH 104/104] Walk long lists by letter, drop the Hakchi-on-USB names search and leave emptied lists --- Makefile | 2 +- README.md | 4 +- mod/etc/options_menu/language/de-DE.lang | 10 -- mod/etc/options_menu/language/en-US.lang | 10 -- mod/etc/options_menu/language/es-ES.lang | 10 -- mod/etc/options_menu/language/fr-FR.lang | 10 -- mod/etc/options_menu/language/it-IT.lang | 10 -- mod/etc/options_menu/language/ja-JP.lang | 10 -- mod/etc/options_menu/language/nl-NL.lang | 10 -- mod/etc/options_menu/language/pt-PT.lang | 10 -- mod/etc/options_menu/language/ru-RU.lang | 10 -- mod/etc/options_menu/scripts/OrphanSaves.sh | 23 +--- pc/push_game_names.ps1 | 2 +- src/framework/list_jump.cpp | 130 ++++++++++++++++++++ src/framework/list_jump.h | 37 ++++++ src/mod_uninstall.cpp | 10 ++ src/save_manager.cpp | 58 +++------ 17 files changed, 200 insertions(+), 156 deletions(-) create mode 100644 src/framework/list_jump.cpp create mode 100644 src/framework/list_jump.h diff --git a/Makefile b/Makefile index df4fb70..178391a 100644 --- a/Makefile +++ b/Makefile @@ -36,7 +36,7 @@ VENDORED_LIBS = endif CXXFLAGS = -std=c++11 -Os $(SDL_CFLAGS) -DMOD_VERSION=\"v$(MOD_VER)\" LDLIBS = $(SDL_LIBS) -SOURCES = src/main.cpp src/command.cpp src/localization.cpp src/framework/badge.cpp src/framework/dialog.cpp src/framework/sdl_context.cpp src/framework/texture.cpp src/framework/controller.cpp src/framework/powerwatch.cpp src/framework/draw_helpers.cpp src/framework/utf8.cpp src/framework/font8x8_lookup.cpp src/framework/uitheme.cpp +SOURCES = src/main.cpp src/command.cpp src/localization.cpp src/framework/badge.cpp src/framework/dialog.cpp src/framework/sdl_context.cpp src/framework/texture.cpp src/framework/controller.cpp src/framework/powerwatch.cpp src/framework/draw_helpers.cpp src/framework/utf8.cpp src/framework/font8x8_lookup.cpp src/framework/uitheme.cpp src/framework/list_jump.cpp OBJECTS = $(SOURCES:.cpp=.o) ALL_OBJECTS = $(OBJECTS) src/daemon.o src/mod_uninstall.o src/save_manager.o src/standby_watchdog.o src/gen_splash.o src/change_combo.o diff --git a/README.md b/README.md index 95a10d2..403763b 100644 --- a/README.md +++ b/README.md @@ -103,6 +103,8 @@ Needs a USB/SD drive with your saves on it. One screen lists the actions on the | Clean Saves of Removed Games › | Lists the save folders of games that are no longer in any menu (all marked; **A** unmarks) and moves the marked ones to `/media/data/deleted_games_saves`. Menu folders (`CLV-S-*`) are never touched | | Restore Saves of Removed Games › | Lists what was moved aside; mark the ones you want back with **A** and press **Start**. A save already on the console is never overwritten | +In the long lists (here and in the Module Uninstaller), **Left**/**Right** page up and down on a tap and walk the list letter by letter while held; **L**/**R** jump a letter at a time. + #### Game names The two "Removed Games" lists show the names of the removed games instead of their codes (`CLV-H-ABCDE`). Every time the console starts, it remembers the names of the games installed at that moment in `game_names.txt` in your saves folder, and a name stays there after its game is removed. The backup keeps the file with your saves, and a restore adds its names without replacing newer ones. Saves without a known name keep showing their code. Getting the names never changes or moves a save. @@ -118,7 +120,7 @@ Games removed before this version was installed were never seen on the console, If Windows refuses to run scripts, use `powershell -ExecutionPolicy Bypass -File push_game_names.ps1 --ip 192.168.1.50`. -The names it sends are added to the ones already on the console. If a portable Hakchi CE folder is on the USB drive itself, press **Select** (or **Y**) in either list to read the names from there instead. +The names it sends are added to the ones already on the console. ### Advanced Options diff --git a/mod/etc/options_menu/language/de-DE.lang b/mod/etc/options_menu/language/de-DE.lang index ac791c5..18de9d2 100644 --- a/mod/etc/options_menu/language/de-DE.lang +++ b/mod/etc/options_menu/language/de-DE.lang @@ -124,13 +124,3 @@ HINT_OK=OK HINT_MOVE=Verschieben HINT_RESTORE=Wiederherstellen SAVE_MANAGER_SELECT_ALL=Alle auswählen -HINT_GAME_NAMES=Spielnamen -SAVE_MANAGER_HAKCHI_SEARCHING=Suche nach Hakchi... -SAVE_MANAGER_HAKCHI_SCANNING=Spiele werden gelesen... -SAVE_MANAGER_HAKCHI_NOT_FOUND=Hakchi wurde auf dem USB-Laufwerk nicht gefunden. -SAVE_MANAGER_HAKCHI_NOT_FOUND_NOTE=Kopiere einen portablen Hakchi-CE-Ordner auf das USB-Laufwerk und versuche es erneut. -SAVE_MANAGER_HAKCHI_FAILED=Die Hakchi-Spiele konnten nicht gelesen werden. -SAVE_MANAGER_HAKCHI_DONE=Spielnamen aktualisiert -SAVE_MANAGER_HAKCHI_GAMES=Spiele in Hakchi -SAVE_MANAGER_HAKCHI_NAMED=Spielstände mit Namen -SAVE_MANAGER_HAKCHI_PARTIAL_NOTE=Spielstände ohne Treffer werden mit ihrem Spielcode angezeigt. diff --git a/mod/etc/options_menu/language/en-US.lang b/mod/etc/options_menu/language/en-US.lang index 23dad4e..e89537c 100644 --- a/mod/etc/options_menu/language/en-US.lang +++ b/mod/etc/options_menu/language/en-US.lang @@ -125,13 +125,3 @@ HINT_OK=OK HINT_MOVE=Move HINT_RESTORE=Restore SAVE_MANAGER_SELECT_ALL=Select all -HINT_GAME_NAMES=Game Names -SAVE_MANAGER_HAKCHI_SEARCHING=Searching for Hakchi... -SAVE_MANAGER_HAKCHI_SCANNING=Scanning games... -SAVE_MANAGER_HAKCHI_NOT_FOUND=Hakchi was not found on the USB drive. -SAVE_MANAGER_HAKCHI_NOT_FOUND_NOTE=Copy a portable Hakchi CE folder to the USB drive and try again. -SAVE_MANAGER_HAKCHI_FAILED=Could not read the Hakchi games. -SAVE_MANAGER_HAKCHI_DONE=Game names updated -SAVE_MANAGER_HAKCHI_GAMES=Games in Hakchi -SAVE_MANAGER_HAKCHI_NAMED=Saves with a name -SAVE_MANAGER_HAKCHI_PARTIAL_NOTE=Saves without a match are shown by their game code. diff --git a/mod/etc/options_menu/language/es-ES.lang b/mod/etc/options_menu/language/es-ES.lang index a30ee9c..60c1923 100644 --- a/mod/etc/options_menu/language/es-ES.lang +++ b/mod/etc/options_menu/language/es-ES.lang @@ -124,13 +124,3 @@ HINT_OK=OK HINT_MOVE=Mover HINT_RESTORE=Restaurar SAVE_MANAGER_SELECT_ALL=Seleccionar todo -HINT_GAME_NAMES=Nombres de juegos -SAVE_MANAGER_HAKCHI_SEARCHING=Buscando Hakchi... -SAVE_MANAGER_HAKCHI_SCANNING=Analizando juegos... -SAVE_MANAGER_HAKCHI_NOT_FOUND=No se encontró Hakchi en la USB. -SAVE_MANAGER_HAKCHI_NOT_FOUND_NOTE=Copia una carpeta portátil de Hakchi CE a la USB e inténtalo de nuevo. -SAVE_MANAGER_HAKCHI_FAILED=No se pudieron leer los juegos de Hakchi. -SAVE_MANAGER_HAKCHI_DONE=Nombres de juegos actualizados -SAVE_MANAGER_HAKCHI_GAMES=Juegos en Hakchi -SAVE_MANAGER_HAKCHI_NAMED=Partidas con nombre -SAVE_MANAGER_HAKCHI_PARTIAL_NOTE=Las partidas sin coincidencia se muestran con el código del juego. diff --git a/mod/etc/options_menu/language/fr-FR.lang b/mod/etc/options_menu/language/fr-FR.lang index 2a45d61..67b10c6 100644 --- a/mod/etc/options_menu/language/fr-FR.lang +++ b/mod/etc/options_menu/language/fr-FR.lang @@ -124,13 +124,3 @@ HINT_OK=OK HINT_MOVE=Déplacer HINT_RESTORE=Restaurer SAVE_MANAGER_SELECT_ALL=Tout sélectionner -HINT_GAME_NAMES=Noms des jeux -SAVE_MANAGER_HAKCHI_SEARCHING=Recherche de Hakchi... -SAVE_MANAGER_HAKCHI_SCANNING=Analyse des jeux... -SAVE_MANAGER_HAKCHI_NOT_FOUND=Hakchi est introuvable sur l'USB. -SAVE_MANAGER_HAKCHI_NOT_FOUND_NOTE=Copiez un dossier portable de Hakchi CE sur l'USB et réessayez. -SAVE_MANAGER_HAKCHI_FAILED=Impossible de lire les jeux de Hakchi. -SAVE_MANAGER_HAKCHI_DONE=Noms des jeux mis à jour -SAVE_MANAGER_HAKCHI_GAMES=Jeux dans Hakchi -SAVE_MANAGER_HAKCHI_NAMED=Sauvegardes nommées -SAVE_MANAGER_HAKCHI_PARTIAL_NOTE=Les sauvegardes sans correspondance gardent le code du jeu. diff --git a/mod/etc/options_menu/language/it-IT.lang b/mod/etc/options_menu/language/it-IT.lang index 3a3c7f6..8ca5d06 100644 --- a/mod/etc/options_menu/language/it-IT.lang +++ b/mod/etc/options_menu/language/it-IT.lang @@ -124,13 +124,3 @@ HINT_OK=OK HINT_MOVE=Sposta HINT_RESTORE=Ripristina SAVE_MANAGER_SELECT_ALL=Seleziona tutto -HINT_GAME_NAMES=Nomi dei giochi -SAVE_MANAGER_HAKCHI_SEARCHING=Ricerca di Hakchi... -SAVE_MANAGER_HAKCHI_SCANNING=Analisi dei giochi... -SAVE_MANAGER_HAKCHI_NOT_FOUND=Hakchi non trovato sulla USB. -SAVE_MANAGER_HAKCHI_NOT_FOUND_NOTE=Copia una cartella portatile di Hakchi CE sulla USB e riprova. -SAVE_MANAGER_HAKCHI_FAILED=Impossibile leggere i giochi di Hakchi. -SAVE_MANAGER_HAKCHI_DONE=Nomi dei giochi aggiornati -SAVE_MANAGER_HAKCHI_GAMES=Giochi in Hakchi -SAVE_MANAGER_HAKCHI_NAMED=Salvataggi con nome -SAVE_MANAGER_HAKCHI_PARTIAL_NOTE=I salvataggi senza corrispondenza mostrano il codice del gioco. diff --git a/mod/etc/options_menu/language/ja-JP.lang b/mod/etc/options_menu/language/ja-JP.lang index 49b2ffe..cf1fc29 100644 --- a/mod/etc/options_menu/language/ja-JP.lang +++ b/mod/etc/options_menu/language/ja-JP.lang @@ -124,13 +124,3 @@ HINT_OK=OK HINT_MOVE=移動 HINT_RESTORE=復元 SAVE_MANAGER_SELECT_ALL=すべて選択 -HINT_GAME_NAMES=ゲーム名 -SAVE_MANAGER_HAKCHI_SEARCHING=Hakchiを検索中... -SAVE_MANAGER_HAKCHI_SCANNING=ゲームを確認中... -SAVE_MANAGER_HAKCHI_NOT_FOUND=USBにHakchiがみつかりません -SAVE_MANAGER_HAKCHI_NOT_FOUND_NOTE=ポータブル版Hakchi CEのフォルダをUSBにコピーしてください。 -SAVE_MANAGER_HAKCHI_FAILED=Hakchiのゲームを読み込めませんでした -SAVE_MANAGER_HAKCHI_DONE=ゲーム名を更新しました -SAVE_MANAGER_HAKCHI_GAMES=Hakchiのゲーム数 -SAVE_MANAGER_HAKCHI_NAMED=名前がわかったセーブ -SAVE_MANAGER_HAKCHI_PARTIAL_NOTE=一致しないセーブはゲームコードで表示されます。 diff --git a/mod/etc/options_menu/language/nl-NL.lang b/mod/etc/options_menu/language/nl-NL.lang index 0f71588..67b789a 100644 --- a/mod/etc/options_menu/language/nl-NL.lang +++ b/mod/etc/options_menu/language/nl-NL.lang @@ -124,13 +124,3 @@ HINT_OK=OK HINT_MOVE=Verplaatsen HINT_RESTORE=Herstellen SAVE_MANAGER_SELECT_ALL=Alles selecteren -HINT_GAME_NAMES=Spelnamen -SAVE_MANAGER_HAKCHI_SEARCHING=Hakchi zoeken... -SAVE_MANAGER_HAKCHI_SCANNING=Spellen scannen... -SAVE_MANAGER_HAKCHI_NOT_FOUND=Hakchi niet gevonden op de USB. -SAVE_MANAGER_HAKCHI_NOT_FOUND_NOTE=Kopieer een draagbare Hakchi CE-map naar de USB en probeer het opnieuw. -SAVE_MANAGER_HAKCHI_FAILED=Kan de Hakchi-spellen niet lezen. -SAVE_MANAGER_HAKCHI_DONE=Spelnamen bijgewerkt -SAVE_MANAGER_HAKCHI_GAMES=Spellen in Hakchi -SAVE_MANAGER_HAKCHI_NAMED=Opslag met naam -SAVE_MANAGER_HAKCHI_PARTIAL_NOTE=Opslag zonder overeenkomst wordt getoond met de spelcode. diff --git a/mod/etc/options_menu/language/pt-PT.lang b/mod/etc/options_menu/language/pt-PT.lang index d909f6d..ccb097f 100644 --- a/mod/etc/options_menu/language/pt-PT.lang +++ b/mod/etc/options_menu/language/pt-PT.lang @@ -124,13 +124,3 @@ HINT_OK=OK HINT_MOVE=Mover HINT_RESTORE=Restaurar SAVE_MANAGER_SELECT_ALL=Selecionar tudo -HINT_GAME_NAMES=Nomes dos Jogos -SAVE_MANAGER_HAKCHI_SEARCHING=A procurar o Hakchi... -SAVE_MANAGER_HAKCHI_SCANNING=A analisar os jogos... -SAVE_MANAGER_HAKCHI_NOT_FOUND=O Hakchi não foi encontrado na USB. -SAVE_MANAGER_HAKCHI_NOT_FOUND_NOTE=Copie uma pasta portátil do Hakchi CE para a USB e tente novamente. -SAVE_MANAGER_HAKCHI_FAILED=Não foi possível ler os jogos do Hakchi. -SAVE_MANAGER_HAKCHI_DONE=Nomes dos jogos atualizados -SAVE_MANAGER_HAKCHI_GAMES=Jogos no Hakchi -SAVE_MANAGER_HAKCHI_NAMED=Jogos guardados com nome -SAVE_MANAGER_HAKCHI_PARTIAL_NOTE=Os jogos guardados sem correspondência são mostrados pelo código do jogo. diff --git a/mod/etc/options_menu/language/ru-RU.lang b/mod/etc/options_menu/language/ru-RU.lang index 85731b5..e3dbf61 100644 --- a/mod/etc/options_menu/language/ru-RU.lang +++ b/mod/etc/options_menu/language/ru-RU.lang @@ -124,13 +124,3 @@ HINT_OK=OK HINT_MOVE=Переместить HINT_RESTORE=Восстановить SAVE_MANAGER_SELECT_ALL=Выбрать все -HINT_GAME_NAMES=Названия игр -SAVE_MANAGER_HAKCHI_SEARCHING=Поиск Hakchi... -SAVE_MANAGER_HAKCHI_SCANNING=Сканирование игр... -SAVE_MANAGER_HAKCHI_NOT_FOUND=Hakchi не найден на USB. -SAVE_MANAGER_HAKCHI_NOT_FOUND_NOTE=Скопируйте портативную папку Hakchi CE на USB и повторите попытку. -SAVE_MANAGER_HAKCHI_FAILED=Не удалось прочитать игры Hakchi. -SAVE_MANAGER_HAKCHI_DONE=Названия игр обновлены -SAVE_MANAGER_HAKCHI_GAMES=Игр в Hakchi -SAVE_MANAGER_HAKCHI_NAMED=Сохранений с названием -SAVE_MANAGER_HAKCHI_PARTIAL_NOTE=Сохранения без совпадения показаны по коду игры. diff --git a/mod/etc/options_menu/scripts/OrphanSaves.sh b/mod/etc/options_menu/scripts/OrphanSaves.sh index 669a996..47a9c93 100644 --- a/mod/etc/options_menu/scripts/OrphanSaves.sh +++ b/mod/etc/options_menu/scripts/OrphanSaves.sh @@ -22,8 +22,6 @@ # OrphanSaves.sh restore FILE move them back (never over an existing save) # OrphanSaves.sh backup entries in the USB backup # OrphanSaves.sh restore-backup FILE copy FILE's entries back from the USB backup -# OrphanSaves.sh hakchi portable Hakchi CE folders found on the USB drive -# OrphanSaves.sh names FILE cache the games of the Hakchi folders in FILE # OrphanSaves.sh merge-names FILE add the entryname lines in FILE to the cache # OrphanSaves.sh merge-backup-names add the names in the USB backup, keeping newer ones # OrphanSaves.sh sync-names add the games installed on the console to the cache @@ -146,11 +144,6 @@ mergeBackupNames() { mergeNames "$backup_root/game_names.txt" older } -# game folders are pruned so the walk stays shallow on big libraries -hakchiRoots() { - find /media -maxdepth 4 -type d \( -name 'games*' -o -name saves -o -name '.*' \) -prune -o -type f -name '[Hh][Aa][Kk][Cc][Hh][Ii].[Ee][Xx][Ee]' -print 2>/dev/null | sed 's#/[^/]*$##' | sort -u -} - # entryname per game folder under the folders on stdin; RetroArch names from files and Exec= gameNames() { local dir @@ -194,13 +187,6 @@ gameNames() { }' | awk -F '\t' '!seen[$1]++' } -hakchiNames() { - local root - while IFS= read -r root; do - printf '%s\n' "$root"/games* - done < "$1" | gameNames -} - installedNames() { local games_root games_root="$(dirname "$(findGameStorage)")" @@ -231,7 +217,6 @@ addNames() { "$@" > "$found" mergeNames "$found" status=$? - [ $status -eq 0 ] && [ "$1" = hakchiNames ] && { grep -c "^CLV-" "$found" || true; } rm -f "$found" return $status } @@ -261,12 +246,6 @@ case "$1" in restore-backup) [ -f "$2" ] && restoreFromBackup "$2" ;; - hakchi) - hakchiRoots - ;; - names) - [ -s "$2" ] && addNames hakchiNames "$2" - ;; merge-names) [ -f "$2" ] && mergeNames "$2" ;; @@ -280,7 +259,7 @@ case "$1" in cat "$names_cache" 2>/dev/null ;; *) - echo "usage: $0 stats|orphans|deleted|move FILE|restore FILE|backup|restore-backup FILE|hakchi|names FILE|merge-names FILE|merge-backup-names|sync-names|cached-names" >&2 + echo "usage: $0 stats|orphans|deleted|move FILE|restore FILE|backup|restore-backup FILE|merge-names FILE|merge-backup-names|sync-names|cached-names" >&2 exit 2 ;; esac diff --git a/pc/push_game_names.ps1 b/pc/push_game_names.ps1 index bdddaa2..5dcf13e 100644 --- a/pc/push_game_names.ps1 +++ b/pc/push_game_names.ps1 @@ -63,7 +63,7 @@ if (-not (Test-Library $HakchiPath)) { Stop-WithError 'Hakchi CE games not found. Put this script next to hakchi.exe, open Hakchi CE first, or pass --hakchi-path with the folder that has the games folders.' } -# same rules as OrphanSaves.sh hakchiNames: code -> Name=, RetroArch content -> name of its game +# same rules as OrphanSaves.sh gameNames: code -> Name=, RetroArch content -> name of its game $utf8 = New-Object System.Text.UTF8Encoding($false) $gameNames = New-Object 'System.Collections.Generic.Dictionary[string,string]' $seen = New-Object 'System.Collections.Generic.HashSet[string]' diff --git a/src/framework/list_jump.cpp b/src/framework/list_jump.cpp new file mode 100644 index 0000000..dbbe2c1 --- /dev/null +++ b/src/framework/list_jump.cpp @@ -0,0 +1,130 @@ +// Copyright (C) 2026 DefKorns +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License +// as published by the Free Software Foundation; either version 3 +// of the License, or (at your option) any later version. + +#include "list_jump.h" +#include "controller.h" + +#include +#include + +namespace +{ + const unsigned int LetterHoldMs = 450; + const unsigned int LetterRepeatMs = 200; + // only the press fires a page, not the key repeat that follows + const unsigned int PageTapMs = 200; + + // first ASCII letter, upper-cased; digits and anything else share '#' + char Initial(const std::string & label) + { + for(char c : label) + if(std::isalpha(static_cast(c))) + return static_cast(std::toupper(static_cast(c))); + else if(std::isdigit(static_cast(c))) + break; + return '#'; + } + + int GroupStart(const std::vector & labels, int index) + { + const char initial = Initial(labels[index]); + while(index > 0 && Initial(labels[index - 1]) == initial) + --index; + return index; + } +} + +ListJump ListJumpInput::Poll(Controller & controller, unsigned int nowMs) +{ + // every timer is read each frame: they only reset when sampled while released + const bool lPressed = controller.HeldRepeat(L); + const bool rPressed = controller.HeldRepeat(R); + const bool leftStep = controller.HeldRepeat(LEFT); + const bool rightStep = controller.HeldRepeat(RIGHT); + const unsigned int leftHeldMs = controller.HeldMillis(LEFT); + const unsigned int rightHeldMs = controller.HeldMillis(RIGHT); + + if(lPressed) + return ListJump::LetterPrev; + if(rPressed) + return ListJump::LetterNext; + + auto Dpad = [&](bool step, unsigned int heldMs, ListJump page, ListJump letter) + { + if(heldMs >= LetterHoldMs) + { + if(nowMs - lastLetterJumpMs < LetterRepeatMs) + return ListJump::None; + lastLetterJumpMs = nowMs; + return letter; + } + return (step && heldMs < PageTapMs) ? page : ListJump::None; + }; + const ListJump left = Dpad(leftStep, leftHeldMs, ListJump::PageUp, ListJump::LetterPrev); + return left != ListJump::None ? left : Dpad(rightStep, rightHeldMs, ListJump::PageDown, ListJump::LetterNext); +} + +int LetterJumpRow(const std::vector & labels, int firstRow, int row, int direction) +{ + const int count = static_cast(labels.size()); + if(count == 0) + return row; + + if(direction > 0) + { + if(row < firstRow) + return firstRow; + const int index = row - firstRow; + const char initial = Initial(labels[index]); + for(int i = index + 1; i < count; ++i) + if(Initial(labels[i]) != initial) + return firstRow + i; + return firstRow + count - 1; + } + + if(row <= firstRow) + return 0; + const int index = row - firstRow; + const int start = GroupStart(labels, index); + if(index > start) + return firstRow + start; + return start == 0 ? 0 : firstRow + GroupStart(labels, start - 1); +} + +void ApplyListJump(ListJump jump, const std::vector & labels, int firstRow, int displayCount, int & listOffset, int & currentId) +{ + const int totalRows = static_cast(labels.size()) + firstRow; + if(jump == ListJump::None || totalRows <= 0 || displayCount <= 0) + return; + const int maxOffset = std::max(0, totalRows - displayCount); + const int row = listOffset + currentId; + + if(jump == ListJump::LetterPrev || jump == ListJump::LetterNext) + { + const int target = LetterJumpRow(labels, firstRow, row, jump == ListJump::LetterNext ? 1 : -1); + // a target already on screen only moves the cursor, otherwise it goes to the top + if(target < listOffset || target >= listOffset + displayCount) + listOffset = std::min(maxOffset, target); + currentId = target - listOffset; + return; + } + + const int delta = jump == ListJump::PageDown ? displayCount : -displayCount; + const int target = std::min(totalRows - 1, std::max(0, row + delta)); + listOffset = std::min(maxOffset, std::max(0, listOffset + delta)); + currentId = target - listOffset; + if(currentId < 0) + { + listOffset = target; + currentId = 0; + } + else if(currentId >= displayCount) + { + listOffset = target - displayCount + 1; + currentId = displayCount - 1; + } +} diff --git a/src/framework/list_jump.h b/src/framework/list_jump.h new file mode 100644 index 0000000..4c7b9aa --- /dev/null +++ b/src/framework/list_jump.h @@ -0,0 +1,37 @@ +// Copyright (C) 2026 DefKorns +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License +// as published by the Free Software Foundation; either version 3 +// of the License, or (at your option) any later version. + +#ifndef LIST_JUMP_H_ +#define LIST_JUMP_H_ + +#include +#include + +class Controller; + +enum class ListJump { None, PageUp, PageDown, LetterPrev, LetterNext }; + +// L/R walk a list by initial letter; D-pad Left/Right page on a tap and walk +// the letters while held. Poll every frame, it keeps the held timers of the +// buttons it reads. +class ListJumpInput +{ +public: + ListJump Poll(Controller & controller, unsigned int nowMs); + +private: + unsigned int lastLetterJumpMs = 0; +}; + +// Row (an index into the whole list) of the previous or next initial letter. +// labels[i] is row firstRow+i; rows before firstRow are not entries (select-all). +int LetterJumpRow(const std::vector & labels, int firstRow, int row, int direction); + +// Moves listOffset and currentId (a row inside the window of displayCount rows). +void ApplyListJump(ListJump jump, const std::vector & labels, int firstRow, int displayCount, int & listOffset, int & currentId); + +#endif diff --git a/src/mod_uninstall.cpp b/src/mod_uninstall.cpp index e6a4af6..6c89ca5 100644 --- a/src/mod_uninstall.cpp +++ b/src/mod_uninstall.cpp @@ -13,6 +13,7 @@ #include "framework/controller.h" #include "framework/powerwatch.h" #include "framework/draw_helpers.h" +#include "framework/list_jump.h" #include "framework/utf8.h" #include "localization.h" @@ -468,6 +469,7 @@ int main(int argc, char * argv[]) return true; }; + ListJumpInput jumpInput; for(;;) { SDL_Event e; @@ -479,6 +481,7 @@ int main(int argc, char * argv[]) if(sdl_context.powerwatch->buttonPress()) return ExitLeaveMenu; + const ListJump jump = jumpInput.Poll(controller, SDL_GetTicks()); if(controller.HeldRepeat(UP) && !mods.empty()) { if(currentId > 0) @@ -493,6 +496,13 @@ int main(int argc, char * argv[]) else if(listOffset+displayCount < modCount) ++listOffset; } + else if(jump != ListJump::None && !mods.empty()) + { + std::vector labels; + for(const Mod & mod : mods) + labels.push_back(mod.name); + ApplyListJump(jump, labels, 0, displayCount, listOffset, currentId); + } else if(controller.GetButtonStatus(A) && !mods.empty()) { Mod & mod = mods[listOffset + currentId]; diff --git a/src/save_manager.cpp b/src/save_manager.cpp index 6f34d19..3a8993a 100644 --- a/src/save_manager.cpp +++ b/src/save_manager.cpp @@ -14,6 +14,7 @@ #include "framework/controller.h" #include "framework/powerwatch.h" #include "framework/draw_helpers.h" +#include "framework/list_jump.h" #include "framework/utf8.h" #include "localization.h" @@ -32,7 +33,6 @@ namespace const std::string OptionsLocation = "/etc/options_menu/"; const std::string ScriptLocation = OptionsLocation + "scripts/"; const std::string SelectionFile = "/tmp/save_manager_selection"; - const std::string HakchiRootsFile = "/tmp/save_manager_hakchi"; const std::string RetroArchPrefix = "retroarch/"; // narrower than the shared theme to fit the detail panels @@ -184,7 +184,7 @@ namespace return textures; } - // entryname lines from the last Hakchi scan + // entryname lines from the name cache GameNames LoadGameNames() { GameNames names; @@ -389,7 +389,7 @@ int main(int, char **) return false; if(confirm && controller.PeekButtonStatus(START) && controller.PeekButtonStatus(SELECT)) { - // still held on return; Select alone is the Hakchi names shortcut + // still held on return controller.GetButtonStatus(START); controller.GetButtonStatus(SELECT); return true; @@ -459,7 +459,6 @@ int main(int, char **) std::vector noneHintLines = MakeLines(WrapToWidth(Translate("SAVE_MANAGER_NONE_HINT"), PanelFontSize, PanelInnerW), PanelFontSize, renderer, UiTheme::TextDim); std::vector noteLines = NoteLines(list.noteKey); Badge actionPill = badges.MakePill("Start", ""); - Badge namesPill = badges.MakePill("Select", Translate("HINT_GAME_NAMES")); GameNames gameNames = LoadGameNames(); std::vector entries; @@ -601,48 +600,21 @@ int main(int, char **) rightEdge = DrawBadge(badgeBack, rightEdge); if(markedCount > 0) rightEdge = DrawBadge(actionPill, rightEdge); - rightEdge = DrawBadge(namesPill, rightEdge); DrawFooterDivider(rightEdge); }; - // read-only: names from a portable Hakchi CE folder on USB - auto RefreshNamesFromHakchi = [&]() + auto Labels = [&]() { - ShowBusy(DrawScreen, "SAVE_MANAGER_HAKCHI_SEARCHING"); - const std::vector roots = ReadLines(OrphanSavesCommand("hakchi")); - if(roots.empty()) - { - DropPendingInput(); - RunDialog(DrawScreen, "SAVE_MANAGER_HAKCHI_NOT_FOUND", {}, "SAVE_MANAGER_HAKCHI_NOT_FOUND_NOTE", false); - return; - } - - ShowBusy(DrawScreen, "SAVE_MANAGER_HAKCHI_SCANNING"); - { - std::ofstream rootsFile(HakchiRootsFile); - for(const std::string & root : roots) - rootsFile << root << "\n"; - } - const std::vector output = ReadLines(OrphanSavesCommand("names " + HakchiRootsFile)); - std::remove(HakchiRootsFile.c_str()); - DropPendingInput(); - if(output.empty()) - { - RunDialog(DrawScreen, "SAVE_MANAGER_HAKCHI_FAILED", {}, "", false); - return; - } - - gameNames = LoadGameNames(); - ApplyNames(); - const int total = static_cast(entries.size()); - const int named = static_cast(std::count_if(entries.begin(), entries.end(), [](const SaveEntry & entry) { return entry.named; })); - RunDialog(DrawScreen, "SAVE_MANAGER_HAKCHI_DONE", - { Translate("SAVE_MANAGER_HAKCHI_GAMES") + ": " + output.back(), - Translate("SAVE_MANAGER_HAKCHI_NAMED") + ": " + std::to_string(named) + " / " + std::to_string(total) }, - named < total ? "SAVE_MANAGER_HAKCHI_PARTIAL_NOTE" : "", false); + std::vector labels; + for(const SaveEntry & entry : entries) + labels.push_back(entry.label); + return labels; }; + ListJumpInput jumpInput; Load(); + if(entries.empty()) + return ExitBack; for(;;) { SDL_Event e; @@ -654,6 +626,7 @@ int main(int, char **) if(sdl_context.powerwatch->buttonPress()) return ExitLeaveMenu; + const ListJump jump = jumpInput.Poll(controller, SDL_GetTicks()); const int entryCount = static_cast(entries.size()); const int rowCount = entryCount + selectAllRows; if(controller.HeldRepeat(UP)) @@ -670,6 +643,8 @@ int main(int, char **) else if(listOffset+displayCount < rowCount) ++listOffset; } + else if(jump != ListJump::None) + ApplyListJump(jump, Labels(), selectAllRows, displayCount, listOffset, currentId); else if(controller.GetButtonStatus(A) && entryCount > 0) { const int row = listOffset + currentId; @@ -685,8 +660,6 @@ int main(int, char **) } else if(controller.GetButtonStatus(B)) return ExitBack; - else if((controller.GetButtonStatus(SELECT) || controller.GetButtonStatus(Y)) && !controller.PeekButtonStatus(START)) - RefreshNamesFromHakchi(); else if(controller.GetButtonStatus(START) && markedCount > 0) { auto marked = Marked(); @@ -715,6 +688,9 @@ int main(int, char **) Load(); if(fromBackup) RunDialog(DrawScreen, ok ? "RESTORE_SAVES_USB_DONE" : "RESTORE_SAVES_USB_FAILED", {}, "", false); + // an empty list would still show a checked "select all" + if(entries.empty()) + return ExitBack; } }

&8aez&h|v%m@@aHj-#$|wD|gXKz#d-S5MnP^MX_@ zE-z1e{tU15l>uBICBH81+QQamBn(UGoh-Yr5PY&ZrWoXFN*I8P!Dm&RgBb?Dy;W%V^2R# zHj#33baeC;MHJ;_&*4%>@KU?~NB3zdy9Q@NxI0(6Q?w`)g$=d@J$cJkD*jkYac2D% zAH{ob#u=-tNABj|JGQVGRMF6tt($~;mR{?jz4z4Gp-WAc=Z%}LEMuxLs+`|k9SClusYNyWs=wb-iT#duxWkMP#y z)yjt^n&LbCj-wX-NbcPELSZoN$4i?8WoA%4uF)v=D?X_=T?Dz1sJW@iIO6Ib`vS%< zkR9}9CS2^A^;?gg1_7%|qEk(%B51;{R55(t8Svq}Vhr`n7*7vy>`s)__@$YL+M@6%4Ga)v{ z@@`2t;s`@}{&5+0L=AU=YRH)ao6xOD_?_72PA3t&KSfP((;iD*Yq2*V5m&#Ne07@n zbh?DsbCq0}IAu|U*Dgm6Jt4`WxOzx~EO!PQX2xdS8S$W3U_k@|=VFh%d4hnZ<`3N(*4|dyur$5xC*n+n$GEDOGFtM<|CtwjuLP)9oPg#sW~e-VHj?A*68& zjhE{*6Q1tP1<#v}-(IyN2Znq!M06xsCtlqC;$_#H9>##A$L+^pN-eU!rH7NP@)Srp zI3{}_#}J~}QF5QjA)_PFH>V$jrUO#O1`9M5C08L!3anlxuNPliY-w`~v2^bMgY4OB zk|QrAcQ?pn=jksW02b^MaoDv`mDKgY_nL~n@s^osqm!WHhqlGh_9Gi58|k+NfS@8w zfRpmJE!SX&X4Oka&c1RE>38(arqq#e{)V?_yoSp$4x4s@)7q6%5Zl6w2Ifq@A3{Ti z;J9e|QYNg4c0O+=?q==AbL#7u4d~NI6(frOufdrNIR~Bogz!?o!iLjR7MQy-qp#Xk zjO-i=tUS|CYAbPL}E$P%MC^yZodCzASh2ZCq{}^BH4fcOdzNjE$5ay25ZUaM`HU%Lzuf)$dV`_I)=#x zwEHSU!hSvM>0B}KfY`g!3mjLxXI@|>D*YQijEA*Jso`rBaZ`&|)R~`dH5A|>`(22+ z63;Fz9!@`1131aKK&#Yvbvx)A4RCh9t(-V*sf-6!?yX7++0(n|??9o@8}mymth>an z6wm-Pf`TYC8$7>WwRS!Ktnwm$yuV`Y%fJn+2EBca*xMO0%eSNaYaLdRXn*STDALXm|{`mRj7e-@oKt6-L6xy%pkB=z&5&OiU!SK1gw0(IDR-EiY3_S(F zWj>fL+;1j^FYg`x(50$U$!p~G0o5g8EG(Mh2!WZ3;BBfHV? z;GEd+f8C$_1)(2eTp!LvS1pq%q&qF&e;&b$W(7pVzH{|z3H0R2iF93{G1~d8_ufho bt*)3Eis66jmDha!Jq!Rs*9cmpZ6EbN+{oGl diff --git a/mod/etc/options_menu/images/preview_shutdown.png b/mod/etc/options_menu/images/preview_shutdown.png index 0595c77a5c1ae986a34593889fe1548a023dcbb4..fcfaf0c350dde2c4f47bb69862d4e7ed1a56f4cc 100644 GIT binary patch literal 4035 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 diff --git a/mod/etc/options_menu/images/ui/gear.png b/mod/etc/options_menu/images/ui/gear.png index ed033a84ff1b7a5711539fd36e575b97633d7da4..cdb23f2172d584b94ec57b076275e49cb2e19a56 100644 GIT binary patch delta 749 zcmV1&Z45rR9TJ$Bdd1*7{0W!2p>nWKEWDhzt zZ{b%+_6(`hF-wXT?vNpCrYy~K6#etrUqR<%%F^=n96LY>2&a>%j}%2xl*kGM;E}df zFqS3&2|yRX0Kg5~fMaQ$f;s>)fXaP+d>KK>^ z)7J%atL6&uO+-G6$Ti2GiO6#ic|y~BA%-!c@QOThyU&(H=*i|lad+ncdWH2p03QmL zFpR?}r$_*H0elakW@h|o3C4he;u>b3!WzR-%-yEge}4nuqHu-=@D;#@2`^cY-wpD{ zTw!!US$}c@ZZDH4Fq-hpcG+M(bKd-)4v%~Yk7eu&5kL#IovOWE#eN@>xY5y+!qDZyH{wo6$jGui)z6W%in zJB2W;mVXYzH7BC;!28ccl&zCes#(qG`CLT-Ore^JUy8^!fKf=rFPMszr{vsM#7-$C zjfBQHJ}ty?o6D$P_A7;?CO&leT4bsUWG{+-6OX)v72R(tSeExoGFsC^K1`}MPqUQLRP)5`@N5Ui?(VSMe#|+&4{BYrwx^lQTjSv$ fr!AFP`FZ{u0t;7Zdkl(%00000NkvXXu0mjfpW|m^ literal 1739 zcmbVNe{9rL9Ph%0fD8yzwk#nwWsVK$wb#3DySkIj-QW(q{k*tM7u~z|ZC7{Kc5S!2 zoy37=kWB={DZ|Z0{&6}`=lqogl{tUKI28;K1=QR4kr;lMg&z@v;_L11f{Gex(!STe z&-;A7KVQ2l%GRXL%bbT{SgOnEs6r!d+;e85rxp%hM8jOgS*Kyxt!c)cggxJ%fnmu< zMNh3>>%NcU<$wtYvKN{{0R^EktZ-FG0emymaW8BVB|Gu+zWoF)3U;D~b<=L81o}j0 zScTQ$G7lea=4}G8>K?o>#36zJ)Bzp}_$7@C*@-AGhvvp-iol}~z1dC_8-jSPy8i%C|SD6`FGqiBX=7!n~!tzFVVh?KNtaRvv}cvVz%QI>Fn5qRZR-A*7= zXM6}K30g^u#R)}>3IT;On`pzQC{W-NIHgtfN5uu6f_@l)lCB}FIe}GtvMy^r`5&kg z%YPC;v30u>8aK2h5J*U9dTASSBNmVwqBT#u0;wvf$*n36OWTl{%M5Q6u0(}EmsO7} z`{S9ah?|TvtjU7s)QFNG2esVmHb4i^p`Ack%OhzUNf&rf%FG3vIsbtflv@_W#`eFV z7>1+sIV&4Sp-K^e4*m@%@LZ#;1_1I<41gv`DN+-GC*sDH$bMNxa>yJ@RNL+5T#}{( ziH9zSoj^{SL{Z?pR$$9(EMQ3h^C4+vt$_3jG?xVl%&pN7K#$-Y91~!5evm0ulQx-gE?JXoEh8 zNN=3q8H6S`>Or8w;wBWR|12E}*-aTeH{$~JSMQ>0*Y#%{j+)+>CXO5Qk~pxSgia|H z9Y+m?5go&lcDfwJp4!gCHD8pCS1hvk4j=HX=*m2@J>!mbm#<>i!0|UW4PNKu0G2)?byV{AC@@1ecygJ^lQh%MGMFF)yjv7 z&f6btNFU4k^juPJBiVC0wdc0;EAAXT^?Qoz>)Ba9I@_EB+xI@RK5I0>EScJ~&c8;z zWnj~(f}W3avb$gI44o}{#3a_)mE;IJeF=VK-cZ^9R=6Sg#rIcFzmvTntNg=^u@6pk zJbh+Y+J&UZm>RCGm{nn2|1LI!ef7!ExR|q+Tw1pI@>8>$3v8*&2IW&eVb@9aQso(L zYIp0i?{ywpxtxp)oSX=Cm+vUnc;E4Jn-*fH zSD($@fR}z*|cjeYs*A-cy^iuBd@IiibQ|^YluWtFN_PpiE lS$nSa?oAy%mYTm+Ui8~zS3X*Y0h2{13)DVvPU* From 9977ebf2f15adf23145407ef26f746a2f03176f1 Mon Sep 17 00:00:00 2001 From: DefKorns Date: Thu, 24 Sep 2026 23:56:43 +0000 Subject: [PATCH 045/104] Add language code badges as language previews --- mod/etc/options_menu/images/preview_lang_de.png | Bin 0 -> 4233 bytes mod/etc/options_menu/images/preview_lang_en.png | Bin 0 -> 4037 bytes mod/etc/options_menu/images/preview_lang_es.png | Bin 0 -> 4727 bytes mod/etc/options_menu/images/preview_lang_fr.png | Bin 0 -> 4262 bytes mod/etc/options_menu/images/preview_lang_it.png | Bin 0 -> 2943 bytes mod/etc/options_menu/images/preview_lang_ja.png | Bin 0 -> 4572 bytes mod/etc/options_menu/images/preview_lang_nl.png | Bin 0 -> 3777 bytes mod/etc/options_menu/images/preview_lang_pt.png | Bin 0 -> 3775 bytes mod/etc/options_menu/images/preview_lang_ru.png | Bin 0 -> 4697 bytes .../options_menu/language/commands/c0010_English | 2 +- .../options_menu/language/commands/c0020_French | 2 +- .../options_menu/language/commands/c0030_German | 2 +- .../options_menu/language/commands/c0040_Spanish | 2 +- .../options_menu/language/commands/c0050_Italian | 2 +- .../options_menu/language/commands/c0060_Dutch | 2 +- .../language/commands/c0070_Portuguese | 2 +- .../options_menu/language/commands/c0080_Russian | 2 +- .../language/commands/c0090_Japanese | 2 +- 18 files changed, 9 insertions(+), 9 deletions(-) create mode 100644 mod/etc/options_menu/images/preview_lang_de.png create mode 100644 mod/etc/options_menu/images/preview_lang_en.png create mode 100644 mod/etc/options_menu/images/preview_lang_es.png create mode 100644 mod/etc/options_menu/images/preview_lang_fr.png create mode 100644 mod/etc/options_menu/images/preview_lang_it.png create mode 100644 mod/etc/options_menu/images/preview_lang_ja.png create mode 100644 mod/etc/options_menu/images/preview_lang_nl.png create mode 100644 mod/etc/options_menu/images/preview_lang_pt.png create mode 100644 mod/etc/options_menu/images/preview_lang_ru.png diff --git a/mod/etc/options_menu/images/preview_lang_de.png b/mod/etc/options_menu/images/preview_lang_de.png new file mode 100644 index 0000000000000000000000000000000000000000..ac5b4da9a8cca64488c6a45a8b0fc3a8d3027140 GIT binary patch literal 4233 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 mbpAKnPz9fS!)Ac8!*E?Ee5%bO`tW literal 0 HcmV?d00001 diff --git a/mod/etc/options_menu/images/preview_lang_en.png b/mod/etc/options_menu/images/preview_lang_en.png new file mode 100644 index 0000000000000000000000000000000000000000..f5ba3544b79c55741e327588d7092b6486d67b7f GIT binary patch literal 4037 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 G_kRG;9o~}w literal 0 HcmV?d00001 diff --git a/mod/etc/options_menu/images/preview_lang_fr.png b/mod/etc/options_menu/images/preview_lang_fr.png new file mode 100644 index 0000000000000000000000000000000000000000..ae34492c4a301cb1e1a00c77987996804c6c7cbb GIT binary patch literal 4262 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 literal 0 HcmV?d00001 diff --git a/mod/etc/options_menu/images/preview_lang_pt.png b/mod/etc/options_menu/images/preview_lang_pt.png new file mode 100644 index 0000000000000000000000000000000000000000..37905e1d4b209fae51ee206a63977b67de1e6462 GIT binary patch literal 3775 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 f&@U=#I8zaIjRjHYT?H-Wq^ZI`P3$`OV literal 0 HcmV?d00001 diff --git a/mod/etc/options_menu/images/preview_lang_ru.png b/mod/etc/options_menu/images/preview_lang_ru.png new file mode 100644 index 0000000000000000000000000000000000000000..da2b247f5dc8264a93374530d78171d4e8747dd5 GIT binary patch literal 4697 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 literal 0 HcmV?d00001 diff --git a/mod/etc/options_menu/language/commands/c0010_English b/mod/etc/options_menu/language/commands/c0010_English index 852e729..a62d845 100644 --- a/mod/etc/options_menu/language/commands/c0010_English +++ b/mod/etc/options_menu/language/commands/c0010_English @@ -2,4 +2,4 @@ COMMAND_NAME=English COMMAND_TYPE=EXTERNAL RESTART_UI=FALSE COMMAND_STR=echo en-US > /etc/options_menu/language.cfg && usleep 50000 && %options_path%/options --commandPath %options_path%/language/commands/ --title "LANGUAGE_OPTIONS" & -PREVIEW_IMAGE=/etc/options_menu/images/preview_placeholder.png +PREVIEW_IMAGE=/etc/options_menu/images/preview_lang_en.png diff --git a/mod/etc/options_menu/language/commands/c0020_French b/mod/etc/options_menu/language/commands/c0020_French index c3f0363..e67543f 100644 --- a/mod/etc/options_menu/language/commands/c0020_French +++ b/mod/etc/options_menu/language/commands/c0020_French @@ -2,4 +2,4 @@ COMMAND_NAME=Français COMMAND_TYPE=EXTERNAL RESTART_UI=FALSE COMMAND_STR=echo fr-FR > /etc/options_menu/language.cfg && usleep 50000 && %options_path%/options --commandPath %options_path%/language/commands/ --title "LANGUAGE_OPTIONS" & -PREVIEW_IMAGE=/etc/options_menu/images/preview_placeholder.png +PREVIEW_IMAGE=/etc/options_menu/images/preview_lang_fr.png diff --git a/mod/etc/options_menu/language/commands/c0030_German b/mod/etc/options_menu/language/commands/c0030_German index d10440e..1348444 100644 --- a/mod/etc/options_menu/language/commands/c0030_German +++ b/mod/etc/options_menu/language/commands/c0030_German @@ -2,4 +2,4 @@ COMMAND_NAME=Deutsch COMMAND_TYPE=EXTERNAL RESTART_UI=FALSE COMMAND_STR=echo de-DE > /etc/options_menu/language.cfg && usleep 50000 && %options_path%/options --commandPath %options_path%/language/commands/ --title "LANGUAGE_OPTIONS" & -PREVIEW_IMAGE=/etc/options_menu/images/preview_placeholder.png +PREVIEW_IMAGE=/etc/options_menu/images/preview_lang_de.png diff --git a/mod/etc/options_menu/language/commands/c0040_Spanish b/mod/etc/options_menu/language/commands/c0040_Spanish index 1e83d7f..fbc3c04 100644 --- a/mod/etc/options_menu/language/commands/c0040_Spanish +++ b/mod/etc/options_menu/language/commands/c0040_Spanish @@ -2,4 +2,4 @@ COMMAND_NAME=Español COMMAND_TYPE=EXTERNAL RESTART_UI=FALSE COMMAND_STR=echo es-ES > /etc/options_menu/language.cfg && usleep 50000 && %options_path%/options --commandPath %options_path%/language/commands/ --title "LANGUAGE_OPTIONS" & -PREVIEW_IMAGE=/etc/options_menu/images/preview_placeholder.png +PREVIEW_IMAGE=/etc/options_menu/images/preview_lang_es.png diff --git a/mod/etc/options_menu/language/commands/c0050_Italian b/mod/etc/options_menu/language/commands/c0050_Italian index c20b15d..7f2cbfe 100644 --- a/mod/etc/options_menu/language/commands/c0050_Italian +++ b/mod/etc/options_menu/language/commands/c0050_Italian @@ -2,4 +2,4 @@ COMMAND_NAME=Italiano COMMAND_TYPE=EXTERNAL RESTART_UI=FALSE COMMAND_STR=echo it-IT > /etc/options_menu/language.cfg && usleep 50000 && %options_path%/options --commandPath %options_path%/language/commands/ --title "LANGUAGE_OPTIONS" & -PREVIEW_IMAGE=/etc/options_menu/images/preview_placeholder.png +PREVIEW_IMAGE=/etc/options_menu/images/preview_lang_it.png diff --git a/mod/etc/options_menu/language/commands/c0060_Dutch b/mod/etc/options_menu/language/commands/c0060_Dutch index 577f3e7..dd82dc8 100644 --- a/mod/etc/options_menu/language/commands/c0060_Dutch +++ b/mod/etc/options_menu/language/commands/c0060_Dutch @@ -2,4 +2,4 @@ COMMAND_NAME=Nederlands COMMAND_TYPE=EXTERNAL RESTART_UI=FALSE COMMAND_STR=echo nl-NL > /etc/options_menu/language.cfg && usleep 50000 && %options_path%/options --commandPath %options_path%/language/commands/ --title "LANGUAGE_OPTIONS" & -PREVIEW_IMAGE=/etc/options_menu/images/preview_placeholder.png +PREVIEW_IMAGE=/etc/options_menu/images/preview_lang_nl.png diff --git a/mod/etc/options_menu/language/commands/c0070_Portuguese b/mod/etc/options_menu/language/commands/c0070_Portuguese index 977562a..fbfd375 100644 --- a/mod/etc/options_menu/language/commands/c0070_Portuguese +++ b/mod/etc/options_menu/language/commands/c0070_Portuguese @@ -2,4 +2,4 @@ COMMAND_NAME=Português (Europeu) COMMAND_TYPE=EXTERNAL RESTART_UI=FALSE COMMAND_STR=echo pt-PT > /etc/options_menu/language.cfg && usleep 50000 && %options_path%/options --commandPath %options_path%/language/commands/ --title "LANGUAGE_OPTIONS" & -PREVIEW_IMAGE=/etc/options_menu/images/preview_placeholder.png +PREVIEW_IMAGE=/etc/options_menu/images/preview_lang_pt.png diff --git a/mod/etc/options_menu/language/commands/c0080_Russian b/mod/etc/options_menu/language/commands/c0080_Russian index ccd0b49..88002e4 100644 --- a/mod/etc/options_menu/language/commands/c0080_Russian +++ b/mod/etc/options_menu/language/commands/c0080_Russian @@ -2,4 +2,4 @@ COMMAND_NAME=Русский COMMAND_TYPE=EXTERNAL RESTART_UI=FALSE COMMAND_STR=echo ru-RU > /etc/options_menu/language.cfg && usleep 50000 && %options_path%/options --commandPath %options_path%/language/commands/ --title "LANGUAGE_OPTIONS" & -PREVIEW_IMAGE=/etc/options_menu/images/preview_placeholder.png +PREVIEW_IMAGE=/etc/options_menu/images/preview_lang_ru.png diff --git a/mod/etc/options_menu/language/commands/c0090_Japanese b/mod/etc/options_menu/language/commands/c0090_Japanese index 280b3f2..2139390 100644 --- a/mod/etc/options_menu/language/commands/c0090_Japanese +++ b/mod/etc/options_menu/language/commands/c0090_Japanese @@ -2,4 +2,4 @@ COMMAND_NAME=日本語 COMMAND_TYPE=EXTERNAL RESTART_UI=FALSE COMMAND_STR=echo ja-JP > /etc/options_menu/language.cfg && usleep 50000 && %options_path%/options --commandPath %options_path%/language/commands/ --title "LANGUAGE_OPTIONS" & -PREVIEW_IMAGE=/etc/options_menu/images/preview_placeholder.png +PREVIEW_IMAGE=/etc/options_menu/images/preview_lang_ja.png From ccc484a824203e477116b0aaa97be05e97daf341 Mon Sep 17 00:00:00 2001 From: DefKorns Date: Thu, 24 Sep 2026 23:57:10 +0000 Subject: [PATCH 046/104] Add per-command preview icons --- .../advanced_commands/c0020_ChangeCombo | 2 +- .../options_menu/advanced_commands/c0030_USB_RW | 2 +- .../advanced_commands/c0040_ClearCache | 2 +- .../advanced_commands/c0050_ModUninstall | 2 +- .../advanced_commands/c0060_DisplayTemp | 2 +- .../advanced_commands/c0070_DisplayTop | 2 +- .../advanced_commands/c0080_BenchMark | 2 +- .../options_menu/advanced_commands/c0090_RADebug | 2 +- .../advanced_commands/c0100_DumpFile | 2 +- .../commands/c0073_SavedGamesOptions | 2 +- .../options_menu/commands/c0078_LanguageOptions | 2 +- .../options_menu/images/preview_backup_nand.png | Bin 0 -> 4091 bytes .../options_menu/images/preview_backup_usb.png | Bin 0 -> 3397 bytes .../options_menu/images/preview_benchmark.png | Bin 0 -> 5951 bytes .../options_menu/images/preview_change_combo.png | Bin 0 -> 3004 bytes .../options_menu/images/preview_clear_cache.png | Bin 0 -> 2925 bytes .../options_menu/images/preview_dump_file.png | Bin 0 -> 3585 bytes mod/etc/options_menu/images/preview_language.png | Bin 0 -> 3982 bytes .../images/preview_mod_uninstall.png | Bin 0 -> 3392 bytes mod/etc/options_menu/images/preview_net_ip.png | Bin 0 -> 3228 bytes .../images/preview_net_reconnect.png | Bin 0 -> 5315 bytes mod/etc/options_menu/images/preview_net_ssid.png | Bin 0 -> 5328 bytes mod/etc/options_menu/images/preview_ra_debug.png | Bin 0 -> 4063 bytes .../options_menu/images/preview_ra_defaults.png | Bin 0 -> 5205 bytes .../images/preview_ra_defaults_all.png | Bin 0 -> 5133 bytes .../images/preview_ra_delete_backups.png | Bin 0 -> 3679 bytes .../images/preview_ra_delete_bios.png | Bin 0 -> 4792 bytes .../images/preview_ra_delete_overrides.png | Bin 0 -> 4251 bytes .../images/preview_ra_delete_remaps.png | Bin 0 -> 4223 bytes .../images/preview_ra_load_screens.png | Bin 0 -> 3869 bytes .../options_menu/images/preview_ra_sync_bios.png | Bin 0 -> 4620 bytes .../options_menu/images/preview_restore_nand.png | Bin 0 -> 4109 bytes .../options_menu/images/preview_restore_usb.png | Bin 0 -> 3403 bytes .../options_menu/images/preview_saved_games.png | Bin 0 -> 2916 bytes .../images/preview_saves_backup_usb.png | Bin 0 -> 3968 bytes .../images/preview_saves_restore_usb.png | Bin 0 -> 3945 bytes mod/etc/options_menu/images/preview_temp.png | Bin 0 -> 3240 bytes mod/etc/options_menu/images/preview_top.png | Bin 0 -> 3507 bytes mod/etc/options_menu/images/preview_usb_rw.png | Bin 0 -> 2090 bytes .../options_menu/network/commands/c0010_ShowIP | 2 +- .../network/commands/c0011_Reconnect | 2 +- .../options_menu/network/commands/c0012_ShowSSID | 2 +- .../network/commands/c0030_BackupNand | 2 +- .../network/commands/c0040_BackupUsb | 2 +- .../network/commands/c0050_RestoreNand | 2 +- .../network/commands/c0060_RestoreUsb | 2 +- .../retroarch/commands/c0010_DefaultSettingsAll | 2 +- .../retroarch/commands/c0020_DefaultSettings | 2 +- .../retroarch/commands/c0030_BackupNand | 2 +- .../retroarch/commands/c0040_BackupUsb | 2 +- .../retroarch/commands/c0050_RestoreNand | 2 +- .../retroarch/commands/c0060_RestoreUsb | 2 +- .../retroarch/commands/c0061_SyncBios | 2 +- .../retroarch/commands/c0065_DeleteBackups | 2 +- .../retroarch/commands/c0070_DeleteOverrides | 2 +- .../retroarch/commands/c0080_DeleteRemaps | 2 +- .../retroarch/commands/c0082_DeleteBios | 2 +- .../retroarch/commands/c0090_ToggleLoadScreens | 2 +- .../saved_games/commands/c0010_BackupSavesUsb | 2 +- .../saved_games/commands/c0020_RestoreSavesUsb | 2 +- 60 files changed, 32 insertions(+), 32 deletions(-) create mode 100644 mod/etc/options_menu/images/preview_backup_nand.png create mode 100644 mod/etc/options_menu/images/preview_backup_usb.png create mode 100644 mod/etc/options_menu/images/preview_benchmark.png create mode 100644 mod/etc/options_menu/images/preview_change_combo.png create mode 100644 mod/etc/options_menu/images/preview_clear_cache.png create mode 100644 mod/etc/options_menu/images/preview_dump_file.png create mode 100644 mod/etc/options_menu/images/preview_language.png create mode 100644 mod/etc/options_menu/images/preview_mod_uninstall.png create mode 100644 mod/etc/options_menu/images/preview_net_ip.png create mode 100644 mod/etc/options_menu/images/preview_net_reconnect.png create mode 100644 mod/etc/options_menu/images/preview_net_ssid.png create mode 100644 mod/etc/options_menu/images/preview_ra_debug.png create mode 100644 mod/etc/options_menu/images/preview_ra_defaults.png create mode 100644 mod/etc/options_menu/images/preview_ra_defaults_all.png create mode 100644 mod/etc/options_menu/images/preview_ra_delete_backups.png create mode 100644 mod/etc/options_menu/images/preview_ra_delete_bios.png create mode 100644 mod/etc/options_menu/images/preview_ra_delete_overrides.png create mode 100644 mod/etc/options_menu/images/preview_ra_delete_remaps.png create mode 100644 mod/etc/options_menu/images/preview_ra_load_screens.png create mode 100644 mod/etc/options_menu/images/preview_ra_sync_bios.png create mode 100644 mod/etc/options_menu/images/preview_restore_nand.png create mode 100644 mod/etc/options_menu/images/preview_restore_usb.png create mode 100644 mod/etc/options_menu/images/preview_saved_games.png create mode 100644 mod/etc/options_menu/images/preview_saves_backup_usb.png create mode 100644 mod/etc/options_menu/images/preview_saves_restore_usb.png create mode 100644 mod/etc/options_menu/images/preview_temp.png create mode 100644 mod/etc/options_menu/images/preview_top.png create mode 100644 mod/etc/options_menu/images/preview_usb_rw.png diff --git a/mod/etc/options_menu/advanced_commands/c0020_ChangeCombo b/mod/etc/options_menu/advanced_commands/c0020_ChangeCombo index 77aa816..9cd87f7 100644 --- a/mod/etc/options_menu/advanced_commands/c0020_ChangeCombo +++ b/mod/etc/options_menu/advanced_commands/c0020_ChangeCombo @@ -2,4 +2,4 @@ COMMAND_NAME=CHANGE_BUTTON_COMBO COMMAND_TYPE=INTERNAL IGNORE_INTERRUPT=TRUE COMMAND_STR=exec %script_dir%/ChangeCombo -PREVIEW_IMAGE=/etc/options_menu/images/preview_placeholder.png +PREVIEW_IMAGE=/etc/options_menu/images/preview_change_combo.png diff --git a/mod/etc/options_menu/advanced_commands/c0030_USB_RW b/mod/etc/options_menu/advanced_commands/c0030_USB_RW index 1761373..743e17a 100644 --- a/mod/etc/options_menu/advanced_commands/c0030_USB_RW +++ b/mod/etc/options_menu/advanced_commands/c0030_USB_RW @@ -3,4 +3,4 @@ COMMAND_TYPE=INTERNAL USB_ONLY=TRUE COMMAND_STR=sh %script_dir%/USB_RW.sh STATE_STR=sh %script_dir%/USB_RW_state.sh -PREVIEW_IMAGE=/etc/options_menu/images/preview_placeholder.png +PREVIEW_IMAGE=/etc/options_menu/images/preview_usb_rw.png diff --git a/mod/etc/options_menu/advanced_commands/c0040_ClearCache b/mod/etc/options_menu/advanced_commands/c0040_ClearCache index b3b5fef..1a217a8 100644 --- a/mod/etc/options_menu/advanced_commands/c0040_ClearCache +++ b/mod/etc/options_menu/advanced_commands/c0040_ClearCache @@ -1,4 +1,4 @@ COMMAND_NAME=CLEAR_CACHE COMMAND_TYPE=INTERNAL COMMAND_STR=free -m && sync && echo 1 > /proc/sys/vm/drop_caches && free -m -PREVIEW_IMAGE=/etc/options_menu/images/preview_placeholder.png +PREVIEW_IMAGE=/etc/options_menu/images/preview_clear_cache.png diff --git a/mod/etc/options_menu/advanced_commands/c0050_ModUninstall b/mod/etc/options_menu/advanced_commands/c0050_ModUninstall index 8e4e749..0f31d3a 100644 --- a/mod/etc/options_menu/advanced_commands/c0050_ModUninstall +++ b/mod/etc/options_menu/advanced_commands/c0050_ModUninstall @@ -2,4 +2,4 @@ COMMAND_NAME=MODULE_UNINSTALLER COMMAND_TYPE=EXTERNAL RESTART_UI=FALSE COMMAND_STR=%options_path%/mod_uninstall/mod_uninstall && sh %script_dir%/ResumeUI.sh & -PREVIEW_IMAGE=/etc/options_menu/images/preview_placeholder.png +PREVIEW_IMAGE=/etc/options_menu/images/preview_mod_uninstall.png diff --git a/mod/etc/options_menu/advanced_commands/c0060_DisplayTemp b/mod/etc/options_menu/advanced_commands/c0060_DisplayTemp index 7344be0..7039828 100644 --- a/mod/etc/options_menu/advanced_commands/c0060_DisplayTemp +++ b/mod/etc/options_menu/advanced_commands/c0060_DisplayTemp @@ -1,4 +1,4 @@ COMMAND_NAME=DISPLAY_TEMP COMMAND_TYPE=INTERNAL COMMAND_STR=echo "Current Temperature: $(cat "/sys/class/thermal/thermal_zone0/temp")c" -PREVIEW_IMAGE=/etc/options_menu/images/preview_placeholder.png +PREVIEW_IMAGE=/etc/options_menu/images/preview_temp.png diff --git a/mod/etc/options_menu/advanced_commands/c0070_DisplayTop b/mod/etc/options_menu/advanced_commands/c0070_DisplayTop index f8411ed..3fdb39d 100644 --- a/mod/etc/options_menu/advanced_commands/c0070_DisplayTop +++ b/mod/etc/options_menu/advanced_commands/c0070_DisplayTop @@ -1,4 +1,4 @@ COMMAND_NAME=RUN_TOP COMMAND_TYPE=INTERNAL COMMAND_STR=top -b -n 1 -PREVIEW_IMAGE=/etc/options_menu/images/preview_placeholder.png +PREVIEW_IMAGE=/etc/options_menu/images/preview_top.png diff --git a/mod/etc/options_menu/advanced_commands/c0080_BenchMark b/mod/etc/options_menu/advanced_commands/c0080_BenchMark index 9a9ac77..72320a1 100644 --- a/mod/etc/options_menu/advanced_commands/c0080_BenchMark +++ b/mod/etc/options_menu/advanced_commands/c0080_BenchMark @@ -1,4 +1,4 @@ COMMAND_NAME=BENCHMARK_TOOL COMMAND_TYPE=INTERNAL COMMAND_STR=sh %script_dir%/BenchMark.sh -PREVIEW_IMAGE=/etc/options_menu/images/preview_placeholder.png +PREVIEW_IMAGE=/etc/options_menu/images/preview_benchmark.png diff --git a/mod/etc/options_menu/advanced_commands/c0090_RADebug b/mod/etc/options_menu/advanced_commands/c0090_RADebug index 8598175..0acbb52 100644 --- a/mod/etc/options_menu/advanced_commands/c0090_RADebug +++ b/mod/etc/options_menu/advanced_commands/c0090_RADebug @@ -3,4 +3,4 @@ COMMAND_TYPE=EXTERNAL RESTART_UI=FALSE USB_ONLY=TRUE COMMAND_STR=sh %script_dir%/RADebug.sh && sh %script_dir%/ResumeUI.sh & -PREVIEW_IMAGE=/etc/options_menu/images/preview_placeholder.png +PREVIEW_IMAGE=/etc/options_menu/images/preview_ra_debug.png diff --git a/mod/etc/options_menu/advanced_commands/c0100_DumpFile b/mod/etc/options_menu/advanced_commands/c0100_DumpFile index 61d83d7..02f2ad2 100644 --- a/mod/etc/options_menu/advanced_commands/c0100_DumpFile +++ b/mod/etc/options_menu/advanced_commands/c0100_DumpFile @@ -2,4 +2,4 @@ COMMAND_NAME=DUMP_FILE_STRUCTURE COMMAND_TYPE=INTERNAL USB_ONLY=TRUE COMMAND_STR=sh %script_dir%/DumpFile.sh -PREVIEW_IMAGE=/etc/options_menu/images/preview_placeholder.png +PREVIEW_IMAGE=/etc/options_menu/images/preview_dump_file.png diff --git a/mod/etc/options_menu/commands/c0073_SavedGamesOptions b/mod/etc/options_menu/commands/c0073_SavedGamesOptions index 96ab30f..a6f44c9 100644 --- a/mod/etc/options_menu/commands/c0073_SavedGamesOptions +++ b/mod/etc/options_menu/commands/c0073_SavedGamesOptions @@ -4,4 +4,4 @@ RESTART_UI=FALSE SUBMENU=TRUE USB_ONLY=TRUE COMMAND_STR=usleep 50000 && %options_path%/options --commandPath %options_path%/saved_games/commands/ --title "SAVED_GAMES_OPTIONS" & -PREVIEW_IMAGE=/etc/options_menu/images/preview_placeholder.png +PREVIEW_IMAGE=/etc/options_menu/images/preview_saved_games.png diff --git a/mod/etc/options_menu/commands/c0078_LanguageOptions b/mod/etc/options_menu/commands/c0078_LanguageOptions index bb9c302..053f6d5 100644 --- a/mod/etc/options_menu/commands/c0078_LanguageOptions +++ b/mod/etc/options_menu/commands/c0078_LanguageOptions @@ -3,4 +3,4 @@ COMMAND_TYPE=EXTERNAL RESTART_UI=FALSE SUBMENU=TRUE COMMAND_STR=usleep 50000 && %options_path%/options --commandPath %options_path%/language/commands/ --title "LANGUAGE_OPTIONS" & -PREVIEW_IMAGE=/etc/options_menu/images/preview_placeholder.png +PREVIEW_IMAGE=/etc/options_menu/images/preview_language.png diff --git a/mod/etc/options_menu/images/preview_backup_nand.png b/mod/etc/options_menu/images/preview_backup_nand.png new file mode 100644 index 0000000000000000000000000000000000000000..c06d831b3cd0349eecfcbe627db94f72811d735d GIT binary patch literal 4091 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 literal 0 HcmV?d00001 diff --git a/mod/etc/options_menu/images/preview_backup_usb.png b/mod/etc/options_menu/images/preview_backup_usb.png new file mode 100644 index 0000000000000000000000000000000000000000..7aa857b2ef4c5bb6227413834f76e85568d4b570 GIT binary patch literal 3397 zcmb7H_fwOL68#cF3m`o}C_;b$O7YS`Y6udz0-+>;6e$XVf^_Kw2;9(%fGE-go)CH$ zX_1cCtHQhV7Lg{+2-4L&{(v{{&HZ6_XLfdGXU^<7b2b@kVZs50LjeHbFvXy)FY4$; zq_HqvWUcJ=)r&s6DcaCB?D=kyZzJw5{|J5C#RbBR&F%j?w=uqob1bV&y?W;F(U}4m zURm2Zp=a*9@)vqF2{=kY?&``!(D;cU>}?8Z*?(c;EwN!!#xbABn5d<8(G&&s2n0VG&(#srhxsJ7h1R^L^Lknq=8CW{bg7*w#H*#LxrDIbhiy<3nDzawyizX?!YF8b^+ z(U^gGkPf5kGR8?w!hA&#@CuvPW2W2`c)N&b*4qv=&Cpz8rnUgX4ECI^MWh{{sY%`m zdD{?WLL#8ZaE+DH1K@|6I_?bq#!N{U^QcudPMjXlDhxd&riqj>kUz|Kgx@Ry`NIj9 zNu#gh3I=bcJhZVrRLAvL;*|Cox)R1D?zDc|jY*^H+TiS}x8NFl`~$A}5)h=Pr{_~g zX@u|($}gYA+r(Va`}2s2`@YWT;(%ixFhQEEJ*CRyR3it#fG}2y(vPr|gkWg2*o_B} zMt;hJ`I`n3?FAdooqfF7i|e=64q17X;Q>l=kabS*we5iV9vH;k*=1OLSfs+}%khXP z@XKSdZ{0Jbv*FMahy-+YeO?@jizc>3YFtuB?&}aVfP$qp$rSaXReFwh z|5$)Kv;{PPFt0{P$iioFrmH{OZlUp5skJz8H45`)OAW3Ov3~xk8H;#i&T_FtitqRqCuNxMmY zS-H6sXb>lrKXVp*EXh%PxH08 zXDITp0-2Ev?4={&!|c=2%5mM}boKGY3UVZR)dD9fiFkfE@A)wvecb;!{!}recvQjX zPL|mF>CZu(^+!LScT6UIjA!?Z*Pu3{+s6lOK6jOWCA37(bKT0CyFX^iZ1lelh-%KdzSx|Gz!yKs&V8e_SpNHRe5Z#yyByH z0v%nND4&1#RxM#>&Z1+(enLvD=`y&V#pL46jnzyKkZjhG%dDma?kkPasii!sPomA+ zzEUvfcZ?@ITKdSpZ06uDAcmVf@D0fCa=PHrM1-n|NV`}Jl)$`T4$XIe$JN{N%A{u6 zBma;bA+2EQE1NZy>hS`XPPwshCNiJT?C+^`d@nP<>Xf@R)eKPO0)KPrJL>bxUW8Mnx zi^mLiNjHfpr-r}EQD$JAfTK#IGf6ttWF&-lBpA>z@xzXv3^6f1WT1^~{%G3_t?_t6 zpI!)T(!E3F@a9;FJAPyoVRO0r97^zBqNGM?7_t8HR8Wq%gxpJeR;XN6nY zEYOP6@2AnRA`}m?Mf$%i$C%*Zj=iGzGkI*7wi%RTjSg$%sK*Yf{7r9Q9!$eUC|(y6pTgJq{yW-BG=Nft}NLxE#v` zx;p~8!?h^xPxcDjHK8Fwg!3peru|J#mmi&x#?9@(=IZs{YrUzD8WPOThY1p*9pcpF zv(-W)6Y61CAy2mE$q|Vp5vE2#%Cj_S)*dVT+%6Ay=WSdj1C5KI!s{XbN#&7eeaq)h z z%R@HzbE6y+=*tsEY!_&7=Blka^ggULR|7RlKTC4ioe?jNv_14F9(^M}R%dnmtx_n! zX%mK&&S>2Cf2{BId<83hdXJZhO#rP`pLSk;w`DSGGax;6(9Eo%YKLzYf#7stTujrn zE=F+;gaVG`SNe$HG3k?UlDmK0-bo-z#GLRG?4ZG)F7YJmTH&rLC@5yMF%D%HMcn># zB3PtVJfUtll(XP-bb9Yb=u$X2;axZU;GLr}Q#yBpd^_rqgorPrkwHUW4pVG3F(LPYrqJGP0>w%^}9YHw0 zF>C3=Z4H`LTiBs%3}weHc5~}Z&Wjz_Y%znPSP3DO#c9KC^&iWD{M1p&0PId5^-=W} z3b5mIdLoAAsWlwWBdts~53VPUN30sft)>pb>(q)LR&50WHgK%it?~!k%qt@HTCfMz zmLfVGm$LbODkf+?=(>egSK;>#kcS1f9g>YS!P|t0|zyyczl? zPv9htF7laN$d5b9dlOGpId{gG7_Z(cd4AF%dyLZ^t1;TLIzP8?RFg#r%VoG@#eL2E zJ9PA}+1(c-ddkn0AnXG{ZT;4)wg&(q8cR+sg}kjKPX& z(9P$90w2GPWjca>^qPT0Jryr_Vhw&l^vkng;BNABpSPBt$+C%0@2|Sh_o%5mSZ+&h zE&FnkOyX~7*{mFBrGe1BVL=W8SWZiRRI9mBAoU;ElK*RLh&zH3j+fa+dE?_C%gz$Z z+Y${9Zo(2pDG~Vv$u6s#*e5bV=YM!@w8%#@ZQpsE(H|sH$gDOQ;@Y-N7jnJC)KFIcB{@i*t=BPgb@1OXm>67X?S*;59(A4r+bN_th8{Z z!W}*-Z(4by*fDzpsNLTmEU}w(C`%&tSUgEhgLUtj(TZ=1;1l+>ffk_l7;MDvi^<5T z2m5ylxHxc7H3C9u7}+!{%+rtEPQ&CgMPNFNIWI5wY!q$YD*P($q=SbIMw^r0ir;!N^^&tUTO!wX|39F0!kp)=36LV55DE} zSo!*)-VQ?qMpPe;T(5i9NO~}u_J{4G$B1lu%lxOMj7tM*q6U@11&S~^iK!)D*B^K3 zP0JAK2YiR#QE4;tYK}r+0~xkh0->H%MO=}PWp&%^UrCJ(|7&F0WwiCMfDWn+tuBBS SX**xMM}VoZ1-c&P8vh^Gq6~Qe literal 0 HcmV?d00001 diff --git a/mod/etc/options_menu/images/preview_benchmark.png b/mod/etc/options_menu/images/preview_benchmark.png new file mode 100644 index 0000000000000000000000000000000000000000..cf0fe4b70b545413cc1f54b0688ca427984d565d GIT binary patch literal 5951 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 literal 0 HcmV?d00001 diff --git a/mod/etc/options_menu/images/preview_change_combo.png b/mod/etc/options_menu/images/preview_change_combo.png new file mode 100644 index 0000000000000000000000000000000000000000..57eeb3b0e23f0093be1a94730df5c5eabab566f5 GIT binary patch literal 3004 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 literal 0 HcmV?d00001 diff --git a/mod/etc/options_menu/images/preview_dump_file.png b/mod/etc/options_menu/images/preview_dump_file.png new file mode 100644 index 0000000000000000000000000000000000000000..ccf6fd7013986e133797f0a180014a36d237a9b1 GIT binary patch literal 3585 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 C-ki(; literal 0 HcmV?d00001 diff --git a/mod/etc/options_menu/images/preview_language.png b/mod/etc/options_menu/images/preview_language.png new file mode 100644 index 0000000000000000000000000000000000000000..644bad573f08f5272faed913b09b97ef9033d496 GIT binary patch literal 3982 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 JhI3wt{{nYy2QL5s literal 0 HcmV?d00001 diff --git a/mod/etc/options_menu/images/preview_net_reconnect.png b/mod/etc/options_menu/images/preview_net_reconnect.png new file mode 100644 index 0000000000000000000000000000000000000000..7fcef8691e66f89192c289ba2a58e6ba0311efc0 GIT binary patch literal 5315 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 NFbzHRYE_%?{{gZsC=dVu literal 0 HcmV?d00001 diff --git a/mod/etc/options_menu/images/preview_net_ssid.png b/mod/etc/options_menu/images/preview_net_ssid.png new file mode 100644 index 0000000000000000000000000000000000000000..390e498335a68e28cb907d99860ebbd29ce36b00 GIT binary patch literal 5328 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 QrBnh4O>0|QkxUrMGWaP=DrB3o%vi>r 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 U7R2b@GvN-Hn^+lF8+s=G4;H1Q7XSbN literal 0 HcmV?d00001 diff --git a/mod/etc/options_menu/images/preview_ra_defaults.png b/mod/etc/options_menu/images/preview_ra_defaults.png new file mode 100644 index 0000000000000000000000000000000000000000..b79dfe7e979a4acf7b4378af7caed01eab4d4bc3 GIT binary patch literal 5205 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 VXVNPh|7t!!T@|KMr)2f^e*nlT^<)45 literal 0 HcmV?d00001 diff --git a/mod/etc/options_menu/images/preview_ra_delete_backups.png b/mod/etc/options_menu/images/preview_ra_delete_backups.png new file mode 100644 index 0000000000000000000000000000000000000000..fff21151c29fadfad133f03fc24bb90a15c1e1dd GIT binary patch literal 3679 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 OBVcJ}i+PE5i~lcpi@xLl literal 0 HcmV?d00001 diff --git a/mod/etc/options_menu/images/preview_ra_delete_bios.png b/mod/etc/options_menu/images/preview_ra_delete_bios.png new file mode 100644 index 0000000000000000000000000000000000000000..f144655938664ee329a2e0f2d4039683d23a14ce GIT binary patch literal 4792 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 literal 0 HcmV?d00001 diff --git a/mod/etc/options_menu/images/preview_ra_delete_overrides.png b/mod/etc/options_menu/images/preview_ra_delete_overrides.png new file mode 100644 index 0000000000000000000000000000000000000000..c323fcb40bf708a38f9a5365f4ef0d2d05db2b19 GIT binary patch literal 4251 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 literal 0 HcmV?d00001 diff --git a/mod/etc/options_menu/images/preview_ra_sync_bios.png b/mod/etc/options_menu/images/preview_ra_sync_bios.png new file mode 100644 index 0000000000000000000000000000000000000000..0acc6f55262306a2e3629f8f38a7cf19f0d3c2ae GIT binary patch literal 4620 zcmb_=X*ARi^!~?Ab`p{rj5a2OP^e+-dweEh5Qeg4AK90DDY9l~5=v&VW*K`Kj6Gzl z2H9nsXhg`bbN(;?uYTwJUfg@ndCt8ro_n5i?s@Jrb5lL;Q)f>B0KjdakGT7n!Db-`0SXHJcQUrE&;clRBAgf(=bBV!>Sn5D49j;j@4%qsXBwJ1aYWOa|fd@jfP7$zuR|YS1DDrPV3=Zx5RHllWEp+36V&YpyI1g_-)(bnlFf%b z3I((zInba{(t?3{aCx2qKt8@Qg=t?fDp6;V!NcS^?Ad$1o*RH>2XeszP+@8m?l=ap z=Tq=JF&s`Xz~80;!&D_2=Z8(OySL$I@Juy`jm8gnFgEgZ2ubcx5U6lI4%@Mpo!1y}dlUy;4wx=Z*Q<$!#u0n4NI`BOi4S*iTEaQ@C-l z`eNKyPf!B{<-l4%lYpOmF?{-bR(y;cF{iX3 zaC102l>I7uG&7tb=p>1fQu>ZAnyc>q0#T&$g$Rfpi&pp zL>{FDP-auk^#8hBpB)+SNn9c|4=U7}MpkUILv+V%NvG}nb~DXnIU=mop#Hv!^|lz# z#fAKb!#>(RutBI@IU%YG{FMb;;SO^)paxWJ%MnCnZL~*^?8fikZ`hDM*{V1hf!nGN z#^WOHgThm>S6SESBJ-90=18M?oeY zRy!C4PM=tG2u#daekA5IdABEf!*7gO|7BRyy8-Qdq`>j?&P_7w)VWyenoJ~0y#E8$ z-NC<28`NvQ>~T?c2{J2-a=tk47{(F(KH4lC{YJ@mswiZzMms}Y1}zkh0t@>+ae@66 zk7i{ghkxd&bD&GhMclL?Lh@r-Zdz2%j%J1p#iOhj}Y31)|C5%G`S*N91V` zex3uV$;HXJdzylbHbMVEgY>E#5>5Ox+-YH0kx@eQL8d5a`U>%8hY}Dtm4i` zTmNPaMsgIXeZ_v8+`N$@C28a|0*2QJuowQ_z*f%NX)RTY$ZoXl$sQbu12e_A(3;Go z=rC^9r7P2l1(&L>QKC=x?6~a6QA5h_lx#G;2&lFp*@xr%hMEh0hdOf2uV+|t&ci+; z4RkzzRWDWfhbX1n({=;2ZPbKYslOrhNg0NwjJ_6jTyy~qc7 ze9pZ9kl)<`two6&PW&W+js`2b5^!N%3+kO~#LZK$!93^RvRzHng1fm$@kw#K@=wFr z^EgV)cJ(P4H|&zck>Vl8G*T{L`KEDr%qNf*eZPrVif4oGX1IHbsyTAR+ z;Jhy5F~P#xAg{+rm!bB0zz&V($Ent^gN3eY751%BUprgTnXeHHhi7}0r}ENfp|4&9 zR$GlXSqtk5<25@)pLeo$uKXs^-}jON_d}5^4y!J@VTCR=l{};KC!}0zC2j^LiHX<$ zF+!o)!xk^8-Y$~1RrxiRQJ=m0k~+NeF?{9M?d@F8`>Cb#)O@S-8qL3u(9Zg^wMxOW zx&x(?ko+TI_$LMceg^- zKA1^+mn&Vbaya@Mg~Jd!8Oc^CwAseC6!DhppWu3jf8GWhV7GL^?bo*5MeO9s{(PbJ zR>>WGJd;$oR=8gvrs2$dY5Ezmy^6t%89IzlbHL!^L4nV?GijM|#-Nl$@)l*Bdfdl= z!Tc5tYVPI)!l=fLZI)olGLGVJB0Ey|1N)uJ86}?H*qqJXSN7*l_|wV895uZN&kML8 zXTQ0@dM(uVh^LL@E^ZKdyPt#Kx9q>M!q5G-k-sGv&k;*34#W?YsAkoEMVsd4HLzb( zh1F=Lsz17S(N+b;zrHPJfE08W&ab>`cUPcuQkzeMLen&>GFqBkpZCFFzGB3pqy}5W zsQ9g3U5E|_1MIGrEy7=`peLr=CuyaD3WK#p?7)y|OYocSW&73@n!4?!mEVJ!w~Y~4 z0$;qsz#Z+qu+|#j`F0HTmXB8x!Jq!Kw-G*o3Hxt_g?yw z^`E}{Bt?EU!~4k(fEsaK}0tGj!tRN;$W!6yY9}>?Vyrw{sR`Ld!Cn$*XaK`OrSW`=Y;ory~|ye{KsHnb!$tq8eMY> zkE=9@tnYsQvCb+`OxNzG@|yjG-K^PLp}rm!9p9~(tG-c~HkZRrI zeYy|>9aAb*OJG(YKH#secc??yQ@zC+ZkI(ra(N(-(SnsJu;;)YNRNIwWXVAe8BS zx%Sd@{-n%-P(VdMPv?OrJV>rLr0yl-*%S|Zt~pN0n#*MqQn#Y_fee3c2f2SDhsAEp zWE5O*NpObdbHwuw>9~k?g)95V5RC5=fM~A{J&3iW00F9u2(DYwWkq?m_qo#%*RK>*eQ8i;Iwy4eOU< zEx-nFTv2WRaR{^G+K@XMs_p9{lX_i!heWSkyFQOB?G>P0I367>6q5XjZea6yk#LBa z?sWXDg6*~(q*A=6Nw_~IXBkVab*~G>^E29Hx~i~t2opoKEq}Nqkb?^<@!U-^2uAyD zc+%b~Oecir_h60h1gl-AH(JvuO-V3&dBZ+kfYE?<>5bY!b-G5T?4$8=B~4GMbfK2N zuU_uwD2v}RuPZaH)M}~zvWIvC#D0kPDYX}n7!*^#Z_EOZKnP8n-lLvGjfCm zkb*-d!p1)(92fE;bJx{v``*)^WkDsFM?>z8ApFW;ri?W0rhMK8gA{XRi&gKa5Yic8fylUcAF!}E-hwP+Fy;b$1opat=Ao6#YUvgJt42O)hQ+J~pdzad7~ z)Y*J4&a5)mj$HD%gm$3fq_5o1+0WHce3A$2Rgs-!53kQNO_WA^wkUTVwM?a}Yo!Gn z9gz5XciTseL5k9do8^$e2?VIJy>zC z;fjZSZMW94nCPKH(#jVukct4eO)A`2W?y|g9*da6s(We@ahz6x_E~~)fkje%hHI{DFS2I)?VaJ0ICu4L zj_R9QO&|SObZU$dZCPJSA8?H}ml!i|j$RJtG%wqj`5Wmp?F^jzQfz#?UYV0Y3R?X< zTnAJchCMZZ522oTmq=gxZaOmTp>#mJb!9!VaL*E4chx`!e>5uFk{%~Ixh1p-}HD`=$B|& z*(ehKun(J&lcUT})AP5L9fuOE>mp=Qb`*@j$Df3Qvm{K88)J#X*o_8fw=n^wX4mO{ zYP7tXs|HBD15U1Z$mJJ0OM!KPKiS^lo^Yo4*!Fs;X3gFj@2uHFSXcka*8FgFA^vt~IUxSjO0bOfRL!R_iR|$iT;HZDZ=R2uy|r|%X1Ed##uut zjIW49>NVPPY-2KEUI1$y@-5(29${^@$@9-~>+#o~`;vk^VmUm;my>yyuJK4Qk1jAf zb^XmsPHcU-$%e(7gP1ba2^fD*E-A5-(v1~YH=B>IQ;X_XTlU$G;?s-f(~sCMU1&>s zQzy%TIt%-MKumf6>hc!nTdi}_WD%&Mfd8&Gu1RshlX_87#eWV<&neMohid8q7Da!o6GKoT_~YUmPjDdcYBXhbyq> zXsJlt9bt*?%pp%)9$>2Cv`v|{l}gKdBIhFG_YUXn1y9PgQD%SJtSXzEx@4f1B^n_o zkal8YG&=C22f4TnEGvHf)MFLMnexl)u%uteSU9JAD%9*^@-&dlR>cyvj##{hRk>^U zGJp#(GCE@3^_w-P)cim@B?)i|IIBtCJ1`Hl1-46~`~0RN}>;zu&qjEvaCXr3`N#{YZCu%ehy+4-tV9hQ5&Z zox`SPS?72qcZ|QpUOjVJ+QfUf-7BD-KZ-m3OaK4? literal 0 HcmV?d00001 diff --git a/mod/etc/options_menu/images/preview_restore_nand.png b/mod/etc/options_menu/images/preview_restore_nand.png new file mode 100644 index 0000000000000000000000000000000000000000..638ba3044eacd7f3688bb9695f8cd18e495ad11f GIT binary patch literal 4109 zcmb7H)mM}cwEboVrKLj&VHoKK38figK$@Xz06`Q%P`U;kU&uo9c`$uMNM&w0ssJNls3}%U;p;6;>fQ5L;0k&6#$_5 zi9)KH`loHD2PE3~(Dk%7PbQMUQDo6-jNByB*hfqPNKq!LEaMpLDg+933^(KEV%C0# zzT}3-u;0OxK~+6SDBF{HDohiFRMk~U>ff3$?BH;S#%VKyhR&f6ALm=b)18PR%4n0u zLzlU&>5rHCepm4xsxVVDfX7oIdIEuB8i#EOwMaUbA4M9@==Q;Z;hGWhO$RkHOu=si zPzAVOTbKo8pu_XOY}6VU8L%PkWx$>^mJbl7O>&Kq$*hb|>R~|pz_BpM7Kww&6-*4~ z-)wtD$1UQZ2)wyij|Dh^XX$takGb4msN8V5TG-xQ;;mT74EX{ENPzg)Fq!LVr?OHy zP}XhR0uTH?pIae4L?UgBwb&bBa@X5ffja0{BV@+YaRS4rsHu-SkO@2ku=_wMsW{(2 za~@axQy_Tgd7Q4E(Y}9^%XDc*;usyw@WNxBS%QPwK?-PPOkx{fL#!E}%c!DyEbx72R(>MwTPviAq3tJUrcw1OLKo6Bwn2F(_mxEoVSUgM_9KG4OCj<#DG|ZV z!i{z@{RJm!ufL%-hsc+~alT5VVkG0f$xo{$X~>0h&Qc-kRSBm}4u^6hVAs^NuT$Xu zkA%k$Sx_MjpGoM2tbLg&a$ES_e}sm~vVJi?-`se=S$K<4`=$BYY6`KC(6FXft@J$v zZzL1EQu6oPJL6hYB-fVw{kV%p?oqcb0)HUo^(<1FQ6vX3peXd22)0O1m=P6J*rYP* zk_QGVU`?86oMg+B{8Xk`+Un59a&#Ycld2PR5*+L0vO6xW0fWzSs%eYnTf6TER1FKN zwa{2Rp+GB6D5kHXa=T5fxI}MA)tWT(Eepy%kW$)s$#(AQIchoascB0!sXD5U0@1;U zo`tmaUWzE*lNj{y_Z;;u&zZ@YsIkog-?LG_=rlai&S#6uGU-YzjOHP8vi`jtJ}#5C zJ6vN)v#WusxiAu45rMF;bhc2{SZNzXXcccw`w3#3DgJX!6N29+5ue|!3wm^y1OuO7 zZd32|VyGZr){R(CW;P@k6;E}sVTlm`C^nm0W*k{oXY})u0ilg$Lno!+&T>ez(KmzI zT5(;Z+1Ca6=m{t&nCRemAff;cdu`^f1Z>A2F7Iw{*B$wjwV#^D)%14B*jwinIOEYB zdxXvs{V*Y-U$2kpdoUeXM(+ zDWlm#j!>*Ra>W4b8_LsNAT^3liSp;Mj8u9X9b{k0Dsd5%+iA!(VB{%(YB(%X6orC> z^8X*NwJnc#z(|<-o@v)^h9PzD2Gs7%w%Z?s3I~AuMk7cR!-Q2bCw0YhDUViPqPchIY z`yJnj^WwxFYB;;$xhK_)>lofqiv4~}Z79B)rE-A$_B!qz#pKzh)6ft6{{3?u-%~xD3C930*A(7tr z>hIhN)RfBytAJowo9p$Bn3}@aU}a@b2sRe55rp?|=-uiJNL)L8QJv$ak3%mlO-y%7 zOm&ZZ0ZmMgS%97E&r)J}I*LPC%yab7zG0+8PCF)qp^vf^X}D(1u?Xmfes=0lBe-|w zIwMRpUQxmHp5PRkPhKakl&O2>je@8sD?`5wP-;#%-17XY{fL1)SgIkRAmTWLu6|Il z2WyU&XKfRiyTP4&z?%N}&+}b9h5ivBv^MLLrl~_rZf@az~ zl?dgVG3OErv8&Q%#XEj;Qzbh#I!ynzj{0`r%xY=-ka20L-1$5iQOAE+ufd9TLG?Xi z>Gc3arNz{yEnwG16>&y(BJiuD;l2*=c=mamn=w0`W}cp3AAY4DJVpDC*#0AN;1;dx z3QQE63XhFQ_BeoDgP4A5&CJ#@TVN2SU~)4FdE(nZ^kgpxR5IQOYl+3LrPalL<8di~ zSN8D%3A$^VeY_{nA>WykoSG+HCu3eKUe3IRupEMq{Px89TJG}{kMDH&8Lo>Y_pD@!_V%E^?7M(g- ziLDHmE^c`LmJhOvJ~Wn4>ZXQR)Zm3p)cw{A`T8@LeZQooshm;KwmM~OQ^cb{hgv~1 zd&wVX;5PwB>JB~?hz31&>$UdJ)8yREfm;)`7@Xvi_`W2+F@OH-prQHI z_f(vm``ho&bMt0uavLUX?s zCf?ck8O<5Ey_&$0n3mJ~#C=%FxpsLnn1fEchL|Bmdv&UG4OtfV?Q%+Lk9$ zz?;9f#jmM+#ARUBuj5m!3}KyS!WjI0aV3Ubh@M zdS!2$oogj4qkVB8)e+ag2)~(F%ZI{tmigt3>#b$8Z+4o@mIAA};@gqT+=SCR2ju&i zGHr&IplG5dL469v`unKE^}u~`KQK4?xh2h8wq~6&)Qq~Qs22y%dyoR5w6(6I-(_iR zJD%k@Yx?T>t!59eL6VjLfz{8dr2-)(E;qN>^^m8*ixznW4JyK+VgH5QW<{XN%=Jci zNk@Zuy1{5KiH6;WWG9|pn zq%A~F^4rUqpi5Q~sMp~VQ+|cN?I90@73SrTXg|{Ohl^>^VBiOup8TFVb97L;`K-|7 z@Ls1<>fFx7&Ba9zogMRJFp&1d*yC)Wt2=AxLntB&8-0A$Z9u8IoLcLaqJC+_mW znH|%A?P}P=oZ_0NkMV-=*UWP7<@rME4wN?1zG<}JD>D4q=!U9v?)BQI}rWtFW~-PqnQ<-Kqf{TB)+xcjTDGR?5-Q&M^2h0hb3U1@caPqRBadMZ?1u~)y%=2pg=A|LmW?=FJ z`$Lq+fMMgpr|)lUR^RE1{@Lu`W^nUu>9q69SXEIvnl)MNQiy;GEJV_wf83Y6w3pT= zB6#Vie_*1|B3cY3j(G=co=BWms>V1q8zNT^;~6Os>aroRyzq}j;Qc^We}Bml)*O#x zGtw_SkBl1Ydd!bA;BeWW8PzW1Tk@k!2maxohJ51;+NI`!-}i>aQMiJ%m11KvfVzd7FcD48oa#$s1z zzcygZ*{2@8UUiBxfJ{uBVN4s`cnWz>T{Z~OrLZSlM>Z>527%<~CFe@l)IiONbC(_A zjM}9j?!>=yA3S8yvs@d1t~O!fN4IxEz+28;(tTlU1dtC)*)Gjzpxz>PyKmPvoz@{^ zCo$jDbFqdP#mqgKi4C?xyJ{-O{IM71-sSo8Ty+ThFFi|9VkrF+%G-Qu zn0U2F2CFiUihn|u-r$byJIz1u`l9%FNbefl%Mh*nWE&r%aKdfEgzW(ohL~-gzvh5% zntPUefURGLVRewNU4ezUv$bRU&_yd#!aXJq5{G>A@G!!7E#Gr0 z?54&_ZAY;7vbX?U^lRsjlv9&hU-O{_)G(KTIQ%y0!kDKh8O8owa}Mwa;GrBwE}=v$G1b0sz2{#TZ$h&+hX| zVP-tTyH ztg(#hyQEF-`DCYKoTZZ_Ytr8o6dzZ#gvro#N&UYvVjy179>WqoCCWH|(%Ra`o^lsb zWA4?Ojok3X{@xGUXEJhui}_bP7Ye_f*@mxu+_Q^ed_x4dxP{F2K)?**=PWKM^JzS& zC1JJf{^BU5c96wTRWer9YS#~71M&f?AbhX!;RofmB)9-zkCuK{WMb&-m=E66MnBX=L4XQs;uKh^+!7H2hFEwIjoC#@as z#J*<(3mBa*;Q}FMP``_~k+&Pl8nZc^RIhcnay8?Q%?6JNeLYDxG|YfKRt&jW-AT37{U*mt;-C(L@ZVd#YW)^RYmfR8a z({k0rr2-4zJm^@bU86bYPpysWY{!UGIjWLBxHn|t+@b`48nM!ERa^<|p+e7p24;xe zqljzKAEdLN*=d!{Mv}(pzj9Fh@9>bAmK4e-$^MmB@TEW#V{3WRfV`dd=Z64U=j0m8 zlEi4=_(ta}*T3Re+Lhu{neGsf0+JtlV=2bWF@YCik6+}BbJ@XAY8Dc)?nMU7pADk9 z2+?$s()3UihLJTk1IEiNDJ0MJKL9NHOs{W+jOuOHei=`S{ahkyA<;%P2JP^2<%;ED z#&(!}RJ40SXF0Kpt^wBiybZ4IpKb`VKJ-dWut(#wm{?07_?Qtd^>_Qd`^ERFi2Yo6 z-|$nn;QB^Uou$+&M(>tWQwPy-y}ze0^-<$qkSSUsbugHk4V7|!5|uDUL1dF*~z-P?@m3+YYt?LDqAlrUU@GeC3j(q)S<+(v8IjV|Bsxy z(|(_@!G682m@=o##EbDrBt0c>`1l`ZIUL<<34Zt{CkP^1=G)XBYyqY-_^IGZ0nrsN z9TUZDn}KG=R(G&K5uuWHdQsYqMH!PiPm`(hwHP4--i&@sxVB@*RFCJ>ba4inJnjbZ z5Pa-pauBTWULsA)lBbIosF#h7S14YC=NdS!JbwNYd1qt|5s*sUOJpvNBE#%5<{TTG zDk&X|Pi41Im3m62kgICXRRNFt^ERM5_~XOr zMMO5wfdRF61mS<_(o`8%J_DY?JMos)5L7POf&p^$+G04;-7DSc8`dM9z%jIS1Nbh5 z7?4H@l^82K|izNC`_PaKvbW2XniHC+@!P3}Vx*J43W8LwaHWsYdgW7Ks zB3kg{>edP-0W5D>iYpGqki6l|bgd`EL*(83c?abANiFHxp+)P&qI7Uh;aUnx#?#Er zH9sGEw?UUPe(nL z?cmnOzQW?of=yke+!wa}WbU!^Vmz7AN2Q3ZH^e+HGtnBr9v}21DXC4Yf`NuUyP52c z&ZSmdUOel0_~EPA11kFxY&LD56Y=q_`BWr#D@*|MswmL6LeI%apUIg8N1n{%+3>|bR>M#GZPO`>~XFf;?8&3M+5spBFpKNx>8pBV!)n4d&Tm4ZbcTUnD17pA3 zv`0MoF_zDg+;S7!;$y+uyoi32602Q3Bq5MBNm_qEgtSgJx2Sf|ZsJ~(X*3T9jwknb zCsMB9G6}h~8PTnKQ>5y3N-ChgQjyC-4F)5hCak{W#`deh4r-MT2A#=%AyVU}Hn>Hf zaqnKrU3y3A@WP z?NvfoJ>VC2HqDD>&&Qh6EPTVizannbe3b(PAlkyDwoiuZOqG^KQ7?2lPT8VJL?j~( zFFX25WVD0G%Zom^uxE>`Zcw#LKRl<%*fn)jct8t$B!gR0mggj4>vkvabCm1wuUe+R z&>^wlc~6jrqIJpwcy=@6aY0sTDT4I#Lyq@c0?~q(`N7@0&x}6B;J}%X!kb`bb4j9e5*mbkiof( z!?#E~{@iY5v$7vTr)q>_TraO8T;HMxptO4Feem+`O{bS?OL1i4m2?^{x6V??B6H^$7RVjl=+@Lfs=2!XW z3`Aj}D)LTy<|jc(UJN$QvIwRgnZj=noh8aYCwf;q$@u?FDgQC&Ag-FYuZZ*vxy5G1 zVQ3DC5f$N0ov}W#JBrJ;uCGJz#d>_^YJ27b{hqdmu+h)5dU1=WI4&8elrPBqr=JVRbK{cc`I`?oXo zL_pUg&erDl!mJBm0fedzY%o+(7SBc@4gJ@#(uU23B!e7&P>;J63M_>39~9_2RZBg% z9W>V>sywV}& SQu5EABmirC)94+_Ip*K5?kKnb literal 0 HcmV?d00001 diff --git a/mod/etc/options_menu/images/preview_saved_games.png b/mod/etc/options_menu/images/preview_saved_games.png new file mode 100644 index 0000000000000000000000000000000000000000..e2c98dbc74881768895453a6f7fcbc7bfc3e3862 GIT binary patch literal 2916 zcmb7GYd8~*_n*6ku*hX@V?=0}O4c|M<$a^bv{&=Kh)002M;ZH;m`jQxj3 zupUyzaI9DV^0J ztuL$3feYBbmrhsj`fZ8S;20usZSET|o9H8$ZEIZ4O*^kU-JvoeF;VHhaSL1r06AVg zO^dn&NsxM+(gbBep#EI>oF4`oo^)mMtc*MWA+Qdd(Oon`WZYO`FDt5r2VI#n`e?HqagViI)KwT+p?k)zHway`NEk7qu5r>LEPFgDjcL^fr5l1q1hqzOWkTRD7x-prMr;K<~IsOo2(F%Ug#)khSmaYQy6h+0K)WW)j{OQ`7?MlYVmEMvEq|aR{JH)IMoYKL2A(nLm^Fl^UHZy?&|eBb5(lTYy_p&kl2d-p{s<^!dm$?$Oq> zy5tX!Zo@02)t`z)R3mlNT54P0p2CS;o4!3o?14vO$pqIQ_VqMw#m|?KTRpc~hNnTv)4N_$ ziUzM3F4YoG(5B{_3Khk!bCPRgoWO@+((#{7!KQCNqyFAw0_BZNTB%wZ(5Q69wW9Ip zj}Ves@4W^Cd+K$6bYP5hP>u>0(A(H`i4`E7gJ1TtNa(jEa_h*=<=dLK70|47fYQa~ z-EmoFylJq-Wk$`cR+hCwTD3pPR(x|fVkpSq(O7_)wSqvksO(t+oB-D?IxPt$ z^-Po$>%)V6qSsaRjPocp3)%oE>DAkT?RiOztc9QB_G9&WX%SGr2U+gBcd4Z^ACbF7 zSG9fS+?cr4Uk<1d){*?scGvk%xrO2(s+H!OU^SWu`{Dp7_%3%#NYR#%8%~))yK|$L=sKu*?ak#q^o)bV<9p zH}K3W9f!>e&3$7%=je3*w>#a%P-s*)2t5^^;`a< z;Zf;*aFp1VnVfehaVQUW<-?5O;auP9H9>fZ|A`C#vT5Y}YWW^$wxD;-7iU z^&Qa=TpH-jXh(Oq=FY}~M^6-`NLWwdT^Cf}7}TOSZk}l!VIi*Cw4Kud^+#gnOo&S- z=!tXZjnJOGfgh0T4V@=5cavw|9f6Xlfj2uHEDN7W=^F*4!)))7zhw!QYkln&3_CYJ z78tz6&fV{Inm?u8nj{Tb?N%Z{F?uBy| zBegco$1Dv0t|rsQ!>40kH^l0l%b8Z8T}CBLG5c5EQYtRu8c!!lR=0Th=w)p3B8&aV z3^NkwI2FAU0L=}!3gUy$s`-ZO&qqhbN~GMA?WED%kGAvudTeYRT$!5Vgshf=N7okM zHbdi2G*scA3~gJQ281pBRJ@JPkh4~}V_o@`=w0PH_37o(&W7T1SO+NqyhP#mF@2~{~v7}sVC%=yuXsH(rO_AAqJ68_>Z;&wQF&Y zEK--FnvzZ_W=$VwhBD2V{kN~zg_!`K!~?gTtB;nYysNJgb^x80FCA4D(e@WBHZ?Bo zIY!S{B$;A-%6Dk&dC_~%Os+o=vF%QB)YwU|A`jkfu;Ym3nB+WBU6<1S6uG{gn=QP#d1>~}_ zn&85BB50y_f`Fm674;~uVHf?#lWs}Sw%oC$(UqQk1as8hyk5#YBG#iZ;&%l$R;2ln vefTy0-gun|(Y>?-Z?(2>FsH$52lDk-Zq|Idn8iH&o&ji!^QcCo$KC${Q;J@W literal 0 HcmV?d00001 diff --git a/mod/etc/options_menu/images/preview_saves_backup_usb.png b/mod/etc/options_menu/images/preview_saves_backup_usb.png new file mode 100644 index 0000000000000000000000000000000000000000..34b843d6aff07fcdec8af84f431515ab73c2394e GIT binary patch literal 3968 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
s|I za$R&#-iqxIC!Iz+Cge~uxV$P*xkCa;TQA4%iN|YH`Bl~l)uaW-On;$ zQcU8q>9%-fGa?1tZrl z)Ce5y9b-);zpFc&p^`pj5N2I$4NcW3jx6$8kvnCWyMa!Q%z)F*lr?~WSwO1{-QxcP D_HI@X literal 0 HcmV?d00001 diff --git a/mod/etc/options_menu/images/preview_saves_restore_usb.png b/mod/etc/options_menu/images/preview_saves_restore_usb.png new file mode 100644 index 0000000000000000000000000000000000000000..da4fba11eea754e27e401b87c258b8991448e21c GIT binary patch literal 3945 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 literal 0 HcmV?d00001 diff --git a/mod/etc/options_menu/images/preview_temp.png b/mod/etc/options_menu/images/preview_temp.png new file mode 100644 index 0000000000000000000000000000000000000000..8f2957e043c97a8b9911b8b50e294e3d586796ed GIT binary patch literal 3240 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