commit df2b257d9303df745c0f36b1ae8f526c8ced03e9 Author: nickpons666 Date: Mon Mar 30 15:29:09 2026 -0600 Add UI translation system with LanguageManager - Create LanguageManager.h/cpp for dynamic language loading from JSON - Add Espanol.json with ~250 translation keys - Modify SohMenu.cpp to apply translations automatically to all widgets - Modify SohMenuSettings.cpp to add language selector dropdown - Add Localization.h/cpp stubs for compilation compatibility - Implement persistent language selection (saves and loads on startup) - Fix string lifetime issues in dropdown using static maps diff --git a/PLAN_TRADUCCION.md b/PLAN_TRADUCCION.md new file mode 100644 index 000000000..57b2aef56 --- /dev/null +++ b/PLAN_TRADUCCION.md @@ -0,0 +1,324 @@ +# Plan de Implementación del Sistema de Traducción + +## Objetivo +Crear un sistema de traducción dinámico que permita cargar idiomas desde archivos JSON externos **sin modificar los textos hardcodeados existentes**. Los textos en el código bleiben en inglés como fallback por defecto. + +--- + +## 1. Funcionamiento Clave + +### 1.1 Comportamiento +- **Sin carpeta de idiomas**: El juego funciona exactamente como ahora con los textos hardcodeados en inglés +- **Con carpeta de idiomas**: Cuando el usuario selecciona un idioma, se carga el JSON y se traducen los textos disponibles +- **Fallback**: Si una traducción no existe en el JSON, se usa el texto hardcodeado (inglés) +- **Persistencia**: El idioma seleccionado se guarda en la configuración y se carga automáticamente al iniciar + +### 1.2 Carpeta de Idiomas (opcional) +``` +/lenguajes/ + ├── Espanol.json + ├── Portugues.json + └── (otros idiomas).json +``` + +**Nota**: No se requiere English.json porque el inglés ya está hardcodeado en el código. + +--- + +## 2. Archivos Creados + +| Archivo | Descripción | +|---------|-------------| +| `soh/soh/SohGui/LanguageManager.h` | Header del manager de idiomas | +| `soh/soh/SohGui/LanguageManager.cpp` | Implementación del manager | +| `lenguajes/Espanol.json` | Traducción español (~250 claves) | +| `soh/soh/Localization.h` | Stub para compatibilidad de compilación | +| `soh/soh/Localization.cpp` | Stub para compatibilidad de compilación | + +--- + +## 3. Archivos Modificados + +| Archivo | Cambios | +|---------|---------| +| `soh/soh/SohGui/SohMenuSettings.cpp` | Agregar selector de idioma dinámico + carga automática al inicio | +| `soh/soh/SohGui/SohMenu.cpp` | Aplicar traducción automática en AddWidget | + +--- + +## 4. Ubicación de la Carpeta de Idiomas + +### 4.1 Ubicaciones Buscadas +El sistema busca la carpeta `lenguajes` en este orden: +1. **Directorio actual** (desde donde se ejecuta el juego) +2. **Directorio de datos de la app** (~/.local/share/com.shipofharkinian.soh/) + +### 4.2 Cómo colocar los archivos +```bash +# Opción 1: Copiar junto al ejecutable (después de compilar) +cp -r lenguajes build-cmake/soh/ + +# Opción 2: En el directorio de datos de la app +# Linux: ~/.local/share/com.shipofharkinian.soh/lenguajes/ +``` + +--- + +## 5. Formato de Archivos JSON + +### 5.1 Estructura del JSON +```json +{ + "language": "Español", + "strings": { + "Settings": "Configuración", + "Enhancements": "Mejoras", + "Randomizer": "Randomizer", + "Network": "Red", + "Dev Tools": "Herramientas de Desarrollo", + "Enabled": "Activado", + "Disabled": "Desactivado", + "Apply": "Aplicar", + "Cancel": "Cancelar" + } +} +``` + +### 5.2 Espanol.json (Completo) +Ya incluye ~250 traducciones para los menús principales: +- Settings (General, Graphics, Audio, Controls, etc.) +- Enhancements +- Randomizer +- Network +- Dev Tools + +--- + +## 6. Implementación del LanguageManager + +### 6.1 LanguageManager.h +```cpp +class LanguageManager { +public: + static LanguageManager& Instance(); + + void Init(); + void LoadLanguage(const std::string& languageName); + std::string GetString(const std::string& key); + std::vector GetAvailableLanguages(); + std::string GetCurrentLanguage(); + bool IsTranslationLoaded(); + +private: + std::string currentLanguage; + std::map translations; + bool translationLoaded; + + void ScanLanguageFiles(); + bool LoadJsonFile(const std::string& path); + std::string GetLanguagesDirectory(); +}; +``` + +### 6.2 Lógica de Funcionamiento + +```cpp +std::string LanguageManager::GetString(const std::string& key) { + // Si no hay traducción cargada, retorna el texto hardcodeado (ingles) + if (!translationLoaded || translations.empty()) { + return key; + } + + // Busca la traducción + auto it = translations.find(key); + if (it != translations.end()) { + return it->second; + } + + // Si no encuentra la traducción, retorna el texto hardcodeado + return key; +} +``` + +### 6.3 Funcionalidades Principales + +1. **Escaneo de idiomas**: Al iniciar, escanea la carpeta `/lenguajes/` y detecta archivos `.json` +2. **Carga bajo demanda**: Solo carga el JSON cuando el usuario selecciona un idioma +3. **Fallback seguro**: Si no existe la traducción, retorna el texto hardcodeado +4. **Selector dinámico**: Genera opciones basadas en archivos encontrados (sin hardcodear nombres) +5. **Búsqueda dual**: Busca primero en directorio actual, luego en directorio de datos +6. **Persistencia**: Guarda el idioma seleccionado en CVAR y lo carga al iniciar + +--- + +## 7. Ejemplo de Uso en el Código + +### 7.1 Sin cambios en el código existente +Los textos hardcodeados permanecen exactamente igual: + +```cpp +// El código queda igual, NO se cambia a L("key") +AddWidget(path, "Settings", WIDGET_SEPARATOR_TEXT); +AddWidget(path, "Language", WIDGET_CVAR_COMBOBOX); +AddWidget(path, "Enabled", WIDGET_CVAR_CHECKBOX); +``` + +### 7.2 La traducción se aplica automáticamente +En `SohMenu.cpp`, método `AddWidget`: + +```cpp +SidebarEntry& entry = sidebar.at(pathInfo.sidebarName); +std::string translatedName = LanguageManager::Instance().GetString(widgetName); +entry.columnWidgets.at(column).push_back({ .name = translatedName, .type = widgetType }); +WidgetInfo& widget = entry.columnWidgets.at(column).back(); +``` + +### 7.3 Cómo funciona +- Los textos en el código bleiben en inglés +- La función `GetString()` se llama al momento de mostrar el texto +- Si hay una traducción cargada y existe la clave, retorna la traducción +- Si no hay traducción o no existe la clave, retorna el texto original (hardcodeado) + +--- + +## 8. Selector de Idioma + +### 8.1 Ubicación +En `SohMenuSettings.cpp` dentro del menú de configuración, sección "Languages" + +### 8.2 Comportamiento +- Muestra todos los archivos `.json` encontrados en `/lenguajes/` +- El nombre del archivo (sin extensión) se muestra en el selector +- Al seleccionar un idioma, carga el archivo JSON correspondiente +- Por defecto (sin acción del usuario), funciona con textos hardcodeados +- Incluye opción "None" para desactivar traducciones +- **El idioma seleccionado se guarda y se carga automáticamente al iniciar el juego** + +### 8.3 Widget implementado +```cpp +// Variables estáticas para mantener los strings vivos +static std::map uiLanguageOptionsStr = { }; +static std::map uiLanguageOptions = { }; + +// Inicialización con carga automática del idioma guardado +static void InitUILanguages() { + LanguageManager::Instance().Init(); + uiLanguageOptionsStr.clear(); + uiLanguageOptions.clear(); + + uiLanguageOptionsStr[0] = "None"; + uiLanguageOptions[0] = "None"; + + int32_t idx = 1; + for (const auto& lang : LanguageManager::Instance().GetAvailableLanguages()) { + if (!lang.empty()) { + uiLanguageOptionsStr[idx] = lang; + uiLanguageOptions[idx] = uiLanguageOptionsStr[idx].c_str(); + idx++; + } + } + + // Cargar idioma guardado automáticamente + int32_t savedLang = CVarGetInteger(CVAR_SETTING("UILanguage"), 0); + if (savedLang > 0 && savedLang < (int32_t)LanguageManager::Instance().GetAvailableLanguages().size() + 1) { + std::string langName = LanguageManager::Instance().GetAvailableLanguages()[savedLang - 1]; + LanguageManager::Instance().LoadLanguage(langName); + } +} + +// Widget del selector +AddWidget(path, "UI Translation", WIDGET_CVAR_COMBOBOX) + .CVar(CVAR_SETTING("UILanguage")) + .Callback([](WidgetInfo& info) { + // Guarda y carga el idioma seleccionado + }) + .Options(ComboboxOptions() + .ComboMap(uiLanguageOptions) + .Tooltip("Select the UI translation language...")); +``` + +--- + +## 9. Proceso de Implementación (Completado) + +### Fase 1: Fundamentos ✅ +1. Crear `LanguageManager.h/cpp` +2. Crear estructura de carpetas `/lenguajes/` + +### Fase 2: Integración ✅ +3. Modificar `SohMenuSettings.cpp` para agregar selector de idioma dinámico +4. Modificar `SohMenu.cpp` para aplicar traducción automática +5. Crear `Localization.h/cpp` (stubs para compilación) +6. **Corregir problema de strings temporales** (usar map estático para mantener strings vivos) +7. **Agregar carga automática del idioma guardado al iniciar** + +### Fase 3: Pruebas ✅ +8. Crear `Espanol.json` de prueba +9. Probar cambio de idioma +10. Corregir rutas de búsqueda +11. Verificar persistencia del idioma seleccionado + +--- + +## 10. Notas Importantes + +- ✅ **Textos hardcodeados permanecen**: El código no se modifica, los textos en inglés quedan como están +- ✅ **Sin carpeta de idiomas funciona igual**: Si no existe la carpeta, el juego funciona exactamente como antes +- ✅ **Fallback automático**: Si falta una traducción, se muestra el texto original +- ✅ **Selector dinámico**: Los nombres de idiomas vienen de los archivos JSON, no están hardcodeados +- ✅ **Fácil agregar idiomas**: Solo hay que crear un nuevo archivo `.json` en la carpeta +- ✅ **Persistencia**: El idioma seleccionado se guarda y se carga automáticamente al iniciar +- ⚠️ **Ubicación de carpeta**: La carpeta `lenguajes` debe estar junto al ejecutable o en el directorio de datos de la app + +--- + +## 11. Cómo Compilar y Ejecutar + +```bash +# 1. Compilar el proyecto +cmake -H. -Bbuild-cmake -GNinja +cmake --build build-cmake -j$(nproc) + +# 2. Copiar carpeta de idiomas junto al ejecutable +cp -r lenguajes build-cmake/soh/ + +# 3. Ejecutar desde la raíz del proyecto +./build-cmake/soh/soh.elf +``` + +--- + +## 12. Pendientes / Mejoras Futuras + +- [ ] Agregar más traducciones al JSON (actualmente ~250 claves) +- [ ] Traducir textos de otros menús (Enhancements, Randomizer, Network, DevTools) +- [ ] Crear archivo Portugues.json +- [ ] Posibilidad de recargar traducciones en tiempo real sin cerrar el juego + +--- + +## 13. Archivos de Referencia con Textos Hardcodeados + +### SohGui (ya analizados) +- `SohMenu_hardcoded.txt` +- `SohMenuSettings_hardcoded.txt` +- `SohMenuEnhancements_hardcoded.txt` +- `SohMenuRandomizer_hardcoded.txt` +- `SohMenuNetwork_hardcoded.txt` +- `SohMenuDevTools_hardcoded.txt` +- `SohMenuBar_hardcoded.txt` +- `ResolutionEditor_hardcoded.txt` + +--- + +## 14. Diferencias con el Plan Anterior + +| Aspecto | Plan Anterior | Plan Actual | +|---------|---------------|--------------| +| English.json | Obligatorio | No necesario (hardcodeado es fallback) | +| Reemplazo de textos | Sí, en todos los archivos | No, solo agregar función GetString | +| Archivos a modificar | 8+ archivos | 2 archivos (SohMenuSettings.cpp, SohMenu.cpp) | +| Funcionamiento sin carpeta | Requiere English.json | Funciona igual que antes | +| Stub Localization | No previsto | Necesario para compilar | +| Persistencia de idioma | No implementada | ✅ Guardado y cargado automáticamente | +| Strings en selector | Implementación con problemas | ✅ Solucionado con map estático | diff --git a/lenguajes/Espanol.json b/lenguajes/Espanol.json new file mode 100644 index 000000000..00640b3f7 --- /dev/null +++ b/lenguajes/Espanol.json @@ -0,0 +1,230 @@ +{ + "language": "Español", + "strings": { + "Settings": "Configuración", + "Enhancements": "Mejoras", + "Randomizer": "Randomizer", + "Network": "Red", + "Dev Tools": "Desarrollo", + "General": "General", + "General Settings": "Configuración General", + "Graphics": "Gráficos", + "Audio": "Audio", + "Controls": "Controles", + "Input Viewer": "Visor de Entrada", + "Notifications": "Notificaciones", + "Mod Menu": "Menú de Módos", + "About": "Acerca de", + "Enabled": "Activado", + "Disabled": "Desactivado", + "On": "Activado", + "Off": "Desactivado", + "Yes": "Sí", + "No": "No", + "Apply": "Aplicar", + "Cancel": "Cancelar", + "Resolution": "Resolución", + "FPS Limit": "Límite de FPS", + "VSync": "Sincronización Vertical", + "Master Volume": "Volumen Principal", + "Master Volume: %d %%": "Volumen Principal: %d %%", + "Main Music Volume: %d %%": "Volumen de Música Principal: %d %%", + "Sub Music Volume: %d %%": "Volumen de Música Secondary: %d %%", + "Fanfare Volume: %d %%": "Volumen de Fanfarria: %d %%", + "Sound Effects Volume: %d %%": "Volumen de Efectos: %d %%", + "Music Volume": "Volumen de Música", + "SFX Volume": "Volumen de Efectos", + "Anti-aliasing (MSAA)": "Antialiasing (MSAA)", + "Menu Settings": "Configuración del Menú", + "Menu Theme": "Tema del Menú", + "Menu Controller Navigation": "Navegación con Mando", + "Allow background inputs": "Permitir entradas en segundo plano", + "Menu Background Opacity": "Opacidad del Fondo del Menú", + "General Settings": "Configuración General", + "Cursor Always Visible": "Cursor Siempre Visible", + "Search In Sidebar": "Buscar en Barra Lateral", + "Search Input Autofocus": "Autofoco en Búsqueda", + "Reset Button Combination:": "Combinación de Botón de Reseteo:", + "Open App Files Folder": "Abrir Carpeta de Archivos", + "Boot": "Arranque", + "Boot Sequence": "Secuencia de Arranque", + "Languages": "Idiomas", + "Translate Title Screen": "Traducir Pantalla de Título", + "Language": "Idioma", + "UI Translation": "Traducción de Interfaz", + "Accessibility": "Accesibilidad", + "Text to Speech": "Texto a Voz", + "Disable Idle Camera Re-Centering": "Desactivar Recentrado de Cámara", + "Disable Screen Flash for Finishing Blow": "Desactivar Flash de Pantalla", + "Disable Jabu Wobble": "Desactivar Tambaleo de Jabu", + "EXPERIMENTAL": "EXPERIMENTAL", + "ImGui Menu Scaling": "Escala del Menú ImGui", + "Ship Of Harkinian": "Ship Of Harkinian", + "Graphics Options": "Opciones de Gráficos", + "Toggle Fullscreen": "Alternar Pantalla Completa", + "Internal Resolution": "Resolución Interna", + "Current FPS": "FPS Actuales", + "Match Refresh Rate": "Coincidir Tasa de Refresco", + "Renderer API (Needs reload)": "API de Renderizado (Requiere Recarga)", + "Enable Vsync": "Activar Vsync", + "Windowed Fullscreen": "Pantalla Completa en Ventana", + "Allow multi-windows": "Permitir Multi-ventanas", + "Texture Filter (Needs reload)": "Filtro de Textura (Requiere Recarga)", + "Advanced Graphics Options": "Opciones Avanzadas de Gráficos", + "Clear Devices": "Limpiar Dispositivos", + "Controller Bindings": "Asignaciones de Mando", + "Popout Bindings Window": "Ventana de Asignaciones", + "Input Viewer Settings": "Configuración del Visor de Entrada", + "Popout Input Viewer Settings": "Ventana de Configuración", + "Position": "Posición", + "Duration (seconds):": "Duración (segundos):", + "Background Opacity": "Opacidad del Fondo", + "Size:": "Tamaño:", + "Test Notification": "Probar Notificación", + "Mute Notification Sound": "Silenciar Sonido de Notificación", + "Popout Mod Menu Window": "Ventana de Menú de Módos", + "Saving": "Guardado", + "Autosave": "Guardado Automático", + "Notification on Autosave": "Notificación de Guardado Automático", + "Remember Save Location": "Recordar Ubicación de Guardado", + "Containers Match Contents": "Contenedores Corresponden al Contenido", + "Containers of Agony": "Contenedores de Agonía", + "Time of Day": "Hora del Día", + "Nighttime GS Always Spawn": "GS Nocturnos Siempre Aparecen", + "Pull Grave During the Day": "Tumbar Durante el Día", + "Dampe Appears All Night": "Dampe Aparece Toda la Noche", + "Exit Market at Night": "Salir del Mercado de Noche", + "Shops and Games Always Open": "Tiendas y Juegos Siempre Abiertos", + "Pause Menu": "Menú de Pausa", + "Allow the Cursor to be on Any Slot": "Permitir Cursor en Cualquier Ranura", + "Pause Warp": "Teletransporte de Pausa", + "Answer Navi Prompt with L Button": "Responder a Navi con Botón L", + "Don't Require Input for Credits Sequence": "No Requiere Input para Secuencia de Créditos", + "Include Held Inputs at the Start of Pause Buffer Input Window": "Incluir Inputs Sostenidos", + "Pause Buffer Input Window: %d frames": "Ventana de Input de Pausa: %d frames", + "Simulated Input Lag: %d frames": "Lag de Input Simulado: %d frames", + "Reworked Targeting": "Cambio de Objetivo Revisado", + "Target Switch Button Combination:": "Combinación de Botón de Cambio de Objetivo:", + "Item Count Messages": "Mensajes de Cantidad de Objetos", + "Gold Skulltula Tokens": "Tokens de Skulltula de Oro", + "Pieces of Heart": "Piezas de Corazón", + "Heart Containers": "Contenedores de Corazón", + "Misc": "Varios", + "Disable Crit Wiggle": "Desactivar Crujido Crítico", + "Better Owl": "Mejor Búho", + "Convenience": "Comodidad", + "Quit Fishing at Door": "Salir de Pescar en Puerta", + "Instant Putaway": "Guardar Instantáneo", + "Navi Timer Resets on Scene Change": "Temporizador de Navi Resetea", + "Link's Cow in Both Time Periods": "Vaca de Link en Ambos Períodos", + "Play Zelda's Lullaby to Open Sleeping Waterfall": "Canción de Zelda Abre Cascada", + "Skip Feeding Jabu-Jabu": "Saltar Alimentar Jabu-Jabu", + "Cutscenes": "Escenas", + "All##Skips": "Todas##Saltos", + "None##Skips": "Ninguna##Saltos", + "Skip Intro": "Saltar Intro", + "Great Fairies": "Hadas Grandes", + "Horse": "Caballo", + "Ganon": "Ganon", + "Dampé": "Dampé", + "Title Screen": "Pantalla de Título", + "File Select": "Selección de Archivo", + "Boss Rush": "Combate de Jefes", + "Skips": "Saltos", + "Rainbow Bridge": "Puente Arcoíris", + "Bridge Requirement": "Requisito del Puente", + "Randomizer": "Randomizer", + "Enhancements": "Mejoras", + "Cheats": "Trampas", + "Randomizer Settings": "Configuración del Randomizer", + "Keyshuffle": "Mezcla de Llaves", + "Maps & Compasses": "Mapas y Brújulas", + "Small Keys": "Llaves Pequeñas", + "Boss Keys": "Llaves de Jefe", + "Skulltulas": "Skulltulas", + "Tokens": "Tokens", + "Stones": "Piedras", + "Medallions": "Medallones", + "Dungeon Items": "Objetos de Mazmorra", + "Start with Consumables": "Empezar con Consumibles", + "Start with Max Rupees": "Empezar con Rupias Máximas", + "Start with Deku Equipment": "Equipamiento Deku Inicial", + "Open Deku Tree": "Abrir Árbol Deku", + "Open Door of Time": "Abrir Puerta del Tiempo", + "Open Kak Bridge": "Abrir Puente de Kakariko", + "Open Market Entrance": "Abrir Entrada del Mercado", + "Open Castle Gate": "Abrir Puerta del Castillo", + "Network": "Red", + "Connect to Server": "Conectar al Servidor", + "Disconnect": "Desconectar", + "Server Address": "Dirección del Servidor", + "Username": "Nombre de Usuario", + "Room ID": "ID de Sala", + "Game Mode": "Modo de Juego", + "Co-op": "Cooperativo", + "Adventure": "Aventura", + "Time Sync": "Sincronización de Tiempo", + "Lag Compensation": "Compensación de Lag", + "Dev Tools": "Herramientas de Desarrollo", + "General": "General", + "Game Interaction": "Interacción del Juego", + "Visual": "Visual", + "Audio": "Audio", + "Cheats": "Trampas", + "Cosmetics": "Cosméticos", + "Restrict Debug Mode": "Restringir Modo Debug", + "Free Camera": "Cámara Libre", + "Frame Advance": "Avance de Fotograma", + "Pause Game": "Pausar Juego", + "Log Object Ages": "Registrar Edades de Objetos", + "Visual Cheats": "Trampas Visuales", + "No UI": "Sin Interfaz", + "Cheat Cheats": "Trampas de Trampas", + "Infinite Gold": "Oro Infinito", + "Infinite Health": "Salud Infinita", + "Infinite Magic": "Magia Infinita", + "Infinite Nails": "Uñas Infinitas", + "Infinite Eggs": "Huevos Infinitos", + "Infinite Arrows": "Flechas Infinitas", + "Unbreakable Umbrella": "Paraguas Irrompible", + "Cosmetics": "Cosméticos", + "Tunic Color": "Color del Túnico", + "Skin Color": "Color de Piel", + "Mirror Shield Frame": "Marco del Escudo Espejo", + "Link's Age": "Edad de Link", + "Default": "Predeterminado", + "Adult": "Adulto", + "Child": "Niño", + "Small": "Pequeño", + "Normal": "Normal", + "Large": "Grande", + "X-Large": "Extra Grande", + "Red": "Rojo", + "Dark Red": "Rojo Oscuro", + "Orange": "Naranja", + "Green": "Verde", + "Dark Green": "Verde Oscuro", + "Light Blue": "Azul Claro", + "Blue": "Azul", + "Dark Blue": "Azul Oscuro", + "Indigo": "Índigo", + "Violet": "Violeta", + "Purple": "Púrpura", + "Brown": "Marrón", + "Gray": "Gris", + "Dark Gray": "Gris Oscuro", + "Three-Point": "Tres Puntos", + "Linear": "Lineal", + "None": "Ninguno", + "Top Left": "Arriba Izquierda", + "Top Right": "Arriba Derecha", + "Bottom Left": "Abajo Izquierda", + "Bottom Right": "Abajo Derecha", + "Hidden": "Oculto", + "Default": "Predeterminado", + "Authentic": "Auténtico", + "File Select": "Selección de Archivo", + "Debug Warp Screen": "Pantalla de Teletransporte Debug", + "Warp Point": "Punto de Teletransporte" + } +} diff --git a/soh/soh/Localization.cpp b/soh/soh/Localization.cpp new file mode 100644 index 000000000..ec62732af --- /dev/null +++ b/soh/soh/Localization.cpp @@ -0,0 +1,7 @@ +#include "Localization.h" + +namespace Localization { + std::string GetLanguageString(const char* key) { + return key; + } +} diff --git a/soh/soh/Localization.h b/soh/soh/Localization.h new file mode 100644 index 000000000..afbddbebf --- /dev/null +++ b/soh/soh/Localization.h @@ -0,0 +1,12 @@ +#ifndef LOCALIZATION_H +#define LOCALIZATION_H + +#include + +namespace Localization { + std::string GetLanguageString(const char* key); +} + +#define LUS_LOC(key) key + +#endif diff --git a/soh/soh/SohGui/LanguageManager.cpp b/soh/soh/SohGui/LanguageManager.cpp new file mode 100644 index 000000000..e304b0120 --- /dev/null +++ b/soh/soh/SohGui/LanguageManager.cpp @@ -0,0 +1,122 @@ +#include "LanguageManager.h" +#include +#include +#include +#include + +using json = nlohmann::json; + +namespace SohGui { + +LanguageManager& LanguageManager::Instance() { + static LanguageManager instance; + return instance; +} + +void LanguageManager::Init() { + ScanLanguageFiles(); +} + +void LanguageManager::ScanLanguageFiles() { + availableLanguages.clear(); + + std::string langDir = GetLanguagesDirectory(); + + if (!std::filesystem::exists(langDir)) { + return; + } + + for (const auto& entry : std::filesystem::directory_iterator(langDir)) { + if (entry.is_regular_file() && entry.path().extension() == ".json") { + std::string filename = entry.path().stem().string(); + availableLanguages.push_back(filename); + } + } +} + +std::string LanguageManager::GetLanguagesDirectory() { + std::string currentDir = std::filesystem::current_path().string(); + + std::string langDir = currentDir + "/lenguajes"; + if (std::filesystem::exists(langDir)) { + return langDir; + } + + std::string appDir = Ship::Context::GetInstance()->GetAppDirectoryPath(); + langDir = appDir + "/lenguajes"; + if (std::filesystem::exists(langDir)) { + return langDir; + } + + return currentDir + "/lenguajes"; +} + +bool LanguageManager::LoadJsonFile(const std::string& path) { + try { + std::ifstream file(path); + if (!file.is_open()) { + return false; + } + + json j; + file >> j; + + translations.clear(); + + if (j.contains("strings") && j["strings"].is_object()) { + for (auto& [key, value] : j["strings"].items()) { + translations[key] = value.get(); + } + } + + translationLoaded = true; + return true; + } catch (const std::exception& e) { + translationLoaded = false; + return false; + } +} + +void LanguageManager::LoadLanguage(const std::string& languageName) { + std::string langDir = GetLanguagesDirectory(); + std::string filePath = langDir + "/" + languageName + ".json"; + + if (!std::filesystem::exists(langDir)) { + std::filesystem::create_directories(langDir); + } + + if (LoadJsonFile(filePath)) { + currentLanguage = languageName; + } else { + translations.clear(); + translationLoaded = false; + currentLanguage = ""; + } +} + +std::string LanguageManager::GetString(const std::string& key) { + if (!translationLoaded || translations.empty()) { + return key; + } + + auto it = translations.find(key); + if (it != translations.end()) { + return it->second; + } + + return key; +} + +std::vector LanguageManager::GetAvailableLanguages() { + return availableLanguages; +} + +std::string LanguageManager::GetCurrentLanguage() { + return currentLanguage; +} + +bool LanguageManager::IsTranslationLoaded() { + return translationLoaded; +} + +} // namespace SohGui diff --git a/soh/soh/SohGui/LanguageManager.h b/soh/soh/SohGui/LanguageManager.h new file mode 100644 index 000000000..e1d21bd62 --- /dev/null +++ b/soh/soh/SohGui/LanguageManager.h @@ -0,0 +1,39 @@ +#ifndef LANGUAGE_MANAGER_H +#define LANGUAGE_MANAGER_H + +#include +#include +#include + +namespace SohGui { + +class LanguageManager { +public: + static LanguageManager& Instance(); + + void Init(); + void LoadLanguage(const std::string& languageName); + std::string GetString(const std::string& key); + std::vector GetAvailableLanguages(); + std::string GetCurrentLanguage(); + bool IsTranslationLoaded(); + +private: + LanguageManager() = default; + ~LanguageManager() = default; + LanguageManager(const LanguageManager&) = delete; + LanguageManager& operator=(const LanguageManager&) = delete; + + std::string currentLanguage; + std::map translations; + bool translationLoaded = false; + std::vector availableLanguages; + + void ScanLanguageFiles(); + bool LoadJsonFile(const std::string& path); + std::string GetLanguagesDirectory(); +}; + +} // namespace SohGui + +#endif // LANGUAGE_MANAGER_H diff --git a/soh/soh/SohGui/SohMenu.cpp b/soh/soh/SohGui/SohMenu.cpp new file mode 100644 index 000000000..0ca18314c --- /dev/null +++ b/soh/soh/SohGui/SohMenu.cpp @@ -0,0 +1,183 @@ +#include "SohMenu.h" +#include "LanguageManager.h" +#include +#include +#include +#include + +extern "C" { +extern PlayState* gPlayState; +} + +extern std::unordered_map warpPointSceneList; + +namespace SohGui { +extern std::shared_ptr mSohMenu; + +using namespace UIWidgets; + +void SohMenu::AddSidebarEntry(std::string sectionName, std::string sidebarName, uint32_t columnCount) { + assert(!sectionName.empty()); + assert(!sidebarName.empty()); + menuEntries.at(sectionName).sidebars.emplace(sidebarName, SidebarEntry{ .columnCount = columnCount }); + menuEntries.at(sectionName).sidebarOrder.push_back(sidebarName); +} + +WidgetInfo& SohMenu::AddWidget(WidgetPath& pathInfo, std::string widgetName, WidgetType widgetType) { + assert(!widgetName.empty()); // Must be unique + assert(menuEntries.contains(pathInfo.sectionName)); // Section/header must already exist + assert(menuEntries.at(pathInfo.sectionName).sidebars.contains(pathInfo.sidebarName)); // Sidebar must already exist + std::unordered_map& sidebar = menuEntries.at(pathInfo.sectionName).sidebars; + uint8_t column = pathInfo.column; + if (sidebar.contains(pathInfo.sidebarName)) { + while (sidebar.at(pathInfo.sidebarName).columnWidgets.size() < column + 1) { + sidebar.at(pathInfo.sidebarName).columnWidgets.push_back({}); + } + } + SidebarEntry& entry = sidebar.at(pathInfo.sidebarName); + std::string translatedName = LanguageManager::Instance().GetString(widgetName); + entry.columnWidgets.at(column).push_back({ .name = translatedName, .type = widgetType }); + WidgetInfo& widget = entry.columnWidgets.at(column).back(); + switch (widgetType) { + case WIDGET_CHECKBOX: + case WIDGET_CVAR_CHECKBOX: + widget.options = std::make_shared(); + break; + case WIDGET_SLIDER_FLOAT: + case WIDGET_CVAR_SLIDER_FLOAT: + widget.options = std::make_shared(); + break; + case WIDGET_CVAR_BTN_SELECTOR: + widget.options = std::make_shared(); + break; + case WIDGET_SLIDER_INT: + case WIDGET_CVAR_SLIDER_INT: + widget.options = std::make_shared(); + break; + case WIDGET_COMBOBOX: + case WIDGET_CVAR_COMBOBOX: + case WIDGET_AUDIO_BACKEND: + case WIDGET_VIDEO_BACKEND: + widget.options = std::make_shared(); + break; + case WIDGET_BUTTON: + widget.options = std::make_shared(); + break; + case WIDGET_WINDOW_BUTTON: + widget.options = std::make_shared(); + break; + case WIDGET_CVAR_COLOR_PICKER: + case WIDGET_COLOR_PICKER: + widget.options = std::make_shared(); + break; + case WIDGET_SEPARATOR_TEXT: + case WIDGET_TEXT: + widget.options = std::make_shared(); + break; + case WIDGET_SEARCH: + case WIDGET_SEPARATOR: + default: + widget.options = std::make_shared(); + } + return widget; +} + +SohMenu::SohMenu(const std::string& consoleVariable, const std::string& name) + : Menu(consoleVariable, name, 0, UIWidgets::Colors::LightBlue) { +} + +void SohMenu::AddMenuElements() { + AddMenuSettings(); + AddMenuEnhancements(); + AddMenuRandomizer(); + AddMenuNetwork(); + AddMenuDevTools(); + + if (CVarGetInteger(CVAR_SETTING("Menu.SidebarSearch"), 0)) { + InsertSidebarSearch(); + } + + for (auto& initFunc : MenuInit::GetInitFuncs()) { + initFunc(); + } + + mMenuElementsInitialized = true; +} + +void SohMenu::InitElement() { + Ship::Menu::InitElement(); + + disabledMap = { + { DISABLE_FOR_NO_VSYNC, + { [](disabledInfo& info) -> bool { + return !Ship::Context::GetInstance()->GetWindow()->CanDisableVerticalSync(); + }, + "Disabling VSync not supported" } }, + { DISABLE_FOR_NO_WINDOWED_FULLSCREEN, + { [](disabledInfo& info) -> bool { + return !Ship::Context::GetInstance()->GetWindow()->SupportsWindowedFullscreen(); + }, + "Windowed Fullscreen not supported" } }, + { DISABLE_FOR_NO_MULTI_VIEWPORT, + { [](disabledInfo& info) -> bool { + return !Ship::Context::GetInstance()->GetWindow()->GetGui()->SupportsViewports(); + }, + "Multi-viewports not supported" } }, + { DISABLE_FOR_NOT_DIRECTX, + { [](disabledInfo& info) -> bool { + return Ship::Context::GetInstance()->GetWindow()->GetWindowBackend() != + Ship::WindowBackend::FAST3D_DXGI_DX11; + }, + "Available Only on DirectX" } }, + { DISABLE_FOR_DIRECTX, + { [](disabledInfo& info) -> bool { + return Ship::Context::GetInstance()->GetWindow()->GetWindowBackend() == + Ship::WindowBackend::FAST3D_DXGI_DX11; + }, + "Not Available on DirectX" } }, + { DISABLE_FOR_MATCH_REFRESH_RATE_ON, + { [](disabledInfo& info) -> bool { return CVarGetInteger(CVAR_SETTING("MatchRefreshRate"), 0); }, + "Match Refresh Rate is Enabled" } }, + { DISABLE_FOR_ADVANCED_RESOLUTION_ON, + { [](disabledInfo& info) -> bool { return CVarGetInteger(CVAR_PREFIX_ADVANCED_RESOLUTION ".Enabled", 0); }, + "Advanced Resolution Enabled" } }, + { DISABLE_FOR_VERTICAL_RES_TOGGLE_ON, + { [](disabledInfo& info) -> bool { + return CVarGetInteger(CVAR_PREFIX_ADVANCED_RESOLUTION ".VerticalResolutionToggle", 0); + }, + "Vertical Resolution Toggle Enabled" } }, + { DISABLE_FOR_LOW_RES_MODE_ON, + { [](disabledInfo& info) -> bool { return CVarGetInteger(CVAR_LOW_RES_MODE, 0); }, "N64 Mode Enabled" } }, + { DISABLE_FOR_NULL_PLAY_STATE, + { [](disabledInfo& info) -> bool { return gPlayState == NULL; }, "Save Not Loaded" } }, + { DISABLE_FOR_DEBUG_MODE_OFF, + { [](disabledInfo& info) -> bool { return !CVarGetInteger(CVAR_DEVELOPER_TOOLS("DebugEnabled"), 0); }, + "Debug Mode is Disabled" } }, + { DISABLE_FOR_FRAME_ADVANCE_OFF, + { [](disabledInfo& info) -> bool { return !(gPlayState != nullptr && gPlayState->frameAdvCtx.enabled); }, + "Frame Advance is Disabled" } }, + { DISABLE_FOR_ADVANCED_RESOLUTION_OFF, + { [](disabledInfo& info) -> bool { return !CVarGetInteger(CVAR_PREFIX_ADVANCED_RESOLUTION ".Enabled", 0); }, + "Advanced Resolution is Disabled" } }, + { DISABLE_FOR_VERTICAL_RESOLUTION_OFF, + { [](disabledInfo& info) -> bool { + return !CVarGetInteger(CVAR_PREFIX_ADVANCED_RESOLUTION ".VerticalResolutionToggle", 0); + }, + "Vertical Resolution Toggle is Off" } }, + }; +} + +void SohMenu::UpdateElement() { + Ship::Menu::UpdateElement(); +} + +void SohMenu::Draw() { + Ship::Menu::Draw(); +} + +void SohMenu::DrawElement() { + if (mMenuElementsInitialized) { + Ship::Menu::DrawElement(); + } +} +} // namespace SohGui diff --git a/soh/soh/SohGui/SohMenuSettings.cpp b/soh/soh/SohGui/SohMenuSettings.cpp new file mode 100644 index 000000000..a5e7dc680 --- /dev/null +++ b/soh/soh/SohGui/SohMenuSettings.cpp @@ -0,0 +1,592 @@ +#include "SohMenu.h" +#include "soh/Notification/Notification.h" +#include "soh/Enhancements/enhancementTypes.h" +#include "SohModals.h" +#include "soh/OTRGlobals.h" +#include +#include "soh/ResourceManagerHelpers.h" +#include "UIWidgets.hpp" +#include "LanguageManager.h" +#include +#include + +extern "C" { +#include "include/z64audio.h" +#include "variables.h" +} + +namespace SohGui { + +extern std::shared_ptr mSohMenu; +extern std::shared_ptr mModalWindow; +using namespace UIWidgets; + +static std::map uiLanguageOptionsStr = { }; +static std::map uiLanguageOptions = { }; + +static void InitUILanguages() { + LanguageManager::Instance().Init(); + uiLanguageOptionsStr.clear(); + uiLanguageOptions.clear(); + + uiLanguageOptionsStr[0] = "None"; + uiLanguageOptions[0] = "None"; + + int32_t idx = 1; + for (const auto& lang : LanguageManager::Instance().GetAvailableLanguages()) { + if (!lang.empty()) { + uiLanguageOptionsStr[idx] = lang; + uiLanguageOptions[idx] = uiLanguageOptionsStr[idx].c_str(); + idx++; + } + } + + int32_t savedLang = CVarGetInteger(CVAR_SETTING("UILanguage"), 0); + if (savedLang > 0 && savedLang < (int32_t)LanguageManager::Instance().GetAvailableLanguages().size() + 1) { + std::string langName = LanguageManager::Instance().GetAvailableLanguages()[savedLang - 1]; + LanguageManager::Instance().LoadLanguage(langName); + } +} + +static std::map imguiScaleOptions = { + { 0, "Small" }, + { 1, "Normal" }, + { 2, "Large" }, + { 3, "X-Large" }, +}; + +static const std::map menuThemeOptions = { + { UIWidgets::Colors::Red, "Red" }, + { UIWidgets::Colors::DarkRed, "Dark Red" }, + { UIWidgets::Colors::Orange, "Orange" }, + { UIWidgets::Colors::Green, "Green" }, + { UIWidgets::Colors::DarkGreen, "Dark Green" }, + { UIWidgets::Colors::LightBlue, "Light Blue" }, + { UIWidgets::Colors::Blue, "Blue" }, + { UIWidgets::Colors::DarkBlue, "Dark Blue" }, + { UIWidgets::Colors::Indigo, "Indigo" }, + { UIWidgets::Colors::Violet, "Violet" }, + { UIWidgets::Colors::Purple, "Purple" }, + { UIWidgets::Colors::Brown, "Brown" }, + { UIWidgets::Colors::Gray, "Gray" }, + { UIWidgets::Colors::DarkGray, "Dark Gray" }, +}; + +static const std::map textureFilteringMap = { + { Fast::FILTER_THREE_POINT, "Three-Point" }, + { Fast::FILTER_LINEAR, "Linear" }, + { Fast::FILTER_NONE, "None" }, +}; + +static const std::map notificationPosition = { + { 0, "Top Left" }, { 1, "Top Right" }, { 2, "Bottom Left" }, { 3, "Bottom Right" }, { 4, "Hidden" }, +}; + +static const std::map bootSequenceLabels = { + { BOOTSEQUENCE_DEFAULT, "Default" }, { BOOTSEQUENCE_AUTHENTIC, "Authentic" }, + { BOOTSEQUENCE_FILESELECT, "File Select" }, { BOOTSEQUENCE_DEBUGWARPSCREEN, "Debug Warp Screen" }, + { BOOTSEQUENCE_WARPPOINT, "Warp Point" }, +}; + +const char* GetGameVersionString(uint32_t index) { + uint32_t gameVersion = ResourceMgr_GetGameVersion(index); + switch (gameVersion) { + case OOT_NTSC_US_10: + return "NTSC 1.0"; + case OOT_NTSC_US_11: + return "NTSC 1.1"; + case OOT_NTSC_US_12: + return "NTSC 1.2"; + case OOT_NTSC_US_GC: + return "NTSC-U GC"; + case OOT_NTSC_JP_GC: + return "NTSC-J GC"; + case OOT_NTSC_JP_GC_CE: + return "NTSC-J GC (Collector's Edition)"; + case OOT_NTSC_US_MQ: + return "NTSC-U MQ"; + case OOT_NTSC_JP_MQ: + return "NTSC-J MQ"; + case OOT_PAL_10: + return "PAL 1.0"; + case OOT_PAL_11: + return "PAL 1.1"; + case OOT_PAL_GC: + return "PAL GC"; + case OOT_PAL_MQ: + return "PAL MQ"; + case OOT_PAL_GC_DBG1: + case OOT_PAL_GC_DBG2: + return "PAL GC-D"; + case OOT_PAL_GC_MQ_DBG: + return "PAL MQ-D"; + case OOT_IQUE_CN: + return "IQUE CN"; + case OOT_IQUE_TW: + return "IQUE TW"; + default: + return "UNKNOWN"; + } +} + +#include "message_data_static.h" +extern "C" MessageTableEntry* sNesMessageEntryTablePtr; +extern "C" MessageTableEntry* sGerMessageEntryTablePtr; +extern "C" MessageTableEntry* sFraMessageEntryTablePtr; +extern "C" MessageTableEntry* sJpnMessageEntryTablePtr; + +static const std::array messageTables = { + &sNesMessageEntryTablePtr, &sGerMessageEntryTablePtr, &sFraMessageEntryTablePtr, &sJpnMessageEntryTablePtr +}; + +void SohMenu::UpdateLanguageMap(std::map& languageMap) { + for (int32_t i = LANGUAGE_ENG; i < LANGUAGE_MAX; i++) { + if (*messageTables.at(i) != NULL) { + if (!languageMap.contains(i)) { + languageMap.insert(std::make_pair(i, languages.at(i))); + } + } else { + languageMap.erase(i); + } + } +} + +void SohMenu::AddMenuSettings() { + InitUILanguages(); + + // Add Settings Menu + AddMenuEntry("Settings", CVAR_SETTING("Menu.SettingsSidebarSection")); + AddSidebarEntry("Settings", "General", 2); + WidgetPath path = { "Settings", "General", SECTION_COLUMN_1 }; + + // General - Settings + AddWidget(path, "Menu Settings", WIDGET_SEPARATOR_TEXT); + AddWidget(path, "Menu Theme", WIDGET_CVAR_COMBOBOX) + .CVar(CVAR_SETTING("Menu.Theme")) + .RaceDisable(false) + .Options(ComboboxOptions() + .Tooltip("Changes the Theme of the Menu Widgets.") + .ComboMap(menuThemeOptions) + .DefaultIndex(Colors::LightBlue)); +#if not defined(__SWITCH__) and not defined(__WIIU__) + AddWidget(path, "Menu Controller Navigation", WIDGET_CVAR_CHECKBOX) + .CVar(CVAR_IMGUI_CONTROLLER_NAV) + .RaceDisable(false) + .Options(CheckboxOptions().Tooltip( + "Allows controller navigation of the port menu (Settings, Enhancements,...)\nCAUTION: " + "This will disable game inputs while the menu is visible.\n\nD-pad to move between " + "items, A to select, B to move up in scope.")); + AddWidget(path, "Allow background inputs", WIDGET_CVAR_CHECKBOX) + .CVar(CVAR_ALLOW_BACKGROUND_INPUTS) + .RaceDisable(false) + .Callback([](WidgetInfo& info) { + SDL_SetHint(SDL_HINT_JOYSTICK_ALLOW_BACKGROUND_EVENTS, + CVarGetInteger(CVAR_ALLOW_BACKGROUND_INPUTS, 1) ? "1" : "0"); + }) + .Options(CheckboxOptions() + .Tooltip("Allows controller inputs to be picked up by the game even when the game window isn't " + "the focused window.") + .DefaultValue(1)); + AddWidget(path, "Menu Background Opacity", WIDGET_CVAR_SLIDER_FLOAT) + .CVar(CVAR_SETTING("Menu.BackgroundOpacity")) + .RaceDisable(false) + .Options(FloatSliderOptions().DefaultValue(0.85f).IsPercentage().Tooltip( + "Sets the opacity of the background of the port menu.")); + + AddWidget(path, "General Settings", WIDGET_SEPARATOR_TEXT); + AddWidget(path, "Cursor Always Visible", WIDGET_CVAR_CHECKBOX) + .CVar(CVAR_SETTING("CursorVisibility")) + .RaceDisable(false) + .Callback([](WidgetInfo& info) { + Ship::Context::GetInstance()->GetWindow()->SetForceCursorVisibility( + CVarGetInteger(CVAR_SETTING("CursorVisibility"), 0)); + }) + .Options(CheckboxOptions().Tooltip("Makes the cursor always visible, even in full screen.")); +#endif + AddWidget(path, "Search In Sidebar", WIDGET_CVAR_CHECKBOX) + .CVar(CVAR_SETTING("Menu.SidebarSearch")) + .RaceDisable(false) + .Callback([](WidgetInfo& info) { + if (CVarGetInteger(CVAR_SETTING("Menu.SidebarSearch"), 0)) { + mSohMenu->InsertSidebarSearch(); + } else { + mSohMenu->RemoveSidebarSearch(); + } + }) + .Options(CheckboxOptions().Tooltip( + "Displays the Search menu as a sidebar entry in Settings instead of in the header.")); + AddWidget(path, "Search Input Autofocus", WIDGET_CVAR_CHECKBOX) + .CVar(CVAR_SETTING("Menu.SearchAutofocus")) + .RaceDisable(false) + .Options(CheckboxOptions().Tooltip( + "Search input box gets autofocus when visible. Does not affect using other widgets.")); + AddWidget(path, "Reset Button Combination:", WIDGET_CVAR_BTN_SELECTOR) + .CVar("gSettings.ResetBtn") + .Options(BtnSelectorOptions().DefaultValue(BTN_CUSTOM_MODIFIER2)); + AddWidget(path, "Open App Files Folder", WIDGET_BUTTON) + .RaceDisable(false) + .Callback([](WidgetInfo& info) { + std::string filesPath = Ship::Context::GetInstance()->GetAppDirectoryPath(); + SDL_OpenURL(std::string("file:///" + std::filesystem::absolute(filesPath).string()).c_str()); + }) + .Options(ButtonOptions().Tooltip("Opens the folder that contains the save and mods folders, etc.")); + + AddWidget(path, "Boot", WIDGET_SEPARATOR_TEXT); + AddWidget(path, "Boot Sequence", WIDGET_CVAR_COMBOBOX) + .CVar(CVAR_SETTING("BootSequence")) + .RaceDisable(false) + .Options(ComboboxOptions() + .DefaultIndex(BOOTSEQUENCE_DEFAULT) + .LabelPosition(LabelPositions::Far) + .ComponentAlignment(ComponentAlignments::Right) + .ComboMap(bootSequenceLabels) + .Tooltip("Configure what happens when starting or resetting the game.\n\n" + "Default: LUS logo -> N64 logo\n" + "Authentic: N64 logo only\n" + "File Select: Skip to file select menu\n" + "Debug Warp Screen: Skip to the debug warp screen\n" + "Warp Point: Skip to active warp point (if set), see Dev Tools -> General")); + + AddWidget(path, "Languages", WIDGET_SEPARATOR_TEXT); + AddWidget(path, "Translate Title Screen", WIDGET_CVAR_CHECKBOX) + .CVar(CVAR_SETTING("TitleScreenTranslation")) + .RaceDisable(false); + AddWidget(path, "Language", WIDGET_CVAR_COMBOBOX) + .CVar(CVAR_SETTING("Languages")) + .RaceDisable(false) + .PreFunc([](WidgetInfo& info) { + auto options = std::static_pointer_cast(info.options); + SohMenu::UpdateLanguageMap(options->comboMap); + }) + .Options(ComboboxOptions() + .LabelPosition(LabelPositions::Far) + .ComponentAlignment(ComponentAlignments::Right) + .ComboMap(languages) + .DefaultIndex(LANGUAGE_ENG)); + AddWidget(path, "UI Translation", WIDGET_CVAR_COMBOBOX) + .CVar(CVAR_SETTING("UILanguage")) + .RaceDisable(false) + .Callback([](WidgetInfo& info) { + int32_t selectedIndex = CVarGetInteger(info.cVar, 0); + SPDLOG_INFO("UI Translation callback: selectedIndex={}", selectedIndex); + if (selectedIndex == 0) { + LanguageManager::Instance().LoadLanguage(""); + } else { + auto languages = LanguageManager::Instance().GetAvailableLanguages(); + if (selectedIndex - 1 < (int32_t)languages.size()) { + std::string langName = languages[selectedIndex - 1]; + SPDLOG_INFO("UI Translation callback: loading={}", langName); + LanguageManager::Instance().LoadLanguage(langName); + SPDLOG_INFO("UI Translation callback: translation loaded={}", LanguageManager::Instance().IsTranslationLoaded()); + } + } + }) + .Options(ComboboxOptions() + .LabelPosition(LabelPositions::Far) + .ComponentAlignment(ComponentAlignments::Right) + .ComboMap(uiLanguageOptions) + .Tooltip("Select the UI translation language. Place .json files in the /lenguajes folder.")); + AddWidget(path, "Accessibility", WIDGET_SEPARATOR_TEXT); +#if defined(_WIN32) || defined(__APPLE__) || defined(ESPEAK) + AddWidget(path, "Text to Speech", WIDGET_CVAR_CHECKBOX) + .CVar(CVAR_SETTING("A11yTTS")) + .RaceDisable(false) + .Options(CheckboxOptions().Tooltip("Enables text to speech for in game dialog")); +#endif + AddWidget(path, "Disable Idle Camera Re-Centering", WIDGET_CVAR_CHECKBOX) + .CVar(CVAR_SETTING("A11yDisableIdleCam")) + .RaceDisable(false) + .Options(CheckboxOptions().Tooltip("Disables the automatic re-centering of the camera when idle.")); + AddWidget(path, "Disable Screen Flash for Finishing Blow", WIDGET_CVAR_CHECKBOX) + .CVar(CVAR_SETTING("A11yNoScreenFlashForFinishingBlow")) + .RaceDisable(false) + .Options(CheckboxOptions().Tooltip("Disables the white screen flash on enemy kill.")); + AddWidget(path, "Disable Jabu Wobble", WIDGET_CVAR_CHECKBOX) + .CVar(CVAR_SETTING("A11yNoJabuWobble")) + .RaceDisable(false) + .Options(CheckboxOptions().Tooltip("Disable the geometry wobble and camera distortion inside Jabu.")); + AddWidget(path, "EXPERIMENTAL", WIDGET_SEPARATOR_TEXT).Options(TextOptions().Color(Colors::Orange)); + AddWidget(path, "ImGui Menu Scaling", WIDGET_CVAR_COMBOBOX) + .CVar(CVAR_SETTING("ImGuiScale")) + .RaceDisable(false) + .Options(ComboboxOptions() + .ComboMap(imguiScaleOptions) + .Tooltip("Changes the scaling of the ImGui menu elements.") + .DefaultIndex(1) + .ComponentAlignment(ComponentAlignments::Right) + .LabelPosition(LabelPositions::Far)) + .Callback([](WidgetInfo& info) { OTRGlobals::Instance->ScaleImGui(); }); + + // General - About + path.column = SECTION_COLUMN_2; + + AddWidget(path, "About", WIDGET_SEPARATOR_TEXT); + AddWidget(path, "Ship Of Harkinian", WIDGET_TEXT); + if (gGitCommitTag[0] != 0) { + AddWidget(path, gBuildVersion, WIDGET_TEXT); + } else { + AddWidget(path, ("Branch: " + std::string(gGitBranch)), WIDGET_TEXT); + AddWidget(path, ("Commit: " + std::string(gGitCommitHash)), WIDGET_TEXT); + } + for (uint32_t i = 0; i < ResourceMgr_GetNumGameVersions(); i++) { + AddWidget(path, GetGameVersionString(i), WIDGET_TEXT); + } + + // Audio Settings + path.sidebarName = "Audio"; + path.column = SECTION_COLUMN_1; + AddSidebarEntry("Settings", "Audio", 3); + + AddWidget(path, "Master Volume: %d %%", WIDGET_CVAR_SLIDER_INT) + .CVar(CVAR_SETTING("Volume.Master")) + .RaceDisable(false) + .Options(IntSliderOptions().Min(0).Max(100).DefaultValue(40).ShowButtons(true).Format("")); + AddWidget(path, "Main Music Volume: %d %%", WIDGET_CVAR_SLIDER_INT) + .CVar(CVAR_SETTING("Volume.MainMusic")) + .RaceDisable(false) + .Options(IntSliderOptions().Min(0).Max(100).DefaultValue(100).ShowButtons(true).Format("")) + .Callback([](WidgetInfo& info) { + Audio_SetGameVolume(SEQ_PLAYER_BGM_MAIN, + ((float)CVarGetInteger(CVAR_SETTING("Volume.MainMusic"), 100) / 100.0f)); + }); + AddWidget(path, "Sub Music Volume: %d %%", WIDGET_CVAR_SLIDER_INT) + .CVar(CVAR_SETTING("Volume.SubMusic")) + .RaceDisable(false) + .Options(IntSliderOptions().Min(0).Max(100).DefaultValue(100).ShowButtons(true).Format("")) + .Callback([](WidgetInfo& info) { + Audio_SetGameVolume(SEQ_PLAYER_BGM_SUB, + ((float)CVarGetInteger(CVAR_SETTING("Volume.SubMusic"), 100) / 100.0f)); + }); + AddWidget(path, "Fanfare Volume: %d %%", WIDGET_CVAR_SLIDER_INT) + .CVar(CVAR_SETTING("Volume.Fanfare")) + .RaceDisable(false) + .Options(IntSliderOptions().Min(0).Max(100).DefaultValue(100).ShowButtons(true).Format("")) + .Callback([](WidgetInfo& info) { + Audio_SetGameVolume(SEQ_PLAYER_FANFARE, + ((float)CVarGetInteger(CVAR_SETTING("Volume.Fanfare"), 100) / 100.0f)); + }); + AddWidget(path, "Sound Effects Volume: %d %%", WIDGET_CVAR_SLIDER_INT) + .CVar(CVAR_SETTING("Volume.SFX")) + .RaceDisable(false) + .Options(IntSliderOptions().Min(0).Max(100).DefaultValue(100).ShowButtons(true).Format("")) + .Callback([](WidgetInfo& info) { + Audio_SetGameVolume(SEQ_PLAYER_SFX, ((float)CVarGetInteger(CVAR_SETTING("Volume.SFX"), 100) / 100.0f)); + }); + AddWidget(path, "Audio API (Needs reload)", WIDGET_AUDIO_BACKEND).RaceDisable(false); + + // Graphics Settings + static int32_t maxFps = 360; + const char* tooltip = "Uses Matrix Interpolation to create extra frames, resulting in smoother graphics. This is " + "purely visual and does not impact game logic, execution of glitches etc.\n\nA higher target " + "FPS than your monitor's refresh rate will waste resources, and might give a worse result."; + path.sidebarName = "Graphics"; + AddSidebarEntry("Settings", "Graphics", 3); + AddWidget(path, "Graphics Options", WIDGET_SEPARATOR_TEXT); + AddWidget(path, "Toggle Fullscreen", WIDGET_BUTTON) + .RaceDisable(false) + .Callback([](WidgetInfo& info) { Ship::Context::GetInstance()->GetWindow()->ToggleFullscreen(); }) + .Options(ButtonOptions().Tooltip("Toggles Fullscreen On/Off.")); + AddWidget(path, "Internal Resolution", WIDGET_CVAR_SLIDER_FLOAT) + .CVar(CVAR_INTERNAL_RESOLUTION) + .RaceDisable(false) + .Callback([](WidgetInfo& info) { + Ship::Context::GetInstance()->GetWindow()->SetResolutionMultiplier( + CVarGetFloat(CVAR_INTERNAL_RESOLUTION, 1)); + }) + .PreFunc([](WidgetInfo& info) { + if (mSohMenu->disabledMap.at(DISABLE_FOR_ADVANCED_RESOLUTION_ON).active && + mSohMenu->disabledMap.at(DISABLE_FOR_VERTICAL_RES_TOGGLE_ON).active) { + info.activeDisables.push_back(DISABLE_FOR_ADVANCED_RESOLUTION_ON); + info.activeDisables.push_back(DISABLE_FOR_VERTICAL_RES_TOGGLE_ON); + } else if (mSohMenu->disabledMap.at(DISABLE_FOR_LOW_RES_MODE_ON).active) { + info.activeDisables.push_back(DISABLE_FOR_LOW_RES_MODE_ON); + } + }) + .Options( + FloatSliderOptions() + .Tooltip("Multiplies your output resolution by the value inputted, as a more intensive but effective " + "form of anti-aliasing.") + .ShowButtons(false) + .IsPercentage() + .Min(0.5f) + .Max(2.0f)); +#ifndef __WIIU__ + AddWidget(path, "Anti-aliasing (MSAA)", WIDGET_CVAR_SLIDER_INT) + .CVar(CVAR_MSAA_VALUE) + .RaceDisable(false) + .Callback([](WidgetInfo& info) { + Ship::Context::GetInstance()->GetWindow()->SetMsaaLevel(CVarGetInteger(CVAR_MSAA_VALUE, 1)); + }) + .Options( + IntSliderOptions() + .Tooltip("Activates MSAA (multi-sample anti-aliasing) from 2x up to 8x, to smooth the edges of " + "rendered geometry.\n" + "Higher sample count will result in smoother edges on models, but may reduce performance.") + .Min(1) + .Max(8) + .DefaultValue(1)); +#endif + auto fps = CVarGetInteger(CVAR_SETTING("InterpolationFPS"), 20); + const char* fpsFormat = fps == 20 ? "Original (%d)" : "%d"; + AddWidget(path, "Current FPS", WIDGET_CVAR_SLIDER_INT) + .CVar(CVAR_SETTING("InterpolationFPS")) + .RaceDisable(false) + .Callback([](WidgetInfo& info) { + auto options = std::static_pointer_cast(info.options); + int32_t defaultValue = options->defaultValue; + if (CVarGetInteger(info.cVar, defaultValue) == defaultValue) { + options->format = "Original (%d)"; + } else { + options->format = "%d"; + } + }) + .PreFunc([](WidgetInfo& info) { + if (mSohMenu->disabledMap.at(DISABLE_FOR_MATCH_REFRESH_RATE_ON).active) + info.activeDisables.push_back(DISABLE_FOR_MATCH_REFRESH_RATE_ON); + }) + .Options(IntSliderOptions().Tooltip(tooltip).Min(20).Max(maxFps).DefaultValue(20).Format(fpsFormat)); + AddWidget(path, "Match Refresh Rate", WIDGET_CVAR_CHECKBOX) + .CVar(CVAR_SETTING("MatchRefreshRate")) + .RaceDisable(false) + .Options(CheckboxOptions().Tooltip("Matches interpolation value to the refresh rate of your display.")); + AddWidget(path, "Renderer API (Needs reload)", WIDGET_VIDEO_BACKEND).RaceDisable(false); + AddWidget(path, "Enable Vsync", WIDGET_CVAR_CHECKBOX) + .CVar(CVAR_VSYNC_ENABLED) + .RaceDisable(false) + .PreFunc([](WidgetInfo& info) { info.isHidden = mSohMenu->disabledMap.at(DISABLE_FOR_NO_VSYNC).active; }) + .Options(CheckboxOptions() + .Tooltip("Removes tearing, but clamps your max FPS to your displays refresh rate.") + .DefaultValue(true)); + AddWidget(path, "Windowed Fullscreen", WIDGET_CVAR_CHECKBOX) + .CVar(CVAR_SDL_WINDOWED_FULLSCREEN) + .RaceDisable(false) + .PreFunc([](WidgetInfo& info) { + info.isHidden = mSohMenu->disabledMap.at(DISABLE_FOR_NO_WINDOWED_FULLSCREEN).active; + }) + .Options(CheckboxOptions().Tooltip("Enables Windowed Fullscreen Mode.")); + AddWidget(path, "Allow multi-windows", WIDGET_CVAR_CHECKBOX) + .CVar(CVAR_ENABLE_MULTI_VIEWPORTS) + .RaceDisable(false) + .PreFunc( + [](WidgetInfo& info) { info.isHidden = mSohMenu->disabledMap.at(DISABLE_FOR_NO_MULTI_VIEWPORT).active; }) + .Options(CheckboxOptions() + .Tooltip("Allows multiple windows to be opened at once. Requires a reload to take effect.") + .DefaultValue(true)); + AddWidget(path, "Texture Filter (Needs reload)", WIDGET_CVAR_COMBOBOX) + .CVar(CVAR_TEXTURE_FILTER) + .RaceDisable(false) + .Options(ComboboxOptions().Tooltip("Sets the applied Texture Filtering.").ComboMap(textureFilteringMap)); + + path.column = SECTION_COLUMN_2; + AddWidget(path, "Advanced Graphics Options", WIDGET_SEPARATOR_TEXT); + + // Controls + path.sidebarName = "Controls"; + path.column = SECTION_COLUMN_1; + AddSidebarEntry("Settings", "Controls", 2); + AddWidget(path, "Clear Devices", WIDGET_BUTTON) + .Callback([](WidgetInfo& info) { + SohGui::mModalWindow->RegisterPopup( + "Clear Config", + "This will completely erase the controls config, including registered devices.\nContinue?", "Clear", + "Cancel", + []() { + Ship::Context::GetInstance()->GetConsoleVariables()->ClearBlock(CVAR_PREFIX_SETTING ".Controllers"); + uint8_t bits = 0; + Ship::Context::GetInstance()->GetControlDeck()->Init(&bits); + }, + nullptr); + }) + .Options(ButtonOptions().Size(Sizes::Inline)); + AddWidget(path, "Controller Bindings", WIDGET_SEPARATOR_TEXT); + AddWidget(path, "Popout Bindings Window", WIDGET_WINDOW_BUTTON) + .CVar(CVAR_WINDOW("ControllerConfiguration")) + .RaceDisable(false) + .WindowName("Configure Controller") + .HideInSearch(true) + .Options(WindowButtonOptions().Tooltip("Enables the separate Bindings Window.")); + + // Input Viewer + path.sidebarName = "Input Viewer"; + AddSidebarEntry("Settings", path.sidebarName, 3); + AddWidget(path, "Input Viewer", WIDGET_SEPARATOR_TEXT); + AddWidget(path, "Toggle Input Viewer", WIDGET_WINDOW_BUTTON) + .CVar(CVAR_WINDOW("InputViewer")) + .RaceDisable(false) + .WindowName("Input Viewer") + .HideInSearch(true) + .Options(WindowButtonOptions().Tooltip("Toggles the Input Viewer.").EmbedWindow(false)); + + AddWidget(path, "Input Viewer Settings", WIDGET_SEPARATOR_TEXT); + AddWidget(path, "Popout Input Viewer Settings", WIDGET_WINDOW_BUTTON) + .CVar(CVAR_WINDOW("InputViewerSettings")) + .RaceDisable(false) + .WindowName("Input Viewer Settings") + .HideInSearch(true) + .Options(WindowButtonOptions().Tooltip("Enables the separate Input Viewer Settings Window.")); + + // Notifications + path.sidebarName = "Notifications"; + path.column = SECTION_COLUMN_1; + AddSidebarEntry("Settings", path.sidebarName, 3); + AddWidget(path, "Position", WIDGET_CVAR_COMBOBOX) + .CVar(CVAR_SETTING("Notifications.Position")) + .RaceDisable(false) + .Options(ComboboxOptions() + .Tooltip("Which corner of the screen notifications appear in.") + .ComboMap(notificationPosition) + .DefaultIndex(3)); + AddWidget(path, "Duration (seconds):", WIDGET_CVAR_SLIDER_FLOAT) + .CVar(CVAR_SETTING("Notifications.Duration")) + .RaceDisable(false) + .Options(FloatSliderOptions() + .Tooltip("How long notifications are displayed for.") + .Format("%.1f") + .Step(0.1f) + .Min(3.0f) + .Max(30.0f) + .DefaultValue(10.0f)); + AddWidget(path, "Background Opacity", WIDGET_CVAR_SLIDER_FLOAT) + .CVar(CVAR_SETTING("Notifications.BgOpacity")) + .RaceDisable(false) + .Options(FloatSliderOptions() + .Tooltip("How opaque the background of notifications is.") + .DefaultValue(0.5f) + .IsPercentage()); + AddWidget(path, "Size:", WIDGET_CVAR_SLIDER_FLOAT) + .CVar(CVAR_SETTING("Notifications.Size")) + .RaceDisable(false) + .Options(FloatSliderOptions() + .Tooltip("How large notifications are.") + .Format("%.1f") + .Step(0.1f) + .Min(1.0f) + .Max(5.0f) + .DefaultValue(1.8f)); + AddWidget(path, "Test Notification", WIDGET_BUTTON) + .RaceDisable(false) + .Callback([](WidgetInfo& info) { + Notification::Emit({ + .itemIcon = "__OTR__textures/icon_item_24_static/gQuestIconGoldSkulltulaTex", + .prefix = "This", + .message = "is a", + .suffix = "test.", + }); + }) + .Options(ButtonOptions().Tooltip("Displays a test notification.")); + AddWidget(path, "Mute Notification Sound", WIDGET_CVAR_CHECKBOX) + .CVar(CVAR_SETTING("Notifications.Mute")) + .RaceDisable(false) + .Options(CheckboxOptions().Tooltip("Prevent notifications from playing a sound.")); + + // Mod Menu + path.sidebarName = "Mod Menu"; + AddSidebarEntry("Settings", path.sidebarName, 1); + AddWidget(path, "Popout Mod Menu Window", WIDGET_WINDOW_BUTTON) + .CVar(CVAR_WINDOW("ModMenu")) + .WindowName("Mod Menu") + .HideInSearch(true) + .Options(WindowButtonOptions().Tooltip("Enables the separate Mod Menu Window.")); +} + +} // namespace SohGui