diff --git a/cmake/windows_resources.rc.in b/cmake/windows_resources.rc.in
index 29a3853c..86375334 100644
--- a/cmake/windows_resources.rc.in
+++ b/cmake/windows_resources.rc.in
@@ -21,7 +21,7 @@ BEGIN
BEGIN
BLOCK "040904b0"
BEGIN
- VALUE "CompanyName", "BongoCat\0"
+ VALUE "CompanyName", "vladelaina\0"
VALUE "FileDescription", "@PROJECT_DESCRIPTION@\0"
VALUE "FileVersion", "@PROJECT_VERSION@\0"
VALUE "InternalName", "BongoCat\0"
diff --git a/docs/README.de-DE.md b/docs/README.de-DE.md
new file mode 100644
index 00000000..61bb1136
--- /dev/null
+++ b/docs/README.de-DE.md
@@ -0,0 +1,252 @@
+
+
+💘 C/C++ × SDL3 × OpenGL, zusammenrühren, drauflos trommeln! Bong~ Bongo Cat!!!
+
+ Sprache wählen ❯ English • 简体中文 • 繁體中文 • Français • Deutsch • 日本語 • 한국어 • Português • Русский • Español • Bahasa Indonesia
+
+
+
+
+
+
+
+
+
+
+
+> [!TIP]
+> Das in der Demo verwendete Modell stammt von [宇痕冫](https://space.bilibili.com/348616056).
+>
+> 🎁 Auf der Suche nach **kostenlosen** Modellen? Wir arbeiten mit talentierten Modellerstellern zusammen, um dir eine große Auswahl an kostenlosen Modellen zu bieten – und erkunden daneben ständig weitere unterhaltsame Desktop-Erlebnisse! Besuche unsere offizielle Website: [bongocat.pet](https://bongocat.pet/models)
+
+
+
+
+
+
+
+
+
+## 📥 Download
+
+
+
+- GitHub Releases
+
+ Lade die neueste Version von den [GitHub Releases](https://github.com/vladelaina/BongoCat/releases/latest) herunter.
+
+## 🛠️ Aus dem Quellcode erstellen
+
+BongoCat verwendet CMake und benötigt einen C11-Compiler, einen C++17-Compiler, CMake 3.24 oder neuer sowie die OpenGL-Entwicklungsdateien für den Desktop. Standardmäßig werden SDL3, yyjson, stb, miniaudio und Nuklear während der Konfigurationsphase automatisch heruntergeladen, daher ist für die erste Konfiguration eine Internetverbindung erforderlich.
+
+Führe die folgenden Befehle im Projektstammverzeichnis aus (dem Verzeichnis, das `CMakeLists.txt` enthält).
+
+### 📋 Plattform-Voraussetzungen
+
+- **Windows:** Visual Studio 2022 (mit der Workload „Desktopentwicklung mit C++“) und CMake. Verwende den MSVC-Generator; MinGW kann das Diagnose-Backend bauen, unterstützt jedoch nicht das Cubism SDK.
+- **macOS:** Xcode Command Line Tools, CMake und Ninja. Weicht die Zielarchitektur von der Standardarchitektur des Hosts ab, gib sie über `CMAKE_OSX_ARCHITECTURES` an.
+- **Linux (Debian/Ubuntu):** GCC oder Clang, Ninja sowie die OpenGL/X11-Header:
+
+ ```bash
+ sudo apt-get update
+ sudo apt-get install -y build-essential cmake ninja-build \
+ libgl1-mesa-dev libx11-dev libxi-dev libxfixes-dev libcurl4-openssl-dev
+ ```
+
+### 🔧 Konfiguration und Build
+
+Verwende unter Linux und macOS einen Einzelkonfigurations-Generator wie Ninja:
+
+```bash
+cmake -S . -B build -G Ninja \
+ -DCMAKE_BUILD_TYPE=Release \
+ -DBONGO_CAT_FETCH_DEPS=ON
+cmake --build build --parallel
+```
+
+Führe die Befehle unter Windows von der Visual Studio 2022 Developer Command Prompt aus (oder einer anderen Shell, in der MSVC verfügbar ist):
+
+```powershell
+cmake -S . -B build -G "Visual Studio 17 2022" -A x64 `
+ -DBONGO_CAT_FETCH_DEPS=ON
+cmake --build build --config Release --parallel
+```
+
+Die ausführbare Datei befindet sich unter `build/BongoCat` auf Linux, unter `build/BongoCat.app/Contents/MacOS/BongoCat` auf macOS und unter `build/Release/BongoCat.exe` für Visual-Studio-Builds auf Windows.
+
+### 🧪 Tests
+
+Die CTest-Ziele sind standardmäßig aktiviert. Führe nach dem Build Folgendes aus:
+
+```bash
+ctest --test-dir build --output-on-failure
+```
+
+Gib bei einem Mehrkonfigurations-Generator wie Visual Studio die Build-Konfiguration explizit an:
+
+```powershell
+ctest --test-dir build -C Release --output-on-failure
+```
+
+### 🎭 Live2D / Cubism SDK (optional)
+
+Wird kein Cubism SDK gefunden, gibt CMake eine Warnung aus und baut das Diagnose-Backend. Dieses Backend dient dem Start und der Plattformdiagnose; es bietet kein Rendering von Live2D-Modellen. Um die vollständige Laufzeit zu bauen, installiere eine kompatible Version des Cubism SDK for Native, platziere sie unter `vendor/CubismSdkForNative` oder gib den Pfad explizit an:
+
+```bash
+cmake -S . -B build -G Ninja \
+ -DCMAKE_BUILD_TYPE=Release \
+ -DBONGO_CAT_CUBISM_SDK=/path/to/CubismSdkForNative \
+ -DBONGO_CAT_REQUIRE_CUBISM=ON
+```
+
+Das SDK muss die Core-Bibliothek, die Framework-Quellen sowie das OpenGL-GLEW-Drittanbieterverzeichnis in der von `cmake/Cubism.cmake` erwarteten Struktur enthalten. Cubism-Builds unter Windows erfordern Visual Studio 2022. Mit `BONGO_CAT_REQUIRE_CUBISM=ON` schlägt die Konfiguration fehl, wenn kein SDK verfügbar ist, anstatt stillschweigend das Diagnose-Backend zu wählen.
+
+### ⚙️ CMake-Optionen
+
+| Option | Standard | Beschreibung |
+| --- | --- | --- |
+| `BONGO_CAT_FETCH_DEPS` | `ON` | Lädt Drittanbieter-Abhängigkeiten in festgelegten Versionen über CMake `FetchContent` herunter. Setze es nur auf `OFF`, wenn SDL3, yyjson, stb, miniaudio und Nuklear bereits für CMake verfügbar sind. |
+| `BONGO_CAT_CUBISM_SDK` | `vendor/CubismSdkForNative` | Pfad zum Cubism SDK for Native. |
+| `BONGO_CAT_REQUIRE_CUBISM` | `OFF` | Lässt die Konfiguration fehlschlagen, wenn kein SDK verfügbar ist. |
+| `BONGO_CAT_WARNINGS_AS_ERRORS` | `OFF` | Behandelt Warnungen des nativen Compilers als Fehler. |
+
+Setze für Offline-Builds `BONGO_CAT_FETCH_DEPS=OFF` und stelle die CMake-Paketkonfigurationen für SDL3 (einschließlich `SDL3-static`) und yyjson bereit; falls stb, Nuklear und miniaudio nicht automatisch gefunden werden können, gib zusätzlich deren Include-Verzeichnisse an:
+
+```bash
+cmake -S . -B build -G Ninja \
+ -DCMAKE_BUILD_TYPE=Release \
+ -DBONGO_CAT_FETCH_DEPS=OFF \
+ -DBONGO_CAT_STB_INCLUDE_DIR=/path/to/stb \
+ -DBONGO_CAT_NUKLEAR_INCLUDE_DIR=/path/to/nuklear \
+ -DBONGO_CAT_MINIAUDIO_INCLUDE_DIR=/path/to/miniaudio
+```
+
+## 📌 Projektstatus
+
+
+
+## 📜 Lizenz
+
+Der Quellcode und die native Laufzeit von BongoCat stehen unter der [AGPL-3.0-only](../LICENSE)-Lizenz.
+
+Das standardmäßig eingebaute Modell (`standard`) bleibt unter der MIT-Lizenz. Die in `resources/assets/models/standard`, `keyboard` und `gamepad` gebündelten Modellressourcen fallen unter die separate [MIT-Lizenz-Deklaration](../LICENSE-MIT). Diese MIT-Lizenz gilt ausschließlich für die Modellressourcen und die zugehörigen Grafiken; sie ändert nichts an der Lizenz des BongoCat-Quellcodes oder der nativen Laufzeit.
+
+## 🧭 Technische Architektur
+
+Die aktuelle native Version basiert auf C/C++, SDL3 und OpenGL. Das folgende Diagramm zeigt den Datenfluss zur Laufzeit; Details zu Build und Packaging findest du in den CMake-Dateien.
+
+### 🔄 Runtime-Eigentum und Frame-Planung
+
+Jeder Prozess besitzt eine `BongoCatApp`-Instanz und eine Ereignis-/Render-Schleife auf dem Hauptthread. Die Plattform-Listener enden an der Eingabegrenze:
+
+```text
+Plattform-Listener (Tastatur/Zeiger)
+ |
+ v
+ C11-Eingabezustand (atomare Flankenwarteschlange + zusammengeführte Zeigerposition)
+ |
+ v
+ Anwendung auf dem Hauptthread <----- SDL3-Ereignisse
+ |
+ v
+ Modellparameter, Overlays und UI-Zustand
+ |
+ v
+ Modell-Update -> OpenGL-Komposition -> Plattform-Präsentation
+```
+
+Der Windows-Raw-Input-Empfänger, der macOS-Quartz-Ereignis-Tap und die Linux-XInput2-Listener laufen außerhalb der Hauptschleife. Tastatur- und Maustastenereignisse gelangen in eine begrenzte atomare Warteschlange; Bewegungen werden separat zusammengefasst und SDL-Ereignisse wecken den Hauptthread. Windows empfängt Hintergrundeingaben über ein Nachrichtenfenster mit `RIDEV_INPUTSINK | RIDEV_DEVNOTIFY`, wobei normale Fensternachrichten erhalten bleiben. Wenn eine andere Anwendung den Cursor ausblendet oder sperrt, nutzt das Modell die Gerätebewegungen; SDL liefert die Cursorposition auf dem Desktop. Beim Entfernen eines Geräts oder Wechsel des Eingabedesktops werden gehaltene Zustände bereinigt. Windows verwendet keine Eingabe-Hooks oder DirectInput mehr. SDL3-Fenster-, Präferenz- und Gamepad-Ereignisse werden auf dem Hauptthread verarbeitet. Kein Plattform-Listener ruft direkt Live2D-, Overlay- oder UI-Code auf.
+
+`bongo_cat_app_run` ist für Update-, Shutdown- und Sekundärprozessparameter verantwortlich, erzwingt den Besitz einer einzelnen Instanz des Hauptprozesses, weist den Anwendungszustand zu, führt die Initialisierung aus, betritt `bongo_cat_app_loop` und aktualisiert anschließend in definierter Reihenfolge den Zustand und gibt Ressourcen frei. Die Initialisierung lädt Konfiguration und Speicherpfade, findet Ressourcen, erstellt das SDL/OpenGL-Pet-Fenster, initialisiert die Plattform-Backends, erstellt Live2D-/Overlay-/Audio-Dienste, scannt eingebaute/installierte/nah gelegene Modellquellen und lädt verfügbare Modelle. `BongoCatApp` hält Einstellungen, Sitzungszustand, Modell- und Verhaltenskataloge, Plattform-Handles sowie Laufzeit-Dienst-Handles.
+
+Installierte Modellpakete verwenden Mver als kanonisches Format. Der Importprozess analysiert die ausgewählte Datei oder das Verzeichnis, findet und validiert Kandidaten, erzeugt einen Paketidentitäts-Fingerabdruck, konvertiert Tauri-Quellen zu Mver, wendet Bild-Patches an und übermittelt das normalisierte Paket an `models_root`. Anschließend werden Laufzeit-Adapter erzeugt und der Katalog aktualisiert. Nahe Quellen werden nur entdeckt, ohne deren Quellverzeichnis zu installieren; ihre Adapter und Prüfergebnisse werden unter `cache_root` außerhalb von `models_root` zwischengespeichert.
+
+Jede Iteration der Hauptschleife wartet auf SDL-/native Aufweckereignisse oder auf die früheste Fälligkeit von Frame-, UI-, Animations- und Zeiger-Treffertests (maximal 250 ms). Die Schleife verteilt gepufferte SDL-Ereignisse, leert die atomare Eingabewarteschlange und führt Freigabe-Wiederherstellung durch, aktualisiert Fenster- und Modell-Aktualisierungsstatus und wendet anschließend Eingabeparameter an. Bei aktiviertem Cubism folgt die Modellfrist `settings.model.max_fps` (Standard 60 FPS); Diagnose-Builds verwenden ein Fallback-Intervall von 100 ms. Die verstrichene Modellzeit wird auf maximal 250 ms begrenzt und in bis zu acht Teilschritte aufgeteilt, die jeweils höchstens 1/30 Sekunde dauern.
+
+Der reguläre Pet-Pfad rendert nur, wenn das Fenster sichtbar, nicht minimiert und als schmutzig markiert ist. Pro Frame wird zunächst der Hintergrund gelöscht, das Modell gezeichnet, dann werden Zeiger-, Tasten- und Effekt-Overlays komponiert und schließlich der Plattform-Präsentator aufgerufen. Vorschauvorgänge können sofortiges Rendern anfordern, Screenshot-Rendering kann die Präsentation überspringen. macOS und Linux tauschen das SDL-OpenGL-Fenster direkt; Windows tauscht direkt, wenn keine geschichtete Darstellung aktiviert ist, andernfalls wird der Framebuffer ausgelesen und `UpdateLayeredWindow` aufgerufen. Die Präferenz-UI besitzt ein eigenes SDL/OpenGL-Fenster und wird separat gerendert und präsentiert.
+
+Die C-Laufzeit ruft die in `include/bongo_cat/model.h` deklarierte ABI auf. Die Live2D-Brücke und die Cubism-Implementierung befinden sich in `src/live2d` und verwenden C++17 nur, wenn das Cubism SDK aktiviert ist; der restliche native Laufzeitcode verwendet C11. Cubism-Typen bleiben hinter undurchsichtigen C-Handles; wenn das SDK nicht verfügbar ist, stellt `src/live2d/live2d_stub.c` das Diagnose-Backend bereit.
+
+```mermaid
+flowchart TB
+ Input(["Tastatur / Maus / Gamepad"])
+ BuiltIn(["Eingebaute Modellressourcen"])
+ Sources(["Externe Modellquellen
Mver, Tauri, .model3.json, Bild-Patches"])
+ Desktop(["Pet-Fenster und Präferenzfenster"])
+ subgraph Runtime["BongoCat Native Runtime"]
+ direction TB
+ Entry["src/main.c
bongo_cat_app_run"]
+ Startup["Start und Initialisierung
Konfiguration, Speicher, Fenster, Plattform"]
+ Loop["SDL3-Hauptschleife
Warten, Verteilen, Aktualisieren, Rendern"]
+ Shutdown["Herunterfahren
Zustand aktualisieren, Dienste stoppen, Ressourcen freigeben"]
+ InputQueue[("Atomarer Eingabezustand
Flankenwarteschlange und zusammengeführte Zeigerposition")]
+ InputDispatch["Eingabeverteilung
Tastenkürzel, Zeigerzuordnung, Modellparameter"]
+ State[("BongoCatApp-Zustand
Einstellungen, Sitzung, Katalog, Laufzeit-Handles")]
+ Import["Modellerkennung und -import
Validierung, Normalisierung zu Mver, Installation/Cache"]
+ Catalog[("Modell- und Verhaltenskatalog")]
+ Live2D["Live2D C ABI
Cubism SDK oder Diagnose-Stub"]
+ Overlay["Overlays und Audio"]
+ Preferences["Präferenzen und Desktop-Shell
Nuklear-UI, Tray, Fensteraktionen"]
+ Compose["OpenGL-Frame-Komposition"]
+ Present["Plattform-Präsentation"]
+ Entry --> Startup --> Loop
+ Loop --> Shutdown
+ Loop --> InputDispatch --> State
+ Loop <--> State
+ State --> Live2D
+ State --> Overlay
+ State <--> Preferences
+ Loop --> Preferences
+ Catalog --> State
+ State --> Compose
+ Live2D --> Compose
+ Overlay --> Compose
+ Compose --> Present
+ Loop --> Compose
+ end
+ subgraph Platform["Plattform-Backends"]
+ direction LR
+ Global["Globale Tastatur/Zeiger-Erfassung
Windows, macOS, Linux"]
+ SDL["SDL3-Ereignisse
Fenster- und Gamepad-Ereignisse"]
+ end
+ Input --> Global --> InputQueue --> InputDispatch
+ Input --> SDL --> Loop
+ BuiltIn --> Catalog
+ Sources --> Import --> Catalog
+ Present --> Desktop
+ Preferences --> Desktop
+```
+
+## ❓ Häufig gestellte Fragen
+
+### 🔒 Zeichnet BongoCat meine Tastatur- oder Mauseingaben auf?
+
+Nein. BongoCat verarbeitet Tastatur- und Mauseingaben lokal, um Animationen und Tastenkürzel anzutreiben. Es zeichnet weder Tastenanschläge, Mausaktionen noch andere Interaktionsdaten auf oder lädt sie hoch. Auch die Konfiguration wird nur lokal gespeichert; die Anwendung enthält keine Werbung, Analyse-Tools oder Code zur Nutzerverfolgung. Bei Update-Checks werden nur öffentliche Versionsmetadaten abgefragt; es werden keine Eingabe-, Konfigurations- oder Nutzungsdaten gesendet.
+
+### 🖼️ Warum OpenGL und nicht Vulkan?
+
+Nicht weil Vulkan schlecht ist, sondern weil BongoCat diese Komplexität nicht benötigt. Die Anwendung rendert hauptsächlich ein Live2D-Modell, einige UI-Ebenen und ein transparentes Desktop-Fenster – das bewältigt OpenGL mühelos, und es harmoniert auf natürliche Weise mit SDL3 und dem OpenGL-Renderer von Cubism. Eine Migration zu Vulkan würde mehr Render- und Synchronisationscode auf drei Desktop-Plattformen erfordern, ohne dem Nutzer einen spürbaren Vorteil zu bringen. Für die aktuellen Arbeitslasten von BongoCat macht OpenGL den Renderer schlanker, besser debugbar und wartbarer und liefert weiterhin die benötigte Leistung.
+
+## 🙏 Besonderer Dank
+> [!TIP]
+> Jeder Schritt von BongoCat wird vom Open-Source-Gedanken getragen. Wir danken allen Community-Beitragenden aufrichtig für ihren selbstlosen Beitrag (unten nach Beitragsdatum sortiert aufgeführt). Eure Unterstützung macht die Begleitung auf dem Desktop freier und echten.❤️🔥
+
+
+
+
+
+
+
+[linux.do](https://linux.do/t/topic/2845597)
+---
+
+
+Copyright © 2026 - **BongoCat**
+By vladelaina
+Made with ❤️ & ⌨️
+
diff --git a/docs/README.es-ES.md b/docs/README.es-ES.md
new file mode 100644
index 00000000..1c58b946
--- /dev/null
+++ b/docs/README.es-ES.md
@@ -0,0 +1,252 @@
+
+
+💘 C/C++ × SDL3 × OpenGL, mézclalo todo, ¡a tocar! ¡Bong~ Bongo Cat!!!
+
+ Elige el idioma ❯ English • 简体中文 • 繁體中文 • Français • Deutsch • 日本語 • 한국어 • Português • Русский • Español • Bahasa Indonesia
+
+
+
+
+
+
+
+
+
+
+
+> [!TIP]
+> El modelo usado en la demo proviene de [宇痕冫](https://space.bilibili.com/348616056).
+>
+> 🎁 ¿Buscas modelos **gratuitos**? Trabajamos con creadores de modelos talentosos para ofrecerte una gran variedad de modelos gratuitos, ¡mientras seguimos explorando experiencias de escritorio aún más divertidas! Visita nuestro sitio web oficial: [bongocat.pet](https://bongocat.pet/models)
+
+
+
+
+
+
+
+
+
+## 📥 Descarga
+
+
+
+- GitHub Releases
+
+ Descarga la última versión desde [GitHub Releases](https://github.com/vladelaina/BongoCat/releases/latest).
+
+## 🛠️ Compilar desde el código fuente
+
+BongoCat usa CMake y requiere un compilador C11, un compilador C++17, CMake 3.24 o superior y los archivos de desarrollo de OpenGL para escritorio. Por defecto, SDL3, yyjson, stb, miniaudio y Nuklear se descargan automáticamente durante la configuración, por lo que la primera configuración requiere conexión a internet.
+
+Ejecuta los siguientes comandos en la raíz del proyecto (el directorio que contiene `CMakeLists.txt`).
+
+### 📋 Requisitos por plataforma
+
+- **Windows:** Visual Studio 2022 (con la carga de trabajo «Desarrollo para escritorio con C++») y CMake. Usa el generador MSVC; MinGW puede compilar el backend de diagnóstico, pero no es compatible con el SDK de Cubism.
+- **macOS:** Xcode Command Line Tools, CMake y Ninja. Si la arquitectura de destino difiere de la predeterminada del host, especifícala mediante `CMAKE_OSX_ARCHITECTURES`.
+- **Linux (Debian/Ubuntu):** GCC o Clang, Ninja y los encabezados de OpenGL/X11:
+
+ ```bash
+ sudo apt-get update
+ sudo apt-get install -y build-essential cmake ninja-build \
+ libgl1-mesa-dev libx11-dev libxi-dev libxfixes-dev libcurl4-openssl-dev
+ ```
+
+### 🔧 Configuración y compilación
+
+En Linux y macOS, usa un generador de configuración única como Ninja:
+
+```bash
+cmake -S . -B build -G Ninja \
+ -DCMAKE_BUILD_TYPE=Release \
+ -DBONGO_CAT_FETCH_DEPS=ON
+cmake --build build --parallel
+```
+
+En Windows, ejecuta los comandos desde el Símbolo del sistema para desarrolladores de Visual Studio 2022 (o cualquier shell donde MSVC esté disponible):
+
+```powershell
+cmake -S . -B build -G "Visual Studio 17 2022" -A x64 `
+ -DBONGO_CAT_FETCH_DEPS=ON
+cmake --build build --config Release --parallel
+```
+
+El ejecutable se encuentra en `build/BongoCat` en Linux, en `build/BongoCat.app/Contents/MacOS/BongoCat` en macOS y en `build/Release/BongoCat.exe` para las compilaciones de Visual Studio en Windows.
+
+### 🧪 Pruebas
+
+Los objetivos de CTest están habilitados por defecto. Ejecuta lo siguiente después de compilar:
+
+```bash
+ctest --test-dir build --output-on-failure
+```
+
+Para un generador de múltiples configuraciones como Visual Studio, especifica explícitamente la configuración de compilación:
+
+```powershell
+ctest --test-dir build -C Release --output-on-failure
+```
+
+### 🎭 Live2D / SDK de Cubism (opcional)
+
+Si no se encuentra el SDK de Cubism, CMake muestra una advertencia y compila el backend de diagnóstico. Este backend está pensado para el arranque y el diagnóstico de plataforma; no proporciona renderizado de modelos Live2D. Para compilar el runtime completo, instala una versión compatible del Cubism SDK for Native, colócalo en `vendor/CubismSdkForNative` o indica explícitamente su ruta:
+
+```bash
+cmake -S . -B build -G Ninja \
+ -DCMAKE_BUILD_TYPE=Release \
+ -DBONGO_CAT_CUBISM_SDK=/path/to/CubismSdkForNative \
+ -DBONGO_CAT_REQUIRE_CUBISM=ON
+```
+
+El SDK debe incluir la biblioteca Core, el código fuente de Framework y el directorio de terceros de OpenGL GLEW en la estructura esperada por `cmake/Cubism.cmake`. Las compilaciones de Cubism en Windows requieren Visual Studio 2022. Con `BONGO_CAT_REQUIRE_CUBISM=ON`, la configuración falla si el SDK no está disponible, en lugar de seleccionar silenciosamente el backend de diagnóstico.
+
+### ⚙️ Opciones de CMake
+
+| Opción | Valor por defecto | Descripción |
+| --- | --- | --- |
+| `BONGO_CAT_FETCH_DEPS` | `ON` | Descarga dependencias de terceros en versiones fijadas mediante `FetchContent` de CMake. Establécelo en `OFF` solo si SDL3, yyjson, stb, miniaudio y Nuklear ya están disponibles para CMake. |
+| `BONGO_CAT_CUBISM_SDK` | `vendor/CubismSdkForNative` | Ruta del Cubism SDK for Native. |
+| `BONGO_CAT_REQUIRE_CUBISM` | `OFF` | Hace fallar la configuración cuando no hay SDK disponible. |
+| `BONGO_CAT_WARNINGS_AS_ERRORS` | `OFF` | Trata las advertencias del compilador nativo como errores. |
+
+Para una compilación sin conexión, establece `BONGO_CAT_FETCH_DEPS=OFF` y proporciona las configuraciones de paquetes de CMake para SDL3 (incluido `SDL3-static`) y yyjson; si stb, Nuklear y miniaudio no se pueden detectar automáticamente, indica también sus directorios de inclusión:
+
+```bash
+cmake -S . -B build -G Ninja \
+ -DCMAKE_BUILD_TYPE=Release \
+ -DBONGO_CAT_FETCH_DEPS=OFF \
+ -DBONGO_CAT_STB_INCLUDE_DIR=/path/to/stb \
+ -DBONGO_CAT_NUKLEAR_INCLUDE_DIR=/path/to/nuklear \
+ -DBONGO_CAT_MINIAUDIO_INCLUDE_DIR=/path/to/miniaudio
+```
+
+## 📌 Estado del proyecto
+
+
+
+## 📜 Licencia
+
+El código fuente y el runtime nativo de BongoCat están bajo la licencia [AGPL-3.0-only](../LICENSE).
+
+El modo de modelo integrado por defecto (`standard`) permanece bajo la licencia MIT. Los recursos de modelo incluidos en `resources/assets/models/standard`, `keyboard` y `gamepad` están cubiertos por la [declaración de licencia MIT](../LICENSE-MIT) independiente. Esta licencia MIT se aplica únicamente a los recursos de los modelos y sus ilustraciones asociadas; no cambia la licencia del código fuente ni del runtime nativo de BongoCat.
+
+## 🧭 Arquitectura técnica
+
+La versión nativa actual se basa en C/C++, SDL3 y OpenGL. El siguiente diagrama destaca el flujo de datos en tiempo de ejecución; consulta los archivos de CMake para los detalles de compilación y empaquetado.
+
+### 🔄 Propiedad del runtime y planificación de fotogramas
+
+Cada proceso posee una instancia de `BongoCatApp` y un bucle de eventos/renderizado en el hilo principal. Los listeners de plataforma se detienen en el límite de entrada:
+
+```text
+Listeners de plataforma (teclado/ puntero)
+ |
+ v
+ Estado de entrada C11 (cola atómica de flancos + posición de puntero fusionada)
+ |
+ v
+ aplicación en el hilo principal <----- eventos SDL3
+ |
+ v
+ parámetros del modelo, superposiciones y estado de la interfaz
+ |
+ v
+ actualización del modelo -> composición OpenGL -> presentación de la plataforma
+```
+
+El receptor Raw Input de Windows, el tap de eventos Quartz de macOS y los listeners XInput2 de Linux se ejecutan fuera del bucle principal. Los eventos de teclas y botones entran en una cola atómica acotada; los movimientos se agrupan por separado y los eventos SDL despiertan el hilo principal. Windows recibe entradas en segundo plano mediante una ventana de mensajes con `RIDEV_INPUTSINK | RIDEV_DEVNOTIFY`, conservando los mensajes normales. Cuando otra aplicación oculta o bloquea el cursor, el modelo usa los movimientos del dispositivo; SDL proporciona la posición del cursor en el escritorio. Los estados pulsados se limpian al desconectar un dispositivo o cambiar el escritorio de entrada. Windows ya no usa hooks de entrada ni DirectInput. Los eventos de ventana, preferencias y gamepad de SDL3 se procesan en el hilo principal. Ningún listener de plataforma llama directamente al código de Live2D, superposiciones o interfaz.
+
+`bongo_cat_app_run` gestiona los parámetros de actualización, cierre y procesos secundarios, hace cumplir la propiedad de una única instancia del proceso principal, asigna el estado de la aplicación, realiza la inicialización, entra en `bongo_cat_app_loop` y luego actualiza el estado y libera los recursos en el orden definido. La inicialización carga la configuración y las rutas de almacenamiento, localiza los recursos, crea la ventana de mascota SDL/OpenGL, inicializa los backends de plataforma, crea los servicios de Live2D/superposiciones/audio, escanea las fuentes de modelos integrados/instalados/cercanos y carga los modelos disponibles. `BongoCatApp` conserva la configuración, el estado de la sesión, los catálogos de modelos y comportamientos, los identificadores de plataforma y los identificadores de servicios en tiempo de ejecución.
+
+Los paquetes de modelos instalados usan Mver como formato canónico. El proceso de importación analiza el archivo o directorio seleccionado, descubre y valida los candidatos, genera una huella de identidad del paquete, convierte las fuentes Tauri a Mver, aplica parches de imagen y envía el paquete normalizado a `models_root`. Después se generan los adaptadores de tiempo de ejecución y se actualiza el catálogo. Las fuentes cercanas solo se descubren sin instalar su árbol de origen; sus adaptadores y resultados de comprobación se almacenan en caché bajo `cache_root`, fuera de `models_root`.
+
+Cada iteración del bucle principal espera activaciones SDL/nativas o el vencimiento más temprano de los plazos de fotograma, interfaz, animación y acierto de puntero (máximo 250 ms). El bucle distribuye los eventos SDL en cola, vacía la cola de entrada atómica y realiza la recuperación de liberaciones, actualiza el estado de la ventana y de actualización del modelo y luego aplica los parámetros de entrada. Con Cubism habilitado, el plazo del modelo sigue `settings.model.max_fps` (predeterminado 60 FPS); las compilaciones de diagnóstico usan un intervalo de reserva de 100 ms. El tiempo transcurrido del modelo se cuenta como máximo 250 ms y se divide en hasta ocho subpasos, cada uno con un objetivo de no más de 1/30 de segundo.
+
+La ruta normal de la mascota solo se renderiza cuando la ventana es visible, no está minimizada y está marcada como sucia. Cada fotograma primero limpia el fondo, dibuja el modelo y luego compone las superposiciones de puntero, teclas y efectos, y finalmente llama al presentador de la plataforma. Las operaciones de vista previa pueden solicitar renderizado inmediato; el renderizado de capturas de pantalla puede omitir la presentación. macOS y Linux intercambian directamente la ventana SDL OpenGL; Windows la intercambia directamente cuando la presentación en capas no está habilitada; de lo contrario, lee el búfer de fotogramas y llama a `UpdateLayeredWindow`. La interfaz de preferencias tiene su propia ventana SDL/OpenGL y se renderiza y presenta por separado.
+
+El runtime C llama a la ABI declarada en `include/bongo_cat/model.h`. El puente Live2D y la implementación de Cubism están en `src/live2d` y solo usan C++17 cuando el SDK de Cubism está habilitado; el resto del runtime nativo usa C11. Los tipos de Cubism se mantienen detrás de identificadores C opacos; cuando el SDK no está disponible, `src/live2d/live2d_stub.c` proporciona el backend de diagnóstico.
+
+```mermaid
+flowchart TB
+ Input(["Teclado / Ratón / Gamepad"])
+ BuiltIn(["Recursos de modelos integrados"])
+ Sources(["Fuentes de modelos externos
Mver, Tauri, .model3.json, parches de imagen"])
+ Desktop(["Ventana de mascota y ventana de preferencias"])
+ subgraph Runtime["Runtime nativo de BongoCat"]
+ direction TB
+ Entry["src/main.c
bongo_cat_app_run"]
+ Startup["Arranque e inicialización
Configuración, almacenamiento, ventana, plataforma"]
+ Loop["Bucle principal SDL3
Esperar, distribuir, actualizar, renderizar"]
+ Shutdown["Apagado
Actualizar estado, detener servicios, liberar recursos"]
+ InputQueue[("Estado de entrada atómico
Cola de flancos y posición de puntero fusionada")]
+ InputDispatch["Distribución de entrada
Atajos, mapeo de puntero, parámetros del modelo"]
+ State[("Estado de BongoCatApp
Configuración, sesión, catálogo, identificadores de runtime")]
+ Import["Descubrimiento e importación de modelos
Validación, normalización a Mver, instalación/caché"]
+ Catalog[("Catálogo de modelos y comportamientos")]
+ Live2D["Live2D C ABI
SDK de Cubism o stub de diagnóstico"]
+ Overlay["Superposiciones y audio"]
+ Preferences["Preferencias y shell de escritorio
UI de Nuklear, bandeja, acciones de ventana"]
+ Compose["Composición de fotogramas OpenGL"]
+ Present["Presentación de la plataforma"]
+ Entry --> Startup --> Loop
+ Loop --> Shutdown
+ Loop --> InputDispatch --> State
+ Loop <--> State
+ State --> Live2D
+ State --> Overlay
+ State <--> Preferences
+ Loop --> Preferences
+ Catalog --> State
+ State --> Compose
+ Live2D --> Compose
+ Overlay --> Compose
+ Compose --> Present
+ Loop --> Compose
+ end
+ subgraph Platform["Backends de plataforma"]
+ direction LR
+ Global["Captura global de teclado/puntero
Windows, macOS, Linux"]
+ SDL["Eventos SDL3
Eventos de ventana y gamepad"]
+ end
+ Input --> Global --> InputQueue --> InputDispatch
+ Input --> SDL --> Loop
+ BuiltIn --> Catalog
+ Sources --> Import --> Catalog
+ Present --> Desktop
+ Preferences --> Desktop
+```
+
+## ❓ Preguntas frecuentes
+
+### 🔒 ¿BongoCat registra mis entradas de teclado o ratón?
+
+No. BongoCat procesa las entradas de teclado y ratón localmente para impulsar animaciones y atajos. No registra ni sube pulsaciones de teclas, acciones del ratón ni otros datos de interacción. La configuración también se guarda solo localmente; la aplicación no incluye anuncios, herramientas de análisis ni código de seguimiento de usuarios. Al comprobar actualizaciones solo se solicitan los metadatos públicos de versión; no se envían datos de entrada, configuración ni uso.
+
+### 🖼️ ¿Por qué OpenGL y no Vulkan?
+
+No porque Vulkan sea malo, sino porque BongoCat no necesita ese nivel de complejidad. La aplicación renderiza principalmente un modelo Live2D, algunas capas de interfaz y una ventana de escritorio transparente; OpenGL lo cubre con facilidad y se integra de forma natural con SDL3 y el renderizador OpenGL de Cubism. Migrar a Vulkan implicaría mantener más código de renderizado y sincronización en tres plataformas de escritorio, sin un beneficio notable para el usuario. Para la carga de trabajo actual de BongoCat, OpenGL hace que el renderizador sea más ligero, más fácil de depurar y mantener, y sigue ofreciendo el rendimiento necesario.
+
+## 🙏 Agradecimientos especiales
+> [!TIP]
+> Cada paso de BongoCat está impulsado por el espíritu del código abierto. Agradecemos sinceramente a todos los colaboradores de la comunidad por sus contribuciones desinteresadas (listadas a continuación en orden cronológico). Es vuestro apoyo lo que hace que la compañía en el escritorio sea más libre y genuina.❤️🔥
+
+
+
+
+
+
+
+[linux.do](https://linux.do/t/topic/2845597)
+---
+
+
+Derechos de autor © 2026 - **BongoCat**
+By vladelaina
+Made with ❤️ & ⌨️
+
diff --git a/docs/README.fr-FR.md b/docs/README.fr-FR.md
new file mode 100644
index 00000000..24df2fa7
--- /dev/null
+++ b/docs/README.fr-FR.md
@@ -0,0 +1,394 @@
+
+
+
+
+ 💘C/C++ × SDL3 × OpenGL, mélangez le tout, assemblez-le ! Bong~ Bongo Cat !!!
+
+
+English • 简体中文 • 繁體中文 • Français • Deutsch • 日本語 • 한국어 • Português • Русский • Español • Bahasa Indonesia
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+> [!TIP]
+> Le modèle présenté dans cette démonstration provient de [宇痕冫](https://space.bilibili.com/348616056).
+>
+> 🎁 Vous cherchez des modèles **gratuits** ? Nous collaborons avec des créateurs de modèles talentueux pour vous proposer une grande variété de modèles gratuits, tout en explorant continuellement de nouvelles expériences de bureau amusantes ! Rendez-vous sur notre site officiel : [bongocat.pet](https://bongocat.pet/models)
+
+
+
+
+
+
+
+
+
+
+
+
+## 📥 Téléchargement
+
+
+
+
+
+- GitHub Releases
+
+ Téléchargez la dernière version depuis [GitHub Releases](https://github.com/vladelaina/BongoCat/releases/latest).
+
+## 🛠️ Compilation depuis les sources
+
+BongoCat utilise CMake et nécessite un compilateur C11, un compilateur C++17,
+CMake 3.24 ou une version plus récente, ainsi que les fichiers de développement
+OpenGL pour environnement de bureau. SDL3, yyjson, stb, miniaudio et Nuklear
+sont téléchargés par défaut lors de la configuration ; la première configuration
+nécessite donc un accès au réseau.
+
+Exécutez les commandes ci-dessous depuis la racine du projet (le répertoire
+contenant `CMakeLists.txt`).
+
+### 📋 Prérequis par plateforme
+
+- **Windows :** Visual Studio 2022 avec la charge de travail Desktop C++ et CMake.
+ Utilisez le générateur MSVC ; MinGW peut compiler le backend de diagnostic,
+ mais il n'est pas pris en charge par le SDK Cubism.
+- **macOS :** Xcode Command Line Tools, CMake et Ninja. Définissez une architecture
+ avec `CMAKE_OSX_ARCHITECTURES` lorsqu'elle diffère de l'architecture par défaut
+ de la machine hôte.
+- **Linux (Debian/Ubuntu) :** GCC ou Clang, Ninja ainsi que les en-têtes OpenGL/X11 :
+ ```bash
+ sudo apt-get update
+ sudo apt-get install -y build-essential cmake ninja-build \
+ libgl1-mesa-dev libx11-dev libxi-dev libxfixes-dev libcurl4-openssl-dev
+ ```
+
+### 🔧 Configuration et compilation
+
+Sous Linux et macOS, utilisez un générateur à configuration unique tel que Ninja :
+
+```bash
+cmake -S . -B build -G Ninja \
+ -DCMAKE_BUILD_TYPE=Release \
+ -DBONGO_CAT_FETCH_DEPS=ON
+cmake --build build --parallel
+```
+
+Sous Windows, exécutez les commandes depuis un shell de développement
+Visual Studio 2022 (ou un autre shell dans lequel MSVC est disponible) :
+
+```powershell
+cmake -S . -B build -G "Visual Studio 17 2022" -A x64 `
+ -DBONGO_CAT_FETCH_DEPS=ON
+cmake --build build --config Release --parallel
+```
+
+L'exécutable est généré dans `build/BongoCat` sous Linux,
+dans `build/BongoCat.app/Contents/MacOS/BongoCat` sous macOS,
+et dans `build/Release/BongoCat.exe` pour les compilations Visual Studio.
+
+### 🧪 Tests
+
+Les cibles CTest sont activées par défaut. Exécutez-les après la compilation :
+
+```bash
+ctest --test-dir build --output-on-failure
+```
+
+Pour un générateur multi-configuration tel que Visual Studio,
+sélectionnez explicitement la configuration de compilation :
+
+```powershell
+ctest --test-dir build -C Release --output-on-failure
+```
+
+### 🎭 Live2D / SDK Cubism (facultatif)
+
+Si le SDK Cubism n'est pas présent, CMake affiche un avertissement et compile
+le backend de diagnostic. Ce backend est destiné au démarrage et aux diagnostics
+de plateforme ; il ne permet pas le rendu des modèles Live2D. Pour compiler
+le runtime complet, installez une version compatible du Cubism SDK for Native
+et placez-la dans `vendor/CubismSdkForNative`, ou indiquez explicitement son
+emplacement :
+
+```bash
+cmake -S . -B build -G Ninja \
+ -DCMAKE_BUILD_TYPE=Release \
+ -DBONGO_CAT_CUBISM_SDK=/path/to/CubismSdkForNative \
+ -DBONGO_CAT_REQUIRE_CUBISM=ON
+```
+
+Le SDK doit contenir sa bibliothèque Core, les sources du Framework ainsi que
+l'arborescence tierce OpenGL GLEW dans la structure attendue par
+`cmake/Cubism.cmake`. Les compilations Cubism sous Windows nécessitent
+Visual Studio 2022. `BONGO_CAT_REQUIRE_CUBISM=ON` fait échouer la configuration
+au lieu de sélectionner silencieusement le backend de diagnostic.
+
+### ⚙️ Options CMake
+
+| Option | Valeur par défaut | Description |
+| --- | --- | --- |
+| `BONGO_CAT_FETCH_DEPS` | `ON` | Télécharge les dépendances tierces aux versions épinglées via `FetchContent` de CMake. Définissez cette option sur `OFF` uniquement si SDL3, yyjson, stb, miniaudio et Nuklear sont déjà accessibles à CMake. |
+| `BONGO_CAT_CUBISM_SDK` | `vendor/CubismSdkForNative` | Chemin vers le Cubism SDK for Native. |
+| `BONGO_CAT_REQUIRE_CUBISM` | `OFF` | Fait échouer la configuration lorsqu'aucun SDK Cubism utilisable n'est disponible. |
+| `BONGO_CAT_WARNINGS_AS_ERRORS` | `OFF` | Traite les avertissements du compilateur natif comme des erreurs. |
+
+Pour une compilation hors ligne avec `BONGO_CAT_FETCH_DEPS=OFF`, fournissez
+les configurations de paquets CMake pour SDL3 (y compris `SDL3-static`) et yyjson,
+ainsi que les répertoires d'inclusion de stb, Nuklear et miniaudio lorsqu'ils ne
+peuvent pas être détectés automatiquement :
+
+```bash
+cmake -S . -B build -G Ninja \
+ -DCMAKE_BUILD_TYPE=Release \
+ -DBONGO_CAT_FETCH_DEPS=OFF \
+ -DBONGO_CAT_STB_INCLUDE_DIR=/path/to/stb \
+ -DBONGO_CAT_NUKLEAR_INCLUDE_DIR=/path/to/nuklear \
+ -DBONGO_CAT_MINIAUDIO_INCLUDE_DIR=/path/to/miniaudio
+```
+
+## 📌 État du projet
+
+
+## 📜 Licence
+
+Le code source et le runtime natif de BongoCat sont distribués sous licence
+[AGPL-3.0-only](LICENSE).
+
+Le mode de modèle intégré par défaut (`standard`) reste sous licence MIT.
+Les ressources de modèles fournies dans `resources/assets/models/standard`,
+`keyboard` et `gamepad` sont couvertes par la [licence MIT distincte](LICENSE-MIT).
+Cette licence MIT s'applique uniquement aux ressources des modèles et aux
+éléments graphiques qui les accompagnent ; elle ne modifie pas la licence du
+code source ni du runtime natif de BongoCat.
+
+## 🧭 Architecture technique
+
+> La version native actuelle repose sur C/C++, SDL3 et OpenGL. Le diagramme ci-dessous se concentre sur le flux de données à l'exécution ; les détails de compilation et de packaging sont gérés par CMake.
+
+### 🔄 Gestion du runtime et ordonnancement des frames
+
+Chaque processus possède une instance de `BongoCatApp` ainsi qu'une boucle
+principale d'événements et de rendu exécutée sur le thread principal.
+Les écouteurs propres à chaque plateforme s'arrêtent à la frontière du
+système d'entrée :
+
+```text
+Écouteurs de plateforme
+(clavier / pointeur)
+ |
+ v
+ État des entrées C11
+ (file atomique de transitions + position du pointeur coalescée)
+ |
+ v
+ application sur le thread principal <----- événements SDL3
+ |
+ v
+ paramètres du modèle, overlay et état de l'interface
+ |
+ v
+ mise à jour du modèle -> composition OpenGL -> présentation plateforme
+```
+
+Le récepteur Raw Input de Windows, l'event tap Quartz de macOS et l'écouteur
+XInput2 de Linux s'exécutent en dehors de la boucle principale. Les transitions
+des touches et des boutons utilisent une file atomique bornée ; les mouvements
+sont regroupés séparément et des événements SDL réveillent le thread principal.
+Windows utilise une fenêtre de messages avec `RIDEV_INPUTSINK | RIDEV_DEVNOTIFY`
+pour recevoir les entrées en arrière-plan, en conservant les messages ordinaires.
+Le modèle utilise les mouvements du périphérique lorsqu'une autre application
+masque ou verrouille le curseur ; SDL fournit sa position sur le bureau.
+Les états sont nettoyés au retrait d'un périphérique ou au changement de bureau.
+Windows n'installe plus de hooks d'entrée et n'utilise plus DirectInput.
+
+Les événements SDL3 liés aux fenêtres, aux préférences et aux manettes sont
+traités sur le thread principal, où les événements de manette sont normalisés
+avant d'atteindre les paramètres du modèle ou les raccourcis. Aucun écouteur
+de plateforme n'appelle directement le code Live2D, celui de l'overlay ou
+celui de l'interface utilisateur.
+
+`bongo_cat_app_run` gère l'arrêt lié aux mises à jour ainsi que les arguments
+des processus secondaires, impose une instance unique pour le processus
+principal, alloue l'état de l'application, exécute l'initialisation, entre dans
+`bongo_cat_app_loop`, puis enregistre l'état et libère les ressources dans un
+ordre défini.
+
+L'initialisation charge la configuration et les chemins de stockage, localise
+les ressources, crée la fenêtre SDL/OpenGL de l'animal de compagnie, initialise
+le backend de la plateforme, crée les services Live2D, d'overlay et audio,
+analyse les sources de modèles intégrées, installées ou locales, puis charge
+un modèle utilisable. `BongoCatApp` possède les paramètres, l'état de session,
+les catalogues de modèles et de comportements, les handles de plateforme ainsi
+que les handles des services d'exécution.
+
+Les paquets de modèles installés utilisent Mver comme format canonique.
+Le processus d'importation résout le fichier ou le répertoire sélectionné,
+découvre et valide les candidats, calcule l'empreinte d'identité du paquet,
+convertit les sources Tauri vers Mver, applique les correctifs d'image et
+enregistre le paquet normalisé sous `models_root`. Il génère ensuite
+l'adaptateur d'exécution et actualise les catalogues.
+
+Les sources locales voisines sont découvertes sans installer leur arborescence
+source ; leurs adaptateurs et résultats d'inspection sont mis en cache en dehors
+de `models_root`, sous `cache_root`.
+
+À chaque itération de la boucle principale, l'application attend un réveil
+SDL/natif ou la première échéance en attente concernant une frame, l'interface
+utilisateur, une animation ou la détection du pointeur, avec un temps d'attente
+maximal de 250 ms. Elle distribue ensuite les événements SDL en attente, vide
+la file atomique des entrées et traite la récupération des relâchements, met à
+jour l'état de la fenêtre et du rafraîchissement des modèles, puis applique les
+paramètres dérivés des entrées.
+
+Lorsque Cubism est activé, l'échéance de mise à jour du modèle suit
+`settings.model.max_fps` (60 FPS par défaut) ; les compilations de diagnostic
+utilisent un intervalle de secours de 100 ms. Le temps écoulé du modèle est
+plafonné à 250 ms et divisé en un maximum de huit sous-étapes, avec pour
+objectif de ne pas dépasser 1/30 s par sous-étape.
+
+Dans le fonctionnement normal de l'animal de compagnie, le rendu n'est effectué
+que lorsque la fenêtre est visible, non réduite et marquée comme nécessitant
+un rafraîchissement. Une frame efface l'arrière-plan, dessine le modèle et
+compose les overlays du pointeur, des touches et des effets avant d'appeler
+le système de présentation de la plateforme.
+
+Les opérations d'aperçu peuvent demander un rendu immédiat, tandis que les
+rendus destinés à une capture peuvent ignorer la présentation. macOS et Linux
+effectuent directement le swap de la fenêtre OpenGL SDL. Windows effectue
+également directement le swap lorsque la présentation en couches est inactive ;
+dans le cas contraire, la frame est relue pour `UpdateLayeredWindow`.
+
+L'interface des préférences possède une fenêtre SDL/OpenGL distincte, rendue et
+présentée indépendamment de la fenêtre de l'animal de compagnie.
+
+Le runtime C appelle l'ABI déclarée dans `include/bongo_cat/model.h`.
+Le pont Live2D et l'implémentation Cubism se trouvent dans `src/live2d` et
+n'utilisent C++17 que lorsque le SDK Cubism est activé ; le reste du runtime
+natif utilise C11. Les types Cubism restent masqués derrière des handles C
+opaques, tandis que `src/live2d/live2d_stub.c` fournit le backend de diagnostic
+lorsque le SDK n'est pas disponible.
+
+```mermaid
+flowchart TB
+ Input(["Clavier / souris / manette"])
+ BuiltIn(["Ressources des modèles intégrés"])
+ Sources(["Sources de modèles externes
Mver, Tauri, .model3.json, correctifs d'image"])
+ Desktop(["Fenêtre de l'animal et fenêtre des préférences"])
+ subgraph Runtime["Runtime natif BongoCat"]
+ direction TB
+ Entry["src/main.c
bongo_cat_app_run"]
+ Startup["Démarrage et initialisation
configuration, stockage, fenêtre, plateforme"]
+ Loop["Boucle principale SDL3
attente, distribution, mise à jour, rendu"]
+ Shutdown["Arrêt
enregistrement de l'état, arrêt des services, libération des ressources"]
+ InputQueue[("État atomique des entrées
file de transitions et position du pointeur coalescée")]
+ InputDispatch["Distribution des entrées
raccourcis, mappage du pointeur, paramètres du modèle"]
+ State[("État BongoCatApp
paramètres, session, catalogues, handles d'exécution")]
+ Import["Découverte et importation des modèles
validation, normalisation vers Mver, installation/cache"]
+ Catalog[("Catalogues des modèles et comportements")]
+ Live2D["ABI C de Live2D
SDK Cubism ou stub de diagnostic"]
+ Overlay["Overlay et audio"]
+ Preferences["Préférences et shell de bureau
interface Nuklear, zone de notification, actions de fenêtre"]
+ Compose["Composition de la frame OpenGL"]
+ Present["Présentation plateforme"]
+ Entry --> Startup --> Loop
+ Loop --> Shutdown
+ Loop --> InputDispatch --> State
+ Loop <--> State
+ State --> Live2D
+ State --> Overlay
+ State <--> Preferences
+ Loop --> Preferences
+ Catalog --> State
+ State --> Compose
+ Live2D --> Compose
+ Overlay --> Compose
+ Compose --> Present
+ Loop --> Compose
+ end
+ subgraph Platform["Backends de plateforme"]
+ direction LR
+ Global["Capture globale du clavier / pointeur
Windows, macOS, Linux"]
+ SDL["Événements SDL3
événements de fenêtre et de manette"]
+ end
+
+ Input --> Global --> InputQueue --> InputDispatch
+ Input --> SDL --> Loop
+ BuiltIn --> Catalog
+ Sources --> Import --> Catalog
+ Present --> Desktop
+ Preferences --> Desktop
+```
+
+## ❓ FAQ
+
+### 🔒 BongoCat enregistre-t-il les entrées de mon clavier ou de ma souris ?
+
+Non. BongoCat traite localement les entrées du clavier et de la souris afin de
+piloter les animations et les raccourcis. Il n'enregistre ni ne téléverse vos
+frappes au clavier, vos actions de souris ou toute autre donnée d'interaction.
+
+La configuration est également stockée localement, et l'application ne contient
+ni publicité, ni outil d'analyse, ni code de suivi des utilisateurs. Lorsqu'une
+vérification de mise à jour est effectuée, l'application demande uniquement les
+métadonnées publiques des versions ; elle n'envoie aucune donnée d'entrée,
+de configuration ou d'utilisation.
+
+### 🖼️ Pourquoi OpenGL plutôt que Vulkan ?
+
+Nous avons choisi OpenGL non pas parce que Vulkan serait mauvais, mais parce que
+BongoCat n'a tout simplement pas besoin de ce niveau de complexité.
+L'application affiche principalement un modèle Live2D, quelques couches
+d'interface et une fenêtre de bureau transparente. OpenGL prend déjà très bien
+en charge cette charge de travail et s'intègre naturellement à SDL3 ainsi qu'au
+renderer OpenGL de Cubism.
+
+Passer à Vulkan impliquerait de maintenir beaucoup plus de code de rendu et de
+synchronisation sur trois plateformes de bureau, sans amélioration perceptible
+pour les utilisateurs. Pour la charge de travail actuelle de BongoCat, OpenGL
+permet de conserver un renderer plus compact, plus simple à déboguer et plus
+facile à maintenir, tout en fournissant les performances dont nous avons besoin.
+
+## État du projet
+
+
+
+## 🙏 Remerciements
+> [!TIP]
+> Chaque pas de BongoCat est porté par l'esprit de l'open source. Nous remercions sincèrement tous les contributeurs de la communauté pour leurs contributions désintéressées (listées ci-dessous par ordre chronologique de contribution). C'est votre soutien qui rend l'accompagnement sur le bureau plus libre et plus authentique.❤️🔥
+
+
+
+
+
+
+
+[linux.do](https://linux.do/t/topic/2845597)
+---
+
+
+
+Copyright © 2026 - **BongoCat**\
+Par vladelaina\
+Fait avec ❤️ & ⌨️
+
+
diff --git a/docs/README.id-ID.md b/docs/README.id-ID.md
new file mode 100644
index 00000000..4eeedc07
--- /dev/null
+++ b/docs/README.id-ID.md
@@ -0,0 +1,375 @@
+
+
+
+
+
+
+ 💘C/C++ × SDL3 × OpenGL, campur semuanya, satukan! Bong~ Bongo Cat!!!
+
+
+English • 简体中文 • 繁體中文 • Français • Deutsch • 日本語 • 한국어 • Português • Русский • Español • Bahasa Indonesia
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+> [!TIP]
+> Model yang ditampilkan dalam demo ini berasal dari [宇痕冫](https://space.bilibili.com/348616056).
+>
+> 🎁 Mencari model **gratis**? Kami bekerja sama dengan kreator model berbakat untuk menghadirkan beragam model gratis, sambil terus mengeksplorasi pengalaman desktop yang lebih seru! Kunjungi situs resmi kami: [bongocat.pet](https://bongocat.pet/models)
+
+
+
+
+
+
+
+
+
+
+
+
+## 📥 Unduh
+
+
+
+
+
+- GitHub Releases
+
+ Unduh rilis terbaru dari [GitHub Releases](https://github.com/vladelaina/BongoCat/releases/latest).
+
+## 🛠️ Membangun dari Kode Sumber
+
+BongoCat menggunakan CMake dan memerlukan compiler C11, compiler C++17, CMake 3.24
+atau yang lebih baru, serta file pengembangan OpenGL desktop. SDL3, yyjson, stb,
+miniaudio, dan Nuklear secara default diunduh saat proses konfigurasi, sehingga
+konfigurasi pertama memerlukan akses jaringan.
+
+Jalankan perintah berikut dari root proyek (direktori yang berisi
+`CMakeLists.txt`).
+
+### 📋 Prasyarat Platform
+
+- **Windows:** Visual Studio 2022 dengan workload Desktop C++ dan CMake.
+ Gunakan generator MSVC; MinGW dapat membangun backend diagnostik tetapi tidak
+ didukung untuk Cubism SDK.
+- **macOS:** Xcode Command Line Tools, CMake, dan Ninja. Pilih arsitektur dengan
+ `CMAKE_OSX_ARCHITECTURES` jika berbeda dari default host.
+- **Linux (Debian/Ubuntu):** GCC atau Clang, Ninja, serta header OpenGL/X11:
+
+ ```bash
+ sudo apt-get update
+ sudo apt-get install -y build-essential cmake ninja-build \
+ libgl1-mesa-dev libx11-dev libxi-dev libxfixes-dev
+ ```
+
+### 🔧 Konfigurasi dan Build
+
+Di Linux dan macOS, gunakan generator single-configuration seperti Ninja:
+
+```bash
+cmake -S . -B build -G Ninja \
+ -DCMAKE_BUILD_TYPE=Release \
+ -DBONGO_CAT_FETCH_DEPS=ON
+cmake --build build --parallel
+```
+
+Di Windows, jalankan dari shell developer Visual Studio 2022 (atau shell lain
+yang menyediakan MSVC):
+
+```powershell
+cmake -S . -B build -G "Visual Studio 17 2022" -A x64 `
+ -DBONGO_CAT_FETCH_DEPS=ON
+cmake --build build --config Release --parallel
+```
+
+File executable dihasilkan di `build/BongoCat` pada Linux, di
+`build/BongoCat.app/Contents/MacOS/BongoCat` di macOS, dan ke
+`build/Release/BongoCat.exe` untuk build Visual Studio.
+
+### 🧪 Pengujian
+
+Target CTest diaktifkan secara default. Jalankan setelah proses build:
+
+```bash
+ctest --test-dir build --output-on-failure
+```
+
+Untuk generator multi-configuration seperti Visual Studio, pilih konfigurasi
+build secara eksplisit:
+
+```powershell
+ctest --test-dir build -C Release --output-on-failure
+```
+
+### 🎭 Live2D / Cubism SDK (Opsional)
+
+Jika Cubism SDK tidak tersedia, CMake akan menampilkan peringatan dan membangun
+backend diagnostik. Backend ini ditujukan untuk diagnostik startup dan platform;
+backend ini tidak menyediakan rendering model Live2D. Untuk membangun runtime
+lengkap, pasang Cubism SDK for Native yang kompatibel lalu tempatkan di
+`vendor/CubismSdkForNative` atau berikan lokasinya secara eksplisit:
+
+```bash
+cmake -S . -B build -G Ninja \
+ -DCMAKE_BUILD_TYPE=Release \
+ -DBONGO_CAT_CUBISM_SDK=/path/to/CubismSdkForNative \
+ -DBONGO_CAT_REQUIRE_CUBISM=ON
+```
+
+SDK harus berisi Core library, source Framework, dan tree third-party OpenGL GLEW
+dengan tata letak yang diharapkan oleh `cmake/Cubism.cmake`. Build Cubism di
+Windows memerlukan Visual Studio 2022. `BONGO_CAT_REQUIRE_CUBISM=ON` membuat
+konfigurasi gagal alih-alih diam-diam memilih backend diagnostik.
+
+### ⚙️ Opsi CMake
+
+| Opsi | Default | Deskripsi |
+| --- | --- | --- |
+| `BONGO_CAT_FETCH_DEPS` | `ON` | Unduh dependency pihak ketiga yang versinya dipin menggunakan CMake `FetchContent`. Atur ke `OFF` hanya jika SDL3, yyjson, stb, miniaudio, dan Nuklear sudah tersedia untuk CMake. |
+| `BONGO_CAT_CUBISM_SDK` | `vendor/CubismSdkForNative` | Path ke Cubism SDK for Native. |
+| `BONGO_CAT_REQUIRE_CUBISM` | `OFF` | Gagalkan konfigurasi jika Cubism SDK yang dapat digunakan tidak tersedia. |
+| `BONGO_CAT_WARNINGS_AS_ERRORS` | `OFF` | Perlakukan warning compiler native sebagai error. |
+
+Untuk build offline dengan `BONGO_CAT_FETCH_DEPS=OFF`, sediakan konfigurasi
+package CMake untuk SDL3 (termasuk `SDL3-static`) dan yyjson, serta direktori
+include untuk stb, Nuklear, dan miniaudio jika tidak dapat ditemukan secara
+otomatis:
+
+```bash
+cmake -S . -B build -G Ninja \
+ -DCMAKE_BUILD_TYPE=Release \
+ -DBONGO_CAT_FETCH_DEPS=OFF \
+ -DBONGO_CAT_STB_INCLUDE_DIR=/path/to/stb \
+ -DBONGO_CAT_NUKLEAR_INCLUDE_DIR=/path/to/nuklear \
+ -DBONGO_CAT_MINIAUDIO_INCLUDE_DIR=/path/to/miniaudio
+```
+
+## 📌 Status Proyek
+
+
+## 📜 Lisensi
+
+Kode sumber dan runtime native BongoCat dilisensikan di bawah
+[AGPL-3.0-only](../LICENSE).
+
+Mode model bawaan default (`standard`) tetap berlisensi MIT. Aset model yang
+disertakan dalam `resources/assets/models/standard`, `keyboard`, dan `gamepad`
+dilindungi oleh [pemberitahuan lisensi MIT](../LICENSE-MIT) terpisah.
+Lisensi MIT tersebut hanya berlaku untuk aset model dan artwork pendampingnya;
+lisensi tersebut tidak mengubah lisensi kode sumber atau runtime native BongoCat.
+
+
+## 🧭 Arsitektur Teknis
+
+> Versi native saat ini dibangun dengan C/C++, SDL3, dan OpenGL. Diagram di bawah berfokus pada aliran data runtime; detail build dan packaging berada di CMake.
+
+### 🔄 Kepemilikan Runtime dan Penjadwalan Frame
+
+Setiap proses memiliki satu `BongoCatApp` serta satu event loop dan render loop
+di thread utama. Listener platform berhenti pada batas input:
+
+```text
+Listener platform
+(keyboard / pointer)
+ |
+ v
+ state input C11
+ (antrean edge atomik + posisi pointer yang digabungkan)
+ |
+ v
+ aplikasi thread utama <----- event SDL3
+ |
+ v
+ parameter model, overlay, dan state UI
+ |
+ v
+ update model -> komposisi OpenGL -> presentasi platform
+```
+
+Penerima Raw Input Windows, event tap Quartz macOS, dan listener XInput2 Linux
+berjalan di luar main loop. Perubahan tombol masuk ke antrean atomik berbatas,
+gerakan digabungkan secara terpisah, dan event SDL membangunkan thread utama.
+Windows memakai jendela khusus pesan dengan `RIDEV_INPUTSINK | RIDEV_DEVNOTIFY`
+untuk menerima input latar belakang sambil mempertahankan pesan jendela biasa.
+Model memakai gerakan perangkat ketika aplikasi lain menyembunyikan atau mengunci
+kursor; SDL menyediakan posisi kursor desktop. Status tombol dibersihkan ketika
+perangkat dilepas atau desktop input berganti. Windows tidak lagi memakai hook
+input atau DirectInput. Event jendela SDL3, preferensi, dan gamepad ditangani di thread utama,
+tempat event gamepad dinormalisasi sebelum mencapai parameter model atau
+shortcut. Tidak ada listener platform yang memanggil kode Live2D, overlay, atau
+UI secara langsung.
+
+`bongo_cat_app_run` menangani update-shutdown dan argumen proses sekunder,
+memastikan kepemilikan single-instance untuk proses utama, mengalokasikan state
+aplikasi, menjalankan inisialisasi, masuk ke `bongo_cat_app_loop`, lalu melakukan
+flush state dan menghancurkan resource dalam urutan yang terdefinisi.
+Inisialisasi memuat konfigurasi dan path penyimpanan, menemukan aset, membuat
+jendela pet SDL/OpenGL, menginisialisasi backend platform, membuat layanan
+Live2D, overlay, dan audio, memindai sumber model bawaan/terpasang/terdekat, lalu
+memuat model yang dapat digunakan. `BongoCatApp` memiliki pengaturan, state
+sesi, katalog model dan perilaku, handle platform, serta handle layanan runtime.
+
+Paket model yang terpasang menggunakan Mver sebagai format kanonis. Workflow
+impor menentukan file atau direktori yang dipilih, menemukan dan memvalidasi
+kandidat, membuat fingerprint identitas paket, mengonversi sumber Tauri ke Mver,
+menerapkan patch gambar, lalu menyimpan paket yang sudah dinormalisasi di bawah
+`models_root`. Setelah itu, workflow menghasilkan adapter runtime dan
+memperbarui katalog. Sumber terdekat ditemukan tanpa memasang source tree-nya;
+adapter dan hasil inspeksinya di-cache di luar `models_root` di bawah
+`cache_root`.
+
+Setiap iterasi main loop menunggu sinyal wakeup SDL/native atau deadline frame, UI,
+animasi, atau pointer-hit terdekat yang masih tertunda (dengan waktu tunggu
+maksimum 250 ms). Iterasi tersebut mendispatch event SDL yang mengantre,
+menguras antrean input atomik dan release recovery, memperbarui state jendela
+dan model-refresh, serta menerapkan parameter yang berasal dari input. Saat
+Cubism aktif, deadline model mengikuti `settings.model.max_fps` (default 60 FPS);
+build diagnostik menggunakan interval fallback 100 ms. Waktu model yang berlalu
+dibatasi hingga 250 ms dan dibagi menjadi maksimal delapan substep, dengan
+target tidak lebih dari 1/30 detik per substep.
+
+Jalur render pet normal hanya melakukan render saat jendela terlihat, tidak diminimalkan,
+dan ditandai dirty. Sebuah frame membersihkan latar belakang, menggambar model, dan
+mengomposisikan overlay pointer, tombol, dan efek sebelum memanggil presenter
+platform. Operasi preview dapat meminta render langsung, sedangkan render
+capture dapat melewati presentasi. macOS dan Linux menukar jendela SDL OpenGL
+secara langsung. Windows melakukan swap langsung saat layered presentation tidak
+aktif; jika aktif, Windows membaca kembali frame untuk `UpdateLayeredWindow`.
+UI preferensi memiliki jendela SDL/OpenGL terpisah yang dirender dan
+dipresentasikan secara independen dari jendela pet.
+
+Runtime C memanggil ABI yang dideklarasikan di `include/bongo_cat/model.h`.
+Bridge Live2D dan implementasi Cubism berada di `src/live2d` dan hanya
+menggunakan C++17 saat Cubism SDK diaktifkan; runtime native lainnya menggunakan
+C11. Tipe Cubism tetap berada di balik handle C yang opaque, sedangkan
+`src/live2d/live2d_stub.c` menyediakan backend diagnostik ketika SDK tidak
+tersedia.
+
+
+```mermaid
+flowchart TB
+ Input(["Keyboard / mouse / gamepad"])
+ BuiltIn(["Aset model bawaan"])
+ Sources(["Sumber model eksternal
Mver, Tauri, .model3.json, patch gambar"])
+ Desktop(["Jendela pet dan jendela preferensi"])
+
+ subgraph Runtime["Runtime native BongoCat"]
+ direction TB
+ Entry["src/main.c
bongo_cat_app_run"]
+ Startup["Startup dan inisialisasi
konfigurasi, penyimpanan, jendela, platform"]
+ Loop["Main loop SDL3
tunggu, dispatch, update, render"]
+ Shutdown["Shutdown
flush state, hentikan layanan, lepaskan resource"]
+ InputQueue[("State input atomik
antrean edge dan posisi pointer yang digabungkan")]
+ InputDispatch["Dispatch input
shortcut, pemetaan pointer, parameter model"]
+ State[("State BongoCatApp
pengaturan, sesi, katalog, handle runtime")]
+ Import["Penemuan dan impor model
validasi, normalisasi ke Mver, instal/cache"]
+ Catalog[("Katalog model dan perilaku")]
+ Live2D["ABI C Live2D
Cubism SDK atau stub diagnostik"]
+ Overlay["Overlay dan audio"]
+ Preferences["Preferensi dan shell desktop
UI Nuklear, tray, aksi jendela"]
+ Compose["Komposisi frame OpenGL"]
+ Present["Presentasi platform"]
+
+ Entry --> Startup --> Loop
+ Loop --> Shutdown
+ Loop --> InputDispatch --> State
+ Loop <--> State
+ State --> Live2D
+ State --> Overlay
+ State <--> Preferences
+ Loop --> Preferences
+ Catalog --> State
+ State --> Compose
+ Live2D --> Compose
+ Overlay --> Compose
+ Compose --> Present
+ Loop --> Compose
+ end
+
+ subgraph Platform["Backend platform"]
+ direction LR
+ Global["Capture keyboard / pointer global
Windows, macOS, Linux"]
+ SDL["Event SDL3
event jendela dan gamepad"]
+ end
+
+ Input --> Global --> InputQueue --> InputDispatch
+ Input --> SDL --> Loop
+ BuiltIn --> Catalog
+ Sources --> Import --> Catalog
+ Present --> Desktop
+ Preferences --> Desktop
+```
+
+## ❓ FAQ
+
+### 🔒 Apakah BongoCat merekam input keyboard atau mouse saya?
+
+Tidak. BongoCat memproses input keyboard dan mouse secara lokal untuk
+menggerakkan animasi dan shortcut. BongoCat tidak merekam atau mengunggah
+keystroke, aksi mouse, atau data interaksi lainnya. Konfigurasi juga disimpan
+secara lokal, dan aplikasi tidak memiliki iklan, alat analitik, atau kode
+pelacakan pengguna. Saat pemeriksaan pembaruan dilakukan, aplikasi hanya meminta
+metadata rilis publik; aplikasi tidak mengirim data input, konfigurasi, atau
+penggunaan.
+
+### 🖼️ Mengapa OpenGL, bukan Vulkan?
+
+Kami memilih OpenGL bukan karena Vulkan buruk, tetapi karena BongoCat tidak
+memerlukan tingkat kompleksitas tersebut. Aplikasi terutama merender satu model
+Live2D, beberapa lapisan UI, dan sebuah jendela desktop transparan. OpenGL sudah
+menangani kebutuhan tersebut dengan nyaman dan bekerja secara alami dengan SDL3
+serta renderer OpenGL milik Cubism. Beralih ke Vulkan berarti harus memelihara
+lebih banyak kode rendering dan sinkronisasi di tiga platform desktop, tanpa
+peningkatan yang terasa bagi pengguna. Untuk beban kerja BongoCat saat ini, OpenGL
+menjaga renderer tetap lebih kecil, lebih mudah di-debug, dan lebih mudah
+dipelihara sambil tetap memberikan performa yang dibutuhkan.
+
+
+
+
+## Status Proyek
+
+
+
+## 🙏 Ucapan Terima Kasih
+> [!TIP]
+> Setiap langkah BongoCat digerakkan oleh semangat open source. Kami dengan tulus berterima kasih kepada seluruh kontributor komunitas atas kontribusi mereka tanpa pamrih (tercantum di bawah berdasarkan urutan tanggal kontribusi). Dukungan Anda membuat pengalaman pendamping desktop menjadi lebih bebas dan autentik.❤️🔥
+
+
+
+
+
+
+
+[linux.do](https://linux.do/t/topic/2845597)
+---
+
+
+
+Copyright © 2026 - **BongoCat**\
+Oleh vladelaina\
+Dibuat dengan ❤️ & ⌨️
+
+
diff --git a/docs/README.ja-JP.md b/docs/README.ja-JP.md
new file mode 100644
index 00000000..d3f5a12f
--- /dev/null
+++ b/docs/README.ja-JP.md
@@ -0,0 +1 @@
+
diff --git a/docs/README.ko-KR.md b/docs/README.ko-KR.md
new file mode 100644
index 00000000..160e6b9c
--- /dev/null
+++ b/docs/README.ko-KR.md
@@ -0,0 +1,252 @@
+
+
+💘 C/C++ × SDL3 × OpenGL,함께 섞어서 마음껏 두드리세요! Bong~ Bongo Cat!!!
+
+ 언어 선택 ❯ English • 简体中文 • 繁體中文 • Français • Deutsch • 日本語 • 한국어 • Português • Русский • Español • Bahasa Indonesia
+
+
+
+
+
+
+
+
+
+
+
+> [!TIP]
+> 데모에 사용된 모델은 [宇痕冫](https://space.bilibili.com/348616056)님의 작품입니다.
+>
+> 🎁 **무료** 모델을 찾으시나요? 저희는 재능 있는 모델 크리에이터들과 협력하여 다양한 무료 모델을 제공하고, 더 재미있는 데스크톱 경험도 끊임없이 탐색하고 있습니다! 공식 웹사이트를 방문하세요: [bongocat.pet](https://bongocat.pet/models)
+
+
+
+
+
+
+
+
+
+## 📥 다운로드
+
+
+
+- GitHub Releases
+
+ [GitHub Releases](https://github.com/vladelaina/BongoCat/releases/latest)에서 최신 버전을 다운로드하세요.
+
+## 🛠️ 소스 코드로 빌드하기
+
+BongoCat은 CMake를 사용하며, C11 컴파일러, C++17 컴파일러, CMake 3.24 이상, 데스크톱 OpenGL 개발 파일이 필요합니다. 기본적으로 SDL3, yyjson, stb, miniaudio 및 Nuklear는 구성 단계에서 자동으로 다운로드되므로, 첫 구성 시 네트워크 연결이 필요합니다.
+
+프로젝트 루트 디렉토리(`CMakeLists.txt`가 있는 디렉토리)에서 다음 명령어를 실행하세요.
+
+### 📋 플랫폼 사전 요구 사항
+
+- **Windows:** Visual Studio 2022("C++를 사용한 데스크톱 개발" 워크로드 설치) 및 CMake. MSVC 생성기를 사용하세요. MinGW는 진단 백엔드를 빌드할 수 있지만 Cubism SDK를 지원하지 않습니다.
+- **macOS:** Xcode Command Line Tools, CMake 및 Ninja. 대상 아키텍처가 호스트 기본 아키텍처와 다른 경우 `CMAKE_OSX_ARCHITECTURES`를 통해 지정하세요.
+- **Linux(Debian/Ubuntu):** GCC 또는 Clang, Ninja, OpenGL/X11 헤더 파일:
+
+ ```bash
+ sudo apt-get update
+ sudo apt-get install -y build-essential cmake ninja-build \
+ libgl1-mesa-dev libx11-dev libxi-dev libxfixes-dev libcurl4-openssl-dev
+ ```
+
+### 🔧 구성 및 빌드
+
+Linux 및 macOS에서는 Ninja와 같은 단일 구성 생성기를 사용하세요:
+
+```bash
+cmake -S . -B build -G Ninja \
+ -DCMAKE_BUILD_TYPE=Release \
+ -DBONGO_CAT_FETCH_DEPS=ON
+cmake --build build --parallel
+```
+
+Windows에서는 Visual Studio 2022 개발자 명령 프롬프트(또는 MSVC를 사용할 수 있는 기타 명령줄)에서 다음을 실행하세요:
+
+```powershell
+cmake -S . -B build -G "Visual Studio 17 2022" -A x64 `
+ -DBONGO_CAT_FETCH_DEPS=ON
+cmake --build build --config Release --parallel
+```
+
+실행 파일 위치: Linux `build/BongoCat`, macOS `build/BongoCat.app/Contents/MacOS/BongoCat`, Visual Studio 빌드 Windows 버전 `build/Release/BongoCat.exe`.
+
+### 🧪 테스트
+
+CTest 대상은 기본적으로 활성화됩니다. 빌드 후 실행:
+
+```bash
+ctest --test-dir build --output-on-failure
+```
+
+Visual Studio와 같은 다중 구성 생성기의 경우 빌드 구성을 명시적으로 지정하세요:
+
+```powershell
+ctest --test-dir build -C Release --output-on-failure
+```
+
+### 🎭 Live2D / Cubism SDK (선택 사항)
+
+Cubism SDK를 찾을 수 없으면 CMake는 경고를 표시하고 진단 백엔드를 빌드합니다. 이 백엔드는 시작 및 플랫폼 진단에 사용되며 Live2D 모델 렌더링을 제공하지 않습니다. 전체 런타임을 빌드하려면 호환되는 Cubism SDK for Native를 설치하고 `vendor/CubismSdkForNative`에 배치하거나 경로를 명시적으로 전달하세요:
+
+```bash
+cmake -S . -B build -G Ninja \
+ -DCMAKE_BUILD_TYPE=Release \
+ -DBONGO_CAT_CUBISM_SDK=/path/to/CubismSdkForNative \
+ -DBONGO_CAT_REQUIRE_CUBISM=ON
+```
+
+SDK에는 Core 라이브러리, Framework 소스 코드, 그리고 `cmake/Cubism.cmake`에서 요구하는 레이아웃의 OpenGL GLEW 서드파티 디렉토리가 포함되어야 합니다. Windows Cubism 빌드에는 Visual Studio 2022가 필요합니다. `BONGO_CAT_REQUIRE_CUBISM=ON`은 SDK를 사용할 수 없을 때 진단 백엔드를 자동으로 선택하는 대신 구성을 실패하게 합니다.
+
+### ⚙️ CMake 옵션
+
+| 옵션 | 기본값 | 설명 |
+| --- | --- | --- |
+| `BONGO_CAT_FETCH_DEPS` | `ON` | CMake `FetchContent`를 사용하여 고정 버전의 서드파티 종속성을 다운로드합니다. SDL3, yyjson, stb, miniaudio 및 Nuklear를 CMake에서 이미 사용할 수 있는 경우에만 `OFF`로 설정하세요. |
+| `BONGO_CAT_CUBISM_SDK` | `vendor/CubismSdkForNative` | Cubism SDK for Native의 경로입니다. |
+| `BONGO_CAT_REQUIRE_CUBISM` | `OFF` | SDK를 사용할 수 없을 때 구성을 실패하게 합니다. |
+| `BONGO_CAT_WARNINGS_AS_ERRORS` | `OFF` | 로컬 컴파일러 경고를 오류로 처리합니다. |
+
+오프라인 빌드 시 `BONGO_CAT_FETCH_DEPS=OFF`로 설정하고, SDL3(`SDL3-static` 포함) 및 yyjson의 CMake 패키지 구성을 제공하세요. stb, Nuklear 및 miniaudio를 자동으로 찾을 수 없는 경우 해당 include 디렉토리도 제공해야 합니다:
+
+```bash
+cmake -S . -B build -G Ninja \
+ -DCMAKE_BUILD_TYPE=Release \
+ -DBONGO_CAT_FETCH_DEPS=OFF \
+ -DBONGO_CAT_STB_INCLUDE_DIR=/path/to/stb \
+ -DBONGO_CAT_NUKLEAR_INCLUDE_DIR=/path/to/nuklear \
+ -DBONGO_CAT_MINIAUDIO_INCLUDE_DIR=/path/to/miniaudio
+```
+
+## 📌 프로젝트 상태
+
+
+
+## 📜 라이선스
+
+BongoCat 소스 코드 및 로컬 런타임은 [AGPL-3.0-only](../LICENSE) 라이선스를 따릅니다.
+
+기본 내장 모델(`standard`)은 여전히 MIT 라이선스를 따릅니다. `resources/assets/models/standard`, `keyboard` 및 `gamepad`에 번들로 제공되는 모델 리소스는 별도의 [MIT 라이선스 고지](../LICENSE-MIT)가 적용됩니다. 해당 MIT 라이선스는 모델 리소스 및 관련 아트워크에만 적용되며, BongoCat 소스 코드나 로컬 런타임의 라이선스를 변경하지 않습니다.
+
+## 🧭 기술 아키텍처
+
+현재 네이티브 버전은 C/C++, SDL3 및 OpenGL을 기반으로 구축되었습니다. 아래 다이어그램은 런타임 데이터 흐름을 중점적으로 보여줍니다. 빌드 및 패키징 세부 사항은 CMake 파일을 참조하세요.
+
+### 🔄 런타임 소유권 및 프레임 스케줄링
+
+각 프로세스는 하나의 `BongoCatApp`과 하나의 메인 스레드 이벤트/렌더링 루프를 소유합니다. 플랫폼 리스너는 입력 경계에서 중단됩니다:
+
+```text
+플랫폼 리스너 (키보드/포인터)
+ |
+ v
+ C11 입력 상태 (원자적 에지 큐 + 병합된 포인터 위치)
+ |
+ v
+ 메인 스레드 애플리케이션 <----- SDL3 이벤트
+ |
+ v
+ 모델 파라미터, 오버레이 및 UI 상태
+ |
+ v
+ 모델 업데이트 -> OpenGL 합성 -> 플랫폼 프레젠테이션
+```
+
+Windows Raw Input 수신기, macOS Quartz 이벤트 탭, Linux XInput2 리스너는 메인 루프 외부에서 실행됩니다. 키와 마우스 버튼 이벤트는 유계 원자 큐에 전달하고, 이동량은 별도로 합치며 SDL 이벤트로 메인 스레드를 깨웁니다. Windows는 메시지 전용 창에 `RIDEV_INPUTSINK | RIDEV_DEVNOTIFY`를 등록하여 일반 창 메시지를 유지하면서 백그라운드 입력을 받습니다. 다른 프로그램이 커서를 숨기거나 잠그면 모델은 장치가 보고한 이동량을 사용하고, 바탕 화면에서는 SDL이 시스템 커서 위치를 제공합니다. 장치를 분리하거나 입력 데스크톱을 전환하면 눌림 상태를 정리합니다. Windows는 더 이상 입력 훅이나 DirectInput을 사용하지 않습니다. SDL3 창, 환경 설정 및 게임패드 이벤트는 메인 스레드에서 처리하며 플랫폼 리스너는 Live2D, 오버레이 또는 UI 코드를 직접 호출하지 않습니다.
+
+`bongo_cat_app_run`은 업데이트, 종료 및 보조 프로세스 파라미터를 담당하여 메인 프로세스의 단일 인스턴스 소유권을 보장하고, 애플리케이션 상태를 할당하며, 초기화를 수행하고, `bongo_cat_app_loop`로 진입한 후 정의된 순서대로 상태를 플러시하고 리소스를 소멸시킵니다. 초기화는 구성 및 저장 경로를 로드하고, 리소스를 찾고, SDL/OpenGL 펫 창을 생성하고, 플랫폼 백엔드를 초기화하고, Live2D/오버레이/오디오 서비스를 생성하고, 내장/설치/근처 모델 소스를 스캔하고, 사용 가능한 모델을 로드합니다. `BongoCatApp`은 설정, 세션 상태, 모델 및 동작 카탈로그, 플랫폼 핸들 및 런타임 서비스 핸들을 보유합니다.
+
+설치된 모델 패키지는 Mver을 표준 형식으로 사용합니다. 가져오기 프로세스는 선택한 파일이나 디렉토리를 구문 분석하고, 후보를 발견 및 검증하고, 패키지 ID 지문을 생성하고, Tauri 소스를 Mver로 변환하고, 이미지 패치를 적용하고, 정규화된 패키지를 `models_root`에 제출합니다. 그런 다음 런타임 어댑터를 생성하고 카탈로그를 새로 고칩니다. 근처 소스는 소스 디렉토리를 설치하지 않고 발견되며, 해당 어댑터와 검사 결과는 `models_root` 외부의 `cache_root` 아래에 캐시됩니다.
+
+각 메인 루프 반복은 SDL/네이티브 웨이크를 기다리거나, 가장 빠른 프레임, UI, 애니메이션 및 포인터 적중 마감 시간(최대 250ms)을 기다립니다. 루프는 대기 중인 SDL 이벤트를 처리하고, 원자 입력 큐를 비우고 해제 복구를 수행하고, 창 및 모델 새로 고침 상태를 업데이트한 다음 입력 파라미터를 적용합니다. Cubism이 활성화되면 모델 마감 시간은 `settings.model.max_fps`(기본값 60 FPS)를 따릅니다. 진단 빌드는 100ms 폴백 간격을 사용합니다. 모델 경과 시간은 최대 250ms까지 계산되며, 각 하위 단계 목표가 1/30초를 초과하지 않도록 최대 8개의 하위 단계로 분할됩니다.
+
+일반 펫 경로는 창이 보이고 최소화되지 않았으며 더티로 표시된 경우에만 렌더링됩니다. 각 프레임은 먼저 배경을 지우고, 모델을 그린 다음, 포인터, 키 및 효과 오버레이를 합성하고, 마지막으로 플랫폼 프리젠터를 호출합니다. 미리보기 작업은 즉시 렌더링을 요청할 수 있고, 스크린샷 렌더링은 프레젠테이션을 건너뛸 수 있습니다. macOS 및 Linux는 SDL OpenGL 창을 직접 교체합니다. Windows는 계층화된 프레젠테이션이 활성화되지 않은 경우 직접 교체하고, 그렇지 않으면 프레임 버퍼를 읽고 `UpdateLayeredWindow`를 호출합니다. 환경 설정 UI는 자체 SDL/OpenGL 창을 소유하고 별도로 렌더링 및 프레젠테이션을 수행합니다.
+
+C 런타임은 `include/bongo_cat/model.h`에 선언된 ABI를 호출합니다. Live2D 브리지 및 Cubism 구현은 `src/live2d`에 있으며, Cubism SDK가 활성화된 경우에만 C++17을 사용합니다. 나머지 로컬 런타임은 C11을 사용합니다. Cubism 타입은 불투명 C 핸들 뒤에 유지됩니다. SDK를 사용할 수 없을 때 `src/live2d/live2d_stub.c`는 진단 백엔드를 제공합니다.
+
+```mermaid
+flowchart TB
+ Input(["키보드 / 마우스 / 게임패드"])
+ BuiltIn(["내장 모델 리소스"])
+ Sources(["외부 모델 소스
Mver, Tauri, .model3.json, 이미지 패치"])
+ Desktop(["펫 창 및 환경 설정 창"])
+ subgraph Runtime["BongoCat 네이티브 런타임"]
+ direction TB
+ Entry["src/main.c
bongo_cat_app_run"]
+ Startup["시작 및 초기화
구성, 저장소, 창, 플랫폼"]
+ Loop["SDL3 메인 루프
대기, 처리, 업데이트, 렌더링"]
+ Shutdown["종료
상태 플러시, 서비스 중지, 리소스 해제"]
+ InputQueue[("원자 입력 상태
에지 큐 및 병합된 포인터 위치")]
+ InputDispatch["입력 처리
단축키, 포인터 매핑, 모델 파라미터"]
+ State[("BongoCatApp 상태
설정, 세션, 카탈로그, 런타임 핸들")]
+ Import["모델 발견 및 가져오기
검증, Mver로 정규화, 설치/캐싱"]
+ Catalog[("모델 및 동작 카탈로그")]
+ Live2D["Live2D C ABI
Cubism SDK 또는 진단 스텁"]
+ Overlay["오버레이 및 오디오"]
+ Preferences["환경 설정 및 데스크톱 셸
Nuklear UI, 트레이, 창 작업"]
+ Compose["OpenGL 프레임 합성"]
+ Present["플랫폼 프레젠테이션"]
+ Entry --> Startup --> Loop
+ Loop --> Shutdown
+ Loop --> InputDispatch --> State
+ Loop <--> State
+ State --> Live2D
+ State --> Overlay
+ State <--> Preferences
+ Loop --> Preferences
+ Catalog --> State
+ State --> Compose
+ Live2D --> Compose
+ Overlay --> Compose
+ Compose --> Present
+ Loop --> Compose
+ end
+ subgraph Platform["플랫폼 백엔드"]
+ direction LR
+ Global["전역 키보드/포인터 캡처
Windows, macOS, Linux"]
+ SDL["SDL3 이벤트
창 및 게임패드 이벤트"]
+ end
+ Input --> Global --> InputQueue --> InputDispatch
+ Input --> SDL --> Loop
+ BuiltIn --> Catalog
+ Sources --> Import --> Catalog
+ Present --> Desktop
+ Preferences --> Desktop
+```
+
+## ❓ 자주 묻는 질문
+
+### 🔒 BongoCat이 제 키보드나 마우스 입력을 기록하나요?
+
+아니요. BongoCat은 애니메이션 구동과 단축키를 위해 키보드 및 마우스 입력을 로컬에서 처리합니다. 키 입력, 마우스 동작 또는 기타 상호 작용 데이터를 기록하거나 업로드하지 않습니다. 구성도 로컬에만 저장되며, 애플리케이션에는 광고, 분석 도구 또는 사용자 추적 코드가 포함되어 있지 않습니다. 업데이트 확인을 수행할 때는 공개 버전 메타데이터만 요청하며, 입력, 구성 또는 사용 데이터를 전송하지 않습니다.
+
+### 🖼️ OpenGL을 사용하는 이유는 무엇인가요? Vulkan은 왜 사용하지 않나요?
+
+Vulkan이 나쁘기 때문이 아니라, BongoCat이 그 정도의 복잡성을 필요로 하지 않기 때문입니다. 애플리케이션은 주로 하나의 Live2D 모델, 소량의 UI 레이어 및 투명 데스크톱 창을 렌더링하며, OpenGL은 이를 쉽게 충족할 수 있고 SDL3 및 Cubism의 OpenGL 렌더러와 자연스럽게 통합됩니다. Vulkan으로 마이그레이션하려면 세 가지 데스크톱 플랫폼에서 더 많은 렌더링 및 동기화 코드를 유지 관리해야 하지만, 사용자에게 눈에 띄는 개선은 없을 것입니다. BongoCat의 현재 워크로드에서 OpenGL은 렌더러를 더 간결하고 디버깅 및 유지 관리하기 쉽게 유지하면서도 필요한 성능을 제공합니다.
+
+## 🙏 특별 감사
+> [!TIP]
+> BongoCat의 모든 걸음은 오픈 소스 정신으로 이루어집니다. 커뮤니티 기여자분들의 사심 없는 기여에 진심으로 감사드립니다(기여 날짜순 정렬). 여러분의 지원이 데스크톱 동반을 더욱 자유롭고 진심 어린 것으로 만들어 줍니다.❤️🔥
+
+
+
+
+
+
+
+[linux.do](https://linux.do/t/topic/2845597)
+---
+
+
+저작권 © 2026 - **BongoCat**
+By vladelaina
+Made with ❤️ & ⌨️
+
diff --git a/docs/README.pt-BR.md b/docs/README.pt-BR.md
new file mode 100644
index 00000000..6f9d31a4
--- /dev/null
+++ b/docs/README.pt-BR.md
@@ -0,0 +1,252 @@
+
+
+💘 C/C++ × SDL3 × OpenGL, misture tudo e bata à vontade! Bong~ Bongo Cat!!!
+
+ Escolha o idioma ❯ English • 简体中文 • 繁體中文 • Français • Deutsch • 日本語 • 한국어 • Português • Русский • Español • Bahasa Indonesia
+
+
+
+
+
+
+
+
+
+
+
+> [!TIP]
+> O modelo usado na demonstração é de [宇痕冫](https://space.bilibili.com/348616056).
+>
+> 🎁 Procurando modelos **gratuitos**? Trabalhamos com criadores de modelos talentosos para trazer uma grande variedade de modelos gratuitos, enquanto exploramos continuamente experiências de desktop ainda mais divertidas! Visite nosso site oficial: [bongocat.pet](https://bongocat.pet/models)
+
+
+
+
+
+
+
+
+
+## 📥 Download
+
+
+
+- GitHub Releases
+
+ Baixe a versão mais recente nas [GitHub Releases](https://github.com/vladelaina/BongoCat/releases/latest).
+
+## 🛠️ Compilar a partir do código-fonte
+
+O BongoCat usa CMake e requer um compilador C11, um compilador C++17, CMake 3.24 ou superior e os arquivos de desenvolvimento de OpenGL para desktop. Por padrão, SDL3, yyjson, stb, miniaudio e Nuklear são baixados automaticamente durante a configuração, portanto a primeira configuração requer conexão com a internet.
+
+Execute os comandos abaixo na raiz do projeto (o diretório que contém `CMakeLists.txt`).
+
+### 📋 Pré-requisitos por plataforma
+
+- **Windows:** Visual Studio 2022 (com a carga de trabalho ‘Desenvolvimento para desktop com C++’) e CMake. Use o gerador MSVC; o MinGW pode compilar o backend de diagnóstico, mas não suporta o SDK do Cubism.
+- **macOS:** Xcode Command Line Tools, CMake e Ninja. Se a arquitetura de destino for diferente da padrão do host, especifique-a por meio de `CMAKE_OSX_ARCHITECTURES`.
+- **Linux (Debian/Ubuntu):** GCC ou Clang, Ninja e os cabeçalhos de OpenGL/X11:
+
+ ```bash
+ sudo apt-get update
+ sudo apt-get install -y build-essential cmake ninja-build \
+ libgl1-mesa-dev libx11-dev libxi-dev libxfixes-dev libcurl4-openssl-dev
+ ```
+
+### 🔧 Configuração e compilação
+
+No Linux e no macOS, use um gerador de configuração única como o Ninja:
+
+```bash
+cmake -S . -B build -G Ninja \
+ -DCMAKE_BUILD_TYPE=Release \
+ -DBONGO_CAT_FETCH_DEPS=ON
+cmake --build build --parallel
+```
+
+No Windows, execute os comandos no Prompt de Comando do Desenvolvedor do Visual Studio 2022 (ou em qualquer shell em que o MSVC esteja disponível):
+
+```powershell
+cmake -S . -B build -G "Visual Studio 17 2022" -A x64 `
+ -DBONGO_CAT_FETCH_DEPS=ON
+cmake --build build --config Release --parallel
+```
+
+O executável fica em `build/BongoCat` no Linux, em `build/BongoCat.app/Contents/MacOS/BongoCat` no macOS e em `build/Release/BongoCat.exe` para compilações do Visual Studio no Windows.
+
+### 🧪 Testes
+
+Os alvos do CTest estão habilitados por padrão. Execute o seguinte após compilar:
+
+```bash
+ctest --test-dir build --output-on-failure
+```
+
+Para um gerador de múltiplas configurações, como o Visual Studio, especifique explicitamente a configuração de compilação:
+
+```powershell
+ctest --test-dir build -C Release --output-on-failure
+```
+
+### 🎭 Live2D / SDK do Cubism (opcional)
+
+Se o SDK do Cubism não for encontrado, o CMake emitirá um aviso e compilará o backend de diagnóstico. Esse backend é destinado à inicialização e ao diagnóstico de plataforma; ele não fornece renderização de modelos Live2D. Para compilar o runtime completo, instale uma versão compatível do Cubism SDK for Native, coloque-a em `vendor/CubismSdkForNative` ou informe o caminho explicitamente:
+
+```bash
+cmake -S . -B build -G Ninja \
+ -DCMAKE_BUILD_TYPE=Release \
+ -DBONGO_CAT_CUBISM_SDK=/path/to/CubismSdkForNative \
+ -DBONGO_CAT_REQUIRE_CUBISM=ON
+```
+
+O SDK deve incluir a biblioteca Core, o código-fonte do Framework e o diretório de terceiros do OpenGL GLEW na estrutura esperada por `cmake/Cubism.cmake`. As compilações de Cubism no Windows exigem o Visual Studio 2022. Com `BONGO_CAT_REQUIRE_CUBISM=ON`, a configuração falha quando o SDK não está disponível, em vez de selecionar silenciosamente o backend de diagnóstico.
+
+### ⚙️ Opções do CMake
+
+| Opção | Valor padrão | Descrição |
+| --- | --- | --- |
+| `BONGO_CAT_FETCH_DEPS` | `ON` | Baixa dependências de terceiros em versões fixadas via `FetchContent` do CMake. Defina como `OFF` apenas se SDL3, yyjson, stb, miniaudio e Nuklear já estiverem disponíveis para o CMake. |
+| `BONGO_CAT_CUBISM_SDK` | `vendor/CubismSdkForNative` | Caminho para o Cubism SDK for Native. |
+| `BONGO_CAT_REQUIRE_CUBISM` | `OFF` | Faz a configuração falhar quando não há SDK disponível. |
+| `BONGO_CAT_WARNINGS_AS_ERRORS` | `OFF` | Trata avisos do compilador nativo como erros. |
+
+Para uma compilação offline, defina `BONGO_CAT_FETCH_DEPS=OFF` e forneça as configurações de pacote do CMake para SDL3 (incluindo `SDL3-static`) e yyjson; se stb, Nuklear e miniaudio não puderem ser detectados automaticamente, informe também seus diretórios de inclusão:
+
+```bash
+cmake -S . -B build -G Ninja \
+ -DCMAKE_BUILD_TYPE=Release \
+ -DBONGO_CAT_FETCH_DEPS=OFF \
+ -DBONGO_CAT_STB_INCLUDE_DIR=/path/to/stb \
+ -DBONGO_CAT_NUKLEAR_INCLUDE_DIR=/path/to/nuklear \
+ -DBONGO_CAT_MINIAUDIO_INCLUDE_DIR=/path/to/miniaudio
+```
+
+## 📌 Status do projeto
+
+
+
+## 📜 Licença
+
+O código-fonte e o runtime nativo do BongoCat estão licenciados sob [AGPL-3.0-only](../LICENSE).
+
+O modo de modelo integrado padrão (`standard`) permanece sob a licença MIT. Os recursos de modelo incluídos em `resources/assets/models/standard`, `keyboard` e `gamepad` são cobertos pela [declaração de licença MIT](../LICENSE-MIT) separada. Essa licença MIT se aplica apenas aos recursos dos modelos e às artes que os acompanham; ela não altera a licença do código-fonte nem do runtime nativo do BongoCat.
+
+## 🧭 Arquitetura técnica
+
+A versão nativa atual é construída com C/C++, SDL3 e OpenGL. O diagrama abaixo destaca o fluxo de dados em tempo de execução; consulte os arquivos do CMake para obter detalhes de compilação e empacotamento.
+
+### 🔄 Propriedade do runtime e agendamento de quadros
+
+Cada processo possui uma instância de `BongoCatApp` e um loop de eventos/renderização no thread principal. Os listeners de plataforma param na fronteira de entrada:
+
+```text
+Listeners de plataforma (teclado/ponteiro)
+ |
+ v
+ Estado de entrada C11 (fila atômica de bordas + posição do ponteiro mesclada)
+ |
+ v
+ aplicação no thread principal <----- eventos SDL3
+ |
+ v
+ parâmetros do modelo, sobreposições e estado da interface
+ |
+ v
+ atualização do modelo -> composição OpenGL -> apresentação da plataforma
+```
+
+O receptor Raw Input do Windows, o tap de eventos Quartz do macOS e os listeners XInput2 do Linux são executados fora do loop principal. Eventos de teclas e botões entram em uma fila atômica limitada; os movimentos são agrupados separadamente e eventos SDL despertam a thread principal. O Windows recebe entradas em segundo plano por uma janela de mensagens com `RIDEV_INPUTSINK | RIDEV_DEVNOTIFY`, preservando as mensagens comuns. Quando outro aplicativo oculta ou bloqueia o cursor, o modelo usa os movimentos do dispositivo; o SDL fornece a posição do cursor na área de trabalho. Estados pressionados são limpos ao desconectar um dispositivo ou mudar a área de trabalho de entrada. O Windows não usa mais hooks de entrada nem DirectInput. Eventos de janela, preferências e gamepad do SDL3 são processados na thread principal. Nenhum listener de plataforma chama diretamente o código de Live2D, sobreposições ou interface.
+
+`bongo_cat_app_run` trata dos parâmetros de atualização, encerramento e processos secundários, impõe a propriedade de instância única do processo principal, aloca o estado do aplicativo, executa a inicialização, entra em `bongo_cat_app_loop` e, em seguida, atualiza o estado e destrói os recursos na ordem definida. A inicialização carrega a configuração e os caminhos de armazenamento, localiza os recursos, cria a janela de mascote SDL/OpenGL, inicializa os backends de plataforma, cria os serviços de Live2D/sobreposições/áudio, verifica fontes de modelos integradas/instaladas/próximas e carrega os modelos disponíveis. `BongoCatApp` mantém as configurações, o estado da sessão, os catálogos de modelos e comportamentos, os identificadores de plataforma e os identificadores de serviços de runtime.
+
+Os pacotes de modelos instalados usam Mver como formato canônico. O processo de importação analisa o arquivo ou diretório selecionado, descobre e valida candidatos, gera uma impressão digital de identidade do pacote, converte fontes Tauri em Mver, aplica patches de imagem e envia o pacote normalizado para `models_root`. Em seguida, os adaptadores de runtime são gerados e o catálogo é atualizado. Fontes próximas são descobertas apenas sem instalar a árvore de origem; seus adaptadores e resultados de verificação são armazenados em cache sob `cache_root`, fora de `models_root`.
+
+Cada iteração do loop principal espera ativações SDL/nativas ou o vencimento mais cedo dos prazos de quadro, interface, animação e acerto do ponteiro (máximo de 250 ms). O loop distribui os eventos SDL enfileirados, esvazia a fila de entrada atômica e executa a recuperação de liberação, atualiza o estado da janela e da atualização do modelo e, em seguida, aplica os parâmetros de entrada. Com o Cubism habilitado, o prazo do modelo segue `settings.model.max_fps` (padrão de 60 FPS); builds de diagnóstico usam um intervalo de reserva de 100 ms. O tempo decorrido do modelo é contado no máximo como 250 ms e dividido em até oito subetapas, cada uma com o objetivo de não mais que 1/30 de segundo.
+
+O caminho normal da mascote é renderizado apenas quando a janela está visível, não minimizada e marcada como suja. Cada quadro primeiro limpa o fundo, desenha o modelo e então compõe as sobreposições de ponteiro, teclas e efeitos, e finalmente chama o apresentador da plataforma. As operações de visualização podem solicitar renderização imediata; a renderização de capturas de tela pode pular a apresentação. macOS e Linux trocam diretamente a janela SDL OpenGL; o Windows a troca diretamente quando a apresentação em camadas não está habilitada; caso contrário, lê o buffer de quadros e chama `UpdateLayeredWindow`. A interface de preferências tem sua própria janela SDL/OpenGL e é renderizada e apresentada separadamente.
+
+O runtime C chama a ABI declarada em `include/bongo_cat/model.h`. A ponte Live2D e a implementação do Cubism estão em `src/live2d` e usam C++17 apenas quando o SDK do Cubism está habilitado; o restante do runtime nativo usa C11. Os tipos do Cubism são mantidos atrás de identificadores C opacos; quando o SDK não está disponível, `src/live2d/live2d_stub.c` fornece o backend de diagnóstico.
+
+```mermaid
+flowchart TB
+ Input(["Teclado / Mouse / Gamepad"])
+ BuiltIn(["Recursos de modelos integrados"])
+ Sources(["Fontes de modelos externos
Mver, Tauri, .model3.json, patches de imagem"])
+ Desktop(["Janela de mascote e janela de preferências"])
+ subgraph Runtime["Runtime nativo do BongoCat"]
+ direction TB
+ Entry["src/main.c
bongo_cat_app_run"]
+ Startup["Inicialização e inicialização
Configuração, armazenamento, janela, plataforma"]
+ Loop["Loop principal SDL3
Aguardar, distribuir, atualizar, renderizar"]
+ Shutdown["Encerramento
Atualizar estado, parar serviços, liberar recursos"]
+ InputQueue[("Estado de entrada atômico
Fila de bordas e posição do ponteiro mesclada")]
+ InputDispatch["Distribuição de entrada
Atalhos, mapeamento do ponteiro, parâmetros do modelo"]
+ State[("Estado do BongoCatApp
Configurações, sessão, catálogo, identificadores de runtime")]
+ Import["Descoberta e importação de modelos
Validação, normalização para Mver, instalação/cache"]
+ Catalog[("Catálogo de modelos e comportamentos")]
+ Live2D["Live2D C ABI
SDK do Cubism ou stub de diagnóstico"]
+ Overlay["Sobreposições e áudio"]
+ Preferences["Preferências e shell de desktop
UI Nuklear, bandeja, ações de janela"]
+ Compose["Composição de quadros OpenGL"]
+ Present["Apresentação da plataforma"]
+ Entry --> Startup --> Loop
+ Loop --> Shutdown
+ Loop --> InputDispatch --> State
+ Loop <--> State
+ State --> Live2D
+ State --> Overlay
+ State <--> Preferences
+ Loop --> Preferences
+ Catalog --> State
+ State --> Compose
+ Live2D --> Compose
+ Overlay --> Compose
+ Compose --> Present
+ Loop --> Compose
+ end
+ subgraph Platform["Backends de plataforma"]
+ direction LR
+ Global["Captura global de teclado/ponteiro
Windows, macOS, Linux"]
+ SDL["Eventos SDL3
Eventos de janela e gamepad"]
+ end
+ Input --> Global --> InputQueue --> InputDispatch
+ Input --> SDL --> Loop
+ BuiltIn --> Catalog
+ Sources --> Import --> Catalog
+ Present --> Desktop
+ Preferences --> Desktop
+```
+
+## ❓ Perguntas frequentes
+
+### 🔒 O BongoCat registra minhas entradas de teclado ou mouse?
+
+Não. O BongoCat processa entradas de teclado e mouse localmente para impulsionar animações e atalhos. Ele não registra nem envia teclas pressionadas, ações do mouse ou outros dados de interação. A configuração também é salva apenas localmente; o aplicativo não contém anúncios, ferramentas de análise nem código de rastreamento de usuários. Ao verificar atualizações, apenas metadados públicos de versão são solicitados; nenhum dado de entrada, configuração ou uso é enviado.
+
+### 🖼️ Por que OpenGL em vez de Vulkan?
+
+Não porque Vulkan seja ruim, mas porque o BongoCat não precisa desse nível de complexidade. O aplicativo renderiza principalmente um modelo Live2D, algumas camadas de interface e uma janela de desktop transparente; o OpenGL dá conta disso facilmente e se integra naturalmente com o SDL3 e o renderizador OpenGL do Cubism. Migrar para Vulkan exigiria manter mais código de renderização e sincronização em três plataformas de desktop, sem benefício perceptível para o usuário. Para a carga de trabalho atual do BongoCat, o OpenGL torna o renderizador mais enxuto, mais fácil de depurar e manter, continuando a oferecer o desempenho necessário.
+
+## 🙏 Agradecimentos especiais
+> [!TIP]
+> Cada passo do BongoCat é movido pelo espírito do código aberto. Agradecemos sinceramente a todos os colaboradores da comunidade por suas contribuições altruístas (listadas abaixo em ordem cronológica de contribuição). É o apoio de vocês que torna a companhia no desktop mais livre e genuína.❤️🔥
+
+
+
+
+
+
+
+[linux.do](https://linux.do/t/topic/2845597)
+---
+
+
+Copyright © 2026 - **BongoCat**
+By vladelaina
+Made with ❤️ & ⌨️
+
diff --git a/docs/README.ru-RU.md b/docs/README.ru-RU.md
new file mode 100644
index 00000000..8509e64d
--- /dev/null
+++ b/docs/README.ru-RU.md
@@ -0,0 +1,252 @@
+
+
+💘 C/C++ × SDL3 × OpenGL — смешай всё и барабань в своё удовольствие! Бонг~ Bongo Cat!!!
+
+ Выберите язык ❯ English • 简体中文 • 繁體中文 • Français • Deutsch • 日本語 • 한국어 • Português • Русский • Español • Bahasa Indonesia
+
+
+
+
+
+
+
+
+
+
+
+> [!TIP]
+> Модель, использованная в демо, принадлежит [宇痕冫](https://space.bilibili.com/348616056).
+>
+> 🎁 Ищете **бесплатные** модели? Мы сотрудничаем с талантливыми создателями моделей, чтобы предложить вам широкий выбор бесплатных моделей, и постоянно исследуем новые интересные возможности для рабочего стола! Загляните на наш официальный сайт: [bongocat.pet](https://bongocat.pet/models)
+
+
+
+
+
+
+
+
+
+## 📥 Загрузка
+
+
+
+- GitHub Releases
+
+ Скачайте последнюю версию из [GitHub Releases](https://github.com/vladelaina/BongoCat/releases/latest).
+
+## 🛠️ Сборка из исходного кода
+
+BongoCat использует CMake и требует компилятор C11, компилятор C++17, CMake 3.24 или новее, а также файлы разработки OpenGL для настольных систем. По умолчанию SDL3, yyjson, stb, miniaudio и Nuklear загружаются автоматически на этапе конфигурации, поэтому для первой настройки требуется подключение к сети.
+
+Выполните следующие команды в корневом каталоге проекта (каталоге, содержащем `CMakeLists.txt`).
+
+### 📋 Предварительные требования по платформам
+
+- **Windows:** Visual Studio 2022 (с рабочей нагрузкой «Разработка классических приложений на C++») и CMake. Используйте генератор MSVC; MinGW может собрать диагностический бэкенд, но не поддерживает Cubism SDK.
+- **macOS:** Xcode Command Line Tools, CMake и Ninja. Если целевая архитектура отличается от архитектуры хоста по умолчанию, укажите её через `CMAKE_OSX_ARCHITECTURES`.
+- **Linux (Debian/Ubuntu):** GCC или Clang, Ninja, а также заголовочные файлы OpenGL/X11:
+
+ ```bash
+ sudo apt-get update
+ sudo apt-get install -y build-essential cmake ninja-build \
+ libgl1-mesa-dev libx11-dev libxi-dev libxfixes-dev libcurl4-openssl-dev
+ ```
+
+### 🔧 Настройка и сборка
+
+В Linux и macOS используйте генератор с одной конфигурацией, например Ninja:
+
+```bash
+cmake -S . -B build -G Ninja \
+ -DCMAKE_BUILD_TYPE=Release \
+ -DBONGO_CAT_FETCH_DEPS=ON
+cmake --build build --parallel
+```
+
+В Windows выполняйте команды из командной строки разработчика Visual Studio 2022 (или любой другой оболочки, где доступен MSVC):
+
+```powershell
+cmake -S . -B build -G "Visual Studio 17 2022" -A x64 `
+ -DBONGO_CAT_FETCH_DEPS=ON
+cmake --build build --config Release --parallel
+```
+
+Исполняемый файл находится в `build/BongoCat` в Linux, в `build/BongoCat.app/Contents/MacOS/BongoCat` в macOS и в `build/Release/BongoCat.exe` для сборок Visual Studio в Windows.
+
+### 🧪 Тестирование
+
+Цели CTest включены по умолчанию. После сборки выполните:
+
+```bash
+ctest --test-dir build --output-on-failure
+```
+
+Для генератора с несколькими конфигурациями, такого как Visual Studio, явно укажите конфигурацию сборки:
+
+```powershell
+ctest --test-dir build -C Release --output-on-failure
+```
+
+### 🎭 Live2D / Cubism SDK (необязательно)
+
+Если Cubism SDK не найден, CMake выдаст предупреждение и соберёт диагностический бэкенд. Этот бэкенд предназначен для запуска и диагностики платформы; он не обеспечивает рендеринг моделей Live2D. Чтобы собрать полный рантайм, установите совместимую версию Cubism SDK for Native, поместите её в `vendor/CubismSdkForNative` или явно укажите путь:
+
+```bash
+cmake -S . -B build -G Ninja \
+ -DCMAKE_BUILD_TYPE=Release \
+ -DBONGO_CAT_CUBISM_SDK=/path/to/CubismSdkForNative \
+ -DBONGO_CAT_REQUIRE_CUBISM=ON
+```
+
+SDK должен включать библиотеку Core, исходный код Framework и каталог сторонних библиотек OpenGL GLEW в структуре, ожидаемой `cmake/Cubism.cmake`. Сборки Cubism в Windows требуют Visual Studio 2022. При `BONGO_CAT_REQUIRE_CUBISM=ON` конфигурация завершится ошибкой, если SDK недоступен, а не будет молча использовать диагностический бэкенд.
+
+### ⚙️ Параметры CMake
+
+| Параметр | По умолчанию | Описание |
+| --- | --- | --- |
+| `BONGO_CAT_FETCH_DEPS` | `ON` | Загружает сторонние зависимости фиксированных версий через `FetchContent` CMake. Установите `OFF`, только если SDL3, yyjson, stb, miniaudio и Nuklear уже доступны для CMake. |
+| `BONGO_CAT_CUBISM_SDK` | `vendor/CubismSdkForNative` | Путь к Cubism SDK for Native. |
+| `BONGO_CAT_REQUIRE_CUBISM` | `OFF` | Завершает конфигурацию ошибкой, если SDK недоступен. |
+| `BONGO_CAT_WARNINGS_AS_ERRORS` | `OFF` | Обрабатывает предупреждения нативного компилятора как ошибки. |
+
+Для автономной сборки установите `BONGO_CAT_FETCH_DEPS=OFF` и предоставьте конфигурации пакетов CMake для SDL3 (включая `SDL3-static`) и yyjson; если stb, Nuklear и miniaudio не могут быть найдены автоматически, укажите также их каталоги включения:
+
+```bash
+cmake -S . -B build -G Ninja \
+ -DCMAKE_BUILD_TYPE=Release \
+ -DBONGO_CAT_FETCH_DEPS=OFF \
+ -DBONGO_CAT_STB_INCLUDE_DIR=/path/to/stb \
+ -DBONGO_CAT_NUKLEAR_INCLUDE_DIR=/path/to/nuklear \
+ -DBONGO_CAT_MINIAUDIO_INCLUDE_DIR=/path/to/miniaudio
+```
+
+## 📌 Статус проекта
+
+
+
+## 📜 Лицензия
+
+Исходный код и нативный рантайм BongoCat распространяются под лицензией [AGPL-3.0-only](../LICENSE).
+
+Встроенный по умолчанию режим модели (`standard`) остаётся под лицензией MIT. Ресурсы моделей, входящие в `resources/assets/models/standard`, `keyboard` и `gamepad`, покрываются отдельным [объявлением о лицензии MIT](../LICENSE-MIT). Эта лицензия MIT распространяется только на ресурсы моделей и сопутствующие художественные материалы; она не меняет лицензию исходного кода или нативного рантайма BongoCat.
+
+## 🧭 Техническая архитектура
+
+Текущая нативная версия построена на C/C++, SDL3 и OpenGL. Диаграмма ниже акцентирует поток данных во время выполнения; подробности сборки и упаковки см. в файлах CMake.
+
+### 🔄 Владение рантаймом и планирование кадров
+
+Каждый процесс владеет экземпляром `BongoCatApp` и циклом событий/рендеринга в главном потоке. Платформенные слушатели останавливаются на границе ввода:
+
+```text
+Платформенные слушатели (клавиатура/указатель)
+ |
+ v
+ Состояние ввода C11 (атомарная очередь фронтов + объединённая позиция указателя)
+ |
+ v
+ приложение в главном потоке <----- события SDL3
+ |
+ v
+ параметры модели, оверлеи и состояние интерфейса
+ |
+ v
+ обновление модели -> композиция OpenGL -> представление на платформе
+```
+
+Приёмник Raw Input в Windows, обработчик событий Quartz в macOS и слушатели XInput2 в Linux работают вне главного цикла. События клавиш и кнопок поступают в ограниченную атомарную очередь; перемещения объединяются отдельно, а события SDL пробуждают главный поток. Windows получает фоновый ввод через окно сообщений с `RIDEV_INPUTSINK | RIDEV_DEVNOTIFY`, сохраняя обычные оконные сообщения. Когда другое приложение скрывает или блокирует курсор, модель использует перемещения устройства; SDL предоставляет положение курсора на рабочем столе. При отключении устройства или смене рабочего стола ввода состояния нажатий очищаются. Windows больше не использует перехватчики ввода или DirectInput. События окна, настроек и геймпада SDL3 обрабатываются в главном потоке. Платформенные слушатели не вызывают напрямую код Live2D, оверлеев или интерфейса.
+
+`bongo_cat_app_run` обрабатывает параметры обновления, завершения и вторичных процессов, обеспечивает владение единственным экземпляром главного процесса, выделяет состояние приложения, выполняет инициализацию, входит в `bongo_cat_app_loop`, а затем обновляет состояние и уничтожает ресурсы в определённом порядке. Инициализация загружает конфигурацию и пути хранения, находит ресурсы, создаёт окно питомца SDL/OpenGL, инициализирует платформенные бэкенды, создаёт сервисы Live2D/оверлеев/аудио, сканирует встроенные/установленные/близлежащие источники моделей и загружает доступные модели. `BongoCatApp` хранит настройки, состояние сеанса, каталоги моделей и поведения, дескрипторы платформы и дескрипторы сервисов рантайма.
+
+Установленные пакеты моделей используют Mver в качестве канонического формата. Процесс импорта анализирует выбранный файл или каталог, обнаруживает и проверяет кандидатов, формирует отпечаток идентичности пакета, преобразует источники Tauri в Mver, применяет патчи изображений и отправляет нормализованный пакет в `models_root`. Затем создаются адаптеры рантайма и обновляется каталог. Близлежащие источники обнаруживаются без установки их дерева исходников; их адаптеры и результаты проверок кэшируются в `cache_root` за пределами `models_root`.
+
+Каждая итерация главного цикла ожидает пробуждения SDL/нативных событий или самого раннего срока кадра, интерфейса, анимации и попадания указателя (максимум 250 мс). Цикл распределяет поставленные в очередь события SDL, очищает атомарную очередь ввода и выполняет восстановление освобождений, обновляет состояние окна и обновления модели, а затем применяет входные параметры. При включённом Cubism срок модели следует `settings.model.max_fps` (по умолчанию 60 FPS); диагностические сборки используют резервный интервал 100 мс. Прошедшее время модели учитывается максимум 250 мс и делится на до восьми подшагов, каждый из которых не превышает 1/30 секунды.
+
+Обычный путь питомца рендерится только тогда, когда окно видимо, не свёрнуто и помечено как изменённое. Каждый кадр сначала очищает фон, рисует модель, затем компонует оверлеи указателя, клавиш и эффектов и, наконец, вызывает презентер платформы. Операции предпросмотра могут запросить немедленный рендеринг; рендеринг скриншотов может пропустить представление. macOS и Linux напрямую обменивают окно SDL OpenGL; Windows обменивает его напрямую, если многослойное представление не включено, в противном случае считывает кадровый буфер и вызывает `UpdateLayeredWindow`. Интерфейс настроек имеет собственное окно SDL/OpenGL и рендерится и представляется отдельно.
+
+Среда выполнения C вызывает ABI, объявленный в `include/bongo_cat/model.h`. Мост Live2D и реализация Cubism находятся в `src/live2d` и используют C++17 только при включённом Cubism SDK; остальной нативный рантайм использует C11. Типы Cubism остаются за непрозрачными дескрипторами C; когда SDK недоступен, `src/live2d/live2d_stub.c` предоставляет диагностический бэкенд.
+
+```mermaid
+flowchart TB
+ Input(["Клавиатура / Мышь / Геймпад"])
+ BuiltIn(["Встроенные ресурсы моделей"])
+ Sources(["Внешние источники моделей
Mver, Tauri, .model3.json, патчи изображений"])
+ Desktop(["Окно питомца и окно настроек"])
+ subgraph Runtime["Нативный рантайм BongoCat"]
+ direction TB
+ Entry["src/main.c
bongo_cat_app_run"]
+ Startup["Запуск и инициализация
Конфигурация, хранилище, окно, платформа"]
+ Loop["Главный цикл SDL3
Ожидание, распределение, обновление, рендеринг"]
+ Shutdown["Завершение
Обновить состояние, остановить сервисы, освободить ресурсы"]
+ InputQueue[("Атомарное состояние ввода
Очередь фронтов и объединённая позиция указателя")]
+ InputDispatch["Распределение ввода
Сочетания клавиш, сопоставление указателя, параметры модели"]
+ State[("Состояние BongoCatApp
Настройки, сеанс, каталог, дескрипторы рантайма")]
+ Import["Обнаружение и импорт моделей
Проверка, нормализация в Mver, установка/кэш"]
+ Catalog[("Каталог моделей и поведений")]
+ Live2D["Live2D C ABI
Cubism SDK или диагностический заглушка"]
+ Overlay["Оверлеи и аудио"]
+ Preferences["Настройки и оболочка рабочего стола
UI Nuklear, трей, операции с окном"]
+ Compose["Композиция кадров OpenGL"]
+ Present["Представление на платформе"]
+ Entry --> Startup --> Loop
+ Loop --> Shutdown
+ Loop --> InputDispatch --> State
+ Loop <--> State
+ State --> Live2D
+ State --> Overlay
+ State <--> Preferences
+ Loop --> Preferences
+ Catalog --> State
+ State --> Compose
+ Live2D --> Compose
+ Overlay --> Compose
+ Compose --> Present
+ Loop --> Compose
+ end
+ subgraph Platform["Платформенные бэкенды"]
+ direction LR
+ Global["Глобальный захват клавиатуры/указателя
Windows, macOS, Linux"]
+ SDL["События SDL3
События окна и геймпада"]
+ end
+ Input --> Global --> InputQueue --> InputDispatch
+ Input --> SDL --> Loop
+ BuiltIn --> Catalog
+ Sources --> Import --> Catalog
+ Present --> Desktop
+ Preferences --> Desktop
+```
+
+## ❓ Часто задаваемые вопросы
+
+### 🔒 Записывает ли BongoCat мой ввод с клавиатуры или мыши?
+
+Нет. BongoCat обрабатывает ввод с клавиатуры и мыши локально для управления анимациями и сочетаниями клавиш. Он не записывает и не загружает нажатия клавиш, действия мыши или другие данные взаимодействия. Конфигурация также сохраняется только локально; приложение не содержит рекламы, аналитических инструментов или кода отслеживания пользователей. При проверке обновлений запрашиваются только публичные метаданные версии; никакие данные ввода, конфигурации или использования не отправляются.
+
+### 🖼️ Почему OpenGL, а не Vulkan?
+
+Не потому что Vulkan плох, а потому что BongoCat не нуждается в такой сложности. Приложение в основном рендерит одну модель Live2D, несколько слоёв интерфейса и прозрачное окно рабочего стола; OpenGL легко справляется с этим и естественно сочетается с SDL3 и рендерером OpenGL от Cubism. Переход на Vulkan потребовал бы поддержки большего объёма кода рендеринга и синхронизации на трёх настольных платформах без заметного преимущества для пользователя. Для текущей нагрузки BongoCat OpenGL делает рендерер более компактным, лёгким в отладке и обслуживании, сохраняя при этом необходимую производительность.
+
+## 🙏 Особая благодарность
+> [!TIP]
+> Каждый шаг BongoCat вдохновлён духом открытого исходного кода. Мы искренне благодарим всех участников сообщества за их бескорыстный вклад (перечислен ниже в хронологическом порядке). Именно ваша поддержка делает настольного спутника более свободным и искренним.❤️🔥
+
+
+
+
+
+
+
+[linux.do](https://linux.do/t/topic/2845597)
+---
+
+
+Авторские права © 2026 - **BongoCat**
+By vladelaina
+Made with ❤️ & ⌨️
+
diff --git a/docs/README.zh-CN.md b/docs/README.zh-CN.md
new file mode 100644
index 00000000..25e7e535
--- /dev/null
+++ b/docs/README.zh-CN.md
@@ -0,0 +1,273 @@
+
+
+💘 C/C++ × SDL3 × OpenGL,搅拌在一起,尽情敲击!Bong~ Bongo Cat!!!
+
+ 支持的操作系统:Windows、macOS、Linux
+
+
+ English • 简体中文 • 繁體中文 • Français • Deutsch • 日本語 • 한국어 • Português • Русский • Español • Bahasa Indonesia
+
+
+
+
+
+
+
+
+
+
+
+> [!NOTE]
+> 演示中使用的模型来自 [宇痕冫](https://space.bilibili.com/348616056)。
+
+>[!TIP]
+> 想找**免费**模型?我们与才华横溢的模型创作者们合作,为您带来丰富多样的免费模型,持续探索更多有趣的桌面体验!请访问我们的官方网站:[bongocat.pet](https://bongocat.pet/models)
+
+
+
+
+
+
+
+
+
+## 📥 下载
+- ### Microsoft Store
+
+
+- ### GitHub Releases
+
+ 从 [GitHub Releases](https://github.com/vladelaina/BongoCat/releases/latest) 下载最新版本。
+
+## 🛠️ 从源码构建
+
+BongoCat 使用 CMake,需要 C11 编译器、C++17 编译器、CMake 3.24 或更高版本,以及桌面 OpenGL 开发文件。默认情况下,SDL3、yyjson、stb、miniaudio 和 Nuklear 会在配置阶段自动下载,因此首次配置需要网络连接。
+
+请在项目根目录(包含 `CMakeLists.txt` 的目录)运行以下命令。
+
+### 平台前置条件
+
+- **Windows:** Visual Studio 2022(安装“使用 C++ 的桌面开发”工作负载)和 CMake。请使用 MSVC 生成器;MinGW 可构建诊断后端,但不支持 Cubism SDK。
+- **macOS:** Xcode Command Line Tools、CMake 和 Ninja。如果目标架构与主机默认架构不同,请通过 `CMAKE_OSX_ARCHITECTURES` 指定。
+- **Linux(Debian/Ubuntu):** GCC 或 Clang、Ninja,以及 OpenGL/X11 头文件:
+
+ ```bash
+ sudo apt-get update
+ sudo apt-get install -y build-essential cmake ninja-build \
+ libgl1-mesa-dev libx11-dev libxi-dev libxfixes-dev libcurl4-openssl-dev
+ ```
+
+### 配置与构建
+
+- **在 Linux 和 macOS 上**,请使用类似 Ninja 这样的单配置生成器运行:
+
+ ```bash
+ cmake -S . -B build -G Ninja \
+ -DCMAKE_BUILD_TYPE=Release \
+ -DBONGO_CAT_FETCH_DEPS=ON
+ cmake --build build --parallel
+ ```
+
+- **在 Windows 上**,请从 Visual Studio 2022 开发者命令行(或 MSVC 可用的其他命令行)运行:
+
+ ```powershell
+ cmake -S . -B build -G "Visual Studio 17 2022" -A x64 `
+ -DBONGO_CAT_FETCH_DEPS=ON
+ cmake --build build --config Release --parallel
+ ```
+> [!NOTE]
+> 可执行文件位置:
+>
+> - Linux:`build/BongoCat`
+> - macOS:`build/BongoCat.app/Contents/MacOS/BongoCat`
+> - Windows(Visual Studio 构建):`build/Release/BongoCat.exe`
+
+### 测试
+
+CTest 目标默认启用。构建后运行:
+
+```bash
+ctest --test-dir build --output-on-failure
+```
+
+对于 Visual Studio 这类多配置生成器,请明确指定构建配置:
+
+```powershell
+ctest --test-dir build -C Release --output-on-failure
+```
+
+### Live2D / Cubism SDK(可选)
+
+如果未找到 Cubism SDK,CMake 会发出警告并构建诊断后端。该后端用于启动和平台诊断,不提供 Live2D 模型渲染。要构建完整运行时,请安装兼容的 Cubism SDK for Native,将其放置在 `vendor/CubismSdkForNative`,或显式传入路径:
+
+```bash
+cmake -S . -B build -G Ninja \
+ -DCMAKE_BUILD_TYPE=Release \
+ -DBONGO_CAT_CUBISM_SDK=/path/to/CubismSdkForNative \
+ -DBONGO_CAT_REQUIRE_CUBISM=ON
+```
+
+SDK 必须包含 Core 库、Framework 源码,以及 `cmake/Cubism.cmake` 所要求布局中的 OpenGL GLEW 第三方目录。Windows Cubism 构建需要 Visual Studio 2022。`BONGO_CAT_REQUIRE_CUBISM=ON` 会在 SDK 不可用时使配置失败,而不是静默选择诊断后端。
+
+### CMake 选项
+
+| 选项 | 默认值 | 说明 |
+| --- | --- | --- |
+| `BONGO_CAT_FETCH_DEPS` | `ON` | 使用 CMake `FetchContent` 下载固定版本的第三方依赖。仅当 SDL3、yyjson、stb、miniaudio 和 Nuklear 已可供 CMake 使用时才设为 `OFF`。 |
+| `BONGO_CAT_CUBISM_SDK` | `vendor/CubismSdkForNative` | Cubism SDK for Native 的路径。 |
+| `BONGO_CAT_REQUIRE_CUBISM` | `OFF` | SDK 不可用时使配置失败。 |
+| `BONGO_CAT_WARNINGS_AS_ERRORS` | `OFF` | 将本地编译器警告视为错误。 |
+
+离线构建时将 `BONGO_CAT_FETCH_DEPS=OFF`,并提供 SDL3(包括 `SDL3-static`)和 yyjson 的 CMake 包配置;如果 stb、Nuklear 和 miniaudio 无法自动发现,还需提供其包含目录:
+
+```bash
+cmake -S . -B build -G Ninja \
+ -DCMAKE_BUILD_TYPE=Release \
+ -DBONGO_CAT_FETCH_DEPS=OFF \
+ -DBONGO_CAT_STB_INCLUDE_DIR=/path/to/stb \
+ -DBONGO_CAT_NUKLEAR_INCLUDE_DIR=/path/to/nuklear \
+ -DBONGO_CAT_MINIAUDIO_INCLUDE_DIR=/path/to/miniaudio
+```
+
+## 📌 项目状态
+
+
+
+## 📜 许可证
+
+BongoCat 源代码和本地运行时采用 [AGPL-3.0-only](../LICENSE) 许可证。
+
+默认内置模型模式(`standard`)仍采用 MIT 许可证。`resources/assets/models/standard`、`keyboard` 和 `gamepad` 中捆绑的模型资源受单独的 [MIT 许可证声明](../LICENSE-MIT) 覆盖。该 MIT 许可证仅适用于模型资源及其配套美术作品,不会改变 BongoCat 源代码或本地运行时的许可证。
+
+## 🧭 技术架构
+
+当前原生版本基于 C/C++、SDL3 和 OpenGL 构建。下图重点展示运行时数据流;构建与打包细节请参阅 CMake 文件。
+
+### 运行时所有权与帧调度
+
+每个进程拥有一个 `BongoCatApp` 和一个主线程事件/渲染循环。平台监听器在输入边界处停止:
+
+```mermaid
+flowchart TB
+ Listener["平台监听器(键盘/指针)"] --> InputState["C11 输入状态(原子边沿队列 + 合并后的指针位置)"]
+ InputState --> MainThread["主线程应用"]
+ SDLEvent["SDL3 事件"] --> MainThread
+ MainThread --> ModelState["模型参数、覆盖层和 UI 状态"]
+
+ subgraph Render["渲染阶段"]
+ direction LR
+ ModelUpdate["模型更新"] --> Compose["OpenGL 合成"] --> Present["平台呈现"]
+ end
+
+ ModelState --> Render
+```
+
+Windows Raw Input 接收器、macOS Quartz 事件 tap 和 Linux XInput2 监听器运行在主循环之外。按键和鼠标按钮边沿进入有界原子队列,移动量单独合并,并通过 SDL 事件唤醒主线程,避免高频移动挤出按键事件。Windows 使用独立消息窗口,以 `RIDEV_INPUTSINK | RIDEV_DEVNOTIFY` 订阅后台键鼠输入并保留普通窗口消息;其他程序隐藏或锁定光标时,模型使用设备上报的移动量,桌面跟随则读取 SDL 提供的系统光标位置。接收器分别管理各设备的按下状态,在设备拔出或输入桌面切换时清理。Windows 不再安装输入钩子或使用 DirectInput,也不向游戏发送输入。SDL3 窗口、偏好设置和手柄事件仍在主线程处理,平台监听器不会直接调用 Live2D、覆盖层或 UI 代码。
+
+`bongo_cat_app_run` 负责更新、关闭和次级进程参数,确保主进程的单实例所有权,分配应用状态,执行初始化,进入 `bongo_cat_app_loop`,随后按定义顺序刷新状态并销毁资源。初始化会加载配置和存储路径、定位资源、创建 SDL/OpenGL 宠物窗口、初始化平台后端、创建 Live2D/覆盖层/音频服务、扫描内置/已安装/附近的模型来源,并加载可用模型。`BongoCatApp` 持有设置、会话状态、模型和行为目录、平台句柄及运行时服务句柄。
+
+已安装的模型包使用 Mver 作为规范格式。导入流程会解析选中的文件或目录,发现并验证候选项,生成包身份指纹,将 Tauri 来源转换为 Mver,应用图像补丁,并将规范化包提交到 `models_root`。随后生成运行时适配器并刷新目录。附近来源只在不安装源目录的情况下被发现;其适配器和检查结果缓存在 `models_root` 之外的 `cache_root` 下。
+
+每次主循环迭代都会等待 SDL/原生唤醒,或等待最早的帧、UI、动画和指针命中截止时间(最长 250 毫秒)。循环分发排队的 SDL 事件,清空原子输入队列并执行释放恢复,更新窗口和模型刷新状态,然后应用输入参数。启用 Cubism 时,模型截止时间遵循 `settings.model.max_fps`(默认为 60 FPS);诊断构建使用 100 毫秒的后备间隔。模型经过的时间最多计为 250 毫秒,并拆分为最多八个子步,每个子步目标不超过 1/30 秒。
+
+常规宠物路径仅在窗口可见、未最小化且标记为脏时渲染。每帧先清空背景,绘制模型,再合成指针、按键和效果覆盖层,最后调用平台呈现器。预览操作可以请求立即渲染,截图渲染可以跳过呈现。macOS 和 Linux 直接交换 SDL OpenGL 窗口;Windows 在未启用分层呈现时直接交换,否则读取帧缓冲并调用 `UpdateLayeredWindow`。偏好设置 UI 拥有独立的 SDL/OpenGL 窗口,并单独渲染和呈现。
+
+C 运行时调用 `include/bongo_cat/model.h` 中声明的 ABI。Live2D 桥接和 Cubism 实现在 `src/live2d` 中,仅在启用 Cubism SDK 时使用 C++17;其余本地运行时使用 C11。Cubism 类型保持在不透明 C 句柄之后;当 SDK 不可用时,`src/live2d/live2d_stub.c` 提供诊断后端。
+
+```mermaid
+flowchart TB
+ Input(["键盘 / 鼠标 / 手柄"])
+ BuiltIn(["内置模型资源"])
+ Sources(["外部模型来源
Mver、Tauri、.model3.json、图像补丁"])
+ Desktop(["宠物窗口和偏好设置窗口"])
+ subgraph Runtime["BongoCat 原生运行时"]
+ direction TB
+ Entry["src/main.c
bongo_cat_app_run"]
+ Startup["启动和初始化
配置、存储、窗口、平台"]
+ Loop["SDL3 主循环
等待、分发、更新、渲染"]
+ Shutdown["关闭
刷新状态、停止服务、释放资源"]
+ InputQueue[("原子输入状态
边沿队列和合并指针位置")]
+ InputDispatch["输入分发
快捷键、指针映射、模型参数"]
+ State[("BongoCatApp 状态
设置、会话、目录、运行时句柄")]
+ Import["模型发现和导入
验证、规范化为 Mver、安装/缓存"]
+ Catalog[("模型和行为目录")]
+ Live2D["Live2D C ABI
Cubism SDK 或诊断存根"]
+ Overlay["覆盖层和音频"]
+ Preferences["偏好设置和桌面外壳
Nuklear UI、托盘、窗口操作"]
+ Compose["OpenGL 帧合成"]
+ Present["平台呈现"]
+ Entry --> Startup --> Loop
+ Loop --> Shutdown
+ Loop --> InputDispatch --> State
+ Loop <--> State
+ State --> Live2D
+ State --> Overlay
+ State <--> Preferences
+ Loop --> Preferences
+ Catalog --> State
+ State --> Compose
+ Live2D --> Compose
+ Overlay --> Compose
+ Compose --> Present
+ Loop --> Compose
+ end
+ subgraph Platform["平台后端"]
+ direction LR
+ Global["全局键盘/指针捕获
Windows、macOS、Linux"]
+ SDL["SDL3 事件
窗口和手柄事件"]
+ end
+ Input --> Global --> InputQueue --> InputDispatch
+ Input --> SDL --> Loop
+ BuiltIn --> Catalog
+ Sources --> Import --> Catalog
+ Present --> Desktop
+ Preferences --> Desktop
+```
+
+## ❓ 常见问题
+
+### BongoCat 会记录我的键盘或鼠标输入吗?
+
+不会。BongoCat 仅在本地即时处理键盘和鼠标输入,用于渲染动画和触发快捷键。它不会记录或上传按键、鼠标操作或其他交互数据。配置也只保存在本地。BongoCat 不包含广告、分析工具或用户跟踪代码。执行更新检查时只会请求公开的版本元数据,不会发送输入、配置或使用数据。
+
+### Linux Wayland 输入
+
+X11 默认使用 XInput2。Wayland 的实验性 evdev 输入默认关闭;阅读
+[输入权限风险](../SECURITY.md#linux-input) 后,可以为单次启动显式启用:
+
+```sh
+BONGOCAT_ENABLE_EVDEV=1 ./build/BongoCat
+```
+
+这个开关不会授予设备权限。不要为此以 root 身份运行程序,或把账户加入
+`input` 组。原始输入可能包含密码框里的按键,监听不会在锁屏或切换会话时自动暂停。
+退出程序才会停止监听,隐藏宠物不会停止;不带该变量重新启动即可恢复默认后端。
+鼠标跟随使用未经加速的设备位移,窗口定位、点击穿透和置顶仍取决于 Wayland 合成器。
+
+### 为什么使用 OpenGL 而不是 Vulkan?
+
+不是因为 Vulkan 不好,而是 BongoCat 不需要那种程度的复杂性。应用主要渲染一个 Live2D 模型、少量 UI 图层和透明桌面窗口,OpenGL 已能轻松满足,并且能自然地与 SDL3 及 Cubism 的 OpenGL 渲染器配合。迁移到 Vulkan 将需要在三个桌面平台维护更多渲染和同步代码,却不会为用户带来明显提升。对于 BongoCat 当前的工作负载,OpenGL 让渲染器更精简、更易调试和维护,同时仍能提供所需性能。
+
+## 🙏 特别感谢
+> [!NOTE]
+> BongoCat 的每一步都得益于开源精神。我们衷心感谢所有社区贡献者的无私奉献(按贡献日期先后排序列于下方)。正是你们的支持,让桌面陪伴更加自由与真诚。❤️🔥
+
+
+
+
+
+
+
+[linux.do](https://linux.do/t/topic/2845597)
+---
+
+
+版权所有 © 2026 - **BongoCat**
+By vladelaina
+Made with ❤️ & ⌨️
+
diff --git a/docs/README.zh-Hant.md b/docs/README.zh-Hant.md
new file mode 100644
index 00000000..ca00aef1
--- /dev/null
+++ b/docs/README.zh-Hant.md
@@ -0,0 +1,288 @@
+
+
+
+
+
+ 💘C/C++ × SDL3 × OpenGL,攪拌均勻,搗碎融合!Bong~ Bongo Cat!!!
+
+
+English • 简体中文 • 繁體中文 • Français • Deutsch • 日本語 • 한국어 • Português • Русский • Español • Bahasa Indonesia
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+> [!TIP]
+> 本示範所使用的模型來自 [宇痕冫](https://space.bilibili.com/348616056)。
+>
+> 🎁 尋找**免費**模型?我們與才華洋溢的模型創作者合作,為您帶來豐富多樣的免費模型,同時持續探索更多有趣的桌面體驗!請瀏覽我們的官方網站:[bongocat.pet](https://bongocat.pet/models)
+
+
+
+
+
+
+
+
+
+
+
+
+## 📥 下載
+
+
+
+
+
+- GitHub Releases
+
+ 從 [GitHub Releases](https://github.com/vladelaina/BongoCat/releases/latest) 下載最新版本。
+
+## 🛠️ 從原始碼建置
+
+BongoCat 使用 CMake,需要 C11 編譯器、C++17 編譯器、CMake 3.24 或更新版本,以及桌面 OpenGL 開發檔案。SDL3、yyjson、stb、miniaudio 和 Nuklear 預設在建置配置時下載,因此首次配置需要網路連線。
+
+請在專案根目錄(包含 `CMakeLists.txt` 的目錄)執行以下命令。
+
+### 📋 平台先決條件
+
+- **Windows:** Visual Studio 2022 並包含「桌面 C++」工作負載和 CMake。請使用 MSVC 產生器;MinGW 可建置診斷後端,但不支援 Cubism SDK。
+- **macOS:** Xcode Command Line Tools、CMake 和 Ninja。若目標架構與主機預設不同,請以 `CMAKE_OSX_ARCHITECTURES` 指定。
+- **Linux(Debian/Ubuntu):** GCC 或 Clang、Ninja,以及 OpenGL/X11 標頭檔:
+
+ ```bash
+ sudo apt-get update
+ sudo apt-get install -y build-essential cmake ninja-build \
+ libgl1-mesa-dev libx11-dev libxi-dev libxfixes-dev libcurl4-openssl-dev
+ ```
+
+### 🔧 配置與建置
+
+在 Linux 和 macOS 上,使用單一配置產生器(如 Ninja):
+
+```bash
+cmake -S . -B build -G Ninja \
+ -DCMAKE_BUILD_TYPE=Release \
+ -DBONGO_CAT_FETCH_DEPS=ON
+cmake --build build --parallel
+```
+
+在 Windows 上,請從 Visual Studio 2022 開發人員命令提示字元(或任何可使用 MSVC 的 Shell)執行:
+
+```powershell
+cmake -S . -B build -G "Visual Studio 17 2022" -A x64 `
+ -DBONGO_CAT_FETCH_DEPS=ON
+cmake --build build --config Release --parallel
+```
+
+執行檔產出位置:
+- Linux:`build/BongoCat`
+- macOS:`build/BongoCat.app/Contents/MacOS/BongoCat`
+- Visual Studio 建置:`build/Release/BongoCat.exe`
+
+### 🧪 測試
+
+CTest 目標預設啟用。建置完成後執行:
+
+```bash
+ctest --test-dir build --output-on-failure
+```
+
+若使用多配置產生器(如 Visual Studio),請明確指定建置配置:
+
+```powershell
+ctest --test-dir build -C Release --output-on-failure
+```
+
+### 🎭 Live2D / Cubism SDK(選用)
+
+若未提供 Cubism SDK,CMake 會發出警告並建置診斷後端。此後端僅供啟動和平台診斷之用,不提供 Live2D 模型渲染。若要建置完整執行時期,請安裝相容的 Cubism SDK for Native,並將其放置在 `vendor/CubismSdkForNative`,或明確傳遞路徑:
+
+```bash
+cmake -S . -B build -G Ninja \
+ -DCMAKE_BUILD_TYPE=Release \
+ -DBONGO_CAT_CUBISM_SDK=/path/to/CubismSdkForNative \
+ -DBONGO_CAT_REQUIRE_CUBISM=ON
+```
+
+SDK 必須包含 Core 函式庫、Framework 原始碼,以及 `cmake/Cubism.cmake` 所預期的 OpenGL GLEW 第三方目錄結構。Windows 上的 Cubism 建置需要 Visual Studio 2022。設定 `BONGO_CAT_REQUIRE_CUBISM=ON` 會在無法使用 Cubism SDK 時使配置失敗,而非靜默改用診斷後端。
+
+### ⚙️ CMake 選項
+
+| 選項 | 預設值 | 說明 |
+| --- | --- | --- |
+| `BONGO_CAT_FETCH_DEPS` | `ON` | 使用 CMake `FetchContent` 下載固定的第三方依賴項。僅在 SDL3、yyjson、stb、miniaudio 和 Nuklear 已可被 CMake 找到時設為 `OFF`。 |
+| `BONGO_CAT_CUBISM_SDK` | `vendor/CubismSdkForNative` | Cubism SDK for Native 的路徑。 |
+| `BONGO_CAT_REQUIRE_CUBISM` | `OFF` | 當無法使用 Cubism SDK 時使配置失敗。 |
+| `BONGO_CAT_WARNINGS_AS_ERRORS` | `OFF` | 將原生編譯器警告視為錯誤。 |
+
+若要進行離線建置,設定 `BONGO_CAT_FETCH_DEPS=OFF`,並提供 SDL3(含 `SDL3-static`)和 yyjson 的 CMake 套件配置,以及 stb、Nuklear、miniaudio 的包含路徑(若無法自動找到):
+
+```bash
+cmake -S . -B build -G Ninja \
+ -DCMAKE_BUILD_TYPE=Release \
+ -DBONGO_CAT_FETCH_DEPS=OFF \
+ -DBONGO_CAT_STB_INCLUDE_DIR=/path/to/stb \
+ -DBONGO_CAT_NUKLEAR_INCLUDE_DIR=/path/to/nuklear \
+ -DBONGO_CAT_MINIAUDIO_INCLUDE_DIR=/path/to/miniaudio
+```
+
+## 📌 專案狀態
+
+
+## 📜 授權條款
+
+BongoCat 原始碼和原生執行時期採用 [AGPL-3.0-only](LICENSE) 授權。
+
+預設內建模型模式(`standard`)仍以 MIT 授權釋出。`resources/assets/models/standard`、`keyboard` 和 `gamepad` 中的模型資產,則受個別的 [MIT 授權聲明](LICENSE-MIT) 規範。該 MIT 授權僅適用於模型資產及其附屬藝術作品,不適用於 BongoCat 原始碼或原生執行時期。
+
+
+## 🧭 技術架構
+
+> 目前原生版本基於 C/C++、SDL3 和 OpenGL 建構。下圖著重於執行時期資料流;建置與打包細節請參閱 CMake。
+
+### 🔄 執行時期所有權與影格排程
+
+每個行程擁有一個 `BongoCatApp` 和一個主執行緒事件與渲染迴圈。平台監聽器僅作用於輸入邊界:
+
+```text
+平台監聽器
+(鍵盤/指標)
+ |
+ v
+ C11 輸入狀態
+(原子邊緣佇列 + 合併指標位置)
+ |
+ v
+ 主執行緒應用程式 <----- SDL3 事件
+ |
+ v
+ 模型參數、覆疊層與 UI 狀態
+ |
+ v
+ 模型更新 -> OpenGL 合成 -> 平台呈現
+```
+
+Windows Raw Input 接收器、macOS Quartz 事件監聽,以及 Linux XInput2 監聽器均執行於主迴圈之外。按鍵和滑鼠按鈕邊緣事件進入有界原子佇列,移動量獨立合併,並透過 SDL 事件喚醒主執行緒。Windows 使用訊息專用視窗,以 `RIDEV_INPUTSINK | RIDEV_DEVNOTIFY` 訂閱背景鍵鼠輸入並保留一般視窗訊息;其他程式隱藏或鎖定游標時,模型使用裝置回報的移動量,桌面跟隨則讀取 SDL 提供的系統游標位置。裝置拔除或輸入桌面切換時會清理按下狀態。Windows 不再安裝輸入鉤子或使用 DirectInput,也不向遊戲傳送輸入。SDL3 視窗、偏好設定和遊戲控制器事件仍在主執行緒處理,平台監聽器不會直接呼叫 Live2D、覆疊層或 UI 程式碼。
+
+`bongo_cat_app_run` 負責處理更新、關閉和輔助行程參數,為主要行程強制執行單一實例所有權,分配應用程式狀態,執行初始化,進入 `bongo_cat_app_loop`,然後依序清除狀態並釋放資源。初始化會載入配置和儲存路徑、定位資產、建立 SDL/OpenGL 寵物視窗、初始化平台後端、建立 Live2D、覆疊層和音訊服務、掃描內建/已安裝/鄰近模型來源,並載入可用的模型。`BongoCatApp` 擁有設定、工作階段狀態、模型與行為目錄、平台控制代碼,以及執行時期服務控制代碼。
+
+已安裝的模型套件使用 Mver 作為標準格式。匯入流程會解析選取的檔案或目錄、探索並驗證候選項目、指紋辨識套件識別、將 Tauri 來源轉換為 Mver、套用影像修補,並將標準化套件提交至 `models_root`,接著產生執行時期轉接器並重新整理目錄。鄰近來源則在不安裝其原始碼樹的情況下被探索;其轉接器和檢查結果會快取在 `models_root` 之外的 `cache_root` 下。
+
+每次主迴圈迭代會等待 SDL/原生喚醒事件,或最早待處理的影格、UI、動畫或指標點擊期限(最長等待 250 毫秒)。它會分派佇列中的 SDL 事件、清空原子輸入佇列與釋放復原、更新視窗與模型重新整理狀態,並套用輸入衍生參數。啟用 Cubism 時,模型期限遵循 `settings.model.max_fps`(預設 60 FPS);診斷版本則使用 100 毫秒的備用間隔。模型經過時間上限為 250 毫秒,並分割為最多八個子步驟,每個子步驟不超過 1/30 秒。
+
+一般寵物路徑僅在視窗可見、未最小化且標記為髒污時才進行渲染。每個影格會清除背景、繪製模型,並合成指標、按鍵與特效覆疊層,最後呼叫平台呈現器。預覽操作可要求立即渲染,而擷取渲染則可能略過呈現。macOS 和 Linux 直接交換 SDL OpenGL 視窗。Windows 在圖層呈現未啟用時直接交換,否則會讀回影格供 `UpdateLayeredWindow` 使用。偏好設定 UI 擁有獨立的 SDL/OpenGL 視窗,且獨立於寵物視窗進行渲染和呈現。
+
+C 執行時期會呼叫 `include/bongo_cat/model.h` 中宣告的 ABI。Live2D 橋接與 Cubism 實作位於 `src/live2d`,且僅在啟用 Cubism SDK 時使用 C++17;其餘原生執行時期則使用 C11。Cubism 類型保留在不透明的 C 控制代碼之後,而 `src/live2d/live2d_stub.c` 則在 SDK 不可用時提供診斷後端。
+
+
+```mermaid
+flowchart TB
+ Input(["鍵盤 / 滑鼠 / 遊戲控制器"])
+ BuiltIn(["內建模型資產"])
+ Sources(["外部模型來源
Mver、Tauri、.model3.json、影像修補"])
+ Desktop(["寵物視窗與偏好設定視窗"])
+
+ subgraph Runtime["BongoCat 原生執行時期"]
+ direction TB
+ Entry["src/main.c
bongo_cat_app_run"]
+ Startup["啟動與初始化
配置、儲存、視窗、平台"]
+ Loop["SDL3 主迴圈
等待、分派、更新、渲染"]
+ Shutdown["關閉
清除狀態、停止服務、釋放資源"]
+ InputQueue[("原子輸入狀態
邊緣佇列與合併指標位置")]
+ InputDispatch["輸入分派
快捷鍵、指標對應、模型參數"]
+ State[("BongoCatApp 狀態
設定、工作階段、目錄、執行時期控制代碼")]
+ Import["模型探索與匯入
驗證、正規化為 Mver、安裝/快取"]
+ Catalog[("模型與行為目錄")]
+ Live2D["Live2D C ABI
Cubism SDK 或診斷存根"]
+ Overlay["覆疊層與音訊"]
+ Preferences["偏好設定與桌面殼層
Nuklear UI、系統匣、視窗動作"]
+ Compose["OpenGL 影格合成"]
+ Present["平台呈現"]
+
+ Entry --> Startup --> Loop
+ Loop --> Shutdown
+ Loop --> InputDispatch --> State
+ Loop <--> State
+ State --> Live2D
+ State --> Overlay
+ State <--> Preferences
+ Loop --> Preferences
+ Catalog --> State
+ State --> Compose
+ Live2D --> Compose
+ Overlay --> Compose
+ Compose --> Present
+ Loop --> Compose
+ end
+
+ subgraph Platform["平台後端"]
+ direction LR
+ Global["全域鍵盤/指標擷取
Windows、macOS、Linux"]
+ SDL["SDL3 事件
視窗與遊戲控制器事件"]
+ end
+
+ Input --> Global --> InputQueue --> InputDispatch
+ Input --> SDL --> Loop
+ BuiltIn --> Catalog
+ Sources --> Import --> Catalog
+ Present --> Desktop
+ Preferences --> Desktop
+```
+
+## ❓ 常見問答
+
+### 🔒 BongoCat 會記錄我的鍵盤或滑鼠輸入嗎?
+
+不會。BongoCat 僅在本地處理鍵盤和滑鼠輸入,以驅動動畫和快捷鍵。它不會記錄或上傳您的按鍵、滑鼠動作或其他互動資料。配置也僅儲存在本地,且應用程式不含廣告、分析工具或使用者追蹤程式碼。執行更新檢查時,僅會請求公開的發行版本元資料;不會傳送輸入、配置或使用資料。
+
+### 🖼️ 為什麼選擇 OpenGL 而非 Vulkan?
+
+我們選擇 OpenGL 並非因為 Vulkan 不好,而是 BongoCat 不需要那樣的複雜度。本應用主要渲染一個 Live2D 模型、少數 UI 層,以及一個透明桌面視窗。OpenGL 已能輕鬆應付,且與 SDL3 和 Cubism 的 OpenGL 渲染器自然整合。轉向 Vulkan 意味著要在三個桌面平台上維護更多渲染和同步程式碼,但對使用者而言並不會有明顯改善。就 BongoCat 目前的工作負載而言,OpenGL 讓渲染器更小巧、更容易除錯和維護,同時仍能提供我們所需的效能。
+
+
+
+## 專案狀態
+
+
+
+## 🙏 特別感謝
+> [!TIP]
+> BongoCat 的每一步都得益於開源精神。我們衷心感謝所有社群貢獻者的無私奉獻(按貢獻日期先後排序列於下方)。正是你們的支持,讓桌面陪伴更加自由與真誠。❤️🔥
+
+
+
+
+
+
+
+[linux.do](https://linux.do/t/topic/2845597)
+---
+
+
+
+Copyright © 2026 - **BongoCat**\
+由 vladelaina 製作\
+以 ❤️ 和 ⌨️ 打造
+
+
diff --git a/docs/feature-audit.md b/docs/feature-audit.md
deleted file mode 100644
index e99ab028..00000000
--- a/docs/feature-audit.md
+++ /dev/null
@@ -1,103 +0,0 @@
-# Native feature audit
-
-This is the completion ledger for the independent native application. `PASS`
-requires direct automated or visual evidence. `PENDING` means the implementation
-exists but the required hardware or operating system has not been exercised.
-A diagnostic drawing never counts as a Live2D visual pass.
-
-## Current evidence
-
-- Windows Cubism Release build: `build-delivery-final/Release/BongoCat.exe` - PASS.
-- Native tests: `ctest --test-dir build-delivery-final -C Release` - PASS, 5/5.
-- Source policy: `check-lines` - PASS; the default is 300 lines and reviewed
- file-specific limits are declared in `cmake/CheckLines.cmake`.
-- Preferences matrix: `build-cubism/visual-audit-preferences/audit.csv` - PASS, 50/50.
-- Real Live2D matrix: `build-cubism/visual-audit-cubism/audit.csv` - PASS, 14/14.
-- Pointer/mouse-mirror matrix: `build-cubism/visual-audit-mouse/audit.csv` - PASS, 5/5.
-- Visible Cubism memory baseline: PENDING remeasurement on the current Release
- build; working set and private committed memory are reported separately.
-- Standalone smoke: 22 embedded shaders, `renderer=cubism-native`, GL error 0.
-- Release ZIP and SHA-256 sidecar generation - PASS.
-- Cubism Native SDK under `vendor/CubismSdkForNative` - PASS, SDK 5 r.5.
-- Tauri-parity pointer mapping and frame-rate-independent damping - PASS.
-
-## Models and rendering
-
-| Requirement | Status | Evidence or remaining proof |
-|---|---|---|
-| Three bundled model packages | PASS | Model tests validate MOC3, textures, settings, motions and expressions. |
-| Background and key overlays | PASS | Standard, keyboard and gamepad idle/input screenshots match their intended composition. |
-| Custom model import/remove | PASS | Valid import/remove smoke passes; malformed and unsafe manifests are rejected. |
-| Model selection | PASS | Model catalog tests plus three real-model startup screenshots. |
-| Real Cubism model rendering | PASS | All models report `renderer=cubism-native`; screenshots show the expected Tauri-parity composition. |
-| Masks, blending and drawable order | PASS | Cubism r.5 renders alpha, masks, face, paws and overlays without OpenGL errors. |
-| Motion playback and fades | PASS | Both bundled motions are accepted and differ from idle by 85.63% and 2.11%. |
-| Expression playback | PASS | Expression 0 is accepted and differs from the same-run idle baseline. |
-| Motion audio | PENDING | Audio loading is implemented; a manual audible check remains. |
-| Mirror visual result | PASS | Real-model mirror differs from idle by 41.95%. |
-
-## Input and behavior
-
-| Requirement | Status | Evidence or remaining proof |
-|---|---|---|
-| Keyboard left/right hands | PASS | Individual and simultaneous real frames differ from idle. |
-| Mouse left/right buttons | PASS | Runtime parameters pass and real frames differ from idle. |
-| Gamepad buttons and four axes | PASS | Runtime parameter assertions and real-frame differences pass. |
-| Global Windows keyboard input | PASS | Injected events reach the native hook and alter overlays. |
-| Global Windows mouse input | PASS | Injected down/up events reach the native input queue. |
-| Global Windows pointer movement | PASS | Low-level hook coordinates, smoothing and parameter mapping are covered. |
-| Pointer-driven Live2D tracking | PASS | Frames differ by 8.74%/9.18%; seven parameters and mirrored X/Z directions pass. |
-| Windows automatic key release | PASS | Core input tests cover scheduled releases. |
-| Gamepad hot plug | PENDING | SDL implementation exists; physical-device testing remains. |
-| Behavior shortcuts | PENDING | Parsing and dispatch exist; a configured global shortcut smoke remains. |
-| macOS input permission | PENDING | Native implementation exists; macOS manual verification remains. |
-
-## Window and application
-
-| Requirement | Status | Evidence or remaining proof |
-|---|---|---|
-| Transparent borderless main window | PASS | Win32 caption, frame, menu and caption buttons are removed. |
-| Preferences window | PASS | 50 screenshots contain no blank frames. |
-| Click-through and always-on-top | PASS | Native style flags pass the window smoke. |
-| Hide on hover | PASS | Enter, hide, pass-through, leave and restore are asserted. |
-| Keep inside monitor | PASS | Off-screen placement is clamped in the geometry smoke. |
-| Menu scale and opacity | PASS | Native size and opacity values are asserted. |
-| Shift plus right-drag resize | PASS | Synthetic gesture verifies scale, size and release. |
-| Capture window presence | PENDING | The pet is hidden from the taskbar/Dock while remaining eligible for OBS window capture; Windows is implemented and macOS/Linux need platform exercise. |
-| System tray | PASS | Native SDL tray handle and state callbacks are asserted. |
-| Right-click context menu | PASS | Production Win32 popup opens Preferences. |
-| Single instance | PASS | A second process exits while the first remains alive. |
-| Autostart | PENDING | Platform implementations exist; OS login testing remains. |
-| Hidden rendering suspension | PASS | Main loop skips model updates while hidden. |
-
-## Preferences, localization and data
-
-| Requirement | Status | Evidence or remaining proof |
-|---|---|---|
-| Cat, general, model, shortcuts and about pages | PASS | Current 50-view matrix. |
-| Model card grid and direct-drawn icons | PASS | Responsive cards are present in both themes. |
-| Simplified/Traditional Chinese, English, French, German, Japanese, Korean, Portuguese, Russian and Spanish | PASS | All ten locales pass shape, glyph, persistence and settings-window smoke checks. |
-| Light and dark themes | PASS | Both themes are covered for every page and language. |
-| System theme | PASS | Auto mode resolves through the native SDL system theme. |
-| Click and drag model import | PASS | Both paths use the validated shared importer. |
-| Atomic configuration persistence | PASS | Tests include truncated-write recovery. |
-| No Electron or Tauri dependency | PASS | Native source/config scan is clean. |
-
-## Performance and platform gates
-
-| Requirement | Status | Evidence or remaining proof |
-|---|---|---|
-| Working set below 100 MiB | PENDING | Repeat the current Release measurement with working set and private committed memory reported separately. |
-| Diagnostic idle CPU | PASS | Visible average 0.0324%; hidden average 0.0182%. |
-| Cubism visible resource use | PASS | At the parity 60 FPS default: 0.94% total CPU on the current 4-CPU environment. |
-| Five-minute bounded-memory runs | PASS | Visible, preferences and hidden modes remain bounded. |
-| 24-hour bounded-memory run | PENDING | The harness exists; an uninterrupted result remains. |
-| Windows 10 and 11 | PENDING | Current Windows passes; a second supported version remains. |
-| macOS x64 and arm64 | PENDING | Requires macOS runners and signed application builds. |
-| Linux X11 | PENDING | Requires a Linux runner with XInput2 and Xfixes. |
-| Linux Wayland reduced-mode reporting | PENDING | Requires a Wayland session. |
-
-The Windows implementation has no remaining Cubism or visual blocker. The
-remaining `PENDING` rows require a current memory rebaseline, external
-hardware, long-duration execution, or operating systems that are not present
-on the current machine.
diff --git a/docs/modal-frame-architecture.md b/docs/modal-frame-architecture.md
deleted file mode 100644
index 2e0c9858..00000000
--- a/docs/modal-frame-architecture.md
+++ /dev/null
@@ -1,40 +0,0 @@
-# Modal menu frame ownership
-
-Native context menus run a nested platform event loop. The SDL application loop
-does not advance while one is open, so animation must be pumped explicitly.
-
-The dependency direction is intentionally one way:
-
-```text
-native modal timer -> BongoCatModalTick -> runtime/modal_frame -> input + Live2D + render
-```
-
-Ownership boundaries:
-
-- `runtime/modal_frame.*` owns elapsed-time continuity and the work performed by
- a modal frame. It is the only modal-related module allowed to coordinate input,
- Live2D, and rendering.
-- `runtime/window_menu_preview.c` owns temporary context-menu previews. It
- delegates animation to the modal frame pump.
-- `runtime/tray.c` owns tray state and supplies a modal frame callback. It does
- not know how a native menu loop is implemented.
-- `platform/windows_borderless.c` owns the desktop menu timer and highlight
- messages. Its callback state is attached to the individual window.
-- `platform/windows_tray.c` owns SDL tray-window subclassing and the tray menu
- timer. Its callback state is attached to the individual tray window.
-- Native window dragging uses the same pump while the operating system owns the
- nested move loop.
-- `platform/macos.m` schedules the same callback in the menu run-loop mode.
- SDL's Linux tray is asynchronous; the custom X11 desktop menu calls the
- callback from its own loop.
-
-Invariants for future changes:
-
-1. Platform menu modules must not include Live2D or application runtime state.
-2. Modal ticks must update `app->last_frame_ns`, preventing a large catch-up
- step when the SDL loop resumes.
-3. The pump must drain model input without dispatching shortcuts before advancing Live2D.
-4. Every native timer and subclass must be removed before its callback userdata
- is destroyed.
-5. Desktop and tray menus must both pass a held-open test with continuing frame
- timestamps and different image hashes after pointer movement.
diff --git a/docs/model-format-audit-2026-07-24.md b/docs/model-format-audit-2026-07-24.md
deleted file mode 100644
index 32ee5e18..00000000
--- a/docs/model-format-audit-2026-07-24.md
+++ /dev/null
@@ -1,126 +0,0 @@
-# Tauri / Bongo-Cat-Mver 模型格式审计
-
-日期:2026-07-24
-原样结构支持更新:2026-07-25
-产品:BongoCat 0.1.0
-
-## 结论
-
-BongoCat 现在以同一条事务式导入流水线支持三类输入:
-
-- Tauri/普通 Live2D v3 模型目录。
-- 带 `config.json` 与 `img/` 的完整 Bongo-Cat-Mver 模型包。
-- 只有图片差异的 Mver patch 包,例如“Z-无按键显示”和“Z-有按键显示”。
-- 放在可执行文件同目录(或其一级子目录)的完整 Mver 原生目录。
-
-真实 Tauri、完整 Mver 和两个 Z patch 均会安装 `standard`、`keyboard`、
-`gamepad` 三个模型,并逐个通过 Cubism 原生运行时加载。导入后的 `payload` 是用户模型目录的
-逐文件原样副本,BongoCat 生成的预览、输入贴图、媒体和元数据只写入并列的 `adapter` 目录;不会向
-用户源目录或 `payload` 增删、改名、合成或覆盖文件。无法安全跨机器迁移的固定画布坐标、
-工作区坐标和设备精灵几何会写入
-`.bongo-cat-import-report.json`,不再被静默忽略。
-
-项目尚未发布,因此用户数据目录中的自定义模型不提供旧统一目录布局兼容或自动迁移;没有当前
-版本旁路描述文件的目录仍会被扫描器忽略。作为明确的生态兼容入口,可执行文件旁的完整 Mver
-目录会直接作为只读源加载,BongoCat 只在用户数据的 `portable-mver` 中缓存合成图片、行为元数据和
-兼容报告,不复制、重命名或修改原目录。内置模型仍由只读的预设扫描路径直接加载。
-
-## 格式契约
-
-| 类型 | 必要结构 | 结果 |
-|---|---|---|
-| Tauri / Live2D | 唯一合法 `.model3.json` 及其必需引用 | 导入一个或多个模型 |
-| 完整 Mver | `config.json`、`img`、合法 mode 配置、编号资源和 Live2D manifest | 每个合法 mode 导入一个模型 |
-| Mver patch | 唯一 patch `img` 根及唯一同名完整基础包 | 分别原样保存 base/patch,由 adapter 按文件覆盖语义解析 |
-| 运行目录 Mver | 可执行文件旁的完整 Mver 根,或包含完整包/图片补丁的模型容器 | 直接引用 Live2D 源文件,只缓存 BongoCat adapter |
-
-同目录多 manifest、多个 patch 根、无基础包或多个基础包都被拒绝。图片 patch 不会被伪装成
-可独立运行的模型。
-
-## 已映射能力
-
-- Live2D v3 MOC、纹理、Physics、Pose、DisplayInfo、Expressions 和 Motions。
-- Tauri 与 Mver 的预览图、背景和三种 mode。
-- Windows VK 到 BongoCat 跨平台快捷键名称的转换,包括组合键和左右修饰键。
-- `input_mode=1` 的 Mver 手柄按钮;`input_mode=0` 保持键盘协议。
-- standard/keyboard/gamepad 的左右手编号图片,支持稀疏编号并报告缺项。
-- `CAT_motion`、`CAT_motion_lock` 和 expression 的逐索引严格映射。
-- Mver `face` 图片表情及 `emoticonKeep` / `emoticonClear`。
-- Mver 独立音频及 `soundKeep` / `soundClear`;关闭 keep 时松键停止。
-- 不同尺寸 straight-alpha 图片合成,不再按最小画布裁切。
-- Z patch 的逐文件覆盖和基础资源继承。
-
-## 明确降级
-
-以下字段依赖 Mver 固定 1400x1400 SFML 画布、Windows 桌面坐标或其专用手臂曲线,不能直接
-持久化到跨平台模型:
-
-- `window_size`、`topWindow`、`workarea`。
-- `offsetX/Y`、`scalar`、`hand_offset`。
-- `l2d_offset`、`l2d_correct`。
-- `leftHanded`、`mouse_force_move`。
-- standard 的 `arm/up/mouse/tablet` 动态设备精灵几何。
-- gamepad 的 `stick_offset_L/R`。
-
-BongoCat 对应功能继续由用户的窗口、鼠标镜像、模型缩放和手柄轴设置控制。导入报告会逐模型记录
-这些降级项、原因、能力布尔值、声明/可用/缺失资源数以及可选 motion Sound 缺失数。
-
-## 审计 TODO
-
-### P0:协议和行为正确性
-
-- [x] M01 固化三类格式契约和稳定的失败规则。
-- [x] M02 将 Z 目录实现为显式 patch 工作流,禁止独立伪模型。
-- [x] M03 校验编号资源、损坏 PNG 和稀疏资源,并输出数量诊断。
-- [x] M04 用 `input_mode` 决定手柄/键盘协议,覆盖低 VK 值和组合键。
-- [x] M05 精确映射 `CAT_motion` / `CAT_motion_lock`,校验配置行数。
-- [x] M06 统一 motion Sound、独立音频、全局开关和 keep/clear 语义。
-
-### P1:Mver 可见行为
-
-- [x] M07 支持图片 `face`、保持、清除和 momentary 松键恢复。
-- [x] M08 审计鼠标/数位板资源;固定画布设备几何改为显式降级报告。
-- [x] M09 对可移植状态做映射,对机器相关状态做逐字段报告。
-- [x] M10 预检全部 manifest 必需引用;缺失 motion Sound 作为可选诊断。
-- [x] M11 同目录多 manifest 明确拒绝,递归候选去重并稳定排序。
-- [x] M12 限制向上找包边界,patch 基础包必须唯一。
-- [x] M13 修复不同尺寸合成裁切,并测试半透明与透明空白像素。
-- [x] M14 保持 staging/rename/rollback 事务,启动时安全清理本应用临时目录。
-- [x] M15 元数据只接受版本 1,只补缺失默认绑定并保留用户覆盖。
-
-### P2:测试、跨平台和组织
-
-- [x] M16 增加纯 C 的 Mver chord/device 协议测试,并保留端到端安装测试。
-- [x] M17 增加环境变量驱动的可选真实样本测试入口。
-- [x] M18 对 Tauri、完整 Mver、两个 Z patch 逐模型执行 Cubism smoke。
-- [x] M19 统一 CTest 命令、测试计数和审计文档。
-- [x] M20 曾将当时的本地审计脚本纳入 300 行门禁并拆分超长脚本;这些脚本现已移出仓库。
-- [x] M21 将 discovery、patch、shortcut、motion、effect、metadata、report、storage 分层。
-- [x] M22 将 Mver 的 Windows VK 解析限制在 adapter,运行时只接收 BongoCat 输入名。
-- [x] M23 清理历史命名噪声;旧 schema 测试变量不再使用 `legacy_path`。
-- [x] M24 将原模型与 BongoCat 适配产物分离为 `payload` / `adapter`,并验证源目录未变化。
-- [x] M25 移除未发布旧自定义目录布局的兼容扫描,只接受当前旁路描述文件。
-- [x] M26 按 Mver 原版相对路径习惯自动发现运行目录模型(包括嵌套的完整包与图片补丁),并保持源目录只读。
-- [x] M27 对原版常见的“配置绑定多于 manifest 入口”仅截取可用入口;手动导入仍保留严格校验。
-
-## 自动化证据
-
-- `model-import-unit`:VK、组合键、`input_mode`、手柄映射,以及运行目录 Mver
- 直接/嵌套容器、图片补丁发现和 adapter 缓存测试。
-- `model-import-formats`:32 个合成端到端场景,包括歧义、回滚、patch、稀疏资源、
- momentary、清除、缺失可选音频、非法 chord 和不同尺寸合成。
-- `model-import-real-samples`:由 `BONGO_CAT_TAURI_SOURCE`、
- `BONGO_CAT_MVER_SOURCE`、`BONGO_CAT_MVER_PATCH_SOURCES` 驱动;未配置时明确 Skip。
-- 合成与真实样本审计会比较导入前后源目录签名,并验证安装 payload 的相对路径和 SHA-256
- 与对应 Tauri、Mver、base、patch 源目录完全一致。
-- Windows 多配置生成器的标准命令为:
- `ctest --test-dir build-cubism -C Release --output-on-failure`。
-
-真实样本结果:Tauri 3/3、A-露西亚 3/3、Z-无按键显示 3/3、Z-有按键显示 3/3,
-安装、报告生成和逐模型 Cubism 加载均通过。
-
-## 外部门禁
-
-本次 Windows 审计不能替代 Linux/macOS 的原生构建,也不能模拟真实手柄热插拔、不同桌面缩放
-和声卡故障。相关代码只使用 SDL、标准 C 和 yyjson 的跨平台接口;这些环境仍应由对应系统的 CI
-和真实硬件回归承担。
diff --git a/docs/parity.md b/docs/parity.md
deleted file mode 100644
index 63bf31ad..00000000
--- a/docs/parity.md
+++ /dev/null
@@ -1,150 +0,0 @@
-# Native completion contract
-
-The standalone native application is complete only when each automated check
-and the matching manual scenario pass without depending on another frontend or
-desktop runtime.
-
-## Model and rendering
-
-- Load all three bundled Cubism `model3` models.
-- Import and remove custom model directories without changing their layout.
-- Render drawable order, masks, blend modes, opacity, multiply and screen color.
-- Run `motion3` motions, `exp3` expressions, fade timing, and motion audio.
-- Preserve mouse, keyboard, and gamepad parameter mappings.
-- Draw the model background and at most one key overlay per hand.
-- Preserve mirror, mouse mirror, model scale, opacity, radius, and FPS controls.
-
-## Input
-
-- Observe global keyboard press and release events.
-- Observe global mouse buttons and coalesced pointer movement.
-- Preserve Windows automatic release timing for system keys.
-- Support SDL game controllers, sticks, triggers, and hot plug.
-- Register behavior shortcuts and application shortcuts.
-- Request the required macOS input-monitoring permission.
-
-## Window and application
-
-- Transparent, borderless, draggable main window with per-pixel alpha.
-- Click-through, always-on-top, hide-on-hover, and keep-in-monitor behavior.
-- Shift plus secondary drag resizing from 10 through 500 percent.
-- Tray and context menus, capture-friendly taskbar/Dock suppression, and single instance handling.
-- Restore window position and size across monitor and DPI changes.
-- Autostart, project information, and application exit.
-
-## Preferences and data
-
-- General, cat, model, behavior, and shortcut preference views.
-- Simplified Chinese, Traditional Chinese, English, French, German, Japanese,
- Korean, Portuguese, Russian, and Spanish.
-- Light, dark, and system themes.
-- Persist native settings and custom models without external project paths.
-- Build, test, run, and package with only files contained in this repository.
-- Persist settings atomically and recover from a truncated write.
-
-## Release gates
-
-- Golden images for idle, key, mouse, controller, motion, and expression states.
-- Recorded input traces produce equivalent parameter values in old and new apps.
-- Windows 10/11, macOS x64/arm64, and Linux X11 manual matrix passes.
-- Wayland reports reduced global-input support instead of silently failing.
-- Idle CPU is below 0.3 percent on the reference machine.
-- Working set is below 100 MiB with one bundled model active.
-- Hidden rendering is suspended and a 24-hour run has no unbounded growth.
-- Every hand-written file passes the configured source-size policy, whose
- default is 300 physical lines with reviewed file-specific limits.
-
-## Cubism Viewer animation equivalence
-
-Live2D Cubism Viewer 5.3.03 is the animation and interaction baseline for every
-model. Motion curves, expression blending, physics, eye blink, pose, idle
-motion, and pointer-driven look updates use the official Cubism Framework
-order and behavior. Source format never selects a second animation runtime.
-Viewer mouse tracking feeds its configured `ParamAngleX`, `ParamAngleY`,
-`ParamEyeBallX`, `ParamEyeBallY`, and, when present, `ParamBodyAngleX` channels
-through the Viewer's own `TargetPoint` variant before physics consumes them at
-the `physics3` authored FPS. That variant uses a `7.2727275 / 30` maximum target
-speed, a 0.15 second acceleration time, a pixel-scale desktop dead zone, and
-the Viewer's additional 1.2 input gain. Parameter scaling selects the authored
-positive or negative extent from the parameter's value immediately before the
-additive update; all five default mappings use weight 1.0. Tracking follows
-pointer movement continuously and keeps the Viewer's TargetPoint response
-curve without requiring a mouse button. Primary and secondary button state
-remains independent for model-authored hand parameters. Viewer-default
-automatic Idle playback is enabled and avoids immediately repeating the same
-Idle motion when alternatives exist.
-Desktop tracking is model-centered by default, so look direction changes only
-when the pointer crosses the pet's visible center. Users can disable the
-setting to retain the legacy screen/work-area-centered coordinate mapping.
-In model-centered mode, the authored hand and device range maps absolutely to
-the full display containing the pet; legacy mode retains Mver relative input.
-The display is selected from the pet's visible center. Negative-coordinate,
-stacked, and mixed-DPI layouts retain desktop coordinates, and display mode,
-topology, or scale changes invalidate and immediately resample the mapping.
-`ParamAngleZ` and model-specific `ParamMouse*` channels are not synthesized by
-the Viewer-equivalent path.
-
-Mver 0.1.6 remains the compatibility baseline for package discovery, imported
-files, configuration, shortcuts, window composition, and projection only. A
-Tauri or standalone Live2D model is imported through the same adapter and then
-runs through the Viewer-equivalent Cubism animation path.
-
-Mver imports retain the source package's `l2d_correct`, `window_size`,
-`l2d_offset`, and horizontal-follow values in `.bongo-cat-mver.json`. The native
-renderer uses the Mver 0.1.6 projection order for those models. Expressions keep
-the authored canvas projection so the Live2D model remains aligned with the
-background and input layers. The compatibility matrix also converts the
-modern Cubism Core canvas units to Mver 0.1.6 units while retaining authored
-Layout translation and `l2d_offset`. Its horizontal option controls following,
-not model mirroring. Window resizing follows the imported reference aspect
-ratio. Pointer-driven head, body, eye, and physics parameters use Mver's
-configured `workarea`, or its DPI-virtualized primary-screen coordinate domain
-when no custom work area is enabled.
-
-The Mver adapter translates the horizontal target sign and work-area mapping
-before the values reach the shared Viewer-equivalent look stage. It does not
-alter Cubism motion curves, expression blending, physics, or look timing.
-
-## Cubism Viewer blind test
-
-The `viewer-sequence` CI scenario captures model-only Native frames and key
-parameters at track and release samples `1, 2, 4, 8, 15, 30`. It does not use
-desktop screenshots, so window decoration and desktop overlays cannot affect
-the Native side. Reference Viewer screenshots are compared with:
-
-```powershell
-cmake\RunCubismViewerBlindTest.ps1 `
- -ViewerDirectory build-delivery-final\viewer-studio-audit\viewer `
- -NativeDirectory build-delivery-final\viewer-studio-audit\native-run\cubism-viewer-audit\native `
- -OutputDirectory build-delivery-final\viewer-studio-audit\blind-test-final
-```
-
-The tool removes transparent, white, and black backgrounds, rejects detached
-desktop overlays, and normalizes each source with its baseline model bounds.
-It writes anonymous randomized A/B ballots, a separate answer key, normalized
-frames, and metrics for the full model, face, hair, and hands. Dynamic metrics
-also report each side's change from `track-000`, preventing a high static image
-score from hiding a different interaction response curve.
-
-The blind-test entry point is a PowerShell wrapper around the native C target
-`cubism_viewer_blind_test`; build it with:
-
-```powershell
-cmake --build build-delivery-final --config Release --target bongo_cat_validation_tools
-```
-
-The standalone C metric tools are `mver_blind_metrics` and
-`mver_phase_metrics`. The former accepts two image paths and optional mask and
-threshold arguments; the latter accepts two frame directories and searches
-their phase lag. The Viewer capture helpers (`cubism_viewer_desktop_capture`
-and `cubism_viewer_drag_capture`) are also C targets and keep their existing
-command-line arguments and trace formats.
-
-## Live2D texture quality
-
-Live2D atlases retain their authored pixel dimensions for both bundled and
-imported models. The loader only downsamples an atlas when it exceeds the
-active GPU's `GL_MAX_TEXTURE_SIZE`, or retries at 2048 pixels after an explicit
-`GL_OUT_OF_MEMORY` upload failure. Model textures use the same linear,
-non-mipmapped sampling as Mver so changing the pet window size does not switch
-filtering behavior or silently select a lower-resolution source.
diff --git a/docs/performance-audit-2026-07-29.md b/docs/performance-audit-2026-07-29.md
deleted file mode 100644
index 52f00124..00000000
--- a/docs/performance-audit-2026-07-29.md
+++ /dev/null
@@ -1,83 +0,0 @@
-# Performance and package audit — 2026-07-29
-
-## Scope
-
-This pass reduces CPU, memory, GPU texture storage, and package size without
-changing pixels, animation timing, frame rate, source resource quality, or
-supported Windows/macOS/Linux behavior.
-
-## Implemented changes
-
-- Shadow masks use `GL_R8` with RGB=`GL_ONE`, A=`GL_RED` swizzling instead of
- storing redundant white RGB channels in `GL_RGBA8`.
-- Same-color radial masks use the same single-channel path and retain the
- original color through draw tinting.
-- Paint texture ownership/upload was separated into `ui_paint_cache` so paint
- algorithms and GPU cache policy remain independently maintainable.
-- Preferences CPU buffers changed from 512/128 KiB to 256/96 KiB after 232
- captured frames showed historical peaks of 100,300/36,324 bytes. Full
- interaction peaked at 47,920/16,458 bytes.
-- Four unreferenced legacy assets are excluded from Windows, macOS, and Linux
- runtime packages while remaining available in the source tree.
-- Visual audit capture now moves the physical pointer outside the window to
- remove non-deterministic hover-state noise.
-
-## Measured results
-
-| Metric | Before | After | Result |
-|---|---:|---:|---:|
-| Preferences paint cache | 9,430,040 B | 3,367,808 B | -6,062,232 B (-64.3%) |
-| Runtime-flow peak working set | 156.051 MiB | 143.973 MiB | -12.078 MiB |
-| Runtime-flow peak private memory | 126.188 MiB | 114.707 MiB | -11.481 MiB |
-| Preferences soak working set | 136.207 MiB | 131.574 MiB | -4.633 MiB |
-| Preferences soak private memory | 112.555 MiB | 107.773 MiB | -4.782 MiB |
-| Hidden soak working set | 107.715 MiB | 102.922 MiB | -4.793 MiB |
-| Hidden soak private memory | 94.586 MiB | 92.383 MiB | -2.203 MiB |
-| Active settings CPU, 3-run mean | 182.292 ms | 156.250 ms | -14.3% |
-| Idle settings CPU, 3-run mean | 0.344% | 0% | no idle work |
-| Embedded PAK | 2,245,899 B | 2,228,107 B | -17,792 B |
-| Windows executable | 5,456,384 B | 5,438,464 B | -17,920 B |
-
-## Visual and behavioral parity
-
-- Fixed-input old/new comparison: 10/10 preferences captures were exactly
- identical across 6,120,000 compared pixels; MAE=0 and RMSE=0.
-- Visual matrix: 53/53 views passed.
-- Overlay matrix: 11/11 views passed; all 8 input scenarios passed.
-- Preferences scenarios: 12/12 passed.
-- Navigation: all 5 pages passed; P90 frame time 8.67 ms, max 10.29 ms.
-- Focus audit passed, including dragging on the first press after focus loss.
-- Cursor audit passed for all 11 cursor targets.
-- Context menu audit passed with Settings first and all nested labels valid.
-- Runtime flow passed all 17 stages with no handle growth or warm recovery
- memory growth.
-
-## Cross-platform and risk decisions
-
-- Shared rendering remains portable C/OpenGL 3.3; macOS keeps its OpenGL 4.1
- core context and the same R8/swizzle semantics.
-- MSVC and MinGW Release builds enforce the 300-line policy.
-- Runtime/package exclusions are mirrored in Windows PAK creation, macOS bundle
- staging, and Linux installation.
-- UPX and whole-pack runtime compression remain intentionally rejected because
- they increase antivirus, signing, startup, and peak-memory risk.
-- PNG and FLAC data were not recompressed: the current assets are already
- compressed, and no verified pixel/PCM-identical optimizer is part of the
- toolchain. Exact duplicate payloads remain deduplicated by the asset packer.
-
-## Evidence
-
-- `build-final/optimization-paint-r8-final`
-- `build-final/visual-parity-paint-r8-after-stable`
-- `build-final/optimization-visual-final`
-- `build-final/optimization-overlay-final`
-- `build-final/optimization-preference-scenarios-final`
-- `build-final/optimization-navigation-final`
-- `build-final/optimization-focus-final`
-- `build-final/optimization-cursor-final`
-- `build-final/optimization-context-menu-final`
-- `build-final/optimization-soak-visible`
-- `build-final/optimization-soak-hidden-final`
-- `build-final/optimization-soak-preferences`
-- `build-final/optimization-runtime-flow`
-- `build-final/optimization-startup-performance`
diff --git a/docs/startup-reliability-audit-2026-07-26.md b/docs/startup-reliability-audit-2026-07-26.md
deleted file mode 100644
index 2c36132f..00000000
--- a/docs/startup-reliability-audit-2026-07-26.md
+++ /dev/null
@@ -1,147 +0,0 @@
-# Startup and preferences reliability audit - updated 2026-07-29
-
-## Scope and outcome
-
-This audit covers the standalone native release on the available Windows host,
-with emphasis on double-click startup, damaged-data recovery, first-frame
-validity, preferences behavior, transparent rendering, resource stability and
-portable paths. The audited binary is:
-
-- `build-final/Release/BongoCat.exe`
-- Source baseline before this final audit set: `378fd43`
-- SHA-256: `F924800D504356AD30034E975A2B9610DE489A109C2A39739E99756BC0B79037`
-- Size: 5,456,384 bytes
-
-The MSVC and MinGW Release configurations each pass 10/10 CTest cases. No
-finite suite can guarantee startup on every possible machine, so signing,
-other Windows versions and real macOS/Linux machines remain explicit gates.
-
-## Startup protections
-
-| Failure class | Implemented protection |
-|---|---|
-| A second launch appeared to do nothing | Notify the primary process, restore it on-screen and raise the window. |
-| Hidden or off-screen saved state | Recover a visible on-screen main window for interactive launch. |
-| Invalid or partially written settings | Strict versioned JSON parsing, defaults on corruption and atomic writes. |
-| Damaged extracted assets | Verify embedded assets by size and SHA-256 and repair the cache. |
-| Damaged custom Live2D model | Validate packages before install/load and fall back to a bundled model. |
-| Unavailable global hooks | Continue startup and persist an actionable diagnostic. |
-| Unsupported multisampling | Retry without MSAA and prove the resulting sample count is zero. |
-| Fatal graphics initialization | Persist startup stage and error logs instead of failing silently. |
-| Deep, spaced or Unicode paths | Use bounded UTF-safe path handling and long-path-aware Windows I/O. |
-| Large portable model trees | Apply depth, directory and time budgets without recursive stack growth. |
-| Interrupted model import | Isolate temporary imports and clean stale residue on startup. |
-| Stale first-frame evidence | Wait for a newly completed audit file and retry transient sharing failures. |
-
-## Automated startup evidence
-
-The test set covers core, i18n, UI, app state, model import units and formats,
-optional real samples, portable startup soak, the startup reliability matrix
-and preferences navigation.
-
-The startup matrix independently verifies:
-
-- fresh extraction and a visible nonblank first frame with `gl_error=0`;
-- actual normal MSAA values of `sample_buffers=1`, `sample_count=4`;
-- forced fallback values of `sample_buffers=0`, `sample_count=0`;
-- same-size embedded-asset corruption and cache repair;
-- malformed settings and malformed model recovery;
-- hidden and far off-screen window recovery;
-- unavailable global hooks;
-- bounded portable-directory scanning;
-- spaces, Unicode and paths longer than the legacy Windows `MAX_PATH` limit;
-- long-path cache reuse, cleanup and model import;
-- persisted fatal OpenGL diagnostics; and
-- second-instance wake-up from hidden and tray-closed states without hanging.
-
-All 30 PowerShell audit scripts parse successfully, `git diff --check` is clean
-and the 300-line handwritten-source policy passes.
-
-## Preferences verification
-
-| Area | Result | Evidence |
-|---|---|---|
-| Visual matrix | PASS 53/53 | 50 preferences combinations plus 3 models |
-| HTML reference parity | PASS | 98.4996% average pixel similarity; 97.9526% minimum page |
-| Main interactions | PASS x6 | Toggle, scroll, navigation, combo, locale and shortcut editing |
-| Sidebar navigation | PASS x10 | 50/50 clicks; all five pages selected every loop |
-| Persistence | PASS | Locale, toggle, stepper and shortcut survive save/restart paths |
-| Dialog/state scenarios | PASS 12/12 | Behavior, expression, Escape, outside-close, shortcut, locale, controls and toast |
-| Pointer feedback | PASS 11/11 | Tabs, close, toggles, combo, shortcut, import/model cards and cancel |
-| Focus recovery | PASS | First inactive-window press drags 56 x 32 px |
-| Tray ordering | PASS | Preferences is the first tray entry |
-| Localization fonts | PASS | Five locales, both themes, 2x oversampling, `1024x8192` atlas |
-
-Configuration is intentionally split by ownership rather than retaining Tauri
-compatibility: user preferences and runtime session state are separately
-versioned and atomically persisted. The interaction audit validates both file
-formats and debounce behavior.
-
-## Rendering integrity
-
-The model overlay now emits premultiplied RGBA and uses independent color/alpha
-blend factors. Preferences retain straight-alpha color blending with an
-independent alpha channel. The software compositor uses the complete
-source-over equation, and model textures use linear no-mipmap filtering aligned
-with the original renderer.
-
-Across 11 model/input captures, each audited frame contains 2,149 to 2,361
-semi-transparent antialiased pixels and zero RGB-greater-than-alpha violations.
-Standard, keyboard and gamepad models, left/right/both hands, releases, stress
-and gamepad-button states all pass.
-
-## Performance and stability
-
-- Cold first valid frame: 1,192 ms.
-- Seven warm launches: 511 ms average, 550 ms P95.
-- Navigation settles in 235.1 ms, with 9.50 ms average, 15.20 ms P90 and
- 16.76 ms maximum audited frame time.
-- Active preferences animation uses about 19.5% CPU on this host; idle is 0%.
-- Main 30-second soak trends about +0.38 MiB with one additional handle.
-- Preferences 30-second soak trends about +0.17 MiB with no handle growth.
-- Full runtime close/reopen/recovery flow stays below 170 MiB working set and
- 140 MiB private bytes and shows no continuing growth.
-
-The larger font atlas is a deliberate quality/performance tradeoff. Baking
-memory is temporary, runtime memory remains within measured budgets, and idle
-rendering does not continue consuming CPU.
-
-## Windows release properties
-
-- x64 GUI executable with the static MSVC runtime; imported dependencies are
- Windows system components.
-- ASLR, high-entropy VA, DEP/NX and Control Flow Guard are enabled.
-- Large application state and startup catalogs live on the heap.
-- The executable is not Authenticode-signed. Signing remains required before
- broad public distribution to establish provenance and reduce SmartScreen
- warnings.
-
-## Cross-platform boundary
-
-Portable configuration, rendering and bounded/atomic I/O are shared. Native
-window, input, tray and startup behavior remain isolated by platform. CI defines
-warnings-as-errors build/test/package jobs for Windows x64, Ubuntu Linux x64 and
-macOS. This Windows host cannot honestly certify other systems' compositor,
-permissions, signing or GUI behavior; their CI and real-machine smokes must pass
-for every release candidate.
-
-## Retained evidence
-
-- `build-final/visual-audit-final-current/audit.csv`
-- `build-final/parity-antialias-current/page0.json` through `page4.json`
-- `build-final/overlay-audit-final-current-3/overlay-audit.csv`
-- `build-final/preference-scenarios-final-current/audit.csv`
-- `build-final/preferences-navigation-stable/result.json`
-- `build-final/preferences-interaction-robust-6/result.json`
-- `build-final/preferences-performance-current-2/result.json`
-- `build-final/focus-current/result.json`
-- `build-final/cursor-current/result.json`
-- `build-final/context-menu-current/result.json`
-- `build-final/runtime-flow-current/runtime-flow-summary.csv`
-
-## Repeatable commands
-
-- `cmake --build build-final --config Release --parallel 2`
-- `ctest --test-dir build-final -C Release --output-on-failure`
-- `ctest --test-dir build-mingw-final -C Release --output-on-failure`
-- `cmake -DROOT=. -P cmake/CheckLines.cmake`
diff --git a/docs/system-audit-2026-07-17.md b/docs/system-audit-2026-07-17.md
deleted file mode 100644
index 1bc93716..00000000
--- a/docs/system-audit-2026-07-17.md
+++ /dev/null
@@ -1,94 +0,0 @@
-# Systematic release audit - updated 2026-07-29
-
-## Scope and result
-
-This audit covers the native cross-platform application on the available
-Windows host: startup and recovery, Cubism rendering, transparent-window
-composition, preferences parity and interaction, tray/context menus, focus and
-dragging, configuration persistence, resource use, source policy and standalone
-delivery. The audited release executable is:
-
-- `build-final/Release/BongoCat.exe`
-- Size: 5,456,384 bytes
-- SHA-256: `F924800D504356AD30034E975A2B9610DE489A109C2A39739E99756BC0B79037`
-
-MSVC Release and MinGW Release both build successfully and pass 10/10 CTest
-cases. No failed automated check is being waived in this report.
-
-## Defects corrected
-
-| Area | Correction | Verification |
-|---|---|---|
-| Overlay edges could develop dark/bright halos | Emit premultiplied RGBA and use independent RGB/alpha blending | 11/11 overlay captures, zero premultiply violations |
-| Preferences alpha accumulated incorrectly | Keep straight-alpha color blending while composing destination alpha independently | Visual, interaction and scenario matrices |
-| Software layer composition ignored destination alpha | Implement the complete source-over equation | Overlay release/input captures |
-| Model textures rendered black or with inconsistent filtering | Match Cubism/Pixi linear no-mipmap filtering and retain bounded anisotropy | Three bundled model captures, `gl_error=0` |
-| Font edges were visibly coarse | Bake horizontal and vertical 2x oversampling | Multilingual `1024x8192` atlas and 53/53 captures |
-| Requested MSAA was assumed rather than measured | Record actual SDL sample buffers/count and assert both normal and fallback paths | Normal `1/4`, fallback `0/0` |
-| Sidebar and shortcut audits depended on fragile desktop injection | Drive SDL-compatible window/input paths and add an internal shortcut smoke event | Ten navigation loops and six full interaction loops |
-| Heavy-load capture could sample a stale/black frame | Retry completed-frame reads and validate capture color/black ratios | Stable visual and interaction runs |
-| Tray preferences entry was below window toggles | Put Preferences first and assert the first SDL tray entry | Tray self-test plus context-menu capture |
-| Inactive-window dragging needed an activation click | Accept the first press after focus loss | First press moved the window 56 x 32 px |
-| Resource budgets no longer reflected the high-quality font atlas | Re-measure and set runtime-flow ceilings to 170/140 MiB | Runtime reopen/recovery flow |
-
-## Automated evidence
-
-| Area | Result |
-|---|---|
-| MSVC Release tests | PASS - 10/10 |
-| MinGW Release tests | PASS - 10/10 |
-| Source policy | PASS - checked handwritten files are at most 300 lines |
-| PowerShell audit syntax | PASS - 30/30 scripts parse |
-| Preferences visual matrix | PASS - 50/50: 5 pages x 5 locales x 2 themes |
-| Main model visual matrix | PASS - standard, keyboard and gamepad |
-| Current HTML pixel comparison | PASS - 98.4996% average similarity; minimum 97.9526% |
-| Preferences scenarios | PASS - 12/12 |
-| Preferences interaction | PASS - toggle, scroll, navigation, combo, locale, shortcut, debounce, formats and persistence |
-| Sidebar stress | PASS - 10 loops, 50/50 selections |
-| Cursor hit regions | PASS - 11/11 |
-| Overlay/input states | PASS - 11/11 |
-| Premultiplied alpha | PASS - 2,149 to 2,361 antialiased pixels per audited frame, zero violations |
-| Tray/context menu | PASS - Preferences is the first item |
-| Focus recovery drag | PASS - movement occurs on the first press |
-| Startup recovery matrix | PASS - corruption, paths, fallback, diagnostics and second instance |
-
-The pixel comparison uses the current native screenshots and the retained HTML
-reference at 900 x 680. It is a measured visual-parity check, not a claim that
-different operating-system text rasterizers will produce byte-identical pixels.
-
-## Performance and stability
-
-- Cold first valid frame: 1,192 ms.
-- Warm first valid frame: 511 ms average, 550 ms P95.
-- Preferences transition: 235.1 ms settled; 9.50 ms average frame, 15.20 ms P90.
-- Preferences animation CPU: about 19.5%; settled idle CPU: 0%.
-- Main 30-second soak: about +0.38 MiB trend, handles +1.
-- Preferences 30-second soak: about +0.17 MiB trend, handles unchanged.
-- Runtime flow, including settings close/reopen and model recovery, shows no
- continuing growth; measured peak was 156.188 MiB working set and 126.125 MiB
- private bytes, below the 170/140 MiB audit budgets.
-
-## Cross-platform boundary
-
-Shared behavior stays in portable SDL/OpenGL/C code. Windows-only focus,
-startup and shell integration remain behind platform source boundaries; macOS
-and Linux have their own platform units. CI defines warnings-as-errors builds,
-tests and packages for Windows x64, Ubuntu Linux x64 and macOS.
-
-The available host proves the MSVC and MinGW Windows paths. Real macOS/Linux GUI
-behavior, compositor differences, permissions, signing and packaging remain
-release gates and must not be represented as locally executed tests.
-
-## Repeatable commands
-
-- `cmake --build build-final --config Release --parallel 2`
-- `ctest --test-dir build-final -C Release --output-on-failure`
-- `ctest --test-dir build-mingw-final -C Release --output-on-failure`
-- `cmake -DROOT=. -P cmake/CheckLines.cmake`
-
-## External release gates
-
-- Authenticode/notarization and platform-specific signing.
-- Real Windows version coverage beyond this host.
-- Real macOS and Linux GUI/compositor smoke tests.
-- Physical gamepad hot-plug, audible output and an uninterrupted long soak.
diff --git a/docs/wechat.md b/docs/wechat.md
new file mode 100644
index 00000000..d082cbad
--- /dev/null
+++ b/docs/wechat.md
@@ -0,0 +1,3 @@
+
+
+
diff --git a/docs/wechat.png b/docs/wechat.png
new file mode 100644
index 00000000..2ca68c5e
Binary files /dev/null and b/docs/wechat.png differ
diff --git a/include/bongo_cat/app.h b/include/bongo_cat/app.h
index b529964e..0ff15ea4 100644
--- a/include/bongo_cat/app.h
+++ b/include/bongo_cat/app.h
@@ -8,19 +8,42 @@
#include "bongo_cat/mouse.h"
#include "bongo_cat/platform.h"
#include "bongo_cat/shortcut.h"
+#include "bongo_cat/sound_shortcut.h"
typedef struct BongoCatAudio BongoCatAudio;
typedef struct BongoCatTray BongoCatTray;
typedef struct BongoCatOverlay BongoCatOverlay;
typedef struct BongoCatPreferences BongoCatPreferences;
typedef struct BongoCatI18n BongoCatI18n;
+typedef struct BongoCatMultiPetRuntime BongoCatMultiPetRuntime;
+typedef struct BongoCatModelRefresh BongoCatModelRefresh;
+typedef struct BongoCatUpdateService BongoCatUpdateService;
+
+#define BONGO_CAT_MODEL_COVER_PENDING_CAP 8
typedef struct BongoCatApp {
- BongoCatConfig config;
+ BongoCatSettings settings;
+ BongoCatSessionState session;
BongoCatInputState input;
+ /* Main-thread-only cumulative input diagnostics. */
+ struct {
+ uint64_t log_ms, drained_keys, ignored_keys, mapped_keys;
+ uint64_t unmapped_keys, no_model_keys, presented_frames;
+ } input_diagnostics;
BongoCatShortcutState shortcut_state;
+ BongoCatSoundShortcutState sound_shortcut_state;
+ bool sound_shortcut_active[BONGO_CAT_BEHAVIOR_BINDING_CAP];
BongoCatModelCatalog models;
BongoCatBehaviorCatalog behaviors;
+ /* One immutable installed package's behavior catalog can be reused when
+ the user toggles back to it. Nearby source models are excluded because
+ their files may change outside the application. Allocate it lazily so
+ single-model child processes do not reserve the catalog. */
+ BongoCatBehaviorCatalog *behavior_cache;
+ char behavior_cache_model_id[BONGO_CAT_ID_CAP];
+ char behavior_cache_digest[65];
+ bool behavior_cache_valid;
+ bool behavior_catalog_valid;
BongoCatLive2DRenderOptions model_render_options;
BongoCatI18n *i18n;
BongoCatPlatform platform;
@@ -29,25 +52,59 @@ typedef struct BongoCatApp {
BongoCatTray *tray;
BongoCatOverlay *overlay;
BongoCatPreferences *preferences;
+ BongoCatMultiPetRuntime *multi_pet;
+ BongoCatModelRefresh *model_refresh;
+ BongoCatUpdateService *update;
SDL_Window *window;
void *gl_context;
- char preferences_path[BONGO_CAT_PATH_CAP];
+ char settings_path[BONGO_CAT_PATH_CAP];
char session_path[BONGO_CAT_PATH_CAP];
+ char config_root[BONGO_CAT_PATH_CAP];
char data_root[BONGO_CAT_PATH_CAP];
+ char models_root[BONGO_CAT_PATH_CAP];
+ char cache_root[BONGO_CAT_PATH_CAP];
+ char state_root[BONGO_CAT_PATH_CAP];
+ char log_root[BONGO_CAT_PATH_CAP];
+ char storage_root[BONGO_CAT_PATH_CAP];
char asset_root[BONGO_CAT_PATH_CAP];
char locale_root[BONGO_CAT_PATH_CAP];
+ char nearby_root[BONGO_CAT_PATH_CAP];
+ char executable_path[BONGO_CAT_PATH_CAP];
+ char primary_state_root[BONGO_CAT_PATH_CAP];
+ char secondary_model_id[BONGO_CAT_ID_CAP];
char smoke_import_path[BONGO_CAT_PATH_CAP];
char smoke_model[BONGO_CAT_ID_CAP];
char smoke_runtime_model[BONGO_CAT_ID_CAP];
char smoke_live2d_scenario[BONGO_CAT_ID_CAP];
char smoke_viewer_trace[BONGO_CAT_PATH_CAP];
- char pending_model_cover_path[BONGO_CAT_PATH_CAP];
+ char pending_model_cover_ids[BONGO_CAT_MODEL_COVER_PENDING_CAP][BONGO_CAT_ID_CAP];
+ char pending_model_cover_paths[BONGO_CAT_MODEL_COVER_PENDING_CAP][BONGO_CAT_PATH_CAP];
+ uint64_t pending_model_cover_retry_ns[BONGO_CAT_MODEL_COVER_PENDING_CAP];
+ unsigned pending_model_cover_attempts[BONGO_CAT_MODEL_COVER_PENDING_CAP];
+ size_t pending_model_cover_count;
char loaded_model[BONGO_CAT_ID_CAP];
+ char loading_model[BONGO_CAT_ID_CAP];
+ BongoCatModelMode loaded_mode;
bool running;
- bool preferences_store_valid;
+ bool settings_store_valid;
bool session_store_valid;
+ bool settings_store_blocked;
+ bool session_store_blocked;
bool autostart_launch;
+ bool secondary_pet;
+ bool secondary_origin_known;
+ bool secondary_control_known;
+ bool secondary_control_visible;
+ bool secondary_control_pass_through;
+ /* Keep the initial transparent window hidden until its first complete
+ model frame is ready. This prevents a platform-dependent black flash. */
+ bool startup_visibility_pending;
+ int secondary_origin_x, secondary_origin_y;
+ uint64_t secondary_control_check_ns;
+ uint64_t secondary_control_failure_ns;
+ uint64_t secondary_settings_check_ns;
uint64_t startup_raise_due_ns;
+ uint64_t model_load_last_frame_ns;
bool smoke;
bool smoke_preferences;
bool smoke_preference_shortcut;
@@ -55,7 +112,6 @@ typedef struct BongoCatApp {
bool smoke_remove_imported;
bool smoke_shortcuts;
bool smoke_menu;
- bool smoke_input_audit;
bool smoke_ignore_global_input;
bool smoke_pass_through;
bool smoke_context_menu;
@@ -64,6 +120,7 @@ typedef struct BongoCatApp {
bool smoke_runtime_flow;
bool smoke_freeze_model;
unsigned smoke_runtime_stage;
+ unsigned model_load_runtime_stage;
unsigned model_selection_serial;
uint64_t smoke_runtime_flow_ns;
int smoke_language;
@@ -79,13 +136,19 @@ typedef struct BongoCatApp {
uint64_t display_recovery_due_ns;
uint64_t mouse_last_ns;
uint64_t frame_audit_bmp_ns;
- uint64_t preferences_saved_hash, preferences_observed_hash;
+ uint64_t random_expression_due_ns;
+ float random_expression_interval_seconds;
+ uint32_t random_expression_state;
+ uint64_t settings_saved_hash, settings_observed_hash;
uint64_t session_saved_hash, session_observed_hash;
- uint64_t preferences_save_due_ns, session_save_due_ns;
+ uint64_t settings_save_due_ns, session_save_due_ns;
BongoCatMouseTracking mouse_tracking;
BongoCatMverPointerState mver_pointer;
bool hover_inside;
bool hover_hidden;
+ bool hover_fade_active;
+ float hover_fade_phase, hover_fade_target, hover_fade_opacity;
+ uint64_t hover_fade_tick_ns, hover_fade_next_ns;
bool pointer_known;
bool pointer_hit_dirty;
bool pointer_transparent;
@@ -95,15 +158,24 @@ typedef struct BongoCatApp {
bool left_mouse_down;
bool right_mouse_down;
bool side_mouse_down;
- bool model_mouse_parameters;
+ bool back_mouse_down, forward_mouse_down;
+ bool mouse_button_event_pending;
bool model_pointer_anchor_ready;
float model_pointer_anchor_x, model_pointer_anchor_y;
+ bool pointer_relative_active;
+ bool pointer_cursor_locked;
bool window_minimized;
double pointer_x, pointer_y;
bool resize_gesture;
float resize_scale_start, resize_scale_target;
int resize_base_width, resize_base_height;
bool resize_pending;
+ bool resize_render_target_pending;
+ /* Native preview ownership and gesture lifetime belong to window_snapshot.c. */
+ void *window_snapshot;
+ bool snapshot_blocked;
+ uint64_t snapshot_deadline_ns;
+ float snapshot_pointer_x, snapshot_pointer_y;
bool wheel_animation_active;
bool wheel_gesture_active;
int resize_pixel_width, resize_pixel_height;
@@ -121,22 +193,46 @@ typedef struct BongoCatApp {
float left_stick_x, left_stick_y;
float right_stick_x, right_stick_y;
bool left_stick_pressed, right_stick_pressed;
+ BongoCatInputEvent active_inputs[
+ BONGO_CAT_INPUT_KEY_STATE_CAP + BONGO_CAT_INPUT_RECOVERY_CAP];
+ size_t active_input_count;
uint32_t active_gamepad;
} BongoCatApp;
int bongo_cat_app_run(int argc, char **argv);
void bongo_cat_app_apply_input(BongoCatApp *app, const BongoCatInputEvent *event);
+void bongo_cat_app_reapply_input(BongoCatApp *app);
void bongo_cat_app_reset_gamepad(BongoCatApp *app);
void bongo_cat_gamepad_event(BongoCatApp *app, const void *sdl_event);
void bongo_cat_app_shortcuts(BongoCatApp *app, const BongoCatInputEvent *event);
bool bongo_cat_app_select_model(BongoCatApp *app, const char *id);
bool bongo_cat_app_select_model_with_error(BongoCatApp *app,
const char *id, BongoCatError *error);
+bool bongo_cat_app_model_active(const BongoCatApp *app, const char *id);
+size_t bongo_cat_app_active_model_count(const BongoCatApp *app);
+bool bongo_cat_app_set_model_active(BongoCatApp *app, const char *id,
+ bool active, BongoCatError *error);
+void bongo_cat_app_set_multiple_pets(BongoCatApp *app, bool enabled);
bool bongo_cat_app_run_behavior(BongoCatApp *app,
const BongoCatBehaviorEntry *behavior);
+size_t bongo_cat_app_selected_motion_count(const BongoCatApp *app);
+void bongo_cat_app_capture_behavior_state(BongoCatApp *app);
+size_t bongo_cat_app_saved_behavior_count(const BongoCatApp *app,
+ const char *model_id);
+void bongo_cat_app_forget_behavior_state(BongoCatApp *app,
+ const char *model_id);
+void bongo_cat_app_restore_behavior_state(BongoCatApp *app,
+ const char *model_id);
BongoCatResult bongo_cat_app_import_model(BongoCatApp *app, const char *source, BongoCatError *error);
BongoCatResult bongo_cat_app_remove_model(BongoCatApp *app, const char *id, BongoCatError *error);
void bongo_cat_app_rescan_models(BongoCatApp *app);
+void bongo_cat_app_refresh_installed_models(BongoCatApp *app);
+void bongo_cat_app_refresh_nearby_models(BongoCatApp *app);
+void bongo_cat_app_request_model_refresh(BongoCatApp *app);
+bool bongo_cat_app_model_refresh_busy(const BongoCatApp *app);
+void bongo_cat_app_request_model_package_refresh(BongoCatApp *app,
+ const char *package_id);
+void bongo_cat_app_request_nearby_model_refresh(BongoCatApp *app);
void bongo_cat_config_store_flush(BongoCatApp *app);
#endif
diff --git a/include/bongo_cat/audio.h b/include/bongo_cat/audio.h
index d442b9fc..6720f3a7 100644
--- a/include/bongo_cat/audio.h
+++ b/include/bongo_cat/audio.h
@@ -8,7 +8,15 @@ typedef struct BongoCatAudio BongoCatAudio;
BongoCatAudio *bongo_cat_audio_create(BongoCatError *error);
void bongo_cat_audio_destroy(BongoCatAudio *audio);
BongoCatResult bongo_cat_audio_play(BongoCatAudio *audio, const char *path, BongoCatError *error);
+/* With overlap disabled, only the same clip restarts; other clips continue. */
+BongoCatResult bongo_cat_audio_play_sound(BongoCatAudio *audio, const char *path,
+ bool overlap, BongoCatError *error);
+void bongo_cat_audio_update(BongoCatAudio *audio);
+void bongo_cat_audio_reset(BongoCatAudio *audio);
void bongo_cat_audio_stop(BongoCatAudio *audio);
+bool bongo_cat_audio_any_playing(const BongoCatAudio *audio);
+bool bongo_cat_audio_is_playing(const BongoCatAudio *audio, const char *path);
+void bongo_cat_audio_stop_path(BongoCatAudio *audio, const char *path);
void bongo_cat_audio_set_enabled(BongoCatAudio *audio, bool enabled);
#endif
diff --git a/include/bongo_cat/common.h b/include/bongo_cat/common.h
index 7d8d5876..b5d3c962 100644
--- a/include/bongo_cat/common.h
+++ b/include/bongo_cat/common.h
@@ -5,21 +5,32 @@
#include
#include
+#include "bongo_cat/version.h"
+
#define BONGO_CAT_NAME "BongoCat"
-#define BONGO_CAT_PET_WINDOW_TITLE "BongoCat - Pet"
-#define BONGO_CAT_SETTINGS_WINDOW_TITLE "BongoCat - Settings"
-#define BONGO_CAT_SLUG "bongo-cat"
+#define BONGO_CAT_PET_WINDOW_TITLE BONGO_CAT_NAME
+#define BONGO_CAT_TRAY_TITLE BONGO_CAT_NAME " " BONGO_CAT_VERSION_DISPLAY " \xC2\xB7 by vladelaina"
+#define BONGO_CAT_SLUG "bongocat"
#define BONGO_CAT_EXECUTABLE "BongoCat"
#define BONGO_CAT_APP_ID "com.bongocat.desktop"
#define BONGO_CAT_NAME_W L"BongoCat"
-#define BONGO_CAT_PET_WINDOW_TITLE_W L"BongoCat - Pet"
-#define BONGO_CAT_VERSION "0.1.0"
+#define BONGO_CAT_PET_WINDOW_TITLE_W BONGO_CAT_NAME_W
#define BONGO_CAT_PATH_CAP 1024
#define BONGO_CAT_ID_CAP 128
#define BONGO_CAT_SHORTCUT_CAP 128
#define BONGO_CAT_MODEL_CAP 128
+#define BONGO_CAT_ACTIVE_MODEL_CAP 8
+#define BONGO_CAT_ADDITIONAL_MODEL_CAP (BONGO_CAT_ACTIVE_MODEL_CAP - 1)
#define BONGO_CAT_BEHAVIOR_CAP 128
-#define BONGO_CAT_AUTO_RELEASE_CAP 64
+#define BONGO_CAT_BEHAVIOR_ID_CAP 384
+#define BONGO_CAT_BEHAVIOR_BINDING_CAP 256
+#define BONGO_CAT_MENU_LABEL_CAP (BONGO_CAT_ID_CAP + BONGO_CAT_SHORTCUT_CAP + 4)
+#define BONGO_CAT_SCHEDULED_RELEASE_CAP 64
+#define BONGO_CAT_UPDATE_VERSION_CAP 32
+#define BONGO_CAT_MODEL_ADAPTER_FILE ".bongo-cat-adapter.json"
+#define BONGO_CAT_MODEL_BUILTIN_MARKER ".bongo-cat-builtin"
+#define BONGO_CAT_MODEL_ADAPTER_SCHEMA 1
+#define BONGO_CAT_MODEL_ADAPTER_GENERATOR 1
typedef enum BongoCatResult {
BONGO_CAT_OK = 0,
diff --git a/include/bongo_cat/config.h b/include/bongo_cat/config.h
index 57a44a30..4d661516 100644
--- a/include/bongo_cat/config.h
+++ b/include/bongo_cat/config.h
@@ -3,16 +3,19 @@
#include "bongo_cat/common.h"
-#define BONGO_CAT_DEFAULT_AUTO_RELEASE_SECONDS 3.0f
#define BONGO_CAT_DEFAULT_MAX_FPS 60
#define BONGO_CAT_DEFAULT_WINDOW_SCALE_PERCENT 100.0f
#define BONGO_CAT_DEFAULT_WINDOW_OPACITY_PERCENT 100.0f
+#define BONGO_CAT_DEFAULT_RANDOM_EXPRESSION_SECONDS 5.0f
+#define BONGO_CAT_DEFAULT_WINDOW_CORNER_PERCENT 6.0f
+#define BONGO_CAT_DEFAULT_HIDE_FADE_SECONDS 0.3f
+#define BONGO_CAT_MAX_HIDE_FADE_SECONDS 3.0f
typedef enum BongoCatTheme { BONGO_CAT_THEME_AUTO, BONGO_CAT_THEME_LIGHT, BONGO_CAT_THEME_DARK } BongoCatTheme;
typedef enum BongoCatLanguage {
BONGO_CAT_LANG_EN_US,
BONGO_CAT_LANG_ZH_CN,
- BONGO_CAT_LANG_ZH_TW,
+ BONGO_CAT_LANG_ZH_HANT,
BONGO_CAT_LANG_FR_FR,
BONGO_CAT_LANG_DE_DE,
BONGO_CAT_LANG_JA_JP,
@@ -27,82 +30,162 @@ typedef enum BongoCatModelMode {
BONGO_CAT_MODE_KEYBOARD,
BONGO_CAT_MODE_GAMEPAD
} BongoCatModelMode;
+typedef enum BongoCatObsBackgroundColor {
+ BONGO_CAT_OBS_BACKGROUND_GREEN,
+ BONGO_CAT_OBS_BACKGROUND_BLUE,
+ BONGO_CAT_OBS_BACKGROUND_RED,
+ BONGO_CAT_OBS_BACKGROUND_MAGENTA,
+ BONGO_CAT_OBS_BACKGROUND_COLOR_COUNT
+} BongoCatObsBackgroundColor;
-typedef struct BongoCatModelOptions {
+typedef struct BongoCatModelPreferences {
+ bool multiple_pets;
bool mirror;
bool mouse_mirror;
bool mouse_centered;
bool ignore_mouse;
- float auto_release_seconds;
int max_fps;
-} BongoCatModelOptions;
+} BongoCatModelPreferences;
-typedef struct BongoCatWindowOptions {
- bool visible;
+typedef struct BongoCatWindowPreferences {
bool pass_through;
bool always_on_top;
bool hide_on_hover;
bool keep_in_screen;
+ bool obs_background;
+ bool random_expression;
+ bool rounded_corners;
+ BongoCatObsBackgroundColor obs_background_color;
+ float hide_delay_seconds;
+ float hide_fade_seconds;
+ float random_expression_interval_seconds;
+ float corner_radius_percent;
+} BongoCatWindowPreferences;
+
+typedef struct BongoCatWindowState {
+ bool visible;
+ bool position_known;
float scale_percent;
float opacity_percent;
- float hide_delay_seconds;
int x;
int y;
int width;
int height;
-} BongoCatWindowOptions;
+ /* The authored composition size inside width/height. The outer window
+ may be larger to hold expression geometry outside the base canvas. */
+ int content_width;
+ int content_height;
+} BongoCatWindowState;
-typedef struct BongoCatAppOptions {
+typedef struct BongoCatApplicationPreferences {
bool autostart;
bool tray_visible;
BongoCatTheme theme;
BongoCatLanguage language;
-} BongoCatAppOptions;
+} BongoCatApplicationPreferences;
-typedef struct BongoCatShortcutOptions {
- char visible_cat[BONGO_CAT_SHORTCUT_CAP];
+typedef struct BongoCatShortcutPreferences {
+ char toggle_pet_visibility[BONGO_CAT_SHORTCUT_CAP];
char visible_preferences[BONGO_CAT_SHORTCUT_CAP];
char mirror[BONGO_CAT_SHORTCUT_CAP];
char pass_through[BONGO_CAT_SHORTCUT_CAP];
char always_on_top[BONGO_CAT_SHORTCUT_CAP];
-} BongoCatShortcutOptions;
+} BongoCatShortcutPreferences;
typedef struct BongoCatBehaviorShortcut {
- char id[BONGO_CAT_PATH_CAP];
+ char id[BONGO_CAT_BEHAVIOR_ID_CAP];
char shortcut[BONGO_CAT_SHORTCUT_CAP];
char label[BONGO_CAT_ID_CAP];
+ bool shortcut_disabled;
} BongoCatBehaviorShortcut;
-typedef struct BongoCatConfig {
- BongoCatModelOptions model;
- BongoCatWindowOptions window;
- BongoCatAppOptions app;
- BongoCatShortcutOptions shortcuts;
- BongoCatBehaviorShortcut behavior_shortcuts[BONGO_CAT_BEHAVIOR_CAP];
+typedef struct BongoCatModelLabel {
+ char id[BONGO_CAT_ID_CAP];
+ char label[BONGO_CAT_ID_CAP];
+} BongoCatModelLabel;
+
+typedef struct BongoCatRemovedModel {
+ char id[BONGO_CAT_ID_CAP];
+} BongoCatRemovedModel;
+
+typedef struct BongoCatActiveBehavior {
+ char model_id[BONGO_CAT_ID_CAP];
+ char behavior_id[BONGO_CAT_BEHAVIOR_ID_CAP];
+} BongoCatActiveBehavior;
+
+#define BONGO_CAT_SETTINGS_EXTENSIONS_CAP 4096
+
+typedef struct BongoCatSettings {
+ BongoCatModelPreferences model;
+ BongoCatWindowPreferences window;
+ BongoCatApplicationPreferences app;
+ BongoCatShortcutPreferences shortcuts;
+ BongoCatBehaviorShortcut behavior_shortcuts[BONGO_CAT_BEHAVIOR_BINDING_CAP];
size_t behavior_shortcut_count;
- char current_model[BONGO_CAT_PATH_CAP];
- BongoCatModelMode current_mode;
-} BongoCatConfig;
+ BongoCatModelLabel model_labels[BONGO_CAT_MODEL_CAP];
+ size_t model_label_count;
+ BongoCatRemovedModel removed_models[BONGO_CAT_MODEL_CAP];
+ size_t removed_model_count;
+ char extensions_json[BONGO_CAT_SETTINGS_EXTENSIONS_CAP];
+} BongoCatSettings;
+
+typedef struct BongoCatSessionState {
+ BongoCatWindowState window;
+ char active_model_id[BONGO_CAT_ID_CAP];
+ int last_update_check_day;
+ char last_update_check_version[BONGO_CAT_UPDATE_VERSION_CAP];
+ char available_update_version[BONGO_CAT_UPDATE_VERSION_CAP];
+ char additional_model_ids[BONGO_CAT_ADDITIONAL_MODEL_CAP][BONGO_CAT_ID_CAP];
+ size_t additional_model_count;
+ BongoCatActiveBehavior active_behaviors[BONGO_CAT_BEHAVIOR_BINDING_CAP];
+ size_t active_behavior_count;
+} BongoCatSessionState;
#ifdef __cplusplus
extern "C" {
#endif
-void bongo_cat_config_defaults(BongoCatConfig *config);
-void bongo_cat_config_validate(BongoCatConfig *config);
-bool bongo_cat_config_shortcut_conflicts(const BongoCatConfig *config,
+void bongo_cat_settings_defaults(BongoCatSettings *settings);
+void bongo_cat_settings_validate(BongoCatSettings *settings);
+void bongo_cat_session_defaults(BongoCatSessionState *session);
+void bongo_cat_session_validate(BongoCatSessionState *session);
+bool bongo_cat_session_model_active(const BongoCatSessionState *session,
+ const char *model_id);
+bool bongo_cat_session_add_model(BongoCatSessionState *session,
+ const char *model_id);
+bool bongo_cat_session_remove_model(BongoCatSessionState *session,
+ const char *model_id);
+void bongo_cat_session_clear_additional_models(BongoCatSessionState *session);
+bool bongo_cat_settings_shortcut_conflicts(const BongoCatSettings *settings,
const char *shortcut, const char *exclude);
-BongoCatResult bongo_cat_preferences_load(const char *path,
- BongoCatConfig *config, BongoCatError *error);
-BongoCatResult bongo_cat_preferences_save(const char *path,
- const BongoCatConfig *config, BongoCatError *error);
+const char *bongo_cat_settings_model_label(const BongoCatSettings *settings,
+ const char *id);
+bool bongo_cat_settings_set_model_label(BongoCatSettings *settings,
+ const char *id, const char *label);
+bool bongo_cat_settings_model_removed(const BongoCatSettings *settings,
+ const char *id);
+bool bongo_cat_settings_set_model_removed(BongoCatSettings *settings,
+ const char *id, bool removed);
+bool bongo_cat_settings_restore_model_package(BongoCatSettings *settings,
+ const char *package_id);
+BongoCatResult bongo_cat_settings_load(const char *path,
+ BongoCatSettings *settings, BongoCatError *error);
+BongoCatResult bongo_cat_settings_save(const char *path,
+ const BongoCatSettings *settings, BongoCatError *error);
BongoCatResult bongo_cat_session_load(const char *path,
- BongoCatConfig *config, BongoCatError *error);
+ BongoCatSessionState *session, BongoCatError *error);
BongoCatResult bongo_cat_session_save(const char *path,
- const BongoCatConfig *config, BongoCatError *error);
+ const BongoCatSessionState *session, BongoCatError *error);
const char *bongo_cat_theme_name(BongoCatTheme value);
const char *bongo_cat_language_name(BongoCatLanguage value);
+bool bongo_cat_language_parse(const char *name, BongoCatLanguage *value);
+bool bongo_cat_language_from_locale(const char *language,
+ const char *country, BongoCatLanguage *value);
const char *bongo_cat_mode_name(BongoCatModelMode value);
+const char *bongo_cat_obs_background_color_name(
+ BongoCatObsBackgroundColor value);
+uint32_t bongo_cat_obs_background_color_rgb(
+ BongoCatObsBackgroundColor value);
#ifdef __cplusplus
}
diff --git a/include/bongo_cat/file.h b/include/bongo_cat/file.h
index a0ebae88..328c3ea7 100644
--- a/include/bongo_cat/file.h
+++ b/include/bongo_cat/file.h
@@ -9,6 +9,7 @@ extern "C" {
#endif
FILE *bongo_cat_file_open(const char *path, const char *mode);
+bool bongo_cat_file_append(const char *path, const void *data, size_t size);
bool bongo_cat_file_remove(const char *path);
bool bongo_cat_file_replace(const char *source, const char *target, bool durable);
diff --git a/include/bongo_cat/gl_api.h b/include/bongo_cat/gl_api.h
index a2f6e91a..778b5b7e 100644
--- a/include/bongo_cat/gl_api.h
+++ b/include/bongo_cat/gl_api.h
@@ -4,6 +4,10 @@
#include "bongo_cat/common.h"
#include
+#ifdef __cplusplus
+extern "C" {
+#endif
+
typedef struct BongoCatGL {
PFNGLCREATESHADERPROC create_shader;
PFNGLSHADERSOURCEPROC shader_source;
@@ -40,4 +44,8 @@ bool bongo_cat_gl_clear_errors(void);
unsigned int bongo_cat_gl_program(BongoCatGL *gl, const char *vertex, const char *fragment,
BongoCatError *error);
+#ifdef __cplusplus
+}
+#endif
+
#endif
diff --git a/include/bongo_cat/image.h b/include/bongo_cat/image.h
index 5026bf50..055a1cff 100644
--- a/include/bongo_cat/image.h
+++ b/include/bongo_cat/image.h
@@ -18,12 +18,14 @@ typedef struct BongoCatImageAlphaMask {
unsigned char pixels[BONGO_CAT_ALPHA_MASK_SIZE * BONGO_CAT_ALPHA_MASK_SIZE];
int width, height;
} BongoCatImageAlphaMask;
+typedef void (*BongoCatImageProgress)(void *userdata, float progress);
#ifdef __cplusplus
extern "C" {
#endif
BongoCatResult bongo_cat_image_load(const char *path, BongoCatImage *image, BongoCatError *error);
+bool bongo_cat_image_info(const char *path, int *width, int *height);
void bongo_cat_image_free(BongoCatImage *image);
unsigned int bongo_cat_image_texture(const char *path, int *width, int *height,
BongoCatError *error);
@@ -32,8 +34,10 @@ unsigned int bongo_cat_image_texture_thumbnail(const char *path, int max_width,
unsigned int bongo_cat_image_texture_resampled(const char *path,
int max_width, int max_height, float rounding, int *width, int *height,
BongoCatError *error);
+/* Returns a premultiplied-alpha texture for the Cubism renderer. */
unsigned int bongo_cat_image_texture_model(const char *path, bool direct_decode,
- int *width, int *height, BongoCatImageAlphaMask *alpha, BongoCatError *error);
+ int *width, int *height, BongoCatImageAlphaMask *alpha,
+ BongoCatImageProgress progress, void *userdata, BongoCatError *error);
void bongo_cat_image_make_alpha_mask(const BongoCatImage *image,
BongoCatImageAlphaMask *mask);
unsigned int bongo_cat_image_composite_texture(const char *base, const char *left,
diff --git a/include/bongo_cat/input.h b/include/bongo_cat/input.h
index c07882e3..29f503b6 100644
--- a/include/bongo_cat/input.h
+++ b/include/bongo_cat/input.h
@@ -29,12 +29,13 @@ typedef struct BongoCatInputEvent {
uint64_t sequence;
} BongoCatInputEvent;
-typedef struct BongoCatAutoRelease {
+typedef struct BongoCatScheduledRelease {
uint64_t deadline_ms;
char name[BONGO_CAT_ID_CAP];
-} BongoCatAutoRelease;
+} BongoCatScheduledRelease;
typedef struct BongoCatInputState {
+ atomic_bool queue_busy;
BongoCatInputEvent queue[BONGO_CAT_INPUT_QUEUE_CAP];
atomic_uint_fast16_t head;
atomic_uint_fast16_t tail;
@@ -48,8 +49,9 @@ typedef struct BongoCatInputState {
BongoCatInputEvent recovery[BONGO_CAT_INPUT_RECOVERY_CAP];
atomic_uint_fast8_t recovery_head;
atomic_uint_fast8_t recovery_tail;
- BongoCatAutoRelease releases[BONGO_CAT_AUTO_RELEASE_CAP];
- size_t release_count;
+ BongoCatScheduledRelease scheduled_releases[
+ BONGO_CAT_SCHEDULED_RELEASE_CAP];
+ size_t scheduled_release_count;
} BongoCatInputState;
void bongo_cat_input_init(BongoCatInputState *state);
@@ -61,9 +63,9 @@ bool bongo_cat_input_mouse(BongoCatInputState *state, double x, double y);
bool bongo_cat_input_take_mouse(BongoCatInputState *state, double *x, double *y);
bool bongo_cat_input_control_down(const BongoCatInputState *state);
bool bongo_cat_input_shift_down(const BongoCatInputState *state);
-void bongo_cat_input_auto_release(BongoCatInputState *state,
+void bongo_cat_input_schedule_release(BongoCatInputState *state,
const BongoCatInputEvent *event, uint64_t delay_ms);
-bool bongo_cat_input_take_release(BongoCatInputState *state, uint64_t now_ms,
- BongoCatInputEvent *event);
+bool bongo_cat_input_take_scheduled_release(BongoCatInputState *state,
+ uint64_t now_ms, BongoCatInputEvent *event);
#endif
diff --git a/include/bongo_cat/json.h b/include/bongo_cat/json.h
index e4b9dbac..7ca8f379 100644
--- a/include/bongo_cat/json.h
+++ b/include/bongo_cat/json.h
@@ -10,6 +10,10 @@ extern "C" {
yyjson_doc *bongo_cat_json_read_file(const char *path,
yyjson_read_flag flags, yyjson_read_err *error);
+/* Model compatibility is applied in memory; authored files are never rewritten. */
+yyjson_doc *bongo_cat_model_json_parse(const char *data, size_t size,
+ bool *normalized);
+yyjson_doc *bongo_cat_model_json_read(const char *path, bool *normalized);
bool bongo_cat_json_write_file(const char *path,
const yyjson_mut_doc *document, yyjson_write_flag flags,
yyjson_write_err *error);
diff --git a/include/bongo_cat/log.h b/include/bongo_cat/log.h
new file mode 100644
index 00000000..772346b7
--- /dev/null
+++ b/include/bongo_cat/log.h
@@ -0,0 +1,20 @@
+#ifndef BONGO_CAT_LOG_H
+#define BONGO_CAT_LOG_H
+
+#include
+
+enum {
+ BONGO_CAT_LOG_LIFECYCLE = SDL_LOG_CATEGORY_CUSTOM,
+ BONGO_CAT_LOG_UPDATE,
+ BONGO_CAT_LOG_INPUT
+};
+
+static inline bool bongo_cat_log_enabled(int category, SDL_LogPriority priority) {
+ return priority >= SDL_LOG_PRIORITY_WARN ||
+ (priority == SDL_LOG_PRIORITY_INFO &&
+ (category == BONGO_CAT_LOG_LIFECYCLE ||
+ category == BONGO_CAT_LOG_UPDATE ||
+ category == BONGO_CAT_LOG_INPUT));
+}
+
+#endif
diff --git a/include/bongo_cat/model.h b/include/bongo_cat/model.h
index b0dd0cc3..b3c7258a 100644
--- a/include/bongo_cat/model.h
+++ b/include/bongo_cat/model.h
@@ -3,14 +3,44 @@
#include "bongo_cat/config.h"
+typedef enum BongoCatModelSourceFormat {
+ BONGO_CAT_MODEL_SOURCE_UNKNOWN,
+ BONGO_CAT_MODEL_SOURCE_BUILTIN,
+ BONGO_CAT_MODEL_SOURCE_TAURI,
+ BONGO_CAT_MODEL_SOURCE_MVER,
+ BONGO_CAT_MODEL_SOURCE_MVER_PATCH
+} BongoCatModelSourceFormat;
+
+typedef enum BongoCatModelCapability {
+ BONGO_CAT_MODEL_CAPABILITY_LIVE2D = 1u << 0,
+ BONGO_CAT_MODEL_CAPABILITY_PREVIEW = 1u << 1,
+ BONGO_CAT_MODEL_CAPABILITY_RUNTIME_ADAPTER = 1u << 2,
+ BONGO_CAT_MODEL_CAPABILITY_INPUT_IMAGES = 1u << 3,
+ BONGO_CAT_MODEL_CAPABILITY_KEYBOARD_INPUT = 1u << 4,
+ BONGO_CAT_MODEL_CAPABILITY_GAMEPAD_INPUT = 1u << 5,
+ BONGO_CAT_MODEL_CAPABILITY_BEHAVIORS = 1u << 6,
+ BONGO_CAT_MODEL_CAPABILITY_AUDIO = 1u << 7,
+ BONGO_CAT_MODEL_CAPABILITY_EFFECTS = 1u << 8,
+ BONGO_CAT_MODEL_CAPABILITY_MVER_PROJECTION = 1u << 9,
+ BONGO_CAT_MODEL_CAPABILITY_POINTER_OVERLAY = 1u << 10,
+ BONGO_CAT_MODEL_CAPABILITY_IMAGE_PATCH = 1u << 11
+} BongoCatModelCapability;
+
typedef struct BongoCatModelEntry {
char id[BONGO_CAT_ID_CAP];
+ char package_id[BONGO_CAT_ID_CAP];
+ char content_digest[65];
+ char family_id[BONGO_CAT_ID_CAP];
char display_name[BONGO_CAT_ID_CAP];
char directory[BONGO_CAT_PATH_CAP];
char adapter_directory[BONGO_CAT_PATH_CAP];
char storage_directory[BONGO_CAT_PATH_CAP];
char setting_file[BONGO_CAT_PATH_CAP];
BongoCatModelMode mode;
+ BongoCatModelSourceFormat source_format;
+ uint32_t capabilities;
+ int adapter_schema;
+ int adapter_generator;
bool preset;
bool managed;
} BongoCatModelEntry;
@@ -36,6 +66,8 @@ typedef struct BongoCatBehaviorEntry {
int index;
BongoCatBehaviorKind kind;
bool momentary;
+ bool sound_overlap;
+ bool sound_clear;
} BongoCatBehaviorEntry;
typedef struct BongoCatBehaviorCatalog {
@@ -52,11 +84,17 @@ BongoCatResult bongo_cat_models_scan(BongoCatModelCatalog *catalog, const char *
bool preset, BongoCatError *error);
const BongoCatModelEntry *bongo_cat_models_find(const BongoCatModelCatalog *catalog,
const char *id);
+bool bongo_cat_model_adapter_metadata_path(const char *directory,
+ char *path, size_t capacity);
+const char *bongo_cat_model_default_name(const BongoCatModelEntry *entry);
+const char *bongo_cat_model_name(const BongoCatSettings *settings,
+ const BongoCatModelEntry *entry);
BongoCatResult bongo_cat_behaviors_load(BongoCatBehaviorCatalog *catalog,
const BongoCatModelEntry *model, BongoCatError *error);
typedef struct BongoCatLive2D BongoCatLive2D;
typedef struct BongoCatParameterRange { float minimum, maximum, value; } BongoCatParameterRange;
+typedef void (*BongoCatLive2DLoadProgress)(void *userdata, float progress);
typedef struct BongoCatLive2DVisualState {
float fit_scale, fit_translate_x, fit_translate_y;
float visible_min_x, visible_min_y, visible_max_x, visible_max_y;
@@ -65,8 +103,16 @@ typedef struct BongoCatLive2DVisualState {
bool fitted, visible, mver_projection;
} BongoCatLive2DVisualState;
+/* Extra transparent space around the authored model canvas, expressed as a
+ fraction of that canvas dimension. For example, top=0.5 reserves half a
+ canvas height above the normal composition. */
+typedef struct BongoCatLive2DFrame {
+ float left, top, right, bottom;
+} BongoCatLive2DFrame;
+
typedef struct BongoCatLive2DRenderOptions {
bool mver_projection;
+ bool auto_frame;
bool source_mirror;
bool custom_pointer_bounds;
bool pointer_left_handed;
@@ -87,8 +133,17 @@ BongoCatLive2D *bongo_cat_live2d_create(const char *asset_root, BongoCatError *e
void bongo_cat_live2d_destroy(BongoCatLive2D *live2d);
BongoCatResult bongo_cat_live2d_load(BongoCatLive2D *live2d, const char *model_dir,
const char *setting_file, bool preset,
- const BongoCatLive2DRenderOptions *render_options, BongoCatError *error);
+ const BongoCatLive2DRenderOptions *render_options,
+ BongoCatLive2DLoadProgress progress, void *userdata,
+ BongoCatError *error);
bool bongo_cat_live2d_ready(const BongoCatLive2D *live2d);
+/* Returns the authored pixel canvas size of the loaded model. */
+bool bongo_cat_live2d_canvas_size(const BongoCatLive2D *live2d,
+ int *width, int *height);
+bool bongo_cat_live2d_frame(const BongoCatLive2D *live2d,
+ BongoCatLive2DFrame *frame);
+bool bongo_cat_live2d_viewport(const BongoCatLive2D *live2d,
+ int *x, int *y, int *width, int *height);
void bongo_cat_live2d_resize(BongoCatLive2D *live2d, int width, int height);
void bongo_cat_live2d_reshape(BongoCatLive2D *live2d, int width, int height);
bool bongo_cat_live2d_update(BongoCatLive2D *live2d, float delta_seconds);
@@ -97,11 +152,31 @@ void bongo_cat_live2d_set_mirror(BongoCatLive2D *live2d, bool mirror);
void bongo_cat_live2d_set_render_options(BongoCatLive2D *live2d,
const BongoCatLive2DRenderOptions *options);
void bongo_cat_live2d_set_dragging(BongoCatLive2D *live2d, float x, float y);
+void bongo_cat_live2d_set_centered_dragging(BongoCatLive2D *live2d,
+ float x, float y);
void bongo_cat_live2d_prepare_viewer_audit(BongoCatLive2D *live2d);
+/* Synchronize drawables from the current state before a cover-only frame. */
+bool bongo_cat_live2d_prepare_cover_capture(BongoCatLive2D *live2d);
bool bongo_cat_live2d_set_parameter(BongoCatLive2D *live2d, const char *id, float value);
bool bongo_cat_live2d_parameter(BongoCatLive2D *live2d, const char *id,
BongoCatParameterRange *range);
bool bongo_cat_live2d_start_motion(BongoCatLive2D *live2d, const char *group, int index);
+bool bongo_cat_live2d_restore_motion_state(BongoCatLive2D *live2d,
+ const char *group, int index);
+bool bongo_cat_live2d_preview_motion(BongoCatLive2D *live2d,
+ const char *group, int index);
+bool bongo_cat_live2d_restore_motion_preview(BongoCatLive2D *live2d);
+bool bongo_cat_live2d_commit_motion_preview(BongoCatLive2D *live2d,
+ const char *group, int index);
+bool bongo_cat_live2d_motion_selected(const BongoCatLive2D *live2d,
+ const char *group, int index);
+bool bongo_cat_live2d_motion_persistent(const BongoCatLive2D *live2d,
+ const char *group, int index);
+bool bongo_cat_live2d_motion_visible(const BongoCatLive2D *live2d,
+ const char *group, int index);
+bool bongo_cat_live2d_motion_same_toggle(const BongoCatLive2D *live2d,
+ const char *left_group, int left_index,
+ const char *right_group, int right_index);
bool bongo_cat_live2d_set_expression(BongoCatLive2D *live2d, int index);
int bongo_cat_live2d_expression(const BongoCatLive2D *live2d);
bool bongo_cat_live2d_visual_state(const BongoCatLive2D *live2d,
diff --git a/include/bongo_cat/mouse.h b/include/bongo_cat/mouse.h
index f8646bcd..b78ce9f3 100644
--- a/include/bongo_cat/mouse.h
+++ b/include/bongo_cat/mouse.h
@@ -2,6 +2,7 @@
#define BONGO_CAT_MOUSE_H
#include
+#include
typedef struct BongoCatMouseTracking {
double target_x;
@@ -12,9 +13,14 @@ typedef struct BongoCatMouseTracking {
bool settled;
} BongoCatMouseTracking;
+bool bongo_cat_mouse_sample_fresh(uint64_t now_ns, uint64_t sample_ns);
void bongo_cat_mouse_target(BongoCatMouseTracking *tracking, double x, double y);
bool bongo_cat_mouse_step(BongoCatMouseTracking *tracking, float delta_seconds,
double *x, double *y);
+float bongo_cat_mouse_centered_ratio(double position, double center,
+ double minimum, double maximum);
+void bongo_cat_mouse_drag_coordinates(float x_ratio, float y_ratio,
+ bool horizontal_mirror, float *x, float *y);
float bongo_cat_mouse_parameter_value(float minimum, float maximum,
float x_ratio, float y_ratio, char axis, bool mirror);
diff --git a/include/bongo_cat/overlay.h b/include/bongo_cat/overlay.h
index b749ad23..c59e5526 100644
--- a/include/bongo_cat/overlay.h
+++ b/include/bongo_cat/overlay.h
@@ -1,14 +1,16 @@
#ifndef BONGO_CAT_OVERLAY_H
#define BONGO_CAT_OVERLAY_H
-#include "bongo_cat/common.h"
+#include "bongo_cat/model.h"
typedef struct BongoCatOverlay BongoCatOverlay;
BongoCatOverlay *bongo_cat_overlay_create(BongoCatError *error);
void bongo_cat_overlay_destroy(BongoCatOverlay *overlay);
-BongoCatResult bongo_cat_overlay_load(BongoCatOverlay *overlay, const char *model_directory,
- BongoCatError *error);
+void bongo_cat_overlay_clear(BongoCatOverlay *overlay);
+BongoCatResult bongo_cat_overlay_load(BongoCatOverlay *overlay,
+ const char *model_directory, bool model_pointer_preferred,
+ const BongoCatLive2DRenderOptions *render_options, BongoCatError *error);
int bongo_cat_overlay_key(BongoCatOverlay *overlay, const char *name, bool pressed);
bool bongo_cat_overlay_effect(BongoCatOverlay *overlay, const char *path);
bool bongo_cat_overlay_hand_active(const BongoCatOverlay *overlay, bool right);
diff --git a/include/bongo_cat/platform.h b/include/bongo_cat/platform.h
index b01a2958..a0a4b031 100644
--- a/include/bongo_cat/platform.h
+++ b/include/bongo_cat/platform.h
@@ -13,9 +13,9 @@ typedef struct BongoCatPlatform {
BongoCatInputState *input;
void *native;
void *presenter;
- void *relative_pointer;
uint32_t wake_event_type;
float window_opacity;
+ bool hover_hide_unavailable;
} BongoCatPlatform;
typedef enum BongoCatMenuAction {
@@ -52,9 +52,11 @@ typedef enum BongoCatMenuAction {
BONGO_CAT_MENU_OPACITY_100,
BONGO_CAT_MENU_EXIT,
BONGO_CAT_MENU_MODEL_ADD,
+ BONGO_CAT_MENU_REMOVE_PET,
BONGO_CAT_MENU_MODEL_FIRST = 1000,
BONGO_CAT_MENU_MOTION_FIRST = 2000,
- BONGO_CAT_MENU_EXPRESSION_FIRST = 3000
+ BONGO_CAT_MENU_EXPRESSION_FIRST = 3000,
+ BONGO_CAT_MENU_AUDIO_FIRST = 4000
} BongoCatMenuAction;
typedef void (*BongoCatMenuPreview)(void *userdata, BongoCatMenuAction action);
@@ -62,18 +64,30 @@ typedef struct BongoCatMenuLabels {
const char *preferences, *hide, *pass_through, *always_on_top;
const char *window_size, *opacity, *model, *add_model, *exit;
const char *wheel_size_hint, *wheel_opacity_hint, *motion, *expression;
- const char *const *model_names, *const *motion_names, *const *expression_names;
- size_t model_count, current_model, motion_count, expression_count, current_expression;
+ const char *const *model_names;
+ const char (*motion_names)[BONGO_CAT_MENU_LABEL_CAP];
+ const char (*expression_names)[BONGO_CAT_MENU_LABEL_CAP];
+ const bool *motion_checked;
+ size_t model_count, current_model, motion_count;
+ size_t expression_count, current_expression;
float scale_percent, opacity_percent;
bool pass_through_checked, always_on_top_checked, dark_theme;
BongoCatMenuPreview preview;
void (*preview_tick)(void *userdata);
BongoCatMenuPreview restore;
void *preview_userdata;
+ const char *remove_pet;
+ bool remove_pet_visible;
+ const char *audio;
+ const char (*audio_names)[BONGO_CAT_MENU_LABEL_CAP];
+ const bool *audio_checked;
+ size_t audio_count;
+ const char *const *model_cover_directories;
} BongoCatMenuLabels;
typedef void (*BongoCatTrayClick)(void *userdata);
typedef void (*BongoCatModalTick)(void *userdata);
+typedef void (*BongoCatTrayRestore)(void *userdata);
BongoCatResult bongo_cat_platform_init(BongoCatPlatform *platform, SDL_Window *window,
BongoCatInputState *input, BongoCatError *error);
@@ -88,11 +102,16 @@ bool bongo_cat_platform_frame_alpha(const BongoCatPlatform *platform,
void bongo_cat_platform_set_visible(BongoCatPlatform *platform, bool visible);
bool bongo_cat_platform_pointer_local(BongoCatPlatform *platform, double screen_x,
double screen_y, float *local_x, float *local_y);
+/* Reports a foreground application's fixed/locked system cursor state. */
+bool bongo_cat_platform_pointer_locked(BongoCatPlatform *platform);
bool bongo_cat_platform_relative_pointer(BongoCatPlatform *platform,
double *x, double *y);
void bongo_cat_platform_relative_pointer_reset(BongoCatPlatform *platform);
+void bongo_cat_platform_relative_pointer_release(BongoCatPlatform *platform);
void bongo_cat_platform_set_always_on_top(BongoCatPlatform *platform, bool enabled);
void bongo_cat_platform_raise_window(SDL_Window *window);
+/* Configure platform-native chrome for the preferences window when available. */
+void bongo_cat_platform_configure_preferences_window(SDL_Window *window);
bool bongo_cat_platform_open_directory(const char *path);
bool bongo_cat_platform_set_geometry(BongoCatPlatform *platform,
int x, int y, int width, int height);
@@ -101,13 +120,23 @@ void bongo_cat_platform_begin_drag(BongoCatPlatform *platform,
bool bongo_cat_platform_dynamic_hit_supported(void);
void bongo_cat_platform_set_tray_callbacks(void *tray,
BongoCatTrayClick left_click, BongoCatModalTick modal_tick,
- void *userdata);
+ BongoCatTrayRestore restore, void *userdata);
bool bongo_cat_platform_single_instance_begin(void);
bool bongo_cat_platform_single_instance_take_wake(void);
+bool bongo_cat_platform_update_shutdown_argument(int argc, char **argv);
+bool bongo_cat_platform_single_instance_take_update_shutdown(void);
void bongo_cat_platform_single_instance_end(void);
BongoCatResult bongo_cat_platform_set_autostart(bool enabled, BongoCatError *error);
BongoCatMenuAction bongo_cat_platform_context_menu(BongoCatPlatform *platform,
const BongoCatMenuLabels *labels);
BongoCatResult bongo_cat_platform_embedded_assets(const char *target, BongoCatError *error);
+#ifdef __APPLE__
+/* Read the permission macOS grants the app right now, without prompting.
+ Callers refresh on a user action or a focus change, never per frame. */
+bool bongo_cat_platform_input_monitoring_authorized(void);
+/* Ask macOS for Input Monitoring; only an explicit user action may call it. */
+bool bongo_cat_platform_input_monitoring_request(void);
+#endif
+
#endif
diff --git a/include/bongo_cat/preferences.h b/include/bongo_cat/preferences.h
index 28613b98..1df98f4b 100644
--- a/include/bongo_cat/preferences.h
+++ b/include/bongo_cat/preferences.h
@@ -13,7 +13,9 @@ void bongo_cat_preferences_destroy(BongoCatPreferences *preferences);
void bongo_cat_preferences_show(BongoCatPreferences *preferences);
void bongo_cat_preferences_close(BongoCatPreferences *preferences);
bool bongo_cat_preferences_visible(const BongoCatPreferences *preferences);
-bool bongo_cat_preferences_needs_frame(const BongoCatPreferences *preferences);
+bool bongo_cat_preferences_needs_frame(BongoCatPreferences *preferences);
+void bongo_cat_preferences_model_watch(BongoCatPreferences *preferences,
+ uint64_t now);
void bongo_cat_preferences_input_begin(BongoCatPreferences *preferences);
void bongo_cat_preferences_input_end(BongoCatPreferences *preferences);
bool bongo_cat_preferences_event(BongoCatPreferences *preferences, const SDL_Event *event);
@@ -21,6 +23,11 @@ bool bongo_cat_preferences_shortcuts_blocked(
const BongoCatPreferences *preferences);
void bongo_cat_preferences_render(BongoCatPreferences *preferences);
void bongo_cat_preferences_invalidate(BongoCatPreferences *preferences);
+void bongo_cat_preferences_process_model_selection(
+ BongoCatPreferences *preferences);
+void bongo_cat_preferences_model_load_progress(
+ BongoCatPreferences *preferences, float progress);
+void bongo_cat_preferences_models_changed(BongoCatPreferences *preferences);
void bongo_cat_preferences_request_model_import(BongoCatPreferences *preferences);
bool bongo_cat_preferences_open_model_import(BongoCatPreferences *preferences,
SDL_Window *parent);
diff --git a/include/bongo_cat/shortcut.h b/include/bongo_cat/shortcut.h
index ca30a493..731fcffc 100644
--- a/include/bongo_cat/shortcut.h
+++ b/include/bongo_cat/shortcut.h
@@ -12,6 +12,8 @@ typedef struct BongoCatShortcutState {
} BongoCatShortcutState;
void bongo_cat_shortcut_init(BongoCatShortcutState *state);
+/* Format UI labels without changing the stored binding. */
+void bongo_cat_shortcut_format(const char *shortcut, char *output, size_t capacity);
bool bongo_cat_shortcut_update(BongoCatShortcutState *state, const BongoCatInputEvent *event);
bool bongo_cat_shortcut_matches(const BongoCatShortcutState *state,
const BongoCatInputEvent *event, const char *shortcut);
diff --git a/include/bongo_cat/sound_shortcut.h b/include/bongo_cat/sound_shortcut.h
new file mode 100644
index 00000000..64b93eef
--- /dev/null
+++ b/include/bongo_cat/sound_shortcut.h
@@ -0,0 +1,18 @@
+#ifndef BONGO_CAT_SOUND_SHORTCUT_H
+#define BONGO_CAT_SOUND_SHORTCUT_H
+
+#include "bongo_cat/input.h"
+
+/* Stores physical keys separately so releasing one Ctrl does not release
+ the other. Only changed input edges require reevaluating bindings. */
+typedef struct BongoCatSoundShortcutState {
+ char held[BONGO_CAT_INPUT_KEY_STATE_CAP][BONGO_CAT_ID_CAP];
+ size_t count;
+} BongoCatSoundShortcutState;
+
+bool bongo_cat_sound_shortcut_update(BongoCatSoundShortcutState *state,
+ const BongoCatInputEvent *event);
+bool bongo_cat_sound_shortcut_down(const BongoCatSoundShortcutState *state,
+ const char *shortcut);
+
+#endif
diff --git a/include/bongo_cat/tray.h b/include/bongo_cat/tray.h
index b445bb48..14ef758a 100644
--- a/include/bongo_cat/tray.h
+++ b/include/bongo_cat/tray.h
@@ -9,6 +9,7 @@ typedef struct BongoCatTray BongoCatTray;
BongoCatTray *bongo_cat_tray_create(BongoCatApp *app, BongoCatError *error);
void bongo_cat_tray_destroy(BongoCatTray *tray);
void bongo_cat_tray_sync(BongoCatTray *tray);
+void bongo_cat_tray_prepare_menu(BongoCatTray *tray, void *native_handle);
bool bongo_cat_tray_self_test(BongoCatTray *tray);
#endif
diff --git a/include/bongo_cat/update.h b/include/bongo_cat/update.h
new file mode 100644
index 00000000..e58033f9
--- /dev/null
+++ b/include/bongo_cat/update.h
@@ -0,0 +1,30 @@
+#ifndef BONGO_CAT_UPDATE_H
+#define BONGO_CAT_UPDATE_H
+
+#include "bongo_cat/common.h"
+
+#define BONGO_CAT_UPDATE_URL_CAP 512
+#define BONGO_CAT_UPDATE_NOTES_CAP 8192
+
+typedef struct BongoCatUpdateRelease {
+ char version[BONGO_CAT_UPDATE_VERSION_CAP];
+ char release_url[BONGO_CAT_UPDATE_URL_CAP];
+ char installer_url[BONGO_CAT_UPDATE_URL_CAP];
+ char portable_url[BONGO_CAT_UPDATE_URL_CAP];
+ char notes[BONGO_CAT_UPDATE_NOTES_CAP];
+} BongoCatUpdateRelease;
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+int bongo_cat_update_compare_versions(const char *left, const char *right);
+bool bongo_cat_update_version_valid(const char *version);
+bool bongo_cat_update_parse_release(const char *json, const char *platform,
+ BongoCatUpdateRelease *release, BongoCatError *error);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif
diff --git a/include/bongo_cat/utf8.h b/include/bongo_cat/utf8.h
index beab0859..1ae9e3f6 100644
--- a/include/bongo_cat/utf8.h
+++ b/include/bongo_cat/utf8.h
@@ -5,7 +5,7 @@
#include
bool bongo_cat_utf8_valid(const char *text);
-bool bongo_cat_utf8_normalize_legacy(const char *text,
+bool bongo_cat_utf8_normalize_mver(const char *text,
char *output, size_t capacity);
#endif
diff --git a/packaging/get-project-version.ps1 b/packaging/get-project-version.ps1
new file mode 100644
index 00000000..f80705bb
--- /dev/null
+++ b/packaging/get-project-version.ps1
@@ -0,0 +1,26 @@
+[CmdletBinding()]
+param()
+
+$ErrorActionPreference = 'Stop'
+Set-StrictMode -Version Latest
+
+$repositoryRoot = Split-Path $PSScriptRoot -Parent
+$cmakeLists = Get-Content -LiteralPath (
+ Join-Path $repositoryRoot 'CMakeLists.txt') -Raw
+if ($cmakeLists -notmatch
+ '(?m)^\s*set\(BONGO_CAT_VERSION\s+"?([0-9]+)\.([0-9]+)\.([0-9]+)') {
+ throw 'Unable to read the BongoCat version from CMakeLists.txt.'
+}
+
+$parts = @([int]$Matches[1], [int]$Matches[2], [int]$Matches[3])
+if ($parts | Where-Object { $_ -gt 65535 }) {
+ throw 'Version components must be between 0 and 65535.'
+}
+if ($parts[0] -eq 0) {
+ throw 'The major version must be greater than zero for Microsoft Store packages.'
+}
+
+[PSCustomObject]@{
+ AppVersion = $parts -join '.'
+ StorePackageVersion = "$($parts[0]).$($parts[1]).$($parts[2]).0"
+}
diff --git a/packaging/linux/bongocat.desktop b/packaging/linux/bongocat.desktop
new file mode 100644
index 00000000..ea8969ec
--- /dev/null
+++ b/packaging/linux/bongocat.desktop
@@ -0,0 +1,8 @@
+[Desktop Entry]
+Type=Application
+Name=BongoCat
+Comment=An animated desktop cat that reacts to your input
+Exec=BongoCat
+Icon=bongocat
+Terminal=false
+Categories=Utility;
diff --git a/packaging/linux/build-appimage.sh b/packaging/linux/build-appimage.sh
new file mode 100644
index 00000000..9fa163ec
--- /dev/null
+++ b/packaging/linux/build-appimage.sh
@@ -0,0 +1,43 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+if [[ $(uname -s) != Linux || $(uname -m) != x86_64 ]]; then
+ echo 'AppImage packaging requires Linux x86_64.' >&2
+ exit 1
+fi
+build_dir=$(cd "${1:?Usage: build-appimage.sh BUILD_DIRECTORY}" && pwd)
+source_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)
+name=$(tr -d '\r\n' < "$build_dir/bongocat-package-name.txt")
+[[ $name =~ ^BongoCat(-Diagnostic)?-[0-9][A-Za-z0-9.+-]*-linux-x64$ ]] || {
+ echo "Unexpected package name: $name" >&2
+ exit 1
+}
+work=$(mktemp -d "$build_dir/appimage.XXXXXXXX")
+trap 'rm -rf -- "$work"' EXIT
+mkdir -p "$build_dir/dist"
+
+# A private staging tree keeps repeated builds free of stale dependencies.
+# Keep assets next to the executable, as expected by the runtime.
+appdir="$work/BongoCat.AppDir"
+cmake --install "$build_dir" --component Runtime --prefix "$appdir/usr/bin"
+
+tool="$work/linuxdeploy.AppImage"
+curl --fail --location --retry 3 --output "$tool" \
+ https://github.com/linuxdeploy/linuxdeploy/releases/download/1-alpha-20250213-2/linuxdeploy-x86_64.AppImage
+chmod +x "$tool"
+export ARCH=x86_64
+export VERSION="${name#BongoCat-}"
+export OUTPUT="$build_dir/dist/$name.AppImage"
+# Build runners do not need FUSE, including subprocess AppImage tools.
+export APPIMAGE_EXTRACT_AND_RUN=1
+pushd "$build_dir" >/dev/null
+"$tool" --appdir "$appdir" \
+ --executable "$appdir/usr/bin/BongoCat" \
+ --desktop-file "$source_dir/packaging/linux/bongocat.desktop" \
+ --icon-file "$source_dir/resources/assets/bongocat.png" \
+ --output appimage
+popd >/dev/null
+test -s "$OUTPUT"
+chmod +x "$OUTPUT"
+(cd "$build_dir/dist" && sha256sum "$name.AppImage" > "$name.AppImage.sha256")
+echo "AppImage ready: $OUTPUT"
diff --git a/packaging/microsoft-store/AppxManifest.xml.in b/packaging/microsoft-store/AppxManifest.xml.in
new file mode 100644
index 00000000..5a174cd4
--- /dev/null
+++ b/packaging/microsoft-store/AppxManifest.xml.in
@@ -0,0 +1,70 @@
+
+
+
+
+
+
+ BongoCat
+ vladelaina
+ A Live2D desktop pet that reacts to keyboard and mouse input.
+ Assets\StoreLogo.png
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/packaging/microsoft-store/README.md b/packaging/microsoft-store/README.md
new file mode 100644
index 00000000..9be53df8
--- /dev/null
+++ b/packaging/microsoft-store/README.md
@@ -0,0 +1,118 @@
+# Microsoft Store packaging
+
+The protected `store-package` job in `.github/workflows/ci.yml` creates a raw
+`bongocat__x64.msix` artifact on pushes to the upstream `main`
+branch of `vladelaina/BongoCat`. The artifact is intentionally unsigned:
+Partner Center signs accepted MSIX packages with the Microsoft Store
+certificate during submission.
+
+## Store identity
+
+- Package name: `vladelaina.bongocat`
+- Publisher: `CN=5503A135-7FA4-466B-815C-DBE627F4065F`
+- Publisher display name: `vladelaina`
+- Package family name: `vladelaina.bongocat_hnew8t3b8e0t6`
+- Package SID: `S-1-15-2-1569586416-2650217304-2351454042-2692954497-3079119466-4141654193-1648471479`
+- Store ID: `9P41MLSX72XW`
+
+The package name and publisher are stored in `AppxManifest.xml.in`. The PFN
+and package SID are derived by Windows and must not be used as signing
+secrets. The Store package is x64; the separate Windows x86 workflow target
+only publishes its desktop installer and portable executable.
+
+The manifest declares a system-managed `BongoCat.lnk` on the current user's
+desktop through the `desktop7:windows.shortcut` extension. Windows creates the
+link during package registration and removes it during package removal. The
+shortcut extension is supported starting with Windows build 19645; older
+supported Windows releases ignore that extension while continuing to install
+and run the app.
+
+## App data isolation
+
+Ordinary EXE, installer, and portable launches store data under
+`%LOCALAPPDATA%\BongoCat`. When the process has an MSIX package identity,
+BongoCat instead resolves the package-private virtualization location as
+`%LOCALAPPDATA%\Packages\\LocalCache\Local\BongoCat`. Configuration,
+session state, imported models, caches, and logs therefore remain separate
+between the desktop and Microsoft Store versions. An explicit
+`--storage-root` argument remains isolated at the requested location.
+In particular, the desktop settings file is
+`%LOCALAPPDATA%\BongoCat\config\settings.json`, while the Store settings file
+is `%LOCALAPPDATA%\Packages\\LocalCache\Local\BongoCat\config\settings.json`.
+
+Package identity is detected at runtime with `GetCurrentPackageFullName`, and
+the selected mode and resolved storage path are written to `BongoCat.log`.
+
+The app version is declared only in the top-level `CMakeLists.txt` and read by
+all Store scripts through `packaging/get-project-version.ps1`. MSIX versions
+must use four numeric components, have a non-zero major component, and use a
+zero revision. The helper therefore converts `major.minor.patch` to the same
+version with a `.0` suffix, for example `1.1.0` to `1.1.0.0`. The app version
+remains in the filename so GitHub artifacts are easy to identify.
+
+## Actions SDK setup
+
+Live2D's licensed Cubism SDK is ignored by Git and cannot be checked into this
+public repository. The protected job downloads the pinned Cubism 5-r.5 archive
+and GLEW 2.2.0 archive from their official upstream URLs and verifies both
+SHA-256 hashes before building. A repository owner may override either URL and
+hash with these secrets when an approved mirror is required:
+
+- `CUBISM_SDK_ARCHIVE_URL` and `CUBISM_SDK_ARCHIVE_SHA256`
+- `CUBISM_GLEW_ARCHIVE_URL` and `CUBISM_GLEW_ARCHIVE_SHA256`
+
+Running the protected workflow is subject to the Live2D Cubism SDK licenses;
+the repository owner is responsible for satisfying their release terms.
+
+The job verifies the digests and required files before configuring CMake with
+`BONGO_CAT_REQUIRE_CUBISM=ON`; it never uploads the diagnostic backend.
+
+## Local build and install check
+
+On Windows with the Windows 10/11 SDK and the local Cubism SDK installed:
+
+```powershell
+cmake -S . -B build-cubism -G "Visual Studio 17 2022" -A x64 `
+ -DBONGO_CAT_REQUIRE_CUBISM=ON -DBONGO_CAT_WARNINGS_AS_ERRORS=ON
+cmake --build build-cubism --config Release --target bongo_cat --parallel 2
+
+.\packaging\microsoft-store\build-store-package.ps1 `
+ -ExecutablePath .\build-cubism\Release\BongoCat.exe
+```
+
+The script writes the unsigned Store submission package to
+`output\microsoft-store\bongocat__x64.msix`. Upload that file
+directly to Partner Center. It cannot be installed locally until Microsoft
+Store signs it. Validate the exact Identity, PFN, manifest and executable
+versions, architecture, payload, and unsigned submission state with:
+
+```powershell
+$projectVersion = .\packaging\get-project-version.ps1
+$msix = ".\output\microsoft-store\bongocat_$($projectVersion.AppVersion)_x64.msix"
+.\packaging\microsoft-store\validate-store-package.ps1 `
+ -PackagePath $msix
+```
+
+The protected Actions job runs this validation before uploading its artifact.
+
+To test local deployment before submission, explicitly build a separate
+self-signed package:
+
+```powershell
+$projectVersion = .\packaging\get-project-version.ps1
+$localBase = ".\output\microsoft-store\bongocat_$($projectVersion.AppVersion)_x64-local-test"
+.\packaging\microsoft-store\build-store-package.ps1 `
+ -ExecutablePath .\build-cubism\Release\BongoCat.exe `
+ -SignForLocalTesting
+
+# Run these certificate/deployment commands from an elevated PowerShell.
+Import-Certificate `
+ "$localBase.cer" `
+ -CertStoreLocation Cert:\LocalMachine\TrustedPeople | Out-Null
+Add-AppxPackage "$localBase.msix"
+Get-AppxPackage -Name vladelaina.bongocat
+```
+
+The `-local-test` certificate and package are only for sideload testing. Do not
+upload either one to Partner Center. The GitHub Actions job never passes
+`-SignForLocalTesting` and always emits the unsigned Store submission package.
diff --git a/packaging/microsoft-store/build-store-package.ps1 b/packaging/microsoft-store/build-store-package.ps1
new file mode 100644
index 00000000..1aed7337
--- /dev/null
+++ b/packaging/microsoft-store/build-store-package.ps1
@@ -0,0 +1,187 @@
+[CmdletBinding()]
+param(
+ [Parameter(Mandatory = $true)]
+ [string]$ExecutablePath,
+
+ [string]$OutputDirectory,
+ [string]$CertificateOutputPath,
+ [ValidateSet('x86', 'x64')]
+ [string]$Architecture = 'x64',
+ [switch]$SignForLocalTesting
+)
+
+$ErrorActionPreference = "Stop"
+Set-StrictMode -Version Latest
+
+$scriptDirectory = Split-Path -Parent $MyInvocation.MyCommand.Path
+$repositoryRoot = (Resolve-Path (Join-Path $scriptDirectory "..\..")).Path
+$projectVersion = & (Join-Path $repositoryRoot 'packaging\get-project-version.ps1')
+$Version = $projectVersion.AppVersion
+$PackageVersion = $projectVersion.StorePackageVersion
+
+if (-not $OutputDirectory) {
+ $OutputDirectory = Join-Path $repositoryRoot "output\microsoft-store"
+}
+
+$ExecutablePath = (Resolve-Path $ExecutablePath).Path
+$OutputDirectory = [System.IO.Path]::GetFullPath($OutputDirectory)
+
+$packageBaseName = if ($SignForLocalTesting) {
+ "bongocat_${Version}_${Architecture}-local-test"
+}
+else {
+ "bongocat_${Version}_${Architecture}"
+}
+$msixPath = Join-Path $OutputDirectory "$packageBaseName.msix"
+$temporaryRoot = Join-Path ([System.IO.Path]::GetTempPath()) ("BongoCatStorePackage_" + [guid]::NewGuid())
+$stagingDirectory = Join-Path $temporaryRoot "package"
+$assetsDirectory = Join-Path $stagingDirectory "Assets"
+$certificatePath = Join-Path $temporaryRoot "BongoCatStoreTemporary.pfx"
+$certificate = $null
+
+if ($CertificateOutputPath -and -not $SignForLocalTesting) {
+ throw "CertificateOutputPath requires SignForLocalTesting. Store submission packages are unsigned."
+}
+if ($SignForLocalTesting -and -not $CertificateOutputPath) {
+ $CertificateOutputPath = Join-Path $OutputDirectory "$packageBaseName.cer"
+}
+
+function Get-WindowsSdkTool {
+ param([Parameter(Mandatory = $true)][string]$Name)
+
+ $programFilesX86 = ${env:ProgramFiles(x86)}
+ $sdkRoot = Join-Path $programFilesX86 "Windows Kits\10\bin"
+ if (Test-Path $sdkRoot) {
+ $sdkVersions = Get-ChildItem $sdkRoot -Directory |
+ Where-Object { $_.Name -match '^\d+\.\d+' } |
+ Sort-Object { [version]$_.Name } -Descending
+
+ foreach ($sdkVersion in $sdkVersions) {
+ $candidate = Join-Path $sdkVersion.FullName "x64\$Name"
+ if (Test-Path $candidate) {
+ return $candidate
+ }
+ }
+ }
+
+ $appCertKitCandidate = Join-Path $programFilesX86 "Windows Kits\10\App Certification Kit\$Name"
+ if (Test-Path $appCertKitCandidate) {
+ return $appCertKitCandidate
+ }
+
+ throw "$Name was not found. Install the Windows 10/11 SDK."
+}
+
+function New-StoreLogo {
+ param(
+ [Parameter(Mandatory = $true)][object]$Source,
+ [Parameter(Mandatory = $true)][int]$Width,
+ [Parameter(Mandatory = $true)][int]$Height,
+ [Parameter(Mandatory = $true)][string]$Destination
+ )
+
+ $bitmap = [System.Drawing.Bitmap]::new(
+ $Width,
+ $Height,
+ [System.Drawing.Imaging.PixelFormat]::Format32bppArgb)
+ try {
+ $graphics = [System.Drawing.Graphics]::FromImage($bitmap)
+ try {
+ $graphics.Clear([System.Drawing.Color]::Transparent)
+ $graphics.CompositingQuality = [System.Drawing.Drawing2D.CompositingQuality]::HighQuality
+ $graphics.InterpolationMode = [System.Drawing.Drawing2D.InterpolationMode]::HighQualityBicubic
+ $graphics.SmoothingMode = [System.Drawing.Drawing2D.SmoothingMode]::HighQuality
+ $graphics.PixelOffsetMode = [System.Drawing.Drawing2D.PixelOffsetMode]::HighQuality
+
+ $scale = [Math]::Min($Width / $Source.Width, $Height / $Source.Height)
+ $drawWidth = [Math]::Max(1, [int][Math]::Round($Source.Width * $scale))
+ $drawHeight = [Math]::Max(1, [int][Math]::Round($Source.Height * $scale))
+ $drawX = [int](($Width - $drawWidth) / 2)
+ $drawY = [int](($Height - $drawHeight) / 2)
+ $graphics.DrawImage($Source, $drawX, $drawY, $drawWidth, $drawHeight)
+ }
+ finally {
+ $graphics.Dispose()
+ }
+
+ $bitmap.Save($Destination, [System.Drawing.Imaging.ImageFormat]::Png)
+ }
+ finally {
+ $bitmap.Dispose()
+ }
+}
+
+try {
+ New-Item -ItemType Directory -Force -Path $OutputDirectory, $stagingDirectory, $assetsDirectory | Out-Null
+ Remove-Item $msixPath -Force -ErrorAction SilentlyContinue
+
+ Copy-Item $ExecutablePath (Join-Path $stagingDirectory "BongoCat.exe")
+
+ $manifestTemplate = Get-Content (Join-Path $scriptDirectory "AppxManifest.xml.in") -Raw
+ $manifest = $manifestTemplate.Replace("@PACKAGE_VERSION@", $PackageVersion)
+ $manifest = $manifest.Replace("@PROCESSOR_ARCHITECTURE@", $Architecture)
+ [System.IO.File]::WriteAllText(
+ (Join-Path $stagingDirectory "AppxManifest.xml"),
+ $manifest,
+ [System.Text.UTF8Encoding]::new($false))
+
+ Add-Type -AssemblyName System.Drawing
+ $sourceLogo = [System.Drawing.Image]::FromFile((Join-Path $repositoryRoot "resources\assets\bongocat.png"))
+ try {
+ New-StoreLogo $sourceLogo 50 50 (Join-Path $assetsDirectory "StoreLogo.png")
+ New-StoreLogo $sourceLogo 44 44 (Join-Path $assetsDirectory "Square44x44Logo.png")
+ New-StoreLogo $sourceLogo 150 150 (Join-Path $assetsDirectory "Square150x150Logo.png")
+ }
+ finally {
+ $sourceLogo.Dispose()
+ }
+
+ $makeAppx = Get-WindowsSdkTool "makeappx.exe"
+ & $makeAppx pack /o /h SHA256 /d $stagingDirectory /p $msixPath
+ if ($LASTEXITCODE -ne 0) {
+ throw "MakeAppx failed with exit code $LASTEXITCODE."
+ }
+
+ if ($SignForLocalTesting) {
+ $signTool = Get-WindowsSdkTool "signtool.exe"
+ $publisher = "CN=5503A135-7FA4-466B-815C-DBE627F4065F"
+ $certificate = New-SelfSignedCertificate `
+ -Type Custom `
+ -KeyUsage DigitalSignature `
+ -KeyExportPolicy Exportable `
+ -CertStoreLocation "Cert:\CurrentUser\My" `
+ -TextExtension @("2.5.29.37={text}1.3.6.1.5.5.7.3.3", "2.5.29.19={text}") `
+ -Subject $publisher `
+ -FriendlyName "BongoCat temporary Microsoft Store package signing"
+
+ $CertificateOutputPath = [System.IO.Path]::GetFullPath($CertificateOutputPath)
+ New-Item -ItemType Directory -Force -Path (Split-Path $CertificateOutputPath -Parent) | Out-Null
+ Export-Certificate -Cert $certificate -FilePath $CertificateOutputPath | Out-Null
+
+ $plainPassword = [guid]::NewGuid().ToString("N")
+ $securePassword = ConvertTo-SecureString $plainPassword -AsPlainText -Force
+ Export-PfxCertificate -Cert $certificate -FilePath $certificatePath -Password $securePassword | Out-Null
+
+ & $signTool sign /fd SHA256 /f $certificatePath /p $plainPassword $msixPath
+ if ($LASTEXITCODE -ne 0) {
+ throw "SignTool failed with exit code $LASTEXITCODE."
+ }
+ }
+
+ if ($SignForLocalTesting) {
+ Write-Host "Signed local test package created: $msixPath"
+ }
+ else {
+ Write-Host "Unsigned Microsoft Store submission package created: $msixPath"
+ }
+ Write-Host "Package identity version: $PackageVersion"
+ if ($SignForLocalTesting) {
+ Write-Host "Certificate exported for local installation: $CertificateOutputPath"
+ }
+}
+finally {
+ if ($certificate) {
+ Remove-Item ("Cert:\CurrentUser\My\" + $certificate.Thumbprint) -Force -ErrorAction SilentlyContinue
+ }
+ Remove-Item $temporaryRoot -Recurse -Force -ErrorAction SilentlyContinue
+}
diff --git a/packaging/microsoft-store/validate-store-package.ps1 b/packaging/microsoft-store/validate-store-package.ps1
new file mode 100644
index 00000000..701bf88e
--- /dev/null
+++ b/packaging/microsoft-store/validate-store-package.ps1
@@ -0,0 +1,199 @@
+[CmdletBinding()]
+param(
+ [Parameter(Mandatory = $true)]
+ [string]$PackagePath,
+
+ [ValidateSet('x86', 'x64')]
+ [string]$ExpectedArchitecture = 'x64'
+)
+
+$ErrorActionPreference = 'Stop'
+Set-StrictMode -Version Latest
+
+$scriptDirectory = Split-Path -Parent $MyInvocation.MyCommand.Path
+$repositoryRoot = (Resolve-Path (Join-Path $scriptDirectory '..\..')).Path
+$projectVersion = & (Join-Path $repositoryRoot 'packaging\get-project-version.ps1')
+$ExpectedAppVersion = $projectVersion.AppVersion
+$ExpectedPackageVersion = $projectVersion.StorePackageVersion
+$PackagePath = (Resolve-Path $PackagePath).Path
+if ((Split-Path $PackagePath -Leaf) -notmatch
+ "^bongocat_$([regex]::Escape($ExpectedAppVersion))_") {
+ throw "Package filename does not match app version $ExpectedAppVersion."
+}
+
+function Get-WindowsSdkTool {
+ param([Parameter(Mandatory = $true)][string]$Name)
+
+ $sdkRoot = Join-Path ${env:ProgramFiles(x86)} 'Windows Kits\10\bin'
+ $sdkVersions = Get-ChildItem $sdkRoot -Directory |
+ Where-Object { $_.Name -match '^\d+\.\d+' } |
+ Sort-Object { [version]$_.Name } -Descending
+ foreach ($sdkVersion in $sdkVersions) {
+ $candidate = Join-Path $sdkVersion.FullName "x64\$Name"
+ if (Test-Path $candidate) { return $candidate }
+ }
+ throw "$Name was not found. Install the Windows 10/11 SDK."
+}
+
+function Get-PeMachine {
+ param([Parameter(Mandatory = $true)][string]$Path)
+
+ $stream = [IO.File]::OpenRead($Path)
+ $reader = [IO.BinaryReader]::new($stream)
+ try {
+ if ($reader.ReadUInt16() -ne 0x5A4D) {
+ throw 'The packaged executable does not have an MZ header.'
+ }
+ $stream.Position = 0x3C
+ $peOffset = $reader.ReadInt32()
+ if ($peOffset -lt 0x40 -or $peOffset -gt $stream.Length - 6) {
+ throw 'The packaged executable has an invalid PE offset.'
+ }
+ $stream.Position = $peOffset
+ if ($reader.ReadUInt32() -ne 0x00004550) {
+ throw 'The packaged executable does not have a PE signature.'
+ }
+ return $reader.ReadUInt16()
+ }
+ finally {
+ $reader.Dispose()
+ $stream.Dispose()
+ }
+}
+
+if (-not ('BongoCatPackageIdentityNative' -as [type])) {
+ Add-Type -TypeDefinition @'
+using System.Runtime.InteropServices;
+using System.Text;
+public static class BongoCatPackageIdentityNative {
+ [DllImport("kernelbase.dll", CharSet = CharSet.Unicode)]
+ public static extern int PackagePublisherIdFromPublisher(
+ string publisher, ref uint length, StringBuilder publisherId);
+}
+'@
+}
+
+$signature = Get-AuthenticodeSignature -FilePath $PackagePath
+if ($signature.Status -ne [Management.Automation.SignatureStatus]::NotSigned) {
+ throw "Store submission package must be unsigned; status is $($signature.Status)."
+}
+
+$temporaryRoot = Join-Path ([IO.Path]::GetTempPath()) (
+ 'BongoCatStoreValidation_' + [guid]::NewGuid())
+try {
+ New-Item -ItemType Directory -Path $temporaryRoot | Out-Null
+ $makeAppx = Get-WindowsSdkTool 'makeappx.exe'
+ & $makeAppx unpack /o /p $PackagePath /d $temporaryRoot | Out-Null
+ if ($LASTEXITCODE -ne 0) {
+ throw "MakeAppx unpack failed with exit code $LASTEXITCODE."
+ }
+
+ $manifestPath = Join-Path $temporaryRoot 'AppxManifest.xml'
+ [xml]$manifest = Get-Content -LiteralPath $manifestPath -Raw
+ $identity = $manifest.Package.Identity
+ $expectedIdentity = @{
+ Name = 'vladelaina.bongocat'
+ Publisher = 'CN=5503A135-7FA4-466B-815C-DBE627F4065F'
+ Version = $ExpectedPackageVersion
+ ProcessorArchitecture = $ExpectedArchitecture
+ }
+ foreach ($field in $expectedIdentity.Keys) {
+ if ($identity.$field -ne $expectedIdentity[$field]) {
+ throw "Manifest Identity $field is '$($identity.$field)'; expected '$($expectedIdentity[$field])'."
+ }
+ }
+
+ $application = $manifest.Package.Applications.Application
+ if ($application.Id -ne 'BongoCat' -or
+ $application.Executable -ne 'BongoCat.exe' -or
+ $application.EntryPoint -ne 'Windows.FullTrustApplication') {
+ throw 'Manifest application entry does not describe the BongoCat full-trust executable.'
+ }
+
+ $namespaceManager = [Xml.XmlNamespaceManager]::new($manifest.NameTable)
+ $namespaceManager.AddNamespace(
+ 'desktop7',
+ 'http://schemas.microsoft.com/appx/manifest/desktop/windows10/7')
+ $shortcutExtension = $manifest.SelectSingleNode(
+ "//*[local-name()='Application']/*[local-name()='Extensions']/" +
+ "desktop7:Extension[@Category='windows.shortcut']",
+ $namespaceManager)
+ $shortcut = if ($shortcutExtension) {
+ $shortcutExtension.SelectSingleNode('desktop7:Shortcut', $namespaceManager)
+ }
+ else {
+ $null
+ }
+ if (-not $shortcutExtension -or
+ $shortcutExtension.Executable -ne 'BongoCat.exe' -or
+ $shortcutExtension.EntryPoint -ne 'Windows.FullTrustApplication' -or
+ -not $shortcut -or
+ $shortcut.File -ne '$(Desktop)\BongoCat.lnk' -or
+ $shortcut.Icon -ne 'BongoCat.exe') {
+ throw 'Manifest does not declare the expected system-managed desktop shortcut.'
+ }
+
+ $requiredFiles = @(
+ 'BongoCat.exe',
+ 'Assets\StoreLogo.png',
+ 'Assets\Square44x44Logo.png',
+ 'Assets\Square150x150Logo.png'
+ )
+ foreach ($relativePath in $requiredFiles) {
+ if (-not (Test-Path -LiteralPath (Join-Path $temporaryRoot $relativePath))) {
+ throw "Required package payload is missing: $relativePath"
+ }
+ }
+ if (Test-Path -LiteralPath (Join-Path $temporaryRoot 'AppxSignature.p7x')) {
+ throw 'Unsigned Store submission package unexpectedly contains AppxSignature.p7x.'
+ }
+
+ $executablePath = Join-Path $temporaryRoot 'BongoCat.exe'
+ $versionInfo = (Get-Item -LiteralPath $executablePath).VersionInfo
+ $packagedAppVersion = '{0}.{1}.{2}' -f $versionInfo.FileMajorPart,
+ $versionInfo.FileMinorPart, $versionInfo.FileBuildPart
+ if ($packagedAppVersion -ne $ExpectedAppVersion) {
+ throw "Packaged executable version is $packagedAppVersion; expected $ExpectedAppVersion."
+ }
+
+ $machine = Get-PeMachine $executablePath
+ $expectedMachine = if ($ExpectedArchitecture -eq 'x64') { 0x8664 } else { 0x014C }
+ if ($machine -ne $expectedMachine) {
+ throw ('Packaged executable machine is 0x{0:X4}; expected {1}.' -f
+ $machine, $ExpectedArchitecture)
+ }
+
+ $publisherLength = 0
+ $publisherResult = [BongoCatPackageIdentityNative]::PackagePublisherIdFromPublisher(
+ $identity.Publisher, [ref]$publisherLength, $null)
+ if ($publisherResult -ne 122) {
+ throw "Publisher ID sizing failed with Windows error $publisherResult."
+ }
+ $publisherId = [Text.StringBuilder]::new([int]$publisherLength)
+ $publisherResult = [BongoCatPackageIdentityNative]::PackagePublisherIdFromPublisher(
+ $identity.Publisher, [ref]$publisherLength, $publisherId)
+ if ($publisherResult -ne 0) {
+ throw "Publisher ID calculation failed with Windows error $publisherResult."
+ }
+ $packageFamilyName = "$($identity.Name)_$publisherId"
+ if ($packageFamilyName -ne 'vladelaina.bongocat_hnew8t3b8e0t6') {
+ throw "Derived package family name is unexpected: $packageFamilyName"
+ }
+
+ Write-Host 'Microsoft Store package validation passed.'
+ Write-Host "Identity: $($identity.Name)"
+ Write-Host "Version: $($identity.Version)"
+ Write-Host "Package family name: $packageFamilyName"
+ Write-Host 'Signature: unsigned (Partner Center submission)'
+}
+finally {
+ $tempPrefix = [IO.Path]::GetFullPath([IO.Path]::GetTempPath()).TrimEnd('\') + '\'
+ $resolvedTemporaryRoot = [IO.Path]::GetFullPath($temporaryRoot)
+ if ($resolvedTemporaryRoot.StartsWith($tempPrefix,
+ [StringComparison]::OrdinalIgnoreCase) -and
+ (Split-Path $resolvedTemporaryRoot -Leaf) -like
+ 'BongoCatStoreValidation_*') {
+ Remove-Item -LiteralPath $resolvedTemporaryRoot -Recurse -Force `
+ -ErrorAction SilentlyContinue
+ }
+}
diff --git a/packaging/windows/BongoCat.iss.in b/packaging/windows/BongoCat.iss.in
new file mode 100644
index 00000000..e2fc9398
--- /dev/null
+++ b/packaging/windows/BongoCat.iss.in
@@ -0,0 +1,48 @@
+#ifndef PayloadDir
+ #error PayloadDir must point to the staged CMake Runtime component
+#endif
+
+[Setup]
+AppId=BongoCat
+AppName=@BONGO_CAT_PACKAGE_PRODUCT@
+AppVersion=@PROJECT_VERSION@
+VersionInfoVersion=@PROJECT_VERSION@
+VersionInfoTextVersion=@PROJECT_VERSION@
+AppPublisher=vladelaina
+AppPublisherURL=@PROJECT_HOMEPAGE_URL@
+AppSupportURL=@PROJECT_HOMEPAGE_URL@
+DefaultDirName={code:GetInstallDir}
+DefaultGroupName=BongoCat
+PrivilegesRequired=lowest
+DisableProgramGroupPage=yes
+DisableWelcomePage=yes
+DisableDirPage=auto
+UsePreviousAppDir=yes
+OutputDir=@CMAKE_BINARY_DIR@/dist
+OutputBaseFilename=@BONGO_CAT_PACKAGE_NAME@-setup
+SetupIconFile=@CMAKE_CURRENT_SOURCE_DIR@/resources/icons/icon.ico
+UninstallDisplayIcon={app}\BongoCat.exe
+Compression=zip
+WizardStyle=modern
+CloseApplications=yes
+CloseApplicationsFilter=BongoCat.exe
+RestartApplications=no
+SetupLogging=yes
+#if "@BONGO_CAT_PACKAGE_ARCH@" == "x64"
+ArchitecturesAllowed=x64compatible
+ArchitecturesInstallIn64BitMode=x64compatible
+#endif
+
+[Files]
+Source: "{#PayloadDir}\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs
+
+[Icons]
+Name: "{userdesktop}\BongoCat"; Filename: "{app}\BongoCat.exe"; Parameters: "--nearby-root=""{userdesktop}"""; WorkingDir: "{userdesktop}"
+Name: "{userprograms}\BongoCat\BongoCat"; Filename: "{app}\BongoCat.exe"; Parameters: "--nearby-root=""{userprograms}\BongoCat"""; WorkingDir: "{userprograms}\BongoCat"
+Name: "{userprograms}\BongoCat\Uninstall"; Filename: "{uninstallexe}"
+
+[Run]
+Filename: "{app}\BongoCat.exe"; Description: "Launch BongoCat"; Flags: nowait postinstall skipifsilent
+
+[Code]
+#include "install-lifecycle.iss"
diff --git a/packaging/windows/build-installer.ps1 b/packaging/windows/build-installer.ps1
new file mode 100644
index 00000000..863b6a0b
--- /dev/null
+++ b/packaging/windows/build-installer.ps1
@@ -0,0 +1,73 @@
+[CmdletBinding()]
+param(
+ [Parameter(Mandatory = $true)][string]$BuildDir,
+ [Parameter(Mandatory = $true)][string]$Configuration,
+ [Parameter(Mandatory = $true)][string]$PackageName
+)
+$ErrorActionPreference = 'Stop'
+$BuildDir = [IO.Path]::GetFullPath($BuildDir)
+$log = Join-Path $BuildDir 'installer.log'
+Set-Content -LiteralPath $log -Value 'BongoCat installer packaging' -Encoding utf8
+
+function Invoke-PackagingCommand {
+ param([string]$Executable, [string[]]$Arguments)
+ # Windows PowerShell wraps native stderr as ErrorRecord objects. Keep
+ # collecting output until the process exits, then check its actual status.
+ $ErrorActionPreference = 'Continue'
+ & $Executable @Arguments 2>&1 | ForEach-Object {
+ $line = $_.ToString()
+ Add-Content -LiteralPath $log -Value $line -Encoding utf8 -ErrorAction Stop
+ Write-Host $line
+ }
+ $status = $LASTEXITCODE
+ if ($status -ne 0) {
+ throw "$(Split-Path $Executable -Leaf) failed (exit $status). Log: $log"
+ }
+}
+
+try {
+ $iscc = & "$PSScriptRoot/find-inno.ps1"
+ Write-Host "Inno Setup compiler: $iscc"
+ # A fresh staging directory avoids shipping files left by an older build.
+ $stage = Join-Path $BuildDir ('inno-stage-' + [Guid]::NewGuid().ToString('N'))
+ try {
+ Invoke-PackagingCommand -Executable 'cmake' -Arguments @(
+ '--install', $BuildDir, '--config', $Configuration,
+ '--component', 'Runtime', '--prefix', $stage)
+ Invoke-PackagingCommand -Executable $iscc -Arguments @(
+ "/DPayloadDir=$stage", (Join-Path $BuildDir 'BongoCat.iss'))
+ $output = Join-Path $BuildDir "dist/$PackageName-setup.exe"
+ # MSBuild launches Windows PowerShell with the runner's inherited module
+ # paths; Get-FileHash may be unavailable there. Use the .NET stream API.
+ $sha256 = [Security.Cryptography.SHA256]::Create()
+ $stream = $null
+ try {
+ $stream = [IO.File]::OpenRead($output)
+ $hash = [BitConverter]::ToString($sha256.ComputeHash($stream)).Replace('-', '').ToLowerInvariant()
+ } finally {
+ if ($null -ne $stream) { $stream.Dispose() }
+ $sha256.Dispose()
+ }
+ "$hash $PackageName-setup.exe" | Set-Content -LiteralPath "$output.sha256" -Encoding ascii
+ } finally {
+ # Only remove the unique stage directory directly below the build directory.
+ $stageFull = [IO.Path]::GetFullPath($stage)
+ $buildFull = [IO.Path]::GetFullPath($BuildDir).TrimEnd('\', '/')
+ if ((Split-Path $stageFull -Parent) -eq $buildFull -and
+ (Split-Path $stageFull -Leaf) -match '^inno-stage-[0-9a-f]{32}$' -and
+ (Test-Path -LiteralPath $stageFull)) {
+ Remove-Item -LiteralPath $stageFull -Recurse -Force
+ }
+ }
+} catch {
+ $summary = $_.Exception.Message
+ Add-Content -LiteralPath $log -Value $summary -Encoding utf8
+ Write-Host 'Installer failure details:'
+ Get-Content -LiteralPath $log -Tail 30 | ForEach-Object { Write-Host $_ }
+ if ($env:GITHUB_ACTIONS -eq 'true') {
+ $detail = (Get-Content -LiteralPath $log -Tail 12) -join "`n"
+ $detail = $detail.Replace('%', '%25').Replace("`r", '%0D').Replace("`n", '%0A')
+ Write-Host "::error title=Inno Setup packaging failed::$detail"
+ }
+ throw
+}
diff --git a/packaging/windows/find-inno.ps1 b/packaging/windows/find-inno.ps1
new file mode 100644
index 00000000..751b4689
--- /dev/null
+++ b/packaging/windows/find-inno.ps1
@@ -0,0 +1,54 @@
+[CmdletBinding()]
+param([switch]$PassThru)
+$ErrorActionPreference = 'Stop'
+$candidates = @()
+$registeredVersions = @{}
+if ($env:BONGO_CAT_ISCC) { $candidates += $env:BONGO_CAT_ISCC.Trim().Trim('"') }
+$command = Get-Command ISCC.exe -ErrorAction SilentlyContinue
+if ($command) { $candidates += $command.Source }
+foreach ($base in @(${env:ProgramFiles(x86)}, $env:ProgramFiles, "$env:LOCALAPPDATA\Programs")) {
+ if ($base) { $candidates += Join-Path $base 'Inno Setup 6\ISCC.exe' }
+}
+foreach ($key in @(
+ 'HKCU:\Software\Microsoft\Windows\CurrentVersion\Uninstall\Inno Setup 6_is1',
+ 'HKCU:\Software\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\Inno Setup 6_is1',
+ 'HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall\Inno Setup 6_is1',
+ 'HKLM:\Software\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\Inno Setup 6_is1')) {
+ $entry = Get-ItemProperty -LiteralPath $key -ErrorAction SilentlyContinue
+ if ($entry -and $entry.InstallLocation) {
+ $path = [IO.Path]::GetFullPath((Join-Path $entry.InstallLocation.Trim().Trim('"') 'ISCC.exe'))
+ $candidates += $path
+ $registeredVersions[$path] = [string]$entry.DisplayVersion
+ }
+}
+$rejected = @()
+foreach ($candidate in ($candidates | Select-Object -Unique)) {
+ if (-not (Test-Path -LiteralPath $candidate -PathType Leaf)) { continue }
+ $path = (Resolve-Path -LiteralPath $candidate).Path
+ $fileVersion = (Get-Item -LiteralPath $path).VersionInfo
+ $version = [version]::new($fileVersion.FileMajorPart, $fileVersion.FileMinorPart,
+ $fileVersion.FileBuildPart, $fileVersion.FilePrivatePart)
+ $versionSource = 'FileVersion'
+ # Some official builds report 0.0.0.0 in ISCC.exe (and ISCmplr.dll).
+ # Use DisplayVersion only from a registration for this exact compiler path.
+ if ($version.Major -eq 0 -and $registeredVersions.ContainsKey($path)) {
+ $parsed = $null
+ if ([version]::TryParse($registeredVersions[$path], [ref]$parsed)) {
+ $version = $parsed
+ $versionSource = 'Registry DisplayVersion'
+ }
+ }
+ if ($version.Major -eq 6 -and $version.Minor -ge 3) {
+ if ($PassThru) {
+ return [PSCustomObject]@{
+ Path = $path
+ Version = $version
+ VersionSource = $versionSource
+ }
+ }
+ return $path
+ }
+ $rejected += "$path (version $version, $versionSource)"
+}
+$details = if ($rejected.Count) { " Found but rejected: $($rejected -join '; ')." } else { '' }
+throw "Inno Setup 6.3 or newer (6.x) is required for packaging. Install from https://jrsoftware.org/isdl.php or set BONGO_CAT_ISCC to ISCC.exe. Normal application builds do not require Inno Setup.$details"
diff --git a/packaging/windows/inno-version.txt b/packaging/windows/inno-version.txt
new file mode 100644
index 00000000..1d420242
--- /dev/null
+++ b/packaging/windows/inno-version.txt
@@ -0,0 +1 @@
+6.7.1
\ No newline at end of file
diff --git a/packaging/windows/install-lifecycle.iss.in b/packaging/windows/install-lifecycle.iss.in
new file mode 100644
index 00000000..8c149d34
--- /dev/null
+++ b/packaging/windows/install-lifecycle.iss.in
@@ -0,0 +1,147 @@
+{ Installation discovery, upgrade migration, and uninstall data policy. }
+const
+ UninstallBase = 'Software\Microsoft\Windows\CurrentVersion\Uninstall\';
+var
+ ExistingDir: String;
+ RemoveUserData: Boolean;
+
+function CompareVersions(Left, Right: String): Integer;
+var
+ LeftVersion, RightVersion: Int64;
+begin
+ if not StrToVersion(Left, LeftVersion) then
+ RaiseException('Invalid installed version: ' + Left);
+ if not StrToVersion(Right, RightVersion) then
+ RaiseException('Invalid package version: ' + Right);
+ Result := ComparePackedVersion(LeftVersion, RightVersion);
+end;
+
+function RegisteredDir(Root: Integer; Key: String): String;
+begin
+ Result := '';
+ RegQueryStringValue(Root, UninstallBase + Key, 'InstallLocation', Result);
+ if (Result = '') and (Key = 'BongoCat') then
+ RegQueryStringValue(Root, 'Software\BongoCat\BongoCat', '', Result);
+ Result := RemoveBackslashUnlessRoot(RemoveQuotes(Result));
+ if not FileExists(Result + '\BongoCat.exe') then Result := '';
+end;
+
+function FindExistingDir: String;
+begin
+ { Prefer Inno's registration after the first migration, in either view. }
+ Result := RegisteredDir(HKCU32, 'BongoCat_is1');
+ if (Result = '') and IsWin64 then Result := RegisteredDir(HKCU64, 'BongoCat_is1');
+ if Result = '' then Result := RegisteredDir(HKCU32, 'BongoCat');
+ if (Result = '') and IsWin64 then Result := RegisteredDir(HKCU64, 'BongoCat');
+ if Result = '' then begin
+ Result := ExpandConstant('{localappdata}\Programs\BongoCat');
+ if not FileExists(Result + '\BongoCat.exe') then Result := '';
+ end;
+ if Result = '' then begin
+ Result := ExpandConstant('{localappdata}\..\LocalPrograms\BongoCat');
+ if not FileExists(Result + '\BongoCat.exe') then Result := '';
+ end;
+end;
+
+function GetInstallDir(Param: String): String;
+begin
+ if ExistingDir <> '' then Result := ExistingDir
+ else Result := ExpandConstant('{localappdata}\Programs\BongoCat');
+end;
+
+function InitializeSetup: Boolean;
+var
+ Version: String;
+begin
+ ExistingDir := FindExistingDir;
+ Result := True;
+ { Equal versions must be allowed: a same-version NSIS -> Inno migration
+ still has to replace the installer registration. Never downgrade. }
+ if (ExistingDir <> '') and
+ GetVersionNumbersString(ExistingDir + '\BongoCat.exe', Version) then
+ if CompareVersions(Version, '@PROJECT_VERSION@') > 0 then begin
+ SuppressibleMsgBox('A newer version of BongoCat is already installed.', mbError, MB_OK, IDOK);
+ Result := False;
+ end;
+end;
+
+procedure InitializeWizard;
+begin
+ if ExistingDir <> '' then begin
+ WizardForm.DirEdit.Text := ExistingDir;
+ WizardForm.DirEdit.Enabled := False;
+ WizardForm.DirBrowseButton.Enabled := False;
+ end;
+end;
+
+function ShouldSkipPage(PageID: Integer): Boolean;
+begin
+ Result := (PageID = wpSelectDir) and (ExistingDir <> '');
+end;
+
+function PrepareToInstall(var NeedsRestart: Boolean): String;
+var
+ App, Version: String;
+ ExitCode: Integer;
+begin
+ Result := '';
+ if (ExistingDir <> '') and
+ (CompareText(RemoveBackslashUnlessRoot(ExpandConstant('{app}')),
+ RemoveBackslashUnlessRoot(ExistingDir)) <> 0) then begin
+ Result := 'An upgrade must use the existing BongoCat installation directory.';
+ Exit;
+ end;
+ App := ExpandConstant('{app}\BongoCat.exe');
+ if GetVersionNumbersString(App, Version) then begin
+ if CompareVersions(Version, '@PROJECT_VERSION@') > 0 then begin
+ Result := 'A newer version of BongoCat is already installed in this directory.';
+ Exit;
+ end;
+ { 0.1.0 treats unknown switches as a normal launch and would wait forever.
+ Inno Restart Manager handles that version and any shutdown failures. }
+ if CompareVersions(Version, '0.1.1') >= 0 then
+ if not Exec(App, '--shutdown-for-update', ExpandConstant('{app}'),
+ SW_HIDE, ewWaitUntilTerminated, ExitCode) then
+ Log('Update shutdown unavailable; using Restart Manager.');
+ end;
+end;
+
+procedure RemoveLegacyRegistration(Root: Integer);
+var
+ Path: String;
+begin
+ Path := RegisteredDir(Root, 'BongoCat');
+ if (Path <> '') and
+ (CompareText(Path, RemoveBackslashUnlessRoot(ExpandConstant('{app}'))) = 0) then begin
+ if not RegDeleteKeyIncludingSubkeys(Root, UninstallBase + 'BongoCat') then
+ Log('Could not remove the legacy NSIS uninstall registration.');
+ RegDeleteKeyIncludingSubkeys(Root, 'Software\BongoCat\BongoCat');
+ end;
+end;
+
+procedure CurStepChanged(CurStep: TSetupStep);
+begin
+ if CurStep = ssPostInstall then begin
+ { Only retire NSIS after successful installation. Do not run its uninstaller:
+ old releases may delete user files. Inno owns only its installed payload. }
+ RemoveLegacyRegistration(HKCU32);
+ if IsWin64 then RemoveLegacyRegistration(HKCU64);
+ DeleteFile(ExpandConstant('{app}\Uninstall.exe'));
+ DeleteFile(ExpandConstant('{userprograms}\BongoCat\Uninstall BongoCat.lnk'));
+ end;
+end;
+
+function InitializeUninstall: Boolean;
+begin
+ RemoveUserData := False;
+ if not UninstallSilent then
+ RemoveUserData := MsgBox('Also delete settings, imported models, and other BongoCat user data?'
+ + #13#10 + 'Choose No to keep your data.', mbConfirmation, MB_YESNO or MB_DEFBUTTON2) = IDYES;
+ Result := True;
+end;
+
+procedure CurUninstallStepChanged(CurUninstallStep: TUninstallStep);
+begin
+ if (CurUninstallStep = usPostUninstall) and RemoveUserData then
+ DelTree(ExpandConstant('{localappdata}\BongoCat'), True, True, True);
+end;
diff --git a/resources/assets/bongocat.png b/resources/assets/bongocat.png
new file mode 100644
index 00000000..31702146
Binary files /dev/null and b/resources/assets/bongocat.png differ
diff --git a/resources/assets/licenses/libwebp.txt b/resources/assets/licenses/libwebp.txt
new file mode 100644
index 00000000..7a6f9954
--- /dev/null
+++ b/resources/assets/licenses/libwebp.txt
@@ -0,0 +1,30 @@
+Copyright (c) 2010, Google Inc. All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+ * Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+
+ * Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in
+ the documentation and/or other materials provided with the
+ distribution.
+
+ * Neither the name of Google nor the names of its contributors may
+ be used to endorse or promote products derived from this software
+ without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
diff --git a/resources/assets/licenses/nanosvg.txt b/resources/assets/licenses/nanosvg.txt
new file mode 100644
index 00000000..f896f2eb
--- /dev/null
+++ b/resources/assets/licenses/nanosvg.txt
@@ -0,0 +1,18 @@
+Copyright (c) 2013-14 Mikko Mononen memon@inside.org
+
+This software is provided 'as-is', without any express or implied
+warranty. In no event will the authors be held liable for any damages
+arising from the use of this software.
+
+Permission is granted to anyone to use this software for any purpose,
+including commercial applications, and to alter it and redistribute it
+freely, subject to the following restrictions:
+
+1. The origin of this software must not be misrepresented; you must not
+claim that you wrote the original software. If you use this software
+in a product, an acknowledgment in the product documentation would be
+appreciated but is not required.
+2. Altered source versions must be plainly marked as such, and must not be
+misrepresented as being the original software.
+3. This notice may not be removed or altered from any source distribution.
+
diff --git a/resources/assets/locales/de-DE.json b/resources/assets/locales/de-DE.json
index a00445b2..8bf5d6a2 100644
--- a/resources/assets/locales/de-DE.json
+++ b/resources/assets/locales/de-DE.json
@@ -9,38 +9,46 @@
"preference": {
"title": "Einstellungen",
"cat": {
- "title": "Anzeige",
+ "title": "Einstellungen",
"labels": {
- "modelSettings": "Modelleinstellungen",
+ "modelSettings": "Modell",
"mirrorMode": "Spiegelmodus",
"mouseMirror": "Maus spiegeln",
"mouseCentered": "Maus auf Desktop-Begleiter zentriert",
"ignoreMouse": "Mausereignisse ignorieren",
- "windowSettings": "Fenstereinstellungen",
+ "windowSettings": "Fenster",
"passThrough": "Klicks durchlassen",
"alwaysOnTop": "Immer im Vordergrund",
"keepInScreen": "Auf dem Bildschirm halten",
"windowSize": "Fenstergröße",
+ "windowCorners": "Fensterrundung (%)",
"opacity": "Deckkraft",
- "autoReleaseDelay": "Verzögerung der automatischen Freigabe",
+ "randomExpression": "Zufällige Ausdrücke",
+ "obsBackground": "Einfarbiger Hintergrund",
"hideOnHover": "Beim Darüberfahren ausblenden",
+ "hideFadeSeconds": "Ausblendedauer (s)",
"maxFPS": "Maximale Bildrate"
},
"hints": {
- "passThrough": "Wenn aktiviert, gehen Klicks durch das Fenster hindurch, ohne es zu beeinflussen.",
+ "hoverUnavailable": "Ausblenden bei Mausberührung ist in dieser Desktop-Umgebung nicht verfügbar.",
+ "gameInput": "Desktop-Tier reagiert im Spiel nicht?",
+ "gameInputHelp": "Versuche, BongoCat als Administrator auszuführen und das Spiel in den Fenstermodus zu versetzen.",
+ "passThrough": "Auch per Rechtsklick auf das Symbol im Infobereich deaktivierbar",
"alwaysOnTop": "Wenn aktiviert, bleibt das Fenster über allen anderen Fenstern.",
"keepInScreen": "Wenn aktiviert, bleibt das Fenster automatisch innerhalb der Bildschirmgrenzen.",
+ "obsBackground": "Fensteraufnahme schwarz?",
+ "obsBackgroundHelp": "OBS: Diese Option aktivieren, die Windows-7-Kompatibilitätsmethode verwenden und den Hintergrund mit einem Farbschlüssel-Filter entfernen.",
"windowSize": "Bewegen Sie die Maus an den Fensterrand oder halten Sie Umschalt gedrückt und ziehen Sie mit der rechten Maustaste.",
- "hideOnHover": "Wenn aktiviert, wird das Fenster beim Darüberfahren mit der Maus automatisch ausgeblendet. Die Verzögerung 0 blendet es sofort aus."
+ "hideOnHover": "Wenn Klickdurchlässigkeit und Immer im Vordergrund aktiviert sind, wird das Haustier beim Darüberfahren sofort ausgeblendet und beim Verlassen wieder angezeigt."
}
},
"general": {
"title": "Allgemein",
"labels": {
- "appSettings": "Anwendungseinstellungen",
+ "appSettings": "Anwendung",
"launchOnStartup": "Beim Systemstart ausführen",
"showTrayIcon": "Infobereichssymbol anzeigen",
- "appearanceSettings": "Darstellung",
+ "appearanceSettings": "Erscheinungsbild",
"themeMode": "Design",
"language": "Sprache",
"permissionsSettings": "Berechtigungen",
@@ -54,8 +62,9 @@
},
"hints": {
"showTrayIcon": "Wenn aktiviert, wird das Anwendungssymbol im Infobereich angezeigt.",
- "inputMonitoringPermission": "Aktivieren Sie die Eingabeüberwachung, um Tastatur- und Mausereignisse vom System zu empfangen.",
- "inputMonitoringPermissionGuide": "Die Berechtigung zur Eingabeüberwachung ist erforderlich, damit die Anwendung Tastatur- und Mausereignisse vom System empfangen kann.\n\nSchritte:\n1. Öffnen Sie Systemeinstellungen > Datenschutz & Sicherheit > Eingabeüberwachung.\n2. Falls BongoCat bereits aufgeführt ist, wählen Sie es aus und entfernen Sie es mit der Schaltfläche \"-\".\n3. Klicken Sie auf \"+\", um BongoCat zu suchen und hinzuzufügen.\n4. Starten Sie die Anwendung neu, damit die Berechtigung wirksam wird.",
+ "autostartFailed": "Die Einstellung für den automatischen Start konnte nicht aktualisiert werden",
+ "inputMonitoringPermission": "Nach dem Erlauben neu starten.",
+ "inputMonitoringPermissionGuide": "Die Eingabeüberwachung erlaubt BongoCat, in anderen Apps gedrückte Tasten zu sehen; die Maus funktioniert auch ohne sie. BongoCat baut die Überwachung beim Start auf, eine Berechtigungsänderung erfordert daher einen Neustart.\n\nSchritte:\n1. Öffnen Sie Systemeinstellungen → Datenschutz & Sicherheit → Eingabeüberwachung.\n2. Schalten Sie den bereits gelisteten BongoCat-Eintrag ein; entfernen Sie keine vorhandenen Einträge.\n3. Falls BongoCat fehlt, klicken Sie auf \"+\" und fügen Sie die laufende Kopie hinzu (prüfen Sie den Speicherort).\n4. Beenden Sie BongoCat vollständig und starten Sie es erneut, damit es die Überwachung neu aufbaut.\n\nWenn die Tastatur weiterhin nicht reagiert, können Sie den BongoCat-Eintrag selbst entfernen und neu hinzufügen.",
"administratorPermission": "Als Administrator kann die Anwendung bestimmte Systemtasten und Eingabeereignisse zuverlässiger erfassen.",
"administratorPermissionGuide": "Für eine zuverlässigere Erfassung bestimmter Systemtasten und Eingabeereignisse wird empfohlen, die Anwendung als Administrator auszuführen.\n\nVorgehen:\n1. Beenden Sie zuerst die Anwendung.\n2. Suchen Sie die exe-Datei oder ihre Verknüpfung im Datei-Explorer.\n3. Wählen Sie die Startart:\n - Einmalig: Rechtsklick und \"Als Administrator ausführen\" wählen.\n - Immer: Rechtsklick > Eigenschaften > Kompatibilität > \"Programm als Administrator ausführen\" aktivieren > OK."
},
@@ -63,7 +72,8 @@
"authorized": "Autorisiert",
"adminEnabled": "Aktiviert",
"authorize": "Aktivieren",
- "viewGuide": "Anleitung anzeigen"
+ "viewGuide": "Anleitung anzeigen",
+ "openSettingsFailed": "Systemeinstellungen → Datenschutz & Sicherheit → Eingabeüberwachung."
},
"buttons": {
"openNow": "Jetzt öffnen",
@@ -74,15 +84,27 @@
},
"model": {
"title": "Modell",
+ "nearbyTitle": "Nahe Modelle",
"labels": {
- "deleteModel": "Modell löschen",
- "nearbyModels": "Benachbarte Ordner"
+ "deleteModel": "Modell löschen"
},
"hints": {
- "deleteSuccess": "Erfolgreich gelöscht",
- "deleteModel": "Möchten Sie dieses Modell wirklich löschen?",
+ "deleteSuccess": "\"%s\" wurde gelöscht",
+ "deleteModel": "Möchten Sie \"%s\" wirklich löschen?",
+ "deleteFailed": "Dieses Modell konnte nicht gelöscht werden",
"importSuccess": "Erfolgreich importiert",
- "clickOrDragToImport": "Zum Importieren klicken oder hierher ziehen",
+ "importExists": "Modell ist bereits vorhanden",
+ "importBatchResult": "Import erfolgreich: %zu; fehlgeschlagen: %zu.\nFehlende Modelle: %s%s.\nErster Fehler: %s",
+ "importMoreFailures": " und %zu weitere",
+ "importUnknownSource": "unbekannte Quelle",
+ "importInvalidSource": "Die ausgewählte Quelle ist nicht mehr vorhanden oder kann nicht verwendet werden",
+ "importInvalidFormat": "Es wurde kein unterstütztes Modell gefunden oder das Modellpaket ist unvollständig",
+ "importFileAccess": "Die Modelldateien konnten nicht gelesen oder gespeichert werden. Prüfen Sie die Dateiberechtigungen und den verfügbaren Speicherplatz",
+ "importOutOfMemory": "Zum Importieren dieses Modells ist nicht genügend Arbeitsspeicher verfügbar",
+ "importLoadFailed": "Das Modell wurde importiert, konnte aber nicht angezeigt werden",
+ "importFailed": "Der Modellimport ist fehlgeschlagen. Prüfen Sie die ausgewählten Dateien und versuchen Sie es erneut",
+ "clickOrDragToImport": "Modellordner wählen oder ziehen",
+ "supportedImportFormats": "Unterstützt Mver und Tauri",
"openDirectoryFailed": "Modellordner konnte nicht geöffnet werden"
},
"tooltips": {
@@ -91,10 +113,12 @@
"moreModels": "Weitere Modelle"
},
"behaviorModal": {
- "title": "Bewegungen und Ausdrücke",
+ "title": "Bewegungen, Ausdrücke und Audio",
"labels": {
"motion": "Bewegung",
"expression": "Ausdruck",
+ "audio": "Audio",
+ "stopAllAudio": "Alle Audios stoppen",
"motionIndex": "Bewegung {index}",
"motionGroupIndex": "Bewegungsgruppe {index}",
"expressionIndex": "Ausdruck {index}"
@@ -171,21 +195,61 @@
}
},
"native": {
+ "about": {
+ "wechat": "WeChat",
+ "wechatHint": "Hover to scan and chat",
+ "loading": "Loading...",
+ "contributorsFailed": "Cannot load contributors. Click to retry.",
+ "qrUnavailable": "QR code unavailable",
+ "reload": "Reload",
+ "qrFailed": "QR code unavailable. Click to retry.",
+ "contributorsTitle": "Open source & community contributions",
+ "contributorsText": "Every step of BongoCat comes from the power of open source. Thank you to all community contributors for making desktop companionship freer and simpler.",
+ "joinTitle": "Want to appear in the contributors list?",
+ "joinText": "Whether you contribute code, improve documentation, report bugs or translate, every contribution makes BongoCat better.",
+ "code": "Code",
+ "docs": "Documentation",
+ "bugs": "Bug reports",
+ "translations": "Translations",
+ "joinGithub": "Contribute on GitHub"
+ },
+ "multiplePets": "Mehrere gleichzeitig anzeigen",
+ "multiplePetsLimit": "Bis zu 8 Desktop-Begleiter k\u00f6nnen gleichzeitig angezeigt werden",
+ "removeDesktopPet": "Diesen Desktop-Begleiter schlie\u00dfen",
"modelLoadFailed": "Dieses Modell kann nicht angezeigt werden",
"noBehaviors": "Keine Einträge",
+ "preferencesWindowTitle": "BongoCat - Settings",
+ "startup": {
+ "failed": "BongoCat konnte nicht gestartet werden.",
+ "detail": "Technische Details finden Sie im Diagnoseprotokoll.",
+ "diagnosticLog": "Diagnoseprotokoll:"
+ },
"support": {
- "title": "Entwicklungsteam unterstützen",
+ "title": "Info",
+ "website": "Website: bongocat.pet",
"heroText": "Vielen Dank für Ihre Unterstützung. Jede Nutzung und Weitergabe hilft BongoCat, weiter zu wachsen.",
"version": "Anwendungsversion",
"checkUpdate": "Nach Updates suchen",
"feedback": "Feedback",
+ "openLogs": "Protokolle öffnen",
+ "openLogsFailed": "Protokollordner konnte nicht geöffnet werden",
"latest": "Bereits aktuell",
+ "checkingUpdate": "Wird geprüft...",
+ "updateAvailable": "Neue Version verfügbar:",
+ "updateFailed": "Updates konnten nicht geprüft werden",
+ "updateHttpFailed": "GitHub meldete den HTTP-Status %s",
+ "downloadUpdate": "Auf v%s aktualisieren",
+ "openStore": "Microsoft Store öffnen",
+ "viewReleases": "Versionen anzeigen",
+ "openUpdateFailed": "Die Update-Seite konnte nicht geöffnet werden",
"works": "Weitere Anwendungen",
- "worksText": "Weitere Software von vladelaina",
- "refactorDeveloper": "(Entwickler, der BongoCat in C neu aufgebaut hat)",
+ "worksText": "Weitere Werke von vladelaina, die einen Blick wert sind",
+ "refactorDeveloper": "Entwickler, der BongoCat in C neu geschrieben hat und zugleich aktueller Maintainer ist",
"community": "Community",
"qqGroup": "QQ-Gruppe",
- "coffee": "Dem Entwicklungsteam einen Kaffee spendieren?"
+ "coffee": "Dem Entwickler einen Kaffee spendieren?",
+ "starOnGitHub": "GitHub-Stern!",
+ "by": "von "
},
"shortcut": {
"toggleCat": "Ein-/ausblenden",
@@ -203,6 +267,7 @@
"select": "Auswählen",
"selected": "Ausgewählt",
"import": "Importieren",
+ "importing": "Wird importiert...",
"builtIn": "integriert"
}
}
diff --git a/resources/assets/locales/en-US.json b/resources/assets/locales/en-US.json
index da82d6a2..6d93b723 100644
--- a/resources/assets/locales/en-US.json
+++ b/resources/assets/locales/en-US.json
@@ -9,38 +9,46 @@
"preference": {
"title": "Preferences",
"cat": {
- "title": "Display",
+ "title": "Preferences",
"labels": {
- "modelSettings": "Model Settings",
+ "modelSettings": "Model",
"mirrorMode": "Mirror Mode",
"mouseMirror": "Mouse Mirror",
"mouseCentered": "Mouse Centered on Desktop Pet",
"ignoreMouse": "Ignore Mouse Events",
- "windowSettings": "Window Settings",
+ "windowSettings": "Window",
"passThrough": "Pass Through",
"alwaysOnTop": "Always on Top",
"keepInScreen": "Keep on Screen",
"windowSize": "Window Size",
+ "windowCorners": "Window Corners (%)",
"opacity": "Opacity",
- "autoReleaseDelay": "Auto Release Delay",
+ "randomExpression": "Random Expressions",
+ "obsBackground": "Solid Background",
"hideOnHover": "Hide on Hover",
+ "hideFadeSeconds": "Fade Duration (s)",
"maxFPS": "Max Frame Rate"
},
"hints": {
- "passThrough": "When enabled, clicks pass through the window without affecting it.",
+ "hoverUnavailable": "Hover hiding is unavailable in the current desktop environment.",
+ "gameInput": "Pet not responding in games?",
+ "gameInputHelp": "Try running BongoCat as administrator and setting the game to windowed mode.",
+ "passThrough": "You can also turn this off by right-clicking the tray icon",
"alwaysOnTop": "When enabled, the window stays above all other windows.",
"keepInScreen": "When enabled, the window automatically stays within the screen boundaries.",
+ "obsBackground": "Window capture is black?",
+ "obsBackgroundHelp": "OBS: enable this option, use the Windows 7 compatibility method, and remove the background with a color key filter.",
"windowSize": "Move mouse to window edge, or hold Shift and right-drag to resize.",
- "hideOnHover": "When enabled, the window hides automatically on mouse hover. You can set a delay; 0 means hide immediately."
+ "hideOnHover": "With Pass Through and Always on Top enabled, hide the pet immediately on hover and show it again when the pointer leaves."
}
},
"general": {
"title": "General",
"labels": {
- "appSettings": "Application Settings",
+ "appSettings": "Application",
"launchOnStartup": "Launch on Startup",
"showTrayIcon": "Show Tray Icon",
- "appearanceSettings": "Appearance Settings",
+ "appearanceSettings": "Appearance",
"themeMode": "Theme",
"language": "Language",
"permissionsSettings": "Permissions Settings",
@@ -54,8 +62,9 @@
},
"hints": {
"showTrayIcon": "When enabled, the app icon is displayed in the system tray.",
- "inputMonitoringPermission": "Enable input monitoring to receive keyboard and mouse events from the system.",
- "inputMonitoringPermissionGuide": "Input Monitoring permission is required for the app to receive keyboard and mouse events from the system.\n\nSteps:\n1. Open System Settings → Privacy & Security → Input Monitoring.\n2. If BongoCat already appears in the list, select it and click the \"-\" button to remove it.\n3. Click the \"+\" button to find and add BongoCat.\n4. Restart the app for the permission to take effect.",
+ "autostartFailed": "Unable to update launch-on-startup settings",
+ "inputMonitoringPermission": "Restart after granting it.",
+ "inputMonitoringPermissionGuide": "Input Monitoring lets BongoCat see keys pressed in other apps; the mouse keeps working without it. BongoCat builds the listener when it starts, so a permission change needs a restart.\n\nSteps:\n1. Open System Settings → Privacy & Security → Input Monitoring.\n2. Turn on the BongoCat entry that is already listed; do not remove existing entries.\n3. If BongoCat is not listed, click the \"+\" button and add the copy you are running (check the app location).\n4. Quit BongoCat completely and start it again, so it rebuilds the listener.\n\nIf the keyboard still stays silent, you can remove and re-add the BongoCat entry yourself.",
"administratorPermission": "Running the app as administrator helps capture some system-level keys and input events more reliably.",
"administratorPermissionGuide": "To capture some system-level keys and input events more reliably, it is recommended to run the app as administrator.\n\nHow to do it:\n1. Exit the app first.\n2. Find the app exe or its shortcut in File Explorer.\n3. Choose how to launch:\n • One-time: Right-click and choose \"Run as administrator\".\n • Always (default): Right-click → Properties → Compatibility tab → check \"Run this program as an administrator\" → OK."
},
@@ -63,7 +72,8 @@
"authorized": "Authorized",
"adminEnabled": "Enabled",
"authorize": "Go to Enable",
- "viewGuide": "View Instructions"
+ "viewGuide": "View Instructions",
+ "openSettingsFailed": "Cannot open System Settings. Open Privacy & Security → Input Monitoring."
},
"buttons": {
"openNow": "Open Now",
@@ -74,15 +84,27 @@
},
"model": {
"title": "Model",
+ "nearbyTitle": "Nearby models",
"labels": {
- "deleteModel": "Delete Model",
- "nearbyModels": "Nearby folders"
+ "deleteModel": "Delete Model"
},
"hints": {
- "deleteSuccess": "Deleted Successfully",
- "deleteModel": "Are you sure you want to delete this model?",
+ "deleteSuccess": "Deleted \"%s\"",
+ "deleteModel": "Are you sure you want to delete \"%s\"?",
+ "deleteFailed": "Unable to delete this model",
"importSuccess": "Imported Successfully",
- "clickOrDragToImport": "Click or drag here to import",
+ "importExists": "Model already exists",
+ "importBatchResult": "Import succeeded: %zu; failed: %zu.\nFailed models: %s%s.\nFirst failure: %s",
+ "importMoreFailures": " and %zu more",
+ "importUnknownSource": "unknown source",
+ "importInvalidSource": "The selected source no longer exists or cannot be used",
+ "importInvalidFormat": "No supported model was found, or the model package is incomplete",
+ "importFileAccess": "The model files could not be read or saved. Check file permissions and available disk space",
+ "importOutOfMemory": "There is not enough memory to import this model",
+ "importLoadFailed": "The model was imported, but it could not be displayed",
+ "importFailed": "Model import failed. Check the selected files and try again",
+ "clickOrDragToImport": "Click or drag a model folder",
+ "supportedImportFormats": "Supports Mver and Tauri formats",
"openDirectoryFailed": "Unable to open model directory"
},
"tooltips": {
@@ -91,10 +113,12 @@
"moreModels": "More Models"
},
"behaviorModal": {
- "title": "Motions and Expressions",
+ "title": "Motions, Expressions and Audio",
"labels": {
"motion": "Motion",
"expression": "Expression",
+ "audio": "Audio",
+ "stopAllAudio": "Stop all audio",
"motionIndex": "Motion {index}",
"motionGroupIndex": "Motion Group {index}",
"expressionIndex": "Expression {index}"
@@ -171,21 +195,61 @@
}
},
"native": {
+ "about": {
+ "wechat": "WeChat",
+ "wechatHint": "Hover to scan and chat",
+ "loading": "Loading...",
+ "contributorsFailed": "Cannot load contributors. Click to retry.",
+ "qrUnavailable": "QR code unavailable",
+ "reload": "Reload",
+ "qrFailed": "QR code unavailable. Click to retry.",
+ "contributorsTitle": "Open source & community contributions",
+ "contributorsText": "Every step of BongoCat comes from the power of open source. Thank you to all community contributors for making desktop companionship freer and simpler.",
+ "joinTitle": "Want to appear in the contributors list?",
+ "joinText": "Whether you contribute code, improve documentation, report bugs or translate, every contribution makes BongoCat better.",
+ "code": "Code",
+ "docs": "Documentation",
+ "bugs": "Bug reports",
+ "translations": "Translations",
+ "joinGithub": "Contribute on GitHub"
+ },
+ "multiplePets": "Display multiple",
+ "multiplePetsLimit": "Up to 8 desktop pets can be displayed at once",
+ "removeDesktopPet": "Close this desktop pet",
"modelLoadFailed": "Unable to display this model",
"noBehaviors": "No items",
+ "preferencesWindowTitle": "BongoCat - Settings",
+ "startup": {
+ "failed": "BongoCat could not start.",
+ "detail": "See the diagnostic log for technical details.",
+ "diagnosticLog": "Diagnostic log:"
+ },
"support": {
- "title": "Support the Dev Team",
+ "title": "About",
+ "website": "Website: bongocat.pet",
"heroText": "Thank you for your support. Every use and share helps BongoCat continue to grow.",
"version": "App version",
"checkUpdate": "Check for updates",
"feedback": "Feedback",
+ "openLogs": "Open logs",
+ "openLogsFailed": "Unable to open logs folder",
"latest": "Already up to date",
+ "checkingUpdate": "Checking...",
+ "updateAvailable": "New version available:",
+ "updateFailed": "Unable to check for updates",
+ "updateHttpFailed": "GitHub returned HTTP status %s",
+ "downloadUpdate": "Update to v%s",
+ "openStore": "Open Microsoft Store",
+ "viewReleases": "View releases",
+ "openUpdateFailed": "Unable to open the update page",
"works": "More apps",
- "worksText": "More software from vladelaina",
- "refactorDeveloper": "(Developer who rebuilt BongoCat in C)",
+ "worksText": "More from vladelaina, well worth a try",
+ "refactorDeveloper": "Developer who rewrote BongoCat in C and is also its current maintainer",
"community": "Community",
"qqGroup": "QQ Group",
- "coffee": "Buy the development team a coffee?"
+ "coffee": "Buy the developer a coffee?",
+ "starOnGitHub": "Star on GitHub!",
+ "by": "by "
},
"shortcut": {
"toggleCat": "Show/Hide",
@@ -203,6 +267,7 @@
"select": "Select",
"selected": "Selected",
"import": "Import",
+ "importing": "Importing...",
"builtIn": "built-in"
}
}
diff --git a/resources/assets/locales/es-ES.json b/resources/assets/locales/es-ES.json
index f2f55a22..72f78eaa 100644
--- a/resources/assets/locales/es-ES.json
+++ b/resources/assets/locales/es-ES.json
@@ -9,38 +9,46 @@
"preference": {
"title": "Preferencias",
"cat": {
- "title": "Pantalla",
+ "title": "Preferencias",
"labels": {
- "modelSettings": "Ajustes del modelo",
+ "modelSettings": "Modelo",
"mirrorMode": "Modo espejo",
"mouseMirror": "Invertir ratón",
"mouseCentered": "Ratón centrado en la mascota",
"ignoreMouse": "Ignorar eventos del ratón",
- "windowSettings": "Ajustes de la ventana",
+ "windowSettings": "Ventana",
"passThrough": "Dejar pasar clics",
"alwaysOnTop": "Siempre visible",
"keepInScreen": "Mantener en pantalla",
"windowSize": "Tamaño de la ventana",
+ "windowCorners": "Esquinas redondeadas (%)",
"opacity": "Opacidad",
- "autoReleaseDelay": "Retardo de liberación automática",
+ "randomExpression": "Expresiones aleatorias",
+ "obsBackground": "Fondo sólido",
"hideOnHover": "Ocultar al pasar el ratón",
+ "hideFadeSeconds": "Duración del fundido (s)",
"maxFPS": "Frecuencia máxima de fotogramas"
},
"hints": {
- "passThrough": "Al activarlo, los clics atraviesan la ventana sin afectarla.",
+ "hoverUnavailable": "Ocultar al pasar el cursor no está disponible en el entorno de escritorio actual.",
+ "gameInput": "¿La mascota no responde al jugar?",
+ "gameInputHelp": "Prueba a ejecutar BongoCat como administrador y a poner el juego en modo ventana.",
+ "passThrough": "También puedes desactivarlo con un clic derecho en el icono de la bandeja",
"alwaysOnTop": "Al activarlo, la ventana permanece por encima de las demás.",
"keepInScreen": "Al activarlo, la ventana se mantiene automáticamente dentro de los límites de la pantalla.",
+ "obsBackground": "¿La captura de ventana aparece en negro?",
+ "obsBackgroundHelp": "OBS: activa esta opción, usa el método compatible con Windows 7 y elimina el fondo con un filtro de clave de color.",
"windowSize": "Mueva el ratón al borde de la ventana o mantenga Mayús y arrastre con el botón derecho para cambiar el tamaño.",
- "hideOnHover": "Al activarlo, la ventana se oculta automáticamente al pasar el ratón. Puede definir un retardo; 0 significa ocultar inmediatamente."
+ "hideOnHover": "Con el paso de clics y Siempre visible activados, la mascota se oculta inmediatamente al pasar el ratón y reaparece al retirarlo."
}
},
"general": {
"title": "General",
"labels": {
- "appSettings": "Ajustes de la aplicación",
+ "appSettings": "Aplicación",
"launchOnStartup": "Iniciar con el sistema",
"showTrayIcon": "Mostrar icono en la bandeja",
- "appearanceSettings": "Ajustes de apariencia",
+ "appearanceSettings": "Apariencia",
"themeMode": "Tema",
"language": "Idioma",
"permissionsSettings": "Ajustes de permisos",
@@ -54,8 +62,9 @@
},
"hints": {
"showTrayIcon": "Al activarlo, el icono de la aplicación aparece en la bandeja del sistema.",
- "inputMonitoringPermission": "Active la supervisión de entrada para recibir eventos de teclado y ratón del sistema.",
- "inputMonitoringPermissionGuide": "Se necesita el permiso de supervisión de entrada para que la aplicación reciba eventos de teclado y ratón del sistema.\n\nPasos:\n1. Abra Ajustes del Sistema > Privacidad y seguridad > Supervisión de entrada.\n2. Si BongoCat ya aparece en la lista, selecciónelo y pulse el botón \"-\" para eliminarlo.\n3. Pulse el botón \"+\" para buscar y añadir BongoCat.\n4. Reinicie la aplicación para que el permiso tenga efecto.",
+ "autostartFailed": "No se pudo actualizar la configuración de inicio automático",
+ "inputMonitoringPermission": "Reinicie tras autorizar.",
+ "inputMonitoringPermissionGuide": "La Supervisión de entrada permite que BongoCat vea las teclas pulsadas en otras aplicaciones; el ratón sigue funcionando sin ella. BongoCat crea la escucha al iniciarse, así que un cambio de permiso requiere reiniciar.\n\nPasos:\n1. Abra Ajustes del Sistema → Privacidad y seguridad → Supervisión de entrada.\n2. Active la entrada de BongoCat que ya aparece en la lista; no elimine entradas existentes.\n3. Si BongoCat no está en la lista, pulse \"+\" y añada la copia que está ejecutando (compruebe la ubicación de la aplicación).\n4. Cierre BongoCat por completo y vuelva a iniciarlo para que reconstruya la escucha.\n\nSi el teclado sigue sin responder, puede eliminar y volver a añadir la entrada de BongoCat.",
"administratorPermission": "Ejecutar la aplicación como administrador ayuda a capturar con mayor fiabilidad algunas teclas del sistema y eventos de entrada.",
"administratorPermissionGuide": "Para capturar con mayor fiabilidad algunas teclas del sistema y eventos de entrada, se recomienda ejecutar la aplicación como administrador.\n\nCómo hacerlo:\n1. Cierre primero la aplicación.\n2. Busque el archivo exe o su acceso directo en el Explorador de archivos.\n3. Elija cómo iniciarla:\n - Una vez: haga clic con el botón derecho y elija \"Ejecutar como administrador\".\n - Siempre: clic derecho > Propiedades > Compatibilidad > marque \"Ejecutar este programa como administrador\" > Aceptar."
},
@@ -63,7 +72,8 @@
"authorized": "Autorizado",
"adminEnabled": "Activado",
"authorize": "Activar",
- "viewGuide": "Ver instrucciones"
+ "viewGuide": "Ver instrucciones",
+ "openSettingsFailed": "Ajustes del Sistema → Privacidad y seguridad → Supervisión de entrada."
},
"buttons": {
"openNow": "Abrir ahora",
@@ -74,15 +84,27 @@
},
"model": {
"title": "Modelo",
+ "nearbyTitle": "Modelos cercanos",
"labels": {
- "deleteModel": "Eliminar modelo",
- "nearbyModels": "Carpetas cercanas"
+ "deleteModel": "Eliminar modelo"
},
"hints": {
- "deleteSuccess": "Eliminado correctamente",
- "deleteModel": "¿Seguro que quiere eliminar este modelo?",
+ "deleteSuccess": "Se eliminó \"%s\"",
+ "deleteModel": "¿Seguro que quiere eliminar \"%s\"?",
+ "deleteFailed": "No se pudo eliminar este modelo",
"importSuccess": "Importado correctamente",
- "clickOrDragToImport": "Haga clic o arrastre aquí para importar",
+ "importExists": "El modelo ya existe",
+ "importBatchResult": "Importación correcta: %zu; fallida: %zu.\nModelos fallidos: %s%s.\nPrimer error: %s",
+ "importMoreFailures": " y %zu más",
+ "importUnknownSource": "fuente desconocida",
+ "importInvalidSource": "La fuente seleccionada ya no existe o no se puede utilizar",
+ "importInvalidFormat": "No se encontró ningún modelo compatible o el paquete del modelo está incompleto",
+ "importFileAccess": "No se pudieron leer o guardar los archivos del modelo. Compruebe los permisos y el espacio disponible en el disco",
+ "importOutOfMemory": "No hay memoria suficiente para importar este modelo",
+ "importLoadFailed": "El modelo se importó, pero no se pudo mostrar",
+ "importFailed": "Error al importar el modelo. Compruebe los archivos seleccionados e inténtelo de nuevo",
+ "clickOrDragToImport": "Haz clic o arrastra una carpeta",
+ "supportedImportFormats": "Compatible con Mver y Tauri",
"openDirectoryFailed": "No se pudo abrir la carpeta del modelo"
},
"tooltips": {
@@ -91,10 +113,12 @@
"moreModels": "Más modelos"
},
"behaviorModal": {
- "title": "Movimientos y expresiones",
+ "title": "Movimientos, expresiones y audio",
"labels": {
"motion": "Movimiento",
"expression": "Expresión",
+ "audio": "Audio",
+ "stopAllAudio": "Detener todo el audio",
"motionIndex": "Movimiento {index}",
"motionGroupIndex": "Grupo de movimientos {index}",
"expressionIndex": "Expresión {index}"
@@ -171,21 +195,61 @@
}
},
"native": {
+ "about": {
+ "wechat": "WeChat",
+ "wechatHint": "Hover to scan and chat",
+ "loading": "Loading...",
+ "contributorsFailed": "Cannot load contributors. Click to retry.",
+ "qrUnavailable": "QR code unavailable",
+ "reload": "Reload",
+ "qrFailed": "QR code unavailable. Click to retry.",
+ "contributorsTitle": "Open source & community contributions",
+ "contributorsText": "Every step of BongoCat comes from the power of open source. Thank you to all community contributors for making desktop companionship freer and simpler.",
+ "joinTitle": "Want to appear in the contributors list?",
+ "joinText": "Whether you contribute code, improve documentation, report bugs or translate, every contribution makes BongoCat better.",
+ "code": "Code",
+ "docs": "Documentation",
+ "bugs": "Bug reports",
+ "translations": "Translations",
+ "joinGithub": "Contribute on GitHub"
+ },
+ "multiplePets": "Mostrar varias a la vez",
+ "multiplePetsLimit": "Se pueden mostrar hasta 8 mascotas a la vez",
+ "removeDesktopPet": "Cerrar esta mascota de escritorio",
"modelLoadFailed": "No se puede mostrar este modelo",
"noBehaviors": "No hay elementos",
+ "preferencesWindowTitle": "BongoCat - Settings",
+ "startup": {
+ "failed": "No se pudo iniciar BongoCat.",
+ "detail": "Consulte el registro de diagnóstico para ver los detalles técnicos.",
+ "diagnosticLog": "Registro de diagnóstico:"
+ },
"support": {
- "title": "Apoyar al equipo de desarrollo",
+ "title": "Acerca de",
+ "website": "Sitio web: bongocat.pet",
"heroText": "Gracias por su apoyo. Cada uso y cada difusión ayudan a BongoCat a seguir creciendo.",
"version": "Versión de la aplicación",
"checkUpdate": "Buscar actualizaciones",
"feedback": "Comentarios",
+ "openLogs": "Abrir registros",
+ "openLogsFailed": "No se pudo abrir la carpeta de registros",
"latest": "Ya está actualizado",
+ "checkingUpdate": "Comprobando...",
+ "updateAvailable": "Nueva versión disponible:",
+ "updateFailed": "No se pudieron buscar actualizaciones",
+ "updateHttpFailed": "GitHub devolvió el estado HTTP %s",
+ "downloadUpdate": "Actualizar a v%s",
+ "openStore": "Abrir Microsoft Store",
+ "viewReleases": "Ver versiones",
+ "openUpdateFailed": "No se pudo abrir la página de actualización",
"works": "Más aplicaciones",
- "worksText": "Más software de vladelaina",
- "refactorDeveloper": "(Desarrollador que reconstruyó BongoCat en C)",
+ "worksText": "Más creaciones de vladelaina que vale la pena probar",
+ "refactorDeveloper": "Desarrollador que reescribió BongoCat en C y que también es su mantenedor actual",
"community": "Comunidad",
"qqGroup": "Grupo de QQ",
- "coffee": "¿Invitar a un café al equipo de desarrollo?"
+ "coffee": "¿Invitar a un café al desarrollador?",
+ "starOnGitHub": "Estrella GitHub!",
+ "by": "por "
},
"shortcut": {
"toggleCat": "Mostrar/ocultar",
@@ -203,6 +267,7 @@
"select": "Seleccionar",
"selected": "Seleccionado",
"import": "Importar",
+ "importing": "Importando...",
"builtIn": "integrado"
}
}
diff --git a/resources/assets/locales/fr-FR.json b/resources/assets/locales/fr-FR.json
index 9aa0feed..d5a5d22b 100644
--- a/resources/assets/locales/fr-FR.json
+++ b/resources/assets/locales/fr-FR.json
@@ -9,38 +9,46 @@
"preference": {
"title": "Préférences",
"cat": {
- "title": "Affichage",
+ "title": "Préférences",
"labels": {
- "modelSettings": "Paramètres du modèle",
+ "modelSettings": "Modèle",
"mirrorMode": "Mode miroir",
"mouseMirror": "Miroir de la souris",
"mouseCentered": "Souris centrée sur le compagnon",
"ignoreMouse": "Ignorer les événements de souris",
- "windowSettings": "Paramètres de la fenêtre",
+ "windowSettings": "Fenêtre",
"passThrough": "Clics traversants",
"alwaysOnTop": "Toujours au premier plan",
"keepInScreen": "Maintenir à l'écran",
"windowSize": "Taille de la fenêtre",
+ "windowCorners": "Coins arrondis (%)",
"opacity": "Opacité",
- "autoReleaseDelay": "Délai de relâchement automatique",
+ "randomExpression": "Expressions aléatoires",
+ "obsBackground": "Arrière-plan uni",
"hideOnHover": "Masquer au survol",
+ "hideFadeSeconds": "Durée du fondu (s)",
"maxFPS": "Fréquence d'images maximale"
},
"hints": {
- "passThrough": "Lorsque cette option est activée, les clics traversent la fenêtre sans l'affecter.",
+ "hoverUnavailable": "Le masquage au survol est indisponible dans cet environnement de bureau.",
+ "gameInput": "La mascotte ne réagit pas en jeu ?",
+ "gameInputHelp": "Essayez de lancer BongoCat en tant qu’administrateur et de passer le jeu en mode fenêtré.",
+ "passThrough": "Désactivation possible aussi par clic droit sur l’icône de la zone de notification",
"alwaysOnTop": "Lorsque cette option est activée, la fenêtre reste au-dessus des autres fenêtres.",
"keepInScreen": "Lorsque cette option est activée, la fenêtre reste automatiquement dans les limites de l'écran.",
+ "obsBackground": "La capture de fenêtre est noire ?",
+ "obsBackgroundHelp": "OBS : activez cette option, utilisez la méthode compatible Windows 7 et supprimez le fond avec un filtre d'incrustation couleur.",
"windowSize": "Placez la souris sur le bord de la fenêtre, ou maintenez Maj et faites glisser avec le bouton droit pour redimensionner.",
- "hideOnHover": "Lorsque cette option est activée, la fenêtre se masque au survol de la souris. Vous pouvez définir un délai ; 0 signifie immédiatement."
+ "hideOnHover": "Lorsque le passage des clics et Toujours au premier plan sont activés, le compagnon se masque immédiatement au survol et réapparaît lorsque le pointeur s’éloigne."
}
},
"general": {
"title": "Général",
"labels": {
- "appSettings": "Paramètres de l'application",
+ "appSettings": "Application",
"launchOnStartup": "Lancer au démarrage",
"showTrayIcon": "Afficher l'icône dans la zone de notification",
- "appearanceSettings": "Paramètres d'apparence",
+ "appearanceSettings": "Apparence",
"themeMode": "Thème",
"language": "Langue",
"permissionsSettings": "Paramètres des autorisations",
@@ -54,8 +62,9 @@
},
"hints": {
"showTrayIcon": "Lorsque cette option est activée, l'icône de l'application apparaît dans la zone de notification.",
- "inputMonitoringPermission": "Activez la surveillance des entrées pour recevoir les événements clavier et souris du système.",
- "inputMonitoringPermissionGuide": "L'autorisation de surveillance des entrées est nécessaire pour recevoir les événements clavier et souris du système.\n\nÉtapes :\n1. Ouvrez Réglages Système > Confidentialité et sécurité > Surveillance de l'entrée.\n2. Si BongoCat figure déjà dans la liste, sélectionnez-le et cliquez sur le bouton \"-\" pour le retirer.\n3. Cliquez sur le bouton \"+\" pour rechercher et ajouter BongoCat.\n4. Redémarrez l'application pour appliquer l'autorisation.",
+ "autostartFailed": "Impossible de modifier le lancement au démarrage",
+ "inputMonitoringPermission": "Redémarrez après l'autorisation.",
+ "inputMonitoringPermissionGuide": "La surveillance des entrées permet à BongoCat de voir les touches pressées dans d'autres applications ; la souris continue de fonctionner sans elle. BongoCat construit l'écoute à son démarrage, un changement d'autorisation demande donc un redémarrage.\n\nÉtapes :\n1. Ouvrez Réglages Système → Confidentialité et sécurité → Surveillance de l'entrée.\n2. Activez l'entrée BongoCat déjà présente dans la liste ; ne supprimez aucune entrée.\n3. Si BongoCat n'est pas listé, cliquez sur \"+\" et ajoutez la copie que vous exécutez (vérifiez son emplacement).\n4. Quittez complètement BongoCat puis relancez-le pour qu'il reconstruise l'écoute.\n\nSi le clavier reste sans réponse, vous pouvez supprimer puis rajouter vous-même l'entrée BongoCat.",
"administratorPermission": "Exécuter l'application en tant qu'administrateur permet de capturer plus fiablement certaines touches système et certains événements d'entrée.",
"administratorPermissionGuide": "Pour capturer plus fiablement certaines touches système et certains événements d'entrée, il est recommandé d'exécuter l'application en tant qu'administrateur.\n\nProcédure :\n1. Quittez d'abord l'application.\n2. Trouvez le fichier exe ou son raccourci dans l'Explorateur de fichiers.\n3. Choisissez le mode de lancement :\n - Une fois : faites un clic droit et choisissez \"Exécuter en tant qu'administrateur\".\n - Toujours : clic droit > Propriétés > onglet Compatibilité > cochez \"Exécuter ce programme en tant qu'administrateur\" > OK."
},
@@ -63,7 +72,8 @@
"authorized": "Autorisé",
"adminEnabled": "Activé",
"authorize": "Activer",
- "viewGuide": "Voir les instructions"
+ "viewGuide": "Voir les instructions",
+ "openSettingsFailed": "Réglages Système → Confidentialité et sécurité → Surveillance de l'entrée."
},
"buttons": {
"openNow": "Ouvrir maintenant",
@@ -74,15 +84,27 @@
},
"model": {
"title": "Modèle",
+ "nearbyTitle": "Modèles à proximité",
"labels": {
- "deleteModel": "Supprimer le modèle",
- "nearbyModels": "Dossiers voisins"
+ "deleteModel": "Supprimer le modèle"
},
"hints": {
- "deleteSuccess": "Suppression réussie",
- "deleteModel": "Voulez-vous vraiment supprimer ce modèle ?",
+ "deleteSuccess": "« %s » a été supprimé",
+ "deleteModel": "Voulez-vous vraiment supprimer \"%s\" ?",
+ "deleteFailed": "Impossible de supprimer ce modèle",
"importSuccess": "Importation réussie",
- "clickOrDragToImport": "Cliquez ou déposez ici pour importer",
+ "importExists": "Le modèle existe déjà",
+ "importBatchResult": "Importations réussies : %zu ; échecs : %zu.\nModèles en échec : %s%s.\nPremier échec : %s",
+ "importMoreFailures": " et %zu autre(s)",
+ "importUnknownSource": "source inconnue",
+ "importInvalidSource": "La source sélectionnée n'existe plus ou ne peut pas être utilisée",
+ "importInvalidFormat": "Aucun modèle pris en charge n'a été trouvé, ou le paquet du modèle est incomplet",
+ "importFileAccess": "Impossible de lire ou d'enregistrer les fichiers du modèle. Vérifiez les autorisations et l'espace disque disponible",
+ "importOutOfMemory": "La mémoire disponible est insuffisante pour importer ce modèle",
+ "importLoadFailed": "Le modèle a été importé, mais il n'a pas pu être affiché",
+ "importFailed": "Échec de l'importation du modèle. Vérifiez les fichiers sélectionnés et réessayez",
+ "clickOrDragToImport": "Cliquez ou déposez un dossier",
+ "supportedImportFormats": "Formats Mver et Tauri pris en charge",
"openDirectoryFailed": "Impossible d'ouvrir le dossier du modèle"
},
"tooltips": {
@@ -91,10 +113,12 @@
"moreModels": "Plus de modèles"
},
"behaviorModal": {
- "title": "Mouvements et expressions",
+ "title": "Mouvements, expressions et audio",
"labels": {
"motion": "Mouvement",
"expression": "Expression",
+ "audio": "Audio",
+ "stopAllAudio": "Arrêter tous les sons",
"motionIndex": "Mouvement {index}",
"motionGroupIndex": "Groupe de mouvements {index}",
"expressionIndex": "Expression {index}"
@@ -171,21 +195,61 @@
}
},
"native": {
+ "about": {
+ "wechat": "WeChat",
+ "wechatHint": "Hover to scan and chat",
+ "loading": "Loading...",
+ "contributorsFailed": "Cannot load contributors. Click to retry.",
+ "qrUnavailable": "QR code unavailable",
+ "reload": "Reload",
+ "qrFailed": "QR code unavailable. Click to retry.",
+ "contributorsTitle": "Open source & community contributions",
+ "contributorsText": "Every step of BongoCat comes from the power of open source. Thank you to all community contributors for making desktop companionship freer and simpler.",
+ "joinTitle": "Want to appear in the contributors list?",
+ "joinText": "Whether you contribute code, improve documentation, report bugs or translate, every contribution makes BongoCat better.",
+ "code": "Code",
+ "docs": "Documentation",
+ "bugs": "Bug reports",
+ "translations": "Translations",
+ "joinGithub": "Contribute on GitHub"
+ },
+ "multiplePets": "Afficher plusieurs modèles",
+ "multiplePetsLimit": "Jusqu'\u00e0 8 mascottes peuvent \u00eatre affich\u00e9es \u00e0 la fois",
+ "removeDesktopPet": "Fermer cette mascotte de bureau",
"modelLoadFailed": "Impossible d'afficher ce modèle",
"noBehaviors": "Aucun élément",
+ "preferencesWindowTitle": "BongoCat - Settings",
+ "startup": {
+ "failed": "Impossible de démarrer BongoCat.",
+ "detail": "Consultez le journal de diagnostic pour les détails techniques.",
+ "diagnosticLog": "Journal de diagnostic :"
+ },
"support": {
- "title": "Soutenir l'équipe de développement",
+ "title": "À propos",
+ "website": "Site web: bongocat.pet",
"heroText": "Merci pour votre soutien. Chaque utilisation et chaque partage aide BongoCat à continuer de grandir.",
"version": "Version de l'application",
"checkUpdate": "Rechercher des mises à jour",
"feedback": "Commentaires",
+ "openLogs": "Ouvrir les journaux",
+ "openLogsFailed": "Impossible d'ouvrir le dossier des journaux",
"latest": "Déjà à jour",
+ "checkingUpdate": "Vérification...",
+ "updateAvailable": "Nouvelle version disponible :",
+ "updateFailed": "Impossible de vérifier les mises à jour",
+ "updateHttpFailed": "GitHub a renvoyé le statut HTTP %s",
+ "downloadUpdate": "Mettre à jour vers v%s",
+ "openStore": "Ouvrir Microsoft Store",
+ "viewReleases": "Voir les versions",
+ "openUpdateFailed": "Impossible d'ouvrir la page de mise à jour",
"works": "Plus d'applications",
- "worksText": "Plus de logiciels de vladelaina",
- "refactorDeveloper": "(Développeur ayant reconstruit BongoCat en C)",
+ "worksText": "D’autres créations de vladelaina à découvrir",
+ "refactorDeveloper": "Développeur qui a réécrit BongoCat en C et qui en est aussi le mainteneur actuel",
"community": "Communauté",
"qqGroup": "Groupe QQ",
- "coffee": "Offrir un café à l'équipe de développement ?"
+ "coffee": "Offrir un café au développeur ?",
+ "starOnGitHub": "Étoile GitHub!",
+ "by": "par "
},
"shortcut": {
"toggleCat": "Afficher/masquer",
@@ -203,6 +267,7 @@
"select": "Sélectionner",
"selected": "Sélectionné",
"import": "Importer",
+ "importing": "Importation...",
"builtIn": "intégré"
}
}
diff --git a/resources/assets/locales/ja-JP.json b/resources/assets/locales/ja-JP.json
index eeae10f7..5ec817aa 100644
--- a/resources/assets/locales/ja-JP.json
+++ b/resources/assets/locales/ja-JP.json
@@ -9,38 +9,46 @@
"preference": {
"title": "設定",
"cat": {
- "title": "表示",
+ "title": "設定",
"labels": {
- "modelSettings": "モデル設定",
+ "modelSettings": "モデル",
"mirrorMode": "ミラーモード",
"mouseMirror": "マウスを反転",
"mouseCentered": "マウスをデスクトップペット中心にする",
"ignoreMouse": "マウスイベントを無視",
- "windowSettings": "ウィンドウ設定",
+ "windowSettings": "ウィンドウ",
"passThrough": "クリックを透過",
"alwaysOnTop": "常に手前に表示",
"keepInScreen": "画面内に保持",
"windowSize": "ウィンドウサイズ",
+ "windowCorners": "ウィンドウの角丸 (%)",
"opacity": "不透明度",
- "autoReleaseDelay": "自動リリースまでの時間",
+ "randomExpression": "ランダム表情",
+ "obsBackground": "単色背景",
"hideOnHover": "ホバー時に非表示",
+ "hideFadeSeconds": "フェード時間 (秒)",
"maxFPS": "最大フレームレート"
},
"hints": {
- "passThrough": "有効にすると、クリックはウィンドウに影響せず背後へ通過します。",
+ "hoverUnavailable": "現在のデスクトップ環境では、マウスを重ねて非表示にする機能は利用できません。",
+ "gameInput": "ゲーム中にデスクトップペットが反応しない?",
+ "gameInputHelp": "BongoCat を管理者として起動し、ゲームをウィンドウモードに設定してみてください。",
+ "passThrough": "トレイアイコンの右クリックからもオフにできます",
"alwaysOnTop": "有効にすると、ウィンドウがほかのすべてのウィンドウより手前に表示されます。",
"keepInScreen": "有効にすると、ウィンドウが自動的に画面の範囲内に収まります。",
+ "obsBackground": "ウィンドウキャプチャが黒い?",
+ "obsBackgroundHelp": "OBS:「Windows 7互換」方式でキャプチャする場合はこの項目を有効にし、カラーキーフィルターで背景を除去してください。",
"windowSize": "マウスをウィンドウ端へ移動するか、Shiftキーを押しながら右ドラッグしてサイズを変更します。",
- "hideOnHover": "有効にすると、マウスを重ねたときにウィンドウが自動的に隠れます。遅延を設定でき、0ではすぐに隠れます。"
+ "hideOnHover": "クリック透過と最前面表示が有効な場合、ペットにマウスを重ねるとすぐに非表示になり、離すと再表示されます。"
}
},
"general": {
"title": "一般",
"labels": {
- "appSettings": "アプリ設定",
+ "appSettings": "アプリ",
"launchOnStartup": "起動時に実行",
"showTrayIcon": "トレイアイコンを表示",
- "appearanceSettings": "外観設定",
+ "appearanceSettings": "外観",
"themeMode": "テーマ",
"language": "言語",
"permissionsSettings": "権限設定",
@@ -54,8 +62,9 @@
},
"hints": {
"showTrayIcon": "有効にすると、システムトレイにアプリアイコンを表示します。",
- "inputMonitoringPermission": "システムからキーボードとマウスのイベントを受け取るには、入力監視を有効にしてください。",
- "inputMonitoringPermissionGuide": "システムからキーボードとマウスのイベントを受け取るには、入力監視の権限が必要です。\n\n手順:\n1. システム設定 > プライバシーとセキュリティ > 入力監視を開きます。\n2. BongoCatがすでに一覧にある場合は選択し、\"-\"ボタンで削除します。\n3. \"+\"ボタンをクリックしてBongoCatを探し、追加します。\n4. 権限を反映するため、アプリを再起動します。",
+ "autostartFailed": "起動時に実行する設定を更新できません",
+ "inputMonitoringPermission": "許可したら再起動してください。",
+ "inputMonitoringPermissionGuide": "入力監視の権限があると、BongoCat は他のアプリでのキー入力を取得できます。マウスは権限なしでも動作します。BongoCat は起動時に監視を構築するため、権限を変更した場合は再起動が必要です。\n\n手順:\n1. 「システム設定」→「プライバシーとセキュリティ」→「入力監視」を開きます。\n2. 一覧にすでにある BongoCat の項目をオンにします。既存の項目は削除しないでください。\n3. 一覧に BongoCat がない場合は「+」をクリックし、現在実行しているアプリを追加します(アプリの場所を確認してください)。\n4. BongoCat を完全に終了してから再起動し、監視を再構築させます。\n\nキーボードが反応しないままの場合は、ご自身で BongoCat の項目を削除して再追加して確認できます。",
"administratorPermission": "管理者として実行すると、一部のシステムキーや入力イベントをより確実に取得できます。",
"administratorPermissionGuide": "一部のシステムキーや入力イベントをより確実に取得するため、アプリを管理者として実行することを推奨します。\n\n手順:\n1. まずアプリを終了します。\n2. エクスプローラーでアプリのexeファイルまたはショートカットを探します。\n3. 起動方法を選びます:\n - 今回のみ:右クリックして\"管理者として実行\"を選びます。\n - 常に:右クリック > プロパティ > 互換性 > \"管理者としてこのプログラムを実行する\"にチェック > OK。"
},
@@ -63,7 +72,8 @@
"authorized": "許可済み",
"adminEnabled": "有効",
"authorize": "有効にする",
- "viewGuide": "手順を表示"
+ "viewGuide": "手順を表示",
+ "openSettingsFailed": "手動で:システム設定 → プライバシーとセキュリティ → 入力監視。"
},
"buttons": {
"openNow": "今すぐ開く",
@@ -74,15 +84,27 @@
},
"model": {
"title": "モデル",
+ "nearbyTitle": "近くのモデル",
"labels": {
- "deleteModel": "モデルを削除",
- "nearbyModels": "近くのフォルダー"
+ "deleteModel": "モデルを削除"
},
"hints": {
- "deleteSuccess": "削除しました",
- "deleteModel": "このモデルを削除しますか?",
+ "deleteSuccess": "「%s」を削除しました",
+ "deleteModel": "「%s」を削除しますか?",
+ "deleteFailed": "このモデルを削除できません",
"importSuccess": "インポートしました",
- "clickOrDragToImport": "クリックまたはここへドラッグしてインポート",
+ "importExists": "モデルは既に存在します",
+ "importBatchResult": "インポート成功: %zu件、失敗: %zu件。\n失敗したモデル: %s%s。\n最初の失敗理由: %s",
+ "importMoreFailures": "、他%zu件",
+ "importUnknownSource": "不明なソース",
+ "importInvalidSource": "選択したインポート元は存在しないか、使用できません",
+ "importInvalidFormat": "対応するモデルが見つからないか、モデルパッケージが不完全です",
+ "importFileAccess": "モデルファイルを読み込みまたは保存できません。ファイルの権限とディスクの空き容量を確認してください",
+ "importOutOfMemory": "メモリが不足しているため、このモデルをインポートできません",
+ "importLoadFailed": "モデルはインポートされましたが、表示できませんでした",
+ "importFailed": "モデルのインポートに失敗しました。選択したファイルを確認して、もう一度お試しください",
+ "clickOrDragToImport": "モデルフォルダーをクリックまたはドラッグ",
+ "supportedImportFormats": "Mver・Tauri形式に対応",
"openDirectoryFailed": "モデルフォルダーを開けません"
},
"tooltips": {
@@ -91,10 +113,12 @@
"moreModels": "その他のモデル"
},
"behaviorModal": {
- "title": "モーションと表情",
+ "title": "モーション・表情・音声",
"labels": {
"motion": "モーション",
"expression": "表情",
+ "audio": "音声",
+ "stopAllAudio": "すべての音声を停止",
"motionIndex": "モーション {index}",
"motionGroupIndex": "モーショングループ {index}",
"expressionIndex": "表情 {index}"
@@ -171,21 +195,61 @@
}
},
"native": {
+ "about": {
+ "wechat": "WeChat",
+ "wechatHint": "Hover to scan and chat",
+ "loading": "Loading...",
+ "contributorsFailed": "Cannot load contributors. Click to retry.",
+ "qrUnavailable": "QR code unavailable",
+ "reload": "Reload",
+ "qrFailed": "QR code unavailable. Click to retry.",
+ "contributorsTitle": "Open source & community contributions",
+ "contributorsText": "Every step of BongoCat comes from the power of open source. Thank you to all community contributors for making desktop companionship freer and simpler.",
+ "joinTitle": "Want to appear in the contributors list?",
+ "joinText": "Whether you contribute code, improve documentation, report bugs or translate, every contribution makes BongoCat better.",
+ "code": "Code",
+ "docs": "Documentation",
+ "bugs": "Bug reports",
+ "translations": "Translations",
+ "joinGithub": "Contribute on GitHub"
+ },
+ "multiplePets": "複数を同時に表示",
+ "multiplePetsLimit": "\u4e00\u5ea6\u306b\u8868\u793a\u3067\u304d\u308b\u30c7\u30b9\u30af\u30c8\u30c3\u30d7\u30da\u30c3\u30c8\u306f 8 \u4f53\u307e\u3067\u3067\u3059",
+ "removeDesktopPet": "\u3053\u306e\u30c7\u30b9\u30af\u30c8\u30c3\u30d7\u30da\u30c3\u30c8\u3092\u9589\u3058\u308b",
"modelLoadFailed": "このモデルを表示できません",
"noBehaviors": "項目がありません",
+ "preferencesWindowTitle": "BongoCat - Settings",
+ "startup": {
+ "failed": "BongoCat を起動できませんでした。",
+ "detail": "技術的な詳細は診断ログを確認してください。",
+ "diagnosticLog": "診断ログ:"
+ },
"support": {
- "title": "開発チームを支援",
+ "title": "このアプリについて",
+ "website": "公式サイト: bongocat.pet",
"heroText": "ご支援ありがとうございます。利用や共有の一つひとつがBongoCatの成長につながります。",
"version": "アプリのバージョン",
"checkUpdate": "更新を確認",
"feedback": "フィードバック",
+ "openLogs": "ログを開く",
+ "openLogsFailed": "ログフォルダーを開けません",
"latest": "最新バージョンです",
+ "checkingUpdate": "確認中...",
+ "updateAvailable": "新しいバージョンがあります:",
+ "updateFailed": "更新を確認できませんでした",
+ "updateHttpFailed": "GitHub が HTTP ステータス %s を返しました",
+ "downloadUpdate": "v%s に更新",
+ "openStore": "Microsoft Store を開く",
+ "viewReleases": "リリースを表示",
+ "openUpdateFailed": "更新ページを開けませんでした",
"works": "その他のアプリ",
- "worksText": "vladelainaのその他のソフトウェア",
- "refactorDeveloper": "(BongoCatをCで再構築した開発者)",
+ "worksText": "vladelainaのほかの作品も、ぜひお試しください",
+ "refactorDeveloper": "BongoCatをC言語で書き直した開発者であり、現在のメンテナーでもある",
"community": "コミュニティ",
"qqGroup": "QQグループ",
- "coffee": "開発チームにコーヒーを贈りますか?"
+ "coffee": "開発者にコーヒーを贈りますか?",
+ "starOnGitHub": "GitHubでスター!",
+ "by": "開発:"
},
"shortcut": {
"toggleCat": "表示/非表示",
@@ -203,6 +267,7 @@
"select": "選択",
"selected": "選択済み",
"import": "インポート",
+ "importing": "インポート中...",
"builtIn": "内蔵"
}
}
diff --git a/resources/assets/locales/ko-KR.json b/resources/assets/locales/ko-KR.json
index 3ad007fc..d57f0c11 100644
--- a/resources/assets/locales/ko-KR.json
+++ b/resources/assets/locales/ko-KR.json
@@ -9,38 +9,46 @@
"preference": {
"title": "환경 설정",
"cat": {
- "title": "표시",
+ "title": "환경 설정",
"labels": {
- "modelSettings": "모델 설정",
+ "modelSettings": "모델",
"mirrorMode": "미러 모드",
"mouseMirror": "마우스 반전",
"mouseCentered": "마우스를 데스크톱 펫 중심으로",
"ignoreMouse": "마우스 이벤트 무시",
- "windowSettings": "창 설정",
+ "windowSettings": "창",
"passThrough": "클릭 통과",
"alwaysOnTop": "항상 위",
"keepInScreen": "화면 안에 유지",
"windowSize": "창 크기",
+ "windowCorners": "창 모서리 둥글기 (%)",
"opacity": "불투명도",
- "autoReleaseDelay": "자동 해제 지연",
+ "randomExpression": "무작위 표정",
+ "obsBackground": "단색 배경",
"hideOnHover": "마우스를 올리면 숨기기",
+ "hideFadeSeconds": "페이드 시간 (초)",
"maxFPS": "최대 프레임 속도"
},
"hints": {
- "passThrough": "활성화하면 클릭이 창에 영향을 주지 않고 뒤로 통과합니다.",
+ "hoverUnavailable": "현재 데스크톱 환경에서는 마우스를 올려 숨기기 기능을 사용할 수 없습니다.",
+ "gameInput": "게임 중 데스크톱 펫이 반응하지 않나요?",
+ "gameInputHelp": "BongoCat을 관리자 권한으로 실행하고 게임을 창 모드로 설정해 보세요.",
+ "passThrough": "트레이 아이콘을 우클릭해서도 끌 수 있습니다",
"alwaysOnTop": "활성화하면 창이 다른 모든 창 위에 유지됩니다.",
"keepInScreen": "활성화하면 창이 자동으로 화면 경계 안에 유지됩니다.",
+ "obsBackground": "창 캡처가 검은 화면인가요?",
+ "obsBackgroundHelp": "OBS: 이 옵션을 켜고 Windows 7 호환 방식으로 캡처한 뒤 색상 키 필터로 배경을 제거하세요.",
"windowSize": "마우스를 창 가장자리로 이동하거나 Shift를 누른 채 오른쪽 버튼으로 끌어 크기를 조절합니다.",
- "hideOnHover": "활성화하면 마우스를 올렸을 때 창이 자동으로 숨겨집니다. 지연 시간을 설정할 수 있으며 0은 즉시 숨김입니다."
+ "hideOnHover": "클릭 통과와 항상 위에 표시가 모두 켜져 있으면 펫에 마우스를 올릴 때 즉시 숨기고, 마우스를 치우면 다시 표시합니다."
}
},
"general": {
"title": "일반",
"labels": {
- "appSettings": "앱 설정",
+ "appSettings": "앱",
"launchOnStartup": "시작할 때 실행",
"showTrayIcon": "트레이 아이콘 표시",
- "appearanceSettings": "모양 설정",
+ "appearanceSettings": "모양",
"themeMode": "테마",
"language": "언어",
"permissionsSettings": "권한 설정",
@@ -54,8 +62,9 @@
},
"hints": {
"showTrayIcon": "활성화하면 시스템 트레이에 앱 아이콘이 표시됩니다.",
- "inputMonitoringPermission": "시스템의 키보드와 마우스 이벤트를 받으려면 입력 모니터링을 활성화하세요.",
- "inputMonitoringPermissionGuide": "앱이 시스템의 키보드와 마우스 이벤트를 받으려면 입력 모니터링 권한이 필요합니다.\n\n단계:\n1. 시스템 설정 > 개인정보 보호 및 보안 > 입력 모니터링을 엽니다.\n2. BongoCat이 이미 목록에 있으면 선택하고 \"-\" 버튼을 눌러 제거합니다.\n3. \"+\" 버튼을 눌러 BongoCat을 찾아 추가합니다.\n4. 권한을 적용하려면 앱을 다시 시작합니다.",
+ "autostartFailed": "시작 프로그램 설정을 변경할 수 없습니다",
+ "inputMonitoringPermission": "허용한 뒤 다시 시작하세요.",
+ "inputMonitoringPermissionGuide": "입력 모니터링 권한이 있으면 BongoCat이 다른 앱에서 누른 키를 받을 수 있습니다. 마우스는 권한 없이도 동작합니다. BongoCat은 시작할 때 감시를 만들기 때문에 권한을 바꾼 뒤에는 다시 시작해야 합니다.\n\n단계:\n1. 시스템 설정 → 개인정보 보호 및 보안 → 입력 모니터링을 엽니다.\n2. 목록에 이미 있는 BongoCat 항목을 켭니다. 기존 항목은 삭제하지 마세요.\n3. 목록에 BongoCat이 없으면 \"+\"를 클릭해 현재 실행 중인 앱을 추가합니다(앱 위치를 확인하세요).\n4. BongoCat을 완전히 종료한 뒤 다시 시작해 감시를 다시 만들게 합니다.\n\n키보드가 계속 반응하지 않으면 BongoCat 항목을 직접 삭제하고 다시 추가해 확인할 수 있습니다.",
"administratorPermission": "앱을 관리자로 실행하면 일부 시스템 키와 입력 이벤트를 더 안정적으로 캡처할 수 있습니다.",
"administratorPermissionGuide": "일부 시스템 키와 입력 이벤트를 더 안정적으로 캡처하려면 앱을 관리자로 실행하는 것이 좋습니다.\n\n방법:\n1. 먼저 앱을 종료합니다.\n2. 파일 탐색기에서 앱의 exe 파일 또는 바로 가기를 찾습니다.\n3. 실행 방법을 선택합니다:\n - 한 번만: 마우스 오른쪽 버튼을 누르고 \"관리자 권한으로 실행\"을 선택합니다.\n - 항상: 오른쪽 클릭 > 속성 > 호환성 > \"관리자 권한으로 이 프로그램 실행\" 선택 > 확인."
},
@@ -63,7 +72,8 @@
"authorized": "허용됨",
"adminEnabled": "활성화됨",
"authorize": "활성화",
- "viewGuide": "안내 보기"
+ "viewGuide": "안내 보기",
+ "openSettingsFailed": "직접 여세요: 시스템 설정 → 개인정보 보호 및 보안 → 입력 모니터링."
},
"buttons": {
"openNow": "지금 열기",
@@ -74,15 +84,27 @@
},
"model": {
"title": "모델",
+ "nearbyTitle": "주변 모델",
"labels": {
- "deleteModel": "모델 삭제",
- "nearbyModels": "주변 폴더"
+ "deleteModel": "모델 삭제"
},
"hints": {
- "deleteSuccess": "삭제 완료",
- "deleteModel": "이 모델을 삭제하시겠습니까?",
+ "deleteSuccess": "\"%s\" 모델을 삭제했습니다",
+ "deleteModel": "\"%s\" 모델을 삭제하시겠습니까?",
+ "deleteFailed": "이 모델을 삭제할 수 없습니다",
"importSuccess": "가져오기 완료",
- "clickOrDragToImport": "클릭하거나 여기로 끌어와 가져오기",
+ "importExists": "모델이 이미 존재합니다",
+ "importBatchResult": "가져오기 성공 %zu개, 실패 %zu개.\n실패한 모델: %s%s.\n첫 번째 실패 원인: %s",
+ "importMoreFailures": " 외 %zu개",
+ "importUnknownSource": "알 수 없는 원본",
+ "importInvalidSource": "선택한 원본이 더 이상 존재하지 않거나 사용할 수 없습니다",
+ "importInvalidFormat": "지원되는 모델을 찾을 수 없거나 모델 패키지가 완전하지 않습니다",
+ "importFileAccess": "모델 파일을 읽거나 저장할 수 없습니다. 파일 권한과 사용 가능한 디스크 공간을 확인하세요",
+ "importOutOfMemory": "메모리가 부족하여 이 모델을 가져올 수 없습니다",
+ "importLoadFailed": "모델을 가져왔지만 표시할 수 없습니다",
+ "importFailed": "모델을 가져오지 못했습니다. 선택한 파일을 확인한 후 다시 시도하세요",
+ "clickOrDragToImport": "모델 폴더를 클릭하거나 끌어오세요",
+ "supportedImportFormats": "Mver 및 Tauri 형식 지원",
"openDirectoryFailed": "모델 폴더를 열 수 없습니다"
},
"tooltips": {
@@ -91,10 +113,12 @@
"moreModels": "더 많은 모델"
},
"behaviorModal": {
- "title": "모션 및 표정",
+ "title": "모션, 표정 및 오디오",
"labels": {
"motion": "모션",
"expression": "표정",
+ "audio": "오디오",
+ "stopAllAudio": "모든 오디오 중지",
"motionIndex": "모션 {index}",
"motionGroupIndex": "모션 그룹 {index}",
"expressionIndex": "표정 {index}"
@@ -171,21 +195,61 @@
}
},
"native": {
+ "about": {
+ "wechat": "WeChat",
+ "wechatHint": "Hover to scan and chat",
+ "loading": "Loading...",
+ "contributorsFailed": "Cannot load contributors. Click to retry.",
+ "qrUnavailable": "QR code unavailable",
+ "reload": "Reload",
+ "qrFailed": "QR code unavailable. Click to retry.",
+ "contributorsTitle": "Open source & community contributions",
+ "contributorsText": "Every step of BongoCat comes from the power of open source. Thank you to all community contributors for making desktop companionship freer and simpler.",
+ "joinTitle": "Want to appear in the contributors list?",
+ "joinText": "Whether you contribute code, improve documentation, report bugs or translate, every contribution makes BongoCat better.",
+ "code": "Code",
+ "docs": "Documentation",
+ "bugs": "Bug reports",
+ "translations": "Translations",
+ "joinGithub": "Contribute on GitHub"
+ },
+ "multiplePets": "여러 개 동시에 표시",
+ "multiplePetsLimit": "\ub370\uc2a4\ud06c\ud1b1 \ud3ab\uc740 \ud55c \ubc88\uc5d0 \ucd5c\ub300 8\uac1c\uae4c\uc9c0 \ud45c\uc2dc\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4",
+ "removeDesktopPet": "\uc774 \ub370\uc2a4\ud06c\ud1b1 \ud3ab \ub2eb\uae30",
"modelLoadFailed": "이 모델을 표시할 수 없습니다",
"noBehaviors": "항목 없음",
+ "preferencesWindowTitle": "BongoCat - Settings",
+ "startup": {
+ "failed": "BongoCat을 시작할 수 없습니다.",
+ "detail": "기술적인 세부 정보는 진단 로그를 확인하세요.",
+ "diagnosticLog": "진단 로그:"
+ },
"support": {
- "title": "개발팀 지원",
+ "title": "정보",
+ "website": "공식 웹사이트: bongocat.pet",
"heroText": "지원해 주셔서 감사합니다. 사용할 때마다, 공유할 때마다 BongoCat이 계속 성장하는 데 도움이 됩니다.",
"version": "앱 버전",
"checkUpdate": "업데이트 확인",
"feedback": "피드백",
+ "openLogs": "로그 열기",
+ "openLogsFailed": "로그 폴더를 열 수 없습니다",
"latest": "최신 버전입니다",
+ "checkingUpdate": "확인 중...",
+ "updateAvailable": "새 버전 사용 가능:",
+ "updateFailed": "업데이트를 확인할 수 없습니다",
+ "updateHttpFailed": "GitHub에서 HTTP 상태 코드 %s를 반환했습니다",
+ "downloadUpdate": "v%s(으)로 업데이트",
+ "openStore": "Microsoft Store 열기",
+ "viewReleases": "릴리스 보기",
+ "openUpdateFailed": "업데이트 페이지를 열 수 없습니다",
"works": "더 많은 앱",
- "worksText": "vladelaina의 다른 소프트웨어",
- "refactorDeveloper": "(BongoCat을 C로 다시 만든 개발자)",
+ "worksText": "vladelaina의 다른 작품도 만나보세요",
+ "refactorDeveloper": "BongoCat을 C 언어로 재작성한 개발자이자 현재 유지보수 담당자",
"community": "커뮤니티",
"qqGroup": "QQ 그룹",
- "coffee": "개발팀에 커피 한 잔을 선물할까요?"
+ "coffee": "개발자에게 커피 한 잔을 선물할까요?",
+ "starOnGitHub": "GitHub 스타!",
+ "by": "제작: "
},
"shortcut": {
"toggleCat": "표시/숨기기",
@@ -203,6 +267,7 @@
"select": "선택",
"selected": "선택됨",
"import": "가져오기",
+ "importing": "가져오는 중...",
"builtIn": "내장"
}
}
diff --git a/resources/assets/locales/pt-BR.json b/resources/assets/locales/pt-BR.json
index 9846659d..669632c2 100644
--- a/resources/assets/locales/pt-BR.json
+++ b/resources/assets/locales/pt-BR.json
@@ -9,38 +9,46 @@
"preference": {
"title": "Preferências",
"cat": {
- "title": "Exibição",
+ "title": "Preferências",
"labels": {
- "modelSettings": "Configurações do Modelo",
+ "modelSettings": "Modelo",
"mirrorMode": "Modo Espelho",
"mouseMirror": "Espelho do Mouse",
"mouseCentered": "Mouse centrado no mascote de mesa",
"ignoreMouse": "Ignorar Eventos do Mouse",
- "windowSettings": "Configurações da Janela",
+ "windowSettings": "Janela",
"passThrough": "Janela Transparente",
"alwaysOnTop": "Sempre no Topo",
"keepInScreen": "Manter na Tela",
"windowSize": "Tamanho da Janela",
+ "windowCorners": "Cantos arredondados (%)",
"opacity": "Opacidade",
- "autoReleaseDelay": "Atraso de Liberação Automática",
+ "randomExpression": "Expressões Aleatórias",
+ "obsBackground": "Fundo sólido",
"hideOnHover": "Ocultar ao Passar o Mouse",
+ "hideFadeSeconds": "Duração do fade (s)",
"maxFPS": "Taxa Máxima de Quadros"
},
"hints": {
- "passThrough": "Quando ativado, a janela não afetará operações em outros aplicativos.",
+ "hoverUnavailable": "Ocultar ao passar o mouse não está disponível no ambiente de desktop atual.",
+ "gameInput": "O mascote não responde ao jogar?",
+ "gameInputHelp": "Tente executar o BongoCat como administrador e colocar o jogo em modo janela.",
+ "passThrough": "Também é possível desativar pelo clique direito no ícone da bandeja",
"alwaysOnTop": "Quando ativado, a janela sempre ficará acima de outros aplicativos.",
"keepInScreen": "Quando ativado, a janela permanece automaticamente dentro dos limites da tela.",
+ "obsBackground": "A captura da janela está preta?",
+ "obsBackgroundHelp": "OBS: ative esta opção, use o método compatível com o Windows 7 e remova o fundo com um filtro de chave de cor.",
"windowSize": "Mova o mouse para a borda da janela ou segure Shift e arraste com o botão direito para redimensionar.",
- "hideOnHover": "Quando ativado, a janela se oculta automaticamente ao passar o mouse. Você pode definir um atraso; 0 significa ocultar imediatamente."
+ "hideOnHover": "Com a passagem de cliques e Sempre no Topo ativados, o mascote se oculta imediatamente ao passar o mouse e reaparece ao afastá-lo."
}
},
"general": {
"title": "Geral",
"labels": {
- "appSettings": "Configurações do aplicativo",
+ "appSettings": "Aplicativo",
"launchOnStartup": "Iniciar na inicialização",
"showTrayIcon": "Mostrar ícone na bandeja",
- "appearanceSettings": "Configurações de aparência",
+ "appearanceSettings": "Aparência",
"themeMode": "Tema",
"language": "Idiomas",
"permissionsSettings": "Configurações de Permissões",
@@ -54,8 +62,9 @@
},
"hints": {
"showTrayIcon": "Quando ativado, o ícone do aplicativo é exibido na bandeja do sistema.",
- "inputMonitoringPermission": "Ative a permissão de monitoramento de entrada para receber eventos de teclado e mouse do sistema para responder às suas ações.",
- "inputMonitoringPermissionGuide": "A permissão de Monitoramento de Entrada é necessária para que o aplicativo receba eventos de teclado e mouse do sistema.\n\nPassos:\n1. Abra as Configurações do Sistema → Privacidade e Segurança → Monitoramento de Entrada.\n2. Se o BongoCat já estiver na lista, selecione-o e clique no botão \"-\" para removê-lo.\n3. Clique no botão \"+\" para localizar e adicionar o BongoCat.\n4. Reinicie o aplicativo para que a permissão entre em vigor.",
+ "autostartFailed": "Não foi possível atualizar a inicialização automática",
+ "inputMonitoringPermission": "Reinicie após autorizar.",
+ "inputMonitoringPermissionGuide": "O Monitoramento de Entrada permite que o BongoCat veja teclas pressionadas em outros aplicativos; o mouse continua funcionando sem ele. O BongoCat cria o monitoramento ao iniciar, então uma mudança de permissão exige reiniciar.\n\nPassos:\n1. Abra Ajustes do Sistema → Privacidade e Segurança → Monitoramento de Entrada.\n2. Ative a entrada do BongoCat que já está na lista; não remova entradas existentes.\n3. Se o BongoCat não estiver na lista, clique em \"+\" e adicione a cópia que você executa (confira o local do aplicativo).\n4. Encerre o BongoCat por completo e inicie de novo para recriar o monitoramento.\n\nSe o teclado continuar sem resposta, você pode remover e adicionar de novo a entrada do BongoCat.",
"administratorPermission": "Executar o aplicativo como administrador ajuda a capturar algumas teclas e eventos de entrada em nível de sistema com mais confiabilidade.",
"administratorPermissionGuide": "Para capturar algumas teclas e eventos de entrada em nível de sistema com mais confiabilidade, é recomendado executar o aplicativo como administrador.\n\nComo fazer:\n1. Feche o aplicativo primeiro.\n2. Encontre o exe do aplicativo ou o atalho no Explorador de Arquivos.\n3. Escolha como iniciar:\n • Apenas desta vez: Clique com o botão direito e escolha \"Executar como administrador\".\n • Sempre (padrão): Botão direito → Propriedades → aba Compatibilidade → marque \"Executar este programa como administrador\" → OK."
},
@@ -63,7 +72,8 @@
"authorized": "Autorizado",
"adminEnabled": "Ativado",
"authorize": "Ir para Ativar",
- "viewGuide": "Ver instruções"
+ "viewGuide": "Ver instruções",
+ "openSettingsFailed": "Ajustes → Privacidade e Segurança → Monitoramento de Entrada."
},
"buttons": {
"openNow": "Abrir Agora",
@@ -74,15 +84,27 @@
},
"model": {
"title": "Modelo",
+ "nearbyTitle": "Modelos próximos",
"labels": {
- "deleteModel": "Excluir modelo",
- "nearbyModels": "Pastas próximas"
+ "deleteModel": "Excluir modelo"
},
"hints": {
- "deleteSuccess": "Excluído com sucesso",
- "deleteModel": "Tem certeza de que deseja excluir este modelo?",
+ "deleteSuccess": "Modelo \"%s\" excluído",
+ "deleteModel": "Tem certeza de que deseja excluir \"%s\"?",
+ "deleteFailed": "Não foi possível excluir este modelo",
"importSuccess": "Importação bem-sucedida",
- "clickOrDragToImport": "Clique ou arraste para importar",
+ "importExists": "O modelo já existe",
+ "importBatchResult": "Importados com sucesso: %zu; falhas: %zu.\nModelos com falha: %s%s.\nPrimeira falha: %s",
+ "importMoreFailures": " e mais %zu",
+ "importUnknownSource": "origem desconhecida",
+ "importInvalidSource": "A origem selecionada não existe mais ou não pode ser usada",
+ "importInvalidFormat": "Nenhum modelo compatível foi encontrado ou o pacote do modelo está incompleto",
+ "importFileAccess": "Não foi possível ler ou salvar os arquivos do modelo. Verifique as permissões e o espaço disponível em disco",
+ "importOutOfMemory": "Não há memória suficiente para importar este modelo",
+ "importLoadFailed": "O modelo foi importado, mas não pôde ser exibido",
+ "importFailed": "Falha ao importar o modelo. Verifique os arquivos selecionados e tente novamente",
+ "clickOrDragToImport": "Clique ou arraste uma pasta",
+ "supportedImportFormats": "Compatível com Mver e Tauri",
"openDirectoryFailed": "Não foi possível abrir a pasta do modelo"
},
"tooltips": {
@@ -91,10 +113,12 @@
"moreModels": "Mais modelos"
},
"behaviorModal": {
- "title": "Movimentos e Expressões",
+ "title": "Movimentos, expressões e áudio",
"labels": {
"motion": "Movimento",
"expression": "Expressão",
+ "audio": "Áudio",
+ "stopAllAudio": "Parar todos os áudios",
"motionIndex": "Movimento {index}",
"motionGroupIndex": "Grupo de Movimento {index}",
"expressionIndex": "Expressão {index}"
@@ -171,21 +195,61 @@
}
},
"native": {
+ "about": {
+ "wechat": "WeChat",
+ "wechatHint": "Hover to scan and chat",
+ "loading": "Loading...",
+ "contributorsFailed": "Cannot load contributors. Click to retry.",
+ "qrUnavailable": "QR code unavailable",
+ "reload": "Reload",
+ "qrFailed": "QR code unavailable. Click to retry.",
+ "contributorsTitle": "Open source & community contributions",
+ "contributorsText": "Every step of BongoCat comes from the power of open source. Thank you to all community contributors for making desktop companionship freer and simpler.",
+ "joinTitle": "Want to appear in the contributors list?",
+ "joinText": "Whether you contribute code, improve documentation, report bugs or translate, every contribution makes BongoCat better.",
+ "code": "Code",
+ "docs": "Documentation",
+ "bugs": "Bug reports",
+ "translations": "Translations",
+ "joinGithub": "Contribute on GitHub"
+ },
+ "multiplePets": "Exibir vários",
+ "multiplePetsLimit": "At\u00e9 8 mascotes podem ser exibidos ao mesmo tempo",
+ "removeDesktopPet": "Fechar este mascote da \u00e1rea de trabalho",
"modelLoadFailed": "N\u00e3o foi poss\u00edvel exibir este modelo",
"noBehaviors": "Nenhum item",
+ "preferencesWindowTitle": "BongoCat - Settings",
+ "startup": {
+ "failed": "Não foi possível iniciar o BongoCat.",
+ "detail": "Consulte o log de diagnóstico para obter detalhes técnicos.",
+ "diagnosticLog": "Log de diagnóstico:"
+ },
"support": {
- "title": "Apoiar a equipe de desenvolvimento",
+ "title": "Sobre",
+ "website": "Site: bongocat.pet",
"heroText": "Obrigado pelo apoio. Cada uso e compartilhamento ajuda o BongoCat a continuar crescendo.",
"version": "Versão do aplicativo",
"checkUpdate": "Verificar atualizações",
"feedback": "Feedback",
+ "openLogs": "Abrir logs",
+ "openLogsFailed": "Não foi possível abrir a pasta de logs",
"latest": "Já está atualizado",
+ "checkingUpdate": "Verificando...",
+ "updateAvailable": "Nova versão disponível:",
+ "updateFailed": "Não foi possível verificar atualizações",
+ "updateHttpFailed": "O GitHub retornou o status HTTP %s",
+ "downloadUpdate": "Atualizar para v%s",
+ "openStore": "Abrir Microsoft Store",
+ "viewReleases": "Ver versões",
+ "openUpdateFailed": "Não foi possível abrir a página de atualização",
"works": "Mais aplicativos",
- "worksText": "Outros softwares de vladelaina",
- "refactorDeveloper": "(Desenvolvedor que reescreveu BongoCat em C)",
+ "worksText": "Outras criações de vladelaina que valem a pena conhecer",
+ "refactorDeveloper": "Desenvolvedor que reescreveu o BongoCat em C e também é seu mantenedor atual",
"community": "Comunidade",
"qqGroup": "Grupo QQ",
- "coffee": "Pagar um café para a equipe de desenvolvimento?"
+ "coffee": "Pagar um café para o desenvolvedor?",
+ "starOnGitHub": "Estrela GitHub!",
+ "by": "por "
},
"shortcut": {
"toggleCat": "Mostrar/Ocultar",
@@ -203,6 +267,7 @@
"select": "Selecionar",
"selected": "Selecionado",
"import": "Importar",
+ "importing": "Importando...",
"builtIn": "integrado"
}
}
diff --git a/resources/assets/locales/ru-RU.json b/resources/assets/locales/ru-RU.json
index 3c3ee100..1c74d027 100644
--- a/resources/assets/locales/ru-RU.json
+++ b/resources/assets/locales/ru-RU.json
@@ -9,38 +9,46 @@
"preference": {
"title": "Настройки",
"cat": {
- "title": "Отображение",
+ "title": "Настройки",
"labels": {
- "modelSettings": "Настройки модели",
+ "modelSettings": "Модель",
"mirrorMode": "Зеркальный режим",
"mouseMirror": "Отражение мыши",
"mouseCentered": "Мышь относительно питомца",
"ignoreMouse": "Игнорировать события мыши",
- "windowSettings": "Настройки окна",
+ "windowSettings": "Окно",
"passThrough": "Пропускать щелчки",
"alwaysOnTop": "Поверх всех окон",
"keepInScreen": "Удерживать на экране",
"windowSize": "Размер окна",
+ "windowCorners": "Скругление углов (%)",
"opacity": "Непрозрачность",
- "autoReleaseDelay": "Задержка автоотпускания",
+ "randomExpression": "Случайные выражения",
+ "obsBackground": "Однотонный фон",
"hideOnHover": "Скрывать при наведении",
+ "hideFadeSeconds": "Длительность затухания (с)",
"maxFPS": "Максимальная частота кадров"
},
"hints": {
- "passThrough": "Если включено, щелчки проходят сквозь окно, не воздействуя на него.",
+ "hoverUnavailable": "Скрытие при наведении недоступно в текущем окружении рабочего стола.",
+ "gameInput": "Питомец не реагирует в игре?",
+ "gameInputHelp": "Попробуйте запустить BongoCat от имени администратора и перевести игру в оконный режим.",
+ "passThrough": "Также можно отключить через правый клик по значку в трее",
"alwaysOnTop": "Если включено, окно остаётся поверх всех остальных окон.",
"keepInScreen": "Если включено, окно автоматически остаётся в границах экрана.",
+ "obsBackground": "Захват окна показывает чёрный экран?",
+ "obsBackgroundHelp": "OBS: включите этот параметр, используйте метод совместимости с Windows 7 и удалите фон фильтром цветового ключа.",
"windowSize": "Подведите мышь к краю окна или удерживайте Shift и перетаскивайте правой кнопкой для изменения размера.",
- "hideOnHover": "Если включено, окно автоматически скрывается при наведении мыши. Можно задать задержку; 0 означает скрытие сразу."
+ "hideOnHover": "Если включены пропуск кликов и отображение поверх всех окон, питомец мгновенно скрывается при наведении и появляется, когда указатель уходит."
}
},
"general": {
"title": "Общие",
"labels": {
- "appSettings": "Настройки приложения",
+ "appSettings": "Приложение",
"launchOnStartup": "Запускать при входе в систему",
"showTrayIcon": "Показывать значок в трее",
- "appearanceSettings": "Настройки оформления",
+ "appearanceSettings": "Оформление",
"themeMode": "Тема",
"language": "Язык",
"permissionsSettings": "Настройки разрешений",
@@ -54,8 +62,9 @@
},
"hints": {
"showTrayIcon": "Если включено, значок приложения отображается в системном трее.",
- "inputMonitoringPermission": "Включите мониторинг ввода, чтобы получать системные события клавиатуры и мыши.",
- "inputMonitoringPermissionGuide": "Разрешение на мониторинг ввода необходимо, чтобы приложение получало системные события клавиатуры и мыши.\n\nШаги:\n1. Откройте Системные настройки > Конфиденциальность и безопасность > Мониторинг ввода.\n2. Если BongoCat уже есть в списке, выберите его и нажмите \"-\", чтобы удалить.\n3. Нажмите \"+\", найдите и добавьте BongoCat.\n4. Перезапустите приложение, чтобы разрешение вступило в силу.",
+ "autostartFailed": "Не удалось изменить настройку автозапуска",
+ "inputMonitoringPermission": "Перезапустите после разрешения.",
+ "inputMonitoringPermissionGuide": "Мониторинг ввода позволяет BongoCat видеть нажатия клавиш в других приложениях; мышь работает и без него. BongoCat создаёт мониторинг при запуске, поэтому после изменения разрешения нужен перезапуск.\n\nШаги:\n1. Откройте Системные настройки → Конфиденциальность и безопасность → Мониторинг ввода.\n2. Включите уже имеющуюся запись BongoCat; не удаляйте существующие записи.\n3. Если BongoCat отсутствует в списке, нажмите \"+\" и добавьте запущенную копию (проверьте расположение приложения).\n4. Полностью закройте BongoCat и запустите снова, чтобы он пересоздал мониторинг.\n\nЕсли клавиатура по-прежнему не реагирует, вы можете сами удалить и заново добавить запись BongoCat.",
"administratorPermission": "Запуск от имени администратора позволяет надёжнее захватывать некоторые системные клавиши и события ввода.",
"administratorPermissionGuide": "Для более надёжного захвата некоторых системных клавиш и событий ввода рекомендуется запускать приложение от имени администратора.\n\nКак это сделать:\n1. Сначала закройте приложение.\n2. Найдите exe-файл приложения или его ярлык в Проводнике.\n3. Выберите способ запуска:\n - Один раз: щёлкните правой кнопкой и выберите \"Запуск от имени администратора\".\n - Всегда: правый щелчок > Свойства > Совместимость > отметьте \"Запускать эту программу от имени администратора\" > ОК."
},
@@ -63,7 +72,8 @@
"authorized": "Разрешено",
"adminEnabled": "Включено",
"authorize": "Включить",
- "viewGuide": "Показать инструкцию"
+ "viewGuide": "Показать инструкцию",
+ "openSettingsFailed": "Системные настройки → Конфиденциальность → Мониторинг ввода."
},
"buttons": {
"openNow": "Открыть сейчас",
@@ -74,15 +84,27 @@
},
"model": {
"title": "Модель",
+ "nearbyTitle": "Ближайшие модели",
"labels": {
- "deleteModel": "Удалить модель",
- "nearbyModels": "Соседние папки"
+ "deleteModel": "Удалить модель"
},
"hints": {
- "deleteSuccess": "Успешно удалено",
- "deleteModel": "Удалить эту модель?",
+ "deleteSuccess": "Модель \"%s\" удалена",
+ "deleteModel": "Удалить модель \"%s\"?",
+ "deleteFailed": "Не удалось удалить эту модель",
"importSuccess": "Успешно импортировано",
- "clickOrDragToImport": "Щёлкните или перетащите сюда для импорта",
+ "importExists": "Модель уже существует",
+ "importBatchResult": "Успешно импортировано: %zu; ошибок: %zu.\nОшибки моделей: %s%s.\nПервая ошибка: %s",
+ "importMoreFailures": " и ещё %zu",
+ "importUnknownSource": "неизвестный источник",
+ "importInvalidSource": "Выбранный источник больше не существует или не может быть использован",
+ "importInvalidFormat": "Поддерживаемая модель не найдена или пакет модели неполон",
+ "importFileAccess": "Не удалось прочитать или сохранить файлы модели. Проверьте права доступа и свободное место на диске",
+ "importOutOfMemory": "Недостаточно памяти для импорта этой модели",
+ "importLoadFailed": "Модель импортирована, но её не удалось отобразить",
+ "importFailed": "Не удалось импортировать модель. Проверьте выбранные файлы и повторите попытку",
+ "clickOrDragToImport": "Выберите или перетащите папку",
+ "supportedImportFormats": "Поддержка форматов Mver и Tauri",
"openDirectoryFailed": "Не удалось открыть папку модели"
},
"tooltips": {
@@ -91,10 +113,12 @@
"moreModels": "Другие модели"
},
"behaviorModal": {
- "title": "Движения и выражения",
+ "title": "Движения, выражения и аудио",
"labels": {
"motion": "Движение",
"expression": "Выражение",
+ "audio": "Аудио",
+ "stopAllAudio": "Остановить все звуки",
"motionIndex": "Движение {index}",
"motionGroupIndex": "Группа движений {index}",
"expressionIndex": "Выражение {index}"
@@ -171,21 +195,61 @@
}
},
"native": {
+ "about": {
+ "wechat": "WeChat",
+ "wechatHint": "Hover to scan and chat",
+ "loading": "Loading...",
+ "contributorsFailed": "Cannot load contributors. Click to retry.",
+ "qrUnavailable": "QR code unavailable",
+ "reload": "Reload",
+ "qrFailed": "QR code unavailable. Click to retry.",
+ "contributorsTitle": "Open source & community contributions",
+ "contributorsText": "Every step of BongoCat comes from the power of open source. Thank you to all community contributors for making desktop companionship freer and simpler.",
+ "joinTitle": "Want to appear in the contributors list?",
+ "joinText": "Whether you contribute code, improve documentation, report bugs or translate, every contribution makes BongoCat better.",
+ "code": "Code",
+ "docs": "Documentation",
+ "bugs": "Bug reports",
+ "translations": "Translations",
+ "joinGithub": "Contribute on GitHub"
+ },
+ "multiplePets": "Показывать несколько",
+ "multiplePetsLimit": "\u041e\u0434\u043d\u043e\u0432\u0440\u0435\u043c\u0435\u043d\u043d\u043e \u043c\u043e\u0436\u043d\u043e \u043e\u0442\u043e\u0431\u0440\u0430\u0437\u0438\u0442\u044c \u0434\u043e 8 \u043f\u0438\u0442\u043e\u043c\u0446\u0435\u0432",
+ "removeDesktopPet": "\u0417\u0430\u043a\u0440\u044b\u0442\u044c \u044d\u0442\u043e\u0433\u043e \u043f\u0438\u0442\u043e\u043c\u0446\u0430",
"modelLoadFailed": "Не удалось отобразить эту модель",
"noBehaviors": "Нет элементов",
+ "preferencesWindowTitle": "BongoCat - Settings",
+ "startup": {
+ "failed": "Не удалось запустить BongoCat.",
+ "detail": "Технические сведения доступны в журнале диагностики.",
+ "diagnosticLog": "Журнал диагностики:"
+ },
"support": {
- "title": "Поддержать команду разработки",
+ "title": "О программе",
+ "website": "Сайт: bongocat.pet",
"heroText": "Спасибо за поддержку. Каждое использование и распространение помогает BongoCat развиваться дальше.",
"version": "Версия приложения",
"checkUpdate": "Проверить обновления",
"feedback": "Обратная связь",
+ "openLogs": "Открыть журналы",
+ "openLogsFailed": "Не удалось открыть папку журналов",
"latest": "Установлена последняя версия",
+ "checkingUpdate": "Проверка...",
+ "updateAvailable": "Доступна новая версия:",
+ "updateFailed": "Не удалось проверить обновления",
+ "updateHttpFailed": "GitHub вернул код HTTP %s",
+ "downloadUpdate": "Обновить до v%s",
+ "openStore": "Открыть Microsoft Store",
+ "viewReleases": "Посмотреть выпуски",
+ "openUpdateFailed": "Не удалось открыть страницу обновления",
"works": "Другие приложения",
- "worksText": "Другие программы от vladelaina",
- "refactorDeveloper": "(Разработчик, переписавший BongoCat на C)",
+ "worksText": "Другие работы vladelaina тоже стоит попробовать",
+ "refactorDeveloper": "Разработчик, переписавший BongoCat на C, и его нынешний мейнтейнер",
"community": "Сообщество",
"qqGroup": "Группа QQ",
- "coffee": "Угостить команду разработки кофе?"
+ "coffee": "Угостить разработчика кофе?",
+ "starOnGitHub": "Звезда GitHub!",
+ "by": "от "
},
"shortcut": {
"toggleCat": "Показать/скрыть",
@@ -203,6 +267,7 @@
"select": "Выбрать",
"selected": "Выбрано",
"import": "Импорт",
+ "importing": "Импорт...",
"builtIn": "встроенная"
}
}
diff --git a/resources/assets/locales/zh-CN.json b/resources/assets/locales/zh-CN.json
index c6e5baa4..9ef57391 100644
--- a/resources/assets/locales/zh-CN.json
+++ b/resources/assets/locales/zh-CN.json
@@ -9,38 +9,46 @@
"preference": {
"title": "偏好设置",
"cat": {
- "title": "显示设置",
+ "title": "偏好",
"labels": {
- "modelSettings": "模型设置",
+ "modelSettings": "模型",
"mirrorMode": "镜像模式",
"mouseMirror": "鼠标镜像",
"mouseCentered": "鼠标以桌宠为中心",
"ignoreMouse": "忽略鼠标事件",
- "windowSettings": "窗口设置",
+ "windowSettings": "窗口",
"passThrough": "窗口穿透",
"alwaysOnTop": "窗口置顶",
"keepInScreen": "保持在屏幕内",
"windowSize": "窗口尺寸",
+ "windowCorners": "窗口圆角 (%)",
"opacity": "不透明度",
- "autoReleaseDelay": "按键自动释放延迟",
+ "randomExpression": "随机表情",
+ "obsBackground": "纯色背景",
"hideOnHover": "鼠标移入隐藏",
+ "hideFadeSeconds": "鼠标移入淡出时长(秒)",
"maxFPS": "最大帧率"
},
"hints": {
- "passThrough": "启用后,窗口不影响对其他应用程序的操作。",
+ "hoverUnavailable": "当前桌面环境暂不支持鼠标移入隐藏。",
+ "gameInput": "游戏中桌宠没反应?",
+ "gameInputHelp": "试试以管理员身份启动 BongoCat,并将游戏设为窗口模式。",
+ "passThrough": "右键托盘图标也可关闭此功能",
"alwaysOnTop": "启用后,窗口始终显示在其他应用程序上方。",
"keepInScreen": "启用后,窗口会自动调整位置,防止超出屏幕边界。",
+ "obsBackground": "窗口捕获黑屏?",
+ "obsBackgroundHelp": "OBS:开启当前选项,使用 Windows 7 兼容捕获,并用色键滤镜移除背景。\n哔哩哔哩直播姬:无需开启当前选项,选择游戏进程捕获,并开启允许窗口透明。",
"windowSize": "将鼠标移至窗口边缘,或按住 Shift 并右键拖动,也可以调整窗口大小。",
- "hideOnHover": "启用后,鼠标悬停窗口时自动隐藏。可设置延迟时间,0 表示立即隐藏。"
+ "hideOnHover": "鼠标穿透和置顶均开启时,鼠标移到桌宠上立即隐藏,移开后恢复显示。"
}
},
"general": {
"title": "通用设置",
"labels": {
- "appSettings": "应用设置",
+ "appSettings": "应用",
"launchOnStartup": "开机自启动",
"showTrayIcon": "显示托盘图标",
- "appearanceSettings": "外观设置",
+ "appearanceSettings": "外观",
"themeMode": "主题",
"language": "语言",
"permissionsSettings": "权限设置",
@@ -54,8 +62,9 @@
},
"hints": {
"showTrayIcon": "启用后,在系统托盘中显示应用图标。",
- "inputMonitoringPermission": "开启输入监控权限,以便接收系统的键盘和鼠标事件来响应你的操作。",
- "inputMonitoringPermissionGuide": "开启输入监控权限后,应用才能接收系统的键盘和鼠标事件,从而响应你的操作。\n\n操作方式:\n1. 打开「系统设置」→「隐私与安全性」→「输入监控」。\n2. 若列表中已存在 BongoCat,请先选中它,点击「-」按钮将其删除。\n3. 点击「+」按钮,找到并添加 BongoCat 应用。\n4. 重启应用,以确保权限正式生效。",
+ "autostartFailed": "无法更新开机启动设置",
+ "inputMonitoringPermission": "授权后请重启",
+ "inputMonitoringPermissionGuide": "输入监控权限让 BongoCat 能接收其他应用中的按键;鼠标移动和点击无需该权限。BongoCat 在启动时建立监听,因此权限变更后需要重启。\n\n操作方式:\n1. 打开「系统设置」→「隐私与安全性」→「输入监控」。\n2. 直接开启列表中已有的 BongoCat 条目,不要删除现有条目。\n3. 若列表中没有 BongoCat,点击「+」按钮,添加你当前运行的那份应用(注意确认应用位置)。\n4. 完全退出 BongoCat 后重新启动,让它重建监听。\n\n若键盘仍然无响应,可由你自行删除并重新添加 BongoCat 条目来排障。",
"administratorPermission": "以管理员身份运行应用,可以更稳定地捕获部分系统级按键与输入事件。",
"administratorPermissionGuide": "为了能更稳定地捕获部分系统级按键与输入事件,建议以管理员身份运行应用。\n\n操作方式:\n1. 先退出应用。\n2. 在资源管理器中找到应用的 exe 或快捷方式。\n3. 根据需要选择启动方式:\n • 仅本次:右键并选择「以管理员身份运行」。\n • 永久默认:右键 → 属性 → 兼容性选项卡 → 勾选「以管理员身份运行此程序」→ 确定。"
},
@@ -63,7 +72,8 @@
"authorized": "已授权",
"adminEnabled": "已启用",
"authorize": "去授权",
- "viewGuide": "查看操作说明"
+ "viewGuide": "查看操作说明",
+ "openSettingsFailed": "无法打开系统设置,请手动打开:隐私与安全性 → 输入监控"
},
"buttons": {
"openNow": "前往开启",
@@ -73,28 +83,42 @@
}
},
"model": {
- "title": "模型管理",
+ "title": "模型",
+ "nearbyTitle": "附近模型",
"labels": {
- "deleteModel": "删除模型",
- "nearbyModels": "附近目录下"
+ "deleteModel": "删除模型"
},
"hints": {
- "deleteSuccess": "删除成功",
- "deleteModel": "你确定要删除此模型吗?",
+ "deleteSuccess": "已删除“%s”",
+ "deleteModel": "你确定要删除“%s”吗?",
+ "deleteFailed": "无法删除此模型",
"importSuccess": "导入成功",
- "clickOrDragToImport": "点击或拖动至此区域导入",
+ "importExists": "模型已存在,无需重复导入",
+ "importBatchResult": "导入成功 %zu 个,导入失败 %zu 个。\n失败模型:%s%s。\n首个失败原因:%s",
+ "importMoreFailures": "等其余 %zu 个",
+ "importUnknownSource": "未知来源",
+ "importInvalidSource": "所选来源已不存在或无法使用",
+ "importInvalidFormat": "未找到支持的模型,或模型包文件不完整",
+ "importFileAccess": "无法读取或保存模型文件,请检查文件权限和可用磁盘空间",
+ "importOutOfMemory": "内存不足,无法导入此模型",
+ "importLoadFailed": "模型已导入,但无法正常显示",
+ "importFailed": "模型导入失败,请检查所选文件后重试",
+ "clickOrDragToImport": "点击或拖入模型文件夹",
+ "supportedImportFormats": "同时支持 Mver 和 Tauri 格式",
"openDirectoryFailed": "无法打开模型目录"
},
"tooltips": {
"createModel": "制作模型",
"convertModel": "转换模型",
- "moreModels": "更多模型"
+ "moreModels": "模型库"
},
"behaviorModal": {
- "title": "动作与表情",
+ "title": "动作、表情与音频",
"labels": {
"motion": "动作",
"expression": "表情",
+ "audio": "音频",
+ "stopAllAudio": "停止所有音频",
"motionIndex": "动作{index}",
"motionGroupIndex": "动作组{index}",
"expressionIndex": "表情{index}"
@@ -171,21 +195,61 @@
}
},
"native": {
+ "about": {
+ "wechat": "微信群",
+ "wechatHint": "悬停扫码,一起交流",
+ "loading": "正在加载...",
+ "contributorsFailed": "贡献者加载失败,点击重试。",
+ "qrUnavailable": "二维码暂时无法加载",
+ "reload": "重新加载",
+ "qrFailed": "二维码暂时无法加载,点击重新加载",
+ "contributorsTitle": "开源共建与 社区贡献",
+ "contributorsText": "BongoCat 的每一步都源于开源的力量。感谢所有社区贡献者的无私付出,是你们让桌面陪伴变得更加自由与纯粹。",
+ "joinTitle": "想出现在贡献者名单中吗?",
+ "joinText": "无论是提交代码、改善文档、报告 Bug 还是贡献多语言翻译,你的每一次参与都在让 BongoCat 变得更好。",
+ "code": "代码贡献",
+ "docs": "文档改进",
+ "bugs": "Bug 反馈",
+ "translations": "翻译国际化",
+ "joinGithub": "参与 GitHub 贡献"
+ },
+ "multiplePets": "同时显示多个",
+ "multiplePetsLimit": "\u6700\u591a\u53ef\u540c\u65f6\u663e\u793a 8 \u4e2a\u684c\u5ba0",
+ "removeDesktopPet": "\u5173\u95ed\u6b64\u684c\u5ba0",
"modelLoadFailed": "\u6a21\u578b\u65e0\u6cd5\u663e\u793a",
"noBehaviors": "暂无可用项目",
+ "preferencesWindowTitle": "BongoCat - Settings",
+ "startup": {
+ "failed": "BongoCat 无法启动。",
+ "detail": "请查看诊断日志了解技术详情。",
+ "diagnosticLog": "诊断日志:"
+ },
"support": {
- "title": "支持开发团队",
+ "title": "关于",
+ "website": "官网: bongocat.pet",
"heroText": "感谢你的支持。每一次使用与分享,都让 BongoCat 继续成长。",
"version": "应用版本",
"checkUpdate": "检查更新",
"feedback": "反馈建议",
+ "openLogs": "打开日志",
+ "openLogsFailed": "无法打开日志文件夹",
"latest": "已是最新版本",
+ "checkingUpdate": "正在检查...",
+ "updateAvailable": "发现新版本:",
+ "updateFailed": "无法检查更新",
+ "updateHttpFailed": "GitHub 返回 HTTP 状态码 %s",
+ "downloadUpdate": "更新至 v%s",
+ "openStore": "打开 Microsoft Store",
+ "viewReleases": "查看发布版本",
+ "openUpdateFailed": "无法打开更新页面",
"works": "更多作品",
- "worksText": "来自 vladelaina 的其他软件",
- "refactorDeveloper": "(将BongoCat用C语言重构的开发者)",
+ "worksText": "来自 vladelaina 的其他作品,同样值得一试",
+ "refactorDeveloper": "将BongoCat用C语言重构的开发者,同时也是现任维护者",
"community": "社区",
"qqGroup": "QQ 群",
- "coffee": "请开发团队喝杯咖啡?"
+ "coffee": "请开发者喝杯咖啡?",
+ "starOnGitHub": "喜欢就点颗星吧!",
+ "by": "by "
},
"shortcut": {
"toggleCat": "显示/隐藏",
@@ -203,6 +267,7 @@
"select": "选择",
"selected": "已选择",
"import": "导入",
+ "importing": "导入中...",
"builtIn": "内置"
}
}
diff --git a/resources/assets/locales/zh-TW.json b/resources/assets/locales/zh-Hant.json
similarity index 52%
rename from resources/assets/locales/zh-TW.json
rename to resources/assets/locales/zh-Hant.json
index 5cd40323..2807fd46 100644
--- a/resources/assets/locales/zh-TW.json
+++ b/resources/assets/locales/zh-Hant.json
@@ -9,38 +9,46 @@
"preference": {
"title": "偏好設定",
"cat": {
- "title": "顯示設定",
+ "title": "偏好",
"labels": {
- "modelSettings": "模型設定",
+ "modelSettings": "模型",
"mirrorMode": "鏡像模式",
"mouseMirror": "滑鼠游標鏡像",
"mouseCentered": "滑鼠以桌寵為中心",
"ignoreMouse": "忽略滑鼠事件",
- "windowSettings": "視窗設定",
+ "windowSettings": "視窗",
"passThrough": "視窗穿透",
"alwaysOnTop": "視窗置頂",
"keepInScreen": "保持在螢幕內",
"windowSize": "視窗尺寸",
+ "windowCorners": "視窗圓角 (%)",
"opacity": "不透明度",
- "autoReleaseDelay": "按鍵自動釋放延遲",
+ "randomExpression": "隨機表情",
+ "obsBackground": "純色背景",
"hideOnHover": "滑鼠游標移入隱藏",
+ "hideFadeSeconds": "隱藏漸變時長 (秒)",
"maxFPS": "最大幀率"
},
"hints": {
- "passThrough": "啟用後,視窗不影響對其他應用程式的操作。",
+ "hoverUnavailable": "目前桌面環境暫不支援滑鼠移入隱藏。",
+ "gameInput": "遊戲中桌寵沒反應?",
+ "gameInputHelp": "試試以系統管理員身分啟動 BongoCat,並將遊戲設為視窗模式。",
+ "passThrough": "右鍵點擊系統匣圖示也可關閉此功能",
"alwaysOnTop": "啟用後,視窗始終顯示在其他應用程式上方。",
"keepInScreen": "啟用後,視窗會自動調整位置,防止超出螢幕邊界。",
+ "obsBackground": "視窗擷取黑屏?",
+ "obsBackgroundHelp": "OBS:開啟目前選項,使用 Windows 7 相容擷取,並用色鍵濾鏡移除背景。\n嗶哩嗶哩直播姬:不需要開啟目前選項,選擇遊戲程序擷取,並開啟允許視窗透明。",
"windowSize": "將滑鼠游標移至視窗邊緣,或按住 Shift 並右鍵拖曳,也可以調整視窗大小。",
- "hideOnHover": "啟用後,滑鼠游標懸停視窗時自動隱藏。可設定延遲時間,0 表示立即隱藏。"
+ "hideOnHover": "滑鼠穿透和置頂皆啟用時,滑鼠移到桌寵上立即隱藏,移開後恢復顯示。"
}
},
"general": {
"title": "一般設定",
"labels": {
- "appSettings": "應用程式設定",
+ "appSettings": "應用程式",
"launchOnStartup": "開機自動啟動",
"showTrayIcon": "顯示托盤圖示",
- "appearanceSettings": "外觀設定",
+ "appearanceSettings": "外觀",
"themeMode": "主題",
"language": "語言",
"permissionsSettings": "權限設定",
@@ -54,8 +62,9 @@
},
"hints": {
"showTrayIcon": "啟用後,在系統托盤中顯示應用程式圖示。",
- "inputMonitoringPermission": "開啟輸入監控權限,以便接收系統的鍵盤和滑鼠游標事件來回應您的操作。",
- "inputMonitoringPermissionGuide": "開啟輸入監控權限後,應用程式才能接收系統的鍵盤和滑鼠事件,進而回應你的操作。\n\n操作方式:\n1. 開啟「系統設定」→「隱私權與安全性」→「輸入監控」。\n2. 若清單中已存在 BongoCat,請先選取它,點擊「-」按鈕將其刪除。\n3. 點擊「+」按鈕,找到並新增 BongoCat 應用程式。\n4. 重新啟動應用程式,以確保權限正式生效。",
+ "autostartFailed": "無法更新開機啟動設定",
+ "inputMonitoringPermission": "授權後請重新啟動",
+ "inputMonitoringPermissionGuide": "輸入監控權限讓 BongoCat 能接收其他應用程式中的按鍵;滑鼠移動和點擊無需該權限。BongoCat 在啟動時建立監聽,因此權限變更後需要重新啟動。\n\n操作方式:\n1. 開啟「系統設定」→「隱私權與安全性」→「輸入監控」。\n2. 直接開啟清單中已有的 BongoCat 項目,不要刪除現有項目。\n3. 若清單中沒有 BongoCat,點擊「+」按鈕,加入你目前執行的那份應用程式(請確認應用程式位置)。\n4. 完全結束 BongoCat 後重新啟動,讓它重建監聽。\n\n若鍵盤仍然沒有反應,可由你自行移除並重新加入 BongoCat 項目來排解問題。",
"administratorPermission": "以系統管理員身分執行應用程式,可以更穩定地擷取部分系統層級按鍵與輸入事件。",
"administratorPermissionGuide": "為了能更穩定地擷取部分系統層級按鍵與輸入事件,建議以系統管理員身分執行應用程式。\n\n操作方式:\n1. 先退出應用程式。\n2. 在檔案總管中找到應用程式的 exe 或捷徑。\n3. 根據需求選擇啟動方式:\n • 僅本次:右鍵並選擇「以系統管理員身分執行」。\n • 永久預設:右鍵 → 內容 → 相容性索引標籤 → 勾選「以系統管理員身分執行此程式」→ 確定。"
},
@@ -63,7 +72,8 @@
"authorized": "已授權",
"adminEnabled": "已啟用",
"authorize": "去授權",
- "viewGuide": "查看操作說明"
+ "viewGuide": "查看操作說明",
+ "openSettingsFailed": "無法開啟系統設定,請手動開啟:隱私權與安全性 → 輸入監控"
},
"buttons": {
"openNow": "前往開啟",
@@ -73,28 +83,42 @@
}
},
"model": {
- "title": "模型管理",
+ "title": "模型",
+ "nearbyTitle": "附近模型",
"labels": {
- "deleteModel": "刪除模型",
- "nearbyModels": "附近資料夾"
+ "deleteModel": "刪除模型"
},
"hints": {
- "deleteSuccess": "刪除成功",
- "deleteModel": "您確定要刪除此模型嗎?",
+ "deleteSuccess": "已刪除「%s」",
+ "deleteModel": "您確定要刪除「%s」嗎?",
+ "deleteFailed": "無法刪除此模型",
"importSuccess": "匯入成功",
- "clickOrDragToImport": "點擊或拖曳至此區域匯入",
+ "importExists": "模型已存在,無需重複匯入",
+ "importBatchResult": "匯入成功 %zu 個,匯入失敗 %zu 個。\n失敗模型:%s%s。\n首個失敗原因:%s",
+ "importMoreFailures": "等其餘 %zu 個",
+ "importUnknownSource": "未知來源",
+ "importInvalidSource": "所選來源已不存在或無法使用",
+ "importInvalidFormat": "找不到支援的模型,或模型套件檔案不完整",
+ "importFileAccess": "無法讀取或儲存模型檔案,請檢查檔案權限和可用磁碟空間",
+ "importOutOfMemory": "記憶體不足,無法匯入此模型",
+ "importLoadFailed": "模型已匯入,但無法正常顯示",
+ "importFailed": "模型匯入失敗,請檢查所選檔案後再試一次",
+ "clickOrDragToImport": "點擊或拖入模型資料夾",
+ "supportedImportFormats": "支援 Mver 與 Tauri 格式",
"openDirectoryFailed": "無法開啟模型目錄"
},
"tooltips": {
"createModel": "製作模型",
"convertModel": "轉換模型",
- "moreModels": "更多模型"
+ "moreModels": "模型庫"
},
"behaviorModal": {
- "title": "動作與表情",
+ "title": "動作、表情與音訊",
"labels": {
"motion": "動作",
"expression": "表情",
+ "audio": "音訊",
+ "stopAllAudio": "停止所有音訊",
"motionIndex": "動作{index}",
"motionGroupIndex": "動作組{index}",
"expressionIndex": "表情{index}"
@@ -171,21 +195,61 @@
}
},
"native": {
+ "about": {
+ "wechat": "微信群組",
+ "wechatHint": "懸停掃碼,一起交流",
+ "loading": "正在載入...",
+ "contributorsFailed": "貢獻者載入失敗,點擊重試。",
+ "qrUnavailable": "QR 碼暫時無法載入",
+ "reload": "重新載入",
+ "qrFailed": "二維碼暫時無法載入,點擊重新載入",
+ "contributorsTitle": "開源共建與 社群貢獻",
+ "contributorsText": "BongoCat 的每一步都源於開源的力量。感謝所有社群貢獻者的無私付出,是你們讓桌面陪伴變得更加自由與純粹。",
+ "joinTitle": "想出現在貢獻者名單中嗎?",
+ "joinText": "無論是提交程式碼、改善文件、回報 Bug 還是貢獻多語言翻譯,你的每一次參與都在讓 BongoCat 變得更好。",
+ "code": "程式碼貢獻",
+ "docs": "文件改進",
+ "bugs": "Bug 回報",
+ "translations": "翻譯國際化",
+ "joinGithub": "參與 GitHub 貢獻"
+ },
+ "multiplePets": "同時顯示多個",
+ "multiplePetsLimit": "\u6700\u591a\u53ef\u540c\u6642\u986f\u793a 8 \u500b\u684c\u5bf5",
+ "removeDesktopPet": "\u95dc\u9589\u6b64\u684c\u5bf5",
"modelLoadFailed": "\u6a21\u578b\u7121\u6cd5\u986f\u793a",
"noBehaviors": "暫無可用項目",
+ "preferencesWindowTitle": "BongoCat - Settings",
+ "startup": {
+ "failed": "BongoCat 無法啟動。",
+ "detail": "請查看診斷日誌以瞭解技術詳情。",
+ "diagnosticLog": "診斷日誌:"
+ },
"support": {
- "title": "支援開發團隊",
+ "title": "關於",
+ "website": "官網: bongocat.pet",
"heroText": "感謝你的支持。每一次使用與分享,都讓 BongoCat 持續成長。",
"version": "應用程式版本",
"checkUpdate": "檢查更新",
"feedback": "意見回饋",
+ "openLogs": "開啟日誌",
+ "openLogsFailed": "無法開啟日誌資料夾",
"latest": "已是最新版本",
+ "checkingUpdate": "正在檢查...",
+ "updateAvailable": "發現新版本:",
+ "updateFailed": "無法檢查更新",
+ "updateHttpFailed": "GitHub 回傳 HTTP 狀態碼 %s",
+ "downloadUpdate": "更新至 v%s",
+ "openStore": "開啟 Microsoft Store",
+ "viewReleases": "查看發佈版本",
+ "openUpdateFailed": "無法開啟更新頁面",
"works": "更多作品",
- "worksText": "來自 vladelaina 的其他軟體",
- "refactorDeveloper": "(使用C語言重構BongoCat的開發者)",
+ "worksText": "來自 vladelaina 的其他作品,同樣值得一試",
+ "refactorDeveloper": "將BongoCat用C語言重構的開發者,同時也是現任維護者",
"community": "社群",
"qqGroup": "QQ 群",
- "coffee": "請開發團隊喝杯咖啡?"
+ "coffee": "請開發者喝杯咖啡?",
+ "starOnGitHub": "喜歡就點顆星吧!",
+ "by": "by "
},
"shortcut": {
"toggleCat": "顯示/隱藏",
@@ -203,6 +267,7 @@
"select": "選擇",
"selected": "已選擇",
"import": "匯入",
+ "importing": "匯入中...",
"builtIn": "內建"
}
}
diff --git a/resources/assets/logo-mac.png b/resources/assets/logo-mac.png
deleted file mode 100644
index 16b3e715..00000000
Binary files a/resources/assets/logo-mac.png and /dev/null differ
diff --git a/resources/assets/logo.png b/resources/assets/logo.png
deleted file mode 100644
index ef21cb48..00000000
Binary files a/resources/assets/logo.png and /dev/null differ
diff --git a/resources/assets/models/LICENSE b/resources/assets/models/LICENSE
new file mode 100644
index 00000000..585a2078
--- /dev/null
+++ b/resources/assets/models/LICENSE
@@ -0,0 +1,30 @@
+MIT License
+
+SPDX-License-Identifier: MIT
+
+Copyright (c) 2026 vladelaina
+
+The bundled model assets in the `standard`, `keyboard`, and `gamepad`
+directories are provided under the MIT License. This applies to those model
+assets and their accompanying textures, motions, expressions, and input
+artwork only; it does not change the AGPL-3.0-only license of the BongoCat
+source. The canonical copy of this notice is also available at
+`../../LICENSE-MIT`.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of these model assets and associated documentation files (the "Software"), to
+deal in the Software without restriction, including without limitation the
+rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+sell copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
diff --git a/resources/assets/tray-mac.png b/resources/assets/tray-mac.png
deleted file mode 100644
index 5932d39a..00000000
Binary files a/resources/assets/tray-mac.png and /dev/null differ
diff --git a/resources/assets/tray.png b/resources/assets/tray.png
deleted file mode 100644
index ef21cb48..00000000
Binary files a/resources/assets/tray.png and /dev/null differ
diff --git a/resources/assets/ui-icons.png b/resources/assets/ui-icons.png
deleted file mode 100644
index bef5a002..00000000
Binary files a/resources/assets/ui-icons.png and /dev/null differ
diff --git a/resources/assets/ui-symbols-1x.png b/resources/assets/ui-symbols-1x.png
deleted file mode 100644
index c22039cc..00000000
Binary files a/resources/assets/ui-symbols-1x.png and /dev/null differ
diff --git a/resources/assets/ui-symbols-4x.png b/resources/assets/ui-symbols-4x.png
deleted file mode 100644
index 3ebbc8e4..00000000
Binary files a/resources/assets/ui-symbols-4x.png and /dev/null differ
diff --git a/resources/icons/128x128.png b/resources/icons/128x128.png
deleted file mode 100644
index 6083ccf8..00000000
Binary files a/resources/icons/128x128.png and /dev/null differ
diff --git a/resources/icons/icon.icns b/resources/icons/icon.icns
index 7d3d0ee3..aaaa04b4 100644
Binary files a/resources/icons/icon.icns and b/resources/icons/icon.icns differ
diff --git a/resources/icons/icon.ico b/resources/icons/icon.ico
index 73a2c9fe..0653770a 100644
Binary files a/resources/icons/icon.ico and b/resources/icons/icon.ico differ
diff --git a/resources/icons/icon.png b/resources/icons/icon.png
deleted file mode 100644
index 610221f1..00000000
Binary files a/resources/icons/icon.png and /dev/null differ
diff --git a/resources/linux/bongo-cat.desktop b/resources/linux/bongo-cat.desktop
deleted file mode 100644
index 22e7de04..00000000
--- a/resources/linux/bongo-cat.desktop
+++ /dev/null
@@ -1,8 +0,0 @@
-[Desktop Entry]
-Type=Application
-Name=BongoCat
-Comment=BongoCat Live2D desktop pet
-Exec=BongoCat
-Icon=bongo-cat
-Categories=Game;Utility;
-Terminal=false
diff --git a/scripts/build-windows.ps1 b/scripts/build-windows.ps1
index 9c1ec41e..821dcd8d 100644
--- a/scripts/build-windows.ps1
+++ b/scripts/build-windows.ps1
@@ -2,10 +2,18 @@ param(
[ValidateSet('Debug', 'Release', 'RelWithDebInfo', 'MinSizeRel')]
[string]$Configuration = 'Release',
[string]$BuildDir = '',
+ [ValidateSet('x64', 'Win32')]
+ [string]$Architecture = 'x64',
[ValidateRange(1, 64)]
[int]$Jobs = 2,
+ [switch]$SkipConfigure,
+ [string[]]$Target = @('bongo_cat'),
[switch]$RequireCubism,
- [switch]$Clean
+ [switch]$Package,
+ [switch]$Clean,
+ # Reapply defaults to existing caches; -SkipConfigure reuses cached values.
+ [bool]$OptimizeReleaseSize = $true,
+ [bool]$OptimizeReleaseIpo = $true
)
$ErrorActionPreference = 'Continue'
@@ -21,6 +29,15 @@ if (-not [IO.Path]::IsPathRooted($BuildDir)) {
$BuildDir = Join-Path $root $BuildDir
}
$BuildDir = [IO.Path]::GetFullPath($BuildDir)
+$Target = @($Target | Where-Object { -not [string]::IsNullOrWhiteSpace($_) })
+if ($Target.Count -eq 0) {
+ Write-Host 'At least one CMake target is required.'
+ exit 1
+}
+if ($Clean -and $SkipConfigure) {
+ Write-Host 'The -Clean and -SkipConfigure options cannot be used together.'
+ exit 1
+}
$esc = [char]27
$pink = '38;2;247;125;170'
$muted = '38;2;80;80;80'
@@ -49,11 +66,36 @@ function Show-FailureLog {
}
}
+function Write-GitHubBuildAnnotations {
+ param([string]$Path)
+ if ($env:GITHUB_ACTIONS -ne 'true' -or
+ -not (Test-Path -LiteralPath $Path -PathType Leaf)) { return }
+ $pattern = 'FAILED:|fatal error|(?:warning|error) [A-Z]+\d+:|LNK\d+|MSB\d+: error|unresolved external|cannot open file|ninja: build stopped'
+ Get-Content -LiteralPath $Path |
+ Where-Object { $_ -match $pattern } |
+ Select-Object -Last 30 |
+ ForEach-Object {
+ $message = $_.Replace('%', '%25').Replace("`r", '%0D').Replace("`n", '%0A')
+ Write-Output "::error title=Windows build failure::$message"
+ }
+}
+
if (-not (Get-Command cmake -ErrorAction SilentlyContinue)) {
Write-Host 'Error: CMake was not found in PATH.'
exit 1
}
+if ($Package -or $Target -contains 'package-installer') {
+ try {
+ $isccPath = & (Join-Path $root 'packaging/windows/find-inno.ps1')
+ $env:Path = "$(Split-Path $isccPath -Parent);$env:Path"
+ Write-Host "Inno Setup compiler: $isccPath"
+ } catch {
+ Write-Host $_.Exception.Message
+ exit 1
+ }
+}
+
if ($Clean -and (Test-Path -LiteralPath $BuildDir)) {
$rootPrefix = $root.TrimEnd('\', '/') + [IO.Path]::DirectorySeparatorChar
$insideRoot = $BuildDir.StartsWith($rootPrefix,
@@ -62,8 +104,38 @@ if ($Clean -and (Test-Path -LiteralPath $BuildDir)) {
Write-Host "Refusing to clean unexpected directory: $BuildDir"
exit 1
}
- Write-BuildProgress 2 'Cleaning previous build...'
- Remove-Item -LiteralPath $BuildDir -Recurse -Force
+ Write-BuildProgress 2 'Cleaning previous build artifacts...'
+ $depsPath = Join-Path $BuildDir '_deps'
+ $preservedDeps = Join-Path (Split-Path $BuildDir -Parent) (
+ '.bongocat-deps-' + [Guid]::NewGuid().ToString('N'))
+ $hasDeps = Test-Path -LiteralPath $depsPath
+ try {
+ if ($hasDeps) {
+ Move-Item -LiteralPath $depsPath -Destination $preservedDeps `
+ -Force -ErrorAction Stop
+ }
+ Remove-Item -LiteralPath $BuildDir -Recurse -Force -ErrorAction Stop
+ New-Item -ItemType Directory -Force -Path $BuildDir | Out-Null
+ if ($hasDeps) {
+ Move-Item -LiteralPath $preservedDeps `
+ -Destination (Join-Path $BuildDir '_deps') -Force `
+ -ErrorAction Stop
+ }
+ } catch {
+ if ($hasDeps -and (Test-Path -LiteralPath $preservedDeps) -and
+ -not (Test-Path -LiteralPath $depsPath)) {
+ New-Item -ItemType Directory -Force -Path $BuildDir | Out-Null
+ Move-Item -LiteralPath $preservedDeps -Destination $depsPath `
+ -Force -ErrorAction SilentlyContinue
+ }
+ Write-Host "Cleaning failed: $($_.Exception.Message)"
+ exit 1
+ }
+}
+
+if ($SkipConfigure -and -not (Test-Path -LiteralPath (Join-Path $BuildDir 'CMakeCache.txt'))) {
+ Write-Host "Cannot skip configuration because no CMake cache exists in: $BuildDir"
+ exit 1
}
New-Item -ItemType Directory -Force -Path $BuildDir | Out-Null
@@ -73,39 +145,46 @@ $start = [DateTime]::UtcNow
Write-Host ''
Write-Host "BongoCat $Configuration build"
-Write-BuildProgress 5 'Configuring project...'
-$configureArgs = @(
- '-S', $root, '-B', $BuildDir,
- '-G', 'Visual Studio 17 2022', '-A', 'x64',
- '-DBONGO_CAT_WARNINGS_AS_ERRORS=ON'
-)
-if ($RequireCubism) { $configureArgs += '-DBONGO_CAT_REQUIRE_CUBISM=ON' }
-$configureWriter = New-Object IO.StreamWriter(
- $configureLog, $false, (New-Object Text.UTF8Encoding($false)))
-$configureActivity = 0
-$configurePercent = 5
-try {
- & cmake @configureArgs 2>&1 | ForEach-Object {
- $configureWriter.WriteLine($_.ToString())
- $configureActivity++
- $nextPercent = [Math]::Min(19,
- 5 + [int][Math]::Floor([Math]::Sqrt($configureActivity)))
- if ($nextPercent -ne $configurePercent) {
- $configurePercent = $nextPercent
- Write-BuildProgress $configurePercent 'Configuring project...'
+if ($SkipConfigure) {
+ Write-BuildProgress 20 'Using existing CMake configuration.' -NewLine
+} else {
+ Write-BuildProgress 5 'Configuring project...'
+ $configureArgs = @(
+ '-S', $root, '-B', $BuildDir,
+ '-G', 'Visual Studio 17 2022', '-A', $Architecture,
+ '-DBONGO_CAT_WARNINGS_AS_ERRORS=ON',
+ "-DBONGO_CAT_OPTIMIZE_RELEASE_SIZE=$($OptimizeReleaseSize.ToString().ToUpperInvariant())",
+ "-DBONGO_CAT_OPTIMIZE_RELEASE_IPO=$($OptimizeReleaseIpo.ToString().ToUpperInvariant())"
+ )
+ if ($RequireCubism) { $configureArgs += '-DBONGO_CAT_REQUIRE_CUBISM=ON' }
+ $configureWriter = New-Object IO.StreamWriter(
+ $configureLog, $false, (New-Object Text.UTF8Encoding($false)))
+ $configureActivity = 0
+ $configurePercent = 5
+ try {
+ & cmake @configureArgs 2>&1 | ForEach-Object {
+ $configureWriter.WriteLine($_.ToString())
+ $configureActivity++
+ $nextPercent = [Math]::Min(19,
+ 5 + [int][Math]::Floor([Math]::Sqrt($configureActivity)))
+ if ($nextPercent -ne $configurePercent) {
+ $configurePercent = $nextPercent
+ Write-BuildProgress $configurePercent 'Configuring project...'
+ }
}
+ $configureStatus = $LASTEXITCODE
+ } finally {
+ $configureWriter.Dispose()
}
- $configureStatus = $LASTEXITCODE
-} finally {
- $configureWriter.Dispose()
-}
-if ($configureStatus -ne 0) {
- Write-BuildProgress 5 'Configuration failed.' -NewLine
- Show-FailureLog @($configureLog)
- Write-Host "Full log: $configureLog"
- exit 1
+ if ($configureStatus -ne 0) {
+ Write-BuildProgress 5 'Configuration failed.' -NewLine
+ Write-GitHubBuildAnnotations $configureLog
+ Show-FailureLog @($configureLog)
+ Write-Host "Full log: $configureLog"
+ exit 1
+ }
+ Write-BuildProgress 20 'Configuration complete.' -NewLine
}
-Write-BuildProgress 20 'Configuration complete.' -NewLine
Remove-Item -LiteralPath $buildLog -Force -ErrorAction SilentlyContinue
$projects = @(Get-ChildItem -LiteralPath $BuildDir -Recurse -Filter '*.vcxproj' `
@@ -117,11 +196,11 @@ foreach ($project in $projects) {
}
$compileItems = [Math]::Max(1, $compileItems)
$buildArgs = @('--build', $BuildDir, '--config', $Configuration,
- '--target', 'bongo_cat', '--parallel', $Jobs)
+ '--target') + $Target + @('--parallel', $Jobs)
$lastPercent = 20
$compiled = 0
$activity = 0
-Write-BuildProgress $lastPercent 'Building BongoCat...'
+Write-BuildProgress $lastPercent ("Building target(s): {0}..." -f ($Target -join ', '))
$buildWriter = New-Object IO.StreamWriter(
$buildLog, $false, (New-Object Text.UTF8Encoding($false)))
try {
@@ -141,7 +220,7 @@ try {
$lastPercent = $percent
$message = if ($compiled -gt 0) {
"Compiling ($compiled files)..."
- } else { 'Building BongoCat...' }
+ } else { "Building target(s): $($Target -join ', ')..." }
Write-BuildProgress $lastPercent $message
}
}
@@ -152,19 +231,55 @@ try {
if ($buildStatus -ne 0) {
Write-BuildProgress $lastPercent 'Build failed.' -NewLine
+ Write-GitHubBuildAnnotations $buildLog
Show-FailureLog @($buildLog)
Write-Host "Full log: $buildLog"
exit $buildStatus
}
-$output = Join-Path (Join-Path $BuildDir $Configuration) 'BongoCat.exe'
-if (-not (Test-Path -LiteralPath $output)) {
- Write-BuildProgress 95 'BongoCat.exe was not produced.' -NewLine
- exit 1
+if ($Target -contains 'bongo_cat') {
+ $output = Join-Path (Join-Path $BuildDir $Configuration) 'BongoCat.exe'
+ if (-not (Test-Path -LiteralPath $output)) {
+ Write-BuildProgress 95 'BongoCat.exe was not produced.' -NewLine
+ exit 1
+ }
}
$elapsedTotal = [DateTime]::UtcNow - $start
Write-BuildProgress 100 'Build complete.' -NewLine
Write-Host ("Build time: {0:mm\:ss}" -f $elapsedTotal)
-Write-Host "Output: $output"
+if ($output) { Write-Host "Output: $output" }
Write-Host "Logs: $buildLog"
+
+if ($Package) {
+ Write-Host ''
+ Write-Host 'Building versioned portable and installer packages...'
+ $packageArgs = @('--build', $BuildDir, '--config', $Configuration,
+ '--target', 'package-portable', 'package-installer', '--parallel', $Jobs)
+ & cmake @packageArgs
+ $packageStatus = $LASTEXITCODE
+ if ($packageStatus -ne 0) {
+ Write-Host 'Package generation failed.'
+ Write-Host 'Ensure Inno Setup 6.3 or newer is installed (ISCC.exe).'
+ Write-Host "Packaging build directory: $BuildDir"
+ exit $packageStatus
+ }
+ $packageNameFile = Join-Path $BuildDir 'bongocat-package-name.txt'
+ if (-not (Test-Path -LiteralPath $packageNameFile)) {
+ Write-Host "Package name file was not produced: $packageNameFile"
+ exit 1
+ }
+ $packageName = (Get-Content -LiteralPath $packageNameFile -Raw).Trim()
+ $packageDist = Join-Path $BuildDir 'dist'
+ $portable = Join-Path $packageDist "$packageName-portable.exe"
+ $installer = Join-Path $packageDist "$packageName-setup.exe"
+ if (-not (Test-Path -LiteralPath $portable) -or
+ -not (Test-Path -LiteralPath $installer)) {
+ Write-Host 'Package generation completed without both expected files.'
+ Write-Host "Expected portable: $portable"
+ Write-Host "Expected installer: $installer"
+ exit 1
+ }
+ Write-Host "Portable package: $portable"
+ Write-Host "Installer package: $installer"
+}
exit 0
diff --git a/scripts/generate-dial-icons.py b/scripts/generate-dial-icons.py
new file mode 100644
index 00000000..3df35f31
--- /dev/null
+++ b/scripts/generate-dial-icons.py
@@ -0,0 +1,115 @@
+"""Convert the reference's SVG icons into static C polylines (no runtime SVG)."""
+from pathlib import Path
+import argparse, re, math, xml.etree.ElementTree as ET
+parser = argparse.ArgumentParser(description=__doc__)
+parser.add_argument('reference', type=Path, help='Path to the reference index.html')
+parser.add_argument('--output', type=Path, default=Path(__file__).resolve().parents[1] /
+ 'src/ui/dial/dial_icons.c')
+args = parser.parse_args()
+html=args.reference.read_text(encoding='utf-8')
+icon_block = re.search(r'const\s+icons\s*=\s*\{(.*?)\n\s*\};', html, re.S)
+if not icon_block:
+ parser.error('Reference does not contain the SVG icons object')
+icons=dict(re.findall(r"^ (\w+): '([^']+)'",icon_block.group(1),re.M))
+order=['settings','hide','pass','pin','size','opacity','motion','expression','audio','model','exit','close','passOn','pinOn']
+def arc(x,y,rx,ry,rotation,large,sweep,nx,ny):
+ rx,ry=abs(rx),abs(ry)
+ if not rx or not ry or (x==nx and y==ny): return [(nx,ny)]
+ phi=math.radians(rotation); co,si=math.cos(phi),math.sin(phi)
+ dx,dy=(x-nx)/2,(y-ny)/2
+ xp,yp=co*dx+si*dy,-si*dx+co*dy
+ k=xp*xp/(rx*rx)+yp*yp/(ry*ry)
+ if k>1: rx*=math.sqrt(k); ry*=math.sqrt(k)
+ q=max(0,(rx*rx*ry*ry-rx*rx*yp*yp-ry*ry*xp*xp)/(rx*rx*yp*yp+ry*ry*xp*xp))
+ factor=(-1 if bool(large)==bool(sweep) else 1)*math.sqrt(q)
+ cxp,cyp=factor*rx*yp/ry,-factor*ry*xp/rx
+ cx,cy=co*cxp-si*cyp+(x+nx)/2,si*cxp+co*cyp+(y+ny)/2
+ a=math.atan2((yp-cyp)/ry,(xp-cxp)/rx)
+ end=math.atan2((-yp-cyp)/ry,(-xp-cxp)/rx)
+ delta=(end-a)%(2*math.pi)
+ if not sweep: delta-=2*math.pi
+ steps=max(2,math.ceil(abs(delta)*max(rx,ry)/0.5))
+ return [(cx+co*rx*math.cos(a+delta*j/steps)-si*ry*math.sin(a+delta*j/steps),cy+si*rx*math.cos(a+delta*j/steps)+co*ry*math.sin(a+delta*j/steps)) for j in range(1,steps+1)]
+def path(data):
+ tokens=re.findall(r'[A-Za-z]|[-+]?(?:\d*\.\d+|\d+)(?:[eE][-+]?\d+)?',data)
+ contours=[]; points=[]; x=y=0; start=(0,0); i=0; cmd=None; last_control=(0,0); previous=None
+ while i'+icons[name]+''):
+ a=el.attrib
+ if el.tag=='path': paths=path(a['d'])
+ elif el.tag=='circle':
+ cx,cy,r=map(float,[a['cx'],a['cy'],a['r']]); paths=[[(cx+r*math.cos(j*math.pi/24),cy+r*math.sin(j*math.pi/24)) for j in range(49)]]
+ elif el.tag=='line': paths=[[(float(a['x1']),float(a['y1'])),(float(a['x2']),float(a['y2']))]]
+ else:
+ v=list(map(float,re.findall(r'[-+]?[\d.]+',a['points']))); pts=list(zip(v[::2],v[1::2])); paths=[pts+[pts[0]] if el.tag=='polygon' else pts]
+ for pts in paths:
+ contours.append((len(allpoints),len(pts))); allpoints+=pts
+ if len(contours) == first:
+ raise ValueError(f'Icon {name!r} has no drawable contours')
+ ranges.append((first,len(contours)-first))
+lines.append('static const DialPoint points[] = {')
+for j in range(0,len(allpoints),4): lines.append(' '+', '.join('{%.4ff, %.4ff}'%p for p in allpoints[j:j+4])+',')
+lines+=['};','static const IconContour contours[] = {']
+for j in range(0,len(contours),6): lines.append(' '+', '.join('{%d, %d}'%p for p in contours[j:j+6])+',')
+lines+=['};','static const IconContour icons[] = {',' '+', '.join('{%d, %d}'%p for p in ranges),'};', '''
+void dial_icon(Dial *d, int icon, float x, float y, float size, uint32_t color) {
+ if (icon < 0 || icon >= (int)(sizeof(icons) / sizeof(icons[0]))) return;
+ IconContour range = icons[icon];
+ for (int i = 0; i < range.count; ++i) {
+ IconContour contour = contours[range.offset + i];
+ DialPoint transformed[512];
+ if (contour.count > 512) continue;
+ for (int j = 0; j < contour.count; ++j) {
+ DialPoint p = points[contour.offset + j];
+ transformed[j] = (DialPoint){x + (p.x - 12) * size / 24,
+ y + (p.y - 12) * size / 24};
+ }
+ dial_stroke(d, transformed, contour.count, 1.95f * size / 24, color, true);
+ }
+}
+''']
+assert max(n for _, n in contours) <= 512
+args.output.write_text('\n'.join(lines),encoding='utf-8')
+print('Generated',len(allpoints),'icon points;',max(n for _,n in contours),'max contour points')
diff --git a/src/core/app_state.c b/src/core/app_state.c
index 9d0e72aa..bfd4a37b 100644
--- a/src/core/app_state.c
+++ b/src/core/app_state.c
@@ -4,6 +4,50 @@
#include
#include
+static size_t active_input_index(const BongoCatApp *app,
+ BongoCatInputKind kind, const char *name) {
+ if (!app || !name) return app ? app->active_input_count : 0;
+ for (size_t i = 0; i < app->active_input_count; ++i) {
+ const BongoCatInputEvent *active = &app->active_inputs[i];
+ if (active->kind == kind && strcmp(active->name, name) == 0) return i;
+ }
+ return app->active_input_count;
+}
+
+static void active_input_remove(BongoCatApp *app, size_t index) {
+ if (!app || index >= app->active_input_count) return;
+ if (index + 1 < app->active_input_count)
+ memmove(&app->active_inputs[index], &app->active_inputs[index + 1],
+ (app->active_input_count - index - 1) * sizeof(app->active_inputs[0]));
+ memset(&app->active_inputs[--app->active_input_count], 0,
+ sizeof(app->active_inputs[0]));
+}
+
+static void active_input_update(BongoCatApp *app,
+ const BongoCatInputEvent *event) {
+ if (!app || !event || !event->name[0]) return;
+ bool tracked = event->kind == BONGO_CAT_INPUT_KEY_DOWN ||
+ event->kind == BONGO_CAT_INPUT_KEY_UP ||
+ event->kind == BONGO_CAT_INPUT_GAMEPAD_BUTTON ||
+ event->kind == BONGO_CAT_INPUT_GAMEPAD_AXIS;
+ if (!tracked) return;
+ bool active = event->kind == BONGO_CAT_INPUT_KEY_DOWN ||
+ (event->kind == BONGO_CAT_INPUT_GAMEPAD_BUTTON && event->value > 0.5f) ||
+ (event->kind == BONGO_CAT_INPUT_GAMEPAD_AXIS && fabsf(event->value) > .001f);
+ BongoCatInputKind kind = event->kind == BONGO_CAT_INPUT_KEY_DOWN ||
+ event->kind == BONGO_CAT_INPUT_KEY_UP
+ ? BONGO_CAT_INPUT_KEY_DOWN : event->kind;
+ size_t index = active_input_index(app, kind, event->name);
+ if (!active) { active_input_remove(app, index); return; }
+ if (index == app->active_input_count) {
+ if (app->active_input_count >=
+ sizeof(app->active_inputs) / sizeof(app->active_inputs[0])) return;
+ index = app->active_input_count++;
+ }
+ app->active_inputs[index] = *event;
+ app->active_inputs[index].kind = kind;
+}
+
static bool stick_active(float x, float y, bool pressed) {
return pressed || fabsf(x) > 0.001f || fabsf(y) > 0.001f;
}
@@ -21,10 +65,12 @@ static void update_hands(BongoCatApp *app) {
(right_stick || bongo_cat_overlay_hand_active(app->overlay, true)) ? 1.0f : 0.0f);
}
-static void apply_key(BongoCatApp *app, const char *name, bool pressed) {
- if (bongo_cat_overlay_key(app->overlay, name, pressed) < 0) return;
+static bool apply_key(BongoCatApp *app, const char *name, bool pressed) {
+ int hand = bongo_cat_overlay_key(app->overlay, name, pressed);
+ if (hand < 0) return false;
update_hands(app);
app->dirty = true;
+ return true;
}
static void set_axis(BongoCatApp *app, const char *id, float input) {
@@ -64,6 +110,16 @@ static void apply_gamepad(BongoCatApp *app, const BongoCatInputEvent *event) {
void bongo_cat_app_reset_gamepad(BongoCatApp *app) {
if (!app || !app->live2d) return;
+ for (size_t i = app->sound_shortcut_state.count; i > 0; --i) {
+ if (strncmp(app->sound_shortcut_state.held[i - 1], "Gamepad:", 8)) continue;
+ --app->sound_shortcut_state.count;
+ if (i - 1 != app->sound_shortcut_state.count)
+ memcpy(app->sound_shortcut_state.held[i - 1],
+ app->sound_shortcut_state.held[app->sound_shortcut_state.count], BONGO_CAT_ID_CAP);
+ }
+ for (size_t i = 0; i < app->settings.behavior_shortcut_count; ++i)
+ if (strstr(app->settings.behavior_shortcuts[i].shortcut, "Gamepad:"))
+ app->sound_shortcut_active[i] = false;
app->left_stick_x = app->left_stick_y = 0.0f;
app->right_stick_x = app->right_stick_y = 0.0f;
app->left_stick_pressed = app->right_stick_pressed = false;
@@ -79,24 +135,60 @@ void bongo_cat_app_reset_gamepad(BongoCatApp *app) {
"RightPaddle2", "LeftPaddle2", "Touchpad", "LeftThumb", "RightThumb"};
for (size_t i = 0; i < sizeof(buttons) / sizeof(buttons[0]); ++i)
apply_key(app, buttons[i], false);
+ for (size_t i = app->active_input_count; i > 0; --i)
+ if (app->active_inputs[i - 1].kind == BONGO_CAT_INPUT_GAMEPAD_BUTTON ||
+ app->active_inputs[i - 1].kind == BONGO_CAT_INPUT_GAMEPAD_AXIS)
+ active_input_remove(app, i - 1);
update_hands(app);
app->dirty = true;
}
void bongo_cat_app_apply_input(BongoCatApp *app, const BongoCatInputEvent *event) {
- if (!app || !event || !app->live2d) return;
+ if (!app || !event) return;
+ bool keyboard = event->kind == BONGO_CAT_INPUT_KEY_DOWN ||
+ event->kind == BONGO_CAT_INPUT_KEY_UP;
+ if (!app->live2d) {
+ if (keyboard) app->input_diagnostics.no_model_keys++;
+ return;
+ }
+ active_input_update(app, event);
switch (event->kind) {
- case BONGO_CAT_INPUT_KEY_DOWN: apply_key(app, event->name, true); break;
- case BONGO_CAT_INPUT_KEY_UP: apply_key(app, event->name, false); break;
+ case BONGO_CAT_INPUT_KEY_DOWN:
+ case BONGO_CAT_INPUT_KEY_UP:
+ if (apply_key(app, event->name, event->kind == BONGO_CAT_INPUT_KEY_DOWN))
+ app->input_diagnostics.mapped_keys++;
+ else app->input_diagnostics.unmapped_keys++;
+ break;
case BONGO_CAT_INPUT_MOUSE_DOWN:
case BONGO_CAT_INPUT_MOUSE_UP: {
- if (strcmp(event->name, "Left") != 0 && strcmp(event->name, "Right") != 0)
+ bool left = strcmp(event->name, "Left") == 0;
+ bool right = strcmp(event->name, "Right") == 0;
+ bool side = strcmp(event->name, "Back") == 0 ||
+ strcmp(event->name, "Forward") == 0;
+ if (!left && !right && !side)
break;
bool down = event->kind == BONGO_CAT_INPUT_MOUSE_DOWN;
- if (strcmp(event->name, "Left") == 0) app->left_mouse_down = down;
- else app->right_mouse_down = down;
+ bool changed;
+ if (left) {
+ changed = app->left_mouse_down != down;
+ app->left_mouse_down = down;
+ } else if (right) {
+ changed = app->right_mouse_down != down;
+ app->right_mouse_down = down;
+ } else {
+ if (strcmp(event->name, "Back") == 0) app->back_mouse_down = down;
+ else app->forward_mouse_down = down;
+ bool side_down = app->back_mouse_down || app->forward_mouse_down;
+ changed = app->side_mouse_down != side_down;
+ app->side_mouse_down = side_down;
+ }
+ if (changed) app->mouse_button_event_pending = true;
if (!down) app->pointer_hit_dirty = true;
- const char *id = strcmp(event->name, "Left") == 0
+ if (side) {
+ app->dirty = true;
+ break;
+ }
+ const char *id = left
? "ParamMouseLeftDown" : "ParamMouseRightDown";
bongo_cat_live2d_set_parameter(app->live2d, id, down ? 1.0f : 0.0f);
app->dirty = true;
@@ -107,3 +199,21 @@ void bongo_cat_app_apply_input(BongoCatApp *app, const BongoCatInputEvent *event
default: break;
}
}
+
+void bongo_cat_app_reapply_input(BongoCatApp *app) {
+ if (!app || !app->live2d) return;
+ bongo_cat_live2d_set_parameter(app->live2d, "ParamMouseLeftDown",
+ app->left_mouse_down ? 1.0f : 0.0f);
+ bongo_cat_live2d_set_parameter(app->live2d, "ParamMouseRightDown",
+ app->right_mouse_down ? 1.0f : 0.0f);
+ for (size_t i = 0; i < app->active_input_count; ++i) {
+ BongoCatInputEvent *event = &app->active_inputs[i];
+ if (event->kind == BONGO_CAT_INPUT_KEY_DOWN)
+ apply_key(app, event->name, true);
+ else if (event->kind == BONGO_CAT_INPUT_GAMEPAD_BUTTON ||
+ event->kind == BONGO_CAT_INPUT_GAMEPAD_AXIS)
+ apply_gamepad(app, event);
+ }
+ update_hands(app);
+ app->dirty = true;
+}
diff --git a/src/core/config_defaults.c b/src/core/config_defaults.c
index 2877b5d0..184eb904 100644
--- a/src/core/config_defaults.c
+++ b/src/core/config_defaults.c
@@ -2,9 +2,12 @@
#include "bongo_cat/utf8.h"
#include
+#include
+#include
#include
-static float clampf(float value, float low, float high) {
+static float clampf_or(float value, float low, float high, float fallback) {
+ if (!isfinite(value)) return fallback;
return value < low ? low : value > high ? high : value;
}
@@ -17,111 +20,218 @@ static bool shortcut_equal(const char *left, const char *right) {
return *left == *right;
}
-bool bongo_cat_config_shortcut_conflicts(const BongoCatConfig *config,
+static bool normalize_text(char *text, size_t capacity) {
+ text[capacity - 1] = '\0';
+ if (!bongo_cat_utf8_valid(text)) {
+ memset(text, 0, capacity);
+ return false;
+ }
+ size_t length = strlen(text);
+ memset(text + length + 1, 0, capacity - length - 1);
+ return true;
+}
+
+bool bongo_cat_settings_shortcut_conflicts(const BongoCatSettings *config,
const char *shortcut, const char *exclude) {
if (!config || !shortcut || !shortcut[0]) return false;
- const char *global[] = {config->shortcuts.visible_cat,
+ const char *global[] = {config->shortcuts.toggle_pet_visibility,
config->shortcuts.visible_preferences, config->shortcuts.mirror,
config->shortcuts.pass_through, config->shortcuts.always_on_top};
for (size_t i = 0; i < sizeof(global) / sizeof(global[0]); ++i)
if (global[i] != exclude && shortcut_equal(global[i], shortcut))
return true;
- for (size_t i = 0; i < config->behavior_shortcut_count; ++i) {
+ size_t behavior_count = config->behavior_shortcut_count;
+ if (behavior_count > BONGO_CAT_BEHAVIOR_BINDING_CAP)
+ behavior_count = BONGO_CAT_BEHAVIOR_BINDING_CAP;
+ /* Model behaviors may share a key; global commands remain exclusive. */
+ for (size_t i = 0; i < behavior_count; ++i)
+ if (config->behavior_shortcuts[i].shortcut == exclude) return false;
+ for (size_t i = 0; i < behavior_count; ++i) {
const char *bound = config->behavior_shortcuts[i].shortcut;
- if (bound != exclude && shortcut_equal(bound, shortcut)) return true;
+ if (shortcut_equal(bound, shortcut)) return true;
}
return false;
}
-static void validate_shortcuts(BongoCatConfig *config) {
- char *global[] = {config->shortcuts.visible_cat,
+static void validate_shortcuts(BongoCatSettings *config) {
+ char *global[] = {config->shortcuts.toggle_pet_visibility,
config->shortcuts.visible_preferences, config->shortcuts.mirror,
config->shortcuts.pass_through, config->shortcuts.always_on_top};
for (size_t i = 0; i < sizeof(global) / sizeof(global[0]); ++i) {
- global[i][BONGO_CAT_SHORTCUT_CAP - 1] = '\0';
+ normalize_text(global[i], BONGO_CAT_SHORTCUT_CAP);
for (size_t j = 0; j < i; ++j)
- if (shortcut_equal(global[i], global[j])) global[i][0] = '\0';
+ if (shortcut_equal(global[i], global[j]))
+ memset(global[i], 0, BONGO_CAT_SHORTCUT_CAP);
}
for (size_t i = 0; i < config->behavior_shortcut_count; ++i) {
char *shortcut = config->behavior_shortcuts[i].shortcut;
bool duplicate = false;
for (size_t j = 0; j < sizeof(global) / sizeof(global[0]); ++j)
duplicate = duplicate || shortcut_equal(shortcut, global[j]);
- for (size_t j = 0; j < i; ++j)
- duplicate = duplicate || shortcut_equal(shortcut,
- config->behavior_shortcuts[j].shortcut);
if (duplicate) shortcut[0] = '\0';
}
}
-void bongo_cat_config_defaults(BongoCatConfig *config) {
+static void compact_behavior_overrides(BongoCatSettings *config) {
+ size_t input_count = config->behavior_shortcut_count;
+ if (input_count > BONGO_CAT_BEHAVIOR_BINDING_CAP)
+ input_count = BONGO_CAT_BEHAVIOR_BINDING_CAP;
+ size_t output_count = 0;
+ for (size_t i = 0; i < input_count; ++i) {
+ BongoCatBehaviorShortcut entry = config->behavior_shortcuts[i];
+ entry.id[sizeof(entry.id) - 1] = '\0';
+ entry.shortcut[sizeof(entry.shortcut) - 1] = '\0';
+ entry.label[sizeof(entry.label) - 1] = '\0';
+ if (!bongo_cat_utf8_valid(entry.id) ||
+ !bongo_cat_utf8_valid(entry.shortcut) ||
+ !bongo_cat_utf8_valid(entry.label)) continue;
+ if (entry.shortcut[0]) entry.shortcut_disabled = false;
+ if (!entry.id[0] || (!entry.shortcut[0] && !entry.label[0] && !entry.shortcut_disabled)) continue;
+ BongoCatBehaviorShortcut canonical = {0};
+ canonical.shortcut_disabled = entry.shortcut_disabled;
+ snprintf(canonical.id, sizeof(canonical.id), "%s", entry.id);
+ snprintf(canonical.shortcut, sizeof(canonical.shortcut), "%s",
+ entry.shortcut);
+ snprintf(canonical.label, sizeof(canonical.label), "%s", entry.label);
+ size_t existing = output_count;
+ for (size_t j = 0; j < output_count; ++j)
+ if (!strcmp(config->behavior_shortcuts[j].id, entry.id)) {
+ existing = j;
+ break;
+ }
+ if (existing < output_count) {
+ if (canonical.shortcut[0] || canonical.shortcut_disabled) {
+ config->behavior_shortcuts[existing].shortcut_disabled = canonical.shortcut_disabled;
+ memset(config->behavior_shortcuts[existing].shortcut, 0,
+ sizeof(config->behavior_shortcuts[existing].shortcut));
+ snprintf(
+ config->behavior_shortcuts[existing].shortcut,
+ sizeof(config->behavior_shortcuts[existing].shortcut),
+ "%s", canonical.shortcut);
+ }
+ if (canonical.label[0]) {
+ memset(config->behavior_shortcuts[existing].label, 0,
+ sizeof(config->behavior_shortcuts[existing].label));
+ snprintf(
+ config->behavior_shortcuts[existing].label,
+ sizeof(config->behavior_shortcuts[existing].label),
+ "%s", canonical.label);
+ }
+ continue;
+ }
+ config->behavior_shortcuts[output_count++] = canonical;
+ }
+ memset(&config->behavior_shortcuts[output_count], 0,
+ (BONGO_CAT_BEHAVIOR_BINDING_CAP - output_count) *
+ sizeof(config->behavior_shortcuts[0]));
+ config->behavior_shortcut_count = output_count;
+}
+
+static void compact_model_overrides(BongoCatSettings *config) {
+ size_t input_count = config->model_label_count;
+ if (input_count > BONGO_CAT_MODEL_CAP) input_count = BONGO_CAT_MODEL_CAP;
+ size_t output_count = 0;
+ for (size_t i = 0; i < input_count; ++i) {
+ BongoCatModelLabel entry = config->model_labels[i];
+ entry.id[sizeof(entry.id) - 1] = '\0';
+ entry.label[sizeof(entry.label) - 1] = '\0';
+ if (!entry.id[0] || !entry.label[0] ||
+ !bongo_cat_utf8_valid(entry.id) ||
+ !bongo_cat_utf8_valid(entry.label)) continue;
+ BongoCatModelLabel canonical = {0};
+ snprintf(canonical.id, sizeof(canonical.id), "%s", entry.id);
+ snprintf(canonical.label, sizeof(canonical.label), "%s", entry.label);
+ size_t existing = output_count;
+ for (size_t j = 0; j < output_count; ++j)
+ if (!strcmp(config->model_labels[j].id, entry.id)) {
+ existing = j;
+ break;
+ }
+ if (existing < output_count) {
+ memset(config->model_labels[existing].label, 0,
+ sizeof(config->model_labels[existing].label));
+ snprintf(config->model_labels[existing].label,
+ sizeof(config->model_labels[existing].label), "%s",
+ canonical.label);
+ continue;
+ }
+ config->model_labels[output_count++] = canonical;
+ }
+ memset(&config->model_labels[output_count], 0,
+ (BONGO_CAT_MODEL_CAP - output_count) *
+ sizeof(config->model_labels[0]));
+ config->model_label_count = output_count;
+}
+
+static void compact_removed_models(BongoCatSettings *config) {
+ size_t input_count = config->removed_model_count;
+ if (input_count > BONGO_CAT_MODEL_CAP) input_count = BONGO_CAT_MODEL_CAP;
+ size_t output_count = 0;
+ for (size_t i = 0; i < input_count; ++i) {
+ BongoCatRemovedModel entry = config->removed_models[i];
+ entry.id[sizeof(entry.id) - 1] = '\0';
+ if (!entry.id[0] || !bongo_cat_utf8_valid(entry.id)) continue;
+ bool duplicate = false;
+ for (size_t j = 0; j < output_count; ++j)
+ if (!strcmp(config->removed_models[j].id, entry.id)) {
+ duplicate = true;
+ break;
+ }
+ if (!duplicate) {
+ BongoCatRemovedModel canonical = {0};
+ snprintf(canonical.id, sizeof(canonical.id), "%s", entry.id);
+ config->removed_models[output_count++] = canonical;
+ }
+ }
+ memset(&config->removed_models[output_count], 0,
+ (BONGO_CAT_MODEL_CAP - output_count) *
+ sizeof(config->removed_models[0]));
+ config->removed_model_count = output_count;
+}
+
+void bongo_cat_settings_defaults(BongoCatSettings *config) {
if (!config) return;
memset(config, 0, sizeof(*config));
- config->model.auto_release_seconds =
- BONGO_CAT_DEFAULT_AUTO_RELEASE_SECONDS;
config->model.mouse_centered = true;
config->model.max_fps = BONGO_CAT_DEFAULT_MAX_FPS;
- config->window.visible = true;
config->window.always_on_top = true;
config->window.keep_in_screen = false;
- config->window.scale_percent = BONGO_CAT_DEFAULT_WINDOW_SCALE_PERCENT;
- config->window.opacity_percent =
- BONGO_CAT_DEFAULT_WINDOW_OPACITY_PERCENT;
- config->window.width = 612;
- config->window.height = 354;
+ config->window.obs_background_color = BONGO_CAT_OBS_BACKGROUND_GREEN;
+ config->window.corner_radius_percent = BONGO_CAT_DEFAULT_WINDOW_CORNER_PERCENT;
+ config->window.hide_fade_seconds = BONGO_CAT_DEFAULT_HIDE_FADE_SECONDS;
+ config->window.random_expression_interval_seconds =
+ BONGO_CAT_DEFAULT_RANDOM_EXPRESSION_SECONDS;
config->app.tray_visible = true;
config->app.theme = BONGO_CAT_THEME_AUTO;
config->app.language = BONGO_CAT_LANG_EN_US;
- config->current_mode = BONGO_CAT_MODE_GAMEPAD;
- memcpy(config->current_model, "standard", sizeof("standard"));
+ memcpy(config->extensions_json, "{}", sizeof("{}"));
}
-void bongo_cat_config_validate(BongoCatConfig *config) {
+void bongo_cat_settings_validate(BongoCatSettings *config) {
if (!config) return;
- config->model.auto_release_seconds = clampf(config->model.auto_release_seconds, 0.05f, 30.0f);
- if (config->model.max_fps < 1) config->model.max_fps = 1;
- if (config->model.max_fps > 240) config->model.max_fps = 240;
- config->window.scale_percent = clampf(config->window.scale_percent, 10.0f, 500.0f);
- config->window.opacity_percent = clampf(config->window.opacity_percent, 10.0f, 100.0f);
- config->window.hide_delay_seconds = clampf(config->window.hide_delay_seconds, 0.0f, 60.0f);
- if (config->window.width < 64) config->window.width = 64;
- if (config->window.height < 64) config->window.height = 64;
- if (config->window.width > 8192) config->window.width = 8192;
- if (config->window.height > 8192) config->window.height = 8192;
- if (config->app.theme > BONGO_CAT_THEME_DARK) config->app.theme = BONGO_CAT_THEME_AUTO;
+ config->model.max_fps = config->model.max_fps > 0 &&
+ config->model.max_fps <= 30 ? 30 : BONGO_CAT_DEFAULT_MAX_FPS;
+ config->window.hide_delay_seconds = clampf_or(
+ config->window.hide_delay_seconds, 0.0f, 60.0f, 0.0f);
+ config->window.hide_fade_seconds = clampf_or(
+ config->window.hide_fade_seconds, 0.0f,
+ BONGO_CAT_MAX_HIDE_FADE_SECONDS, BONGO_CAT_DEFAULT_HIDE_FADE_SECONDS);
+ config->window.corner_radius_percent = clampf_or(
+ config->window.corner_radius_percent, 0.0f, 50.0f,
+ BONGO_CAT_DEFAULT_WINDOW_CORNER_PERCENT);
+ config->window.random_expression_interval_seconds = clampf_or(
+ config->window.random_expression_interval_seconds, 1.0f, 3600.0f,
+ BONGO_CAT_DEFAULT_RANDOM_EXPRESSION_SECONDS);
+ if ((unsigned)config->window.obs_background_color >=
+ BONGO_CAT_OBS_BACKGROUND_COLOR_COUNT)
+ config->window.obs_background_color = BONGO_CAT_OBS_BACKGROUND_GREEN;
+ if ((unsigned)config->app.theme > BONGO_CAT_THEME_DARK)
+ config->app.theme = BONGO_CAT_THEME_AUTO;
if ((unsigned)config->app.language >= BONGO_CAT_LANG_COUNT)
config->app.language = BONGO_CAT_LANG_EN_US;
- if (config->current_mode > BONGO_CAT_MODE_GAMEPAD) config->current_mode = BONGO_CAT_MODE_STANDARD;
- config->current_model[sizeof(config->current_model) - 1] = '\0';
- if (config->behavior_shortcut_count > BONGO_CAT_BEHAVIOR_CAP)
- config->behavior_shortcut_count = BONGO_CAT_BEHAVIOR_CAP;
- for (size_t i = 0; i < config->behavior_shortcut_count; ++i) {
- config->behavior_shortcuts[i].id[BONGO_CAT_PATH_CAP - 1] = '\0';
- config->behavior_shortcuts[i].shortcut[BONGO_CAT_SHORTCUT_CAP - 1] = '\0';
- config->behavior_shortcuts[i].label[BONGO_CAT_ID_CAP - 1] = '\0';
- char normalized[BONGO_CAT_ID_CAP] = {0};
- if (bongo_cat_utf8_normalize_legacy(
- config->behavior_shortcuts[i].label, normalized,
- sizeof(normalized)))
- memcpy(config->behavior_shortcuts[i].label, normalized,
- sizeof(normalized));
- else config->behavior_shortcuts[i].label[0] = '\0';
- }
+ compact_behavior_overrides(config);
+ compact_model_overrides(config);
+ compact_removed_models(config);
validate_shortcuts(config);
-}
-
-const char *bongo_cat_theme_name(BongoCatTheme value) {
- const char *names[] = {"auto", "light", "dark"};
- return value <= BONGO_CAT_THEME_DARK ? names[value] : names[0];
-}
-
-const char *bongo_cat_language_name(BongoCatLanguage value) {
- const char *names[] = {"en-US", "zh-CN", "zh-TW", "fr-FR", "de-DE",
- "ja-JP", "ko-KR", "pt-BR", "ru-RU", "es-ES"};
- return (unsigned)value < BONGO_CAT_LANG_COUNT ? names[value] : names[0];
-}
-
-const char *bongo_cat_mode_name(BongoCatModelMode value) {
- const char *names[] = {"standard", "keyboard", "gamepad"};
- return value <= BONGO_CAT_MODE_GAMEPAD ? names[value] : names[0];
+ compact_behavior_overrides(config);
}
diff --git a/src/core/config_fields.c b/src/core/config_fields.c
new file mode 100644
index 00000000..54e80d32
--- /dev/null
+++ b/src/core/config_fields.c
@@ -0,0 +1,156 @@
+#include "config_internal.h"
+
+#include
+#include
+#include
+#include
+
+bool bongo_cat_config_type_error(const char *description,
+ BongoCatError *error, const char *field, const char *expected) {
+ bongo_cat_error_set(error, BONGO_CAT_ERROR_FORMAT,
+ "%s field '%s' must be %s", description, field, expected);
+ return false;
+}
+
+bool bongo_cat_config_read_value(const char *description, yyjson_val *object,
+ const char *key, yyjson_val **target, BongoCatError *error) {
+ *target = NULL;
+ yyjson_obj_iter iterator = yyjson_obj_iter_with(object);
+ yyjson_val *name;
+ while ((name = yyjson_obj_iter_next(&iterator))) {
+ if (!yyjson_equals_str(name, key)) continue;
+ if (*target) return bongo_cat_config_type_error(
+ description, error, key, "present only once");
+ *target = yyjson_obj_iter_get_val(name);
+ }
+ return true;
+}
+
+bool bongo_cat_config_read_object(const char *description,
+ yyjson_val *parent, const char *key, yyjson_val **target,
+ BongoCatError *error) {
+ yyjson_val *value;
+ if (!bongo_cat_config_read_value(
+ description, parent, key, &value, error)) return false;
+ *target = NULL;
+ if (!value) return true;
+ if (!yyjson_is_obj(value)) return bongo_cat_config_type_error(
+ description, error, key, "an object");
+ *target = value;
+ return true;
+}
+
+bool bongo_cat_config_read_array(const char *description,
+ yyjson_val *parent, const char *key, yyjson_val **target,
+ BongoCatError *error) {
+ yyjson_val *value;
+ if (!bongo_cat_config_read_value(
+ description, parent, key, &value, error)) return false;
+ *target = NULL;
+ if (!value) return true;
+ if (!yyjson_is_arr(value)) return bongo_cat_config_type_error(
+ description, error, key, "an array");
+ *target = value;
+ return true;
+}
+
+bool bongo_cat_config_read_bool(const char *description,
+ yyjson_val *object, const char *key, bool *target, BongoCatError *error) {
+ yyjson_val *value;
+ if (!bongo_cat_config_read_value(
+ description, object, key, &value, error)) return false;
+ if (!value) return true;
+ if (!yyjson_is_bool(value)) return bongo_cat_config_type_error(
+ description, error, key, "a boolean");
+ *target = yyjson_get_bool(value);
+ return true;
+}
+
+static bool integer_value(yyjson_val *value, int *target) {
+ if (yyjson_is_sint(value)) {
+ int64_t number = yyjson_get_sint(value);
+ if (number >= INT_MIN && number <= INT_MAX) {
+ *target = (int)number;
+ return true;
+ }
+ } else if (yyjson_is_uint(value)) {
+ uint64_t number = yyjson_get_uint(value);
+ if (number <= INT_MAX) {
+ *target = (int)number;
+ return true;
+ }
+ }
+ return false;
+}
+
+bool bongo_cat_config_read_int(const char *description,
+ yyjson_val *object, const char *key, int *target, bool required,
+ BongoCatError *error) {
+ yyjson_val *value;
+ if (!bongo_cat_config_read_value(
+ description, object, key, &value, error)) return false;
+ if (!value && !required) return true;
+ if (!value || !integer_value(value, target))
+ return bongo_cat_config_type_error(description, error, key,
+ "an integer in the supported range");
+ return true;
+}
+
+bool bongo_cat_config_read_float(const char *description,
+ yyjson_val *object, const char *key, float *target, BongoCatError *error) {
+ yyjson_val *value;
+ if (!bongo_cat_config_read_value(
+ description, object, key, &value, error)) return false;
+ if (!value) return true;
+ if (!yyjson_is_num(value)) return bongo_cat_config_type_error(
+ description, error, key, "a number");
+ double number = yyjson_get_num(value);
+ if (!isfinite(number) || number < -FLT_MAX || number > FLT_MAX)
+ return bongo_cat_config_type_error(description, error, key,
+ "a finite number in the supported range");
+ *target = (float)number;
+ return true;
+}
+
+bool bongo_cat_config_read_string(const char *description,
+ yyjson_val *object, const char *key, const char **target, size_t *length,
+ BongoCatError *error) {
+ yyjson_val *value;
+ *target = NULL;
+ *length = 0;
+ if (!bongo_cat_config_read_value(
+ description, object, key, &value, error)) return false;
+ if (!value) return true;
+ if (!yyjson_is_str(value)) return bongo_cat_config_type_error(
+ description, error, key, "a string");
+ const char *text = yyjson_get_str(value);
+ size_t text_length = yyjson_get_len(value);
+ if (strlen(text) != text_length)
+ return bongo_cat_config_type_error(description, error, key,
+ "a string without embedded nulls");
+ *target = text;
+ *length = text_length;
+ return true;
+}
+
+bool bongo_cat_config_copy_text(const char *description, char *target,
+ size_t capacity, const char *value, size_t length, const char *field,
+ BongoCatError *error) {
+ if (!value) return true;
+ if (length >= capacity) return bongo_cat_config_type_error(
+ description, error, field, "a shorter string");
+ memset(target, 0, capacity);
+ memcpy(target, value, length);
+ return true;
+}
+
+bool bongo_cat_config_read_text(const char *description,
+ yyjson_val *object, const char *key, char *target, size_t capacity,
+ BongoCatError *error) {
+ const char *value;
+ size_t length;
+ return bongo_cat_config_read_string(description, object, key, &value,
+ &length, error) &&
+ bongo_cat_config_copy_text(description, target, capacity, value,
+ length, key, error);
+}
diff --git a/src/core/config_file.c b/src/core/config_file.c
index 26dfa5f1..3b9019aa 100644
--- a/src/core/config_file.c
+++ b/src/core/config_file.c
@@ -1,5 +1,6 @@
#include "config_internal.h"
#include "bongo_cat/file.h"
+#include "bongo_cat/path.h"
#include
#include
@@ -13,32 +14,87 @@
#include
#endif
-yyjson_doc *bongo_cat_config_read_document(const char *path,
- const char *format, BongoCatError *error) {
+static yyjson_val *unique_member(yyjson_val *object, const char *name,
+ bool *duplicate) {
+ yyjson_val *found = NULL;
+ yyjson_obj_iter iterator = yyjson_obj_iter_with(object);
+ yyjson_val *key;
+ while ((key = yyjson_obj_iter_next(&iterator))) {
+ if (!yyjson_equals_str(key, name)) continue;
+ if (found) {
+ *duplicate = true;
+ return NULL;
+ }
+ found = yyjson_obj_iter_get_val(key);
+ }
+ return found;
+}
+
+BongoCatResult bongo_cat_config_read_document(const char *path,
+ const char *format, int schema, yyjson_doc **document,
+ BongoCatError *error) {
+ if (!path || !format || !document) return BONGO_CAT_ERROR_ARGUMENT;
+ *document = NULL;
+ uint64_t file_size = 0;
+ if (!bongo_cat_path_file_size(path, &file_size)) {
+ bongo_cat_error_set(error, BONGO_CAT_ERROR_IO,
+ "Cannot inspect configuration file: %s", path);
+ return BONGO_CAT_ERROR_IO;
+ }
+ if (!file_size || file_size > BONGO_CAT_CONFIG_FILE_CAP) {
+ bongo_cat_error_set(error, BONGO_CAT_ERROR_FORMAT,
+ "Configuration file size must be between 1 and %u bytes",
+ (unsigned)BONGO_CAT_CONFIG_FILE_CAP);
+ return BONGO_CAT_ERROR_FORMAT;
+ }
yyjson_read_err json_error = {0};
FILE *file = bongo_cat_file_open(path, "rb");
- yyjson_doc *document = file ? yyjson_read_fp(file, 0, NULL, &json_error) : NULL;
- if (file) fclose(file);
- if (!document) {
- bongo_cat_error_set(error, BONGO_CAT_ERROR_FORMAT,
- "Invalid configuration JSON: %s",
- json_error.msg ? json_error.msg : "cannot open file");
- return NULL;
+ if (!file) {
+ bongo_cat_error_set(error, BONGO_CAT_ERROR_IO,
+ "Cannot open configuration file: %s", path);
+ return BONGO_CAT_ERROR_IO;
+ }
+ yyjson_doc *parsed = yyjson_read_fp(file, 0, NULL, &json_error);
+ if (fclose(file) != 0) {
+ yyjson_doc_free(parsed);
+ bongo_cat_error_set(error, BONGO_CAT_ERROR_IO,
+ "Cannot finish reading configuration file: %s", path);
+ return BONGO_CAT_ERROR_IO;
+ }
+ if (!parsed) {
+ BongoCatResult result = json_error.code ==
+ YYJSON_READ_ERROR_MEMORY_ALLOCATION ? BONGO_CAT_ERROR_MEMORY :
+ json_error.code == YYJSON_READ_ERROR_FILE_OPEN ||
+ json_error.code == YYJSON_READ_ERROR_FILE_READ ?
+ BONGO_CAT_ERROR_IO : BONGO_CAT_ERROR_FORMAT;
+ bongo_cat_error_set(error, result,
+ result == BONGO_CAT_ERROR_FORMAT ?
+ "Invalid configuration JSON: %s" :
+ "Cannot read configuration file: %s",
+ json_error.msg ? json_error.msg : path);
+ return result;
}
- yyjson_val *root = yyjson_doc_get_root(document);
- yyjson_val *format_value = yyjson_obj_get(root, "format");
- yyjson_val *version = yyjson_obj_get(root, "version");
+ yyjson_val *root = yyjson_doc_get_root(parsed);
+ bool duplicate = false;
+ yyjson_val *format_value = yyjson_is_obj(root) ?
+ unique_member(root, "format", &duplicate) : NULL;
+ yyjson_val *version = yyjson_is_obj(root) ?
+ unique_member(root, "schemaVersion", &duplicate) : NULL;
+ bool schema_matches = yyjson_is_sint(version) ?
+ yyjson_get_sint(version) == schema : yyjson_is_uint(version) &&
+ yyjson_get_uint(version) == (uint64_t)schema;
if (!yyjson_is_obj(root) || !yyjson_is_str(format_value) ||
+ yyjson_get_len(format_value) != strlen(format) ||
strcmp(yyjson_get_str(format_value), format) != 0 ||
- !yyjson_is_int(version) ||
- yyjson_get_sint(version) != BONGO_CAT_CONFIG_VERSION) {
- yyjson_doc_free(document);
+ !schema_matches || duplicate) {
+ yyjson_doc_free(parsed);
bongo_cat_error_set(error, BONGO_CAT_ERROR_FORMAT,
- "Unsupported configuration format; expected %s version %d",
- format, BONGO_CAT_CONFIG_VERSION);
- return NULL;
+ "Unsupported configuration format; expected %s schema %d",
+ format, schema);
+ return BONGO_CAT_ERROR_FORMAT;
}
- return document;
+ *document = parsed;
+ return BONGO_CAT_OK;
}
static bool sync_file(const char *path) {
@@ -81,17 +137,28 @@ BongoCatResult bongo_cat_config_write_document(const char *path,
yyjson_mut_doc *document, const char *description, BongoCatError *error) {
char temporary[BONGO_CAT_PATH_CAP + 8];
int length = snprintf(temporary, sizeof(temporary), "%s.tmp", path);
- if (length < 0 || (size_t)length >= sizeof(temporary))
+ if (length < 0 || (size_t)length >= sizeof(temporary)) {
+ bongo_cat_error_set(error, BONGO_CAT_ERROR_ARGUMENT,
+ "Configuration path is too long");
return BONGO_CAT_ERROR_ARGUMENT;
+ }
yyjson_write_err json_error = {0};
FILE *file = bongo_cat_file_open(temporary, "wb");
bool written = file && yyjson_mut_write_fp(file, document,
YYJSON_WRITE_PRETTY, NULL, &json_error);
if (file && fclose(file) != 0) written = false;
if (!written) {
- bongo_cat_error_set(error, BONGO_CAT_ERROR_IO, "Cannot write %s: %s",
+ bongo_cat_file_remove(temporary);
+ BongoCatResult result = json_error.code ==
+ YYJSON_WRITE_ERROR_MEMORY_ALLOCATION ? BONGO_CAT_ERROR_MEMORY :
+ json_error.code == YYJSON_WRITE_ERROR_INVALID_PARAMETER ||
+ json_error.code == YYJSON_WRITE_ERROR_INVALID_VALUE_TYPE ||
+ json_error.code == YYJSON_WRITE_ERROR_NAN_OR_INF ||
+ json_error.code == YYJSON_WRITE_ERROR_INVALID_STRING ?
+ BONGO_CAT_ERROR_FORMAT : BONGO_CAT_ERROR_IO;
+ bongo_cat_error_set(error, result, "Cannot write %s: %s",
description, json_error.msg ? json_error.msg : "cannot open file");
- return BONGO_CAT_ERROR_IO;
+ return result;
}
if (!sync_file(temporary)) {
bongo_cat_file_remove(temporary);
diff --git a/src/core/config_internal.h b/src/core/config_internal.h
index 3412cd4a..1f4f9c4b 100644
--- a/src/core/config_internal.h
+++ b/src/core/config_internal.h
@@ -4,11 +4,40 @@
#include "bongo_cat/config.h"
#include
-#define BONGO_CAT_CONFIG_VERSION 2
+#define BONGO_CAT_SETTINGS_SCHEMA 1
+#define BONGO_CAT_SESSION_SCHEMA 1
+#define BONGO_CAT_CONFIG_FILE_CAP (1024u * 1024u)
-yyjson_doc *bongo_cat_config_read_document(const char *path,
- const char *format, BongoCatError *error);
+BongoCatResult bongo_cat_config_read_document(const char *path,
+ const char *format, int schema, yyjson_doc **document,
+ BongoCatError *error);
BongoCatResult bongo_cat_config_write_document(const char *path,
yyjson_mut_doc *document, const char *description, BongoCatError *error);
+bool bongo_cat_config_type_error(const char *description,
+ BongoCatError *error, const char *field, const char *expected);
+bool bongo_cat_config_read_value(const char *description, yyjson_val *object,
+ const char *key, yyjson_val **target, BongoCatError *error);
+bool bongo_cat_config_read_object(const char *description,
+ yyjson_val *parent, const char *key, yyjson_val **target,
+ BongoCatError *error);
+bool bongo_cat_config_read_array(const char *description,
+ yyjson_val *parent, const char *key, yyjson_val **target,
+ BongoCatError *error);
+bool bongo_cat_config_read_bool(const char *description,
+ yyjson_val *object, const char *key, bool *target, BongoCatError *error);
+bool bongo_cat_config_read_int(const char *description,
+ yyjson_val *object, const char *key, int *target, bool required,
+ BongoCatError *error);
+bool bongo_cat_config_read_float(const char *description,
+ yyjson_val *object, const char *key, float *target, BongoCatError *error);
+bool bongo_cat_config_read_string(const char *description,
+ yyjson_val *object, const char *key, const char **target, size_t *length,
+ BongoCatError *error);
+bool bongo_cat_config_copy_text(const char *description, char *target,
+ size_t capacity, const char *value, size_t length, const char *field,
+ BongoCatError *error);
+bool bongo_cat_config_read_text(const char *description,
+ yyjson_val *object, const char *key, char *target, size_t capacity,
+ BongoCatError *error);
#endif
diff --git a/src/core/config_io.c b/src/core/config_io.c
index 0d8243f0..1d6f9bd1 100644
--- a/src/core/config_io.c
+++ b/src/core/config_io.c
@@ -1,177 +1,278 @@
#include "config_internal.h"
#include "bongo_cat/path.h"
-#include
+#include
#include
-#define PREFERENCES_FORMAT "bongo-cat/preferences"
+#define SETTINGS_FORMAT "bongocat/settings"
+#define type_error(error, field, expected) bongo_cat_config_type_error( \
+ "Settings", error, field, expected)
+#define read_value(object, key, target, error) bongo_cat_config_read_value( \
+ "Settings", object, key, target, error)
+#define read_object(object, key, target, error) bongo_cat_config_read_object( \
+ "Settings", object, key, target, error)
+#define read_array(object, key, target, error) bongo_cat_config_read_array( \
+ "Settings", object, key, target, error)
+#define read_bool(object, key, target, error) bongo_cat_config_read_bool( \
+ "Settings", object, key, target, error)
+#define read_int(object, key, target, error) bongo_cat_config_read_int( \
+ "Settings", object, key, target, false, error)
+#define read_float(object, key, target, error) bongo_cat_config_read_float( \
+ "Settings", object, key, target, error)
+#define read_string(object, key, target, length, error) \
+ bongo_cat_config_read_string( \
+ "Settings", object, key, target, length, error)
+#define read_text(object, key, target, capacity, error) \
+ bongo_cat_config_read_text( \
+ "Settings", object, key, target, capacity, error)
+#define copy_text(target, capacity, value, length, field, error) \
+ bongo_cat_config_copy_text( \
+ "Settings", target, capacity, value, length, field, error)
-static bool get_bool(yyjson_val *obj, const char *key, bool fallback) {
- yyjson_val *value = yyjson_obj_get(obj, key);
- return yyjson_is_bool(value) ? yyjson_get_bool(value) : fallback;
+static bool parse_theme(const char *value, BongoCatTheme *target) {
+ if (!strcmp(value, "auto")) *target = BONGO_CAT_THEME_AUTO;
+ else if (!strcmp(value, "light")) *target = BONGO_CAT_THEME_LIGHT;
+ else if (!strcmp(value, "dark")) *target = BONGO_CAT_THEME_DARK;
+ else return false;
+ return true;
}
-static int get_int(yyjson_val *obj, const char *key, int fallback) {
- yyjson_val *value = yyjson_obj_get(obj, key);
- return yyjson_is_num(value) ? (int)yyjson_get_sint(value) : fallback;
-}
-static float get_float(yyjson_val *obj, const char *key, float fallback) {
- yyjson_val *value = yyjson_obj_get(obj, key);
- return yyjson_is_num(value) ? (float)yyjson_get_num(value) : fallback;
-}
-static const char *get_string(yyjson_val *obj, const char *key) {
- yyjson_val *value = yyjson_obj_get(obj, key);
- return yyjson_is_str(value) ? yyjson_get_str(value) : NULL;
-}
-static void copy_string(char *target, size_t capacity, const char *value) {
- if (value) snprintf(target, capacity, "%s", value);
-}
-static BongoCatTheme parse_theme(const char *value) {
- if (value && strcmp(value, "light") == 0) return BONGO_CAT_THEME_LIGHT;
- if (value && strcmp(value, "dark") == 0) return BONGO_CAT_THEME_DARK;
- return BONGO_CAT_THEME_AUTO;
-}
-static BongoCatLanguage parse_language(const char *value) {
- if (value) for (int i = 0; i < BONGO_CAT_LANG_COUNT; ++i)
- if (strcmp(value, bongo_cat_language_name((BongoCatLanguage)i)) == 0)
- return (BongoCatLanguage)i;
- return BONGO_CAT_LANG_EN_US;
+
+static bool parse_background_color(const char *value,
+ BongoCatObsBackgroundColor *target) {
+ for (int i = 0; i < BONGO_CAT_OBS_BACKGROUND_COLOR_COUNT; ++i)
+ if (!strcmp(value, bongo_cat_obs_background_color_name(
+ (BongoCatObsBackgroundColor)i))) {
+ *target = (BongoCatObsBackgroundColor)i;
+ return true;
+ }
+ return false;
}
-static void read_model(yyjson_val *obj, BongoCatModelOptions *value) {
- if (!yyjson_is_obj(obj)) return;
- value->mirror = get_bool(obj, "mirror", value->mirror);
- value->mouse_mirror = get_bool(obj, "mouseMirror", value->mouse_mirror);
- value->mouse_centered = get_bool(obj, "mouseCenterTracking", value->mouse_centered);
- value->ignore_mouse = get_bool(obj, "ignoreMouse", value->ignore_mouse);
- value->auto_release_seconds = get_float(obj, "autoReleaseDelay", value->auto_release_seconds);
- value->max_fps = get_int(obj, "maxFPS", value->max_fps);
+static bool read_model(yyjson_val *object, BongoCatModelPreferences *value,
+ BongoCatError *error) {
+ return read_bool(object, "multiplePets", &value->multiple_pets, error) &&
+ read_bool(object, "modelMirrored", &value->mirror, error) &&
+ read_bool(object, "pointerMirrored", &value->mouse_mirror, error) &&
+ read_bool(object, "centerPointerTracking", &value->mouse_centered,
+ error) &&
+ read_bool(object, "ignorePointerInput", &value->ignore_mouse, error) &&
+ read_int(object, "maximumFps", &value->max_fps, error);
}
-static void read_window(yyjson_val *obj, BongoCatWindowOptions *value) {
- if (!yyjson_is_obj(obj)) return;
- value->pass_through = get_bool(obj, "passThrough", value->pass_through);
- value->always_on_top = get_bool(obj, "alwaysOnTop", value->always_on_top);
- value->hide_on_hover = get_bool(obj, "hideOnHover", value->hide_on_hover);
- value->keep_in_screen = get_bool(obj, "keepInScreen", value->keep_in_screen);
- value->hide_delay_seconds = get_float(obj, "hideOnHoverDelay", value->hide_delay_seconds);
+
+static bool read_window(yyjson_val *object, BongoCatWindowPreferences *value,
+ BongoCatError *error) {
+ if (!read_bool(object, "clickThrough", &value->pass_through, error) ||
+ !read_bool(object, "alwaysOnTop", &value->always_on_top, error) ||
+ !read_bool(object, "hideOnPointerOver", &value->hide_on_hover, error) ||
+ !read_bool(object, "keepOnScreen", &value->keep_in_screen, error) ||
+ !read_bool(object, "captureBackground", &value->obs_background,
+ error) ||
+ !read_bool(object, "randomExpression", &value->random_expression,
+ error) ||
+ !read_bool(object, "roundedCorners", &value->rounded_corners, error) ||
+ !read_float(object, "cornerRadiusPercent", &value->corner_radius_percent,
+ error) ||
+ !read_float(object, "hideDelaySeconds", &value->hide_delay_seconds,
+ error) ||
+ !read_float(object, "hideFadeSeconds", &value->hide_fade_seconds,
+ error) ||
+ !read_float(object, "randomExpressionIntervalSeconds",
+ &value->random_expression_interval_seconds,
+ error)) return false;
+ const char *color;
+ size_t length;
+ if (!read_string(object, "captureBackgroundColor", &color, &length,
+ error)) return false;
+ (void)length;
+ if (color && !parse_background_color(color, &value->obs_background_color))
+ return type_error(error, "captureBackgroundColor",
+ "a supported color string");
+ return true;
}
-static void read_app(yyjson_val *obj, BongoCatAppOptions *value) {
- if (!yyjson_is_obj(obj)) return;
- value->autostart = get_bool(obj, "autostart", value->autostart);
- value->tray_visible = get_bool(obj, "trayVisible", value->tray_visible);
- value->theme = parse_theme(get_string(obj, "theme"));
- value->language = parse_language(get_string(obj, "language"));
+
+static bool read_app(yyjson_val *object, BongoCatApplicationPreferences *value,
+ BongoCatError *error) {
+ if (!read_bool(object, "launchAtLogin", &value->autostart, error) ||
+ !read_bool(object, "showTrayIcon", &value->tray_visible, error))
+ return false;
+ const char *text;
+ size_t length;
+ if (!read_string(object, "theme", &text, &length, error)) return false;
+ (void)length;
+ if (text && !parse_theme(text, &value->theme))
+ return type_error(error, "theme", "auto, light, or dark");
+ if (!read_string(object, "language", &text, &length, error)) return false;
+ if (text && !bongo_cat_language_parse(text, &value->language))
+ return type_error(error, "language", "a supported locale string");
+ return true;
}
-static void read_shortcuts(yyjson_val *obj, BongoCatShortcutOptions *value) {
- if (!yyjson_is_obj(obj)) return;
- copy_string(value->visible_cat, sizeof(value->visible_cat), get_string(obj, "visibleCat"));
- copy_string(value->visible_preferences, sizeof(value->visible_preferences),
- get_string(obj, "visiblePreference"));
- copy_string(value->mirror, sizeof(value->mirror), get_string(obj, "mirrorMode"));
- copy_string(value->pass_through, sizeof(value->pass_through), get_string(obj, "penetrable"));
- copy_string(value->always_on_top, sizeof(value->always_on_top), get_string(obj, "alwaysOnTop"));
+
+static bool read_shortcuts(yyjson_val *object,
+ BongoCatShortcutPreferences *value, BongoCatError *error) {
+ return read_text(object, "toggleVisibility", value->toggle_pet_visibility,
+ sizeof(value->toggle_pet_visibility), error) &&
+ read_text(object, "openSettings", value->visible_preferences,
+ sizeof(value->visible_preferences), error) &&
+ read_text(object, "toggleModelMirror", value->mirror,
+ sizeof(value->mirror), error) &&
+ read_text(object, "toggleClickThrough", value->pass_through,
+ sizeof(value->pass_through), error) &&
+ read_text(object, "toggleAlwaysOnTop", value->always_on_top,
+ sizeof(value->always_on_top), error);
}
-static void read_behaviors(yyjson_val *array, BongoCatConfig *config) {
- if (!yyjson_is_arr(array)) return;
- config->behavior_shortcut_count = 0;
- size_t index, count; yyjson_val *item;
+
+static bool read_behaviors(yyjson_val *array, BongoCatSettings *settings,
+ BongoCatError *error) {
+ if (!array) return true;
+ if (yyjson_arr_size(array) > BONGO_CAT_BEHAVIOR_BINDING_CAP)
+ return type_error(error, "behaviorOverrides", "a smaller array");
+ settings->behavior_shortcut_count = 0;
+ size_t index, count;
+ yyjson_val *item;
yyjson_arr_foreach(array, index, count, item) {
- const char *id = get_string(item, "id");
- const char *shortcut = get_string(item, "shortcut");
- const char *label = get_string(item, "label");
- if (!id || (!shortcut && !label) ||
- config->behavior_shortcut_count >= BONGO_CAT_BEHAVIOR_CAP)
- continue;
- BongoCatBehaviorShortcut *entry =
- &config->behavior_shortcuts[config->behavior_shortcut_count++];
- copy_string(entry->id, sizeof(entry->id), id);
- copy_string(entry->shortcut, sizeof(entry->shortcut), shortcut);
- copy_string(entry->label, sizeof(entry->label), label);
+ if (!yyjson_is_obj(item))
+ return type_error(error, "behaviorOverrides[]", "an object");
+ const char *id, *shortcut, *label;
+ size_t id_length, shortcut_length, label_length;
+ if (!read_string(item, "behaviorId", &id, &id_length, error) ||
+ !read_string(item, "shortcut", &shortcut, &shortcut_length,
+ error) ||
+ !read_string(item, "displayName", &label, &label_length, error))
+ return false;
+ if (!id || !id_length)
+ return type_error(error, "behaviorId", "a non-empty string");
+ BongoCatBehaviorShortcut *entry = &settings->behavior_shortcuts[
+ settings->behavior_shortcut_count++];
+ memset(entry, 0, sizeof(*entry));
+ if (!read_bool(item, "shortcutDisabled", &entry->shortcut_disabled, error)) return false;
+ if (!copy_text(entry->id, sizeof(entry->id), id, id_length,
+ "behaviorId", error) ||
+ !copy_text(entry->shortcut, sizeof(entry->shortcut), shortcut,
+ shortcut_length, "shortcut", error) ||
+ !copy_text(entry->label, sizeof(entry->label), label,
+ label_length, "displayName", error)) return false;
}
+ return true;
}
-BongoCatResult bongo_cat_preferences_load(const char *path,
- BongoCatConfig *config, BongoCatError *error) {
- if (!path || !config) return BONGO_CAT_ERROR_ARGUMENT;
- if (!bongo_cat_path_is_file(path)) return BONGO_CAT_OK;
- yyjson_doc *document = bongo_cat_config_read_document(path, PREFERENCES_FORMAT, error);
- if (!document) return BONGO_CAT_ERROR_FORMAT;
- BongoCatConfig loaded = *config;
- yyjson_val *root = yyjson_doc_get_root(document);
- read_model(yyjson_obj_get(root, "model"), &loaded.model);
- read_window(yyjson_obj_get(root, "window"), &loaded.window);
- read_app(yyjson_obj_get(root, "app"), &loaded.app);
- read_shortcuts(yyjson_obj_get(root, "shortcuts"), &loaded.shortcuts);
- read_behaviors(yyjson_obj_get(root, "behaviorShortcuts"), &loaded);
- yyjson_doc_free(document);
- bongo_cat_config_validate(&loaded);
- *config = loaded;
- return BONGO_CAT_OK;
+static bool read_model_labels(yyjson_val *array, BongoCatSettings *settings,
+ BongoCatError *error) {
+ if (!array) return true;
+ if (yyjson_arr_size(array) > BONGO_CAT_MODEL_CAP)
+ return type_error(error, "modelOverrides", "a smaller array");
+ settings->model_label_count = 0;
+ size_t index, count;
+ yyjson_val *item;
+ yyjson_arr_foreach(array, index, count, item) {
+ if (!yyjson_is_obj(item))
+ return type_error(error, "modelOverrides[]", "an object");
+ const char *id, *label;
+ size_t id_length, label_length;
+ if (!read_string(item, "modelId", &id, &id_length, error) ||
+ !read_string(item, "displayName", &label, &label_length, error))
+ return false;
+ if (!id || !id_length || !label || !label_length)
+ return type_error(error, "modelOverrides[]",
+ "non-empty modelId and displayName strings");
+ BongoCatModelLabel *entry =
+ &settings->model_labels[settings->model_label_count++];
+ memset(entry, 0, sizeof(*entry));
+ if (!copy_text(entry->id, sizeof(entry->id), id, id_length,
+ "modelId", error) ||
+ !copy_text(entry->label, sizeof(entry->label), label,
+ label_length, "displayName", error)) return false;
+ }
+ return true;
}
-static void write_model(yyjson_mut_doc *doc, yyjson_mut_val *obj,
- const BongoCatModelOptions *v) {
- yyjson_mut_obj_add_bool(doc, obj, "mirror", v->mirror);
- yyjson_mut_obj_add_bool(doc, obj, "mouseMirror", v->mouse_mirror);
- yyjson_mut_obj_add_bool(doc, obj, "mouseCenterTracking", v->mouse_centered);
- yyjson_mut_obj_add_bool(doc, obj, "ignoreMouse", v->ignore_mouse);
- yyjson_mut_obj_add_real(doc, obj, "autoReleaseDelay", v->auto_release_seconds);
- yyjson_mut_obj_add_int(doc, obj, "maxFPS", v->max_fps);
-}
-static void write_window(yyjson_mut_doc *doc, yyjson_mut_val *obj,
- const BongoCatWindowOptions *v) {
- yyjson_mut_obj_add_bool(doc, obj, "passThrough", v->pass_through);
- yyjson_mut_obj_add_bool(doc, obj, "alwaysOnTop", v->always_on_top);
- yyjson_mut_obj_add_bool(doc, obj, "hideOnHover", v->hide_on_hover);
- yyjson_mut_obj_add_bool(doc, obj, "keepInScreen", v->keep_in_screen);
- yyjson_mut_obj_add_real(doc, obj, "hideOnHoverDelay", v->hide_delay_seconds);
-}
-static void write_app(yyjson_mut_doc *doc, yyjson_mut_val *obj,
- const BongoCatAppOptions *v) {
- yyjson_mut_obj_add_bool(doc, obj, "autostart", v->autostart);
- yyjson_mut_obj_add_bool(doc, obj, "trayVisible", v->tray_visible);
- yyjson_mut_obj_add_strcpy(doc, obj, "theme", bongo_cat_theme_name(v->theme));
- yyjson_mut_obj_add_strcpy(doc, obj, "language", bongo_cat_language_name(v->language));
-}
-static void write_shortcuts(yyjson_mut_doc *doc, yyjson_mut_val *obj,
- const BongoCatShortcutOptions *v) {
- yyjson_mut_obj_add_strcpy(doc, obj, "visibleCat", v->visible_cat);
- yyjson_mut_obj_add_strcpy(doc, obj, "visiblePreference", v->visible_preferences);
- yyjson_mut_obj_add_strcpy(doc, obj, "mirrorMode", v->mirror);
- yyjson_mut_obj_add_strcpy(doc, obj, "penetrable", v->pass_through);
- yyjson_mut_obj_add_strcpy(doc, obj, "alwaysOnTop", v->always_on_top);
+static bool read_removed_models(yyjson_val *array, BongoCatSettings *settings,
+ BongoCatError *error) {
+ if (!array) return true;
+ if (yyjson_arr_size(array) > BONGO_CAT_MODEL_CAP)
+ return type_error(error, "removedModels", "a smaller array");
+ settings->removed_model_count = 0;
+ size_t index, count;
+ yyjson_val *item;
+ yyjson_arr_foreach(array, index, count, item) {
+ const char *id = yyjson_get_str(item);
+ size_t length = yyjson_get_len(item);
+ if (!yyjson_is_str(item) || !id || !length || strlen(id) != length)
+ return type_error(error, "removedModels[]",
+ "a non-empty string without embedded nulls");
+ BongoCatRemovedModel *entry =
+ &settings->removed_models[settings->removed_model_count++];
+ memset(entry, 0, sizeof(*entry));
+ if (!copy_text(entry->id, sizeof(entry->id), id, length,
+ "removedModels[]", error)) return false;
+ }
+ return true;
}
-static void write_behaviors(yyjson_mut_doc *doc, yyjson_mut_val *root,
- const BongoCatConfig *config) {
- yyjson_mut_val *array = yyjson_mut_arr(doc);
- yyjson_mut_obj_add_val(doc, root, "behaviorShortcuts", array);
- for (size_t i = 0; i < config->behavior_shortcut_count; ++i) {
- const BongoCatBehaviorShortcut *value = &config->behavior_shortcuts[i];
- if (!value->id[0] || (!value->shortcut[0] && !value->label[0])) continue;
- yyjson_mut_val *item = yyjson_mut_obj(doc);
- yyjson_mut_obj_add_strcpy(doc, item, "id", value->id);
- if (value->shortcut[0])
- yyjson_mut_obj_add_strcpy(doc, item, "shortcut", value->shortcut);
- if (value->label[0])
- yyjson_mut_obj_add_strcpy(doc, item, "label", value->label);
- yyjson_mut_arr_add_val(array, item);
+
+static BongoCatResult read_extensions(yyjson_val *value,
+ BongoCatSettings *settings, BongoCatError *error) {
+ if (!value) return BONGO_CAT_OK;
+ if (!yyjson_is_obj(value)) {
+ type_error(error, "extensions", "an object");
+ return BONGO_CAT_ERROR_FORMAT;
}
+ size_t length = 0;
+ char *json = yyjson_val_write(value, YYJSON_WRITE_NOFLAG, &length);
+ if (!json) {
+ bongo_cat_error_set(error, BONGO_CAT_ERROR_MEMORY,
+ "Cannot preserve settings extensions");
+ return BONGO_CAT_ERROR_MEMORY;
+ }
+ if (length >= sizeof(settings->extensions_json)) {
+ free(json);
+ bongo_cat_error_set(error, BONGO_CAT_ERROR_FORMAT,
+ "Settings extensions exceed the %u-byte limit",
+ (unsigned)(sizeof(settings->extensions_json) - 1));
+ return BONGO_CAT_ERROR_FORMAT;
+ }
+ memset(settings->extensions_json, 0, sizeof(settings->extensions_json));
+ memcpy(settings->extensions_json, json, length + 1);
+ free(json);
+ return BONGO_CAT_OK;
}
-BongoCatResult bongo_cat_preferences_save(const char *path,
- const BongoCatConfig *config, BongoCatError *error) {
- if (!path || !config) return BONGO_CAT_ERROR_ARGUMENT;
- yyjson_mut_doc *doc = yyjson_mut_doc_new(NULL);
- if (!doc) return BONGO_CAT_ERROR_MEMORY;
- yyjson_mut_val *root = yyjson_mut_obj(doc); yyjson_mut_doc_set_root(doc, root);
- yyjson_mut_obj_add_strcpy(doc, root, "format", PREFERENCES_FORMAT);
- yyjson_mut_obj_add_int(doc, root, "version", BONGO_CAT_CONFIG_VERSION);
- write_model(doc, yyjson_mut_obj_add_obj(doc, root, "model"), &config->model);
- write_window(doc, yyjson_mut_obj_add_obj(doc, root, "window"), &config->window);
- write_app(doc, yyjson_mut_obj_add_obj(doc, root, "app"), &config->app);
- write_shortcuts(doc, yyjson_mut_obj_add_obj(doc, root, "shortcuts"), &config->shortcuts);
- write_behaviors(doc, root, config);
- BongoCatResult result = bongo_cat_config_write_document(path, doc,
- "preferences file", error);
- yyjson_mut_doc_free(doc);
- return result;
+BongoCatResult bongo_cat_settings_load(const char *path,
+ BongoCatSettings *settings, BongoCatError *error) {
+ if (!path || !settings) return BONGO_CAT_ERROR_ARGUMENT;
+ if (!bongo_cat_path_is_file(path)) return BONGO_CAT_OK;
+ yyjson_doc *document = NULL;
+ BongoCatResult result = bongo_cat_config_read_document(path,
+ SETTINGS_FORMAT, BONGO_CAT_SETTINGS_SCHEMA, &document, error);
+ if (result != BONGO_CAT_OK) return result;
+ BongoCatSettings loaded = *settings;
+ yyjson_val *root = yyjson_doc_get_root(document);
+ yyjson_val *model = NULL;
+ yyjson_val *window = NULL;
+ yyjson_val *application = NULL;
+ yyjson_val *shortcuts = NULL;
+ yyjson_val *behaviors = NULL;
+ yyjson_val *models = NULL;
+ yyjson_val *removed_models = NULL;
+ yyjson_val *extensions_value = NULL;
+ bool valid = read_object(root, "rendering", &model, error) &&
+ read_object(root, "window", &window, error) &&
+ read_object(root, "application", &application, error) &&
+ read_object(root, "shortcuts", &shortcuts, error) &&
+ read_array(root, "behaviorOverrides", &behaviors, error) &&
+ read_array(root, "modelOverrides", &models, error) &&
+ read_array(root, "removedModels", &removed_models, error) &&
+ read_value(root, "extensions", &extensions_value, error) &&
+ (!model || read_model(model, &loaded.model, error)) &&
+ (!window || read_window(window, &loaded.window, error)) &&
+ (!application || read_app(application, &loaded.app, error)) &&
+ (!shortcuts || read_shortcuts(shortcuts, &loaded.shortcuts, error)) &&
+ read_behaviors(behaviors, &loaded, error) &&
+ read_model_labels(models, &loaded, error) &&
+ read_removed_models(removed_models, &loaded, error);
+ if (!valid) result = BONGO_CAT_ERROR_FORMAT;
+ if (valid) result = read_extensions(extensions_value, &loaded, error);
+ yyjson_doc_free(document);
+ if (result != BONGO_CAT_OK) return result;
+ bongo_cat_settings_validate(&loaded);
+ *settings = loaded;
+ return BONGO_CAT_OK;
}
diff --git a/src/core/config_model_state.c b/src/core/config_model_state.c
new file mode 100644
index 00000000..db92414e
--- /dev/null
+++ b/src/core/config_model_state.c
@@ -0,0 +1,132 @@
+#include "bongo_cat/config.h"
+#include "bongo_cat/utf8.h"
+
+#include
+#include
+#include
+
+const char *bongo_cat_settings_model_label(const BongoCatSettings *config,
+ const char *id) {
+ if (!config || !id) return NULL;
+ size_t count = config->model_label_count;
+ if (count > BONGO_CAT_MODEL_CAP) count = BONGO_CAT_MODEL_CAP;
+ for (size_t i = 0; i < count; ++i)
+ if (!strcmp(config->model_labels[i].id, id))
+ return config->model_labels[i].label;
+ return NULL;
+}
+
+bool bongo_cat_settings_set_model_label(BongoCatSettings *config,
+ const char *id, const char *label) {
+ if (!config || !id || !id[0] || strlen(id) >= BONGO_CAT_ID_CAP ||
+ !bongo_cat_utf8_valid(id)) return false;
+ bongo_cat_settings_validate(config);
+ size_t index = config->model_label_count;
+ for (size_t i = 0; i < config->model_label_count; ++i)
+ if (!strcmp(config->model_labels[i].id, id)) {
+ index = i;
+ break;
+ }
+ if (!label || !label[0]) {
+ if (index == config->model_label_count) return false;
+ if (index + 1 < config->model_label_count)
+ memmove(&config->model_labels[index],
+ &config->model_labels[index + 1],
+ (config->model_label_count - index - 1) *
+ sizeof(config->model_labels[0]));
+ config->model_label_count--;
+ memset(&config->model_labels[config->model_label_count], 0,
+ sizeof(config->model_labels[0]));
+ return true;
+ }
+ if (strlen(label) >= BONGO_CAT_ID_CAP || !bongo_cat_utf8_valid(label))
+ return false;
+ if (index < config->model_label_count) {
+ if (!strcmp(config->model_labels[index].label, label)) return false;
+ memset(config->model_labels[index].label, 0,
+ sizeof(config->model_labels[index].label));
+ snprintf(config->model_labels[index].label,
+ sizeof(config->model_labels[index].label), "%s", label);
+ return true;
+ }
+ if (config->model_label_count >= BONGO_CAT_MODEL_CAP) return false;
+ BongoCatModelLabel *value =
+ &config->model_labels[config->model_label_count++];
+ memset(value, 0, sizeof(*value));
+ snprintf(value->id, sizeof(value->id), "%s", id);
+ snprintf(value->label, sizeof(value->label), "%s", label);
+ return true;
+}
+
+bool bongo_cat_settings_model_removed(const BongoCatSettings *config,
+ const char *id) {
+ if (!config || !id) return false;
+ size_t count = config->removed_model_count;
+ if (count > BONGO_CAT_MODEL_CAP) count = BONGO_CAT_MODEL_CAP;
+ for (size_t i = 0; i < count; ++i)
+ if (!strcmp(config->removed_models[i].id, id)) return true;
+ return false;
+}
+
+bool bongo_cat_settings_set_model_removed(BongoCatSettings *config,
+ const char *id, bool removed) {
+ if (!config || !id || !id[0] || strlen(id) >= BONGO_CAT_ID_CAP ||
+ !bongo_cat_utf8_valid(id)) return false;
+ bongo_cat_settings_validate(config);
+ size_t index = config->removed_model_count;
+ for (size_t i = 0; i < config->removed_model_count; ++i)
+ if (!strcmp(config->removed_models[i].id, id)) {
+ index = i;
+ break;
+ }
+ if (removed) {
+ if (index < config->removed_model_count) return false;
+ if (config->removed_model_count >= BONGO_CAT_MODEL_CAP) return false;
+ BongoCatRemovedModel *entry =
+ &config->removed_models[config->removed_model_count++];
+ memset(entry, 0, sizeof(*entry));
+ snprintf(entry->id, sizeof(entry->id), "%s", id);
+ return true;
+ }
+ if (index == config->removed_model_count) return false;
+ if (index + 1 < config->removed_model_count)
+ memmove(&config->removed_models[index],
+ &config->removed_models[index + 1],
+ (config->removed_model_count - index - 1) *
+ sizeof(config->removed_models[0]));
+ config->removed_model_count--;
+ memset(&config->removed_models[config->removed_model_count], 0,
+ sizeof(config->removed_models[0]));
+ return true;
+}
+
+static bool package_model_id(const char *id, const char *package_id) {
+ size_t length = package_id ? strlen(package_id) : 0;
+ if (!id || !length || strncmp(id, package_id, length) != 0) return false;
+ const char *suffix = id + length;
+ if (!suffix[0]) return true;
+ if (*suffix++ != '~' || !*suffix) return false;
+ while (*suffix)
+ if (!isdigit((unsigned char)*suffix++)) return false;
+ return true;
+}
+
+bool bongo_cat_settings_restore_model_package(BongoCatSettings *config,
+ const char *package_id) {
+ if (!config || !package_id || !package_id[0]) return false;
+ bongo_cat_settings_validate(config);
+ size_t output = 0;
+ bool changed = false;
+ for (size_t i = 0; i < config->removed_model_count; ++i) {
+ BongoCatRemovedModel entry = config->removed_models[i];
+ if (package_model_id(entry.id, package_id)) {
+ changed = true;
+ continue;
+ }
+ config->removed_models[output++] = entry;
+ }
+ memset(&config->removed_models[output], 0,
+ (BONGO_CAT_MODEL_CAP - output) * sizeof(config->removed_models[0]));
+ config->removed_model_count = output;
+ return changed;
+}
diff --git a/src/core/config_names.c b/src/core/config_names.c
new file mode 100644
index 00000000..26f56c98
--- /dev/null
+++ b/src/core/config_names.c
@@ -0,0 +1,117 @@
+#include "bongo_cat/config.h"
+
+#include
+
+const char *bongo_cat_theme_name(BongoCatTheme value) {
+ const char *names[] = {"auto", "light", "dark"};
+ return (unsigned)value <= BONGO_CAT_THEME_DARK ? names[value] : names[0];
+}
+
+const char *bongo_cat_language_name(BongoCatLanguage value) {
+ const char *names[] = {"en-US", "zh-CN", "zh-Hant", "fr-FR", "de-DE",
+ "ja-JP", "ko-KR", "pt-BR", "ru-RU", "es-ES"};
+ return (unsigned)value < BONGO_CAT_LANG_COUNT ? names[value] : names[0];
+}
+
+bool bongo_cat_language_parse(const char *name, BongoCatLanguage *value) {
+ if (!name || !value) return false;
+ if (!strcmp(name, "zh-TW")) {
+ *value = BONGO_CAT_LANG_ZH_HANT;
+ return true;
+ }
+ for (int i = 0; i < BONGO_CAT_LANG_COUNT; ++i) {
+ if (strcmp(name, bongo_cat_language_name((BongoCatLanguage)i)))
+ continue;
+ *value = (BongoCatLanguage)i;
+ return true;
+ }
+ return false;
+}
+
+static bool locale_part_equal(const char *text, size_t length,
+ const char *expected) {
+ if (length != strlen(expected)) return false;
+ for (size_t i = 0; i < length; ++i) {
+ unsigned char left = (unsigned char)text[i];
+ unsigned char right = (unsigned char)expected[i];
+ if (left >= 'A' && left <= 'Z') left += 'a' - 'A';
+ if (right >= 'A' && right <= 'Z') right += 'a' - 'A';
+ if (left != right) return false;
+ }
+ return true;
+}
+
+static bool locale_has_part(const char *text, const char *expected) {
+ if (!text) return false;
+ while (*text) {
+ while (*text == '-' || *text == '_') text++;
+ const char *end = text;
+ while (*end && *end != '-' && *end != '_') end++;
+ if (locale_part_equal(text, (size_t)(end - text), expected))
+ return true;
+ text = end;
+ }
+ return false;
+}
+
+static bool locale_language_is(const char *language, const char *expected) {
+ if (!language) return false;
+ const char *end = language;
+ while (*end && *end != '-' && *end != '_') end++;
+ return locale_part_equal(language, (size_t)(end - language), expected);
+}
+
+bool bongo_cat_language_from_locale(const char *language,
+ const char *country, BongoCatLanguage *value) {
+ if (!language || !value) return false;
+ if (locale_language_is(language, "zh")) {
+ bool hant = locale_has_part(language, "Hant") ||
+ locale_has_part(country, "Hant");
+ bool hans = locale_has_part(language, "Hans") ||
+ locale_has_part(country, "Hans");
+ bool traditional_region = locale_has_part(language, "TW") ||
+ locale_has_part(language, "HK") ||
+ locale_has_part(language, "MO") ||
+ locale_has_part(country, "TW") ||
+ locale_has_part(country, "HK") ||
+ locale_has_part(country, "MO");
+ *value = hant || (!hans && traditional_region) ?
+ BONGO_CAT_LANG_ZH_HANT : BONGO_CAT_LANG_ZH_CN;
+ return true;
+ }
+ static const struct {
+ const char *code;
+ BongoCatLanguage value;
+ } supported[] = {{"en", BONGO_CAT_LANG_EN_US},
+ {"fr", BONGO_CAT_LANG_FR_FR}, {"de", BONGO_CAT_LANG_DE_DE},
+ {"ja", BONGO_CAT_LANG_JA_JP}, {"ko", BONGO_CAT_LANG_KO_KR},
+ {"pt", BONGO_CAT_LANG_PT_BR}, {"ru", BONGO_CAT_LANG_RU_RU},
+ {"es", BONGO_CAT_LANG_ES_ES}};
+ for (size_t i = 0; i < sizeof(supported) / sizeof(supported[0]); ++i)
+ if (locale_language_is(language, supported[i].code)) {
+ *value = supported[i].value;
+ return true;
+ }
+ return false;
+}
+
+const char *bongo_cat_mode_name(BongoCatModelMode value) {
+ const char *names[] = {"standard", "keyboard", "gamepad"};
+ return (unsigned)value <= BONGO_CAT_MODE_GAMEPAD ? names[value] : names[0];
+}
+
+const char *bongo_cat_obs_background_color_name(
+ BongoCatObsBackgroundColor value) {
+ static const char *names[] = {
+ "#00ff00", "#0000ff", "#ff0000", "#ff00ff"};
+ return (unsigned)value < BONGO_CAT_OBS_BACKGROUND_COLOR_COUNT ?
+ names[value] : names[BONGO_CAT_OBS_BACKGROUND_GREEN];
+}
+
+uint32_t bongo_cat_obs_background_color_rgb(
+ BongoCatObsBackgroundColor value) {
+ static const uint32_t colors[] = {
+ 0x00ff00, 0x0000ff, 0xff0000, 0xff00ff};
+ return (unsigned)value < BONGO_CAT_OBS_BACKGROUND_COLOR_COUNT ?
+ colors[value] : colors[BONGO_CAT_OBS_BACKGROUND_GREEN];
+}
diff --git a/src/core/config_session_defaults.c b/src/core/config_session_defaults.c
new file mode 100644
index 00000000..42cac653
--- /dev/null
+++ b/src/core/config_session_defaults.c
@@ -0,0 +1,178 @@
+#include "bongo_cat/config.h"
+#include "bongo_cat/utf8.h"
+
+#include
+#include
+#include
+
+static float clampf_or(float value, float low, float high, float fallback) {
+ if (!isfinite(value)) return fallback;
+ return value < low ? low : value > high ? high : value;
+}
+
+static bool normalize_text(char *text, size_t capacity) {
+ text[capacity - 1] = '\0';
+ if (!bongo_cat_utf8_valid(text)) {
+ memset(text, 0, capacity);
+ return false;
+ }
+ size_t length = strlen(text);
+ memset(text + length + 1, 0, capacity - length - 1);
+ return true;
+}
+
+void bongo_cat_session_defaults(BongoCatSessionState *session) {
+ if (!session) return;
+ memset(session, 0, sizeof(*session));
+ session->window.visible = true;
+ session->window.scale_percent = BONGO_CAT_DEFAULT_WINDOW_SCALE_PERCENT;
+ session->window.opacity_percent =
+ BONGO_CAT_DEFAULT_WINDOW_OPACITY_PERCENT;
+ session->window.width = 612;
+ session->window.height = 354;
+ session->window.content_width = 612;
+ session->window.content_height = 354;
+ memcpy(session->active_model_id, "standard", sizeof("standard"));
+}
+
+static void compact_active_behaviors(BongoCatSessionState *session) {
+ size_t count = session->active_behavior_count;
+ if (count > BONGO_CAT_BEHAVIOR_BINDING_CAP)
+ count = BONGO_CAT_BEHAVIOR_BINDING_CAP;
+ size_t output = 0;
+ for (size_t i = 0; i < count; ++i) {
+ BongoCatActiveBehavior entry = session->active_behaviors[i];
+ if (!normalize_text(entry.model_id, sizeof(entry.model_id)) ||
+ !normalize_text(entry.behavior_id, sizeof(entry.behavior_id)) ||
+ !entry.model_id[0] || !entry.behavior_id[0]) continue;
+ bool duplicate = false;
+ for (size_t j = 0; j < output; ++j)
+ if (!strcmp(session->active_behaviors[j].model_id,
+ entry.model_id) &&
+ !strcmp(session->active_behaviors[j].behavior_id,
+ entry.behavior_id)) {
+ duplicate = true;
+ break;
+ }
+ if (!duplicate) session->active_behaviors[output++] = entry;
+ }
+ if (output < BONGO_CAT_BEHAVIOR_BINDING_CAP)
+ memset(&session->active_behaviors[output], 0,
+ (BONGO_CAT_BEHAVIOR_BINDING_CAP - output) *
+ sizeof(session->active_behaviors[0]));
+ session->active_behavior_count = output;
+}
+
+static void compact_additional_models(BongoCatSessionState *session) {
+ size_t count = session->additional_model_count;
+ if (count > BONGO_CAT_ADDITIONAL_MODEL_CAP)
+ count = BONGO_CAT_ADDITIONAL_MODEL_CAP;
+ size_t output = 0;
+ for (size_t i = 0; i < count; ++i) {
+ char model_id[BONGO_CAT_ID_CAP];
+ memcpy(model_id, session->additional_model_ids[i], sizeof(model_id));
+ if (!normalize_text(model_id, sizeof(model_id)) || !model_id[0] ||
+ !strcmp(model_id, session->active_model_id)) continue;
+ bool duplicate = false;
+ for (size_t j = 0; j < output; ++j)
+ if (!strcmp(session->additional_model_ids[j], model_id)) {
+ duplicate = true;
+ break;
+ }
+ if (!duplicate) {
+ memset(session->additional_model_ids[output], 0,
+ sizeof(session->additional_model_ids[0]));
+ snprintf(session->additional_model_ids[output++],
+ sizeof(session->additional_model_ids[0]), "%s", model_id);
+ }
+ }
+ memset(&session->additional_model_ids[output], 0,
+ (BONGO_CAT_ADDITIONAL_MODEL_CAP - output) *
+ sizeof(session->additional_model_ids[0]));
+ session->additional_model_count = output;
+}
+
+void bongo_cat_session_validate(BongoCatSessionState *session) {
+ if (!session) return;
+ session->window.scale_percent = clampf_or(session->window.scale_percent,
+ 10.0f, 500.0f, BONGO_CAT_DEFAULT_WINDOW_SCALE_PERCENT);
+ session->window.opacity_percent = clampf_or(
+ session->window.opacity_percent, 10.0f, 100.0f,
+ BONGO_CAT_DEFAULT_WINDOW_OPACITY_PERCENT);
+ if (session->window.width < 64) session->window.width = 64;
+ if (session->window.height < 64) session->window.height = 64;
+ if (session->window.width > 8192) session->window.width = 8192;
+ if (session->window.height > 8192) session->window.height = 8192;
+ if (session->window.content_width < 64)
+ session->window.content_width = session->window.width;
+ if (session->window.content_height < 64)
+ session->window.content_height = session->window.height;
+ if (session->window.content_width > 8192)
+ session->window.content_width = 8192;
+ if (session->window.content_height > 8192)
+ session->window.content_height = 8192;
+ if (!normalize_text(session->active_model_id,
+ sizeof(session->active_model_id)) ||
+ !session->active_model_id[0])
+ memcpy(session->active_model_id, "standard", sizeof("standard"));
+ if (session->last_update_check_day < 0 ||
+ session->last_update_check_day > 99991231)
+ session->last_update_check_day = 0;
+ normalize_text(session->last_update_check_version,
+ sizeof(session->last_update_check_version));
+ normalize_text(session->available_update_version,
+ sizeof(session->available_update_version));
+ compact_additional_models(session);
+ compact_active_behaviors(session);
+}
+
+bool bongo_cat_session_model_active(const BongoCatSessionState *session,
+ const char *model_id) {
+ if (!session || !model_id || !model_id[0]) return false;
+ if (!strcmp(session->active_model_id, model_id)) return true;
+ size_t count = session->additional_model_count;
+ if (count > BONGO_CAT_ADDITIONAL_MODEL_CAP)
+ count = BONGO_CAT_ADDITIONAL_MODEL_CAP;
+ for (size_t i = 0; i < count; ++i)
+ if (!strcmp(session->additional_model_ids[i], model_id)) return true;
+ return false;
+}
+
+bool bongo_cat_session_add_model(BongoCatSessionState *session,
+ const char *model_id) {
+ if (!session || !model_id || !model_id[0] ||
+ strlen(model_id) >= BONGO_CAT_ID_CAP ||
+ !bongo_cat_utf8_valid(model_id)) return false;
+ bongo_cat_session_validate(session);
+ if (bongo_cat_session_model_active(session, model_id)) return true;
+ if (session->additional_model_count >= BONGO_CAT_ADDITIONAL_MODEL_CAP)
+ return false;
+ snprintf(session->additional_model_ids[session->additional_model_count++],
+ sizeof(session->additional_model_ids[0]), "%s", model_id);
+ return true;
+}
+
+bool bongo_cat_session_remove_model(BongoCatSessionState *session,
+ const char *model_id) {
+ if (!session || !model_id) return false;
+ for (size_t i = 0; i < session->additional_model_count; ++i) {
+ if (strcmp(session->additional_model_ids[i], model_id)) continue;
+ if (i + 1 < session->additional_model_count)
+ memmove(&session->additional_model_ids[i],
+ &session->additional_model_ids[i + 1],
+ (session->additional_model_count - i - 1) *
+ sizeof(session->additional_model_ids[0]));
+ memset(&session->additional_model_ids[
+ --session->additional_model_count], 0,
+ sizeof(session->additional_model_ids[0]));
+ return true;
+ }
+ return false;
+}
+
+void bongo_cat_session_clear_additional_models(BongoCatSessionState *session) {
+ if (!session) return;
+ memset(session->additional_model_ids, 0,
+ sizeof(session->additional_model_ids));
+ session->additional_model_count = 0;
+}
diff --git a/src/core/config_session_io.c b/src/core/config_session_io.c
index da5b943f..fcbd3e9c 100644
--- a/src/core/config_session_io.c
+++ b/src/core/config_session_io.c
@@ -2,66 +2,258 @@
#include "bongo_cat/path.h"
#include
+#include
-#define SESSION_FORMAT "bongo-cat/session"
+#define SESSION_FORMAT "bongocat/session"
+#define type_error(error, field, expected) bongo_cat_config_type_error( \
+ "Session", error, field, expected)
+#define read_value(object, key, target, error) bongo_cat_config_read_value( \
+ "Session", object, key, target, error)
+#define read_object(object, key, target, error) bongo_cat_config_read_object( \
+ "Session", object, key, target, error)
+#define read_array(object, key, target, error) bongo_cat_config_read_array( \
+ "Session", object, key, target, error)
+#define read_bool(object, key, target, error) bongo_cat_config_read_bool( \
+ "Session", object, key, target, error)
+#define read_int(object, key, target, required, error) \
+ bongo_cat_config_read_int( \
+ "Session", object, key, target, required, error)
+#define read_float(object, key, target, error) bongo_cat_config_read_float( \
+ "Session", object, key, target, error)
+#define read_string(object, key, target, length, error) \
+ bongo_cat_config_read_string( \
+ "Session", object, key, target, length, error)
+#define copy_text(target, capacity, value, length, field, error) \
+ bongo_cat_config_copy_text( \
+ "Session", target, capacity, value, length, field, error)
-static bool get_bool(yyjson_val *obj, const char *key, bool fallback) {
- yyjson_val *value = yyjson_obj_get(obj, key);
- return yyjson_is_bool(value) ? yyjson_get_bool(value) : fallback;
+static bool read_active_model(yyjson_val *root,
+ BongoCatSessionState *session, BongoCatError *error) {
+ yyjson_val *value;
+ if (!read_value(root, "activeModelId", &value, error)) return false;
+ if (!value) return true;
+ if (!yyjson_is_str(value))
+ return type_error(error, "activeModelId", "a string");
+ const char *text = yyjson_get_str(value);
+ size_t length = yyjson_get_len(value);
+ if (!length || length >= sizeof(session->active_model_id) ||
+ strlen(text) != length)
+ return type_error(error, "activeModelId",
+ "a non-empty string within the supported length");
+ memset(session->active_model_id, 0, sizeof(session->active_model_id));
+ memcpy(session->active_model_id, text, length);
+ return true;
}
-static int get_int(yyjson_val *obj, const char *key, int fallback) {
- yyjson_val *value = yyjson_obj_get(obj, key);
- return yyjson_is_num(value) ? (int)yyjson_get_sint(value) : fallback;
+
+static bool read_active_behaviors(yyjson_val *array,
+ BongoCatSessionState *session, BongoCatError *error) {
+ if (!array) return true;
+ if (yyjson_arr_size(array) > BONGO_CAT_BEHAVIOR_BINDING_CAP)
+ return type_error(error, "activeBehaviors", "a smaller array");
+ session->active_behavior_count = 0;
+ size_t index, count;
+ yyjson_val *item;
+ yyjson_arr_foreach(array, index, count, item) {
+ if (!yyjson_is_obj(item))
+ return type_error(error, "activeBehaviors[]", "an object");
+ const char *model_id, *behavior_id;
+ size_t model_length, behavior_length;
+ if (!read_string(item, "modelId", &model_id, &model_length,
+ error) ||
+ !read_string(item, "behaviorId", &behavior_id,
+ &behavior_length, error)) return false;
+ if (!model_id || !model_length || !behavior_id || !behavior_length)
+ return type_error(error, "activeBehaviors[]",
+ "non-empty modelId and behaviorId strings");
+ BongoCatActiveBehavior *entry = &session->active_behaviors[
+ session->active_behavior_count++];
+ memset(entry, 0, sizeof(*entry));
+ if (!copy_text(entry->model_id, sizeof(entry->model_id), model_id,
+ model_length, "modelId", error) ||
+ !copy_text(entry->behavior_id, sizeof(entry->behavior_id),
+ behavior_id, behavior_length, "behaviorId", error))
+ return false;
+ }
+ return true;
}
-static float get_float(yyjson_val *obj, const char *key, float fallback) {
- yyjson_val *value = yyjson_obj_get(obj, key);
- return yyjson_is_num(value) ? (float)yyjson_get_num(value) : fallback;
+
+static bool read_additional_models(yyjson_val *array,
+ BongoCatSessionState *session, BongoCatError *error) {
+ if (!array) return true;
+ if (yyjson_arr_size(array) > BONGO_CAT_ADDITIONAL_MODEL_CAP)
+ return type_error(error, "additionalModelIds", "a smaller array");
+ session->additional_model_count = 0;
+ size_t index, count;
+ yyjson_val *item;
+ yyjson_arr_foreach(array, index, count, item) {
+ if (!yyjson_is_str(item) || !yyjson_get_len(item) ||
+ yyjson_get_len(item) >= BONGO_CAT_ID_CAP ||
+ strlen(yyjson_get_str(item)) != yyjson_get_len(item))
+ return type_error(error, "additionalModelIds[]",
+ "a non-empty model id within the supported length");
+ snprintf(session->additional_model_ids[
+ session->additional_model_count++], BONGO_CAT_ID_CAP, "%s",
+ yyjson_get_str(item));
+ }
+ return true;
}
BongoCatResult bongo_cat_session_load(const char *path,
- BongoCatConfig *config, BongoCatError *error) {
- if (!path || !config) return BONGO_CAT_ERROR_ARGUMENT;
+ BongoCatSessionState *session, BongoCatError *error) {
+ if (!path || !session) return BONGO_CAT_ERROR_ARGUMENT;
if (!bongo_cat_path_is_file(path)) return BONGO_CAT_OK;
- yyjson_doc *document = bongo_cat_config_read_document(path, SESSION_FORMAT, error);
- if (!document) return BONGO_CAT_ERROR_FORMAT;
- BongoCatConfig loaded = *config;
+ yyjson_doc *document = NULL;
+ BongoCatResult result = bongo_cat_config_read_document(path,
+ SESSION_FORMAT, BONGO_CAT_SESSION_SCHEMA, &document, error);
+ if (result != BONGO_CAT_OK) return result;
+ BongoCatSessionState loaded = *session;
yyjson_val *root = yyjson_doc_get_root(document);
- yyjson_val *window = yyjson_obj_get(root, "window");
- if (yyjson_is_obj(window)) {
- loaded.window.visible = get_bool(window, "visible", loaded.window.visible);
- loaded.window.scale_percent = get_float(window, "scale", loaded.window.scale_percent);
- loaded.window.opacity_percent = get_float(window, "opacity", loaded.window.opacity_percent);
- loaded.window.x = get_int(window, "x", loaded.window.x);
- loaded.window.y = get_int(window, "y", loaded.window.y);
- loaded.window.width = get_int(window, "width", loaded.window.width);
- loaded.window.height = get_int(window, "height", loaded.window.height);
+ yyjson_val *window = NULL;
+ yyjson_val *active_behaviors = NULL;
+ yyjson_val *additional_models = NULL;
+ bool valid = read_object(root, "window", &window, error) &&
+ read_array(root, "activeBehaviors", &active_behaviors, error) &&
+ read_array(root, "additionalModelIds", &additional_models, error);
+ if (valid && window) {
+ valid = read_bool(window, "visible", &loaded.window.visible, error) &&
+ read_float(window, "scalePercent",
+ &loaded.window.scale_percent, error) &&
+ read_float(window, "opacityPercent",
+ &loaded.window.opacity_percent, error);
+ yyjson_val *position = NULL;
+ yyjson_val *size = NULL;
+ if (valid) valid = read_object(
+ window, "position", &position, error);
+ if (valid) valid = read_object(window, "size", &size, error);
+ if (valid && position) {
+ valid = read_int(position, "x", &loaded.window.x, true, error) &&
+ read_int(position, "y", &loaded.window.y, true, error);
+ if (valid) loaded.window.position_known = true;
+ }
+ if (valid && size) {
+ int content_width = 0, content_height = 0;
+ valid = read_int(size, "width", &loaded.window.width, true,
+ error) &&
+ read_int(size, "height", &loaded.window.height, true, error) &&
+ read_int(size, "contentWidth", &content_width, false, error) &&
+ read_int(size, "contentHeight", &content_height, false, error);
+ if (valid) {
+ loaded.window.content_width = content_width > 0
+ ? content_width : loaded.window.width;
+ loaded.window.content_height = content_height > 0
+ ? content_height : loaded.window.height;
+ }
+ }
}
- yyjson_val *model = yyjson_obj_get(root, "currentModel");
- if (yyjson_is_str(model)) snprintf(loaded.current_model,
- sizeof(loaded.current_model), "%s", yyjson_get_str(model));
+ if (valid) valid = read_active_model(root, &loaded, error) &&
+ read_int(root, "lastUpdateCheckDay",
+ &loaded.last_update_check_day, false, error) &&
+ bongo_cat_config_read_text("Session", root,
+ "lastUpdateCheckVersion", loaded.last_update_check_version,
+ sizeof(loaded.last_update_check_version), error) &&
+ bongo_cat_config_read_text("Session", root,
+ "availableUpdateVersion", loaded.available_update_version,
+ sizeof(loaded.available_update_version), error) &&
+ read_additional_models(additional_models, &loaded, error) &&
+ read_active_behaviors(active_behaviors, &loaded, error);
yyjson_doc_free(document);
- bongo_cat_config_validate(&loaded);
- *config = loaded;
+ if (!valid) return BONGO_CAT_ERROR_FORMAT;
+ bongo_cat_session_validate(&loaded);
+ *session = loaded;
return BONGO_CAT_OK;
}
+static BongoCatResult build_error(BongoCatError *error) {
+ bongo_cat_error_set(error, BONGO_CAT_ERROR_MEMORY,
+ "Cannot allocate session JSON");
+ return BONGO_CAT_ERROR_MEMORY;
+}
+
+static bool write_active_behaviors(yyjson_mut_doc *doc,
+ yyjson_mut_val *root, const BongoCatSessionState *session) {
+ yyjson_mut_val *array = yyjson_mut_arr(doc);
+ if (!array || !yyjson_mut_obj_add_val(
+ doc, root, "activeBehaviors", array)) return false;
+ for (size_t i = 0; i < session->active_behavior_count; ++i) {
+ const BongoCatActiveBehavior *entry =
+ &session->active_behaviors[i];
+ yyjson_mut_val *item = yyjson_mut_obj(doc);
+ if (!item || !yyjson_mut_obj_add_strcpy(
+ doc, item, "modelId", entry->model_id) ||
+ !yyjson_mut_obj_add_strcpy(
+ doc, item, "behaviorId", entry->behavior_id) ||
+ !yyjson_mut_arr_add_val(array, item)) return false;
+ }
+ return true;
+}
+
+static bool write_additional_models(yyjson_mut_doc *doc,
+ yyjson_mut_val *root, const BongoCatSessionState *session) {
+ yyjson_mut_val *array = yyjson_mut_arr(doc);
+ if (!array || !yyjson_mut_obj_add_val(
+ doc, root, "additionalModelIds", array)) return false;
+ for (size_t i = 0; i < session->additional_model_count; ++i)
+ if (!yyjson_mut_arr_add_strcpy(
+ doc, array, session->additional_model_ids[i])) return false;
+ return true;
+}
+
BongoCatResult bongo_cat_session_save(const char *path,
- const BongoCatConfig *config, BongoCatError *error) {
- if (!path || !config) return BONGO_CAT_ERROR_ARGUMENT;
+ const BongoCatSessionState *session, BongoCatError *error) {
+ if (!path || !session) return BONGO_CAT_ERROR_ARGUMENT;
+ BongoCatSessionState canonical = *session;
+ bongo_cat_session_validate(&canonical);
yyjson_mut_doc *doc = yyjson_mut_doc_new(NULL);
- if (!doc) return BONGO_CAT_ERROR_MEMORY;
- yyjson_mut_val *root = yyjson_mut_obj(doc); yyjson_mut_doc_set_root(doc, root);
- yyjson_mut_obj_add_strcpy(doc, root, "format", SESSION_FORMAT);
- yyjson_mut_obj_add_int(doc, root, "version", BONGO_CAT_CONFIG_VERSION);
- yyjson_mut_val *window = yyjson_mut_obj_add_obj(doc, root, "window");
- yyjson_mut_obj_add_bool(doc, window, "visible", config->window.visible);
- yyjson_mut_obj_add_real(doc, window, "scale", config->window.scale_percent);
- yyjson_mut_obj_add_real(doc, window, "opacity", config->window.opacity_percent);
- yyjson_mut_obj_add_int(doc, window, "x", config->window.x);
- yyjson_mut_obj_add_int(doc, window, "y", config->window.y);
- yyjson_mut_obj_add_int(doc, window, "width", config->window.width);
- yyjson_mut_obj_add_int(doc, window, "height", config->window.height);
- yyjson_mut_obj_add_strcpy(doc, root, "currentModel", config->current_model);
+ if (!doc) return build_error(error);
+ yyjson_mut_val *root = yyjson_mut_obj(doc);
+ if (!root) {
+ yyjson_mut_doc_free(doc);
+ return build_error(error);
+ }
+ yyjson_mut_doc_set_root(doc, root);
+ bool built = yyjson_mut_obj_add_strcpy(
+ doc, root, "format", SESSION_FORMAT) &&
+ yyjson_mut_obj_add_int(doc, root, "schemaVersion",
+ BONGO_CAT_SESSION_SCHEMA);
+ yyjson_mut_val *window = built ?
+ yyjson_mut_obj_add_obj(doc, root, "window") : NULL;
+ built = built && window &&
+ yyjson_mut_obj_add_bool(doc, window, "visible",
+ canonical.window.visible) &&
+ yyjson_mut_obj_add_real(doc, window, "scalePercent",
+ canonical.window.scale_percent) &&
+ yyjson_mut_obj_add_real(doc, window, "opacityPercent",
+ canonical.window.opacity_percent);
+ if (built && canonical.window.position_known) {
+ yyjson_mut_val *position =
+ yyjson_mut_obj_add_obj(doc, window, "position");
+ built = position &&
+ yyjson_mut_obj_add_int(doc, position, "x", canonical.window.x) &&
+ yyjson_mut_obj_add_int(doc, position, "y", canonical.window.y);
+ }
+ yyjson_mut_val *size = built ?
+ yyjson_mut_obj_add_obj(doc, window, "size") : NULL;
+ built = built && size &&
+ yyjson_mut_obj_add_int(doc, size, "width", canonical.window.width) &&
+ yyjson_mut_obj_add_int(doc, size, "height", canonical.window.height) &&
+ yyjson_mut_obj_add_int(doc, size, "contentWidth",
+ canonical.window.content_width) &&
+ yyjson_mut_obj_add_int(doc, size, "contentHeight",
+ canonical.window.content_height) &&
+ yyjson_mut_obj_add_strcpy(doc, root, "activeModelId",
+ canonical.active_model_id) &&
+ yyjson_mut_obj_add_int(doc, root, "lastUpdateCheckDay",
+ canonical.last_update_check_day) &&
+ yyjson_mut_obj_add_strcpy(doc, root, "lastUpdateCheckVersion",
+ canonical.last_update_check_version) &&
+ yyjson_mut_obj_add_strcpy(doc, root, "availableUpdateVersion",
+ canonical.available_update_version) &&
+ write_additional_models(doc, root, &canonical) &&
+ write_active_behaviors(doc, root, &canonical);
+ if (!built) {
+ yyjson_mut_doc_free(doc);
+ return build_error(error);
+ }
BongoCatResult result = bongo_cat_config_write_document(path, doc,
"session file", error);
yyjson_mut_doc_free(doc);
diff --git a/src/core/config_settings_save.c b/src/core/config_settings_save.c
new file mode 100644
index 00000000..e6104783
--- /dev/null
+++ b/src/core/config_settings_save.c
@@ -0,0 +1,222 @@
+#include "config_internal.h"
+
+#include
+
+#define SETTINGS_FORMAT "bongocat/settings"
+
+static bool write_model(yyjson_mut_doc *doc, yyjson_mut_val *object,
+ const BongoCatModelPreferences *value) {
+ return object &&
+ yyjson_mut_obj_add_bool(doc, object, "multiplePets",
+ value->multiple_pets) &&
+ yyjson_mut_obj_add_bool(doc, object, "modelMirrored", value->mirror) &&
+ yyjson_mut_obj_add_bool(doc, object, "pointerMirrored",
+ value->mouse_mirror) &&
+ yyjson_mut_obj_add_bool(doc, object, "centerPointerTracking",
+ value->mouse_centered) &&
+ yyjson_mut_obj_add_bool(doc, object, "ignorePointerInput",
+ value->ignore_mouse) &&
+ yyjson_mut_obj_add_int(doc, object, "maximumFps", value->max_fps);
+}
+
+static bool write_window(yyjson_mut_doc *doc, yyjson_mut_val *object,
+ const BongoCatWindowPreferences *value) {
+ return object &&
+ yyjson_mut_obj_add_bool(doc, object, "clickThrough",
+ value->pass_through) &&
+ yyjson_mut_obj_add_bool(doc, object, "alwaysOnTop",
+ value->always_on_top) &&
+ yyjson_mut_obj_add_bool(doc, object, "hideOnPointerOver",
+ value->hide_on_hover) &&
+ yyjson_mut_obj_add_bool(doc, object, "keepOnScreen",
+ value->keep_in_screen) &&
+ yyjson_mut_obj_add_bool(doc, object, "captureBackground",
+ value->obs_background) &&
+ yyjson_mut_obj_add_bool(doc, object, "randomExpression",
+ value->random_expression) &&
+ yyjson_mut_obj_add_bool(doc, object, "roundedCorners",
+ value->rounded_corners) &&
+ yyjson_mut_obj_add_real(doc, object, "cornerRadiusPercent",
+ value->corner_radius_percent) &&
+ yyjson_mut_obj_add_strcpy(doc, object, "captureBackgroundColor",
+ bongo_cat_obs_background_color_name(
+ value->obs_background_color)) &&
+ yyjson_mut_obj_add_real(doc, object, "hideDelaySeconds",
+ value->hide_delay_seconds) &&
+ yyjson_mut_obj_add_real(doc, object, "hideFadeSeconds",
+ value->hide_fade_seconds) &&
+ yyjson_mut_obj_add_real(doc, object,
+ "randomExpressionIntervalSeconds",
+ value->random_expression_interval_seconds);
+}
+
+static bool write_app(yyjson_mut_doc *doc, yyjson_mut_val *object,
+ const BongoCatApplicationPreferences *value) {
+ return object &&
+ yyjson_mut_obj_add_bool(doc, object, "launchAtLogin",
+ value->autostart) &&
+ yyjson_mut_obj_add_bool(doc, object, "showTrayIcon",
+ value->tray_visible) &&
+ yyjson_mut_obj_add_strcpy(doc, object, "theme",
+ bongo_cat_theme_name(value->theme)) &&
+ yyjson_mut_obj_add_strcpy(doc, object, "language",
+ bongo_cat_language_name(value->language));
+}
+
+static bool write_shortcuts(yyjson_mut_doc *doc, yyjson_mut_val *object,
+ const BongoCatShortcutPreferences *value) {
+ return object &&
+ yyjson_mut_obj_add_strcpy(doc, object, "toggleVisibility",
+ value->toggle_pet_visibility) &&
+ yyjson_mut_obj_add_strcpy(doc, object, "openSettings",
+ value->visible_preferences) &&
+ yyjson_mut_obj_add_strcpy(doc, object, "toggleModelMirror",
+ value->mirror) &&
+ yyjson_mut_obj_add_strcpy(doc, object, "toggleClickThrough",
+ value->pass_through) &&
+ yyjson_mut_obj_add_strcpy(doc, object, "toggleAlwaysOnTop",
+ value->always_on_top);
+}
+
+static bool write_behaviors(yyjson_mut_doc *doc, yyjson_mut_val *root,
+ const BongoCatSettings *settings) {
+ yyjson_mut_val *array = yyjson_mut_arr(doc);
+ if (!array || !yyjson_mut_obj_add_val(
+ doc, root, "behaviorOverrides", array)) return false;
+ for (size_t i = 0; i < settings->behavior_shortcut_count; ++i) {
+ const BongoCatBehaviorShortcut *value =
+ &settings->behavior_shortcuts[i];
+ yyjson_mut_val *item = yyjson_mut_obj(doc);
+ if (!item || !yyjson_mut_obj_add_strcpy(
+ doc, item, "behaviorId", value->id) ||
+ (value->shortcut_disabled && !yyjson_mut_obj_add_bool(
+ doc, item, "shortcutDisabled", true)) ||
+ (value->shortcut[0] && !yyjson_mut_obj_add_strcpy(
+ doc, item, "shortcut", value->shortcut)) ||
+ (value->label[0] && !yyjson_mut_obj_add_strcpy(
+ doc, item, "displayName", value->label)) ||
+ !yyjson_mut_arr_add_val(array, item)) return false;
+ }
+ return true;
+}
+
+static bool write_model_labels(yyjson_mut_doc *doc, yyjson_mut_val *root,
+ const BongoCatSettings *settings) {
+ yyjson_mut_val *array = yyjson_mut_arr(doc);
+ if (!array || !yyjson_mut_obj_add_val(
+ doc, root, "modelOverrides", array)) return false;
+ for (size_t i = 0; i < settings->model_label_count; ++i) {
+ const BongoCatModelLabel *value = &settings->model_labels[i];
+ yyjson_mut_val *item = yyjson_mut_obj(doc);
+ if (!item || !yyjson_mut_obj_add_strcpy(
+ doc, item, "modelId", value->id) ||
+ !yyjson_mut_obj_add_strcpy(
+ doc, item, "displayName", value->label) ||
+ !yyjson_mut_arr_add_val(array, item)) return false;
+ }
+ return true;
+}
+
+static bool write_removed_models(yyjson_mut_doc *doc, yyjson_mut_val *root,
+ const BongoCatSettings *settings) {
+ yyjson_mut_val *array = yyjson_mut_arr(doc);
+ if (!array || !yyjson_mut_obj_add_val(
+ doc, root, "removedModels", array)) return false;
+ for (size_t i = 0; i < settings->removed_model_count; ++i)
+ if (!yyjson_mut_arr_add_strcpy(doc, array,
+ settings->removed_models[i].id)) return false;
+ return true;
+}
+
+static yyjson_mut_val *write_extensions(yyjson_mut_doc *target,
+ const BongoCatSettings *settings, BongoCatResult *failure,
+ BongoCatError *error) {
+ *failure = BONGO_CAT_OK;
+ const char *json = settings->extensions_json;
+ const char *end = memchr(json, '\0', sizeof(settings->extensions_json));
+ if (!end) {
+ *failure = BONGO_CAT_ERROR_FORMAT;
+ bongo_cat_error_set(error, BONGO_CAT_ERROR_FORMAT,
+ "Settings extensions are not null-terminated");
+ return NULL;
+ }
+ size_t length = (size_t)(end - json);
+ if (!length) {
+ json = "{}";
+ length = 2;
+ }
+ yyjson_read_err read_error = {0};
+ yyjson_doc *source = yyjson_read_opts(
+ (char *)json, length, 0, NULL, &read_error);
+ yyjson_val *root = source ? yyjson_doc_get_root(source) : NULL;
+ if (!yyjson_is_obj(root)) {
+ yyjson_doc_free(source);
+ BongoCatResult result = read_error.code ==
+ YYJSON_READ_ERROR_MEMORY_ALLOCATION ? BONGO_CAT_ERROR_MEMORY :
+ BONGO_CAT_ERROR_FORMAT;
+ *failure = result;
+ bongo_cat_error_set(error, result,
+ "Settings extensions are not a valid JSON object");
+ return NULL;
+ }
+ yyjson_mut_val *copy = yyjson_val_mut_copy(target, root);
+ yyjson_doc_free(source);
+ if (!copy) {
+ *failure = BONGO_CAT_ERROR_MEMORY;
+ bongo_cat_error_set(error, BONGO_CAT_ERROR_MEMORY,
+ "Cannot allocate settings extensions");
+ }
+ return copy;
+}
+
+static BongoCatResult build_error(BongoCatError *error) {
+ bongo_cat_error_set(error, BONGO_CAT_ERROR_MEMORY,
+ "Cannot allocate settings JSON");
+ return BONGO_CAT_ERROR_MEMORY;
+}
+
+BongoCatResult bongo_cat_settings_save(const char *path,
+ const BongoCatSettings *settings, BongoCatError *error) {
+ if (!path || !settings) return BONGO_CAT_ERROR_ARGUMENT;
+ BongoCatSettings canonical = *settings;
+ bongo_cat_settings_validate(&canonical);
+ yyjson_mut_doc *doc = yyjson_mut_doc_new(NULL);
+ if (!doc) return build_error(error);
+ yyjson_mut_val *root = yyjson_mut_obj(doc);
+ if (!root) {
+ yyjson_mut_doc_free(doc);
+ return build_error(error);
+ }
+ yyjson_mut_doc_set_root(doc, root);
+ BongoCatResult extension_result = BONGO_CAT_OK;
+ yyjson_mut_val *extensions = write_extensions(
+ doc, &canonical, &extension_result, error);
+ if (!extensions) {
+ yyjson_mut_doc_free(doc);
+ return extension_result;
+ }
+ bool built = yyjson_mut_obj_add_strcpy(
+ doc, root, "format", SETTINGS_FORMAT) &&
+ yyjson_mut_obj_add_int(doc, root, "schemaVersion",
+ BONGO_CAT_SETTINGS_SCHEMA) &&
+ write_model(doc, yyjson_mut_obj_add_obj(doc, root, "rendering"),
+ &canonical.model) &&
+ write_window(doc, yyjson_mut_obj_add_obj(doc, root, "window"),
+ &canonical.window) &&
+ write_app(doc, yyjson_mut_obj_add_obj(doc, root, "application"),
+ &canonical.app) &&
+ write_shortcuts(doc, yyjson_mut_obj_add_obj(doc, root, "shortcuts"),
+ &canonical.shortcuts) &&
+ write_behaviors(doc, root, &canonical) &&
+ write_model_labels(doc, root, &canonical) &&
+ write_removed_models(doc, root, &canonical) &&
+ yyjson_mut_obj_add_val(doc, root, "extensions", extensions);
+ if (!built) {
+ yyjson_mut_doc_free(doc);
+ return build_error(error);
+ }
+ BongoCatResult result = bongo_cat_config_write_document(path, doc,
+ "settings file", error);
+ yyjson_mut_doc_free(doc);
+ return result;
+}
diff --git a/src/core/file.c b/src/core/file.c
index e299d6d9..bb3d0c77 100644
--- a/src/core/file.c
+++ b/src/core/file.c
@@ -21,6 +21,37 @@ FILE *bongo_cat_file_open(const char *path, const char *mode) {
free(path_wide); return file;
}
+bool bongo_cat_file_append(const char *path, const void *data, size_t size) {
+ if (!path || !path[0] || (!data && size)) return false;
+ wchar_t *wide = bongo_cat_windows_wide(path);
+ HANDLE file = wide ? CreateFileW(wide, GENERIC_WRITE,
+ FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, NULL,
+ OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL) : INVALID_HANDLE_VALUE;
+ free(wide);
+ if (file == INVALID_HANDLE_VALUE) return false;
+ OVERLAPPED lock = {0};
+ bool locked = LockFileEx(file, LOCKFILE_EXCLUSIVE_LOCK, 0,
+ MAXDWORD, MAXDWORD, &lock) != FALSE;
+ LARGE_INTEGER end = {0};
+ bool positioned = locked && SetFilePointerEx(file, end, NULL,
+ FILE_END) != FALSE;
+ const unsigned char *cursor = data;
+ size_t remaining = size;
+ bool written = positioned;
+ while (written && remaining) {
+ DWORD chunk = remaining > MAXDWORD ? MAXDWORD : (DWORD)remaining;
+ DWORD count = 0;
+ written = WriteFile(file, cursor, chunk, &count, NULL) != FALSE &&
+ count > 0;
+ cursor += count;
+ remaining -= count;
+ }
+ if (locked && !UnlockFileEx(file, 0, MAXDWORD, MAXDWORD, &lock))
+ written = false;
+ if (!CloseHandle(file)) written = false;
+ return written;
+}
+
bool bongo_cat_file_remove(const char *path) {
wchar_t *wide = bongo_cat_windows_wide(path);
bool removed = wide && _wremove(wide) == 0;
@@ -36,9 +67,38 @@ bool bongo_cat_file_replace(const char *source, const char *target, bool durable
free(source_wide); free(target_wide); return replaced;
}
#else
+#include
+#include
#include
+#include
+#include
FILE *bongo_cat_file_open(const char *path, const char *mode) { return fopen(path, mode); }
+
+static bool lock_file(int file, int operation) {
+ int result;
+ do result = flock(file, operation); while (result != 0 && errno == EINTR);
+ return result == 0;
+}
+
+bool bongo_cat_file_append(const char *path, const void *data, size_t size) {
+ if (!path || !path[0] || (!data && size)) return false;
+ int file = open(path, O_WRONLY | O_CREAT | O_APPEND, 0666);
+ if (file < 0) return false;
+ bool locked = lock_file(file, LOCK_EX);
+ const unsigned char *cursor = data;
+ size_t remaining = size;
+ bool written = locked;
+ while (written && remaining) {
+ ssize_t count = write(file, cursor, remaining);
+ if (count < 0 && errno == EINTR) continue;
+ if (count <= 0) written = false;
+ else { cursor += count; remaining -= (size_t)count; }
+ }
+ if (locked && !lock_file(file, LOCK_UN)) written = false;
+ if (close(file) != 0) written = false;
+ return written;
+}
bool bongo_cat_file_remove(const char *path) { return remove(path) == 0; }
bool bongo_cat_file_replace(const char *source, const char *target, bool durable) {
(void)durable; return rename(source, target) == 0;
diff --git a/src/core/input_state.c b/src/core/input_state.c
index 872d6b7f..4ab9508b 100644
--- a/src/core/input_state.c
+++ b/src/core/input_state.c
@@ -5,6 +5,7 @@
void bongo_cat_input_init(BongoCatInputState *state) {
if (!state) return;
+ atomic_init(&state->queue_busy, false);
memset(state->queue, 0, sizeof(state->queue));
atomic_init(&state->head, 0);
atomic_init(&state->tail, 0);
@@ -18,8 +19,8 @@ void bongo_cat_input_init(BongoCatInputState *state) {
memset(state->recovery, 0, sizeof(state->recovery));
atomic_init(&state->recovery_head, 0);
atomic_init(&state->recovery_tail, 0);
- memset(state->releases, 0, sizeof(state->releases));
- state->release_count = 0;
+ memset(state->scheduled_releases, 0, sizeof(state->scheduled_releases));
+ state->scheduled_release_count = 0;
}
bool bongo_cat_input_edge(bool states[BONGO_CAT_INPUT_KEY_STATE_CAP],
@@ -46,8 +47,7 @@ static void update_modifiers(BongoCatInputState *state, const BongoCatInputEvent
else atomic_fetch_and_explicit(value, (uint8_t)~mask, memory_order_release);
}
-bool bongo_cat_input_push(BongoCatInputState *state, const BongoCatInputEvent *event) {
- if (!state || !event) return false;
+static bool push_locked(BongoCatInputState *state, const BongoCatInputEvent *event) {
update_modifiers(state, event);
uint64_t sequence = atomic_fetch_add_explicit(&state->next_sequence, 1,
memory_order_relaxed);
@@ -77,8 +77,7 @@ bool bongo_cat_input_push(BongoCatInputState *state, const BongoCatInputEvent *e
return true;
}
-bool bongo_cat_input_pop(BongoCatInputState *state, BongoCatInputEvent *event) {
- if (!state || !event) return false;
+static bool pop_locked(BongoCatInputState *state, BongoCatInputEvent *event) {
uint16_t tail = (uint16_t)atomic_load_explicit(&state->tail, memory_order_relaxed);
uint16_t head = (uint16_t)atomic_load_explicit(&state->head, memory_order_acquire);
uint8_t recovery_tail = (uint8_t)atomic_load_explicit(
@@ -102,6 +101,29 @@ bool bongo_cat_input_pop(BongoCatInputState *state, BongoCatInputEvent *event) {
return true;
}
+/* Platform listeners and SDL/gamepad events can produce concurrently. Protect
+ both rings together so recovery releases remain ordered with normal events.
+ The critical section only copies fixed-size events; it never waits on I/O. */
+static void lock_queue(BongoCatInputState *state) {
+ while (atomic_exchange_explicit(&state->queue_busy, true, memory_order_acquire)) {}
+}
+
+bool bongo_cat_input_push(BongoCatInputState *state, const BongoCatInputEvent *event) {
+ if (!state || !event) return false;
+ lock_queue(state);
+ bool pushed = push_locked(state, event);
+ atomic_store_explicit(&state->queue_busy, false, memory_order_release);
+ return pushed;
+}
+
+bool bongo_cat_input_pop(BongoCatInputState *state, BongoCatInputEvent *event) {
+ if (!state || !event) return false;
+ lock_queue(state);
+ bool popped = pop_locked(state, event);
+ atomic_store_explicit(&state->queue_busy, false, memory_order_release);
+ return popped;
+}
+
bool bongo_cat_input_mouse(BongoCatInputState *state, double x, double y) {
if (!state) return false;
atomic_store_explicit(&state->mouse_x, x, memory_order_relaxed);
@@ -127,19 +149,20 @@ bool bongo_cat_input_shift_down(const BongoCatInputState *state) {
}
static size_t release_index(const BongoCatInputState *state, const char *name) {
- for (size_t i = 0; i < state->release_count; ++i)
- if (strcmp(state->releases[i].name, name) == 0) return i;
- return state->release_count;
+ for (size_t i = 0; i < state->scheduled_release_count; ++i)
+ if (strcmp(state->scheduled_releases[i].name, name) == 0) return i;
+ return state->scheduled_release_count;
}
static void remove_release(BongoCatInputState *state, size_t index) {
- if (index >= state->release_count) return;
- state->releases[index] = state->releases[state->release_count - 1];
- memset(&state->releases[--state->release_count], 0,
- sizeof(state->releases[0]));
+ if (index >= state->scheduled_release_count) return;
+ state->scheduled_releases[index] =
+ state->scheduled_releases[state->scheduled_release_count - 1];
+ memset(&state->scheduled_releases[--state->scheduled_release_count], 0,
+ sizeof(state->scheduled_releases[0]));
}
-void bongo_cat_input_auto_release(BongoCatInputState *state,
+void bongo_cat_input_schedule_release(BongoCatInputState *state,
const BongoCatInputEvent *event, uint64_t delay_ms) {
if (!state || !event || !event->name[0] ||
(event->kind != BONGO_CAT_INPUT_KEY_DOWN && event->kind != BONGO_CAT_INPUT_KEY_UP)) return;
@@ -148,24 +171,27 @@ void bongo_cat_input_auto_release(BongoCatInputState *state,
remove_release(state, index);
return;
}
- if (index == state->release_count) {
- if (state->release_count >= BONGO_CAT_AUTO_RELEASE_CAP) return;
- index = state->release_count++;
- snprintf(state->releases[index].name,
- sizeof(state->releases[index].name), "%s", event->name);
+ if (index == state->scheduled_release_count) {
+ if (state->scheduled_release_count >=
+ BONGO_CAT_SCHEDULED_RELEASE_CAP) return;
+ index = state->scheduled_release_count++;
+ snprintf(state->scheduled_releases[index].name,
+ sizeof(state->scheduled_releases[index].name), "%s", event->name);
}
- state->releases[index].deadline_ms = event->timestamp_ms + delay_ms;
+ state->scheduled_releases[index].deadline_ms =
+ event->timestamp_ms + delay_ms;
}
-bool bongo_cat_input_take_release(BongoCatInputState *state, uint64_t now_ms,
- BongoCatInputEvent *event) {
+bool bongo_cat_input_take_scheduled_release(BongoCatInputState *state,
+ uint64_t now_ms, BongoCatInputEvent *event) {
if (!state || !event) return false;
- for (size_t i = 0; i < state->release_count; ++i) {
- if (state->releases[i].deadline_ms > now_ms) continue;
+ for (size_t i = 0; i < state->scheduled_release_count; ++i) {
+ if (state->scheduled_releases[i].deadline_ms > now_ms) continue;
memset(event, 0, sizeof(*event));
event->kind = BONGO_CAT_INPUT_KEY_UP;
event->timestamp_ms = now_ms;
- snprintf(event->name, sizeof(event->name), "%s", state->releases[i].name);
+ snprintf(event->name, sizeof(event->name), "%s",
+ state->scheduled_releases[i].name);
remove_release(state, i);
return true;
}
diff --git a/src/core/model_behavior.c b/src/core/model_behavior.c
index 1ac255aa..50902e93 100644
--- a/src/core/model_behavior.c
+++ b/src/core/model_behavior.c
@@ -70,11 +70,11 @@ static bool read_expressions(BongoCatBehaviorCatalog *catalog,
return true;
}
-static bool read_mver_assets(BongoCatBehaviorCatalog *catalog,
+static bool read_adapter_assets(BongoCatBehaviorCatalog *catalog,
const BongoCatModelEntry *model) {
char path[BONGO_CAT_PATH_CAP];
- if (!bongo_cat_path_join(path, sizeof(path), model->adapter_directory,
- ".bongo-cat-mver.json")) return false;
+ if (!bongo_cat_model_adapter_metadata_path(model->adapter_directory,
+ path, sizeof(path))) return false;
yyjson_doc *document = bongo_cat_json_read_file(path, 0, NULL);
if (!document) return true;
yyjson_val *items = yyjson_obj_get(yyjson_doc_get_root(document), "bindings");
@@ -97,6 +97,7 @@ static bool read_mver_assets(BongoCatBehaviorCatalog *catalog,
model->adapter_directory)) {
ok = false; break;
}
+ catalog->entries[catalog->count - 1].sound_clear = sound;
continue;
}
const char *asset = yyjson_get_str(yyjson_obj_get(item, effect ? "effect" : "sound"));
@@ -109,8 +110,11 @@ static bool read_mver_assets(BongoCatBehaviorCatalog *catalog,
if (!add_behavior(catalog, model, effect ? BONGO_CAT_BEHAVIOR_EFFECT :
BONGO_CAT_BEHAVIOR_SOUND, NULL, current, label, asset,
model->adapter_directory)) { ok = false; break; }
- catalog->entries[catalog->count - 1].momentary =
- yyjson_get_bool(yyjson_obj_get(item, "momentary"));
+ BongoCatBehaviorEntry *entry = &catalog->entries[catalog->count - 1];
+ bool momentary = yyjson_get_bool(yyjson_obj_get(item, "momentary"));
+ entry->momentary = effect && momentary;
+ yyjson_val *overlap = yyjson_obj_get(item, "overlap");
+ entry->sound_overlap = sound && (overlap ? yyjson_get_bool(overlap) : true);
}
yyjson_doc_free(document);
return ok;
@@ -123,19 +127,16 @@ BongoCatResult bongo_cat_behaviors_load(BongoCatBehaviorCatalog *catalog,
char path[BONGO_CAT_PATH_CAP];
if (!bongo_cat_path_join(path, sizeof(path), model->directory, model->setting_file))
return BONGO_CAT_ERROR_FORMAT;
- yyjson_read_err json_error = {0};
- FILE *file = bongo_cat_file_open(path, "rb");
- yyjson_doc *document = file ? yyjson_read_fp(file, 0, NULL, &json_error) : NULL;
- if (file) fclose(file);
+ yyjson_doc *document = bongo_cat_model_json_read(path, NULL);
if (!document) {
bongo_cat_error_set(error, BONGO_CAT_ERROR_FORMAT, "Cannot read model setting: %s",
- json_error.msg ? json_error.msg : "cannot open file");
+ path);
return BONGO_CAT_ERROR_FORMAT;
}
yyjson_val *references = yyjson_obj_get(yyjson_doc_get_root(document), "FileReferences");
bool ok = read_motions(catalog, model, yyjson_obj_get(references, "Motions")) &&
read_expressions(catalog, model, yyjson_obj_get(references, "Expressions")) &&
- read_mver_assets(catalog, model);
+ read_adapter_assets(catalog, model);
yyjson_doc_free(document);
if (!ok) {
bongo_cat_error_set(error, BONGO_CAT_ERROR_FORMAT, "Too many model behaviors");
diff --git a/src/core/model_catalog.c b/src/core/model_catalog.c
index 3065d324..03dbec95 100644
--- a/src/core/model_catalog.c
+++ b/src/core/model_catalog.c
@@ -1,11 +1,9 @@
#include "bongo_cat/file.h"
-#include "bongo_cat/json.h"
#include "bongo_cat/model.h"
#include "bongo_cat/path.h"
#include
#include
-#include
#ifdef _WIN32
#include "windows_utf8.h"
@@ -19,6 +17,13 @@ void bongo_cat_models_init(BongoCatModelCatalog *catalog) {
if (catalog) memset(catalog, 0, sizeof(*catalog));
}
+bool bongo_cat_model_adapter_metadata_path(const char *directory,
+ char *path, size_t capacity) {
+ return directory && path && capacity &&
+ bongo_cat_path_join(path, capacity, directory,
+ BONGO_CAT_MODEL_ADAPTER_FILE);
+}
+
static BongoCatModelMode infer_mode(const char *directory) {
char path[BONGO_CAT_PATH_CAP], stored[16] = {0};
bongo_cat_path_join(path, sizeof(path), directory, ".bongo-cat-mode");
@@ -41,79 +46,30 @@ static BongoCatModelMode infer_mode(const char *directory) {
return bongo_cat_path_is_dir(path) ? BONGO_CAT_MODE_KEYBOARD : BONGO_CAT_MODE_STANDARD;
}
-static bool safe_relative(const char *value) {
- if (!value || !value[0] || value[0] == '/' || value[0] == '\\' || strchr(value, ':'))
- return false;
- const char *cursor = value;
- while (*cursor) {
- while (*cursor == '/' || *cursor == '\\') cursor++;
- if (cursor[0] == '.' && cursor[1] == '.' &&
- (!cursor[2] || cursor[2] == '/' || cursor[2] == '\\')) return false;
- cursor = strpbrk(cursor, "/\\");
- if (!cursor) break;
- }
- return true;
-}
-
-static BongoCatModelMode stored_mode(const char *value) {
- if (value && strcmp(value, "keyboard") == 0) return BONGO_CAT_MODE_KEYBOARD;
- if (value && strcmp(value, "gamepad") == 0) return BONGO_CAT_MODE_GAMEPAD;
- return BONGO_CAT_MODE_STANDARD;
-}
-
-static bool add_package(BongoCatModelCatalog *catalog, const char *directory,
- bool preset, bool *handled) {
- char descriptor[BONGO_CAT_PATH_CAP];
- *handled = bongo_cat_path_join(descriptor, sizeof(descriptor), directory,
- ".bongo-cat-package.json") && bongo_cat_path_is_file(descriptor);
- if (!*handled) return true;
- yyjson_doc *document = bongo_cat_json_read_file(descriptor, 0, NULL);
- yyjson_val *root = document ? yyjson_doc_get_root(document) : NULL;
- const char *model = yyjson_get_str(yyjson_obj_get(root, "directory"));
- const char *adapter = yyjson_get_str(yyjson_obj_get(root, "adapter"));
- const char *setting = yyjson_get_str(yyjson_obj_get(root, "setting"));
- bool valid = yyjson_is_obj(root) &&
- yyjson_get_int(yyjson_obj_get(root, "schemaVersion")) == 1 &&
- safe_relative(model) && safe_relative(adapter) && safe_relative(setting) &&
- catalog->count < BONGO_CAT_MODEL_CAP;
- BongoCatModelEntry *entry = valid ? &catalog->entries[catalog->count] : NULL;
- if (entry) valid = bongo_cat_path_join(entry->directory,
- sizeof(entry->directory), directory, model) &&
- bongo_cat_path_join(entry->adapter_directory,
- sizeof(entry->adapter_directory), directory, adapter);
- char setting_path[BONGO_CAT_PATH_CAP];
- if (entry) valid = bongo_cat_path_join(setting_path, sizeof(setting_path),
- entry->directory, setting) && bongo_cat_path_is_file(setting_path) &&
- bongo_cat_path_is_dir(entry->adapter_directory);
- if (valid) {
- snprintf(entry->id, sizeof(entry->id), "%s", bongo_cat_path_name(directory));
- snprintf(entry->storage_directory, sizeof(entry->storage_directory), "%s", directory);
- snprintf(entry->setting_file, sizeof(entry->setting_file), "%s", setting);
- entry->mode = stored_mode(yyjson_get_str(yyjson_obj_get(root, "mode")));
- entry->preset = preset;
- catalog->count++;
- }
- yyjson_doc_free(document);
- return valid;
+static bool builtin_marker(const char *directory) {
+ char marker[BONGO_CAT_PATH_CAP];
+ return bongo_cat_path_join(marker, sizeof(marker), directory,
+ BONGO_CAT_MODEL_BUILTIN_MARKER) && bongo_cat_path_is_file(marker);
}
static bool add_model(BongoCatModelCatalog *catalog, const char *directory, bool preset) {
- bool handled = false;
- bool package_ok = add_package(catalog, directory, preset, &handled);
- if (handled) return package_ok;
- if (!package_ok) return false;
- if (!preset) return true;
+ if (!preset && !builtin_marker(directory)) return true;
if (catalog->count >= BONGO_CAT_MODEL_CAP) return false;
char setting[BONGO_CAT_PATH_CAP];
if (!bongo_cat_path_find_suffix(directory, ".model3.json", setting, sizeof(setting))) return true;
BongoCatModelEntry *entry = &catalog->entries[catalog->count++];
+ memset(entry, 0, sizeof(*entry));
snprintf(entry->id, sizeof(entry->id), "%s", bongo_cat_path_name(directory));
+ snprintf(entry->package_id, sizeof(entry->package_id), "%s", entry->id);
snprintf(entry->directory, sizeof(entry->directory), "%s", directory);
snprintf(entry->adapter_directory, sizeof(entry->adapter_directory), "%s", directory);
snprintf(entry->storage_directory, sizeof(entry->storage_directory), "%s", directory);
snprintf(entry->setting_file, sizeof(entry->setting_file), "%s", setting);
entry->mode = infer_mode(directory);
- entry->preset = preset;
+ entry->source_format = BONGO_CAT_MODEL_SOURCE_BUILTIN;
+ entry->capabilities = BONGO_CAT_MODEL_CAPABILITY_LIVE2D |
+ BONGO_CAT_MODEL_CAPABILITY_PREVIEW;
+ entry->preset = preset || builtin_marker(directory);
return true;
}
@@ -183,3 +139,19 @@ const BongoCatModelEntry *bongo_cat_models_find(const BongoCatModelCatalog *cata
}
return NULL;
}
+
+const char *bongo_cat_model_default_name(const BongoCatModelEntry *entry) {
+ if (!entry) return "model";
+ if (entry->display_name[0]) return entry->display_name;
+ if (!strcmp(entry->id, "standard")) return "Standard";
+ if (!strcmp(entry->id, "keyboard")) return "Keyboard";
+ if (!strcmp(entry->id, "gamepad")) return "Gamepad";
+ return entry->id;
+}
+
+const char *bongo_cat_model_name(const BongoCatSettings *settings,
+ const BongoCatModelEntry *entry) {
+ if (!entry) return "model";
+ const char *custom = bongo_cat_settings_model_label(settings, entry->id);
+ return custom && custom[0] ? custom : bongo_cat_model_default_name(entry);
+}
diff --git a/src/core/model_json.c b/src/core/model_json.c
new file mode 100644
index 00000000..6c19af74
--- /dev/null
+++ b/src/core/model_json.c
@@ -0,0 +1,61 @@
+#include "bongo_cat/json.h"
+#include "bongo_cat/file.h"
+#include "bongo_cat/path.h"
+
+#include
+#include
+
+static size_t skip_space(const char *data, size_t size, size_t pos) {
+ while (pos < size && (data[pos] == ' ' || data[pos] == '\t' ||
+ data[pos] == '\r' || data[pos] == '\n')) pos++;
+ return pos;
+}
+
+yyjson_doc *bongo_cat_model_json_parse(const char *data, size_t size,
+ bool *normalized) {
+ if (normalized) *normalized = false;
+ if (!data || !size || size > 4u * 1024u * 1024u) return NULL;
+ yyjson_doc *document = yyjson_read(data, size, 0);
+ if (document) return document;
+ yyjson_read_flag flags = YYJSON_READ_ALLOW_COMMENTS |
+ YYJSON_READ_ALLOW_TRAILING_COMMAS | YYJSON_READ_ALLOW_BOM;
+ yyjson_read_err error = {0};
+ document = yyjson_read_opts((char *)data, size, flags, NULL, &error);
+ if (!document && error.pos < size && data[error.pos] == ']') {
+ size_t second = skip_space(data, size, error.pos + 1);
+ size_t third = skip_space(data, size, second + 1);
+ /* Some hand-edited Mver manifests have one extra '] }' pair. */
+ if (second < size && third < size && data[second] == '}' && data[third] == '}') {
+ char *copy = malloc(size);
+ if (!copy) return NULL;
+ memcpy(copy, data, size);
+ copy[error.pos] = copy[second] = ' ';
+ document = yyjson_read_opts(copy, size, flags, NULL, NULL);
+ free(copy);
+ yyjson_val *root = document ? yyjson_doc_get_root(document) : NULL;
+ if (!yyjson_is_obj(root) ||
+ !yyjson_is_obj(yyjson_obj_get(root, "FileReferences"))) {
+ yyjson_doc_free(document);
+ document = NULL;
+ }
+ }
+ }
+ if (normalized) *normalized = document != NULL;
+ return document;
+}
+
+yyjson_doc *bongo_cat_model_json_read(const char *path, bool *normalized) {
+ if (normalized) *normalized = false;
+ uint64_t size;
+ if (!bongo_cat_path_file_size(path, &size) || !size ||
+ size > 4u * 1024u * 1024u) return NULL;
+ char *data = malloc((size_t)size);
+ if (!data) return NULL;
+ FILE *file = bongo_cat_file_open(path, "rb");
+ bool loaded = file && fread(data, 1, (size_t)size, file) == (size_t)size;
+ if (file && fclose(file) != 0) loaded = false;
+ yyjson_doc *document = loaded
+ ? bongo_cat_model_json_parse(data, (size_t)size, normalized) : NULL;
+ free(data);
+ return document;
+}
diff --git a/src/core/mouse_tracking.c b/src/core/mouse_tracking.c
index ed68f331..d8fa7ced 100644
--- a/src/core/mouse_tracking.c
+++ b/src/core/mouse_tracking.c
@@ -2,6 +2,11 @@
#include
+bool bongo_cat_mouse_sample_fresh(uint64_t now_ns, uint64_t sample_ns) {
+ return sample_ns && now_ns >= sample_ns &&
+ now_ns - sample_ns < 100000000ull;
+}
+
void bongo_cat_mouse_target(BongoCatMouseTracking *tracking, double x, double y) {
if (!tracking) return;
tracking->target_x = x;
@@ -36,16 +41,57 @@ bool bongo_cat_mouse_step(BongoCatMouseTracking *tracking, float delta_seconds,
return true;
}
+float bongo_cat_mouse_centered_ratio(double position, double center,
+ double minimum, double maximum) {
+ if (!isfinite(position) || !isfinite(center) || !isfinite(minimum) ||
+ !isfinite(maximum) || maximum <= minimum) return 0.5f;
+ if (center < minimum) center = minimum;
+ if (center > maximum) center = maximum;
+ if (position < center) {
+ double span = center - minimum;
+ if (span <= 0.0) return 0.5f;
+ double ratio = 0.5 * (position - minimum) / span;
+ return (float)(ratio < 0.0 ? 0.0 : ratio);
+ }
+ if (position > center) {
+ double span = maximum - center;
+ if (span <= 0.0) return 0.5f;
+ double ratio = 0.5 + 0.5 * (position - center) / span;
+ return (float)(ratio > 1.0 ? 1.0 : ratio);
+ }
+ return 0.5f;
+}
+
+static float clamp_pointer_ratio(float value) {
+ if (!isfinite(value)) return 0.5f;
+ if (value < 0.0f) return 0.0f;
+ if (value > 1.0f) return 1.0f;
+ return value;
+}
+
+void bongo_cat_mouse_drag_coordinates(float x_ratio, float y_ratio,
+ bool horizontal_mirror, float *x, float *y) {
+ float horizontal = clamp_pointer_ratio(x_ratio);
+ float vertical = clamp_pointer_ratio(y_ratio);
+ if (horizontal_mirror) horizontal = 1.0f - horizontal;
+ if (x) *x = 2.0f * horizontal - 1.0f;
+ if (y) *y = 1.0f - 2.0f * vertical;
+}
+
+static float parameter_from_normalized(float minimum, float maximum,
+ float normalized) {
+ if (!isfinite(minimum) || !isfinite(maximum) || maximum < minimum)
+ return 0.0f;
+ if (minimum <= 0.0f && maximum >= 0.0f)
+ return normalized >= 0.0f ? normalized * maximum :
+ -normalized * minimum;
+ return minimum + (normalized + 1.0f) * 0.5f * (maximum - minimum);
+}
+
float bongo_cat_mouse_parameter_value(float minimum, float maximum,
float x_ratio, float y_ratio, char axis, bool mirror) {
- float value;
- if (axis == 'Z') {
- float x = 1.0f - 2.0f * x_ratio;
- float y = 1.0f - 2.0f * y_ratio;
- value = x * y * minimum;
- } else {
- float ratio = axis == 'Y' ? y_ratio : x_ratio;
- value = maximum - ratio * (maximum - minimum);
- }
- return axis != 'Y' && mirror ? -value : value;
+ float x = 0.0f, y = 0.0f;
+ bongo_cat_mouse_drag_coordinates(x_ratio, y_ratio, mirror, &x, &y);
+ float normalized = axis == 'Y' ? y : axis == 'Z' ? -x * y : x;
+ return parameter_from_normalized(minimum, maximum, normalized);
}
diff --git a/src/core/path_io.c b/src/core/path_io.c
index 5dd763ac..1005144b 100644
--- a/src/core/path_io.c
+++ b/src/core/path_io.c
@@ -90,7 +90,8 @@ bool bongo_cat_path_copy_file(const char *source, const char *target) {
unsigned char buffer[8192]; bool ok = true; size_t count;
while ((count = fread(buffer, 1, sizeof(buffer), input)) > 0)
if (fwrite(buffer, 1, count, output) != count) { ok = false; break; }
- if (ferror(input) || fclose(input) != 0) ok = false;
+ if (ferror(input)) ok = false;
+ if (fclose(input) != 0) ok = false;
if (fclose(output) != 0) ok = false;
if (!ok) remove(target);
return ok;
diff --git a/src/core/sha256.c b/src/core/sha256.c
index dac0ded6..6f27bf99 100644
--- a/src/core/sha256.c
+++ b/src/core/sha256.c
@@ -92,7 +92,8 @@ BongoCatResult bongo_cat_sha256_file(const char *path, char output[65], BongoCat
Sha256 value; initialize(&value);
unsigned char buffer[8192]; size_t count;
while ((count = fread(buffer, 1, sizeof(buffer), file)) > 0) update(&value, buffer, count);
- bool ok = !ferror(file) && fclose(file) == 0;
+ bool ok = !ferror(file);
+ if (fclose(file) != 0) ok = false;
if (!ok) { bongo_cat_error_set(error, BONGO_CAT_ERROR_IO, "Cannot read file"); return BONGO_CAT_ERROR_IO; }
finish(&value, output); return BONGO_CAT_OK;
}
diff --git a/src/core/shortcut.c b/src/core/shortcut.c
index 3568c9ea..280ea7cf 100644
--- a/src/core/shortcut.c
+++ b/src/core/shortcut.c
@@ -58,6 +58,68 @@ static const char *canonical_key(const char *name, char output[16]) {
return name;
}
+static const char *display_key(const char *token, size_t length) {
+ static const struct { const char *name; const char *label; } names[] = {
+ {"Control", "Ctrl"}, {"Ctrl", "Ctrl"},
+#if defined(_WIN32)
+ {"Meta", "Win"}, {"Super", "Win"}, {"Command", "Win"},
+#elif defined(__APPLE__)
+ {"Meta", "Cmd"}, {"Super", "Cmd"}, {"Command", "Cmd"},
+#else
+ {"Meta", "Super"}, {"Command", "Super"},
+#endif
+ {"Escape", "Esc"}, {"Delete", "Del"}, {"Insert", "Ins"},
+ {"PageUp", "PgUp"}, {"PageDown", "PgDn"},
+ {"PrintScreen", "PrtSc"}, {"Return", "Enter"},
+ {"ArrowUp", "\xE2\x86\x91"}, {"ArrowDown", "\xE2\x86\x93"},
+ {"ArrowLeft", "\xE2\x86\x90"}, {"ArrowRight", "\xE2\x86\x92"},
+ {"UpArrow", "\xE2\x86\x91"}, {"DownArrow", "\xE2\x86\x93"},
+ {"LeftArrow", "\xE2\x86\x90"}, {"RightArrow", "\xE2\x86\x92"},
+ {"BackQuote", "`"}, {"Minus", "-"}, {"Equal", "="},
+ {"BracketLeft", "["}, {"BracketRight", "]"},
+ {"Backslash", "\\"}, {"Semicolon", ";"}, {"Quote", "'"},
+ {"Comma", ","}, {"Period", "."}, {"Slash", "/"},
+ {"Kp0", "Num 0"}, {"Kp1", "Num 1"}, {"Kp2", "Num 2"},
+ {"Kp3", "Num 3"}, {"Kp4", "Num 4"}, {"Kp5", "Num 5"},
+ {"Kp6", "Num 6"}, {"Kp7", "Num 7"}, {"Kp8", "Num 8"},
+ {"Kp9", "Num 9"}, {"KpMultiply", "Num *"}, {"KpPlus", "Num +"},
+ {"KpMinus", "Num -"}, {"KpDecimal", "Num ."}, {"KpDivide", "Num /"}
+ };
+ for (size_t i = 0; i < sizeof(names) / sizeof(names[0]); ++i) {
+ if (strlen(names[i].name) != length) continue;
+ size_t j = 0;
+ while (j < length && tolower((unsigned char)token[j]) ==
+ tolower((unsigned char)names[i].name[j])) ++j;
+ if (j == length) return names[i].label;
+ }
+ return NULL;
+}
+
+void bongo_cat_shortcut_format(const char *shortcut, char *output, size_t capacity) {
+ if (!output || !capacity) return;
+ output[0] = '\0';
+ if (!shortcut) return;
+ if (strncmp(shortcut, "Gamepad:", 8) == 0) {
+ snprintf(output, capacity, "%s", shortcut);
+ return;
+ }
+ size_t used = 0;
+ while (*shortcut && used < capacity - 1) {
+ const char *plus = strchr(shortcut, '+');
+ size_t length = plus ? (size_t)(plus - shortcut) : strlen(shortcut);
+ const char *label = display_key(shortcut, length);
+ const char *shown = label ? label : shortcut;
+ size_t count = label ? strlen(label) : length;
+ if (count > capacity - 1 - used) count = capacity - 1 - used;
+ memcpy(output + used, shown, count);
+ used += count;
+ if (!plus) break;
+ if (used < capacity - 1) output[used++] = '+';
+ shortcut = plus + 1;
+ }
+ output[used] = '\0';
+}
+
static bool modifier_token(const char *token, size_t length, bool *control,
bool *shift, bool *alt, bool *meta) {
char value[24];
diff --git a/src/core/sound_shortcut.c b/src/core/sound_shortcut.c
new file mode 100644
index 00000000..108b3239
--- /dev/null
+++ b/src/core/sound_shortcut.c
@@ -0,0 +1,84 @@
+#include "bongo_cat/sound_shortcut.h"
+
+#include
+#include
+#include
+
+static bool equal(const char *a, const char *b) {
+ while (*a && *b) {
+ if (tolower((unsigned char)*a++) != tolower((unsigned char)*b++)) return false;
+ }
+ return *a == *b;
+}
+
+static const char *canonical(const char *key) {
+ if ((strncmp(key, "Key", 3) == 0 || strncmp(key, "Num", 3) == 0) &&
+ key[3] && !key[4]) return key + 3;
+ if (strcmp(key, "UpArrow") == 0) return "ArrowUp";
+ if (strcmp(key, "DownArrow") == 0) return "ArrowDown";
+ if (strcmp(key, "LeftArrow") == 0) return "ArrowLeft";
+ if (strcmp(key, "RightArrow") == 0) return "ArrowRight";
+ if (strcmp(key, "Return") == 0) return "Enter";
+ return key;
+}
+
+static bool token_matches(const char *token, const char *held) {
+ if (equal(token, canonical(held))) return true;
+ if (equal(token, "Control") || equal(token, "Ctrl"))
+ return equal(held, "ControlLeft") || equal(held, "ControlRight");
+ if (equal(token, "Shift"))
+ return equal(held, "ShiftLeft") || equal(held, "ShiftRight");
+ if (equal(token, "Alt")) return equal(held, "Alt") || equal(held, "AltGr");
+ if (equal(token, "Super") || equal(token, "Command")) return equal(held, "Meta");
+ if (equal(token, "-")) return equal(held, "Minus");
+ if (equal(token, "=")) return equal(held, "Equal");
+ return false;
+}
+
+bool bongo_cat_sound_shortcut_update(BongoCatSoundShortcutState *state,
+ const BongoCatInputEvent *event) {
+ if (!state || !event || !event->name[0]) return false;
+ bool down;
+ char name[BONGO_CAT_ID_CAP];
+ if (event->kind == BONGO_CAT_INPUT_GAMEPAD_BUTTON) {
+ down = event->value > 0.5f;
+ if (strlen(event->name) + 8 >= sizeof(name)) return false;
+ snprintf(name, sizeof(name), "Gamepad:%s", event->name);
+ } else {
+ if (event->kind != BONGO_CAT_INPUT_KEY_DOWN && event->kind != BONGO_CAT_INPUT_KEY_UP &&
+ event->kind != BONGO_CAT_INPUT_MOUSE_DOWN && event->kind != BONGO_CAT_INPUT_MOUSE_UP)
+ return false;
+ down = event->kind == BONGO_CAT_INPUT_KEY_DOWN || event->kind == BONGO_CAT_INPUT_MOUSE_DOWN;
+ snprintf(name, sizeof(name), "%s", event->name);
+ }
+ for (size_t i = 0; i < state->count; ++i) {
+ if (strcmp(state->held[i], name) != 0) continue;
+ if (down) return false;
+ --state->count;
+ if (i != state->count) memcpy(state->held[i], state->held[state->count], sizeof(name));
+ return true;
+ }
+ if (!down || state->count >= BONGO_CAT_INPUT_KEY_STATE_CAP) return false;
+ snprintf(state->held[state->count++], sizeof(name), "%s", name);
+ return true;
+}
+
+bool bongo_cat_sound_shortcut_down(const BongoCatSoundShortcutState *state,
+ const char *shortcut) {
+ if (!state || !shortcut || !*shortcut) return false;
+ const char *cursor = shortcut;
+ while (*cursor) {
+ const char *plus = strchr(cursor, '+');
+ size_t length = plus ? (size_t)(plus - cursor) : strlen(cursor);
+ char token[BONGO_CAT_ID_CAP];
+ if (!length || length >= sizeof(token)) return false;
+ memcpy(token, cursor, length); token[length] = '\0';
+ bool found = false;
+ for (size_t i = 0; i < state->count && !found; ++i)
+ found = token_matches(token, state->held[i]);
+ if (!found) return false;
+ if (!plus) return true;
+ cursor = plus + 1;
+ }
+ return false;
+}
diff --git a/src/core/update_release.c b/src/core/update_release.c
new file mode 100644
index 00000000..6e9c7590
--- /dev/null
+++ b/src/core/update_release.c
@@ -0,0 +1,119 @@
+#include "bongo_cat/update.h"
+
+#include
+#include
+#include
+
+#define RELEASE_PREFIX \
+ "https://github.com/vladelaina/BongoCat/releases/tag/"
+#define DOWNLOAD_PREFIX \
+ "https://github.com/vladelaina/BongoCat/releases/download/"
+
+static bool copy_text(char *target, size_t capacity, yyjson_val *value,
+ bool required, bool truncate) {
+ if (!value || yyjson_is_null(value)) return !required;
+ if (!yyjson_is_str(value)) return false;
+ const char *text = yyjson_get_str(value);
+ size_t length = yyjson_get_len(value);
+ if (!text || strlen(text) != length || (required && !length)) return false;
+ if (length >= capacity) {
+ if (!truncate) return false;
+ length = capacity - 1;
+ }
+ memcpy(target, text, length);
+ target[length] = '\0';
+ return true;
+}
+
+static bool safe_url(const char *url, const char *prefix) {
+ if (!url || strncmp(url, prefix, strlen(prefix)) != 0) return false;
+ for (const unsigned char *at = (const unsigned char *)url; *at; ++at)
+ if (*at <= 0x20 || *at == '\\') return false;
+ return true;
+}
+
+static bool copy_asset_url(yyjson_val *asset, const char *expected,
+ char *target, size_t capacity) {
+ yyjson_val *name_value = yyjson_obj_get(asset, "name");
+ yyjson_val *url_value = yyjson_obj_get(asset, "browser_download_url");
+ if (!yyjson_is_str(name_value) || !yyjson_is_str(url_value) ||
+ strcmp(yyjson_get_str(name_value), expected) != 0) return false;
+ const char *url = yyjson_get_str(url_value);
+ const char *filename = url ? strrchr(url, '/') : NULL;
+ if (!safe_url(url, DOWNLOAD_PREFIX) || !filename ||
+ strcmp(filename + 1, expected) != 0 || strlen(url) >= capacity)
+ return false;
+ snprintf(target, capacity, "%s", url);
+ return true;
+}
+
+static bool read_assets(yyjson_val *root, const char *platform,
+ BongoCatUpdateRelease *release) {
+ yyjson_val *assets = yyjson_obj_get(root, "assets");
+ if (!yyjson_is_arr(assets)) return false;
+ char installer[128] = {0}, portable[128] = {0};
+ int installer_length = 0, portable_length = 0;
+ if (strncmp(platform, "windows-", 8) == 0) {
+ installer_length = snprintf(installer, sizeof(installer),
+ "BongoCat-%s-%s-setup.exe", release->version, platform);
+ portable_length = snprintf(portable, sizeof(portable),
+ "BongoCat-%s-%s-portable.exe", release->version, platform);
+ } else if (strcmp(platform, "linux-x64") == 0) {
+ portable_length = snprintf(portable, sizeof(portable),
+ "BongoCat-%s-%s.tar.gz", release->version, platform);
+ } else if (strcmp(platform, "macos-x64") == 0 ||
+ strcmp(platform, "macos-arm64") == 0) {
+ portable_length = snprintf(portable, sizeof(portable),
+ "BongoCat-%s-%s.zip", release->version, platform);
+ } else return false;
+ if (installer_length < 0 || (size_t)installer_length >= sizeof(installer) ||
+ portable_length < 0 || (size_t)portable_length >= sizeof(portable))
+ return false;
+ size_t index, count;
+ yyjson_val *asset;
+ yyjson_arr_foreach(assets, index, count, asset) {
+ if (!yyjson_is_obj(asset)) continue;
+ if (installer[0] && !release->installer_url[0])
+ copy_asset_url(asset, installer, release->installer_url,
+ sizeof(release->installer_url));
+ if (!release->portable_url[0]) copy_asset_url(asset, portable,
+ release->portable_url, sizeof(release->portable_url));
+ }
+ return true;
+}
+
+bool bongo_cat_update_parse_release(const char *json, const char *platform,
+ BongoCatUpdateRelease *release, BongoCatError *error) {
+ if (!json || !platform || !release) return false;
+ memset(release, 0, sizeof(*release));
+ yyjson_doc *document = yyjson_read(json, strlen(json), 0);
+ yyjson_val *root = document ? yyjson_doc_get_root(document) : NULL;
+ yyjson_val *draft = yyjson_is_obj(root)
+ ? yyjson_obj_get(root, "draft") : NULL;
+ yyjson_val *prerelease = yyjson_is_obj(root)
+ ? yyjson_obj_get(root, "prerelease") : NULL;
+ bool valid = yyjson_is_obj(root) && yyjson_is_bool(draft) &&
+ yyjson_is_bool(prerelease) && !yyjson_get_bool(draft) &&
+ !yyjson_get_bool(prerelease);
+ char tag[BONGO_CAT_UPDATE_VERSION_CAP + 1] = {0};
+ if (valid) valid = copy_text(tag, sizeof(tag),
+ yyjson_obj_get(root, "tag_name"), true, false);
+ const char *version = tag[0] == 'v' || tag[0] == 'V' ? tag + 1 : tag;
+ if (valid) valid = bongo_cat_update_version_valid(version);
+ if (valid) snprintf(release->version, sizeof(release->version), "%s",
+ version);
+ if (valid) valid = copy_text(release->release_url,
+ sizeof(release->release_url), yyjson_obj_get(root, "html_url"), true,
+ false) &&
+ safe_url(release->release_url, RELEASE_PREFIX);
+ if (valid) {
+ yyjson_val *body = yyjson_obj_get(root, "body");
+ if (body && !copy_text(release->notes, sizeof(release->notes),
+ body, false, true)) valid = false;
+ }
+ if (valid) valid = read_assets(root, platform, release);
+ yyjson_doc_free(document);
+ if (!valid) bongo_cat_error_set(error, BONGO_CAT_ERROR_FORMAT,
+ "GitHub returned invalid BongoCat release metadata");
+ return valid;
+}
diff --git a/src/core/update_version.c b/src/core/update_version.c
new file mode 100644
index 00000000..672e16c9
--- /dev/null
+++ b/src/core/update_version.c
@@ -0,0 +1,138 @@
+#include "bongo_cat/update.h"
+
+#include
+#include
+#include
+
+typedef struct ParsedVersion {
+ uint64_t core[3];
+ const char *prerelease;
+ size_t prerelease_length;
+ bool valid;
+} ParsedVersion;
+
+static bool identifier_character(char value) {
+ unsigned char byte = (unsigned char)value;
+ return isalnum(byte) || value == '-';
+}
+
+static bool parse_number(const char **cursor, uint64_t *number) {
+ const char *start = *cursor;
+ if (!isdigit((unsigned char)*start)) return false;
+ if (*start == '0' && isdigit((unsigned char)start[1])) return false;
+ uint64_t value = 0;
+ while (isdigit((unsigned char)**cursor)) {
+ unsigned digit = (unsigned)(**cursor - '0');
+ if (value > (UINT64_MAX - digit) / 10) return false;
+ value = value * 10 + digit;
+ (*cursor)++;
+ }
+ *number = value;
+ return true;
+}
+
+static bool validate_identifiers(const char *value, size_t length) {
+ if (!length) return false;
+ size_t identifier_start = 0;
+ for (size_t i = 0; i <= length; ++i) {
+ if (i < length && value[i] != '.') {
+ if (!identifier_character(value[i])) return false;
+ continue;
+ }
+ if (i == identifier_start) return false;
+ bool numeric = true;
+ for (size_t j = identifier_start; j < i; ++j)
+ if (!isdigit((unsigned char)value[j])) numeric = false;
+ if (numeric && i - identifier_start > 1 &&
+ value[identifier_start] == '0') return false;
+ identifier_start = i + 1;
+ }
+ return true;
+}
+
+static ParsedVersion parse_version(const char *text) {
+ ParsedVersion parsed = {0};
+ if (!text || !text[0]) return parsed;
+ const char *cursor = text;
+ if (*cursor == 'v' || *cursor == 'V') cursor++;
+ for (size_t i = 0; i < 3; ++i) {
+ if (!parse_number(&cursor, &parsed.core[i])) return parsed;
+ if (i < 2) {
+ if (*cursor != '.') return parsed;
+ cursor++;
+ }
+ }
+ if (*cursor == '-') {
+ const char *start = ++cursor;
+ while (*cursor && *cursor != '+') cursor++;
+ parsed.prerelease = start;
+ parsed.prerelease_length = (size_t)(cursor - start);
+ if (!validate_identifiers(start, parsed.prerelease_length))
+ return (ParsedVersion){0};
+ }
+ if (*cursor == '+') {
+ const char *start = ++cursor;
+ while (*cursor) cursor++;
+ if (!validate_identifiers(start, (size_t)(cursor - start)))
+ return (ParsedVersion){0};
+ }
+ parsed.valid = *cursor == '\0';
+ return parsed;
+}
+
+static int compare_identifier(const char *left, size_t left_length,
+ const char *right, size_t right_length) {
+ bool left_numeric = true, right_numeric = true;
+ for (size_t i = 0; i < left_length; ++i)
+ if (!isdigit((unsigned char)left[i])) left_numeric = false;
+ for (size_t i = 0; i < right_length; ++i)
+ if (!isdigit((unsigned char)right[i])) right_numeric = false;
+ if (left_numeric != right_numeric) return left_numeric ? -1 : 1;
+ if (left_numeric && left_length != right_length)
+ return left_length < right_length ? -1 : 1;
+ size_t common = left_length < right_length ? left_length : right_length;
+ int compared = memcmp(left, right, common);
+ if (compared) return compared < 0 ? -1 : 1;
+ return left_length == right_length ? 0 : left_length < right_length ? -1 : 1;
+}
+
+static int compare_prerelease(const ParsedVersion *left,
+ const ParsedVersion *right) {
+ if (!left->prerelease_length || !right->prerelease_length) {
+ if (left->prerelease_length == right->prerelease_length) return 0;
+ return left->prerelease_length ? -1 : 1;
+ }
+ size_t left_at = 0, right_at = 0;
+ while (left_at < left->prerelease_length &&
+ right_at < right->prerelease_length) {
+ size_t left_end = left_at, right_end = right_at;
+ while (left_end < left->prerelease_length &&
+ left->prerelease[left_end] != '.') left_end++;
+ while (right_end < right->prerelease_length &&
+ right->prerelease[right_end] != '.') right_end++;
+ int compared = compare_identifier(left->prerelease + left_at,
+ left_end - left_at, right->prerelease + right_at,
+ right_end - right_at);
+ if (compared) return compared;
+ left_at = left_end + 1;
+ right_at = right_end + 1;
+ }
+ bool left_done = left_at > left->prerelease_length;
+ bool right_done = right_at > right->prerelease_length;
+ return left_done == right_done ? 0 : left_done ? -1 : 1;
+}
+
+int bongo_cat_update_compare_versions(const char *left, const char *right) {
+ ParsedVersion parsed_left = parse_version(left);
+ ParsedVersion parsed_right = parse_version(right);
+ if (!parsed_left.valid || !parsed_right.valid) return 0;
+ for (size_t i = 0; i < 3; ++i) {
+ if (parsed_left.core[i] == parsed_right.core[i]) continue;
+ return parsed_left.core[i] < parsed_right.core[i] ? -1 : 1;
+ }
+ return compare_prerelease(&parsed_left, &parsed_right);
+}
+
+bool bongo_cat_update_version_valid(const char *version) {
+ return parse_version(version).valid;
+}
diff --git a/src/core/utf8.c b/src/core/utf8.c
index dfdc6b3b..e05440d3 100644
--- a/src/core/utf8.c
+++ b/src/core/utf8.c
@@ -45,7 +45,7 @@ bool bongo_cat_utf8_valid(const char *text) {
return true;
}
-bool bongo_cat_utf8_normalize_legacy(const char *text,
+bool bongo_cat_utf8_normalize_mver(const char *text,
char *output, size_t capacity) {
if (!text || !output || !capacity) return false;
size_t length = strlen(text);
diff --git a/src/live2d/cubism_bridge.cpp b/src/live2d/cubism_bridge.cpp
index 3f86f2e3..d2257b69 100644
--- a/src/live2d/cubism_bridge.cpp
+++ b/src/live2d/cubism_bridge.cpp
@@ -1,26 +1,24 @@
#include "bongo_cat/file.h"
-#include "bongo_cat/memory.h"
#include "bongo_cat/model.h"
-#if defined(CSM_TARGET_WIN_GL) || defined(CSM_TARGET_LINUX_GL)
+#if defined(CSM_TARGET_WIN_GL) || defined(CSM_TARGET_LINUX_GL) || defined(CSM_TARGET_MAC_GL)
#include
#endif
-#include "cubism_model.hpp"
+#include "cubism_runtime.hpp"
#include
#include
+#include
+#include
#include
#include
-#include
#include
#ifdef _WIN32
#include
#endif
-
using Csm::CubismFramework;
namespace {
-
class Allocator final : public Csm::ICubismAllocator {
public:
void *Allocate(const Csm::csmSizeType size) override { return std::malloc(size); }
@@ -78,7 +76,7 @@ void release_file(Csm::csmByte *bytes) { std::free(bytes); }
bool start_framework(BongoCatError *error) {
if (runtime_count++) return true;
-#if defined(CSM_TARGET_WIN_GL) || defined(CSM_TARGET_LINUX_GL)
+#if defined(CSM_TARGET_WIN_GL) || defined(CSM_TARGET_LINUX_GL) || defined(CSM_TARGET_MAC_GL)
glewExperimental = GL_TRUE;
GLenum glew_result = glewInit();
glGetError();
@@ -89,7 +87,10 @@ bool start_framework(BongoCatError *error) {
return false;
}
if (!glCreateShader || !glShaderSource || !glCompileShader ||
- !glGetShaderiv || !glCreateProgram || !glGenFramebuffers) {
+ !glGetShaderiv || !glCreateProgram || !glGenFramebuffers ||
+ !glGenBuffers || !glBindBuffer || !glBufferData || !glDeleteBuffers ||
+ !glGenVertexArrays || !glBindVertexArray || !glDeleteVertexArrays ||
+ !glEnableVertexAttribArray || !glVertexAttribPointer) {
runtime_count = 0;
bongo_cat_error_set(error, BONGO_CAT_ERROR_PLATFORM,
"Required OpenGL 3.3 functions are unavailable");
@@ -119,43 +120,7 @@ void stop_framework() {
} // namespace
-struct BongoCatLive2D {
- bongo_cat::NativeModel *model;
- int width = 612;
- int height = 354;
-};
-
-static bool restore_previous(BongoCatLive2D *runtime,
- bongo_cat::NativeModel *previous, BongoCatError *primary) noexcept {
- if (!previous) { runtime->model = nullptr; return true; }
- BongoCatError restore_error = {};
- try {
- if (previous->load_textures(&restore_error)) {
- runtime->model = previous;
- return true;
- }
- } catch (const std::bad_alloc &) {
- bongo_cat_error_set(&restore_error, BONGO_CAT_ERROR_MEMORY,
- "Out of memory while restoring the previous model");
- } catch (const std::exception &exception) {
- bongo_cat_error_set(&restore_error, BONGO_CAT_ERROR_CUBISM,
- "Previous model restore failed: %s", exception.what());
- } catch (...) {
- bongo_cat_error_set(&restore_error, BONGO_CAT_ERROR_CUBISM,
- "Previous model restore failed with an unknown exception");
- }
- if (primary) {
- char original[sizeof(primary->message)];
- std::snprintf(original, sizeof(original), "%s", primary->message);
- bongo_cat_error_set(primary, primary->code,
- "%s; restore failed: %s", original, restore_error.message);
- }
- delete previous;
- runtime->model = nullptr;
- return false;
-}
-
-extern "C" BongoCatLive2D *bongo_cat_live2d_create(const char *asset_root,
+static BongoCatLive2D *create_runtime(const char *asset_root,
BongoCatError *error) {
resource_root = asset_root ? asset_root : "";
if (!start_framework(error)) return nullptr;
@@ -167,76 +132,44 @@ extern "C" BongoCatLive2D *bongo_cat_live2d_create(const char *asset_root,
return runtime;
}
+extern "C" BongoCatLive2D *bongo_cat_live2d_create(const char *asset_root,
+ BongoCatError *error) {
+ return create_runtime(asset_root, error);
+}
+
extern "C" void bongo_cat_live2d_destroy(BongoCatLive2D *runtime) {
if (!runtime) return;
+ if (runtime->retired_count) SDL_Log("[runtime] Live2D resource handoff: "
+ "stage=retirement-flush queue=%u current_context=%p",
+ runtime->retired_count, (void *)SDL_GL_GetCurrentContext());
+ for (unsigned i = 0; i < runtime->retired_count; ++i)
+ delete runtime->retired[i].model;
delete runtime->model;
delete runtime;
stop_framework();
}
-extern "C" BongoCatResult bongo_cat_live2d_load(BongoCatLive2D *runtime,
- const char *directory, const char *setting, bool preset,
- const BongoCatLive2DRenderOptions *render_options, BongoCatError *error) {
- if (!runtime) return BONGO_CAT_ERROR_ARGUMENT;
- bongo_cat::NativeModel *previous = runtime->model;
- bongo_cat::NativeModel *model = nullptr;
- bool previous_released = false;
- try {
- model = new(std::nothrow) bongo_cat::NativeModel();
- if (!model) {
- bongo_cat_error_set(error, BONGO_CAT_ERROR_MEMORY,
- "Cannot allocate Live2D model");
- return BONGO_CAT_ERROR_MEMORY;
- }
- if (render_options) model->set_render_options(*render_options);
- if (!model->load(directory, setting, preset, error)) {
- delete model;
- return error ? error->code : BONGO_CAT_ERROR_CUBISM;
- }
- if (previous) {
- previous_released = true;
- previous->release_render_resources();
- glFinish();
- }
- model->reshape(runtime->width, runtime->height);
- if (!model->load_textures(error)) {
- BongoCatResult result = error ? error->code : BONGO_CAT_ERROR_CUBISM;
- delete model;
- glFinish();
- restore_previous(runtime, previous, error);
- glFinish();
- bongo_cat_platform_trim_memory();
- return result;
- }
- delete previous;
- runtime->model = model;
- glFinish();
- bongo_cat_platform_trim_memory();
- return BONGO_CAT_OK;
- } catch (const std::bad_alloc &) {
- bongo_cat_error_set(error, BONGO_CAT_ERROR_MEMORY,
- "Out of memory while loading the Live2D model");
- } catch (const std::exception &exception) {
- bongo_cat_error_set(error, BONGO_CAT_ERROR_CUBISM,
- "Live2D model load failed: %s", exception.what());
- } catch (...) {
- bongo_cat_error_set(error, BONGO_CAT_ERROR_CUBISM,
- "Live2D model load failed with an unknown exception");
- }
- delete model;
- if (previous_released) {
- glFinish();
- restore_previous(runtime, previous, error);
- glFinish();
- bongo_cat_platform_trim_memory();
- }
- return error ? error->code : BONGO_CAT_ERROR_CUBISM;
-}
-
extern "C" bool bongo_cat_live2d_ready(const BongoCatLive2D *runtime) {
return runtime && runtime->model;
}
+extern "C" bool bongo_cat_live2d_canvas_size(const BongoCatLive2D *runtime,
+ int *width, int *height) {
+ return runtime && runtime->model &&
+ runtime->model->canvas_size(width, height);
+}
+
+extern "C" bool bongo_cat_live2d_frame(const BongoCatLive2D *runtime,
+ BongoCatLive2DFrame *frame) {
+ return runtime && runtime->model && runtime->model->frame(frame);
+}
+
+extern "C" bool bongo_cat_live2d_viewport(const BongoCatLive2D *runtime,
+ int *x, int *y, int *width, int *height) {
+ return runtime && runtime->model &&
+ runtime->model->viewport(x, y, width, height);
+}
+
extern "C" void bongo_cat_live2d_resize(BongoCatLive2D *runtime, int width, int height) {
if (!runtime) return;
if (width > 0 && height > 0) {
@@ -254,26 +187,49 @@ extern "C" void bongo_cat_live2d_reshape(BongoCatLive2D *runtime, int width, int
if (runtime->model) runtime->model->reshape(width, height);
}
extern "C" bool bongo_cat_live2d_update(BongoCatLive2D *runtime, float elapsed) {
- return runtime && runtime->model && runtime->model->update(elapsed);
+ if (!runtime) return false;
+ bool changed = runtime->model && runtime->model->update(elapsed);
+ // Finish deferred releases even when the replacement model is static.
+ return changed || runtime->retired_count > 0;
}
extern "C" void bongo_cat_live2d_draw(BongoCatLive2D *runtime) {
- if (runtime && runtime->model) runtime->model->draw();
+ if (!runtime) return;
+ unsigned keep = 0, released = 0;
+ for (unsigned i = 0; i < runtime->retired_count; ++i) {
+ BongoCatRetiredModel item = runtime->retired[i];
+ if (item.frames_remaining) item.frames_remaining--;
+ if (!item.frames_remaining) {
+ delete item.model;
+ released++;
+ } else runtime->retired[keep++] = item;
+ }
+ runtime->retired_count = keep;
+ if (released) SDL_Log("[runtime] Live2D resource handoff: "
+ "stage=retirement-complete released=%u queue=%u current_context=%p "
+ "gl_error=0x%x", released, keep, (void *)SDL_GL_GetCurrentContext(),
+ (unsigned)glGetError());
+ if (runtime->model) runtime->model->draw();
}
extern "C" void bongo_cat_live2d_set_mirror(BongoCatLive2D *runtime, bool mirror) {
- if (runtime && runtime->model) runtime->model->set_mirror(mirror);
-}
+ if (runtime && runtime->model) runtime->model->set_mirror(mirror); }
extern "C" void bongo_cat_live2d_set_render_options(BongoCatLive2D *runtime,
const BongoCatLive2DRenderOptions *options) {
if (runtime && runtime->model && options)
- runtime->model->set_render_options(*options);
-}
+ runtime->model->set_render_options(*options); }
extern "C" void bongo_cat_live2d_set_dragging(BongoCatLive2D *runtime,
float x, float y) {
- if (runtime && runtime->model) runtime->model->set_dragging(x, y);
-}
+ if (runtime && runtime->model) runtime->model->set_dragging(x, y); }
+extern "C" void bongo_cat_live2d_set_centered_dragging(BongoCatLive2D *runtime,
+ float x, float y) {
+ if (runtime && runtime->model) runtime->model->set_dragging(x, y, true); }
extern "C" void bongo_cat_live2d_prepare_viewer_audit(BongoCatLive2D *runtime) {
if (runtime && runtime->model) runtime->model->prepare_viewer_audit();
}
+extern "C" bool bongo_cat_live2d_prepare_cover_capture(
+ BongoCatLive2D *runtime) {
+ return runtime && runtime->model &&
+ runtime->model->prepare_cover_capture();
+}
extern "C" bool bongo_cat_live2d_set_parameter(BongoCatLive2D *runtime, const char *id, float value) {
return runtime && runtime->model && runtime->model->set_parameter(id, value);
}
@@ -285,13 +241,29 @@ extern "C" bool bongo_cat_live2d_parameter(BongoCatLive2D *runtime, const char *
extern "C" bool bongo_cat_live2d_start_motion(BongoCatLive2D *runtime, const char *group, int index) {
return runtime && runtime->model && runtime->model->start_motion(group, index);
}
-extern "C" bool bongo_cat_live2d_set_expression(BongoCatLive2D *runtime, int index) {
- return runtime && runtime->model && runtime->model->set_expression(index);
+extern "C" bool bongo_cat_live2d_restore_motion_state(BongoCatLive2D *runtime,
+ const char *group, int index) {
+ return runtime && runtime->model &&
+ runtime->model->restore_motion_state(group, index);
}
+extern "C" bool bongo_cat_live2d_preview_motion(BongoCatLive2D *runtime, const char *group, int index) {
+ return runtime && runtime->model && runtime->model->preview_motion(group, index); }
+extern "C" bool bongo_cat_live2d_restore_motion_preview(BongoCatLive2D *runtime) { return runtime && runtime->model && runtime->model->restore_motion_preview(); }
+extern "C" bool bongo_cat_live2d_commit_motion_preview(BongoCatLive2D *runtime, const char *group, int index) {
+ return runtime && runtime->model && runtime->model->commit_motion_preview(group, index); }
+extern "C" bool bongo_cat_live2d_motion_selected(const BongoCatLive2D *runtime, const char *group, int index) { return runtime && runtime->model && runtime->model->motion_selected(group, index); }
+extern "C" bool bongo_cat_live2d_motion_persistent(const BongoCatLive2D *runtime,
+ const char *group, int index) {
+ return runtime && runtime->model &&
+ runtime->model->motion_persistent(group, index); }
+extern "C" bool bongo_cat_live2d_motion_visible(const BongoCatLive2D *runtime, const char *group, int index) {
+ return runtime && runtime->model && runtime->model->motion_visible(group, index); }
+extern "C" bool bongo_cat_live2d_motion_same_toggle(
+ const BongoCatLive2D *runtime, const char *left_group, int left_index,
+ const char *right_group, int right_index) {
+ return runtime && runtime->model && runtime->model->motion_same_toggle(
+ left_group, left_index, right_group, right_index); }
+extern "C" bool bongo_cat_live2d_set_expression(BongoCatLive2D *runtime, int index) {
+ return runtime && runtime->model && runtime->model->set_expression(index); }
extern "C" int bongo_cat_live2d_expression(const BongoCatLive2D *runtime) {
- return runtime && runtime->model ? runtime->model->expression() : -1;
-}
-extern "C" bool bongo_cat_live2d_visual_state(const BongoCatLive2D *runtime,
- BongoCatLive2DVisualState *state) {
- return runtime && runtime->model && runtime->model->visual_state(state);
-}
+ return runtime && runtime->model ? runtime->model->expression() : -1; }
diff --git a/src/live2d/cubism_bridge_load.cpp b/src/live2d/cubism_bridge_load.cpp
new file mode 100644
index 00000000..1df2ea95
--- /dev/null
+++ b/src/live2d/cubism_bridge_load.cpp
@@ -0,0 +1,89 @@
+#include "bongo_cat/model.h"
+#if defined(CSM_TARGET_WIN_GL) || defined(CSM_TARGET_LINUX_GL)
+#include
+#endif
+#include "cubism_runtime.hpp"
+
+#include
+#include
+#include
+#include
+
+static void retire_previous(BongoCatLive2D *runtime,
+ bongo_cat::NativeModel *previous) {
+ if (!runtime || !previous) return;
+ if (runtime->retired_count == runtime->retired_capacity) {
+ SDL_LogWarn(SDL_LOG_CATEGORY_VIDEO,
+ "Live2D retirement queue reached its limit; releasing the oldest model");
+ delete runtime->retired[0].model;
+ for (unsigned i = 1; i < runtime->retired_count; ++i)
+ runtime->retired[i - 1] = runtime->retired[i];
+ runtime->retired_count--;
+ }
+ BongoCatRetiredModel *slot = &runtime->retired[runtime->retired_count++];
+ slot->model = previous;
+ slot->frames_remaining = 3;
+}
+
+extern "C" BongoCatResult bongo_cat_live2d_load(BongoCatLive2D *runtime,
+ const char *directory, const char *setting, bool preset,
+ const BongoCatLive2DRenderOptions *render_options,
+ BongoCatLive2DLoadProgress progress, void *userdata,
+ BongoCatError *error) {
+ if (!runtime) return BONGO_CAT_ERROR_ARGUMENT;
+ bongo_cat::NativeModel *previous = runtime->model;
+ bongo_cat::NativeModel *model = nullptr;
+ try {
+ model = new(std::nothrow) bongo_cat::NativeModel();
+ if (!model) {
+ bongo_cat_error_set(error, BONGO_CAT_ERROR_MEMORY,
+ "Cannot allocate Live2D model");
+ return BONGO_CAT_ERROR_MEMORY;
+ }
+ if (render_options) model->set_render_options(*render_options);
+ if (!model->load(directory, setting, preset, progress, userdata, error)) {
+ delete model;
+ return error ? error->code : BONGO_CAT_ERROR_CUBISM;
+ }
+ /* Keep the previous renderer alive until the replacement is complete. */
+ model->reshape(runtime->width, runtime->height);
+ if (!model->load_textures(error, progress, userdata)) {
+ BongoCatResult result = error ? error->code : BONGO_CAT_ERROR_CUBISM;
+ delete model;
+ return result;
+ }
+ GLenum ready_error = glGetError();
+ SDL_Log("[runtime] Live2D resource handoff: stage=replacement-ready "
+ "new_textures=%zu previous_textures=%zu current_window=%p "
+ "current_context=%p gl_error=0x%x",
+ model->texture_count(), previous ? previous->texture_count() : 0,
+ (void *)SDL_GL_GetCurrentWindow(),
+ (void *)SDL_GL_GetCurrentContext(), (unsigned)ready_error);
+ if (progress) progress(userdata, 1.0f);
+ runtime->model = model;
+ retire_previous(runtime, previous);
+ SDL_Log("[runtime] Live2D resource handoff: stage=retirement-queued "
+ "previous=%d previous_textures=%zu queue=%u frames=%u",
+ previous != nullptr,
+ previous ? previous->texture_count() : 0, runtime->retired_count,
+ previous ? 3u : 0u);
+ GLenum retired_error = glGetError();
+ SDL_Log("[runtime] Live2D resource handoff: stage=complete "
+ "new_textures=%zu current_window=%p current_context=%p gl_error=0x%x",
+ model->texture_count(),
+ (void *)SDL_GL_GetCurrentWindow(),
+ (void *)SDL_GL_GetCurrentContext(), (unsigned)retired_error);
+ return BONGO_CAT_OK;
+ } catch (const std::bad_alloc &) {
+ bongo_cat_error_set(error, BONGO_CAT_ERROR_MEMORY,
+ "Out of memory while loading the Live2D model");
+ } catch (const std::exception &exception) {
+ bongo_cat_error_set(error, BONGO_CAT_ERROR_CUBISM,
+ "Live2D model load failed: %s", exception.what());
+ } catch (...) {
+ bongo_cat_error_set(error, BONGO_CAT_ERROR_CUBISM,
+ "Live2D model load failed with an unknown exception");
+ }
+ delete model;
+ return error ? error->code : BONGO_CAT_ERROR_CUBISM;
+}
diff --git a/src/live2d/cubism_bridge_visual.cpp b/src/live2d/cubism_bridge_visual.cpp
new file mode 100644
index 00000000..acc20b43
--- /dev/null
+++ b/src/live2d/cubism_bridge_visual.cpp
@@ -0,0 +1,6 @@
+#include "cubism_runtime.hpp"
+
+extern "C" bool bongo_cat_live2d_visual_state(
+ const BongoCatLive2D *runtime, BongoCatLive2DVisualState *state) {
+ return runtime && runtime->model && runtime->model->visual_state(state);
+}
diff --git a/src/live2d/cubism_core_profile.hpp b/src/live2d/cubism_core_profile.hpp
new file mode 100644
index 00000000..cddc45db
--- /dev/null
+++ b/src/live2d/cubism_core_profile.hpp
@@ -0,0 +1,68 @@
+#pragma once
+
+#include
+
+namespace bongo_cat {
+
+struct CoreProfileBuffers {
+ GLuint vao = 0;
+ GLuint buffers[3] = {};
+
+ CoreProfileBuffers() = default;
+ CoreProfileBuffers(const CoreProfileBuffers &) = delete;
+ CoreProfileBuffers &operator=(const CoreProfileBuffers &) = delete;
+
+ void release() {
+ glDeleteVertexArrays(1, &vao);
+ glDeleteBuffers(3, buffers);
+ vao = 0;
+ for (auto &buffer : buffers) buffer = 0;
+ }
+};
+
+// SDK callbacks use the buffers of the model currently being drawn.
+class CoreProfileBinding {
+public:
+ explicit CoreProfileBinding(CoreProfileBuffers &resources)
+ : resources_(resources), previous_(active_) {
+ glGetIntegerv(GL_VERTEX_ARRAY_BINDING, &previous_vao_);
+ glGetIntegerv(GL_ARRAY_BUFFER_BINDING, &previous_array_);
+ if (!resources_.vao) glGenVertexArrays(1, &resources_.vao);
+ if (!resources_.buffers[0]) glGenBuffers(3, resources_.buffers);
+ glBindVertexArray(resources_.vao);
+ active_ = this;
+ }
+
+ ~CoreProfileBinding() {
+ glBindVertexArray((GLuint)previous_vao_);
+ glBindBuffer(GL_ARRAY_BUFFER, (GLuint)previous_array_);
+ active_ = previous_;
+ }
+
+ CoreProfileBinding(const CoreProfileBinding &) = delete;
+ CoreProfileBinding &operator=(const CoreProfileBinding &) = delete;
+
+ static void attribute(unsigned slot, GLuint location,
+ const void *data, GLsizeiptr size) {
+ glBindBuffer(GL_ARRAY_BUFFER, active_->resources_.buffers[slot]);
+ glBufferData(GL_ARRAY_BUFFER, size, data, GL_STREAM_DRAW);
+ glVertexAttribPointer(location, 2, GL_FLOAT, GL_FALSE,
+ sizeof(GLfloat) * 2, nullptr);
+ }
+
+ static void draw(GLsizei count, const GLushort *indices) {
+ glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, active_->resources_.buffers[2]);
+ glBufferData(GL_ELEMENT_ARRAY_BUFFER, count * sizeof(GLushort),
+ indices, GL_STREAM_DRAW);
+ glDrawElements(GL_TRIANGLES, count, GL_UNSIGNED_SHORT, nullptr);
+ }
+
+private:
+ CoreProfileBuffers &resources_;
+ CoreProfileBinding *previous_;
+ GLint previous_vao_ = 0;
+ GLint previous_array_ = 0;
+ inline static thread_local CoreProfileBinding *active_ = nullptr;
+};
+
+} // namespace bongo_cat
diff --git a/src/live2d/cubism_expression.cpp b/src/live2d/cubism_expression.cpp
new file mode 100644
index 00000000..09b25718
--- /dev/null
+++ b/src/live2d/cubism_expression.cpp
@@ -0,0 +1,78 @@
+#include "cubism_model.hpp"
+
+#include
+#include
+#include
+
+namespace bongo_cat {
+
+bool NativeModel::set_expression(int index) {
+ if (index == -1) {
+ expression_index_ = -1;
+ expression_frame_pending_ = false;
+ auto *entries = _expressionManager->GetCubismMotionQueueEntries();
+ expression_clearing_ = entries && entries->GetSize() > 0;
+ if (!expression_clearing_) {
+ _expressionManager->StopAllMotions();
+ return true;
+ }
+ for (Csm::csmUint32 i = 0; i < entries->GetSize(); ++i) {
+ Csm::CubismMotionQueueEntry *entry = entries->At(i);
+ if (!entry) continue;
+ Csm::ACubismMotion *motion = entry->GetCubismMotion();
+ if (motion) {
+ float fade_out = motion->GetFadeOutTime();
+ entry->SetFadeout(fade_out < 0.0f ? 0.0f : fade_out);
+ }
+ }
+ return true;
+ }
+ if (index < 0 || (size_t)index >= expression_names_.size()) return false;
+ auto found = expressions_.find(expression_names_[(size_t)index]);
+ if (found == expressions_.end()) return false;
+ Csm::CubismMotionQueueEntryHandle handle = _expressionManager->StartMotion(
+ found->second, false);
+ if (handle == Csm::InvalidMotionQueueEntryHandleValue) return false;
+ expression_clearing_ = false;
+ expression_index_ = index;
+ expression_frame_pending_ = true;
+ return true;
+}
+
+void NativeModel::settle_pending_expression_for_cover() {
+ if (!expression_frame_pending_ || !_model || expression_index_ < 0 ||
+ (size_t)expression_index_ >= expression_names_.size()) return;
+ auto found = expressions_.find(expression_names_[(size_t)expression_index_]);
+ if (found == expressions_.end()) {
+ expression_frame_pending_ = false;
+ return;
+ }
+ float fade_seconds = found->second->GetFadeInTime();
+ if (!std::isfinite(fade_seconds) || fade_seconds < 0.0f)
+ fade_seconds = 0.0f;
+ /* Only the expression clock is advanced. The model baseline already
+ contains restored persistent motions, while idle effects stay put. */
+ _model->LoadParameters();
+ _expressionManager->UpdateMotion(_model, 0.0f);
+ if (fade_seconds > 0.0f)
+ _expressionManager->UpdateMotion(_model, fade_seconds);
+ expression_frame_pending_ = false;
+}
+
+void NativeModel::expire_expression_fade() {
+ if (!expression_clearing_) return;
+ auto *entries = _expressionManager->GetCubismMotionQueueEntries();
+ if (!entries || entries->GetSize() == 0) {
+ expression_clearing_ = false;
+ return;
+ }
+ for (Csm::csmUint32 i = 0; i < entries->GetSize(); ++i) {
+ Csm::CubismMotionQueueEntry *entry = entries->At(i);
+ if (entry && (entry->GetEndTime() < 0.0f ||
+ entry->GetStateTime() < entry->GetEndTime())) return;
+ }
+ _expressionManager->StopAllMotions();
+ expression_clearing_ = false;
+}
+
+} // namespace bongo_cat
diff --git a/src/live2d/cubism_model.hpp b/src/live2d/cubism_model.hpp
index f296ddb9..c91b5aca 100644
--- a/src/live2d/cubism_model.hpp
+++ b/src/live2d/cubism_model.hpp
@@ -1,6 +1,10 @@
#ifndef BONGO_CAT_CUBISM_MODEL_HPP
#define BONGO_CAT_CUBISM_MODEL_HPP
+#ifdef CSM_TARGET_MAC_GL
+#include "cubism_core_profile.hpp"
+#endif
+
#include "bongo_cat/model.h"
#include "bongo_cat/image.h"
@@ -11,6 +15,7 @@
#include
#include
#include