diff --git a/.clang-format b/.clang-format new file mode 100644 index 00000000..82e14f3d --- /dev/null +++ b/.clang-format @@ -0,0 +1,125 @@ +# Clang-Format Configuration for wayland-bongocat +# Best practices for C23-compatible code +--- +# Base style +BasedOnStyle: LLVM +IndentWidth: 2 +--- +# Language +Language: Cpp +Standard: Latest + +# Indentation +IndentWidth: 2 +TabWidth: 2 +UseTab: Never +IndentCaseLabels: false +IndentPPDirectives: AfterHash +IndentExternBlock: NoIndent + +# Column limit +ColumnLimit: 120 + +# Alignment +AlignAfterOpenBracket: Align +AlignArrayOfStructures: Left +AlignConsecutiveAssignments: false +AlignConsecutiveBitFields: Consecutive +AlignConsecutiveDeclarations: false +AlignConsecutiveMacros: Consecutive +AlignEscapedNewlines: Left +AlignOperands: Align +AlignTrailingComments: true + +# Pointer and reference alignment +PointerAlignment: Right +ReferenceAlignment: Left +DerivePointerAlignment: false + +# Braces +BreakBeforeBraces: Attach +BraceWrapping: + AfterCaseLabel: false + AfterControlStatement: Never + AfterEnum: false + AfterFunction: false + AfterStruct: false + AfterUnion: false + AfterExternBlock: false + BeforeElse: false + BeforeWhile: false + IndentBraces: false + +# Line breaks +AllowAllArgumentsOnNextLine: true +AllowAllParametersOfDeclarationOnNextLine: true +AllowShortBlocksOnASingleLine: Empty +AllowShortCaseLabelsOnASingleLine: false +AllowShortEnumsOnASingleLine: false +AllowShortFunctionsOnASingleLine: Empty +AllowShortIfStatementsOnASingleLine: Never +AllowShortLoopsOnASingleLine: false +AlwaysBreakAfterReturnType: None +AlwaysBreakBeforeMultilineStrings: false +BinPackArguments: true +BinPackParameters: true +BreakBeforeBinaryOperators: None +BreakBeforeTernaryOperators: true +BreakStringLiterals: true +NamespaceIndentation: Inner +FixNamespaceComments: true +BreakConstructorInitializers: BeforeComma +ConstructorInitializerAllOnOneLineOrOnePerLine: true +AllowAllConstructorInitializersOnNextLine: false +IndentWrappedFunctionNames: false +AlwaysBreakTemplateDeclarations: Yes + +# Spaces +SpaceAfterCStyleCast: false +SpaceAfterLogicalNot: false +SpaceAroundPointerQualifiers: Default +SpaceBeforeAssignmentOperators: true +SpaceBeforeCaseColon: false +SpaceBeforeParens: ControlStatements +SpaceBeforeSquareBrackets: false +SpaceInEmptyBlock: false +SpaceInEmptyParentheses: false +SpacesBeforeTrailingComments: 2 +SpacesInAngles: Never +SpacesInCStyleCastParentheses: false +SpacesInConditionalStatement: false +SpacesInContainerLiterals: false +SpacesInParentheses: false +SpacesInSquareBrackets: false + +# Includes +SortIncludes: CaseSensitive +IncludeBlocks: Regroup +IncludeCategories: + # Project headers first (quoted includes) + - Regex: '^"' + Priority: 1 + # System headers + - Regex: "^<" + Priority: 2 + +# Penalties (to control line breaking decisions) +PenaltyBreakAssignment: 2 +PenaltyBreakBeforeFirstCallParameter: 19 +PenaltyBreakComment: 300 +PenaltyBreakFirstLessLess: 120 +PenaltyBreakString: 1000 +PenaltyExcessCharacter: 1000000 +PenaltyReturnTypeOnItsOwnLine: 60 + +# Miscellaneous +KeepEmptyLinesAtTheStartOfBlocks: false +MaxEmptyLinesToKeep: 1 +ReflowComments: true +SeparateDefinitionBlocks: Leave +InsertBraces: false +RemoveBracesLLVM: false +--- +Language: JavaScript +IndentWidth: 2 +ColumnLimit: 120 \ No newline at end of file diff --git a/.clang-tidy b/.clang-tidy new file mode 100644 index 00000000..044b6725 --- /dev/null +++ b/.clang-tidy @@ -0,0 +1,134 @@ +# Clang-Tidy Configuration for wayland-bongocat +# Static analysis for C code with best practices + +# Enable checks +Checks: > + -*, + bugprone-*, + -bugprone-easily-swappable-parameters, + -bugprone-reserved-identifier, + clang-analyzer-*, + -clang-analyzer-security.insecureAPI.DeprecatedOrUnsafeBufferHandling, + concurrency-*, + misc-*, + -misc-no-recursion, + performance-*, + portability-*, + readability-*, + -readability-identifier-length, + -readability-magic-numbers, + -readability-function-cognitive-complexity, + -readability-else-after-return, + -readability-uppercase-literal-suffix, + -misc-use-anonymous-namespace, + -readability-non-const-parameter, + -performance-enum-size, + +# Only analyze project headers, not system/lib headers +HeaderFilterRegex: '.*/(include|src)/.*\.(h|hpp)$' + +# Treat warnings as warnings (not errors by default) +WarningsAsErrors: '' + +# Check options +CheckOptions: + # Naming conventions (C style) + - key: readability-identifier-naming.GlobalConstantCase + value: UPPER_CASE + - key: readability-identifier-naming.MacroDefinitionCase + value: UPPER_CASE + - key: readability-identifier-naming.FunctionCase + value: lower_case + - key: readability-identifier-naming.VariableCase + value: lower_case + - key: readability-identifier-naming.ParameterCase + value: lower_case + - key: readability-identifier-naming.StructCase + value: lower_case + - key: readability-identifier-naming.TypedefCase + value: lower_case + - key: readability-identifier-naming.TypedefSuffix + value: '_t' + - key: readability-identifier-naming.EnumCase + value: lower_case +# - key: readability-identifier-naming.EnumConstantCase +# value: UPPER_CASE + - key: readability-identifier-naming.MemberIgnoredRegexp + value: '^_?[a-z]+(_[a-z0-9]+)*$' + - key: readability-identifier-naming.PublicMemberIgnoredRegexp + value: '^_?[a-z]+(_[a-z0-9]+)*$' + # Naming conventions (C++ style) + - key: readability-identifier-naming.NamespaceCase + value: lower_case + - key: readability-identifier-naming.ClassCase + value: CamelCase + - key: readability-identifier-naming.ClassMemberCase + value: camelBack + - key: readability-identifier-naming.PrivateMemberCase + value: camelBack + - key: readability-identifier-naming.ProtectedMemberCase + value: camelBack + - key: readability-identifier-naming.PublicMemberCase + value: lower_case + - key: readability-identifier-naming.StaticConstantCase + value: UPPER_CASE + - key: readability-identifier-naming.ClassConstantCase + value: CamelCase + - key: readability-identifier-naming.ConstexprMethodCase + value: CamelCase + - key: readability-identifier-naming.ClassMethodCase + value: camelBack + - key: readability-identifier-naming.PublicMethodCase + value: camelBack + - key: readability-identifier-naming.PrivateMethodCase + value: camelBack + - key: readability-identifier-naming.ProtectedMethodCase + value: camelBack + - key: readability-identifier-naming.ConstexprMethodCase + value: camelBack + - key: readability-identifier-naming.LocalVariableCase + value: lower_case + - key: readability-identifier-naming.ConstantParameterCase + value: lower_case + - key: readability-identifier-naming.ParameterPackCase + value: lower_case + - key: readability-identifier-naming.LocalConstantCase + value: lower_case + - key: readability-identifier-naming.ConstexprVariableCase + value: UPPER_CASE + - key: readability-identifier-naming.TemplateParameterCase + value: CamelCase + - key: readability-identifier-naming.TemplateTemplateParameterCase + value: CamelCase + + # Braces around statements + - key: readability-braces-around-statements.ShortStatementLines + value: '1' + + # Function size limits (advisory) + - key: readability-function-size.LineThreshold + value: '200' + - key: readability-function-size.StatementThreshold + value: '100' + - key: readability-function-size.ParameterThreshold + value: '8' + + # Performance checks + - key: performance-unnecessary-value-param.AllowedTypes + value: '' + + # Misc settings + - key: misc-non-private-member-variables-in-classes.IgnoreClassesWithAllMemberVariablesBeingPublic + value: 'true' + - key: readability-identifier-length.IgnoredVariableNames + value: 'i|j|k|x|y|z' + - key: readability-identifier-length.IgnoredParameterNames + value: 'i|j|k|x|y|z' + +# System header directories to ignore +SystemHeaders: false + +# Use colors in diagnostics +UseColor: true + +#FormatStyle: file diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000..2a573b3d --- /dev/null +++ b/.editorconfig @@ -0,0 +1,52 @@ +# EditorConfig - Cross-editor consistency +# https://editorconfig.org + +root = true + +# Default settings for all files +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true +indent_style = space +indent_size = 2 + +# C/C++ source and headers +[*.{c,h}] +indent_size = 2 +max_line_length = 80 + +[*.{cpp,hpp}] +indent_size = 2 +max_line_length = 80 + +# Makefile (requires tabs) +[Makefile] +indent_style = tab +indent_size = 8 + +# Shell scripts +[*.sh] +indent_size = 2 + +# Markdown +[*.md] +trim_trailing_whitespace = false +max_line_length = off + +# XML (Wayland protocols) +[*.xml] +indent_size = 2 + +# YAML/Config files +[*.{yml,yaml,conf}] +indent_size = 2 + +# Nix files +[*.nix] +indent_size = 2 + +# JSON +[*.json] +indent_size = 2 diff --git a/CHANGELOG.md b/CHANGELOG.md index dc30e86a..66f0283a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,13 +2,43 @@ All notable changes to this project will be documented in this file. +## [3.6.0] - 2025-12-08 + +### Fixed + +- pull from upstream [1.3.2](https://github.com/saatvik333/wayland-bongocat/releases/tag/v1.3.2) + - **Monitor Reconnection** - Overlay now survives monitor disconnect/reconnect + - **Dynamic Overlay Resize** - Changing `overlay_height` via config reload + +### Added + +- pull from upstream [1.3.1](https://github.com/saatvik333/wayland-bongocat/releases/tag/v1.3.1) + - **Keyboard Hand Mapping** - Left half of keyboard triggers left cat hand, right half triggers right hand + - New config option `enable_hand_mapping=1` (enabled by default) + - Box filter + alpha blending + + +--- + +## [3.5.1] - 2025-12-06 + +### Improved + +- pull from upstream [1.3.0](https://github.com/saatvik333/wayland-bongocat/releases/tag/v1.3.0) + +### Fixed + +- fix include pmd set in pkmn + ## [3.5.0] - 2025-12-05 ### Added + - **Add NEW sprite sheets** - pmd (experimental) - up to gen. 8 ### Fixed + - working (CPU) animation stuck - fix dm sleep animation - fix find-devices crash @@ -16,6 +46,7 @@ All notable changes to this project will be documented in this file. ## [3.4.0] - 2025-11-22 ### Added + - Add running animation - animation speed-up when 100% CPU usage @@ -23,38 +54,45 @@ All notable changes to this project will be documented in this file. ## [3.3.1] - 2025-11-15 ### Fixed + - randomize at start-up - KDE (KWin) rendering (animation freeze) ## [3.3.0] - 2025-11-05 ### Added + - **Custom Sprite Sheets** - load custom sprite sheet - [Examples](./examples) ### Fixed + - fix pre-loading pen/pen20 ## [3.2.3] - 2025-10-30 ### Added + - **Add NEW sprite sheet set** - misc - [neko](https://github.com/eliot-akira/neko?tab=readme-ov-file#readme) ## [3.2.2] - 2025-10-23 ### Added + - **Add Missing Sprites** - add pen/pen20 dm versions - pen - pen20 ### Improved + - refactor animation system - improve animations - smoother animation transitions ## [3.2.1] - 2025-10-11 ### Fixed + - fix epoll draining - fix TSAN warnings - fix default config @@ -63,12 +101,14 @@ All notable changes to this project will be documented in this file. ## [3.2.0] - 2025-10-11 ### Added + - **More Sprites** - add more MS Agents - Merlin - Rover - **Movement** - Digimon can walk ### Fixed + - fix config watcher - use urandom device for RNG seeding - fix auto-detect resolution with multi-monitor setup @@ -76,6 +116,7 @@ All notable changes to this project will be documented in this file. ## [3.1.2] - 2025-09-05 ### Fixed + - config file handling - improve input devices check - improve config watcher @@ -84,11 +125,13 @@ All notable changes to this project will be documented in this file. ## [3.1.1] - 2025-08-30 ### Fixed + - improve CPU usage animation ## [3.1.0] - 2025-08-28 ### Added + - **CPU usage** - digimon react to CPU usage - stdin config - pipe config via `stdin` with `--config -` - Extend `find-devices` functionality @@ -97,20 +140,24 @@ All notable changes to this project will be documented in this file. - add `--include-mouse` option ### Fixed + - possible race condition and threading bugs ### Improved + - **add pngle** - alternative image loader; less RAM usage ## [3.0.1] - 2025-19-18 ### Fixed + - read config with comments ## [3.0.0] - 2025-09-15 ### Added + - **more Sprite** - add pkmn sprites - [menu sprites](https://archives.bulbagarden.net/wiki/Category:Animated_menu_sprites) @@ -120,6 +167,7 @@ All notable changes to this project will be documented in this file. _include changes from upstream_ ### Added + - **more Sprite** - add custom colored dm sprites (optional) - toggle bar visibility via signal (SIGUSR1) - strict mode - only start up when valid config @@ -128,19 +176,23 @@ _include changes from upstream_ ## [2.4.0] - 2025-09-05 ### Added + - `random` option - randomize animation_index at start (Digimon sprites) ### Improved + - **Lazy Loading** - load sprite sheets on demand ## [2.3.0] - 2025-08-30 ### Added + - **more Sprite** - add more MS agents (optional) - [Links](https://www.spriters-resource.com/pc_computer/microsoftofficexp/asset/104490/) ### Improved + - make toggle more robust @@ -159,19 +211,23 @@ _include changes from 1.2.5 (upstream)_ ## [2.1.1] - 2025-08-25 ### Added + - add CMakePresets ### Improved + - multi-threading locking - fix sanitizer warnings and errors (UB, data races, deadlocks) - update thread config via epoll ### Fixed + - fullscreen support for multi-monitor (hyprland) ## [2.1.0] - 2025-08-22 ### Added + - **More Sprite** - add Clippy (MS Agent) ## [2.0.0] - 2025-08-22 @@ -198,16 +254,19 @@ _keep it as close as possible to the original, it's still C and Linux developmen _include changes from 1.2.4 (upstream)_ ### Added + - **Overwrite config parameter** - Overwrite config setting with CLI Parameters - BREAKING CHANGE: **Multiple processes** - Processes per screen possible (pid file per screen (`output_name`)) - **Reload Config with Signal** - Reload current config with `SIGURS2` signal ### Improved + - replace input fork with thread - signal handling, use epoll - CMake: add more compile options/feature-flags (`BONGOCAT_DISABLE_MEMORY_STATISTICS`, `BONGOCAT_LOG_LEVEL`) ### Fixed + - fix wayland memory leaks (toplevel) - fix potential memory leaks @@ -215,6 +274,7 @@ _include changes from 1.2.4 (upstream)_ ## [1.3.0] - 2025-08-06 ### Added + - **More Sprite** - add Digimon sprite - New `animation_name` option - Add minimal [dm - Version 1](https://humulos.com/digimon/dm/) Digimons @@ -226,10 +286,12 @@ _include changes from 1.2.4 (upstream)_ - Add KPM reaction - New `happy_kpm` option: Minimum keystrokes per minute (KPM) required to trigger the happy animation ### Improved + - BREAKING CHANGE: **C23** - use new C standard C23 - add Logger MACROs ### Fixed + - fix config reload crashes - fix potential memory leaks - code cleanup @@ -238,28 +300,34 @@ _include changes from 1.2.4 (upstream)_ ## [1.2.5] - 2025-08-26 (upstream) ### Added + - **Enhanced Configuration System** - New config variables for fine-tuning appearance and behavior - **Sleep Mode** - Scheduled or idle-based sleep mode with customizable timing ### Fixed + - **Fixed Positioning** - Fine-tune position, defaults to center ### Improved + - **Default Values** - Refined default configuration values for better out-of-box experience ## [1.2.4] - 2025-08-08 ### Added + - **Multi-Monitor Support** - Choose which monitor to display bongocat on using the `monitor` configuration option - **Monitor Detection** - Automatic detection of available monitors with fallback to first monitor if specified monitor not found - **XDG Output Protocol** - Proper Wayland protocol implementation for monitor identification ### Fixed + - **Memory Leaks** - Fixed memory leak in monitor configuration cleanup - **Process Cleanup** - Resolved child process cleanup warnings during shutdown - **Segmentation Fault** - Fixed crash during application exit related to Wayland resource cleanup ### Improved + - **Error Handling** - Better error messages when specified monitor is not found - **Resource Management** - Improved cleanup order for Wayland resources - **Logging** - Enhanced debug logging for monitor detection and selection @@ -267,11 +335,13 @@ _include changes from 1.2.4 (upstream)_ ## [1.2.3] - 2025-08-02 ### Added + - **Smart Fullscreen Detection** - Automatically hides overlay during fullscreen applications for a cleaner experience - **Enhanced Artwork** - Custom-drawn bongocat image files by [@Shreyabardia](https://github.com/Shreyabardia) - **Modular Architecture** - Reorganized codebase into logical modules for better maintainability ### Improved + - **Signal Handling** - Fixed duplicate log messages during shutdown - **Code Organization** - Separated concerns into core, graphics, platform, config, and utils modules - **Build System** - Updated to support new modular structure @@ -279,18 +349,21 @@ _include changes from 1.2.4 (upstream)_ ## [1.2.2] - Previous Release ### Added + - Automatic screen detection for all sizes and orientations - Enhanced performance optimizations ## [1.2.1] - Previous Release ### Added + - Configuration hot-reload system - Dynamic device detection ## [1.2.0] - Previous Release ### Added + - Hot-reload configuration support - Dynamic Bluetooth/USB keyboard detection - Performance optimizations with adaptive monitoring @@ -299,6 +372,7 @@ _include changes from 1.2.4 (upstream)_ ## [1.1.x] - Previous Releases ### Added + - Multi-device support - Embedded assets - Cross-platform compatibility (x86_64 and ARM64) diff --git a/CMakeLists.txt b/CMakeLists.txt index 55ad7559..1495d312 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -6,46 +6,53 @@ set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_C_STANDARD_REQUIRED ON) include(FetchContent) if(CMAKE_VERSION VERSION_GREATER_EQUAL "3.24.0") - cmake_policy(SET CMP0135 NEW) + cmake_policy(SET CMP0135 NEW) endif() get_property(BUILDING_MULTI_CONFIG GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG) if(BUILDING_MULTI_CONFIG) - if(NOT CMAKE_BUILD_TYPE) - # Make sure that all supported configuration types have their associated conan packages available. You can reduce this list to only the - # configuration types you use, but only if one is not forced-set on the command line for VS - message(TRACE "Setting up multi-config build types") - set(CMAKE_CONFIGURATION_TYPES - Debug Release RelWithDebInfo MinSizeRel - CACHE STRING "Enabled build types" FORCE) - else() - message(TRACE "User chose a specific build type, so we are using that") - set(CMAKE_CONFIGURATION_TYPES - ${CMAKE_BUILD_TYPE} - CACHE STRING "Enabled build types" FORCE) - endif() + if(NOT CMAKE_BUILD_TYPE) + # Make sure that all supported configuration types have their associated conan packages available. You can reduce this list to only the + # configuration types you use, but only if one is not forced-set on the command line for VS + message(TRACE "Setting up multi-config build types") + set(CMAKE_CONFIGURATION_TYPES + Debug Release RelWithDebInfo MinSizeRel + CACHE STRING "Enabled build types" FORCE) + else() + message(TRACE "User chose a specific build type, so we are using that") + set(CMAKE_CONFIGURATION_TYPES + ${CMAKE_BUILD_TYPE} + CACHE STRING "Enabled build types" FORCE) + endif() endif() if(NOT CMAKE_BUILD_TYPE) - set(CMAKE_BUILD_TYPE Release CACHE STRING "Choose the build type." FORCE) + set(CMAKE_BUILD_TYPE + Release + CACHE STRING "Choose the build type." FORCE) endif() - - -project(bongocat LANGUAGES C CXX VERSION 3.5.0) +project( + bongocat + LANGUAGES C CXX + VERSION 3.6.0) # Feature Flags include(CMakeDependentOption) option(FEATURE_BONGOCAT_EMBEDDED_ASSETS "Include bongocat assets (default)" ON) option(FEATURE_ENABLE_DM_EMBEDDED_ASSETS "Enable include dm embedded assets" OFF) cmake_dependent_option(FEATURE_DM_EMBEDDED_ASSETS "Include dm embedded assets" OFF FEATURE_ENABLE_DM_EMBEDDED_ASSETS OFF) -cmake_dependent_option(FEATURE_DM20_EMBEDDED_ASSETS "Include dm20 embedded assets (replaces original dm)" OFF FEATURE_ENABLE_DM_EMBEDDED_ASSETS OFF) +cmake_dependent_option(FEATURE_DM20_EMBEDDED_ASSETS "Include dm20 embedded assets (replaces original dm)" OFF + FEATURE_ENABLE_DM_EMBEDDED_ASSETS OFF) cmake_dependent_option(FEATURE_DMC_EMBEDDED_ASSETS "Include dmc embedded assets" OFF FEATURE_ENABLE_DM_EMBEDDED_ASSETS OFF) cmake_dependent_option(FEATURE_DMX_EMBEDDED_ASSETS "Include dmx embedded assets" OFF FEATURE_ENABLE_DM_EMBEDDED_ASSETS OFF) cmake_dependent_option(FEATURE_PEN_EMBEDDED_ASSETS "Include pen embedded assets" OFF FEATURE_ENABLE_DM_EMBEDDED_ASSETS OFF) -cmake_dependent_option(FEATURE_PEN20_EMBEDDED_ASSETS "Include pen20 embedded assets (replaces original pen)" OFF FEATURE_ENABLE_DM_EMBEDDED_ASSETS OFF) -cmake_dependent_option(FEATURE_DMALL_EMBEDDED_ASSETS "Include custom colored dm (replace dmc) embedded assets" OFF FEATURE_ENABLE_DM_EMBEDDED_ASSETS OFF) +cmake_dependent_option(FEATURE_PEN20_EMBEDDED_ASSETS "Include pen20 embedded assets (replaces original pen)" OFF + FEATURE_ENABLE_DM_EMBEDDED_ASSETS OFF) +cmake_dependent_option(FEATURE_DMALL_EMBEDDED_ASSETS "Include custom colored dm (replace dmc) embedded assets" OFF + FEATURE_ENABLE_DM_EMBEDDED_ASSETS OFF) option(FEATURE_MS_AGENT_EMBEDDED_ASSETS "Include MS agent (Clippy) embedded assets" OFF) -cmake_dependent_option(FEATURE_MORE_MS_AGENT_EMBEDDED_ASSETS "Include more MS agents (Links) embedded assets" OFF FEATURE_MS_AGENT_EMBEDDED_ASSETS OFF) +cmake_dependent_option(FEATURE_MORE_MS_AGENT_EMBEDDED_ASSETS "Include more MS agents (Links) embedded assets" OFF + FEATURE_MS_AGENT_EMBEDDED_ASSETS OFF) option(FEATURE_ENABLE_PKMN_EMBEDDED_ASSETS "Enable include pkmn embedded assets" OFF) option(FEATURE_ENABLE_PMD_EMBEDDED_ASSETS "Enable include pkmn pmd (replace pkmn) embedded assets" OFF) option(FEATURE_MISC_EMBEDDED_ASSETS "Enable include misc embedded assets" OFF) @@ -55,7 +62,8 @@ option(FEATURE_DISABLE_LOGGER "Disable Logger (makes enable_debug option obsolet option(FEATURE_PRELOAD_ASSETS "Preload available assets (More RAM usage, faster sprite switching on hot-reload)" OFF) option(FEATURE_MULTI_VERSIONS "Build multiple versions with different assets for installation" ON) option(FEATURE_LAZY_LOAD_ASSETS "No Preload assets (Less RAM usage, more CPU usage, sprite lazy-load on config reload) (Recommended)" ON) -option(FEATURE_USE_HYBRID_IMAGE_BACKEND "Use pngle or stb_image as assets (png) loader (Less RAM usage, more loading time, balanced) (Recommended)" ON) +option(FEATURE_USE_HYBRID_IMAGE_BACKEND + "Use pngle or stb_image as assets (png) loader (Less RAM usage, more loading time, balanced) (Recommended)" ON) option(FEATURE_USE_PNGLE "Use pngle as assets (png) loader (Less RAM usage, more loading time; replace stb_image)" OFF) option(FEATURE_CUSTOM_SPRITE_SHEETS "Enable custom sprite sheet at runtime" ON) @@ -63,339 +71,422 @@ option(ENABLE_ASAN "Enable Address Sanitizer" OFF) option(ENABLE_UBSAN "Enable Undefined Behavior Sanitizer" OFF) option(ENABLE_TSAN "Enable Thread Sanitizer" OFF) -# project_options -# More Warnings +# project_options More Warnings set(CLANG_WARNINGS - -Wall - -Wextra # reasonable and standard - -Wextra-semi # Warn about semicolon after in-class function definition. - -Wshadow # warn the user if a variable declaration shadows one from a parent context - -Wnon-virtual-dtor # warn the user if a class with virtual functions has a non-virtual destructor. This helps - # catch hard to track down memory errors - -Wold-style-cast # warn for c-style casts - -Wcast-align # warn for potential performance problem casts - -Wunused # warn on anything being unused - -Woverloaded-virtual # warn if you overload (not override) a virtual function - -Wpedantic # warn if non-standard C++ is used - -Wconversion # warn on type conversions that may lose data - -Wsign-conversion # warn on sign conversions - -Wnull-dereference # warn if a null dereference is detected - -Wdouble-promotion # warn if float is implicit promoted to double - -Wformat=2 # warn on security issues around functions that format output (ie printf) - -Wimplicit-fallthrough # warn on statements that fallthrough without an explicit annotation + -Wall + -Wextra # reasonable and standard + -Wextra-semi # Warn about semicolon after in-class function definition. + -Wshadow # warn the user if a variable declaration shadows one from a parent context + -Wnon-virtual-dtor # warn the user if a class with virtual functions has a non-virtual destructor. This helps + # catch hard to track down memory errors + -Wold-style-cast # warn for c-style casts + -Wcast-align # warn for potential performance problem casts + -Wunused # warn on anything being unused + -Woverloaded-virtual # warn if you overload (not override) a virtual function + -Wpedantic # warn if non-standard C++ is used + -Wconversion # warn on type conversions that may lose data + -Wsign-conversion # warn on sign conversions + -Wnull-dereference # warn if a null dereference is detected + -Wdouble-promotion # warn if float is implicit promoted to double + -Wformat=2 # warn on security issues around functions that format output (ie printf) + -Wimplicit-fallthrough # warn on statements that fallthrough without an explicit annotation ) set(GCC_WARNINGS - ${CLANG_WARNINGS} - -Wmisleading-indentation # warn if indentation implies blocks where blocks do not exist - -Wduplicated-cond # warn if if / else chain has duplicated conditions - -Wduplicated-branches # warn if if / else branches have duplicated code - -Wlogical-op # warn about logical operations being used where bitwise were probably wanted - -Wuseless-cast # warn if you perform a cast to the same type + ${CLANG_WARNINGS} + -Wmisleading-indentation # warn if indentation implies blocks where blocks do not exist + -Wduplicated-cond # warn if if / else chain has duplicated conditions + -Wduplicated-branches # warn if if / else branches have duplicated code + -Wlogical-op # warn about logical operations being used where bitwise were probably wanted + -Wuseless-cast # warn if you perform a cast to the same type ) set(C_WARNING_FLAGS - -Wall -Wextra -Wpedantic -Wformat=2 -Wstrict-prototypes - -Wmissing-prototypes -Wold-style-definition -Wredundant-decls - -Wnested-externs -Wmissing-include-dirs -Wlogical-op - -Wjump-misses-init -Wdouble-promotion -Wshadow -) + -Wall + -Wextra + -Wpedantic + -Wformat=2 + -Wstrict-prototypes + -Wmissing-prototypes + -Wold-style-definition + -Wredundant-decls + -Wnested-externs + -Wmissing-include-dirs + -Wlogical-op + -Wjump-misses-init + -Wdouble-promotion + -Wshadow) set(GCC_C_WARNING_FLAGS - -Wall -Wextra -Wpedantic -Wshadow -Wcast-align -Wunused - -Wconversion -Wsign-conversion -Wformat=2 -Wimplicit-fallthrough - -Wnull-dereference -Wdouble-promotion -Wmissing-prototypes - -Wstrict-prototypes -Wpointer-arith -Wundef - -Wmissing-field-initializers -Wvla) + -Wall + -Wextra + -Wpedantic + -Wshadow + -Wcast-align + -Wunused + -Wconversion + -Wsign-conversion + -Wformat=2 + -Wimplicit-fallthrough + -Wnull-dereference + -Wdouble-promotion + -Wmissing-prototypes + -Wstrict-prototypes + -Wpointer-arith + -Wundef + -Wmissing-field-initializers + -Wvla) set(CLANG_C_WARNING_FLAGS - -Wall -Wextra -Wpedantic -Wshadow -Wcast-align -Wunused - -Wconversion -Wsign-conversion -Wformat=2 -Wimplicit-fallthrough - -Wnull-dereference -Wdouble-promotion -Wmissing-prototypes - -Wstrict-prototypes -Wpointer-arith -Wundef - -Wmissing-field-initializers -Wvla - -Wmisleading-indentation -Wduplicated-cond -Wduplicated-branches -Wlogical-op) + -Wall + -Wextra + -Wpedantic + -Wshadow + -Wcast-align + -Wunused + -Wconversion + -Wsign-conversion + -Wformat=2 + -Wimplicit-fallthrough + -Wnull-dereference + -Wdouble-promotion + -Wmissing-prototypes + -Wstrict-prototypes + -Wpointer-arith + -Wundef + -Wmissing-field-initializers + -Wvla + -Wmisleading-indentation + -Wduplicated-cond + -Wduplicated-branches + -Wlogical-op) add_library(project_warnings INTERFACE) target_compile_options(project_warnings INTERFACE $<$:${C_WARNING_FLAGS}>) -target_compile_options(project_warnings INTERFACE - $<$,$>:${CLANG_C_WARNINGS}> - $<$,$>:${GCC_C_WARNINGS}> - $<$,$>:${CLANG_WARNINGS}> - $<$,$>:${GCC_WARNINGS}> -) +target_compile_options( + project_warnings + INTERFACE $<$,$>:${CLANG_C_WARNINGS}> + $<$,$>:${GCC_C_WARNINGS}> + $<$,$>:${CLANG_WARNINGS}> + $<$,$>:${GCC_WARNINGS}>) add_library(project_options INTERFACE) -#target_compile_options(project_options INTERFACE $<$:/external:anglebrackets /external:W0>) -#target_compile_options(project_options INTERFACE $<$:/external:anglebrackets /external:W0>) +# target_compile_options(project_options INTERFACE $<$:/external:anglebrackets /external:W0>) +# target_compile_options(project_options INTERFACE $<$:/external:anglebrackets /external:W0>) set_target_properties(project_options PROPERTIES INTERPROCEDURAL_OPTIMIZATION_RELEASE ON) set_target_properties(project_options PROPERTIES INTERPROCEDURAL_OPTIMIZATION_RELWITHDEBINFO ON) add_library(project_sanitizers INTERFACE) if(CMAKE_CXX_COMPILER_ID MATCHES "Clang|GNU") - # Check for incompatible options - if(ENABLE_ASAN AND ENABLE_TSAN) - message(FATAL_ERROR "ASan and TSan are incompatible. Please enable only one of them.") - endif() - if(ENABLE_ASAN) - target_compile_options(project_sanitizers INTERFACE -fsanitize=address) - target_link_options(project_sanitizers INTERFACE -fsanitize=address) - endif() - if(ENABLE_UBSAN) - target_compile_options(project_sanitizers INTERFACE -fsanitize=undefined) - target_link_options(project_sanitizers INTERFACE -fsanitize=undefined) - endif() - if(ENABLE_TSAN) - target_compile_options(project_sanitizers INTERFACE -fsanitize=thread) - target_link_options(project_sanitizers INTERFACE -fsanitize=thread) - endif() + # Check for incompatible options + if(ENABLE_ASAN AND ENABLE_TSAN) + message(FATAL_ERROR "ASan and TSan are incompatible. Please enable only one of them.") + endif() + if(ENABLE_ASAN) + target_compile_options(project_sanitizers INTERFACE -fsanitize=address) + target_link_options(project_sanitizers INTERFACE -fsanitize=address) + endif() + if(ENABLE_UBSAN) + target_compile_options(project_sanitizers INTERFACE -fsanitize=undefined) + target_link_options(project_sanitizers INTERFACE -fsanitize=undefined) + endif() + if(ENABLE_TSAN) + target_compile_options(project_sanitizers INTERFACE -fsanitize=thread) + target_link_options(project_sanitizers INTERFACE -fsanitize=thread) + endif() endif() set(PROTOCOLS_DIR ${PROJECT_SOURCE_DIR}/protocols) add_subdirectory(protocols) - set(SRC_DIR ${PROJECT_SOURCE_DIR}/src) set(INCLUDE_DIR ${PROJECT_SOURCE_DIR}/include) add_subdirectory(src) - - add_executable(bongocat) -if (FEATURE_BONGOCAT_EMBEDDED_ASSETS) - target_link_libraries(bongocat PRIVATE assets_bongocat_loader assets_bongocat assets_bongocat_feature assets_bongocat_interface) - message(STATUS "Include bongocat assets") +if(FEATURE_BONGOCAT_EMBEDDED_ASSETS) + target_link_libraries(bongocat PRIVATE assets_bongocat_loader assets_bongocat assets_bongocat_feature assets_bongocat_interface) + message(STATUS "Include bongocat assets") endif() -if (FEATURE_ENABLE_DM_EMBEDDED_ASSETS) - if (NOT FEATURE_DM20_EMBEDDED_ASSETS AND NOT FEATURE_DM_EMBEDDED_ASSETS AND NOT FEATURE_DMX_EMBEDDED_ASSETS AND NOT FEATURE_DMC_EMBEDDED_ASSETS) - target_link_libraries(bongocat PRIVATE assets_min_dm_loader assets_min_dm assets_min_dm_feature assets_min_dm_interface) - message(STATUS "Include min_dm assets") - else() - if (FEATURE_DM20_EMBEDDED_ASSETS) - target_link_libraries(bongocat PRIVATE assets_dm20_loader assets_dm20 assets_dm20_feature assets_dm20_interface) - message(STATUS "Include dm20 assets") - elseif (FEATURE_DM_EMBEDDED_ASSETS) - target_link_libraries(bongocat PRIVATE assets_dm_loader assets_dm assets_dm_feature assets_dm_interface) - message(STATUS "Include dm assets") - endif() - if (FEATURE_DMX_EMBEDDED_ASSETS) - target_link_libraries(bongocat PRIVATE assets_dmx_loader assets_dmx assets_dmx_feature assets_dmx_interface) - message(STATUS "Include dmx assets") - endif() - if (FEATURE_PEN20_EMBEDDED_ASSETS) - target_link_libraries(bongocat PRIVATE assets_pen20_loader assets_pen20 assets_pen20_feature assets_pen20_interface) - message(STATUS "Include pen20 assets") - elseif (FEATURE_PEN_EMBEDDED_ASSETS) - target_link_libraries(bongocat PRIVATE assets_pen_loader assets_pen assets_pen_feature assets_pen_interface) - message(STATUS "Include pen assets") - endif() - if (FEATURE_DMALL_EMBEDDED_ASSETS) - target_link_libraries(bongocat PRIVATE assets_dmall_loader assets_dmall assets_dmall_feature assets_dmall_interface) - message(STATUS "Include dmall assets") - elseif (FEATURE_DMC_EMBEDDED_ASSETS) - target_link_libraries(bongocat PRIVATE assets_dmc_loader assets_dmc assets_dmc_feature assets_dmc_interface) - message(STATUS "Include dmc assets") - endif() +if(FEATURE_ENABLE_DM_EMBEDDED_ASSETS) + if(NOT FEATURE_DM20_EMBEDDED_ASSETS + AND NOT FEATURE_DM_EMBEDDED_ASSETS + AND NOT FEATURE_DMX_EMBEDDED_ASSETS + AND NOT FEATURE_DMC_EMBEDDED_ASSETS) + target_link_libraries(bongocat PRIVATE assets_min_dm_loader assets_min_dm assets_min_dm_feature assets_min_dm_interface) + message(STATUS "Include min_dm assets") + else() + if(FEATURE_DM20_EMBEDDED_ASSETS) + target_link_libraries(bongocat PRIVATE assets_dm20_loader assets_dm20 assets_dm20_feature assets_dm20_interface) + message(STATUS "Include dm20 assets") + elseif(FEATURE_DM_EMBEDDED_ASSETS) + target_link_libraries(bongocat PRIVATE assets_dm_loader assets_dm assets_dm_feature assets_dm_interface) + message(STATUS "Include dm assets") endif() -endif() -if (FEATURE_MS_AGENT_EMBEDDED_ASSETS) - if (FEATURE_MORE_MS_AGENT_EMBEDDED_ASSETS) - target_link_libraries(bongocat PRIVATE assets_more_ms_agent_loader assets_more_ms_agent assets_more_ms_agent_feature assets_more_ms_agent_interface) - else() - target_link_libraries(bongocat PRIVATE assets_ms_agent_loader assets_ms_agent assets_ms_agent_feature assets_ms_agent_interface) + if(FEATURE_DMX_EMBEDDED_ASSETS) + target_link_libraries(bongocat PRIVATE assets_dmx_loader assets_dmx assets_dmx_feature assets_dmx_interface) + message(STATUS "Include dmx assets") endif() - message(STATUS "Include MS agent assets") + if(FEATURE_PEN20_EMBEDDED_ASSETS) + target_link_libraries(bongocat PRIVATE assets_pen20_loader assets_pen20 assets_pen20_feature assets_pen20_interface) + message(STATUS "Include pen20 assets") + elseif(FEATURE_PEN_EMBEDDED_ASSETS) + target_link_libraries(bongocat PRIVATE assets_pen_loader assets_pen assets_pen_feature assets_pen_interface) + message(STATUS "Include pen assets") + endif() + if(FEATURE_DMALL_EMBEDDED_ASSETS) + target_link_libraries(bongocat PRIVATE assets_dmall_loader assets_dmall assets_dmall_feature assets_dmall_interface) + message(STATUS "Include dmall assets") + elseif(FEATURE_DMC_EMBEDDED_ASSETS) + target_link_libraries(bongocat PRIVATE assets_dmc_loader assets_dmc assets_dmc_feature assets_dmc_interface) + message(STATUS "Include dmc assets") + endif() + endif() +endif() +if(FEATURE_MS_AGENT_EMBEDDED_ASSETS) + if(FEATURE_MORE_MS_AGENT_EMBEDDED_ASSETS) + target_link_libraries(bongocat PRIVATE assets_more_ms_agent_loader assets_more_ms_agent assets_more_ms_agent_feature + assets_more_ms_agent_interface) + else() + target_link_libraries(bongocat PRIVATE assets_ms_agent_loader assets_ms_agent assets_ms_agent_feature assets_ms_agent_interface) + endif() + message(STATUS "Include MS agent assets") +endif() +if(FEATURE_PMD_EMBEDDED_ASSETS) + target_link_libraries(bongocat PRIVATE assets_pmd_loader assets_pmd assets_pmd_feature assets_pmd_interface assets_custom_loader) + message(STATUS "Include pkmn pmd assets") +elseif(FEATURE_PKMN_EMBEDDED_ASSETS) + target_link_libraries(bongocat PRIVATE assets_pkmn_loader assets_pkmn assets_pkmn_feature assets_pkmn_interface) + message(STATUS "Include pkmn assets") endif() -if (FEATURE_PMD_EMBEDDED_ASSETS) - target_link_libraries(bongocat PRIVATE assets_pmd_loader assets_pmd assets_pmd_feature assets_pmd_interface assets_custom_loader) - message(STATUS "Include pkmn pmd assets") -elseif (FEATURE_PKMN_EMBEDDED_ASSETS) - target_link_libraries(bongocat PRIVATE assets_pkmn_loader assets_pkmn assets_pkmn_feature assets_pkmn_interface) - message(STATUS "Include pkmn assets") +if(FEATURE_MISC_EMBEDDED_ASSETS) + target_link_libraries(bongocat PRIVATE assets_misc_loader assets_misc assets_misc_feature assets_misc_interface assets_custom_loader) + message(STATUS "Include misc assets") endif() -if (FEATURE_MISC_EMBEDDED_ASSETS) - target_link_libraries(bongocat PRIVATE assets_misc_loader assets_misc assets_misc_feature assets_misc_interface assets_custom_loader) - message(STATUS "Include misc assets") +if(FEATURE_CUSTOM_SPRITE_SHEETS) + target_compile_definitions(bongocat PRIVATE FEATURE_CUSTOM_SPRITE_SHEETS) + target_link_libraries(bongocat PRIVATE assets_custom_sprite_sheet_feature assets_custom_loader) endif() -if (FEATURE_CUSTOM_SPRITE_SHEETS) - target_compile_definitions(bongocat PRIVATE FEATURE_CUSTOM_SPRITE_SHEETS) - target_link_libraries(bongocat PRIVATE assets_custom_sprite_sheet_feature assets_custom_loader) -endif () # @NOTE(assets): 1.1. link embedded assets target_link_libraries(bongocat PRIVATE bongocat_base bongocat_options bongocat_libs) -if (FEATURE_MULTI_VERSIONS) - add_executable(bongocat-dm-classic) - target_link_libraries(bongocat-dm-classic PRIVATE assets_bongocat_loader assets_bongocat assets_bongocat_feature assets_bongocat_interface) # include bongocat as fallback - target_link_libraries(bongocat-dm-classic PRIVATE assets_dm_loader assets_dm assets_dm_feature assets_dm_interface) - target_link_libraries(bongocat-dm-classic PRIVATE assets_dm20_loader assets_dm20 assets_dm20_feature assets_dm20_interface) - target_link_libraries(bongocat-dm-classic PRIVATE assets_dmx_loader assets_dmx assets_dmx_feature assets_dmx_interface) - target_link_libraries(bongocat-dm-classic PRIVATE assets_pen_loader assets_pen assets_pen_feature assets_pen_interface) - target_link_libraries(bongocat-dm-classic PRIVATE assets_pen20_loader assets_pen20 assets_pen20_feature assets_pen20_interface) - target_link_libraries(bongocat-dm-classic PRIVATE bongocat_base bongocat_options bongocat_libs) - - add_executable(bongocat-dm-colored) - target_link_libraries(bongocat-dm-colored PRIVATE assets_bongocat_loader assets_bongocat assets_bongocat_feature assets_bongocat_interface) - target_link_libraries(bongocat-dm-colored PRIVATE assets_dm20_loader assets_dm20 assets_dm20_feature assets_dm20_interface) - target_link_libraries(bongocat-dm-colored PRIVATE assets_dmx_loader assets_dmx assets_dmx_feature assets_dmx_interface) - target_link_libraries(bongocat-dm-colored PRIVATE assets_dmc_loader assets_dmc assets_dmc_feature assets_dmc_interface) - target_link_libraries(bongocat-dm-colored PRIVATE assets_pen20_loader assets_pen20 assets_pen20_feature assets_pen20_interface) - target_link_libraries(bongocat-dm-colored PRIVATE bongocat_base bongocat_options bongocat_libs) - - add_executable(bongocat-dm-colored-all) - target_link_libraries(bongocat-dm-colored-all PRIVATE assets_bongocat_loader assets_bongocat assets_bongocat_feature assets_bongocat_interface) - target_link_libraries(bongocat-dm-colored-all PRIVATE assets_dmall_loader assets_dmall assets_dmall_feature assets_dmall_interface) - target_link_libraries(bongocat-dm-colored-all PRIVATE bongocat_base bongocat_options bongocat_libs) - - add_executable(bongocat-dm) - target_link_libraries(bongocat-dm PRIVATE assets_bongocat_loader assets_bongocat assets_bongocat_feature assets_bongocat_interface) - target_link_libraries(bongocat-dm PRIVATE assets_dm20_loader assets_dm20 assets_dm20_feature assets_dm20_interface) - target_link_libraries(bongocat-dm PRIVATE assets_dmx_loader assets_dmx assets_dmx_feature assets_dmx_interface) - target_link_libraries(bongocat-dm PRIVATE assets_pen20_loader assets_pen20 assets_pen20_feature assets_pen20_interface) - target_link_libraries(bongocat-dm PRIVATE assets_dmall_loader assets_dmall assets_dmall_feature assets_dmall_interface) - target_link_libraries(bongocat-dm PRIVATE bongocat_base bongocat_options bongocat_libs) - - add_executable(bongocat-ms-agent) - target_link_libraries(bongocat-ms-agent PRIVATE assets_bongocat_loader assets_bongocat assets_bongocat_feature assets_bongocat_interface) - target_link_libraries(bongocat-ms-agent PRIVATE assets_more_ms_agent_loader assets_more_ms_agent assets_ms_agent_feature assets_more_ms_agent_interface) - target_link_libraries(bongocat-ms-agent PRIVATE bongocat_base bongocat_options bongocat_libs) - - add_executable(bongocat-pkmn) - target_link_libraries(bongocat-pkmn PRIVATE assets_bongocat_loader assets_bongocat assets_bongocat_feature assets_bongocat_interface) - target_link_libraries(bongocat-pkmn PRIVATE assets_pkmn_loader assets_pkmn assets_pkmn_feature assets_pkmn_interface) - target_link_libraries(bongocat-pkmn PRIVATE assets_pmd_loader assets_pmd assets_pmd_feature assets_pmd_interface assets_custom_loader) - target_link_libraries(bongocat-pkmn PRIVATE bongocat_base bongocat_options bongocat_libs) - - add_executable(bongocat-all) - target_link_libraries(bongocat-all PRIVATE assets_bongocat_loader assets_bongocat assets_bongocat_feature assets_bongocat_interface) # include bongocat as fallback - target_link_libraries(bongocat-all PRIVATE assets_dm_loader assets_dm assets_dm_feature assets_dm_interface) - target_link_libraries(bongocat-all PRIVATE assets_dm20_loader assets_dm20 assets_dm20_feature assets_dm20_interface) - target_link_libraries(bongocat-all PRIVATE assets_dmx_loader assets_dmx assets_dmx_feature assets_dmx_interface) - target_link_libraries(bongocat-all PRIVATE assets_dmc_loader assets_dmc assets_dmc_feature assets_dmc_interface) - target_link_libraries(bongocat-all PRIVATE assets_pen_loader assets_pen assets_pen_feature assets_pen_interface) - target_link_libraries(bongocat-all PRIVATE assets_pen20_loader assets_pen20 assets_pen20_feature assets_pen20_interface) - target_link_libraries(bongocat-all PRIVATE assets_dmall_loader assets_dmall assets_dmall_feature assets_dmall_interface) - target_link_libraries(bongocat-all PRIVATE assets_more_ms_agent_loader assets_more_ms_agent assets_more_ms_agent_feature assets_more_ms_agent_interface) - target_link_libraries(bongocat-all PRIVATE assets_pkmn_loader assets_pkmn assets_pkmn_feature assets_pkmn_interface) - # TODO: include pmd in -all when ready (experimental) - target_link_libraries(bongocat-all PRIVATE - # only include pmd for testing - $<$:assets_pmd_loader assets_pmd assets_pmd_feature assets_pmd_interface assets_custom_loader> - $<$:assets_pmd_loader assets_pmd assets_pmd_feature assets_pmd_interface assets_custom_loader> - $<$:assets_pmd_loader assets_pmd assets_pmd_feature assets_pmd_interface assets_custom_loader> - ) - #target_link_libraries(bongocat-all PRIVATE assets_pmd_loader assets_pmd assets_pmd_feature assets_pmd_interface assets_custom_loader) - target_link_libraries(bongocat-all PRIVATE assets_misc_loader assets_misc assets_misc_feature assets_misc_interface assets_custom_loader) - target_link_libraries(bongocat-all PRIVATE assets_custom_sprite_sheet_feature assets_custom_loader) - target_link_libraries(bongocat-all PRIVATE bongocat_base bongocat_options bongocat_libs) - - add_executable(bongocat-neko) - target_link_libraries(bongocat-neko PRIVATE assets_bongocat_loader assets_bongocat assets_bongocat_feature assets_bongocat_interface) - target_link_libraries(bongocat-neko PRIVATE assets_misc_loader assets_misc assets_misc_feature assets_misc_interface assets_custom_loader) - target_link_libraries(bongocat-neko PRIVATE bongocat_base bongocat_options bongocat_libs) - - # @NOTE(assets): 1.2. add exec for multi versions +if(FEATURE_MULTI_VERSIONS) + add_executable(bongocat-dm-classic) + target_link_libraries(bongocat-dm-classic PRIVATE assets_bongocat_loader assets_bongocat assets_bongocat_feature + assets_bongocat_interface) # include bongocat as fallback + target_link_libraries(bongocat-dm-classic PRIVATE assets_dm_loader assets_dm assets_dm_feature assets_dm_interface) + target_link_libraries(bongocat-dm-classic PRIVATE assets_dm20_loader assets_dm20 assets_dm20_feature assets_dm20_interface) + target_link_libraries(bongocat-dm-classic PRIVATE assets_dmx_loader assets_dmx assets_dmx_feature assets_dmx_interface) + target_link_libraries(bongocat-dm-classic PRIVATE assets_pen_loader assets_pen assets_pen_feature assets_pen_interface) + target_link_libraries(bongocat-dm-classic PRIVATE assets_pen20_loader assets_pen20 assets_pen20_feature assets_pen20_interface) + target_link_libraries(bongocat-dm-classic PRIVATE bongocat_base bongocat_options bongocat_libs) + + add_executable(bongocat-dm-colored) + target_link_libraries(bongocat-dm-colored PRIVATE assets_bongocat_loader assets_bongocat assets_bongocat_feature + assets_bongocat_interface) + target_link_libraries(bongocat-dm-colored PRIVATE assets_dm20_loader assets_dm20 assets_dm20_feature assets_dm20_interface) + target_link_libraries(bongocat-dm-colored PRIVATE assets_dmx_loader assets_dmx assets_dmx_feature assets_dmx_interface) + target_link_libraries(bongocat-dm-colored PRIVATE assets_dmc_loader assets_dmc assets_dmc_feature assets_dmc_interface) + target_link_libraries(bongocat-dm-colored PRIVATE assets_pen20_loader assets_pen20 assets_pen20_feature assets_pen20_interface) + target_link_libraries(bongocat-dm-colored PRIVATE bongocat_base bongocat_options bongocat_libs) + + add_executable(bongocat-dm-colored-all) + target_link_libraries(bongocat-dm-colored-all PRIVATE assets_bongocat_loader assets_bongocat assets_bongocat_feature + assets_bongocat_interface) + target_link_libraries(bongocat-dm-colored-all PRIVATE assets_dmall_loader assets_dmall assets_dmall_feature assets_dmall_interface) + target_link_libraries(bongocat-dm-colored-all PRIVATE bongocat_base bongocat_options bongocat_libs) + + add_executable(bongocat-dm) + target_link_libraries(bongocat-dm PRIVATE assets_bongocat_loader assets_bongocat assets_bongocat_feature assets_bongocat_interface) + target_link_libraries(bongocat-dm PRIVATE assets_dm20_loader assets_dm20 assets_dm20_feature assets_dm20_interface) + target_link_libraries(bongocat-dm PRIVATE assets_dmx_loader assets_dmx assets_dmx_feature assets_dmx_interface) + target_link_libraries(bongocat-dm PRIVATE assets_pen20_loader assets_pen20 assets_pen20_feature assets_pen20_interface) + target_link_libraries(bongocat-dm PRIVATE assets_dmall_loader assets_dmall assets_dmall_feature assets_dmall_interface) + target_link_libraries(bongocat-dm PRIVATE bongocat_base bongocat_options bongocat_libs) + + add_executable(bongocat-ms-agent) + target_link_libraries(bongocat-ms-agent PRIVATE assets_bongocat_loader assets_bongocat assets_bongocat_feature assets_bongocat_interface) + target_link_libraries(bongocat-ms-agent PRIVATE assets_more_ms_agent_loader assets_more_ms_agent assets_ms_agent_feature + assets_more_ms_agent_interface) + target_link_libraries(bongocat-ms-agent PRIVATE bongocat_base bongocat_options bongocat_libs) + + add_executable(bongocat-pkmn) + target_link_libraries(bongocat-pkmn PRIVATE assets_bongocat_loader assets_bongocat assets_bongocat_feature assets_bongocat_interface) + target_link_libraries(bongocat-pkmn PRIVATE assets_pkmn_loader assets_pkmn assets_pkmn_feature assets_pkmn_interface) + target_link_libraries(bongocat-pkmn PRIVATE assets_pmd_loader assets_pmd assets_pmd_feature assets_pmd_interface assets_custom_loader) + target_link_libraries(bongocat-pkmn PRIVATE bongocat_base bongocat_options bongocat_libs) + + add_executable(bongocat-all) + target_link_libraries(bongocat-all PRIVATE assets_bongocat_loader assets_bongocat assets_bongocat_feature assets_bongocat_interface + )# include bongocat as fallback + target_link_libraries(bongocat-all PRIVATE assets_dm_loader assets_dm assets_dm_feature assets_dm_interface) + target_link_libraries(bongocat-all PRIVATE assets_dm20_loader assets_dm20 assets_dm20_feature assets_dm20_interface) + target_link_libraries(bongocat-all PRIVATE assets_dmx_loader assets_dmx assets_dmx_feature assets_dmx_interface) + target_link_libraries(bongocat-all PRIVATE assets_dmc_loader assets_dmc assets_dmc_feature assets_dmc_interface) + target_link_libraries(bongocat-all PRIVATE assets_pen_loader assets_pen assets_pen_feature assets_pen_interface) + target_link_libraries(bongocat-all PRIVATE assets_pen20_loader assets_pen20 assets_pen20_feature assets_pen20_interface) + target_link_libraries(bongocat-all PRIVATE assets_dmall_loader assets_dmall assets_dmall_feature assets_dmall_interface) + target_link_libraries(bongocat-all PRIVATE assets_more_ms_agent_loader assets_more_ms_agent assets_more_ms_agent_feature + assets_more_ms_agent_interface) + target_link_libraries(bongocat-all PRIVATE assets_pkmn_loader assets_pkmn assets_pkmn_feature assets_pkmn_interface) + # TODO: include pmd in -all when ready (experimental) + target_link_libraries( + bongocat-all + PRIVATE # only include pmd for testing + $<$:assets_pmd_loader + assets_pmd + assets_pmd_feature + assets_pmd_interface + assets_custom_loader> + $<$:assets_pmd_loader + assets_pmd + assets_pmd_feature + assets_pmd_interface + assets_custom_loader> + $<$:assets_pmd_loader + assets_pmd + assets_pmd_feature + assets_pmd_interface + assets_custom_loader>) + # target_link_libraries(bongocat-all PRIVATE assets_pmd_loader assets_pmd assets_pmd_feature assets_pmd_interface assets_custom_loader) + target_link_libraries(bongocat-all PRIVATE assets_misc_loader assets_misc assets_misc_feature assets_misc_interface assets_custom_loader) + target_link_libraries(bongocat-all PRIVATE assets_custom_sprite_sheet_feature assets_custom_loader) + target_link_libraries(bongocat-all PRIVATE bongocat_base bongocat_options bongocat_libs) + + add_executable(bongocat-neko) + target_link_libraries(bongocat-neko PRIVATE assets_bongocat_loader assets_bongocat assets_bongocat_feature assets_bongocat_interface) + target_link_libraries(bongocat-neko PRIVATE assets_misc_loader assets_misc assets_misc_feature assets_misc_interface assets_custom_loader) + target_link_libraries(bongocat-neko PRIVATE bongocat_base bongocat_options bongocat_libs) + + # @NOTE(assets): 1.2. add exec for multi versions endif() - +# Formatting +include(cmake/CPM.cmake) +cpmaddpackage( + NAME + Format.cmake + VERSION + 1.8.3 + GITHUB_REPOSITORY + TheLartians/Format.cmake + OPTIONS + # set to yes skip cmake formatting + "FORMAT_SKIP_CMAKE NO" + # set to yes skip clang formatting + "FORMAT_SKIP_CLANG NO" + # path to exclude (optional, supports regular expressions) + "CMAKE_FORMAT_EXCLUDE cmake/CPM.cmake" + # extra arguments for cmake_format (optional) + "CMAKE_FORMAT_EXTRA_ARGS -c ${PROJECT_SOURCE_DIR}/cmake-format.yaml" + # path to exclude (optional, supports regular expressions) + "CMAKE_FORMAT_EXCLUDE lib|assets|protocols|docs|examples|nix") # Install include(GNUInstallDirs) -if (FEATURE_MULTI_VERSIONS) - # sudo cmake --install ./cmake-build-release-all-assets-multi - install(TARGETS bongocat bongocat-dm-classic bongocat-dm bongocat-ms-agent bongocat-pkmn bongocat-all COMPONENT bongocat-targets RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}) +if(FEATURE_MULTI_VERSIONS) + # sudo cmake --install ./cmake-build-release-all-assets-multi + install( + TARGETS bongocat bongocat-dm-classic bongocat-dm bongocat-ms-agent bongocat-pkmn bongocat-all + COMPONENT bongocat-targets + RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}) else() - install(TARGETS bongocat RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}) + install(TARGETS bongocat RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}) +endif() +install( + FILES bongocat.conf.example + DESTINATION ${CMAKE_INSTALL_DATADIR}/bongocat + RENAME bongocat.conf.example) +install( + PROGRAMS scripts/find_input_devices.sh + DESTINATION ${CMAKE_INSTALL_BINDIR} + RENAME bongocat-find-devices) +if(FEATURE_MULTI_VERSIONS) + # more configs + install(FILES examples/clippy.bongocat.conf examples/digimon.bongocat.conf examples/pokemon.bongocat.conf + DESTINATION ${CMAKE_INSTALL_DATADIR}/bongocat) endif() -install(FILES bongocat.conf DESTINATION ${CMAKE_INSTALL_DATADIR}/bongocat RENAME bongocat.conf.example) -install(PROGRAMS scripts/find_input_devices.sh DESTINATION ${CMAKE_INSTALL_BINDIR} RENAME bongocat-find-devices) -if (FEATURE_MULTI_VERSIONS) - # more configs - install(FILES examples/clippy.bongocat.conf examples/digimon.bongocat.conf examples/pokemon.bongocat.conf DESTINATION ${CMAKE_INSTALL_DATADIR}/bongocat) -endif () # man pages find_program(PANDOC pandoc) -if (PANDOC) - # List all binaries - set(BINARY_TARGETS - bongocat - bongocat-dm-classic - bongocat-dm - bongocat-ms-agent - bongocat-pkmn - bongocat-all - ) - - set(ALL_MAN_OUTPUTS) - foreach(BIN ${BINARY_TARGETS}) - set(MD_FILE_1 ${PROJECT_SOURCE_DIR}/docs/${BIN}.md) - set(MAN_OUTPUT_1 ${CMAKE_CURRENT_BINARY_DIR}/${BIN}.1) - # man exec - add_custom_command( - OUTPUT ${MAN_OUTPUT_1} - WORKING_DIRECTORY ${PROJECT_SOURCE_DIR}/docs - COMMAND ${PANDOC} --lua-filter=${PROJECT_SOURCE_DIR}/docs/include.lua -s -t man -o ${MAN_OUTPUT_1} ${MD_FILE_1} - DEPENDS ${MD_FILE_1} - COMMENT "Generating man page for ${BIN} using Pandoc" - VERBATIM - ) - - set(MD_FILE_5 ${PROJECT_SOURCE_DIR}/docs/${BIN}.conf.md) - set(MAN_OUTPUT_5 ${CMAKE_CURRENT_BINARY_DIR}/${BIN}.5) - # man conf - add_custom_command( - OUTPUT ${MAN_OUTPUT_5} - WORKING_DIRECTORY ${PROJECT_SOURCE_DIR}/docs - COMMAND ${PANDOC} --lua-filter=${PROJECT_SOURCE_DIR}/docs/include.lua -s -t man -o ${MAN_OUTPUT_5} ${PROJECT_SOURCE_DIR}/docs/begin.base.bongocat.conf.md ${MD_FILE_5} ${PROJECT_SOURCE_DIR}/docs/end.base.bongocat.conf.md - DEPENDS ${MD_FILE_5} ${PROJECT_SOURCE_DIR}/docs/begin.base.bongocat.conf.md ${MD_FILE_5} ${PROJECT_SOURCE_DIR}/docs/end.base.bongocat.conf.md - COMMENT "Generating man page for ${BIN}.conf using Pandoc" - VERBATIM - ) - - # Target to build manpages - add_custom_target(manpages-${BIN} ALL DEPENDS ${MAN_OUTPUT_1} ${MAN_OUTPUT_5}) - list(APPEND ALL_MAN_OUTPUTS ${MAN_OUTPUT_1} ${MAN_OUTPUT_5}) - - # Install manpage - install( - FILES ${MAN_OUTPUT_1} - DESTINATION ${CMAKE_INSTALL_MANDIR}/man1 - ) - install( - FILES ${MAN_OUTPUT_5} - DESTINATION ${CMAKE_INSTALL_MANDIR}/man5 - ) - endforeach() - - # find-devices - set(FD_MD_FILE_1 ${PROJECT_SOURCE_DIR}/docs/bongocat-find-devices.md) - set(FD_MAN_OUTPUT_1 ${CMAKE_CURRENT_BINARY_DIR}/bongocat-find-devices.1) +if(PANDOC) + # List all binaries + set(BINARY_TARGETS bongocat bongocat-dm-classic bongocat-dm bongocat-ms-agent bongocat-pkmn bongocat-all) + + set(ALL_MAN_OUTPUTS) + foreach(BIN ${BINARY_TARGETS}) + set(MD_FILE_1 ${PROJECT_SOURCE_DIR}/docs/${BIN}.md) + set(MAN_OUTPUT_1 ${CMAKE_CURRENT_BINARY_DIR}/${BIN}.1) + # man exec add_custom_command( - OUTPUT ${FD_MAN_OUTPUT_1} - WORKING_DIRECTORY ${PROJECT_SOURCE_DIR}/docs - COMMAND ${PANDOC} --lua-filter=${PROJECT_SOURCE_DIR}/docs/include.lua -s -t man -o ${FD_MAN_OUTPUT_1} ${FD_MD_FILE_1} - DEPENDS ${FD_MD_FILE_1} - COMMENT "Generating man page for bongocat-find-devices using Pandoc" - VERBATIM - ) - add_custom_target(manpages-bongocat-find-devices ALL DEPENDS ${FD_MAN_OUTPUT_1}) - list(APPEND ALL_MAN_OUTPUTS ${FD_MAN_OUTPUT_1}) - install( - FILES ${FD_MAN_OUTPUT_1} - DESTINATION ${CMAKE_INSTALL_MANDIR}/man1 - ) - - add_custom_target(manpages ALL DEPENDS ${ALL_MAN_OUTPUTS}) + OUTPUT ${MAN_OUTPUT_1} + WORKING_DIRECTORY ${PROJECT_SOURCE_DIR}/docs + COMMAND ${PANDOC} --lua-filter=${PROJECT_SOURCE_DIR}/docs/include.lua -s -t man -o ${MAN_OUTPUT_1} ${MD_FILE_1} + DEPENDS ${MD_FILE_1} + COMMENT "Generating man page for ${BIN} using Pandoc" + VERBATIM) + + set(MD_FILE_5 ${PROJECT_SOURCE_DIR}/docs/${BIN}.conf.md) + set(MAN_OUTPUT_5 ${CMAKE_CURRENT_BINARY_DIR}/${BIN}.5) + # man conf + add_custom_command( + OUTPUT ${MAN_OUTPUT_5} + WORKING_DIRECTORY ${PROJECT_SOURCE_DIR}/docs + COMMAND ${PANDOC} --lua-filter=${PROJECT_SOURCE_DIR}/docs/include.lua -s -t man -o ${MAN_OUTPUT_5} + ${PROJECT_SOURCE_DIR}/docs/begin.base.bongocat.conf.md ${MD_FILE_5} ${PROJECT_SOURCE_DIR}/docs/end.base.bongocat.conf.md + DEPENDS ${MD_FILE_5} ${PROJECT_SOURCE_DIR}/docs/begin.base.bongocat.conf.md ${MD_FILE_5} + ${PROJECT_SOURCE_DIR}/docs/end.base.bongocat.conf.md + COMMENT "Generating man page for ${BIN}.conf using Pandoc" + VERBATIM) + + # Target to build manpages + add_custom_target(manpages-${BIN} ALL DEPENDS ${MAN_OUTPUT_1} ${MAN_OUTPUT_5}) + list(APPEND ALL_MAN_OUTPUTS ${MAN_OUTPUT_1} ${MAN_OUTPUT_5}) + + # Install manpage + install(FILES ${MAN_OUTPUT_1} DESTINATION ${CMAKE_INSTALL_MANDIR}/man1) + install(FILES ${MAN_OUTPUT_5} DESTINATION ${CMAKE_INSTALL_MANDIR}/man5) + endforeach() + + # find-devices + set(FD_MD_FILE_1 ${PROJECT_SOURCE_DIR}/docs/bongocat-find-devices.md) + set(FD_MAN_OUTPUT_1 ${CMAKE_CURRENT_BINARY_DIR}/bongocat-find-devices.1) + add_custom_command( + OUTPUT ${FD_MAN_OUTPUT_1} + WORKING_DIRECTORY ${PROJECT_SOURCE_DIR}/docs + COMMAND ${PANDOC} --lua-filter=${PROJECT_SOURCE_DIR}/docs/include.lua -s -t man -o ${FD_MAN_OUTPUT_1} ${FD_MD_FILE_1} + DEPENDS ${FD_MD_FILE_1} + COMMENT "Generating man page for bongocat-find-devices using Pandoc" + VERBATIM) + add_custom_target(manpages-bongocat-find-devices ALL DEPENDS ${FD_MAN_OUTPUT_1}) + list(APPEND ALL_MAN_OUTPUTS ${FD_MAN_OUTPUT_1}) + install(FILES ${FD_MAN_OUTPUT_1} DESTINATION ${CMAKE_INSTALL_MANDIR}/man1) + + add_custom_target(manpages ALL DEPENDS ${ALL_MAN_OUTPUTS}) endif() # Package +cpmaddpackage("gh:TheLartians/PackageProject.cmake@1.13.0") set(CPACK_PACKAGE_NAME "bongocat") -set(CPACK_PACKAGE_VERSION "3.5.0") +set(CPACK_PACKAGE_VERSION "3.6.0") set(CPACK_PACKAGE_CONTACT "hircreacc@gmail.com") set(CPACK_PACKAGE_DESCRIPTION_SUMMARY "A delightful Wayland overlay that displays an animated V-Pet reacting to your keyboard input! ") set(CPACK_RESOURCE_FILE_LICENSE "${PROJECT_SOURCE_DIR}/LICENSE") set(CPACK_GENERATOR "TGZ;ZIP") -#set(CPACK_DEBIAN_PACKAGE_DEPENDS "libc6, libstdc++6, libgcc-s1, libwayland-client0, libffi8") -include(CPack) +packageproject( + # the name of the target to export + NAME + ${PROJECT_NAME} + # the version of the target to export + VERSION + ${PROJECT_VERSION} + # a temporary directory to create the config files + BINARY_DIR + ${PROJECT_BINARY_DIR} + # should match the target's INSTALL_INTERFACE include directory + INCLUDE_DESTINATION + include/${PROJECT_NAME}-${PROJECT_VERSION} + # (optional) define the project's version compatibility, defaults to `AnyNewerVersion` supported values: + # `AnyNewerVersion|SameMajorVersion|SameMinorVersion|ExactVersion` + COMPATIBILITY + SameMajorVersion + # (optional) option to generate CPack variables + CPACK + YES) # If MSVC is being used, and ASAN is enabled, we need to set the debugger environment so that it behaves well with MSVC's debugger, and we -# can run the target from visual studio -#if(MSVC) -# get_all_installable_targets(all_targets) -# message("all_targets=${all_targets}") -# set_target_properties(${all_targets} PROPERTIES VS_DEBUGGER_ENVIRONMENT "PATH=$(VC_ExecutablePath_x64);%PATH%") -#endif() +# can run the target from visual studio if(MSVC) get_all_installable_targets(all_targets) message("all_targets=${all_targets}") +# set_target_properties(${all_targets} PROPERTIES VS_DEBUGGER_ENVIRONMENT "PATH=$(VC_ExecutablePath_x64);%PATH%") endif() diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index af2f1b5f..3225778f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -5,18 +5,6 @@ This guide will help you get started, whether you want to report issues, suggest --- -## Table of Contents - -1. [Reporting Issues](#reporting-issues) -2. [Feature Requests](#feature-requests) -3. [Development Setup](#development-setup) -4. [Code Standards](#code-standards) -5. [Submitting Pull Requests](#submitting-pull-requests) -6. [Style Guidelines](#style-guidelines) -7. [License](#license) - ---- - ## Reporting Issues If you encounter a bug or unexpected behavior: @@ -30,8 +18,6 @@ If you encounter a bug or unexpected behavior: > Please avoid sending private screenshots of proprietary content; only include relevant logs and minimal reproduction steps. ---- - ## Feature Requests Feature requests are welcome! To submit a request: @@ -45,13 +31,16 @@ Feature requests are welcome! To submit a request: --- -## Development Setup +## Getting Started ### Prerequisites -See [Building from Source](README.md#-building-from-source) for detailed instructions. +- Wayland compositor with layer shell support +- GCC or Clang (C++23/C23 support) +- wayland-client, wayland-protocols +- Make and CMake -Quick start: +### Building ```bash git clone https://github.com/furudbat/wayland-vpets.git @@ -60,11 +49,21 @@ cmake -S . -B build -DCMAKE_BUILD_TYPE=Debug cmake --build build ```` -> Legacy `make debug` is supported for old Bongo Cat workflows, or you can just use CMake. +> Legacy `make debug` is supported for old Bongo Cat dev workflows. ---- +`make debug` provides a quick debug build. +You can inspect the old workflow in the old [`Makefile`](Makefile.old). +_**Note:** The binary name in AUR is `wpets`, but during development and `make install` it is still `bongocat`._ -## Code Standards +### Running + +```bash +./build/bongocat -c bongocat.conf -w +``` + +## Development Workflow + +### Code Standards Follow the project’s coding guidelines: @@ -76,6 +75,13 @@ Follow the project’s coding guidelines: * **Assets**: Embed large assets in separate TUs * **Global State**: Avoid globals, pass context structs +_Run `make format` before committing_ + +#### Moving to C++ + +This project is migrated to C++ while retaining a C-style foundation for performance, Wayland compatibility and mosty compatible with [upstream](https://github.com/saatvik333/wayland-bongocat). +The codebase remains largely C under the hood, using Linux + Wayland libraries, while gradually adopting modern C++ practices for safety and maintainability. + ### Key practices * **Modern Compiler Features:** Requires C23/C++23 (`#embed`) @@ -105,23 +111,20 @@ Follow the project’s coding guidelines: * Prefer `create` functions with RVO instead of `init` with out-parameters * Stop all threads before releasing memory ---- - -## Submitting Pull Requests +### Making Changes 1. Fork the repository and create a branch for your feature or fix: - ```bash git checkout -b feature/my-new-feature ``` 2. Make your changes following the code standards. -3. Test your changes thoroughly, including multi-monitor and keyboard input scenarios. +3. Test your changes thoroughly, including multi-monitor and keyboard input scenarios (if you can). 4. Commit your changes with clear messages: ```bash git commit -m "feat: Add support for X feature" ``` -5. Push your branch and open a Pull Request against `main`. +5. Push your branch and open a Pull Request against `develop`. 6. Include a description of what your PR changes and any relevant screenshots or logs. --- @@ -130,18 +133,92 @@ Follow the project’s coding guidelines: * **Branch Naming**: `feature/xxx`, `bugfix/xxx`, `docs/xxx` * **Commit Messages**: Use present tense, concise, and descriptive -* **Formatting**: try to Follow existing formatting and indentation, -- @TODO: add `.clang-format` and `.clang-tidy` +* **Formatting**: try to Follow existing formatting and indentation * **Documentation**: Update relevant README sections if needed - * when adding new configuration, pls update [`bongocat.conf`](bongocat.conf) + * when adding new configuration, pls update [`bongocat.conf.example`](bongocat.conf.example) * when adding new program arguments update [`cli_show_help` in main](src/core/main.cpp) * update [man pages](docs/fragments/options.md) ---- +### Commit Messages + +Use conventional commits: + +``` +feat: add new feature +fix: resolve bug +docs: update documentation +refactor: improve code structure +``` + +## Code Structure + + +- `assets/` - Sprite sheets and media +- `src/` - Core application logic and platform-specific code +- `include/` - Headers +- `scripts/` - Utilities and codegen +- `lib/` - External libraries + +``` +wayland-vpets/ +├── assets/ # sprite sheets and media resources +├── Dockerfiles/ # Container build definitions +├── examples/ # Example configurations +├── include/ # Header files (same structure as src/) +├── lib/ # External libraries (image loader) +├── nix/ # NixOS integration +├── protocols/ # Generated Wayland protocols +├── scripts/ # Codegen and utility scripts +└── src/ # Source code +├──── config/ # Configuration system implementation +├──── core/ # Core application logic (main) +├──── embedded_assets/ # Embedded assets +├──── graphics/ # Rendering and graphics implementation +├──── image_loader/ # Assets loading implementations +├──── platform/ # Platform-specific code (input and wayland) +└──── utils/ # General utilities +``` + + +## Testing + +```bash +# Run with debug logging +./build/bongocat -c bongocat.conf -w +# Build with Sanitizers (UBSAN,ASAN) enabled for checking for memory leaks +``` + +### Test Scripts + +```bash +./scripts/test_bongocat.sh +``` + +There are also some test scripts, they are just for running, reloading and changing config for integration tests, meaning it's just for triggering the `asserts`. +Also test on your own and trust your eyes when testing four your rice :) + +## Reporting Issues + +When reporting bugs, please include: + +- Your compositor (Sway, Hyprland, etc.) +- Config file contents +- Debug output (`enable_debug=1`) ## License All contributions must comply with the project’s MIT License. By submitting code, you agree to license your contributions under MIT. +
+Copyright + +This project is **free**, **non-commercial** and not associated with these entities. +Pokémon are owned by Nintendo, Creatures Inc. and GAME FREAK Inc. +Digimon and all related characters, and associated images are owned by Bandai Co., Ltd, Akiyoshi Hongo, and Toei Animation Co., Ltd. +Clippy and other MS Agents are owed by Microsoft. +See [COPYRIGHT](assets/COPYRIGHT.md) for more details. +
+ --- Thank you for helping make **Wayland Bongo Cat + V-Pets** a better, more delightful overlay! 💖 diff --git a/Makefile b/Makefile index dcfb1f32..c08c9681 100644 --- a/Makefile +++ b/Makefile @@ -1,22 +1,30 @@ # keep Makefile for older scripts, cmake still needed + +# Directories +SRCDIR = src +INCDIR = include BUILDDIR = build +OBJDIR = $(BUILDDIR)/obj +PROTOCOLDIR = protocols +WAYLAND_PROTOCOLS_DIR ?= /usr/share/wayland-protocols ONLY_BONGOCAT ?= 1 ifeq ($(ONLY_BONGOCAT),1) FEATURES = -DFEATURE_BONGOCAT_EMBEDDED_ASSETS else - FEATURES = -DFEATURE_BONGOCAT_EMBEDDED_ASSETS -DFEATURE_ENABLE_DM_EMBEDDED_ASSETS -DFEATURE_MS_AGENT_EMBEDDED_ASSETS + FEATURES = -DFEATURE_BONGOCAT_EMBEDDED_ASSETS -DFEATURE_ENABLE_DM_EMBEDDED_ASSETS -DFEATURE_MS_AGENT_EMBEDDED_ASSETS -DFEATURE_PKMN_EMBEDDED_ASSETS endif +# Target executable +TARGET = $(BUILDDIR)/bongocat + +.PHONY: all format format-check lint # Default target default: build all: build -.PHONY: all build release build-debug debug clean default install - - # Release build build: cmake -S . -B build -DCMAKE_BUILD_TYPE=Release @@ -43,6 +51,49 @@ pack: release doc doc: release cmake --build build --target manpages -# Clean build directory -clean: - rm -rf build +SOURCES = $(shell find $(SRCDIR) -name '*.cpp' ! -path '*/embedded_assets/**/*.c??' ! -path '*/image_loader/*.c' ! -path '*/image_loader/**/*.cpp') +# Static analysis +analyze: + clang-tidy -p build $(SOURCES) + +# Memory check (requires valgrind) +memcheck: debug + valgrind --leak-check=full --show-leak-kinds=all --track-origins=yes ./$(TARGET) + +# Performance profiling +profile: release + perf record -g ./$(TARGET) + perf report + +# ============================================================================= +# CODE QUALITY TARGETS +# ============================================================================= + +# Find all project source files (exclude lib/ and protocols/) +PROJECT_SOURCES = $(shell find $(SRCDIR) -name '*.cpp' ! -path '*/embedded_assets/**/*.c??' ! -path '*/image_loader/*.c' ! -path '*/image_loader/**/*.cpp') +PROJECT_HEADERS = $(shell find $(INCDIR) -name '*.h' ! -path '*/embedded_assets/**/*.h' ! -path '*/image_loader/**/*.h') +ALL_PROJECT_FILES = $(PROJECT_SOURCES) $(PROJECT_HEADERS) + +# Format all project source files +format: + cmake --build build --target fix-format + +# Check if formatting is correct (for CI) +format-check: + cmake --build build --target check-format + +# Static analysis with clang-tidy (uses .clang-tidy config) +lint: protocols + @echo "Running static analysis..." + @clang-tidy -p build $(PROJECT_SOURCES) 2>/dev/null || true + @echo "Static analysis complete." + +# Alias for lint +analyze: lint + +# Generate compile_commands.json for IDE support, or just use cmake and presets +# Run: make compiledb +compiledb: clean + cmake -S . -B build -DCMAKE_EXPORT_COMPILE_COMMANDS=ON + +.PHONY: compiledb \ No newline at end of file diff --git a/Makefile.old b/Makefile.old index 8fdbcb6f..464226f0 100644 --- a/Makefile.old +++ b/Makefile.old @@ -16,14 +16,14 @@ PROTOCOLDIR = protocols WAYLAND_PROTOCOLS_DIR ?= /usr/share/wayland-protocols # Base flags -BASE_CFLAGS = -std=c23 -I$(INCDIR) -I$(SRCDIR) -isystem lib -isystem protocols # -fembed-dir=assets/ +BASE_CFLAGS = -std=c2x -I$(INCDIR) -I$(SRCDIR) -isystem lib -isystem protocols BASE_CFLAGS += -Wall -Wextra -Wpedantic -Wformat=2 -Wstrict-prototypes BASE_CFLAGS += -Wmissing-prototypes -Wold-style-definition -Wredundant-decls BASE_CFLAGS += -Wnested-externs -Wmissing-include-dirs -Wlogical-op BASE_CFLAGS += -Wjump-misses-init -Wdouble-promotion -Wshadow -BASE_CFLAGS += -fstack-protector-strong +BASE_CFLAGS += -fstack-protector-strong -D_FORTIFY_SOURCE=2 -BASE_CXXFLAGS = -std=c++23 -I$(INCDIR) -I$(SRCDIR) -isystem lib -isystem protocols # -fembed-dir=assets/ +BASE_CXXFLAGS = -std=c++23 -I$(INCDIR) -I$(SRCDIR) -isystem lib -isystem protocols BASE_CXXFLAGS += -Wall -Wextra -Wpedantic -Wformat=2 BASE_CXXFLAGS += -Wredundant-decls BASE_CXXFLAGS += -Wmissing-include-dirs -Wlogical-op @@ -34,8 +34,8 @@ ifeq ($(ONLY_BONGOCAT),1) BASE_CFLAGS += -DFEATURE_BONGOCAT_EMBEDDED_ASSETS BASE_CXXFLAGS += -DFEATURE_BONGOCAT_EMBEDDED_ASSETS else - BASE_CFLAGS += -DFEATURE_BONGOCAT_EMBEDDED_ASSETS -DFEATURE_ENABLE_DM_EMBEDDED_ASSETS -DFEATURE_MS_AGENT_EMBEDDED_ASSETS - BASE_CXXFLAGS += -DFEATURE_BONGOCAT_EMBEDDED_ASSETS -DFEATURE_ENABLE_DM_EMBEDDED_ASSETS -DFEATURE_MS_AGENT_EMBEDDED_ASSETS + BASE_CFLAGS += -DFEATURE_BONGOCAT_EMBEDDED_ASSETS -DFEATURE_ENABLE_DM_EMBEDDED_ASSETS -DFEATURE_MS_AGENT_EMBEDDED_ASSETS -DFEATURE_PKMN_EMBEDDED_ASSETS + BASE_CXXFLAGS += -DFEATURE_BONGOCAT_EMBEDDED_ASSETS -DFEATURE_ENABLE_DM_EMBEDDED_ASSETS -DFEATURE_MS_AGENT_EMBEDDED_ASSETS -DFEATURE_PKMN_EMBEDDED_ASSETS BASE_CFLAGS += -I$(INCDIR)/embedded_assets/min_dm -I$(INCDIR)/image_loader/min_dm -I$(SRCDIR)/embedded_assets/min_dm/include BASE_CXXFLAGS += -I$(INCDIR)/embedded_assets/min_dm -I$(INCDIR)/image_loader/min_dm -I$(SRCDIR)/embedded_assets/min_dm/include endif @@ -76,6 +76,7 @@ CXX_SRC = $(SRCDIR)/image_loader/load_images.cpp \ $(SRCDIR)/platform/wayland.cpp \ $(SRCDIR)/platform/wayland_callbacks.cpp \ $(SRCDIR)/platform/wayland_hyprland.cpp \ + $(SRCDIR)/platform/wayland_setups.cpp \ $(SRCDIR)/platform/wayland_sway.cpp \ $(SRCDIR)/graphics/bar.cpp \ $(SRCDIR)/graphics/animation.cpp \ @@ -83,19 +84,22 @@ CXX_SRC = $(SRCDIR)/image_loader/load_images.cpp \ $(SRCDIR)/graphics/drawing_images.cpp \ $(SRCDIR)/config/config_watcher.cpp \ $(SRCDIR)/config/config.cpp + C_SRC = $(SRCDIR)/image_loader/stb_image.c ifeq ($(ONLY_BONGOCAT),1) C_SRC += $(SRCDIR)/embedded_assets/bongocat/bongocat_images.c CXX_SRC += $(SRCDIR)/embedded_assets/bongocat/bongocat_get_sprite_sheet.cpp $(SRCDIR)/image_loader/bongocat/load_images_bongocat.cpp else - C_SRC += $(SRCDIR)/embedded_assets/bongocat/bongocat_images.c $(SRCDIR)/embedded_assets/min_dm/min_dm_images.c $(SRCDIR)/embedded_assets/ms_agent/ms_agent_images.c + C_SRC += $(SRCDIR)/embedded_assets/bongocat/bongocat_images.c $(SRCDIR)/embedded_assets/min_dm/min_dm_images.c $(SRCDIR)/embedded_assets/ms_agent/ms_agent_images.c $(SRCDIR)/embedded_assets/pkmn/pkmn_images.c CXX_SRC += $(SRCDIR)/embedded_assets/bongocat/bongocat_get_sprite_sheet.cpp \ $(SRCDIR)/image_loader/bongocat/load_images_bongocat.cpp \ $(SRCDIR)/image_loader/min_dm/load_images_min_dm.cpp \ $(SRCDIR)/image_loader/min_dm/min_dm_load_sprite_sheet.cpp \ $(SRCDIR)/image_loader/base_dm/load_dm.cpp \ - $(SRCDIR)/embedded_assets/min_dm/min_dm_get_sprite_sheet.cpp \ + $(SRCDIR)/image_loader/pkmn/load_images_pkmn.cpp \ $(SRCDIR)/image_loader/ms_agent/load_images_ms_agent.cpp \ + $(SRCDIR)/embedded_assets/pkmn/pkmn_get_sprite_sheet.cpp \ + $(SRCDIR)/embedded_assets/min_dm/min_dm_get_sprite_sheet.cpp \ $(SRCDIR)/embedded_assets/ms_agent/embedded_assets_ms_agent.cpp endif @@ -110,7 +114,7 @@ PROTOCOL_OBJECTS = $(C_PROTOCOL_SRC:$(PROTOCOLDIR)/%.c=$(OBJDIR)/%.o) # Target executable TARGET = $(BUILDDIR)/bongocat -.PHONY: all clean protocols +.PHONY: all clean protocols embed-assets format format-check lint all: protocols $(TARGET) @@ -128,22 +132,31 @@ $(OBJDIR): mkdir -p $(OBJDIR)/embedded_assets/dm20 mkdir -p $(OBJDIR)/embedded_assets/dmc mkdir -p $(OBJDIR)/embedded_assets/dmx + mkdir -p $(OBJDIR)/embedded_assets/pen + mkdir -p $(OBJDIR)/embedded_assets/pen20 mkdir -p $(OBJDIR)/embedded_assets/dmall mkdir -p $(OBJDIR)/embedded_assets/min_dm mkdir -p $(OBJDIR)/embedded_assets/ms_agent + mkdir -p $(OBJDIR)/embedded_assets/misc mkdir -p $(OBJDIR)/embedded_assets/pkmn + mkdir -p $(OBJDIR)/embedded_assets/pmd mkdir -p $(OBJDIR)/graphics mkdir -p $(OBJDIR)/image_loader mkdir -p $(OBJDIR)/image_loader/base_dm mkdir -p $(OBJDIR)/image_loader/bongocat + mkdir -p $(OBJDIR)/image_loader/custom mkdir -p $(OBJDIR)/image_loader/dm mkdir -p $(OBJDIR)/image_loader/dm20 mkdir -p $(OBJDIR)/image_loader/dmc mkdir -p $(OBJDIR)/image_loader/dmx + mkdir -p $(OBJDIR)/image_loader/pen + mkdir -p $(OBJDIR)/image_loader/pen20 mkdir -p $(OBJDIR)/image_loader/dmall mkdir -p $(OBJDIR)/image_loader/min_dm mkdir -p $(OBJDIR)/image_loader/ms_agent + mkdir -p $(OBJDIR)/image_loader/misc mkdir -p $(OBJDIR)/image_loader/pkmn + mkdir -p $(OBJDIR)/image_loader/pmd mkdir -p $(OBJDIR)/platform mkdir -p $(OBJDIR)/utils mkdir -p $(BUILDDIR) @@ -186,7 +199,7 @@ release: install: $(TARGET) install -D $(TARGET) $(DESTDIR)/usr/local/bin/bongocat - install -D bongocat.conf $(DESTDIR)/usr/local/share/bongocat/bongocat.conf.example + install -D bongocat.conf.example $(DESTDIR)/usr/local/share/bongocat/bongocat.conf.example install -D scripts/find_input_devices.sh $(DESTDIR)/usr/local/bin/bongocat-find-devices uninstall: @@ -194,9 +207,10 @@ uninstall: rm -f $(DESTDIR)/usr/local/bin/bongocat-find-devices rm -rf $(DESTDIR)/usr/local/share/bongocat +SOURCES = $(CXX_SRC) $(C_SRC) # Static analysis analyze: - clang-tidy $(SOURCES) -- $(CFLAGS) + clang-tidy $(SOURCES) -- $(CFLAGS) $(CXXFLAGS) # Memory check (requires valgrind) memcheck: debug @@ -207,4 +221,44 @@ profile: release perf record -g ./$(TARGET) perf report -.PHONY: debug release install uninstall analyze memcheck profile +.PHONY: debug release install uninstall analyze memcheck profile format format-check lint + + +# ============================================================================= +# CODE QUALITY TARGETS +# ============================================================================= + +# Find all project source files (exclude lib/ and protocols/) +PROJECT_SOURCES = $(CXX_SRC) $(C_SRC) +PROJECT_HEADERS = $(shell find $(INCDIR) -name '*.h' ! -path '*/embedded_assets/**/*.h' ! -path '*/image_loader/**/*.h') +ALL_PROJECT_FILES = $(PROJECT_SOURCES) $(PROJECT_HEADERS) + +# Format all project source files +format: + @echo "Formatting source files..." + @clang-format -i $(ALL_PROJECT_FILES) + @echo "Done! Formatted $(words $(ALL_PROJECT_FILES)) files." + +# Check if formatting is correct (for CI) +format-check: + @echo "Checking code formatting..." + @clang-format --dry-run --Werror $(ALL_PROJECT_FILES) + @echo "All files are properly formatted." + +# Static analysis with clang-tidy (uses .clang-tidy config) +lint: protocols + @echo "Running static analysis..." + @clang-tidy $(PROJECT_SOURCES) -- $(CFLAGS) 2>/dev/null || true + @echo "Static analysis complete." + +# Alias for lint +analyze: lint + +# Generate compile_commands.json for IDE support (requires bear) +# Run: make compiledb +compiledb: clean + @echo "Generating compile_commands.json..." + @bear -- $(MAKE) all 2>/dev/null || (echo "Note: 'bear' not installed. Install with: sudo pacman -S bear" && false) + @echo "compile_commands.json generated!" + +.PHONY: compiledb diff --git a/README.md b/README.md index f8c6279e..2f398bc7 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,10 @@ # Bongo Cat + V-Pets Wayland Overlay [![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](https://opensource.org/licenses/MIT) -[![Version](https://img.shields.io/badge/version-3.5.0-blue.svg)](https://github.com/furudbat/wayland-vpets/releases) +[![Version](https://img.shields.io/badge/version-3.6.0-blue.svg)](https://github.com/furudbat/wayland-vpets/releases) [![Release Build](https://github.com/furudbat/wayland-vpets/actions/workflows/release.yml/badge.svg?branch=main)](https://github.com/furudbat/wayland-vpets/actions/workflows/release.yml) -A delightful Wayland overlay that displays an animated V-Pet reacting to your keyboard input! -Perfect for streamers, content creators, or anyone who wants to add some fun to their desktop. - -## 🖼️ Demo +A cute Wayland overlay that shows an animated pets reacting to your keyboard input. ![Bongocat - Demo](assets/demo.gif) _Classic Bongocat_ @@ -21,295 +18,156 @@ _Clippy_ ![Pokemon Charizard - Demo](assets/pokemon-demo.png) _Pokemon_ -## ✨ Features +## Features -- **🐈 More Pets** - More Sprite to choose from +- **🐈 More Pets** - Bongocat 😺 - - Digimon V-Pets 🦖 (v1.3.0) - - Clippy 📎 (v2.1.0) - - Pokemon 🐭 (v3.0.0) - - Misc 🐈‍⬛ (v3.2.2) -- **🎯 Real-time Animation** - Bongo cat reacts instantly to keyboard input -- **🔥 Hot-Reload Configuration** - Modify settings without restarting (v1.2.0) -- **🔄 Dynamic Device Detection** - Automatically detects Bluetooth/USB keyboards (v1.2.0) -- **⚡ Performance Optimized** - Adaptive monitoring and batch processing (v1.4.0) - - triggers rendering, only when needed (v2.0.0) -- **🖥️ Screen Detection** - Automatic screen detection for all sizes and orientations (v1.2.2) -- **🎮 Smart Fullscreen Detection** - Automatically hides during fullscreen applications (v1.2.3) -- **🖥️ Multi-Monitor Support** - Choose which monitor to display on in multi-monitor setups (v1.2.4) -- **😴 Sleep Mode** - Scheduled or idle-based sleep mode with custom timing (v1.2.5) -- **🎨 Customizable Appearance** - Fine-tune position, size, alignment, and opacity -- **💾 Lightweight** - Minimal resource usage (~10MB RAM, depends on the loaded sprites) - - Lazy loading - Load only used assets into RAM (v2.4.0) -- **🎛️ Multi-device Support** - Monitor multiple keyboards simultaneously -- **🏗️ Cross-platform** - Works on x86_64 and ARM64 -- **😄 Happy Frame** - Reach KPM (Keystroke per minute) to trigger the happy frame (Digimon) -- **🎲 Random Frame** - Randomize sprite frame at start up (Digimon) (v2.4.0) -- **🔲 CPU Stat** - React to CPU usage (Digimon) (v3.1.0) -- **↔️ Movement** - Movement on screen (Digimon) (v3.2.0) -- **🔢 Custom Sprite-Sheet** - Load your own Sprite Sheet at runtime (v3.3.0) - -## 🏁 Getting Started - -### 1. Install the App, Arch Linux (Recommended) - -```bash -# Install using yay -yay -S wpets - -# Or using paru -paru -S wpets -``` - -### 2. Configure Permissions - -```bash -# Add user to input group for keyboard access -sudo usermod -a -G input $USER -# Log out and back in for changes to take effect -``` - -### 3. Find Your Input Devices - -```bash -# Installed via AUR -wpets-find-devices -``` - -### 4. Configure Bongo Cat - -Create or edit `~/.config/bongocat.conf`: - -```ini -# Example minimal configuration -cat_x_offset=0 -cat_y_offset=0 -cat_align=center -cat_height=60 -overlay_height=80 -overlay_opacity=150 -overlay_position=top -layer=overlay -fps=60 -enable_antialiasing=1 -animation_name=bongocat -keypress_duration=200 - -# add devices found with find-devices -keyboard_device=/dev/input/event4 # Keyboard -``` - -Full configuration reference: see the [Configuration Section](#%EF%B8%8F-configuration) below. - -### 5. Run the Overlay - -```bash -wpets --watch-config --config ~/.config/bongocat.conf -``` - - - -## 🚀 Installation - -### Arch Linux (Recommended) + - Digimon V-Pets 🦖 + - Clippy 📎 + - Pokemon 🐭 + - Misc & custom sprite sheets 🐈‍⬛ +- 🎯 Real-time keyboard animation +- 🔥 Hot-reload configuration +- 🎮 Auto-hides in fullscreen apps +- 🖥️ Multi-monitor support +- 😴 Idle/scheduled sleep mode +- 😄 Happy animation when reach KPM (Keystroke per minute) +- 🎲 Randomize sprite frame at start up +- 🔲 React to CPU usage +- ↔️ Movement on screen +- ⚡ Lightweight (~10MB RAM) + + +## Quick Start + +### Install ```bash -# Install using yay +# Arch Linux yay -S wpets - -# Using paru -paru -S wpets - -# Run immediately -wpets --watch-config - -# Custom config with hot-reload -wpets --config ~/.config/bongocat.conf --watch-config - - -# drop-in replacement for bongocat -wpets --config ~/.config/bongocat.conf --watch-config - -# only pokemon sprites -wpets-pkmn --config ~/.config/pkmn.bongocat.conf --watch-config - -# all sprites available (Recommended) -wpets-all --config ~/.config/bongocat.conf --watch-config ``` -_`wpets` is the default **minimal** binary. **`wpets-all`** and `wpets-pkmn` are variants with specific sprite sets._ -_Just use `wpets-all` with all sprites included, try out all the pets._ - -#### From Source ⚠️ +#### Other distros - build from source ```bash -# Install dependencies -pacman -S gcc make cmake libinput wayland wayland-protocols - -# Clone repository git clone https://github.com/furudbat/wayland-vpets.git cd wayland-vpets - -# Build cmake -S . -B build -DCMAKE_BUILD_TYPE=Release cmake --build build - -# Install - ⚠️ - If you only want to test without replacing bongocat, run the binary directly from `./build/` instead of installing. -sudo cmake --install build ``` -**⚠️ this will overwrite the original installation of [bongocat](https://github.com/saatvik333/wayland-bongocat) ⚠️** - - +_If you only want to test without replacing bongocat, run the binary directly from `./build/` instead of installing._ -### Other Distributions - -
-Fedora +##### Install ⚠️ ```bash -# Install dependencies -sudo dnf install wayland-devel wayland-protocols-devel gcc make cmake - -# Build from source -git clone https://github.com/furudbat/wayland-vpets.git -cd wayland-vpets -cmake -B build -cmake --build build - -# Run -./build/bongocat -``` - -
- -
-NixOS - -```bash -# Quick start with flakes -nix run github:furudbat/wayland-vpets -- --watch-config - -# Install to user profile -nix profile install github:furudbat/wayland-vpets +sudo cmake --install build ``` +⚠️ **this can overwrite the original installation of bongocat** ⚠️ -📖 **For comprehensive NixOS setup, see [nix/NIXOS.md](nix/NIXOS.md)** -
-## 🎮 Run - -### 1. Setup Permissions (once) +### Setup Permissions ```bash -# Add your user to the input group sudo usermod -a -G input $USER -# Log out and back in for changes to take effect +# Log out and back in ``` -### 3. Run with Hot-Reload +### Find Your Keyboard ```bash -# AUR installation -wpets --watch-config - -# From source -./build/bongocat --watch-config +wpets-find-devices # or ./scripts/find_input_devices.sh ``` -## ⚙️ Configuration +### Run -Once installed, you can customize Bongo Cat Bar using a simple config file. +```bash +wpets-all --watch-config +``` -### Basic Configuration +## Configuration -Create or edit `bongocat.conf`: +Create `~/.config/bongocat/bongocat.conf`: ```ini -# Position settings -cat_x_offset=0 # Horizontal offset from center position -cat_y_offset=0 # Vertical offset from default position -cat_align=center # Horizontal alignment in the bar (left/center/right) - -# Size settings -cat_height=80 # Height of bongo cat (10-200) +# Bongo Cat Configuration File +# Edit these values to customize your bongo cat overlay -# Visual settings -mirror_x=0 # Flip horizontally (mirror across Y axis) -mirror_y=0 # Flip vertically (mirror across X axis) +# Save this file to: ~/.config/bongocat/bongocat.conf +# Run with: wpets-all --watch-config --config ~/.config/bongocat/bongocat.conf -# Anti-aliasing settings -enable_antialiasing=1 # Use bilinear interpolation for smooth scaling (0=off, 1=on) - -# Overlay settings (requires restart) -overlay_height=60 # Height of the entire overlay bar (20-300) -overlay_opacity=150 # Background opacity (0-255) -overlay_position=top # Position on screen (top/bottom) +# Position & Size +cat_height=80 +cat_align=center +# cat_x_offset=0 +# cat_y_offset=0 -# Animation settings -idle_frame=0 # Frame to show when idle (0-3) -fps=60 # Frame rate (1-120) -keypress_duration=100 # Animation duration (ms) +# Appearance +enable_antialiasing=1 +overlay_height=80 +overlay_opacity=0 +overlay_position=bottom +# mirror_x=0 +# mirror_y=0 -# Input devices (add multiple lines for multiple keyboards) +# Input device (run wpet-find-devices to find yours) keyboard_device=/dev/input/event4 -# keyboard_device=/dev/input/event20 # External/Bluetooth keyboard - -# Multi-monitor support -monitor=eDP-1 # Specify which monitor to display on (optional) -# Sleep mode settings -enable_scheduled_sleep=0 # Enable scheduled sleep mode (0=off, 1=on) -sleep_begin=20:00 # Begin of sleeping phase (HH:MM) -sleep_end=06:00 # End of sleeping phase (HH:MM) -idle_sleep_timeout=0 # Inactivity timeout before sleep (seconds, 0=off) +# Multi-monitor (optional - auto-detects by default) +# monitor=HDMI-A-1 -# Debug -enable_debug=0 # Show debug messages +# Sleep mode (optional) +# idle_sleep_timeout=300 +# enable_scheduled_sleep=0 +# sleep_begin=22:00 +# sleep_end=06:00 ``` -### Configuration Reference - -| Setting | Type | Range / Options | Default | Description | -|---------------------------|---------|------------------------------------------------------------|---------------|-------------------------------------------------------------------------------------------| -| `cat_height` | Integer | 10–200 | 40 | Height of bongo cat in pixels (width auto-calculated to maintain aspect ratio) | -| `cat_x_offset` | Integer | -16000 to 16000 | 100 | Horizontal offset from center (behavior depends on `cat_align`) | -| `cat_y_offset` | Integer | -16000 to 16000 | 10 | Vertical offset from center (positive=down, negative=up) | -| `cat_align` | String | "center", "left", "right" | "center" | Horizontal alignment of cat inside overlay bar | -| `overlay_height` | Integer | 20–300 | 50 | Height of the entire overlay bar | -| `overlay_position` | String | "top" or "bottom" | "top" | Position of overlay on screen | -| `overlay_opacity` | Integer | 0–255 | 60 | Background opacity (0=transparent, 255=opaque) | -| `overlay_layer` | String | "overlay", "top", "bottom" or "background" | "overlay" | Surface layer of overlay on screen | -| `animation_name` | String | "bongocat", ``, "clippy" or `` | "bongocat" | Name of the V-Pet sprite (see list below) | -| `invert_color` | Boolean | 0 or 1 | 0 | Invert color (useful for white digimon sprites & dark mode) | -| `idle_frame` | Integer | 0–2 (varies by sprite type) | 0 | Which frame to use when idle (sprite-specific options) | -| `idle_animation` | Boolean | 0 or 1 | 0 | Enable idle animation | -| `animation_speed` | Integer | 0–5000 | 0 | Frame duration in ms (0 = use `fps`) | -| `keypress_duration` | Integer | 50–5000 | 100 | Duration to display keypress animation (ms) | -| `mirror_x` | Boolean | 0 or 1 | 0 | Flip cat horizontally (mirror across Y axis) | -| `mirror_y` | Boolean | 0 or 1 | 0 | Flip cat vertically (mirror across X axis) | -| `test_animation_duration` | Integer | 0–5000 | 0 | Duration of test animation (ms) (deprecated, use `animation_speed`) | -| `test_animation_interval` | Integer | 0–60 | 0 | Interval for test animation in seconds (0 = disabled, deprecated) | -| `fps` | Integer | 1–144 | 60 | Animation frame rate | -| `input_fps` | Integer | 0–144 | 0 | Input thread frame rate (0 = use `fps`) | -| `enable_scheduled_sleep` | Boolean | 0 or 1 | 0 | Enable scheduled sleep mode | -| `sleep_begin` | String | "00:00" – "23:59" | "00:00" | Start time of scheduled sleep (24h format) | -| `sleep_end` | String | "00:00" – "23:59" | "00:00" | End time of scheduled sleep (24h format) | -| `idle_sleep_timeout` | Integer | 0+ | 0 | Time of inactivity before entering sleep (0 = disabled) (in seconds) | -| `happy_kpm` | Integer | 0–10000 | 0 | Minimum keystrokes per minute to trigger happy animation (0 = disabled) | -| `keyboard_device` | String | Valid `/dev/input/*` path(s) | \ | Input device path (multiple entries allowed) | -| `enable_antialiasing` | Boolean | 0 or 1 | 1 | Enable bilinear interpolation for smooth scaling (Bongocat and MS Agent only) | -| `enable_debug` | Boolean | 0 or 1 | 0 | Enable debug logging | -| `monitor` | String | Monitor name | Auto-detect | Which monitor to display on (e.g., "eDP-1", "HDMI-A-1") | -| `random` | Boolean | 0 or 1 | 0 | Randomize `animation_index` (`animation_name` needs to be set as base sprite sheet set) | -| `random_on_reload` | Boolean | 0 or 1 | 0 | Randomize `animation_index` when reloading config (`random` needs to be `1`) | -| `update_rate` | Integer | 0–10000 | 0 | Check (CPU) states rate (0 = disabled) (in milliseconds) | -| `cpu_threshold` | Double | 0–100 | 0 | Threshold of CPU usage for triggering work animation (0 = disabled) | -| `movement_radius` | Integer | 0-8000 | 0 | Radius of moving area (0 = disabled) | -| `movement_speed` | Integer | 0–5000 | 0 | Movement speed (0 = disabled) | -| `enable_movement_debug` | Boolean | 0 or 1 | 0 | Show Movement area | -| `cpu_running_factor` | Double | 0.0–50.0 | 0 | Speed up factor for 100% CPU, it's linear so the animation slowly speed up (0 = disabled) | +### Options Reference + +
+Click to expand all options + +| **Option** | **Values** | **Default** | **Description** | +|---------------------------|--------------------------------------------------------------------|---------------|-------------------------------------------------------------------------------------------| +| `cat_height` | 8–1024 | 40 | Height of bongo cat in pixels (width auto-calculated to maintain aspect ratio) | +| `cat_x_offset` | -16000 to 16000 | 100 | Horizontal offset from center (behavior depends on `cat_align`) | +| `cat_y_offset` | -16000 to 16000 | 10 | Vertical offset from center (positive=down, negative=up) | +| `cat_align` | "center", "left", "right" | "center" | Horizontal alignment of cat inside overlay bar | +| `overlay_height` | 16–2560 | 50 | Height of the entire overlay bar | +| `overlay_position` | "top" or "bottom" | "top" | Position of overlay on screen | +| `overlay_opacity` | 0–255 | 60 | Background opacity (0=transparent, 255=opaque) | +| `overlay_layer` | "overlay", "top", "bottom" or "background" | "overlay" | Surface layer of overlay on screen | +| `animation_name` | "bongocat", ``, "clippy", `` or "neko" | "bongocat" | Name of the V-Pet sprite (see list below) | +| `invert_color` | 0 or 1 | 0 | Invert color (useful for white digimon sprites & dark mode) | +| `idle_frame` | 0–2 (varies by sprite type) | 0 | Which frame to use when idle (sprite-specific options) | +| `idle_animation` | 0 or 1 | 0 | Enable idle animation | +| `animation_speed` | 0–5000 | 0 | Frame duration in ms (0 = use `fps`) | +| `keypress_duration` | 50–5000 | 100 | Duration to display keypress animation (ms) | +| `mirror_x` | 0 or 1 | 0 | Flip cat horizontally (mirror across Y axis) | +| `mirror_y` | 0 or 1 | 0 | Flip cat vertically (mirror across X axis) | +| `test_animation_duration` | 0–5000 | 0 | Duration of test animation (ms) (deprecated, use `animation_speed`) | +| `test_animation_interval` | 0–60 | 0 | Interval for test animation in seconds (0 = disabled, deprecated) | +| `fps` | 1–144 | 60 | Animation frame rate | +| `input_fps` | 0–144 | 0 | Input thread frame rate (0 = use `fps`) | +| `enable_scheduled_sleep` | 0 or 1 | 0 | Enable scheduled sleep mode | +| `sleep_begin` | "00:00" – "23:59" | "00:00" | Start time of scheduled sleep (24h format) | +| `sleep_end` | "00:00" – "23:59" | "00:00" | End time of scheduled sleep (24h format) | +| `idle_sleep_timeout` | 0+ | 0 | Time of inactivity before entering sleep (0 = disabled) (in seconds) | +| `happy_kpm` | 0–10000 | 0 | Minimum keystrokes per minute to trigger happy animation (0 = disabled) | +| `keyboard_device` | Valid `/dev/input/*` path(s) | \ | Input device path (multiple entries allowed) | +| `enable_antialiasing` | 0 or 1 | 1 | Enable bilinear interpolation for smooth scaling (Bongocat and MS Agent only) | +| `enable_debug` | 0 or 1 | 0 | Enable debug logging | +| `monitor` | Monitor name | Auto-detect | Which monitor to display on (e.g., "eDP-1", "HDMI-A-1") | +| `random` | 0 or 1 | 0 | Randomize `animation_index` (`animation_name` needs to be set as base sprite sheet set) | +| `random_on_reload` | 0 or 1 | 0 | Randomize `animation_index` when reloading config (`random` needs to be `1`) | +| `update_rate` | 0–10000 | 0 | Check (CPU) states rate (0 = disabled) (in milliseconds) | +| `cpu_threshold` | 0–100 | 0 | Threshold of CPU usage for triggering work animation (0 = disabled) | +| `movement_radius` | 0-8000 | 0 | Radius of moving area (0 = disabled) | +| `movement_speed` | 0–5000 | 0 | Movement speed (0 = disabled) | +| `enable_movement_debug` | 0 or 1 | 0 | Show Movement area | +| `cpu_running_factor` | 0.0–50.0 | 0 | Speed up factor for 100% CPU, it's linear so the animation slowly speed up (0 = disabled) | #### Available Sprites (`animation_name`) @@ -334,53 +192,53 @@ _If you build with ALL assets included you can void naming conflicts by using th #### Custom Sprite Sheet (`custom_...`) -| **Key** | **Type** | **Range / Example** | **Default** | **Description** | -| ------------------------------------- | -------- |---------------------|-------------------|--------------------------------------------------------------------------------------| -| `animation_name` | String | `"custom"` | | Must be `"custom"` for custom-options to work | -| `custom_sprite_sheet_filename` | String | Path to image file | | Path to the custom sprite sheet image (**must be png**) | -| `custom_idle_frames` | Integer | 1-500 | 0 (disabled) | Number of frames for idle animation | -| `custom_boring_frames` | Integer | 1-500 | 0 (disabled) | Number of frames for boring animation | -| `custom_start_writing_frames` | Integer | 1-500 | 0 (disabled) | Number of frames for start writing animation | -| `custom_writing_frames` | Integer | 1-500 | 0 (disabled) | Number of frames for writing animation | -| `custom_end_writing_frames` | Integer | 1-500 | 0 (disabled) | Number of frames for end writing animation | -| `custom_happy_frames` | Integer | 1-500 | 0 (disabled) | Number of frames for happy animation | -| `custom_asleep_frames` | Integer | 1-500 | 0 (disabled) | Number of frames for falling asleep animation | -| `custom_sleep_frames` | Integer | 1-500 | 0 (disabled) | Number of frames for sleeping animation | -| `custom_wake_up_frames` | Integer | 1-500 | 0 (disabled) | Number of frames for waking up animation | -| `custom_start_working_frames` | Integer | 1-500 | 0 (disabled) | Number of frames for start working animation | -| `custom_working_frames` | Integer | 1-500 | 0 (disabled) | Number of frames for working animation | -| `custom_end_working_frames` | Integer | 1-500 | 0 (disabled) | Number of frames for end working animation | -| `custom_start_moving_frames` | Integer | 1-500 | 0 (disabled) | Number of frames for start moving animation | -| `custom_moving_frames` | Integer | 1-500 | 0 (disabled) | Number of frames for moving animation | -| `custom_end_moving_frames` | Integer | 1-500 | 0 (disabled) | Number of frames for end moving animation | -| `custom_toggle_writing_frames` | Boolean | 0 or 1 | -1 (auto) | Toggle writing frames when writing (`custom_writing_frames` needs to be `2`) | -| `custom_toggle_writing_frames_random` | Boolean | 0 or 1 | -1 (auto) | Randomize writing frame when start writing (`custom_writing_frames` needs to be `2`) | -| `custom_mirror_x_moving` | Boolean | 0 or 1 | -1 (ignore) | Mirror frames horizontally when moving | -| `custom_idle_row` | Integer | 1-15 | -1 (auto) | Row nr for idle animation in sprite sheet | -| `custom_boring_row` | Integer | 1-15 | -1 (auto) | Row nr for boring animation | -| `custom_start_writing_row` | Integer | 1-15 | -1 (auto) | Row nr for start writing animation | -| `custom_writing_row` | Integer | 1-15 | -1 (auto) | Row nr for writing animation | -| `custom_end_writing_row` | Integer | 1-15 | -1 (auto) | Row nr for end writing animation | -| `custom_happy_row` | Integer | 1-15 | -1 (auto) | Row nr for happy animation | -| `custom_asleep_row` | Integer | 1-15 | -1 (auto) | Row nr for asleep animation | -| `custom_sleep_row` | Integer | 1-15 | -1 (auto) | Row nr for sleep animation | -| `custom_wake_up_row` | Integer | 1-15 | -1 (auto) | Row nr for wake-up animation | -| `custom_start_working_row` | Integer | 1-15 | -1 (auto) | Row nr for start working animation | -| `custom_working_row` | Integer | 1-15 | -1 (auto) | Row nr for working animation | -| `custom_end_working_row` | Integer | 1-15 | -1 (auto) | Row nr for end working animation | -| `custom_start_moving_row` | Integer | 1-15 | -1 (auto) | Row nr for start moving animation | -| `custom_moving_row` | Integer | 1-15 | -1 (auto) | Row nr for moving animation | -| `custom_end_moving_row` | Integer | 1-15 | -1 (auto) | Row nr for end moving animation | - +| **Option** | **Values** | **Default** | **Description** | +|---------------------------------------------|--------------------|------------------|--------------------------------------------------------------------------------------| +| `animation_name` | `"custom"` | | Must be `"custom"` for custom-options to work | +| `custom_sprite_sheet_filename` | Path to image file | | Path to the custom sprite sheet image (**must be png**) | +| `custom_idle_frames` | 1-500 | 0 (disabled) | Number of frames for idle animation | +| `custom_boring_frames` | 1-500 | 0 (disabled) | Number of frames for boring animation | +| `custom_start_writing_frames` | 1-500 | 0 (disabled) | Number of frames for start writing animation | +| `custom_writing_frames` | 1-500 | 0 (disabled) | Number of frames for writing animation | +| `custom_end_writing_frames` | 1-500 | 0 (disabled) | Number of frames for end writing animation | +| `custom_happy_frames` | 1-500 | 0 (disabled) | Number of frames for happy animation | +| `custom_asleep_frames` | 1-500 | 0 (disabled) | Number of frames for falling asleep animation | +| `custom_sleep_frames` | 1-500 | 0 (disabled) | Number of frames for sleeping animation | +| `custom_wake_up_frames` | 1-500 | 0 (disabled) | Number of frames for waking up animation | +| `custom_start_working_frames` | 1-500 | 0 (disabled) | Number of frames for start working animation | +| `custom_working_frames` | 1-500 | 0 (disabled) | Number of frames for working animation | +| `custom_end_working_frames` | 1-500 | 0 (disabled) | Number of frames for end working animation | +| `custom_start_moving_frames` | 1-500 | 0 (disabled) | Number of frames for start moving animation | +| `custom_moving_frames` | 1-500 | 0 (disabled) | Number of frames for moving animation | +| `custom_end_moving_frames` | 1-500 | 0 (disabled) | Number of frames for end moving animation | +| `custom_toggle_writing_frames` | 0 or 1 | -1 (auto) | Toggle writing frames when writing (`custom_writing_frames` needs to be `2`) | +| `custom_toggle_writing_frames_random` | 0 or 1 | -1 (auto) | Randomize writing frame when start writing (`custom_writing_frames` needs to be `2`) | +| `custom_mirror_x_moving` | 0 or 1 | -1 (ignore) | Mirror frames horizontally when moving | +| `custom_idle_row` | 1-15 | -1 (auto) | Row nr for idle animation in sprite sheet | +| `custom_boring_row` | 1-15 | -1 (auto) | Row nr for boring animation | +| `custom_start_writing_row` | 1-15 | -1 (auto) | Row nr for start writing animation | +| `custom_writing_row` | 1-15 | -1 (auto) | Row nr for writing animation | +| `custom_end_writing_row` | 1-15 | -1 (auto) | Row nr for end writing animation | +| `custom_happy_row` | 1-15 | -1 (auto) | Row nr for happy animation | +| `custom_asleep_row` | 1-15 | -1 (auto) | Row nr for asleep animation | +| `custom_sleep_row` | 1-15 | -1 (auto) | Row nr for sleep animation | +| `custom_wake_up_row` | 1-15 | -1 (auto) | Row nr for wake-up animation | +| `custom_start_working_row` | 1-15 | -1 (auto) | Row nr for start working animation | +| `custom_working_row` | 1-15 | -1 (auto) | Row nr for working animation | +| `custom_end_working_row` | 1-15 | -1 (auto) | Row nr for end working animation | +| `custom_start_moving_row` | 1-15 | -1 (auto) | Row nr for start moving animation | +| `custom_moving_row` | 1-15 | -1 (auto) | Row nr for moving animation | +| `custom_end_moving_row` | 1-15 | -1 (auto) | Row nr for end moving animation | See [examples](examples/custom-sprite-sheets) for more details. -## 🔧 Usage +
+ -### Command Line Options +## Command Line ```bash -wpets [OPTIONS] +wpets-all [OPTIONS] Options: -h, --help Show this help message @@ -398,30 +256,38 @@ Options: ### Examples ```bash -# Basic usage +# Basic usage (bongocat) wpets -# With hot-reload (recommended) +# Run immediately (bongocat) wpets --watch-config -# Custom config with hot-reload -wpets --config ~/.config/bongocat.conf --watch-config +# Custom config with hot-reload (bongocat) +wpets --watch-config --config ~/.config/bongocat/bongocat.conf -# Debug mode -wpets --watch-config --config bongocat.conf -# Toggle mode -wpets --toggle +# drop-in replacement for bongocat (bongocat) +wpets --watch-config --config ~/.config/bongocat/bongocat.conf -# Custom config with hot-reload and custom output_name -wpets --watch-config --output-name DP-2 --config ~/.config/bongocat.conf +# pokemon sprites +wpets-pkmn --watch-config --config ~/.config/bongocat/pkmn.bongocat.conf +wpets-pkmn --watch-config --config ~/.config/bongocat/pmd.bongocat.conf + +# all sprites available (Recommended) +wpets-all --watch-config --config ~/.config/bongocat/bongocat.conf +wpets-all --watch-config --config ~/.config/bongocat/digimon.bongocat.conf +wpets-all --watch-config --config ~/.config/bongocat/clippy.bongocat.conf +wpets-all --watch-config --config ~/.config/bongocat/neko.bongocat.conf ``` -See [`examples/`](examples) for more configs. +_`wpets` is the default **minimal** binary. **`wpets-all`** and `wpets-pkmn` are variants with specific sprite sets._ +_Just use `wpets-all` with all sprites included, try out all the pets._ + +See [examples/](examples) for more configs. #### Hyprland -For [`hyprland`](https://hypr.land/) users, you can autostart `wpets` in your `hyprland.conf`: +For [hyprland](https://hypr.land/) users, you can autostart `wpets` in your `hyprland.conf`: ```ini # Auto start @@ -431,359 +297,45 @@ exec-once = wpets-all --watch-config --config ~/.config/bongocat/screen3.bongoca exec-once = wpets-all --watch-config --config ~/.config/bongocat/screen4.bongocat.conf --random ``` -## 🛠️ Building from Source - -### Prerequisites - -Before building, ensure your system has the required tools and libraries: - -#### Required: - -- Wayland compositor with layer shell support -- C23/C++23 compiler (GCC 15+ or Clang 19+) -- CMake and make -- `libwayland-client` -- `wayland-protocols` -- `wayland-scanner` -- `libudev` - -##### Arch Linux / Manjaro: - -```bash -sudo pacman -S git gcc g++ clang cmake base-devel libinput wayland wayland-protocols systemd-libs` -``` - -##### Fedora: - -```bash -sudo dnf install -y @c-development git-core cmake glibc-langpack-en 'pkgconfig(libevdev)' 'pkgconfig(libinput)' 'pkgconfig(libudev)' 'pkgconfig(wayland-client)' 'pkgconfig(wayland-protocols)' -``` - -##### Debian / Ubuntu: - -```bash -sudo apt-get install build-essential cmake libinput-dev libudev-dev libwayland-dev wayland-protocols -``` - -### Build Instructions - -```bash -# Clone repository -git clone https://github.com/furudbat/wayland-vpets.git -cd wayland-vpets - -# Build (Release or Debug) -cmake -S . -B build -DCMAKE_BUILD_TYPE=Release # Or Debug -cmake --build build - -# Clean -cmake --build build --target clean -``` - -The build process automatically: - -1. Generates Wayland protocol files -2. Compiles with optimizations and security hardening -3. Embeds assets directly in the binary -4. Links with required libraries - - - -## 🔍 Device Discovery - -The `wpets-find-devices` tool provides professional input device analysis with a clean, user-friendly interface: - -```bash -$ wpets-find-devices - -╔══════════════════════════════════════════════════════════════════╗ -║ Wayland Bongo Cat - Input Device Discovery v3.1.0 ║ -╚══════════════════════════════════════════════════════════════════╝ - -[SCAN] Scanning for input devices... - -[DEVICES] Found Input Devices: -┌─────────────────────────────────────────────────────────────────┐ -│ Device: AT Translated Set 2 keyboard │ -│ Path: /dev/input/event4 │ -│ Type: Keyboard │ -│ Status: [OK] Accessible │ -└─────────────────────────────────────────────────────────────────┘ - -┌─────────────────────────────────────────────────────────────────┐ -│ Device: Logitech MX Keys │ -│ Path: /dev/input/event20 │ -│ Type: Keyboard (Bluetooth) │ -│ Status: [OK] Accessible │ -└─────────────────────────────────────────────────────────────────┘ - -[CONFIG] Configuration Suggestions: -Add these lines to your bongocat.conf: - -keyboard_device=/dev/input/event4 # AT Translated Set 2 keyboard -keyboard_device=/dev/input/event20 # Logitech MX Keys -``` - -### Advanced Features - -```bash -# Show all input devices (including mice, touchpads) -wpets-find-devices --all - -# Generate complete configuration file -wpets-find-devices --generate-config > bongocat.conf - -# Generate complete configuration file with named devices by-id -wpets-find-devices --generate-config --by-id > bongocat.conf - -# Test device responsiveness (requires root) -sudo wpets-find-devices --test - -# Show detailed device information -wpets-find-devices --verbose - -# Get help and usage information -wpets-find-devices --help -``` - -### Key Features - -- **Smart Detection** - Automatically identifies keyboards vs other input devices -- **Device Classification** - Distinguishes between built-in, Bluetooth, and USB keyboards -- **Permission Checking** - Verifies device accessibility and provides fix suggestions -- **Config Generation** - Creates ready-to-use configuration snippets -- **Device Testing** - Integrated evtest functionality for troubleshooting -- **Professional UI** - Clean, colorized output with status indicators -- **Error Handling** - Comprehensive error messages and troubleshooting guidance - -## 📊 Performance - -### System Requirements - -This program is lightweight and runs even on very modern desktop systems. -Minimal builds require only a few MB of RAM, whereas asset-heavy builds use more. - -## 🖥️ System Requirements - -| | Minimum | Recommended | -|----------------|----------------------------------------------------------|------------------------------------------------------------------------------| -| **CPU** | Any modern **x86_64** or **ARM64** processor (SSE2/NEON) | Dual-core **x86_64** or **ARM64** processor | -| **RAM** | **8 MB free** (minimal build with minimal assets) | **64 MB free** (full builds with all assets, preloaded, and config overhead) | -| **Storage** | **6 MB free** (binary + config files) | **20 MB free** (multiple binaries/builds + config files) | -| **Compositor** | Wayland with **wlr-layer-shell** protocol support | Modern Wayland compositor (Sway, Hyprland, Wayfire, KDE Plasma 6) | - - -### Tested Compositors - -- ✅ **Hyprland** - Full support -- ✅ **Sway** - Full support -- ✅ **Wayfire** - Compatible -- ✅ **KDE Wayland** - Compatible -- ❌ **GNOME Wayland** - Unsupported - -## 🐛 Troubleshooting - -Enable debug logging for detailed output: -```bash -# ensure enable_debug=1 in bongocat.conf -wpets --watch-config --config bongocat.conf -``` - -### Common Issues +## Troubleshooting
-Permission denied accessing `/dev/input/eventX` - -**Solution:** +Permission denied on input device ```bash -# Add user to input group (recommended) sudo usermod -a -G input $USER -# Log out and back in - -# Or create udev rule -echo 'KERNEL=="event*", GROUP="input", MODE="0664"' | sudo tee /etc/udev/rules.d/99-input.rules -sudo udevadm control --reload-rules -``` - -
- -
-Keyboard input not detected - -**Diagnosis:** - -```bash -# Find correct device -wpets-find-devices - -# Test device manually -sudo evtest /dev/input/event4 +# Then log out and back in ``` -**Solution:** Update `keyboard_device` in `bongocat.conf` with correct path. -
-Overlay not visible or clickable - -**Check:** +Cat not responding to keyboard -- Ensure compositor supports `wlr-layer-shell-unstable-v1` -- Verify `WAYLAND_DISPLAY` environment variable is set -- Try different `overlay_opacity` values - -**Tested compositors:** Hyprland, Sway, Wayfire +1. Run `wpets-find-devices` to find correct device +2. Update `keyboard_device` in config +3. Restart bongocat
-Multi-monitor setup issues - -**Finding monitor names:** - -```bash -# Using wlr-randr (recommended) -wlr-randr - -# Using swaymsg (Sway only) -swaymsg -t get_outputs +Not showing on correct monitor -# Check bongocat logs for detected monitors -wpets --watch-config # Look for "xdg-output name received" messages -``` - -**Configuration:** +Add `monitor=YOUR_MONITOR` to config. Find monitor names with `wlr-randr` or `hyprctl monitors`. -```ini -# Specify exact monitor name -monitor=eDP-1 # Laptop screen -monitor=HDMI-A-1 # External HDMI monitor -monitor=DP-1 # DisplayPort monitor -``` - -**Troubleshooting:** - -- If monitor name is wrong, bongocat falls back to first available monitor -- Monitor names are case-sensitive -- Remove or comment out `monitor=` line to use auto-detection
-
-Build errors - -**Common fixes:** - -- Install development packages: `libwayland-dev wayland-protocols` -- Ensure C23/C++23 compiler: GCC 15+ or Clang 19+ _(requires [`#embed`](https://en.cppreference.com/w/c/preprocessor/embed.html) feature)_ -- Install `wayland-scanner` package -
- -### Getting Help - -1. Enable debug logging: `wpets --watch-config` (ensure `enable_debug=1`) -2. Check compositor compatibility -3. Verify all dependencies are installed -4. Test with minimal configuration - -## 🏗️ Architecture - -### Project Structure - -- `assets/` - Sprite sheets and media -- `src/` - Core application logic and platform-specific code -- `include/` - Headers -- `scripts/` - Utilities and codegen -- `lib/` - External libraries - -``` -wayland-vpets/ -├── assets/ # sprite sheets and media resources -├── Dockerfiles/ # Container build definitions -├── examples/ # Example configurations -├── include/ # Header files (same structure as src/) -├── lib/ # External libraries (image loader) -├── nix/ # NixOS integration -├── protocols/ # Generated Wayland protocols -├── scripts/ # Codegen and utility scripts -└── src/ # Source code -├──── config/ # Configuration system implementation -├──── core/ # Core application logic (main) -├──── embedded_assets/ # Embedded assets -├──── graphics/ # Rendering and graphics implementation -├──── image_loader/ # Assets loading implementations -├──── platform/ # Platform-specific code (input and wayland) -└──── utils/ # General utilities -``` - -## 🤝 Contributing - -This project follows industry best practices with a modular architecture. -Issues and pull requests are welcome! See [CONTRIBUTING.md](CONTRIBUTING.md) for details. - - -### Development Setup - -#### Build with CMake (Recommended) - -```bash -cmake -S . -B build -DCMAKE_BUILD_TYPE=Debug -cmake --build build -``` - -#### Legacy `make` (for old `bongocat` users) +## Building ```bash git clone https://github.com/furudbat/wayland-vpets.git cd wayland-vpets -make debug +cmake -S . -B build -DCMAKE_BUILD_TYPE=Release +cmake --build build ``` -`make debug` provides a quick debug build. -You can inspect the old workflow in the old [`Makefile`](Makefile.old). -_**Note:** The binary name in AUR is `wpets`, but during development and `make install` it is still `bongocat`._ - -### Code Standards - -- C23/C++23 standard compliance -- Comprehensive error handling -- Memory safety with leak detection -- Extensive documentation - -#### Moving to C++ - -The project is gradually migrating to C++ while retaining a C-style foundation for performance and Wayland compatibility. - -For more details, see the [Code Standards section in CONTRIBUTING](CONTRIBUTING.md#code-standards). - -The project remains largely C under the hood, using Linux + Wayland libraries, while gradually adopting modern C++ practices for safety and maintainability. - -## 📄 License - -MIT License - see [LICENSE](LICENSE) file for details. - -## 🙏 Acknowledgments - -Built with ❤️ for the Wayland community. Special thanks to: - -- Redditor: [u/akonzu](https://www.reddit.com/user/akonzu/) for the inspiration -- [@Shreyabardia](https://github.com/Shreyabardia) for the beautiful custom-drawn bongo cat artwork -- All the contributors and users -- [Waybar](https://github.com/Alexays/Waybar) - - -
-Copyright - -This project is **free**, **non-commercial** and not associated with these entities. -Pokémon are owned by Nintendo, Creatures Inc. and GAME FREAK Inc. -Digimon and all related characters, and associated images are owned by Bandai Co., Ltd, Akiyoshi Hongo, and Toei Animation Co., Ltd. -Clippy and other MS Agents are owed by Microsoft. -See [COPYRIGHT](assets/COPYRIGHT.md) for more details. -
+**Requirements:** wayland-client, wayland-protocols, gcc/clang, make, cmake ---- +## License -**₍^. .^₎ Wayland Bongo Cat Overlay v3.5.0** - Making desktops more delightful, one keystroke at a time! -Now with Digimon V-Pets, Clippy and Pokémon. +MIT License - see [LICENSE](LICENSE) \ No newline at end of file diff --git a/bongocat.conf b/bongocat.conf.example similarity index 92% rename from bongocat.conf rename to bongocat.conf.example index 709f69f7..f5ebb4dd 100644 --- a/bongocat.conf +++ b/bongocat.conf.example @@ -1,22 +1,26 @@ # Bongo Cat Configuration File # Edit these values to customize your bongo cat overlay +# Save this file to: ~/.config/bongocat/bongocat.conf +# Run with: wpets-all --watch-config + + # NOTE: OVERLAY SETTINGS DOESN'T WORK WITH HOT RELOAD, NEEDS BONGOCAT RESTART # DRAWN LAYERS GETS GLITCHY SOMETIMES, BETTER TO RESTART WHEN THESE CHANGE # Overlay settings # overlay_height: Height of the entire overlay bar -overlay_height=60 +overlay_height=80 # overlay_position: Position of the overlay on screen # Options: "top" or "bottom" overlay_position=top -# layer: layer for surface of overlay (default: "overlay") -# Options: "overlay", "top", "bottom", "background" +# layer: layer for surface of overlay (default: "top") +# Options: "overlay", "top", "bottom", "background"; top (above windows), overlay (always visible) #overlay_layer=top # Transparency settings # overlay_opacity: Opacity of the overlay background (0-255) # 0 = fully transparent, 255 = fully opaque -overlay_opacity=60 +overlay_opacity=0 # Position settings (in pixels) # cat_x_offset: Horizontal offset from center position @@ -47,7 +51,7 @@ enable_antialiasing=1 # Size settings # cat_height: Height of the bongo cat in pixels # Width is automatically calculated to maintain aspect ratio -cat_height=60 +cat_height=80 # animation_name: Sprite name (CASE SENSITIVE) # Default Option: "bongocat" @@ -98,7 +102,7 @@ idle_frame=0 # Requires both sleep_begin and sleep_end to be defined enable_scheduled_sleep=0 # Start time for scheduled sleep mode (24-hour format: hh:mm) -sleep_begin=21:00 +sleep_begin=22:00 # End time for scheduled sleep mode (24-hour format: hh:mm) sleep_end=06:00 @@ -123,6 +127,10 @@ happy_kpm=400 # keypress_duration: How long to show animation after keypress keypress_duration=100 +# enable_hand_mapping: Left half of keyboard triggers left cat hand, right half triggers right hand +# 0=random hands, 1=left keys -> left hand, right keys -> right hand +enable_hand_mapping=1 + # idle_animation: Enable idle animation (0 = off, 1 = on) idle_animation=0 # animation_speed: Time for frame until next frame (optional, in milliseconds) (0 = use fps) @@ -170,4 +178,4 @@ enable_debug=0 # Specify which monitor to display bongocat on (optional) # Use wlr-randr or swaymsg -t get_outputs to find monitor names # If not specified or monitor not found, uses first available monitor -#monitor=eDP-1 \ No newline at end of file +#monitor=HDMI-A-1 \ No newline at end of file diff --git a/cmake-format.yaml b/cmake-format.yaml new file mode 100644 index 00000000..c31ccc87 --- /dev/null +++ b/cmake-format.yaml @@ -0,0 +1,219 @@ +_help_parse: Options affecting listfile parsing +parse: + _help_additional_commands: + - Specify structure for custom cmake functions + additional_commands: + foo: + flags: + - BAR + - BAZ + kwargs: + HEADERS: '*' + SOURCES: '*' + DEPENDS: '*' + _help_vartags: + - Specify variable tags. + vartags: [] + _help_proptags: + - Specify property tags. + proptags: [] +_help_format: Options affecting formatting. +format: + _help_line_width: + - How wide to allow formatted cmake files + line_width: 140 + _help_tab_size: + - How many spaces to tab for indent + tab_size: 2 + _help_max_subgroups_hwrap: + - If an argument group contains more than this many sub-groups + - (parg or kwarg groups) then force it to a vertical layout. + max_subgroups_hwrap: 2 + _help_max_pargs_hwrap: + - If a positional argument group contains more than this many + - arguments, then force it to a vertical layout. + max_pargs_hwrap: 6 + _help_max_rows_cmdline: + - If a cmdline positional group consumes more than this many + - lines without nesting, then invalidate the layout (and nest) + max_rows_cmdline: 2 + _help_separate_ctrl_name_with_space: + - If true, separate flow control names from their parentheses + - with a space + separate_ctrl_name_with_space: false + _help_separate_fn_name_with_space: + - If true, separate function names from parentheses with a + - space + separate_fn_name_with_space: false + _help_dangle_parens: + - If a statement is wrapped to more than one line, than dangle + - the closing parenthesis on its own line. + dangle_parens: false + _help_dangle_align: + - If the trailing parenthesis must be 'dangled' on its on + - 'line, then align it to this reference: `prefix`: the start' + - 'of the statement, `prefix-indent`: the start of the' + - 'statement, plus one indentation level, `child`: align to' + - the column of the arguments + dangle_align: prefix + _help_min_prefix_chars: + - If the statement spelling length (including space and + - parenthesis) is smaller than this amount, then force reject + - nested layouts. + min_prefix_chars: 4 + _help_max_prefix_chars: + - If the statement spelling length (including space and + - parenthesis) is larger than the tab width by more than this + - amount, then force reject un-nested layouts. + max_prefix_chars: 10 + _help_max_lines_hwrap: + - If a candidate layout is wrapped horizontally but it exceeds + - this many lines, then reject the layout. + max_lines_hwrap: 2 + _help_line_ending: + - What style line endings to use in the output. + line_ending: unix + _help_command_case: + - Format command names consistently as 'lower' or 'upper' case + command_case: canonical + _help_keyword_case: + - Format keywords consistently as 'lower' or 'upper' case + keyword_case: unchanged + _help_always_wrap: + - A list of command names which should always be wrapped + always_wrap: [] + _help_enable_sort: + - If true, the argument lists which are known to be sortable + - will be sorted lexicographicall + enable_sort: true + _help_autosort: + - If true, the parsers may infer whether or not an argument + - list is sortable (without annotation). + autosort: false + _help_require_valid_layout: + - By default, if cmake-format cannot successfully fit + - everything into the desired linewidth it will apply the + - last, most agressive attempt that it made. If this flag is + - True, however, cmake-format will print error, exit with non- + - zero status code, and write-out nothing + require_valid_layout: false + _help_layout_passes: + - A dictionary mapping layout nodes to a list of wrap + - decisions. See the documentation for more information. + layout_passes: {} +_help_markup: Options affecting comment reflow and formatting. +markup: + _help_bullet_char: + - What character to use for bulleted lists + bullet_char: '*' + _help_enum_char: + - What character to use as punctuation after numerals in an + - enumerated list + enum_char: . + _help_first_comment_is_literal: + - If comment markup is enabled, don't reflow the first comment + - block in each listfile. Use this to preserve formatting of + - your copyright/license statements. + first_comment_is_literal: false + _help_literal_comment_pattern: + - If comment markup is enabled, don't reflow any comment block + - which matches this (regex) pattern. Default is `None` + - (disabled). + literal_comment_pattern: null + _help_fence_pattern: + - Regular expression to match preformat fences in comments + - default= ``r'^\s*([`~]{3}[`~]*)(.*)$'`` + fence_pattern: ^\s*([`~]{3}[`~]*)(.*)$ + _help_ruler_pattern: + - Regular expression to match rulers in comments default= + - '``r''^\s*[^\w\s]{3}.*[^\w\s]{3}$''``' + ruler_pattern: ^\s*[^\w\s]{3}.*[^\w\s]{3}$ + _help_explicit_trailing_pattern: + - If a comment line matches starts with this pattern then it + - is explicitly a trailing comment for the preceeding + - argument. Default is '#<' + explicit_trailing_pattern: '#<' + _help_hashruler_min_length: + - If a comment line starts with at least this many consecutive + - hash characters, then don't lstrip() them off. This allows + - for lazy hash rulers where the first hash char is not + - separated by space + hashruler_min_length: 10 + _help_canonicalize_hashrulers: + - If true, then insert a space between the first hash char and + - remaining hash chars in a hash ruler, and normalize its + - length to fill the column + canonicalize_hashrulers: true + _help_enable_markup: + - enable comment markup parsing and reflow + enable_markup: true +_help_lint: Options affecting the linter +lint: + _help_disabled_codes: + - a list of lint codes to disable + disabled_codes: [] + _help_function_pattern: + - regular expression pattern describing valid function names + function_pattern: '[0-9a-z_]+' + _help_macro_pattern: + - regular expression pattern describing valid macro names + macro_pattern: '[0-9A-Z_]+' + _help_global_var_pattern: + - regular expression pattern describing valid names for + - variables with global scope + global_var_pattern: '[0-9A-Z][0-9A-Z_]+' + _help_internal_var_pattern: + - regular expression pattern describing valid names for + - variables with global scope (but internal semantic) + internal_var_pattern: _[0-9A-Z][0-9A-Z_]+ + _help_local_var_pattern: + - regular expression pattern describing valid names for + - variables with local scope + local_var_pattern: '[0-9a-z_]+' + _help_private_var_pattern: + - regular expression pattern describing valid names for + - privatedirectory variables + private_var_pattern: _[0-9a-z_]+ + _help_public_var_pattern: + - regular expression pattern describing valid names for + - publicdirectory variables + public_var_pattern: '[0-9A-Z][0-9A-Z_]+' + _help_keyword_pattern: + - regular expression pattern describing valid names for + - keywords used in functions or macros + keyword_pattern: '[0-9A-Z_]+' + _help_max_conditionals_custom_parser: + - In the heuristic for C0201, how many conditionals to match + - within a loop in before considering the loop a parser. + max_conditionals_custom_parser: 2 + _help_min_statement_spacing: + - Require at least this many newlines between statements + min_statement_spacing: 1 + _help_max_statement_spacing: + - Require no more than this many newlines between statements + max_statement_spacing: 1 + max_returns: 6 + max_branches: 12 + max_arguments: 5 + max_localvars: 15 + max_statements: 50 +_help_encode: Options affecting file encoding +encode: + _help_emit_byteorder_mark: + - If true, emit the unicode byte-order mark (BOM) at the start + - of the file + emit_byteorder_mark: false + _help_input_encoding: + - Specify the encoding of the input file. Defaults to utf-8 + input_encoding: utf-8 + _help_output_encoding: + - Specify the encoding of the output file. Defaults to utf-8. + - Note that cmake only claims to support utf-8 so be careful + - when using anything else + output_encoding: utf-8 +_help_misc: Miscellaneous configurations options. +misc: + _help_per_command: + - A dictionary containing any per-command configuration + - overrides. Currently only `command_case` is supported. + per_command: {} diff --git a/cmake/CPM.cmake b/cmake/CPM.cmake new file mode 100644 index 00000000..fcc9c8ae --- /dev/null +++ b/cmake/CPM.cmake @@ -0,0 +1,22 @@ +# SPDX-License-Identifier: MIT +# +# SPDX-FileCopyrightText: Copyright (c) 2019-2023 Lars Melchior and contributors + +set(CPM_DOWNLOAD_VERSION 0.42.0) +set(CPM_HASH_SUM "2020b4fc42dba44817983e06342e682ecfc3d2f484a581f11cc5731fbe4dce8a") + +if(CPM_SOURCE_CACHE) + set(CPM_DOWNLOAD_LOCATION "${CPM_SOURCE_CACHE}/cpm/CPM_${CPM_DOWNLOAD_VERSION}.cmake") +elseif(DEFINED ENV{CPM_SOURCE_CACHE}) + set(CPM_DOWNLOAD_LOCATION "$ENV{CPM_SOURCE_CACHE}/cpm/CPM_${CPM_DOWNLOAD_VERSION}.cmake") +else() + set(CPM_DOWNLOAD_LOCATION "${CMAKE_BINARY_DIR}/cmake/CPM_${CPM_DOWNLOAD_VERSION}.cmake") +endif() + +# Expand relative path. This is important if the provided path contains a tilde (~) +get_filename_component(CPM_DOWNLOAD_LOCATION ${CPM_DOWNLOAD_LOCATION} ABSOLUTE) + +file(DOWNLOAD https://github.com/cpm-cmake/CPM.cmake/releases/download/v${CPM_DOWNLOAD_VERSION}/CPM.cmake ${CPM_DOWNLOAD_LOCATION} + EXPECTED_HASH SHA256=${CPM_HASH_SUM}) + +include(${CPM_DOWNLOAD_LOCATION}) diff --git a/docs/begin.base.bongocat.conf.md b/docs/begin.base.bongocat.conf.md index 2fbcc016..c9fc6c78 100644 --- a/docs/begin.base.bongocat.conf.md +++ b/docs/begin.base.bongocat.conf.md @@ -1,6 +1,6 @@ % BONGOCAT.CONF(5) % -% September 2025 +% December 2025 # NAME @@ -34,6 +34,9 @@ Changes to some settings require restarting Bongo Cat to take effect. - **overlay_position**: Screen position of overlay. Options: `top`, `bottom`. - **overlay_layer**: Layer of overlay. Options: `overlay`, `top`, `bottom` or `background`. +_Some overlay settings require a restart of the application_ +_**overlay_height** should work on config reload, it may take a second to reappear_ + # FRAME RATE - **fps**: Animation frames per second. diff --git a/examples/custom-sprite-sheets/README.md b/examples/custom-sprite-sheets/README.md index b31a6008..cd2315d9 100644 --- a/examples/custom-sprite-sheets/README.md +++ b/examples/custom-sprite-sheets/README.md @@ -3,6 +3,8 @@ Custom sprite sheets has a full animation per row. To determine the number of rows, the number of frames needs to be provided per row. +_NOTE: Bigger sprite sheet means more RAM usage_ + - `Idle` -- Idle Pose - `Boring` -- Boring animation for inactivity - `StartWriting` -- First keystroke @@ -24,7 +26,6 @@ To determine the number of rows, the number of frames needs to be provided per r Rows can be skipped by not providing the frames/columns, but the order of the rows needs to be the same. - ## General Example **Sprite Sheet** @@ -178,7 +179,7 @@ custom_rows=1 ## Know issues -### extra sprite when sprite is flipping (moving) +### part of other frame is shown when sprite is flipped (while moving) Please add some left and right empty padding in your frames. Doing to some rounding error, when flipping the frame, some pixels can be visible from the nearer frames. \ No newline at end of file diff --git a/examples/minimal.bongocat.conf b/examples/minimal.bongocat.conf index e53d7752..de6b2c30 100644 --- a/examples/minimal.bongocat.conf +++ b/examples/minimal.bongocat.conf @@ -4,9 +4,9 @@ cat_y_offset=0 cat_align=center cat_height=60 overlay_height=80 -overlay_opacity=150 +overlay_opacity=0 overlay_position=top -layer=overlay +layer=top fps=60 enable_antialiasing=1 animation_name=bongocat diff --git a/include/config/config.h b/include/config/config.h index ec75e715..f5fdca79 100644 --- a/include/config/config.h +++ b/include/config/config.h @@ -1,446 +1,480 @@ #ifndef BONGOCAT_CONFIG_H #define BONGOCAT_CONFIG_H -#include "embedded_assets/custom/custom_sprite.h" #include "core/bongocat.h" +#include "embedded_assets/custom/custom_sprite.h" #include "utils/error.h" -#include + #include +#include #include namespace bongocat::config { - enum class overlay_position_t : uint8_t { - POSITION_TOP, - POSITION_BOTTOM, - /* - POSITION_TOP_LEFT, - POSITION_BOTTOM_LEFT, - POSITION_TOP_RIGHT, - POSITION_BOTTOM_RIGHT, - */ - }; - inline static constexpr const char* POSITION_TOP_STR = "top"; - inline static constexpr const char* POSITION_BOTTOM_STR = "bottom"; - - enum class layer_type_t : int8_t { - LAYER_BACKGROUND = 0, - LAYER_BOTTOM = 1, - LAYER_TOP = 2, - LAYER_OVERLAY = 3, - }; - inline static constexpr const char* LAYER_BACKGROUND_STR = "background"; - inline static constexpr const char* LAYER_BOTTOM_STR = "bottom"; - inline static constexpr const char* LAYER_TOP_STR = "top"; - inline static constexpr const char* LAYER_OVERLAY_STR = "overlay"; - - enum class align_type_t : int8_t { - ALIGN_CENTER = 0, - ALIGN_LEFT = -1, - ALIGN_RIGHT = 1, - }; - inline static constexpr const char* ALIGN_CENTER_STR = "center"; - inline static constexpr const char* ALIGN_LEFT_STR = "left"; - inline static constexpr const char* ALIGN_RIGHT_STR = "right"; - - struct config_time_t { - int hour{0}; - int min{0}; - }; - - struct config_t; - void config_free_keyboard_devices(config_t& config); - void config_copy_keyboard_devices_from(config_t& config, const config_t& other); - void cleanup(config_t& config); - - enum class config_animation_sprite_sheet_layout_t : uint8_t { - None, - Bongocat, - Dm, - MsAgent, - Pkmn, - Custom, - }; - enum class config_animation_dm_set_t : uint8_t { - None, - min_dm, - dm, - dm20, - dmx, - pen, - pen20, - dmc, - dmall, - }; - enum class config_animation_custom_set_t : uint8_t { - None, - misc, - pmd, - custom, - }; - - struct config_t { - char *output_name{nullptr}; - char *keyboard_devices[input::MAX_INPUT_DEVICES]{}; - int32_t num_keyboard_devices{0}; - int32_t cat_x_offset{0}; - int32_t cat_y_offset{0}; - int32_t cat_height{0}; - int32_t overlay_height{0}; - int32_t idle_frame{0}; - int32_t keypress_duration_ms{0}; - int32_t test_animation_duration_ms{0}; - int32_t test_animation_interval_sec{0}; - int32_t animation_speed_ms{0}; - int32_t fps{0}; - int32_t overlay_opacity{0}; - int32_t mirror_x{0}; // reflect across Y axis (horizontal flip) - int32_t mirror_y{0}; // reflect across X axis (vertical flip) - int32_t enable_antialiasing{0}; // enable bilinear interpolation for smooth scaling - int32_t enable_debug{0}; - layer_type_t layer{layer_type_t::LAYER_TOP}; - overlay_position_t overlay_position{overlay_position_t::POSITION_TOP}; - - int32_t animation_index{0}; - int32_t invert_color{0}; - int32_t padding_x{0}; - int32_t padding_y{0}; - - int32_t enable_scheduled_sleep{0}; - config_time_t sleep_begin; - config_time_t sleep_end; - int32_t idle_sleep_timeout_sec{0}; - - int32_t happy_kpm{0}; - - align_type_t cat_align{align_type_t::ALIGN_CENTER}; - - config_animation_sprite_sheet_layout_t animation_sprite_sheet_layout{config_animation_sprite_sheet_layout_t::None}; - config_animation_dm_set_t animation_dm_set{config_animation_dm_set_t::None}; - config_animation_custom_set_t animation_custom_set{config_animation_custom_set_t::None}; - int32_t idle_animation{0}; - int32_t input_fps{0}; - int32_t randomize_index{0}; - int32_t randomize_on_reload{0}; - - int32_t update_rate_ms{0}; - double cpu_threshold{0}; - double cpu_running_factor{0}; - - int32_t movement_radius{0}; - int32_t enable_movement_debug{0}; - int32_t movement_speed{0}; - double movement_wait_factor{0}; - - int32_t screen_width{0}; - - char *custom_sprite_sheet_filename{nullptr}; // must be png file - assets::custom_animation_settings_t custom_sprite_sheet_settings{}; - - // for keep old index when reload config - bool _keep_old_animation_index{false}; - bool _strict{false}; - bool _custom{false}; // is custom sprite sheet - char *_animation_name{nullptr}; // original animation_anim from parsing config - char *_loaded_animation_fqname{nullptr}; - - - // Make Config movable and copyable - config_t() { - for (size_t i = 0; i < input::MAX_INPUT_DEVICES; ++i) { - keyboard_devices[i] = nullptr; - } - } - ~config_t() { - cleanup(*this); - } - - config_t(const config_t& other) - : cat_x_offset(other.cat_x_offset), - cat_y_offset(other.cat_y_offset), - cat_height(other.cat_height), - overlay_height(other.overlay_height), - idle_frame(other.idle_frame), - keypress_duration_ms(other.keypress_duration_ms), - test_animation_duration_ms(other.test_animation_duration_ms), - test_animation_interval_sec(other.test_animation_interval_sec), - animation_speed_ms(other.animation_speed_ms), - fps(other.fps), - overlay_opacity(other.overlay_opacity), - mirror_x(other.mirror_x), - mirror_y(other.mirror_y), - enable_antialiasing(other.enable_antialiasing), - enable_debug(other.enable_debug), - layer(other.layer), - overlay_position(other.overlay_position), - animation_index(other.animation_index), - invert_color(other.invert_color), - padding_x(other.padding_x), - padding_y(other.padding_y), - enable_scheduled_sleep(other.enable_scheduled_sleep), - sleep_begin(other.sleep_begin), - sleep_end(other.sleep_end), - idle_sleep_timeout_sec(other.idle_sleep_timeout_sec), - happy_kpm(other.happy_kpm), - cat_align(other.cat_align), - animation_sprite_sheet_layout(other.animation_sprite_sheet_layout), - animation_dm_set(other.animation_dm_set), - animation_custom_set(other.animation_custom_set), - idle_animation(other.idle_animation), - input_fps(other.input_fps), - randomize_index(other.randomize_index), - randomize_on_reload(other.randomize_on_reload), - update_rate_ms(other.update_rate_ms), - cpu_threshold(other.cpu_threshold), - cpu_running_factor(other.cpu_running_factor), - movement_radius(other.movement_radius), - enable_movement_debug(other.enable_movement_debug), - movement_speed(other.movement_speed), - movement_wait_factor(other.movement_wait_factor), - screen_width(other.screen_width), - custom_sprite_sheet_settings(other.custom_sprite_sheet_settings), - _keep_old_animation_index(other._keep_old_animation_index), - _strict(other._strict), - _custom(other._custom) - { - output_name = other.output_name ? strdup(other.output_name) : nullptr; - config_copy_keyboard_devices_from(*this, other); - custom_sprite_sheet_filename = other.custom_sprite_sheet_filename ? strdup(other.custom_sprite_sheet_filename) : nullptr; - _animation_name = other._animation_name ? strdup(other._animation_name) : nullptr; - _loaded_animation_fqname = other._loaded_animation_fqname ? strdup(other._loaded_animation_fqname) : nullptr; - } - - config_t& operator=(const config_t& other) { - if (this != &other) { - cleanup(*this); - - cat_x_offset = other.cat_x_offset; - cat_y_offset = other.cat_y_offset; - cat_height = other.cat_height; - overlay_height = other.overlay_height; - idle_frame = other.idle_frame; - keypress_duration_ms = other.keypress_duration_ms; - test_animation_duration_ms = other.test_animation_duration_ms; - test_animation_interval_sec = other.test_animation_interval_sec; - animation_speed_ms = other.animation_speed_ms; - fps = other.fps; - overlay_opacity = other.overlay_opacity; - mirror_x = other.mirror_x; - mirror_y = other.mirror_y; - enable_antialiasing = other.enable_antialiasing; - enable_debug = other.enable_debug; - layer = other.layer; - overlay_position = other.overlay_position; - animation_index = other.animation_index; - invert_color = other.invert_color; - padding_x = other.padding_x; - padding_y = other.padding_y; - enable_scheduled_sleep = other.enable_scheduled_sleep; - sleep_begin = other.sleep_begin; - sleep_end = other.sleep_end; - idle_sleep_timeout_sec = other.idle_sleep_timeout_sec; - happy_kpm = other.happy_kpm; - cat_align = other.cat_align; - animation_sprite_sheet_layout = other.animation_sprite_sheet_layout; - animation_dm_set = other.animation_dm_set; - animation_custom_set = other.animation_custom_set; - idle_animation = other.idle_animation; - input_fps = other.input_fps; - randomize_index = other.randomize_index; - randomize_on_reload = other.randomize_on_reload; - update_rate_ms = other.update_rate_ms; - movement_radius = other.movement_radius; - enable_movement_debug = other.enable_movement_debug; - movement_speed = other.movement_speed; - movement_wait_factor = other.movement_wait_factor; - cpu_threshold = other.cpu_threshold; - cpu_running_factor = other.cpu_running_factor; - screen_width = other.screen_width; - custom_sprite_sheet_settings = other.custom_sprite_sheet_settings; - _keep_old_animation_index = other._keep_old_animation_index; - _strict = other._strict; - _custom = other._custom; - - output_name = other.output_name ? strdup(other.output_name) : nullptr; - config_copy_keyboard_devices_from(*this, other); - custom_sprite_sheet_filename = other.custom_sprite_sheet_filename ? strdup(other.custom_sprite_sheet_filename) : nullptr; - _animation_name = other._animation_name ? strdup(other._animation_name) : nullptr; - _loaded_animation_fqname = other._loaded_animation_fqname ? strdup(other._loaded_animation_fqname) : nullptr; - } - return *this; - } - - config_t(config_t&& other) noexcept - : output_name(other.output_name), - num_keyboard_devices(other.num_keyboard_devices), - cat_x_offset(other.cat_x_offset), - cat_y_offset(other.cat_y_offset), - cat_height(other.cat_height), - overlay_height(other.overlay_height), - idle_frame(other.idle_frame), - keypress_duration_ms(other.keypress_duration_ms), - test_animation_duration_ms(other.test_animation_duration_ms), - test_animation_interval_sec(other.test_animation_interval_sec), - animation_speed_ms(other.animation_speed_ms), - fps(other.fps), - overlay_opacity(other.overlay_opacity), - mirror_x(other.mirror_x), - mirror_y(other.mirror_y), - enable_antialiasing(other.enable_antialiasing), - enable_debug(other.enable_debug), - layer(other.layer), - overlay_position(other.overlay_position), - animation_index(other.animation_index), - invert_color(other.invert_color), - padding_x(other.padding_x), - padding_y(other.padding_y), - enable_scheduled_sleep(other.enable_scheduled_sleep), - sleep_begin(other.sleep_begin), - sleep_end(other.sleep_end), - idle_sleep_timeout_sec(other.idle_sleep_timeout_sec), - happy_kpm(other.happy_kpm), - cat_align(other.cat_align), - animation_sprite_sheet_layout(other.animation_sprite_sheet_layout), - animation_dm_set(other.animation_dm_set), - animation_custom_set(other.animation_custom_set), - idle_animation(other.idle_animation), - input_fps(other.input_fps), - randomize_index(other.randomize_index), - randomize_on_reload(other.randomize_on_reload), - update_rate_ms(other.update_rate_ms), - cpu_threshold(other.cpu_threshold), - cpu_running_factor(other.cpu_running_factor), - movement_radius(other.movement_radius), - enable_movement_debug(other.enable_movement_debug), - movement_speed(other.movement_speed), - movement_wait_factor(other.movement_wait_factor), - screen_width(other.screen_width), - custom_sprite_sheet_filename(other.custom_sprite_sheet_filename), - custom_sprite_sheet_settings(other.custom_sprite_sheet_settings), - _keep_old_animation_index(other._keep_old_animation_index), - _strict(other._strict), - _custom(other._custom), - _animation_name(other._animation_name), - _loaded_animation_fqname(other._loaded_animation_fqname) - { - for (int i = 0; i < num_keyboard_devices; ++i) { - keyboard_devices[i] = other.keyboard_devices[i]; - other.keyboard_devices[i] = nullptr; - } - other.num_keyboard_devices = 0; - other.output_name = nullptr; - other.custom_sprite_sheet_filename = nullptr; - other._animation_name = nullptr; - other._loaded_animation_fqname = nullptr; - } - - config_t& operator=(config_t&& other) noexcept { - if (this != &other) { - cleanup(*this); - - output_name = other.output_name; - num_keyboard_devices = other.num_keyboard_devices; - cat_x_offset = other.cat_x_offset; - cat_y_offset = other.cat_y_offset; - cat_height = other.cat_height; - overlay_height = other.overlay_height; - idle_frame = other.idle_frame; - keypress_duration_ms = other.keypress_duration_ms; - test_animation_duration_ms = other.test_animation_duration_ms; - test_animation_interval_sec = other.test_animation_interval_sec; - animation_speed_ms = other.animation_speed_ms; - fps = other.fps; - overlay_opacity = other.overlay_opacity; - mirror_x = other.mirror_x; - mirror_y = other.mirror_y; - enable_antialiasing = other.enable_antialiasing; - enable_debug = other.enable_debug; - layer = other.layer; - overlay_position = other.overlay_position; - animation_index = other.animation_index; - invert_color = other.invert_color; - padding_x = other.padding_x; - padding_y = other.padding_y; - enable_scheduled_sleep = other.enable_scheduled_sleep; - sleep_begin = other.sleep_begin; - sleep_end = other.sleep_end; - idle_sleep_timeout_sec = other.idle_sleep_timeout_sec; - happy_kpm = other.happy_kpm; - cat_align = other.cat_align; - animation_sprite_sheet_layout = other.animation_sprite_sheet_layout; - animation_dm_set = other.animation_dm_set; - animation_custom_set = other.animation_custom_set; - idle_animation = other.idle_animation; - input_fps = other.input_fps; - randomize_index = other.randomize_index; - randomize_on_reload = other.randomize_on_reload; - update_rate_ms = other.update_rate_ms; - cpu_threshold = other.cpu_threshold; - cpu_running_factor = other.cpu_running_factor; - movement_radius = other.movement_radius; - enable_movement_debug = other.enable_movement_debug; - movement_speed = other.movement_speed; - movement_wait_factor = other.movement_wait_factor; - custom_sprite_sheet_filename = other.custom_sprite_sheet_filename; - screen_width = other.screen_width; - custom_sprite_sheet_settings = other.custom_sprite_sheet_settings; - _keep_old_animation_index = other._keep_old_animation_index; - _strict = other._strict; - _custom = other._custom; - _animation_name = other._animation_name; - _loaded_animation_fqname = other._loaded_animation_fqname; - - for (int i = 0; i < num_keyboard_devices; ++i) { - keyboard_devices[i] = other.keyboard_devices[i]; - other.keyboard_devices[i] = nullptr; - } - other.num_keyboard_devices = 0; - other.output_name = nullptr; - other.custom_sprite_sheet_filename = nullptr; - other._animation_name = nullptr; - other._loaded_animation_fqname = nullptr; - } - return *this; - } - }; - inline void cleanup(config_t& config) { - if (config.output_name) ::free(config.output_name); - config.output_name = nullptr; - config_free_keyboard_devices(config); - if (config.custom_sprite_sheet_filename) ::free(config.custom_sprite_sheet_filename); - config.custom_sprite_sheet_filename = nullptr; - if (config._animation_name) ::free(config._animation_name); - config._animation_name = nullptr; - if (config._loaded_animation_fqname) ::free(config._loaded_animation_fqname); - config._loaded_animation_fqname = nullptr; +enum class overlay_position_t : uint8_t { + POSITION_TOP, + POSITION_BOTTOM, + /* + POSITION_TOP_LEFT, + POSITION_BOTTOM_LEFT, + POSITION_TOP_RIGHT, + POSITION_BOTTOM_RIGHT, + */ +}; +inline static constexpr const char *POSITION_TOP_STR = "top"; +inline static constexpr const char *POSITION_BOTTOM_STR = "bottom"; + +enum class layer_type_t : int8_t { + LAYER_BACKGROUND = 0, + LAYER_BOTTOM = 1, + LAYER_TOP = 2, + LAYER_OVERLAY = 3, +}; +inline static constexpr const char *LAYER_BACKGROUND_STR = "background"; +inline static constexpr const char *LAYER_BOTTOM_STR = "bottom"; +inline static constexpr const char *LAYER_TOP_STR = "top"; +inline static constexpr const char *LAYER_OVERLAY_STR = "overlay"; + +enum class align_type_t : int8_t { + ALIGN_CENTER = 0, + ALIGN_LEFT = -1, + ALIGN_RIGHT = 1, +}; +inline static constexpr const char *ALIGN_CENTER_STR = "center"; +inline static constexpr const char *ALIGN_LEFT_STR = "left"; +inline static constexpr const char *ALIGN_RIGHT_STR = "right"; + +// ============================================================================= +// CONFIGURATION FUNCTIONS +// ============================================================================= + +struct config_t; +void config_free_keyboard_devices(config_t& config); +void config_copy_keyboard_devices_from(config_t& config, const config_t& other); +void cleanup(config_t& config); + +// ============================================================================= +// CONFIGURATION TYPES +// ============================================================================= + +struct config_time_t { + int hour{0}; + int min{0}; +}; +enum class config_animation_sprite_sheet_layout_t : uint8_t { + None, + Bongocat, + Dm, + MsAgent, + Pkmn, + Custom, +}; +enum class config_animation_dm_set_t : uint8_t { + None, + min_dm, + dm, + dm20, + dmx, + pen, + pen20, + dmc, + dmall, +}; +enum class config_animation_custom_set_t : uint8_t { + None, + misc, + pmd, + custom, +}; + +struct config_t { + char *output_name{BONGOCAT_NULLPTR}; + char *keyboard_devices[input::MAX_INPUT_DEVICES]{}; + int32_t num_keyboard_devices{0}; + int32_t cat_x_offset{0}; + int32_t cat_y_offset{0}; + int32_t cat_height{0}; + int32_t overlay_height{0}; + int32_t idle_frame{0}; + int32_t keypress_duration_ms{0}; + int32_t test_animation_duration_ms{0}; + int32_t test_animation_interval_sec{0}; + int32_t animation_speed_ms{0}; + int32_t fps{0}; + int32_t overlay_opacity{0}; + int32_t mirror_x{0}; // reflect across Y axis (horizontal flip) + int32_t mirror_y{0}; // reflect across X axis (vertical flip) + int32_t enable_antialiasing{0}; // enable bilinear interpolation for smooth scaling + int32_t enable_debug{0}; + layer_type_t layer{layer_type_t::LAYER_TOP}; + overlay_position_t overlay_position{overlay_position_t::POSITION_TOP}; + + int32_t animation_index{0}; + int32_t invert_color{0}; + int32_t padding_x{0}; + int32_t padding_y{0}; + + int32_t enable_scheduled_sleep{0}; + config_time_t sleep_begin; + config_time_t sleep_end; + int32_t idle_sleep_timeout_sec{0}; + + int32_t happy_kpm{0}; + + align_type_t cat_align{align_type_t::ALIGN_CENTER}; + + config_animation_sprite_sheet_layout_t animation_sprite_sheet_layout{config_animation_sprite_sheet_layout_t::None}; + config_animation_dm_set_t animation_dm_set{config_animation_dm_set_t::None}; + config_animation_custom_set_t animation_custom_set{config_animation_custom_set_t::None}; + int32_t idle_animation{0}; + int32_t input_fps{0}; + int32_t randomize_index{0}; + int32_t randomize_on_reload{0}; + + int32_t update_rate_ms{0}; + double cpu_threshold{0}; + double cpu_running_factor{0}; + + int32_t movement_radius{0}; + int32_t enable_movement_debug{0}; + int32_t movement_speed{0}; + double movement_wait_factor{0}; + + int32_t screen_width{0}; + + char *custom_sprite_sheet_filename{BONGOCAT_NULLPTR}; // must be png file + assets::custom_animation_settings_t custom_sprite_sheet_settings{}; + + int32_t enable_hand_mapping{0}; + + // for keep old index when reload config + bool _keep_old_animation_index{false}; + bool _strict{false}; + bool _custom{false}; // is custom sprite sheet + char *_animation_name{BONGOCAT_NULLPTR}; // original animation_anim from parsing config + char *_loaded_animation_fqname{BONGOCAT_NULLPTR}; + + // Make Config movable and copyable + config_t() { + for (size_t i = 0; i < input::MAX_INPUT_DEVICES; ++i) { + keyboard_devices[i] = BONGOCAT_NULLPTR; } - - inline void config_free_keyboard_devices(config_t& config) { - assert(config.num_keyboard_devices >= 0); - for (size_t i = 0; i < static_cast(config.num_keyboard_devices) && i < input::MAX_INPUT_DEVICES; i++) { - if (config.keyboard_devices[i]) ::free(config.keyboard_devices[i]); - config.keyboard_devices[i] = nullptr; - } - config.num_keyboard_devices = 0; + } + ~config_t() { + cleanup(*this); + } + + config_t(const config_t& other) + : cat_x_offset(other.cat_x_offset) + , cat_y_offset(other.cat_y_offset) + , cat_height(other.cat_height) + , overlay_height(other.overlay_height) + , idle_frame(other.idle_frame) + , keypress_duration_ms(other.keypress_duration_ms) + , test_animation_duration_ms(other.test_animation_duration_ms) + , test_animation_interval_sec(other.test_animation_interval_sec) + , animation_speed_ms(other.animation_speed_ms) + , fps(other.fps) + , overlay_opacity(other.overlay_opacity) + , mirror_x(other.mirror_x) + , mirror_y(other.mirror_y) + , enable_antialiasing(other.enable_antialiasing) + , enable_debug(other.enable_debug) + , layer(other.layer) + , overlay_position(other.overlay_position) + , animation_index(other.animation_index) + , invert_color(other.invert_color) + , padding_x(other.padding_x) + , padding_y(other.padding_y) + , enable_scheduled_sleep(other.enable_scheduled_sleep) + , sleep_begin(other.sleep_begin) + , sleep_end(other.sleep_end) + , idle_sleep_timeout_sec(other.idle_sleep_timeout_sec) + , happy_kpm(other.happy_kpm) + , cat_align(other.cat_align) + , animation_sprite_sheet_layout(other.animation_sprite_sheet_layout) + , animation_dm_set(other.animation_dm_set) + , animation_custom_set(other.animation_custom_set) + , idle_animation(other.idle_animation) + , input_fps(other.input_fps) + , randomize_index(other.randomize_index) + , randomize_on_reload(other.randomize_on_reload) + , update_rate_ms(other.update_rate_ms) + , cpu_threshold(other.cpu_threshold) + , cpu_running_factor(other.cpu_running_factor) + , movement_radius(other.movement_radius) + , enable_movement_debug(other.enable_movement_debug) + , movement_speed(other.movement_speed) + , movement_wait_factor(other.movement_wait_factor) + , screen_width(other.screen_width) + , custom_sprite_sheet_settings(other.custom_sprite_sheet_settings) + , enable_hand_mapping(other.enable_hand_mapping) + , _keep_old_animation_index(other._keep_old_animation_index) + , _strict(other._strict) + , _custom(other._custom) { + output_name = other.output_name != BONGOCAT_NULLPTR ? strdup(other.output_name) : BONGOCAT_NULLPTR; + config_copy_keyboard_devices_from(*this, other); + custom_sprite_sheet_filename = other.custom_sprite_sheet_filename != BONGOCAT_NULLPTR + ? strdup(other.custom_sprite_sheet_filename) + : BONGOCAT_NULLPTR; + _animation_name = other._animation_name != BONGOCAT_NULLPTR ? strdup(other._animation_name) : BONGOCAT_NULLPTR; + _loaded_animation_fqname = + other._loaded_animation_fqname != BONGOCAT_NULLPTR ? strdup(other._loaded_animation_fqname) : BONGOCAT_NULLPTR; + } + + config_t& operator=(const config_t& other) { + if (this != &other) { + cleanup(*this); + + cat_x_offset = other.cat_x_offset; + cat_y_offset = other.cat_y_offset; + cat_height = other.cat_height; + overlay_height = other.overlay_height; + idle_frame = other.idle_frame; + keypress_duration_ms = other.keypress_duration_ms; + test_animation_duration_ms = other.test_animation_duration_ms; + test_animation_interval_sec = other.test_animation_interval_sec; + animation_speed_ms = other.animation_speed_ms; + fps = other.fps; + overlay_opacity = other.overlay_opacity; + mirror_x = other.mirror_x; + mirror_y = other.mirror_y; + enable_antialiasing = other.enable_antialiasing; + enable_debug = other.enable_debug; + layer = other.layer; + overlay_position = other.overlay_position; + animation_index = other.animation_index; + invert_color = other.invert_color; + padding_x = other.padding_x; + padding_y = other.padding_y; + enable_scheduled_sleep = other.enable_scheduled_sleep; + sleep_begin = other.sleep_begin; + sleep_end = other.sleep_end; + idle_sleep_timeout_sec = other.idle_sleep_timeout_sec; + happy_kpm = other.happy_kpm; + cat_align = other.cat_align; + animation_sprite_sheet_layout = other.animation_sprite_sheet_layout; + animation_dm_set = other.animation_dm_set; + animation_custom_set = other.animation_custom_set; + idle_animation = other.idle_animation; + input_fps = other.input_fps; + randomize_index = other.randomize_index; + randomize_on_reload = other.randomize_on_reload; + update_rate_ms = other.update_rate_ms; + movement_radius = other.movement_radius; + enable_movement_debug = other.enable_movement_debug; + movement_speed = other.movement_speed; + movement_wait_factor = other.movement_wait_factor; + cpu_threshold = other.cpu_threshold; + cpu_running_factor = other.cpu_running_factor; + screen_width = other.screen_width; + custom_sprite_sheet_settings = other.custom_sprite_sheet_settings; + enable_hand_mapping = other.enable_hand_mapping; + _keep_old_animation_index = other._keep_old_animation_index; + _strict = other._strict; + _custom = other._custom; + + output_name = other.output_name != BONGOCAT_NULLPTR ? strdup(other.output_name) : BONGOCAT_NULLPTR; + config_copy_keyboard_devices_from(*this, other); + custom_sprite_sheet_filename = other.custom_sprite_sheet_filename != BONGOCAT_NULLPTR + ? strdup(other.custom_sprite_sheet_filename) + : BONGOCAT_NULLPTR; + _animation_name = other._animation_name != BONGOCAT_NULLPTR ? strdup(other._animation_name) : BONGOCAT_NULLPTR; + _loaded_animation_fqname = other._loaded_animation_fqname != BONGOCAT_NULLPTR + ? strdup(other._loaded_animation_fqname) + : BONGOCAT_NULLPTR; } - inline void config_copy_keyboard_devices_from(config_t& config, const config_t& other) { - config_free_keyboard_devices(config); - config.num_keyboard_devices = other.num_keyboard_devices; - assert(config.num_keyboard_devices >= 0); - for (size_t i = 0; i < static_cast(config.num_keyboard_devices) && i < input::MAX_INPUT_DEVICES; i++) { - config.keyboard_devices[i] = other.keyboard_devices[i] ? strdup(other.keyboard_devices[i]) : nullptr; - } + return *this; + } + + config_t(config_t&& other) noexcept + : output_name(other.output_name) + , num_keyboard_devices(other.num_keyboard_devices) + , cat_x_offset(other.cat_x_offset) + , cat_y_offset(other.cat_y_offset) + , cat_height(other.cat_height) + , overlay_height(other.overlay_height) + , idle_frame(other.idle_frame) + , keypress_duration_ms(other.keypress_duration_ms) + , test_animation_duration_ms(other.test_animation_duration_ms) + , test_animation_interval_sec(other.test_animation_interval_sec) + , animation_speed_ms(other.animation_speed_ms) + , fps(other.fps) + , overlay_opacity(other.overlay_opacity) + , mirror_x(other.mirror_x) + , mirror_y(other.mirror_y) + , enable_antialiasing(other.enable_antialiasing) + , enable_debug(other.enable_debug) + , layer(other.layer) + , overlay_position(other.overlay_position) + , animation_index(other.animation_index) + , invert_color(other.invert_color) + , padding_x(other.padding_x) + , padding_y(other.padding_y) + , enable_scheduled_sleep(other.enable_scheduled_sleep) + , sleep_begin(other.sleep_begin) + , sleep_end(other.sleep_end) + , idle_sleep_timeout_sec(other.idle_sleep_timeout_sec) + , happy_kpm(other.happy_kpm) + , cat_align(other.cat_align) + , animation_sprite_sheet_layout(other.animation_sprite_sheet_layout) + , animation_dm_set(other.animation_dm_set) + , animation_custom_set(other.animation_custom_set) + , idle_animation(other.idle_animation) + , input_fps(other.input_fps) + , randomize_index(other.randomize_index) + , randomize_on_reload(other.randomize_on_reload) + , update_rate_ms(other.update_rate_ms) + , cpu_threshold(other.cpu_threshold) + , cpu_running_factor(other.cpu_running_factor) + , movement_radius(other.movement_radius) + , enable_movement_debug(other.enable_movement_debug) + , movement_speed(other.movement_speed) + , movement_wait_factor(other.movement_wait_factor) + , screen_width(other.screen_width) + , custom_sprite_sheet_filename(other.custom_sprite_sheet_filename) + , custom_sprite_sheet_settings(other.custom_sprite_sheet_settings) + , enable_hand_mapping(other.enable_hand_mapping) + , _keep_old_animation_index(other._keep_old_animation_index) + , _strict(other._strict) + , _custom(other._custom) + , _animation_name(other._animation_name) + , _loaded_animation_fqname(other._loaded_animation_fqname) { + for (int i = 0; i < num_keyboard_devices; ++i) { + keyboard_devices[i] = other.keyboard_devices[i]; + other.keyboard_devices[i] = BONGOCAT_NULLPTR; } + other.num_keyboard_devices = 0; + other.output_name = BONGOCAT_NULLPTR; + other.custom_sprite_sheet_filename = BONGOCAT_NULLPTR; + other._animation_name = BONGOCAT_NULLPTR; + other._loaded_animation_fqname = BONGOCAT_NULLPTR; + } + + config_t& operator=(config_t&& other) noexcept { + if (this != &other) { + cleanup(*this); + + output_name = other.output_name; + num_keyboard_devices = other.num_keyboard_devices; + cat_x_offset = other.cat_x_offset; + cat_y_offset = other.cat_y_offset; + cat_height = other.cat_height; + overlay_height = other.overlay_height; + idle_frame = other.idle_frame; + keypress_duration_ms = other.keypress_duration_ms; + test_animation_duration_ms = other.test_animation_duration_ms; + test_animation_interval_sec = other.test_animation_interval_sec; + animation_speed_ms = other.animation_speed_ms; + fps = other.fps; + overlay_opacity = other.overlay_opacity; + mirror_x = other.mirror_x; + mirror_y = other.mirror_y; + enable_antialiasing = other.enable_antialiasing; + enable_debug = other.enable_debug; + layer = other.layer; + overlay_position = other.overlay_position; + animation_index = other.animation_index; + invert_color = other.invert_color; + padding_x = other.padding_x; + padding_y = other.padding_y; + enable_scheduled_sleep = other.enable_scheduled_sleep; + sleep_begin = other.sleep_begin; + sleep_end = other.sleep_end; + idle_sleep_timeout_sec = other.idle_sleep_timeout_sec; + happy_kpm = other.happy_kpm; + cat_align = other.cat_align; + animation_sprite_sheet_layout = other.animation_sprite_sheet_layout; + animation_dm_set = other.animation_dm_set; + animation_custom_set = other.animation_custom_set; + idle_animation = other.idle_animation; + input_fps = other.input_fps; + randomize_index = other.randomize_index; + randomize_on_reload = other.randomize_on_reload; + update_rate_ms = other.update_rate_ms; + cpu_threshold = other.cpu_threshold; + cpu_running_factor = other.cpu_running_factor; + movement_radius = other.movement_radius; + enable_movement_debug = other.enable_movement_debug; + movement_speed = other.movement_speed; + movement_wait_factor = other.movement_wait_factor; + custom_sprite_sheet_filename = other.custom_sprite_sheet_filename; + screen_width = other.screen_width; + custom_sprite_sheet_settings = other.custom_sprite_sheet_settings; + enable_hand_mapping = other.enable_hand_mapping; + _keep_old_animation_index = other._keep_old_animation_index; + _strict = other._strict; + _custom = other._custom; + _animation_name = other._animation_name; + _loaded_animation_fqname = other._loaded_animation_fqname; + + for (int i = 0; i < num_keyboard_devices; ++i) { + keyboard_devices[i] = other.keyboard_devices[i]; + other.keyboard_devices[i] = BONGOCAT_NULLPTR; + } + other.num_keyboard_devices = 0; + other.output_name = BONGOCAT_NULLPTR; + other.custom_sprite_sheet_filename = BONGOCAT_NULLPTR; + other._animation_name = BONGOCAT_NULLPTR; + other._loaded_animation_fqname = BONGOCAT_NULLPTR; + } + return *this; + } +}; +inline void cleanup(config_t& config) { + if (config.output_name != BONGOCAT_NULLPTR) { + ::free(config.output_name); + config.output_name = BONGOCAT_NULLPTR; + } + config_free_keyboard_devices(config); + if (config.custom_sprite_sheet_filename != BONGOCAT_NULLPTR) { + ::free(config.custom_sprite_sheet_filename); + config.custom_sprite_sheet_filename = BONGOCAT_NULLPTR; + } + if (config._animation_name != BONGOCAT_NULLPTR) { + ::free(config._animation_name); + config._animation_name = BONGOCAT_NULLPTR; + } + if (config._loaded_animation_fqname != BONGOCAT_NULLPTR) { + ::free(config._loaded_animation_fqname); + config._loaded_animation_fqname = BONGOCAT_NULLPTR; + } +} - struct load_config_overwrite_parameters_t { - const char* output_name{nullptr}; - int32_t randomize_index{-1}; - int32_t strict{-1}; - const char* animation_name{nullptr}; - }; - [[nodiscard]] created_result_t load(const char *config_file_path, load_config_overwrite_parameters_t overwrite_parameters); - void reset(config_t& config); - - void set_defaults(config_t& config); +inline void config_free_keyboard_devices(config_t& config) { + assert(config.num_keyboard_devices >= 0); + for (size_t i = 0; i < static_cast(config.num_keyboard_devices) && i < input::MAX_INPUT_DEVICES; i++) { + if (config.keyboard_devices[i] != BONGOCAT_NULLPTR) { + ::free(config.keyboard_devices[i]); + config.keyboard_devices[i] = BONGOCAT_NULLPTR; + } + } + config.num_keyboard_devices = 0; +} +inline void config_copy_keyboard_devices_from(config_t& config, const config_t& other) { + config_free_keyboard_devices(config); + config.num_keyboard_devices = other.num_keyboard_devices; + assert(config.num_keyboard_devices >= 0); + for (size_t i = 0; i < static_cast(config.num_keyboard_devices) && i < input::MAX_INPUT_DEVICES; i++) { + config.keyboard_devices[i] = + other.keyboard_devices[i] != BONGOCAT_NULLPTR ? strdup(other.keyboard_devices[i]) : BONGOCAT_NULLPTR; + } } -#endif // BONGOCAT_CONFIG_H \ No newline at end of file +// ============================================================================= +// CONFIGURATION FUNCTIONS +// ============================================================================= + +struct load_config_overwrite_parameters_t { + const char *output_name{BONGOCAT_NULLPTR}; + int32_t randomize_index{-1}; + int32_t strict{-1}; + const char *animation_name{BONGOCAT_NULLPTR}; +}; +BONGOCAT_NODISCARD created_result_t load(const char *config_file_path, + load_config_overwrite_parameters_t overwrite_parameters); +void reset(config_t& config); + +void set_defaults(config_t& config); +} // namespace bongocat::config + +#endif // BONGOCAT_CONFIG_H \ No newline at end of file diff --git a/include/config/config_watcher.h b/include/config/config_watcher.h index 44d31897..57de6d62 100644 --- a/include/config/config_watcher.h +++ b/include/config/config_watcher.h @@ -3,68 +3,80 @@ #include "core/bongocat.h" #include "utils/system_memory.h" + +#include #include #include -#include - namespace bongocat::config { - // Config watcher constants - inline static constexpr size_t INOTIFY_EVENT_SIZE = sizeof(struct inotify_event); - inline static constexpr size_t INOTIFY_BUF_LEN = 1024 * (INOTIFY_EVENT_SIZE + 16); - - struct config_watcher_t; - void stop_watcher(config_watcher_t& watcher); - void cleanup_watcher(config_watcher_t& watcher); - - // Config watcher structure - struct config_watcher_t { - platform::FileDescriptor inotify_fd; - platform::FileDescriptor wd_file; - platform::FileDescriptor wd_dir; - char* config_path{nullptr}; - - platform::FileDescriptor reload_efd; - - pthread_t _watcher_thread{0}; - atomic_bool _running{false}; - - - config_watcher_t() = default; - ~config_watcher_t() { - cleanup_watcher(*this); - } - - config_watcher_t(const config_watcher_t&) = delete; - config_watcher_t& operator=(const config_watcher_t&) = delete; - config_watcher_t(config_watcher_t&& other) noexcept = delete; - config_watcher_t& operator=(config_watcher_t&& other) noexcept = delete; - }; - inline void cleanup_watcher(config_watcher_t& watcher) { - stop_watcher(watcher); - - // remove watches first (requires inotify fd still open) - if (watcher.inotify_fd._fd >= 0 && watcher.wd_file._fd >= 0) { - inotify_rm_watch(watcher.inotify_fd._fd, watcher.wd_file._fd); - watcher.wd_file._fd = -1; - } - if (watcher.inotify_fd._fd >= 0 && watcher.wd_dir._fd >= 0) { - inotify_rm_watch(watcher.inotify_fd._fd, watcher.wd_dir._fd); - watcher.wd_dir._fd = -1; - } - - close_fd(watcher.inotify_fd); - close_fd(watcher.reload_efd); - - if (watcher.config_path) { - ::free(watcher.config_path); - watcher.config_path = nullptr; - } - } - - // Config watcher function declarations - [[nodiscard]] created_result_t> create_watcher(const char *config_path); - void start_watcher(config_watcher_t& watcher); +// Inotify buffer sizing +inline static constexpr size_t INOTIFY_EVENT_SIZE = sizeof(struct inotify_event); +inline static constexpr size_t INOTIFY_BUF_LEN = 1024 * (INOTIFY_EVENT_SIZE + 16); + +// ============================================================================= +// TYPE DEFINITIONS +// ============================================================================= + +struct config_watcher_t; +// Stop watching for config changes +void stop_watcher(config_watcher_t& watcher); + +// Cleanup config watcher resources +void cleanup_watcher(config_watcher_t& watcher); + +// Config watcher structure +struct config_watcher_t { + platform::FileDescriptor inotify_fd; + platform::FileDescriptor wd_file; + platform::FileDescriptor wd_dir; + char *config_path{BONGOCAT_NULLPTR}; + + platform::FileDescriptor reload_efd; + + pthread_t _watcher_thread{0}; + atomic_bool _running{false}; + + config_watcher_t() = default; + ~config_watcher_t() { + cleanup_watcher(*this); + } + + config_watcher_t(const config_watcher_t&) = delete; + config_watcher_t& operator=(const config_watcher_t&) = delete; + config_watcher_t(config_watcher_t&& other) noexcept = delete; + config_watcher_t& operator=(config_watcher_t&& other) noexcept = delete; +}; +inline void cleanup_watcher(config_watcher_t& watcher) { + stop_watcher(watcher); + + // remove watches first (requires inotify fd still open) + if (watcher.inotify_fd._fd >= 0 && watcher.wd_file._fd >= 0) { + inotify_rm_watch(watcher.inotify_fd._fd, watcher.wd_file._fd); + watcher.wd_file._fd = -1; + } + if (watcher.inotify_fd._fd >= 0 && watcher.wd_dir._fd >= 0) { + inotify_rm_watch(watcher.inotify_fd._fd, watcher.wd_dir._fd); + watcher.wd_dir._fd = -1; + } + + close_fd(watcher.inotify_fd); + close_fd(watcher.reload_efd); + + if (watcher.config_path != BONGOCAT_NULLPTR) { + ::free(watcher.config_path); + watcher.config_path = BONGOCAT_NULLPTR; + } } -#endif // BONGOCAT_CONFIG_WATCHER_H \ No newline at end of file +// ============================================================================= +// CONFIG WATCHER FUNCTIONS +// ============================================================================= + +// Initialize config watcher +BONGOCAT_NODISCARD created_result_t> create_watcher(const char *config_path); + +// Start watching for config changes +void start_watcher(config_watcher_t& watcher); +} // namespace bongocat::config + +#endif // BONGOCAT_CONFIG_WATCHER_H \ No newline at end of file diff --git a/include/core/bongocat.h b/include/core/bongocat.h index 1354970b..6b412d9f 100644 --- a/include/core/bongocat.h +++ b/include/core/bongocat.h @@ -1,13 +1,25 @@ #ifndef BONGOCAT_BONGOCAT_H #define BONGOCAT_BONGOCAT_H -#include -#include +// POSIX feature test macro - must be before includes +#define _POSIX_C_SOURCE 200809L + #include "utils/error.h" #include "utils/memory.h" +#include +#include + +// ============================================================================= +// VERSION +// ============================================================================= + // Version -inline static constexpr const char* BONGOCAT_VERSION = "3.5.0"; +inline static constexpr const char *BONGOCAT_VERSION = "3.6.0"; + +// ============================================================================= +// COMPILE-TIME CONSTANTS +// ============================================================================= // Common constants inline static constexpr int32_t DEFAULT_SCREEN_WIDTH = 1920; @@ -17,223 +29,227 @@ inline static constexpr int32_t RGBA_CHANNELS = 4; inline static constexpr int32_t BGRA_CHANNELS = 4; namespace bongocat { - template - struct created_result_t { - T result{}; - bongocat_error_t error{bongocat_error_t::BONGOCAT_SUCCESS}; - - created_result_t() = default; - explicit(false) created_result_t(bongocat_error_t err) : error(err) {} - explicit(false) created_result_t(T&& res) : result(bongocat::move(res)), error(bongocat_error_t::BONGOCAT_SUCCESS) {} - }; - - namespace features { - // experimental - inline static constexpr bool BongocatIdleAnimation = false; - inline static constexpr bool BongocatBoringAnimation = false; - +template +struct created_result_t { + T result{}; + bongocat_error_t error{bongocat_error_t::BONGOCAT_SUCCESS}; + + created_result_t() = default; + explicit(false) created_result_t(bongocat_error_t err) : error(err) {} + explicit(false) created_result_t(T&& res) : result(bongocat::move(res)) {} +}; + +// feature flags +namespace features { + // experimental + inline static constexpr bool BongocatIdleAnimation = false; + inline static constexpr bool BongocatBoringAnimation = false; #ifndef NDEBUG - inline static constexpr bool Debug = true; + inline static constexpr bool Debug = true; #else - inline static constexpr bool Debug = false; + inline static constexpr bool Debug = false; #endif #ifdef FEATURE_BONGOCAT_EMBEDDED_ASSETS - inline static constexpr bool EnableBongocatEmbeddedAssets = true; + inline static constexpr bool EnableBongocatEmbeddedAssets = true; #else - inline static constexpr bool EnableBongocatEmbeddedAssets = false; + inline static constexpr bool EnableBongocatEmbeddedAssets = false; #endif #ifdef FEATURE_ENABLE_DM_EMBEDDED_ASSETS - inline static constexpr bool EnableDmEmbeddedAssets = true; -#ifdef FEATURE_DM_EMBEDDED_ASSETS - inline static constexpr bool EnableFullDmEmbeddedAssets = true; -#else - inline static constexpr bool EnableFullDmEmbeddedAssets = false; -#endif -#ifdef FEATURE_DM20_EMBEDDED_ASSETS - inline static constexpr bool EnableDm20EmbeddedAssets = true; -#else - inline static constexpr bool EnableDm20EmbeddedAssets = false; -#endif -#ifdef FEATURE_DMC_EMBEDDED_ASSETS - inline static constexpr bool EnableDmcEmbeddedAssets = true; -#else - inline static constexpr bool EnableDmcEmbeddedAssets = false; -#endif -#ifdef FEATURE_DMX_EMBEDDED_ASSETS - inline static constexpr bool EnableDmxEmbeddedAssets = true; -#else - inline static constexpr bool EnableDmxEmbeddedAssets = false; -#endif -#ifdef FEATURE_PEN_EMBEDDED_ASSETS - inline static constexpr bool EnablePenEmbeddedAssets = true; -#else - inline static constexpr bool EnablePenEmbeddedAssets = false; -#endif -#ifdef FEATURE_PEN20_EMBEDDED_ASSETS - inline static constexpr bool EnablePen20EmbeddedAssets = true; -#else - inline static constexpr bool EnablePen20EmbeddedAssets = false; -#endif -#ifdef FEATURE_DMALL_EMBEDDED_ASSETS - inline static constexpr bool EnableDmAllEmbeddedAssets = true; -#else - inline static constexpr bool EnableDmAllEmbeddedAssets = false; -#endif -#if !defined(FEATURE_DM_EMBEDDED_ASSETS) && !defined(FEATURE_DM20_EMBEDDED_ASSETS) && !defined(FEATURE_DMC_EMBEDDED_ASSETS) && !defined(FEATURE_DMX_EMBEDDED_ASSETS) && !defined(FEATURE_PEN20_EMBEDDED_ASSETS) && !defined(FEATURE_DMALL_EMBEDDED_ASSETS) - inline static constexpr bool EnableMinDmEmbeddedAssets = true; -#else - inline static constexpr bool EnableMinDmEmbeddedAssets = false; -#endif -#else - inline static constexpr bool EnableDmEmbeddedAssets = false; - inline static constexpr bool EnableFullDmEmbeddedAssets = false; - inline static constexpr bool EnableDm20EmbeddedAssets = false; - inline static constexpr bool EnableDmcEmbeddedAssets = false; - inline static constexpr bool EnableDmxEmbeddedAssets = false; - inline static constexpr bool EnablePenEmbeddedAssets = false; - inline static constexpr bool EnablePen20EmbeddedAssets = false; - inline static constexpr bool EnableMinDmEmbeddedAssets = false; - inline static constexpr bool EnableDmAllEmbeddedAssets = false; + inline static constexpr bool EnableDmEmbeddedAssets = true; +# ifdef FEATURE_DM_EMBEDDED_ASSETS + inline static constexpr bool EnableFullDmEmbeddedAssets = true; +# else + inline static constexpr bool EnableFullDmEmbeddedAssets = false; +# endif +# ifdef FEATURE_DM20_EMBEDDED_ASSETS + inline static constexpr bool EnableDm20EmbeddedAssets = true; +# else + inline static constexpr bool EnableDm20EmbeddedAssets = false; +# endif +# ifdef FEATURE_DMC_EMBEDDED_ASSETS + inline static constexpr bool EnableDmcEmbeddedAssets = true; +# else + inline static constexpr bool EnableDmcEmbeddedAssets = false; +# endif +# ifdef FEATURE_DMX_EMBEDDED_ASSETS + inline static constexpr bool EnableDmxEmbeddedAssets = true; +# else + inline static constexpr bool EnableDmxEmbeddedAssets = false; +# endif +# ifdef FEATURE_PEN_EMBEDDED_ASSETS + inline static constexpr bool EnablePenEmbeddedAssets = true; +# else + inline static constexpr bool EnablePenEmbeddedAssets = false; +# endif +# ifdef FEATURE_PEN20_EMBEDDED_ASSETS + inline static constexpr bool EnablePen20EmbeddedAssets = true; +# else + inline static constexpr bool EnablePen20EmbeddedAssets = false; +# endif +# ifdef FEATURE_DMALL_EMBEDDED_ASSETS + inline static constexpr bool EnableDmAllEmbeddedAssets = true; +# else + inline static constexpr bool EnableDmAllEmbeddedAssets = false; +# endif +# if !defined(FEATURE_DM_EMBEDDED_ASSETS) && !defined(FEATURE_DM20_EMBEDDED_ASSETS) && \ + !defined(FEATURE_DMC_EMBEDDED_ASSETS) && !defined(FEATURE_DMX_EMBEDDED_ASSETS) && \ + !defined(FEATURE_PEN20_EMBEDDED_ASSETS) && !defined(FEATURE_DMALL_EMBEDDED_ASSETS) + inline static constexpr bool EnableMinDmEmbeddedAssets = true; +# else + inline static constexpr bool EnableMinDmEmbeddedAssets = false; +# endif +#else + inline static constexpr bool EnableDmEmbeddedAssets = false; + inline static constexpr bool EnableFullDmEmbeddedAssets = false; + inline static constexpr bool EnableDm20EmbeddedAssets = false; + inline static constexpr bool EnableDmcEmbeddedAssets = false; + inline static constexpr bool EnableDmxEmbeddedAssets = false; + inline static constexpr bool EnablePenEmbeddedAssets = false; + inline static constexpr bool EnablePen20EmbeddedAssets = false; + inline static constexpr bool EnableMinDmEmbeddedAssets = false; + inline static constexpr bool EnableDmAllEmbeddedAssets = false; #endif #ifdef FEATURE_MS_AGENT_EMBEDDED_ASSETS - inline static constexpr bool EnableMsAgentEmbeddedAssets = true; + inline static constexpr bool EnableMsAgentEmbeddedAssets = true; #else - inline static constexpr bool EnableMsAgentEmbeddedAssets = false; + inline static constexpr bool EnableMsAgentEmbeddedAssets = false; #endif #ifdef FEATURE_PKMN_EMBEDDED_ASSETS - inline static constexpr bool EnablePkmnEmbeddedAssets = true; + inline static constexpr bool EnablePkmnEmbeddedAssets = true; #else - inline static constexpr bool EnablePkmnEmbeddedAssets = false; + inline static constexpr bool EnablePkmnEmbeddedAssets = false; #endif #ifdef FEATURE_PMD_EMBEDDED_ASSETS - inline static constexpr bool EnablePmdEmbeddedAssets = true; + inline static constexpr bool EnablePmdEmbeddedAssets = true; #else - inline static constexpr bool EnablePmdEmbeddedAssets = false; + inline static constexpr bool EnablePmdEmbeddedAssets = false; #endif #ifdef FEATURE_MISC_EMBEDDED_ASSETS - inline static constexpr bool EnableMiscEmbeddedAssets = true; + inline static constexpr bool EnableMiscEmbeddedAssets = true; #else - inline static constexpr bool EnableMiscEmbeddedAssets = false; + inline static constexpr bool EnableMiscEmbeddedAssets = false; #endif #if !defined(BONGOCAT_DISABLE_MEMORY_STATISTICS) || defined(BONGOCAT_ENABLE_MEMORY_STATISTICS) - inline static constexpr bool EnableMemoryStatistics = true; + inline static constexpr bool EnableMemoryStatistics = true; #else - inline static constexpr bool EnableMemoryStatistics = false; + inline static constexpr bool EnableMemoryStatistics = false; #endif #if !defined(BONGOCAT_DISABLE_LOGGER) || defined(BONGOCAT_ENABLE_LOGGER) - inline static constexpr bool EnableLogger = true; + inline static constexpr bool EnableLogger = true; #else - inline static constexpr bool EnableLogger = false; + inline static constexpr bool EnableLogger = false; #endif #ifdef FEATURE_PRELOAD_ASSETS - inline static constexpr bool EnablePreloadAssets = true; + inline static constexpr bool EnablePreloadAssets = true; #else - inline static constexpr bool EnablePreloadAssets = false; + inline static constexpr bool EnablePreloadAssets = false; #endif #ifdef FEATURE_LAZY_LOAD_ASSETS - inline static constexpr bool EnableLazyLoadAssets = true; + inline static constexpr bool EnableLazyLoadAssets = true; #else - inline static constexpr bool EnableLazyLoadAssets = false; + inline static constexpr bool EnableLazyLoadAssets = false; #endif #ifdef FEATURE_USE_HYBRID_IMAGE_BACKEND - inline static constexpr bool UseHybridImageBackend = true; + inline static constexpr bool UseHybridImageBackend = true; #else - inline static constexpr bool UseHybridImageBackend = false; + inline static constexpr bool UseHybridImageBackend = false; -#ifdef FEATURE_USE_PNGLE - inline static constexpr bool UsePngleImageBackend = true; -#else - inline static constexpr bool UsePngleImageBackend = false; -#endif +# ifdef FEATURE_USE_PNGLE + inline static constexpr bool UsePngleImageBackend = true; +# else + inline static constexpr bool UsePngleImageBackend = false; +# endif -#ifdef FEATURE_USE_STB_IMAGE - inline static constexpr bool UseStbImageBackend = true; -#else - inline static constexpr bool UseStbImageBackend = false; -#endif +# ifdef FEATURE_USE_STB_IMAGE + inline static constexpr bool UseStbImageBackend = true; +# else + inline static constexpr bool UseStbImageBackend = false; +# endif #endif #ifdef FEATURE_CUSTOM_SPRITE_SHEETS - inline static constexpr bool EnableCustomSpriteSheetsAssets = true; + inline static constexpr bool EnableCustomSpriteSheetsAssets = true; #else - inline static constexpr bool EnableCustomSpriteSheetsAssets = false; + inline static constexpr bool EnableCustomSpriteSheetsAssets = false; #endif - } - - // Global constants - namespace input { - inline static constexpr size_t MAX_INPUT_DEVICES = 256; - static_assert(MAX_INPUT_DEVICES <= INT32_MAX); - } - namespace platform { - inline static constexpr double ENABLED_MIN_CPU_PERCENT = 1.0; // in percent - inline static constexpr double TRIGGER_ANIMATION_CPU_DIFF_PERCENT = 1.0; // in percent - } - - - template - requires std::is_enum_v && - (std::is_same_v, uint32_t> || std::is_same_v, uint64_t>) - [[nodiscard]] inline constexpr Enum flag_or(Enum lhs, Enum rhs) noexcept { - return static_cast(static_cast>(lhs) | static_cast>(rhs)); - } - template - requires std::is_enum_v && - (std::is_same_v, uint32_t> || std::is_same_v, uint64_t>) - [[nodiscard]] inline constexpr Enum flag_and(Enum lhs, Enum rhs) noexcept { - return static_cast(static_cast>(lhs) & static_cast>(rhs)); - } - template - requires std::is_enum_v && - (std::is_same_v, uint32_t> || std::is_same_v, uint64_t>) - [[nodiscard]] inline constexpr Enum flag_xor(Enum lhs, Enum rhs) noexcept { - return static_cast(static_cast>(lhs) ^ static_cast>(rhs)); - } - template - requires std::is_enum_v && - (std::is_same_v, uint32_t> || std::is_same_v, uint64_t>) - [[nodiscard]] inline constexpr Enum flag_not(Enum rhs) noexcept { - return static_cast(~static_cast>(rhs)); - } - template - requires std::is_enum_v && - (std::is_same_v, uint32_t> || std::is_same_v, uint64_t>) - [[nodiscard]] inline constexpr Enum flag_add(Enum lhs, Enum rhs) noexcept { - lhs = flag_or(lhs, rhs); - return lhs; - } - template - requires std::is_enum_v && - (std::is_same_v, uint32_t> || std::is_same_v, uint64_t>) - [[nodiscard]] inline constexpr Enum flag_remove(Enum lhs, Enum rhs) noexcept { - return static_cast(static_cast(lhs) & ~static_cast(rhs)); - } - template - requires std::is_enum_v && - (std::is_same_v, uint32_t> || std::is_same_v, uint64_t>) - [[nodiscard]] inline constexpr Enum flag_assign(Enum lhs, Enum rhs) noexcept { - lhs = flag_and(lhs, rhs); - return lhs; - } - template - requires std::is_enum_v && - (std::is_same_v, uint32_t> || std::is_same_v, uint64_t>) - [[nodiscard]] inline constexpr bool has_flag(Enum value, Enum flag) noexcept { - return (static_cast(value) & static_cast(flag)) != 0; - } +} // namespace features + +// Global constants +namespace input { + inline static constexpr size_t MAX_INPUT_DEVICES = 256; + static_assert(MAX_INPUT_DEVICES <= INT32_MAX); +} // namespace input +namespace platform { + inline static constexpr double ENABLED_MIN_CPU_PERCENT = 1.0; // in percent + inline static constexpr double TRIGGER_ANIMATION_CPU_DIFF_PERCENT = 1.0; // in percent +} // namespace platform + +template + requires std::is_enum_v && (std::is_same_v, uint32_t> || + std::is_same_v, uint64_t>) +BONGOCAT_NODISCARD constexpr Enum flag_or(Enum lhs, Enum rhs) noexcept { + return static_cast(static_cast>(lhs) | + static_cast>(rhs)); +} +template + requires std::is_enum_v && (std::is_same_v, uint32_t> || + std::is_same_v, uint64_t>) +BONGOCAT_NODISCARD constexpr Enum flag_and(Enum lhs, Enum rhs) noexcept { + return static_cast(static_cast>(lhs) & + static_cast>(rhs)); +} +template + requires std::is_enum_v && (std::is_same_v, uint32_t> || + std::is_same_v, uint64_t>) +BONGOCAT_NODISCARD constexpr Enum flag_xor(Enum lhs, Enum rhs) noexcept { + return static_cast(static_cast>(lhs) ^ + static_cast>(rhs)); +} +template + requires std::is_enum_v && (std::is_same_v, uint32_t> || + std::is_same_v, uint64_t>) +BONGOCAT_NODISCARD constexpr Enum flag_not(Enum rhs) noexcept { + return static_cast(~static_cast>(rhs)); +} +template + requires std::is_enum_v && (std::is_same_v, uint32_t> || + std::is_same_v, uint64_t>) +BONGOCAT_NODISCARD constexpr Enum flag_add(Enum lhs, Enum rhs) noexcept { + lhs = flag_or(lhs, rhs); + return lhs; +} +template + requires std::is_enum_v && (std::is_same_v, uint32_t> || + std::is_same_v, uint64_t>) +BONGOCAT_NODISCARD constexpr Enum flag_remove(Enum lhs, Enum rhs) noexcept { + return static_cast(static_cast(lhs) & ~static_cast(rhs)); +} +template + requires std::is_enum_v && (std::is_same_v, uint32_t> || + std::is_same_v, uint64_t>) +BONGOCAT_NODISCARD constexpr Enum flag_assign(Enum lhs, Enum rhs) noexcept { + lhs = flag_and(lhs, rhs); + return lhs; +} +template + requires std::is_enum_v && (std::is_same_v, uint32_t> || + std::is_same_v, uint64_t>) +BONGOCAT_NODISCARD constexpr bool has_flag(Enum value, Enum flag) noexcept { + return (static_cast(value) & static_cast(flag)) != 0; } +} // namespace bongocat -#endif // BONGOCAT_BONGOCAT_H \ No newline at end of file +#endif // BONGOCAT_BONGOCAT_H \ No newline at end of file diff --git a/include/embedded_assets/.clang-format b/include/embedded_assets/.clang-format new file mode 100644 index 00000000..15f9b6fc --- /dev/null +++ b/include/embedded_assets/.clang-format @@ -0,0 +1,2 @@ +DisableFormat: true +SortIncludes: Never \ No newline at end of file diff --git a/include/embedded_assets/bongocat/bongocat.h b/include/embedded_assets/bongocat/bongocat.h index 3a02c382..7258d1dd 100644 --- a/include/embedded_assets/bongocat/bongocat.h +++ b/include/embedded_assets/bongocat/bongocat.h @@ -1,33 +1,37 @@ #ifndef BONGOCAT_EMBEDDED_ASSETS_BONGOCAT_H #define BONGOCAT_EMBEDDED_ASSETS_BONGOCAT_H -#include "graphics/sprite_sheet.h" #include "core/bongocat.h" #include "embedded_assets/embedded_image.h" +#include "graphics/sprite_sheet.h" + #include -namespace bongocat::animation { struct animation_context_t; } +namespace bongocat::animation { +struct animation_thread_context_t; +} namespace bongocat::assets { - // Bongocat Frames - inline static constexpr int BONGOCAT_FRAME_BOTH_UP = 0; - inline static constexpr int BONGOCAT_FRAME_LEFT_DOWN = 1; - inline static constexpr int BONGOCAT_FRAME_RIGHT_DOWN = 2; - inline static constexpr int BONGOCAT_FRAME_BOTH_DOWN = 3; +// Bongocat Frames +inline static constexpr int BONGOCAT_FRAME_BOTH_UP = 0; +inline static constexpr int BONGOCAT_FRAME_LEFT_DOWN = 1; +inline static constexpr int BONGOCAT_FRAME_RIGHT_DOWN = 2; +inline static constexpr int BONGOCAT_FRAME_BOTH_DOWN = 3; - inline static constexpr size_t BONGOCAT_SPRITE_SHEET_COLS = 4; - inline static constexpr size_t BONGOCAT_SPRITE_SHEET_ROWS = 1; - inline static constexpr size_t BONGOCAT_SPRITE_SHEET_ROW = 0; +inline static constexpr size_t BONGOCAT_SPRITE_SHEET_COLS = 4; +inline static constexpr size_t BONGOCAT_SPRITE_SHEET_ROWS = 1; +inline static constexpr size_t BONGOCAT_SPRITE_SHEET_ROW = 0; - // apparently - inline static constexpr int BONGOCAT_FRAME_WIDTH = 864; - inline static constexpr int BONGOCAT_FRAME_HEIGHT = 360; +// apparently +inline static constexpr int BONGOCAT_FRAME_WIDTH = 864; +inline static constexpr int BONGOCAT_FRAME_HEIGHT = 360; - inline static constexpr size_t BONGOCAT_EMBEDDED_IMAGES_COUNT = animation::BONGOCAT_NUM_FRAMES; - inline static constexpr size_t BONGOCAT_ANIMATIONS_COUNT = 1; +inline static constexpr size_t BONGOCAT_EMBEDDED_IMAGES_COUNT = animation::BONGOCAT_NUM_FRAMES; +inline static constexpr size_t BONGOCAT_ANIMATIONS_COUNT = 1; - [[nodiscard]] extern embedded_image_t get_bongocat_sprite(size_t i); - [[nodiscard]] extern created_result_t get_bongocat_sprite_sheet(const animation::animation_context_t& ctx, int index); -} +BONGOCAT_NODISCARD extern embedded_image_t get_bongocat_sprite(size_t i); +BONGOCAT_NODISCARD extern created_result_t +get_bongocat_sprite_sheet(const animation::animation_thread_context_t& ctx, int index); +} // namespace bongocat::assets -#endif // BONGOCAT_EMBEDDED_ASSETS_BONGOCAT_H +#endif // BONGOCAT_EMBEDDED_ASSETS_BONGOCAT_H diff --git a/include/embedded_assets/bongocat/bongocat.hpp b/include/embedded_assets/bongocat/bongocat.hpp index a4ec3986..9d66181a 100644 --- a/include/embedded_assets/bongocat/bongocat.hpp +++ b/include/embedded_assets/bongocat/bongocat.hpp @@ -4,22 +4,22 @@ #include namespace bongocat::assets { - // Name: Bongo Cat - inline static constexpr char BONGOCAT_FQID_ARR[] = "bongocat"; - inline static constexpr const char* BONGOCAT_FQID = BONGOCAT_FQID_ARR; - inline static constexpr std::size_t BONGOCAT_FQID_LEN = sizeof(BONGOCAT_FQID_ARR)-1; - inline static constexpr char BONGOCAT_ID_ARR[] = "bongocat"; - inline static constexpr const char* BONGOCAT_ID = BONGOCAT_ID_ARR; - inline static constexpr std::size_t BONGOCAT_ID_LEN = sizeof(BONGOCAT_ID_ARR)-1; - inline static constexpr char BONGOCAT_NAME_ARR[] = "bongocat"; - inline static constexpr const char* BONGOCAT_NAME = BONGOCAT_NAME_ARR; - inline static constexpr std::size_t BONGOCAT_NAME_LEN = sizeof(BONGOCAT_NAME_ARR)-1; - inline static constexpr char BONGOCAT_FQNAME_ARR[] = "bongocat"; - inline static constexpr const char* BONGOCAT_FQNAME = BONGOCAT_FQNAME_ARR; - inline static constexpr std::size_t BONGOCAT_FQNAME_LEN = sizeof(BONGOCAT_FQNAME_ARR)-1; - inline static constexpr std::size_t BONGOCAT_ANIM_INDEX = 0; +// Name: Bongo Cat +inline static constexpr char BONGOCAT_FQID_ARR[] = "bongocat"; +inline static constexpr const char *BONGOCAT_FQID = BONGOCAT_FQID_ARR; +inline static constexpr std::size_t BONGOCAT_FQID_LEN = sizeof(BONGOCAT_FQID_ARR) - 1; +inline static constexpr char BONGOCAT_ID_ARR[] = "bongocat"; +inline static constexpr const char *BONGOCAT_ID = BONGOCAT_ID_ARR; +inline static constexpr std::size_t BONGOCAT_ID_LEN = sizeof(BONGOCAT_ID_ARR) - 1; +inline static constexpr char BONGOCAT_NAME_ARR[] = "bongocat"; +inline static constexpr const char *BONGOCAT_NAME = BONGOCAT_NAME_ARR; +inline static constexpr std::size_t BONGOCAT_NAME_LEN = sizeof(BONGOCAT_NAME_ARR) - 1; +inline static constexpr char BONGOCAT_FQNAME_ARR[] = "bongocat"; +inline static constexpr const char *BONGOCAT_FQNAME = BONGOCAT_FQNAME_ARR; +inline static constexpr std::size_t BONGOCAT_FQNAME_LEN = sizeof(BONGOCAT_FQNAME_ARR) - 1; +inline static constexpr std::size_t BONGOCAT_ANIM_INDEX = 0; - inline static constexpr std::size_t BONGOCAT_ANIM_COUNT = 1; -} +inline static constexpr std::size_t BONGOCAT_ANIM_COUNT = 1; +} // namespace bongocat::assets -#endif // BONGOCAT_EMBEDDED_ASSETS_BONGOCAT_HPP +#endif // BONGOCAT_EMBEDDED_ASSETS_BONGOCAT_HPP diff --git a/include/embedded_assets/bongocat/bongocat_images.h b/include/embedded_assets/bongocat/bongocat_images.h index 646a7859..e8a459d0 100644 --- a/include/embedded_assets/bongocat/bongocat_images.h +++ b/include/embedded_assets/bongocat/bongocat_images.h @@ -4,7 +4,7 @@ #include // Embedded asset data -extern const unsigned char bongo_cat_both_up_png[]; +extern const unsigned char bongo_cat_both_up_png[]; extern const size_t bongo_cat_both_up_png_size; extern const unsigned char bongo_cat_left_down_png[]; @@ -16,4 +16,4 @@ extern const size_t bongo_cat_right_down_png_size; extern const unsigned char bongo_cat_both_down_png[]; extern const size_t bongo_cat_both_down_png_size; -#endif // BONGOCAT_EMBEDDED_ASSETS_BONGOCAT_IMAGES_H +#endif // BONGOCAT_EMBEDDED_ASSETS_BONGOCAT_IMAGES_H diff --git a/include/embedded_assets/custom/custom_sprite.h b/include/embedded_assets/custom/custom_sprite.h index 8a3fa7e9..b9669ae3 100644 --- a/include/embedded_assets/custom/custom_sprite.h +++ b/include/embedded_assets/custom/custom_sprite.h @@ -5,179 +5,212 @@ #include namespace bongocat::assets { - inline static constexpr size_t CUSTOM_SPRITE_SHEET_ROW_IDLE = 0; - inline static constexpr size_t CUSTOM_SPRITE_SHEET_ROW_BORING = 1; - inline static constexpr size_t CUSTOM_SPRITE_SHEET_ROW_START_WRITING = 2; - inline static constexpr size_t CUSTOM_SPRITE_SHEET_ROW_WRITING = 3; - inline static constexpr size_t CUSTOM_SPRITE_SHEET_ROW_END_WRITING = 4; - inline static constexpr size_t CUSTOM_SPRITE_SHEET_ROW_HAPPY = 5; - inline static constexpr size_t CUSTOM_SPRITE_SHEET_ROW_ASLEEP = 6; - inline static constexpr size_t CUSTOM_SPRITE_SHEET_ROW_SLEEP = 7; - inline static constexpr size_t CUSTOM_SPRITE_SHEET_ROW_WAKE_UP = 8; - inline static constexpr size_t CUSTOM_SPRITE_SHEET_ROW_START_WORKING = 9; - inline static constexpr size_t CUSTOM_SPRITE_SHEET_ROW_WORKING = 10; - inline static constexpr size_t CUSTOM_SPRITE_SHEET_ROW_END_WORKING = 11; - inline static constexpr size_t CUSTOM_SPRITE_SHEET_ROW_START_MOVING = 12; - inline static constexpr size_t CUSTOM_SPRITE_SHEET_ROW_MOVING = 13; - inline static constexpr size_t CUSTOM_SPRITE_SHEET_ROW_END_MOVING = 14; - inline static constexpr size_t CUSTOM_SPRITE_SHEET_ROW_START_RUNNING = 15; - inline static constexpr size_t CUSTOM_SPRITE_SHEET_ROW_RUNNING = 16; - inline static constexpr size_t CUSTOM_SPRITE_SHEET_ROW_END_RUNNING = 17; - enum class CustomAnimations : uint8_t { - Idle = CUSTOM_SPRITE_SHEET_ROW_IDLE, - Boring = CUSTOM_SPRITE_SHEET_ROW_BORING, - StartWriting = CUSTOM_SPRITE_SHEET_ROW_START_WRITING, - Writing = CUSTOM_SPRITE_SHEET_ROW_WRITING, - EndWriting = CUSTOM_SPRITE_SHEET_ROW_END_WRITING, - Happy = CUSTOM_SPRITE_SHEET_ROW_HAPPY, - ASleep = CUSTOM_SPRITE_SHEET_ROW_ASLEEP, - Sleep = CUSTOM_SPRITE_SHEET_ROW_SLEEP, - WakeUp = CUSTOM_SPRITE_SHEET_ROW_WAKE_UP, - StartWorking = CUSTOM_SPRITE_SHEET_ROW_START_WORKING, - Working = CUSTOM_SPRITE_SHEET_ROW_WORKING, - EndWorking = CUSTOM_SPRITE_SHEET_ROW_END_WORKING, - StartMoving = CUSTOM_SPRITE_SHEET_ROW_START_MOVING, - Moving = CUSTOM_SPRITE_SHEET_ROW_MOVING, - EndMoving = CUSTOM_SPRITE_SHEET_ROW_END_MOVING, - StartRunning = CUSTOM_SPRITE_SHEET_ROW_START_RUNNING, - Running = CUSTOM_SPRITE_SHEET_ROW_RUNNING, - EndRunning = CUSTOM_SPRITE_SHEET_ROW_END_RUNNING, - }; - inline static constexpr size_t CUSTOM_SPRITE_SHEET_MAX_ROWS = 18; - - // custom (sprite sheet) - inline static constexpr char CUSTOM_ID_ARR[] = "custom"; - inline static constexpr const char* CUSTOM_ID = CUSTOM_ID_ARR; - inline static constexpr std::size_t CUSTOM_ID_LEN = sizeof(CUSTOM_ID_ARR)-1; - inline static constexpr char CUSTOM_NAME_ARR[] = "custom"; - inline static constexpr const char* CUSTOM_NAME = CUSTOM_NAME_ARR; - inline static constexpr std::size_t CUSTOM_NAME_LEN = sizeof(CUSTOM_NAME_ARR)-1; - - - static inline constexpr int CUSTOM_HAPPY_CHANCE_PERCENT = 60; - - - struct custom_animation_settings_t { - int32_t idle_frames{0}; - - int32_t boring_frames{0}; - - int32_t start_writing_frames{0}; - int32_t writing_frames{0}; - int32_t end_writing_frames{0}; - int32_t happy_frames{0}; - - int32_t asleep_frames{0}; - int32_t sleep_frames{0}; - int32_t wake_up_frames{0}; - - int32_t start_working_frames{0}; - int32_t working_frames{0}; - int32_t end_working_frames{0}; - - int32_t start_moving_frames{0}; - int32_t moving_frames{0}; - int32_t end_moving_frames{0}; - - int32_t start_running_frames{0}; - int32_t running_frames{0}; - int32_t end_running_frames{0}; - - int32_t feature_toggle_writing_frames{-1}; - int32_t feature_toggle_writing_frames_random{-1}; - int32_t feature_mirror_x_moving{-1}; - - // row lines (optional) - int32_t idle_row_index{-1}; - - int32_t boring_row_index{-1}; - - int32_t start_writing_row_index{-1}; - int32_t writing_row_index{-1}; - int32_t end_writing_row_index{-1}; - int32_t happy_row_index{-1}; - - int32_t asleep_row_index{-1}; - int32_t sleep_row_index{-1}; - int32_t wake_up_row_index{-1}; - - int32_t start_working_row_index{-1}; - int32_t working_row_index{-1}; - int32_t end_working_row_index{-1}; - - int32_t start_moving_row_index{-1}; - int32_t moving_row_index{-1}; - int32_t end_moving_row_index{-1}; - - int32_t start_running_row_index{-1}; - int32_t running_row_index{-1}; - int32_t end_running_row_index{-1}; - - int32_t rows{-1}; - }; - - inline int get_custom_animation_settings_rows_count(const custom_animation_settings_t& sprite_sheet_settings) { - if (sprite_sheet_settings.rows >= 1) { - return sprite_sheet_settings.rows; - } - - int sprite_sheet_rows{0}; - - // detect sprite sheet rows - if (sprite_sheet_settings.idle_frames > 0) sprite_sheet_rows++; - if (sprite_sheet_settings.boring_frames > 0) sprite_sheet_rows++; - - if (sprite_sheet_settings.start_writing_frames > 0) sprite_sheet_rows++; - if (sprite_sheet_settings.writing_frames > 0) sprite_sheet_rows++; - if (sprite_sheet_settings.end_writing_frames > 0) sprite_sheet_rows++; - if (sprite_sheet_settings.happy_frames > 0) sprite_sheet_rows++; - - if (sprite_sheet_settings.asleep_frames > 0) sprite_sheet_rows++; - if (sprite_sheet_settings.sleep_frames > 0) sprite_sheet_rows++; - if (sprite_sheet_settings.wake_up_frames > 0) sprite_sheet_rows++; - - if (sprite_sheet_settings.start_working_frames > 0) sprite_sheet_rows++; - if (sprite_sheet_settings.working_frames > 0) sprite_sheet_rows++; - if (sprite_sheet_settings.end_working_frames > 0) sprite_sheet_rows++; - - if (sprite_sheet_settings.start_moving_frames > 0) sprite_sheet_rows++; - if (sprite_sheet_settings.moving_frames > 0) sprite_sheet_rows++; - if (sprite_sheet_settings.end_moving_frames > 0) sprite_sheet_rows++; - - if (sprite_sheet_settings.start_running_frames > 0) sprite_sheet_rows++; - if (sprite_sheet_settings.running_frames > 0) sprite_sheet_rows++; - if (sprite_sheet_settings.end_running_frames > 0) sprite_sheet_rows++; - - return sprite_sheet_rows; - } - inline int get_custom_animation_settings_max_cols(const custom_animation_settings_t& sprite_sheet_settings) { - int sprite_sheet_cols = sprite_sheet_settings.idle_frames; - - if (sprite_sheet_settings.boring_frames >= sprite_sheet_cols) sprite_sheet_cols = sprite_sheet_settings.boring_frames; - - if (sprite_sheet_settings.start_writing_frames >= sprite_sheet_cols) sprite_sheet_cols = sprite_sheet_settings.start_writing_frames; - if (sprite_sheet_settings.writing_frames >= sprite_sheet_cols) sprite_sheet_cols = sprite_sheet_settings.writing_frames; - if (sprite_sheet_settings.end_writing_frames >= sprite_sheet_cols) sprite_sheet_cols = sprite_sheet_settings.end_writing_frames; - if (sprite_sheet_settings.happy_frames >= sprite_sheet_cols) sprite_sheet_cols = sprite_sheet_settings.happy_frames; - - if (sprite_sheet_settings.asleep_frames >= sprite_sheet_cols) sprite_sheet_cols = sprite_sheet_settings.asleep_frames; - if (sprite_sheet_settings.sleep_frames >= sprite_sheet_cols) sprite_sheet_cols = sprite_sheet_settings.sleep_frames; - if (sprite_sheet_settings.wake_up_frames >= sprite_sheet_cols) sprite_sheet_cols = sprite_sheet_settings.wake_up_frames; - - if (sprite_sheet_settings.start_working_frames >= sprite_sheet_cols) sprite_sheet_cols = sprite_sheet_settings.start_working_frames; - if (sprite_sheet_settings.working_frames >= sprite_sheet_cols) sprite_sheet_cols = sprite_sheet_settings.working_frames; - if (sprite_sheet_settings.end_working_frames >= sprite_sheet_cols) sprite_sheet_cols = sprite_sheet_settings.end_working_frames; - - if (sprite_sheet_settings.start_moving_frames >= sprite_sheet_cols) sprite_sheet_cols = sprite_sheet_settings.start_moving_frames; - if (sprite_sheet_settings.moving_frames >= sprite_sheet_cols) sprite_sheet_cols = sprite_sheet_settings.moving_frames; - if (sprite_sheet_settings.end_moving_frames >= sprite_sheet_cols) sprite_sheet_cols = sprite_sheet_settings.end_moving_frames; - - if (sprite_sheet_settings.start_running_frames >= sprite_sheet_cols) sprite_sheet_cols = sprite_sheet_settings.start_running_frames; - if (sprite_sheet_settings.running_frames >= sprite_sheet_cols) sprite_sheet_cols = sprite_sheet_settings.running_frames; - if (sprite_sheet_settings.end_running_frames >= sprite_sheet_cols) sprite_sheet_cols = sprite_sheet_settings.end_running_frames; - - return sprite_sheet_cols; - } +inline static constexpr size_t CUSTOM_SPRITE_SHEET_ROW_IDLE = 0; +inline static constexpr size_t CUSTOM_SPRITE_SHEET_ROW_BORING = 1; +inline static constexpr size_t CUSTOM_SPRITE_SHEET_ROW_START_WRITING = 2; +inline static constexpr size_t CUSTOM_SPRITE_SHEET_ROW_WRITING = 3; +inline static constexpr size_t CUSTOM_SPRITE_SHEET_ROW_END_WRITING = 4; +inline static constexpr size_t CUSTOM_SPRITE_SHEET_ROW_HAPPY = 5; +inline static constexpr size_t CUSTOM_SPRITE_SHEET_ROW_ASLEEP = 6; +inline static constexpr size_t CUSTOM_SPRITE_SHEET_ROW_SLEEP = 7; +inline static constexpr size_t CUSTOM_SPRITE_SHEET_ROW_WAKE_UP = 8; +inline static constexpr size_t CUSTOM_SPRITE_SHEET_ROW_START_WORKING = 9; +inline static constexpr size_t CUSTOM_SPRITE_SHEET_ROW_WORKING = 10; +inline static constexpr size_t CUSTOM_SPRITE_SHEET_ROW_END_WORKING = 11; +inline static constexpr size_t CUSTOM_SPRITE_SHEET_ROW_START_MOVING = 12; +inline static constexpr size_t CUSTOM_SPRITE_SHEET_ROW_MOVING = 13; +inline static constexpr size_t CUSTOM_SPRITE_SHEET_ROW_END_MOVING = 14; +inline static constexpr size_t CUSTOM_SPRITE_SHEET_ROW_START_RUNNING = 15; +inline static constexpr size_t CUSTOM_SPRITE_SHEET_ROW_RUNNING = 16; +inline static constexpr size_t CUSTOM_SPRITE_SHEET_ROW_END_RUNNING = 17; +enum class CustomAnimations : uint8_t { + Idle = CUSTOM_SPRITE_SHEET_ROW_IDLE, + Boring = CUSTOM_SPRITE_SHEET_ROW_BORING, + StartWriting = CUSTOM_SPRITE_SHEET_ROW_START_WRITING, + Writing = CUSTOM_SPRITE_SHEET_ROW_WRITING, + EndWriting = CUSTOM_SPRITE_SHEET_ROW_END_WRITING, + Happy = CUSTOM_SPRITE_SHEET_ROW_HAPPY, + ASleep = CUSTOM_SPRITE_SHEET_ROW_ASLEEP, + Sleep = CUSTOM_SPRITE_SHEET_ROW_SLEEP, + WakeUp = CUSTOM_SPRITE_SHEET_ROW_WAKE_UP, + StartWorking = CUSTOM_SPRITE_SHEET_ROW_START_WORKING, + Working = CUSTOM_SPRITE_SHEET_ROW_WORKING, + EndWorking = CUSTOM_SPRITE_SHEET_ROW_END_WORKING, + StartMoving = CUSTOM_SPRITE_SHEET_ROW_START_MOVING, + Moving = CUSTOM_SPRITE_SHEET_ROW_MOVING, + EndMoving = CUSTOM_SPRITE_SHEET_ROW_END_MOVING, + StartRunning = CUSTOM_SPRITE_SHEET_ROW_START_RUNNING, + Running = CUSTOM_SPRITE_SHEET_ROW_RUNNING, + EndRunning = CUSTOM_SPRITE_SHEET_ROW_END_RUNNING, +}; +inline static constexpr size_t CUSTOM_SPRITE_SHEET_MAX_ROWS = 18; + +// custom (sprite sheet) +inline static constexpr char CUSTOM_ID_ARR[] = "custom"; +inline static constexpr const char *CUSTOM_ID = CUSTOM_ID_ARR; +inline static constexpr std::size_t CUSTOM_ID_LEN = sizeof(CUSTOM_ID_ARR) - 1; +inline static constexpr char CUSTOM_NAME_ARR[] = "custom"; +inline static constexpr const char *CUSTOM_NAME = CUSTOM_NAME_ARR; +inline static constexpr std::size_t CUSTOM_NAME_LEN = sizeof(CUSTOM_NAME_ARR) - 1; + +static inline constexpr int CUSTOM_HAPPY_CHANCE_PERCENT = 60; + +struct custom_animation_settings_t { + int32_t idle_frames{0}; + + int32_t boring_frames{0}; + + int32_t start_writing_frames{0}; + int32_t writing_frames{0}; + int32_t end_writing_frames{0}; + int32_t happy_frames{0}; + + int32_t asleep_frames{0}; + int32_t sleep_frames{0}; + int32_t wake_up_frames{0}; + + int32_t start_working_frames{0}; + int32_t working_frames{0}; + int32_t end_working_frames{0}; + + int32_t start_moving_frames{0}; + int32_t moving_frames{0}; + int32_t end_moving_frames{0}; + + int32_t start_running_frames{0}; + int32_t running_frames{0}; + int32_t end_running_frames{0}; + + int32_t feature_toggle_writing_frames{-1}; + int32_t feature_toggle_writing_frames_random{-1}; + int32_t feature_mirror_x_moving{-1}; + + // row lines (optional) + int32_t idle_row_index{-1}; + + int32_t boring_row_index{-1}; + + int32_t start_writing_row_index{-1}; + int32_t writing_row_index{-1}; + int32_t end_writing_row_index{-1}; + int32_t happy_row_index{-1}; + + int32_t asleep_row_index{-1}; + int32_t sleep_row_index{-1}; + int32_t wake_up_row_index{-1}; + + int32_t start_working_row_index{-1}; + int32_t working_row_index{-1}; + int32_t end_working_row_index{-1}; + + int32_t start_moving_row_index{-1}; + int32_t moving_row_index{-1}; + int32_t end_moving_row_index{-1}; + + int32_t start_running_row_index{-1}; + int32_t running_row_index{-1}; + int32_t end_running_row_index{-1}; + + int32_t rows{-1}; +}; + +inline int get_custom_animation_settings_rows_count(const custom_animation_settings_t& sprite_sheet_settings) { + if (sprite_sheet_settings.rows >= 1) { + return sprite_sheet_settings.rows; + } + + int sprite_sheet_rows{0}; + + // detect sprite sheet rows + if (sprite_sheet_settings.idle_frames > 0) + sprite_sheet_rows++; + if (sprite_sheet_settings.boring_frames > 0) + sprite_sheet_rows++; + + if (sprite_sheet_settings.start_writing_frames > 0) + sprite_sheet_rows++; + if (sprite_sheet_settings.writing_frames > 0) + sprite_sheet_rows++; + if (sprite_sheet_settings.end_writing_frames > 0) + sprite_sheet_rows++; + if (sprite_sheet_settings.happy_frames > 0) + sprite_sheet_rows++; + + if (sprite_sheet_settings.asleep_frames > 0) + sprite_sheet_rows++; + if (sprite_sheet_settings.sleep_frames > 0) + sprite_sheet_rows++; + if (sprite_sheet_settings.wake_up_frames > 0) + sprite_sheet_rows++; + + if (sprite_sheet_settings.start_working_frames > 0) + sprite_sheet_rows++; + if (sprite_sheet_settings.working_frames > 0) + sprite_sheet_rows++; + if (sprite_sheet_settings.end_working_frames > 0) + sprite_sheet_rows++; + + if (sprite_sheet_settings.start_moving_frames > 0) + sprite_sheet_rows++; + if (sprite_sheet_settings.moving_frames > 0) + sprite_sheet_rows++; + if (sprite_sheet_settings.end_moving_frames > 0) + sprite_sheet_rows++; + + if (sprite_sheet_settings.start_running_frames > 0) + sprite_sheet_rows++; + if (sprite_sheet_settings.running_frames > 0) + sprite_sheet_rows++; + if (sprite_sheet_settings.end_running_frames > 0) + sprite_sheet_rows++; + + return sprite_sheet_rows; +} +inline int get_custom_animation_settings_max_cols(const custom_animation_settings_t& sprite_sheet_settings) { + int sprite_sheet_cols = sprite_sheet_settings.idle_frames; + + if (sprite_sheet_settings.boring_frames >= sprite_sheet_cols) + sprite_sheet_cols = sprite_sheet_settings.boring_frames; + + if (sprite_sheet_settings.start_writing_frames >= sprite_sheet_cols) + sprite_sheet_cols = sprite_sheet_settings.start_writing_frames; + if (sprite_sheet_settings.writing_frames >= sprite_sheet_cols) + sprite_sheet_cols = sprite_sheet_settings.writing_frames; + if (sprite_sheet_settings.end_writing_frames >= sprite_sheet_cols) + sprite_sheet_cols = sprite_sheet_settings.end_writing_frames; + if (sprite_sheet_settings.happy_frames >= sprite_sheet_cols) + sprite_sheet_cols = sprite_sheet_settings.happy_frames; + + if (sprite_sheet_settings.asleep_frames >= sprite_sheet_cols) + sprite_sheet_cols = sprite_sheet_settings.asleep_frames; + if (sprite_sheet_settings.sleep_frames >= sprite_sheet_cols) + sprite_sheet_cols = sprite_sheet_settings.sleep_frames; + if (sprite_sheet_settings.wake_up_frames >= sprite_sheet_cols) + sprite_sheet_cols = sprite_sheet_settings.wake_up_frames; + + if (sprite_sheet_settings.start_working_frames >= sprite_sheet_cols) + sprite_sheet_cols = sprite_sheet_settings.start_working_frames; + if (sprite_sheet_settings.working_frames >= sprite_sheet_cols) + sprite_sheet_cols = sprite_sheet_settings.working_frames; + if (sprite_sheet_settings.end_working_frames >= sprite_sheet_cols) + sprite_sheet_cols = sprite_sheet_settings.end_working_frames; + + if (sprite_sheet_settings.start_moving_frames >= sprite_sheet_cols) + sprite_sheet_cols = sprite_sheet_settings.start_moving_frames; + if (sprite_sheet_settings.moving_frames >= sprite_sheet_cols) + sprite_sheet_cols = sprite_sheet_settings.moving_frames; + if (sprite_sheet_settings.end_moving_frames >= sprite_sheet_cols) + sprite_sheet_cols = sprite_sheet_settings.end_moving_frames; + + if (sprite_sheet_settings.start_running_frames >= sprite_sheet_cols) + sprite_sheet_cols = sprite_sheet_settings.start_running_frames; + if (sprite_sheet_settings.running_frames >= sprite_sheet_cols) + sprite_sheet_cols = sprite_sheet_settings.running_frames; + if (sprite_sheet_settings.end_running_frames >= sprite_sheet_cols) + sprite_sheet_cols = sprite_sheet_settings.end_running_frames; + + return sprite_sheet_cols; } +} // namespace bongocat::assets -#endif // BONGOCAT_EMBEDDED_ASSETS_CUSTOM_SPRITE_H +#endif // BONGOCAT_EMBEDDED_ASSETS_CUSTOM_SPRITE_H diff --git a/include/embedded_assets/embedded_image.h b/include/embedded_assets/embedded_image.h index ad421fb7..c5cc019e 100644 --- a/include/embedded_assets/embedded_image.h +++ b/include/embedded_assets/embedded_image.h @@ -2,33 +2,34 @@ #define BONGOCAT_EMBEDDED_ASSETS_IMAGE_H #include "config/config.h" + #include namespace bongocat::assets { - struct embedded_image_t { - const unsigned char *data{nullptr}; - size_t size{0}; - const char *name{""}; - }; +struct embedded_image_t { + const unsigned char *data{BONGOCAT_NULLPTR}; + size_t size{0}; + const char *name{""}; +}; - struct config_animation_entry_t { - const char* name{""}; - const char* id{""}; - const char* fqid{""}; - const char* fqname{""}; - int anim_index{0}; - config::config_animation_dm_set_t set{config::config_animation_dm_set_t::None}; - config::config_animation_sprite_sheet_layout_t layout{config::config_animation_sprite_sheet_layout_t::None}; - }; - struct config_custom_animation_entry_t { - const char* name{""}; - const char* id{""}; - const char* fqid{""}; - const char* fqname{""}; - int anim_index{0}; - config::config_animation_custom_set_t set{config::config_animation_custom_set_t::None}; - config::config_animation_sprite_sheet_layout_t layout{config::config_animation_sprite_sheet_layout_t::None}; - }; -} +struct config_animation_entry_t { + const char *name{""}; + const char *id{""}; + const char *fqid{""}; + const char *fqname{""}; + int anim_index{0}; + config::config_animation_dm_set_t set{config::config_animation_dm_set_t::None}; + config::config_animation_sprite_sheet_layout_t layout{config::config_animation_sprite_sheet_layout_t::None}; +}; +struct config_custom_animation_entry_t { + const char *name{""}; + const char *id{""}; + const char *fqid{""}; + const char *fqname{""}; + int anim_index{0}; + config::config_animation_custom_set_t set{config::config_animation_custom_set_t::None}; + config::config_animation_sprite_sheet_layout_t layout{config::config_animation_sprite_sheet_layout_t::None}; +}; +} // namespace bongocat::assets -#endif // BONGOCAT_EMBEDDED_ASSETS_IMAGE_H +#endif // BONGOCAT_EMBEDDED_ASSETS_IMAGE_H diff --git a/include/embedded_assets/min_dm/min_dm.hpp b/include/embedded_assets/min_dm/min_dm.hpp index c483e783..235e871f 100644 --- a/include/embedded_assets/min_dm/min_dm.hpp +++ b/include/embedded_assets/min_dm/min_dm.hpp @@ -4,91 +4,91 @@ #include namespace bongocat::assets { - // Botamon - inline static constexpr std::size_t DM_BOTAMON_SPRITE_SHEET_COLS = 11; - inline static constexpr std::size_t DM_BOTAMON_SPRITE_SHEET_ROWS = 1; - inline static constexpr std::size_t DM_BOTAMON_SPRITE_SHEET_FRAMES_COUNT = 11; - inline static constexpr std::size_t DM_BOTAMON_ANIM_INDEX = 0; - - // Koromon - inline static constexpr std::size_t DM_KOROMON_SPRITE_SHEET_COLS = 9; - inline static constexpr std::size_t DM_KOROMON_SPRITE_SHEET_ROWS = 1; - inline static constexpr std::size_t DM_KOROMON_SPRITE_SHEET_FRAMES_COUNT = 9; - inline static constexpr std::size_t DM_KOROMON_ANIM_INDEX = 1; - - // Agumon - inline static constexpr std::size_t DM_AGUMON_SPRITE_SHEET_COLS = 9; - inline static constexpr std::size_t DM_AGUMON_SPRITE_SHEET_ROWS = 1; - inline static constexpr std::size_t DM_AGUMON_SPRITE_SHEET_FRAMES_COUNT = 9; - inline static constexpr std::size_t DM_AGUMON_ANIM_INDEX = 2; - - // Betamon - inline static constexpr std::size_t DM_BETAMON_SPRITE_SHEET_COLS = 10; - inline static constexpr std::size_t DM_BETAMON_SPRITE_SHEET_ROWS = 1; - inline static constexpr std::size_t DM_BETAMON_SPRITE_SHEET_FRAMES_COUNT = 10; - inline static constexpr std::size_t DM_BETAMON_ANIM_INDEX = 3; - - // Greymon - inline static constexpr std::size_t DM_GREYMON_SPRITE_SHEET_COLS = 10; - inline static constexpr std::size_t DM_GREYMON_SPRITE_SHEET_ROWS = 1; - inline static constexpr std::size_t DM_GREYMON_SPRITE_SHEET_FRAMES_COUNT = 10; - inline static constexpr std::size_t DM_GREYMON_ANIM_INDEX = 4; - - // Tyranomon - inline static constexpr std::size_t DM_TYRANOMON_SPRITE_SHEET_COLS = 9; - inline static constexpr std::size_t DM_TYRANOMON_SPRITE_SHEET_ROWS = 1; - inline static constexpr std::size_t DM_TYRANOMON_SPRITE_SHEET_FRAMES_COUNT = 9; - inline static constexpr std::size_t DM_TYRANOMON_ANIM_INDEX = 5; - - // Devimon - inline static constexpr std::size_t DM_DEVIMON_SPRITE_SHEET_COLS = 9; - inline static constexpr std::size_t DM_DEVIMON_SPRITE_SHEET_ROWS = 1; - inline static constexpr std::size_t DM_DEVIMON_SPRITE_SHEET_FRAMES_COUNT = 9; - inline static constexpr std::size_t DM_DEVIMON_ANIM_INDEX = 6; - - // Meramon - inline static constexpr std::size_t DM_MERAMON_SPRITE_SHEET_COLS = 9; - inline static constexpr std::size_t DM_MERAMON_SPRITE_SHEET_ROWS = 1; - inline static constexpr std::size_t DM_MERAMON_SPRITE_SHEET_FRAMES_COUNT = 9; - inline static constexpr std::size_t DM_MERAMON_ANIM_INDEX = 7; - - // Airdramon - inline static constexpr std::size_t DM_AIRDRAMON_SPRITE_SHEET_COLS = 9; - inline static constexpr std::size_t DM_AIRDRAMON_SPRITE_SHEET_ROWS = 1; - inline static constexpr std::size_t DM_AIRDRAMON_SPRITE_SHEET_FRAMES_COUNT = 9; - inline static constexpr std::size_t DM_AIRDRAMON_ANIM_INDEX = 8; - - // Seadramon - inline static constexpr std::size_t DM_SEADRAMON_SPRITE_SHEET_COLS = 8; - inline static constexpr std::size_t DM_SEADRAMON_SPRITE_SHEET_ROWS = 1; - inline static constexpr std::size_t DM_SEADRAMON_SPRITE_SHEET_FRAMES_COUNT = 8; - inline static constexpr std::size_t DM_SEADRAMON_ANIM_INDEX = 9; - - // Numemon - inline static constexpr std::size_t DM_NUMEMON_SPRITE_SHEET_COLS = 9; - inline static constexpr std::size_t DM_NUMEMON_SPRITE_SHEET_ROWS = 1; - inline static constexpr std::size_t DM_NUMEMON_SPRITE_SHEET_FRAMES_COUNT = 9; - inline static constexpr std::size_t DM_NUMEMON_ANIM_INDEX = 10; - - // Metal Greymon - inline static constexpr std::size_t DM_METAL_GREYMON_SPRITE_SHEET_COLS = 9; - inline static constexpr std::size_t DM_METAL_GREYMON_SPRITE_SHEET_ROWS = 1; - inline static constexpr std::size_t DM_METAL_GREYMON_SPRITE_SHEET_FRAMES_COUNT = 9; - inline static constexpr std::size_t DM_METAL_GREYMON_ANIM_INDEX = 11; - - // Mamemon - inline static constexpr std::size_t DM_MAMEMON_SPRITE_SHEET_COLS = 9; - inline static constexpr std::size_t DM_MAMEMON_SPRITE_SHEET_ROWS = 1; - inline static constexpr std::size_t DM_MAMEMON_SPRITE_SHEET_FRAMES_COUNT = 9; - inline static constexpr std::size_t DM_MAMEMON_ANIM_INDEX = 12; - - // Monzaemon - inline static constexpr std::size_t DM_MONZAEMON_SPRITE_SHEET_COLS = 9; - inline static constexpr std::size_t DM_MONZAEMON_SPRITE_SHEET_ROWS = 1; - inline static constexpr std::size_t DM_MONZAEMON_SPRITE_SHEET_FRAMES_COUNT = 9; - inline static constexpr std::size_t DM_MONZAEMON_ANIM_INDEX = 13; - - inline static constexpr std::size_t MIN_DM_ANIM_COUNT = 14; -} - -#endif // DM_EMBEDDED_ASSETS_HPP \ No newline at end of file +// Botamon +inline static constexpr std::size_t DM_BOTAMON_SPRITE_SHEET_COLS = 11; +inline static constexpr std::size_t DM_BOTAMON_SPRITE_SHEET_ROWS = 1; +inline static constexpr std::size_t DM_BOTAMON_SPRITE_SHEET_FRAMES_COUNT = 11; +inline static constexpr std::size_t DM_BOTAMON_ANIM_INDEX = 0; + +// Koromon +inline static constexpr std::size_t DM_KOROMON_SPRITE_SHEET_COLS = 9; +inline static constexpr std::size_t DM_KOROMON_SPRITE_SHEET_ROWS = 1; +inline static constexpr std::size_t DM_KOROMON_SPRITE_SHEET_FRAMES_COUNT = 9; +inline static constexpr std::size_t DM_KOROMON_ANIM_INDEX = 1; + +// Agumon +inline static constexpr std::size_t DM_AGUMON_SPRITE_SHEET_COLS = 9; +inline static constexpr std::size_t DM_AGUMON_SPRITE_SHEET_ROWS = 1; +inline static constexpr std::size_t DM_AGUMON_SPRITE_SHEET_FRAMES_COUNT = 9; +inline static constexpr std::size_t DM_AGUMON_ANIM_INDEX = 2; + +// Betamon +inline static constexpr std::size_t DM_BETAMON_SPRITE_SHEET_COLS = 10; +inline static constexpr std::size_t DM_BETAMON_SPRITE_SHEET_ROWS = 1; +inline static constexpr std::size_t DM_BETAMON_SPRITE_SHEET_FRAMES_COUNT = 10; +inline static constexpr std::size_t DM_BETAMON_ANIM_INDEX = 3; + +// Greymon +inline static constexpr std::size_t DM_GREYMON_SPRITE_SHEET_COLS = 10; +inline static constexpr std::size_t DM_GREYMON_SPRITE_SHEET_ROWS = 1; +inline static constexpr std::size_t DM_GREYMON_SPRITE_SHEET_FRAMES_COUNT = 10; +inline static constexpr std::size_t DM_GREYMON_ANIM_INDEX = 4; + +// Tyranomon +inline static constexpr std::size_t DM_TYRANOMON_SPRITE_SHEET_COLS = 9; +inline static constexpr std::size_t DM_TYRANOMON_SPRITE_SHEET_ROWS = 1; +inline static constexpr std::size_t DM_TYRANOMON_SPRITE_SHEET_FRAMES_COUNT = 9; +inline static constexpr std::size_t DM_TYRANOMON_ANIM_INDEX = 5; + +// Devimon +inline static constexpr std::size_t DM_DEVIMON_SPRITE_SHEET_COLS = 9; +inline static constexpr std::size_t DM_DEVIMON_SPRITE_SHEET_ROWS = 1; +inline static constexpr std::size_t DM_DEVIMON_SPRITE_SHEET_FRAMES_COUNT = 9; +inline static constexpr std::size_t DM_DEVIMON_ANIM_INDEX = 6; + +// Meramon +inline static constexpr std::size_t DM_MERAMON_SPRITE_SHEET_COLS = 9; +inline static constexpr std::size_t DM_MERAMON_SPRITE_SHEET_ROWS = 1; +inline static constexpr std::size_t DM_MERAMON_SPRITE_SHEET_FRAMES_COUNT = 9; +inline static constexpr std::size_t DM_MERAMON_ANIM_INDEX = 7; + +// Airdramon +inline static constexpr std::size_t DM_AIRDRAMON_SPRITE_SHEET_COLS = 9; +inline static constexpr std::size_t DM_AIRDRAMON_SPRITE_SHEET_ROWS = 1; +inline static constexpr std::size_t DM_AIRDRAMON_SPRITE_SHEET_FRAMES_COUNT = 9; +inline static constexpr std::size_t DM_AIRDRAMON_ANIM_INDEX = 8; + +// Seadramon +inline static constexpr std::size_t DM_SEADRAMON_SPRITE_SHEET_COLS = 8; +inline static constexpr std::size_t DM_SEADRAMON_SPRITE_SHEET_ROWS = 1; +inline static constexpr std::size_t DM_SEADRAMON_SPRITE_SHEET_FRAMES_COUNT = 8; +inline static constexpr std::size_t DM_SEADRAMON_ANIM_INDEX = 9; + +// Numemon +inline static constexpr std::size_t DM_NUMEMON_SPRITE_SHEET_COLS = 9; +inline static constexpr std::size_t DM_NUMEMON_SPRITE_SHEET_ROWS = 1; +inline static constexpr std::size_t DM_NUMEMON_SPRITE_SHEET_FRAMES_COUNT = 9; +inline static constexpr std::size_t DM_NUMEMON_ANIM_INDEX = 10; + +// Metal Greymon +inline static constexpr std::size_t DM_METAL_GREYMON_SPRITE_SHEET_COLS = 9; +inline static constexpr std::size_t DM_METAL_GREYMON_SPRITE_SHEET_ROWS = 1; +inline static constexpr std::size_t DM_METAL_GREYMON_SPRITE_SHEET_FRAMES_COUNT = 9; +inline static constexpr std::size_t DM_METAL_GREYMON_ANIM_INDEX = 11; + +// Mamemon +inline static constexpr std::size_t DM_MAMEMON_SPRITE_SHEET_COLS = 9; +inline static constexpr std::size_t DM_MAMEMON_SPRITE_SHEET_ROWS = 1; +inline static constexpr std::size_t DM_MAMEMON_SPRITE_SHEET_FRAMES_COUNT = 9; +inline static constexpr std::size_t DM_MAMEMON_ANIM_INDEX = 12; + +// Monzaemon +inline static constexpr std::size_t DM_MONZAEMON_SPRITE_SHEET_COLS = 9; +inline static constexpr std::size_t DM_MONZAEMON_SPRITE_SHEET_ROWS = 1; +inline static constexpr std::size_t DM_MONZAEMON_SPRITE_SHEET_FRAMES_COUNT = 9; +inline static constexpr std::size_t DM_MONZAEMON_ANIM_INDEX = 13; + +inline static constexpr std::size_t MIN_DM_ANIM_COUNT = 14; +} // namespace bongocat::assets + +#endif // DM_EMBEDDED_ASSETS_HPP \ No newline at end of file diff --git a/include/embedded_assets/min_dm/min_dm_images.h b/include/embedded_assets/min_dm/min_dm_images.h index 2d55959b..f2a1697b 100644 --- a/include/embedded_assets/min_dm/min_dm_images.h +++ b/include/embedded_assets/min_dm/min_dm_images.h @@ -59,4 +59,4 @@ extern const size_t dm_mamemon_png_size; extern const unsigned char dm_monzaemon_png[]; extern const size_t dm_monzaemon_png_size; -#endif // DM_EMBEDDED_ASSETS_IMAGES_H \ No newline at end of file +#endif // DM_EMBEDDED_ASSETS_IMAGES_H \ No newline at end of file diff --git a/include/embedded_assets/min_dm/min_dm_sprite.h b/include/embedded_assets/min_dm/min_dm_sprite.h index 8aab16c1..db5b2502 100644 --- a/include/embedded_assets/min_dm/min_dm_sprite.h +++ b/include/embedded_assets/min_dm/min_dm_sprite.h @@ -4,7 +4,7 @@ #include "embedded_assets/embedded_image.h" namespace bongocat::assets { - [[nodiscard]] extern embedded_image_t get_min_dm_sprite_sheet(size_t i); +BONGOCAT_NODISCARD extern embedded_image_t get_min_dm_sprite_sheet(size_t i); } #endif \ No newline at end of file diff --git a/include/embedded_assets/misc/misc.hpp b/include/embedded_assets/misc/misc.hpp index b26b8122..a7991b25 100644 --- a/include/embedded_assets/misc/misc.hpp +++ b/include/embedded_assets/misc/misc.hpp @@ -1,45 +1,46 @@ #ifndef MISC_EMBEDDED_ASSETS_HPP #define MISC_EMBEDDED_ASSETS_HPP -#include #include "embedded_assets/custom/custom_sprite.h" +#include + namespace bongocat::assets { - // neko - inline static constexpr char MISC_NEKO_FQID_ARR[] = "misc:neko"; - inline static constexpr const char* MISC_NEKO_FQID = MISC_NEKO_FQID_ARR; - inline static constexpr std::size_t MISC_NEKO_FQID_LEN = sizeof(MISC_NEKO_FQID_ARR)-1; - inline static constexpr char MISC_NEKO_ID_ARR[] = "neko"; - inline static constexpr const char* MISC_NEKO_ID = MISC_NEKO_ID_ARR; - inline static constexpr std::size_t MISC_NEKO_ID_LEN = sizeof(MISC_NEKO_ID_ARR)-1; - inline static constexpr char MISC_NEKO_NAME_ARR[] = "neko"; - inline static constexpr const char* MISC_NEKO_NAME = MISC_NEKO_NAME_ARR; - inline static constexpr std::size_t MISC_NEKO_NAME_LEN = sizeof(MISC_NEKO_NAME_ARR)-1; - inline static constexpr char MISC_NEKO_FQNAME_ARR[] = "misc:neko"; - inline static constexpr const char* MISC_NEKO_FQNAME = MISC_NEKO_FQNAME_ARR; - inline static constexpr std::size_t MISC_NEKO_FQNAME_LEN = sizeof(MISC_NEKO_FQNAME_ARR)-1; - inline static constexpr std::size_t MISC_NEKO_ANIM_INDEX = 0; - inline static constexpr custom_animation_settings_t MISC_NEKO_SPRITE_SHEET_SETTINGS { - .idle_frames = 2, - .boring_frames = 2, - .writing_frames = 2, - .happy_frames = 2, - .asleep_frames = 2, - .sleep_frames = 2, - .wake_up_frames = 1, - .working_frames = 2, - .moving_frames = 2, - .feature_toggle_writing_frames = 1, - }; - inline static constexpr std::size_t MISC_NEKO_SPRITE_SHEET_ROWS = 9; - inline static constexpr std::size_t MISC_NEKO_SPRITE_SHEET_MAX_COLS = 2; +// neko +inline static constexpr char MISC_NEKO_FQID_ARR[] = "misc:neko"; +inline static constexpr const char *MISC_NEKO_FQID = MISC_NEKO_FQID_ARR; +inline static constexpr std::size_t MISC_NEKO_FQID_LEN = sizeof(MISC_NEKO_FQID_ARR) - 1; +inline static constexpr char MISC_NEKO_ID_ARR[] = "neko"; +inline static constexpr const char *MISC_NEKO_ID = MISC_NEKO_ID_ARR; +inline static constexpr std::size_t MISC_NEKO_ID_LEN = sizeof(MISC_NEKO_ID_ARR) - 1; +inline static constexpr char MISC_NEKO_NAME_ARR[] = "neko"; +inline static constexpr const char *MISC_NEKO_NAME = MISC_NEKO_NAME_ARR; +inline static constexpr std::size_t MISC_NEKO_NAME_LEN = sizeof(MISC_NEKO_NAME_ARR) - 1; +inline static constexpr char MISC_NEKO_FQNAME_ARR[] = "misc:neko"; +inline static constexpr const char *MISC_NEKO_FQNAME = MISC_NEKO_FQNAME_ARR; +inline static constexpr std::size_t MISC_NEKO_FQNAME_LEN = sizeof(MISC_NEKO_FQNAME_ARR) - 1; +inline static constexpr std::size_t MISC_NEKO_ANIM_INDEX = 0; +inline static constexpr custom_animation_settings_t MISC_NEKO_SPRITE_SHEET_SETTINGS{ + .idle_frames = 2, + .boring_frames = 2, + .writing_frames = 2, + .happy_frames = 2, + .asleep_frames = 2, + .sleep_frames = 2, + .wake_up_frames = 1, + .working_frames = 2, + .moving_frames = 2, + .feature_toggle_writing_frames = 1, +}; +inline static constexpr std::size_t MISC_NEKO_SPRITE_SHEET_ROWS = 9; +inline static constexpr std::size_t MISC_NEKO_SPRITE_SHEET_MAX_COLS = 2; - inline static constexpr std::size_t MAX_MISC_ANIM_INDEX = 0; - inline static constexpr std::size_t MISC_ANIM_COUNT = 1; - // custom sprite sheet (at run time) - inline static constexpr std::size_t CUSTOM_ANIM_INDEX = 1; +inline static constexpr std::size_t MAX_MISC_ANIM_INDEX = 0; +inline static constexpr std::size_t MISC_ANIM_COUNT = 1; +// custom sprite sheet (at run time) +inline static constexpr std::size_t CUSTOM_ANIM_INDEX = 1; - inline static constexpr size_t MISC_MAX_SPRITE_SHEET_COL_FRAMES = 2; -} +inline static constexpr size_t MISC_MAX_SPRITE_SHEET_COL_FRAMES = 2; +} // namespace bongocat::assets -#endif // MISC_EMBEDDED_ASSETS_HPP \ No newline at end of file +#endif // MISC_EMBEDDED_ASSETS_HPP \ No newline at end of file diff --git a/include/embedded_assets/misc/misc_images.h b/include/embedded_assets/misc/misc_images.h index 880a1a5a..62ce6582 100644 --- a/include/embedded_assets/misc/misc_images.h +++ b/include/embedded_assets/misc/misc_images.h @@ -7,4 +7,4 @@ extern const unsigned char misc_neko_png[]; extern const size_t misc_neko_png_size; -#endif // MISC_EMBEDDED_ASSETS_IMAGES_H \ No newline at end of file +#endif // MISC_EMBEDDED_ASSETS_IMAGES_H \ No newline at end of file diff --git a/include/embedded_assets/misc/misc_sprite.h b/include/embedded_assets/misc/misc_sprite.h index 756be7ba..6d219285 100644 --- a/include/embedded_assets/misc/misc_sprite.h +++ b/include/embedded_assets/misc/misc_sprite.h @@ -4,11 +4,11 @@ #include "embedded_assets/embedded_image.h" namespace bongocat::assets { - inline static constexpr size_t MISC_SPRITE_SHEET_EMBEDDED_IMAGES_COUNT = 1; - inline static constexpr size_t MISC_ANIMATIONS_COUNT = 1; +inline static constexpr size_t MISC_SPRITE_SHEET_EMBEDDED_IMAGES_COUNT = 1; +inline static constexpr size_t MISC_ANIMATIONS_COUNT = 1; - [[nodiscard]] extern embedded_image_t get_misc_sprite_sheet(size_t i); - [[nodiscard]] extern custom_animation_settings_t get_misc_sprite_sheet_columns(size_t i); -} +BONGOCAT_NODISCARD extern embedded_image_t get_misc_sprite_sheet(size_t i); +BONGOCAT_NODISCARD extern custom_animation_settings_t get_misc_sprite_sheet_columns(size_t i); +} // namespace bongocat::assets #endif \ No newline at end of file diff --git a/include/embedded_assets/ms_agent/ms_agent.hpp b/include/embedded_assets/ms_agent/ms_agent.hpp index e7fa11c7..e2aed5b8 100644 --- a/include/embedded_assets/ms_agent/ms_agent.hpp +++ b/include/embedded_assets/ms_agent/ms_agent.hpp @@ -4,115 +4,114 @@ #include namespace bongocat::assets { - // Name: Clippy - inline static constexpr size_t CLIPPY_SPRITE_SHEET_COLS = 40; - inline static constexpr size_t CLIPPY_SPRITE_SHEET_ROWS = 6; - inline static constexpr size_t CLIPPY_FRAMES_IDLE = 4; - inline static constexpr size_t CLIPPY_FRAMES_BORING = 40; - inline static constexpr size_t CLIPPY_FRAMES_START_WRITING = 9; - inline static constexpr size_t CLIPPY_FRAMES_WRITING = 35; - inline static constexpr size_t CLIPPY_FRAMES_END_WRITING = 5; - inline static constexpr size_t CLIPPY_FRAMES_SLEEP = 19; - inline static constexpr size_t CLIPPY_FRAMES_WAKE_UP = 16; +// Name: Clippy +inline static constexpr size_t CLIPPY_SPRITE_SHEET_COLS = 40; +inline static constexpr size_t CLIPPY_SPRITE_SHEET_ROWS = 6; +inline static constexpr size_t CLIPPY_FRAMES_IDLE = 4; +inline static constexpr size_t CLIPPY_FRAMES_BORING = 40; +inline static constexpr size_t CLIPPY_FRAMES_START_WRITING = 9; +inline static constexpr size_t CLIPPY_FRAMES_WRITING = 35; +inline static constexpr size_t CLIPPY_FRAMES_END_WRITING = 5; +inline static constexpr size_t CLIPPY_FRAMES_SLEEP = 19; +inline static constexpr size_t CLIPPY_FRAMES_WAKE_UP = 16; - inline static constexpr char CLIPPY_FQID_ARR[] = "ms_agent:clippy"; - inline static constexpr const char* CLIPPY_FQID = CLIPPY_FQID_ARR; - inline static constexpr std::size_t CLIPPY_FQID_LEN = sizeof(CLIPPY_FQID_ARR)-1; - inline static constexpr char CLIPPY_ID_ARR[] = "clippy"; - inline static constexpr const char* CLIPPY_ID = CLIPPY_ID_ARR; - inline static constexpr std::size_t CLIPPY_ID_LEN = sizeof(CLIPPY_ID_ARR)-1; - inline static constexpr char CLIPPY_NAME_ARR[] = "Clippy"; - inline static constexpr const char* CLIPPY_NAME = CLIPPY_NAME_ARR; - inline static constexpr std::size_t CLIPPY_NAME_LEN = sizeof(CLIPPY_NAME_ARR)-1; - inline static constexpr char CLIPPY_FQNAME_ARR[] = "ms_agent:Clippy"; - inline static constexpr const char* CLIPPY_FQNAME = CLIPPY_FQNAME_ARR; - inline static constexpr std::size_t CLIPPY_FQNAME_LEN = sizeof(CLIPPY_FQNAME_ARR)-1; - inline static constexpr size_t CLIPPY_ANIM_INDEX = 0; +inline static constexpr char CLIPPY_FQID_ARR[] = "ms_agent:clippy"; +inline static constexpr const char *CLIPPY_FQID = CLIPPY_FQID_ARR; +inline static constexpr std::size_t CLIPPY_FQID_LEN = sizeof(CLIPPY_FQID_ARR) - 1; +inline static constexpr char CLIPPY_ID_ARR[] = "clippy"; +inline static constexpr const char *CLIPPY_ID = CLIPPY_ID_ARR; +inline static constexpr std::size_t CLIPPY_ID_LEN = sizeof(CLIPPY_ID_ARR) - 1; +inline static constexpr char CLIPPY_NAME_ARR[] = "Clippy"; +inline static constexpr const char *CLIPPY_NAME = CLIPPY_NAME_ARR; +inline static constexpr std::size_t CLIPPY_NAME_LEN = sizeof(CLIPPY_NAME_ARR) - 1; +inline static constexpr char CLIPPY_FQNAME_ARR[] = "ms_agent:Clippy"; +inline static constexpr const char *CLIPPY_FQNAME = CLIPPY_FQNAME_ARR; +inline static constexpr std::size_t CLIPPY_FQNAME_LEN = sizeof(CLIPPY_FQNAME_ARR) - 1; +inline static constexpr size_t CLIPPY_ANIM_INDEX = 0; #ifdef FEATURE_MORE_MS_AGENT_EMBEDDED_ASSETS - // Name: Links - inline static constexpr size_t LINKS_SPRITE_SHEET_COLS = 35; - inline static constexpr size_t LINKS_SPRITE_SHEET_ROWS = 6; - inline static constexpr size_t LINKS_FRAMES_IDLE = 3; - inline static constexpr size_t LINKS_FRAMES_BORING = 18; - inline static constexpr size_t LINKS_FRAMES_START_WRITING = 13; - inline static constexpr size_t LINKS_FRAMES_WRITING = 35; - inline static constexpr size_t LINKS_FRAMES_END_WRITING = 5; - inline static constexpr size_t LINKS_FRAMES_SLEEP = 20; - inline static constexpr size_t LINKS_FRAMES_WAKE_UP = 14; +// Name: Links +inline static constexpr size_t LINKS_SPRITE_SHEET_COLS = 35; +inline static constexpr size_t LINKS_SPRITE_SHEET_ROWS = 6; +inline static constexpr size_t LINKS_FRAMES_IDLE = 3; +inline static constexpr size_t LINKS_FRAMES_BORING = 18; +inline static constexpr size_t LINKS_FRAMES_START_WRITING = 13; +inline static constexpr size_t LINKS_FRAMES_WRITING = 35; +inline static constexpr size_t LINKS_FRAMES_END_WRITING = 5; +inline static constexpr size_t LINKS_FRAMES_SLEEP = 20; +inline static constexpr size_t LINKS_FRAMES_WAKE_UP = 14; - inline static constexpr char LINKS_FQID_ARR[] = "ms_agent:links"; - inline static constexpr const char* LINKS_FQID = LINKS_FQID_ARR; - inline static constexpr std::size_t LINKS_FQID_LEN = sizeof(LINKS_FQID_ARR)-1; - inline static constexpr char LINKS_ID_ARR[] = "links"; - inline static constexpr const char* LINKS_ID = LINKS_ID_ARR; - inline static constexpr std::size_t LINKS_ID_LEN = sizeof(LINKS_ID_ARR)-1; - inline static constexpr char LINKS_NAME_ARR[] = "Links"; - inline static constexpr const char* LINKS_NAME = LINKS_NAME_ARR; - inline static constexpr std::size_t LINKS_NAME_LEN = sizeof(LINKS_NAME_ARR)-1; - inline static constexpr char LINKS_FQNAME_ARR[] = "ms_agent:Links"; - inline static constexpr const char* LINKS_FQNAME = LINKS_FQNAME_ARR; - inline static constexpr std::size_t LINKS_FQNAME_LEN = sizeof(LINKS_FQNAME_ARR)-1; - inline static constexpr size_t LINKS_ANIM_INDEX = 1; +inline static constexpr char LINKS_FQID_ARR[] = "ms_agent:links"; +inline static constexpr const char *LINKS_FQID = LINKS_FQID_ARR; +inline static constexpr std::size_t LINKS_FQID_LEN = sizeof(LINKS_FQID_ARR) - 1; +inline static constexpr char LINKS_ID_ARR[] = "links"; +inline static constexpr const char *LINKS_ID = LINKS_ID_ARR; +inline static constexpr std::size_t LINKS_ID_LEN = sizeof(LINKS_ID_ARR) - 1; +inline static constexpr char LINKS_NAME_ARR[] = "Links"; +inline static constexpr const char *LINKS_NAME = LINKS_NAME_ARR; +inline static constexpr std::size_t LINKS_NAME_LEN = sizeof(LINKS_NAME_ARR) - 1; +inline static constexpr char LINKS_FQNAME_ARR[] = "ms_agent:Links"; +inline static constexpr const char *LINKS_FQNAME = LINKS_FQNAME_ARR; +inline static constexpr std::size_t LINKS_FQNAME_LEN = sizeof(LINKS_FQNAME_ARR) - 1; +inline static constexpr size_t LINKS_ANIM_INDEX = 1; - // Name: Rover - inline static constexpr size_t ROVER_SPRITE_SHEET_COLS = 102; - inline static constexpr size_t ROVER_SPRITE_SHEET_ROWS = 6; - inline static constexpr size_t ROVER_FRAMES_IDLE = 1; - inline static constexpr size_t ROVER_FRAMES_BORING = 102; - inline static constexpr size_t ROVER_FRAMES_START_WRITING = 9; - inline static constexpr size_t ROVER_FRAMES_WRITING = 34; - inline static constexpr size_t ROVER_FRAMES_END_WRITING = 12; - inline static constexpr size_t ROVER_FRAMES_SLEEP = 85; - inline static constexpr size_t ROVER_FRAMES_WAKE_UP = 14; +// Name: Rover +inline static constexpr size_t ROVER_SPRITE_SHEET_COLS = 102; +inline static constexpr size_t ROVER_SPRITE_SHEET_ROWS = 6; +inline static constexpr size_t ROVER_FRAMES_IDLE = 1; +inline static constexpr size_t ROVER_FRAMES_BORING = 102; +inline static constexpr size_t ROVER_FRAMES_START_WRITING = 9; +inline static constexpr size_t ROVER_FRAMES_WRITING = 34; +inline static constexpr size_t ROVER_FRAMES_END_WRITING = 12; +inline static constexpr size_t ROVER_FRAMES_SLEEP = 85; +inline static constexpr size_t ROVER_FRAMES_WAKE_UP = 14; - inline static constexpr char ROVER_FQID_ARR[] = "ms_agent:rover"; - inline static constexpr const char* ROVER_FQID = ROVER_FQID_ARR; - inline static constexpr std::size_t ROVER_FQID_LEN = sizeof(ROVER_FQID_ARR)-1; - inline static constexpr char ROVER_ID_ARR[] = "rover"; - inline static constexpr const char* ROVER_ID = ROVER_ID_ARR; - inline static constexpr std::size_t ROVER_ID_LEN = sizeof(ROVER_ID_ARR)-1; - inline static constexpr char ROVER_NAME_ARR[] = "Rover"; - inline static constexpr const char* ROVER_NAME = ROVER_NAME_ARR; - inline static constexpr std::size_t ROVER_NAME_LEN = sizeof(ROVER_NAME_ARR)-1; - inline static constexpr char ROVER_FQNAME_ARR[] = "ms_agent:Rover"; - inline static constexpr const char* ROVER_FQNAME = ROVER_FQNAME_ARR; - inline static constexpr std::size_t ROVER_FQNAME_LEN = sizeof(ROVER_FQNAME_ARR)-1; - inline static constexpr size_t ROVER_ANIM_INDEX = 2; +inline static constexpr char ROVER_FQID_ARR[] = "ms_agent:rover"; +inline static constexpr const char *ROVER_FQID = ROVER_FQID_ARR; +inline static constexpr std::size_t ROVER_FQID_LEN = sizeof(ROVER_FQID_ARR) - 1; +inline static constexpr char ROVER_ID_ARR[] = "rover"; +inline static constexpr const char *ROVER_ID = ROVER_ID_ARR; +inline static constexpr std::size_t ROVER_ID_LEN = sizeof(ROVER_ID_ARR) - 1; +inline static constexpr char ROVER_NAME_ARR[] = "Rover"; +inline static constexpr const char *ROVER_NAME = ROVER_NAME_ARR; +inline static constexpr std::size_t ROVER_NAME_LEN = sizeof(ROVER_NAME_ARR) - 1; +inline static constexpr char ROVER_FQNAME_ARR[] = "ms_agent:Rover"; +inline static constexpr const char *ROVER_FQNAME = ROVER_FQNAME_ARR; +inline static constexpr std::size_t ROVER_FQNAME_LEN = sizeof(ROVER_FQNAME_ARR) - 1; +inline static constexpr size_t ROVER_ANIM_INDEX = 2; - // Name: Merlin - inline static constexpr size_t MERLIN_SPRITE_SHEET_COLS = 22; - inline static constexpr size_t MERLIN_SPRITE_SHEET_ROWS = 6; - inline static constexpr size_t MERLIN_FRAMES_IDLE = 1; - inline static constexpr size_t MERLIN_FRAMES_BORING = 22; - inline static constexpr size_t MERLIN_FRAMES_START_WRITING = 6; - inline static constexpr size_t MERLIN_FRAMES_WRITING = 14; - inline static constexpr size_t MERLIN_FRAMES_END_WRITING = 6; - inline static constexpr size_t MERLIN_FRAMES_SLEEP = 20; - inline static constexpr size_t MERLIN_FRAMES_WAKE_UP = 6; +// Name: Merlin +inline static constexpr size_t MERLIN_SPRITE_SHEET_COLS = 22; +inline static constexpr size_t MERLIN_SPRITE_SHEET_ROWS = 6; +inline static constexpr size_t MERLIN_FRAMES_IDLE = 1; +inline static constexpr size_t MERLIN_FRAMES_BORING = 22; +inline static constexpr size_t MERLIN_FRAMES_START_WRITING = 6; +inline static constexpr size_t MERLIN_FRAMES_WRITING = 14; +inline static constexpr size_t MERLIN_FRAMES_END_WRITING = 6; +inline static constexpr size_t MERLIN_FRAMES_SLEEP = 20; +inline static constexpr size_t MERLIN_FRAMES_WAKE_UP = 6; - inline static constexpr char MERLIN_FQID_ARR[] = "ms_agent:merlin"; - inline static constexpr const char* MERLIN_FQID = MERLIN_FQID_ARR; - inline static constexpr std::size_t MERLIN_FQID_LEN = sizeof(MERLIN_FQID_ARR)-1; - inline static constexpr char MERLIN_ID_ARR[] = "merlin"; - inline static constexpr const char* MERLIN_ID = MERLIN_ID_ARR; - inline static constexpr std::size_t MERLIN_ID_LEN = sizeof(MERLIN_ID_ARR)-1; - inline static constexpr char MERLIN_NAME_ARR[] = "Merlin"; - inline static constexpr const char* MERLIN_NAME = MERLIN_NAME_ARR; - inline static constexpr std::size_t MERLIN_NAME_LEN = sizeof(MERLIN_NAME_ARR)-1; - inline static constexpr char MERLIN_FQNAME_ARR[] = "ms_agent:Merlin"; - inline static constexpr const char* MERLIN_FQNAME = MERLIN_FQNAME_ARR; - inline static constexpr std::size_t MERLIN_FQNAME_LEN = sizeof(MERLIN_FQNAME_ARR)-1; - inline static constexpr size_t MERLIN_ANIM_INDEX = 3; +inline static constexpr char MERLIN_FQID_ARR[] = "ms_agent:merlin"; +inline static constexpr const char *MERLIN_FQID = MERLIN_FQID_ARR; +inline static constexpr std::size_t MERLIN_FQID_LEN = sizeof(MERLIN_FQID_ARR) - 1; +inline static constexpr char MERLIN_ID_ARR[] = "merlin"; +inline static constexpr const char *MERLIN_ID = MERLIN_ID_ARR; +inline static constexpr std::size_t MERLIN_ID_LEN = sizeof(MERLIN_ID_ARR) - 1; +inline static constexpr char MERLIN_NAME_ARR[] = "Merlin"; +inline static constexpr const char *MERLIN_NAME = MERLIN_NAME_ARR; +inline static constexpr std::size_t MERLIN_NAME_LEN = sizeof(MERLIN_NAME_ARR) - 1; +inline static constexpr char MERLIN_FQNAME_ARR[] = "ms_agent:Merlin"; +inline static constexpr const char *MERLIN_FQNAME = MERLIN_FQNAME_ARR; +inline static constexpr std::size_t MERLIN_FQNAME_LEN = sizeof(MERLIN_FQNAME_ARR) - 1; +inline static constexpr size_t MERLIN_ANIM_INDEX = 3; - - inline static constexpr size_t MS_AGENTS_ANIM_COUNT = 4; - /// @TODO: determine the biggest cols from MS agents - inline static constexpr size_t MS_AGENT_MAX_SPRITE_SHEET_COL_FRAMES = ROVER_SPRITE_SHEET_COLS; +inline static constexpr size_t MS_AGENTS_ANIM_COUNT = 4; +/// @TODO: determine the biggest cols from MS agents +inline static constexpr size_t MS_AGENT_MAX_SPRITE_SHEET_COL_FRAMES = ROVER_SPRITE_SHEET_COLS; #else - inline static constexpr size_t MS_AGENTS_ANIM_COUNT = 1; - inline static constexpr size_t MS_AGENT_MAX_SPRITE_SHEET_COL_FRAMES = CLIPPY_SPRITE_SHEET_COLS; +inline static constexpr size_t MS_AGENTS_ANIM_COUNT = 1; +inline static constexpr size_t MS_AGENT_MAX_SPRITE_SHEET_COL_FRAMES = CLIPPY_SPRITE_SHEET_COLS; #endif -} +} // namespace bongocat::assets -#endif // BONGOCAT_EMBEDDED_ASSETS_CLIPPY_HPP +#endif // BONGOCAT_EMBEDDED_ASSETS_CLIPPY_HPP diff --git a/include/embedded_assets/ms_agent/ms_agent_images.h b/include/embedded_assets/ms_agent/ms_agent_images.h index ae37bab5..7733a917 100644 --- a/include/embedded_assets/ms_agent/ms_agent_images.h +++ b/include/embedded_assets/ms_agent/ms_agent_images.h @@ -18,4 +18,4 @@ extern const unsigned char merlin_png[]; extern const size_t merlin_png_size; #endif -#endif // BONGOCAT_EMBEDDED_ASSETS_CLIPPY_IMAGES_H +#endif // BONGOCAT_EMBEDDED_ASSETS_CLIPPY_IMAGES_H diff --git a/include/embedded_assets/ms_agent/ms_agent_sprite.h b/include/embedded_assets/ms_agent/ms_agent_sprite.h index 113c9d86..266a5027 100644 --- a/include/embedded_assets/ms_agent/ms_agent_sprite.h +++ b/include/embedded_assets/ms_agent/ms_agent_sprite.h @@ -2,90 +2,91 @@ #define BONGOCAT_EMBEDDED_ASSETS_CLIPPY_H #include "embedded_assets/embedded_image.h" + #include #include namespace bongocat::assets { - struct ms_agent_animation_indices_t { - int32_t start_index_frame_idle{0}; - int32_t end_index_frame_idle{0}; +struct ms_agent_animation_indices_t { + int32_t start_index_frame_idle{0}; + int32_t end_index_frame_idle{0}; - int32_t start_index_frame_boring{0}; - int32_t end_index_frame_boring{0}; + int32_t start_index_frame_boring{0}; + int32_t end_index_frame_boring{0}; - int32_t start_index_frame_start_writing{0}; - int32_t end_index_frame_start_writing{0}; - int32_t start_index_frame_writing{0}; - int32_t end_index_frame_writing{0}; - int32_t start_index_frame_end_writing{0}; - int32_t end_index_frame_end_writing{0}; + int32_t start_index_frame_start_writing{0}; + int32_t end_index_frame_start_writing{0}; + int32_t start_index_frame_writing{0}; + int32_t end_index_frame_writing{0}; + int32_t start_index_frame_end_writing{0}; + int32_t end_index_frame_end_writing{0}; - int32_t start_index_frame_sleep{0}; - int32_t end_index_frame_sleep{0}; + int32_t start_index_frame_sleep{0}; + int32_t end_index_frame_sleep{0}; - int32_t start_index_frame_wake_up{0}; - int32_t end_index_frame_wake_up{0}; + int32_t start_index_frame_wake_up{0}; + int32_t end_index_frame_wake_up{0}; - int32_t start_index_frame_start_working{0}; - int32_t end_index_frame_start_working{0}; - int32_t start_index_frame_working{0}; - int32_t end_index_frame_working{0}; - int32_t start_index_frame_end_working{0}; - int32_t end_index_frame_end_working{0}; + int32_t start_index_frame_start_working{0}; + int32_t end_index_frame_start_working{0}; + int32_t start_index_frame_working{0}; + int32_t end_index_frame_working{0}; + int32_t start_index_frame_end_working{0}; + int32_t end_index_frame_end_working{0}; - int32_t start_index_frame_start_moving{0}; - int32_t end_index_frame_start_moving{0}; - int32_t start_index_frame_moving{0}; - int32_t end_index_frame_moving{0}; - int32_t start_index_frame_end_moving{0}; - int32_t end_index_frame_end_moving{0}; + int32_t start_index_frame_start_moving{0}; + int32_t end_index_frame_start_moving{0}; + int32_t start_index_frame_moving{0}; + int32_t end_index_frame_moving{0}; + int32_t start_index_frame_end_moving{0}; + int32_t end_index_frame_end_moving{0}; - int32_t start_index_frame_happy{0}; - int32_t end_index_frame_happy{0}; - }; + int32_t start_index_frame_happy{0}; + int32_t end_index_frame_happy{0}; +}; - inline static constexpr size_t MS_AGENT_SPRITE_SHEET_ROW_IDLE = 0; - inline static constexpr size_t MS_AGENT_SPRITE_SHEET_ROW_BORING = 0; - inline static constexpr size_t MS_AGENT_SPRITE_SHEET_ROW_START_WRITING = 1; - inline static constexpr size_t MS_AGENT_SPRITE_SHEET_ROW_WRITING = 2; - inline static constexpr size_t MS_AGENT_SPRITE_SHEET_ROW_END_WRITING = 3; - inline static constexpr size_t MS_AGENT_SPRITE_SHEET_ROW_SLEEP = 4; - inline static constexpr size_t MS_AGENT_SPRITE_SHEET_ROW_WAKE_UP = 5; - inline static constexpr size_t MS_AGENT_SPRITE_SHEET_ROW_START_WORKING = 6; - inline static constexpr size_t MS_AGENT_SPRITE_SHEET_ROW_WORKING = 7; - inline static constexpr size_t MS_AGENT_SPRITE_SHEET_ROW_END_WORKING = 8; - inline static constexpr size_t MS_AGENT_SPRITE_SHEET_ROW_START_MOVING = 9; - inline static constexpr size_t MS_AGENT_SPRITE_SHEET_ROW_MOVING = 10; - inline static constexpr size_t MS_AGENT_SPRITE_SHEET_ROW_END_MOVING = 11; - inline static constexpr size_t MS_AGENT_SPRITE_SHEET_ROW_HAPPY = 12; - enum class ClippyAnimations : uint8_t { - Idle = MS_AGENT_SPRITE_SHEET_ROW_IDLE, - Boring = MS_AGENT_SPRITE_SHEET_ROW_BORING, - StartWriting = MS_AGENT_SPRITE_SHEET_ROW_START_WRITING, - Writing = MS_AGENT_SPRITE_SHEET_ROW_WRITING, - EndWriting = MS_AGENT_SPRITE_SHEET_ROW_END_WRITING, - Sleep = MS_AGENT_SPRITE_SHEET_ROW_SLEEP, - WakeUp = MS_AGENT_SPRITE_SHEET_ROW_WAKE_UP, - // optional - StartWorking = MS_AGENT_SPRITE_SHEET_ROW_START_WORKING, - Working = MS_AGENT_SPRITE_SHEET_ROW_WORKING, - EndWorking = MS_AGENT_SPRITE_SHEET_ROW_END_WORKING, - StartMoving = MS_AGENT_SPRITE_SHEET_ROW_START_MOVING, - Moving = MS_AGENT_SPRITE_SHEET_ROW_MOVING, - EndMoving = MS_AGENT_SPRITE_SHEET_ROW_END_MOVING, - Happy = MS_AGENT_SPRITE_SHEET_ROW_HAPPY, - }; +inline static constexpr size_t MS_AGENT_SPRITE_SHEET_ROW_IDLE = 0; +inline static constexpr size_t MS_AGENT_SPRITE_SHEET_ROW_BORING = 0; +inline static constexpr size_t MS_AGENT_SPRITE_SHEET_ROW_START_WRITING = 1; +inline static constexpr size_t MS_AGENT_SPRITE_SHEET_ROW_WRITING = 2; +inline static constexpr size_t MS_AGENT_SPRITE_SHEET_ROW_END_WRITING = 3; +inline static constexpr size_t MS_AGENT_SPRITE_SHEET_ROW_SLEEP = 4; +inline static constexpr size_t MS_AGENT_SPRITE_SHEET_ROW_WAKE_UP = 5; +inline static constexpr size_t MS_AGENT_SPRITE_SHEET_ROW_START_WORKING = 6; +inline static constexpr size_t MS_AGENT_SPRITE_SHEET_ROW_WORKING = 7; +inline static constexpr size_t MS_AGENT_SPRITE_SHEET_ROW_END_WORKING = 8; +inline static constexpr size_t MS_AGENT_SPRITE_SHEET_ROW_START_MOVING = 9; +inline static constexpr size_t MS_AGENT_SPRITE_SHEET_ROW_MOVING = 10; +inline static constexpr size_t MS_AGENT_SPRITE_SHEET_ROW_END_MOVING = 11; +inline static constexpr size_t MS_AGENT_SPRITE_SHEET_ROW_HAPPY = 12; +enum class ClippyAnimations : uint8_t { + Idle = MS_AGENT_SPRITE_SHEET_ROW_IDLE, + Boring = MS_AGENT_SPRITE_SHEET_ROW_BORING, + StartWriting = MS_AGENT_SPRITE_SHEET_ROW_START_WRITING, + Writing = MS_AGENT_SPRITE_SHEET_ROW_WRITING, + EndWriting = MS_AGENT_SPRITE_SHEET_ROW_END_WRITING, + Sleep = MS_AGENT_SPRITE_SHEET_ROW_SLEEP, + WakeUp = MS_AGENT_SPRITE_SHEET_ROW_WAKE_UP, + // optional + StartWorking = MS_AGENT_SPRITE_SHEET_ROW_START_WORKING, + Working = MS_AGENT_SPRITE_SHEET_ROW_WORKING, + EndWorking = MS_AGENT_SPRITE_SHEET_ROW_END_WORKING, + StartMoving = MS_AGENT_SPRITE_SHEET_ROW_START_MOVING, + Moving = MS_AGENT_SPRITE_SHEET_ROW_MOVING, + EndMoving = MS_AGENT_SPRITE_SHEET_ROW_END_MOVING, + Happy = MS_AGENT_SPRITE_SHEET_ROW_HAPPY, +}; #ifdef FEATURE_MORE_MS_AGENT_EMBEDDED_ASSETS - inline static constexpr size_t MS_AGENTS_SPRITE_SHEET_EMBEDDED_IMAGES_COUNT = 4; - inline static constexpr size_t MS_AGENTS_ANIMATIONS_COUNT = 4; +inline static constexpr size_t MS_AGENTS_SPRITE_SHEET_EMBEDDED_IMAGES_COUNT = 4; +inline static constexpr size_t MS_AGENTS_ANIMATIONS_COUNT = 4; #else - inline static constexpr size_t MS_AGENTS_SPRITE_SHEET_EMBEDDED_IMAGES_COUNT = 1; - inline static constexpr size_t MS_AGENTS_ANIMATIONS_COUNT = 1; +inline static constexpr size_t MS_AGENTS_SPRITE_SHEET_EMBEDDED_IMAGES_COUNT = 1; +inline static constexpr size_t MS_AGENTS_ANIMATIONS_COUNT = 1; #endif - [[nodiscard]] embedded_image_t get_ms_agent_sprite_sheet(size_t i); - [[nodiscard]] ms_agent_animation_indices_t get_ms_agent_animation_indices(size_t i); -} +BONGOCAT_NODISCARD embedded_image_t get_ms_agent_sprite_sheet(size_t i); +BONGOCAT_NODISCARD ms_agent_animation_indices_t get_ms_agent_animation_indices(size_t i); +} // namespace bongocat::assets -#endif // BONGOCAT_EMBEDDED_ASSETS_CLIPPY_H +#endif // BONGOCAT_EMBEDDED_ASSETS_CLIPPY_H diff --git a/include/graphics/animation.h b/include/graphics/animation.h index deaa36ed..e2abe1d6 100644 --- a/include/graphics/animation.h +++ b/include/graphics/animation.h @@ -1,35 +1,49 @@ #ifndef BONGOCAT_ANIMATION_H #define BONGOCAT_ANIMATION_H +#include "animation_context.h" +#include "animation_thread_context.h" #include "config/config.h" -#include "utils/error.h" #include "platform/input_context.h" #include "platform/update_context.h" -#include "animation_context.h" -#include "global_animation_session.h" +#include "utils/error.h" namespace bongocat::animation { - enum class trigger_animation_cause_mask_t : uint64_t { - NONE = 0, - Init = (1u << 0), - KeyPress = (1u << 1), - IdleUpdate = (1u << 2), - CpuUpdate = (1u << 3), - UpdateConfig = (1u << 4), - Timeout = (1u << 5), - }; - - [[nodiscard]] created_result_t> create(const config::config_t& config); - [[nodiscard]] bongocat_error_t start(animation_session_t& ctx, platform::input::input_context_t& input, platform::update::update_context_t& upd, const config::config_t& config, platform::CondVariable& configs_reloaded_cond, atomic_uint64_t& config_generation); - void trigger(animation_session_t& ctx, trigger_animation_cause_mask_t cause); - void trigger_update_config(animation_session_t& ctx, const config::config_t& config, uint64_t config_generation); - void update_config(animation_context_t& ctx, const config::config_t& config, uint64_t new_gen); - created_result_t hot_load_animation(animation_context_t& ctx); - [[nodiscard]] animation_t& get_current_animation(animation_context_t& ctx); - - namespace details { - created_result_t anim_load_custom_animation(animation_context_t& ctx, const config::config_t& config); - } +enum class trigger_animation_cause_mask_t : uint64_t { + NONE = 0, + Init = (1U << 0), + KeyPress = (1U << 1), + IdleUpdate = (1U << 2), + CpuUpdate = (1U << 3), + UpdateConfig = (1U << 4), + Timeout = (1U << 5), +}; + +// ============================================================================= +// ANIMATION LIFECYCLE +// ============================================================================= + +// Initialize animation system - must be checked +BONGOCAT_NODISCARD created_result_t> create(const config::config_t& config); + +// Start animation thread - must be checked +BONGOCAT_NODISCARD bongocat_error_t start(animation_context_t& ctx, platform::input::input_context_t& input, + platform::update::update_context_t& upd, const config::config_t& config, + platform::CondVariable& configs_reloaded_cond, + atomic_uint64_t& config_generation); + +// Trigger key press animation +void trigger(animation_context_t& ctx, trigger_animation_cause_mask_t cause); +void trigger_update_config(animation_context_t& ctx, const config::config_t& config, uint64_t config_generation); + +void update_config(animation_thread_context_t& ctx, const config::config_t& config, uint64_t new_gen); +created_result_t hot_load_animation(animation_thread_context_t& ctx); +BONGOCAT_NODISCARD animation_t& get_current_animation(animation_thread_context_t& ctx); + +namespace details { + created_result_t anim_load_custom_animation(animation_thread_context_t& ctx, + const config::config_t& config); } +} // namespace bongocat::animation -#endif // BONGOCAT_ANIMATION_H \ No newline at end of file +#endif // BONGOCAT_ANIMATION_H \ No newline at end of file diff --git a/include/graphics/animation_context.h b/include/graphics/animation_context.h index 39ba5e13..85e3c0ec 100644 --- a/include/graphics/animation_context.h +++ b/include/graphics/animation_context.h @@ -1,61 +1,66 @@ -#ifndef BONGOCAT_ANIMATION_CONTEXT_H -#define BONGOCAT_ANIMATION_CONTEXT_H +#ifndef BONGOCAT_ANIMATION_EVENT_CONTEXT_H +#define BONGOCAT_ANIMATION_EVENT_CONTEXT_H -#include "animation_shared_memory.h" -#include "config/config.h" -#include "utils/system_memory.h" -#include "utils/random.h" -#include +#include "graphics/animation_thread_context.h" +#include "platform/input_context.h" +#include "platform/update_context.h" namespace bongocat::animation { - struct animation_context_t; - void stop(animation_context_t& ctx); - void cleanup(animation_context_t& ctx); - - struct animation_context_t { - // local copy from other thread, update after reload (shared memory) - platform::MMapMemory _local_copy_config; - platform::MMapMemory shm; - - // Animation system state - atomic_bool _animation_running{false}; - pthread_t _anim_thread{0}; - platform::random_xoshiro128 _rng; - // lock for shm - platform::Mutex anim_lock; - - // config reload threading - platform::FileDescriptor update_config_efd; // get new_gen from here - atomic_uint64_t config_seen_generation{0}; - platform::CondVariable config_updated; - - - animation_context_t() = default; - ~animation_context_t() { - cleanup(*this); - } - - animation_context_t(const animation_context_t&) = delete; - animation_context_t& operator=(const animation_context_t&) = delete; - animation_context_t(animation_context_t&& other) = delete; - animation_context_t& operator=(animation_context_t&& other) = delete; - }; - inline void cleanup(animation_context_t& ctx) { - if (atomic_load(&ctx._animation_running)) { - stop(ctx); - // ctx.anim_lock should be unlocked - } - atomic_store(&ctx._animation_running, false); - ctx._anim_thread = 0; - - close_fd(ctx.update_config_efd); - atomic_store(&ctx.config_seen_generation, 0); - - platform::release_allocated_mmap_memory(ctx.shm); - platform::release_allocated_mmap_memory(ctx._local_copy_config); - ctx._rng = platform::random_xoshiro128(0); - } +struct animation_context_t; +void stop(animation_context_t& anim_ctx); +void cleanup(animation_context_t& anim_ctx); + +struct animation_context_t { + animation_thread_context_t thread_context; + + // event file descriptor + platform::FileDescriptor trigger_efd; + platform::FileDescriptor render_efd; + + // globals (references) + const config::config_t *_config{BONGOCAT_NULLPTR}; + platform::CondVariable *_configs_reloaded_cond{BONGOCAT_NULLPTR}; + platform::input::input_context_t *_input{BONGOCAT_NULLPTR}; + platform::update::update_context_t *_update{BONGOCAT_NULLPTR}; + atomic_uint64_t *_config_generation{BONGOCAT_NULLPTR}; + atomic_bool ready{false}; + platform::CondVariable init_cond; + + animation_context_t() = default; + ~animation_context_t() { + cleanup(*this); + } + + animation_context_t(const animation_context_t& other) = delete; + animation_context_t& operator=(const animation_context_t& other) = delete; + animation_context_t(animation_context_t&& other) noexcept = delete; + animation_context_t& operator=(animation_context_t&& other) noexcept = delete; +}; +inline void stop(animation_context_t& anim_ctx) { + stop(anim_ctx.thread_context); + + anim_ctx._config = BONGOCAT_NULLPTR; + anim_ctx._configs_reloaded_cond = BONGOCAT_NULLPTR; + anim_ctx._config_generation = BONGOCAT_NULLPTR; + + anim_ctx.thread_context.config_updated.notify_all(); + atomic_store(&anim_ctx.ready, false); + anim_ctx.init_cond.notify_all(); +} +inline void cleanup(animation_context_t& anim_ctx) { + cleanup(anim_ctx.thread_context); + + platform::close_fd(anim_ctx.trigger_efd); + platform::close_fd(anim_ctx.render_efd); + + anim_ctx._config = BONGOCAT_NULLPTR; + anim_ctx._input = BONGOCAT_NULLPTR; + anim_ctx._update = BONGOCAT_NULLPTR; + anim_ctx._configs_reloaded_cond = BONGOCAT_NULLPTR; + atomic_store(&anim_ctx.ready, false); + anim_ctx.init_cond.notify_all(); } +} // namespace bongocat::animation -#endif //BONGOCAT_ANIMATION_CONTEXT_H +#endif diff --git a/include/graphics/animation_shared_memory.h b/include/graphics/animation_shared_memory.h index 4e8b7a72..712de431 100644 --- a/include/graphics/animation_shared_memory.h +++ b/include/graphics/animation_shared_memory.h @@ -1,261 +1,270 @@ #ifndef BONGOCAT_ANIMATION_SHARED_MEMORY_H #define BONGOCAT_ANIMATION_SHARED_MEMORY_H -#include "sprite_sheet.h" #include "config/config.h" +#include "sprite_sheet.h" #include "utils/system_memory.h" #include "utils/time.h" namespace bongocat::animation { - enum class animation_player_custom_overwrite_mirror_x : uint32_t { - None, - NoMirror, - Mirror - }; - struct animation_player_result_t { - int32_t sprite_sheet_col{0}; - int32_t sprite_sheet_row{0}; - animation_player_custom_overwrite_mirror_x overwrite_mirror_x{animation_player_custom_overwrite_mirror_x::None}; - }; - - struct animation_shared_memory_t { - // animation state - animation_player_result_t animation_player_result{}; - int32_t anim_index{0}; - config::config_animation_sprite_sheet_layout_t anim_type{config::config_animation_sprite_sheet_layout_t::None}; - config::config_animation_dm_set_t anim_dm_set{config::config_animation_dm_set_t::None}; - config::config_animation_custom_set_t anim_custom_set{config::config_animation_custom_set_t::None}; - float movement_offset_x{0.0}; - float anim_direction{0.0}; - - // Animation frame data for sprite sheet preload - platform::MMapArray bongocat_anims; - platform::MMapArray dm_anims; - platform::MMapArray dm20_anims; - platform::MMapArray dmc_anims; - platform::MMapArray dmx_anims; - platform::MMapArray pen_anims; - platform::MMapArray pen20_anims; - platform::MMapArray dmall_anims; - platform::MMapArray min_dm_anims; - platform::MMapArray ms_anims; - platform::MMapArray pkmn_anims; - platform::MMapArray misc_anims; - platform::MMapArray pmd_anims; - - // for sprite sheet hot reload (or custom sprite sheet) - animation_t anim; - - animation_shared_memory_t() = default; - ~animation_shared_memory_t() { - anim_type = config::config_animation_sprite_sheet_layout_t::None; - anim_dm_set = config::config_animation_dm_set_t::None; - anim_custom_set = config::config_animation_custom_set_t::None; - animation_player_result = {}; - anim_index = 0; - movement_offset_x = 0; - anim_direction = 0; - - for (size_t i = 0; i < bongocat_anims.count; i++) { - cleanup_animation(bongocat_anims[i]); - } - platform::release_allocated_mmap_array(bongocat_anims); - - for (size_t i = 0; i < dm_anims.count; i++) { - cleanup_animation(dm_anims[i]); - } - platform::release_allocated_mmap_array(dm_anims); - - for (size_t i = 0; i < dm20_anims.count; i++) { - cleanup_animation(dm20_anims[i]); - } - platform::release_allocated_mmap_array(dm20_anims); - - for (size_t i = 0; i < dmc_anims.count; i++) { - cleanup_animation(dmc_anims[i]); - } - platform::release_allocated_mmap_array(dmc_anims); - - for (size_t i = 0; i < dmx_anims.count; i++) { - cleanup_animation(dmx_anims[i]); - } - platform::release_allocated_mmap_array(dmx_anims); - - for (size_t i = 0; i < pen_anims.count; i++) { - cleanup_animation(pen_anims[i]); - } - platform::release_allocated_mmap_array(pen_anims); - - for (size_t i = 0; i < pen20_anims.count; i++) { - cleanup_animation(pen20_anims[i]); - } - platform::release_allocated_mmap_array(pen20_anims); - - for (size_t i = 0; i < dmall_anims.count; i++) { - cleanup_animation(dmall_anims[i]); - } - platform::release_allocated_mmap_array(dmall_anims); - - for (size_t i = 0; i < min_dm_anims.count; i++) { - cleanup_animation(min_dm_anims[i]); - } - platform::release_allocated_mmap_array(min_dm_anims); - - for (size_t i = 0; i < ms_anims.count; i++) { - cleanup_animation(ms_anims[i]); - } - platform::release_allocated_mmap_array(ms_anims); - - for (size_t i = 0; i < pkmn_anims.count; i++) { - cleanup_animation(pkmn_anims[i]); - } - platform::release_allocated_mmap_array(pkmn_anims); - - for (size_t i = 0; i < misc_anims.count; i++) { - cleanup_animation(misc_anims[i]); - } - platform::release_allocated_mmap_array(misc_anims); - - for (size_t i = 0; i < pmd_anims.count; i++) { - cleanup_animation(pmd_anims[i]); - } - platform::release_allocated_mmap_array(pmd_anims); - - cleanup_animation(anim); - } - animation_shared_memory_t(const animation_shared_memory_t& other) - : animation_player_result(other.animation_player_result), anim_index(other.anim_index), anim_type(other.anim_type), anim_dm_set(other.anim_dm_set) - { - bongocat_anims = other.bongocat_anims; - dm_anims = other.dm_anims; - dm20_anims = other.dm20_anims; - dmc_anims = other.dmc_anims; - dmx_anims = other.dmx_anims; - pen_anims = other.pen_anims; - pen20_anims = other.pen20_anims; - dmall_anims = other.dmall_anims; - min_dm_anims = other.min_dm_anims; - ms_anims = other.ms_anims; - pkmn_anims = other.pkmn_anims; - misc_anims = other.misc_anims; - pmd_anims = other.pmd_anims; - - anim = other.anim; - } - animation_shared_memory_t& operator=(const animation_shared_memory_t& other) { - if (this != &other) { - anim_type = other.anim_type; - anim_dm_set = other.anim_dm_set; - anim_index = other.anim_index; - animation_player_result = other.animation_player_result; - - bongocat_anims = other.bongocat_anims; - dm_anims = other.dm_anims; - dm20_anims = other.dm20_anims; - dmc_anims = other.dmc_anims; - dmx_anims = other.dmx_anims; - pen_anims = other.pen_anims; - pen20_anims = other.pen20_anims; - dmall_anims = other.dmall_anims; - min_dm_anims = other.min_dm_anims; - ms_anims = other.ms_anims; - pkmn_anims = other.pkmn_anims; - misc_anims = other.misc_anims; - pmd_anims = other.pmd_anims; - - anim = other.anim; - } - return *this; - } - - animation_shared_memory_t(animation_shared_memory_t&& other) noexcept - : animation_player_result(other.animation_player_result), anim_index(other.anim_index), anim_type(other.anim_type), anim_dm_set(other.anim_dm_set), anim_custom_set(other.anim_custom_set) - { - bongocat_anims = bongocat::move(other.bongocat_anims); - dm_anims = bongocat::move(other.dm_anims); - dm20_anims = bongocat::move(other.dm20_anims); - dmc_anims = bongocat::move(other.dmc_anims); - dmx_anims = bongocat::move(other.dmx_anims); - pen_anims = bongocat::move(other.pen_anims); - pen20_anims = bongocat::move(other.pen20_anims); - dmall_anims = bongocat::move(other.dmall_anims); - min_dm_anims = bongocat::move(other.min_dm_anims); - ms_anims = bongocat::move(other.ms_anims); - pkmn_anims = bongocat::move(other.pkmn_anims); - misc_anims = bongocat::move(other.misc_anims); - pmd_anims = bongocat::move(other.pmd_anims); - - anim = bongocat::move(other.anim); - - cleanup_animation(other.anim); - platform::release_allocated_mmap_array(other.bongocat_anims); - platform::release_allocated_mmap_array(other.dm_anims); - platform::release_allocated_mmap_array(other.dm20_anims); - platform::release_allocated_mmap_array(other.dmc_anims); - platform::release_allocated_mmap_array(other.dmx_anims); - platform::release_allocated_mmap_array(other.pen_anims); - platform::release_allocated_mmap_array(other.pen20_anims); - platform::release_allocated_mmap_array(other.dmall_anims); - platform::release_allocated_mmap_array(other.min_dm_anims); - platform::release_allocated_mmap_array(other.ms_anims); - platform::release_allocated_mmap_array(other.pkmn_anims); - platform::release_allocated_mmap_array(other.misc_anims); - platform::release_allocated_mmap_array(other.pmd_anims); - - other.anim_type = config::config_animation_sprite_sheet_layout_t::None; - other.anim_dm_set = config::config_animation_dm_set_t::None; - other.anim_custom_set = config::config_animation_custom_set_t::None; - other.anim_index = 0; - other.animation_player_result = {}; - } - animation_shared_memory_t& operator=(animation_shared_memory_t&& other) noexcept { - if (this != &other) { - animation_player_result = other.animation_player_result; - anim_index = other.anim_index; - anim_type = other.anim_type; - anim_dm_set = other.anim_dm_set; - anim_custom_set = other.anim_custom_set; - - bongocat_anims = bongocat::move(other.bongocat_anims); - dm_anims = bongocat::move(other.dm_anims); - dm20_anims = bongocat::move(other.dm20_anims); - dmc_anims = bongocat::move(other.dmc_anims); - dmx_anims = bongocat::move(other.dmx_anims); - pen_anims = bongocat::move(other.pen_anims); - pen20_anims = bongocat::move(other.pen20_anims); - dmall_anims = bongocat::move(other.dmall_anims); - min_dm_anims = bongocat::move(other.min_dm_anims); - ms_anims = bongocat::move(other.ms_anims); - pkmn_anims = bongocat::move(other.pkmn_anims); - misc_anims = bongocat::move(other.misc_anims); - pmd_anims = bongocat::move(other.pmd_anims); - - anim = bongocat::move(other.anim); - - cleanup_animation(other.anim); - platform::release_allocated_mmap_array(other.bongocat_anims); - platform::release_allocated_mmap_array(other.dm_anims); - platform::release_allocated_mmap_array(other.dm20_anims); - platform::release_allocated_mmap_array(other.dmc_anims); - platform::release_allocated_mmap_array(other.dmx_anims); - platform::release_allocated_mmap_array(other.pen_anims); - platform::release_allocated_mmap_array(other.pen20_anims); - platform::release_allocated_mmap_array(other.dmall_anims); - platform::release_allocated_mmap_array(other.min_dm_anims); - platform::release_allocated_mmap_array(other.ms_anims); - platform::release_allocated_mmap_array(other.pkmn_anims); - platform::release_allocated_mmap_array(other.misc_anims); - platform::release_allocated_mmap_array(other.pmd_anims); - - other.anim_type = config::config_animation_sprite_sheet_layout_t::None; - other.anim_dm_set = config::config_animation_dm_set_t::None; - other.anim_custom_set = config::config_animation_custom_set_t::None; - other.anim_index = 0; - other.animation_player_result = {}; - } - return *this; - } - }; -} - -#endif // BONGOCAT_ANIMATION_SHARED_MEMORY_H \ No newline at end of file +enum class animation_player_custom_overwrite_mirror_x : uint8_t { + None, + NoMirror, + Mirror +}; +struct animation_player_result_t { + int32_t sprite_sheet_col{0}; + int32_t sprite_sheet_row{0}; + animation_player_custom_overwrite_mirror_x overwrite_mirror_x{animation_player_custom_overwrite_mirror_x::None}; +}; + +// ============================================================================= +// ANIMATION STATE (shared memory between threads) +// ============================================================================= + +struct animation_shared_memory_t { + // animation state + animation_player_result_t animation_player_result{}; + int32_t anim_index{0}; + config::config_animation_sprite_sheet_layout_t anim_type{config::config_animation_sprite_sheet_layout_t::None}; + config::config_animation_dm_set_t anim_dm_set{config::config_animation_dm_set_t::None}; + config::config_animation_custom_set_t anim_custom_set{config::config_animation_custom_set_t::None}; + float movement_offset_x{0.0}; + float anim_direction{0.0}; + + // Animation frame data for sprite sheet preload + platform::MMapArray bongocat_anims; + platform::MMapArray dm_anims; + platform::MMapArray dm20_anims; + platform::MMapArray dmc_anims; + platform::MMapArray dmx_anims; + platform::MMapArray pen_anims; + platform::MMapArray pen20_anims; + platform::MMapArray dmall_anims; + platform::MMapArray min_dm_anims; + platform::MMapArray ms_anims; + platform::MMapArray pkmn_anims; + platform::MMapArray misc_anims; + platform::MMapArray pmd_anims; + + // for sprite sheet hot reload (or custom sprite sheet) + animation_t anim; + + animation_shared_memory_t() = default; + ~animation_shared_memory_t() { + anim_type = config::config_animation_sprite_sheet_layout_t::None; + anim_dm_set = config::config_animation_dm_set_t::None; + anim_custom_set = config::config_animation_custom_set_t::None; + animation_player_result = {}; + anim_index = 0; + movement_offset_x = 0; + anim_direction = 0; + + for (size_t i = 0; i < bongocat_anims.count; i++) { + cleanup_animation(bongocat_anims[i]); + } + platform::release_allocated_mmap_array(bongocat_anims); + + for (size_t i = 0; i < dm_anims.count; i++) { + cleanup_animation(dm_anims[i]); + } + platform::release_allocated_mmap_array(dm_anims); + + for (size_t i = 0; i < dm20_anims.count; i++) { + cleanup_animation(dm20_anims[i]); + } + platform::release_allocated_mmap_array(dm20_anims); + + for (size_t i = 0; i < dmc_anims.count; i++) { + cleanup_animation(dmc_anims[i]); + } + platform::release_allocated_mmap_array(dmc_anims); + + for (size_t i = 0; i < dmx_anims.count; i++) { + cleanup_animation(dmx_anims[i]); + } + platform::release_allocated_mmap_array(dmx_anims); + + for (size_t i = 0; i < pen_anims.count; i++) { + cleanup_animation(pen_anims[i]); + } + platform::release_allocated_mmap_array(pen_anims); + + for (size_t i = 0; i < pen20_anims.count; i++) { + cleanup_animation(pen20_anims[i]); + } + platform::release_allocated_mmap_array(pen20_anims); + + for (size_t i = 0; i < dmall_anims.count; i++) { + cleanup_animation(dmall_anims[i]); + } + platform::release_allocated_mmap_array(dmall_anims); + + for (size_t i = 0; i < min_dm_anims.count; i++) { + cleanup_animation(min_dm_anims[i]); + } + platform::release_allocated_mmap_array(min_dm_anims); + + for (size_t i = 0; i < ms_anims.count; i++) { + cleanup_animation(ms_anims[i]); + } + platform::release_allocated_mmap_array(ms_anims); + + for (size_t i = 0; i < pkmn_anims.count; i++) { + cleanup_animation(pkmn_anims[i]); + } + platform::release_allocated_mmap_array(pkmn_anims); + + for (size_t i = 0; i < misc_anims.count; i++) { + cleanup_animation(misc_anims[i]); + } + platform::release_allocated_mmap_array(misc_anims); + + for (size_t i = 0; i < pmd_anims.count; i++) { + cleanup_animation(pmd_anims[i]); + } + platform::release_allocated_mmap_array(pmd_anims); + + cleanup_animation(anim); + } + animation_shared_memory_t(const animation_shared_memory_t& other) + : animation_player_result(other.animation_player_result) + , anim_index(other.anim_index) + , anim_type(other.anim_type) + , anim_dm_set(other.anim_dm_set) { + bongocat_anims = other.bongocat_anims; + dm_anims = other.dm_anims; + dm20_anims = other.dm20_anims; + dmc_anims = other.dmc_anims; + dmx_anims = other.dmx_anims; + pen_anims = other.pen_anims; + pen20_anims = other.pen20_anims; + dmall_anims = other.dmall_anims; + min_dm_anims = other.min_dm_anims; + ms_anims = other.ms_anims; + pkmn_anims = other.pkmn_anims; + misc_anims = other.misc_anims; + pmd_anims = other.pmd_anims; + + anim = other.anim; + } + animation_shared_memory_t& operator=(const animation_shared_memory_t& other) { + if (this != &other) { + anim_type = other.anim_type; + anim_dm_set = other.anim_dm_set; + anim_index = other.anim_index; + animation_player_result = other.animation_player_result; + + bongocat_anims = other.bongocat_anims; + dm_anims = other.dm_anims; + dm20_anims = other.dm20_anims; + dmc_anims = other.dmc_anims; + dmx_anims = other.dmx_anims; + pen_anims = other.pen_anims; + pen20_anims = other.pen20_anims; + dmall_anims = other.dmall_anims; + min_dm_anims = other.min_dm_anims; + ms_anims = other.ms_anims; + pkmn_anims = other.pkmn_anims; + misc_anims = other.misc_anims; + pmd_anims = other.pmd_anims; + + anim = other.anim; + } + return *this; + } + + animation_shared_memory_t(animation_shared_memory_t&& other) noexcept + : animation_player_result(other.animation_player_result) + , anim_index(other.anim_index) + , anim_type(other.anim_type) + , anim_dm_set(other.anim_dm_set) + , anim_custom_set(other.anim_custom_set) { + bongocat_anims = bongocat::move(other.bongocat_anims); + dm_anims = bongocat::move(other.dm_anims); + dm20_anims = bongocat::move(other.dm20_anims); + dmc_anims = bongocat::move(other.dmc_anims); + dmx_anims = bongocat::move(other.dmx_anims); + pen_anims = bongocat::move(other.pen_anims); + pen20_anims = bongocat::move(other.pen20_anims); + dmall_anims = bongocat::move(other.dmall_anims); + min_dm_anims = bongocat::move(other.min_dm_anims); + ms_anims = bongocat::move(other.ms_anims); + pkmn_anims = bongocat::move(other.pkmn_anims); + misc_anims = bongocat::move(other.misc_anims); + pmd_anims = bongocat::move(other.pmd_anims); + + anim = bongocat::move(other.anim); + + cleanup_animation(other.anim); + platform::release_allocated_mmap_array(other.bongocat_anims); + platform::release_allocated_mmap_array(other.dm_anims); + platform::release_allocated_mmap_array(other.dm20_anims); + platform::release_allocated_mmap_array(other.dmc_anims); + platform::release_allocated_mmap_array(other.dmx_anims); + platform::release_allocated_mmap_array(other.pen_anims); + platform::release_allocated_mmap_array(other.pen20_anims); + platform::release_allocated_mmap_array(other.dmall_anims); + platform::release_allocated_mmap_array(other.min_dm_anims); + platform::release_allocated_mmap_array(other.ms_anims); + platform::release_allocated_mmap_array(other.pkmn_anims); + platform::release_allocated_mmap_array(other.misc_anims); + platform::release_allocated_mmap_array(other.pmd_anims); + + other.anim_type = config::config_animation_sprite_sheet_layout_t::None; + other.anim_dm_set = config::config_animation_dm_set_t::None; + other.anim_custom_set = config::config_animation_custom_set_t::None; + other.anim_index = 0; + other.animation_player_result = {}; + } + animation_shared_memory_t& operator=(animation_shared_memory_t&& other) noexcept { + if (this != &other) { + animation_player_result = other.animation_player_result; + anim_index = other.anim_index; + anim_type = other.anim_type; + anim_dm_set = other.anim_dm_set; + anim_custom_set = other.anim_custom_set; + + bongocat_anims = bongocat::move(other.bongocat_anims); + dm_anims = bongocat::move(other.dm_anims); + dm20_anims = bongocat::move(other.dm20_anims); + dmc_anims = bongocat::move(other.dmc_anims); + dmx_anims = bongocat::move(other.dmx_anims); + pen_anims = bongocat::move(other.pen_anims); + pen20_anims = bongocat::move(other.pen20_anims); + dmall_anims = bongocat::move(other.dmall_anims); + min_dm_anims = bongocat::move(other.min_dm_anims); + ms_anims = bongocat::move(other.ms_anims); + pkmn_anims = bongocat::move(other.pkmn_anims); + misc_anims = bongocat::move(other.misc_anims); + pmd_anims = bongocat::move(other.pmd_anims); + + anim = bongocat::move(other.anim); + + cleanup_animation(other.anim); + platform::release_allocated_mmap_array(other.bongocat_anims); + platform::release_allocated_mmap_array(other.dm_anims); + platform::release_allocated_mmap_array(other.dm20_anims); + platform::release_allocated_mmap_array(other.dmc_anims); + platform::release_allocated_mmap_array(other.dmx_anims); + platform::release_allocated_mmap_array(other.pen_anims); + platform::release_allocated_mmap_array(other.pen20_anims); + platform::release_allocated_mmap_array(other.dmall_anims); + platform::release_allocated_mmap_array(other.min_dm_anims); + platform::release_allocated_mmap_array(other.ms_anims); + platform::release_allocated_mmap_array(other.pkmn_anims); + platform::release_allocated_mmap_array(other.misc_anims); + platform::release_allocated_mmap_array(other.pmd_anims); + + other.anim_type = config::config_animation_sprite_sheet_layout_t::None; + other.anim_dm_set = config::config_animation_dm_set_t::None; + other.anim_custom_set = config::config_animation_custom_set_t::None; + other.anim_index = 0; + other.animation_player_result = {}; + } + return *this; + } +}; +} // namespace bongocat::animation + +#endif // BONGOCAT_ANIMATION_SHARED_MEMORY_H \ No newline at end of file diff --git a/include/graphics/animation_thread_context.h b/include/graphics/animation_thread_context.h new file mode 100644 index 00000000..eeec8f2f --- /dev/null +++ b/include/graphics/animation_thread_context.h @@ -0,0 +1,66 @@ +#ifndef BONGOCAT_ANIMATION_CONTEXT_H +#define BONGOCAT_ANIMATION_CONTEXT_H + +#include "animation_shared_memory.h" +#include "config/config.h" +#include "utils/random.h" +#include "utils/system_memory.h" + +#include + +namespace bongocat::animation { + +// ============================================================================= +// ANIMATION STATE +// ============================================================================= + +struct animation_thread_context_t; +void stop(animation_thread_context_t& ctx); +// Cleanup animation resources +void cleanup(animation_thread_context_t& ctx); + +struct animation_thread_context_t { + // local copy from other thread, update after reload (shared memory) + platform::MMapMemory _local_copy_config; + platform::MMapMemory shm; + + // Animation system state + atomic_bool _animation_running{false}; + pthread_t _anim_thread{0}; + platform::random_xoshiro128 _rng; + // lock for shm + platform::Mutex anim_lock; + + // config reload threading + platform::FileDescriptor update_config_efd; // get new_gen from here + atomic_uint64_t config_seen_generation{0}; + platform::CondVariable config_updated; + + animation_thread_context_t() = default; + ~animation_thread_context_t() { + cleanup(*this); + } + + animation_thread_context_t(const animation_thread_context_t&) = delete; + animation_thread_context_t& operator=(const animation_thread_context_t&) = delete; + animation_thread_context_t(animation_thread_context_t&& other) = delete; + animation_thread_context_t& operator=(animation_thread_context_t&& other) = delete; +}; +inline void cleanup(animation_thread_context_t& ctx) { + if (atomic_load(&ctx._animation_running)) { + stop(ctx); + // ctx.anim_lock should be unlocked + } + atomic_store(&ctx._animation_running, false); + ctx._anim_thread = 0; + + close_fd(ctx.update_config_efd); + atomic_store(&ctx.config_seen_generation, 0); + + platform::release_allocated_mmap_memory(ctx.shm); + platform::release_allocated_mmap_memory(ctx._local_copy_config); + ctx._rng = platform::random_xoshiro128(0); +} +} // namespace bongocat::animation + +#endif // BONGOCAT_ANIMATION_CONTEXT_H diff --git a/include/graphics/drawing.h b/include/graphics/drawing.h index 28236ef3..20ee42ec 100644 --- a/include/graphics/drawing.h +++ b/include/graphics/drawing.h @@ -5,32 +5,37 @@ #include namespace bongocat::animation { - enum class blit_image_color_option_flags_t : uint32_t { - Invisible = (1u << 0), - Normal = (1u << 1), - Invert = (1u << 2), - MirrorX = (1u << 3), - MirrorY = (1u << 4), - BilinearInterpolation = (1u << 5), - }; - enum class blit_image_color_order_t : uint8_t { - RGBA, - BGRA - }; +enum class blit_image_color_option_flags_t : uint32_t { + Invisible = (1u << 0), + Normal = (1u << 1), + Invert = (1u << 2), + MirrorX = (1u << 3), + MirrorY = (1u << 4), + BilinearInterpolation = (1u << 5), + DisableThresholdAlpha = (1u << 6), +}; +enum class blit_image_color_order_t : uint8_t { + RGBA, + BGRA +}; - void drawing_copy_pixel(uint8_t *dest, int dest_channels, int dest_idx, - const unsigned char *src, int src_channels, int src_idx, - blit_image_color_option_flags_t option, - blit_image_color_order_t dest_order, - blit_image_color_order_t src_order); - void blit_image_scaled(uint8_t *dest, size_t dest_size, int dest_w, int dest_h, int dest_channels, - const unsigned char *src, size_t src_size, int src_w, int src_h, int src_channels, - int src_x, int src_y, - int frame_w, int frame_h, - int offset_x, int offset_y, int target_w, int target_h, - blit_image_color_order_t dest_order, - blit_image_color_order_t src_order, - blit_image_color_option_flags_t options); -} +// ============================================================================= +// RENDERING UTILITIES +// ============================================================================= -#endif // BONGOCAT_ANIMATION_DRAWING_IMAGES_H \ No newline at end of file +void drawing_copy_pixel(uint8_t *dest, int dest_channels, int dest_idx, const unsigned char *src, int src_channels, + int src_idx, blit_image_color_option_flags_t option, blit_image_color_order_t dest_order, + blit_image_color_order_t src_order); +void drawing_blend_pixel(uint8_t *dest, int dest_channels, int dest_idx, uint8_t src_r, uint8_t src_g, uint8_t src_b, + uint8_t src_a, int src_channels, blit_image_color_option_flags_t options, + blit_image_color_order_t dest_order, blit_image_color_order_t src_order); + +// Blit scaled image to destination buffer +void blit_image_scaled(uint8_t *dest, size_t dest_size, int dest_w, int dest_h, int dest_channels, + const unsigned char *src, size_t src_size, int src_w, int src_h, int src_channels, int src_x, + int src_y, int frame_w, int frame_h, int offset_x, int offset_y, int target_w, int target_h, + blit_image_color_order_t dest_order, blit_image_color_order_t src_order, + blit_image_color_option_flags_t options); +} // namespace bongocat::animation + +#endif // BONGOCAT_ANIMATION_DRAWING_IMAGES_H \ No newline at end of file diff --git a/include/graphics/embedded_assets_dms.h b/include/graphics/embedded_assets_dms.h index 504409e9..a520f736 100644 --- a/include/graphics/embedded_assets_dms.h +++ b/include/graphics/embedded_assets_dms.h @@ -3,133 +3,138 @@ #include -#if !defined(FEATURE_DM_EMBEDDED_ASSETS) && !defined(FEATURE_DM20_EMBEDDED_ASSETS) && !defined(FEATURE_DMC_EMBEDDED_ASSETS) && !defined(FEATURE_DMX_EMBEDDED_ASSETS) && !defined(FEATURE_PEN_EMBEDDED_ASSETS) && !defined(FEATURE_PEN20_EMBEDDED_ASSETS) && !defined(FEATURE_DMALL_EMBEDDED_ASSETS) -#ifdef FEATURE_ENABLE_DM_EMBEDDED_ASSETS +#if !defined(FEATURE_DM_EMBEDDED_ASSETS) && !defined(FEATURE_DM20_EMBEDDED_ASSETS) && \ + !defined(FEATURE_DMC_EMBEDDED_ASSETS) && !defined(FEATURE_DMX_EMBEDDED_ASSETS) && \ + !defined(FEATURE_PEN_EMBEDDED_ASSETS) && !defined(FEATURE_PEN20_EMBEDDED_ASSETS) && \ + !defined(FEATURE_DMALL_EMBEDDED_ASSETS) +# ifdef FEATURE_ENABLE_DM_EMBEDDED_ASSETS // Fallback dm (minimal set) -#ifndef FEATURE_MIN_DM_EMBEDDED_ASSETS -#define FEATURE_MIN_DM_EMBEDDED_ASSETS -#endif -#include "embedded_assets/min_dm/min_dm.hpp" +# ifndef FEATURE_MIN_DM_EMBEDDED_ASSETS +# define FEATURE_MIN_DM_EMBEDDED_ASSETS +# endif +# include "embedded_assets/min_dm/min_dm.hpp" namespace bongocat::assets { - inline static constexpr size_t DM_ANIM_COUNT = MIN_DM_ANIM_COUNT; +inline static constexpr size_t DM_ANIM_COUNT = MIN_DM_ANIM_COUNT; } -#else +# else namespace bongocat::assets { - inline static constexpr size_t MIN_DM_ANIM_COUNT = 0; - inline static constexpr size_t DM_ANIM_COUNT = 0; -} -#endif +inline static constexpr size_t MIN_DM_ANIM_COUNT = 0; +inline static constexpr size_t DM_ANIM_COUNT = 0; +} // namespace bongocat::assets +# endif namespace bongocat::assets { - inline static constexpr size_t DM20_ANIM_COUNT = 0; - inline static constexpr size_t PEN_ANIM_COUNT = 0; - inline static constexpr size_t PEN20_ANIM_COUNT = 0; - inline static constexpr size_t DMX_ANIM_COUNT = 0; - inline static constexpr size_t DMC_ANIM_COUNT = 0; - inline static constexpr size_t DMALL_ANIM_COUNT = 0; -} +inline static constexpr size_t DM20_ANIM_COUNT = 0; +inline static constexpr size_t PEN_ANIM_COUNT = 0; +inline static constexpr size_t PEN20_ANIM_COUNT = 0; +inline static constexpr size_t DMX_ANIM_COUNT = 0; +inline static constexpr size_t DMC_ANIM_COUNT = 0; +inline static constexpr size_t DMALL_ANIM_COUNT = 0; +} // namespace bongocat::assets // feature full assets #else namespace bongocat::assets { - inline static constexpr size_t MIN_DM_ANIM_COUNT = 0; +inline static constexpr size_t MIN_DM_ANIM_COUNT = 0; } /// dm -#ifdef FEATURE_DM_EMBEDDED_ASSETS -#include "embedded_assets/dm/dm.hpp" -#else +# ifdef FEATURE_DM_EMBEDDED_ASSETS +# include "embedded_assets/dm/dm.hpp" +# else namespace bongocat::assets { - inline static constexpr size_t DM_ANIM_COUNT = 0; +inline static constexpr size_t DM_ANIM_COUNT = 0; } -#endif +# endif /// dm20 -#ifdef FEATURE_DM20_EMBEDDED_ASSETS -#include "embedded_assets/dm20/dm20.hpp" -#else +# ifdef FEATURE_DM20_EMBEDDED_ASSETS +# include "embedded_assets/dm20/dm20.hpp" +# else namespace bongocat::assets { - inline static constexpr size_t DM20_ANIM_COUNT = 0; +inline static constexpr size_t DM20_ANIM_COUNT = 0; } -#endif +# endif /// pen -#ifdef FEATURE_PEN_EMBEDDED_ASSETS -#include "embedded_assets/pen/pen.hpp" -#else +# ifdef FEATURE_PEN_EMBEDDED_ASSETS +# include "embedded_assets/pen/pen.hpp" +# else namespace bongocat::assets { - inline static constexpr size_t PEN_ANIM_COUNT = 0; +inline static constexpr size_t PEN_ANIM_COUNT = 0; } -#endif +# endif /// pen20 -#ifdef FEATURE_PEN20_EMBEDDED_ASSETS -#include "embedded_assets/pen20/pen20.hpp" -#else +# ifdef FEATURE_PEN20_EMBEDDED_ASSETS +# include "embedded_assets/pen20/pen20.hpp" +# else namespace bongocat::assets { - inline static constexpr size_t PEN20_ANIM_COUNT = 0; +inline static constexpr size_t PEN20_ANIM_COUNT = 0; } -#endif +# endif /// dmx -#ifdef FEATURE_DMX_EMBEDDED_ASSETS -#include "embedded_assets/dmx/dmx.hpp" -#else +# ifdef FEATURE_DMX_EMBEDDED_ASSETS +# include "embedded_assets/dmx/dmx.hpp" +# else namespace bongocat::assets { - inline static constexpr size_t DMX_ANIM_COUNT = 0; +inline static constexpr size_t DMX_ANIM_COUNT = 0; } -#endif +# endif /// dmc -#ifdef FEATURE_DMC_EMBEDDED_ASSETS -#include "embedded_assets/dmc/dmc.hpp" -#else +# ifdef FEATURE_DMC_EMBEDDED_ASSETS +# include "embedded_assets/dmc/dmc.hpp" +# else namespace bongocat::assets { - inline static constexpr size_t DMC_ANIM_COUNT = 0; +inline static constexpr size_t DMC_ANIM_COUNT = 0; } -#endif +# endif /// dmall -#ifdef FEATURE_DMALL_EMBEDDED_ASSETS -#include "embedded_assets/dmall/dmall.hpp" -#else +# ifdef FEATURE_DMALL_EMBEDDED_ASSETS +# include "embedded_assets/dmall/dmall.hpp" +# else namespace bongocat::assets { - inline static constexpr size_t DMALL_ANIM_COUNT = 0; +inline static constexpr size_t DMALL_ANIM_COUNT = 0; } -#endif +# endif #endif namespace bongocat::assets { - inline static constexpr size_t DM_ANIMATIONS_COUNT = DM_ANIM_COUNT+DM20_ANIM_COUNT+PEN_ANIM_COUNT+PEN20_ANIM_COUNT+DMX_ANIM_COUNT+DMC_ANIM_COUNT+DMALL_ANIM_COUNT; +inline static constexpr size_t DM_ANIMATIONS_COUNT = DM_ANIM_COUNT + DM20_ANIM_COUNT + PEN_ANIM_COUNT + + PEN20_ANIM_COUNT + DMX_ANIM_COUNT + DMC_ANIM_COUNT + + DMALL_ANIM_COUNT; } namespace bongocat::assets { - static inline constexpr int DM_FRAME_IDLE1 = 0; - static inline constexpr int DM_FRAME_IDLE2 = 1; - static inline constexpr int DM_FRAME_ANGRY = 2; // Angry/Refuse- or Hurt-Fallback, Eat Frame Fallback - static inline constexpr int DM_FRAME_DOWN = 3; // Sleep/Discipline Fallback - static inline constexpr int DM_FRAME_HAPPY = 4; - static inline constexpr int DM_FRAME_EAT1 = 5; - static inline constexpr int DM_FRAME_SLEEP = 6; - static inline constexpr int DM_FRAME_REFUSE = 7; - static inline constexpr int DM_FRAME_SAD = 8; - - // Optional frames - static inline constexpr int DM_FRAME_LOSE1 = 9; - static inline constexpr int DM_FRAME_EAT2 = 10; - static inline constexpr int DM_FRAME_LOSE2 = 11; - static inline constexpr int DM_FRAME_ATTACK = 12; - - static inline constexpr int DM_FRAME_MOVEMENT1 = 13; - static inline constexpr int DM_FRAME_MOVEMENT2 = 14; - - static inline constexpr int DM_FRAME_ATTACK_2 = 15; - - inline static constexpr size_t DM_SPRITE_SHEET_MAX_COLS = 16; - inline static constexpr size_t DM_SPRITE_SHEET_ROWS = 1; - inline static constexpr size_t DM_SPRITE_SHEET_ROW = 0; - - static inline constexpr int DM_HAPPY_CHANCE_PERCENT = 60; -} +static inline constexpr int DM_FRAME_IDLE1 = 0; +static inline constexpr int DM_FRAME_IDLE2 = 1; +static inline constexpr int DM_FRAME_ANGRY = 2; // Angry/Refuse- or Hurt-Fallback, Eat Frame Fallback +static inline constexpr int DM_FRAME_DOWN = 3; // Sleep/Discipline Fallback +static inline constexpr int DM_FRAME_HAPPY = 4; +static inline constexpr int DM_FRAME_EAT1 = 5; +static inline constexpr int DM_FRAME_SLEEP = 6; +static inline constexpr int DM_FRAME_REFUSE = 7; +static inline constexpr int DM_FRAME_SAD = 8; + +// Optional frames +static inline constexpr int DM_FRAME_LOSE1 = 9; +static inline constexpr int DM_FRAME_EAT2 = 10; +static inline constexpr int DM_FRAME_LOSE2 = 11; +static inline constexpr int DM_FRAME_ATTACK = 12; + +static inline constexpr int DM_FRAME_MOVEMENT1 = 13; +static inline constexpr int DM_FRAME_MOVEMENT2 = 14; + +static inline constexpr int DM_FRAME_ATTACK_2 = 15; + +inline static constexpr size_t DM_SPRITE_SHEET_MAX_COLS = 16; +inline static constexpr size_t DM_SPRITE_SHEET_ROWS = 1; +inline static constexpr size_t DM_SPRITE_SHEET_ROW = 0; + +static inline constexpr int DM_HAPPY_CHANCE_PERCENT = 60; +} // namespace bongocat::assets #endif \ No newline at end of file diff --git a/include/graphics/embedded_assets_pkmn.h b/include/graphics/embedded_assets_pkmn.h index c24675d9..c5d05424 100644 --- a/include/graphics/embedded_assets_pkmn.h +++ b/include/graphics/embedded_assets_pkmn.h @@ -5,34 +5,34 @@ /// pkmn #ifdef FEATURE_PKMN_EMBEDDED_ASSETS -#include "embedded_assets/pkmn/pkmn.hpp" +# include "embedded_assets/pkmn/pkmn.hpp" #else namespace bongocat::assets { - inline static constexpr size_t PKMN_ANIM_COUNT = 0; +inline static constexpr size_t PKMN_ANIM_COUNT = 0; } #endif /// pmd (pkmn) #ifdef FEATURE_PMD_EMBEDDED_ASSETS -#include "embedded_assets/pmd/pmd.hpp" +# include "embedded_assets/pmd/pmd.hpp" #else namespace bongocat::assets { - inline static constexpr size_t PMD_ANIM_COUNT = 0; +inline static constexpr size_t PMD_ANIM_COUNT = 0; } #endif namespace bongocat::assets { - inline static constexpr size_t PKMN_ANIMATIONS_COUNT = PKMN_ANIM_COUNT; - inline static constexpr size_t PMD_ANIMATIONS_COUNT = PMD_ANIM_COUNT; -} +inline static constexpr size_t PKMN_ANIMATIONS_COUNT = PKMN_ANIM_COUNT; +inline static constexpr size_t PMD_ANIMATIONS_COUNT = PMD_ANIM_COUNT; +} // namespace bongocat::assets namespace bongocat::assets { - static inline constexpr int PKMN_FRAME_IDLE1 = 0; - static inline constexpr int PKMN_FRAME_IDLE2 = 1; +static inline constexpr int PKMN_FRAME_IDLE1 = 0; +static inline constexpr int PKMN_FRAME_IDLE2 = 1; - inline static constexpr size_t PKMN_SPRITE_SHEET_COLS = 2; - inline static constexpr size_t PKMN_SPRITE_SHEET_ROWS = 1; - inline static constexpr size_t PKMN_SPRITE_SHEET_ROW = 0; -} +inline static constexpr size_t PKMN_SPRITE_SHEET_COLS = 2; +inline static constexpr size_t PKMN_SPRITE_SHEET_ROWS = 1; +inline static constexpr size_t PKMN_SPRITE_SHEET_ROW = 0; +} // namespace bongocat::assets #endif \ No newline at end of file diff --git a/include/graphics/global_animation_session.h b/include/graphics/global_animation_session.h deleted file mode 100644 index 04a299e4..00000000 --- a/include/graphics/global_animation_session.h +++ /dev/null @@ -1,68 +0,0 @@ -#ifndef BONGOCAT_ANIMATION_EVENT_CONTEXT_H -#define BONGOCAT_ANIMATION_EVENT_CONTEXT_H - -#include "graphics/animation_context.h" -#include "platform/input_context.h" -#include "platform/update_context.h" - -namespace bongocat::animation { - - struct animation_session_t; - void stop(animation_session_t& anim_ctx); - void cleanup(animation_session_t& anim_ctx); - - struct animation_session_t { - animation_context_t anim; - - // event file descriptor - platform::FileDescriptor trigger_efd; - platform::FileDescriptor render_efd; - - // globals (references) - const config::config_t *_config{nullptr}; - platform::CondVariable *_configs_reloaded_cond{nullptr}; - platform::input::input_context_t *_input{nullptr}; - platform::update::update_context_t *_update{nullptr}; - atomic_uint64_t *_config_generation{nullptr}; - atomic_bool ready; - platform::CondVariable init_cond; - - - - animation_session_t() = default; - ~animation_session_t() { - cleanup(*this); - } - - animation_session_t(const animation_session_t& other) = delete; - animation_session_t& operator=(const animation_session_t& other) = delete; - animation_session_t(animation_session_t&& other) noexcept = delete; - animation_session_t& operator=(animation_session_t&& other) noexcept = delete; - }; - inline void stop(animation_session_t& anim_ctx) { - stop(anim_ctx.anim); - - anim_ctx._config = nullptr; - anim_ctx._configs_reloaded_cond = nullptr; - anim_ctx._config_generation = nullptr; - - anim_ctx.anim.config_updated.notify_all(); - atomic_store(&anim_ctx.ready, false); - anim_ctx.init_cond.notify_all(); - } - inline void cleanup(animation_session_t& anim_ctx) { - cleanup(anim_ctx.anim); - - platform::close_fd(anim_ctx.trigger_efd); - platform::close_fd(anim_ctx.render_efd); - - anim_ctx._config = nullptr; - anim_ctx._input = nullptr; - anim_ctx._update = nullptr; - anim_ctx._configs_reloaded_cond = nullptr; - atomic_store(&anim_ctx.ready, false); - anim_ctx.init_cond.notify_all(); - } -} - -#endif diff --git a/include/graphics/sprite_sheet.h b/include/graphics/sprite_sheet.h index bf875283..173c8c42 100644 --- a/include/graphics/sprite_sheet.h +++ b/include/graphics/sprite_sheet.h @@ -2,535 +2,581 @@ #define BONGOCAT_ANIMATION_SPRITE_SHEET_H #include "utils/memory.h" + #include namespace bongocat::animation { - // bongocat: both-up, left-down, right-down, both-down - inline static constexpr size_t BONGOCAT_NUM_FRAMES = 4; - // dm: Idle 1, Idle 2, Angry, Down1, Happy, Eat1, Sleep1, Refuse, Down2 ~~, Eat2, Sleep2, Attack~~ - inline static constexpr size_t MAX_DIGIMON_FRAMES = 16; - // pkmn: Idle 1, Idle 2 - inline static constexpr size_t MAX_PKMN_FRAMES = 2; - // @NOTE: MS agents can have more frames and are more custom - - inline static constexpr size_t MAX_NUM_FRAMES = 16; - - inline static constexpr size_t MAX_ANIMATION_FRAMES = 4; - // @NOTE: MS agents can have more frames per row and are more custom - - struct sprite_sheet_animation_frame_t { - bool valid{false}; - int32_t col{0}; - int32_t row{0}; - }; - - struct generic_sprite_sheet_image_t { - int32_t sprite_sheet_width{0}; - int32_t sprite_sheet_height{0}; - int32_t channels{0}; - AllocatedArray pixels; - }; - - struct dm_sprite_sheet_frames_t { - sprite_sheet_animation_frame_t idle_1; // 0 - sprite_sheet_animation_frame_t idle_2; // 1 - sprite_sheet_animation_frame_t angry; // 2 - sprite_sheet_animation_frame_t down; // 3 - sprite_sheet_animation_frame_t happy; // 4 - sprite_sheet_animation_frame_t eat_1; // 5 - sprite_sheet_animation_frame_t sleep; // 6 - sprite_sheet_animation_frame_t refuse; // 7 - sprite_sheet_animation_frame_t sad; // 8 - - // optional - sprite_sheet_animation_frame_t lose_1; // 9 - sprite_sheet_animation_frame_t eat_2; //10 - sprite_sheet_animation_frame_t lose_2; //11 - sprite_sheet_animation_frame_t attack_1; //12 - - // extra frames - sprite_sheet_animation_frame_t movement_1; //13 - sprite_sheet_animation_frame_t movement_2; //14 - sprite_sheet_animation_frame_t attack_2; //15 - }; - - struct sprite_sheet_animations_t { - int32_t idle[MAX_ANIMATION_FRAMES]{}; - int32_t boring[MAX_ANIMATION_FRAMES]{}; - int32_t writing[MAX_ANIMATION_FRAMES]{}; - int32_t sleep[MAX_ANIMATION_FRAMES]{}; - int32_t wake_up[MAX_ANIMATION_FRAMES]{}; - int32_t working[MAX_ANIMATION_FRAMES]{}; // attack - int32_t moving[MAX_ANIMATION_FRAMES]{}; - int32_t happy[MAX_ANIMATION_FRAMES]{}; - int32_t running[MAX_ANIMATION_FRAMES]{}; - }; - - struct dm_sprite_sheet_t { - generic_sprite_sheet_image_t image; - - int32_t frame_width{0}; - int32_t frame_height{0}; - int32_t total_frames{0}; - - dm_sprite_sheet_frames_t frames; - - sprite_sheet_animations_t animations; - }; - - struct pkmn_sprite_sheet_t { - generic_sprite_sheet_image_t image; - - int32_t frame_width{0}; - int32_t frame_height{0}; - int32_t total_frames{0}; - - sprite_sheet_animation_frame_t idle_1; - sprite_sheet_animation_frame_t idle_2; - - sprite_sheet_animations_t animations; - }; - - struct bongocat_sprite_sheet_t { - generic_sprite_sheet_image_t image; - - int32_t frame_width{0}; - int32_t frame_height{0}; - int32_t total_frames{0}; - - sprite_sheet_animation_frame_t both_up; - sprite_sheet_animation_frame_t left_down; - sprite_sheet_animation_frame_t right_down; - sprite_sheet_animation_frame_t both_down; - - sprite_sheet_animations_t animations; - }; - - struct ms_agent_sprite_sheet_animation_section_t { - bool valid{false}; - int32_t start_col{0}; - int32_t end_col{0}; - int32_t row{0}; - }; - struct ms_agent_sprite_sheet_t { - generic_sprite_sheet_image_t image; - - int32_t frame_width{0}; - int32_t frame_height{0}; - - ms_agent_sprite_sheet_animation_section_t idle; - ms_agent_sprite_sheet_animation_section_t boring; - - ms_agent_sprite_sheet_animation_section_t start_writing; - ms_agent_sprite_sheet_animation_section_t writing; - ms_agent_sprite_sheet_animation_section_t end_writing; - - ms_agent_sprite_sheet_animation_section_t sleep; - ms_agent_sprite_sheet_animation_section_t wake_up; - - ms_agent_sprite_sheet_animation_section_t start_working; - ms_agent_sprite_sheet_animation_section_t working; - ms_agent_sprite_sheet_animation_section_t end_working; - - ms_agent_sprite_sheet_animation_section_t start_moving; - ms_agent_sprite_sheet_animation_section_t moving; - ms_agent_sprite_sheet_animation_section_t end_moving; - - ms_agent_sprite_sheet_animation_section_t happy; - - ms_agent_sprite_sheet_animation_section_t start_running; - ms_agent_sprite_sheet_animation_section_t running; - ms_agent_sprite_sheet_animation_section_t end_running; - }; - - struct custom_sprite_sheet_animation_section_t { - bool valid{false}; - int32_t start_col{0}; - int32_t end_col{0}; - int32_t row{0}; - }; - struct custom_sprite_sheet_t { - generic_sprite_sheet_image_t image; - - int32_t frame_width{0}; - int32_t frame_height{0}; - - custom_sprite_sheet_animation_section_t idle; - custom_sprite_sheet_animation_section_t boring; - - custom_sprite_sheet_animation_section_t start_writing; - custom_sprite_sheet_animation_section_t writing; - custom_sprite_sheet_animation_section_t end_writing; - custom_sprite_sheet_animation_section_t happy; - - custom_sprite_sheet_animation_section_t fall_asleep; - custom_sprite_sheet_animation_section_t sleep; - custom_sprite_sheet_animation_section_t wake_up; - - custom_sprite_sheet_animation_section_t start_working; - custom_sprite_sheet_animation_section_t working; - custom_sprite_sheet_animation_section_t end_working; - - custom_sprite_sheet_animation_section_t start_moving; - custom_sprite_sheet_animation_section_t moving; - custom_sprite_sheet_animation_section_t end_moving; - - custom_sprite_sheet_animation_section_t start_running; - custom_sprite_sheet_animation_section_t running; - custom_sprite_sheet_animation_section_t end_running; - - // features - bool feature_idle{false}; - bool feature_boring{false}; - bool feature_writing{false}; - bool feature_writing_happy{false}; - bool feature_sleep{false}; - bool feature_sleep_wake_up{false}; - bool feature_working{false}; - bool feature_moving{false}; - bool feature_running{false}; - bool feature_writing_toggle_frames{false}; - bool feature_writing_toggle_frames_random{false}; - }; - - struct generic_sprite_sheet_t { - generic_sprite_sheet_image_t image{}; - - int32_t frame_width{0}; - int32_t frame_height{0}; - int32_t total_frames{0}; - - sprite_sheet_animation_frame_t frames[MAX_NUM_FRAMES]; - }; - - struct animation_t; - void cleanup_animation(animation_t& anim); - - // sprite_sheet variant - struct animation_t { - union { - bongocat_sprite_sheet_t bongocat; - dm_sprite_sheet_t dm; - ms_agent_sprite_sheet_t ms_agent; - pkmn_sprite_sheet_t pkmn; - custom_sprite_sheet_t custom; - generic_sprite_sheet_t sprite_sheet; - }; - enum class Type : uint8_t { Generic, Bongocat, Dm, MsAgent, Pkmn, Custom } type{Type::Generic}; - - animation_t() { - sprite_sheet.image.pixels.data = nullptr; - sprite_sheet.image.pixels.count = 0; - sprite_sheet.image.channels = 0; - sprite_sheet.image.sprite_sheet_width = 0; - sprite_sheet.image.sprite_sheet_height = 0; - sprite_sheet.frame_width = 0; - sprite_sheet.frame_height = 0; - sprite_sheet.total_frames = 0; - for (size_t i = 0; i < MAX_NUM_FRAMES; i++) { - sprite_sheet.frames[i] = {}; - } - type = Type::Generic; - } - ~animation_t() { - cleanup_animation(*this); - } - - animation_t(const animation_t& other) { - type = other.type; - switch (other.type) { - case Type::Bongocat: bongocat = other.bongocat; break; - case Type::Dm: dm = other.dm; break; - case Type::MsAgent: ms_agent = other.ms_agent; break; - case Type::Pkmn: pkmn = other.pkmn; break; - case Type::Custom: custom = other.custom; break; - case Type::Generic: sprite_sheet = other.sprite_sheet; break; - } - } - animation_t& operator=(const animation_t& other) { - if (this != &other) { - cleanup_animation(*this); - type = other.type; - switch (other.type) { - case Type::Bongocat: bongocat = other.bongocat; break; - case Type::Dm: dm = other.dm; break; - case Type::MsAgent: ms_agent = other.ms_agent; break; - case Type::Pkmn: pkmn = other.pkmn; break; - case Type::Custom: custom = other.custom; break; - case Type::Generic: sprite_sheet = other.sprite_sheet; break; - } - } - return *this; - } - - animation_t(animation_t&& other) noexcept { - type = other.type; - switch (other.type) { - case Type::Bongocat: - new (&bongocat) bongocat_sprite_sheet_t(bongocat::move(other.bongocat)); - break; - case Type::Dm: - new (&dm) dm_sprite_sheet_t(bongocat::move(other.dm)); - break; - case Type::MsAgent: - new (&ms_agent) ms_agent_sprite_sheet_t(bongocat::move(other.ms_agent)); - break; - case Type::Pkmn: - new (&pkmn) pkmn_sprite_sheet_t(bongocat::move(other.pkmn)); - break; - case Type::Custom: - new (&custom) custom_sprite_sheet_t(bongocat::move(other.custom)); - break; - case Type::Generic: - new (&sprite_sheet) generic_sprite_sheet_t(bongocat::move(other.sprite_sheet)); - break; - } - other.type = Type::Generic; - new (&other.sprite_sheet) generic_sprite_sheet_t(); - } - animation_t& operator=(animation_t&& other) noexcept { - if (this != &other) { - cleanup_animation(*this); - type = other.type; - switch (other.type) { - case Type::Bongocat: - new (&bongocat) bongocat_sprite_sheet_t(bongocat::move(other.bongocat)); - break; - case Type::Dm: - new (&dm) dm_sprite_sheet_t(bongocat::move(other.dm)); - break; - case Type::MsAgent: - new (&ms_agent) ms_agent_sprite_sheet_t(bongocat::move(other.ms_agent)); - break; - case Type::Pkmn: - new (&pkmn) pkmn_sprite_sheet_t(bongocat::move(other.pkmn)); - break; - case Type::Custom: - new (&custom) custom_sprite_sheet_t(bongocat::move(other.custom)); - break; - case Type::Generic: - new (&sprite_sheet) generic_sprite_sheet_t(bongocat::move(other.sprite_sheet)); - break; - } - other.type = Type::Generic; - new (&other.sprite_sheet) generic_sprite_sheet_t(); - } - return *this; - } - - explicit animation_t(bongocat_sprite_sheet_t&& sheet) noexcept - : bongocat(bongocat::move(sheet)), type(Type::Bongocat) {} - - explicit animation_t(dm_sprite_sheet_t&& sheet) noexcept - : dm(bongocat::move(sheet)), type(Type::Dm) {} - - explicit animation_t(ms_agent_sprite_sheet_t&& sheet) noexcept - : ms_agent(bongocat::move(sheet)), type(Type::MsAgent) {} - - explicit animation_t(pkmn_sprite_sheet_t&& sheet) noexcept - : pkmn(bongocat::move(sheet)), type(Type::Pkmn) {} - - explicit animation_t(custom_sprite_sheet_t&& sheet) noexcept - : custom(bongocat::move(sheet)), type(Type::Custom) {} - - explicit animation_t(generic_sprite_sheet_t&& sheet) noexcept - : sprite_sheet(bongocat::move(sheet)), type(Type::Generic) {} - - - animation_t& operator=(bongocat_sprite_sheet_t&& sheet) noexcept { - cleanup_animation(*this); - new (&bongocat) bongocat_sprite_sheet_t(bongocat::move(sheet)); - type = Type::Bongocat; - return *this; - } - animation_t& operator=(dm_sprite_sheet_t&& sheet) noexcept { - cleanup_animation(*this); - new (&dm) dm_sprite_sheet_t(bongocat::move(sheet)); - type = Type::Dm; - return *this; - } - animation_t& operator=(ms_agent_sprite_sheet_t&& sheet) noexcept { - cleanup_animation(*this); - new (&ms_agent) ms_agent_sprite_sheet_t(bongocat::move(sheet)); - type = Type::MsAgent; - return *this; - } - animation_t& operator=(pkmn_sprite_sheet_t&& sheet) noexcept { - cleanup_animation(*this); - new (&pkmn) pkmn_sprite_sheet_t(bongocat::move(sheet)); - type = Type::Pkmn; - return *this; - } - animation_t& operator=(custom_sprite_sheet_t&& sheet) noexcept { - cleanup_animation(*this); - new (&custom) custom_sprite_sheet_t(bongocat::move(sheet)); - type = Type::Custom; - return *this; - } - animation_t& operator=(generic_sprite_sheet_t&& sheet) noexcept { - cleanup_animation(*this); - new (&sprite_sheet) generic_sprite_sheet_t(bongocat::move(sheet)); - type = Type::Generic; - return *this; - } - }; - inline void cleanup_animation(generic_sprite_sheet_t& sprite_sheet) { - release_allocated_array(sprite_sheet.image.pixels); - sprite_sheet.image.sprite_sheet_width = 0; - sprite_sheet.image.sprite_sheet_height = 0; - sprite_sheet.image.channels = 0; - sprite_sheet.frame_width = 0; - sprite_sheet.frame_height = 0; - sprite_sheet.total_frames = 0; - for (size_t i = 0; i < MAX_NUM_FRAMES; i++) { - sprite_sheet.frames[i] = {}; - } +// bongocat: both-up, left-down, right-down, both-down +inline static constexpr size_t BONGOCAT_NUM_FRAMES = 4; +// dm: Idle 1, Idle 2, Angry, Down1, Happy, Eat1, Sleep1, Refuse, Down2 ~~, Eat2, Sleep2, Attack~~ +inline static constexpr size_t MAX_DIGIMON_FRAMES = 16; +// pkmn: Idle 1, Idle 2 +inline static constexpr size_t MAX_PKMN_FRAMES = 2; +// @NOTE: MS agents can have more frames and are more custom + +inline static constexpr size_t MAX_NUM_FRAMES = 16; + +inline static constexpr size_t MAX_ANIMATION_FRAMES = 4; +// @NOTE: MS agents can have more frames per row and are more custom + +struct sprite_sheet_animation_frame_t { + bool valid{false}; + int32_t col{0}; + int32_t row{0}; +}; + +struct generic_sprite_sheet_image_t { + int32_t sprite_sheet_width{0}; + int32_t sprite_sheet_height{0}; + int32_t channels{0}; + AllocatedArray pixels; +}; + +struct dm_sprite_sheet_frames_t { + sprite_sheet_animation_frame_t idle_1; // 0 + sprite_sheet_animation_frame_t idle_2; // 1 + sprite_sheet_animation_frame_t angry; // 2 + sprite_sheet_animation_frame_t down; // 3 + sprite_sheet_animation_frame_t happy; // 4 + sprite_sheet_animation_frame_t eat_1; // 5 + sprite_sheet_animation_frame_t sleep; // 6 + sprite_sheet_animation_frame_t refuse; // 7 + sprite_sheet_animation_frame_t sad; // 8 + + // optional + sprite_sheet_animation_frame_t lose_1; // 9 + sprite_sheet_animation_frame_t eat_2; // 10 + sprite_sheet_animation_frame_t lose_2; // 11 + sprite_sheet_animation_frame_t attack_1; // 12 + + // extra frames + sprite_sheet_animation_frame_t movement_1; // 13 + sprite_sheet_animation_frame_t movement_2; // 14 + sprite_sheet_animation_frame_t attack_2; // 15 +}; + +struct sprite_sheet_animations_t { + int32_t idle[MAX_ANIMATION_FRAMES]{}; + int32_t boring[MAX_ANIMATION_FRAMES]{}; + int32_t writing[MAX_ANIMATION_FRAMES]{}; + int32_t sleep[MAX_ANIMATION_FRAMES]{}; + int32_t wake_up[MAX_ANIMATION_FRAMES]{}; + int32_t working[MAX_ANIMATION_FRAMES]{}; // attack + int32_t moving[MAX_ANIMATION_FRAMES]{}; + int32_t happy[MAX_ANIMATION_FRAMES]{}; + int32_t running[MAX_ANIMATION_FRAMES]{}; +}; + +struct dm_sprite_sheet_t { + generic_sprite_sheet_image_t image; + + int32_t frame_width{0}; + int32_t frame_height{0}; + int32_t total_frames{0}; + + dm_sprite_sheet_frames_t frames; + + sprite_sheet_animations_t animations; +}; + +struct pkmn_sprite_sheet_t { + generic_sprite_sheet_image_t image; + + int32_t frame_width{0}; + int32_t frame_height{0}; + int32_t total_frames{0}; + + sprite_sheet_animation_frame_t idle_1; + sprite_sheet_animation_frame_t idle_2; + + sprite_sheet_animations_t animations; +}; + +struct bongocat_sprite_sheet_animations_t { + int32_t idle[MAX_ANIMATION_FRAMES]{}; + int32_t boring[MAX_ANIMATION_FRAMES]{}; + int32_t writing[MAX_ANIMATION_FRAMES]{}; + int32_t sleep[MAX_ANIMATION_FRAMES]{}; + int32_t wake_up[MAX_ANIMATION_FRAMES]{}; + int32_t working[MAX_ANIMATION_FRAMES]{}; // attack + int32_t moving[MAX_ANIMATION_FRAMES]{}; + int32_t happy[MAX_ANIMATION_FRAMES]{}; + int32_t running[MAX_ANIMATION_FRAMES]{}; + // extras + int32_t left_writing[MAX_ANIMATION_FRAMES]{}; + int32_t right_writing[MAX_ANIMATION_FRAMES]{}; +}; + +struct bongocat_sprite_sheet_t { + generic_sprite_sheet_image_t image; + + int32_t frame_width{0}; + int32_t frame_height{0}; + int32_t total_frames{0}; + + sprite_sheet_animation_frame_t both_up; + sprite_sheet_animation_frame_t left_down; + sprite_sheet_animation_frame_t right_down; + sprite_sheet_animation_frame_t both_down; + + bongocat_sprite_sheet_animations_t animations; +}; + +struct ms_agent_sprite_sheet_animation_section_t { + bool valid{false}; + int32_t start_col{0}; + int32_t end_col{0}; + int32_t row{0}; +}; +struct ms_agent_sprite_sheet_t { + generic_sprite_sheet_image_t image; + + int32_t frame_width{0}; + int32_t frame_height{0}; + + ms_agent_sprite_sheet_animation_section_t idle; + ms_agent_sprite_sheet_animation_section_t boring; + + ms_agent_sprite_sheet_animation_section_t start_writing; + ms_agent_sprite_sheet_animation_section_t writing; + ms_agent_sprite_sheet_animation_section_t end_writing; + + ms_agent_sprite_sheet_animation_section_t sleep; + ms_agent_sprite_sheet_animation_section_t wake_up; + + ms_agent_sprite_sheet_animation_section_t start_working; + ms_agent_sprite_sheet_animation_section_t working; + ms_agent_sprite_sheet_animation_section_t end_working; + + ms_agent_sprite_sheet_animation_section_t start_moving; + ms_agent_sprite_sheet_animation_section_t moving; + ms_agent_sprite_sheet_animation_section_t end_moving; + + ms_agent_sprite_sheet_animation_section_t happy; + + ms_agent_sprite_sheet_animation_section_t start_running; + ms_agent_sprite_sheet_animation_section_t running; + ms_agent_sprite_sheet_animation_section_t end_running; +}; + +struct custom_sprite_sheet_animation_section_t { + bool valid{false}; + int32_t start_col{0}; + int32_t end_col{0}; + int32_t row{0}; +}; +struct custom_sprite_sheet_t { + generic_sprite_sheet_image_t image; + + int32_t frame_width{0}; + int32_t frame_height{0}; + + custom_sprite_sheet_animation_section_t idle; + custom_sprite_sheet_animation_section_t boring; + + custom_sprite_sheet_animation_section_t start_writing; + custom_sprite_sheet_animation_section_t writing; + custom_sprite_sheet_animation_section_t end_writing; + custom_sprite_sheet_animation_section_t happy; + + custom_sprite_sheet_animation_section_t fall_asleep; + custom_sprite_sheet_animation_section_t sleep; + custom_sprite_sheet_animation_section_t wake_up; + + custom_sprite_sheet_animation_section_t start_working; + custom_sprite_sheet_animation_section_t working; + custom_sprite_sheet_animation_section_t end_working; + + custom_sprite_sheet_animation_section_t start_moving; + custom_sprite_sheet_animation_section_t moving; + custom_sprite_sheet_animation_section_t end_moving; + + custom_sprite_sheet_animation_section_t start_running; + custom_sprite_sheet_animation_section_t running; + custom_sprite_sheet_animation_section_t end_running; + + // features + bool feature_idle{false}; + bool feature_boring{false}; + bool feature_writing{false}; + bool feature_writing_happy{false}; + bool feature_sleep{false}; + bool feature_sleep_wake_up{false}; + bool feature_working{false}; + bool feature_moving{false}; + bool feature_running{false}; + bool feature_writing_toggle_frames{false}; + bool feature_writing_toggle_frames_random{false}; +}; + +struct generic_sprite_sheet_t { + generic_sprite_sheet_image_t image{}; + + int32_t frame_width{0}; + int32_t frame_height{0}; + int32_t total_frames{0}; + + sprite_sheet_animation_frame_t frames[MAX_NUM_FRAMES]; +}; + +struct animation_t; +void cleanup_animation(animation_t& anim); + +// sprite_sheet variant +struct animation_t { + union { + bongocat_sprite_sheet_t bongocat; + dm_sprite_sheet_t dm; + ms_agent_sprite_sheet_t ms_agent; + pkmn_sprite_sheet_t pkmn; + custom_sprite_sheet_t custom; + generic_sprite_sheet_t sprite_sheet; + }; + enum class type_t : uint8_t { + Generic, + Bongocat, + Dm, + MsAgent, + Pkmn, + Custom + } type{type_t::Generic}; + + animation_t() { + sprite_sheet.image.pixels.data = BONGOCAT_NULLPTR; + sprite_sheet.image.pixels.count = 0; + sprite_sheet.image.channels = 0; + sprite_sheet.image.sprite_sheet_width = 0; + sprite_sheet.image.sprite_sheet_height = 0; + sprite_sheet.frame_width = 0; + sprite_sheet.frame_height = 0; + sprite_sheet.total_frames = 0; + for (size_t i = 0; i < MAX_NUM_FRAMES; i++) { + sprite_sheet.frames[i] = {}; + } + type = type_t::Generic; + } + ~animation_t() { + cleanup_animation(*this); + } + + animation_t(const animation_t& other) { + type = other.type; + switch (other.type) { + case type_t::Bongocat: + bongocat = other.bongocat; + break; + case type_t::Dm: + dm = other.dm; + break; + case type_t::MsAgent: + ms_agent = other.ms_agent; + break; + case type_t::Pkmn: + pkmn = other.pkmn; + break; + case type_t::Custom: + custom = other.custom; + break; + case type_t::Generic: + sprite_sheet = other.sprite_sheet; + break; } - inline void cleanup_animation(dm_sprite_sheet_t& sprite_sheet) { - release_allocated_array(sprite_sheet.image.pixels); - sprite_sheet.image.sprite_sheet_width = 0; - sprite_sheet.image.sprite_sheet_height = 0; - sprite_sheet.image.channels = 0; - sprite_sheet.frame_width = 0; - sprite_sheet.frame_height = 0; - sprite_sheet.total_frames = 0; - sprite_sheet.animations = {}; + } + animation_t& operator=(const animation_t& other) { + if (this != &other) { + cleanup_animation(*this); + type = other.type; + switch (other.type) { + case type_t::Bongocat: + bongocat = other.bongocat; + break; + case type_t::Dm: + dm = other.dm; + break; + case type_t::MsAgent: + ms_agent = other.ms_agent; + break; + case type_t::Pkmn: + pkmn = other.pkmn; + break; + case type_t::Custom: + custom = other.custom; + break; + case type_t::Generic: + sprite_sheet = other.sprite_sheet; + break; + } } - inline void cleanup_animation(pkmn_sprite_sheet_t& sprite_sheet) { - release_allocated_array(sprite_sheet.image.pixels); - sprite_sheet.image.sprite_sheet_width = 0; - sprite_sheet.image.sprite_sheet_height = 0; - sprite_sheet.image.channels = 0; - sprite_sheet.frame_width = 0; - sprite_sheet.frame_height = 0; - sprite_sheet.total_frames = 0; - sprite_sheet.animations = {}; + return *this; + } + + animation_t(animation_t&& other) noexcept { + type = other.type; + switch (other.type) { + case type_t::Bongocat: + new (&bongocat) bongocat_sprite_sheet_t(bongocat::move(other.bongocat)); + break; + case type_t::Dm: + new (&dm) dm_sprite_sheet_t(bongocat::move(other.dm)); + break; + case type_t::MsAgent: + new (&ms_agent) ms_agent_sprite_sheet_t(bongocat::move(other.ms_agent)); + break; + case type_t::Pkmn: + new (&pkmn) pkmn_sprite_sheet_t(bongocat::move(other.pkmn)); + break; + case type_t::Custom: + new (&custom) custom_sprite_sheet_t(bongocat::move(other.custom)); + break; + case type_t::Generic: + new (&sprite_sheet) generic_sprite_sheet_t(bongocat::move(other.sprite_sheet)); + break; } - inline void cleanup_animation(bongocat_sprite_sheet_t& sprite_sheet) { - release_allocated_array(sprite_sheet.image.pixels); - sprite_sheet.image.sprite_sheet_width = 0; - sprite_sheet.image.sprite_sheet_height = 0; - sprite_sheet.image.channels = 0; - sprite_sheet.frame_width = 0; - sprite_sheet.frame_height = 0; - sprite_sheet.total_frames = 0; - sprite_sheet.animations = {}; + other.type = type_t::Generic; + new (&other.sprite_sheet) generic_sprite_sheet_t(); + } + animation_t& operator=(animation_t&& other) noexcept { + if (this != &other) { + cleanup_animation(*this); + type = other.type; + switch (other.type) { + case type_t::Bongocat: + new (&bongocat) bongocat_sprite_sheet_t(bongocat::move(other.bongocat)); + break; + case type_t::Dm: + new (&dm) dm_sprite_sheet_t(bongocat::move(other.dm)); + break; + case type_t::MsAgent: + new (&ms_agent) ms_agent_sprite_sheet_t(bongocat::move(other.ms_agent)); + break; + case type_t::Pkmn: + new (&pkmn) pkmn_sprite_sheet_t(bongocat::move(other.pkmn)); + break; + case type_t::Custom: + new (&custom) custom_sprite_sheet_t(bongocat::move(other.custom)); + break; + case type_t::Generic: + new (&sprite_sheet) generic_sprite_sheet_t(bongocat::move(other.sprite_sheet)); + break; + } + other.type = type_t::Generic; + new (&other.sprite_sheet) generic_sprite_sheet_t(); } - inline void cleanup_animation(ms_agent_sprite_sheet_t& sprite_sheet) { - release_allocated_array(sprite_sheet.image.pixels); - sprite_sheet.image.sprite_sheet_width = 0; - sprite_sheet.image.sprite_sheet_height = 0; - sprite_sheet.image.channels = 0; - sprite_sheet.frame_width = 0; - sprite_sheet.frame_height = 0; + return *this; + } + + explicit animation_t(bongocat_sprite_sheet_t&& sheet) noexcept + : bongocat(bongocat::move(sheet)) + , type(type_t::Bongocat) {} + + explicit animation_t(dm_sprite_sheet_t&& sheet) noexcept : dm(bongocat::move(sheet)), type(type_t::Dm) {} + + explicit animation_t(ms_agent_sprite_sheet_t&& sheet) noexcept + : ms_agent(bongocat::move(sheet)) + , type(type_t::MsAgent) {} + + explicit animation_t(pkmn_sprite_sheet_t&& sheet) noexcept : pkmn(bongocat::move(sheet)), type(type_t::Pkmn) {} + + explicit animation_t(custom_sprite_sheet_t&& sheet) noexcept : custom(bongocat::move(sheet)), type(type_t::Custom) {} + + explicit animation_t(generic_sprite_sheet_t&& sheet) noexcept + : sprite_sheet(bongocat::move(sheet)) + , type(type_t::Generic) {} + + animation_t& operator=(bongocat_sprite_sheet_t&& sheet) noexcept { + cleanup_animation(*this); + new (&bongocat) bongocat_sprite_sheet_t(bongocat::move(sheet)); + type = type_t::Bongocat; + return *this; + } + animation_t& operator=(dm_sprite_sheet_t&& sheet) noexcept { + cleanup_animation(*this); + new (&dm) dm_sprite_sheet_t(bongocat::move(sheet)); + type = type_t::Dm; + return *this; + } + animation_t& operator=(ms_agent_sprite_sheet_t&& sheet) noexcept { + cleanup_animation(*this); + new (&ms_agent) ms_agent_sprite_sheet_t(bongocat::move(sheet)); + type = type_t::MsAgent; + return *this; + } + animation_t& operator=(pkmn_sprite_sheet_t&& sheet) noexcept { + cleanup_animation(*this); + new (&pkmn) pkmn_sprite_sheet_t(bongocat::move(sheet)); + type = type_t::Pkmn; + return *this; + } + animation_t& operator=(custom_sprite_sheet_t&& sheet) noexcept { + cleanup_animation(*this); + new (&custom) custom_sprite_sheet_t(bongocat::move(sheet)); + type = type_t::Custom; + return *this; + } + animation_t& operator=(generic_sprite_sheet_t&& sheet) noexcept { + cleanup_animation(*this); + new (&sprite_sheet) generic_sprite_sheet_t(bongocat::move(sheet)); + type = type_t::Generic; + return *this; + } +}; +inline void cleanup_animation(generic_sprite_sheet_t& sprite_sheet) { + release_allocated_array(sprite_sheet.image.pixels); + sprite_sheet.image.sprite_sheet_width = 0; + sprite_sheet.image.sprite_sheet_height = 0; + sprite_sheet.image.channels = 0; + sprite_sheet.frame_width = 0; + sprite_sheet.frame_height = 0; + sprite_sheet.total_frames = 0; + for (size_t i = 0; i < MAX_NUM_FRAMES; i++) { + sprite_sheet.frames[i] = {}; + } +} +inline void cleanup_animation(dm_sprite_sheet_t& sprite_sheet) { + release_allocated_array(sprite_sheet.image.pixels); + sprite_sheet.image.sprite_sheet_width = 0; + sprite_sheet.image.sprite_sheet_height = 0; + sprite_sheet.image.channels = 0; + sprite_sheet.frame_width = 0; + sprite_sheet.frame_height = 0; + sprite_sheet.total_frames = 0; + sprite_sheet.animations = {}; +} +inline void cleanup_animation(pkmn_sprite_sheet_t& sprite_sheet) { + release_allocated_array(sprite_sheet.image.pixels); + sprite_sheet.image.sprite_sheet_width = 0; + sprite_sheet.image.sprite_sheet_height = 0; + sprite_sheet.image.channels = 0; + sprite_sheet.frame_width = 0; + sprite_sheet.frame_height = 0; + sprite_sheet.total_frames = 0; + sprite_sheet.animations = {}; +} +inline void cleanup_animation(bongocat_sprite_sheet_t& sprite_sheet) { + release_allocated_array(sprite_sheet.image.pixels); + sprite_sheet.image.sprite_sheet_width = 0; + sprite_sheet.image.sprite_sheet_height = 0; + sprite_sheet.image.channels = 0; + sprite_sheet.frame_width = 0; + sprite_sheet.frame_height = 0; + sprite_sheet.total_frames = 0; + sprite_sheet.animations = {}; +} +inline void cleanup_animation(ms_agent_sprite_sheet_t& sprite_sheet) { + release_allocated_array(sprite_sheet.image.pixels); + sprite_sheet.image.sprite_sheet_width = 0; + sprite_sheet.image.sprite_sheet_height = 0; + sprite_sheet.image.channels = 0; + sprite_sheet.frame_width = 0; + sprite_sheet.frame_height = 0; - sprite_sheet.idle = {}; - sprite_sheet.boring = {}; + sprite_sheet.idle = {}; + sprite_sheet.boring = {}; - sprite_sheet.start_writing = {}; - sprite_sheet.writing = {}; - sprite_sheet.end_writing = {}; + sprite_sheet.start_writing = {}; + sprite_sheet.writing = {}; + sprite_sheet.end_writing = {}; - sprite_sheet.sleep = {}; - sprite_sheet.wake_up = {}; + sprite_sheet.sleep = {}; + sprite_sheet.wake_up = {}; - sprite_sheet.start_working = {}; - sprite_sheet.working = {}; - sprite_sheet.end_working = {}; + sprite_sheet.start_working = {}; + sprite_sheet.working = {}; + sprite_sheet.end_working = {}; - sprite_sheet.start_moving = {}; - sprite_sheet.moving = {}; - sprite_sheet.end_moving = {}; + sprite_sheet.start_moving = {}; + sprite_sheet.moving = {}; + sprite_sheet.end_moving = {}; - sprite_sheet.happy = {}; - } - inline void cleanup_animation(custom_sprite_sheet_t& sprite_sheet) { - release_allocated_array(sprite_sheet.image.pixels); - sprite_sheet.image.sprite_sheet_width = 0; - sprite_sheet.image.sprite_sheet_height = 0; - sprite_sheet.image.channels = 0; - sprite_sheet.frame_width = 0; - sprite_sheet.frame_height = 0; - - sprite_sheet.idle = {}; - sprite_sheet.boring = {}; - - sprite_sheet.start_writing = {}; - sprite_sheet.writing = {}; - sprite_sheet.end_writing = {}; - sprite_sheet.happy = {}; - - sprite_sheet.fall_asleep = {}; - sprite_sheet.sleep = {}; - sprite_sheet.wake_up = {}; - - sprite_sheet.start_working = {}; - sprite_sheet.working = {}; - sprite_sheet.end_working = {}; - - sprite_sheet.start_moving = {}; - sprite_sheet.moving = {}; - sprite_sheet.end_moving = {}; - } - inline void cleanup_animation(animation_t& anim) { - switch (anim.type) { - case animation_t::Type::Bongocat: - release_allocated_array(anim.bongocat.image.pixels); - anim.bongocat.image.sprite_sheet_width = 0; - anim.bongocat.image.sprite_sheet_height = 0; - anim.bongocat.image.channels = 0; - anim.bongocat.frame_width = 0; - anim.bongocat.frame_height = 0; - break; - case animation_t::Type::Dm: - release_allocated_array(anim.dm.image.pixels); - anim.dm.image.sprite_sheet_width = 0; - anim.dm.image.sprite_sheet_height = 0; - anim.dm.image.channels = 0; - anim.dm.frame_width = 0; - anim.dm.frame_height = 0; - - anim.dm.total_frames = 0; - break; - case animation_t::Type::MsAgent: - release_allocated_array(anim.ms_agent.image.pixels); - anim.ms_agent.image.sprite_sheet_width = 0; - anim.ms_agent.image.sprite_sheet_height = 0; - anim.ms_agent.image.channels = 0; - anim.ms_agent.frame_width = 0; - anim.ms_agent.frame_height = 0; - break; - case animation_t::Type::Pkmn: - release_allocated_array(anim.pkmn.image.pixels); - anim.pkmn.image.sprite_sheet_width = 0; - anim.pkmn.image.sprite_sheet_height = 0; - anim.pkmn.image.channels = 0; - anim.pkmn.frame_width = 0; - anim.pkmn.frame_height = 0; - break; - case animation_t::Type::Custom: - release_allocated_array(anim.custom.image.pixels); - anim.custom.image.sprite_sheet_width = 0; - anim.custom.image.sprite_sheet_height = 0; - anim.custom.image.channels = 0; - anim.custom.frame_width = 0; - anim.custom.frame_height = 0; - break; - case animation_t::Type::Generic: - release_allocated_array(anim.sprite_sheet.image.pixels); - anim.sprite_sheet.image.sprite_sheet_width = 0; - anim.sprite_sheet.image.sprite_sheet_height = 0; - anim.sprite_sheet.image.channels = 0; - anim.sprite_sheet.frame_width = 0; - anim.sprite_sheet.frame_height = 0; - - anim.sprite_sheet.total_frames = 0; - for (size_t i = 0; i < MAX_NUM_FRAMES; i++) { - anim.sprite_sheet.frames[i] = {}; - } - break; - } + sprite_sheet.happy = {}; +} +inline void cleanup_animation(custom_sprite_sheet_t& sprite_sheet) { + release_allocated_array(sprite_sheet.image.pixels); + sprite_sheet.image.sprite_sheet_width = 0; + sprite_sheet.image.sprite_sheet_height = 0; + sprite_sheet.image.channels = 0; + sprite_sheet.frame_width = 0; + sprite_sheet.frame_height = 0; + + sprite_sheet.idle = {}; + sprite_sheet.boring = {}; + + sprite_sheet.start_writing = {}; + sprite_sheet.writing = {}; + sprite_sheet.end_writing = {}; + sprite_sheet.happy = {}; + + sprite_sheet.fall_asleep = {}; + sprite_sheet.sleep = {}; + sprite_sheet.wake_up = {}; + + sprite_sheet.start_working = {}; + sprite_sheet.working = {}; + sprite_sheet.end_working = {}; + + sprite_sheet.start_moving = {}; + sprite_sheet.moving = {}; + sprite_sheet.end_moving = {}; +} +inline void cleanup_animation(animation_t& anim) { + switch (anim.type) { + case animation_t::type_t::Bongocat: + release_allocated_array(anim.bongocat.image.pixels); + anim.bongocat.image.sprite_sheet_width = 0; + anim.bongocat.image.sprite_sheet_height = 0; + anim.bongocat.image.channels = 0; + anim.bongocat.frame_width = 0; + anim.bongocat.frame_height = 0; + break; + case animation_t::type_t::Dm: + release_allocated_array(anim.dm.image.pixels); + anim.dm.image.sprite_sheet_width = 0; + anim.dm.image.sprite_sheet_height = 0; + anim.dm.image.channels = 0; + anim.dm.frame_width = 0; + anim.dm.frame_height = 0; + + anim.dm.total_frames = 0; + break; + case animation_t::type_t::MsAgent: + release_allocated_array(anim.ms_agent.image.pixels); + anim.ms_agent.image.sprite_sheet_width = 0; + anim.ms_agent.image.sprite_sheet_height = 0; + anim.ms_agent.image.channels = 0; + anim.ms_agent.frame_width = 0; + anim.ms_agent.frame_height = 0; + break; + case animation_t::type_t::Pkmn: + release_allocated_array(anim.pkmn.image.pixels); + anim.pkmn.image.sprite_sheet_width = 0; + anim.pkmn.image.sprite_sheet_height = 0; + anim.pkmn.image.channels = 0; + anim.pkmn.frame_width = 0; + anim.pkmn.frame_height = 0; + break; + case animation_t::type_t::Custom: + release_allocated_array(anim.custom.image.pixels); + anim.custom.image.sprite_sheet_width = 0; + anim.custom.image.sprite_sheet_height = 0; + anim.custom.image.channels = 0; + anim.custom.frame_width = 0; + anim.custom.frame_height = 0; + break; + case animation_t::type_t::Generic: + release_allocated_array(anim.sprite_sheet.image.pixels); + anim.sprite_sheet.image.sprite_sheet_width = 0; + anim.sprite_sheet.image.sprite_sheet_height = 0; + anim.sprite_sheet.image.channels = 0; + anim.sprite_sheet.frame_width = 0; + anim.sprite_sheet.frame_height = 0; + + anim.sprite_sheet.total_frames = 0; + for (size_t i = 0; i < MAX_NUM_FRAMES; i++) { + anim.sprite_sheet.frames[i] = {}; } + break; + } } +} // namespace bongocat::animation -#endif //BONGOCAT_ANIMATION_SPRITE_SHEET_H +#endif // BONGOCAT_ANIMATION_SPRITE_SHEET_H diff --git a/include/image_loader/.clang-format b/include/image_loader/.clang-format new file mode 100644 index 00000000..15f9b6fc --- /dev/null +++ b/include/image_loader/.clang-format @@ -0,0 +1,2 @@ +DisableFormat: true +SortIncludes: Never \ No newline at end of file diff --git a/include/image_loader/base_dm/load_dm.h b/include/image_loader/base_dm/load_dm.h index 70d6f829..a9e590ce 100644 --- a/include/image_loader/base_dm/load_dm.h +++ b/include/image_loader/base_dm/load_dm.h @@ -1,10 +1,12 @@ #pragma once #include "core/bongocat.h" -#include "graphics/sprite_sheet.h" #include "embedded_assets/embedded_image.h" +#include "graphics/sprite_sheet.h" namespace bongocat::animation { - struct animation_context_t; - [[nodiscard]] created_result_t load_dm_anim(const animation_context_t& ctx, int anim_index, const assets::embedded_image_t& sprite_sheet_image, int sprite_sheet_cols, int sprite_sheet_rows); -} +struct animation_thread_context_t; +BONGOCAT_NODISCARD created_result_t load_dm_anim(const animation_thread_context_t& ctx, int anim_index, + const assets::embedded_image_t& sprite_sheet_image, + int sprite_sheet_cols, int sprite_sheet_rows); +} // namespace bongocat::animation diff --git a/include/image_loader/bongocat/load_images_bongocat.h b/include/image_loader/bongocat/load_images_bongocat.h index e1d68de8..d0fdcb83 100644 --- a/include/image_loader/bongocat/load_images_bongocat.h +++ b/include/image_loader/bongocat/load_images_bongocat.h @@ -5,9 +5,12 @@ #include "image_loader/load_images.h" namespace bongocat::animation { - struct animation_context_t; - [[nodiscard]] created_result_t load_bongocat_anim(int anim_index, get_sprite_callback_t get_sprite, size_t embedded_images_count); - bongocat_error_t init_bongocat_anim(animation_context_t& ctx, int anim_index, get_sprite_callback_t get_sprite, size_t embedded_images_count); +struct animation_thread_context_t; +BONGOCAT_NODISCARD created_result_t +load_bongocat_anim(int anim_index, get_sprite_callback_t get_sprite, size_t embedded_images_count); +bongocat_error_t init_bongocat_anim(animation_thread_context_t& ctx, int anim_index, get_sprite_callback_t get_sprite, + size_t embedded_images_count); - [[nodiscard]] created_result_t load_bongocat_sprite_sheet(const animation_context_t& /*ctx*/, int index); -} +BONGOCAT_NODISCARD created_result_t load_bongocat_sprite_sheet(const animation_thread_context_t& /*ctx*/, + int index); +} // namespace bongocat::animation diff --git a/include/image_loader/custom/load_custom.h b/include/image_loader/custom/load_custom.h index 31076d29..b36c2f7a 100644 --- a/include/image_loader/custom/load_custom.h +++ b/include/image_loader/custom/load_custom.h @@ -1,55 +1,61 @@ #pragma once #include "core/bongocat.h" -#include "graphics/sprite_sheet.h" -#include "embedded_assets/embedded_image.h" #include "embedded_assets/custom/custom_sprite.h" +#include "embedded_assets/embedded_image.h" +#include "graphics/sprite_sheet.h" #include "utils/system_memory.h" namespace bongocat::assets { - struct custom_image_t; +struct custom_image_t; } namespace bongocat::animation { - void free_custom_sprite_sheet_file(assets::custom_image_t& image) noexcept; +void free_custom_sprite_sheet_file(assets::custom_image_t& image) noexcept; } namespace bongocat::assets { - struct custom_image_t { - platform::MMapFileContent data; - char *name{nullptr}; - - custom_image_t() = default; - custom_image_t(const custom_image_t& other) = delete; - custom_image_t(custom_image_t&& other) noexcept - : data(bongocat::move(other.data)), name(other.name) { - other.name = nullptr; - } - ~custom_image_t() { - animation::free_custom_sprite_sheet_file(*this); - } - - custom_image_t& operator=(const custom_image_t& other) = delete; - custom_image_t& operator=(custom_image_t&& other) noexcept { - if (this != &other) { - animation::free_custom_sprite_sheet_file(*this); - - data = bongocat::move(other.data); - - if (name) ::free(name); - name = nullptr; - name = other.name; - - other.name = nullptr; - } - return *this; - } - }; -} +struct custom_image_t { + platform::MMapFileContent data; + char *name{BONGOCAT_NULLPTR}; + + custom_image_t() = default; + custom_image_t(const custom_image_t& other) = delete; + custom_image_t(custom_image_t&& other) noexcept : data(bongocat::move(other.data)), name(other.name) { + other.name = BONGOCAT_NULLPTR; + } + ~custom_image_t() { + animation::free_custom_sprite_sheet_file(*this); + } + + custom_image_t& operator=(const custom_image_t& other) = delete; + custom_image_t& operator=(custom_image_t&& other) noexcept { + if (this != &other) { + animation::free_custom_sprite_sheet_file(*this); + + data = bongocat::move(other.data); + + if (name != BONGOCAT_NULLPTR) { + ::free(name); + name = BONGOCAT_NULLPTR; + } + name = other.name; + + other.name = BONGOCAT_NULLPTR; + } + return *this; + } +}; +} // namespace bongocat::assets namespace bongocat::animation { - struct animation_context_t; - [[nodiscard]] created_result_t load_custom_sprite_sheet_file(const char* filename); +struct animation_thread_context_t; +BONGOCAT_NODISCARD created_result_t load_custom_sprite_sheet_file(const char *filename); - [[nodiscard]] created_result_t load_custom_anim(const animation_context_t& ctx, const assets::custom_image_t& sprite_sheet_image, const assets::custom_animation_settings_t& sprite_sheet_settings); - [[nodiscard]] created_result_t load_custom_anim(const animation_context_t& ctx, const assets::embedded_image_t& sprite_sheet_image, const assets::custom_animation_settings_t& sprite_sheet_settings); -} +BONGOCAT_NODISCARD created_result_t +load_custom_anim(const animation_thread_context_t& ctx, const assets::custom_image_t& sprite_sheet_image, + const assets::custom_animation_settings_t& sprite_sheet_settings); + +BONGOCAT_NODISCARD created_result_t +load_custom_anim(const animation_thread_context_t& ctx, const assets::embedded_image_t& sprite_sheet_image, + const assets::custom_animation_settings_t& sprite_sheet_settings); +} // namespace bongocat::animation diff --git a/include/image_loader/dm/load_images_dm.h b/include/image_loader/dm/load_images_dm.h index 39e39387..877df87f 100644 --- a/include/image_loader/dm/load_images_dm.h +++ b/include/image_loader/dm/load_images_dm.h @@ -5,8 +5,8 @@ #include "embedded_assets/embedded_image.h" namespace bongocat::animation { - struct animation_context_t; - bongocat_error_t init_dm_anim(animation_context_t& ctx, int anim_index, const assets::embedded_image_t& sprite_sheet_image, int sprite_sheet_cols, int sprite_sheet_rows); + struct animation_thread_context_t; + bongocat_error_t init_dm_anim(animation_thread_context_t& ctx, int anim_index, const assets::embedded_image_t& sprite_sheet_image, int sprite_sheet_cols, int sprite_sheet_rows); - [[nodiscard]] created_result_t load_dm_sprite_sheet(const animation_context_t& ctx, int index); + [[nodiscard]] created_result_t load_dm_sprite_sheet(const animation_thread_context_t& ctx, int index); } diff --git a/include/image_loader/dm20/load_images_dm20.h b/include/image_loader/dm20/load_images_dm20.h index c760c8f0..c631a360 100644 --- a/include/image_loader/dm20/load_images_dm20.h +++ b/include/image_loader/dm20/load_images_dm20.h @@ -5,8 +5,8 @@ #include "embedded_assets/embedded_image.h" namespace bongocat::animation { - struct animation_context_t; - bongocat_error_t init_dm20_anim(animation_context_t& ctx, int anim_index, const assets::embedded_image_t& sprite_sheet_image, int sprite_sheet_cols, int sprite_sheet_rows); + struct animation_thread_context_t; + bongocat_error_t init_dm20_anim(animation_thread_context_t& ctx, int anim_index, const assets::embedded_image_t& sprite_sheet_image, int sprite_sheet_cols, int sprite_sheet_rows); - [[nodiscard]] created_result_t load_dm20_sprite_sheet(const animation_context_t& ctx, int index); + [[nodiscard]] created_result_t load_dm20_sprite_sheet(const animation_thread_context_t& ctx, int index); } diff --git a/include/image_loader/dmall/load_images_dmall.h b/include/image_loader/dmall/load_images_dmall.h index 99adada1..c36108af 100644 --- a/include/image_loader/dmall/load_images_dmall.h +++ b/include/image_loader/dmall/load_images_dmall.h @@ -5,8 +5,8 @@ #include "embedded_assets/embedded_image.h" namespace bongocat::animation { - struct animation_context_t; - bongocat_error_t init_dmall_anim(animation_context_t& ctx, int anim_index, const assets::embedded_image_t& sprite_sheet_image, int sprite_sheet_cols, int sprite_sheet_rows); + struct animation_thread_context_t; + bongocat_error_t init_dmall_anim(animation_thread_context_t& ctx, int anim_index, const assets::embedded_image_t& sprite_sheet_image, int sprite_sheet_cols, int sprite_sheet_rows); - [[nodiscard]] created_result_t load_dmall_sprite_sheet(const animation_context_t& ctx, int index); + [[nodiscard]] created_result_t load_dmall_sprite_sheet(const animation_thread_context_t& ctx, int index); } diff --git a/include/image_loader/dmc/load_images_dmc.h b/include/image_loader/dmc/load_images_dmc.h index 5cdda86c..85cadb72 100644 --- a/include/image_loader/dmc/load_images_dmc.h +++ b/include/image_loader/dmc/load_images_dmc.h @@ -5,8 +5,8 @@ #include "embedded_assets/embedded_image.h" namespace bongocat::animation { - struct animation_context_t; - bongocat_error_t init_dmc_anim(animation_context_t& ctx, int anim_index, const assets::embedded_image_t& sprite_sheet_image, int sprite_sheet_cols, int sprite_sheet_rows); + struct animation_thread_context_t; + bongocat_error_t init_dmc_anim(animation_thread_context_t& ctx, int anim_index, const assets::embedded_image_t& sprite_sheet_image, int sprite_sheet_cols, int sprite_sheet_rows); - [[nodiscard]] created_result_t load_dmc_sprite_sheet(const animation_context_t& ctx, int index); + [[nodiscard]] created_result_t load_dmc_sprite_sheet(const animation_thread_context_t& ctx, int index); } diff --git a/include/image_loader/dmx/load_images_dmx.h b/include/image_loader/dmx/load_images_dmx.h index 497fcb63..0281a450 100644 --- a/include/image_loader/dmx/load_images_dmx.h +++ b/include/image_loader/dmx/load_images_dmx.h @@ -5,8 +5,8 @@ #include "embedded_assets/embedded_image.h" namespace bongocat::animation { - struct animation_context_t; - bongocat_error_t init_dmx_anim(animation_context_t& ctx, int anim_index, const assets::embedded_image_t& sprite_sheet_image, int sprite_sheet_cols, int sprite_sheet_rows); + struct animation_thread_context_t; + bongocat_error_t init_dmx_anim(animation_thread_context_t& ctx, int anim_index, const assets::embedded_image_t& sprite_sheet_image, int sprite_sheet_cols, int sprite_sheet_rows); - [[nodiscard]] created_result_t load_dmx_sprite_sheet(const animation_context_t& ctx, int index); + [[nodiscard]] created_result_t load_dmx_sprite_sheet(const animation_thread_context_t& ctx, int index); } diff --git a/include/image_loader/load_images.h b/include/image_loader/load_images.h index c08324cd..bdc7fcf1 100644 --- a/include/image_loader/load_images.h +++ b/include/image_loader/load_images.h @@ -1,95 +1,102 @@ #ifndef BONGOCAT_EMBEDDED_LOAD_IMAGES_H #define BONGOCAT_EMBEDDED_LOAD_IMAGES_H -#include "graphics/sprite_sheet.h" -#include "core/bongocat.h" #include "config/config.h" -#include "utils/memory.h" +#include "core/bongocat.h" #include "embedded_assets/embedded_image.h" +#include "graphics/sprite_sheet.h" +#include "utils/memory.h" + #include namespace bongocat::animation { - // ============================================================================= - // IMAGE LOADING MODULE - // ============================================================================= - - struct Image; - created_result_t load_image(const unsigned char *data, size_t size, int desired_channels = RGBA_CHANNELS); - void cleanup_image(Image& image); - void init_image_loader(); - - struct Image { - unsigned char *pixels{nullptr}; - int width{0}; - int height{0}; - int channels{0}; - - Image() = default; - ~Image() { - cleanup_image(*this); - } - - Image(const Image& other) - : width(other.width), height(other.height), channels(other.channels) - { - assert(width >= 0); - assert(height >= 0); - assert(channels >= 0); - const size_t data_size = static_cast(width) * static_cast(height) * static_cast(channels); - pixels = static_cast(::malloc(data_size)); - ::memcpy(pixels, other.pixels, data_size); - } - Image& operator=(const Image& other) { - if (this == &other) return *this; - - cleanup_image(*this); - - width = other.width; - height = other.height; - channels = other.channels; - - assert(width >= 0); - assert(height >= 0); - assert(channels >= 0); - const size_t data_size = static_cast(width) * static_cast(height) * static_cast(channels); - pixels = static_cast(::malloc(data_size)); - ::memcpy(pixels, other.pixels, data_size); - - return *this; - } - - Image(Image&& other) noexcept - : pixels(other.pixels), width(other.width), height(other.height), channels(other.channels) - { - other.pixels = nullptr; - other.width = 0; - other.height = 0; - other.channels = 0; - } - Image& operator=(Image&& other) noexcept { - if (this == &other) return *this; - - cleanup_image(*this); - - pixels = other.pixels; - width = other.width; - height = other.height; - channels = other.channels; - - other.pixels = nullptr; - other.width = 0; - other.height = 0; - other.channels = 0; - - return *this; - } - }; - - using get_sprite_callback_t = assets::embedded_image_t (*)(size_t); - - struct animation_context_t; - [[nodiscard]] created_result_t anim_sprite_sheet_from_embedded_images(get_sprite_callback_t get_sprite, size_t embedded_images_count); - [[nodiscard]] created_result_t load_sprite_sheet_anim(const config::config_t& config, const assets::embedded_image_t& sprite_sheet_image, int sprite_sheet_cols, int sprite_sheet_rows); -} +// ============================================================================= +// IMAGE LOADING MODULE +// ============================================================================= + +class Image; +created_result_t load_image(const unsigned char *data, size_t size, int desired_channels = RGBA_CHANNELS); +void cleanup_image(Image& image); +void init_image_loader(); + +class Image { +public: + unsigned char *pixels{BONGOCAT_NULLPTR}; + int width{0}; + int height{0}; + int channels{0}; + + Image() = default; + ~Image() { + cleanup_image(*this); + } + + Image(const Image& other) : width(other.width), height(other.height), channels(other.channels) { + assert(width >= 0); + assert(height >= 0); + assert(channels >= 0); + const size_t data_size = static_cast(width) * static_cast(height) * static_cast(channels); + pixels = static_cast(::malloc(data_size)); + ::memcpy(pixels, other.pixels, data_size); + } + Image& operator=(const Image& other) { + if (this == &other) { + return *this; + } + + cleanup_image(*this); + + width = other.width; + height = other.height; + channels = other.channels; + + assert(width >= 0); + assert(height >= 0); + assert(channels >= 0); + const size_t data_size = static_cast(width) * static_cast(height) * static_cast(channels); + pixels = static_cast(::malloc(data_size)); + ::memcpy(pixels, other.pixels, data_size); + + return *this; + } + + Image(Image&& other) noexcept + : pixels(other.pixels), width(other.width), height(other.height), channels(other.channels) { + other.pixels = BONGOCAT_NULLPTR; + other.width = 0; + other.height = 0; + other.channels = 0; + } + Image& operator=(Image&& other) noexcept { + if (this == &other) { + return *this; + } + + cleanup_image(*this); + + pixels = other.pixels; + width = other.width; + height = other.height; + channels = other.channels; + + other.pixels = BONGOCAT_NULLPTR; + other.width = 0; + other.height = 0; + other.channels = 0; + + return *this; + } +}; + +using get_sprite_callback_t = assets::embedded_image_t (*)(size_t); + +struct animation_thread_context_t; +BONGOCAT_NODISCARD created_result_t +anim_sprite_sheet_from_embedded_images(get_sprite_callback_t get_sprite, size_t embedded_images_count); + +BONGOCAT_NODISCARD created_result_t +load_sprite_sheet_anim(const config::config_t& config, const assets::embedded_image_t& sprite_sheet_image, + int sprite_sheet_cols, int sprite_sheet_rows); +} // namespace bongocat::animation #endif \ No newline at end of file diff --git a/include/image_loader/min_dm/load_images_min_dm.h b/include/image_loader/min_dm/load_images_min_dm.h index fc9f6c22..a64c5e20 100644 --- a/include/image_loader/min_dm/load_images_min_dm.h +++ b/include/image_loader/min_dm/load_images_min_dm.h @@ -1,12 +1,14 @@ #pragma once #include "core/bongocat.h" -#include "graphics/sprite_sheet.h" #include "embedded_assets/embedded_image.h" +#include "graphics/sprite_sheet.h" namespace bongocat::animation { - struct animation_context_t; - bongocat_error_t init_min_dm_anim(animation_context_t& ctx, int anim_index, const assets::embedded_image_t& sprite_sheet_image, int sprite_sheet_cols, int sprite_sheet_rows); +struct animation_thread_context_t; +bongocat_error_t init_min_dm_anim(animation_thread_context_t& ctx, int anim_index, + const assets::embedded_image_t& sprite_sheet_image, int sprite_sheet_cols, + int sprite_sheet_rows); - [[nodiscard]] created_result_t load_min_dm_sprite_sheet(const animation_context_t& ctx, int index); -} +BONGOCAT_NODISCARD created_result_t load_min_dm_sprite_sheet(const animation_thread_context_t& ctx, int index); +} // namespace bongocat::animation diff --git a/include/image_loader/misc/load_images_misc.h b/include/image_loader/misc/load_images_misc.h index cc8b13c4..b1c9122e 100644 --- a/include/image_loader/misc/load_images_misc.h +++ b/include/image_loader/misc/load_images_misc.h @@ -1,13 +1,15 @@ #pragma once #include "core/bongocat.h" -#include "graphics/sprite_sheet.h" -#include "embedded_assets/embedded_image.h" #include "embedded_assets/custom/custom_sprite.h" +#include "embedded_assets/embedded_image.h" +#include "graphics/sprite_sheet.h" namespace bongocat::animation { - struct animation_context_t; - bongocat_error_t init_misc_anim(animation_context_t& ctx, int anim_index, const assets::embedded_image_t& sprite_sheet_image, const assets::custom_animation_settings_t& sprite_sheet_settings); +struct animation_thread_context_t; +bongocat_error_t init_misc_anim(animation_thread_context_t& ctx, int anim_index, + const assets::embedded_image_t& sprite_sheet_image, + const assets::custom_animation_settings_t& sprite_sheet_settings); - [[nodiscard]] created_result_t load_misc_sprite_sheet(const animation_context_t& ctx, int index); -} +BONGOCAT_NODISCARD created_result_t load_misc_sprite_sheet(const animation_thread_context_t& ctx, int index); +} // namespace bongocat::animation diff --git a/include/image_loader/ms_agent/load_images_ms_agent.h b/include/image_loader/ms_agent/load_images_ms_agent.h index 776341b4..2f66d952 100644 --- a/include/image_loader/ms_agent/load_images_ms_agent.h +++ b/include/image_loader/ms_agent/load_images_ms_agent.h @@ -1,15 +1,24 @@ #pragma once #include "core/bongocat.h" -#include "graphics/sprite_sheet.h" #include "embedded_assets/embedded_image.h" #include "embedded_assets/ms_agent/ms_agent_sprite.h" +#include "graphics/sprite_sheet.h" namespace bongocat::animation { - struct animation_context_t; - [[nodiscard]] created_result_t load_ms_agent_sprite_sheet(const config::config_t& config, const assets::embedded_image_t& sprite_sheet_image, int sprite_sheet_cols, int sprite_sheet_rows); - [[nodiscard]] created_result_t load_ms_agent_anim(const animation_context_t& ctx, int anim_index, const assets::embedded_image_t& sprite_sheet_image, int sprite_sheet_cols, int sprite_sheet_rows, const assets::ms_agent_animation_indices_t& animation_data); - created_result_t init_ms_agent_anim(animation_context_t& ctx, int anim_index, const assets::embedded_image_t& sprite_sheet_image, int sprite_sheet_cols, int sprite_sheet_rows, const assets::ms_agent_animation_indices_t& animation_data); +struct animation_thread_context_t; +BONGOCAT_NODISCARD created_result_t +load_ms_agent_sprite_sheet(const config::config_t& config, const assets::embedded_image_t& sprite_sheet_image, + int sprite_sheet_cols, int sprite_sheet_rows); +BONGOCAT_NODISCARD created_result_t +load_ms_agent_anim(const animation_thread_context_t& ctx, int anim_index, const assets::embedded_image_t& sprite_sheet_image, + int sprite_sheet_cols, int sprite_sheet_rows, + const assets::ms_agent_animation_indices_t& animation_data); +created_result_t +init_ms_agent_anim(animation_thread_context_t& ctx, int anim_index, const assets::embedded_image_t& sprite_sheet_image, + int sprite_sheet_cols, int sprite_sheet_rows, + const assets::ms_agent_animation_indices_t& animation_data); - [[nodiscard]] created_result_t load_ms_agent_sprite_sheet(const animation_context_t& ctx, int index); -} +BONGOCAT_NODISCARD created_result_t load_ms_agent_sprite_sheet(const animation_thread_context_t& ctx, + int index); +} // namespace bongocat::animation diff --git a/include/image_loader/pen/load_images_pen.h b/include/image_loader/pen/load_images_pen.h index 94dd7217..4eadf5e5 100644 --- a/include/image_loader/pen/load_images_pen.h +++ b/include/image_loader/pen/load_images_pen.h @@ -5,8 +5,8 @@ #include "embedded_assets/embedded_image.h" namespace bongocat::animation { - struct animation_context_t; - bongocat_error_t init_pen_anim(animation_context_t& ctx, int anim_index, const assets::embedded_image_t& sprite_sheet_image, int sprite_sheet_cols, int sprite_sheet_rows); + struct animation_thread_context_t; + bongocat_error_t init_pen_anim(animation_thread_context_t& ctx, int anim_index, const assets::embedded_image_t& sprite_sheet_image, int sprite_sheet_cols, int sprite_sheet_rows); - [[nodiscard]] created_result_t load_pen_sprite_sheet(const animation_context_t& ctx, int index); + [[nodiscard]] created_result_t load_pen_sprite_sheet(const animation_thread_context_t& ctx, int index); } diff --git a/include/image_loader/pen20/load_images_pen20.h b/include/image_loader/pen20/load_images_pen20.h index 70156954..61cf255a 100644 --- a/include/image_loader/pen20/load_images_pen20.h +++ b/include/image_loader/pen20/load_images_pen20.h @@ -5,8 +5,8 @@ #include "embedded_assets/embedded_image.h" namespace bongocat::animation { - struct animation_context_t; - bongocat_error_t init_pen20_anim(animation_context_t& ctx, int anim_index, const assets::embedded_image_t& sprite_sheet_image, int sprite_sheet_cols, int sprite_sheet_rows); + struct animation_thread_context_t; + bongocat_error_t init_pen20_anim(animation_thread_context_t& ctx, int anim_index, const assets::embedded_image_t& sprite_sheet_image, int sprite_sheet_cols, int sprite_sheet_rows); - [[nodiscard]] created_result_t load_pen20_sprite_sheet(const animation_context_t& ctx, int index); + [[nodiscard]] created_result_t load_pen20_sprite_sheet(const animation_thread_context_t& ctx, int index); } diff --git a/include/image_loader/pkmn/load_images_pkmn.h b/include/image_loader/pkmn/load_images_pkmn.h index 93d0251a..cc61f4d5 100644 --- a/include/image_loader/pkmn/load_images_pkmn.h +++ b/include/image_loader/pkmn/load_images_pkmn.h @@ -5,10 +5,10 @@ #include "embedded_assets/embedded_image.h" namespace bongocat::animation { - struct animation_context_t; - [[nodiscard]] created_result_t load_pkmn_anim(const animation_context_t& ctx, int anim_index, const assets::embedded_image_t& sprite_sheet_image, int sprite_sheet_cols, int sprite_sheet_rows); + struct animation_thread_context_t; + [[nodiscard]] created_result_t load_pkmn_anim(const animation_thread_context_t& ctx, int anim_index, const assets::embedded_image_t& sprite_sheet_image, int sprite_sheet_cols, int sprite_sheet_rows); - bongocat_error_t init_pkmn_anim(animation_context_t& ctx, int anim_index, const assets::embedded_image_t& sprite_sheet_image, int sprite_sheet_cols, int sprite_sheet_rows); + bongocat_error_t init_pkmn_anim(animation_thread_context_t& ctx, int anim_index, const assets::embedded_image_t& sprite_sheet_image, int sprite_sheet_cols, int sprite_sheet_rows); - [[nodiscard]] created_result_t load_pkmn_sprite_sheet(const animation_context_t& ctx, int index); + [[nodiscard]] created_result_t load_pkmn_sprite_sheet(const animation_thread_context_t& ctx, int index); } diff --git a/include/image_loader/pmd/load_images_pmd.h b/include/image_loader/pmd/load_images_pmd.h index 43946875..88d2be18 100644 --- a/include/image_loader/pmd/load_images_pmd.h +++ b/include/image_loader/pmd/load_images_pmd.h @@ -6,8 +6,8 @@ #include "embedded_assets/custom/custom_sprite.h" namespace bongocat::animation { - struct animation_context_t; - bongocat_error_t init_pmd_anim(animation_context_t& ctx, int anim_index, const assets::embedded_image_t& sprite_sheet_image, const assets::custom_animation_settings_t& sprite_sheet_settings); + struct animation_thread_context_t; + bongocat_error_t init_pmd_anim(animation_thread_context_t& ctx, int anim_index, const assets::embedded_image_t& sprite_sheet_image, const assets::custom_animation_settings_t& sprite_sheet_settings); - [[nodiscard]] created_result_t load_pmd_sprite_sheet(const animation_context_t& ctx, int index); + [[nodiscard]] created_result_t load_pmd_sprite_sheet(const animation_thread_context_t& ctx, int index); } diff --git a/include/platform/global_wayland_session.h b/include/platform/global_wayland_session.h deleted file mode 100644 index 433f2257..00000000 --- a/include/platform/global_wayland_session.h +++ /dev/null @@ -1,162 +0,0 @@ -#ifndef BONGOCAT_GLOBAL_WAYLAND_CONTEXT_CONTEXT_H -#define BONGOCAT_GLOBAL_WAYLAND_CONTEXT_CONTEXT_H - -#include "platform/wayland-protocols.hpp" - -#include "wayland_context.h" -#include "graphics/animation_context.h" -#include "graphics/global_animation_session.h" -#include - -namespace bongocat::platform::wayland { - // max. windows to track for fullscreen detection - inline static constexpr size_t MAX_TOP_LEVELS = 128; - inline static constexpr size_t MAX_OUTPUTS = 8; // Maximum monitor outputs to store - inline static constexpr size_t OUTPUT_NAME_SIZE = 128; - - // ============================================================================= - // FULLSCREEN DETECTION MODULE - // ============================================================================= - - struct fullscreen_detector_t { - struct zwlr_foreign_toplevel_manager_v1 *manager{nullptr}; - bool has_fullscreen_toplevel{false}; - timeval last_check{}; - }; - struct tracked_toplevel_t { - struct zwlr_foreign_toplevel_handle_v1 *handle{nullptr}; - wl_output *output{nullptr}; - bool is_fullscreen{false}; - }; - - // ============================================================================= - // SCREEN DIMENSION MANAGEMENT - // ============================================================================= - - enum class screen_info_received_flags_t : uint32_t { - None = (1u << 0), - Mode = (1u << 1), - Geometry = (1u << 2), - }; - struct screen_info_t { - struct wl_output *wl_output{nullptr}; // ref of output - int screen_width{0}; - int screen_height{0}; - int transform{0}; - int raw_width{0}; - int raw_height{0}; - screen_info_received_flags_t received{screen_info_received_flags_t::None}; - }; - - enum class output_ref_received_flags_t : uint32_t { - None = (1u << 0), - Name = (1u << 1), - LogicalPosition = (1u << 2), - LogicalSize = (1u << 3), - }; - // Output monitor reference structure - struct output_ref_t { - struct wl_output *wl_output{nullptr}; - zxdg_output_v1 *xdg_output{nullptr}; - uint32_t name{0}; // Registry name - char name_str[OUTPUT_NAME_SIZE]{}; // From xdg-output - int32_t x{0}; - int32_t y{0}; - int32_t width{0}; - int32_t height{0}; - output_ref_received_flags_t received{output_ref_received_flags_t::None}; - // monitor ID in Hyprland - int64_t hypr_id{-1}; - }; - - struct wayland_session_t; - void cleanup_wayland(wayland_session_t& ctx); - - struct wayland_session_t { - wayland_context_t wayland_context; - animation::animation_session_t *animation_trigger_context{nullptr}; - - tracked_toplevel_t tracked_toplevels[MAX_TOP_LEVELS]; - size_t num_toplevels{0}; - - output_ref_t outputs[MAX_OUTPUTS]; - size_t output_count{0}; - zxdg_output_manager_v1 *xdg_output_manager{nullptr}; - - fullscreen_detector_t fs_detector; - - screen_info_t screen_infos[MAX_OUTPUTS]; - atomic_bool ready{false}; - - - - wayland_session_t() { - for (size_t i = 0; i < MAX_TOP_LEVELS; i++) { - tracked_toplevels[i] = {}; - } - for (size_t i = 0; i < MAX_OUTPUTS; i++) { - outputs[i] = {}; - } - } - ~wayland_session_t() { - cleanup_wayland(*this); - } - - wayland_session_t(const wayland_session_t&) = delete; - wayland_session_t& operator=(const wayland_session_t&) = delete; - wayland_session_t(wayland_session_t&& other) noexcept = delete; - wayland_session_t& operator=(wayland_session_t&& other) noexcept = delete; - }; - - inline void cleanup_wayland(wayland_session_t& ctx) { - atomic_store(&ctx.ready, false); - - // First destroy xdg_output objects - for (size_t i = 0; i < ctx.output_count; ++i) { - if (ctx.outputs[i].xdg_output) { - zxdg_output_v1_destroy(ctx.outputs[i].xdg_output); - ctx.outputs[i].xdg_output = nullptr; - } - } - - // Then destroy the manager - if (ctx.xdg_output_manager) { - zxdg_output_manager_v1_destroy(ctx.xdg_output_manager); - ctx.xdg_output_manager = nullptr; - } - - // Finally destroy wl_output objects - for (size_t i = 0; i < ctx.output_count; ++i) { - if (ctx.outputs[i].wl_output) { - wl_output_destroy(ctx.outputs[i].wl_output); - ctx.outputs[i].wl_output = nullptr; - } - ctx.outputs[i] = {}; - ctx.outputs[i].wl_output = nullptr; - } - ctx.output_count = 0; - - if (ctx.fs_detector.manager) { - zwlr_foreign_toplevel_manager_v1_destroy(ctx.fs_detector.manager); - ctx.fs_detector.manager = nullptr; - } - - for (size_t i = 0; i < ctx.num_toplevels; ++i) { - if (ctx.tracked_toplevels[i].handle) zwlr_foreign_toplevel_handle_v1_destroy(ctx.tracked_toplevels[i].handle); - ctx.tracked_toplevels[i] = {}; - } - ctx.num_toplevels = 0; - - ctx.fs_detector = {}; - for (size_t i = 0; i < MAX_OUTPUTS; ++i) { - ctx.screen_infos[i] = {}; - } - - // clean up wayland context - cleanup_wayland_context(ctx.wayland_context); - - ctx.animation_trigger_context = nullptr; - } -} - -#endif diff --git a/include/platform/input.h b/include/platform/input.h index dae6259c..b1696303 100644 --- a/include/platform/input.h +++ b/include/platform/input.h @@ -1,17 +1,30 @@ #ifndef BONGOCAT_INPUT_H #define BONGOCAT_INPUT_H -#include "input_context.h" #include "config/config.h" -#include "graphics/global_animation_session.h" +#include "graphics/animation_context.h" +#include "input_context.h" #include "utils/error.h" namespace bongocat::platform::input { - [[nodiscard]] created_result_t> create(const config::config_t& config); - [[nodiscard]] bongocat_error_t start(input_context_t& input, animation::animation_session_t& trigger_ctx, const config::config_t& config, CondVariable& configs_reloaded_cond, atomic_uint64_t& config_generation); - [[nodiscard]] bongocat_error_t restart(input_context_t& input, animation::animation_session_t& trigger_ctx, const config::config_t& config, CondVariable& configs_reloaded_cond, atomic_uint64_t& config_generation); - void trigger_update_config(input_context_t& ctx, const config::config_t& config, uint64_t config_generation); - void update_config(input_context_t& ctx, const config::config_t& config, uint64_t new_gen); -} -#endif // BONGOCAT_INPUT_H \ No newline at end of file +// ============================================================================= +// INPUT MONITORING FUNCTIONS +// ============================================================================= + +BONGOCAT_NODISCARD created_result_t> create(const config::config_t& config); + +// Start input monitoring - must be checked +BONGOCAT_NODISCARD bongocat_error_t start(input_context_t& input, animation::animation_context_t& animation_ctx, + const config::config_t& config, CondVariable& configs_reloaded_cond, + atomic_uint64_t& config_generation); + +// Restart input monitoring with new devices - must be checked +BONGOCAT_NODISCARD bongocat_error_t restart(input_context_t& input, animation::animation_context_t& animation_ctx, + const config::config_t& config, CondVariable& configs_reloaded_cond, + atomic_uint64_t& config_generation); +void trigger_update_config(input_context_t& ctx, const config::config_t& config, uint64_t config_generation); +void update_config(input_context_t& ctx, const config::config_t& config, uint64_t new_gen); +} // namespace bongocat::platform::input + +#endif // BONGOCAT_INPUT_H \ No newline at end of file diff --git a/include/platform/input_context.h b/include/platform/input_context.h index ae65d44b..84ee0d4e 100644 --- a/include/platform/input_context.h +++ b/include/platform/input_context.h @@ -5,149 +5,163 @@ #include "input_shared_memory.h" #include "utils/system_memory.h" #include "utils/time.h" + +#include #include #include -#include namespace bongocat::platform::input { - enum class input_unique_file_type_t : uint8_t { - NONE, - File, - Symlink, - }; - struct input_unique_file_t; - void cleanup(input_unique_file_t& file); - struct input_unique_file_t { - const char* _device_path{nullptr}; // original string from config (ref to input_context_t._device_paths[i]) - char* canonical_path{nullptr}; // resolved real path (malloc'd) - FileDescriptor fd; - input_unique_file_type_t type{input_unique_file_type_t::NONE}; - - input_unique_file_t() = default; - ~input_unique_file_t() { - cleanup(*this); - } - - input_unique_file_t(const input_unique_file_t& other) = delete; - input_unique_file_t& operator=(const input_unique_file_t& other) = delete; - - input_unique_file_t(input_unique_file_t&& other) noexcept - : _device_path(other._device_path), - canonical_path(other.canonical_path), - fd(bongocat::move(other.fd)), - type(other.type) - { - other._device_path = nullptr; - other.canonical_path = nullptr; - other.type = input_unique_file_type_t::NONE; - } - input_unique_file_t& operator=(input_unique_file_t&& other) noexcept { - if (this != &other) { - cleanup(*this); - - _device_path = other._device_path; - canonical_path = other.canonical_path; - fd = bongocat::move(other.fd); - type = other.type; - - other._device_path = nullptr; - other.canonical_path = nullptr; - other.type = input_unique_file_type_t::NONE; - } - return *this; - } - }; - inline void cleanup(input_unique_file_t& file) { - close_fd(file.fd); - file._device_path = nullptr; - if (file.canonical_path) ::free(file.canonical_path); - file.canonical_path = nullptr; - file.type = input_unique_file_type_t::NONE; +enum class input_unique_file_type_t : uint8_t { + NONE, + File, + Symlink, +}; +struct input_unique_file_t; +void cleanup(input_unique_file_t& file); +struct input_unique_file_t { + const char *_device_path{BONGOCAT_NULLPTR}; // original string from config (ref to input_context_t._device_paths[i]) + char *canonical_path{BONGOCAT_NULLPTR}; // resolved real path (malloc'd) + FileDescriptor fd; + input_unique_file_type_t type{input_unique_file_type_t::NONE}; + + input_unique_file_t() = default; + ~input_unique_file_t() { + cleanup(*this); + } + + input_unique_file_t(const input_unique_file_t& other) = delete; + input_unique_file_t& operator=(const input_unique_file_t& other) = delete; + + input_unique_file_t(input_unique_file_t&& other) noexcept + : _device_path(other._device_path) + , canonical_path(other.canonical_path) + , fd(bongocat::move(other.fd)) + , type(other.type) { + other._device_path = BONGOCAT_NULLPTR; + other.canonical_path = BONGOCAT_NULLPTR; + other.type = input_unique_file_type_t::NONE; + } + input_unique_file_t& operator=(input_unique_file_t&& other) noexcept { + if (this != &other) { + cleanup(*this); + + _device_path = other._device_path; + canonical_path = other.canonical_path; + fd = bongocat::move(other.fd); + type = other.type; + + other._device_path = BONGOCAT_NULLPTR; + other.canonical_path = BONGOCAT_NULLPTR; + other.type = input_unique_file_type_t::NONE; } + return *this; + } +}; +inline void cleanup(input_unique_file_t& file) { + close_fd(file.fd); + file._device_path = BONGOCAT_NULLPTR; + if (file.canonical_path != BONGOCAT_NULLPTR) { + ::free(file.canonical_path); + file.canonical_path = BONGOCAT_NULLPTR; + } + file.type = input_unique_file_type_t::NONE; +} - struct input_context_t; - void stop(input_context_t& ctx); - void cleanup(input_context_t& ctx); - - struct input_context_t { - // local copy from other thread, update after reload (shared memory) - MMapMemory _local_copy_config; - MMapMemory shm; - - atomic_bool _capture_input_running{false}; - pthread_t _input_thread{0}; - atomic_int _input_kpm_counter{0}; - timestamp_ms_t _latest_kpm_update_ms{0}; - // lock for shm - Mutex input_lock; - - // thread context - AllocatedArray _device_paths; // local copy of devices (from config) - AllocatedArray _unique_paths_indices; - size_t _unique_paths_indices_capacity{0}; // keep real _unique_paths_indices count here, shrink _unique_paths_indices.count to used unique_paths_indices - AllocatedArray _unique_devices; - /// udev monitoring - udev *_udev{nullptr}; - udev_monitor *_udev_mon{nullptr}; - int _udev_fd{-1}; - - // config reload threading - FileDescriptor update_config_efd; // get new_gen from here - atomic_uint64_t config_seen_generation{0}; - platform::CondVariable config_updated; - - // globals (references) - const config::config_t *_config{nullptr}; - platform::CondVariable *_configs_reloaded_cond{nullptr}; - atomic_uint64_t* _config_generation{nullptr}; - atomic_bool ready; - platform::CondVariable init_cond; - - input_context_t() = default; - ~input_context_t() { - cleanup(*this); - } - - input_context_t(const input_context_t&) = delete; - input_context_t& operator=(const input_context_t&) = delete; - input_context_t(input_context_t&& other) noexcept = delete; - input_context_t& operator=(input_context_t&& other) noexcept = delete; - }; - inline void cleanup(input_context_t& ctx) { - if (atomic_load(&ctx._capture_input_running)) { - stop(ctx); - // input_lock should be unlocked - } - atomic_store(&ctx._capture_input_running, false); - ctx._input_thread = 0; - - release_allocated_array(ctx._unique_devices); - ctx._unique_paths_indices_capacity = 0; - release_allocated_array(ctx._unique_paths_indices); - for (size_t i = 0; i < ctx._device_paths.count; i++) { - if (ctx._device_paths[i]) ::free(ctx._device_paths[i]); - ctx._device_paths[i] = nullptr; - } - release_allocated_array(ctx._device_paths); - - if (ctx._udev_mon) udev_monitor_unref(ctx._udev_mon); - if (ctx._udev) udev_unref(ctx._udev); - ctx._udev_mon = nullptr; - ctx._udev = nullptr; - ctx._udev_fd = -1; - - close_fd(ctx.update_config_efd); - atomic_store(&ctx.config_seen_generation, 0); - - release_allocated_mmap_memory(ctx._local_copy_config); - release_allocated_mmap_memory(ctx.shm); - - ctx._config = nullptr; - ctx._configs_reloaded_cond = nullptr; - ctx._config_generation = nullptr; - atomic_store(&ctx.ready, false); - ctx.init_cond.notify_all(); +struct input_context_t; +void stop(input_context_t& ctx); +// Cleanup input monitoring resources +void cleanup(input_context_t& ctx); + +// ============================================================================= +// INPUT STATE +// ============================================================================= + +struct input_context_t { + // local copy from other thread, update after reload (shared memory) + MMapMemory _local_copy_config; + MMapMemory shm; + + atomic_bool _capture_input_running{false}; + pthread_t _input_thread{0}; + atomic_int _input_kpm_counter{0}; + timestamp_ms_t _latest_kpm_update_ms{0}; + // lock for shm + Mutex input_lock; + + // thread context + AllocatedArray _device_paths; // local copy of devices (from config) + AllocatedArray _unique_paths_indices; + size_t _unique_paths_indices_capacity{0}; // keep real _unique_paths_indices count here, shrink + // _unique_paths_indices.count to used unique_paths_indices + AllocatedArray _unique_devices; + /// udev monitoring + udev *_udev{BONGOCAT_NULLPTR}; + udev_monitor *_udev_mon{BONGOCAT_NULLPTR}; + int _udev_fd{-1}; + + // config reload threading + FileDescriptor update_config_efd; // get new_gen from here + atomic_uint64_t config_seen_generation{0}; + platform::CondVariable config_updated; + + // globals (references) + const config::config_t *_config{BONGOCAT_NULLPTR}; + platform::CondVariable *_configs_reloaded_cond{BONGOCAT_NULLPTR}; + atomic_uint64_t *_config_generation{BONGOCAT_NULLPTR}; + atomic_bool ready; + platform::CondVariable init_cond; + + input_context_t() = default; + ~input_context_t() { + cleanup(*this); + } + + input_context_t(const input_context_t&) = delete; + input_context_t& operator=(const input_context_t&) = delete; + input_context_t(input_context_t&& other) noexcept = delete; + input_context_t& operator=(input_context_t&& other) noexcept = delete; +}; +inline void cleanup(input_context_t& ctx) { + if (atomic_load(&ctx._capture_input_running)) { + stop(ctx); + // input_lock should be unlocked + } + atomic_store(&ctx._capture_input_running, false); + ctx._input_thread = 0; + + release_allocated_array(ctx._unique_devices); + ctx._unique_paths_indices_capacity = 0; + release_allocated_array(ctx._unique_paths_indices); + for (size_t i = 0; i < ctx._device_paths.count; i++) { + if (ctx._device_paths[i] != BONGOCAT_NULLPTR) { + ::free(ctx._device_paths[i]); + ctx._device_paths[i] = BONGOCAT_NULLPTR; } + } + release_allocated_array(ctx._device_paths); + + if (ctx._udev_mon != BONGOCAT_NULLPTR) { + udev_monitor_unref(ctx._udev_mon); + ctx._udev_mon = BONGOCAT_NULLPTR; + } + if (ctx._udev != BONGOCAT_NULLPTR) { + udev_unref(ctx._udev); + ctx._udev = BONGOCAT_NULLPTR; + } + ctx._udev_fd = -1; + + close_fd(ctx.update_config_efd); + atomic_store(&ctx.config_seen_generation, 0); + + release_allocated_mmap_memory(ctx._local_copy_config); + release_allocated_mmap_memory(ctx.shm); + + ctx._config = BONGOCAT_NULLPTR; + ctx._configs_reloaded_cond = BONGOCAT_NULLPTR; + ctx._config_generation = BONGOCAT_NULLPTR; + atomic_store(&ctx.ready, false); + ctx.init_cond.notify_all(); } +} // namespace bongocat::platform::input -#endif // BONGOCAT_INPUT_CONTEXT_H \ No newline at end of file +#endif // BONGOCAT_INPUT_CONTEXT_H \ No newline at end of file diff --git a/include/platform/input_shared_memory.h b/include/platform/input_shared_memory.h index 73753eeb..d2976d7f 100644 --- a/include/platform/input_shared_memory.h +++ b/include/platform/input_shared_memory.h @@ -2,59 +2,78 @@ #define BONGOCAT_INPUT_SHARED_MEMORY_H #include "utils/time.h" + #include namespace bongocat::platform::input { - struct input_shared_memory_t { - int32_t any_key_pressed{0}; - int32_t kpm{0}; // keystrokes per minute - atomic_int input_counter{0}; - timestamp_ms_t last_key_pressed_timestamp{0}; - - - - input_shared_memory_t() = default; - ~input_shared_memory_t() = default; - - input_shared_memory_t(const input_shared_memory_t& other) - : any_key_pressed(other.any_key_pressed), - kpm(other.kpm), - input_counter(atomic_load(&other.input_counter)), - last_key_pressed_timestamp(other.last_key_pressed_timestamp) {} - input_shared_memory_t& operator=(const input_shared_memory_t& other) { - if (this != &other) { - any_key_pressed = other.any_key_pressed; - kpm = other.kpm; - atomic_store(&input_counter, atomic_load(&other.input_counter)); - last_key_pressed_timestamp = other.last_key_pressed_timestamp; - } - return *this; - } - - input_shared_memory_t(input_shared_memory_t&& other) noexcept - : any_key_pressed(other.any_key_pressed), - kpm(other.kpm), - last_key_pressed_timestamp(other.last_key_pressed_timestamp) { - atomic_store(&input_counter, atomic_load(&other.input_counter)); - - other.any_key_pressed = 0; - other.kpm = 0; - atomic_store(&other.input_counter, 0); - } - input_shared_memory_t& operator=(input_shared_memory_t&& other) noexcept { - if (this != &other) { - any_key_pressed = other.any_key_pressed; - kpm = other.kpm; - atomic_store(&input_counter, atomic_load(&other.input_counter)); - last_key_pressed_timestamp = other.last_key_pressed_timestamp; - - other.any_key_pressed = 0; - other.kpm = 0; - atomic_store(&other.input_counter, 0); - } - return *this; - } - }; -} - -#endif // BONGOCAT_INPUT_SHARED_MEMORY_H \ No newline at end of file + +// ============================================================================= +// INPUT STATE (shared memory) +// ============================================================================= + +enum class input_hand_mapping_t : int32_t { + None, + Left, + Right +}; + +struct input_shared_memory_t { + /// @DEPRECATED: not really needed anymore, use events + int32_t any_key_pressed{0}; + + int32_t kpm{0}; // keystrokes per minute + atomic_int input_counter{0}; + timestamp_ms_t last_key_pressed_timestamp{0}; + input_hand_mapping_t hand_mapping{input_hand_mapping_t::None}; + + input_shared_memory_t() = default; + ~input_shared_memory_t() = default; + + input_shared_memory_t(const input_shared_memory_t& other) + : any_key_pressed(other.any_key_pressed) + , kpm(other.kpm) + , input_counter(atomic_load(&other.input_counter)) + , last_key_pressed_timestamp(other.last_key_pressed_timestamp) + , hand_mapping(other.hand_mapping) {} + input_shared_memory_t& operator=(const input_shared_memory_t& other) { + if (this != &other) { + any_key_pressed = other.any_key_pressed; + kpm = other.kpm; + atomic_store(&input_counter, atomic_load(&other.input_counter)); + last_key_pressed_timestamp = other.last_key_pressed_timestamp; + hand_mapping = other.hand_mapping; + } + return *this; + } + + input_shared_memory_t(input_shared_memory_t&& other) noexcept + : any_key_pressed(other.any_key_pressed) + , kpm(other.kpm) + , last_key_pressed_timestamp(other.last_key_pressed_timestamp) + , hand_mapping(other.hand_mapping) { + atomic_store(&input_counter, atomic_load(&other.input_counter)); + + other.any_key_pressed = 0; + other.kpm = 0; + atomic_store(&other.input_counter, 0); + other.hand_mapping = input_hand_mapping_t::None; + } + input_shared_memory_t& operator=(input_shared_memory_t&& other) noexcept { + if (this != &other) { + any_key_pressed = other.any_key_pressed; + kpm = other.kpm; + atomic_store(&input_counter, atomic_load(&other.input_counter)); + last_key_pressed_timestamp = other.last_key_pressed_timestamp; + hand_mapping = other.hand_mapping; + + other.any_key_pressed = 0; + other.kpm = 0; + atomic_store(&other.input_counter, 0); + other.hand_mapping = input_hand_mapping_t::None; + } + return *this; + } +}; +} // namespace bongocat::platform::input + +#endif // BONGOCAT_INPUT_SHARED_MEMORY_H \ No newline at end of file diff --git a/include/platform/update.h b/include/platform/update.h index f2859c09..968f573f 100644 --- a/include/platform/update.h +++ b/include/platform/update.h @@ -1,18 +1,22 @@ #ifndef BONGOCAT_UPDATE_H #define BONGOCAT_UPDATE_H -#include "update_context.h" #include "config/config.h" -#include "graphics/global_animation_session.h" +#include "graphics/animation_context.h" +#include "update_context.h" #include "utils/error.h" namespace bongocat::platform::update { - [[nodiscard]] created_result_t> create(const config::config_t& config); - [[nodiscard]] bongocat_error_t start(update_context_t& input, animation::animation_session_t& trigger_ctx, const config::config_t& config, CondVariable& configs_reloaded_cond, atomic_uint64_t& config_generation); - [[nodiscard]] bongocat_error_t restart(update_context_t& input, animation::animation_session_t& trigger_ctx, const config::config_t& config, CondVariable& configs_reloaded_cond, atomic_uint64_t& config_generation); - void trigger_update_config(update_context_t& ctx, const config::config_t& config, uint64_t config_generation); - void update_config(update_context_t& ctx, const config::config_t& config, uint64_t new_gen); - const cpu_snapshot_t& get_latest_snapshot(update_context_t& ctx); -} +BONGOCAT_NODISCARD created_result_t> create(const config::config_t& config); +BONGOCAT_NODISCARD bongocat_error_t start(update_context_t& input, animation::animation_context_t& animation_ctx, + const config::config_t& config, CondVariable& configs_reloaded_cond, + atomic_uint64_t& config_generation); +BONGOCAT_NODISCARD bongocat_error_t restart(update_context_t& input, animation::animation_context_t& animation_ctx, + const config::config_t& config, CondVariable& configs_reloaded_cond, + atomic_uint64_t& config_generation); +void trigger_update_config(update_context_t& ctx, const config::config_t& config, uint64_t config_generation); +void update_config(update_context_t& ctx, const config::config_t& config, uint64_t new_gen); +const cpu_snapshot_t& get_latest_snapshot(update_context_t& ctx); +} // namespace bongocat::platform::update -#endif // BONGOCAT_UPDATE_H \ No newline at end of file +#endif // BONGOCAT_UPDATE_H \ No newline at end of file diff --git a/include/platform/update_context.h b/include/platform/update_context.h index b4837487..533efb43 100644 --- a/include/platform/update_context.h +++ b/include/platform/update_context.h @@ -5,74 +5,75 @@ #include "update_shared_memory.h" #include "utils/system_memory.h" #include "utils/time.h" + #include #include namespace bongocat::platform::update { - struct update_context_t; - void stop(update_context_t& ctx); - void cleanup(update_context_t& ctx); +struct update_context_t; +void stop(update_context_t& ctx); +void cleanup(update_context_t& ctx); - struct update_context_t { - // local copy from other thread, update after reload (shared memory) - MMapMemory _local_copy_config; - MMapMemory shm; +struct update_context_t { + // local copy from other thread, update after reload (shared memory) + MMapMemory _local_copy_config; + MMapMemory shm; - atomic_bool _running{false}; - pthread_t _update_thread{0}; - // lock for shm - Mutex update_lock; - platform::CondVariable update_cond; + atomic_bool _running{false}; + pthread_t _update_thread{0}; + // lock for shm + Mutex update_lock; + platform::CondVariable update_cond; - // thread context - FileDescriptor fd_stat; - FileDescriptor fd_present; + // thread context + FileDescriptor fd_stat; + FileDescriptor fd_present; - // config reload threading - FileDescriptor update_config_efd; // get new_gen from here - atomic_uint64_t config_seen_generation{0}; - platform::CondVariable config_updated; + // config reload threading + FileDescriptor update_config_efd; // get new_gen from here + atomic_uint64_t config_seen_generation{0}; + platform::CondVariable config_updated; - // globals (references) - const config::config_t *_config{nullptr}; - platform::CondVariable *_configs_reloaded_cond{nullptr}; - atomic_uint64_t* _config_generation{nullptr}; - atomic_bool ready; - platform::CondVariable init_cond; + // globals (references) + const config::config_t *_config{BONGOCAT_NULLPTR}; + platform::CondVariable *_configs_reloaded_cond{BONGOCAT_NULLPTR}; + atomic_uint64_t *_config_generation{BONGOCAT_NULLPTR}; + atomic_bool ready; + platform::CondVariable init_cond; - update_context_t() = default; - ~update_context_t() { - cleanup(*this); - } + update_context_t() = default; + ~update_context_t() { + cleanup(*this); + } - update_context_t(const update_context_t&) = delete; - update_context_t& operator=(const update_context_t&) = delete; - update_context_t(update_context_t&& other) noexcept = delete; - update_context_t& operator=(update_context_t&& other) noexcept = delete; - }; - inline void cleanup(update_context_t& ctx) { - if (atomic_load(&ctx._running)) { - stop(ctx); - // input_lock should be unlocked - } - atomic_store(&ctx._running, false); - ctx._update_thread = 0; + update_context_t(const update_context_t&) = delete; + update_context_t& operator=(const update_context_t&) = delete; + update_context_t(update_context_t&& other) noexcept = delete; + update_context_t& operator=(update_context_t&& other) noexcept = delete; +}; +inline void cleanup(update_context_t& ctx) { + if (atomic_load(&ctx._running)) { + stop(ctx); + // input_lock should be unlocked + } + atomic_store(&ctx._running, false); + ctx._update_thread = 0; - close_fd(ctx.fd_present); - close_fd(ctx.fd_stat); + close_fd(ctx.fd_present); + close_fd(ctx.fd_stat); - close_fd(ctx.update_config_efd); - atomic_store(&ctx.config_seen_generation, 0); + close_fd(ctx.update_config_efd); + atomic_store(&ctx.config_seen_generation, 0); - release_allocated_mmap_memory(ctx._local_copy_config); - release_allocated_mmap_memory(ctx.shm); + release_allocated_mmap_memory(ctx._local_copy_config); + release_allocated_mmap_memory(ctx.shm); - ctx._config = nullptr; - ctx._configs_reloaded_cond = nullptr; - ctx._config_generation = nullptr; - atomic_store(&ctx.ready, false); - ctx.init_cond.notify_all(); - } + ctx._config = BONGOCAT_NULLPTR; + ctx._configs_reloaded_cond = BONGOCAT_NULLPTR; + ctx._config_generation = BONGOCAT_NULLPTR; + atomic_store(&ctx.ready, false); + ctx.init_cond.notify_all(); } +} // namespace bongocat::platform::update -#endif // BONGOCAT_INPUT_CONTEXT_H \ No newline at end of file +#endif // BONGOCAT_INPUT_CONTEXT_H \ No newline at end of file diff --git a/include/platform/update_shared_memory.h b/include/platform/update_shared_memory.h index d8bf2965..9087f32d 100644 --- a/include/platform/update_shared_memory.h +++ b/include/platform/update_shared_memory.h @@ -5,38 +5,39 @@ #include "input_shared_memory.h" #include "utils/system_memory.h" #include "utils/time.h" + #include #include namespace bongocat::platform::update { - inline static constexpr size_t MaxCpus = 256; - inline static constexpr size_t CpuSnapshotRingBufferMaxHistory = 8; +inline static constexpr size_t MaxCpus = 256; +inline static constexpr size_t CpuSnapshotRingBufferMaxHistory = 8; - struct cpu_stat_t { - size_t idle_time{0}; - size_t total_time{0}; - }; - struct cpu_snapshot_t { - cpu_stat_t stats[MaxCpus]{}; - size_t count{0}; // how many CPUs in this snapshot - }; +struct cpu_stat_t { + size_t idle_time{0}; + size_t total_time{0}; +}; +struct cpu_snapshot_t { + cpu_stat_t stats[MaxCpus]{}; + size_t count{0}; // how many CPUs in this snapshot +}; - struct cpu_snapshot_ring_buffer_t { - // ring buffer for snapshots - cpu_snapshot_t history[CpuSnapshotRingBufferMaxHistory]{}; - size_t head{0}; // next write position - size_t stored{0}; // number of valid snapshots - }; +struct cpu_snapshot_ring_buffer_t { + // ring buffer for snapshots + cpu_snapshot_t history[CpuSnapshotRingBufferMaxHistory]{}; + size_t head{0}; // next write position + size_t stored{0}; // number of valid snapshots +}; - struct update_shared_memory_t { - cpu_snapshot_ring_buffer_t cpu_snapshots; - const cpu_snapshot_t* latest_snapshot{nullptr}; - double avg_cpu_usage{0}; - double max_cpu_usage{0}; - double last_avg_cpu_usage{0}; - double last_max_cpu_usage{0}; - bool cpu_active{false}; - }; -} +struct update_shared_memory_t { + cpu_snapshot_ring_buffer_t cpu_snapshots; + const cpu_snapshot_t *latest_snapshot{BONGOCAT_NULLPTR}; + double avg_cpu_usage{0}; + double max_cpu_usage{0}; + double last_avg_cpu_usage{0}; + double last_max_cpu_usage{0}; + bool cpu_active{false}; +}; +} // namespace bongocat::platform::update -#endif // BONGOCAT_INPUT_CONTEXT_H \ No newline at end of file +#endif // BONGOCAT_INPUT_CONTEXT_H \ No newline at end of file diff --git a/include/platform/wayland-protocols.hpp b/include/platform/wayland-protocols.hpp index 9477a11c..bbf50f62 100644 --- a/include/platform/wayland-protocols.hpp +++ b/include/platform/wayland-protocols.hpp @@ -1,27 +1,27 @@ #pragma once #ifdef __cplusplus -//#define namespace zwl_namespace -#if defined(__GNUC__) || defined(__clang__) -#pragma GCC diagnostic push -#pragma GCC diagnostic ignored "-Wdouble-promotion" -#pragma GCC diagnostic ignored "-Wsign-compare" -#pragma GCC diagnostic ignored "-Wunused-function" -#pragma GCC diagnostic ignored "-Wold-style-cast" -//#pragma GCC diagnostic ignored "-Wsign-conversion" -#endif +// #define namespace zwl_namespace +# if defined(__GNUC__) || defined(__clang__) +# pragma GCC diagnostic push +# pragma GCC diagnostic ignored "-Wdouble-promotion" +# pragma GCC diagnostic ignored "-Wsign-compare" +# pragma GCC diagnostic ignored "-Wunused-function" +# pragma GCC diagnostic ignored "-Wold-style-cast" +// #pragma GCC diagnostic ignored "-Wsign-conversion" +# endif extern "C" { -#include "../protocols/wlr-foreign-toplevel-management-v1-client-protocol.h" -#include "../protocols/xdg-output-unstable-v1-client-protocol.h" -#include "../protocols/xdg-shell-client-protocol.h" -#include "../protocols/zwlr-layer-shell-v1-client-protocol.h" +# include "../protocols/wlr-foreign-toplevel-management-v1-client-protocol.h" +# include "../protocols/xdg-output-unstable-v1-client-protocol.h" +# include "../protocols/xdg-shell-client-protocol.h" +# include "../protocols/zwlr-layer-shell-v1-client-protocol.h" } -#if defined(__GNUC__) || defined(__clang__) -#pragma GCC diagnostic pop -#endif -//#undef namespace +# if defined(__GNUC__) || defined(__clang__) +# pragma GCC diagnostic pop +# endif +// #undef namespace #else -#include "../protocols/wlr-foreign-toplevel-management-v1-client-protocol.h" -#include "../protocols/xdg-output-unstable-v1-client-protocol.h" -#include "../protocols/xdg-shell-client-protocol.h" -#include "../protocols/zwlr-layer-shell-v1-client-protocol.h" +# include "../protocols/wlr-foreign-toplevel-management-v1-client-protocol.h" +# include "../protocols/xdg-output-unstable-v1-client-protocol.h" +# include "../protocols/xdg-shell-client-protocol.h" +# include "../protocols/zwlr-layer-shell-v1-client-protocol.h" #endif \ No newline at end of file diff --git a/include/platform/wayland.h b/include/platform/wayland.h index b2a376e7..7ba3a87c 100644 --- a/include/platform/wayland.h +++ b/include/platform/wayland.h @@ -1,27 +1,44 @@ #ifndef BONGOCAT_WAYLAND_H #define BONGOCAT_WAYLAND_H -#include "config/config_watcher.h" -#include "wayland_context.h" -#include "graphics/global_animation_session.h" -#include "global_wayland_session.h" #include "config/config.h" +#include "config/config_watcher.h" +#include "graphics/animation_context.h" #include "utils/error.h" +#include "wayland_context.h" +#include "wayland_thread_context.h" + #include namespace bongocat::platform::wayland { - using config_reload_callback_t = void (*)(); +using config_reload_callback_t = void (*)(); + +// ============================================================================= +// WAYLAND LIFECYCLE FUNCTIONS +// ============================================================================= + +// Initialize Wayland connection - must be checked +BONGOCAT_NODISCARD created_result_t> +create(animation::animation_context_t& animation_ctx, const config::config_t& config); +BONGOCAT_NODISCARD bongocat_error_t setup(wayland_context_t& ctx, animation::animation_context_t& animation_ctx); + +// Run Wayland event loop - must be checked +BONGOCAT_NODISCARD bongocat_error_t run(wayland_context_t& ctx, volatile sig_atomic_t& running, int signal_fd, + input::input_context_t& input, const config::config_t& config, + const config::config_watcher_t *config_watcher, + config_reload_callback_t config_reload_callback); + +// Update configuration +void update_config(wayland_context_t& ctx, const config::config_t& config, + animation::animation_context_t& animation_ctx); - [[nodiscard]] created_result_t> create(animation::animation_session_t& anim, const config::config_t& config); - [[nodiscard]] bongocat_error_t setup(wayland_session_t& ctx, animation::animation_session_t& anim); - [[nodiscard]] bongocat_error_t run(wayland_session_t& ctx, volatile sig_atomic_t& running, int signal_fd, input::input_context_t& input, const config::config_t& config, const config::config_watcher_t* config_watcher, config_reload_callback_t config_reload_callback); - void update_config(wayland_context_t& ctx, const config::config_t& config, animation::animation_session_t& trigger_ctx); +// Get detected screen width +BONGOCAT_NODISCARD int get_screen_width(const wayland_context_t& ctx); - [[nodiscard]] FileDescriptor create_shm(off_t size); +// Get current layer name for logging +BONGOCAT_NODISCARD const char *get_current_layer_name(); - [[nodiscard]] int get_screen_width(const wayland_session_t& ctx); - [[nodiscard]] const char* get_current_layer_name(); - bongocat_error_t request_render(animation::animation_session_t& trigger_ctx); -} +bongocat_error_t request_render(animation::animation_context_t& animation_ctx); +} // namespace bongocat::platform::wayland -#endif // BONGOCAT_WAYLAND_H \ No newline at end of file +#endif // BONGOCAT_WAYLAND_H \ No newline at end of file diff --git a/include/platform/wayland_callbacks.h b/include/platform/wayland_callbacks.h index 1cdc31a2..7367200e 100644 --- a/include/platform/wayland_callbacks.h +++ b/include/platform/wayland_callbacks.h @@ -2,164 +2,144 @@ #define BONGOCAT_WAYLAND_CALLBACKS_H #include "wayland-protocols.hpp" +#include "wayland_context.h" +#include #include #include -#include namespace bongocat::platform::wayland::details { - // ============================================================================= - // ZXDG LISTENER IMPLEMENTATION - // ============================================================================= - - extern void handle_xdg_output_name(void *data, zxdg_output_v1 *xdg_output, const char *name); - - extern void handle_xdg_output_logical_position(void *data, zxdg_output_v1 *xdg_output, int32_t x, int32_t y); - extern void handle_xdg_output_logical_size(void *data, zxdg_output_v1 *xdg_output, int32_t width, int32_t height); - extern void handle_xdg_output_done(void *data, zxdg_output_v1 *xdg_output); - extern void handle_xdg_output_description(void *data, zxdg_output_v1 *xdg_output, const char *description); - - /// @NOTE: xdg_output_listeners MUST pass data as output_ref_t, see zxdg_output_v1_add_listener - inline static constexpr zxdg_output_v1_listener xdg_output_listener = { - .logical_position = handle_xdg_output_logical_position, - .logical_size = handle_xdg_output_logical_size, - .done = handle_xdg_output_done, - .name = handle_xdg_output_name, - .description = handle_xdg_output_description - }; - - - // ============================================================================= - // FULLSCREEN DETECTION IMPLEMENTATION - // ============================================================================= - - // Foreign toplevel protocol event handlers - extern void fs_handle_toplevel_state(void *data, zwlr_foreign_toplevel_handle_v1 *handle, - wl_array *state); - - extern void fs_handle_toplevel_closed(void *data, zwlr_foreign_toplevel_handle_v1 *handle); - - // Minimal event handlers for unused events - extern void fs_handle_title(void *data, zwlr_foreign_toplevel_handle_v1 *handle, const char *title); - extern void fs_handle_app_id(void *data, zwlr_foreign_toplevel_handle_v1 *handle, const char *app_id); - extern void fs_handle_output_enter(void *data, zwlr_foreign_toplevel_handle_v1 *handle, wl_output *output); - extern void fs_handle_output_leave(void *data, zwlr_foreign_toplevel_handle_v1 *handle, wl_output *output); - extern void fs_handle_done(void *data, zwlr_foreign_toplevel_handle_v1 *handle); - extern void fs_handle_parent(void *data, zwlr_foreign_toplevel_handle_v1 *handle, zwlr_foreign_toplevel_handle_v1 *parent); - - /// @NOTE: fs_toplevel_listener MUST pass data as output_ref_t, see zwlr_foreign_toplevel_handle_v1_add_listener - inline static constexpr zwlr_foreign_toplevel_handle_v1_listener fs_toplevel_listener = { - .title = fs_handle_title, - .app_id = fs_handle_app_id, - .output_enter = fs_handle_output_enter, - .output_leave = fs_handle_output_leave, - .state = fs_handle_toplevel_state, - .done = fs_handle_done, - .closed = fs_handle_toplevel_closed, - .parent = fs_handle_parent, - }; - - extern void fs_update_state_fallback(wayland_session_t& ctx); - - extern void fs_handle_manager_toplevel(void *data, zwlr_foreign_toplevel_manager_v1 *manager, - zwlr_foreign_toplevel_handle_v1 *toplevel); - extern void fs_handle_manager_finished(void *data, zwlr_foreign_toplevel_manager_v1 *manager); - - /// @NOTE: fs_manager_listeners MUST pass data as wayland_listeners_context_t, see zwlr_foreign_toplevel_manager_v1_add_listener - inline static constexpr zwlr_foreign_toplevel_manager_v1_listener fs_manager_listener = { - .toplevel = fs_handle_manager_toplevel, - .finished = fs_handle_manager_finished, - }; - - // ============================================================================= - // WAYLAND EVENT HANDLERS - // ============================================================================= - - extern void layer_surface_configure(void *data, - zwlr_layer_surface_v1 *ls, - uint32_t serial, uint32_t w, uint32_t h); - extern void layer_surface_closed(void *data, zwlr_layer_surface_v1 *ls); - - /// @NOTE: layer_listeners MUST pass data as wayland_listeners_context_t, see zwlr_layer_surface_v1_add_listener - inline static constexpr zwlr_layer_surface_v1_listener layer_listener = { - .configure = layer_surface_configure, - .closed = layer_surface_closed, - }; - - - extern void xdg_wm_base_ping(void *data, xdg_wm_base *wm_base, uint32_t serial); - - /// @NOTE: xdg_wm_base_listeners MUST pass data as wayland_listeners_context_t, see xdg_wm_base_add_listener - inline static constexpr xdg_wm_base_listener xdg_wm_base_listener = { - .ping = xdg_wm_base_ping, - }; - - extern void output_geometry(void *data, - wl_output *wl_output, - int32_t x, - int32_t y, - int32_t physical_width, - int32_t physical_height, - int32_t subpixel, - const char *make, - const char *model, - int32_t transform); - extern void output_mode(void *data , - wl_output *wl_output, - uint32_t flags, int32_t width, int32_t height, - int32_t refresh); - extern void output_done(void *data, wl_output *wl_output); - extern void output_scale(void *data, - wl_output *wl_output, - int32_t factor); - extern void output_name(void *data, wl_output *wl_output, const char *name); - extern void output_description(void *data, wl_output *wl_output, const char *name); - - /// @NOTE: output_listeners MUST pass data as wayland_listeners_context_t, see wl_output_add_listener - inline static constexpr wl_output_listener output_listener = { - .geometry = output_geometry, - .mode = output_mode, - .done = output_done, - .scale = output_scale, - .name = output_name, - .description = output_description, - }; - - // ============================================================================= - // WAYLAND PROTOCOL REGISTRY - // ============================================================================= - - extern void registry_global(void *data , wl_registry *reg, - uint32_t name, const char *iface, - uint32_t ver); - extern void registry_remove(void *data, - wl_registry *registry, - uint32_t name); - - /// @NOTE: reg_listeners MUST pass data as wayland_listeners_context_t, see zxdg_output_v1_add_listener - inline static constexpr wl_registry_listener reg_listener = { - .global = registry_global, - .global_remove = registry_remove - }; - - // ============================================================================= - // FRAME DRAWING HANDLER - // ============================================================================= - - extern void buffer_release(void *data, wl_buffer *buffer); - - /// @NOTE: buffer_listeners MUST pass data as wayland_shm_buffer_t, see wl_buffer_add_listener - inline static constexpr wl_buffer_listener buffer_listener = { - .release = buffer_release, - }; - - extern void frame_done(void *data, wl_callback *cb, uint32_t time); - - /// @NOTE: frame_listeners MUST pass data as wayland_listeners_context_t, see wl_callback_add_listener - inline static constexpr wl_callback_listener frame_listener = { - .done = frame_done - }; -} +// ============================================================================= +// ZXDG LISTENER IMPLEMENTATION +// ============================================================================= + +extern void handle_xdg_output_name(void *data, zxdg_output_v1 *xdg_output, const char *name); + +extern void handle_xdg_output_logical_position(void *data, zxdg_output_v1 *xdg_output, int32_t x, int32_t y); +extern void handle_xdg_output_logical_size(void *data, zxdg_output_v1 *xdg_output, int32_t width, int32_t height); +extern void handle_xdg_output_done(void *data, zxdg_output_v1 *xdg_output); +extern void handle_xdg_output_description(void *data, zxdg_output_v1 *xdg_output, const char *description); + +/// @NOTE: xdg_output_listeners MUST pass data as output_ref_t, see zxdg_output_v1_add_listener +inline static constexpr zxdg_output_v1_listener xdg_output_listener = {.logical_position = + handle_xdg_output_logical_position, + .logical_size = handle_xdg_output_logical_size, + .done = handle_xdg_output_done, + .name = handle_xdg_output_name, + .description = handle_xdg_output_description}; + +// ============================================================================= +// FULLSCREEN DETECTION IMPLEMENTATION +// ============================================================================= + +// Foreign toplevel protocol event handlers +extern void fs_handle_toplevel_state(void *data, zwlr_foreign_toplevel_handle_v1 *handle, wl_array *state); + +extern void fs_handle_toplevel_closed(void *data, zwlr_foreign_toplevel_handle_v1 *handle); + +// Minimal event handlers for unused events +extern void fs_handle_title(void *data, zwlr_foreign_toplevel_handle_v1 *handle, const char *title); +extern void fs_handle_app_id(void *data, zwlr_foreign_toplevel_handle_v1 *handle, const char *app_id); +extern void fs_handle_output_enter(void *data, zwlr_foreign_toplevel_handle_v1 *handle, wl_output *output); +extern void fs_handle_output_leave(void *data, zwlr_foreign_toplevel_handle_v1 *handle, wl_output *output); +extern void fs_handle_done(void *data, zwlr_foreign_toplevel_handle_v1 *handle); +extern void fs_handle_parent(void *data, zwlr_foreign_toplevel_handle_v1 *handle, + zwlr_foreign_toplevel_handle_v1 *parent); + +/// @NOTE: fs_toplevel_listener MUST pass data as output_ref_t, see zwlr_foreign_toplevel_handle_v1_add_listener +inline static constexpr zwlr_foreign_toplevel_handle_v1_listener fs_toplevel_listener = { + .title = fs_handle_title, + .app_id = fs_handle_app_id, + .output_enter = fs_handle_output_enter, + .output_leave = fs_handle_output_leave, + .state = fs_handle_toplevel_state, + .done = fs_handle_done, + .closed = fs_handle_toplevel_closed, + .parent = fs_handle_parent, +}; + +extern void fs_update_state_fallback(wayland_context_t& ctx); + +extern void fs_handle_manager_toplevel(void *data, zwlr_foreign_toplevel_manager_v1 *manager, + zwlr_foreign_toplevel_handle_v1 *toplevel); +extern void fs_handle_manager_finished(void *data, zwlr_foreign_toplevel_manager_v1 *manager); + +/// @NOTE: fs_manager_listeners MUST pass data as wayland_listeners_context_t, see +/// zwlr_foreign_toplevel_manager_v1_add_listener +inline static constexpr zwlr_foreign_toplevel_manager_v1_listener fs_manager_listener = { + .toplevel = fs_handle_manager_toplevel, + .finished = fs_handle_manager_finished, +}; + +// ============================================================================= +// WAYLAND EVENT HANDLERS +// ============================================================================= + +extern void layer_surface_configure(void *data, zwlr_layer_surface_v1 *ls, uint32_t serial, uint32_t w, uint32_t h); +extern void layer_surface_closed(void *data, zwlr_layer_surface_v1 *ls); + +/// @NOTE: layer_listeners MUST pass data as wayland_listeners_context_t, see zwlr_layer_surface_v1_add_listener +inline static constexpr zwlr_layer_surface_v1_listener layer_listener = { + .configure = layer_surface_configure, + .closed = layer_surface_closed, +}; + +extern void xdg_wm_base_ping(void *data, xdg_wm_base *wm_base, uint32_t serial); + +/// @NOTE: xdg_wm_base_listeners MUST pass data as wayland_listeners_context_t, see xdg_wm_base_add_listener +inline static constexpr xdg_wm_base_listener xdg_wm_base_listener = { + .ping = xdg_wm_base_ping, +}; + +extern void output_geometry(void *data, wl_output *wl_output, int32_t x, int32_t y, int32_t physical_width, + int32_t physical_height, int32_t subpixel, const char *make, const char *model, + int32_t transform); +extern void output_mode(void *data, wl_output *wl_output, uint32_t flags, int32_t width, int32_t height, + int32_t refresh); +extern void output_done(void *data, wl_output *wl_output); +extern void output_scale(void *data, wl_output *wl_output, int32_t factor); +extern void output_name(void *data, wl_output *wl_output, const char *name); +extern void output_description(void *data, wl_output *wl_output, const char *name); + +/// @NOTE: output_listeners MUST pass data as wayland_listeners_context_t, see wl_output_add_listener +inline static constexpr wl_output_listener output_listener = { + .geometry = output_geometry, + .mode = output_mode, + .done = output_done, + .scale = output_scale, + .name = output_name, + .description = output_description, +}; + +// ============================================================================= +// WAYLAND PROTOCOL REGISTRY +// ============================================================================= + +extern void registry_global(void *data, wl_registry *reg, uint32_t name, const char *iface, uint32_t ver); +extern void registry_remove(void *data, wl_registry *registry, uint32_t name); + +/// @NOTE: reg_listeners MUST pass data as wayland_listeners_context_t, see zxdg_output_v1_add_listener +inline static constexpr wl_registry_listener reg_listener = {.global = registry_global, + .global_remove = registry_remove}; + +// ============================================================================= +// FRAME DRAWING HANDLER +// ============================================================================= + +extern void buffer_release(void *data, wl_buffer *buffer); + +/// @NOTE: buffer_listeners MUST pass data as wayland_shm_buffer_t, see wl_buffer_add_listener +inline static constexpr wl_buffer_listener buffer_listener = { + .release = buffer_release, +}; + +extern void frame_done(void *data, wl_callback *cb, uint32_t time); + +/// @NOTE: frame_listeners MUST pass data as wayland_listeners_context_t, see wl_callback_add_listener +inline static constexpr wl_callback_listener frame_listener = {.done = frame_done}; + +void wayland_handle_output_reconnect(struct wl_output *new_output, uint32_t registry_name, const char *output_name); +} // namespace bongocat::platform::wayland::details #endif \ No newline at end of file diff --git a/include/platform/wayland_context.h b/include/platform/wayland_context.h index 05f67edf..54f38781 100644 --- a/include/platform/wayland_context.h +++ b/include/platform/wayland_context.h @@ -1,156 +1,170 @@ -#ifndef BONGOCAT_WAYLAND_CONTEXT_H -#define BONGOCAT_WAYLAND_CONTEXT_H +#ifndef BONGOCAT_GLOBAL_WAYLAND_CONTEXT_CONTEXT_H +#define BONGOCAT_GLOBAL_WAYLAND_CONTEXT_CONTEXT_H -struct zwlr_layer_shell_v1; -struct zwlr_layer_surface_v1; +#include "graphics/animation_context.h" +#include "graphics/animation_thread_context.h" #include "platform/wayland-protocols.hpp" +#include "wayland_thread_context.h" -#include "wayland_shared_memory.h" -#include "config/config.h" -#include -#include - +#include namespace bongocat::platform::wayland { - inline static constexpr int MAX_ATTEMPTS = 4096; - - struct wayland_context_t; - void cleanup_wayland_context(wayland_context_t& ctx); - - enum class bar_visibility_t : bool { Hide = false, Show = true }; - - struct screen_info_t; - - struct wayland_context_t { - wl_display *display{nullptr}; - wl_compositor *compositor{nullptr}; - wl_shm *shm{nullptr}; - zwlr_layer_shell_v1 *layer_shell{nullptr}; - struct xdg_wm_base *xdg_wm_base{nullptr}; - wl_output *output{nullptr}; - wl_surface *surface{nullptr}; - zwlr_layer_surface_v1 *layer_surface{nullptr}; - - // local copy from other thread, update after reload (shared memory) - MMapMemory _local_copy_config; - MMapMemory ctx_shm; - bar_visibility_t bar_visibility {bar_visibility_t::Show}; - - int32_t _bar_height{0}; - int32_t _screen_width{0}; - char* _output_name_str{nullptr}; // ref to existing name in output, Will default to automatic one if kept null - bool _fullscreen_detected{false}; - screen_info_t *_screen_info{nullptr}; - - // frame done callback data - wl_callback *_frame_cb{nullptr}; - Mutex _frame_cb_lock; - atomic_bool _frame_pending{false}; - atomic_bool _redraw_after_frame{false}; - timestamp_ms_t _last_frame_timestamp_ms{0}; - - - - wayland_context_t() = default; - ~wayland_context_t() { - cleanup_wayland_context(*this); - } - - wayland_context_t(const wayland_context_t&) = delete; - wayland_context_t& operator=(const wayland_context_t&) = delete; - wayland_context_t(wayland_context_t&& other) noexcept = delete; - wayland_context_t& operator=(wayland_context_t&& other) noexcept = delete; - }; - - inline void cleanup_wayland_context(wayland_context_t& ctx) { - if (ctx.ctx_shm.ptr && ctx.ctx_shm.ptr != MAP_FAILED) { - atomic_store(&ctx.ctx_shm->configured, false); - } - - // drain pending events - if (ctx.display) { - wl_display_flush(ctx.display); - wl_display_roundtrip(ctx.display); - int attempts = 0; - while (wl_display_dispatch_pending(ctx.display) > 0 && attempts <= MAX_ATTEMPTS) { - attempts++; - } - if (attempts >= MAX_ATTEMPTS && wl_display_dispatch_pending(ctx.display) > 0) { - BONGOCAT_LOG_ERROR("Cant fully drain wayland display, max attempts: %i", attempts); - } - } - - // release frame.done handler - atomic_store(&ctx._frame_pending, false); - atomic_store(&ctx._redraw_after_frame, false); - // ctx._frame_cb_lock should be unlocked - if (ctx._frame_cb) wl_callback_destroy(ctx._frame_cb); - ctx._frame_cb = nullptr; - ctx._last_frame_timestamp_ms = 0; - - // surfaces - if (ctx.layer_surface) { - zwlr_layer_surface_v1_destroy(ctx.layer_surface); - ctx.layer_surface = nullptr; - } - if (ctx.surface) { - wl_surface_destroy(ctx.surface); - ctx.surface = nullptr; - } - - if (ctx.layer_shell) { - zwlr_layer_shell_v1_destroy(ctx.layer_shell); - ctx.layer_shell = nullptr; - } - if (ctx.xdg_wm_base) { - xdg_wm_base_destroy(ctx.xdg_wm_base); - ctx.xdg_wm_base = nullptr; - } - if (ctx.shm) { - wl_shm_destroy(ctx.shm); - ctx.shm = nullptr; - } - if (ctx.compositor) { - wl_compositor_destroy(ctx.compositor); - ctx.compositor = nullptr; - } - - // release shm - if (ctx.ctx_shm.ptr && ctx.ctx_shm.ptr != MAP_FAILED) { - for (size_t i = 0; i < WAYLAND_NUM_BUFFERS; i++) { - cleanup_shm_buffer(ctx.ctx_shm->buffers[i]); - } - } - release_allocated_mmap_memory(ctx.ctx_shm); - release_allocated_mmap_memory(ctx._local_copy_config); - - if (ctx.display) { - wl_display_disconnect(ctx.display); - ctx.display = nullptr; - } - - - // Note: output is just a reference to one of the outputs[] entries - // It will be destroyed when we destroy the outputs[] array above - ctx.output = nullptr; - - // Reset state - ctx.display = nullptr; - ctx.compositor = nullptr; - ctx.shm = nullptr; - ctx.layer_shell = nullptr; - ctx.xdg_wm_base = nullptr; - ctx.output = nullptr; - ctx.surface = nullptr; - ctx.layer_surface = nullptr; - ctx._output_name_str = nullptr; - ctx._frame_pending = false; - ctx._redraw_after_frame = false; - ctx._bar_height = 0; - ctx._screen_width = 0; - ctx._fullscreen_detected = false; - ctx._screen_info = nullptr; +// max. windows to track for fullscreen detection +inline static constexpr size_t MAX_TOP_LEVELS = 128; +inline static constexpr size_t MAX_OUTPUTS = 8; // Maximum monitor outputs to store +inline static constexpr size_t OUTPUT_NAME_SIZE = 128; + +// ============================================================================= +// FULLSCREEN DETECTION MODULE +// ============================================================================= + +struct fullscreen_detector_t { + struct zwlr_foreign_toplevel_manager_v1 *manager{BONGOCAT_NULLPTR}; + bool has_fullscreen_toplevel{false}; + timeval last_check{}; +}; +struct tracked_toplevel_t { + struct zwlr_foreign_toplevel_handle_v1 *handle{BONGOCAT_NULLPTR}; + wl_output *output{BONGOCAT_NULLPTR}; + bool is_fullscreen{false}; +}; + +// ============================================================================= +// SCREEN DIMENSION MANAGEMENT +// ============================================================================= + +enum class screen_info_received_flags_t : uint32_t { + None = (1U << 0), + Mode = (1U << 1), + Geometry = (1U << 2), +}; +struct screen_info_t { + struct wl_output *wl_output{BONGOCAT_NULLPTR}; // ref of output + int screen_width{0}; + int screen_height{0}; + int transform{0}; + int raw_width{0}; + int raw_height{0}; + screen_info_received_flags_t received{screen_info_received_flags_t::None}; +}; + +struct wayland_context_t; + +enum class output_ref_received_flags_t : uint32_t { + None = (1u << 0), + Name = (1u << 1), + LogicalPosition = (1u << 2), + LogicalSize = (1u << 3), +}; +// Output monitor reference structure +struct output_ref_t { + struct wl_output *wl_output{BONGOCAT_NULLPTR}; + zxdg_output_v1 *xdg_output{BONGOCAT_NULLPTR}; + uint32_t name{0}; // Registry name + char name_str[OUTPUT_NAME_SIZE]{}; // From xdg-output + int32_t x{0}; + int32_t y{0}; + int32_t width{0}; + int32_t height{0}; + output_ref_received_flags_t received{output_ref_received_flags_t::None}; + // monitor ID in Hyprland + int64_t hypr_id{-1}; + // back reference + wayland_context_t *wayland{BONGOCAT_NULLPTR}; +}; + +void cleanup_wayland(wayland_context_t& ctx); + +struct wayland_context_t { + wayland_thread_context thread_context; + animation::animation_context_t *animation_context{BONGOCAT_NULLPTR}; + + tracked_toplevel_t tracked_toplevels[MAX_TOP_LEVELS]; + size_t num_toplevels{0}; + + output_ref_t outputs[MAX_OUTPUTS]; + size_t output_count{0}; + zxdg_output_manager_v1 *xdg_output_manager{BONGOCAT_NULLPTR}; + + fullscreen_detector_t fs_detector; + + screen_info_t screen_infos[MAX_OUTPUTS]; + atomic_bool ready{false}; + + // Output reconnection handling + atomic_bool _output_lost{false}; // Set when our output disconnects + + wayland_context_t() { + for (size_t i = 0; i < MAX_TOP_LEVELS; i++) { + tracked_toplevels[i] = {}; + } + for (size_t i = 0; i < MAX_OUTPUTS; i++) { + outputs[i] = {}; + } + } + ~wayland_context_t() { + cleanup_wayland(*this); + } + + wayland_context_t(const wayland_context_t&) = delete; + wayland_context_t& operator=(const wayland_context_t&) = delete; + wayland_context_t(wayland_context_t&& other) noexcept = delete; + wayland_context_t& operator=(wayland_context_t&& other) noexcept = delete; +}; + +inline void cleanup_wayland(wayland_context_t& ctx) { + atomic_store(&ctx.ready, false); + + // First destroy xdg_output objects + for (size_t i = 0; i < ctx.output_count; ++i) { + if (ctx.outputs[i].xdg_output != BONGOCAT_NULLPTR) { + zxdg_output_v1_destroy(ctx.outputs[i].xdg_output); + ctx.outputs[i].xdg_output = BONGOCAT_NULLPTR; + } + } + + // Then destroy the manager + if (ctx.xdg_output_manager != BONGOCAT_NULLPTR) { + zxdg_output_manager_v1_destroy(ctx.xdg_output_manager); + ctx.xdg_output_manager = BONGOCAT_NULLPTR; + } + + // Finally destroy wl_output objects + for (size_t i = 0; i < ctx.output_count; ++i) { + if (ctx.outputs[i].wl_output != BONGOCAT_NULLPTR) { + wl_output_destroy(ctx.outputs[i].wl_output); + ctx.outputs[i].wl_output = BONGOCAT_NULLPTR; } + ctx.outputs[i] = {}; + ctx.outputs[i].wl_output = BONGOCAT_NULLPTR; + ctx.outputs[i].wayland = BONGOCAT_NULLPTR; + } + ctx.output_count = 0; + + if (ctx.fs_detector.manager != BONGOCAT_NULLPTR) { + zwlr_foreign_toplevel_manager_v1_destroy(ctx.fs_detector.manager); + ctx.fs_detector.manager = BONGOCAT_NULLPTR; + } + + for (size_t i = 0; i < ctx.num_toplevels; ++i) { + if (ctx.tracked_toplevels[i].handle != BONGOCAT_NULLPTR) { + zwlr_foreign_toplevel_handle_v1_destroy(ctx.tracked_toplevels[i].handle); + ctx.tracked_toplevels[i].handle = BONGOCAT_NULLPTR; + } + ctx.tracked_toplevels[i] = {}; + } + ctx.num_toplevels = 0; + + ctx.fs_detector = {}; + for (size_t i = 0; i < MAX_OUTPUTS; ++i) { + ctx.screen_infos[i] = {}; + } + + // clean up wayland context + cleanup_wayland_context(ctx.thread_context); + + ctx.animation_context = BONGOCAT_NULLPTR; } +} // namespace bongocat::platform::wayland -#endif // BONGOCAT_WAYLAND_CONTEXT_H \ No newline at end of file +#endif diff --git a/include/platform/wayland_setups.h b/include/platform/wayland_setups.h new file mode 100644 index 00000000..dbd4ffcd --- /dev/null +++ b/include/platform/wayland_setups.h @@ -0,0 +1,20 @@ +#ifndef BONGOCAT_WAYLAND_SETUPS_H +#define BONGOCAT_WAYLAND_SETUPS_H + +#include "graphics/animation.h" +#include "platform/wayland_context.h" +#include "platform/wayland_shared_memory.h" + +namespace bongocat::platform::wayland::details { +/// @TODO: use created_result_t for shm +// Create shared memory buffer - returns fd or -1 on error +BONGOCAT_NODISCARD FileDescriptor create_shm(off_t size); + +BONGOCAT_NODISCARD bongocat_error_t wayland_setup_protocols(wayland_context_t& ctx); +BONGOCAT_NODISCARD bongocat_error_t wayland_update_screen_info(wayland_context_t& ctx); +BONGOCAT_NODISCARD bongocat_error_t wayland_setup_surface(wayland_context_t& ctx); +BONGOCAT_NODISCARD bongocat_error_t wayland_setup_buffer(wayland_thread_context& wayland_context, + animation::animation_context_t& animation_ctx); +} // namespace bongocat::platform::wayland::details + +#endif \ No newline at end of file diff --git a/include/platform/wayland_shared_memory.h b/include/platform/wayland_shared_memory.h index d335c2b5..7cd33594 100644 --- a/include/platform/wayland_shared_memory.h +++ b/include/platform/wayland_shared_memory.h @@ -1,137 +1,135 @@ #ifndef BONGOCAT_WAYLAND_SHARED_MEMORY_H #define BONGOCAT_WAYLAND_SHARED_MEMORY_H -#include "graphics/global_animation_session.h" -#include +#include "graphics/animation_context.h" + #include +#include namespace bongocat::platform::wayland { - // single-, double- or triple-buffer - // @FIXME: fix for double buffer on (KWin) - inline static constexpr size_t WAYLAND_NUM_BUFFERS = 1; - - struct wayland_shm_buffer_t; - void cleanup_shm_buffer(wayland_shm_buffer_t& buffer); - - struct wayland_context_t; - - struct wayland_shm_buffer_t { - wl_buffer *buffer{nullptr}; - MMapFileBuffer pixels; - atomic_bool busy{false}; // 0: free / 1: busy - atomic_bool pending{false}; // 0/1: a render was requested while busy - size_t index{0}; // index track from wayland_shared_memory_t.buffers - - // extra context for listeners - animation::animation_session_t *_animation_trigger_context{nullptr}; - wayland_context_t *_wayland_context{nullptr}; // parent ref. for buffer_release - - - - wayland_shm_buffer_t() = default; - ~wayland_shm_buffer_t() { - cleanup_shm_buffer(*this); - } - - wayland_shm_buffer_t(const wayland_shm_buffer_t&) = delete; - wayland_shm_buffer_t& operator=(const wayland_shm_buffer_t&) = delete; - - wayland_shm_buffer_t(wayland_shm_buffer_t&& other) noexcept - : buffer(other.buffer), - pixels(bongocat::move(other.pixels)), - index(other.index), - _animation_trigger_context(other._animation_trigger_context), - _wayland_context(other._wayland_context) - { - atomic_store(&busy, atomic_load(&other.busy)); - atomic_store(&pending, atomic_load(&other.pending)); - - other.buffer = nullptr; - other.index = 0; - other._animation_trigger_context = nullptr; - atomic_store(&other.busy, false); - atomic_store(&other.pending, false); - } - wayland_shm_buffer_t& operator=(wayland_shm_buffer_t&& other) noexcept { - if (this != &other) { - buffer = other.buffer; - pixels = bongocat::move(other.pixels); - atomic_store(&busy, atomic_load(&other.busy)); - atomic_store(&pending, atomic_load(&other.pending)); - index = other.index; - _animation_trigger_context = other._animation_trigger_context; - _wayland_context = other._wayland_context; - - other.buffer = nullptr; - other.index = 0; - other._animation_trigger_context = nullptr; - other._wayland_context = nullptr; - atomic_store(&other.busy, false); - atomic_store(&other.pending, false); - } - return *this; - } - }; - - // Wayland globals - struct wayland_shared_memory_t { - wayland_shm_buffer_t buffers[WAYLAND_NUM_BUFFERS]; - size_t current_buffer_index{0}; - atomic_bool configured{false}; - - wayland_shared_memory_t() = default; - ~wayland_shared_memory_t() { - atomic_store(&configured, false); - current_buffer_index = 0; - for (size_t i = 0; i < WAYLAND_NUM_BUFFERS; i++) { - cleanup_shm_buffer(buffers[i]); - } - } - - wayland_shared_memory_t(const wayland_shared_memory_t&) = delete; - wayland_shared_memory_t& operator=(const wayland_shared_memory_t&) = delete; - - wayland_shared_memory_t(wayland_shared_memory_t&& other) noexcept - : buffers{} - { - atomic_store(&configured, false); - current_buffer_index = other.current_buffer_index; - // Manually move each buffer - for (size_t i = 0; i < WAYLAND_NUM_BUFFERS; i++) { - buffers[i] = bongocat::move(other.buffers[i]); - } - atomic_store(&configured, atomic_load(&other.configured)); - - other.current_buffer_index = 0; - atomic_store(&other.configured, false); - } - wayland_shared_memory_t& operator=(wayland_shared_memory_t&& other) noexcept { - if (this != &other) { - atomic_store(&configured, false); - current_buffer_index = other.current_buffer_index; - for (size_t i = 0; i < WAYLAND_NUM_BUFFERS; ++i) { - buffers[i] = move(other.buffers[i]); - } - atomic_store(&configured, atomic_load(&other.configured)); - - other.current_buffer_index = 0; - atomic_store(&other.configured, false); - } - return *this; - } - }; - - inline void cleanup_shm_buffer(wayland_shm_buffer_t& buffer) { - atomic_store(&buffer.pending, false); - atomic_store(&buffer.busy, true); - if (buffer.buffer) wl_buffer_destroy(buffer.buffer); - buffer.buffer = nullptr; - release_allocated_mmap_file_buffer(buffer.pixels); - atomic_store(&buffer.busy, false); - buffer.index = 0; - buffer._animation_trigger_context = nullptr; - buffer._wayland_context = nullptr; +// single-, double- or triple-buffer +// @FIXME: fix for double buffer on (KWin) +inline static constexpr size_t WAYLAND_NUM_BUFFERS = 1; + +struct wayland_shm_buffer_t; +void cleanup_shm_buffer(wayland_shm_buffer_t& buffer); + +struct wayland_thread_context; + +struct wayland_shm_buffer_t { + wl_buffer *buffer{BONGOCAT_NULLPTR}; + MMapFileBuffer pixels; + atomic_bool busy{false}; // 0: free / 1: busy + atomic_bool pending{false}; // 0/1: a render was requested while busy + size_t index{0}; // index track from wayland_shared_memory_t.buffers + + // extra context for listeners + animation::animation_context_t *_animation_context{BONGOCAT_NULLPTR}; + wayland_thread_context *_wayland_thread_context{BONGOCAT_NULLPTR}; // parent ref. for buffer_release + + wayland_shm_buffer_t() = default; + ~wayland_shm_buffer_t() { + cleanup_shm_buffer(*this); + } + + wayland_shm_buffer_t(const wayland_shm_buffer_t&) = delete; + wayland_shm_buffer_t& operator=(const wayland_shm_buffer_t&) = delete; + + wayland_shm_buffer_t(wayland_shm_buffer_t&& other) noexcept + : buffer(other.buffer) + , pixels(bongocat::move(other.pixels)) + , index(other.index) + , _animation_context(other._animation_context) + , _wayland_thread_context(other._wayland_thread_context) { + atomic_store(&busy, atomic_load(&other.busy)); + atomic_store(&pending, atomic_load(&other.pending)); + + other.buffer = BONGOCAT_NULLPTR; + other.index = 0; + other._animation_context = BONGOCAT_NULLPTR; + atomic_store(&other.busy, false); + atomic_store(&other.pending, false); + } + wayland_shm_buffer_t& operator=(wayland_shm_buffer_t&& other) noexcept { + if (this != &other) { + buffer = other.buffer; + pixels = bongocat::move(other.pixels); + atomic_store(&busy, atomic_load(&other.busy)); + atomic_store(&pending, atomic_load(&other.pending)); + index = other.index; + _animation_context = other._animation_context; + _wayland_thread_context = other._wayland_thread_context; + + other.buffer = BONGOCAT_NULLPTR; + other.index = 0; + other._animation_context = BONGOCAT_NULLPTR; + other._wayland_thread_context = BONGOCAT_NULLPTR; + atomic_store(&other.busy, false); + atomic_store(&other.pending, false); + } + return *this; + } +}; + +// Wayland globals +struct wayland_shared_memory_t { + wayland_shm_buffer_t buffers[WAYLAND_NUM_BUFFERS]; + size_t current_buffer_index{0}; + atomic_bool configured{false}; + + wayland_shared_memory_t() = default; + ~wayland_shared_memory_t() { + atomic_store(&configured, false); + current_buffer_index = 0; + for (size_t i = 0; i < WAYLAND_NUM_BUFFERS; i++) { + cleanup_shm_buffer(buffers[i]); + } + } + + wayland_shared_memory_t(const wayland_shared_memory_t&) = delete; + wayland_shared_memory_t& operator=(const wayland_shared_memory_t&) = delete; + + wayland_shared_memory_t(wayland_shared_memory_t&& other) noexcept : buffers{} { + atomic_store(&configured, false); + current_buffer_index = other.current_buffer_index; + // Manually move each buffer + for (size_t i = 0; i < WAYLAND_NUM_BUFFERS; i++) { + buffers[i] = bongocat::move(other.buffers[i]); + } + atomic_store(&configured, atomic_load(&other.configured)); + + other.current_buffer_index = 0; + atomic_store(&other.configured, false); + } + wayland_shared_memory_t& operator=(wayland_shared_memory_t&& other) noexcept { + if (this != &other) { + atomic_store(&configured, false); + current_buffer_index = other.current_buffer_index; + for (size_t i = 0; i < WAYLAND_NUM_BUFFERS; ++i) { + buffers[i] = move(other.buffers[i]); + } + atomic_store(&configured, atomic_load(&other.configured)); + + other.current_buffer_index = 0; + atomic_store(&other.configured, false); } + return *this; + } +}; + +inline void cleanup_shm_buffer(wayland_shm_buffer_t& buffer) { + atomic_store(&buffer.pending, false); + atomic_store(&buffer.busy, true); + if (buffer.buffer != BONGOCAT_NULLPTR) { + wl_buffer_destroy(buffer.buffer); + buffer.buffer = BONGOCAT_NULLPTR; + } + release_allocated_mmap_file_buffer(buffer.pixels); + atomic_store(&buffer.busy, false); + buffer.index = 0; + buffer._animation_context = BONGOCAT_NULLPTR; + buffer._wayland_thread_context = BONGOCAT_NULLPTR; } +} // namespace bongocat::platform::wayland -#endif // BONGOCAT_WAYLAND_SHARED_MEMORY_H \ No newline at end of file +#endif // BONGOCAT_WAYLAND_SHARED_MEMORY_H \ No newline at end of file diff --git a/include/platform/wayland_thread_context.h b/include/platform/wayland_thread_context.h new file mode 100644 index 00000000..9a9c3aaa --- /dev/null +++ b/include/platform/wayland_thread_context.h @@ -0,0 +1,197 @@ +#ifndef BONGOCAT_WAYLAND_CONTEXT_H +#define BONGOCAT_WAYLAND_CONTEXT_H + +struct zwlr_layer_shell_v1; +struct zwlr_layer_surface_v1; +#include "config/config.h" +#include "platform/wayland-protocols.hpp" +#include "wayland_shared_memory.h" + +#include +#include + +namespace bongocat::platform::wayland { +inline static constexpr int MAX_ATTEMPTS = 4096; + +struct wayland_thread_context; +// Cleanup Wayland resources +void cleanup_wayland_context(wayland_thread_context& ctx); + +enum class bar_visibility_t : bool { + Hide = false, + Show = true +}; + +struct screen_info_t; + +struct wayland_thread_context { + wl_display *display{BONGOCAT_NULLPTR}; + wl_compositor *compositor{BONGOCAT_NULLPTR}; + wl_shm *shm{BONGOCAT_NULLPTR}; + zwlr_layer_shell_v1 *layer_shell{BONGOCAT_NULLPTR}; + struct xdg_wm_base *xdg_wm_base{BONGOCAT_NULLPTR}; + wl_output *output{BONGOCAT_NULLPTR}; + wl_surface *surface{BONGOCAT_NULLPTR}; + zwlr_layer_surface_v1 *layer_surface{BONGOCAT_NULLPTR}; + struct wl_registry *registry{BONGOCAT_NULLPTR}; + + // Output reconnection handling + uint32_t bound_output_name{0}; // Registry name of our bound output + bool using_named_output{false}; // True if user specified an output name + + // local copy from other thread, update after reload (shared memory) + MMapMemory _local_copy_config; + MMapMemory ctx_shm; + bar_visibility_t bar_visibility{bar_visibility_t::Show}; + + int32_t _bar_height{0}; + int32_t _screen_width{0}; + // ref to existing name in output, Will default to automatic one if kept null + char *_output_name_str{BONGOCAT_NULLPTR}; + bool _fullscreen_detected{false}; + screen_info_t *_screen_info{BONGOCAT_NULLPTR}; + + // frame done callback data + wl_callback *_frame_cb{BONGOCAT_NULLPTR}; + Mutex _frame_cb_lock; + atomic_bool _frame_pending{false}; + atomic_bool _redraw_after_frame{false}; + timestamp_ms_t _last_frame_timestamp_ms{0}; + + wayland_thread_context() = default; + ~wayland_thread_context() { + cleanup_wayland_context(*this); + } + + wayland_thread_context(const wayland_thread_context&) = delete; + wayland_thread_context& operator=(const wayland_thread_context&) = delete; + wayland_thread_context(wayland_thread_context&& other) noexcept = delete; + wayland_thread_context& operator=(wayland_thread_context&& other) noexcept = delete; +}; + +inline void cleanup_wayland_context_protocols(wayland_thread_context& ctx) { + if (ctx.ctx_shm) { + atomic_store(&ctx.ctx_shm->configured, false); + } + + if (ctx.registry != BONGOCAT_NULLPTR) { + wl_registry_destroy(ctx.registry); + ctx.registry = BONGOCAT_NULLPTR; + } +} +inline void cleanup_wayland_context_surface(wayland_thread_context& ctx) { + if (ctx.ctx_shm) { + atomic_store(&ctx.ctx_shm->configured, false); + } + + if (ctx.layer_surface != BONGOCAT_NULLPTR) { + zwlr_layer_surface_v1_destroy(ctx.layer_surface); + ctx.layer_surface = BONGOCAT_NULLPTR; + } + if (ctx.surface != BONGOCAT_NULLPTR) { + wl_surface_destroy(ctx.surface); + ctx.surface = BONGOCAT_NULLPTR; + } +} +inline void cleanup_wayland_context_buffer(wayland_thread_context& ctx) { + if (ctx.ctx_shm) { + atomic_store(&ctx.ctx_shm->configured, false); + } + + if (ctx.ctx_shm) { + for (size_t i = 0; i < WAYLAND_NUM_BUFFERS; i++) { + cleanup_shm_buffer(ctx.ctx_shm->buffers[i]); + } + + ctx.ctx_shm->current_buffer_index = 0; + } + + ctx._screen_width = 0; + ctx._bar_height = 0; +} +inline void cleanup_wayland_context(wayland_thread_context& ctx) { + if (ctx.ctx_shm) { + atomic_store(&ctx.ctx_shm->configured, false); + } + + // drain pending events + if (ctx.display != BONGOCAT_NULLPTR) { + wl_display_flush(ctx.display); + wl_display_roundtrip(ctx.display); + int attempts = 0; + while (wl_display_dispatch_pending(ctx.display) > 0 && attempts <= MAX_ATTEMPTS) { + attempts++; + } + if (attempts >= MAX_ATTEMPTS && wl_display_dispatch_pending(ctx.display) > 0) { + BONGOCAT_LOG_ERROR("Cant fully drain wayland display, max attempts: %i", attempts); + } + } + + cleanup_wayland_context_protocols(ctx); + + // release frame.done handler + atomic_store(&ctx._frame_pending, false); + atomic_store(&ctx._redraw_after_frame, false); + // ctx._frame_cb_lock should be unlocked + if (ctx._frame_cb != BONGOCAT_NULLPTR) { + wl_callback_destroy(ctx._frame_cb); + ctx._frame_cb = BONGOCAT_NULLPTR; + } + ctx._last_frame_timestamp_ms = 0; + + // surfaces + cleanup_wayland_context_surface(ctx); + + if (ctx.layer_shell != BONGOCAT_NULLPTR) { + zwlr_layer_shell_v1_destroy(ctx.layer_shell); + ctx.layer_shell = BONGOCAT_NULLPTR; + } + if (ctx.xdg_wm_base != BONGOCAT_NULLPTR) { + xdg_wm_base_destroy(ctx.xdg_wm_base); + ctx.xdg_wm_base = BONGOCAT_NULLPTR; + } + if (ctx.shm != BONGOCAT_NULLPTR) { + wl_shm_destroy(ctx.shm); + ctx.shm = BONGOCAT_NULLPTR; + } + if (ctx.compositor != BONGOCAT_NULLPTR) { + wl_compositor_destroy(ctx.compositor); + ctx.compositor = BONGOCAT_NULLPTR; + } + + // release shm + cleanup_wayland_context_buffer(ctx); + release_allocated_mmap_memory(ctx.ctx_shm); + release_allocated_mmap_memory(ctx._local_copy_config); + + if (ctx.display != BONGOCAT_NULLPTR) { + wl_display_disconnect(ctx.display); + ctx.display = BONGOCAT_NULLPTR; + } + + // Note: output is just a reference to one of the outputs[] entries + // It will be destroyed when we destroy the outputs[] array above + ctx.output = BONGOCAT_NULLPTR; + ctx.bound_output_name = 0; + ctx.using_named_output = false; + + // Reset state + ctx.display = BONGOCAT_NULLPTR; + ctx.compositor = BONGOCAT_NULLPTR; + ctx.shm = BONGOCAT_NULLPTR; + ctx.layer_shell = BONGOCAT_NULLPTR; + ctx.xdg_wm_base = BONGOCAT_NULLPTR; + ctx.output = BONGOCAT_NULLPTR; + ctx.surface = BONGOCAT_NULLPTR; + ctx.layer_surface = BONGOCAT_NULLPTR; + ctx._output_name_str = BONGOCAT_NULLPTR; + ctx._frame_pending = false; + ctx._redraw_after_frame = false; + ctx._bar_height = 0; + ctx._screen_width = 0; + ctx._fullscreen_detected = false; + ctx._screen_info = BONGOCAT_NULLPTR; +} +} // namespace bongocat::platform::wayland + +#endif // BONGOCAT_WAYLAND_CONTEXT_H \ No newline at end of file diff --git a/include/utils/error.h b/include/utils/error.h index 76cdbd9f..b65ce86b 100644 --- a/include/utils/error.h +++ b/include/utils/error.h @@ -5,112 +5,170 @@ #include #include +// ============================================================================= +// C23 MODERN FEATURES +// ============================================================================= + +// Nodiscard attribute for functions that return values that must be used +#ifdef __cplusplus +# define BONGOCAT_NODISCARD [[nodiscard]] +#else +# if __STDC_VERSION__ >= 202311L +# define BONGOCAT_NODISCARD [[nodiscard]] +# else +# define BONGOCAT_NODISCARD __attribute__((warn_unused_result)) +# endif +#endif + +#ifdef __cplusplus +# define BONGOCAT_NULLPTR nullptr +#else +// Null pointer (C23 nullptr or fallback) +# if __STDC_VERSION__ >= 202311L +# define BONGOCAT_NULLPTR nullptr +# else +# define BONGOCAT_NULLPTR NULL +# endif +#endif + +// Unreachable code hint for optimizer +#if __STDC_VERSION__ >= 202311L +# define BONGOCAT_UNREACHABLE() unreachable() +#else +# define BONGOCAT_UNREACHABLE() __builtin_unreachable() +#endif + namespace bongocat { - // Error codes - enum class bongocat_error_t : uint8_t { - BONGOCAT_SUCCESS = 0, - BONGOCAT_ERROR_MEMORY, - BONGOCAT_ERROR_FILE_IO, - BONGOCAT_ERROR_WAYLAND, - BONGOCAT_ERROR_CONFIG, - BONGOCAT_ERROR_INPUT, - BONGOCAT_ERROR_ANIMATION, - BONGOCAT_ERROR_THREAD, - BONGOCAT_ERROR_INVALID_PARAM, - BONGOCAT_ERROR_IMAGE, - }; - - // Error handling macros -#define BONGOCAT_CHECK_NULL(ptr, error_code) \ - { \ - if ((ptr) == nullptr) { \ - BONGOCAT_LOG_ERROR("NULL pointer: %s at %s:%d", #ptr, __FILE__, __LINE__); \ - return (error_code); \ - } \ - } - -#define BONGOCAT_CHECK_ERROR(condition, error_code, message) \ - { \ - if (condition) { \ - BONGOCAT_LOG_ERROR("%s at %s:%d", message, __FILE__, __LINE__); \ - return (error_code); \ - } \ - } + +// ============================================================================= +// ERROR CODES +// ============================================================================= + +// Error codes +enum class bongocat_error_t : uint8_t { + BONGOCAT_SUCCESS = 0, + BONGOCAT_ERROR_MEMORY, + BONGOCAT_ERROR_FILE_IO, + BONGOCAT_ERROR_WAYLAND, + BONGOCAT_ERROR_CONFIG, + BONGOCAT_ERROR_INPUT, + BONGOCAT_ERROR_ANIMATION, + BONGOCAT_ERROR_THREAD, + BONGOCAT_ERROR_INVALID_PARAM, + BONGOCAT_ERROR_IMAGE, +}; + +// ============================================================================= +// GUARD CLAUSE MACROS +// ============================================================================= + +// Guard clause for null pointer - returns early with error +#define BONGOCAT_CHECK_NULL(ptr, error_code) \ + do { \ + if ((ptr) == BONGOCAT_NULLPTR) { \ + BONGOCAT_LOG_ERROR("NULL pointer: %s at %s:%d", #ptr, __FILE__, __LINE__); \ + return (error_code); \ + } \ + } while (0) + +// Guard clause for error conditions - returns early with error +#define BONGOCAT_CHECK_ERROR(condition, error_code, message) \ + do { \ + if (condition) { \ + BONGOCAT_LOG_ERROR("%s at %s:%d", message, __FILE__, __LINE__); \ + return (error_code); \ + } \ + } while (0) + +// Guard clause for boolean conditions - returns early with value +#define BONGOCAT_GUARD(condition, return_value) \ + do { \ + if (condition) { \ + return (return_value); \ + } \ + } while (0) + +// ============================================================================= +// LOGGING FUNCTIONS +// ============================================================================= #if !defined(BONGOCAT_DISABLE_LOGGER) || defined(BONGOCAT_ENABLE_LOGGER) - namespace details { - // Logging functions - void log_error(const char *format, ...); - void log_warning(const char *format, ...); - void log_info(const char *format, ...); - void log_debug(const char *format, ...); - void log_verbose(const char *format, ...); - } +namespace details { + // Logging functions + void log_error(const char *format, ...); + void log_warning(const char *format, ...); + void log_info(const char *format, ...); + void log_debug(const char *format, ...); + void log_verbose(const char *format, ...); +} // namespace details #endif - // Error handling initialization - void error_init(bool enable_debug); - [[nodiscard]] const char* error_string(bongocat_error_t error); +// ============================================================================= +// ERROR HANDLING +// ============================================================================= - // 1 = Error - // 2 = Warning - // 3 = Info - // 4 = Debug - // 5 = Verbose +// Error handling initialization +void error_init(bool enable_debug); +BONGOCAT_NODISCARD const char *error_string(bongocat_error_t error); + +// 1 = Error +// 2 = Warning +// 3 = Info +// 4 = Debug +// 5 = Verbose #ifndef BONGOCAT_LOG_LEVEL -#define BONGOCAT_LOG_LEVEL 3 +# define BONGOCAT_LOG_LEVEL 3 #endif - namespace features { +namespace features { #if defined(BONGOCAT_LOG_LEVEL) - inline static constexpr int LogLevel = BONGOCAT_LOG_LEVEL; + inline static constexpr int LogLevel = BONGOCAT_LOG_LEVEL; #endif - } +} // namespace features #if (!defined(BONGOCAT_DISABLE_LOGGER) || defined(BONGOCAT_ENABLE_LOGGER)) && BONGOCAT_LOG_LEVEL >= 1 -#define BONGOCAT_LOG_ERROR(format, ...) ::bongocat::details::log_error(format __VA_OPT__(,) __VA_ARGS__) +# define BONGOCAT_LOG_ERROR(format, ...) ::bongocat::details::log_error(format __VA_OPT__(, ) __VA_ARGS__) #else -#define BONGOCAT_LOG_ERROR(format, ...) +# define BONGOCAT_LOG_ERROR(format, ...) #endif #if (!defined(BONGOCAT_DISABLE_LOGGER) || defined(BONGOCAT_ENABLE_LOGGER)) && BONGOCAT_LOG_LEVEL >= 2 -#define BONGOCAT_LOG_WARNING(format, ...) ::bongocat::details::log_warning(format __VA_OPT__(,) __VA_ARGS__) +# define BONGOCAT_LOG_WARNING(format, ...) ::bongocat::details::log_warning(format __VA_OPT__(, ) __VA_ARGS__) #else -#define BONGOCAT_LOG_WARNING(format, ...) +# define BONGOCAT_LOG_WARNING(format, ...) #endif #if (!defined(BONGOCAT_DISABLE_LOGGER) || defined(BONGOCAT_ENABLE_LOGGER)) && BONGOCAT_LOG_LEVEL >= 3 -#define BONGOCAT_LOG_INFO(format, ...) ::bongocat::details::log_info(format __VA_OPT__(,) __VA_ARGS__) +# define BONGOCAT_LOG_INFO(format, ...) ::bongocat::details::log_info(format __VA_OPT__(, ) __VA_ARGS__) #else -#define BONGOCAT_LOG_INFO(format, ...) +# define BONGOCAT_LOG_INFO(format, ...) #endif #if (!defined(BONGOCAT_DISABLE_LOGGER) || defined(BONGOCAT_ENABLE_LOGGER)) && BONGOCAT_LOG_LEVEL >= 4 -#define BONGOCAT_LOG_DEBUG(format, ...) ::bongocat::details::log_debug(format __VA_OPT__(,) __VA_ARGS__) +# define BONGOCAT_LOG_DEBUG(format, ...) ::bongocat::details::log_debug(format __VA_OPT__(, ) __VA_ARGS__) #else -#define BONGOCAT_LOG_DEBUG(format, ...) +# define BONGOCAT_LOG_DEBUG(format, ...) #endif #if (!defined(BONGOCAT_DISABLE_LOGGER) || defined(BONGOCAT_ENABLE_LOGGER)) && BONGOCAT_LOG_LEVEL >= 5 -#define BONGOCAT_LOG_VERBOSE(format, ...) ::bongocat::details::log_verbose(format __VA_OPT__(,) __VA_ARGS__) +# define BONGOCAT_LOG_VERBOSE(format, ...) ::bongocat::details::log_verbose(format __VA_OPT__(, ) __VA_ARGS__) #else -#define BONGOCAT_LOG_VERBOSE(format, ...) +# define BONGOCAT_LOG_VERBOSE(format, ...) #endif - - inline int check_errno([[maybe_unused]] const char* fd_name) { - int err = errno; - // supress compiler warning +inline int check_errno([[maybe_unused]] const char *fd_name) { + const int err = errno; + // supress compiler warning #if EAGAIN == EWOULDBLOCK - if (err != EAGAIN && err != -1) { - BONGOCAT_LOG_ERROR("Error reading %s: %s", fd_name, strerror(err)); - } + if (err != EAGAIN && err != -1) { + BONGOCAT_LOG_ERROR("Error reading %s: %s", fd_name, strerror(err)); + } #else - if (err != EAGAIN && err != EWOULDBLOCK && err != -1) { - BONGOCAT_LOG_ERROR("Error reading %s: %s", fd_name, strerror(err)); - } + if (err != EAGAIN && err != EWOULDBLOCK && err != -1) { + BONGOCAT_LOG_ERROR("Error reading %s: %s", fd_name, strerror(err)); + } #endif - return err; - } + return err; } +} // namespace bongocat -#endif // ERROR_H \ No newline at end of file +#endif // ERROR_H \ No newline at end of file diff --git a/include/utils/memory.h b/include/utils/memory.h index 4116e6f3..e17bb3f4 100644 --- a/include/utils/memory.h +++ b/include/utils/memory.h @@ -3,435 +3,494 @@ #include "./time.h" #include "utils/error.h" -#include + #include #include +#include #if defined(__GNUC__) || defined(__GNUG__) -#include +# include #endif namespace bongocat { - // Memory pool for efficient allocation - struct memory_pool_t { - void *data{nullptr}; - size_t size{0}; - size_t used{0}; - size_t alignment{0}; - }; - - // Safe memory allocation functions - void* malloc(size_t size); - void* calloc(size_t count, size_t size); - void* realloc(void *ptr, size_t size); - void free(void *ptr); - - // Memory pool functions - [[nodiscard]] memory_pool_t* memory_pool_create(size_t size, size_t alignment); - void* memory_pool_alloc(memory_pool_t& pool, size_t size); - void memory_pool_reset(memory_pool_t& pool); - void memory_pool_destroy(memory_pool_t& pool); + +// ============================================================================= +// MEMORY POOL +// ============================================================================= + +// Memory pool for efficient allocation +struct memory_pool_t { + void *data{BONGOCAT_NULLPTR}; + size_t size{0}; + size_t used{0}; + size_t alignment{0}; +}; + +// ============================================================================= +// MEMORY ALLOCATION FUNCTIONS +// ============================================================================= + +// Safe memory allocation functions +BONGOCAT_NODISCARD void *malloc(size_t size); +BONGOCAT_NODISCARD void *calloc(size_t count, size_t size); +BONGOCAT_NODISCARD void *realloc(void *ptr, size_t size); +void free(void *ptr); + +// ============================================================================= +// MEMORY POOL FUNCTIONS +// ============================================================================= + +// Memory pool functions +BONGOCAT_NODISCARD memory_pool_t *memory_pool_create(size_t size, size_t alignment); +BONGOCAT_NODISCARD void *memory_pool_alloc(memory_pool_t& pool, size_t size); +void memory_pool_reset(memory_pool_t& pool); +void memory_pool_destroy(memory_pool_t& pool); + +// ============================================================================= +// MEMORY STATISTICS +// ============================================================================= #if !defined(BONGOCAT_DISABLE_MEMORY_STATISTICS) || defined(BONGOCAT_ENABLE_MEMORY_STATISTICS) - // Memory statistics - struct memory_stats_t { - atomic_size_t total_allocated; - atomic_size_t current_allocated; - atomic_size_t peak_allocated; - atomic_size_t allocation_count; - atomic_size_t free_count; - }; - - void memory_get_stats(memory_stats_t& stats); +// Memory statistics +struct memory_stats_t { + atomic_size_t total_allocated; + atomic_size_t current_allocated; + atomic_size_t peak_allocated; + atomic_size_t allocation_count; + atomic_size_t free_count; +}; + +void memory_get_stats(memory_stats_t& stats); #endif - void memory_print_stats(); +void memory_print_stats(); + +// ============================================================================= +// DEBUG BUILD FEATURES +// ============================================================================= - // Memory leak detection (debug builds) +// Memory leak detection (debug builds) #ifndef NDEBUG -#define BONGOCAT_MALLOC(size) ::bongocat::malloc_debug(size, __FILE__, __LINE__) -#define BONGOCAT_FREE(ptr) ::bongocat::free_debug(ptr, __FILE__, __LINE__) - void* malloc_debug(size_t size, const char *file, int line); - void free_debug(void *ptr, const char *file, int line); - void memory_leak_check(); +# define BONGOCAT_MALLOC(size) ::bongocat::malloc_debug(size, __FILE__, __LINE__) +# define BONGOCAT_FREE(ptr) ::bongocat::free_debug(ptr, __FILE__, __LINE__) +void *malloc_debug(size_t size, const char *file, int line); +void free_debug(void *ptr, const char *file, int line); +void memory_leak_check(); #else -#define BONGOCAT_MALLOC(size) ::bongocat::malloc(size) -#define BONGOCAT_FREE(ptr) ::bongocat::free(ptr) +# define BONGOCAT_MALLOC(size) ::bongocat::malloc(size) +# define BONGOCAT_FREE(ptr) ::bongocat::free(ptr) #endif -#define BONGOCAT_SAFE_FREE(ptr) \ - { \ - if (ptr) { \ - BONGOCAT_FREE(ptr); \ - (ptr) = NULL; \ - } \ - } +// ============================================================================= +// RAII CLEANUP MACROS +// ============================================================================= + +// Cleanup function for auto-freeing malloc'd memory +/* +inline void auto_free_impl(void *ptr) { + void **p = static_cast(ptr); + if (*p) { + free(*p); + *p = BONGOCAT_NULLPTR; + } +} +*/ + +// Auto-free heap allocations when variable goes out of scope +// #define BONGOCAT_AUTO_FREE __attribute__((cleanup(bongocat::auto_free_impl))) + +#define BONGOCAT_SAFE_FREE(ptr) \ + do { \ + if (ptr) { \ + free(reinterpret_cast(ptr)); \ + (ptr) = BONGOCAT_NULLPTR; \ + } \ + } while (0) + +template +constexpr std::size_t LEN_ARRAY(const T (&)[N]) noexcept { + return N; +} - template - constexpr std::size_t LEN_ARRAY(const T (&)[N]) noexcept { - return N; - } +// Cleanup implementation for auto pool (must be after memory_pool_t definition) +/* +inline void bongocat_auto_pool_impl(struct memory_pool **pool) { + if (*pool) { + memory_pool_destroy(*pool); + *pool = BONGOCAT_NULLPTR; + } +} +*/ +// Auto-destroy memory pool when variable goes out of scope +// #define BONGOCAT_AUTO_POOL __attribute__((cleanup(bongocat::auto_pool_impl))) - template - struct is_trivially_copyable { +template +struct is_trivially_copyable { #if defined(__clang__) - inline static constexpr bool value = __is_trivially_copyable(T); + inline static constexpr bool value = __is_trivially_copyable(T); #elif defined(__GNUC__) || defined(__GNUG__) - inline static constexpr bool value = __is_trivially_copyable(T); + inline static constexpr bool value = __is_trivially_copyable(T); #elif defined(_MSC_VER) - inline static constexpr bool value = __is_trivially_copyable(T); + inline static constexpr bool value = __is_trivially_copyable(T); #else - inline static constexpr bool value = false; + inline static constexpr bool value = false; #endif - }; +}; - template - struct is_trivially_destructible { +template +struct is_trivially_destructible { #if defined(__clang__) - inline static constexpr bool value = __is_trivially_destructible(T); + inline static constexpr bool value = __is_trivially_destructible(T); #elif defined(__GNUC__) || defined(__GNUG__) - // GCC requires `typename T` to be fully resolved - //static constexpr bool value = __is_trivially_destructible(T); - /// @FIXME: expected nested-name-specifier before »T« [-Wtemplate-body] - /// Fallback: use STL - inline static constexpr bool value = std::is_trivially_destructible_v; + // GCC requires `typename T` to be fully resolved + // static constexpr bool value = __is_trivially_destructible(T); + /// @FIXME: expected nested-name-specifier before »T« [-Wtemplate-body] + /// Fallback: use STL + inline static constexpr bool value = std::is_trivially_destructible_v; #elif defined(_MSC_VER) - inline static constexpr bool value = __is_trivially_destructible(T); + inline static constexpr bool value = __is_trivially_destructible(T); #else - inline static constexpr bool value = false; + inline static constexpr bool value = false; #endif - }; - - template - struct AllocatedMemory; - template - void release_allocated_memory(AllocatedMemory& memory) noexcept; - - template - struct AllocatedMemory { - T* ptr{nullptr}; - size_t _size_bytes{0}; - - constexpr AllocatedMemory() = default; - ~AllocatedMemory() noexcept { - release_allocated_memory(*this); - } - - explicit AllocatedMemory(decltype(nullptr)) noexcept {} - AllocatedMemory& operator=(decltype(nullptr)) noexcept { - release_allocated_memory(*this); - return *this; - } - - AllocatedMemory(const AllocatedMemory& other) - : _size_bytes(other._size_bytes) - { - _size_bytes = sizeof(T); - if (other.ptr != nullptr && _size_bytes > 0) { - ptr = static_cast(BONGOCAT_MALLOC(_size_bytes)); - if (ptr != nullptr) { - if constexpr (is_trivially_copyable::value) { - memcpy(ptr, other.ptr, _size_bytes); - } else { - new (ptr) T(*other.ptr); - } - } else { - _size_bytes = 0; - ptr = nullptr; - BONGOCAT_LOG_ERROR("memory allocation failed"); - } - } else { - _size_bytes = 0; - } - } - AllocatedMemory& operator=(const AllocatedMemory& other) { - if (this != &other) { - release_allocated_memory(*this); - _size_bytes = sizeof(T); - if (other.ptr != nullptr && _size_bytes > 0) { - ptr = static_cast(BONGOCAT_MALLOC(_size_bytes)); - if (ptr) { - if constexpr (is_trivially_copyable::value) { - memcpy(ptr, other.ptr, _size_bytes); - } else { - new (ptr) T(*other.ptr); - } - } else { - _size_bytes = 0; - ptr = nullptr; - BONGOCAT_LOG_ERROR("memory allocation failed"); - } - } else { - _size_bytes = 0; - } - } - return *this; - } - - AllocatedMemory(AllocatedMemory&& other) noexcept - : ptr(other.ptr), _size_bytes(other._size_bytes) - { - other.ptr = nullptr; - other._size_bytes = 0; - } - AllocatedMemory& operator=(AllocatedMemory&& other) noexcept { - if (this != &other) { - release_allocated_memory(*this); - ptr = other.ptr; - _size_bytes = other._size_bytes; - other.ptr = nullptr; - other._size_bytes = 0; - } - return *this; - } - - constexpr operator bool() const noexcept { - return ptr != nullptr; - } - - T& operator*() { - assert(ptr); - return *ptr; - } - constexpr const T& operator*() const { - assert(ptr); - return *ptr; - } - T* operator->() { - assert(ptr); - return ptr; - } - constexpr const T* operator->() const { - assert(ptr); - return ptr; - } - explicit operator T*() noexcept { - return ptr; - } - constexpr explicit operator const T*() const noexcept { - return ptr; - } - - constexpr bool operator==(decltype(nullptr)) const noexcept { - return ptr == nullptr; - } - constexpr bool operator!=(decltype(nullptr)) const noexcept { - return ptr != nullptr; - } - }; - template - void release_allocated_memory(AllocatedMemory& memory) noexcept { - if (memory.ptr != nullptr) { - if constexpr (!is_trivially_destructible::value) { - memory.ptr->~T(); - } - BONGOCAT_SAFE_FREE(memory.ptr); - memory.ptr = nullptr; - memory._size_bytes = 0; - } +}; + +// ============================================================================= +// RAII CLEANUP +// ============================================================================= + +template +class AllocatedMemory; +template +void release_allocated_memory(AllocatedMemory& memory) noexcept; + +template +class AllocatedMemory { +public: + T *ptr{BONGOCAT_NULLPTR}; + size_t _size_bytes{0}; + + constexpr AllocatedMemory() = default; + ~AllocatedMemory() noexcept { + release_allocated_memory(*this); + } + + explicit AllocatedMemory(decltype(BONGOCAT_NULLPTR)) noexcept {} + AllocatedMemory& operator=(decltype(BONGOCAT_NULLPTR)) noexcept { + release_allocated_memory(*this); + return *this; + } + + AllocatedMemory(const AllocatedMemory& other) : _size_bytes(other._size_bytes) { + _size_bytes = sizeof(T); + if (other.ptr != BONGOCAT_NULLPTR && _size_bytes > 0) { + ptr = static_cast(BONGOCAT_MALLOC(_size_bytes)); + if (ptr != BONGOCAT_NULLPTR) { + if constexpr (is_trivially_copyable::value) { + memcpy(ptr, other.ptr, _size_bytes); + } else { + new (ptr) T(*other.ptr); + } + } else { + _size_bytes = 0; + ptr = BONGOCAT_NULLPTR; + BONGOCAT_LOG_ERROR("memory allocation failed"); + } + } else { + _size_bytes = 0; } - template - [[nodiscard]] inline static AllocatedMemory make_null_memory() noexcept { - return AllocatedMemory(); + } + AllocatedMemory& operator=(const AllocatedMemory& other) { + if (this != &other) { + release_allocated_memory(*this); + _size_bytes = sizeof(T); + if (other.ptr != BONGOCAT_NULLPTR && _size_bytes > 0) { + ptr = static_cast(BONGOCAT_MALLOC(_size_bytes)); + if (ptr) { + if constexpr (is_trivially_copyable::value) { + memcpy(ptr, other.ptr, _size_bytes); + } else { + new (ptr) T(*other.ptr); + } + } else { + _size_bytes = 0; + ptr = BONGOCAT_NULLPTR; + BONGOCAT_LOG_ERROR("memory allocation failed"); + } + } else { + _size_bytes = 0; + } } - template - [[nodiscard]] inline static AllocatedMemory make_allocated_memory() { - AllocatedMemory ret; - ret._size_bytes = sizeof(T); - if (ret._size_bytes > 0) { - ret.ptr = static_cast(BONGOCAT_MALLOC(ret._size_bytes)); - if (ret.ptr != nullptr) { - // default ctor - new (ret.ptr) T(); - return ret; - } else { - BONGOCAT_LOG_ERROR("memory allocation failed"); - } - } - ret._size_bytes = 0; - ret.ptr = nullptr; - return ret; + return *this; + } + + AllocatedMemory(AllocatedMemory&& other) noexcept : ptr(other.ptr), _size_bytes(other._size_bytes) { + other.ptr = BONGOCAT_NULLPTR; + other._size_bytes = 0; + } + AllocatedMemory& operator=(AllocatedMemory&& other) noexcept { + if (this != &other) { + release_allocated_memory(*this); + ptr = other.ptr; + _size_bytes = other._size_bytes; + other.ptr = BONGOCAT_NULLPTR; + other._size_bytes = 0; } + return *this; + } + + constexpr operator bool() const noexcept { + return ptr != BONGOCAT_NULLPTR; + } + + T& operator*() { + assert(ptr); + return *ptr; + } + constexpr const T& operator*() const { + assert(ptr); + return *ptr; + } + T *operator->() { + assert(ptr); + return ptr; + } + constexpr const T *operator->() const { + assert(ptr); + return ptr; + } + explicit operator T *() noexcept { + return ptr; + } + constexpr explicit operator const T *() const noexcept { + return ptr; + } + + constexpr bool operator==(decltype(BONGOCAT_NULLPTR)) const noexcept { + return ptr == BONGOCAT_NULLPTR; + } + constexpr bool operator!=(decltype(BONGOCAT_NULLPTR)) const noexcept { + return ptr != BONGOCAT_NULLPTR; + } +}; +template +void release_allocated_memory(AllocatedMemory& memory) noexcept { + if (memory.ptr != BONGOCAT_NULLPTR) { + if constexpr (!is_trivially_destructible::value) { + memory.ptr->~T(); + } + BONGOCAT_SAFE_FREE(memory.ptr); + memory.ptr = BONGOCAT_NULLPTR; + memory._size_bytes = 0; + } +} +template +BONGOCAT_NODISCARD inline static AllocatedMemory make_null_memory() noexcept { + return AllocatedMemory(); +} +template +BONGOCAT_NODISCARD inline static AllocatedMemory make_allocated_memory() { + AllocatedMemory ret; + ret._size_bytes = sizeof(T); + if (ret._size_bytes > 0) { + ret.ptr = static_cast(BONGOCAT_MALLOC(ret._size_bytes)); + if (ret.ptr != BONGOCAT_NULLPTR) { + // default ctor + new (ret.ptr) T(); + return ret; + } else { + BONGOCAT_LOG_ERROR("memory allocation failed"); + } + } + ret._size_bytes = 0; + ret.ptr = BONGOCAT_NULLPTR; + return ret; +} +template +class AllocatedArray; +template +void release_allocated_array(AllocatedArray& memory) noexcept; + +template +class AllocatedArray { +public: + T *data{BONGOCAT_NULLPTR}; + size_t count{0}; + size_t _size_bytes{0}; + + constexpr AllocatedArray() = default; + ~AllocatedArray() noexcept { + release_allocated_array(*this); + } + + explicit AllocatedArray(decltype(BONGOCAT_NULLPTR)) noexcept {} + AllocatedArray& operator=(decltype(BONGOCAT_NULLPTR)) noexcept { + release_allocated_array(*this); + return *this; + } + + explicit AllocatedArray(size_t p_count) : count(p_count), _size_bytes(sizeof(T) * count) { + if (_size_bytes > 0) { + data = static_cast(BONGOCAT_MALLOC(_size_bytes)); + if (data) { + return; + } else { + BONGOCAT_LOG_ERROR("malloc array failed: %zu bytes", _size_bytes); + } + } + count = 0; + _size_bytes = 0; + } + + AllocatedArray(const AllocatedArray& other) : count(other.count), _size_bytes(other._size_bytes) { + if (other.data && _size_bytes > 0) { + data = static_cast(BONGOCAT_MALLOC(_size_bytes)); + if (data) { + if constexpr (is_trivially_copyable::value) { + memcpy(data, other.data, _size_bytes); + } else { + for (size_t i = 0; i < other.count; i++) { + *data[i] = *other.data[i]; + } + } + return; + } else { + BONGOCAT_LOG_ERROR("file mmap failed in copy constructor"); + } + } - template - struct AllocatedArray; - template - void release_allocated_array(AllocatedArray& memory) noexcept; - - template - struct AllocatedArray { - T* data{nullptr}; - size_t count{0}; - size_t _size_bytes{0}; - - constexpr AllocatedArray() = default; - ~AllocatedArray() noexcept { - release_allocated_array(*this); - } - - explicit AllocatedArray(decltype(nullptr)) noexcept {} - AllocatedArray& operator=(decltype(nullptr)) noexcept { - release_allocated_array(*this); - return *this; - } - - explicit AllocatedArray(size_t p_count) - : count(p_count), _size_bytes(sizeof(T) * count) - { - if (_size_bytes > 0) { - data = static_cast(BONGOCAT_MALLOC(_size_bytes)); - if (data) { - return; - } else { - BONGOCAT_LOG_ERROR("malloc array failed: %zu bytes", _size_bytes); - } - } - count = 0; - _size_bytes = 0; - } - - AllocatedArray(const AllocatedArray& other) - : count(other.count), _size_bytes(other._size_bytes) - { - if (other.data && _size_bytes > 0) { - data = static_cast(BONGOCAT_MALLOC(_size_bytes)); - if (data) { - if constexpr (is_trivially_copyable::value) { - memcpy(data, other.data, _size_bytes); - } else { - for (size_t i = 0; i < other.count; i++) { - *data[i] = *other.data[i]; - } - } - return; - } else { - BONGOCAT_LOG_ERROR("file mmap failed in copy constructor"); - } - } - - count = 0; - _size_bytes = 0; - data = nullptr; - } - AllocatedArray& operator=(const AllocatedArray& other) { - if (this != &other) { - release_allocated_array(*this); - count = other.count; - _size_bytes = other._size_bytes; - if (other.data && _size_bytes > 0) { - data = static_cast(BONGOCAT_MALLOC(_size_bytes)); - if (data) { - if constexpr (is_trivially_copyable::value) { - memcpy(data, other.data, _size_bytes); - } else { - for (size_t i = 0; i < other.count; i++) { - *data[i] = *other.data[i]; - } - } - return *this; - } else { - BONGOCAT_LOG_ERROR("mmap buffer failed in copy assignment"); - } - } - - count = 0; - _size_bytes = 0; - data = nullptr; - } - return *this; - } - - AllocatedArray(AllocatedArray&& other) noexcept - : data(other.data), count(other.count), _size_bytes(other._size_bytes) - { - other.data = nullptr; - other.count = 0; - other._size_bytes = 0; - } - AllocatedArray& operator=(AllocatedArray&& other) noexcept { - if (this != &other) { - release_allocated_array(*this); - data = other.data; - count = other.count; - _size_bytes = other._size_bytes; - other.data = nullptr; - other.count = 0; - other._size_bytes = 0; - } - return *this; - } - - T& operator[](size_t index) { - assert(index < count); - return data[index]; - } - constexpr const T& operator[](size_t index) const { - assert(index < count); - return data[index]; - } - - constexpr explicit operator bool() const noexcept { - return data != nullptr; - } - - constexpr bool operator==(decltype(nullptr)) const noexcept { - return data == nullptr; - } - constexpr bool operator!=(decltype(nullptr)) const noexcept { - return data != nullptr; - } - }; - template - void release_allocated_array(AllocatedArray& memory) noexcept { - if (memory.data != nullptr) { - if constexpr (!is_trivially_destructible::value) { - for (size_t i = 0; i < memory.count; i++) { - memory.data[i].~T(); - } + count = 0; + _size_bytes = 0; + data = BONGOCAT_NULLPTR; + } + AllocatedArray& operator=(const AllocatedArray& other) { + if (this != &other) { + release_allocated_array(*this); + count = other.count; + _size_bytes = other._size_bytes; + if (other.data && _size_bytes > 0) { + data = static_cast(BONGOCAT_MALLOC(_size_bytes)); + if (data) { + if constexpr (is_trivially_copyable::value) { + memcpy(data, other.data, _size_bytes); + } else { + for (size_t i = 0; i < other.count; i++) { + *data[i] = *other.data[i]; } - BONGOCAT_SAFE_FREE(memory.data); - memory.data = nullptr; - memory.count = 0; - memory._size_bytes = 0; + } + return *this; + } else { + BONGOCAT_LOG_ERROR("mmap buffer failed in copy assignment"); } - } + } - template - [[nodiscard]] inline static AllocatedArray make_unallocated_array() noexcept { - return AllocatedArray(); + count = 0; + _size_bytes = 0; + data = BONGOCAT_NULLPTR; } - template - [[nodiscard]] inline static AllocatedArray make_allocated_array_uninitialized(size_t count) { - return count > 0 ? AllocatedArray(count) : AllocatedArray(); + return *this; + } + + AllocatedArray(AllocatedArray&& other) noexcept + : data(other.data) + , count(other.count) + , _size_bytes(other._size_bytes) { + other.data = BONGOCAT_NULLPTR; + other.count = 0; + other._size_bytes = 0; + } + AllocatedArray& operator=(AllocatedArray&& other) noexcept { + if (this != &other) { + release_allocated_array(*this); + data = other.data; + count = other.count; + _size_bytes = other._size_bytes; + other.data = BONGOCAT_NULLPTR; + other.count = 0; + other._size_bytes = 0; } - template - [[nodiscard]] inline static AllocatedArray make_allocated_array(size_t count) { - auto ret= count > 0 ? AllocatedArray(count) : AllocatedArray(); - for (size_t i = 0;i < ret.count;i++) { - new (&ret.data[i]) T(); - } - return ret; - } - template - [[nodiscard]] inline static AllocatedArray make_allocated_array_with_value(size_t count, const T& value) { - auto ret= count > 0 ? AllocatedArray(count) : AllocatedArray(); - for (size_t i = 0;i < ret.count;i++) { - ret.data[i] = value; - } - return ret; + return *this; + } + + T& operator[](size_t index) { + assert(index < count); + return data[index]; + } + constexpr const T& operator[](size_t index) const { + assert(index < count); + return data[index]; + } + + constexpr explicit operator bool() const noexcept { + return data != BONGOCAT_NULLPTR; + } + + constexpr bool operator==(decltype(BONGOCAT_NULLPTR)) const noexcept { + return data == BONGOCAT_NULLPTR; + } + constexpr bool operator!=(decltype(BONGOCAT_NULLPTR)) const noexcept { + return data != BONGOCAT_NULLPTR; + } +}; +template +void release_allocated_array(AllocatedArray& memory) noexcept { + if (memory.data != BONGOCAT_NULLPTR) { + if constexpr (!is_trivially_destructible::value) { + for (size_t i = 0; i < memory.count; i++) { + memory.data[i].~T(); + } } + BONGOCAT_SAFE_FREE(memory.data); + memory.data = BONGOCAT_NULLPTR; + memory.count = 0; + memory._size_bytes = 0; + } +} - // remove_reference implementation (no STL) - template struct remove_reference { typedef T type; }; - template struct remove_reference { typedef T type; }; - template struct remove_reference { typedef T type; }; +template +BONGOCAT_NODISCARD inline static AllocatedArray make_unallocated_array() noexcept { + return AllocatedArray(); +} +template +BONGOCAT_NODISCARD inline static AllocatedArray make_allocated_array_uninitialized(size_t count) { + return count > 0 ? AllocatedArray(count) : AllocatedArray(); +} +template +BONGOCAT_NODISCARD inline static AllocatedArray make_allocated_array(size_t count) { + auto ret = count > 0 ? AllocatedArray(count) : AllocatedArray(); + for (size_t i = 0; i < ret.count; i++) { + new (&ret.data[i]) T(); + } + return ret; +} +template +BONGOCAT_NODISCARD inline static AllocatedArray make_allocated_array_with_value(size_t count, const T& value) { + auto ret = count > 0 ? AllocatedArray(count) : AllocatedArray(); + for (size_t i = 0; i < ret.count; i++) { + ret.data[i] = value; + } + return ret; +} - // move implementation (no STL) - template - inline typename remove_reference::type&& move(T&& t) { - typedef typename remove_reference::type U; - return static_cast(t); - } +// remove_reference implementation (no STL) +template +struct remove_reference { + typedef T type; +}; +template +struct remove_reference { + typedef T type; +}; +template +struct remove_reference { + typedef T type; +}; + +// move implementation (no STL) +template +inline typename remove_reference::type&& move(T&& t) { + typedef typename remove_reference::type U; + return static_cast(t); } +} // namespace bongocat -#endif // BONGOCAT_MEMORY_H \ No newline at end of file +#endif // BONGOCAT_MEMORY_H \ No newline at end of file diff --git a/include/utils/random.h b/include/utils/random.h index 4b36188c..cffd16ce 100644 --- a/include/utils/random.h +++ b/include/utils/random.h @@ -1,9 +1,9 @@ #ifndef BONGOCAT_RANDOM_H_ #define BONGOCAT_RANDOM_H_ -#include -#include #include +#include +#include namespace bongocat::platform { @@ -64,7 +64,9 @@ class random_xoshiro128 { /// Constructor with seed value. ///\param seed The new seed value. //*************************************************************************** - constexpr explicit random_xoshiro128(uint32_t seed) { initialise(seed); } + constexpr explicit random_xoshiro128(uint32_t seed) { + initialise(seed); + } //*************************************************************************** /// Initialises the sequence with a new seed value. @@ -81,7 +83,9 @@ class random_xoshiro128 { //*************************************************************************** /// Get the next random_xoshiro128 number. //*************************************************************************** - [[nodiscard]] constexpr uint32_t operator()() { return next(); } + [[nodiscard]] constexpr uint32_t operator()() { + return next(); + } //*************************************************************************** /// Get the next random_xoshiro128 number in a specified inclusive range. @@ -95,7 +99,9 @@ class random_xoshiro128 { return range(0, UINT32_MAX); } - [[nodiscard]] constexpr inline uint32_t range_min(uint32_t min) { return range(min, UINT32_MAX); } + [[nodiscard]] constexpr inline uint32_t range_min(uint32_t min) { + return range(min, UINT32_MAX); + } private: uint32_t state[4]{}; @@ -113,7 +119,9 @@ class random_xoshiro128 { The state must be seeded so that it is not everywhere zero. */ - static inline constexpr uint32_t rotl(const uint32_t x, int k) { return (x << k) | (x >> (32 - k)); } + static inline constexpr uint32_t rotl(const uint32_t x, int k) { + return (x << k) | (x >> (32 - k)); + } constexpr uint32_t next() { const uint32_t result = rotl(state[1] * 5, 7) * 9; @@ -132,7 +140,6 @@ class random_xoshiro128 { return result; } - /* This is the jump function for the generator. It is equivalent to 2^64 calls to next(); it can be used to generate 2^64 non-overlapping subsequences for parallel computations. */ @@ -163,7 +170,6 @@ class random_xoshiro128 { } */ - /* This is the long-jump function for the generator. It is equivalent to 2^96 calls to next(); it can be used to generate 2^32 starting points, from each of which jump() will generate 2^32 non-overlapping @@ -199,6 +205,6 @@ class random_xoshiro128 { // for seeding extern uint32_t slow_rand(); -} +} // namespace bongocat::platform #endif \ No newline at end of file diff --git a/include/utils/system_memory.h b/include/utils/system_memory.h index f60c5793..67cfa5c7 100644 --- a/include/utils/system_memory.h +++ b/include/utils/system_memory.h @@ -5,1206 +5,1184 @@ #include "./time.h" #include "core/bongocat.h" #include "utils/error.h" -#include -#include -#include -#include + #include #include +#include +#include +#include #include #include - +#include namespace bongocat::platform { - int join_thread_with_timeout(pthread_t& thread, time_ms_t timeout_ms); - int stop_thread_graceful_or_cancel(pthread_t& thread, atomic_bool& running_flag); - - struct Mutex { - pthread_mutex_t pt_mutex{}; - - Mutex() { - pthread_mutexattr_t attr; - pthread_mutexattr_init(&attr); - pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_ERRORCHECK); - if (pthread_mutex_init(&pt_mutex, &attr) != 0) { - BONGOCAT_LOG_ERROR("Failed to initialize mutex"); - } - pthread_mutexattr_destroy(&attr); - } - ~Mutex() { - int rc = pthread_mutex_destroy(&pt_mutex); - if (rc == EBUSY) { - // still locked → try to unlock first - rc = pthread_mutex_unlock(&pt_mutex); - if (rc != 0) { - BONGOCAT_LOG_ERROR("pthread_mutex_unlock in destructor failed"); - } - pthread_mutex_destroy(&pt_mutex); - } else if (rc != 0) { - BONGOCAT_LOG_ERROR("pthread_mutex_destroy failed"); - } - } - - Mutex(const Mutex&) = delete; - Mutex& operator=(const Mutex&) = delete; - Mutex(Mutex&&) = delete; - Mutex& operator=(Mutex&&) = delete; - - void _lock() { - if (const int rc = pthread_mutex_lock(&pt_mutex); rc != 0) { - BONGOCAT_LOG_ERROR("pthread_mutex_lock failed"); - } - } - void _unlock() { - if (const int rc = pthread_mutex_unlock(&pt_mutex); rc != 0) { - if (rc != EPERM) { // ignore "not owner" - BONGOCAT_LOG_ERROR("pthread_mutex_unlock failed"); - } - } - } - - /* - explicit operator pthread_mutex_t() const noexcept { - return pt_mutex; - } - */ - }; - struct LockGuard { - explicit LockGuard(Mutex& m) : pt_mutex(&m.pt_mutex) { - if (const int rc = pthread_mutex_lock(pt_mutex); rc != 0) { - BONGOCAT_LOG_ERROR("LockGuard: pthread_mutex_lock failed"); - } - } - explicit LockGuard(pthread_mutex_t& m) : pt_mutex(&m) { - if (const int rc = pthread_mutex_lock(pt_mutex); rc != 0) { - BONGOCAT_LOG_ERROR("LockGuard: pthread_mutex_lock failed"); - } - } - ~LockGuard() { - if (const int rc = pthread_mutex_unlock(pt_mutex); rc != 0) { - BONGOCAT_LOG_ERROR("LockGuard: pthread_mutex_unlock failed"); - } - } - - // No copying, no move - LockGuard(const LockGuard&) = delete; - LockGuard& operator=(const LockGuard&) = delete; - LockGuard(const LockGuard&&) = delete; - LockGuard&& operator=(const LockGuard&&) = delete; - - pthread_mutex_t *pt_mutex{nullptr}; - }; - - struct SingleCondVariable; - void cond_destroy(SingleCondVariable& cond); - struct SingleCondVariable { - Mutex mutex; - pthread_cond_t cond; - atomic_bool _predicate{false}; - bool _inited{false}; - - SingleCondVariable() { - pthread_cond_init(&cond, nullptr); - _inited = true; - } - - ~SingleCondVariable() { - cond_destroy(*this); - } - - // No copying, no move - SingleCondVariable(const SingleCondVariable&) = delete; - SingleCondVariable& operator=(const SingleCondVariable&) = delete; - SingleCondVariable(const SingleCondVariable&&) = delete; - SingleCondVariable&& operator=(const SingleCondVariable&&) = delete; - }; - inline void cond_destroy(SingleCondVariable& cond) { - atomic_store(&cond._predicate, true); - if (cond._inited) pthread_cond_broadcast(&cond.cond); - if (cond._inited) pthread_cond_destroy(&cond.cond); - cond._inited = false; +int join_thread_with_timeout(pthread_t& thread, time_ms_t timeout_ms); +int stop_thread_graceful_or_cancel(pthread_t& thread, atomic_bool& running_flag); + +class Mutex { +public: + pthread_mutex_t pt_mutex{}; + + Mutex() { + pthread_mutexattr_t attr; + pthread_mutexattr_init(&attr); + pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_ERRORCHECK); + if (pthread_mutex_init(&pt_mutex, &attr) != 0) { + BONGOCAT_LOG_ERROR("Failed to initialize mutex"); } + pthread_mutexattr_destroy(&attr); + } + ~Mutex() { + int rc = pthread_mutex_destroy(&pt_mutex); + if (rc == EBUSY) { + // still locked → try to unlock first + rc = pthread_mutex_unlock(&pt_mutex); + if (rc != 0) { + BONGOCAT_LOG_ERROR("pthread_mutex_unlock in destructor failed"); + } + pthread_mutex_destroy(&pt_mutex); + } else if (rc != 0) { + BONGOCAT_LOG_ERROR("pthread_mutex_destroy failed"); + } + } - struct CondVariable { - CondVariable() { - pthread_mutex_init(&_mutex, nullptr); - pthread_cond_init(&_cond, nullptr); - } - - // No copying, no move - CondVariable(const CondVariable&) = delete; - CondVariable& operator=(const CondVariable&) = delete; - CondVariable(const CondVariable&&) = delete; - CondVariable&& operator=(const CondVariable&&) = delete; - - ~CondVariable() { - pthread_cond_broadcast(&_cond); - pthread_cond_destroy(&_cond); - pthread_mutex_destroy(&_mutex); - } - - template - [[deprecated("better use timedwait")]] int wait(Predicate&& pred) { - int ret = 0; - pthread_mutex_lock(&_mutex); - while (!pred()) { - ret = pthread_cond_wait(&_cond, &_mutex); - } - pthread_mutex_unlock(&_mutex); - return ret; - } - - template - int timedwait(Predicate&& pred, time_ms_t timeout_ms) { - struct timespec ts; - clock_gettime(CLOCK_REALTIME, &ts); - ts.tv_sec += timeout_ms / 1000; - ts.tv_nsec += (timeout_ms % 1000) * 1000000LL; - // normalize time - if (ts.tv_nsec >= 1000000000LL) { - ts.tv_sec++; - ts.tv_nsec -= 1000000000LL; - } - - int ret = 0; - pthread_mutex_lock(&_mutex); - while (!pred()) { - ret = pthread_cond_timedwait(&_cond, &_mutex, &ts); - if (ret == ETIMEDOUT) { - pthread_mutex_unlock(&_mutex); - return ret; - } - } - pthread_mutex_unlock(&_mutex); - return ret; - } - - void notify_all() { - pthread_mutex_lock(&_mutex); - pthread_cond_broadcast(&_cond); - pthread_mutex_unlock(&_mutex); - } - - pthread_mutex_t _mutex; - pthread_cond_t _cond; - }; - - struct CondVarGuard { - explicit CondVarGuard(pthread_mutex_t& m, pthread_cond_t& c, atomic_bool& pred) - : _mutex(m), _cond(c), _predicate(pred) - { - pthread_mutex_lock(&_mutex); - } - explicit CondVarGuard(SingleCondVariable& cond) - : _mutex(cond.mutex.pt_mutex), _cond(cond.cond), _predicate(cond._predicate) - { - pthread_mutex_lock(&_mutex); - } - - ~CondVarGuard() { - pthread_mutex_unlock(&_mutex); - } - - // No copying, no move - CondVarGuard(const CondVarGuard&) = delete; - CondVariable& operator=(const CondVarGuard&) = delete; - CondVarGuard(const CondVarGuard&&) = delete; - CondVarGuard&& operator=(const CondVarGuard&&) = delete; - - // Wait until predicate becomes true - [[deprecated("better use timedwait")]] int wait() { - int ret = 0; - while (!atomic_load(&_predicate)) { - ret = pthread_cond_wait(&_cond, &_mutex); - } - return ret; - } - - int timedwait(time_ms_t timeout) { - struct timespec ts; - clock_gettime(CLOCK_REALTIME, &ts); - ts.tv_sec += timeout / 1000; - ts.tv_nsec += (timeout % 1000) * 1000000LL; - // normalize time - if (ts.tv_nsec >= 1000000000LL) { - ts.tv_sec++; - ts.tv_nsec -= 1000000000LL; - } - - int ret = 0; - while (!atomic_load(&_predicate)) { - ret = pthread_cond_timedwait(&_cond, &_mutex, &ts); - } - return ret; - } - - // Set predicate and signal all waiting threads - void notify() { - atomic_store(&_predicate, true); - pthread_cond_broadcast(&_cond); - } - - pthread_mutex_t& _mutex; - pthread_cond_t& _cond; - atomic_bool& _predicate; - }; - - - template - struct MMapMemory; - template - void release_allocated_mmap_memory(MMapMemory& memory) noexcept; - - template - struct MMapMemory { - T* ptr{nullptr}; - size_t _size_bytes{0}; - - constexpr MMapMemory() = default; - ~MMapMemory() noexcept { - release_allocated_mmap_memory(*this); - } - - explicit MMapMemory(decltype(nullptr)) noexcept {} - MMapMemory& operator=(decltype(nullptr)) noexcept { - release_allocated_mmap_memory(*this); - return *this; - } - - MMapMemory(const MMapMemory& other) - : _size_bytes(other._size_bytes) - { - if (other.ptr && _size_bytes > 0) { - ptr = static_cast(mmap(nullptr, _size_bytes, - PROT_READ | PROT_WRITE, - MAP_SHARED | MAP_ANONYMOUS, - -1, 0)); - if (ptr != MAP_FAILED) { - if constexpr (is_trivially_copyable::value) { - memcpy(ptr, other.ptr, _size_bytes); - } else { - // assign/copy - new (ptr) T(*other.ptr); - } - return; - } else { - BONGOCAT_LOG_ERROR("mmap failed in copy constructor"); - } - } - _size_bytes = 0; - ptr = nullptr; - } - MMapMemory& operator=(const MMapMemory& other) { - if (this != &other) { - release_allocated_mmap_memory(*this); - _size_bytes = other._size_bytes; - if (_size_bytes > 0) { - ptr = static_cast(mmap(nullptr, _size_bytes, - PROT_READ | PROT_WRITE, - MAP_SHARED | MAP_ANONYMOUS, - -1, 0)); - if (ptr != MAP_FAILED) { - if constexpr (is_trivially_copyable::value) { - memcpy(ptr, other.ptr, _size_bytes); - } else { - // assign/copy - new (ptr) T(*other.ptr); - } - return *this; - } else { - BONGOCAT_LOG_ERROR("mmap failed in copy assignment"); - } - } - _size_bytes = 0; - ptr = nullptr; - } - return *this; - } - - MMapMemory(MMapMemory&& other) noexcept - : ptr(other.ptr), _size_bytes(other._size_bytes) - { - other.ptr = nullptr; - other._size_bytes = 0; - } - MMapMemory& operator=(MMapMemory&& other) noexcept { - if (this != &other) { - release_allocated_mmap_memory(*this); - ptr = other.ptr; - _size_bytes = other._size_bytes; - other.ptr = nullptr; - other._size_bytes = 0; - } - return *this; - } - - T& operator*() { - assert(ptr && ptr != MAP_FAILED); - return *ptr; - } - constexpr const T& operator*() const { - assert(ptr && ptr != MAP_FAILED); - return *ptr; - } - T* operator->() { - assert(ptr && ptr != MAP_FAILED); - return ptr; - } - constexpr const T* operator->() const { - assert(ptr && ptr != MAP_FAILED); - return ptr; - } - explicit operator T*() noexcept { - return ptr; - } - constexpr explicit operator const T*() const noexcept { - return ptr; - } - - constexpr explicit operator bool() const noexcept { - return ptr != nullptr && ptr != MAP_FAILED; - } + Mutex(const Mutex&) = delete; + Mutex& operator=(const Mutex&) = delete; + Mutex(Mutex&&) = delete; + Mutex& operator=(Mutex&&) = delete; - constexpr bool operator==(decltype(nullptr)) const noexcept { - return ptr == nullptr; - } - constexpr bool operator!=(decltype(nullptr)) const noexcept { - return ptr != nullptr; - } - }; - template - void release_allocated_mmap_memory(MMapMemory& memory) noexcept { - if (memory.ptr != nullptr) { - if constexpr (!is_trivially_destructible::value) { - memory.ptr->~T(); - } - munmap(memory.ptr, memory._size_bytes); - memory.ptr = nullptr; - memory._size_bytes = 0; - } + void _lock() { + if (const int rc = pthread_mutex_lock(&pt_mutex); rc != 0) { + BONGOCAT_LOG_ERROR("pthread_mutex_lock failed"); } - template - [[nodiscard]] inline static MMapMemory make_unallocated_mmap() noexcept { - return MMapMemory(); + } + void _unlock() { + if (const int rc = pthread_mutex_unlock(&pt_mutex); rc != 0) { + if (rc != EPERM) { // ignore "not owner" + BONGOCAT_LOG_ERROR("pthread_mutex_unlock failed"); + } } - // Allocate shared memory using mmap - template - [[nodiscard]] inline static MMapMemory make_allocated_mmap() { - MMapMemory ret; - ret._size_bytes = sizeof(T); - if (ret._size_bytes > 0) { - ret.ptr = static_cast(mmap(nullptr, ret._size_bytes, - PROT_READ | PROT_WRITE, - MAP_SHARED | MAP_ANONYMOUS, - -1, 0)); - if (ret.ptr && ret.ptr != MAP_FAILED) { - // default ctor - new (ret.ptr) T(); - return ret; - } else { - BONGOCAT_LOG_ERROR("mmap failed"); - } - } - ret.ptr = nullptr; - ret._size_bytes = 0; - return ret; + } + + /* + explicit operator pthread_mutex_t() const noexcept { + return pt_mutex; + } + */ +}; + +class LockGuard { +public: + explicit LockGuard(Mutex& m) : pt_mutex(&m.pt_mutex) { + if (const int rc = pthread_mutex_lock(pt_mutex); rc != 0) { + BONGOCAT_LOG_ERROR("LockGuard: pthread_mutex_lock failed"); } - template - [[nodiscard]] inline static MMapMemory make_unallocated_mmap_value(const T& value) { - auto ret = make_allocated_mmap(); - if (ret.ptr != nullptr) { - *ret.ptr = value; - } - return ret; + } + explicit LockGuard(pthread_mutex_t& m) : pt_mutex(&m) { + if (const int rc = pthread_mutex_lock(pt_mutex); rc != 0) { + BONGOCAT_LOG_ERROR("LockGuard: pthread_mutex_lock failed"); } - - - template - struct MMapArray; - template - void release_allocated_mmap_array(MMapArray& memory) noexcept; - - template - struct MMapArray { - T* data{nullptr}; - size_t count{0}; - size_t _size_bytes{0}; - - constexpr MMapArray() = default; - ~MMapArray() noexcept { - release_allocated_mmap_array(*this); - } - - explicit MMapArray(decltype(nullptr)) noexcept {} - MMapArray& operator=(decltype(nullptr)) noexcept { - release_allocated_mmap_array(*this); - return *this; - } - - // Allocate shared memory using mmap and count - explicit MMapArray(size_t p_count) - : count(p_count), _size_bytes(sizeof(T) * count) - { - if (_size_bytes > 0) { - data = static_cast(mmap(nullptr, _size_bytes, - PROT_READ | PROT_WRITE, - MAP_SHARED | MAP_ANONYMOUS, - -1, 0)); - if (data != MAP_FAILED) { - return; - } else { - BONGOCAT_LOG_ERROR("mmap buffer failed"); - } - } - data = nullptr; - count = 0; - _size_bytes = 0; - } - - MMapArray(const MMapArray& other) - : count(other.count), _size_bytes(other._size_bytes) - { - if (other.data && _size_bytes > 0) { - data = static_cast(mmap(nullptr, _size_bytes, - PROT_READ | PROT_WRITE, - MAP_SHARED | MAP_ANONYMOUS, - -1, 0)); - if (data != MAP_FAILED) { - if constexpr (is_trivially_copyable::value) { - memcpy(data, other.data, _size_bytes); - } else { - for (size_t i = 0; i < other.count; i++) { - *data[i] = *other.data[i]; - } - } - return; - } else { - BONGOCAT_LOG_ERROR("file mmap failed in copy constructor"); - } - } - data = nullptr; - count = 0; - _size_bytes = 0; - } - MMapArray& operator=(const MMapArray& other) { - if (this != &other) { - release_allocated_mmap_array(*this); - count = other.count; - _size_bytes = other._size_bytes; - if (other.data && _size_bytes > 0) { - data = static_cast(mmap(nullptr, _size_bytes, - PROT_READ | PROT_WRITE, - MAP_SHARED | MAP_ANONYMOUS, - -1, 0)); - if (data != MAP_FAILED) { - if constexpr (is_trivially_copyable::value) { - memcpy(data, other.data, _size_bytes); - } else { - for (size_t i = 0; i < other.count; i++) { - data[i] = other.data[i]; - } - } - return *this; - } else { - BONGOCAT_LOG_ERROR("mmap buffer failed in copy assignment"); - } - } - data = nullptr; - count = 0; - _size_bytes = 0; - } - return *this; - } - - MMapArray(MMapArray&& other) noexcept - : data(other.data), count(other.count), _size_bytes(other._size_bytes) - { - other.data = nullptr; - other.count = 0; - other._size_bytes = 0; - } - MMapArray& operator=(MMapArray&& other) noexcept { - if (this != &other) { - release_allocated_mmap_array(*this); - data = other.data; - count = other.count; - _size_bytes = other._size_bytes; - other.data = nullptr; - other.count = 0; - other._size_bytes = 0; - } - return *this; - } - - - T& operator[](size_t index) { - assert(index < count); - return data[index]; - } - constexpr const T& operator[](size_t index) const { - assert(index < count); - return data[index]; - } - - constexpr explicit operator bool() const noexcept { - return data != nullptr && data != MAP_FAILED; - } - - constexpr bool operator==(decltype(nullptr)) const noexcept { - return data == nullptr; - } - constexpr bool operator!=(decltype(nullptr)) const noexcept { - return data != nullptr; - } - }; - template - void release_allocated_mmap_array(MMapArray& memory) noexcept { - if (memory.data) { - if constexpr (!is_trivially_destructible::value) { - for (size_t i = 0; i < memory.count; i++) { - memory.data[i].~T(); - } - } - munmap(memory.data, memory._size_bytes); - memory.data = nullptr; - memory.count = 0; - memory._size_bytes = 0; - } + } + ~LockGuard() { + if (const int rc = pthread_mutex_unlock(pt_mutex); rc != 0) { + BONGOCAT_LOG_ERROR("LockGuard: pthread_mutex_unlock failed"); } - template - [[nodiscard]] inline static MMapArray make_unallocated_mmap_array() noexcept { - return MMapArray(); + } + + // No copying, no move + LockGuard(const LockGuard&) = delete; + LockGuard& operator=(const LockGuard&) = delete; + LockGuard(const LockGuard&&) = delete; + LockGuard&& operator=(const LockGuard&&) = delete; + + pthread_mutex_t *pt_mutex{BONGOCAT_NULLPTR}; +}; + +class SingleCondVariable; +void cond_destroy(SingleCondVariable& cond); +class SingleCondVariable { +public: + Mutex mutex; + pthread_cond_t cond; + atomic_bool _predicate{false}; + bool _inited{false}; + + SingleCondVariable() { + pthread_cond_init(&cond, BONGOCAT_NULLPTR); + _inited = true; + } + + ~SingleCondVariable() { + cond_destroy(*this); + } + + // No copying, no move + SingleCondVariable(const SingleCondVariable&) = delete; + SingleCondVariable& operator=(const SingleCondVariable&) = delete; + SingleCondVariable(const SingleCondVariable&&) = delete; + SingleCondVariable&& operator=(const SingleCondVariable&&) = delete; +}; +inline void cond_destroy(SingleCondVariable& cond) { + atomic_store(&cond._predicate, true); + if (cond._inited) + pthread_cond_broadcast(&cond.cond); + if (cond._inited) + pthread_cond_destroy(&cond.cond); + cond._inited = false; +} + +class CondVariable { +public: + CondVariable() { + pthread_mutex_init(&_mutex, BONGOCAT_NULLPTR); + pthread_cond_init(&_cond, BONGOCAT_NULLPTR); + } + + // No copying, no move + CondVariable(const CondVariable&) = delete; + CondVariable& operator=(const CondVariable&) = delete; + CondVariable(const CondVariable&&) = delete; + CondVariable&& operator=(const CondVariable&&) = delete; + + ~CondVariable() { + pthread_cond_broadcast(&_cond); + pthread_cond_destroy(&_cond); + pthread_mutex_destroy(&_mutex); + } + + template + [[deprecated("better use timedwait")]] int wait(Predicate&& pred) { + int ret = 0; + pthread_mutex_lock(&_mutex); + while (!pred()) { + ret = pthread_cond_wait(&_cond, &_mutex); } - template - [[nodiscard]] inline static MMapArray make_allocated_mmap_array_uninitialized(size_t count) { - return count > 0? MMapArray(count) : MMapArray(); + pthread_mutex_unlock(&_mutex); + return ret; + } + + template + int timedwait(Predicate&& pred, time_ms_t timeout_ms) { + struct timespec ts; + clock_gettime(CLOCK_REALTIME, &ts); + ts.tv_sec += timeout_ms / 1000; + ts.tv_nsec += (timeout_ms % 1000) * 1000000LL; + // normalize time + if (ts.tv_nsec >= 1000000000LL) { + ts.tv_sec++; + ts.tv_nsec -= 1000000000LL; } - template - [[nodiscard]] inline static MMapArray make_allocated_mmap_array(size_t count) { - auto ret= count > 0 ? MMapArray(count) : MMapArray(); - for (size_t i = 0;i < ret.count;i++) { - new (&ret.data[i]) T(); - } + + int ret = 0; + pthread_mutex_lock(&_mutex); + while (!pred()) { + ret = pthread_cond_timedwait(&_cond, &_mutex, &ts); + if (ret == ETIMEDOUT) { + pthread_mutex_unlock(&_mutex); return ret; + } + } + pthread_mutex_unlock(&_mutex); + return ret; + } + + void notify_all() { + pthread_mutex_lock(&_mutex); + pthread_cond_broadcast(&_cond); + pthread_mutex_unlock(&_mutex); + } + + pthread_mutex_t _mutex; + pthread_cond_t _cond; +}; + +class CondVarGuard { +public: + explicit CondVarGuard(pthread_mutex_t& m, pthread_cond_t& c, atomic_bool& pred) + : _mutex(m) + , _cond(c) + , _predicate(pred) { + pthread_mutex_lock(&_mutex); + } + explicit CondVarGuard(SingleCondVariable& cond) + : _mutex(cond.mutex.pt_mutex) + , _cond(cond.cond) + , _predicate(cond._predicate) { + pthread_mutex_lock(&_mutex); + } + + ~CondVarGuard() { + pthread_mutex_unlock(&_mutex); + } + + // No copying, no move + CondVarGuard(const CondVarGuard&) = delete; + CondVariable& operator=(const CondVarGuard&) = delete; + CondVarGuard(const CondVarGuard&&) = delete; + CondVarGuard&& operator=(const CondVarGuard&&) = delete; + + // Wait until predicate becomes true + [[deprecated("better use timedwait")]] int wait() { + int ret = 0; + while (!atomic_load(&_predicate)) { + ret = pthread_cond_wait(&_cond, &_mutex); + } + return ret; + } + + int timedwait(time_ms_t timeout) { + struct timespec ts; + clock_gettime(CLOCK_REALTIME, &ts); + ts.tv_sec += timeout / 1000; + ts.tv_nsec += (timeout % 1000) * 1000000LL; + // normalize time + if (ts.tv_nsec >= 1000000000LL) { + ts.tv_sec++; + ts.tv_nsec -= 1000000000LL; } - template - struct MMapFile; - template - void release_allocated_mmap_file(MMapFile& memory) noexcept; - - template - struct MMapFile { - T* ptr{nullptr}; - size_t _size_bytes{0}; - int _fd{-1}; - off_t _offset{0}; - - constexpr MMapFile() = default; - ~MMapFile() noexcept { - release_allocated_mmap_file(*this); - } - - explicit MMapFile(decltype(nullptr)) noexcept {} - MMapFile& operator=(decltype(nullptr)) noexcept { - release_allocated_mmap_file(*this); - return *this; - } - - explicit MMapFile(int fd, off_t offset = 0) - : _size_bytes(sizeof(T)), _fd(fd), _offset(offset) - { - if (_size_bytes > 0) { - ptr = mmap(nullptr, _size_bytes, - PROT_READ | PROT_WRITE, - MAP_SHARED, - _fd, _offset); - if (ptr != MAP_FAILED) { - return; - } else { - BONGOCAT_LOG_ERROR("mmap failed to map file"); - } - } - ptr = nullptr; - _size_bytes = 0; - } - - MMapFile(const MMapFile& other) - : _size_bytes(other._size_bytes), _fd(other._fd), _offset(other._offset) - { - if (other.ptr && _size_bytes > 0) { - ptr = mmap(nullptr, _size_bytes, - PROT_READ | PROT_WRITE, - MAP_SHARED, - _fd, _offset); - if (ptr != MAP_FAILED) { - if constexpr (is_trivially_copyable::value) { - memcpy(ptr, other.ptr, _size_bytes); - } else { - new (ptr) T(*other.ptr); - } - return; - } else { - BONGOCAT_LOG_ERROR("file mmap failed in copy constructor"); - } - } - ptr = nullptr; - _size_bytes = 0; - } - MMapFile& operator=(const MMapFile& other) { - if (this != &other) { - release_allocated_mmap_file(*this); - _size_bytes = other._size_bytes; - _fd = other._fd; - _offset = other._offset; - if (other.ptr && _size_bytes > 0) { - ptr = mmap(nullptr, _size_bytes, - PROT_READ | PROT_WRITE, - MAP_SHARED, - _fd, _offset); - if (ptr) { - if constexpr (is_trivially_copyable::value) { - memcpy(ptr, other.ptr, _size_bytes); - } else { - new (ptr) T(*other.ptr); - } - return *this; - } else { - BONGOCAT_LOG_ERROR("file mmap failed in copy assignment"); - } - } - ptr = nullptr; - _size_bytes = 0; - } - return *this; - } - - MMapFile(MMapFile&& other) noexcept - : ptr(other.ptr), _size_bytes(other._size_bytes), _fd(other._fd), _offset(other._offset) - { - other.ptr = nullptr; - other._size_bytes = 0; - other._fd = -1; - other._offset = 0; - } - MMapFile& operator=(MMapFile&& other) noexcept { - if (this != &other) { - release_allocated_mmap_file(*this); - ptr = other.ptr; - _size_bytes = other._size_bytes; - _fd = other._fd; - _offset = other._offset; - other.ptr = nullptr; - other._size_bytes = 0; - other._fd = -1; - other._offset = 0; - } - return *this; - } - - T& operator*() { - assert(ptr); - return *ptr; - } - - T* operator->() { - return ptr; - } - - constexpr explicit operator bool() const noexcept { - return ptr != nullptr && ptr != MAP_FAILED; - } - - constexpr bool operator==(decltype(nullptr)) const noexcept { - return ptr == nullptr; - } - constexpr bool operator!=(decltype(nullptr)) const noexcept { - return ptr != nullptr; - } - }; - template - void release_allocated_mmap_file(MMapFile& memory) noexcept { - if (memory.ptr) { - if constexpr (!is_trivially_destructible::value) { - memory.ptr->~T(); - } - munmap(memory.ptr, memory._size_bytes); - memory.ptr = nullptr; - memory._size_bytes = 0; - memory._fd = -1; - memory._offset = 0; - } + int ret = 0; + while (!atomic_load(&_predicate)) { + ret = pthread_cond_timedwait(&_cond, &_mutex, &ts); } - template - [[nodiscard]] inline static MMapFile make_unallocated_mmap_file() noexcept { - return MMapFile(); + return ret; + } + + // Set predicate and signal all waiting threads + void notify() { + atomic_store(&_predicate, true); + pthread_cond_broadcast(&_cond); + } + + pthread_mutex_t& _mutex; + pthread_cond_t& _cond; + atomic_bool& _predicate; +}; + +template +class MMapMemory; +template +void release_allocated_mmap_memory(MMapMemory& memory) noexcept; + +template +class MMapMemory { +public: + T *ptr{BONGOCAT_NULLPTR}; + size_t _size_bytes{0}; + + constexpr MMapMemory() = default; + ~MMapMemory() noexcept { + release_allocated_mmap_memory(*this); + } + + explicit MMapMemory(decltype(BONGOCAT_NULLPTR)) noexcept {} + MMapMemory& operator=(decltype(BONGOCAT_NULLPTR)) noexcept { + release_allocated_mmap_memory(*this); + return *this; + } + + MMapMemory(const MMapMemory& other) : _size_bytes(other._size_bytes) { + if (other.ptr && _size_bytes > 0) { + ptr = static_cast( + mmap(BONGOCAT_NULLPTR, _size_bytes, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANONYMOUS, -1, 0)); + if (ptr != MAP_FAILED) { + if constexpr (is_trivially_copyable::value) { + memcpy(ptr, other.ptr, _size_bytes); + } else { + // assign/copy + new (ptr) T(*other.ptr); + } + return; + } else { + BONGOCAT_LOG_ERROR("mmap failed in copy constructor"); + } } - template - [[nodiscard]] inline static MMapFile make_allocated_mmap_file_uninitialized(int fd, off_t offset = 0) { - return MMapFile(fd, offset); + _size_bytes = 0; + ptr = BONGOCAT_NULLPTR; + } + MMapMemory& operator=(const MMapMemory& other) { + if (this != &other) { + release_allocated_mmap_memory(*this); + _size_bytes = other._size_bytes; + if (_size_bytes > 0) { + ptr = static_cast( + mmap(BONGOCAT_NULLPTR, _size_bytes, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANONYMOUS, -1, 0)); + if (ptr != MAP_FAILED) { + if constexpr (is_trivially_copyable::value) { + memcpy(ptr, other.ptr, _size_bytes); + } else { + // assign/copy + new (ptr) T(*other.ptr); + } + return *this; + } else { + BONGOCAT_LOG_ERROR("mmap failed in copy assignment"); + } + } + _size_bytes = 0; + ptr = BONGOCAT_NULLPTR; } - template - [[nodiscard]] inline static MMapFile make_allocated_mmap_file_defaulted(int fd, off_t offset = 0) { - auto ret = MMapFile(fd, offset); - if (ret.ptr) { - new (ret.ptr) T(); - } - return ret; + return *this; + } + + MMapMemory(MMapMemory&& other) noexcept : ptr(other.ptr), _size_bytes(other._size_bytes) { + other.ptr = BONGOCAT_NULLPTR; + other._size_bytes = 0; + } + MMapMemory& operator=(MMapMemory&& other) noexcept { + if (this != &other) { + release_allocated_mmap_memory(*this); + ptr = other.ptr; + _size_bytes = other._size_bytes; + other.ptr = BONGOCAT_NULLPTR; + other._size_bytes = 0; } - template - [[nodiscard]] inline static MMapFile make_allocated_mmap_file_value(const T& value, int fd, off_t offset = 0) { - auto ret = MMapFile(fd, offset); - for (size_t i = 0;i < ret.size;i++) { - *ret.ptr = value; - } - return ret; + return *this; + } + + T& operator*() { + assert(ptr && ptr != MAP_FAILED); + return *ptr; + } + constexpr const T& operator*() const { + assert(ptr && ptr != MAP_FAILED); + return *ptr; + } + T *operator->() { + assert(ptr && ptr != MAP_FAILED); + return ptr; + } + constexpr const T *operator->() const { + assert(ptr && ptr != MAP_FAILED); + return ptr; + } + explicit operator T *() noexcept { + return ptr; + } + constexpr explicit operator const T *() const noexcept { + return ptr; + } + + constexpr explicit operator bool() const noexcept { + return ptr != BONGOCAT_NULLPTR && ptr != MAP_FAILED; + } + + constexpr bool operator==(decltype(BONGOCAT_NULLPTR)) const noexcept { + return ptr == BONGOCAT_NULLPTR; + } + constexpr bool operator!=(decltype(BONGOCAT_NULLPTR)) const noexcept { + return ptr != BONGOCAT_NULLPTR; + } +}; +template +void release_allocated_mmap_memory(MMapMemory& memory) noexcept { + if (memory.ptr != BONGOCAT_NULLPTR) { + if constexpr (!is_trivially_destructible::value) { + memory.ptr->~T(); } + munmap(memory.ptr, memory._size_bytes); + memory.ptr = BONGOCAT_NULLPTR; + memory._size_bytes = 0; + } +} +template +BONGOCAT_NODISCARD inline static MMapMemory make_unallocated_mmap() noexcept { + return MMapMemory(); +} +// Allocate shared memory using mmap +template +BONGOCAT_NODISCARD inline static MMapMemory make_allocated_mmap() { + MMapMemory ret; + ret._size_bytes = sizeof(T); + if (ret._size_bytes > 0) { + ret.ptr = static_cast( + mmap(BONGOCAT_NULLPTR, ret._size_bytes, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANONYMOUS, -1, 0)); + if (ret.ptr && ret.ptr != MAP_FAILED) { + // default ctor + new (ret.ptr) T(); + return ret; + } else { + BONGOCAT_LOG_ERROR("mmap failed"); + } + } + ret.ptr = BONGOCAT_NULLPTR; + ret._size_bytes = 0; + return ret; +} +template +BONGOCAT_NODISCARD inline static MMapMemory make_unallocated_mmap_value(const T& value) { + auto ret = make_allocated_mmap(); + if (ret.ptr != BONGOCAT_NULLPTR) { + *ret.ptr = value; + } + return ret; +} - - template - struct MMapFileBuffer; - template - void release_allocated_mmap_file_buffer(MMapFileBuffer& memory) noexcept; - - template - struct MMapFileBuffer { - T* data{nullptr}; - size_t count{0}; - size_t _size_bytes{0}; - int _fd{-1}; - off_t _offset{0}; - - constexpr MMapFileBuffer() = default; - ~MMapFileBuffer() noexcept { - release_allocated_mmap_file_buffer(*this); - } - - explicit MMapFileBuffer(decltype(nullptr)) noexcept {} - MMapFileBuffer& operator=(decltype(nullptr)) noexcept { - release_allocated_mmap_file_buffer(*this); - return *this; - } - - // Allocate shared memory using mmap - MMapFileBuffer(size_t p_count, int fd, off_t offset = 0) - : count(p_count), _size_bytes(sizeof(T) * count), _fd(fd), _offset(offset) - { - if (_size_bytes > 0) { - data = static_cast(mmap(nullptr, _size_bytes, - PROT_READ | PROT_WRITE, - MAP_SHARED, - _fd, _offset)); - if (data != MAP_FAILED) { - return; - } else { - BONGOCAT_LOG_ERROR("mmap buffer failed to map file"); - } - } - data = nullptr; - count = 0; - _size_bytes = 0; - } - - MMapFileBuffer(const MMapFileBuffer& other) - : count(other.count), _size_bytes(other._size_bytes), _fd(other._fd), _offset(other._offset) - { - if (other.data && _size_bytes > 0) { - data = static_cast(mmap(nullptr, _size_bytes, - PROT_READ | PROT_WRITE, - MAP_SHARED, - _fd, _offset)); - if (data != MAP_FAILED) { - if constexpr (is_trivially_copyable::value) { - memcpy(data, other.data, _size_bytes); - } else { - for (size_t i = 0; i < other.count; i++) { - *data[i] = *other.data[i]; - } - } - return; - } else { - BONGOCAT_LOG_ERROR("file mmap failed in copy constructor"); - } - } - data = nullptr; - count = 0; - _size_bytes = 0; - } - MMapFileBuffer& operator=(const MMapFileBuffer& other) { - if (this != &other) { - release_allocated_mmap_file(*this); - count = other.count; - _size_bytes = other._size_bytes; - _fd = other._fd; - _offset = other._offset; - if (_size_bytes > 0) { - data = static_cast(mmap(nullptr, _size_bytes, - PROT_READ | PROT_WRITE, - MAP_SHARED, - _fd, _offset)); - if (data != MAP_FAILED) { - if constexpr (is_trivially_copyable::value) { - memcpy(data, other.data, _size_bytes); - } else { - for (size_t i = 0; i < other.count; i++) { - *data[i] = *other.data[i]; - } - } - return *this; - } else { - BONGOCAT_LOG_ERROR("file mmap buffer failed in copy assignment"); - } - } - data = nullptr; - count = 0; - _size_bytes = 0; - } - return *this; - } - - MMapFileBuffer(MMapFileBuffer&& other) noexcept - : data(other.data), count(other.count), _size_bytes(other._size_bytes), _fd(other._fd), _offset(other._offset) - { - other.data = nullptr; - other.count = 0; - other._size_bytes = 0; - other._fd = -1; - other._offset = 0; - } - MMapFileBuffer& operator=(MMapFileBuffer&& other) noexcept { - if (this != &other) { - release_allocated_mmap_file_buffer(*this); - data = other.data; - count = other.count; - _size_bytes = other._size_bytes; - _fd = other._fd; - _offset = other._offset; - other.data = nullptr; - other.count = 0; - other._size_bytes = 0; - other._fd = -1; - other._offset = 0; +template +class MMapArray; +template +void release_allocated_mmap_array(MMapArray& memory) noexcept; + +template +class MMapArray { +public: + T *data{BONGOCAT_NULLPTR}; + size_t count{0}; + size_t _size_bytes{0}; + + constexpr MMapArray() = default; + ~MMapArray() noexcept { + release_allocated_mmap_array(*this); + } + + explicit MMapArray(decltype(BONGOCAT_NULLPTR)) noexcept {} + MMapArray& operator=(decltype(BONGOCAT_NULLPTR)) noexcept { + release_allocated_mmap_array(*this); + return *this; + } + + // Allocate shared memory using mmap and count + explicit MMapArray(size_t p_count) : count(p_count), _size_bytes(sizeof(T) * count) { + if (_size_bytes > 0) { + data = static_cast( + mmap(BONGOCAT_NULLPTR, _size_bytes, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANONYMOUS, -1, 0)); + if (data != MAP_FAILED) { + return; + } else { + BONGOCAT_LOG_ERROR("mmap buffer failed"); + } + } + data = BONGOCAT_NULLPTR; + count = 0; + _size_bytes = 0; + } + + MMapArray(const MMapArray& other) : count(other.count), _size_bytes(other._size_bytes) { + if (other.data && _size_bytes > 0) { + data = static_cast( + mmap(BONGOCAT_NULLPTR, _size_bytes, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANONYMOUS, -1, 0)); + if (data != MAP_FAILED) { + if constexpr (is_trivially_copyable::value) { + memcpy(data, other.data, _size_bytes); + } else { + for (size_t i = 0; i < other.count; i++) { + *data[i] = *other.data[i]; + } + } + return; + } else { + BONGOCAT_LOG_ERROR("file mmap failed in copy constructor"); + } + } + data = BONGOCAT_NULLPTR; + count = 0; + _size_bytes = 0; + } + MMapArray& operator=(const MMapArray& other) { + if (this != &other) { + release_allocated_mmap_array(*this); + count = other.count; + _size_bytes = other._size_bytes; + if (other.data && _size_bytes > 0) { + data = static_cast( + mmap(BONGOCAT_NULLPTR, _size_bytes, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANONYMOUS, -1, 0)); + if (data != MAP_FAILED) { + if constexpr (is_trivially_copyable::value) { + memcpy(data, other.data, _size_bytes); + } else { + for (size_t i = 0; i < other.count; i++) { + data[i] = other.data[i]; } - return *this; - } - - - T& operator[](size_t index) { - assert(index < count); - return data[index]; - } - const T& operator[](size_t index) const { - assert(index < count); - return data[index]; - } - - constexpr explicit operator bool() const noexcept { - return data != nullptr && data != MAP_FAILED; - } + } + return *this; + } else { + BONGOCAT_LOG_ERROR("mmap buffer failed in copy assignment"); + } + } + data = BONGOCAT_NULLPTR; + count = 0; + _size_bytes = 0; + } + return *this; + } + + MMapArray(MMapArray&& other) noexcept : data(other.data), count(other.count), _size_bytes(other._size_bytes) { + other.data = BONGOCAT_NULLPTR; + other.count = 0; + other._size_bytes = 0; + } + MMapArray& operator=(MMapArray&& other) noexcept { + if (this != &other) { + release_allocated_mmap_array(*this); + data = other.data; + count = other.count; + _size_bytes = other._size_bytes; + other.data = BONGOCAT_NULLPTR; + other.count = 0; + other._size_bytes = 0; + } + return *this; + } + + T& operator[](size_t index) { + assert(index < count); + return data[index]; + } + constexpr const T& operator[](size_t index) const { + assert(index < count); + return data[index]; + } + + constexpr explicit operator bool() const noexcept { + return data != BONGOCAT_NULLPTR && data != MAP_FAILED; + } + + constexpr bool operator==(decltype(BONGOCAT_NULLPTR)) const noexcept { + return data == BONGOCAT_NULLPTR; + } + constexpr bool operator!=(decltype(BONGOCAT_NULLPTR)) const noexcept { + return data != BONGOCAT_NULLPTR; + } +}; +template +void release_allocated_mmap_array(MMapArray& memory) noexcept { + if (memory.data) { + if constexpr (!is_trivially_destructible::value) { + for (size_t i = 0; i < memory.count; i++) { + memory.data[i].~T(); + } + } + munmap(memory.data, memory._size_bytes); + memory.data = BONGOCAT_NULLPTR; + memory.count = 0; + memory._size_bytes = 0; + } +} +template +BONGOCAT_NODISCARD inline static MMapArray make_unallocated_mmap_array() noexcept { + return MMapArray(); +} +template +BONGOCAT_NODISCARD inline static MMapArray make_allocated_mmap_array_uninitialized(size_t count) { + return count > 0 ? MMapArray(count) : MMapArray(); +} +template +BONGOCAT_NODISCARD inline static MMapArray make_allocated_mmap_array(size_t count) { + auto ret = count > 0 ? MMapArray(count) : MMapArray(); + for (size_t i = 0; i < ret.count; i++) { + new (&ret.data[i]) T(); + } + return ret; +} - constexpr bool operator==(decltype(nullptr)) const noexcept { - return data == nullptr; - } - constexpr bool operator!=(decltype(nullptr)) const noexcept { - return data != nullptr; - } - }; - template - void release_allocated_mmap_file_buffer(MMapFileBuffer& memory) noexcept { - if (memory.data) { - if constexpr (!is_trivially_destructible::value) { - for (size_t i = 0; i < memory.count; i++) { - memory.data[i].~T(); - } - } - munmap(memory.data, memory._size_bytes); - memory.data = nullptr; - memory.count = 0; - memory._size_bytes = 0; - memory._fd = -1; - memory._offset = 0; - } +template +class MMapFile; +template +void release_allocated_mmap_file(MMapFile& memory) noexcept; + +template +class MMapFile { +public: + T *ptr{BONGOCAT_NULLPTR}; + size_t _size_bytes{0}; + int _fd{-1}; + off_t _offset{0}; + + constexpr MMapFile() = default; + ~MMapFile() noexcept { + release_allocated_mmap_file(*this); + } + + explicit MMapFile(decltype(BONGOCAT_NULLPTR)) noexcept {} + MMapFile& operator=(decltype(BONGOCAT_NULLPTR)) noexcept { + release_allocated_mmap_file(*this); + return *this; + } + + explicit MMapFile(int fd, off_t offset = 0) : _size_bytes(sizeof(T)), _fd(fd), _offset(offset) { + if (_size_bytes > 0) { + ptr = mmap(BONGOCAT_NULLPTR, _size_bytes, PROT_READ | PROT_WRITE, MAP_SHARED, _fd, _offset); + if (ptr != MAP_FAILED) { + return; + } else { + BONGOCAT_LOG_ERROR("mmap failed to map file"); + } } - template - [[nodiscard]] inline static MMapFileBuffer make_unallocated_mmap_file_buffer() { - return MMapFileBuffer(); + ptr = BONGOCAT_NULLPTR; + _size_bytes = 0; + } + + MMapFile(const MMapFile& other) : _size_bytes(other._size_bytes), _fd(other._fd), _offset(other._offset) { + if (other.ptr && _size_bytes > 0) { + ptr = mmap(BONGOCAT_NULLPTR, _size_bytes, PROT_READ | PROT_WRITE, MAP_SHARED, _fd, _offset); + if (ptr != MAP_FAILED) { + if constexpr (is_trivially_copyable::value) { + memcpy(ptr, other.ptr, _size_bytes); + } else { + new (ptr) T(*other.ptr); + } + return; + } else { + BONGOCAT_LOG_ERROR("file mmap failed in copy constructor"); + } } - template - [[nodiscard]] inline static MMapFileBuffer make_allocated_mmap_file_buffer_uninitialized(size_t count, int fd, off_t offset = 0) { - return MMapFileBuffer(count, fd, offset); + ptr = BONGOCAT_NULLPTR; + _size_bytes = 0; + } + MMapFile& operator=(const MMapFile& other) { + if (this != &other) { + release_allocated_mmap_file(*this); + _size_bytes = other._size_bytes; + _fd = other._fd; + _offset = other._offset; + if (other.ptr && _size_bytes > 0) { + ptr = mmap(BONGOCAT_NULLPTR, _size_bytes, PROT_READ | PROT_WRITE, MAP_SHARED, _fd, _offset); + if (ptr) { + if constexpr (is_trivially_copyable::value) { + memcpy(ptr, other.ptr, _size_bytes); + } else { + new (ptr) T(*other.ptr); + } + return *this; + } else { + BONGOCAT_LOG_ERROR("file mmap failed in copy assignment"); + } + } + ptr = BONGOCAT_NULLPTR; + _size_bytes = 0; } - template - [[nodiscard]] inline static MMapFileBuffer make_allocated_mmap_file_buffer_defaulted(size_t count, int fd, off_t offset = 0) { - auto ret = count > 0 ? MMapFileBuffer(count, fd, offset) : MMapFileBuffer(); - for (size_t i = 0;i < ret.count;i++) { - new (&ret.data[i]) T(); - } - return ret; + return *this; + } + + MMapFile(MMapFile&& other) noexcept + : ptr(other.ptr) + , _size_bytes(other._size_bytes) + , _fd(other._fd) + , _offset(other._offset) { + other.ptr = BONGOCAT_NULLPTR; + other._size_bytes = 0; + other._fd = -1; + other._offset = 0; + } + MMapFile& operator=(MMapFile&& other) noexcept { + if (this != &other) { + release_allocated_mmap_file(*this); + ptr = other.ptr; + _size_bytes = other._size_bytes; + _fd = other._fd; + _offset = other._offset; + other.ptr = BONGOCAT_NULLPTR; + other._size_bytes = 0; + other._fd = -1; + other._offset = 0; } - template - [[nodiscard]] inline static MMapFileBuffer make_allocated_mmap_file_buffer_value(const T& value, size_t count, int fd, off_t offset = 0) { - auto ret = count > 0 ? MMapFileBuffer(count, fd, offset) : MMapFileBuffer(); - for (size_t i = 0;i < ret.count;i++) { - ret.data[i] = value; - } - return ret; + return *this; + } + + T& operator*() { + assert(ptr); + return *ptr; + } + + T *operator->() { + return ptr; + } + + constexpr explicit operator bool() const noexcept { + return ptr != BONGOCAT_NULLPTR && ptr != MAP_FAILED; + } + + constexpr bool operator==(decltype(BONGOCAT_NULLPTR)) const noexcept { + return ptr == BONGOCAT_NULLPTR; + } + constexpr bool operator!=(decltype(BONGOCAT_NULLPTR)) const noexcept { + return ptr != BONGOCAT_NULLPTR; + } +}; +template +void release_allocated_mmap_file(MMapFile& memory) noexcept { + if (memory.ptr) { + if constexpr (!is_trivially_destructible::value) { + memory.ptr->~T(); } + munmap(memory.ptr, memory._size_bytes); + memory.ptr = BONGOCAT_NULLPTR; + memory._size_bytes = 0; + memory._fd = -1; + memory._offset = 0; + } +} +template +BONGOCAT_NODISCARD inline static MMapFile make_unallocated_mmap_file() noexcept { + return MMapFile(); +} +template +BONGOCAT_NODISCARD inline static MMapFile make_allocated_mmap_file_uninitialized(int fd, off_t offset = 0) { + return MMapFile(fd, offset); +} +template +BONGOCAT_NODISCARD inline static MMapFile make_allocated_mmap_file_defaulted(int fd, off_t offset = 0) { + auto ret = MMapFile(fd, offset); + if (ret.ptr) { + new (ret.ptr) T(); + } + return ret; +} +template +BONGOCAT_NODISCARD inline static MMapFile make_allocated_mmap_file_value(const T& value, int fd, off_t offset = 0) { + auto ret = MMapFile(fd, offset); + for (size_t i = 0; i < ret.size; i++) { + *ret.ptr = value; + } + return ret; +} - struct FileDescriptor; - void close_fd(FileDescriptor& fd) noexcept; - - struct FileDescriptor { - int _fd{-1}; - - constexpr FileDescriptor() = default; - explicit FileDescriptor(int fd) noexcept : _fd(fd) {} - ~FileDescriptor() noexcept { - close_fd(*this); - } - - explicit FileDescriptor(decltype(nullptr)) noexcept {} - FileDescriptor& operator=(decltype(nullptr)) noexcept { - close_fd(*this); - return *this; - } - - FileDescriptor(const FileDescriptor&) = delete; - FileDescriptor& operator=(const FileDescriptor&) = delete; - - FileDescriptor(FileDescriptor&& other) noexcept - : _fd(other._fd) - { - other._fd = -1; - } - FileDescriptor& operator=(FileDescriptor&& other) noexcept { - if (this != &other) { - close_fd(*this); - _fd = other._fd; - other._fd = -1; - } - return *this; - } - - // Check if valid - /* - explicit(false) operator bool() const noexcept { - return _fd >= 0; - } - */ - - // conversion to int - /* - explicit operator int() const noexcept { - return _fd; - } - */ - }; - inline void close_fd(FileDescriptor& fd) noexcept { - if (fd._fd >= 0) { - ::close(fd._fd); - fd._fd = -1; - } +template +class MMapFileBuffer; +template +void release_allocated_mmap_file_buffer(MMapFileBuffer& memory) noexcept; + +template +class MMapFileBuffer { +public: + T *data{BONGOCAT_NULLPTR}; + size_t count{0}; + size_t _size_bytes{0}; + int _fd{-1}; + off_t _offset{0}; + + constexpr MMapFileBuffer() = default; + ~MMapFileBuffer() noexcept { + release_allocated_mmap_file_buffer(*this); + } + + explicit MMapFileBuffer(decltype(BONGOCAT_NULLPTR)) noexcept {} + MMapFileBuffer& operator=(decltype(BONGOCAT_NULLPTR)) noexcept { + release_allocated_mmap_file_buffer(*this); + return *this; + } + + // Allocate shared memory using mmap + MMapFileBuffer(size_t p_count, int fd, off_t offset = 0) + : count(p_count) + , _size_bytes(sizeof(T) * count) + , _fd(fd) + , _offset(offset) { + if (_size_bytes > 0) { + data = static_cast(mmap(BONGOCAT_NULLPTR, _size_bytes, PROT_READ | PROT_WRITE, MAP_SHARED, _fd, _offset)); + if (data != MAP_FAILED) { + return; + } else { + BONGOCAT_LOG_ERROR("mmap buffer failed to map file"); + } } - - - - struct MMapFileContent; - void release_allocated_mmap_file_content(MMapFileContent& memory) noexcept; - - struct MMapFileContent { - /// @NOTE: memory is private (not sharable within threads) - unsigned char* data{nullptr}; - off_t _size_bytes{0}; - FileDescriptor _fd{-1}; - off_t _offset{0}; - - constexpr MMapFileContent() = default; - ~MMapFileContent() noexcept { - release_allocated_mmap_file_content(*this); - } - - explicit MMapFileContent(decltype(nullptr)) noexcept {} - MMapFileContent& operator=(decltype(nullptr)) noexcept { - release_allocated_mmap_file_content(*this); - return *this; - } - - // Allocate shared memory using mmap - explicit MMapFileContent(FileDescriptor&& fd, off_t offset = 0) - : _fd(bongocat::move(fd)), _offset(offset) - { - // Get file size - struct stat st {}; - if (::fstat(_fd._fd, &st) < 0) { - close_fd(fd); - return; - } - if (st.st_size <= 0) { - close_fd(fd); - return; - } - if (st.st_size <= offset) { - close_fd(fd); - return; - } - _size_bytes = st.st_size - _offset; - - long page_size = sysconf(_SC_PAGE_SIZE); - assert(page_size > 0); - off_t aligned_offset = (_offset / page_size) * page_size; - off_t delta = _offset - aligned_offset; - _size_bytes = st.st_size - _offset; - size_t map_length = static_cast(_size_bytes + delta); - - if (_size_bytes > 0) { - void* mapped = mmap(nullptr, map_length, - PROT_READ, - MAP_PRIVATE, - _fd._fd, aligned_offset); - if (mapped != MAP_FAILED) { - data = static_cast(mapped) + delta; - return; - } else { - BONGOCAT_LOG_ERROR("mmap file content failed to map file"); - } - } - - data = nullptr; - _size_bytes = 0; - close_fd(fd); - } - - MMapFileContent(MMapFileContent&& other) noexcept - : data(other.data), _size_bytes(other._size_bytes), _fd(bongocat::move(other._fd)), _offset(other._offset) - { - other.data = nullptr; - other._size_bytes = 0; - other._fd._fd = -1; - other._offset = 0; - } - MMapFileContent& operator=(MMapFileContent&& other) noexcept { - if (this != &other) { - release_allocated_mmap_file_content(*this); - data = other.data; - _size_bytes = other._size_bytes; - _fd = bongocat::move(other._fd); - _offset = other._offset; - other.data = nullptr; - other._size_bytes = 0; - other._fd._fd = -1; - other._offset = 0; + data = BONGOCAT_NULLPTR; + count = 0; + _size_bytes = 0; + } + + MMapFileBuffer(const MMapFileBuffer& other) + : count(other.count) + , _size_bytes(other._size_bytes) + , _fd(other._fd) + , _offset(other._offset) { + if (other.data && _size_bytes > 0) { + data = static_cast(mmap(BONGOCAT_NULLPTR, _size_bytes, PROT_READ | PROT_WRITE, MAP_SHARED, _fd, _offset)); + if (data != MAP_FAILED) { + if constexpr (is_trivially_copyable::value) { + memcpy(data, other.data, _size_bytes); + } else { + for (size_t i = 0; i < other.count; i++) { + *data[i] = *other.data[i]; + } + } + return; + } else { + BONGOCAT_LOG_ERROR("file mmap failed in copy constructor"); + } + } + data = BONGOCAT_NULLPTR; + count = 0; + _size_bytes = 0; + } + MMapFileBuffer& operator=(const MMapFileBuffer& other) { + if (this != &other) { + release_allocated_mmap_file(*this); + count = other.count; + _size_bytes = other._size_bytes; + _fd = other._fd; + _offset = other._offset; + if (_size_bytes > 0) { + data = static_cast(mmap(BONGOCAT_NULLPTR, _size_bytes, PROT_READ | PROT_WRITE, MAP_SHARED, _fd, _offset)); + if (data != MAP_FAILED) { + if constexpr (is_trivially_copyable::value) { + memcpy(data, other.data, _size_bytes); + } else { + for (size_t i = 0; i < other.count; i++) { + *data[i] = *other.data[i]; } - return *this; - } - - const unsigned char& operator[](size_t index) const { - assert(_size_bytes >= 0); - assert(index < static_cast(_size_bytes)); - return data[index]; - } + } + return *this; + } else { + BONGOCAT_LOG_ERROR("file mmap buffer failed in copy assignment"); + } + } + data = BONGOCAT_NULLPTR; + count = 0; + _size_bytes = 0; + } + return *this; + } + + MMapFileBuffer(MMapFileBuffer&& other) noexcept + : data(other.data) + , count(other.count) + , _size_bytes(other._size_bytes) + , _fd(other._fd) + , _offset(other._offset) { + other.data = BONGOCAT_NULLPTR; + other.count = 0; + other._size_bytes = 0; + other._fd = -1; + other._offset = 0; + } + MMapFileBuffer& operator=(MMapFileBuffer&& other) noexcept { + if (this != &other) { + release_allocated_mmap_file_buffer(*this); + data = other.data; + count = other.count; + _size_bytes = other._size_bytes; + _fd = other._fd; + _offset = other._offset; + other.data = BONGOCAT_NULLPTR; + other.count = 0; + other._size_bytes = 0; + other._fd = -1; + other._offset = 0; + } + return *this; + } + + T& operator[](size_t index) { + assert(index < count); + return data[index]; + } + const T& operator[](size_t index) const { + assert(index < count); + return data[index]; + } + + constexpr explicit operator bool() const noexcept { + return data != BONGOCAT_NULLPTR && data != MAP_FAILED; + } + + constexpr bool operator==(decltype(BONGOCAT_NULLPTR)) const noexcept { + return data == BONGOCAT_NULLPTR; + } + constexpr bool operator!=(decltype(BONGOCAT_NULLPTR)) const noexcept { + return data != BONGOCAT_NULLPTR; + } +}; +template +void release_allocated_mmap_file_buffer(MMapFileBuffer& memory) noexcept { + if (memory.data) { + if constexpr (!is_trivially_destructible::value) { + for (size_t i = 0; i < memory.count; i++) { + memory.data[i].~T(); + } + } + munmap(memory.data, memory._size_bytes); + memory.data = BONGOCAT_NULLPTR; + memory.count = 0; + memory._size_bytes = 0; + memory._fd = -1; + memory._offset = 0; + } +} +template +BONGOCAT_NODISCARD inline static MMapFileBuffer make_unallocated_mmap_file_buffer() { + return MMapFileBuffer(); +} +template +BONGOCAT_NODISCARD inline static MMapFileBuffer make_allocated_mmap_file_buffer_uninitialized(size_t count, int fd, + off_t offset = 0) { + return MMapFileBuffer(count, fd, offset); +} +template +BONGOCAT_NODISCARD inline static MMapFileBuffer make_allocated_mmap_file_buffer_defaulted(size_t count, int fd, + off_t offset = 0) { + auto ret = count > 0 ? MMapFileBuffer(count, fd, offset) : MMapFileBuffer(); + for (size_t i = 0; i < ret.count; i++) { + new (&ret.data[i]) T(); + } + return ret; +} +template +BONGOCAT_NODISCARD inline static MMapFileBuffer make_allocated_mmap_file_buffer_value(const T& value, size_t count, + int fd, off_t offset = 0) { + auto ret = count > 0 ? MMapFileBuffer(count, fd, offset) : MMapFileBuffer(); + for (size_t i = 0; i < ret.count; i++) { + ret.data[i] = value; + } + return ret; +} - constexpr explicit operator bool() const noexcept { - return data != nullptr && data != MAP_FAILED && _fd._fd >= 0; - } +class FileDescriptor; +void close_fd(FileDescriptor& fd) noexcept; + +class FileDescriptor { +public: + int _fd{-1}; + + constexpr FileDescriptor() = default; + explicit FileDescriptor(int fd) noexcept : _fd(fd) {} + ~FileDescriptor() noexcept { + close_fd(*this); + } + + explicit FileDescriptor(decltype(BONGOCAT_NULLPTR)) noexcept {} + FileDescriptor& operator=(decltype(BONGOCAT_NULLPTR)) noexcept { + close_fd(*this); + return *this; + } + + FileDescriptor(const FileDescriptor&) = delete; + FileDescriptor& operator=(const FileDescriptor&) = delete; + + FileDescriptor(FileDescriptor&& other) noexcept : _fd(other._fd) { + other._fd = -1; + } + FileDescriptor& operator=(FileDescriptor&& other) noexcept { + if (this != &other) { + close_fd(*this); + _fd = other._fd; + other._fd = -1; + } + return *this; + } + + // Check if valid + /* + explicit(false) operator bool() const noexcept { + return _fd >= 0; + } + */ + + // conversion to int + /* + explicit operator int() const noexcept { + return _fd; + } + */ +}; +inline void close_fd(FileDescriptor& fd) noexcept { + if (fd._fd >= 0) { + ::close(fd._fd); + } + fd._fd = -1; +} - constexpr bool operator==(decltype(nullptr)) const noexcept { - return data == nullptr; - } - constexpr bool operator!=(decltype(nullptr)) const noexcept { - return data != nullptr; - } - }; - inline void release_allocated_mmap_file_content(MMapFileContent& memory) noexcept { - if (memory.data) { - assert(memory._size_bytes >= 0); - munmap(memory.data, static_cast(memory._size_bytes)); - close_fd(memory._fd); - memory.data = nullptr; - memory._size_bytes = 0; - memory._offset = 0; - } +class MMapFileContent; +void release_allocated_mmap_file_content(MMapFileContent& memory) noexcept; + +class MMapFileContent { +public: + /// @NOTE: memory is private (not sharable within threads) + unsigned char *data{BONGOCAT_NULLPTR}; + off_t _size_bytes{0}; + FileDescriptor _fd{-1}; + off_t _offset{0}; + + constexpr MMapFileContent() = default; + ~MMapFileContent() noexcept { + release_allocated_mmap_file_content(*this); + } + + explicit MMapFileContent(decltype(BONGOCAT_NULLPTR)) noexcept {} + MMapFileContent& operator=(decltype(BONGOCAT_NULLPTR)) noexcept { + release_allocated_mmap_file_content(*this); + return *this; + } + + // Allocate shared memory using mmap + explicit MMapFileContent(FileDescriptor&& fd, off_t offset = 0) : _fd(bongocat::move(fd)), _offset(offset) { + // Get file size + struct stat st{}; + if (::fstat(_fd._fd, &st) < 0) { + close_fd(fd); + return; } - [[nodiscard]] inline static MMapFileContent make_unallocated_mmap_file_content() { - return {}; + if (st.st_size <= 0) { + close_fd(fd); + return; } - [[nodiscard]] inline static created_result_t make_allocated_mmap_file_content(FileDescriptor&& fd, off_t offset = 0) { - // Get file size - struct stat st {}; - if (::fstat(fd._fd, &st) < 0) { - BONGOCAT_LOG_ERROR("Failed to open file for mmap: fd=%d", fd._fd); - return bongocat_error_t::BONGOCAT_ERROR_FILE_IO; - } - if (st.st_size <= 0) { - BONGOCAT_LOG_ERROR("Failed to open file for mmap, fstat failed on file descriptor: fd=%d", fd._fd); - return bongocat_error_t::BONGOCAT_ERROR_FILE_IO; - } - if (st.st_size <= offset) { - BONGOCAT_LOG_ERROR("Failed to open file for mmap, Invalid mmap offset (beyond EOF): fd=%d", fd._fd); - return bongocat_error_t::BONGOCAT_ERROR_FILE_IO; - } - - return MMapFileContent(bongocat::move(fd), offset); + if (st.st_size <= offset) { + close_fd(fd); + return; + } + _size_bytes = st.st_size - _offset; + + const long page_size = sysconf(_SC_PAGE_SIZE); + assert(page_size > 0); + const off_t aligned_offset = (_offset / page_size) * page_size; + const off_t delta = _offset - aligned_offset; + _size_bytes = st.st_size - _offset; + const size_t map_length = static_cast(_size_bytes + delta); + + if (_size_bytes > 0) { + void *mapped = mmap(BONGOCAT_NULLPTR, map_length, PROT_READ, MAP_PRIVATE, _fd._fd, aligned_offset); + if (mapped != MAP_FAILED) { + data = static_cast(mapped) + delta; + return; + } else { + BONGOCAT_LOG_ERROR("mmap file content failed to map file"); + } } - [[nodiscard]] inline static created_result_t make_allocated_mmap_file_content_open(const char* filename, off_t offset = 0) { - int fd = ::open(filename, O_RDONLY); - if (fd < 0) { - BONGOCAT_LOG_ERROR("Failed to open file for mmap: %s", filename); - return bongocat_error_t::BONGOCAT_ERROR_FILE_IO; - } - // Get file size - struct stat st {}; - if (::fstat(fd, &st) < 0) { - BONGOCAT_LOG_ERROR("Failed to open file for mmap: %s", filename); - return bongocat_error_t::BONGOCAT_ERROR_FILE_IO; - } - if (st.st_size <= 0) { - BONGOCAT_LOG_ERROR("Failed to open file for mmap, fstat failed on file descriptor: %s", filename); - return bongocat_error_t::BONGOCAT_ERROR_FILE_IO; - } - if (st.st_size <= offset) { - BONGOCAT_LOG_ERROR("Failed to open file for mmap, Invalid mmap offset (beyond EOF): %s", filename); - return bongocat_error_t::BONGOCAT_ERROR_FILE_IO; - } - return MMapFileContent(FileDescriptor(fd), offset); + data = BONGOCAT_NULLPTR; + _size_bytes = 0; + close_fd(fd); + } + + MMapFileContent(MMapFileContent&& other) noexcept + : data(other.data) + , _size_bytes(other._size_bytes) + , _fd(bongocat::move(other._fd)) + , _offset(other._offset) { + other.data = BONGOCAT_NULLPTR; + other._size_bytes = 0; + other._fd._fd = -1; + other._offset = 0; + } + MMapFileContent& operator=(MMapFileContent&& other) noexcept { + if (this != &other) { + release_allocated_mmap_file_content(*this); + data = other.data; + _size_bytes = other._size_bytes; + _fd = bongocat::move(other._fd); + _offset = other._offset; + other.data = BONGOCAT_NULLPTR; + other._size_bytes = 0; + other._fd._fd = -1; + other._offset = 0; } + return *this; + } + + const unsigned char& operator[](size_t index) const { + assert(_size_bytes >= 0); + assert(index < static_cast(_size_bytes)); + return data[index]; + } + + constexpr explicit operator bool() const noexcept { + return data != BONGOCAT_NULLPTR && data != MAP_FAILED && _fd._fd >= 0; + } + + constexpr bool operator==(decltype(BONGOCAT_NULLPTR)) const noexcept { + return data == BONGOCAT_NULLPTR; + } + constexpr bool operator!=(decltype(BONGOCAT_NULLPTR)) const noexcept { + return data != BONGOCAT_NULLPTR; + } +}; +inline void release_allocated_mmap_file_content(MMapFileContent& memory) noexcept { + if (memory.data != BONGOCAT_NULLPTR) { + assert(memory._size_bytes >= 0); + munmap(memory.data, static_cast(memory._size_bytes)); + close_fd(memory._fd); + memory.data = BONGOCAT_NULLPTR; + memory._size_bytes = 0; + memory._offset = 0; + } +} +BONGOCAT_NODISCARD inline static MMapFileContent make_unallocated_mmap_file_content() { + return {}; +} +BONGOCAT_NODISCARD inline static created_result_t make_allocated_mmap_file_content(FileDescriptor&& fd, + off_t offset = 0) { + // Get file size + struct stat st{}; + if (::fstat(fd._fd, &st) < 0) { + BONGOCAT_LOG_ERROR("Failed to open file for mmap: fd=%d", fd._fd); + return bongocat_error_t::BONGOCAT_ERROR_FILE_IO; + } + if (st.st_size <= 0) { + BONGOCAT_LOG_ERROR("Failed to open file for mmap, fstat failed on file descriptor: fd=%d", fd._fd); + return bongocat_error_t::BONGOCAT_ERROR_FILE_IO; + } + if (st.st_size <= offset) { + BONGOCAT_LOG_ERROR("Failed to open file for mmap, Invalid mmap offset (beyond EOF): fd=%d", fd._fd); + return bongocat_error_t::BONGOCAT_ERROR_FILE_IO; + } + + return MMapFileContent(bongocat::move(fd), offset); +} +BONGOCAT_NODISCARD inline static created_result_t +make_allocated_mmap_file_content_open(const char *filename, off_t offset = 0) { + int fd = ::open(filename, O_RDONLY); + if (fd < 0) { + BONGOCAT_LOG_ERROR("Failed to open file for mmap: %s", filename); + return bongocat_error_t::BONGOCAT_ERROR_FILE_IO; + } + // Get file size + struct stat st{}; + if (::fstat(fd, &st) < 0) { + BONGOCAT_LOG_ERROR("Failed to open file for mmap: %s", filename); + return bongocat_error_t::BONGOCAT_ERROR_FILE_IO; + } + if (st.st_size <= 0) { + BONGOCAT_LOG_ERROR("Failed to open file for mmap, fstat failed on file descriptor: %s", filename); + return bongocat_error_t::BONGOCAT_ERROR_FILE_IO; + } + if (st.st_size <= offset) { + BONGOCAT_LOG_ERROR("Failed to open file for mmap, Invalid mmap offset (beyond EOF): %s", filename); + return bongocat_error_t::BONGOCAT_ERROR_FILE_IO; + } + + return MMapFileContent(FileDescriptor(fd), offset); +} - struct drain_event_result_t { uint64_t result{0}; int err{0}; }; - inline drain_event_result_t drain_event(pollfd& pfd, int max_attempts, [[maybe_unused]] const char* fd_name = nullptr) noexcept { - drain_event_result_t ret; - if (pfd.revents & POLLIN) { - ssize_t rc{0}; - uint64_t u{0}; - int err; - int attempts = 0; - do { - rc = read(pfd.fd, &u, sizeof(u)); - err = errno; - if (rc == sizeof(u)) { - ret.result = u; - } - attempts++; - } while (rc == sizeof(u) && attempts < max_attempts); - if (max_attempts > 1 && rc < 0) { - ret.err = err; - // supress compiler warning +struct drain_event_result_t { + uint64_t result{0}; + int err{0}; +}; +inline drain_event_result_t drain_event(pollfd& pfd, int max_attempts, + [[maybe_unused]] const char *fd_name = BONGOCAT_NULLPTR) noexcept { + drain_event_result_t ret; + if (pfd.revents & POLLIN) { + ssize_t rc{0}; + uint64_t u{0}; + int err; + int attempts = 0; + do { + rc = read(pfd.fd, &u, sizeof(u)); + err = errno; + if (rc == sizeof(u)) { + ret.result = u; + } + attempts++; + } while (rc == sizeof(u) && attempts < max_attempts); + if (max_attempts > 1 && rc < 0) { + ret.err = err; + // supress compiler warning #if EAGAIN == EWOULDBLOCK - if (ret.err != EAGAIN && ret.err != -1) { - if (fd_name) { - BONGOCAT_LOG_ERROR("Error reading %s: %s", fd_name, strerror(ret.err)); - } - } + if (ret.err != EAGAIN && ret.err != -1) { + if (fd_name) { + BONGOCAT_LOG_ERROR("Error reading %s: %s", fd_name, strerror(ret.err)); + } + } #else - if (ret.err != EAGAIN && ret.err != EWOULDBLOCK && ret.err != -1) { - if (fd_name) { - BONGOCAT_LOG_ERROR("Error reading %s: %s", fd_name, strerror(ret.err)); - } - } -#endif - } + if (ret.err != EAGAIN && ret.err != EWOULDBLOCK && ret.err != -1) { + if (fd_name) { + BONGOCAT_LOG_ERROR("Error reading %s: %s", fd_name, strerror(ret.err)); } - return ret; + } +#endif } + } + return ret; } +} // namespace bongocat::platform -#endif // BONGOCAT_SYSTEM_MEMORY_H \ No newline at end of file +#endif // BONGOCAT_SYSTEM_MEMORY_H \ No newline at end of file diff --git a/include/utils/time.h b/include/utils/time.h index b70def6f..b5434747 100644 --- a/include/utils/time.h +++ b/include/utils/time.h @@ -4,18 +4,18 @@ #include namespace bongocat::platform { - using timestamp_us_t = int64_t; - using timestamp_ms_t = int64_t; - using time_us_t = int64_t; - using time_ms_t = int64_t; - using time_ns_t = int64_t; - using time_sec_t = int64_t; +using timestamp_us_t = int64_t; +using timestamp_ms_t = int64_t; +using time_us_t = int64_t; +using time_ms_t = int64_t; +using time_ns_t = int64_t; +using time_sec_t = int64_t; - [[nodiscard]] timestamp_us_t get_current_time_us(); - [[nodiscard]] timestamp_ms_t get_current_time_ms(); +[[nodiscard]] timestamp_us_t get_current_time_us(); +[[nodiscard]] timestamp_ms_t get_current_time_ms(); - [[nodiscard]] time_us_t get_uptime_us(); - [[nodiscard]] time_ms_t get_uptime_ms(); -} +[[nodiscard]] time_us_t get_uptime_us(); +[[nodiscard]] time_ms_t get_uptime_ms(); +} // namespace bongocat::platform -#endif // BONGOCAT_TIME_H \ No newline at end of file +#endif // BONGOCAT_TIME_H \ No newline at end of file diff --git a/lib/.clang-format b/lib/.clang-format new file mode 100644 index 00000000..15f9b6fc --- /dev/null +++ b/lib/.clang-format @@ -0,0 +1,2 @@ +DisableFormat: true +SortIncludes: Never \ No newline at end of file diff --git a/nix/common.nix b/nix/common.nix index 513de9f6..e3339736 100644 --- a/nix/common.nix +++ b/nix/common.nix @@ -11,33 +11,42 @@ text = '' # Auto-generated config for `wayland-bongocat` - # Cat + # Cat position and size cat_x_offset=${toString cfg.catXOffset} cat_y_offset=${toString cfg.catYOffset} cat_height=${toString cfg.catHeight} + cat_align=${cfg.catAlign} - # Overlay - overlay_position=${toString cfg.overlayPosition} + # Visual settings + mirror_x=${if cfg.mirrorX then "1" else "0"} + mirror_y=${if cfg.mirrorY then "1" else "0"} + enable_antialiasing=${if cfg.enableAntialiasing then "1" else "0"} + + # Overlay settings + overlay_position=${cfg.overlayPosition} overlay_height=${toString cfg.overlayHeight} + overlay_opacity=${toString cfg.overlayOpacity} + layer=${cfg.layer} - # Animations + # Animation settings idle_frame=${toString cfg.idleFrame} keypress_duration=${toString cfg.keypressDuration} test_animation_duration=${toString cfg.testAnimationDuration} test_animation_interval=${toString cfg.testAnimationInterval} - - # Performance fps=${toString cfg.fps} - overlay_opacity=${toString cfg.overlayOpacity} + enable_hand_mapping=${if cfg.enableHandMapping then "1" else "0"} + + # Sleep mode + idle_sleep_timeout=${toString cfg.idleSleepTimeout} + enable_scheduled_sleep=${if cfg.enableScheduledSleep then "1" else "0"} + sleep_begin=${cfg.sleepBegin} + sleep_end=${cfg.sleepEnd} # Debug mode - enable_debug=${ - if cfg.enableDebug - then "1" - else "0" - } + enable_debug=${if cfg.enableDebug then "1" else "0"} - monitor=${toString cfg.monitor} + # Monitor + monitor=${cfg.monitor} # Input devices ${lib.concatMapStringsSep "\n" (device: "keyboard_device=${device}") cfg.inputDevices} @@ -110,10 +119,62 @@ in { # Size catHeight = lib.mkOption { type = lib.types.int; - default = 40; + default = 80; example = 50; description = "Height of the bongo cat in pixels"; }; + catAlign = lib.mkOption { + type = lib.types.str; + default = "center"; + example = "right"; + description = "Horizontal alignment: left, center, or right"; + }; + + # Visual settings + mirrorX = lib.mkOption { + type = lib.types.bool; + default = false; + description = "Flip cat horizontally"; + }; + mirrorY = lib.mkOption { + type = lib.types.bool; + default = false; + description = "Flip cat vertically"; + }; + enableAntialiasing = lib.mkOption { + type = lib.types.bool; + default = true; + description = "Enable smooth scaling (recommended)"; + }; + layer = lib.mkOption { + type = lib.types.str; + default = "top"; + example = "overlay"; + description = "Layer type: top or overlay"; + }; + + # Sleep mode + idleSleepTimeout = lib.mkOption { + type = lib.types.int; + default = 0; + example = 300; + description = "Seconds of inactivity before sleep (0 = disabled)"; + }; + enableScheduledSleep = lib.mkOption { + type = lib.types.bool; + default = false; + description = "Enable scheduled sleep mode"; + }; + sleepBegin = lib.mkOption { + type = lib.types.str; + default = "22:00"; + description = "Sleep schedule start time (HH:MM)"; + }; + sleepEnd = lib.mkOption { + type = lib.types.str; + default = "06:00"; + description = "Sleep schedule end time (HH:MM)"; + }; # Animations idleFrame = lib.mkOption { @@ -148,6 +209,11 @@ in { example = 120; description = "Animation framerate (FPS)"; }; + enableHandMapping = lib.mkOption { + type = lib.types.bool; + default = true; + description = "Map left/right keyboard halves to left/right cat hands"; + }; # Input devices inputDevices = lib.mkOption { diff --git a/nix/default.nix b/nix/default.nix index 1329c7cc..35cafe2b 100644 --- a/nix/default.nix +++ b/nix/default.nix @@ -11,7 +11,7 @@ }: stdenv.mkDerivation (finalAttrs: { pname = "wayland-vpets"; - version = "3.5.0"; + version = "3.6.0"; src = ../.; # Build toolchain and dependencies diff --git a/protocols/.clang-format b/protocols/.clang-format new file mode 100644 index 00000000..15f9b6fc --- /dev/null +++ b/protocols/.clang-format @@ -0,0 +1,2 @@ +DisableFormat: true +SortIncludes: Never \ No newline at end of file diff --git a/scripts/find_input_devices.sh b/scripts/find_input_devices.sh index 81aaa51a..b7c36b9b 100755 --- a/scripts/find_input_devices.sh +++ b/scripts/find_input_devices.sh @@ -1,270 +1,318 @@ -#!/usr/bin/bash - -# Wayland Bongo Cat - Input Device Discovery Tool -# Professional input device finder with comprehensive analysis -# Version: 3.1.0 +#!/usr/bin/env bash +# ═══════════════════════════════════════════════════════════════════════════════ +# Bongo Cat - Input Device Discovery Tool v4.0.0 +# Interactive keyboard detection by listening for actual key events +# ═══════════════════════════════════════════════════════════════════════════════ set -euo pipefail -# Color setup - detect if colors are supported -if [[ -t 1 ]] && [[ "${TERM:-}" != "dumb" ]] && [[ "${NO_COLOR:-}" != "1" ]]; then - readonly RED='\033[0;31m' - readonly GREEN='\033[0;32m' - readonly YELLOW='\033[1;33m' - readonly BLUE='\033[0;34m' - readonly PURPLE='\033[0;35m' - readonly CYAN='\033[0;36m' - readonly WHITE='\033[1;37m' - readonly NC='\033[0m' # No Color - readonly USE_COLORS=true +VERSION="4.0.0" +SCRIPT_NAME="wpets-find-devices" + +# Colors +if [[ -t 1 ]] && [[ "${NO_COLOR:-}" != "1" ]]; then + RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' + BLUE='\033[0;34m' CYAN='\033[0;36m' BOLD='\033[1m' DIM='\033[2m' NC='\033[0m' else - # No colors - use empty strings - readonly RED='' - readonly GREEN='' - readonly YELLOW='' - readonly BLUE='' - readonly PURPLE='' - readonly CYAN='' - readonly WHITE='' - readonly NC='' - readonly USE_COLORS=false + RED='' GREEN='' YELLOW='' BLUE='' CYAN='' BOLD='' DIM='' NC='' fi -# Helper function for colored output -print_colored() { - local color="$1" - local text="$2" - if [[ "$USE_COLORS" == "true" ]]; then - echo -e "${color}${text}${NC}" - else - echo "$text" - fi -} +# ───────────────────────────────────────────────────────────────────────────── +# Helper Functions +# ───────────────────────────────────────────────────────────────────────────── -# Status symbols -readonly CHECK="[OK]" -readonly CROSS="[ERROR]" -readonly WARNING="[WARN]" -readonly INFO="[INFO]" -readonly SEARCH="[SCAN]" -readonly KEYBOARD="[DEVICES]" -readonly CONFIG="[CONFIG]" -readonly TEST="[TEST]" - -# Script metadata -readonly SCRIPT_NAME="bongocat-find-devices" -readonly VERSION="3.1.0" - -# Command line options -SHOW_ALL=false -PREFER_BYID=false -GENERATE_CONFIG=false -TEST_DEVICES=false -VERBOSE=false -IGNORE_DEVICES=() -GENERATE_DEVICES_ONLY=false -INCLUDE_MOUSE_DEVICES=false - -# Usage information -usage() { - if [[ "$USE_COLORS" == "true" ]]; then - printf "${WHITE}%s v%s${NC}\n" "$SCRIPT_NAME" "$VERSION" - printf "Professional input device discovery for Wayland Bongo Cat\n\n" - printf "${WHITE}USAGE:${NC}\n" - printf " %s [OPTIONS]\n\n" "$0" - printf "${WHITE}OPTIONS:${NC}\n" - printf " -a, --all Show all input devices (including mice, touchpads)\n" - printf " -i, --by-id Show input devices as id (symlink, if available)\n" - printf " -e, --ignore-device Ignore device (multiple arguments)\n" - printf " -g, --generate-config Generate configuration file to stdout\n" - printf " -d, --devices-only Print Input devices only (when generating configuration)\n" - printf " -m, --include-mouse Include Mouse Device in config\n" - printf " -t, --test Test device responsiveness (requires root)\n" - printf " -v, --verbose Show detailed device information\n" - printf " -h, --help Show this help message\n\n" - printf "${WHITE}EXAMPLES:${NC}\n" - printf " %s # Basic device discovery\n" "$0" - printf " %s --all --verbose # Comprehensive device analysis\n" "$0" - printf " %s --generate-config > bongocat.conf # Generate config file\n\n" "$0" - printf "${WHITE}DESCRIPTION:${NC}\n" - printf " This tool scans your system for input devices and provides configuration\n" - printf " suggestions for Wayland Bongo Cat. It identifies keyboards, checks\n" - printf " permissions, and generates ready-to-use configuration snippets.\n\n" - printf "${WHITE}MONITOR DETECTION:${NC}\n" - printf " For multi-monitor setups, use these commands to find monitor names:\n" - printf " • wlr-randr # List all monitors (recommended)\n" - printf " • swaymsg -t get_outputs # Sway compositor only\n" - printf " • bongocat logs show detected monitors during startup\n\n" - else - cat << EOF -${SCRIPT_NAME} v${VERSION} -Professional input device discovery for Wayland Bongo Cat - -USAGE: - $0 [OPTIONS] - -OPTIONS: - -a, --all Show all input devices (including mice, touchpads) - -i, --by-id Show input devices as id (symlink, if available) - -e, --ignore-device Ignore device (multiple arguments) - -g, --generate-config Generate configuration file to stdout - -d, --devices-only Print Input devices only (when generating configuration) - -m, --include-mouse Include Mouse Device in config - -t, --test Test device responsiveness (requires root) - -v, --verbose Show detailed device information - -h, --help Show this help message - -EXAMPLES: - $0 # Basic device discovery - $0 --all --verbose # Comprehensive device analysis - $0 --generate-config > bongocat.conf # Generate config file - -DESCRIPTION: - This tool scans your system for input devices and provides configuration - suggestions for Wayland Bongo Cat. It identifies keyboards, checks - permissions, and generates ready-to-use configuration snippets. - -MONITOR DETECTION: - For multi-monitor setups, use these commands to find monitor names: - • wlr-randr # List all monitors (recommended) - • swaymsg -t get_outputs # Sway compositor only - • bongocat logs show detected monitors during startup +info() { echo -e "${BLUE}→${NC} $*"; } +success() { echo -e "${GREEN}✓${NC} $*"; } +warn() { echo -e "${YELLOW}!${NC} $*"; } +error() { echo -e "${RED}✗${NC} $*" >&2; } -EOF - fi +header() { + echo + echo -e "${BOLD}$*${NC}" + echo -e "${BLUE}$(printf '─%.0s' {1..60})${NC}" } -# Parse command line arguments -parse_args() { - while [[ $# -gt 0 ]]; do - case $1 in - -a|--all) - SHOW_ALL=true - shift - ;; - -i|--by-id) - PREFER_BYID=true - shift - ;; - -e|--ignore-device) - if [[ -n "$2" ]]; then - IGNORE_DEVICES+=("$2") - shift 2 - else - echo "Error: --ignore-device requires a value" >&2 - exit 1 - fi - ;; - -g|--generate-config) - GENERATE_CONFIG=true - shift - ;; - -d|--devices-only) - GENERATE_DEVICES_ONLY=true - shift - ;; - -m|--include-mouse) - INCLUDE_MOUSE_DEVICES=true - shift - ;; - -t|--test) - TEST_DEVICES=true - shift - ;; - -v|--verbose) - VERBOSE=true - shift - ;; - -h|--help) - usage - exit 0 - ;; - *) - if [[ "$USE_COLORS" == "true" ]]; then - echo -e "${RED}Error: Unknown option '$1'${NC}" >&2 - else - echo "Error: Unknown option '$1'" >&2 - fi - echo "Use --help for usage information." >&2 - exit 1 - ;; - esac +# ───────────────────────────────────────────────────────────────────────────── +# Device Discovery +# ───────────────────────────────────────────────────────────────────────────── + +device_is_ignored() { + local name="$1" + shift + local ignore_devices=("$@") + for pattern in "${ignore_devices[@]}"; do + if [[ "$name" =~ $pattern ]]; then + return 0 # ignored + fi done + return 1 # not ignored } -# Print header -print_header() { - if [[ "$GENERATE_CONFIG" == "false" ]]; then - if [[ "$USE_COLORS" == "true" ]]; then - echo -e "${PURPLE}╔══════════════════════════════════════════════════════════════════╗${NC}" - echo -e "${PURPLE}║${NC} ${WHITE}Wayland Bongo Cat - Input Device Discovery v${VERSION}${NC} ${PURPLE}║${NC}" - echo -e "${PURPLE}╚══════════════════════════════════════════════════════════════════╝${NC}" - else - echo "==================================================================" - echo " Wayland Bongo Cat - Input Device Discovery v${VERSION}" - echo "==================================================================" +# Get all event devices with kbd handler (potential keyboards) +get_kbd_devices() { + local prefer_byid="$1" # "true" = prefer /dev/input/by-id symlinks + shift 1 + local ignore_devices=("$@") # now receives array properly + + local devices=() + + if [[ ! -r /proc/bus/input/devices ]]; then + return 1 + fi + + local name="" handlers="" capabilities="" + + while IFS= read -r line; do + case "$line" in + N:\ Name=\"*\") + name="${line#N: Name=\"}" + name="${name%\"}" + ;; + H:\ Handlers=*) + handlers="${line#H: Handlers=}" + ;; + B:\ EV=*) + capabilities="${line#B: EV=}" + ;; + "") + if [ ${#ignore_devices[@]} -gt 0 ]; then + # Skip ignored devices + if device_is_ignored "$name" "${ignore_devices[@]}"; then + name="" handlers="" capabilities="" + continue + fi fi - echo - fi -} + if [[ "$handlers" =~ kbd ]] && [[ "$handlers" =~ event ]]; then + local event + event=$(echo "$handlers" | grep -o 'event[0-9]*' | head -1) + local real_path="/dev/input/$event" + if [[ -n "$event" ]] && [[ -e "$real_path" ]]; then + local device_path="$real_path" + if [[ "$prefer_byid" == "true" ]]; then + local symlink + symlink=$(find -L /dev/input/by-id/ -samefile "$real_path" -print -quit 2>/dev/null) + [[ -n "$symlink" ]] && device_path="$symlink" + fi + devices+=("$event|$name|$handlers|$capabilities|$device_path") + fi + fi + name="" handlers="" capabilities="" + ;; + esac + done < /proc/bus/input/devices -# Check if running as root -check_permissions() { - if [[ $EUID -eq 0 ]]; then - return 0 # Running as root - else - return 1 # Not running as root - fi + printf '%s\n' "${devices[@]}" } -# Check if user is in input group -check_input_group() { - if groups | grep -q '\binput\b'; then - return 0 # User is in input group - else - return 1 # User is not in input group - fi +# Check if device is readable +check_device() { + local path="$1" + [[ -r "$path" ]] } -# Get device accessibility status -get_device_status() { - local device="$1" +# ───────────────────────────────────────────────────────────────────────────── +# Interactive Detection +# ───────────────────────────────────────────────────────────────────────────── + +# Listen for key events on a device (runs in background) +listen_device() { + local device="$1" + local output_file="$2" + local timeout="$3" + + # Use timeout + cat to read raw events + # Key events are detected by the presence of specific byte patterns + # Type 1 (EV_KEY) events indicate keyboard activity + timeout "$timeout" cat "/dev/input/$device" 2>/dev/null | head -c 1000 > "$output_file" & + echo $! +} - # Check existence - if [[ ! -e "$device" ]]; then - return 2 # missing +# Detect keyboards interactively +interactive_detect() { + local timeout="${1:-5}" + local prefer_byid="$2" # "true" = prefer /dev/input/by-id symlinks + local devices + devices=$(get_kbd_devices "${prefer_byid}") || { error "Cannot read device list"; return 1; } + + if [[ -z "$devices" ]]; then + error "No input devices with kbd handler found" + info "Try: sudo $SCRIPT_NAME --interactive" + return 1 + fi + + # Check permissions + local has_permission=false + while IFS='|' read -r event name handlers device_path; do + if check_device "/dev/input/$event"; then + has_permission=true + break fi + done <<< "$devices" - # Get file metadata - local mode owner group - mode=$(stat -c "%a" "$device") - owner=$(stat -c "%u" "$device") - group=$(stat -c "%g" "$device") - - # Determine readability - local readable=0 - local euid=$(id -u) - local user_groups=$(id -Gn) - local group_name - group_name=$(getent group "$group" | cut -d: -f1) - - if [[ $euid -eq "$owner" ]] || echo "$user_groups" | grep -qw "$group_name"; then - readable=1 + if [[ "$has_permission" == "false" ]]; then + error "Cannot read input devices (permission denied)" + echo + info "Fix with: ${CYAN}sudo usermod -a -G input \$USER${NC}" + info "Then log out and back in" + echo + info "Or run: ${CYAN}sudo $SCRIPT_NAME --interactive${NC}" + return 1 + fi + + # Create temp directory for output files + local tmpdir + tmpdir=$(mktemp -d) + trap "rm -rf '$tmpdir'" EXIT + + header "Interactive Keyboard Detection" + echo + echo -e " ${BOLD}Press keys on ALL your keyboards for ${timeout} seconds...${NC}" + echo -e " ${DIM}(Internal laptop keyboard, external keyboards, etc.)${NC}" + echo + + # Start listening on all accessible devices + local pids=() + local device_list=() + + while IFS='|' read -r event name handlers device_path; do + if check_device "/dev/input/$event"; then + local outfile="$tmpdir/$event" + local pid + pid=$(listen_device "$event" "$outfile" "$timeout") + pids+=("$pid") + device_list+=("$event|$name|$outfile|$device_path") fi + done <<< "$devices" + + # Show countdown + for ((i=timeout; i>0; i--)); do + echo -ne "\r ${CYAN}Listening... ${i}s remaining ${NC} " + sleep 1 + done + echo -e "\r ${GREEN}✓ Detection complete!${NC} " + + # Wait for all listeners to finish + for pid in "${pids[@]}"; do + wait "$pid" 2>/dev/null || true + done + + echo + + # Check which devices received input + local detected_keyboards=() + local other_devices=() - if (( readable )); then - return 0 # accessible + for entry in "${device_list[@]}"; do + IFS='|' read -r event name outfile device_path <<< "$entry" + + if [[ -s "$outfile" ]]; then + # Device received input - it's a keyboard! + detected_keyboards+=("$event|$name|$device_path") else - return 1 # permission denied + other_devices+=("$event|$name|$device_path") fi + done + + # Show results + header "Detection Results" + + if [[ ${#detected_keyboards[@]} -eq 0 ]]; then + warn "No keyboards detected" + echo + info "Make sure you pressed keys during the detection window" + info "Try again with: $SCRIPT_NAME --interactive" + return 1 + fi + + echo -e " ${GREEN}Detected keyboards:${NC}" + for entry in "${detected_keyboards[@]}"; do + IFS='|' read -r event name device_path <<< "$entry" + echo -e " ${GREEN}✓${NC} ${BOLD}$name${NC}" + echo -e " ${CYAN}$device_path${NC}" + done + + if [[ ${#other_devices[@]} -gt 0 ]]; then + echo + echo -e " ${DIM}Other devices (no input detected):${NC}" + for entry in "${other_devices[@]}"; do + IFS='|' read -r event name device_path <<< "$entry" + echo -e " ${DIM}○ $name (/dev/input/$event)${NC}" + done + fi + + # Config suggestion + header "Add to Config" + echo -e " ${BOLD}~/.config/bongocat/bongocat.conf:${NC}" + echo + for entry in "${detected_keyboards[@]}"; do + IFS='|' read -r event name device_path <<< "$entry" + echo -e " ${CYAN}keyboard_device=$device_path${NC} ${BOLD}# $name${NC}" + done + + echo +} + +# ───────────────────────────────────────────────────────────────────────────── +# Quick Mode (non-interactive, name-based) +# ───────────────────────────────────────────────────────────────────────────── + +# Guess if device is keyboard by name +is_likely_keyboard() { + local name="$1" + local name_lower + name_lower=$(echo "$name" | tr '[:upper:]' '[:lower:]') + + # Exclude obvious non-keyboards + [[ "$name_lower" =~ (button|hotkey|speaker|video|consumer|system|avrcp|mouse|touchpad|trackpad) ]] && return 1 + + # Include devices with "keyboard" in name + [[ "$name_lower" =~ keyboard ]] && return 0 + + # Include standard laptop keyboard + [[ "$name_lower" =~ "at translated set 2" ]] && return 0 + + return 1 +} +is_likely_mouse() { + local name="$1" + local name_lower + name_lower=$(echo "$name" | tr '[:upper:]' '[:lower:]') + + # obvious non-keyboards + [[ "$name_lower" =~ (mouse|touchpad|trackpad) ]] && return 0 + + # Include devices with "mouse" in name + [[ "$name_lower" =~ mouse ]] && return 0 + + return 1 } +generate_config() { + local devices=("$@") + + if [[ -z "$devices" ]]; then + return 1 + fi + + # Calculate max path length for alignment + local maxlen=0 + for entry in "${devices[@]}"; do + IFS='|' read -r event name device_path <<< "$entry" + [[ ${#device_path} -gt $maxlen ]] && maxlen=${#device_path} + done + + # Config suggestion + for entry in "${devices[@]}"; do + IFS='|' read -r event name device_path <<< "$entry" + #echo -e " ${CYAN}keyboard_device=$device_path${NC} ${BOLD}# $name${NC}" + printf "${CYAN}keyboard_device=%-${maxlen}s${NC} ${BOLD}# %s ${NC}\n" "$device_path" "$name" + done +} -# Get device type based on name, capabilities, and handlers get_device_type() { local device_name="$1" - local capabilities="$2" - local handlers="$3" + local handlers="$2" + local capabilities="$3" # Convert to lowercase for matching local name_lower=$(echo "$device_name" | tr '[:upper:]' '[:lower:]') @@ -272,374 +320,233 @@ get_device_type() { # Check for keyboard indicators # Look for "kbd" handler (most reliable), keyboard in name, or keyboard-like capabilities - if [[ "$handlers_lower" =~ kbd ]] || [[ "$name_lower" =~ keyboard ]] || [[ "$capabilities" =~ "120013" ]] || [[ "$capabilities" =~ "12001f" ]]; then + if [[ "$name_lower" =~ mouse ]] || [[ "$handlers_lower" =~ mouse ]]; then + echo "MOUSE" + elif [[ "$name_lower" =~ keyboard ]] || [[ "$capabilities" =~ "120013" ]] || [[ "$capabilities" =~ "12001f" ]]; then # Determine keyboard type if [[ "$name_lower" =~ (bluetooth|wireless|bt) ]] || [[ "$handlers_lower" =~ bluetooth ]]; then - echo "Keyboard (Bluetooth)" + echo "KEYBOARD" elif [[ "$name_lower" =~ (usb|external) ]]; then - echo "Keyboard (USB)" + echo "KEYBOARD" else # Check if it's likely a Bluetooth keyboard based on common brands if [[ "$name_lower" =~ (keychron|logitech|corsair|razer|steelseries|apple|microsoft) ]] && [[ ! "$name_lower" =~ (mouse|trackpad|touchpad) ]]; then - echo "Keyboard (Bluetooth)" + echo "KEYBOARD" else - echo "Keyboard" + echo "KEYBOARD" fi fi - elif [[ "$name_lower" =~ mouse ]] || [[ "$handlers_lower" =~ mouse ]]; then - echo "Mouse" elif [[ "$capabilities" =~ (110000|17|7) ]]; then - echo "Mouse" + echo "MOUSE" elif [[ "$name_lower" =~ (touchpad|trackpad|synaptics) ]]; then echo "Touchpad" elif [[ "$name_lower" =~ (touchscreen|touch) ]]; then echo "Touchscreen" + elif [[ "$handlers_lower" =~ kbd ]]; then + if is_likely_keyboard "$name"; then + echo "KEYBOARD" + else + echo "other" + fi else - echo "Input Device" + echo "other" fi } -device_is_ignored() { - local name="$1" - shift - local ignore_devices=("$@") - for pattern in "${ignore_devices[@]}"; do - if [[ "$name" =~ $pattern ]]; then - return 0 # ignored - fi - done - return 1 # not ignored -} -# Parse device information from /proc/bus/input/devices -parse_devices() { - local show_all="$1" - local prefer_byid="$2" # "true" = prefer /dev/input/by-id symlinks - shift 2 - local ignore_devices=("$@") # now receives array properly - local devices=() - - if [[ ! -r /proc/bus/input/devices ]]; then - echo -e "${RED}${CROSS} Cannot read /proc/bus/input/devices${NC}" >&2 - echo -e "${INFO} Try running with sudo for full device information" >&2 - return 1 - fi - - local current_name="" - local current_handlers="" - local current_capabilities="" - - while IFS= read -r line; do - if [[ "$line" =~ ^I: ]]; then - current_name="" - current_handlers="" - current_capabilities="" - elif [[ "$line" =~ ^N:\ Name=\"(.*)\" ]]; then - current_name="${BASH_REMATCH[1]}" - elif [[ "$line" =~ ^H:\ Handlers=(.*) ]]; then - current_handlers="${BASH_REMATCH[1]}" - elif [[ "$line" =~ ^B:\ EV=(.*) ]]; then - current_capabilities="${BASH_REMATCH[1]}" - elif [[ "$line" =~ ^$ ]] && [[ -n "$current_name" ]]; then - if [ ${#ignore_devices[@]} -gt 0 ]; then - # Skip ignored devices - if device_is_ignored "$current_name" "${ignore_devices[@]}"; then - continue - fi - fi - - local device_type - device_type=$(get_device_type "$current_name" "$current_capabilities" "$current_handlers") - - local event_handlers=($(echo "$current_handlers" | grep -o 'event[0-9]\+' || true)) - - if [[ "$show_all" == "true" ]] || [[ "$device_type" =~ Keyboard ]]; then - for handler in "${event_handlers[@]}"; do - local real_path="/dev/input/$handler" - if [[ -e "$real_path" ]]; then - local device_path="$real_path" - - if [[ "$prefer_byid" == "true" ]]; then - local symlink - symlink=$(find -L /dev/input/by-id/ -samefile "$real_path" -print -quit 2>/dev/null) - [[ -n "$symlink" ]] && device_path="$symlink" - fi - - devices+=("$current_name|$device_path|$device_type") - fi - done - fi - fi - done < /proc/bus/input/devices - - # Handle last block if file didn't end with newline - if [[ -n "$current_name" ]] && ! device_is_ignored "$current_name" "${ignore_devices[@]}"; then - local device_type - device_type=$(get_device_type "$current_name" "$current_capabilities" "$current_handlers") - local event_handlers=($(echo "$current_handlers" | grep -o 'event[0-9]\+' || true)) - - if [[ "$show_all" == "true" ]] || [[ "$device_type" =~ Keyboard ]]; then - for handler in "${event_handlers[@]}"; do - local real_path="/dev/input/$handler" - if [[ -e "$real_path" ]]; then - local device_path="$real_path" - - if [[ "$prefer_byid" == "true" ]]; then - local symlink - symlink=$(find -L /dev/input/by-id/ -samefile "$real_path" -print -quit 2>/dev/null) - [[ -n "$symlink" ]] && device_path="$symlink" - fi - - devices+=("$current_name|$device_path|$device_type") - fi - done +quick_detect() { + local show_all="$1" + local prefer_byid="$2" # "true" = prefer /dev/input/by-id symlinks + local include_mouse_devices="$3" + shift 3 + local ignore_devices=("$@") # now receives array properly + + local devices + devices=$(get_kbd_devices "${prefer_byid}" "${ignore_devices[@]}") || { error "Cannot read devices"; return 1; } + + if [[ -z "$devices" ]]; then + warn "No input devices found" + return 1 + fi + + echo + echo -e "${BOLD}🐱 Bongo Cat Device Discovery${NC} v$VERSION" + + header "Detected Devices" + + local keyboards=() + local mouses=() + local others=() + local config_devices=() + local type="" + + while IFS='|' read -r event name handlers capabilities device_path; do + local status="ok" + check_device "/dev/input/$event" || status="denied" + type=$(get_device_type "${name}" "${handlers}" "${capabilities}") + + if is_likely_mouse "$name"; then + if [[ "$include_mouse_devices" == "true" ]]; then + echo -e " ${GREEN}✓${NC} ${GREEN}[$type]${NC} ${BOLD}$name${NC}" + config_devices+=("$event|$name|$device_path") + else + echo -e " ${DIM}○ [$type] $name${NC}" + if [[ "$show_all" == "true" ]]; then + config_devices+=("$event|$name|$device_path") fi + fi + mouses+=("$event|$name") + elif is_likely_keyboard "$name"; then + echo -e " ${GREEN}✓${NC} ${GREEN}[$type]${NC} ${BOLD}$name${NC}" + keyboards+=("$event|$name|$device_path") + config_devices+=("$event|$name|$device_path") + else + echo -e " ${DIM}○ [$type] $name${NC}" + others+=("$event|$name") + if [[ "$show_all" == "true" ]]; then + config_devices+=("$event|$name|$device_path") + fi fi + echo -e " ${CYAN}/dev/input/$event${NC}" + done <<< "$devices" - printf '%s\n' "${devices[@]}" + if [[ ${#keyboards[@]} -eq 0 ]]; then + echo + warn "Could not auto-detect keyboards by name" + info "Use interactive mode: ${CYAN}$SCRIPT_NAME --interactive${NC}" + return 1 + fi + + # Config suggestion + header "Add to Config" + echo -e " ${BOLD}~/.config/bongocat/bongocat.conf:${NC}" + echo + generate_config "${config_devices[@]}" + + echo + echo -e " ${DIM}Not accurate? Use: $SCRIPT_NAME --interactive${NC}" + echo } -# Display device information -display_devices() { - local show_all="$1" - local prefer_byid="$2" - shift 2 - local ignore_devices=("$@") - local devices - - if [[ "$GENERATE_CONFIG" == "false" ]]; then - if [[ "$USE_COLORS" == "true" ]]; then - echo -e "${SEARCH} ${WHITE}Scanning for input devices...${NC}" - else - echo "${SEARCH} Scanning for input devices..." - fi - echo - fi - - # Get device list - if ! devices=$(parse_devices "$show_all" "$prefer_byid" "${ignore_devices[@]}"); then - return 1 - fi - - if [[ -z "$devices" ]]; then - if [[ "$GENERATE_CONFIG" == "false" ]]; then - echo -e "${WARNING} ${YELLOW}No input devices found${NC}" - echo -e "${INFO} Try running with sudo: ${WHITE}sudo $0${NC}" - fi - return 1 - fi - - local keyboard_devices=() - local mouse_devices=() - local accessible_keyboards=() - local accessible_mice=() - - if [[ "$GENERATE_CONFIG" == "false" ]]; then - echo -e "${KEYBOARD} ${WHITE}Found Input Devices:${NC}" - fi - - # Process and display each device - local status - local status_code - while IFS='|' read -r name path type; do - if [[ "$GENERATE_CONFIG" == "false" ]]; then - echo -e "${BLUE}┌───────────────────────────────────────────────────────────────────────────┐${NC}" - echo -e "${BLUE}│${NC} ${WHITE}Device:${NC} $(printf "%-60s" "$name") ${BLUE} │${NC}" - echo -e "${BLUE}│${NC} ${WHITE}Path:${NC} $(printf "%-60s" "$path") ${BLUE} │${NC}" - echo -e "${BLUE}│${NC} ${WHITE}Type:${NC} $(printf "%-60s" "$type") ${BLUE} │${NC}" - - set +e - get_device_status "$path" - status_code=$? # capture numeric return - set -e - - case $status_code in - 0) status="${GREEN}${CHECK} Accessible${NC}" ;; - 1) status="${RED}[ERROR] Permission Denied${NC}" ;; - 2) status="${YELLOW}[MISSING] Device not found${NC}" ;; - esac - - echo -e "${BLUE}│${NC} ${WHITE}Status:${NC} $status $(printf "%*s" $((66 - ${#status} + 10)) "") ${BLUE} │${NC}" - echo -e "${BLUE}└───────────────────────────────────────────────────────────────────────────┘${NC}" - echo - fi - - # Collect devices for configuration - if [[ "$type" =~ Keyboard ]]; then - keyboard_devices+=("$path|$name") - if [[ -r "$path" ]]; then - accessible_keyboards+=("$path|$name") - fi - elif [[ "$type" =~ Mouse ]]; then - mouse_devices+=("$path|$name") - if [[ -r "$path" ]]; then - accessible_mice+=("$path|$name") - fi - fi - done <<< "$devices" - - local all_input_devices=() - if [[ "$INCLUDE_MOUSE_DEVICES" == "true" ]]; then - all_input_devices+=("${keyboard_devices[@]}" "${mouse_devices[@]}") - else - all_input_devices+=("${keyboard_devices[@]}") - fi - - if [[ "${#all_input_devices[@]}" -gt 0 ]]; then - generate_config_suggestions "${all_input_devices[@]}" - else - if [[ "$GENERATE_CONFIG" == "false" ]]; then - echo -e "${WARNING} ${YELLOW}No keyboard or mouse devices found${NC}" +quick_config() { + local show_all="$1" + local prefer_byid="$2" # "true" = prefer /dev/input/by-id symlinks + local include_mouse_devices="$3" + shift 3 + local ignore_devices=("$@") # now receives array properly + + local devices + devices=$(get_kbd_devices "${prefer_byid}" "${ignore_devices[@]}") || { error "Cannot read devices"; return 1; } + + if [[ -z "$devices" ]]; then + return 1 + fi + + local keyboards=() + local mouses=() + local others=() + local config_devices=() + + while IFS='|' read -r event name handlers capabilities device_path; do + local status="ok" + check_device "/dev/input/$event" || status="denied" + local type + type=$(get_device_type "${name}" "${handlers}" "${capabilities}") + + if is_likely_mouse "$name"; then + if [[ "$include_mouse_devices" == "true" ]]; then + config_devices+=("$event|$name|$device_path") + else + if [[ "$show_all" == "true" ]]; then + config_devices+=("$event|$name|$device_path") fi - fi - - # Show permission help if needed - if [[ "$INCLUDE_MOUSE_DEVICES" == "true" ]]; then - local total_accessible=$(( ${#accessible_keyboards[@]} + ${#accessible_mice[@]} )) - if [[ "$total_accessible" -lt "${#all_input_devices[@]}" ]] && [[ "$GENERATE_CONFIG" == "false" ]]; then - show_permission_help fi + mouses+=("$event|$name|$device_path") + elif is_likely_keyboard "$name"; then + keyboards+=("$event|$name|$device_path") + config_devices+=("$event|$name|$device_path") else - if [[ "${#accessible_keyboards[@]}" -lt "${#keyboard_devices[@]}" ]] && [[ "$GENERATE_CONFIG" == "false" ]]; then - show_permission_help + others+=("$event|$name|$device_path") + if [[ "$show_all" == "true" ]]; then + config_devices+=("$event|$name|$device_path") fi fi -} + done <<< "$devices" -# Generate configuration suggestions -generate_config_suggestions() { - local devices=("$@") - - if [[ "$GENERATE_CONFIG" == "true" ]]; then - # Calculate max path length for alignment - local maxlen=0 - for device_info in "${devices[@]}"; do - IFS='|' read -r path name <<< "$device_info" - [[ ${#path} -gt $maxlen ]] && maxlen=${#path} - done - - # Generate full config file - if [[ "$GENERATE_DEVICES_ONLY" == "false" ]]; then - cat << 'EOF' -# Wayland Bongo Cat Configuration -# Generated by bongocat-find-devices - -# Visual settings -cat_height=50 # Size of bongo cat (16-128) -cat_x_offset=0 # Horizontal position offset -cat_y_offset=0 # Vertical position offset -cat_align=center # Horizontal alignment -overlay_opacity=0 # Background opacity (0-255) -overlay_height=60 # Height of the entire overlay bar - -# Animation settings -fps=60 # Frame rate (1-144) -keypress_duration=100 # Animation duration (ms) -test_animation_interval=0 # Test animation every N seconds (0=off) -keypress_duration=200 # How long to show animation after keypress - -# Sprite settings -enable_antialiasing=1 -animation_name=bongocat - -EOF - fi - - printf "# Input devices\n" - for device_info in "${devices[@]}"; do - IFS='|' read -r path name <<< "$device_info" - - if [[ -L "$path" ]]; then - local resolved - resolved=$(readlink -f "$path" 2>/dev/null || echo "$path") - printf "keyboard_device=%-${maxlen}s # %s (%s)\n" "$path" "$name" "$resolved" - else - printf "keyboard_device=%-${maxlen}s # %s\n" "$path" "$name" - fi - done - - if [[ "$GENERATE_DEVICES_ONLY" == "false" ]]; then - cat << 'EOF' - -# Debug -enable_debug=1 # Show debug messages -EOF - fi - else - echo -e "${CONFIG} ${WHITE}Configuration Suggestions:${NC}" - echo -e "${WHITE}Add these lines to your bongocat.conf:${NC}" - echo - - # Calculate max path length for alignment - local maxlen=0 - for device_info in "${devices[@]}"; do - IFS='|' read -r path name <<< "$device_info" - [[ ${#path} -gt $maxlen ]] && maxlen=${#path} - done - - for device_info in "${devices[@]}"; do - IFS='|' read -r path name <<< "$device_info" - if [[ -L "$path" ]]; then - local resolved - resolved=$(readlink -f "$path" 2>/dev/null || echo "$path") - printf "${CYAN}keyboard_device=%-${maxlen}s${NC} ${WHITE}# %s (%s)${NC}\n" "$path" "$name" "$resolved" - else - printf "${CYAN}keyboard_device=%-${maxlen}s${NC} ${WHITE}# %s ${NC}\n" "$path" "$name" - fi - done - echo - fi -} + if [[ ${#keyboards[@]} -eq 0 ]]; then + return 1 + fi -# Show permission help -show_permission_help() { - echo -e "${WARNING} ${WHITE}Permission Issues Detected:${NC}" - echo -e "${INFO} Some devices are not accessible. To fix this:" - echo - echo -e "${WHITE}1. Add your user to the input group:${NC}" - echo -e " ${CYAN}sudo usermod -a -G input \$USER${NC}" - echo -e " ${YELLOW}(Log out and back in for changes to take effect)${NC}" - echo - echo -e "${WHITE}2. Or create a udev rule:${NC}" - echo -e " ${CYAN}echo 'KERNEL==\"event*\", GROUP=\"input\", MODE=\"0664\"' | sudo tee /etc/udev/rules.d/99-input.rules${NC}" - echo -e " ${CYAN}sudo udevadm control --reload-rules${NC}" - echo + # Config suggestion + generate_config "${config_devices[@]}" } -# Test device responsiveness -test_devices() { - if ! command -v evtest >/dev/null 2>&1; then - echo -e "${RED}${CROSS} evtest not found${NC}" >&2 - echo -e "${INFO} Install with: ${WHITE}sudo apt install evtest${NC} (Ubuntu/Debian)" >&2 - echo -e "${INFO} Or: ${WHITE}sudo pacman -S evtest${NC} (Arch Linux)" >&2 - return 1 - fi - - if ! check_permissions; then - echo -e "${RED}${CROSS} Root privileges required for device testing${NC}" >&2 - echo -e "${INFO} Run with: ${WHITE}sudo $0 --test${NC}" >&2 - return 1 - fi - - echo -e "${TEST} ${WHITE}Device Testing Mode${NC}" - echo -e "${INFO} This will launch evtest for device testing" - echo -e "${INFO} Press Ctrl+C to exit evtest when done" - echo - - exec evtest +# ───────────────────────────────────────────────────────────────────────────── +# Main +# ───────────────────────────────────────────────────────────────────────────── + +show_usage() { + cat << EOF +${BOLD}$SCRIPT_NAME${NC} v$VERSION - Find keyboards for Bongo Cat + +${BOLD}USAGE${NC} + $SCRIPT_NAME [OPTIONS] + +${BOLD}OPTIONS${NC} + --all Show all input devices (including mice, touchpads) + --by-id Show input devices as id (symlink, if available) + --ignore-device PATTERN Ignore device (multiple arguments) + --include-mouse Include Mouse Device in config + -i, --interactive Detect keyboards by listening for key presses (recommended) + -t, --timeout SEC Detection timeout in seconds (default: 5) + -g, --generate Output config lines only (for piping) + -h, --help Show this help + +${BOLD}EXAMPLES${NC} + $SCRIPT_NAME # Quick detection (name-based) + $SCRIPT_NAME -i # Interactive detection (recommended) + $SCRIPT_NAME -i -t 10 # Interactive with 10 second timeout + $SCRIPT_NAME --generate > bongocat.conf # Generate config file + + { cat bongocat.conf.example; "$SCRIPT_NAME" --generate --devices-only; } > bongocat.conf +EOF } -# Main function main() { - parse_args "$@" - - if [[ "$TEST_DEVICES" == "true" ]]; then - test_devices - return $? - fi - - print_header - display_devices "$SHOW_ALL" "$PREFER_BYID" "${IGNORE_DEVICES[@]}" + local mode="quick" + local timeout=5 + local show_all=false + local prefer_byid=false + local include_mouse_devices=false + local ignore_devices=() + + while [[ $# -gt 0 ]]; do + case "$1" in + --all) show_all=true; shift ;; + --by-id) prefer_byid=true; shift ;; + --include-mouse) include_mouse_devices=true; shift ;; + --ignore-device) + if [[ -n "$2" ]]; then + ignore_devices+=("$2") + shift 2 + else + echo "Error: --ignore-device requires a value" >&2 + exit 1 + fi + ;; + -i|--interactive) mode="interactive"; shift ;; + -t|--timeout) timeout="$2"; shift 2 ;; + -g|--generate) mode="generate"; shift ;; + --generate-config) mode="generate"; shift ;; + -h|--help) show_usage; exit 0 ;; + *) error "Unknown option: $1"; show_usage; exit 1 ;; + esac + done + + case "$mode" in + interactive) interactive_detect "$timeout" "$prefer_byid" ;; + quick) quick_detect "$show_all" "$prefer_byid" "$include_mouse_devices" "${ignore_devices[@]}" ;; + generate) quick_config "$show_all" "$prefer_byid" "$include_mouse_devices" "${ignore_devices[@]}" ;; + esac } -# Run main function with all arguments -main "$@" +main "$@" \ No newline at end of file diff --git a/scripts/test-nix-build.sh b/scripts/test-nix-build.sh deleted file mode 100755 index de38c072..00000000 --- a/scripts/test-nix-build.sh +++ /dev/null @@ -1,38 +0,0 @@ -#!/usr/bin/env bash -set -e - -echo "🧪 Testing Nix builds for wayland-bongocat" -echo "==========================================" - -# Test flake build -echo "📦 Testing flake build..." -if nix flake check --no-build 2>/dev/null; then - echo "✅ Flake check: SUCCESS" - - if nix build --no-link 2>/dev/null; then - echo "✅ Flake build: SUCCESS" - else - echo "❌ Flake build: FAILED" - exit 1 - fi -else - echo "⚠️ Nix flakes not available or flake invalid, skipping" -fi - -# Test development shell -echo "" -echo "🔧 Testing development shell..." -if command -v nix-shell >/dev/null 2>&1; then - if nix-shell nix/shell.nix --run "echo 'Shell works'" >/dev/null 2>&1; then - echo "✅ Development shell: SUCCESS" - else - echo "❌ Development shell: FAILED" - exit 1 - fi -else - echo "⚠️ nix-shell not available, skipping" -fi - -echo "" -echo "🎉 All available Nix builds completed successfully!" -echo "" diff --git a/scripts/test_bongocat.sh b/scripts/test_bongocat.sh index ceb7026f..88571a3f 100755 --- a/scripts/test_bongocat.sh +++ b/scripts/test_bongocat.sh @@ -35,7 +35,7 @@ cleanup() { } trap cleanup EXIT - echo "[INFO] Test Program: ${PROGRAM} --config $CONFIG (pid=${PID})" +echo "[INFO] Test Program: ${PROGRAM} --config $CONFIG (pid=${PID})" echo "[TEST] Sending SIGUSR2..." echo "[INFO] Send SIGUSR2" @@ -385,7 +385,7 @@ for i in {1..5}; do sleep 1 done echo "[TEST] Start with stdin default config..." -cat bongocat.conf | "$PROGRAM" --ignore-running --config - & +cat bongocat.conf.example | "$PROGRAM" --ignore-running --config - & PID=$! sleep 10 # --- verify running --- diff --git a/scripts/test_bongocat_10.sh b/scripts/test_bongocat_10.sh new file mode 100755 index 00000000..11292c51 --- /dev/null +++ b/scripts/test_bongocat_10.sh @@ -0,0 +1,405 @@ +#!/usr/bin/env bash + +set -euo pipefail + +for group in relwithdebinfo-tsan debug-all-assets-preload debug relwithdebinfo; do + find ./cmake-build-* -type f -executable -name "bongocat*" | grep -i "$group" | while read -r PROGRAM; do + WORKDIR=$(mktemp -d) + CONFIG="$WORKDIR/test.bongocat.conf" # config file to modify + OG_CONFIG=./examples/test.bongocat.conf + cp $OG_CONFIG $CONFIG + + echo "[INFO] Test Program: ${PROGRAM} --config $CONFIG ..." + echo "[TEST] Starting program..." + "$PROGRAM" --config "$CONFIG" --ignore-running & + PID=$! + echo "[TEST] Program PID = $PID" + sleep 5 + sed -i -E 's/^animation_name=.*/animation_name=agumon/' "$CONFIG" + sleep 5 + echo "[INFO] Send SIGUSR2" + kill -USR2 "$PID" + sleep 3 + + echo "[INFO] Test Program: ${PROGRAM} --config $CONFIG (pid=${PID})" + + # --- trap cleanup --- + cleanup() { + echo "[TEST] Cleaning up..." + kill -9 "$PID" 2>/dev/null || true + cp $OG_CONFIG $CONFIG + rm -rf "$WORKDIR" + } + trap cleanup EXIT + + echo "[TEST] Sending SIGUSR2..." + echo "[INFO] Send SIGUSR2" + kill -USR2 "$PID" + sleep 3 + echo "[INFO] Send SIGUSR2" + kill -USR2 "$PID" + sleep 5 + echo "[INFO] Spam SIGUSR2" + kill -USR2 "$PID" + kill -USR2 "$PID" + kill -USR2 "$PID" + kill -USR2 "$PID" + sleep 7 + + # --- function to toggle idle_sleep_timeout --- + toggle_config() { + if grep -q '^idle_sleep_timeout=10' "$CONFIG"; then + new=3600 + else + new=10 + fi + sed -i -E "s/^idle_sleep_timeout=[0-9]+/idle_sleep_timeout=$new/" "$CONFIG" + echo "[TEST] Setting idle_sleep_timeout=$new" + } + + # --- modify config to trigger hot reload --- + sed -i -E 's/^cpu_threshold=[0-9]+/cpu_threshold=0/' "$CONFIG" + sed -i 's/^enable_scheduled_sleep=1/enable_scheduled_sleep=0/' "$CONFIG" + sed -i -E 's/^animation_name=.*/animation_name=agumon/' "$CONFIG" + sleep 3 + echo "[TEST] Sending SIGUSR2..." + echo "[INFO] Send SIGUSR2" + kill -USR2 "$PID" + sleep 3 + toggle_config + sleep 10 + toggle_config + sleep 10 + echo "[TEST] Trigger Sleep" + echo "[INFO] Enable idle_sleep_timeout..." + sed -i -E "s/^idle_sleep_timeout=[0-9]+/idle_sleep_timeout=10/" "$CONFIG" + echo "[TEST] Sending SIGUSR2..." + echo "[INFO] Send SIGUSR2" + kill -USR2 "$PID" + sleep 3 + sleep 5 + sed -i 's/^enable_scheduled_sleep=0/enable_scheduled_sleep=1/' "$CONFIG" + echo "[TEST] Sending SIGUSR2..." + echo "[INFO] Send SIGUSR2" + kill -USR2 "$PID" + sleep 3 + sleep 20 + echo "[TEST] Wake up Sleep" + if [[ -f "/proc/$PID/fd/0" ]]; then + printf '\e' > /proc/$PID/fd/0 + sleep 5 + fi + echo "[INFO] Disable idle_sleep_timeout..." + sed -i -E "s/^idle_sleep_timeout=[0-9]+/idle_sleep_timeout=3600/" "$CONFIG" + sleep 5 + echo "[TEST] Sending SIGUSR2..." + echo "[INFO] Send SIGUSR2" + kill -USR2 "$PID" + sleep 3 + sed -i 's/^enable_scheduled_sleep=1/enable_scheduled_sleep=0/' "$CONFIG" + sleep 5 + echo "[TEST] Sending SIGUSR2..." + echo "[INFO] Send SIGUSR2" + kill -USR2 "$PID" + sleep 3 + echo "[TEST] Change animation sprite" + echo "[INFO] Set animation_name..." + sed -i -E 's/^animation_name=.*/animation_name=agumon/' "$CONFIG" + sleep 5 + echo "[INFO] Set animation_name..." + sed -i -E 's/^animation_name=.*/animation_name=greymon/' "$CONFIG" + sleep 3 + echo "[TEST] Sending SIGUSR2..." + echo "[INFO] Send SIGUSR2" + kill -USR2 "$PID" + sleep 5 + + echo "[TEST] Invalid animation sprite" + echo "[INFO] Set animation_name..." + sed -i -E 's/^animation_name=.*/animation_name=NoNo/' "$CONFIG" + sleep 5 + echo "[INFO] Set animation_name..." + sed -i -E 's/^animation_name=.*/animation_name=greymon/' "$CONFIG" + sleep 5 + echo "[TEST] Sending SIGUSR2..." + echo "[INFO] Send SIGUSR2" + kill -USR2 "$PID" + sleep 3 + + echo "[TEST] move and delete config..." + echo "[INFO] Move Config: $CONFIG > ${CONFIG}.del" + mv $CONFIG "${CONFIG}.del" + sleep 5 + rm "${CONFIG}.del" + sleep 5 + echo "[INFO] Recreate Config: $CONFIG" + cp ./examples/digimon.bongocat.conf $CONFIG + sleep 5 + echo "[INFO] Delete Config: $CONFIG" + rm $CONFIG + sleep 5 + echo "[INFO] Recreate Config: $CONFIG" + cp ./examples/digimon.bongocat.conf $CONFIG + sleep 3 + + echo "[INFO] Disable sleep" + sed -i 's/^enable_scheduled_sleep=1/enable_scheduled_sleep=0/' "$CONFIG" + sed -i -E 's/^animation_name=.*/animation_name=agumon/' "$CONFIG" + sleep 10 + if [[ -f "/proc/$PID/fd/0" ]]; then + # --- simulate pressing ESC --- + echo "[TEST] Sending ESC key..." + echo "[INFO] Send stdin" + printf '\e' > /proc/$PID/fd/0 + sleep 5 + echo "[INFO] Send stdin" + printf '\e' > /proc/$PID/fd/0 + sleep 1 + # a bit of a spam + echo "[INFO] Spam stdin" + printf '\e' > /proc/$PID/fd/0 + printf '\e' > /proc/$PID/fd/0 + printf '\e' > /proc/$PID/fd/0 + printf '\e' > /proc/$PID/fd/0 + sleep 5 + echo "[INFO] Spam stdin slower" + printf '\e' > /proc/$PID/fd/0 + sleep 1 + printf '\e' > /proc/$PID/fd/0 + sleep 1 + printf '\e' > /proc/$PID/fd/0 + sleep 1 + printf '\e' > /proc/$PID/fd/0 + sleep 3 + fi + + echo "[INFO] Disable sleep" + sed -i 's/^enable_scheduled_sleep=1/enable_scheduled_sleep=0/' "$CONFIG" + sed -i -E 's/^animation_name=.*/animation_name=agumon/' "$CONFIG" + sleep 5 + echo "[TEST] Sending SIGUSR2..." + echo "[INFO] Send SIGUSR2" + kill -USR2 "$PID" + sleep 3 + echo "[INFO] Send SIGUSR2" + kill -USR2 "$PID" + sleep 5 + echo "[INFO] Spam SIGUSR2" + kill -USR2 "$PID" + kill -USR2 "$PID" + kill -USR2 "$PID" + kill -USR2 "$PID" + sleep 10 + echo "[INFO] Spam SIGUSR2 slower" + kill -USR2 "$PID" + sleep 5 + kill -USR2 "$PID" + sleep 3 + kill -USR2 "$PID" + sleep 2 + kill -USR2 "$PID" + sleep 15 + + echo "[TEST] Sending SIGUSR1..." + echo "[INFO] Send SIGUSR1" + kill -USR1 "$PID" + sleep 2 + echo "[INFO] Send SIGUSR1" + kill -USR1 "$PID" + sleep 2 + + echo "[TEST] replace config..." + echo "[INFO] Replace Config: $CONFIG > ${CONFIG}.del" + cp ./examples/dmc.bongocat.conf $CONFIG + sleep 5 + echo "[TEST] Sending ESC key..." + if [[ -f "/proc/$PID/fd/0" ]]; then + echo "[INFO] Send stdin" + printf '\e' > /proc/$PID/fd/0 + sleep 2 + printf '\e' > /proc/$PID/fd/0 + sleep 2 + printf '\e' > /proc/$PID/fd/0 + sleep 5 + fi + echo "[INFO] Restore old config" + cp $OG_CONFIG $CONFIG + sleep 5 + + echo "[TEST] Fully replace config (Digimon -> Clippy): $CONFIG" + cp ./examples/clippy.bongocat.conf $CONFIG + sleep 5 + + + # --- send SIGTERM --- + echo "[INFO] Sending SIGTERM..." + kill -TERM "$PID" + sleep 15 + echo "[INFO] Wait for TERM" + # wait up to 5 seconds + for i in {1..5}; do + if ! kill -0 "$PID" 2>/dev/null; then + break + fi + sleep 1 + done + echo "[TEST] Re-start..." + "$PROGRAM" --ignore-running --strict --config "$CONFIG" & + PID=$! + sleep 10 + echo "[TEST] Load biggest assets" + echo "[INFO] Set Sprite Sheet: Links" + sed -i -E 's/^invert_color=[0-9]+/invert_color=0/' "$CONFIG" + sed -i -E 's/^animation_name=.*/animation_name=Links/' "$CONFIG" + sleep 2 + echo "[INFO] Send SIGUSR2" + kill -USR2 "$PID" # Reload config + sleep 5 + echo "[INFO] Set Sprite Sheet: pkmn:dialga" + sed -i -E 's/^invert_color=[0-9]+/invert_color=0/' "$CONFIG" + sed -i -E 's/^animation_name=.*/animation_name=pkmn:dialga/' "$CONFIG" + sleep 2 + echo "[INFO] Send SIGUSR2" + kill -USR2 "$PID" # Reload config + sleep 2 + echo "[INFO] Set Sprite Sheet: dmx:Hexeblaumon" + sed -i -E 's/^invert_color=[0-9]+/invert_color=1/' "$CONFIG" + sed -i -E 's/^animation_name=.*/animation_name=dmx:Hexeblaumon/' "$CONFIG" + sleep 2 + echo "[INFO] Send SIGUSR2" + kill -USR2 "$PID" # Reload config + sleep 2 + echo "[INFO] Set Sprite Sheet: dm20:Omegamon" + sed -i -E 's/^invert_color=[0-9]+/invert_color=1/' "$CONFIG" + sed -i -E 's/^animation_name=.*/animation_name=dm20:Omegamon/' "$CONFIG" + sleep 2 + echo "[INFO] Send SIGUSR2" + kill -USR2 "$PID" # Reload config + sleep 2 + echo "[INFO] Set Sprite Sheet: pen20:Megalo Growmon" + sed -i -E 's/^invert_color=[0-9]+/invert_color=1/' "$CONFIG" + sed -i -E 's/^animation_name=.*/animation_name=pen20:Megalo Growmon/' "$CONFIG" + echo "[INFO] Send SIGUSR2" + kill -USR2 "$PID" # Reload config + sleep 2 + echo "[INFO] Set Sprite Sheet: dmc:Omegamon" + sed -i -E 's/^invert_color=[0-9]+/invert_color=0/' "$CONFIG" + sed -i -E 's/^animation_name=.*/animation_name=dmc:Omegamon/' "$CONFIG" + sleep 2 + echo "[INFO] Send SIGUSR2" + kill -USR2 "$PID" # Reload config + sleep 2 + echo "[INFO] Set Sprite Sheet: dm:Coronamon" + sed -i -E 's/^invert_color=[0-9]+/invert_color=1/' "$CONFIG" + sed -i -E 's/^animation_name=.*/animation_name=dm:Coronamon/' "$CONFIG" + sleep 2 + echo "[INFO] Send SIGUSR2" + kill -USR2 "$PID" # Reload config + sleep 2 + echo "[INFO] Set Sprite Sheet: Metal Greymon" + sed -i -E 's/^invert_color=[0-9]+/invert_color=0/' "$CONFIG" + sed -i -E 's/^animation_name=.*/animation_name=Metal Greymon/' "$CONFIG" + sleep 2 + echo "[INFO] Send SIGUSR2" + kill -USR2 "$PID" # Reload config + sleep 2 + echo "[INFO] Set Sprite Sheet: pmd:dialga" + sed -i -E 's/^invert_color=[0-9]+/invert_color=0/' "$CONFIG" + sed -i -E 's/^animation_name=.*/animation_name=pmd:dialga/' "$CONFIG" + sleep 2 + echo "[INFO] Send SIGUSR2" + kill -USR2 "$PID" # Reload config + sleep 5 + + + echo "[TEST] CPU threshold" + echo "[INFO] Enable CPU threshold" + sed -i -E 's/^invert_color=[0-9]+/invert_color=1/' "$CONFIG" + sed -i -E 's/^animation_name=.*/animation_name=dm20:Agumon/' "$CONFIG" + sed -i -E 's/^update_rate=[0-9]+/update_rate=1000/' "$CONFIG" + sed -i -E 's/^cpu_threshold=[0-9]+/cpu_threshold=30/' "$CONFIG" + echo "[INFO] Send SIGUSR2" + kill -USR2 "$PID" # Reload config + sleep 2 + if command -v stress-ng >/dev/null 2>&1; then + echo "[INFO] Running stress-ng to generate load" + stress-ng --cpu 0 --timeout 15s --metrics-brief & + sleep 20 + elif command -v stress >/dev/null 2>&1; then + echo "[INFO] Running stress to generate load" + stress --cpu "$(nproc)" --timeout 15s & + sleep 20 + else + echo "[WARN] No stress tool found, skipping load generation" + fi + echo "[INFO] Disable CPU threshold" + sed -i -E 's/^update_rate=[0-9]+/update_rate=0/' "$CONFIG" + sed -i -E 's/^cpu_threshold=[0-9]+/cpu_threshold=90/' "$CONFIG" + echo "[INFO] Send SIGUSR2" + kill -USR2 "$PID" # Reload config + sleep 5 + + # --- verify running --- + if kill -0 "$PID" 2>/dev/null; then + echo "[PASS] Process $PID still running!" + else + echo "[FAIL] Process terminated" + exit 1 + fi + + # --- send SIGTERM --- + echo "[INFO] Sending SIGTERM..." + kill -TERM "$PID" + echo "[TEST] Reload config while terminating..." + # set config when terminating + sed -i -E 's/^animation_name=.*/animation_name=Tyranomon/' "$CONFIG" + echo "[INFO] Send SIGUSR2" + kill -USR2 "$PID" # Reload config + sleep 5 + sleep 15 + echo "[INFO] Wait for TERM" + # wait up to 5 seconds + for i in {1..5}; do + if ! kill -0 "$PID" 2>/dev/null; then + break + fi + sleep 1 + done + echo "[TEST] Start with stdin config..." + cat "$CONFIG" | "$PROGRAM" --ignore-running --strict --config - & + PID=$! + sleep 10 + sed -i -E 's/^animation_name=.*/animation_name=agumon/' "$CONFIG" + sleep 5 + # --- verify running --- + if kill -0 "$PID" 2>/dev/null; then + echo "[PASS] Process $PID still running!" + else + echo "[FAIL] Process terminated" + exit 1 + fi + + + # --- send SIGTERM --- + echo "[TEST] Sending SIGTERM..." + kill -TERM "$PID" + sleep 10 + echo "[INFO] Wait for TERM" + # wait up to 5 seconds + for i in {1..5}; do + if ! kill -0 "$PID" 2>/dev/null; then + break + fi + sleep 1 + done + + # --- verify not running --- + if kill -0 "$PID" 2>/dev/null; then + echo "[FAIL] Process $PID still running!" + kill -9 "$PID" 2>/dev/null + exit 1 + else + echo "[PASS] Process terminated successfully" + fi + done +done \ No newline at end of file diff --git a/scripts/test_bongocat_8.sh b/scripts/test_bongocat_8.sh index 11292c51..6b68425c 100755 --- a/scripts/test_bongocat_8.sh +++ b/scripts/test_bongocat_8.sh @@ -2,404 +2,159 @@ set -euo pipefail -for group in relwithdebinfo-tsan debug-all-assets-preload debug relwithdebinfo; do - find ./cmake-build-* -type f -executable -name "bongocat*" | grep -i "$group" | while read -r PROGRAM; do - WORKDIR=$(mktemp -d) - CONFIG="$WORKDIR/test.bongocat.conf" # config file to modify - OG_CONFIG=./examples/test.bongocat.conf - cp $OG_CONFIG $CONFIG - - echo "[INFO] Test Program: ${PROGRAM} --config $CONFIG ..." +#make debug +#PROGRAM="./cmake-build-debug-all-assets-colored-preload/bongocat" +#PROGRAM="./cmake-build-debug-all-assets-preload/bongocat-all" +PROGRAM="./cmake-build-debug/bongocat-all" +#PROGRAM="./build/bongocat-all" + +WORKDIR=$(mktemp -d) +CONFIG="$WORKDIR/test.bongocat.conf" # config file to modify +OG_CONFIG=./examples/test.bongocat.conf +cp $OG_CONFIG $CONFIG + +sed -i -E 's/^cat_height=[0-9]+/cat_height=96/' "$CONFIG" +sed -i -E 's/^overlay_height=[0-9]+/overlay_height=128/' "$CONFIG" +sed -i -E 's/^enable_antialiasing=[0-9]+/enable_antialiasing=1/' "$CONFIG" + +if [[ $# -ge 1 ]]; then + PID="$1" + CONFIG="$2" + cp $CONFIG "${CONFIG}.bak" + OG_CONFIG="${CONFIG}.bak" + echo "[TEST] Using provided PID = $PID" +else echo "[TEST] Starting program..." "$PROGRAM" --config "$CONFIG" --ignore-running & PID=$! echo "[TEST] Program PID = $PID" sleep 5 - sed -i -E 's/^animation_name=.*/animation_name=agumon/' "$CONFIG" - sleep 5 - echo "[INFO] Send SIGUSR2" - kill -USR2 "$PID" - sleep 3 - - echo "[INFO] Test Program: ${PROGRAM} --config $CONFIG (pid=${PID})" - - # --- trap cleanup --- - cleanup() { - echo "[TEST] Cleaning up..." - kill -9 "$PID" 2>/dev/null || true - cp $OG_CONFIG $CONFIG - rm -rf "$WORKDIR" - } - trap cleanup EXIT - - echo "[TEST] Sending SIGUSR2..." - echo "[INFO] Send SIGUSR2" - kill -USR2 "$PID" - sleep 3 - echo "[INFO] Send SIGUSR2" - kill -USR2 "$PID" - sleep 5 - echo "[INFO] Spam SIGUSR2" - kill -USR2 "$PID" - kill -USR2 "$PID" - kill -USR2 "$PID" - kill -USR2 "$PID" - sleep 7 - - # --- function to toggle idle_sleep_timeout --- - toggle_config() { - if grep -q '^idle_sleep_timeout=10' "$CONFIG"; then - new=3600 - else - new=10 - fi - sed -i -E "s/^idle_sleep_timeout=[0-9]+/idle_sleep_timeout=$new/" "$CONFIG" - echo "[TEST] Setting idle_sleep_timeout=$new" - } - - # --- modify config to trigger hot reload --- - sed -i -E 's/^cpu_threshold=[0-9]+/cpu_threshold=0/' "$CONFIG" - sed -i 's/^enable_scheduled_sleep=1/enable_scheduled_sleep=0/' "$CONFIG" - sed -i -E 's/^animation_name=.*/animation_name=agumon/' "$CONFIG" - sleep 3 - echo "[TEST] Sending SIGUSR2..." - echo "[INFO] Send SIGUSR2" - kill -USR2 "$PID" - sleep 3 - toggle_config - sleep 10 - toggle_config - sleep 10 - echo "[TEST] Trigger Sleep" - echo "[INFO] Enable idle_sleep_timeout..." - sed -i -E "s/^idle_sleep_timeout=[0-9]+/idle_sleep_timeout=10/" "$CONFIG" - echo "[TEST] Sending SIGUSR2..." - echo "[INFO] Send SIGUSR2" - kill -USR2 "$PID" - sleep 3 - sleep 5 - sed -i 's/^enable_scheduled_sleep=0/enable_scheduled_sleep=1/' "$CONFIG" - echo "[TEST] Sending SIGUSR2..." - echo "[INFO] Send SIGUSR2" - kill -USR2 "$PID" - sleep 3 - sleep 20 - echo "[TEST] Wake up Sleep" - if [[ -f "/proc/$PID/fd/0" ]]; then - printf '\e' > /proc/$PID/fd/0 - sleep 5 - fi - echo "[INFO] Disable idle_sleep_timeout..." - sed -i -E "s/^idle_sleep_timeout=[0-9]+/idle_sleep_timeout=3600/" "$CONFIG" - sleep 5 - echo "[TEST] Sending SIGUSR2..." - echo "[INFO] Send SIGUSR2" - kill -USR2 "$PID" - sleep 3 - sed -i 's/^enable_scheduled_sleep=1/enable_scheduled_sleep=0/' "$CONFIG" - sleep 5 - echo "[TEST] Sending SIGUSR2..." - echo "[INFO] Send SIGUSR2" - kill -USR2 "$PID" - sleep 3 - echo "[TEST] Change animation sprite" - echo "[INFO] Set animation_name..." - sed -i -E 's/^animation_name=.*/animation_name=agumon/' "$CONFIG" - sleep 5 - echo "[INFO] Set animation_name..." - sed -i -E 's/^animation_name=.*/animation_name=greymon/' "$CONFIG" - sleep 3 - echo "[TEST] Sending SIGUSR2..." - echo "[INFO] Send SIGUSR2" - kill -USR2 "$PID" - sleep 5 - - echo "[TEST] Invalid animation sprite" - echo "[INFO] Set animation_name..." - sed -i -E 's/^animation_name=.*/animation_name=NoNo/' "$CONFIG" - sleep 5 - echo "[INFO] Set animation_name..." - sed -i -E 's/^animation_name=.*/animation_name=greymon/' "$CONFIG" - sleep 5 - echo "[TEST] Sending SIGUSR2..." - echo "[INFO] Send SIGUSR2" - kill -USR2 "$PID" - sleep 3 - - echo "[TEST] move and delete config..." - echo "[INFO] Move Config: $CONFIG > ${CONFIG}.del" - mv $CONFIG "${CONFIG}.del" - sleep 5 - rm "${CONFIG}.del" - sleep 5 - echo "[INFO] Recreate Config: $CONFIG" - cp ./examples/digimon.bongocat.conf $CONFIG - sleep 5 - echo "[INFO] Delete Config: $CONFIG" - rm $CONFIG - sleep 5 - echo "[INFO] Recreate Config: $CONFIG" - cp ./examples/digimon.bongocat.conf $CONFIG - sleep 3 - - echo "[INFO] Disable sleep" - sed -i 's/^enable_scheduled_sleep=1/enable_scheduled_sleep=0/' "$CONFIG" - sed -i -E 's/^animation_name=.*/animation_name=agumon/' "$CONFIG" - sleep 10 - if [[ -f "/proc/$PID/fd/0" ]]; then - # --- simulate pressing ESC --- - echo "[TEST] Sending ESC key..." - echo "[INFO] Send stdin" - printf '\e' > /proc/$PID/fd/0 - sleep 5 - echo "[INFO] Send stdin" - printf '\e' > /proc/$PID/fd/0 - sleep 1 - # a bit of a spam - echo "[INFO] Spam stdin" - printf '\e' > /proc/$PID/fd/0 - printf '\e' > /proc/$PID/fd/0 - printf '\e' > /proc/$PID/fd/0 - printf '\e' > /proc/$PID/fd/0 - sleep 5 - echo "[INFO] Spam stdin slower" - printf '\e' > /proc/$PID/fd/0 - sleep 1 - printf '\e' > /proc/$PID/fd/0 - sleep 1 - printf '\e' > /proc/$PID/fd/0 - sleep 1 - printf '\e' > /proc/$PID/fd/0 - sleep 3 - fi - - echo "[INFO] Disable sleep" - sed -i 's/^enable_scheduled_sleep=1/enable_scheduled_sleep=0/' "$CONFIG" - sed -i -E 's/^animation_name=.*/animation_name=agumon/' "$CONFIG" - sleep 5 - echo "[TEST] Sending SIGUSR2..." - echo "[INFO] Send SIGUSR2" - kill -USR2 "$PID" - sleep 3 - echo "[INFO] Send SIGUSR2" - kill -USR2 "$PID" - sleep 5 - echo "[INFO] Spam SIGUSR2" - kill -USR2 "$PID" - kill -USR2 "$PID" - kill -USR2 "$PID" - kill -USR2 "$PID" - sleep 10 - echo "[INFO] Spam SIGUSR2 slower" - kill -USR2 "$PID" - sleep 5 - kill -USR2 "$PID" - sleep 3 - kill -USR2 "$PID" - sleep 2 - kill -USR2 "$PID" - sleep 15 +fi - echo "[TEST] Sending SIGUSR1..." - echo "[INFO] Send SIGUSR1" - kill -USR1 "$PID" - sleep 2 - echo "[INFO] Send SIGUSR1" - kill -USR1 "$PID" - sleep 2 - - echo "[TEST] replace config..." - echo "[INFO] Replace Config: $CONFIG > ${CONFIG}.del" - cp ./examples/dmc.bongocat.conf $CONFIG - sleep 5 - echo "[TEST] Sending ESC key..." - if [[ -f "/proc/$PID/fd/0" ]]; then - echo "[INFO] Send stdin" - printf '\e' > /proc/$PID/fd/0 - sleep 2 - printf '\e' > /proc/$PID/fd/0 - sleep 2 - printf '\e' > /proc/$PID/fd/0 - sleep 5 - fi - echo "[INFO] Restore old config" +# --- trap cleanup --- +cleanup() { + echo "[TEST] Cleaning up..." + kill -9 "$PID" 2>/dev/null || true cp $OG_CONFIG $CONFIG - sleep 5 - - echo "[TEST] Fully replace config (Digimon -> Clippy): $CONFIG" - cp ./examples/clippy.bongocat.conf $CONFIG - sleep 5 - - - # --- send SIGTERM --- - echo "[INFO] Sending SIGTERM..." - kill -TERM "$PID" - sleep 15 - echo "[INFO] Wait for TERM" - # wait up to 5 seconds - for i in {1..5}; do - if ! kill -0 "$PID" 2>/dev/null; then - break - fi - sleep 1 - done - echo "[TEST] Re-start..." - "$PROGRAM" --ignore-running --strict --config "$CONFIG" & - PID=$! - sleep 10 - echo "[TEST] Load biggest assets" - echo "[INFO] Set Sprite Sheet: Links" - sed -i -E 's/^invert_color=[0-9]+/invert_color=0/' "$CONFIG" - sed -i -E 's/^animation_name=.*/animation_name=Links/' "$CONFIG" - sleep 2 - echo "[INFO] Send SIGUSR2" - kill -USR2 "$PID" # Reload config - sleep 5 - echo "[INFO] Set Sprite Sheet: pkmn:dialga" - sed -i -E 's/^invert_color=[0-9]+/invert_color=0/' "$CONFIG" - sed -i -E 's/^animation_name=.*/animation_name=pkmn:dialga/' "$CONFIG" - sleep 2 - echo "[INFO] Send SIGUSR2" - kill -USR2 "$PID" # Reload config - sleep 2 - echo "[INFO] Set Sprite Sheet: dmx:Hexeblaumon" - sed -i -E 's/^invert_color=[0-9]+/invert_color=1/' "$CONFIG" - sed -i -E 's/^animation_name=.*/animation_name=dmx:Hexeblaumon/' "$CONFIG" - sleep 2 - echo "[INFO] Send SIGUSR2" - kill -USR2 "$PID" # Reload config - sleep 2 - echo "[INFO] Set Sprite Sheet: dm20:Omegamon" - sed -i -E 's/^invert_color=[0-9]+/invert_color=1/' "$CONFIG" - sed -i -E 's/^animation_name=.*/animation_name=dm20:Omegamon/' "$CONFIG" - sleep 2 - echo "[INFO] Send SIGUSR2" - kill -USR2 "$PID" # Reload config - sleep 2 - echo "[INFO] Set Sprite Sheet: pen20:Megalo Growmon" - sed -i -E 's/^invert_color=[0-9]+/invert_color=1/' "$CONFIG" - sed -i -E 's/^animation_name=.*/animation_name=pen20:Megalo Growmon/' "$CONFIG" - echo "[INFO] Send SIGUSR2" - kill -USR2 "$PID" # Reload config - sleep 2 - echo "[INFO] Set Sprite Sheet: dmc:Omegamon" - sed -i -E 's/^invert_color=[0-9]+/invert_color=0/' "$CONFIG" - sed -i -E 's/^animation_name=.*/animation_name=dmc:Omegamon/' "$CONFIG" - sleep 2 - echo "[INFO] Send SIGUSR2" - kill -USR2 "$PID" # Reload config - sleep 2 - echo "[INFO] Set Sprite Sheet: dm:Coronamon" - sed -i -E 's/^invert_color=[0-9]+/invert_color=1/' "$CONFIG" - sed -i -E 's/^animation_name=.*/animation_name=dm:Coronamon/' "$CONFIG" - sleep 2 - echo "[INFO] Send SIGUSR2" - kill -USR2 "$PID" # Reload config - sleep 2 - echo "[INFO] Set Sprite Sheet: Metal Greymon" - sed -i -E 's/^invert_color=[0-9]+/invert_color=0/' "$CONFIG" - sed -i -E 's/^animation_name=.*/animation_name=Metal Greymon/' "$CONFIG" - sleep 2 - echo "[INFO] Send SIGUSR2" - kill -USR2 "$PID" # Reload config - sleep 2 - echo "[INFO] Set Sprite Sheet: pmd:dialga" - sed -i -E 's/^invert_color=[0-9]+/invert_color=0/' "$CONFIG" - sed -i -E 's/^animation_name=.*/animation_name=pmd:dialga/' "$CONFIG" - sleep 2 - echo "[INFO] Send SIGUSR2" - kill -USR2 "$PID" # Reload config - sleep 5 - - - echo "[TEST] CPU threshold" - echo "[INFO] Enable CPU threshold" - sed -i -E 's/^invert_color=[0-9]+/invert_color=1/' "$CONFIG" - sed -i -E 's/^animation_name=.*/animation_name=dm20:Agumon/' "$CONFIG" - sed -i -E 's/^update_rate=[0-9]+/update_rate=1000/' "$CONFIG" - sed -i -E 's/^cpu_threshold=[0-9]+/cpu_threshold=30/' "$CONFIG" - echo "[INFO] Send SIGUSR2" - kill -USR2 "$PID" # Reload config - sleep 2 - if command -v stress-ng >/dev/null 2>&1; then - echo "[INFO] Running stress-ng to generate load" - stress-ng --cpu 0 --timeout 15s --metrics-brief & - sleep 20 - elif command -v stress >/dev/null 2>&1; then - echo "[INFO] Running stress to generate load" - stress --cpu "$(nproc)" --timeout 15s & - sleep 20 - else - echo "[WARN] No stress tool found, skipping load generation" - fi - echo "[INFO] Disable CPU threshold" - sed -i -E 's/^update_rate=[0-9]+/update_rate=0/' "$CONFIG" - sed -i -E 's/^cpu_threshold=[0-9]+/cpu_threshold=90/' "$CONFIG" - echo "[INFO] Send SIGUSR2" - kill -USR2 "$PID" # Reload config - sleep 5 - - # --- verify running --- - if kill -0 "$PID" 2>/dev/null; then - echo "[PASS] Process $PID still running!" - else - echo "[FAIL] Process terminated" - exit 1 - fi - - # --- send SIGTERM --- - echo "[INFO] Sending SIGTERM..." - kill -TERM "$PID" - echo "[TEST] Reload config while terminating..." - # set config when terminating - sed -i -E 's/^animation_name=.*/animation_name=Tyranomon/' "$CONFIG" - echo "[INFO] Send SIGUSR2" - kill -USR2 "$PID" # Reload config - sleep 5 - sleep 15 - echo "[INFO] Wait for TERM" - # wait up to 5 seconds - for i in {1..5}; do - if ! kill -0 "$PID" 2>/dev/null; then - break - fi - sleep 1 - done - echo "[TEST] Start with stdin config..." - cat "$CONFIG" | "$PROGRAM" --ignore-running --strict --config - & - PID=$! - sleep 10 - sed -i -E 's/^animation_name=.*/animation_name=agumon/' "$CONFIG" - sleep 5 - # --- verify running --- - if kill -0 "$PID" 2>/dev/null; then - echo "[PASS] Process $PID still running!" - else - echo "[FAIL] Process terminated" - exit 1 - fi - - - # --- send SIGTERM --- - echo "[TEST] Sending SIGTERM..." - kill -TERM "$PID" - sleep 10 - echo "[INFO] Wait for TERM" - # wait up to 5 seconds - for i in {1..5}; do - if ! kill -0 "$PID" 2>/dev/null; then - break - fi - sleep 1 - done - - # --- verify not running --- - if kill -0 "$PID" 2>/dev/null; then - echo "[FAIL] Process $PID still running!" - kill -9 "$PID" 2>/dev/null - exit 1 - else - echo "[PASS] Process terminated successfully" + rm -rf "$WORKDIR" +} +trap cleanup EXIT + +echo "[INFO] Test Program: ${PROGRAM} --config $CONFIG (pid=${PID})" + +echo "[TEST] Sending SIGUSR2..." +echo "[INFO] Send SIGUSR2" +kill -USR2 "$PID" +sleep 3 +echo "[INFO] Send SIGUSR2" +kill -USR2 "$PID" +sleep 5 +echo "[INFO] Spam SIGUSR2" +kill -USR2 "$PID" +kill -USR2 "$PID" +kill -USR2 "$PID" +kill -USR2 "$PID" +sleep 7 + +echo "[TEST] Change overlay settings" +echo "[INFO] Set overlay_height" +sed -i -E 's/^cat_height=[0-9]+/cat_height=96/' "$CONFIG" +sed -i -E 's/^overlay_height=[0-9]+/overlay_height=100/' "$CONFIG" +echo "[INFO] Send SIGUSR2" +kill -USR2 "$PID" # Reload config +sleep 10 +echo "[INFO] Set overlay_position" +sed -i -E 's/^overlay_position=.*/overlay_position=top/' "$CONFIG" +echo "[INFO] Send SIGUSR2" +kill -USR2 "$PID" # Reload config +sleep 10 +echo "[INFO] Set overlay_position" +sed -i -E 's/^overlay_position=.*/overlay_position=bottom/' "$CONFIG" +echo "[INFO] Send SIGUSR2" +kill -USR2 "$PID" # Reload config +sleep 10 +echo "[INFO] Set overlay_layer" +sed -i -E 's/^overlay_layer=.*/overlay_layer=overlay/' "$CONFIG" +echo "[INFO] Send SIGUSR2" +kill -USR2 "$PID" # Reload config +sleep 10 +echo "[INFO] Set overlay_layer" +sed -i -E 's/^overlay_layer=.*/overlay_layer=top/' "$CONFIG" +echo "[INFO] Send SIGUSR2" +kill -USR2 "$PID" # Reload config +sleep 10 +echo "[INFO] Set overlay_height" +sed -i -E 's/^overlay_height=[0-9]+/overlay_height=128/' "$CONFIG" +echo "[INFO] Send SIGUSR2" +kill -USR2 "$PID" # Reload config +sleep 10 + +echo "[INFO] Set max size" +sed -i -E 's/^cat_height=[0-9]+/cat_height=1024/' "$CONFIG" +sed -i -E 's/^overlay_height=[0-9]+/overlay_height=2560/' "$CONFIG" +echo "[INFO] Send SIGUSR2" +kill -USR2 "$PID" # Reload config +sleep 15 +echo "[INFO] Set min size" +sed -i -E 's/^cat_height=[0-9]+/cat_height=16/' "$CONFIG" +sed -i -E 's/^overlay_height=[0-9]+/overlay_height=32/' "$CONFIG" +echo "[INFO] Send SIGUSR2" +kill -USR2 "$PID" # Reload config +sleep 15 + +sleep 20 + +# --- verify running --- +if kill -0 "$PID" 2>/dev/null; then + echo "[PASS] Process $PID still running!" +else + echo "[FAIL] Process terminated" + exit 1 +fi + +echo "[INFO] Set overlay_height" +sed -i -E 's/^cat_height=[0-9]+/cat_height=128/' "$CONFIG" +sed -i -E 's/^overlay_height=[0-9]+/overlay_height=256/' "$CONFIG" +echo "[TEST] Set Monitor" +echo "[INFO] Set monitor" +sed -i -E 's/^monitor=.*/monitor=HDMI-A-1/' "$CONFIG" +echo "[INFO] Send SIGUSR2" +kill -USR2 "$PID" # Reload config +sleep 10 +echo "[INFO] Set monitor" +sed -i -E 's/^monitor=.*/monitor=DP-1/' "$CONFIG" +echo "[INFO] Send SIGUSR2" +kill -USR2 "$PID" # Reload config +sleep 10 + +sleep 20 + +# --- verify running --- +if kill -0 "$PID" 2>/dev/null; then + echo "[PASS] Process $PID still running!" +else + echo "[FAIL] Process terminated" + exit 1 +fi + +# --- send SIGTERM --- +echo "[TEST] Sending SIGTERM..." +kill -TERM "$PID" +sleep 10 +echo "[INFO] Wait for TERM" +# wait up to 5 seconds +for i in {1..5}; do + if ! kill -0 "$PID" 2>/dev/null; then + break fi - done -done \ No newline at end of file + sleep 1 +done + +# --- verify not running --- +if kill -0 "$PID" 2>/dev/null; then + echo "[FAIL] Process $PID still running!" + kill -9 "$PID" 2>/dev/null + exit 1 +else + echo "[PASS] Process terminated successfully" +fi diff --git a/scripts/test_bongocat_9.sh b/scripts/test_bongocat_9.sh new file mode 100755 index 00000000..510cd3c6 --- /dev/null +++ b/scripts/test_bongocat_9.sh @@ -0,0 +1,32 @@ +#!/usr/bin/env bash + +set -euo pipefail + +#make debug +#PROGRAM="./cmake-build-debug-all-assets-colored-preload/bongocat" +PROGRAM="./cmake-build-debug/bongocat-all" +#PROGRAM="./build/bongocat-all" + +echo "[TEST] Starting program (with wrong config)..." +"$PROGRAM" --ignore-running --config 123.not-found.bongocat.conf & +PID=$! +echo "[TEST] Program PID = $PID" +sleep 5 + +# --- trap cleanup --- +cleanup() { + echo "[TEST] Cleaning up..." + kill -9 "$PID" 2>/dev/null || true +} +trap cleanup EXIT + +echo "[INFO] Test Program: ${PROGRAM} (pid=${PID})" + +# --- verify not running --- +if kill -0 "$PID" 2>/dev/null; then + echo "[FAIL] Process $PID still running!" + kill -9 "$PID" 2>/dev/null + exit 1 +else + echo "[PASS] Process terminated successfully" +fi diff --git a/scripts/test_toggle.sh b/scripts/test_toggle.sh index 216d0a6b..1976c553 100755 --- a/scripts/test_toggle.sh +++ b/scripts/test_toggle.sh @@ -15,7 +15,7 @@ if [[ $# -ge 1 ]]; then echo "[TEST] Using provided PID = $TOGGLE_PID" else echo "[TEST] Starting program..." - "$PROGRAM" --toggle --config bongocat.conf & + "$PROGRAM" --toggle --config bongocat.conf.example & TOGGLE_PID=$! echo "[TEST] Program PID = $TOGGLE_PID" sleep 20 @@ -25,7 +25,7 @@ sleep 2 # Toggle off echo "[TEST] Sending --toggle to stop the instance..." -"$PROGRAM" --toggle --config bongocat.conf +"$PROGRAM" --toggle --config bongocat.conf.example # Wait for shutdown for i in {1..10}; do if ! kill -0 "$TOGGLE_PID" 2>/dev/null; then break; fi @@ -42,7 +42,7 @@ fi # Toggle on (should start new instance) echo "[TEST] Sending --toggle to start a new instance..." -"$PROGRAM" --toggle --config bongocat.conf & +"$PROGRAM" --toggle --config bongocat.conf.example & NEW_PID=$! sleep 2 diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index a752b502..cd24e983 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -4,67 +4,59 @@ target_link_libraries(assets_custom_sprite_sheet_feature INTERFACE assets_custom # base options for all execs add_library(bongocat_options INTERFACE) -if (FEATURE_DISABLE_LOGGER) - target_compile_definitions(bongocat_options INTERFACE BONGOCAT_DISABLE_MEMORY_STATISTICS BONGOCAT_DISABLE_LOGGER) +if(FEATURE_DISABLE_LOGGER) + target_compile_definitions(bongocat_options INTERFACE BONGOCAT_DISABLE_MEMORY_STATISTICS BONGOCAT_DISABLE_LOGGER) endif() -if (FEATURE_PRELOAD_ASSETS) - target_compile_definitions(bongocat_options INTERFACE FEATURE_PRELOAD_ASSETS) +if(FEATURE_PRELOAD_ASSETS) + target_compile_definitions(bongocat_options INTERFACE FEATURE_PRELOAD_ASSETS) endif() -if (FEATURE_LAZY_LOAD_ASSETS) - target_compile_definitions(bongocat_options INTERFACE FEATURE_LAZY_LOAD_ASSETS) +if(FEATURE_LAZY_LOAD_ASSETS) + target_compile_definitions(bongocat_options INTERFACE FEATURE_LAZY_LOAD_ASSETS) endif() -if (FEATURE_CUSTOM_SPRITE_SHEETS) - target_compile_definitions(bongocat_options INTERFACE FEATURE_CUSTOM_SPRITE_SHEETS) - target_link_libraries(bongocat_options INTERFACE assets_custom_sprite_sheet_feature assets_custom_loader) +if(FEATURE_CUSTOM_SPRITE_SHEETS) + target_compile_definitions(bongocat_options INTERFACE FEATURE_CUSTOM_SPRITE_SHEETS) + target_link_libraries(bongocat_options INTERFACE assets_custom_sprite_sheet_feature assets_custom_loader) endif() target_link_libraries(bongocat_options INTERFACE project_warnings project_options project_sanitizers) -target_compile_options(bongocat_options INTERFACE - $<$:-fstack-protector-strong> - $<$:-fstack-protector-strong> -) +target_compile_options(bongocat_options INTERFACE $<$:-fstack-protector-strong> + $<$:-fstack-protector-strong>) # supress warning: embed is a Clang extension [-Wc23-extensions] -target_compile_options(bongocat_options INTERFACE - $<$,$>:-std=gnu23> - $<$,$>:-std=gnu++26> - $<$,$>:-Wno-c23-extensions> - $<$,$>:-Wno-c23-extensions> -) +target_compile_options( + bongocat_options + INTERFACE $<$,$>:-std=gnu23> + $<$,$>:-std=gnu++26> + $<$,$>:-Wno-c23-extensions> + $<$,$>:-Wno-c23-extensions>) # no exceptions -target_compile_options(bongocat_options INTERFACE - $<$: - -fno-exceptions -fno-unwind-tables -fno-rtti - -nostdlib++ - > -) +target_compile_options(bongocat_options INTERFACE $<$: -fno-exceptions -fno-unwind-tables -fno-rtti -nostdlib++ >) # optimization -target_compile_options(bongocat_options INTERFACE - $<$: - -march=native - -fomit-frame-pointer - -funroll-loops - -finline-functions - > - $<$: - -fno-inline-functions - -fomit-frame-pointer - -fno-asynchronous-unwind-tables - -fno-unroll-loops - -fmerge-all-constants - -fno-math-errno - -fno-stack-protector - > -) -target_compile_definitions(bongocat_options INTERFACE - $<$:BONGOCAT_LOG_LEVEL=5> # Verbose - $<$:BONGOCAT_LOG_LEVEL=4> # Debug - $<$:BONGOCAT_LOG_LEVEL=3> # Info - #$<$:BONGOCAT_LOG_LEVEL=0> # NONE - $<$:BONGOCAT_DISABLE_MEMORY_STATISTICS> - $<$:BONGOCAT_DISABLE_LOGGER> -) +target_compile_options( + bongocat_options + INTERFACE $<$: + -march=native + -fomit-frame-pointer + -funroll-loops + -finline-functions + > + $<$: + -fno-inline-functions + -fomit-frame-pointer + -fno-asynchronous-unwind-tables + -fno-unroll-loops + -fmerge-all-constants + -fno-math-errno + -fno-stack-protector + >) +target_compile_definitions( + bongocat_options + INTERFACE $<$:BONGOCAT_LOG_LEVEL=5> # Verbose + $<$:BONGOCAT_LOG_LEVEL=4> # Debug + $<$:BONGOCAT_LOG_LEVEL=3> # Info + # $<$:BONGOCAT_LOG_LEVEL=0> # NONE + $<$:BONGOCAT_DISABLE_MEMORY_STATISTICS> + $<$:BONGOCAT_DISABLE_LOGGER>) target_link_options(bongocat_options INTERFACE $<$:-Wl,--gc-sections -s>) - add_library(bongocat_libs INTERFACE) # wayland-client dependency find_package(PkgConfig REQUIRED) @@ -85,25 +77,25 @@ target_link_options(bongocat_libs INTERFACE -Wl,--gc-sections) # base for assembling exec(s) set(SOURCES - utils/error.cpp - utils/memory.cpp - utils/random.cpp - utils/system_memory.cpp - utils/time.cpp - config/config.cpp - config/config_watcher.cpp - core/main.cpp - graphics/animation.cpp - graphics/animation_init.cpp - graphics/bar.cpp - graphics/drawing_images.cpp - platform/input.cpp - platform/update.cpp - platform/wayland.cpp - platform/wayland_callbacks.cpp - platform/wayland_hyprland.cpp - platform/wayland_sway.cpp -) + utils/error.cpp + utils/memory.cpp + utils/random.cpp + utils/system_memory.cpp + utils/time.cpp + config/config.cpp + config/config_watcher.cpp + core/main.cpp + graphics/animation.cpp + graphics/animation_init.cpp + graphics/bar.cpp + graphics/drawing_images.cpp + platform/input.cpp + platform/update.cpp + platform/wayland.cpp + platform/wayland_callbacks.cpp + platform/wayland_hyprland.cpp + platform/wayland_setups.cpp + platform/wayland_sway.cpp) add_library(bongocat_base INTERFACE) target_sources(bongocat_base INTERFACE ${SOURCES}) target_include_directories(bongocat_base INTERFACE ${INCLUDE_DIR}) @@ -124,4 +116,4 @@ add_subdirectory(embedded_assets/ms_agent) add_subdirectory(embedded_assets/pkmn) add_subdirectory(embedded_assets/misc) add_subdirectory(embedded_assets/pmd) -add_subdirectory(image_loader) \ No newline at end of file +add_subdirectory(image_loader) diff --git a/src/config/config.cpp b/src/config/config.cpp index f25cd33e..278167a1 100644 --- a/src/config/config.cpp +++ b/src/config/config.cpp @@ -1,1839 +1,1977 @@ #include "config/config.h" -#include "core/bongocat.h" -#include "utils/error.h" -#include "graphics/animation_context.h" -#include -#include -#include -#include -#include "graphics/embedded_assets_dms.h" -#include "graphics/embedded_assets_pkmn.h" +#include "core/bongocat.h" #include "embedded_assets/bongocat/bongocat.h" #include "embedded_assets/bongocat/bongocat.hpp" +#include "embedded_assets/misc/misc.hpp" +#include "embedded_assets/misc/misc_sprite.h" #include "embedded_assets/ms_agent/ms_agent.hpp" #include "embedded_assets/ms_agent/ms_agent_sprite.h" #include "embedded_assets/pkmn/pkmn_sprite.h" -#include "embedded_assets/misc/misc.hpp" -#include "embedded_assets/misc/misc_sprite.h" #include "embedded_assets/pmd/pmd_sprite.h" +#include "graphics/animation_thread_context.h" +#include "graphics/embedded_assets_dms.h" +#include "graphics/embedded_assets_pkmn.h" +#include "utils/error.h" + +#include +#include +#include +#include #ifdef FEATURE_DM_EMBEDDED_ASSETS -#include "dm_config_parse_animation_name.h" +# include "dm_config_parse_animation_name.h" #endif #ifdef FEATURE_DM20_EMBEDDED_ASSETS -#include "dm20_config_parse_animation_name.h" +# include "dm20_config_parse_animation_name.h" #endif #ifdef FEATURE_DMX_EMBEDDED_ASSETS -#include "dmx_config_parse_animation_name.h" +# include "dmx_config_parse_animation_name.h" #endif #ifdef FEATURE_DMC_EMBEDDED_ASSETS -#include "dmc_config_parse_animation_name.h" +# include "dmc_config_parse_animation_name.h" #endif #ifdef FEATURE_PEN_EMBEDDED_ASSETS -#include "pen_config_parse_animation_name.h" +# include "pen_config_parse_animation_name.h" #endif #ifdef FEATURE_PEN20_EMBEDDED_ASSETS -#include "pen20_config_parse_animation_name.h" +# include "pen20_config_parse_animation_name.h" #endif #ifdef FEATURE_DMALL_EMBEDDED_ASSETS -#include "dmall_config_parse_animation_name.h" +# include "dmall_config_parse_animation_name.h" #endif #ifdef FEATURE_PKMN_EMBEDDED_ASSETS -#include "pkmn_config_parse_animation_name.h" +# include "pkmn_config_parse_animation_name.h" #endif #ifdef FEATURE_PMD_EMBEDDED_ASSETS -#include "pmd_config_parse_animation_name.h" +# include "pmd_config_parse_animation_name.h" #endif - // ============================================================================= // CONFIGURATION CONSTANTS AND VALIDATION RANGES // ============================================================================= namespace bongocat::config { - static inline constexpr int MIN_CAT_HEIGHT = 10; - static inline constexpr int MAX_CAT_HEIGHT = 200; - static inline constexpr int MIN_OVERLAY_HEIGHT = 20; - static inline constexpr int MAX_OVERLAY_HEIGHT = 300; - static inline constexpr int MIN_FPS = 1; - static inline constexpr int MAX_FPS = 144; - static inline constexpr int MIN_DURATION_MS = 10; - static inline constexpr int MAX_DURATION_MS = 5000; - static inline constexpr int MAX_INTERVAL_SEC = 3600; - static inline constexpr int MIN_KPM = 0; - static inline constexpr int MAX_KPM = 10000; - static inline constexpr double MAX_CPU_THRESHOLD = 100.0; - static inline constexpr double MAX_CPU_RUNNING_FACTOR = 50.0; - static inline constexpr int MAX_UPDATE_RATE_MS = 60 * 60 * 1000; - static inline constexpr int MAX_SLEEP_TIMEOUT_SEC = 30 * 24 * 60 * 60; - static inline constexpr int MIN_OFFSET = -16000; - static inline constexpr int MAX_OFFSET = 16000; - static inline constexpr int MIN_MOVEMENT_RADIUS = 0; - static inline constexpr int MAX_MOVEMENT_RADIUS = MAX_OFFSET/2; - - static inline constexpr int MIN_CUSTOM_FRAMES = 0; - static inline constexpr int MAX_CUSTOM_FRAMES = 512; - static inline constexpr int MIN_CUSTOM_ROWS = 0; - static_assert(assets::CUSTOM_SPRITE_SHEET_MAX_ROWS > 0); - static inline constexpr int MAX_CUSTOM_ROWS = assets::CUSTOM_SPRITE_SHEET_MAX_ROWS-1; - - static_assert(MIN_FPS > 0, "FPS cannot be zero, for math reasons"); - - // Default settings - static inline constexpr auto DEFAULT_DEVICE = "/dev/input/event4"; - static inline constexpr auto DEFAULT_CONFIG_FILE_PATH = "bongocat.conf"; - - static inline constexpr int32_t DEFAULT_CAT_X_OFFSET = 100; - static inline constexpr int32_t DEFAULT_CAT_Y_OFFSET = 10; - static inline constexpr int32_t DEFAULT_CAT_HEIGHT = 40; - static inline constexpr int32_t DEFAULT_OVERLAY_HEIGHT = 50; - static inline constexpr int32_t DEFAULT_IDLE_FRAME = 0; - static inline constexpr platform::time_ms_t DEFAULT_KEYPRESS_DURATION_MS = 100; - static inline constexpr int32_t DEFAULT_OVERLAY_OPACITY = 60; - static inline constexpr int32_t DEFAULT_ANIMATION_INDEX = 0; - static inline constexpr layer_type_t DEFAULT_LAYER = layer_type_t::LAYER_OVERLAY; - static inline constexpr overlay_position_t DEFAULT_OVERLAY_POSITION = overlay_position_t::POSITION_TOP; - static inline constexpr int32_t DEFAULT_HAPPY_KPM = 0; - static inline constexpr platform::time_sec_t DEFAULT_IDLE_SLEEP_TIMEOUT_SEC = 0; - static inline constexpr align_type_t DEFAULT_CAT_ALIGN = align_type_t::ALIGN_CENTER; - static inline constexpr platform::time_ms_t DEFAULT_TEST_ANIMATION_DURATION_MS = 0; - static inline constexpr platform::time_sec_t DEFAULT_TEST_ANIMATION_INTERVAL_SEC = 0; - static inline constexpr int32_t DEFAULT_ENABLE_ANTIALIASING = 1; - static inline constexpr double DEFAULT_MOVEMENT_WAIT_FACTOR = 5.1; - - // Debug-specific defaults +static inline constexpr int MIN_CAT_HEIGHT = 8; +static inline constexpr int MAX_CAT_HEIGHT = 1024; +static inline constexpr int MIN_OVERLAY_HEIGHT = 16; +static inline constexpr int MAX_OVERLAY_HEIGHT = 2560; +static inline constexpr int MIN_FPS = 1; +static inline constexpr int MAX_FPS = 144; +static inline constexpr int MIN_DURATION_MS = 10; +static inline constexpr int MAX_DURATION_MS = 5000; +static inline constexpr int MAX_INTERVAL_SEC = 3600; +static inline constexpr int MIN_KPM = 0; +static inline constexpr int MAX_KPM = 10000; +static inline constexpr double MAX_CPU_THRESHOLD = 100.0; +static inline constexpr double MAX_CPU_RUNNING_FACTOR = 50.0; +static inline constexpr int MAX_UPDATE_RATE_MS = 60 * 60 * 1000; +static inline constexpr int MAX_SLEEP_TIMEOUT_SEC = 30 * 24 * 60 * 60; +static inline constexpr int MIN_OFFSET = -16000; +static inline constexpr int MAX_OFFSET = 16000; +static inline constexpr int MIN_MOVEMENT_RADIUS = 0; +static inline constexpr int MAX_MOVEMENT_RADIUS = MAX_OFFSET / 2; + +static inline constexpr int MIN_CUSTOM_FRAMES = 0; +static inline constexpr int MAX_CUSTOM_FRAMES = 512; +static inline constexpr int MIN_CUSTOM_ROWS = 0; +static_assert(assets::CUSTOM_SPRITE_SHEET_MAX_ROWS > 0); +static inline constexpr int MAX_CUSTOM_ROWS = assets::CUSTOM_SPRITE_SHEET_MAX_ROWS - 1; + +static_assert(MIN_FPS > 0, "FPS cannot be zero, for math reasons"); + +// Default settings +static inline constexpr auto DEFAULT_DEVICE = "/dev/input/event4"; +static inline constexpr auto DEFAULT_CONFIG_FILE_PATH = "bongocat.conf"; + +static inline constexpr int32_t DEFAULT_CAT_X_OFFSET = 100; +static inline constexpr int32_t DEFAULT_CAT_Y_OFFSET = 10; +static inline constexpr int32_t DEFAULT_CAT_HEIGHT = 40; +static inline constexpr int32_t DEFAULT_OVERLAY_HEIGHT = 80; +static inline constexpr int32_t DEFAULT_IDLE_FRAME = 0; +static inline constexpr platform::time_ms_t DEFAULT_KEYPRESS_DURATION_MS = 100; +static inline constexpr int32_t DEFAULT_OVERLAY_OPACITY = 0; +static inline constexpr int32_t DEFAULT_ANIMATION_INDEX = 0; +static inline constexpr layer_type_t DEFAULT_LAYER = layer_type_t::LAYER_TOP; +static inline constexpr overlay_position_t DEFAULT_OVERLAY_POSITION = overlay_position_t::POSITION_TOP; +static inline constexpr int32_t DEFAULT_HAPPY_KPM = 0; +static inline constexpr platform::time_sec_t DEFAULT_IDLE_SLEEP_TIMEOUT_SEC = 0; +static inline constexpr align_type_t DEFAULT_CAT_ALIGN = align_type_t::ALIGN_CENTER; +static inline constexpr platform::time_ms_t DEFAULT_TEST_ANIMATION_DURATION_MS = 0; +static inline constexpr platform::time_sec_t DEFAULT_TEST_ANIMATION_INTERVAL_SEC = 0; +static inline constexpr int32_t DEFAULT_ENABLE_ANTIALIASING = 1; +static inline constexpr double DEFAULT_MOVEMENT_WAIT_FACTOR = 5.1; +static inline constexpr int32_t DEFAULT_ENABLE_HAND_MAPPING = 1; + +// Debug-specific defaults #ifndef NDEBUG - static inline constexpr int32_t DEFAULT_ENABLE_DEBUG = 1; +static inline constexpr int32_t DEFAULT_ENABLE_DEBUG = 1; #else - static inline constexpr int32_t DEFAULT_ENABLE_DEBUG = 0; +static inline constexpr int32_t DEFAULT_ENABLE_DEBUG = 0; #endif +static inline constexpr auto CAT_X_OFFSET_KEY = "cat_x_offset"; +static inline constexpr auto CAT_Y_OFFSET_KEY = "cat_y_offset"; +static inline constexpr auto CAT_HEIGHT_KEY = "cat_height"; +static inline constexpr auto OVERLAY_HEIGHT_KEY = "overlay_height"; +static inline constexpr auto OVERLAY_POSITION_KEY = "overlay_position"; +static inline constexpr auto ANIMATION_NAME_KEY = "animation_name"; +static inline constexpr auto INVERT_COLOR_KEY = "invert_color"; +static inline constexpr auto PADDING_X_KEY = "padding_x"; +static inline constexpr auto PADDING_Y_KEY = "padding_y"; +static inline constexpr auto IDLE_FRAME_KEY = "idle_frame"; +static inline constexpr auto ENABLE_SCHEDULED_SLEEP_KEY = "enable_scheduled_sleep"; +static inline constexpr auto SLEEP_BEGIN_KEY = "sleep_begin"; +static inline constexpr auto SLEEP_END_KEY = "sleep_end"; +static inline constexpr auto IDLE_SLEEP_TIMEOUT_KEY = "idle_sleep_timeout"; +static inline constexpr auto HAPPY_KPM_KEY = "happy_kpm"; +static inline constexpr auto KEYPRESS_DURATION_KEY = "keypress_duration"; +static inline constexpr auto TEST_ANIMATION_DURATION_KEY = "test_animation_duration"; +static inline constexpr auto TEST_ANIMATION_INTERVAL_KEY = "test_animation_interval"; +static inline constexpr auto ANIMATION_SPEED_KEY = "animation_speed"; +static inline constexpr auto FPS_KEY = "fps"; +static inline constexpr auto OVERLAY_OPACITY_KEY = "overlay_opacity"; +static inline constexpr auto ENABLE_DEBUG_KEY = "enable_debug"; +static inline constexpr auto KEYBOARD_DEVICE_KEY = "keyboard_device"; +static inline constexpr auto KEYBOARD_DEVICES_KEY = "keyboard_devices"; +static inline constexpr auto ANIMATION_INDEX_KEY = "animation_index"; +static inline constexpr auto LAYER_KEY = "layer"; ///< DEPRECATED: use overlay_layer +static inline constexpr auto OVERLAY_LAYER_KEY = "overlay_layer"; +static inline constexpr auto CAT_ALIGN_KEY = "cat_align"; +static inline constexpr auto IDLE_ANIMATION_KEY = "idle_animation"; +static inline constexpr auto INPUT_FPS_KEY = "input_fps"; +static inline constexpr auto MIRROR_X_KEY = "mirror_x"; +static inline constexpr auto MIRROR_Y_KEY = "mirror_y"; +static inline constexpr auto RANDOM_KEY = "random"; +static inline constexpr auto RANDOM_ON_RELOAD_KEY = "random_on_reload"; +static inline constexpr auto ENABLE_ANTIALIASING_KEY = "enable_antialiasing"; +static inline constexpr auto UPDATE_RATE_KEY = "update_rate"; +static inline constexpr auto CPU_THRESHOLD_KEY = "cpu_threshold"; +static inline constexpr auto CPU_RUNNING_FACTOR_KEY = "cpu_running_factor"; +static inline constexpr auto MOVEMENT_RADIUS_KEY = "movement_radius"; +static inline constexpr auto ENABLE_MOVEMENT_DEBUG_KEY = "enable_movement_debug"; +static inline constexpr auto MOVEMENT_SPEED_KEY = "movement_speed"; +static inline constexpr auto MOVEMENT_WAIT_FACTOR_KEY = "movement_wait_factor"; +static inline constexpr auto SCREEN_WIDTH_KEY = "screen_width"; +static inline constexpr auto MONITOR_KEY = "monitor"; +static inline constexpr auto OUTPUT_NAME_KEY = "output_name"; // monitor alt key +static inline constexpr auto ENABLE_HAND_MAPPING_KEY = "enable_hand_mapping"; + +static inline constexpr auto CUSTOM_SPRITE_SHEET_FILENAME_KEY = "custom_sprite_sheet_filename"; +static inline constexpr auto CUSTOM_IDLE_FRAMES_KEY = "custom_idle_frames"; +static inline constexpr auto CUSTOM_BORING_FRAMES_KEY = "custom_boring_frames"; +static inline constexpr auto CUSTOM_START_WRITING_FRAMES_KEY = "custom_start_writing_frames"; +static inline constexpr auto CUSTOM_WRITING_FRAMES_KEY = "custom_writing_frames"; +static inline constexpr auto CUSTOM_END_WRITING_FRAMES_KEY = "custom_end_writing_frames"; +static inline constexpr auto CUSTOM_HAPPY_FRAMES_KEY = "custom_happy_frames"; +static inline constexpr auto CUSTOM_ASLEEP_FRAMES_KEY = "custom_asleep_frames"; +static inline constexpr auto CUSTOM_SLEEP_FRAMES_KEY = "custom_sleep_frames"; +static inline constexpr auto CUSTOM_WAKE_UP_FRAMES_KEY = "custom_wake_up_frames"; +static inline constexpr auto CUSTOM_START_WORKING_FRAMES_KEY = "custom_start_working_frames"; +static inline constexpr auto CUSTOM_WORKING_FRAMES_KEY = "custom_working_frames"; +static inline constexpr auto CUSTOM_END_WORKING_FRAMES_KEY = "custom_end_working_frames"; +static inline constexpr auto CUSTOM_START_MOVING_FRAMES_KEY = "custom_start_moving_frames"; +static inline constexpr auto CUSTOM_MOVING_FRAMES_KEY = "custom_moving_frames"; +static inline constexpr auto CUSTOM_END_MOVING_FRAMES_KEY = "custom_end_moving_frames"; +static inline constexpr auto CUSTOM_START_RUNNING_FRAMES_KEY = "custom_start_running_frames"; +static inline constexpr auto CUSTOM_RUNNING_FRAMES_KEY = "custom_running_frames"; +static inline constexpr auto CUSTOM_END_RUNNING_FRAMES_KEY = "custom_end_running_frames"; + +static inline constexpr auto CUSTOM_TOGGLE_WRITING_FRAMES_KEY = "custom_toggle_writing_frames"; +static inline constexpr auto CUSTOM_TOGGLE_WRITING_FRAMES_RANDOM_KEY = "custom_toggle_writing_frames_random"; +static inline constexpr auto CUSTOM_MIRROR_X_MOVING_KEY = "custom_mirror_x_moving"; + +static inline constexpr auto CUSTOM_IDLE_ROW_KEY = "custom_idle_row"; +static inline constexpr auto CUSTOM_BORING_ROW_KEY = "custom_boring_row"; +static inline constexpr auto CUSTOM_START_WRITING_ROW_KEY = "custom_start_writing_row"; +static inline constexpr auto CUSTOM_WRITING_ROW_KEY = "custom_writing_row"; +static inline constexpr auto CUSTOM_END_WRITING_ROW_KEY = "custom_end_writing_row"; +static inline constexpr auto CUSTOM_HAPPY_ROW_KEY = "custom_happy_row"; +static inline constexpr auto CUSTOM_ASLEEP_ROW_KEY = "custom_asleep_row"; +static inline constexpr auto CUSTOM_SLEEP_ROW_KEY = "custom_sleep_row"; +static inline constexpr auto CUSTOM_WAKE_UP_ROW_KEY = "custom_wake_up_row"; +static inline constexpr auto CUSTOM_START_WORKING_ROW_KEY = "custom_start_working_row"; +static inline constexpr auto CUSTOM_WORKING_ROW_KEY = "custom_working_row"; +static inline constexpr auto CUSTOM_END_WORKING_ROW_KEY = "custom_end_working_row"; +static inline constexpr auto CUSTOM_START_MOVING_ROW_KEY = "custom_start_moving_row"; +static inline constexpr auto CUSTOM_MOVING_ROW_KEY = "custom_moving_row"; +static inline constexpr auto CUSTOM_END_MOVING_ROW_KEY = "custom_end_moving_row"; +static inline constexpr auto CUSTOM_START_RUNNING_ROW_KEY = "custom_start_running_row"; +static inline constexpr auto CUSTOM_RUNNING_ROW_KEY = "custom_running_row"; +static inline constexpr auto CUSTOM_END_RUNNING_ROW_KEY = "custom_end_running_row"; +static inline constexpr auto CUSTOM_ROWS_KEY = "custom_rows"; + +static inline constexpr size_t KEY_BUF = 256; +static inline constexpr size_t VALUE_BUF = PATH_MAX + 256; // max value + comment +static inline constexpr size_t LINE_BUF = KEY_BUF - 1 + VALUE_BUF - 1 + 1 + 1; // key + '=' + value + '\0' - static inline constexpr auto CAT_X_OFFSET_KEY = "cat_x_offset"; - static inline constexpr auto CAT_Y_OFFSET_KEY = "cat_y_offset"; - static inline constexpr auto CAT_HEIGHT_KEY = "cat_height"; - static inline constexpr auto OVERLAY_HEIGHT_KEY = "overlay_height"; - static inline constexpr auto OVERLAY_POSITION_KEY = "overlay_position"; - static inline constexpr auto ANIMATION_NAME_KEY = "animation_name"; - static inline constexpr auto INVERT_COLOR_KEY = "invert_color"; - static inline constexpr auto PADDING_X_KEY = "padding_x"; - static inline constexpr auto PADDING_Y_KEY = "padding_y"; - static inline constexpr auto IDLE_FRAME_KEY = "idle_frame"; - static inline constexpr auto ENABLE_SCHEDULED_SLEEP_KEY = "enable_scheduled_sleep"; - static inline constexpr auto SLEEP_BEGIN_KEY = "sleep_begin"; - static inline constexpr auto SLEEP_END_KEY = "sleep_end"; - static inline constexpr auto IDLE_SLEEP_TIMEOUT_KEY = "idle_sleep_timeout"; - static inline constexpr auto HAPPY_KPM_KEY = "happy_kpm"; - static inline constexpr auto KEYPRESS_DURATION_KEY = "keypress_duration"; - static inline constexpr auto TEST_ANIMATION_DURATION_KEY = "test_animation_duration"; - static inline constexpr auto TEST_ANIMATION_INTERVAL_KEY = "test_animation_interval"; - static inline constexpr auto ANIMATION_SPEED_KEY = "animation_speed"; - static inline constexpr auto FPS_KEY = "fps"; - static inline constexpr auto OVERLAY_OPACITY_KEY = "overlay_opacity"; - static inline constexpr auto ENABLE_DEBUG_KEY = "enable_debug"; - static inline constexpr auto KEYBOARD_DEVICE_KEY = "keyboard_device"; - static inline constexpr auto KEYBOARD_DEVICES_KEY = "keyboard_devices"; - static inline constexpr auto ANIMATION_INDEX_KEY = "animation_index"; - static inline constexpr auto LAYER_KEY = "layer"; ///< DEPRECATED: use overlay_layer - static inline constexpr auto OVERLAY_LAYER_KEY = "overlay_layer"; - static inline constexpr auto CAT_ALIGN_KEY = "cat_align"; - static inline constexpr auto IDLE_ANIMATION_KEY = "idle_animation"; - static inline constexpr auto INPUT_FPS_KEY = "input_fps"; - static inline constexpr auto MIRROR_X_KEY = "mirror_x"; - static inline constexpr auto MIRROR_Y_KEY = "mirror_y"; - static inline constexpr auto RANDOM_KEY = "random"; - static inline constexpr auto RANDOM_ON_RELOAD_KEY = "random_on_reload"; - static inline constexpr auto ENABLE_ANTIALIASING_KEY = "enable_antialiasing"; - static inline constexpr auto UPDATE_RATE_KEY = "update_rate"; - static inline constexpr auto CPU_THRESHOLD_KEY = "cpu_threshold"; - static inline constexpr auto CPU_RUNNING_FACTOR_KEY = "cpu_running_factor"; - static inline constexpr auto MOVEMENT_RADIUS_KEY = "movement_radius"; - static inline constexpr auto ENABLE_MOVEMENT_DEBUG_KEY = "enable_movement_debug"; - static inline constexpr auto MOVEMENT_SPEED_KEY = "movement_speed"; - static inline constexpr auto MOVEMENT_WAIT_FACTOR_KEY = "movement_wait_factor"; - static inline constexpr auto SCREEN_WIDTH_KEY = "screen_width"; - static inline constexpr auto MONITOR_KEY = "monitor"; - static inline constexpr auto OUTPUT_NAME_KEY = "output_name"; // monitor alt key - - static inline constexpr auto CUSTOM_SPRITE_SHEET_FILENAME_KEY = "custom_sprite_sheet_filename"; - static inline constexpr auto CUSTOM_IDLE_FRAMES_KEY = "custom_idle_frames"; - static inline constexpr auto CUSTOM_BORING_FRAMES_KEY = "custom_boring_frames"; - static inline constexpr auto CUSTOM_START_WRITING_FRAMES_KEY = "custom_start_writing_frames"; - static inline constexpr auto CUSTOM_WRITING_FRAMES_KEY = "custom_writing_frames"; - static inline constexpr auto CUSTOM_END_WRITING_FRAMES_KEY = "custom_end_writing_frames"; - static inline constexpr auto CUSTOM_HAPPY_FRAMES_KEY = "custom_happy_frames"; - static inline constexpr auto CUSTOM_ASLEEP_FRAMES_KEY = "custom_asleep_frames"; - static inline constexpr auto CUSTOM_SLEEP_FRAMES_KEY = "custom_sleep_frames"; - static inline constexpr auto CUSTOM_WAKE_UP_FRAMES_KEY = "custom_wake_up_frames"; - static inline constexpr auto CUSTOM_START_WORKING_FRAMES_KEY = "custom_start_working_frames"; - static inline constexpr auto CUSTOM_WORKING_FRAMES_KEY = "custom_working_frames"; - static inline constexpr auto CUSTOM_END_WORKING_FRAMES_KEY = "custom_end_working_frames"; - static inline constexpr auto CUSTOM_START_MOVING_FRAMES_KEY = "custom_start_moving_frames"; - static inline constexpr auto CUSTOM_MOVING_FRAMES_KEY = "custom_moving_frames"; - static inline constexpr auto CUSTOM_END_MOVING_FRAMES_KEY = "custom_end_moving_frames"; - static inline constexpr auto CUSTOM_START_RUNNING_FRAMES_KEY = "custom_start_running_frames"; - static inline constexpr auto CUSTOM_RUNNING_FRAMES_KEY = "custom_running_frames"; - static inline constexpr auto CUSTOM_END_RUNNING_FRAMES_KEY = "custom_end_running_frames"; - - static inline constexpr auto CUSTOM_TOGGLE_WRITING_FRAMES_KEY = "custom_toggle_writing_frames"; - static inline constexpr auto CUSTOM_TOGGLE_WRITING_FRAMES_RANDOM_KEY = "custom_toggle_writing_frames_random"; - static inline constexpr auto CUSTOM_MIRROR_X_MOVING_KEY = "custom_mirror_x_moving"; - - static inline constexpr auto CUSTOM_IDLE_ROW_KEY = "custom_idle_row"; - static inline constexpr auto CUSTOM_BORING_ROW_KEY = "custom_boring_row"; - static inline constexpr auto CUSTOM_START_WRITING_ROW_KEY = "custom_start_writing_row"; - static inline constexpr auto CUSTOM_WRITING_ROW_KEY = "custom_writing_row"; - static inline constexpr auto CUSTOM_END_WRITING_ROW_KEY = "custom_end_writing_row"; - static inline constexpr auto CUSTOM_HAPPY_ROW_KEY = "custom_happy_row"; - static inline constexpr auto CUSTOM_ASLEEP_ROW_KEY = "custom_asleep_row"; - static inline constexpr auto CUSTOM_SLEEP_ROW_KEY = "custom_sleep_row"; - static inline constexpr auto CUSTOM_WAKE_UP_ROW_KEY = "custom_wake_up_row"; - static inline constexpr auto CUSTOM_START_WORKING_ROW_KEY = "custom_start_working_row"; - static inline constexpr auto CUSTOM_WORKING_ROW_KEY = "custom_working_row"; - static inline constexpr auto CUSTOM_END_WORKING_ROW_KEY = "custom_end_working_row"; - static inline constexpr auto CUSTOM_START_MOVING_ROW_KEY = "custom_start_moving_row"; - static inline constexpr auto CUSTOM_MOVING_ROW_KEY = "custom_moving_row"; - static inline constexpr auto CUSTOM_END_MOVING_ROW_KEY = "custom_end_moving_row"; - static inline constexpr auto CUSTOM_START_RUNNING_ROW_KEY = "custom_start_running_row"; - static inline constexpr auto CUSTOM_RUNNING_ROW_KEY = "custom_running_row"; - static inline constexpr auto CUSTOM_END_RUNNING_ROW_KEY = "custom_end_running_row"; - static inline constexpr auto CUSTOM_ROWS_KEY = "custom_rows"; - - static inline constexpr size_t KEY_BUF = 256; - static inline constexpr size_t VALUE_BUF = PATH_MAX + 256; // max value + comment - static inline constexpr size_t LINE_BUF = KEY_BUF-1 + VALUE_BUF-1 + 1 + 1; // key + '=' + value + '\0' - - // ============================================================================= - // CONFIGURATION VALIDATION MODULE - // ============================================================================= - - static constexpr uint64_t config_clamp_int(int& value, int min, int max, [[maybe_unused]] const char *name) { - if (value < min || value > max) { - BONGOCAT_LOG_WARNING("%s %d out of range [%d-%d], clamping", name, value, min, max); - value = (value < min) ? min : max; - return (1u << 0); - } - return 0; +// ============================================================================= +// CONFIGURATION VALIDATION MODULE +// ============================================================================= + +static constexpr uint64_t config_clamp_int(int& value, int min, int max, [[maybe_unused]] const char *name) { + if (value < min || value > max) { + BONGOCAT_LOG_WARNING("%s %d out of range [%d-%d], clamping", name, value, min, max); + value = (value < min) ? min : max; + return (1u << 0); + } + return 0; +} +static constexpr uint64_t config_clamp_double(double& value, double min, double max, + [[maybe_unused]] const char *name) { + if (value < min || value > max) { + BONGOCAT_LOG_WARNING("%s %.2f out of range [%.0f-%.0f], clamping", name, value, min, max); + value = (value < min) ? min : max; + return (1u << 0); + } + return 0; +} + +static constexpr uint64_t config_validate_max_int(const int& value, int max, [[maybe_unused]] const char *name) { + if (value > max) { + BONGOCAT_LOG_WARNING("%s %d out of range [%d], clamping", name, value, max); + return (1u << 0); + } + return 0; +} + +static uint64_t config_validate_dimensions(config_t& config) { + uint64_t ret{0}; + ret |= config_clamp_int(config.cat_height, MIN_CAT_HEIGHT, MAX_CAT_HEIGHT, CAT_HEIGHT_KEY); + ret |= config_clamp_int(config.overlay_height, MIN_OVERLAY_HEIGHT, MAX_OVERLAY_HEIGHT, OVERLAY_HEIGHT_KEY); + ret |= config_clamp_int(config.cat_x_offset, MIN_OFFSET, MAX_OFFSET, CAT_X_OFFSET_KEY); + ret |= config_clamp_int(config.cat_y_offset, MIN_OFFSET, MAX_OFFSET, CAT_Y_OFFSET_KEY); + ret |= config_clamp_int(config.movement_radius, MIN_MOVEMENT_RADIUS, MAX_MOVEMENT_RADIUS, CAT_Y_OFFSET_KEY); + ret |= config_clamp_int(config.padding_x, 0, MAX_OFFSET, PADDING_X_KEY); + ret |= config_clamp_int(config.padding_y, 0, MAX_OFFSET, PADDING_Y_KEY); + ret |= config_clamp_int(config.screen_width, 0, MAX_OFFSET, SCREEN_WIDTH_KEY); + return ret; +} + +static uint64_t config_validate_timing(config_t& config) { + uint64_t ret{0}; + ret |= config_clamp_int(config.fps, MIN_FPS, MAX_FPS, FPS_KEY); + ret |= config_clamp_int(config.keypress_duration_ms, MIN_DURATION_MS, MAX_DURATION_MS, KEYPRESS_DURATION_KEY); + ret |= config_clamp_int(config.test_animation_duration_ms, 0, MAX_DURATION_MS, TEST_ANIMATION_DURATION_KEY); + ret |= config_clamp_int(config.animation_speed_ms, 0, MAX_DURATION_MS, ANIMATION_SPEED_KEY); + ret |= config_clamp_int(config.idle_sleep_timeout_sec, 0, MAX_SLEEP_TIMEOUT_SEC, IDLE_SLEEP_TIMEOUT_KEY); + ret |= config_clamp_int(config.input_fps, 0, MAX_FPS, INPUT_FPS_KEY); + ret |= config_clamp_int(config.movement_speed, 0, MAX_DURATION_MS, MOVEMENT_SPEED_KEY); + + // Validate interval (0 is allowed to disable) + if (config.test_animation_interval_sec < 0 || config.test_animation_interval_sec > MAX_INTERVAL_SEC) { + BONGOCAT_LOG_WARNING("%s %d out of range [0-%dsec], clamping", TEST_ANIMATION_INTERVAL_KEY, + config.test_animation_interval_sec, MAX_INTERVAL_SEC); + config.test_animation_interval_sec = (config.test_animation_interval_sec < 0) ? 0 : MAX_INTERVAL_SEC; + ret = (1u << 1); + } + if (config.animation_speed_ms < 0 || config.animation_speed_ms > MAX_INTERVAL_SEC * 1000) { + BONGOCAT_LOG_WARNING("%s %d out of range [0-%dms], clamping", ANIMATION_SPEED_KEY, + config.test_animation_interval_sec, MAX_INTERVAL_SEC * 1000); + config.animation_speed_ms = (config.animation_speed_ms < 0) ? 0 : MAX_INTERVAL_SEC * 1000; + ret = (1u << 2); + } + return ret; +} + +static uint64_t config_validate_kpm(config_t& config) { + return config_clamp_int(config.happy_kpm, MIN_KPM, MAX_KPM, HAPPY_KPM_KEY); +} + +static uint64_t config_validate_update(config_t& config) { + uint64_t ret{0}; + + ret |= config_clamp_int(config.update_rate_ms, 0, MAX_UPDATE_RATE_MS, UPDATE_RATE_KEY); + ret |= config_clamp_double(config.cpu_threshold, 0, MAX_CPU_THRESHOLD, CPU_THRESHOLD_KEY); + ret |= config_clamp_double(config.cpu_running_factor, 0, MAX_CPU_RUNNING_FACTOR, CPU_RUNNING_FACTOR_KEY); + + return ret; +} + +static uint64_t config_validate_custom(config_t& config) { + using namespace assets; + uint64_t ret{0}; + + if (config._custom) { + if (config.custom_sprite_sheet_settings.feature_toggle_writing_frames >= 0) { + config.custom_sprite_sheet_settings.feature_toggle_writing_frames = + config.custom_sprite_sheet_settings.feature_toggle_writing_frames >= 1 ? 1 : 0; } - static constexpr uint64_t config_clamp_double(double& value, double min, double max, [[maybe_unused]] const char *name) { - if (value < min || value > max) { - BONGOCAT_LOG_WARNING("%s %.2f out of range [%.0f-%.0f], clamping", name, value, min, max); - value = (value < min) ? min : max; - return (1u << 0); - } - return 0; + if (config.custom_sprite_sheet_settings.feature_toggle_writing_frames_random >= 0) { + config.custom_sprite_sheet_settings.feature_toggle_writing_frames_random = + config.custom_sprite_sheet_settings.feature_toggle_writing_frames_random >= 1 ? 1 : 0; } - - static constexpr uint64_t config_validate_max_int(const int& value, int max, [[maybe_unused]] const char *name) { - if (value > max) { - BONGOCAT_LOG_WARNING("%s %d out of range [%d], clamping", name, value, max); - return (1u << 0); - } - return 0; - } - - static uint64_t config_validate_dimensions(config_t& config) { - uint64_t ret{0}; - ret |= config_clamp_int(config.cat_height, MIN_CAT_HEIGHT, MAX_CAT_HEIGHT, CAT_HEIGHT_KEY); - ret |= config_clamp_int(config.overlay_height, MIN_OVERLAY_HEIGHT, MAX_OVERLAY_HEIGHT, OVERLAY_HEIGHT_KEY); - ret |= config_clamp_int(config.cat_x_offset, MIN_OFFSET, MAX_OFFSET, CAT_X_OFFSET_KEY); - ret |= config_clamp_int(config.cat_y_offset, MIN_OFFSET, MAX_OFFSET, CAT_Y_OFFSET_KEY); - ret |= config_clamp_int(config.movement_radius, MIN_MOVEMENT_RADIUS, MAX_MOVEMENT_RADIUS, CAT_Y_OFFSET_KEY); - ret |= config_clamp_int(config.padding_x, 0, MAX_OFFSET, PADDING_X_KEY); - ret |= config_clamp_int(config.padding_y, 0, MAX_OFFSET, PADDING_Y_KEY); - ret |= config_clamp_int(config.screen_width, 0, MAX_OFFSET, SCREEN_WIDTH_KEY); - return ret; + if (config.custom_sprite_sheet_settings.feature_mirror_x_moving >= 0) { + config.custom_sprite_sheet_settings.feature_mirror_x_moving = + config.custom_sprite_sheet_settings.feature_mirror_x_moving >= 1 ? 1 : 0; } - static uint64_t config_validate_timing(config_t& config) { - uint64_t ret{0}; - ret |= config_clamp_int(config.fps, MIN_FPS, MAX_FPS, FPS_KEY); - ret |= config_clamp_int(config.keypress_duration_ms, MIN_DURATION_MS, MAX_DURATION_MS, KEYPRESS_DURATION_KEY); - ret |= config_clamp_int(config.test_animation_duration_ms, 0, MAX_DURATION_MS, TEST_ANIMATION_DURATION_KEY); - ret |= config_clamp_int(config.animation_speed_ms, 0, MAX_DURATION_MS, ANIMATION_SPEED_KEY); - ret |= config_clamp_int(config.idle_sleep_timeout_sec, 0, MAX_SLEEP_TIMEOUT_SEC, IDLE_SLEEP_TIMEOUT_KEY); - ret |= config_clamp_int(config.input_fps, 0, MAX_FPS, INPUT_FPS_KEY); - ret |= config_clamp_int(config.movement_speed, 0, MAX_DURATION_MS, MOVEMENT_SPEED_KEY); - - // Validate interval (0 is allowed to disable) - if (config.test_animation_interval_sec < 0 || config.test_animation_interval_sec > MAX_INTERVAL_SEC) { - BONGOCAT_LOG_WARNING("%s %d out of range [0-%dsec], clamping", - TEST_ANIMATION_INTERVAL_KEY, config.test_animation_interval_sec, MAX_INTERVAL_SEC); - config.test_animation_interval_sec = (config.test_animation_interval_sec < 0) ? 0 : MAX_INTERVAL_SEC; - ret = (1u << 1); - } - if (config.animation_speed_ms < 0 || config.animation_speed_ms > MAX_INTERVAL_SEC*1000) { - BONGOCAT_LOG_WARNING("%s %d out of range [0-%dms], clamping", - ANIMATION_SPEED_KEY, config.test_animation_interval_sec, MAX_INTERVAL_SEC*1000); - config.animation_speed_ms = (config.animation_speed_ms < 0) ? 0 : MAX_INTERVAL_SEC*1000; - ret = (1u << 2); - } - return ret; + // clamp cols + ret |= config_clamp_int(config.custom_sprite_sheet_settings.idle_frames, MIN_CUSTOM_FRAMES, MAX_CUSTOM_FRAMES, + CUSTOM_IDLE_FRAMES_KEY); + + ret |= config_clamp_int(config.custom_sprite_sheet_settings.boring_frames, MIN_CUSTOM_FRAMES, MAX_CUSTOM_FRAMES, + CUSTOM_BORING_FRAMES_KEY); + + ret |= config_clamp_int(config.custom_sprite_sheet_settings.start_writing_frames, MIN_CUSTOM_FRAMES, + MAX_CUSTOM_FRAMES, CUSTOM_START_WRITING_FRAMES_KEY); + ret |= config_clamp_int(config.custom_sprite_sheet_settings.writing_frames, MIN_CUSTOM_FRAMES, MAX_CUSTOM_FRAMES, + CUSTOM_WRITING_FRAMES_KEY); + ret |= config_clamp_int(config.custom_sprite_sheet_settings.end_writing_frames, MIN_CUSTOM_FRAMES, + MAX_CUSTOM_FRAMES, CUSTOM_END_WRITING_FRAMES_KEY); + ret |= config_clamp_int(config.custom_sprite_sheet_settings.happy_frames, MIN_CUSTOM_FRAMES, MAX_CUSTOM_FRAMES, + CUSTOM_HAPPY_FRAMES_KEY); + + ret |= config_clamp_int(config.custom_sprite_sheet_settings.asleep_frames, MIN_CUSTOM_FRAMES, MAX_CUSTOM_FRAMES, + CUSTOM_ASLEEP_FRAMES_KEY); + ret |= config_clamp_int(config.custom_sprite_sheet_settings.sleep_frames, MIN_CUSTOM_FRAMES, MAX_CUSTOM_FRAMES, + CUSTOM_SLEEP_FRAMES_KEY); + ret |= config_clamp_int(config.custom_sprite_sheet_settings.wake_up_frames, MIN_CUSTOM_FRAMES, MAX_CUSTOM_FRAMES, + CUSTOM_WAKE_UP_FRAMES_KEY); + + ret |= config_clamp_int(config.custom_sprite_sheet_settings.start_working_frames, MIN_CUSTOM_FRAMES, + MAX_CUSTOM_FRAMES, CUSTOM_START_WORKING_FRAMES_KEY); + ret |= config_clamp_int(config.custom_sprite_sheet_settings.working_frames, MIN_CUSTOM_FRAMES, MAX_CUSTOM_FRAMES, + CUSTOM_WORKING_FRAMES_KEY); + ret |= config_clamp_int(config.custom_sprite_sheet_settings.end_working_frames, MIN_CUSTOM_FRAMES, + MAX_CUSTOM_FRAMES, CUSTOM_END_WORKING_FRAMES_KEY); + + ret |= config_clamp_int(config.custom_sprite_sheet_settings.start_moving_frames, MIN_CUSTOM_FRAMES, + MAX_CUSTOM_FRAMES, CUSTOM_START_MOVING_FRAMES_KEY); + ret |= config_clamp_int(config.custom_sprite_sheet_settings.moving_frames, MIN_CUSTOM_FRAMES, MAX_CUSTOM_FRAMES, + CUSTOM_MOVING_FRAMES_KEY); + ret |= config_clamp_int(config.custom_sprite_sheet_settings.end_moving_frames, MIN_CUSTOM_FRAMES, MAX_CUSTOM_FRAMES, + CUSTOM_END_MOVING_FRAMES_KEY); + + ret |= config_clamp_int(config.custom_sprite_sheet_settings.start_running_frames, MIN_CUSTOM_FRAMES, + MAX_CUSTOM_FRAMES, CUSTOM_START_RUNNING_FRAMES_KEY); + ret |= config_clamp_int(config.custom_sprite_sheet_settings.running_frames, MIN_CUSTOM_FRAMES, MAX_CUSTOM_FRAMES, + CUSTOM_RUNNING_FRAMES_KEY); + ret |= config_clamp_int(config.custom_sprite_sheet_settings.end_running_frames, MIN_CUSTOM_FRAMES, + MAX_CUSTOM_FRAMES, CUSTOM_END_RUNNING_FRAMES_KEY); + + // clamp rows + if (config.custom_sprite_sheet_settings.idle_row_index >= 0) { + ret |= config_clamp_int(config.custom_sprite_sheet_settings.idle_row_index, MIN_CUSTOM_ROWS, MAX_CUSTOM_ROWS, + CUSTOM_IDLE_ROW_KEY); } - - static uint64_t config_validate_kpm(config_t& config) { - return config_clamp_int(config.happy_kpm, MIN_KPM, MAX_KPM, HAPPY_KPM_KEY); + if (config.custom_sprite_sheet_settings.boring_row_index >= 0) { + ret |= config_clamp_int(config.custom_sprite_sheet_settings.boring_row_index, MIN_CUSTOM_ROWS, MAX_CUSTOM_ROWS, + CUSTOM_BORING_ROW_KEY); } - - static uint64_t config_validate_update(config_t& config) { - uint64_t ret{0}; - - ret |= config_clamp_int(config.update_rate_ms, 0, MAX_UPDATE_RATE_MS, UPDATE_RATE_KEY); - ret |= config_clamp_double(config.cpu_threshold, 0, MAX_CPU_THRESHOLD, CPU_THRESHOLD_KEY); - ret |= config_clamp_double(config.cpu_running_factor, 0, MAX_CPU_RUNNING_FACTOR, CPU_RUNNING_FACTOR_KEY); - - return ret; + if (config.custom_sprite_sheet_settings.start_writing_row_index >= 0) { + ret |= config_clamp_int(config.custom_sprite_sheet_settings.start_writing_row_index, MIN_CUSTOM_ROWS, + MAX_CUSTOM_ROWS, CUSTOM_START_WRITING_ROW_KEY); } - - static uint64_t config_validate_custom(config_t& config) { - using namespace assets; - uint64_t ret{0}; - - if (config._custom) { - if (config.custom_sprite_sheet_settings.feature_toggle_writing_frames >= 0) { - config.custom_sprite_sheet_settings.feature_toggle_writing_frames = config.custom_sprite_sheet_settings.feature_toggle_writing_frames ? 1 : 0; - } - if (config.custom_sprite_sheet_settings.feature_toggle_writing_frames_random >= 0) { - config.custom_sprite_sheet_settings.feature_toggle_writing_frames_random = config.custom_sprite_sheet_settings.feature_toggle_writing_frames_random ? 1 : 0; - } - if (config.custom_sprite_sheet_settings.feature_mirror_x_moving >= 0) { - config.custom_sprite_sheet_settings.feature_mirror_x_moving = config.custom_sprite_sheet_settings.feature_mirror_x_moving ? 1 : 0; - } - - // clamp cols - ret |= config_clamp_int(config.custom_sprite_sheet_settings.idle_frames, MIN_CUSTOM_FRAMES, MAX_CUSTOM_FRAMES, CUSTOM_IDLE_FRAMES_KEY); - - ret |= config_clamp_int(config.custom_sprite_sheet_settings.boring_frames, MIN_CUSTOM_FRAMES, MAX_CUSTOM_FRAMES, CUSTOM_BORING_FRAMES_KEY); - - ret |= config_clamp_int(config.custom_sprite_sheet_settings.start_writing_frames, MIN_CUSTOM_FRAMES, MAX_CUSTOM_FRAMES, CUSTOM_START_WRITING_FRAMES_KEY); - ret |= config_clamp_int(config.custom_sprite_sheet_settings.writing_frames, MIN_CUSTOM_FRAMES, MAX_CUSTOM_FRAMES, CUSTOM_WRITING_FRAMES_KEY); - ret |= config_clamp_int(config.custom_sprite_sheet_settings.end_writing_frames, MIN_CUSTOM_FRAMES, MAX_CUSTOM_FRAMES, CUSTOM_END_WRITING_FRAMES_KEY); - ret |= config_clamp_int(config.custom_sprite_sheet_settings.happy_frames, MIN_CUSTOM_FRAMES, MAX_CUSTOM_FRAMES, CUSTOM_HAPPY_FRAMES_KEY); - - ret |= config_clamp_int(config.custom_sprite_sheet_settings.asleep_frames, MIN_CUSTOM_FRAMES, MAX_CUSTOM_FRAMES, CUSTOM_ASLEEP_FRAMES_KEY); - ret |= config_clamp_int(config.custom_sprite_sheet_settings.sleep_frames, MIN_CUSTOM_FRAMES, MAX_CUSTOM_FRAMES, CUSTOM_SLEEP_FRAMES_KEY); - ret |= config_clamp_int(config.custom_sprite_sheet_settings.wake_up_frames, MIN_CUSTOM_FRAMES, MAX_CUSTOM_FRAMES, CUSTOM_WAKE_UP_FRAMES_KEY); - - ret |= config_clamp_int(config.custom_sprite_sheet_settings.start_working_frames, MIN_CUSTOM_FRAMES, MAX_CUSTOM_FRAMES, CUSTOM_START_WORKING_FRAMES_KEY); - ret |= config_clamp_int(config.custom_sprite_sheet_settings.working_frames, MIN_CUSTOM_FRAMES, MAX_CUSTOM_FRAMES, CUSTOM_WORKING_FRAMES_KEY); - ret |= config_clamp_int(config.custom_sprite_sheet_settings.end_working_frames, MIN_CUSTOM_FRAMES, MAX_CUSTOM_FRAMES, CUSTOM_END_WORKING_FRAMES_KEY); - - ret |= config_clamp_int(config.custom_sprite_sheet_settings.start_moving_frames, MIN_CUSTOM_FRAMES, MAX_CUSTOM_FRAMES, CUSTOM_START_MOVING_FRAMES_KEY); - ret |= config_clamp_int(config.custom_sprite_sheet_settings.moving_frames, MIN_CUSTOM_FRAMES, MAX_CUSTOM_FRAMES, CUSTOM_MOVING_FRAMES_KEY); - ret |= config_clamp_int(config.custom_sprite_sheet_settings.end_moving_frames, MIN_CUSTOM_FRAMES, MAX_CUSTOM_FRAMES, CUSTOM_END_MOVING_FRAMES_KEY); - - ret |= config_clamp_int(config.custom_sprite_sheet_settings.start_running_frames, MIN_CUSTOM_FRAMES, MAX_CUSTOM_FRAMES, CUSTOM_START_RUNNING_FRAMES_KEY); - ret |= config_clamp_int(config.custom_sprite_sheet_settings.running_frames, MIN_CUSTOM_FRAMES, MAX_CUSTOM_FRAMES, CUSTOM_RUNNING_FRAMES_KEY); - ret |= config_clamp_int(config.custom_sprite_sheet_settings.end_running_frames, MIN_CUSTOM_FRAMES, MAX_CUSTOM_FRAMES, CUSTOM_END_RUNNING_FRAMES_KEY); - - // clamp rows - if (config.custom_sprite_sheet_settings.idle_row_index >= 0) { - ret |= config_clamp_int(config.custom_sprite_sheet_settings.idle_row_index, MIN_CUSTOM_ROWS, MAX_CUSTOM_ROWS, CUSTOM_IDLE_ROW_KEY); - } - if (config.custom_sprite_sheet_settings.boring_row_index >= 0) { - ret |= config_clamp_int(config.custom_sprite_sheet_settings.boring_row_index, MIN_CUSTOM_ROWS, MAX_CUSTOM_ROWS, CUSTOM_BORING_ROW_KEY); - } - if (config.custom_sprite_sheet_settings.start_writing_row_index >= 0) { - ret |= config_clamp_int(config.custom_sprite_sheet_settings.start_writing_row_index, MIN_CUSTOM_ROWS, MAX_CUSTOM_ROWS, CUSTOM_START_WRITING_ROW_KEY); - } - if (config.custom_sprite_sheet_settings.writing_row_index >= 0) { - ret |= config_clamp_int(config.custom_sprite_sheet_settings.writing_row_index, MIN_CUSTOM_ROWS, MAX_CUSTOM_ROWS, CUSTOM_WRITING_ROW_KEY); - } - if (config.custom_sprite_sheet_settings.end_writing_row_index >= 0) { - ret |= config_clamp_int(config.custom_sprite_sheet_settings.end_writing_row_index, MIN_CUSTOM_ROWS, MAX_CUSTOM_ROWS, CUSTOM_END_WRITING_ROW_KEY); - } - if (config.custom_sprite_sheet_settings.happy_row_index >= 0) { - ret |= config_clamp_int(config.custom_sprite_sheet_settings.happy_row_index, MIN_CUSTOM_ROWS, MAX_CUSTOM_ROWS, CUSTOM_HAPPY_ROW_KEY); - } - if (config.custom_sprite_sheet_settings.asleep_row_index >= 0) { - ret |= config_clamp_int(config.custom_sprite_sheet_settings.asleep_row_index, MIN_CUSTOM_ROWS, MAX_CUSTOM_ROWS, CUSTOM_ASLEEP_ROW_KEY); - } - if (config.custom_sprite_sheet_settings.sleep_row_index >= 0) { - ret |= config_clamp_int(config.custom_sprite_sheet_settings.sleep_row_index, MIN_CUSTOM_ROWS, MAX_CUSTOM_ROWS, CUSTOM_SLEEP_ROW_KEY); - } - if (config.custom_sprite_sheet_settings.wake_up_row_index >= 0) { - ret |= config_clamp_int(config.custom_sprite_sheet_settings.wake_up_row_index, MIN_CUSTOM_ROWS, MAX_CUSTOM_ROWS, CUSTOM_WAKE_UP_ROW_KEY); - } - if (config.custom_sprite_sheet_settings.start_working_row_index >= 0) { - ret |= config_clamp_int(config.custom_sprite_sheet_settings.start_working_row_index, MIN_CUSTOM_ROWS, MAX_CUSTOM_ROWS, CUSTOM_START_WORKING_ROW_KEY); - } - if (config.custom_sprite_sheet_settings.working_row_index >= 0) { - ret |= config_clamp_int(config.custom_sprite_sheet_settings.working_row_index, MIN_CUSTOM_ROWS, MAX_CUSTOM_ROWS, CUSTOM_WORKING_ROW_KEY); - } - if (config.custom_sprite_sheet_settings.end_working_row_index >= 0) { - ret |= config_clamp_int(config.custom_sprite_sheet_settings.end_working_row_index, MIN_CUSTOM_ROWS, MAX_CUSTOM_ROWS, CUSTOM_END_WORKING_ROW_KEY); - } - if (config.custom_sprite_sheet_settings.start_moving_row_index >= 0) { - ret |= config_clamp_int(config.custom_sprite_sheet_settings.start_moving_row_index, MIN_CUSTOM_ROWS, MAX_CUSTOM_ROWS, CUSTOM_START_MOVING_ROW_KEY); - } - if (config.custom_sprite_sheet_settings.moving_row_index >= 0) { - ret |= config_clamp_int(config.custom_sprite_sheet_settings.moving_row_index, MIN_CUSTOM_ROWS, MAX_CUSTOM_ROWS, CUSTOM_MOVING_ROW_KEY); - } - if (config.custom_sprite_sheet_settings.end_moving_row_index >= 0) { - ret |= config_clamp_int(config.custom_sprite_sheet_settings.end_moving_row_index, MIN_CUSTOM_ROWS, MAX_CUSTOM_ROWS, CUSTOM_END_MOVING_ROW_KEY); - } - if (config.custom_sprite_sheet_settings.start_running_row_index >= 0) { - ret |= config_clamp_int(config.custom_sprite_sheet_settings.start_running_row_index, MIN_CUSTOM_ROWS, MAX_CUSTOM_ROWS, CUSTOM_START_RUNNING_ROW_KEY); - } - if (config.custom_sprite_sheet_settings.running_row_index >= 0) { - ret |= config_clamp_int(config.custom_sprite_sheet_settings.running_row_index, MIN_CUSTOM_ROWS, MAX_CUSTOM_ROWS, CUSTOM_RUNNING_ROW_KEY); - } - if (config.custom_sprite_sheet_settings.end_running_row_index >= 0) { - ret |= config_clamp_int(config.custom_sprite_sheet_settings.end_running_row_index, MIN_CUSTOM_ROWS, MAX_CUSTOM_ROWS, CUSTOM_END_RUNNING_ROW_KEY); - } - if (config.custom_sprite_sheet_settings.rows >= 0) { - ret |= config_clamp_int(config.custom_sprite_sheet_settings.rows, 1, MAX_CUSTOM_ROWS, CUSTOM_ROWS_KEY); - } - - - const int sprite_sheet_cols = get_custom_animation_settings_max_cols(config.custom_sprite_sheet_settings); - const int sprite_sheet_rows = get_custom_animation_settings_rows_count(config.custom_sprite_sheet_settings); - - if (sprite_sheet_cols <= 0) { - BONGOCAT_LOG_WARNING("custom sprite sheet has no columns"); - ret |= (1u << 3); - } - - // validate cols - ret |= config_validate_max_int(config.custom_sprite_sheet_settings.idle_frames, sprite_sheet_cols, CUSTOM_IDLE_FRAMES_KEY); - - ret |= config_validate_max_int(config.custom_sprite_sheet_settings.boring_frames, sprite_sheet_cols, CUSTOM_BORING_FRAMES_KEY); - - ret |= config_validate_max_int(config.custom_sprite_sheet_settings.start_writing_frames, sprite_sheet_cols, CUSTOM_START_WRITING_FRAMES_KEY); - ret |= config_validate_max_int(config.custom_sprite_sheet_settings.writing_frames, sprite_sheet_cols, CUSTOM_WRITING_FRAMES_KEY); - ret |= config_validate_max_int(config.custom_sprite_sheet_settings.end_writing_frames, sprite_sheet_cols, CUSTOM_END_WRITING_FRAMES_KEY); - ret |= config_validate_max_int(config.custom_sprite_sheet_settings.happy_frames, sprite_sheet_cols, CUSTOM_HAPPY_FRAMES_KEY); - - ret |= config_validate_max_int(config.custom_sprite_sheet_settings.asleep_frames, sprite_sheet_cols, CUSTOM_ASLEEP_FRAMES_KEY); - ret |= config_validate_max_int(config.custom_sprite_sheet_settings.sleep_frames, sprite_sheet_cols, CUSTOM_SLEEP_FRAMES_KEY); - ret |= config_validate_max_int(config.custom_sprite_sheet_settings.wake_up_frames, sprite_sheet_cols, CUSTOM_WAKE_UP_FRAMES_KEY); - - ret |= config_validate_max_int(config.custom_sprite_sheet_settings.start_working_frames, sprite_sheet_cols, CUSTOM_START_WORKING_FRAMES_KEY); - ret |= config_validate_max_int(config.custom_sprite_sheet_settings.working_frames, sprite_sheet_cols, CUSTOM_WORKING_FRAMES_KEY); - ret |= config_validate_max_int(config.custom_sprite_sheet_settings.end_working_frames, sprite_sheet_cols, CUSTOM_END_WORKING_FRAMES_KEY); - - ret |= config_validate_max_int(config.custom_sprite_sheet_settings.start_moving_frames, sprite_sheet_cols, CUSTOM_START_MOVING_FRAMES_KEY); - ret |= config_validate_max_int(config.custom_sprite_sheet_settings.moving_frames, sprite_sheet_cols, CUSTOM_MOVING_FRAMES_KEY); - ret |= config_validate_max_int(config.custom_sprite_sheet_settings.end_moving_frames, sprite_sheet_cols, CUSTOM_END_MOVING_FRAMES_KEY); - - ret |= config_validate_max_int(config.custom_sprite_sheet_settings.start_running_frames, sprite_sheet_cols, CUSTOM_START_RUNNING_FRAMES_KEY); - ret |= config_validate_max_int(config.custom_sprite_sheet_settings.running_frames, sprite_sheet_cols, CUSTOM_RUNNING_FRAMES_KEY); - ret |= config_validate_max_int(config.custom_sprite_sheet_settings.end_running_frames, sprite_sheet_cols, CUSTOM_END_RUNNING_FRAMES_KEY); - - // validate rows - if (sprite_sheet_rows > 0) { - if (config.custom_sprite_sheet_settings.idle_row_index >= 0) { - ret |= config_validate_max_int(config.custom_sprite_sheet_settings.idle_row_index, sprite_sheet_rows-1, CUSTOM_IDLE_ROW_KEY); - } - if (config.custom_sprite_sheet_settings.boring_row_index >= 0) { - ret |= config_validate_max_int(config.custom_sprite_sheet_settings.boring_row_index, sprite_sheet_rows-1, CUSTOM_BORING_ROW_KEY); - } - if (config.custom_sprite_sheet_settings.start_writing_row_index >= 0) { - ret |= config_validate_max_int(config.custom_sprite_sheet_settings.start_writing_row_index, sprite_sheet_rows-1, CUSTOM_START_WRITING_ROW_KEY); - } - if (config.custom_sprite_sheet_settings.writing_row_index >= 0) { - ret |= config_validate_max_int(config.custom_sprite_sheet_settings.writing_row_index, sprite_sheet_rows-1, CUSTOM_WRITING_ROW_KEY); - } - if (config.custom_sprite_sheet_settings.end_writing_row_index >= 0) { - ret |= config_validate_max_int(config.custom_sprite_sheet_settings.end_writing_row_index, sprite_sheet_rows-1, CUSTOM_END_WRITING_ROW_KEY); - } - if (config.custom_sprite_sheet_settings.happy_row_index >= 0) { - ret |= config_validate_max_int(config.custom_sprite_sheet_settings.happy_row_index, sprite_sheet_rows-1, CUSTOM_HAPPY_ROW_KEY); - } - if (config.custom_sprite_sheet_settings.asleep_row_index >= 0) { - ret |= config_validate_max_int(config.custom_sprite_sheet_settings.asleep_row_index, sprite_sheet_rows-1, CUSTOM_ASLEEP_ROW_KEY); - } - if (config.custom_sprite_sheet_settings.sleep_row_index >= 0) { - ret |= config_validate_max_int(config.custom_sprite_sheet_settings.sleep_row_index, sprite_sheet_rows-1, CUSTOM_SLEEP_ROW_KEY); - } - if (config.custom_sprite_sheet_settings.wake_up_row_index >= 0) { - ret |= config_validate_max_int(config.custom_sprite_sheet_settings.wake_up_row_index, sprite_sheet_rows-1, CUSTOM_WAKE_UP_ROW_KEY); - } - if (config.custom_sprite_sheet_settings.start_working_row_index >= 0) { - ret |= config_validate_max_int(config.custom_sprite_sheet_settings.start_working_row_index, sprite_sheet_rows-1, CUSTOM_START_WORKING_ROW_KEY); - } - if (config.custom_sprite_sheet_settings.working_row_index >= 0) { - ret |= config_validate_max_int(config.custom_sprite_sheet_settings.working_row_index, sprite_sheet_rows-1, CUSTOM_WORKING_ROW_KEY); - } - if (config.custom_sprite_sheet_settings.end_working_row_index >= 0) { - ret |= config_validate_max_int(config.custom_sprite_sheet_settings.end_working_row_index, sprite_sheet_rows-1, CUSTOM_END_WORKING_ROW_KEY); - } - if (config.custom_sprite_sheet_settings.start_moving_row_index >= 0) { - ret |= config_validate_max_int(config.custom_sprite_sheet_settings.start_moving_row_index, sprite_sheet_rows-1, CUSTOM_START_MOVING_ROW_KEY); - } - if (config.custom_sprite_sheet_settings.moving_row_index >= 0) { - ret |= config_validate_max_int(config.custom_sprite_sheet_settings.moving_row_index, sprite_sheet_rows-1, CUSTOM_MOVING_ROW_KEY); - } - if (config.custom_sprite_sheet_settings.end_moving_row_index >= 0) { - ret |= config_validate_max_int(config.custom_sprite_sheet_settings.end_moving_row_index, sprite_sheet_rows-1, CUSTOM_END_MOVING_ROW_KEY); - } - if (config.custom_sprite_sheet_settings.start_running_row_index >= 0) { - ret |= config_validate_max_int(config.custom_sprite_sheet_settings.start_running_row_index, sprite_sheet_rows-1, CUSTOM_START_RUNNING_ROW_KEY); - } - if (config.custom_sprite_sheet_settings.running_row_index >= 0) { - ret |= config_validate_max_int(config.custom_sprite_sheet_settings.running_row_index, sprite_sheet_rows-1, CUSTOM_RUNNING_ROW_KEY); - } - if (config.custom_sprite_sheet_settings.end_running_row_index >= 0) { - ret |= config_validate_max_int(config.custom_sprite_sheet_settings.end_running_row_index, sprite_sheet_rows-1, CUSTOM_END_RUNNING_ROW_KEY); - } - } else { - if (config.custom_sprite_sheet_settings.rows <= 0) { - BONGOCAT_LOG_WARNING("custom sprite sheet has no rows"); - ret |= (1u << 4); - } - } - - - // validate sprite sheet file - if (config.custom_sprite_sheet_filename != nullptr && strlen(config.custom_sprite_sheet_filename) != 0) { - constexpr size_t PNG_SIGNATURE_SIZE = 8; - constexpr unsigned char PNG_SIGNATURE[PNG_SIGNATURE_SIZE] = { - 0x89, 'P', 'N', 'G', '\r', '\n', 0x1A, '\n' - }; - - // Try to open the file - platform::FileDescriptor fd (open(config.custom_sprite_sheet_filename, O_RDONLY)); - if (fd._fd < 0) { - BONGOCAT_LOG_ERROR("Custom Sprite Sheet doesn't exist or can't be opened: %s", config.custom_sprite_sheet_filename); - ret |= (1u << 6); - return ret; - } - - struct stat st; - if (fstat(fd._fd, &st) < 0) { - BONGOCAT_LOG_ERROR("Custom Sprite Sheet can't be opened: %s", config.custom_sprite_sheet_filename); - ret |= (1u << 7); - return ret; - } - if (st.st_size == 0) { - BONGOCAT_LOG_ERROR("Custom Sprite Sheet is an empty file: %s", config.custom_sprite_sheet_filename); - ret |= (1u << 8); - return ret; - } - - unsigned char header[PNG_SIGNATURE_SIZE] = {0}; - const ssize_t n = read(fd._fd, header, PNG_SIGNATURE_SIZE); - if (n < static_cast(PNG_SIGNATURE_SIZE)) { - BONGOCAT_LOG_ERROR("Failed to read PNG header: %s", config.custom_sprite_sheet_filename); - ret |= (1u << 9); - return ret; - } - if (memcmp(header, PNG_SIGNATURE, PNG_SIGNATURE_SIZE) != 0) { - BONGOCAT_LOG_ERROR("Invalid PNG signature: %s", config.custom_sprite_sheet_filename); - ret |= (1u << 10); - return ret; - } - } else { - // empty custom_sprite_sheet_filename - BONGOCAT_LOG_WARNING("custom_sprite_sheet_filename is empty"); - ret |= (1u << 5); - } - - // validate frames - if (config.custom_sprite_sheet_settings.idle_frames <= 0) { - BONGOCAT_LOG_WARNING("custom sprite sheet needs at least an idle animation"); - ret |= (1u << 11); - } - - if (config.custom_sprite_sheet_settings.wake_up_frames > 0 && - (config.custom_sprite_sheet_settings.asleep_frames <= 0 || config.custom_sprite_sheet_settings.sleep_frames <= 0)) { - BONGOCAT_LOG_WARNING("custom sprite sheet has a wake up animation, but no sleep animation"); - //ret |= (1u << 12); - // to hard of an error in strict mode, just print warning - } - - if (config.custom_sprite_sheet_settings.feature_mirror_x_moving >= 0 && - (config.custom_sprite_sheet_settings.start_moving_frames <= 0 && config.custom_sprite_sheet_settings.moving_frames <= 0 && config.custom_sprite_sheet_settings.end_moving_frames <= 0)) { - BONGOCAT_LOG_WARNING("feature_mirror_x_moving for custom sprite sheet is used, but has no moving animation"); - //ret |= (1u << 13); - } - - if ((config.custom_sprite_sheet_settings.feature_toggle_writing_frames >= 0 || config.custom_sprite_sheet_settings.feature_toggle_writing_frames_random >= 0) && - (config.custom_sprite_sheet_settings.start_writing_frames <= 0 && config.custom_sprite_sheet_settings.writing_frames <= 0 && config.custom_sprite_sheet_settings.end_writing_frames <= 0)) { - BONGOCAT_LOG_WARNING("feature_toggle_writing_frames for custom sprite sheet is used, but has no writing animation"); - //ret |= (1u << 14); - } - - if (config.enable_scheduled_sleep >= 0 && - (config.custom_sprite_sheet_settings.asleep_frames <= 0 && config.custom_sprite_sheet_settings.sleep_frames <= 0)) { - BONGOCAT_LOG_WARNING("enable_scheduled_sleep is enabled, but custom sprite sheet has no sleep animation"); - //ret |= (1u << 15); - } - if (config.happy_kpm >= 0 && - (config.custom_sprite_sheet_settings.happy_frames <= 0)) { - BONGOCAT_LOG_WARNING("happy_kpm is used, but custom sprite sheet has no happy animation"); - //ret |= (1u << 16); - } - } - - return ret; + if (config.custom_sprite_sheet_settings.writing_row_index >= 0) { + ret |= config_clamp_int(config.custom_sprite_sheet_settings.writing_row_index, MIN_CUSTOM_ROWS, MAX_CUSTOM_ROWS, + CUSTOM_WRITING_ROW_KEY); } - - static uint64_t config_validate_appearance(config_t& config) { - using namespace assets; - using namespace animation; - uint64_t ret{0}; - // Validate opacity - ret |= config_clamp_int(config.overlay_opacity, 0, 255, OVERLAY_OPACITY_KEY); - - switch (config.animation_sprite_sheet_layout) { - case config_animation_sprite_sheet_layout_t::None: - BONGOCAT_LOG_WARNING("Cant determine sprite sheet layout"); - /// @TODO: move validation error codes (1 << ..) into constants - ret |= (1u << 17); - break; - case config_animation_sprite_sheet_layout_t::Bongocat: - if constexpr (features::EnableBongocatEmbeddedAssets) { - // Validate animation index - assert(BONGOCAT_ANIMATIONS_COUNT <= INT_MAX); - if (config.animation_index < 0 || config.animation_index >= static_cast(BONGOCAT_ANIMATIONS_COUNT)) { - BONGOCAT_LOG_WARNING("%s %d out of range [0-%d], resetting to 0", - ANIMATION_INDEX_KEY, config.animation_index, BONGOCAT_ANIMATIONS_COUNT - 1); - config.animation_index = 0; - ret |= (1u << 18); - } - // Validate idle frame - assert(animation::BONGOCAT_NUM_FRAMES <= INT_MAX); - if (config.idle_frame < 0 || config.idle_frame >= static_cast(BONGOCAT_NUM_FRAMES)) { - BONGOCAT_LOG_WARNING("%s %d out of range [0-%d], resetting to 0", - IDLE_FRAME_KEY, config.idle_frame, BONGOCAT_NUM_FRAMES - 1); - config.idle_frame = 0; - ret |= (1u << 19); - } - } - break; - case config_animation_sprite_sheet_layout_t::Dm: - if constexpr (features::EnableDmEmbeddedAssets) { - // Validate animation index - assert(DM_ANIMATIONS_COUNT <= INT_MAX); - if (config.animation_index < 0 || config.animation_index >= static_cast(DM_ANIMATIONS_COUNT)) { - BONGOCAT_LOG_WARNING("%s %d out of range [0-%d], resetting to 0", - ANIMATION_INDEX_KEY, config.animation_index, assets::DM_ANIMATIONS_COUNT - 1); - config.animation_index = 0; - ret |= (1u << 20); - } - // Validate idle frame - assert(animation::MAX_DIGIMON_FRAMES <= INT_MAX); - if (config.idle_frame < 0 || config.idle_frame >= static_cast(animation::MAX_DIGIMON_FRAMES)) { - BONGOCAT_LOG_WARNING("%s %d out of range [0-%d], resetting to 0", - IDLE_FRAME_KEY, config.idle_frame, animation::MAX_DIGIMON_FRAMES - 1); - config.idle_frame = 0; - ret |= (1u << 21); - } - } - break; - case config_animation_sprite_sheet_layout_t::Pkmn: - if constexpr (features::EnablePkmnEmbeddedAssets) { - // Validate animation index - assert(DM_ANIMATIONS_COUNT <= INT_MAX); - if (config.animation_index < 0 || config.animation_index >= static_cast(PKMN_ANIMATIONS_COUNT)) { - BONGOCAT_LOG_WARNING("%s %d out of range [0-%d], resetting to 0", - ANIMATION_INDEX_KEY, config.animation_index, assets::PKMN_ANIMATIONS_COUNT - 1); - config.animation_index = 0; - ret |= (1u << 22); - } - // Validate idle frame - assert(animation::MAX_DIGIMON_FRAMES <= INT_MAX); - if (config.idle_frame < 0 || config.idle_frame >= static_cast(MAX_PKMN_FRAMES)) { - BONGOCAT_LOG_WARNING("%s %d out of range [0-%d], resetting to 0", - IDLE_FRAME_KEY, config.idle_frame, MAX_PKMN_FRAMES - 1); - config.idle_frame = 0; - ret |= (1u << 23); - } - } - break; - case config_animation_sprite_sheet_layout_t::MsAgent: - if constexpr (features::EnableMsAgentEmbeddedAssets) { - // Validate animation index - assert(assets::MS_AGENTS_ANIMATIONS_COUNT <= INT_MAX); - if (config.animation_index < 0 || config.animation_index >= static_cast(MS_AGENTS_ANIMATIONS_COUNT)) { - BONGOCAT_LOG_WARNING("%s %d out of range [0-%d], resetting to 0", - ANIMATION_INDEX_KEY, config.animation_index, MS_AGENTS_ANIMATIONS_COUNT - 1); - config.animation_index = 0; - ret |= (1u << 24); - } - // Validate idle frame - assert(assets::MS_AGENT_MAX_SPRITE_SHEET_COL_FRAMES <= INT_MAX); - if (config.idle_frame < 0 || config.idle_frame >= static_cast(MS_AGENT_MAX_SPRITE_SHEET_COL_FRAMES)) { - BONGOCAT_LOG_WARNING("%s %d out of range [0-%d], resetting to 0", - IDLE_FRAME_KEY, config.idle_frame, MS_AGENT_MAX_SPRITE_SHEET_COL_FRAMES - 1); - config.idle_frame = 0; - ret |= (1u << 25); - } - } - break; - case config_animation_sprite_sheet_layout_t::Custom: - assert(CUSTOM_ANIM_INDEX <= INT_MAX); - assert(MAX_MISC_ANIM_INDEX <= INT_MAX); - if constexpr (features::EnableCustomSpriteSheetsAssets) { - if (config._custom) { - assert(config.animation_custom_set == config_animation_custom_set_t::custom); - // Validate animation index - if (config.animation_index < 0 || config.animation_index > static_cast(CUSTOM_ANIM_INDEX)) { - BONGOCAT_LOG_WARNING("%s %d out of range [%d], resetting to 0", - ANIMATION_INDEX_KEY, config.animation_index, CUSTOM_ANIM_INDEX); - config.animation_index = 0; - ret |= (1u << 26); - } - /// @TODO: validate max (idle) frames - } - } - if (!config._custom) { - switch (config.animation_custom_set) { - case config_animation_custom_set_t::None: - break; - case config_animation_custom_set_t::misc: - if constexpr (features::EnableMiscEmbeddedAssets) { - // Validate animation index - if (config.animation_index < 0 || config.animation_index > static_cast(MAX_MISC_ANIM_INDEX)) { - BONGOCAT_LOG_WARNING("%s %d out of range [0-%d], resetting to 0", - ANIMATION_INDEX_KEY, config.animation_index, MAX_MISC_ANIM_INDEX); - config.animation_index = 0; - ret |= (1u << 27); - } - // Validate idle frame - assert(assets::MISC_MAX_SPRITE_SHEET_COL_FRAMES <= INT_MAX); - if (config.idle_frame < 0 || config.idle_frame >= static_cast(MISC_MAX_SPRITE_SHEET_COL_FRAMES)) { - BONGOCAT_LOG_WARNING("%s %d out of range [0-%d], resetting to 0", - IDLE_FRAME_KEY, config.idle_frame, assets::MISC_MAX_SPRITE_SHEET_COL_FRAMES - 1); - config.idle_frame = 0; - ret |= (1u << 28); - } - } - break; - case config_animation_custom_set_t::pmd: - if constexpr (features::EnablePmdEmbeddedAssets) { - assert(assets::PMD_ANIM_COUNT <= INT_MAX); - // Validate animation index - if (config.animation_index < 0 || config.animation_index >= static_cast(PMD_ANIM_COUNT)) { - BONGOCAT_LOG_WARNING("%s %d out of range [0-%d], resetting to 0", - ANIMATION_INDEX_KEY, config.animation_index, PMD_ANIM_COUNT - 1); - config.animation_index = 0; - ret |= (1uz << 29); - } - // Validate idle frame - if (config.idle_frame < 0 || config.idle_frame >= static_cast(PMD_ANIM_COUNT)) { - BONGOCAT_LOG_WARNING("%s %d out of range [0-%d], resetting to 0", - IDLE_FRAME_KEY, config.idle_frame, assets::PMD_ANIM_COUNT - 1); - config.idle_frame = 0; - ret |= (1uz << 30); - } - } - break; - case config_animation_custom_set_t::custom: - break; - } - } - break; - /// @NOTE(assets): 5. add animation_index validation - } - return ret; + if (config.custom_sprite_sheet_settings.end_writing_row_index >= 0) { + ret |= config_clamp_int(config.custom_sprite_sheet_settings.end_writing_row_index, MIN_CUSTOM_ROWS, + MAX_CUSTOM_ROWS, CUSTOM_END_WRITING_ROW_KEY); + } + if (config.custom_sprite_sheet_settings.happy_row_index >= 0) { + ret |= config_clamp_int(config.custom_sprite_sheet_settings.happy_row_index, MIN_CUSTOM_ROWS, MAX_CUSTOM_ROWS, + CUSTOM_HAPPY_ROW_KEY); + } + if (config.custom_sprite_sheet_settings.asleep_row_index >= 0) { + ret |= config_clamp_int(config.custom_sprite_sheet_settings.asleep_row_index, MIN_CUSTOM_ROWS, MAX_CUSTOM_ROWS, + CUSTOM_ASLEEP_ROW_KEY); + } + if (config.custom_sprite_sheet_settings.sleep_row_index >= 0) { + ret |= config_clamp_int(config.custom_sprite_sheet_settings.sleep_row_index, MIN_CUSTOM_ROWS, MAX_CUSTOM_ROWS, + CUSTOM_SLEEP_ROW_KEY); + } + if (config.custom_sprite_sheet_settings.wake_up_row_index >= 0) { + ret |= config_clamp_int(config.custom_sprite_sheet_settings.wake_up_row_index, MIN_CUSTOM_ROWS, MAX_CUSTOM_ROWS, + CUSTOM_WAKE_UP_ROW_KEY); + } + if (config.custom_sprite_sheet_settings.start_working_row_index >= 0) { + ret |= config_clamp_int(config.custom_sprite_sheet_settings.start_working_row_index, MIN_CUSTOM_ROWS, + MAX_CUSTOM_ROWS, CUSTOM_START_WORKING_ROW_KEY); + } + if (config.custom_sprite_sheet_settings.working_row_index >= 0) { + ret |= config_clamp_int(config.custom_sprite_sheet_settings.working_row_index, MIN_CUSTOM_ROWS, MAX_CUSTOM_ROWS, + CUSTOM_WORKING_ROW_KEY); + } + if (config.custom_sprite_sheet_settings.end_working_row_index >= 0) { + ret |= config_clamp_int(config.custom_sprite_sheet_settings.end_working_row_index, MIN_CUSTOM_ROWS, + MAX_CUSTOM_ROWS, CUSTOM_END_WORKING_ROW_KEY); + } + if (config.custom_sprite_sheet_settings.start_moving_row_index >= 0) { + ret |= config_clamp_int(config.custom_sprite_sheet_settings.start_moving_row_index, MIN_CUSTOM_ROWS, + MAX_CUSTOM_ROWS, CUSTOM_START_MOVING_ROW_KEY); + } + if (config.custom_sprite_sheet_settings.moving_row_index >= 0) { + ret |= config_clamp_int(config.custom_sprite_sheet_settings.moving_row_index, MIN_CUSTOM_ROWS, MAX_CUSTOM_ROWS, + CUSTOM_MOVING_ROW_KEY); + } + if (config.custom_sprite_sheet_settings.end_moving_row_index >= 0) { + ret |= config_clamp_int(config.custom_sprite_sheet_settings.end_moving_row_index, MIN_CUSTOM_ROWS, + MAX_CUSTOM_ROWS, CUSTOM_END_MOVING_ROW_KEY); + } + if (config.custom_sprite_sheet_settings.start_running_row_index >= 0) { + ret |= config_clamp_int(config.custom_sprite_sheet_settings.start_running_row_index, MIN_CUSTOM_ROWS, + MAX_CUSTOM_ROWS, CUSTOM_START_RUNNING_ROW_KEY); + } + if (config.custom_sprite_sheet_settings.running_row_index >= 0) { + ret |= config_clamp_int(config.custom_sprite_sheet_settings.running_row_index, MIN_CUSTOM_ROWS, MAX_CUSTOM_ROWS, + CUSTOM_RUNNING_ROW_KEY); + } + if (config.custom_sprite_sheet_settings.end_running_row_index >= 0) { + ret |= config_clamp_int(config.custom_sprite_sheet_settings.end_running_row_index, MIN_CUSTOM_ROWS, + MAX_CUSTOM_ROWS, CUSTOM_END_RUNNING_ROW_KEY); + } + if (config.custom_sprite_sheet_settings.rows >= 0) { + ret |= config_clamp_int(config.custom_sprite_sheet_settings.rows, 1, MAX_CUSTOM_ROWS, CUSTOM_ROWS_KEY); } - static uint64_t config_validate_enums(config_t& config) { - uint64_t ret{0}; - // Validate layer - if (config.layer != layer_type_t::LAYER_BACKGROUND && - config.layer != layer_type_t::LAYER_BOTTOM && - config.layer != layer_type_t::LAYER_TOP && - config.layer != layer_type_t::LAYER_OVERLAY) { - BONGOCAT_LOG_WARNING("Invalid layer %d, resetting to top", config.layer); - config.layer = layer_type_t::LAYER_TOP; - ret |= (1uz << 31); - } - - // Validate overlay_position - if (config.overlay_position != overlay_position_t::POSITION_TOP && config.overlay_position != overlay_position_t::POSITION_BOTTOM) { - BONGOCAT_LOG_WARNING("Invalid %s %d, resetting to top", OVERLAY_OPACITY_KEY, config.overlay_position); - config.overlay_position = overlay_position_t::POSITION_TOP; - ret |= (1uz << 32); - } + const int sprite_sheet_cols = get_custom_animation_settings_max_cols(config.custom_sprite_sheet_settings); + const int sprite_sheet_rows = get_custom_animation_settings_rows_count(config.custom_sprite_sheet_settings); - // Validate cat_align - if (config.cat_align != align_type_t::ALIGN_CENTER && config.cat_align != align_type_t::ALIGN_LEFT && config.cat_align != align_type_t::ALIGN_RIGHT) { - BONGOCAT_LOG_WARNING("Invalid %s %d, resetting to center", CAT_ALIGN_KEY, config.cat_align); - config.cat_align = align_type_t::ALIGN_CENTER; - ret |= (1uz << 33); - } + if (sprite_sheet_cols <= 0) { + BONGOCAT_LOG_WARNING("custom sprite sheet has no columns"); + ret |= (1u << 3); + } - return ret; + // validate cols + ret |= config_validate_max_int(config.custom_sprite_sheet_settings.idle_frames, sprite_sheet_cols, + CUSTOM_IDLE_FRAMES_KEY); + + ret |= config_validate_max_int(config.custom_sprite_sheet_settings.boring_frames, sprite_sheet_cols, + CUSTOM_BORING_FRAMES_KEY); + + ret |= config_validate_max_int(config.custom_sprite_sheet_settings.start_writing_frames, sprite_sheet_cols, + CUSTOM_START_WRITING_FRAMES_KEY); + ret |= config_validate_max_int(config.custom_sprite_sheet_settings.writing_frames, sprite_sheet_cols, + CUSTOM_WRITING_FRAMES_KEY); + ret |= config_validate_max_int(config.custom_sprite_sheet_settings.end_writing_frames, sprite_sheet_cols, + CUSTOM_END_WRITING_FRAMES_KEY); + ret |= config_validate_max_int(config.custom_sprite_sheet_settings.happy_frames, sprite_sheet_cols, + CUSTOM_HAPPY_FRAMES_KEY); + + ret |= config_validate_max_int(config.custom_sprite_sheet_settings.asleep_frames, sprite_sheet_cols, + CUSTOM_ASLEEP_FRAMES_KEY); + ret |= config_validate_max_int(config.custom_sprite_sheet_settings.sleep_frames, sprite_sheet_cols, + CUSTOM_SLEEP_FRAMES_KEY); + ret |= config_validate_max_int(config.custom_sprite_sheet_settings.wake_up_frames, sprite_sheet_cols, + CUSTOM_WAKE_UP_FRAMES_KEY); + + ret |= config_validate_max_int(config.custom_sprite_sheet_settings.start_working_frames, sprite_sheet_cols, + CUSTOM_START_WORKING_FRAMES_KEY); + ret |= config_validate_max_int(config.custom_sprite_sheet_settings.working_frames, sprite_sheet_cols, + CUSTOM_WORKING_FRAMES_KEY); + ret |= config_validate_max_int(config.custom_sprite_sheet_settings.end_working_frames, sprite_sheet_cols, + CUSTOM_END_WORKING_FRAMES_KEY); + + ret |= config_validate_max_int(config.custom_sprite_sheet_settings.start_moving_frames, sprite_sheet_cols, + CUSTOM_START_MOVING_FRAMES_KEY); + ret |= config_validate_max_int(config.custom_sprite_sheet_settings.moving_frames, sprite_sheet_cols, + CUSTOM_MOVING_FRAMES_KEY); + ret |= config_validate_max_int(config.custom_sprite_sheet_settings.end_moving_frames, sprite_sheet_cols, + CUSTOM_END_MOVING_FRAMES_KEY); + + ret |= config_validate_max_int(config.custom_sprite_sheet_settings.start_running_frames, sprite_sheet_cols, + CUSTOM_START_RUNNING_FRAMES_KEY); + ret |= config_validate_max_int(config.custom_sprite_sheet_settings.running_frames, sprite_sheet_cols, + CUSTOM_RUNNING_FRAMES_KEY); + ret |= config_validate_max_int(config.custom_sprite_sheet_settings.end_running_frames, sprite_sheet_cols, + CUSTOM_END_RUNNING_FRAMES_KEY); + + // validate rows + if (sprite_sheet_rows > 0) { + if (config.custom_sprite_sheet_settings.idle_row_index >= 0) { + ret |= config_validate_max_int(config.custom_sprite_sheet_settings.idle_row_index, sprite_sheet_rows - 1, + CUSTOM_IDLE_ROW_KEY); + } + if (config.custom_sprite_sheet_settings.boring_row_index >= 0) { + ret |= config_validate_max_int(config.custom_sprite_sheet_settings.boring_row_index, sprite_sheet_rows - 1, + CUSTOM_BORING_ROW_KEY); + } + if (config.custom_sprite_sheet_settings.start_writing_row_index >= 0) { + ret |= config_validate_max_int(config.custom_sprite_sheet_settings.start_writing_row_index, + sprite_sheet_rows - 1, CUSTOM_START_WRITING_ROW_KEY); + } + if (config.custom_sprite_sheet_settings.writing_row_index >= 0) { + ret |= config_validate_max_int(config.custom_sprite_sheet_settings.writing_row_index, sprite_sheet_rows - 1, + CUSTOM_WRITING_ROW_KEY); + } + if (config.custom_sprite_sheet_settings.end_writing_row_index >= 0) { + ret |= config_validate_max_int(config.custom_sprite_sheet_settings.end_writing_row_index, sprite_sheet_rows - 1, + CUSTOM_END_WRITING_ROW_KEY); + } + if (config.custom_sprite_sheet_settings.happy_row_index >= 0) { + ret |= config_validate_max_int(config.custom_sprite_sheet_settings.happy_row_index, sprite_sheet_rows - 1, + CUSTOM_HAPPY_ROW_KEY); + } + if (config.custom_sprite_sheet_settings.asleep_row_index >= 0) { + ret |= config_validate_max_int(config.custom_sprite_sheet_settings.asleep_row_index, sprite_sheet_rows - 1, + CUSTOM_ASLEEP_ROW_KEY); + } + if (config.custom_sprite_sheet_settings.sleep_row_index >= 0) { + ret |= config_validate_max_int(config.custom_sprite_sheet_settings.sleep_row_index, sprite_sheet_rows - 1, + CUSTOM_SLEEP_ROW_KEY); + } + if (config.custom_sprite_sheet_settings.wake_up_row_index >= 0) { + ret |= config_validate_max_int(config.custom_sprite_sheet_settings.wake_up_row_index, sprite_sheet_rows - 1, + CUSTOM_WAKE_UP_ROW_KEY); + } + if (config.custom_sprite_sheet_settings.start_working_row_index >= 0) { + ret |= config_validate_max_int(config.custom_sprite_sheet_settings.start_working_row_index, + sprite_sheet_rows - 1, CUSTOM_START_WORKING_ROW_KEY); + } + if (config.custom_sprite_sheet_settings.working_row_index >= 0) { + ret |= config_validate_max_int(config.custom_sprite_sheet_settings.working_row_index, sprite_sheet_rows - 1, + CUSTOM_WORKING_ROW_KEY); + } + if (config.custom_sprite_sheet_settings.end_working_row_index >= 0) { + ret |= config_validate_max_int(config.custom_sprite_sheet_settings.end_working_row_index, sprite_sheet_rows - 1, + CUSTOM_END_WORKING_ROW_KEY); + } + if (config.custom_sprite_sheet_settings.start_moving_row_index >= 0) { + ret |= config_validate_max_int(config.custom_sprite_sheet_settings.start_moving_row_index, + sprite_sheet_rows - 1, CUSTOM_START_MOVING_ROW_KEY); + } + if (config.custom_sprite_sheet_settings.moving_row_index >= 0) { + ret |= config_validate_max_int(config.custom_sprite_sheet_settings.moving_row_index, sprite_sheet_rows - 1, + CUSTOM_MOVING_ROW_KEY); + } + if (config.custom_sprite_sheet_settings.end_moving_row_index >= 0) { + ret |= config_validate_max_int(config.custom_sprite_sheet_settings.end_moving_row_index, sprite_sheet_rows - 1, + CUSTOM_END_MOVING_ROW_KEY); + } + if (config.custom_sprite_sheet_settings.start_running_row_index >= 0) { + ret |= config_validate_max_int(config.custom_sprite_sheet_settings.start_running_row_index, + sprite_sheet_rows - 1, CUSTOM_START_RUNNING_ROW_KEY); + } + if (config.custom_sprite_sheet_settings.running_row_index >= 0) { + ret |= config_validate_max_int(config.custom_sprite_sheet_settings.running_row_index, sprite_sheet_rows - 1, + CUSTOM_RUNNING_ROW_KEY); + } + if (config.custom_sprite_sheet_settings.end_running_row_index >= 0) { + ret |= config_validate_max_int(config.custom_sprite_sheet_settings.end_running_row_index, sprite_sheet_rows - 1, + CUSTOM_END_RUNNING_ROW_KEY); + } + } else { + if (config.custom_sprite_sheet_settings.rows <= 0) { + BONGOCAT_LOG_WARNING("custom sprite sheet has no rows"); + ret |= (1u << 4); + } } - static uint64_t config_validate_time(config_t& config) { - uint64_t ret{0}; - if (config.enable_scheduled_sleep) { - const int begin_minutes = config.sleep_begin.hour * 60 + config.sleep_begin.min; - const int end_minutes = config.sleep_end.hour * 60 + config.sleep_end.min; + // validate sprite sheet file + if (config.custom_sprite_sheet_filename != BONGOCAT_NULLPTR && strlen(config.custom_sprite_sheet_filename) != 0) { + constexpr size_t PNG_SIGNATURE_SIZE = 8; + constexpr unsigned char PNG_SIGNATURE[PNG_SIGNATURE_SIZE] = {0x89, 'P', 'N', 'G', '\r', '\n', 0x1A, '\n'}; + + // Try to open the file + platform::FileDescriptor fd(open(config.custom_sprite_sheet_filename, O_RDONLY)); + if (fd._fd < 0) { + BONGOCAT_LOG_ERROR("Custom Sprite Sheet doesn't exist or can't be opened: %s", + config.custom_sprite_sheet_filename); + ret |= (1u << 6); + return ret; + } - if (begin_minutes == end_minutes) { - BONGOCAT_LOG_WARNING("Sleep mode is enabled, but time is equal: %02d:%02d, disable sleep mode", config.sleep_begin.hour, config.sleep_begin.min); + struct stat st; + if (fstat(fd._fd, &st) < 0) { + BONGOCAT_LOG_ERROR("Custom Sprite Sheet can't be opened: %s", config.custom_sprite_sheet_filename); + ret |= (1u << 7); + return ret; + } + if (st.st_size == 0) { + BONGOCAT_LOG_ERROR("Custom Sprite Sheet is an empty file: %s", config.custom_sprite_sheet_filename); + ret |= (1u << 8); + return ret; + } - config.enable_scheduled_sleep = 0; - //config.sleep_begin.hour = 0; - //config.sleep_begin.min = 0; - //config.sleep_end.hour = 0; - //config.sleep_end.min = 0; - ret |= (1uz << 34); - } - } + unsigned char header[PNG_SIGNATURE_SIZE] = {0}; + const ssize_t n = read(fd._fd, header, PNG_SIGNATURE_SIZE); + if (n < static_cast(PNG_SIGNATURE_SIZE)) { + BONGOCAT_LOG_ERROR("Failed to read PNG header: %s", config.custom_sprite_sheet_filename); + ret |= (1u << 9); + return ret; + } + if (memcmp(header, PNG_SIGNATURE, PNG_SIGNATURE_SIZE) != 0) { + BONGOCAT_LOG_ERROR("Invalid PNG signature: %s", config.custom_sprite_sheet_filename); + ret |= (1u << 10); return ret; + } + } else { + // empty custom_sprite_sheet_filename + BONGOCAT_LOG_WARNING("custom_sprite_sheet_filename is empty"); + ret |= (1u << 5); } - static bongocat_error_t config_validate(config_t& config) { - uint64_t ret{0}; - // Normalize boolean values - config.enable_debug = config.enable_debug ? 1 : 0; - config.invert_color = config.invert_color ? 1 : 0; - config.idle_animation = config.idle_animation ? 1 : 0; - config.enable_scheduled_sleep = config.enable_scheduled_sleep ? 1 : 0; - config.mirror_x = config.mirror_x ? 1 : 0; - config.mirror_y = config.mirror_y ? 1 : 0; - config.randomize_index = config.randomize_index ? 1 : 0; - config.randomize_on_reload = config.randomize_on_reload ? 1 : 0; - config.enable_antialiasing = config.enable_antialiasing ? 1 : 0; - config.enable_movement_debug = config.enable_movement_debug ? 1 : 0; - - ret |= config_validate_dimensions(config); - ret |= config_validate_timing(config); - ret |= config_validate_appearance(config); - ret |= config_validate_enums(config); - ret |= config_validate_time(config); - ret |= config_validate_kpm(config); - ret |= config_validate_update(config); - ret |= config_validate_custom(config); - - if (config._strict) { - if (ret != 0) [[unlikely]] { - BONGOCAT_LOG_ERROR("Failed to load configuration in strict mode: %x", ret); - return bongocat_error_t::BONGOCAT_ERROR_INVALID_PARAM; - } - } - return bongocat_error_t::BONGOCAT_SUCCESS; + // validate frames + if (config.custom_sprite_sheet_settings.idle_frames <= 0) { + BONGOCAT_LOG_WARNING("custom sprite sheet needs at least an idle animation"); + ret |= (1u << 11); } - // ============================================================================= - // DEVICE MANAGEMENT MODULE - // ============================================================================= - - static bongocat_error_t config_add_keyboard_device(config_t& config, const char *device_path) { - BONGOCAT_CHECK_NULL(device_path, bongocat_error_t::BONGOCAT_ERROR_INVALID_PARAM); - - assert(config.num_keyboard_devices >= 0 && config.num_keyboard_devices < INT_MAX-1); - - const int old_num_keyboard_devices = config.num_keyboard_devices; - - assert(input::MAX_INPUT_DEVICES <= INT_MAX); - if (old_num_keyboard_devices >= static_cast(input::MAX_INPUT_DEVICES)) { - BONGOCAT_LOG_WARNING("Can not add more devices from config, max. reach: %d", input::MAX_INPUT_DEVICES); - return config._strict ? bongocat_error_t::BONGOCAT_ERROR_INVALID_PARAM : bongocat_error_t::BONGOCAT_SUCCESS; - } - const int new_num_keyboard_devices = old_num_keyboard_devices + 1; - assert(new_num_keyboard_devices >= 0); - assert(static_cast(new_num_keyboard_devices) <= input::MAX_INPUT_DEVICES); - - // Add new device path - config.keyboard_devices[old_num_keyboard_devices] = strdup(device_path); - if (!config.keyboard_devices[old_num_keyboard_devices]) { - // free new copied strings - for (int i = 0; i < old_num_keyboard_devices; i++) { - if (config.keyboard_devices[i]) ::free(config.keyboard_devices[i]); - config.keyboard_devices[i] = nullptr; - } - config.num_keyboard_devices = old_num_keyboard_devices; - BONGOCAT_LOG_ERROR("Failed to copy new keyboard device path"); - return bongocat_error_t::BONGOCAT_ERROR_MEMORY; - } - - // update new size - config.num_keyboard_devices = new_num_keyboard_devices; - - return bongocat_error_t::BONGOCAT_SUCCESS; + if (config.custom_sprite_sheet_settings.wake_up_frames > 0 && + (config.custom_sprite_sheet_settings.asleep_frames <= 0 || + config.custom_sprite_sheet_settings.sleep_frames <= 0)) { + BONGOCAT_LOG_WARNING("custom sprite sheet has a wake up animation, but no sleep animation"); + // ret |= (1u << 12); + // to hard of an error in strict mode, just print warning } - static void config_cleanup_devices(config_t& config) { - assert(config.num_keyboard_devices >= 0); - for (size_t i = 0; i < input::MAX_INPUT_DEVICES; i++) { - if (i < static_cast(config.num_keyboard_devices)) { - if (config.keyboard_devices[i]) ::free(config.keyboard_devices[i]); - } - config.keyboard_devices[i] = nullptr; - } - config.num_keyboard_devices = 0; + if (config.custom_sprite_sheet_settings.feature_mirror_x_moving >= 0 && + (config.custom_sprite_sheet_settings.start_moving_frames <= 0 && + config.custom_sprite_sheet_settings.moving_frames <= 0 && + config.custom_sprite_sheet_settings.end_moving_frames <= 0)) { + BONGOCAT_LOG_WARNING("feature_mirror_x_moving for custom sprite sheet is used, but has no moving animation"); + // ret |= (1u << 13); } - // ============================================================================= - // CONFIGURATION PARSING MODULE - // ============================================================================= - - static char* config_trim_str(char *key) { - char *key_start = key; - while (*key_start == ' ' || *key_start == '\t') key_start++; + if ((config.custom_sprite_sheet_settings.feature_toggle_writing_frames >= 0 || + config.custom_sprite_sheet_settings.feature_toggle_writing_frames_random >= 0) && + (config.custom_sprite_sheet_settings.start_writing_frames <= 0 && + config.custom_sprite_sheet_settings.writing_frames <= 0 && + config.custom_sprite_sheet_settings.end_writing_frames <= 0)) { + BONGOCAT_LOG_WARNING( + "feature_toggle_writing_frames for custom sprite sheet is used, but has no writing animation"); + // ret |= (1u << 14); + } - char *key_end = key_start + strlen(key_start) - 1; - while (key_end > key_start && (*key_end == ' ' || *key_end == '\t')) { - *key_end = '\0'; - key_end--; + if (config.enable_scheduled_sleep >= 0 && (config.custom_sprite_sheet_settings.asleep_frames <= 0 && + config.custom_sprite_sheet_settings.sleep_frames <= 0)) { + BONGOCAT_LOG_WARNING("enable_scheduled_sleep is enabled, but custom sprite sheet has no sleep animation"); + // ret |= (1u << 15); + } + if (config.happy_kpm >= 0 && (config.custom_sprite_sheet_settings.happy_frames <= 0)) { + BONGOCAT_LOG_WARNING("happy_kpm is used, but custom sprite sheet has no happy animation"); + // ret |= (1u << 16); + } + } + + return ret; +} + +static uint64_t config_validate_appearance(config_t& config) { + using namespace assets; + using namespace animation; + uint64_t ret{0}; + // Validate opacity + ret |= config_clamp_int(config.overlay_opacity, 0, 255, OVERLAY_OPACITY_KEY); + + switch (config.animation_sprite_sheet_layout) { + case config_animation_sprite_sheet_layout_t::None: + BONGOCAT_LOG_WARNING("Cant determine sprite sheet layout"); + /// @TODO: move validation error codes (1 << ..) into constants + ret |= (1u << 17); + break; + case config_animation_sprite_sheet_layout_t::Bongocat: + if constexpr (features::EnableBongocatEmbeddedAssets) { + // Validate animation index + assert(BONGOCAT_ANIMATIONS_COUNT <= INT_MAX); + if (config.animation_index < 0 || config.animation_index >= static_cast(BONGOCAT_ANIMATIONS_COUNT)) { + BONGOCAT_LOG_WARNING("%s %d out of range [0-%d], resetting to 0", ANIMATION_INDEX_KEY, config.animation_index, + BONGOCAT_ANIMATIONS_COUNT - 1); + config.animation_index = 0; + ret |= (1u << 18); + } + // Validate idle frame + assert(animation::BONGOCAT_NUM_FRAMES <= INT_MAX); + if (config.idle_frame < 0 || config.idle_frame >= static_cast(BONGOCAT_NUM_FRAMES)) { + BONGOCAT_LOG_WARNING("%s %d out of range [0-%d], resetting to 0", IDLE_FRAME_KEY, config.idle_frame, + BONGOCAT_NUM_FRAMES - 1); + config.idle_frame = 0; + ret |= (1u << 19); + } + } + break; + case config_animation_sprite_sheet_layout_t::Dm: + if constexpr (features::EnableDmEmbeddedAssets) { + // Validate animation index + assert(DM_ANIMATIONS_COUNT <= INT_MAX); + if (config.animation_index < 0 || config.animation_index >= static_cast(DM_ANIMATIONS_COUNT)) { + BONGOCAT_LOG_WARNING("%s %d out of range [0-%d], resetting to 0", ANIMATION_INDEX_KEY, config.animation_index, + assets::DM_ANIMATIONS_COUNT - 1); + config.animation_index = 0; + ret |= (1u << 20); + } + // Validate idle frame + assert(animation::MAX_DIGIMON_FRAMES <= INT_MAX); + if (config.idle_frame < 0 || config.idle_frame >= static_cast(animation::MAX_DIGIMON_FRAMES)) { + BONGOCAT_LOG_WARNING("%s %d out of range [0-%d], resetting to 0", IDLE_FRAME_KEY, config.idle_frame, + animation::MAX_DIGIMON_FRAMES - 1); + config.idle_frame = 0; + ret |= (1u << 21); + } + } + break; + case config_animation_sprite_sheet_layout_t::Pkmn: + if constexpr (features::EnablePkmnEmbeddedAssets) { + // Validate animation index + assert(DM_ANIMATIONS_COUNT <= INT_MAX); + if (config.animation_index < 0 || config.animation_index >= static_cast(PKMN_ANIMATIONS_COUNT)) { + BONGOCAT_LOG_WARNING("%s %d out of range [0-%d], resetting to 0", ANIMATION_INDEX_KEY, config.animation_index, + assets::PKMN_ANIMATIONS_COUNT - 1); + config.animation_index = 0; + ret |= (1u << 22); + } + // Validate idle frame + assert(animation::MAX_DIGIMON_FRAMES <= INT_MAX); + if (config.idle_frame < 0 || config.idle_frame >= static_cast(MAX_PKMN_FRAMES)) { + BONGOCAT_LOG_WARNING("%s %d out of range [0-%d], resetting to 0", IDLE_FRAME_KEY, config.idle_frame, + MAX_PKMN_FRAMES - 1); + config.idle_frame = 0; + ret |= (1u << 23); + } + } + break; + case config_animation_sprite_sheet_layout_t::MsAgent: + if constexpr (features::EnableMsAgentEmbeddedAssets) { + // Validate animation index + assert(assets::MS_AGENTS_ANIMATIONS_COUNT <= INT_MAX); + if (config.animation_index < 0 || config.animation_index >= static_cast(MS_AGENTS_ANIMATIONS_COUNT)) { + BONGOCAT_LOG_WARNING("%s %d out of range [0-%d], resetting to 0", ANIMATION_INDEX_KEY, config.animation_index, + MS_AGENTS_ANIMATIONS_COUNT - 1); + config.animation_index = 0; + ret |= (1u << 24); + } + // Validate idle frame + assert(assets::MS_AGENT_MAX_SPRITE_SHEET_COL_FRAMES <= INT_MAX); + if (config.idle_frame < 0 || config.idle_frame >= static_cast(MS_AGENT_MAX_SPRITE_SHEET_COL_FRAMES)) { + BONGOCAT_LOG_WARNING("%s %d out of range [0-%d], resetting to 0", IDLE_FRAME_KEY, config.idle_frame, + MS_AGENT_MAX_SPRITE_SHEET_COL_FRAMES - 1); + config.idle_frame = 0; + ret |= (1u << 25); + } + } + break; + case config_animation_sprite_sheet_layout_t::Custom: + assert(CUSTOM_ANIM_INDEX <= INT_MAX); + assert(MAX_MISC_ANIM_INDEX <= INT_MAX); + if constexpr (features::EnableCustomSpriteSheetsAssets) { + if (config._custom) { + assert(config.animation_custom_set == config_animation_custom_set_t::custom); + // Validate animation index + if (config.animation_index < 0 || config.animation_index > static_cast(CUSTOM_ANIM_INDEX)) { + BONGOCAT_LOG_WARNING("%s %d out of range [%d], resetting to 0", ANIMATION_INDEX_KEY, config.animation_index, + CUSTOM_ANIM_INDEX); + config.animation_index = 0; + ret |= (1u << 26); } - - return key_start; + /// @TODO: validate max (idle) frames + } } - - static bongocat_error_t config_parse_integer_key(config_t& config, const char *key, const char *value) { - errno = 0; - char* endptr_int = nullptr; - const int int_value = static_cast(strtol(value, &endptr_int, 10)); - if (errno != 0 || endptr_int == value || *endptr_int != '\0') { - return bongocat_error_t::BONGOCAT_ERROR_INVALID_PARAM; + if (!config._custom) { + switch (config.animation_custom_set) { + case config_animation_custom_set_t::None: + break; + case config_animation_custom_set_t::misc: + if constexpr (features::EnableMiscEmbeddedAssets) { + // Validate animation index + if (config.animation_index < 0 || config.animation_index > static_cast(MAX_MISC_ANIM_INDEX)) { + BONGOCAT_LOG_WARNING("%s %d out of range [0-%d], resetting to 0", ANIMATION_INDEX_KEY, + config.animation_index, MAX_MISC_ANIM_INDEX); + config.animation_index = 0; + ret |= (1u << 27); + } + // Validate idle frame + assert(assets::MISC_MAX_SPRITE_SHEET_COL_FRAMES <= INT_MAX); + if (config.idle_frame < 0 || config.idle_frame >= static_cast(MISC_MAX_SPRITE_SHEET_COL_FRAMES)) { + BONGOCAT_LOG_WARNING("%s %d out of range [0-%d], resetting to 0", IDLE_FRAME_KEY, config.idle_frame, + assets::MISC_MAX_SPRITE_SHEET_COL_FRAMES - 1); + config.idle_frame = 0; + ret |= (1u << 28); + } } - - if (strcmp(key, CAT_X_OFFSET_KEY) == 0) { - config.cat_x_offset = int_value; - } else if (strcmp(key, CAT_Y_OFFSET_KEY) == 0) { - config.cat_y_offset = int_value; - } else if (strcmp(key, CAT_HEIGHT_KEY) == 0) { - config.cat_height = int_value; - } else if (strcmp(key, OVERLAY_HEIGHT_KEY) == 0) { - config.overlay_height = int_value; - } else if (strcmp(key, IDLE_FRAME_KEY) == 0) { - config.idle_frame = int_value; - } else if (strcmp(key, KEYPRESS_DURATION_KEY) == 0) { - config.keypress_duration_ms = int_value; - } else if (strcmp(key, TEST_ANIMATION_DURATION_KEY) == 0) { - config.test_animation_duration_ms = int_value; - } else if (strcmp(key, TEST_ANIMATION_INTERVAL_KEY) == 0) { - config.test_animation_interval_sec = int_value; - } else if (strcmp(key, FPS_KEY) == 0) { - config.fps = int_value; - } else if (strcmp(key, OVERLAY_OPACITY_KEY) == 0) { - config.overlay_opacity = int_value; - } else if (strcmp(key, MIRROR_X_KEY) == 0) { - config.mirror_x = int_value; - } else if (strcmp(key, MIRROR_Y_KEY) == 0) { - config.mirror_y = int_value; - } else if (strcmp(key, ENABLE_ANTIALIASING_KEY) == 0) { - config.enable_antialiasing = int_value; - } else if (strcmp(key, ENABLE_DEBUG_KEY) == 0) { - config.enable_debug = int_value; - } else if (strcmp(key, ANIMATION_INDEX_KEY) == 0) { - config.animation_index = int_value; - } else if (strcmp(key, INVERT_COLOR_KEY) == 0) { - config.invert_color = int_value; - } else if (strcmp(key, PADDING_X_KEY) == 0) { - config.padding_x = int_value; - } else if (strcmp(key, PADDING_Y_KEY) == 0) { - config.padding_y = int_value; - } else if (strcmp(key, ENABLE_SCHEDULED_SLEEP_KEY) == 0) { - config.enable_scheduled_sleep = int_value; - } else if (strcmp(key, IDLE_SLEEP_TIMEOUT_KEY) == 0) { - config.idle_sleep_timeout_sec = int_value; - } else if (strcmp(key, HAPPY_KPM_KEY) == 0) { - config.happy_kpm = int_value; - } else if (strcmp(key, ANIMATION_SPEED_KEY) == 0) { - config.animation_speed_ms = int_value; - } else if (strcmp(key, IDLE_ANIMATION_KEY) == 0) { - config.idle_animation = int_value; - } else if (strcmp(key, INPUT_FPS_KEY) == 0) { - config.input_fps = int_value; - } else if (strcmp(key, RANDOM_KEY) == 0) { - config.randomize_index = int_value; - } else if (strcmp(key, RANDOM_ON_RELOAD_KEY) == 0) { - config.randomize_on_reload = int_value; - } else if (strcmp(key, UPDATE_RATE_KEY) == 0) { - config.update_rate_ms = int_value; - } else if (strcmp(key, CPU_THRESHOLD_KEY) == 0) { - config.cpu_threshold = int_value; - } else if (strcmp(key, CPU_RUNNING_FACTOR_KEY) == 0) { - config.cpu_running_factor = int_value; - } else if (strcmp(key, MOVEMENT_RADIUS_KEY) == 0) { - config.movement_radius = int_value; - } else if (strcmp(key, ENABLE_MOVEMENT_DEBUG_KEY) == 0) { - config.enable_movement_debug = int_value; - } else if (strcmp(key, MOVEMENT_SPEED_KEY) == 0) { - config.movement_speed = int_value; - } else if (strcmp(key, MOVEMENT_WAIT_FACTOR_KEY) == 0) { - config.movement_wait_factor = int_value; - } else if (strcmp(key, SCREEN_WIDTH_KEY) == 0) { - config.screen_width = int_value; - } else if (strcmp(key, CUSTOM_IDLE_FRAMES_KEY) == 0) { - config.custom_sprite_sheet_settings.idle_frames = int_value; - } else if (strcmp(key, CUSTOM_BORING_FRAMES_KEY) == 0) { - config.custom_sprite_sheet_settings.boring_frames = int_value; - } else if (strcmp(key, CUSTOM_START_WRITING_FRAMES_KEY) == 0) { - config.custom_sprite_sheet_settings.start_writing_frames = int_value; - } else if (strcmp(key, CUSTOM_WRITING_FRAMES_KEY) == 0) { - config.custom_sprite_sheet_settings.writing_frames = int_value; - } else if (strcmp(key, CUSTOM_END_WRITING_FRAMES_KEY) == 0) { - config.custom_sprite_sheet_settings.end_writing_frames = int_value; - } else if (strcmp(key, CUSTOM_HAPPY_FRAMES_KEY) == 0) { - config.custom_sprite_sheet_settings.happy_frames = int_value; - } else if (strcmp(key, CUSTOM_ASLEEP_FRAMES_KEY) == 0) { - config.custom_sprite_sheet_settings.asleep_frames = int_value; - } else if (strcmp(key, CUSTOM_SLEEP_FRAMES_KEY) == 0) { - config.custom_sprite_sheet_settings.sleep_frames = int_value; - } else if (strcmp(key, CUSTOM_WAKE_UP_FRAMES_KEY) == 0) { - config.custom_sprite_sheet_settings.wake_up_frames = int_value; - } else if (strcmp(key, CUSTOM_START_WORKING_FRAMES_KEY) == 0) { - config.custom_sprite_sheet_settings.start_working_frames = int_value; - } else if (strcmp(key, CUSTOM_WORKING_FRAMES_KEY) == 0) { - config.custom_sprite_sheet_settings.working_frames = int_value; - } else if (strcmp(key, CUSTOM_END_WORKING_FRAMES_KEY) == 0) { - config.custom_sprite_sheet_settings.end_working_frames = int_value; - } else if (strcmp(key, CUSTOM_START_MOVING_FRAMES_KEY) == 0) { - config.custom_sprite_sheet_settings.start_moving_frames = int_value; - } else if (strcmp(key, CUSTOM_MOVING_FRAMES_KEY) == 0) { - config.custom_sprite_sheet_settings.moving_frames = int_value; - } else if (strcmp(key, CUSTOM_END_MOVING_FRAMES_KEY) == 0) { - config.custom_sprite_sheet_settings.end_moving_frames = int_value; - } else if (strcmp(key, CUSTOM_START_RUNNING_FRAMES_KEY) == 0) { - config.custom_sprite_sheet_settings.start_running_frames = int_value; - } else if (strcmp(key, CUSTOM_RUNNING_FRAMES_KEY) == 0) { - config.custom_sprite_sheet_settings.running_frames = int_value; - } else if (strcmp(key, CUSTOM_END_RUNNING_FRAMES_KEY) == 0) { - config.custom_sprite_sheet_settings.end_running_frames = int_value; - } else if (strcmp(key, CUSTOM_TOGGLE_WRITING_FRAMES_KEY) == 0) { - config.custom_sprite_sheet_settings.feature_toggle_writing_frames = int_value; - } else if (strcmp(key, CUSTOM_TOGGLE_WRITING_FRAMES_RANDOM_KEY) == 0) { - config.custom_sprite_sheet_settings.feature_toggle_writing_frames_random = int_value; - } else if (strcmp(key, CUSTOM_MIRROR_X_MOVING_KEY) == 0) { - config.custom_sprite_sheet_settings.feature_mirror_x_moving = int_value; - } else if (strcmp(key, CUSTOM_IDLE_ROW_KEY) == 0) { - config.custom_sprite_sheet_settings.idle_row_index = int_value - 1; - } else if (strcmp(key, CUSTOM_BORING_ROW_KEY) == 0) { - config.custom_sprite_sheet_settings.boring_row_index = int_value - 1; - } else if (strcmp(key, CUSTOM_START_WRITING_ROW_KEY) == 0) { - config.custom_sprite_sheet_settings.start_writing_row_index = int_value - 1; - } else if (strcmp(key, CUSTOM_WRITING_ROW_KEY) == 0) { - config.custom_sprite_sheet_settings.writing_row_index = int_value - 1; - } else if (strcmp(key, CUSTOM_END_WRITING_ROW_KEY) == 0) { - config.custom_sprite_sheet_settings.end_writing_row_index = int_value - 1; - } else if (strcmp(key, CUSTOM_HAPPY_ROW_KEY) == 0) { - config.custom_sprite_sheet_settings.happy_row_index = int_value - 1; - } else if (strcmp(key, CUSTOM_ASLEEP_ROW_KEY) == 0) { - config.custom_sprite_sheet_settings.asleep_row_index = int_value - 1; - } else if (strcmp(key, CUSTOM_SLEEP_ROW_KEY) == 0) { - config.custom_sprite_sheet_settings.sleep_row_index = int_value - 1; - } else if (strcmp(key, CUSTOM_WAKE_UP_ROW_KEY) == 0) { - config.custom_sprite_sheet_settings.wake_up_row_index = int_value - 1; - } else if (strcmp(key, CUSTOM_START_WORKING_ROW_KEY) == 0) { - config.custom_sprite_sheet_settings.start_working_row_index = int_value - 1; - } else if (strcmp(key, CUSTOM_WORKING_ROW_KEY) == 0) { - config.custom_sprite_sheet_settings.working_row_index = int_value - 1; - } else if (strcmp(key, CUSTOM_END_WORKING_ROW_KEY) == 0) { - config.custom_sprite_sheet_settings.end_working_row_index = int_value - 1; - } else if (strcmp(key, CUSTOM_START_MOVING_ROW_KEY) == 0) { - config.custom_sprite_sheet_settings.start_moving_row_index = int_value - 1; - } else if (strcmp(key, CUSTOM_MOVING_ROW_KEY) == 0) { - config.custom_sprite_sheet_settings.moving_row_index = int_value - 1; - } else if (strcmp(key, CUSTOM_END_MOVING_ROW_KEY) == 0) { - config.custom_sprite_sheet_settings.end_moving_row_index = int_value - 1; - } else if (strcmp(key, CUSTOM_START_RUNNING_ROW_KEY) == 0) { - config.custom_sprite_sheet_settings.start_running_row_index = int_value - 1; - } else if (strcmp(key, CUSTOM_RUNNING_ROW_KEY) == 0) { - config.custom_sprite_sheet_settings.running_row_index = int_value - 1; - } else if (strcmp(key, CUSTOM_END_RUNNING_ROW_KEY) == 0) { - config.custom_sprite_sheet_settings.end_running_row_index = int_value - 1; - } else if (strcmp(key, CUSTOM_ROWS_KEY) == 0) { - config.custom_sprite_sheet_settings.rows = int_value; - } else { - return bongocat_error_t::BONGOCAT_ERROR_INVALID_PARAM; // Unknown key + break; + case config_animation_custom_set_t::pmd: + if constexpr (features::EnablePmdEmbeddedAssets) { + assert(assets::PMD_ANIM_COUNT <= INT_MAX); + // Validate animation index + if (config.animation_index < 0 || config.animation_index >= static_cast(PMD_ANIM_COUNT)) { + BONGOCAT_LOG_WARNING("%s %d out of range [0-%d], resetting to 0", ANIMATION_INDEX_KEY, + config.animation_index, PMD_ANIM_COUNT - 1); + config.animation_index = 0; + ret |= (1uz << 29); + } + // Validate idle frame + if (config.idle_frame < 0 || config.idle_frame >= static_cast(PMD_ANIM_COUNT)) { + BONGOCAT_LOG_WARNING("%s %d out of range [0-%d], resetting to 0", IDLE_FRAME_KEY, config.idle_frame, + assets::PMD_ANIM_COUNT - 1); + config.idle_frame = 0; + ret |= (1uz << 30); + } } - - return bongocat_error_t::BONGOCAT_SUCCESS; + break; + case config_animation_custom_set_t::custom: + break; + } + } + break; + /// @NOTE(assets): 5. add animation_index validation + } + return ret; +} + +static uint64_t config_validate_enums(config_t& config) { + uint64_t ret{0}; + // Validate layer + if (config.layer != layer_type_t::LAYER_BACKGROUND && config.layer != layer_type_t::LAYER_BOTTOM && + config.layer != layer_type_t::LAYER_TOP && config.layer != layer_type_t::LAYER_OVERLAY) { + BONGOCAT_LOG_WARNING("Invalid layer %d, resetting to top", config.layer); + config.layer = layer_type_t::LAYER_TOP; + ret |= (1uz << 31); + } + + // Validate overlay_position + if (config.overlay_position != overlay_position_t::POSITION_TOP && + config.overlay_position != overlay_position_t::POSITION_BOTTOM) { + BONGOCAT_LOG_WARNING("Invalid %s %d, resetting to top", OVERLAY_OPACITY_KEY, config.overlay_position); + config.overlay_position = overlay_position_t::POSITION_TOP; + ret |= (1uz << 32); + } + + // Validate cat_align + if (config.cat_align != align_type_t::ALIGN_CENTER && config.cat_align != align_type_t::ALIGN_LEFT && + config.cat_align != align_type_t::ALIGN_RIGHT) { + BONGOCAT_LOG_WARNING("Invalid %s %d, resetting to center", CAT_ALIGN_KEY, config.cat_align); + config.cat_align = align_type_t::ALIGN_CENTER; + ret |= (1uz << 33); + } + + return ret; +} + +static uint64_t config_validate_time(config_t& config) { + uint64_t ret{0}; + if (config.enable_scheduled_sleep >= 1) { + const int begin_minutes = (config.sleep_begin.hour * 60) + config.sleep_begin.min; + const int end_minutes = (config.sleep_end.hour * 60) + config.sleep_end.min; + + if (begin_minutes == end_minutes) { + BONGOCAT_LOG_WARNING("Sleep mode is enabled, but time is equal: %02d:%02d, disable sleep mode", + config.sleep_begin.hour, config.sleep_begin.min); + + config.enable_scheduled_sleep = 0; + // config.sleep_begin.hour = 0; + // config.sleep_begin.min = 0; + // config.sleep_end.hour = 0; + // config.sleep_end.min = 0; + ret |= (1uz << 34); } + } + return ret; +} + +static bongocat_error_t config_validate(config_t& config) { + uint64_t ret{0}; + // Normalize boolean values + config.enable_debug = config.enable_debug >= 1 ? 1 : 0; + config.invert_color = config.invert_color >= 1 ? 1 : 0; + config.idle_animation = config.idle_animation >= 1 ? 1 : 0; + config.enable_scheduled_sleep = config.enable_scheduled_sleep >= 1 ? 1 : 0; + config.mirror_x = config.mirror_x >= 1 ? 1 : 0; + config.mirror_y = config.mirror_y >= 1 ? 1 : 0; + config.randomize_index = config.randomize_index >= 1 ? 1 : 0; + config.randomize_on_reload = config.randomize_on_reload >= 1 ? 1 : 0; + config.enable_antialiasing = config.enable_antialiasing >= 1 ? 1 : 0; + config.enable_movement_debug = config.enable_movement_debug >= 1 ? 1 : 0; + config.enable_hand_mapping = config.enable_hand_mapping >= 1 ? 1 : 0; + + ret |= config_validate_dimensions(config); + ret |= config_validate_timing(config); + ret |= config_validate_appearance(config); + ret |= config_validate_enums(config); + ret |= config_validate_time(config); + ret |= config_validate_kpm(config); + ret |= config_validate_update(config); + ret |= config_validate_custom(config); + + if (config._strict) { + if (ret != 0) [[unlikely]] { + BONGOCAT_LOG_ERROR("Failed to load configuration in strict mode: %x", ret); + return bongocat_error_t::BONGOCAT_ERROR_INVALID_PARAM; + } + } + return bongocat_error_t::BONGOCAT_SUCCESS; +} - static bongocat_error_t config_parse_double_key(config_t& config, const char *key, const char *value) { - errno = 0; - char* endptr_double = nullptr; - const double double_value = strtod(value, &endptr_double); - if (errno != 0 || endptr_double == value || *endptr_double != '\0') { - return bongocat_error_t::BONGOCAT_ERROR_INVALID_PARAM; - } +// ============================================================================= +// DEVICE MANAGEMENT MODULE +// ============================================================================= - if (strcmp(key, CPU_THRESHOLD_KEY) == 0) { - config.cpu_threshold = double_value; - } else if (strcmp(key, CPU_RUNNING_FACTOR_KEY) == 0) { - config.cpu_running_factor = double_value; - } else if (strcmp(key, MOVEMENT_WAIT_FACTOR_KEY) == 0) { - config.movement_wait_factor = double_value; - } else { - return bongocat_error_t::BONGOCAT_ERROR_INVALID_PARAM; // Unknown key - } +static bongocat_error_t config_add_keyboard_device(config_t& config, const char *device_path) { + BONGOCAT_CHECK_NULL(device_path, bongocat_error_t::BONGOCAT_ERROR_INVALID_PARAM); + + assert(config.num_keyboard_devices >= 0 && config.num_keyboard_devices < INT_MAX - 1); + + const int old_num_keyboard_devices = config.num_keyboard_devices; + + assert(input::MAX_INPUT_DEVICES <= INT_MAX); + if (old_num_keyboard_devices >= static_cast(input::MAX_INPUT_DEVICES)) { + BONGOCAT_LOG_WARNING("Can not add more devices from config, max. reach: %d", input::MAX_INPUT_DEVICES); + return config._strict ? bongocat_error_t::BONGOCAT_ERROR_INVALID_PARAM : bongocat_error_t::BONGOCAT_SUCCESS; + } + const int new_num_keyboard_devices = old_num_keyboard_devices + 1; + assert(new_num_keyboard_devices >= 0); + assert(static_cast(new_num_keyboard_devices) <= input::MAX_INPUT_DEVICES); + + // Add new device path + config.keyboard_devices[old_num_keyboard_devices] = strdup(device_path); + if (config.keyboard_devices[old_num_keyboard_devices] == BONGOCAT_NULLPTR) [[unlikely]] { + // free new copied strings + for (int i = 0; i < old_num_keyboard_devices; i++) { + if (config.keyboard_devices[i] != BONGOCAT_NULLPTR) { + ::free(config.keyboard_devices[i]); + config.keyboard_devices[i] = BONGOCAT_NULLPTR; + } + } + config.num_keyboard_devices = old_num_keyboard_devices; + BONGOCAT_LOG_ERROR("Failed to copy new keyboard device path"); + return bongocat_error_t::BONGOCAT_ERROR_MEMORY; + } + + // update new size + config.num_keyboard_devices = new_num_keyboard_devices; + + return bongocat_error_t::BONGOCAT_SUCCESS; +} + +static void config_cleanup_devices(config_t& config) { + assert(config.num_keyboard_devices >= 0); + for (size_t i = 0; i < input::MAX_INPUT_DEVICES; i++) { + if (i < static_cast(config.num_keyboard_devices)) { + if (config.keyboard_devices[i] != BONGOCAT_NULLPTR) { + ::free(config.keyboard_devices[i]); + config.keyboard_devices[i] = BONGOCAT_NULLPTR; + } + } + } + config.num_keyboard_devices = 0; +} - return bongocat_error_t::BONGOCAT_SUCCESS; - } - - static bongocat_error_t config_parse_enum_key(config_t& config, const char *key, const char *value) { - if (strcmp(key, LAYER_KEY) == 0 || strcmp(key, OVERLAY_LAYER_KEY) == 0) { - if (strcmp(value, LAYER_TOP_STR) == 0) { - config.layer = layer_type_t::LAYER_TOP; - } else if (strcmp(value, LAYER_OVERLAY_STR) == 0) { - config.layer = layer_type_t::LAYER_OVERLAY; - } else if (strcmp(value, LAYER_BOTTOM_STR) == 0) { - config.layer = layer_type_t::LAYER_BOTTOM; - } else if (strcmp(value, LAYER_BACKGROUND_STR) == 0) { - config.layer = layer_type_t::LAYER_BACKGROUND; - } else { - BONGOCAT_LOG_WARNING("Invalid %s '%s', using 'top'", LAYER_KEY, value); - config.layer = layer_type_t::LAYER_TOP; - } - } else if (strcmp(key, OVERLAY_POSITION_KEY) == 0) { - if (strcmp(value, POSITION_TOP_STR) == 0) { - config.overlay_position = overlay_position_t::POSITION_TOP; - } else if (strcmp(value, POSITION_BOTTOM_STR) == 0) { - config.overlay_position = overlay_position_t::POSITION_BOTTOM; - } else { - BONGOCAT_LOG_WARNING("Invalid %s '%s', using 'top'", OVERLAY_POSITION_KEY, value); - config.overlay_position = overlay_position_t::POSITION_TOP; - } - } else if (strcmp(key, CAT_ALIGN_KEY) == 0) { - if (strcmp(value, ALIGN_CENTER_STR) == 0) { - config.cat_align = align_type_t::ALIGN_CENTER; - } else if (strcmp(value, ALIGN_LEFT_STR) == 0) { - config.cat_align = align_type_t::ALIGN_LEFT; - } else if (strcmp(value, ALIGN_RIGHT_STR) == 0) { - config.cat_align = align_type_t::ALIGN_RIGHT; - } else { - BONGOCAT_LOG_WARNING("Invalid %s '%s', using 'center'", CAT_ALIGN_KEY, value); - config.cat_align = align_type_t::ALIGN_CENTER; - } - } else { - return bongocat_error_t::BONGOCAT_ERROR_INVALID_PARAM; // Unknown key - } +// ============================================================================= +// CONFIGURATION PARSING MODULE +// ============================================================================= - return bongocat_error_t::BONGOCAT_SUCCESS; +static char *config_trim_str(char *key) { + char *key_start = key; + while (*key_start == ' ' || *key_start == '\t') { + key_start++; + } + + char *key_end = key_start + strlen(key_start) - 1; + while (key_end > key_start && (*key_end == ' ' || *key_end == '\t')) { + *key_end = '\0'; + key_end--; + } + + return key_start; +} + +static bongocat_error_t config_parse_integer_key(config_t& config, const char *key, const char *value) { + errno = 0; + char *endptr_int = BONGOCAT_NULLPTR; + const int int_value = static_cast(strtol(value, &endptr_int, 10)); + if (errno != 0 || endptr_int == value || *endptr_int != '\0') { + return bongocat_error_t::BONGOCAT_ERROR_INVALID_PARAM; + } + + if (strcmp(key, CAT_X_OFFSET_KEY) == 0) { + config.cat_x_offset = int_value; + } else if (strcmp(key, CAT_Y_OFFSET_KEY) == 0) { + config.cat_y_offset = int_value; + } else if (strcmp(key, CAT_HEIGHT_KEY) == 0) { + config.cat_height = int_value; + } else if (strcmp(key, OVERLAY_HEIGHT_KEY) == 0) { + config.overlay_height = int_value; + } else if (strcmp(key, IDLE_FRAME_KEY) == 0) { + config.idle_frame = int_value; + } else if (strcmp(key, KEYPRESS_DURATION_KEY) == 0) { + config.keypress_duration_ms = int_value; + } else if (strcmp(key, TEST_ANIMATION_DURATION_KEY) == 0) { + config.test_animation_duration_ms = int_value; + } else if (strcmp(key, TEST_ANIMATION_INTERVAL_KEY) == 0) { + config.test_animation_interval_sec = int_value; + } else if (strcmp(key, FPS_KEY) == 0) { + config.fps = int_value; + } else if (strcmp(key, OVERLAY_OPACITY_KEY) == 0) { + config.overlay_opacity = int_value; + } else if (strcmp(key, MIRROR_X_KEY) == 0) { + config.mirror_x = int_value; + } else if (strcmp(key, MIRROR_Y_KEY) == 0) { + config.mirror_y = int_value; + } else if (strcmp(key, ENABLE_ANTIALIASING_KEY) == 0) { + config.enable_antialiasing = int_value; + } else if (strcmp(key, ENABLE_DEBUG_KEY) == 0) { + config.enable_debug = int_value; + } else if (strcmp(key, ANIMATION_INDEX_KEY) == 0) { + config.animation_index = int_value; + } else if (strcmp(key, INVERT_COLOR_KEY) == 0) { + config.invert_color = int_value; + } else if (strcmp(key, PADDING_X_KEY) == 0) { + config.padding_x = int_value; + } else if (strcmp(key, PADDING_Y_KEY) == 0) { + config.padding_y = int_value; + } else if (strcmp(key, ENABLE_SCHEDULED_SLEEP_KEY) == 0) { + config.enable_scheduled_sleep = int_value; + } else if (strcmp(key, IDLE_SLEEP_TIMEOUT_KEY) == 0) { + config.idle_sleep_timeout_sec = int_value; + } else if (strcmp(key, HAPPY_KPM_KEY) == 0) { + config.happy_kpm = int_value; + } else if (strcmp(key, ANIMATION_SPEED_KEY) == 0) { + config.animation_speed_ms = int_value; + } else if (strcmp(key, IDLE_ANIMATION_KEY) == 0) { + config.idle_animation = int_value; + } else if (strcmp(key, INPUT_FPS_KEY) == 0) { + config.input_fps = int_value; + } else if (strcmp(key, RANDOM_KEY) == 0) { + config.randomize_index = int_value; + } else if (strcmp(key, RANDOM_ON_RELOAD_KEY) == 0) { + config.randomize_on_reload = int_value; + } else if (strcmp(key, UPDATE_RATE_KEY) == 0) { + config.update_rate_ms = int_value; + } else if (strcmp(key, CPU_THRESHOLD_KEY) == 0) { + config.cpu_threshold = int_value; + } else if (strcmp(key, CPU_RUNNING_FACTOR_KEY) == 0) { + config.cpu_running_factor = int_value; + } else if (strcmp(key, MOVEMENT_RADIUS_KEY) == 0) { + config.movement_radius = int_value; + } else if (strcmp(key, ENABLE_MOVEMENT_DEBUG_KEY) == 0) { + config.enable_movement_debug = int_value; + } else if (strcmp(key, MOVEMENT_SPEED_KEY) == 0) { + config.movement_speed = int_value; + } else if (strcmp(key, MOVEMENT_WAIT_FACTOR_KEY) == 0) { + config.movement_wait_factor = int_value; + } else if (strcmp(key, SCREEN_WIDTH_KEY) == 0) { + config.screen_width = int_value; + } else if (strcmp(key, ENABLE_HAND_MAPPING_KEY) == 0) { + config.enable_hand_mapping = int_value; + } else if (strcmp(key, CUSTOM_IDLE_FRAMES_KEY) == 0) { + config.custom_sprite_sheet_settings.idle_frames = int_value; + } else if (strcmp(key, CUSTOM_BORING_FRAMES_KEY) == 0) { + config.custom_sprite_sheet_settings.boring_frames = int_value; + } else if (strcmp(key, CUSTOM_START_WRITING_FRAMES_KEY) == 0) { + config.custom_sprite_sheet_settings.start_writing_frames = int_value; + } else if (strcmp(key, CUSTOM_WRITING_FRAMES_KEY) == 0) { + config.custom_sprite_sheet_settings.writing_frames = int_value; + } else if (strcmp(key, CUSTOM_END_WRITING_FRAMES_KEY) == 0) { + config.custom_sprite_sheet_settings.end_writing_frames = int_value; + } else if (strcmp(key, CUSTOM_HAPPY_FRAMES_KEY) == 0) { + config.custom_sprite_sheet_settings.happy_frames = int_value; + } else if (strcmp(key, CUSTOM_ASLEEP_FRAMES_KEY) == 0) { + config.custom_sprite_sheet_settings.asleep_frames = int_value; + } else if (strcmp(key, CUSTOM_SLEEP_FRAMES_KEY) == 0) { + config.custom_sprite_sheet_settings.sleep_frames = int_value; + } else if (strcmp(key, CUSTOM_WAKE_UP_FRAMES_KEY) == 0) { + config.custom_sprite_sheet_settings.wake_up_frames = int_value; + } else if (strcmp(key, CUSTOM_START_WORKING_FRAMES_KEY) == 0) { + config.custom_sprite_sheet_settings.start_working_frames = int_value; + } else if (strcmp(key, CUSTOM_WORKING_FRAMES_KEY) == 0) { + config.custom_sprite_sheet_settings.working_frames = int_value; + } else if (strcmp(key, CUSTOM_END_WORKING_FRAMES_KEY) == 0) { + config.custom_sprite_sheet_settings.end_working_frames = int_value; + } else if (strcmp(key, CUSTOM_START_MOVING_FRAMES_KEY) == 0) { + config.custom_sprite_sheet_settings.start_moving_frames = int_value; + } else if (strcmp(key, CUSTOM_MOVING_FRAMES_KEY) == 0) { + config.custom_sprite_sheet_settings.moving_frames = int_value; + } else if (strcmp(key, CUSTOM_END_MOVING_FRAMES_KEY) == 0) { + config.custom_sprite_sheet_settings.end_moving_frames = int_value; + } else if (strcmp(key, CUSTOM_START_RUNNING_FRAMES_KEY) == 0) { + config.custom_sprite_sheet_settings.start_running_frames = int_value; + } else if (strcmp(key, CUSTOM_RUNNING_FRAMES_KEY) == 0) { + config.custom_sprite_sheet_settings.running_frames = int_value; + } else if (strcmp(key, CUSTOM_END_RUNNING_FRAMES_KEY) == 0) { + config.custom_sprite_sheet_settings.end_running_frames = int_value; + } else if (strcmp(key, CUSTOM_TOGGLE_WRITING_FRAMES_KEY) == 0) { + config.custom_sprite_sheet_settings.feature_toggle_writing_frames = int_value; + } else if (strcmp(key, CUSTOM_TOGGLE_WRITING_FRAMES_RANDOM_KEY) == 0) { + config.custom_sprite_sheet_settings.feature_toggle_writing_frames_random = int_value; + } else if (strcmp(key, CUSTOM_MIRROR_X_MOVING_KEY) == 0) { + config.custom_sprite_sheet_settings.feature_mirror_x_moving = int_value; + } else if (strcmp(key, CUSTOM_IDLE_ROW_KEY) == 0) { + config.custom_sprite_sheet_settings.idle_row_index = int_value - 1; + } else if (strcmp(key, CUSTOM_BORING_ROW_KEY) == 0) { + config.custom_sprite_sheet_settings.boring_row_index = int_value - 1; + } else if (strcmp(key, CUSTOM_START_WRITING_ROW_KEY) == 0) { + config.custom_sprite_sheet_settings.start_writing_row_index = int_value - 1; + } else if (strcmp(key, CUSTOM_WRITING_ROW_KEY) == 0) { + config.custom_sprite_sheet_settings.writing_row_index = int_value - 1; + } else if (strcmp(key, CUSTOM_END_WRITING_ROW_KEY) == 0) { + config.custom_sprite_sheet_settings.end_writing_row_index = int_value - 1; + } else if (strcmp(key, CUSTOM_HAPPY_ROW_KEY) == 0) { + config.custom_sprite_sheet_settings.happy_row_index = int_value - 1; + } else if (strcmp(key, CUSTOM_ASLEEP_ROW_KEY) == 0) { + config.custom_sprite_sheet_settings.asleep_row_index = int_value - 1; + } else if (strcmp(key, CUSTOM_SLEEP_ROW_KEY) == 0) { + config.custom_sprite_sheet_settings.sleep_row_index = int_value - 1; + } else if (strcmp(key, CUSTOM_WAKE_UP_ROW_KEY) == 0) { + config.custom_sprite_sheet_settings.wake_up_row_index = int_value - 1; + } else if (strcmp(key, CUSTOM_START_WORKING_ROW_KEY) == 0) { + config.custom_sprite_sheet_settings.start_working_row_index = int_value - 1; + } else if (strcmp(key, CUSTOM_WORKING_ROW_KEY) == 0) { + config.custom_sprite_sheet_settings.working_row_index = int_value - 1; + } else if (strcmp(key, CUSTOM_END_WORKING_ROW_KEY) == 0) { + config.custom_sprite_sheet_settings.end_working_row_index = int_value - 1; + } else if (strcmp(key, CUSTOM_START_MOVING_ROW_KEY) == 0) { + config.custom_sprite_sheet_settings.start_moving_row_index = int_value - 1; + } else if (strcmp(key, CUSTOM_MOVING_ROW_KEY) == 0) { + config.custom_sprite_sheet_settings.moving_row_index = int_value - 1; + } else if (strcmp(key, CUSTOM_END_MOVING_ROW_KEY) == 0) { + config.custom_sprite_sheet_settings.end_moving_row_index = int_value - 1; + } else if (strcmp(key, CUSTOM_START_RUNNING_ROW_KEY) == 0) { + config.custom_sprite_sheet_settings.start_running_row_index = int_value - 1; + } else if (strcmp(key, CUSTOM_RUNNING_ROW_KEY) == 0) { + config.custom_sprite_sheet_settings.running_row_index = int_value - 1; + } else if (strcmp(key, CUSTOM_END_RUNNING_ROW_KEY) == 0) { + config.custom_sprite_sheet_settings.end_running_row_index = int_value - 1; + } else if (strcmp(key, CUSTOM_ROWS_KEY) == 0) { + config.custom_sprite_sheet_settings.rows = int_value; + } else { + return bongocat_error_t::BONGOCAT_ERROR_INVALID_PARAM; // Unknown key + } + + return bongocat_error_t::BONGOCAT_SUCCESS; +} + +static bongocat_error_t config_parse_double_key(config_t& config, const char *key, const char *value) { + errno = 0; + char *endptr_double = BONGOCAT_NULLPTR; + const double double_value = strtod(value, &endptr_double); + if (errno != 0 || endptr_double == value || *endptr_double != '\0') { + return bongocat_error_t::BONGOCAT_ERROR_INVALID_PARAM; + } + + if (strcmp(key, CPU_THRESHOLD_KEY) == 0) { + config.cpu_threshold = double_value; + } else if (strcmp(key, CPU_RUNNING_FACTOR_KEY) == 0) { + config.cpu_running_factor = double_value; + } else if (strcmp(key, MOVEMENT_WAIT_FACTOR_KEY) == 0) { + config.movement_wait_factor = double_value; + } else { + return bongocat_error_t::BONGOCAT_ERROR_INVALID_PARAM; // Unknown key + } + + return bongocat_error_t::BONGOCAT_SUCCESS; +} + +static bongocat_error_t config_parse_enum_key(config_t& config, const char *key, const char *value) { + if (strcmp(key, LAYER_KEY) == 0 || strcmp(key, OVERLAY_LAYER_KEY) == 0) { + if (strcmp(value, LAYER_TOP_STR) == 0) { + config.layer = layer_type_t::LAYER_TOP; + } else if (strcmp(value, LAYER_OVERLAY_STR) == 0) { + config.layer = layer_type_t::LAYER_OVERLAY; + } else if (strcmp(value, LAYER_BOTTOM_STR) == 0) { + config.layer = layer_type_t::LAYER_BOTTOM; + } else if (strcmp(value, LAYER_BACKGROUND_STR) == 0) { + config.layer = layer_type_t::LAYER_BACKGROUND; + } else { + BONGOCAT_LOG_WARNING("Invalid %s '%s', using 'top'", LAYER_KEY, value); + config.layer = layer_type_t::LAYER_TOP; + } + } else if (strcmp(key, OVERLAY_POSITION_KEY) == 0) { + if (strcmp(value, POSITION_TOP_STR) == 0) { + config.overlay_position = overlay_position_t::POSITION_TOP; + } else if (strcmp(value, POSITION_BOTTOM_STR) == 0) { + config.overlay_position = overlay_position_t::POSITION_BOTTOM; + } else { + BONGOCAT_LOG_WARNING("Invalid %s '%s', using 'top'", OVERLAY_POSITION_KEY, value); + config.overlay_position = overlay_position_t::POSITION_TOP; + } + } else if (strcmp(key, CAT_ALIGN_KEY) == 0) { + if (strcmp(value, ALIGN_CENTER_STR) == 0) { + config.cat_align = align_type_t::ALIGN_CENTER; + } else if (strcmp(value, ALIGN_LEFT_STR) == 0) { + config.cat_align = align_type_t::ALIGN_LEFT; + } else if (strcmp(value, ALIGN_RIGHT_STR) == 0) { + config.cat_align = align_type_t::ALIGN_RIGHT; + } else { + BONGOCAT_LOG_WARNING("Invalid %s '%s', using 'center'", CAT_ALIGN_KEY, value); + config.cat_align = align_type_t::ALIGN_CENTER; + } + } else { + return bongocat_error_t::BONGOCAT_ERROR_INVALID_PARAM; // Unknown key + } + + return bongocat_error_t::BONGOCAT_SUCCESS; +} + +bongocat_error_t config_parse_time(const char *value, int& hour, int& min) { + char *endptr = BONGOCAT_NULLPTR; + errno = 0; + + // Parse hour + const long h = strtol(value, &endptr, 10); + if (endptr == value || *endptr != ':' || errno == ERANGE || h < 0 || h > 23) { + return bongocat_error_t::BONGOCAT_ERROR_INVALID_PARAM; + } + + // Parse minute + value = endptr + 1; // skip ':' + errno = 0; + const long m = strtol(value, &endptr, 10); + if (endptr == value || *endptr != '\0' || errno == ERANGE || m < 0 || m > 59) { + return bongocat_error_t::BONGOCAT_ERROR_INVALID_PARAM; + } + + hour = static_cast(h); + min = static_cast(m); + return bongocat_error_t::BONGOCAT_SUCCESS; +} +static bongocat_error_t config_parse_string(config_t& config, const char *key, const char *value, + const load_config_overwrite_parameters_t& overwrite_parameters) { + using namespace assets; + if (strcmp(key, MONITOR_KEY) == 0 || strcmp(key, OUTPUT_NAME_KEY) == 0) { + if (config.output_name != BONGOCAT_NULLPTR) { + ::free(config.output_name); + config.output_name = BONGOCAT_NULLPTR; + } + if (value != BONGOCAT_NULLPTR && value[0] != '\0') { + config.output_name = strdup(value); + if (config.output_name == BONGOCAT_NULLPTR) { + BONGOCAT_LOG_ERROR("Failed to allocate memory for interface output"); + return bongocat_error_t::BONGOCAT_ERROR_MEMORY; + } + } else { + config.output_name = BONGOCAT_NULLPTR; + } + } else if (strcmp(key, CUSTOM_SPRITE_SHEET_FILENAME_KEY) == 0) { + if (config.custom_sprite_sheet_filename != BONGOCAT_NULLPTR) { + ::free(config.custom_sprite_sheet_filename); + config.custom_sprite_sheet_filename = BONGOCAT_NULLPTR; + } + if (value != BONGOCAT_NULLPTR && value[0] != '\0') { + config.custom_sprite_sheet_filename = strdup(value); + if (config.custom_sprite_sheet_filename == BONGOCAT_NULLPTR) { + BONGOCAT_LOG_ERROR("Failed to allocate memory for custom sprite sheet filename"); + return bongocat_error_t::BONGOCAT_ERROR_MEMORY; + } + } else { + config.custom_sprite_sheet_filename = BONGOCAT_NULLPTR; + } + } else if (strcmp(key, SLEEP_BEGIN_KEY) == 0) { + if (value != BONGOCAT_NULLPTR && value[0] != '\0') { + int hour{0}; + int min{0}; + if (config_parse_time(value, hour, min) != bongocat_error_t::BONGOCAT_SUCCESS) { + return bongocat_error_t::BONGOCAT_ERROR_INVALID_PARAM; // Invalid time format + } + if (hour < 0 || hour > 23 || min < 0 || min > 59) { + return bongocat_error_t::BONGOCAT_ERROR_INVALID_PARAM; // Invalid time format + } + + config.sleep_begin.hour = hour; + config.sleep_begin.min = min; + } else { + config.sleep_begin.hour = 0; + config.sleep_begin.min = 0; + } + } else if (strcmp(key, SLEEP_END_KEY) == 0) { + if (value != BONGOCAT_NULLPTR && value[0] != '\0') { + int hour{0}; + int min{0}; + if (config_parse_time(value, hour, min) != bongocat_error_t::BONGOCAT_SUCCESS) { + return bongocat_error_t::BONGOCAT_ERROR_INVALID_PARAM; // Invalid time format + } + if (hour < 0 || hour > 23 || min < 0 || min > 59) { + return bongocat_error_t::BONGOCAT_ERROR_INVALID_PARAM; // Invalid time format + } + + config.sleep_end.hour = hour; + config.sleep_end.min = min; + } else { + config.sleep_end.hour = 0; + config.sleep_end.min = 0; + } + } else if (strcmp(key, ANIMATION_NAME_KEY) == 0) { + using namespace assets; + if (overwrite_parameters.animation_name != BONGOCAT_NULLPTR) { + value = overwrite_parameters.animation_name; } + // set config._animation_name + if (config._animation_name != BONGOCAT_NULLPTR) { + ::free(config._animation_name); + config._animation_name = BONGOCAT_NULLPTR; + } + config._animation_name = value != BONGOCAT_NULLPTR ? strdup(value) : BONGOCAT_NULLPTR; - bongocat_error_t config_parse_time(const char *value, int& hour, int& min) { - char *endptr = nullptr; - errno = 0; - - // Parse hour - const long h = strtol(value, &endptr, 10); - if (endptr == value || *endptr != ':' || errno == ERANGE || h < 0 || h > 23) { - return bongocat_error_t::BONGOCAT_ERROR_INVALID_PARAM; - } + if (config._loaded_animation_fqname != BONGOCAT_NULLPTR) { + ::free(config._loaded_animation_fqname); + config._loaded_animation_fqname = BONGOCAT_NULLPTR; + } - // Parse minute - value = endptr + 1; // skip ':' - errno = 0; - const long m = strtol(value, &endptr, 10); - if (endptr == value || *endptr != '\0' || errno == ERANGE || m < 0 || m > 59) { - return bongocat_error_t::BONGOCAT_ERROR_INVALID_PARAM; - } + // reset state + config.animation_sprite_sheet_layout = config_animation_sprite_sheet_layout_t::None; + config.animation_dm_set = config_animation_dm_set_t::None; + config.animation_custom_set = config_animation_custom_set_t::None; + config.animation_index = -1; + + // is fully name like dm:..., dm20:..., dmc:... + [[maybe_unused]] const bool is_fqn = strchr(value, ':') != BONGOCAT_NULLPTR; + bool animation_found = false; + + if constexpr (features::EnableBongocatEmbeddedAssets) { + // check for bongocat + if (strcmp(value, BONGOCAT_NAME) == 0 || strcmp(value, BONGOCAT_ID) == 0 || strcmp(value, BONGOCAT_FQID) == 0 || + strcmp(value, BONGOCAT_FQNAME) == 0) { + config.animation_index = BONGOCAT_ANIM_INDEX; + config.animation_sprite_sheet_layout = config_animation_sprite_sheet_layout_t::Bongocat; + config._loaded_animation_fqname = strdup(BONGOCAT_FQNAME); + } + + animation_found = config.animation_index >= 0; + } - hour = static_cast(h); - min = static_cast(m); - return bongocat_error_t::BONGOCAT_SUCCESS; - } - static bongocat_error_t config_parse_string(config_t& config, const char *key, const char *value, const load_config_overwrite_parameters_t& overwrite_parameters) { - using namespace assets; - if (strcmp(key, MONITOR_KEY) == 0 || strcmp(key, OUTPUT_NAME_KEY) == 0) { - if (config.output_name) { - ::free(config.output_name); - config.output_name = nullptr; - } - if (value && value[0] != '\0') { - config.output_name = strdup(value); - if (!config.output_name) { - BONGOCAT_LOG_ERROR("Failed to allocate memory for interface output"); - return bongocat_error_t::BONGOCAT_ERROR_MEMORY; - } - } else { - config.output_name = nullptr; - } - } else if (strcmp(key, CUSTOM_SPRITE_SHEET_FILENAME_KEY) == 0) { - if (config.custom_sprite_sheet_filename) { - ::free(config.custom_sprite_sheet_filename); - config.custom_sprite_sheet_filename = nullptr; - } - if (value && value[0] != '\0') { - config.custom_sprite_sheet_filename = strdup(value); - if (!config.custom_sprite_sheet_filename) { - BONGOCAT_LOG_ERROR("Failed to allocate memory for custom sprite sheet filename"); - return bongocat_error_t::BONGOCAT_ERROR_MEMORY; - } - } else { - config.custom_sprite_sheet_filename = nullptr; - } - } else if (strcmp(key, SLEEP_BEGIN_KEY) == 0) { - if (value && value[0] != '\0') { - int hour{0}; - int min{0}; - if (config_parse_time(value, hour, min) != bongocat_error_t::BONGOCAT_SUCCESS) { - return bongocat_error_t::BONGOCAT_ERROR_INVALID_PARAM; // Invalid time format - } - if (hour < 0 || hour > 23 || min < 0 || min > 59) { - return bongocat_error_t::BONGOCAT_ERROR_INVALID_PARAM; // Invalid time format - } - - config.sleep_begin.hour = hour; - config.sleep_begin.min = min; - } else { - config.sleep_begin.hour = 0; - config.sleep_begin.min = 0; - } - } else if (strcmp(key, SLEEP_END_KEY) == 0) { - if (value && value[0] != '\0') { - int hour{0}; - int min{0}; - if (config_parse_time(value, hour, min) != bongocat_error_t::BONGOCAT_SUCCESS) { - return bongocat_error_t::BONGOCAT_ERROR_INVALID_PARAM; // Invalid time format - } - if (hour < 0 || hour > 23 || min < 0 || min > 59) { - return bongocat_error_t::BONGOCAT_ERROR_INVALID_PARAM; // Invalid time format - } - - config.sleep_end.hour = hour; - config.sleep_end.min = min; - } else { - config.sleep_end.hour = 0; - config.sleep_end.min = 0; - } - } else if (strcmp(key, ANIMATION_NAME_KEY) == 0) { - using namespace assets; - if (overwrite_parameters.animation_name) { - value = overwrite_parameters.animation_name; - } - - // set config._animation_name - if (config._animation_name) ::free(config._animation_name); - config._animation_name = nullptr; - config._animation_name = value ? strdup(value) : nullptr; - - if (config._loaded_animation_fqname) ::free(config._loaded_animation_fqname); - config._loaded_animation_fqname = nullptr; - - // reset state - config.animation_sprite_sheet_layout = config_animation_sprite_sheet_layout_t::None; - config.animation_dm_set = config_animation_dm_set_t::None; - config.animation_custom_set = config_animation_custom_set_t::None; - config.animation_index = -1; - - - // is fully name like dm:..., dm20:..., dmc:... - [[maybe_unused]] const bool is_fqn = strchr(value, ':') != nullptr; - bool animation_found = false; - - if constexpr (features::EnableBongocatEmbeddedAssets) { - // check for bongocat - if (strcmp(value, BONGOCAT_NAME) == 0 || - strcmp(value, BONGOCAT_ID) == 0 || - strcmp(value, BONGOCAT_FQID) == 0 || - strcmp(value, BONGOCAT_FQNAME) == 0) { - config.animation_index = BONGOCAT_ANIM_INDEX; - config.animation_sprite_sheet_layout = config_animation_sprite_sheet_layout_t::Bongocat; - config._loaded_animation_fqname = strdup(BONGOCAT_FQNAME); - } - - animation_found = config.animation_index >= 0; - } - - // check for dm - if constexpr (features::EnableDmEmbeddedAssets) { - using namespace assets; + // check for dm + if constexpr (features::EnableDmEmbeddedAssets) { + using namespace assets; #ifdef FEATURE_MIN_DM_EMBEDDED_ASSETS - if ((!is_fqn && animation_found) || (is_fqn && !animation_found) || (!is_fqn && !animation_found)) { // overwrite animation when needed, priorities the fq names -#include "min_dm_config_parse_enum_key.cpp.inl" - if (config.animation_index >= 0) { - assert(found_index >= 0); - /// @TODO: get fqname of min_dm - //config._loaded_animation_fqname = strdup(get_config_animation_name_min_dm(static_cast(found_index)).fqname); - BONGOCAT_LOG_DEBUG("Animation found for %s", value); - } - } - animation_found = config.animation_index >= 0; + if ((!is_fqn && animation_found) || (is_fqn && !animation_found) || + (!is_fqn && !animation_found)) { // overwrite animation when needed, priorities the fq names +# include "min_dm_config_parse_enum_key.cpp.inl" + if (config.animation_index >= 0) { + assert(found_index >= 0); + /// @TODO: get fqname of min_dm + // config._loaded_animation_fqname = + // strdup(get_config_animation_name_min_dm(static_cast(found_index)).fqname); + BONGOCAT_LOG_DEBUG("Animation found for %s", value); + } + } + animation_found = config.animation_index >= 0; #endif - /// @TODO: use macros (or templates) to reduce copy&paste code + /// @TODO: use macros (or templates) to reduce copy&paste code - /// @NOTE(assets): 3. add more dm versions here, config animation_name parsing + /// @NOTE(assets): 3. add more dm versions here, config animation_name parsing #ifdef FEATURE_DM_EMBEDDED_ASSETS - if ((!is_fqn && animation_found) || (is_fqn && !animation_found) || (!is_fqn && !animation_found)) { // overwrite animation when needed, priorities the fq names - const int found_index = config_parse_animation_name_dm(config, value); - if (found_index >= 0) { - assert(found_index >= 0); - if (config._loaded_animation_fqname) ::free(config._loaded_animation_fqname); - config._loaded_animation_fqname = nullptr; - config._loaded_animation_fqname = strdup(get_config_animation_name_dm(static_cast(found_index)).fqname); - BONGOCAT_LOG_DEBUG("Animation found for %s: %s", value, config._loaded_animation_fqname); - } - animation_found = config.animation_index >= 0; - } + if ((!is_fqn && animation_found) || (is_fqn && !animation_found) || + (!is_fqn && !animation_found)) { // overwrite animation when needed, priorities the fq names + const int found_index = config_parse_animation_name_dm(config, value); + if (found_index >= 0) { + assert(found_index >= 0); + if (config._loaded_animation_fqname != BONGOCAT_NULLPTR) { + ::free(config._loaded_animation_fqname); + config._loaded_animation_fqname = BONGOCAT_NULLPTR; + } + config._loaded_animation_fqname = + strdup(get_config_animation_name_dm(static_cast(found_index)).fqname); + BONGOCAT_LOG_DEBUG("Animation found for %s: %s", value, config._loaded_animation_fqname); + } + animation_found = config.animation_index >= 0; + } #endif #ifdef FEATURE_DM20_EMBEDDED_ASSETS - // overwrite animation when not found or full name - if ((!is_fqn && animation_found) || (is_fqn && !animation_found) || (!is_fqn && !animation_found)) { - const int found_index = config_parse_animation_name_dm20(config, value); - if (found_index >= 0) { - assert(found_index >= 0); - if (config._loaded_animation_fqname) ::free(config._loaded_animation_fqname); - config._loaded_animation_fqname = nullptr; - config._loaded_animation_fqname = strdup(get_config_animation_name_dm20(static_cast(found_index)).fqname); - BONGOCAT_LOG_DEBUG("Animation found for %s: %s", value, config._loaded_animation_fqname); - } - animation_found = config.animation_index >= 0; - } + // overwrite animation when not found or full name + if ((!is_fqn && animation_found) || (is_fqn && !animation_found) || (!is_fqn && !animation_found)) { + const int found_index = config_parse_animation_name_dm20(config, value); + if (found_index >= 0) { + assert(found_index >= 0); + if (config._loaded_animation_fqname != BONGOCAT_NULLPTR) { + ::free(config._loaded_animation_fqname); + config._loaded_animation_fqname = BONGOCAT_NULLPTR; + } + config._loaded_animation_fqname = + strdup(get_config_animation_name_dm20(static_cast(found_index)).fqname); + BONGOCAT_LOG_DEBUG("Animation found for %s: %s", value, config._loaded_animation_fqname); + } + animation_found = config.animation_index >= 0; + } #endif #ifdef FEATURE_DMX_EMBEDDED_ASSETS - // overwrite animation when not found or full name - if ((!is_fqn && animation_found) || (is_fqn && !animation_found) || (!is_fqn && !animation_found)) { - const int found_index = config_parse_animation_name_dmx(config, value); - if (found_index >= 0) { - assert(found_index >= 0); - if (config._loaded_animation_fqname) ::free(config._loaded_animation_fqname); - config._loaded_animation_fqname = nullptr; - config._loaded_animation_fqname = strdup(get_config_animation_name_dmx(static_cast(found_index)).fqname); - BONGOCAT_LOG_DEBUG("Animation found for %s: %s", value, config._loaded_animation_fqname); - } - animation_found = config.animation_index >= 0; - } + // overwrite animation when not found or full name + if ((!is_fqn && animation_found) || (is_fqn && !animation_found) || (!is_fqn && !animation_found)) { + const int found_index = config_parse_animation_name_dmx(config, value); + if (found_index >= 0) { + assert(found_index >= 0); + if (config._loaded_animation_fqname != BONGOCAT_NULLPTR) { + ::free(config._loaded_animation_fqname); + config._loaded_animation_fqname = BONGOCAT_NULLPTR; + } + config._loaded_animation_fqname = + strdup(get_config_animation_name_dmx(static_cast(found_index)).fqname); + BONGOCAT_LOG_DEBUG("Animation found for %s: %s", value, config._loaded_animation_fqname); + } + animation_found = config.animation_index >= 0; + } #endif #ifdef FEATURE_DMC_EMBEDDED_ASSETS - // overwrite animation when not found or full name - if ((!is_fqn && animation_found) || (is_fqn && !animation_found) || (!is_fqn && !animation_found)) { - const int found_index = config_parse_animation_name_dmc(config, value); - if (found_index >= 0) { - assert(config.animation_index >= 0); - if (config._loaded_animation_fqname) ::free(config._loaded_animation_fqname); - config._loaded_animation_fqname = nullptr; - config._loaded_animation_fqname = strdup(get_config_animation_name_dmc(static_cast(found_index)).fqname); - BONGOCAT_LOG_DEBUG("Animation found for %s: %s", value, config._loaded_animation_fqname); - } - animation_found = config.animation_index >= 0; - } + // overwrite animation when not found or full name + if ((!is_fqn && animation_found) || (is_fqn && !animation_found) || (!is_fqn && !animation_found)) { + const int found_index = config_parse_animation_name_dmc(config, value); + if (found_index >= 0) { + assert(config.animation_index >= 0); + if (config._loaded_animation_fqname != BONGOCAT_NULLPTR) { + ::free(config._loaded_animation_fqname); + config._loaded_animation_fqname = BONGOCAT_NULLPTR; + } + config._loaded_animation_fqname = + strdup(get_config_animation_name_dmc(static_cast(found_index)).fqname); + BONGOCAT_LOG_DEBUG("Animation found for %s: %s", value, config._loaded_animation_fqname); + } + animation_found = config.animation_index >= 0; + } #endif #ifdef FEATURE_PEN_EMBEDDED_ASSETS - // overwrite animation when not found or full name - if ((!is_fqn && animation_found) || (is_fqn && !animation_found) || (!is_fqn && !animation_found)) { - const int found_index = config_parse_animation_name_pen(config, value); - if (found_index >= 0) { - assert(found_index >= 0); - if (config._loaded_animation_fqname) ::free(config._loaded_animation_fqname); - config._loaded_animation_fqname = nullptr; - config._loaded_animation_fqname = strdup(get_config_animation_name_pen(static_cast(found_index)).fqname); - BONGOCAT_LOG_DEBUG("Animation found for %s: %s", value, config._loaded_animation_fqname); - } - animation_found = config.animation_index >= 0; - } + // overwrite animation when not found or full name + if ((!is_fqn && animation_found) || (is_fqn && !animation_found) || (!is_fqn && !animation_found)) { + const int found_index = config_parse_animation_name_pen(config, value); + if (found_index >= 0) { + assert(found_index >= 0); + if (config._loaded_animation_fqname != BONGOCAT_NULLPTR) { + ::free(config._loaded_animation_fqname); + config._loaded_animation_fqname = BONGOCAT_NULLPTR; + } + config._loaded_animation_fqname = + strdup(get_config_animation_name_pen(static_cast(found_index)).fqname); + BONGOCAT_LOG_DEBUG("Animation found for %s: %s", value, config._loaded_animation_fqname); + } + animation_found = config.animation_index >= 0; + } #endif #ifdef FEATURE_PEN20_EMBEDDED_ASSETS - // overwrite animation when not found or full name - if ((!is_fqn && animation_found) || (is_fqn && !animation_found) || (!is_fqn && !animation_found)) { - const int found_index = config_parse_animation_name_pen20(config, value); - if (found_index >= 0) { - assert(found_index >= 0); - if (config._loaded_animation_fqname) ::free(config._loaded_animation_fqname); - config._loaded_animation_fqname = nullptr; - config._loaded_animation_fqname = strdup(get_config_animation_name_pen20(static_cast(found_index)).fqname); - BONGOCAT_LOG_DEBUG("Animation found for %s: %s", value, config._loaded_animation_fqname); - } - animation_found = config.animation_index >= 0; - } + // overwrite animation when not found or full name + if ((!is_fqn && animation_found) || (is_fqn && !animation_found) || (!is_fqn && !animation_found)) { + const int found_index = config_parse_animation_name_pen20(config, value); + if (found_index >= 0) { + assert(found_index >= 0); + if (config._loaded_animation_fqname != BONGOCAT_NULLPTR) { + ::free(config._loaded_animation_fqname); + config._loaded_animation_fqname = BONGOCAT_NULLPTR; + } + config._loaded_animation_fqname = + strdup(get_config_animation_name_pen20(static_cast(found_index)).fqname); + BONGOCAT_LOG_DEBUG("Animation found for %s: %s", value, config._loaded_animation_fqname); + } + animation_found = config.animation_index >= 0; + } #endif #ifdef FEATURE_DMALL_EMBEDDED_ASSETS - // overwrite animation when not found or full name - if ((!is_fqn && animation_found) || (is_fqn && !animation_found) || (!is_fqn && !animation_found)) { - const int found_index = config_parse_animation_name_dmall(config, value); - if (found_index >= 0) { - assert(found_index >= 0); - if (config._loaded_animation_fqname) ::free(config._loaded_animation_fqname); - config._loaded_animation_fqname = nullptr; - config._loaded_animation_fqname = strdup(get_config_animation_name_dmall(static_cast(found_index)).fqname); - BONGOCAT_LOG_DEBUG("Animation found for %s: %s", value, config._loaded_animation_fqname); - } - animation_found = config.animation_index >= 0; - } + // overwrite animation when not found or full name + if ((!is_fqn && animation_found) || (is_fqn && !animation_found) || (!is_fqn && !animation_found)) { + const int found_index = config_parse_animation_name_dmall(config, value); + if (found_index >= 0) { + assert(found_index >= 0); + if (config._loaded_animation_fqname != BONGOCAT_NULLPTR) { + ::free(config._loaded_animation_fqname); + config._loaded_animation_fqname = BONGOCAT_NULLPTR; + } + config._loaded_animation_fqname = + strdup(get_config_animation_name_dmall(static_cast(found_index)).fqname); + BONGOCAT_LOG_DEBUG("Animation found for %s: %s", value, config._loaded_animation_fqname); + } + animation_found = config.animation_index >= 0; + } #endif - } - - // check for MS agent - if constexpr (features::EnableMsAgentEmbeddedAssets) { - // check for ms pets (clippy) - if (strcmp(value, CLIPPY_NAME) == 0 || - strcmp(value, CLIPPY_ID) == 0 || - strcmp(value, CLIPPY_FQID) == 0 || - strcmp(value, CLIPPY_FQNAME) == 0) { - config.animation_index = CLIPPY_ANIM_INDEX; - config.animation_sprite_sheet_layout = config_animation_sprite_sheet_layout_t::MsAgent; - if (config._loaded_animation_fqname) ::free(config._loaded_animation_fqname); - config._loaded_animation_fqname = nullptr; - config._loaded_animation_fqname = strdup(CLIPPY_FQNAME); - } + } + + // check for MS agent + if constexpr (features::EnableMsAgentEmbeddedAssets) { + // check for ms pets (clippy) + if (strcmp(value, CLIPPY_NAME) == 0 || strcmp(value, CLIPPY_ID) == 0 || strcmp(value, CLIPPY_FQID) == 0 || + strcmp(value, CLIPPY_FQNAME) == 0) { + config.animation_index = CLIPPY_ANIM_INDEX; + config.animation_sprite_sheet_layout = config_animation_sprite_sheet_layout_t::MsAgent; + if (config._loaded_animation_fqname != BONGOCAT_NULLPTR) { + ::free(config._loaded_animation_fqname); + config._loaded_animation_fqname = BONGOCAT_NULLPTR; + } + config._loaded_animation_fqname = strdup(CLIPPY_FQNAME); + } #ifdef FEATURE_MORE_MS_AGENT_EMBEDDED_ASSETS - /// @NOTE(assets): 4. add more MS Agents here - // Links - if (strcmp(value, LINKS_NAME) == 0 || - strcmp(value, LINKS_ID) == 0 || - strcmp(value, LINKS_FQID) == 0 || - strcmp(value, LINKS_FQNAME) == 0) { - config.animation_index = LINKS_ANIM_INDEX; - config.animation_sprite_sheet_layout = config_animation_sprite_sheet_layout_t::MsAgent; - if (config._loaded_animation_fqname) ::free(config._loaded_animation_fqname); - config._loaded_animation_fqname = nullptr; - config._loaded_animation_fqname = strdup(LINKS_FQNAME); - } - // Rover - if (strcmp(value, ROVER_NAME) == 0 || - strcmp(value, ROVER_ID) == 0 || - strcmp(value, ROVER_FQID) == 0 || - strcmp(value, ROVER_FQNAME) == 0) { - config.animation_index = ROVER_ANIM_INDEX; - config.animation_sprite_sheet_layout = config_animation_sprite_sheet_layout_t::MsAgent; - if (config._loaded_animation_fqname) ::free(config._loaded_animation_fqname); - config._loaded_animation_fqname = nullptr; - config._loaded_animation_fqname = strdup(ROVER_FQNAME); - } - // Merlin - if (strcmp(value, MERLIN_NAME) == 0 || - strcmp(value, MERLIN_ID) == 0 || - strcmp(value, MERLIN_FQID) == 0 || - strcmp(value, MERLIN_FQNAME) == 0) { - config.animation_index = MERLIN_ANIM_INDEX; - config.animation_sprite_sheet_layout = config_animation_sprite_sheet_layout_t::MsAgent; - if (config._loaded_animation_fqname) ::free(config._loaded_animation_fqname); - config._loaded_animation_fqname = nullptr; - config._loaded_animation_fqname = strdup(MERLIN_FQNAME); - } + /// @NOTE(assets): 4. add more MS Agents here + // Links + if (strcmp(value, LINKS_NAME) == 0 || strcmp(value, LINKS_ID) == 0 || strcmp(value, LINKS_FQID) == 0 || + strcmp(value, LINKS_FQNAME) == 0) { + config.animation_index = LINKS_ANIM_INDEX; + config.animation_sprite_sheet_layout = config_animation_sprite_sheet_layout_t::MsAgent; + if (config._loaded_animation_fqname != BONGOCAT_NULLPTR) { + ::free(config._loaded_animation_fqname); + config._loaded_animation_fqname = BONGOCAT_NULLPTR; + } + config._loaded_animation_fqname = strdup(LINKS_FQNAME); + } + // Rover + if (strcmp(value, ROVER_NAME) == 0 || strcmp(value, ROVER_ID) == 0 || strcmp(value, ROVER_FQID) == 0 || + strcmp(value, ROVER_FQNAME) == 0) { + config.animation_index = ROVER_ANIM_INDEX; + config.animation_sprite_sheet_layout = config_animation_sprite_sheet_layout_t::MsAgent; + if (config._loaded_animation_fqname != BONGOCAT_NULLPTR) { + ::free(config._loaded_animation_fqname); + config._loaded_animation_fqname = BONGOCAT_NULLPTR; + } + config._loaded_animation_fqname = strdup(ROVER_FQNAME); + } + // Merlin + if (strcmp(value, MERLIN_NAME) == 0 || strcmp(value, MERLIN_ID) == 0 || strcmp(value, MERLIN_FQID) == 0 || + strcmp(value, MERLIN_FQNAME) == 0) { + config.animation_index = MERLIN_ANIM_INDEX; + config.animation_sprite_sheet_layout = config_animation_sprite_sheet_layout_t::MsAgent; + if (config._loaded_animation_fqname != BONGOCAT_NULLPTR) { + ::free(config._loaded_animation_fqname); + config._loaded_animation_fqname = BONGOCAT_NULLPTR; + } + config._loaded_animation_fqname = strdup(MERLIN_FQNAME); + } #endif - animation_found = config.animation_index >= 0; - } + animation_found = config.animation_index >= 0; + } - // check for pkmn - if constexpr (features::EnablePkmnEmbeddedAssets) { - using namespace assets; + // check for pkmn + if constexpr (features::EnablePkmnEmbeddedAssets) { + using namespace assets; #ifdef FEATURE_PKMN_EMBEDDED_ASSETS - if ((!is_fqn && animation_found) || (is_fqn && !animation_found) || (!is_fqn && !animation_found)) { - const int found_index = config_parse_animation_name_pkmn(config, value); - if (found_index >= 0) { - assert(found_index >= 0); - if (config._loaded_animation_fqname) ::free(config._loaded_animation_fqname); - config._loaded_animation_fqname = nullptr; - config._loaded_animation_fqname = strdup(get_config_animation_name_pkmn(static_cast(found_index)).fqname); - BONGOCAT_LOG_DEBUG("Animation found for %s: %s", value, config._loaded_animation_fqname); - } - animation_found = config.animation_index >= 0; - } + if ((!is_fqn && animation_found) || (is_fqn && !animation_found) || (!is_fqn && !animation_found)) { + const int found_index = config_parse_animation_name_pkmn(config, value); + if (found_index >= 0) { + assert(found_index >= 0); + if (config._loaded_animation_fqname != BONGOCAT_NULLPTR) { + ::free(config._loaded_animation_fqname); + config._loaded_animation_fqname = BONGOCAT_NULLPTR; + } + config._loaded_animation_fqname = + strdup(get_config_animation_name_pkmn(static_cast(found_index)).fqname); + BONGOCAT_LOG_DEBUG("Animation found for %s: %s", value, config._loaded_animation_fqname); + } + animation_found = config.animation_index >= 0; + } #endif - } - // check for pmd (pkmn) - if constexpr (features::EnablePmdEmbeddedAssets) { - using namespace assets; + } + // check for pmd (pkmn) + if constexpr (features::EnablePmdEmbeddedAssets) { + using namespace assets; #ifdef FEATURE_PMD_EMBEDDED_ASSETS - if ((!is_fqn && animation_found) || (is_fqn && !animation_found) || (!is_fqn && !animation_found)) { - const int found_index = config_parse_animation_name_pmd(config, value); - if (found_index >= 0) { - assert(found_index >= 0); - if (config._loaded_animation_fqname) ::free(config._loaded_animation_fqname); - config._loaded_animation_fqname = nullptr; - config._loaded_animation_fqname = strdup(get_config_animation_name_pmd(static_cast(found_index)).fqname); - BONGOCAT_LOG_DEBUG("Animation found for %s: %s", value, config._loaded_animation_fqname); - } - animation_found = config.animation_index >= 0; - } -#endif - } - - // check for Misc (neko) - if constexpr (features::EnableMiscEmbeddedAssets) { - // check neko - if (strcmp(value, MISC_NEKO_NAME) == 0 || - strcmp(value, MISC_NEKO_ID) == 0 || - strcmp(value, MISC_NEKO_FQID) == 0 || - strcmp(value, MISC_NEKO_FQNAME) == 0) { - config.animation_index = MISC_NEKO_ANIM_INDEX; - config.animation_sprite_sheet_layout = config_animation_sprite_sheet_layout_t::Custom; - config.animation_custom_set = config_animation_custom_set_t::misc; - if (config._loaded_animation_fqname) ::free(config._loaded_animation_fqname); - config._loaded_animation_fqname = nullptr; - config._loaded_animation_fqname = strdup(MISC_NEKO_FQNAME); - animation_found = config.animation_index >= 0; - } - } - /// @NOTE(assets): 4. add more config animation_name parsring here - - // check for custom sprite sheet - if constexpr (features::EnableCustomSpriteSheetsAssets) { - // check custom - if (strcmp(value, CUSTOM_NAME) == 0 || - strcmp(value, CUSTOM_ID) == 0) { - config.animation_index = CUSTOM_ANIM_INDEX; - config.animation_sprite_sheet_layout = config_animation_sprite_sheet_layout_t::Custom; - config.animation_custom_set = config_animation_custom_set_t::custom; - if (config._loaded_animation_fqname) ::free(config._loaded_animation_fqname); - config._loaded_animation_fqname = nullptr; - config._loaded_animation_fqname = config.custom_sprite_sheet_filename ? strdup(config.custom_sprite_sheet_filename) : nullptr; - animation_found = config.animation_index >= 0; - config._custom = config.animation_index == CUSTOM_ANIM_INDEX; - - if (config.custom_sprite_sheet_filename == nullptr || strlen(config.custom_sprite_sheet_filename) <= 0) { - BONGOCAT_LOG_WARNING("custom_sprite_sheet_filename required for custom sprite sheet"); - animation_found = false; - } - } - } - - animation_found = config.animation_index >= 0 && config.animation_sprite_sheet_layout != config_animation_sprite_sheet_layout_t::None; - if (!animation_found) { - if (config.animation_index >= 0 && config.animation_sprite_sheet_layout == config_animation_sprite_sheet_layout_t::None) { - BONGOCAT_LOG_WARNING("animation_index is set, but not animation_type (unknown type for index=%i and value='%s')", config.animation_index, value); - } - if (config._strict) { - BONGOCAT_LOG_ERROR("Invalid %s '%s'", ANIMATION_NAME_KEY, value, BONGOCAT_NAME); - return bongocat_error_t::BONGOCAT_ERROR_INVALID_PARAM; - } - BONGOCAT_LOG_WARNING("Invalid %s '%s', using '%s'", ANIMATION_NAME_KEY, value, BONGOCAT_NAME); - config.animation_index = BONGOCAT_ANIM_INDEX; - config.animation_sprite_sheet_layout = config_animation_sprite_sheet_layout_t::Bongocat; - config.animation_dm_set = config_animation_dm_set_t::None; - config.animation_custom_set = config_animation_custom_set_t::None; - if (config._loaded_animation_fqname) ::free(config._loaded_animation_fqname); - config._loaded_animation_fqname = nullptr; - config._loaded_animation_fqname = strdup(BONGOCAT_FQNAME); - } - } else { - return bongocat_error_t::BONGOCAT_ERROR_INVALID_PARAM; // Unknown key + if ((!is_fqn && animation_found) || (is_fqn && !animation_found) || (!is_fqn && !animation_found)) { + const int found_index = config_parse_animation_name_pmd(config, value); + if (found_index >= 0) { + assert(found_index >= 0); + if (config._loaded_animation_fqname != BONGOCAT_NULLPTR) { + ::free(config._loaded_animation_fqname); + config._loaded_animation_fqname = BONGOCAT_NULLPTR; + } + config._loaded_animation_fqname = + strdup(get_config_animation_name_pmd(static_cast(found_index)).fqname); + BONGOCAT_LOG_DEBUG("Animation found for %s: %s", value, config._loaded_animation_fqname); } - - return bongocat_error_t::BONGOCAT_SUCCESS; + animation_found = config.animation_index >= 0; + } +#endif } - static bongocat_error_t config_parse_key_value(config_t& config, const char *key, const char *value, const load_config_overwrite_parameters_t& overwrite_parameters) { - // Try integer keys first - if (config_parse_integer_key(config, key, value) == bongocat_error_t::BONGOCAT_SUCCESS) { - return bongocat_error_t::BONGOCAT_SUCCESS; - } - // Try double keys first - if (config_parse_double_key(config, key, value) == bongocat_error_t::BONGOCAT_SUCCESS) { - return bongocat_error_t::BONGOCAT_SUCCESS; - } - - // Try enum keys - if (config_parse_enum_key(config, key, value) == bongocat_error_t::BONGOCAT_SUCCESS) { - return bongocat_error_t::BONGOCAT_SUCCESS; + // check for Misc (neko) + if constexpr (features::EnableMiscEmbeddedAssets) { + // check neko + if (strcmp(value, MISC_NEKO_NAME) == 0 || strcmp(value, MISC_NEKO_ID) == 0 || + strcmp(value, MISC_NEKO_FQID) == 0 || strcmp(value, MISC_NEKO_FQNAME) == 0) { + config.animation_index = MISC_NEKO_ANIM_INDEX; + config.animation_sprite_sheet_layout = config_animation_sprite_sheet_layout_t::Custom; + config.animation_custom_set = config_animation_custom_set_t::misc; + if (config._loaded_animation_fqname != BONGOCAT_NULLPTR) { + ::free(config._loaded_animation_fqname); + config._loaded_animation_fqname = BONGOCAT_NULLPTR; } - - // Try string - if (config_parse_string(config, key, value, overwrite_parameters) == bongocat_error_t::BONGOCAT_SUCCESS) { - return bongocat_error_t::BONGOCAT_SUCCESS; + config._loaded_animation_fqname = strdup(MISC_NEKO_FQNAME); + animation_found = config.animation_index >= 0; + } + } + /// @NOTE(assets): 4. add more config animation_name parsring here + + // check for custom sprite sheet + if constexpr (features::EnableCustomSpriteSheetsAssets) { + // check custom + if (strcmp(value, CUSTOM_NAME) == 0 || strcmp(value, CUSTOM_ID) == 0) { + config.animation_index = CUSTOM_ANIM_INDEX; + config.animation_sprite_sheet_layout = config_animation_sprite_sheet_layout_t::Custom; + config.animation_custom_set = config_animation_custom_set_t::custom; + if (config._loaded_animation_fqname != BONGOCAT_NULLPTR) { + ::free(config._loaded_animation_fqname); + config._loaded_animation_fqname = BONGOCAT_NULLPTR; } - - // Handle device keys - if (strcmp(key, KEYBOARD_DEVICE_KEY) == 0 || strcmp(key, KEYBOARD_DEVICES_KEY) == 0) { - return config_add_keyboard_device(config, value); + config._loaded_animation_fqname = config.custom_sprite_sheet_filename != BONGOCAT_NULLPTR + ? strdup(config.custom_sprite_sheet_filename) + : BONGOCAT_NULLPTR; + animation_found = config.animation_index >= 0; + config._custom = config.animation_index == CUSTOM_ANIM_INDEX; + + if (config.custom_sprite_sheet_filename == BONGOCAT_NULLPTR || + strlen(config.custom_sprite_sheet_filename) <= 0) { + BONGOCAT_LOG_WARNING("custom_sprite_sheet_filename required for custom sprite sheet"); + animation_found = false; } + } + } - // Unknown key + animation_found = config.animation_index >= 0 && + config.animation_sprite_sheet_layout != config_animation_sprite_sheet_layout_t::None; + if (!animation_found) { + if (config.animation_index >= 0 && + config.animation_sprite_sheet_layout == config_animation_sprite_sheet_layout_t::None) { + BONGOCAT_LOG_WARNING( + "animation_index is set, but not animation_type (unknown type for index=%i and value='%s')", + config.animation_index, value); + } + if (config._strict) { + BONGOCAT_LOG_ERROR("Invalid %s '%s'", ANIMATION_NAME_KEY, value, BONGOCAT_NAME); return bongocat_error_t::BONGOCAT_ERROR_INVALID_PARAM; + } + BONGOCAT_LOG_WARNING("Invalid %s '%s', using '%s'", ANIMATION_NAME_KEY, value, BONGOCAT_NAME); + config.animation_index = BONGOCAT_ANIM_INDEX; + config.animation_sprite_sheet_layout = config_animation_sprite_sheet_layout_t::Bongocat; + config.animation_dm_set = config_animation_dm_set_t::None; + config.animation_custom_set = config_animation_custom_set_t::None; + if (config._loaded_animation_fqname != BONGOCAT_NULLPTR) { + ::free(config._loaded_animation_fqname); + config._loaded_animation_fqname = BONGOCAT_NULLPTR; + } + config._loaded_animation_fqname = strdup(BONGOCAT_FQNAME); } - - static bool config_is_comment_or_empty(const char *line) { - return (line[0] == '#' || line[0] == '\0' || strspn(line, " \t") == strlen(line)); - } - - - static bongocat_error_t config_parse_file(FILE *file, config_t& config, const load_config_overwrite_parameters_t& overwrite_parameters) { - char line[LINE_BUF] = {0}; - char key[KEY_BUF] = {0}; - char value[VALUE_BUF] = {0}; - int line_number = 0; - bongocat_error_t result = bongocat_error_t::BONGOCAT_SUCCESS; - - while (fgets(line, sizeof(line), file)) { - line_number++; - - // Remove trailing newline - size_t len = strlen(line); - if (len > 0 && line[len - 1] == '\n') { - line[len - 1] = '\0'; - } - - // Skip comments and empty lines - if (config_is_comment_or_empty(line)) { - continue; - } - - // Parse key=value pairs - static_assert(VALUE_BUF >= PATH_MAX); - static_assert(255 < KEY_BUF); - static_assert(4351 < VALUE_BUF); - if (sscanf(line, " %255[^=]=%4351[^\n]", key, value) == 2) { - // Cut off trailing comment in value - char *comment = strchr(value, '#'); - if (comment) { - *comment = '\0'; // terminate string before '#' - } - - char *trimmed_key = config_trim_str(key); - char *trimmed_value = config_trim_str(value); - - bongocat_error_t parse_result = config_parse_key_value(config, trimmed_key, trimmed_value, overwrite_parameters); - if (parse_result == bongocat_error_t::BONGOCAT_ERROR_INVALID_PARAM) { - BONGOCAT_LOG_WARNING("Unknown configuration key '%s' at line %d", trimmed_key, line_number); - } else if (parse_result != bongocat_error_t::BONGOCAT_SUCCESS) { - result = parse_result; - break; - } - } else if (strlen(line) > 0) { - BONGOCAT_LOG_WARNING("Invalid configuration line %d: %s", line_number, line); - } - } - - return result; + } else { + return bongocat_error_t::BONGOCAT_ERROR_INVALID_PARAM; // Unknown key + } + + return bongocat_error_t::BONGOCAT_SUCCESS; +} + +static bongocat_error_t config_parse_key_value(config_t& config, const char *key, const char *value, + const load_config_overwrite_parameters_t& overwrite_parameters) { + // Try integer keys first + if (config_parse_integer_key(config, key, value) == bongocat_error_t::BONGOCAT_SUCCESS) { + return bongocat_error_t::BONGOCAT_SUCCESS; + } + // Try double keys first + if (config_parse_double_key(config, key, value) == bongocat_error_t::BONGOCAT_SUCCESS) { + return bongocat_error_t::BONGOCAT_SUCCESS; + } + + // Try enum keys + if (config_parse_enum_key(config, key, value) == bongocat_error_t::BONGOCAT_SUCCESS) { + return bongocat_error_t::BONGOCAT_SUCCESS; + } + + // Try string + if (config_parse_string(config, key, value, overwrite_parameters) == bongocat_error_t::BONGOCAT_SUCCESS) { + return bongocat_error_t::BONGOCAT_SUCCESS; + } + + // Handle device keys + if (strcmp(key, KEYBOARD_DEVICE_KEY) == 0 || strcmp(key, KEYBOARD_DEVICES_KEY) == 0) { + return config_add_keyboard_device(config, value); + } + + // Unknown key + return bongocat_error_t::BONGOCAT_ERROR_INVALID_PARAM; +} + +static bool config_is_comment_or_empty(const char *line) { + return (line[0] == '#' || line[0] == '\0' || strspn(line, " \t") == strlen(line)); +} + +static bongocat_error_t config_parse_file(FILE *file, config_t& config, + const load_config_overwrite_parameters_t& overwrite_parameters) { + char line[LINE_BUF] = {0}; + char key[KEY_BUF] = {0}; + char value[VALUE_BUF] = {0}; + int line_number = 0; + bongocat_error_t result = bongocat_error_t::BONGOCAT_SUCCESS; + + while (fgets(line, sizeof(line), file)) { + line_number++; + + // Remove trailing newline + size_t len = strlen(line); + if (len > 0 && line[len - 1] == '\n') { + line[len - 1] = '\0'; } - static bongocat_error_t config_parse_file(config_t& config, const char *config_file_path, load_config_overwrite_parameters_t overwrite_parameters) { - const char *file_path = config_file_path ? config_file_path : DEFAULT_CONFIG_FILE_PATH; - - FILE *file = fopen(file_path, "r"); - if (!file) { - if (overwrite_parameters.strict >= 0) { - BONGOCAT_LOG_INFO("Config file '%s' not found", file_path); - return bongocat_error_t::BONGOCAT_ERROR_FILE_IO; - } - BONGOCAT_LOG_INFO("Config file '%s' not found, using defaults", file_path); - return bongocat_error_t::BONGOCAT_SUCCESS; - } - - bongocat_error_t result = config_parse_file(file, config, overwrite_parameters); - - fclose(file); - - if (result == bongocat_error_t::BONGOCAT_SUCCESS) { - BONGOCAT_LOG_INFO("Loaded configuration from %s", file_path); - } + // Skip comments and empty lines + if (config_is_comment_or_empty(line)) { + continue; + } - return result; + // Parse key=value pairs + static_assert(VALUE_BUF >= PATH_MAX); + static_assert(255 < KEY_BUF); + static_assert(4351 < VALUE_BUF); + if (sscanf(line, " %255[^=]=%4351[^\n]", key, value) == 2) { + // Cut off trailing comment in value + char *comment = strchr(value, '#'); + if (comment != BONGOCAT_NULLPTR) { + *comment = '\0'; // terminate string before '#' + } + + const char *trimmed_key = config_trim_str(key); + const char *trimmed_value = config_trim_str(value); + + const bongocat_error_t parse_result = + config_parse_key_value(config, trimmed_key, trimmed_value, overwrite_parameters); + if (parse_result == bongocat_error_t::BONGOCAT_ERROR_INVALID_PARAM) { + BONGOCAT_LOG_WARNING("Unknown configuration key '%s' at line %d", trimmed_key, line_number); + } else if (parse_result != bongocat_error_t::BONGOCAT_SUCCESS) { + result = parse_result; + break; + } + } else if (strlen(line) > 0) { + BONGOCAT_LOG_WARNING("Invalid configuration line %d: %s", line_number, line); } + } - static bongocat_error_t config_parse_stdin(config_t& config, const load_config_overwrite_parameters_t& overwrite_parameters) { - FILE *file = stdin; + return result; +} - bongocat_error_t result = config_parse_file(file, config, overwrite_parameters); - if (result == bongocat_error_t::BONGOCAT_SUCCESS) { - BONGOCAT_LOG_INFO("Loaded configuration from stdin"); - } +static bongocat_error_t config_parse_file(config_t& config, const char *config_file_path, + load_config_overwrite_parameters_t overwrite_parameters) { + const char *file_path = config_file_path != BONGOCAT_NULLPTR ? config_file_path : DEFAULT_CONFIG_FILE_PATH; - return result; + FILE *file = fopen(file_path, "r"); + if (file == BONGOCAT_NULLPTR) { + if (overwrite_parameters.strict >= 0) { + BONGOCAT_LOG_INFO("Config file '%s' not found", file_path); + return bongocat_error_t::BONGOCAT_ERROR_FILE_IO; } + BONGOCAT_LOG_INFO("Config file '%s' not found, using defaults", file_path); + return bongocat_error_t::BONGOCAT_SUCCESS; + } - // ============================================================================= - // DEFAULT CONFIGURATION MODULE - // ============================================================================= - - void set_defaults(config_t& config) { - config_t cfg{}; + const bongocat_error_t result = config_parse_file(file, config, overwrite_parameters); + fclose(file); + file = BONGOCAT_NULLPTR; - cfg.output_name = nullptr; - assert(input::MAX_INPUT_DEVICES <= INT_MAX); - for (int i = 0; i < static_cast(input::MAX_INPUT_DEVICES); i++) { - cfg.keyboard_devices[i] = nullptr; - } - cfg.num_keyboard_devices = 0; - cfg.cat_x_offset = DEFAULT_CAT_X_OFFSET; - cfg.cat_y_offset = DEFAULT_CAT_Y_OFFSET; - cfg.cat_height = DEFAULT_CAT_HEIGHT; - cfg.overlay_height = DEFAULT_OVERLAY_HEIGHT; - cfg.idle_frame = DEFAULT_IDLE_FRAME; - cfg.keypress_duration_ms = DEFAULT_KEYPRESS_DURATION_MS; - cfg.test_animation_duration_ms = DEFAULT_TEST_ANIMATION_DURATION_MS; - cfg.test_animation_interval_sec = DEFAULT_TEST_ANIMATION_INTERVAL_SEC; - cfg.fps = DEFAULT_FPS; - cfg.overlay_opacity = DEFAULT_OVERLAY_OPACITY; - cfg.mirror_x = 0; - cfg.mirror_y = 0; - cfg.enable_antialiasing = DEFAULT_ENABLE_ANTIALIASING; - cfg.enable_debug = DEFAULT_ENABLE_DEBUG; - cfg.layer = DEFAULT_LAYER; - cfg.overlay_position = DEFAULT_OVERLAY_POSITION; - cfg.animation_index = DEFAULT_ANIMATION_INDEX; - cfg.invert_color = 0; - cfg.padding_x = 0; - cfg.padding_y = 0; - cfg.enable_scheduled_sleep = 0; - cfg.sleep_begin = {}; - cfg.sleep_end = {}; - cfg.idle_sleep_timeout_sec = DEFAULT_IDLE_SLEEP_TIMEOUT_SEC; - cfg.happy_kpm = DEFAULT_HAPPY_KPM; - cfg.cat_align = DEFAULT_CAT_ALIGN; - cfg.animation_sprite_sheet_layout = config_animation_sprite_sheet_layout_t::Bongocat; - cfg.animation_dm_set = config_animation_dm_set_t::None; - cfg.animation_custom_set = config_animation_custom_set_t::None; - cfg.idle_animation = 0; - cfg.input_fps = 0; // when 0 fallback to fps - cfg.randomize_index = 0; - cfg.randomize_on_reload = 0; - cfg.movement_wait_factor = DEFAULT_MOVEMENT_WAIT_FACTOR; - cfg.screen_width = 0; - cfg.custom_sprite_sheet_filename = nullptr; - cfg.custom_sprite_sheet_settings = {}; - cfg._keep_old_animation_index = false; - cfg._strict = false; - cfg._custom = false; - cfg._animation_name = nullptr; - cfg._loaded_animation_fqname = nullptr; - - config = bongocat::move(cfg); - } - - static bongocat_error_t config_set_default_devices(config_t& config) { - if (config.num_keyboard_devices == 0) { - return config_add_keyboard_device(config, DEFAULT_DEVICE); - } + if (result == bongocat_error_t::BONGOCAT_SUCCESS) { + BONGOCAT_LOG_INFO("Loaded configuration from %s", file_path); + } - return bongocat_error_t::BONGOCAT_SUCCESS; - } - - static void config_log_summary(const config_t& config) { - using namespace assets; - BONGOCAT_LOG_DEBUG("Configuration loaded successfully"); - BONGOCAT_LOG_DEBUG(" Overlay Height: %dpx", config.overlay_height); - switch (config.animation_sprite_sheet_layout) { - case config_animation_sprite_sheet_layout_t::None: - break; - case config_animation_sprite_sheet_layout_t::Bongocat: - assert(config._loaded_animation_fqname); - BONGOCAT_LOG_DEBUG(" Cat: '%s' %dx%d at offset (%d,%d)", config._loaded_animation_fqname, - config.cat_height, (config.cat_height * BONGOCAT_FRAME_WIDTH) / BONGOCAT_FRAME_HEIGHT, - config.cat_x_offset, config.cat_y_offset); - break; - case config_animation_sprite_sheet_layout_t::Dm: - assert(config._loaded_animation_fqname); - BONGOCAT_LOG_DEBUG(" dm: '%s' %03d/%03d (set=%d) at offset (%d,%d)", config._loaded_animation_fqname, - config.animation_index, DM_ANIMATIONS_COUNT, config.animation_dm_set, - config.cat_x_offset, config.cat_y_offset); - break; - case config_animation_sprite_sheet_layout_t::Pkmn: - assert(config._loaded_animation_fqname); - assert(PKMN_ANIMATIONS_COUNT <= INT32_MAX); - BONGOCAT_LOG_DEBUG(" pkmn: '%s' %03d/%03d at offset (%d,%d)", config._loaded_animation_fqname, - config.animation_index, PKMN_ANIMATIONS_COUNT, - config.cat_x_offset, config.cat_y_offset); - break; - case config_animation_sprite_sheet_layout_t::MsAgent: - assert(config._loaded_animation_fqname); - assert(MS_AGENTS_ANIMATIONS_COUNT <= INT32_MAX); - BONGOCAT_LOG_DEBUG(" MS Agent: '%s' %02d/%02d at offset (%d,%d)", config._loaded_animation_fqname, - config.animation_index, MS_AGENTS_ANIMATIONS_COUNT, - config.cat_x_offset, config.cat_y_offset); - break; - case config_animation_sprite_sheet_layout_t::Custom: - switch (config.animation_custom_set) { - case config_animation_custom_set_t::None: - break; - case config_animation_custom_set_t::misc: - assert(config._loaded_animation_fqname); - assert(MISC_ANIM_COUNT <= INT32_MAX); - BONGOCAT_LOG_DEBUG(" Misc: '%s' %03d/%03d at offset (%d,%d)", config._loaded_animation_fqname, - config.animation_index, MISC_ANIMATIONS_COUNT, - config.cat_x_offset, config.cat_y_offset); - break; - case config_animation_custom_set_t::pmd: - assert(config._loaded_animation_fqname); - assert(PMD_ANIM_COUNT <= INT32_MAX); - BONGOCAT_LOG_DEBUG(" pkmn pmd: '%s' %04d/%04d at offset (%d,%d)", config._loaded_animation_fqname, - config.animation_index, PMD_ANIMATIONS_COUNT, - config.cat_x_offset, config.cat_y_offset); - break; - case config_animation_custom_set_t::custom: - assert(config.custom_sprite_sheet_filename); - assert(config._custom); - BONGOCAT_LOG_DEBUG(" Custom: %s at offset (%d,%d)", config.custom_sprite_sheet_filename, - config.cat_x_offset, config.cat_y_offset); - break; - } - break; - } - BONGOCAT_LOG_DEBUG(" FPS: %d, Opacity: %d, Random: %d", config.fps, config.overlay_opacity, config.randomize_index); - BONGOCAT_LOG_DEBUG(" Mirror: X=%d, Y=%d", config.mirror_x, config.mirror_y); - BONGOCAT_LOG_DEBUG(" Anti-aliasing: %s", config.enable_antialiasing ? "enabled" : "disabled"); - BONGOCAT_LOG_DEBUG(" Position: %s", config.overlay_position == overlay_position_t::POSITION_TOP ? "top" : "bottom"); - BONGOCAT_LOG_DEBUG(" Alignment: %d", config.cat_align, config.cat_align == align_type_t::ALIGN_CENTER ? "(center)" : ""); - BONGOCAT_LOG_DEBUG(" Layer: %s", config.layer == layer_type_t::LAYER_TOP ? "top" : "overlay"); - BONGOCAT_LOG_DEBUG(" Output Screen: %s", config.output_name); - } - - // ============================================================================= - // PUBLIC API IMPLEMENTATION - // ============================================================================= - - created_result_t load(const char *config_file_path, load_config_overwrite_parameters_t overwrite_parameters) { - BONGOCAT_CHECK_NULL(config_file_path, bongocat_error_t::BONGOCAT_ERROR_INVALID_PARAM); - - config_t ret; - set_defaults(ret); - - // Parse config file and override defaults - bongocat_error_t result = bongocat_error_t::BONGOCAT_ERROR_CONFIG; - if (strcmp(config_file_path, "-") == 0) { - result = config_parse_stdin(ret, overwrite_parameters); - } else { - result = config_parse_file(ret, config_file_path, overwrite_parameters); - } - if (result != bongocat_error_t::BONGOCAT_SUCCESS) [[unlikely]] { - BONGOCAT_LOG_ERROR("Failed to parse configuration file: %s", bongocat::error_string(result)); - return result; - } + return result; +} - if (overwrite_parameters.output_name) { - if (ret.output_name) ::free(ret.output_name); - ret.output_name = strdup(overwrite_parameters.output_name); - } - if (overwrite_parameters.randomize_index >= 0) { - ret.randomize_index = overwrite_parameters.randomize_index ? 1 : 0; - } - if (overwrite_parameters.strict >= 0) { - ret._strict = overwrite_parameters.strict >= 1; - } +static bongocat_error_t config_parse_stdin(config_t& config, + const load_config_overwrite_parameters_t& overwrite_parameters) { + FILE *file = stdin; - if (ret.input_fps <= 0) { - ret.input_fps = ret.fps; - } + const bongocat_error_t result = config_parse_file(file, config, overwrite_parameters); + if (result == bongocat_error_t::BONGOCAT_SUCCESS) { + BONGOCAT_LOG_INFO("Loaded configuration from stdin"); + } - // Set default keyboard device if none specified - if (ret.num_keyboard_devices == 0) { - if (!ret._strict) { - result = config_set_default_devices(ret); - if (result != bongocat_error_t::BONGOCAT_SUCCESS) { - BONGOCAT_LOG_ERROR("Failed to set default keyboard devices: %s", bongocat::error_string(result)); - return result; - } - } - } + return result; +} - // Validate and sanitize configuration - result = config_validate(ret); - if (result != bongocat_error_t::BONGOCAT_SUCCESS) { - BONGOCAT_LOG_ERROR("Configuration validation failed: %s", bongocat::error_string(result)); - return result; - } +// ============================================================================= +// DEFAULT CONFIGURATION MODULE +// ============================================================================= - if (ret.num_keyboard_devices == 0) { - if (!ret._strict) { - // Set default keyboard device if none specified - result = config_set_default_devices(ret); - if (result != bongocat_error_t::BONGOCAT_SUCCESS) { - BONGOCAT_LOG_ERROR("Failed to set default keyboard devices: %s", bongocat::error_string(result)); - return result; - } else { - BONGOCAT_LOG_INFO("No device loaded, use default keyboard device: %s", DEFAULT_DEVICE); - } - } else { - BONGOCAT_LOG_INFO("No device loaded"); - } - } +void set_defaults(config_t& config) { + config_t cfg{}; + + cfg.output_name = BONGOCAT_NULLPTR; + assert(input::MAX_INPUT_DEVICES <= INT_MAX); + for (int i = 0; i < static_cast(input::MAX_INPUT_DEVICES); i++) { + cfg.keyboard_devices[i] = BONGOCAT_NULLPTR; + } + cfg.num_keyboard_devices = 0; + cfg.cat_x_offset = DEFAULT_CAT_X_OFFSET; + cfg.cat_y_offset = DEFAULT_CAT_Y_OFFSET; + cfg.cat_height = DEFAULT_CAT_HEIGHT; + cfg.overlay_height = DEFAULT_OVERLAY_HEIGHT; + cfg.idle_frame = DEFAULT_IDLE_FRAME; + cfg.keypress_duration_ms = DEFAULT_KEYPRESS_DURATION_MS; + cfg.test_animation_duration_ms = DEFAULT_TEST_ANIMATION_DURATION_MS; + cfg.test_animation_interval_sec = DEFAULT_TEST_ANIMATION_INTERVAL_SEC; + cfg.fps = DEFAULT_FPS; + cfg.overlay_opacity = DEFAULT_OVERLAY_OPACITY; + cfg.mirror_x = 0; + cfg.mirror_y = 0; + cfg.enable_antialiasing = DEFAULT_ENABLE_ANTIALIASING; + cfg.enable_debug = DEFAULT_ENABLE_DEBUG; + cfg.enable_hand_mapping = DEFAULT_ENABLE_HAND_MAPPING; + cfg.layer = DEFAULT_LAYER; + cfg.overlay_position = DEFAULT_OVERLAY_POSITION; + cfg.animation_index = DEFAULT_ANIMATION_INDEX; + cfg.invert_color = 0; + cfg.padding_x = 0; + cfg.padding_y = 0; + cfg.enable_scheduled_sleep = 0; + cfg.sleep_begin = {}; + cfg.sleep_end = {}; + cfg.idle_sleep_timeout_sec = DEFAULT_IDLE_SLEEP_TIMEOUT_SEC; + cfg.happy_kpm = DEFAULT_HAPPY_KPM; + cfg.cat_align = DEFAULT_CAT_ALIGN; + cfg.animation_sprite_sheet_layout = config_animation_sprite_sheet_layout_t::Bongocat; + cfg.animation_dm_set = config_animation_dm_set_t::None; + cfg.animation_custom_set = config_animation_custom_set_t::None; + cfg.idle_animation = 0; + cfg.input_fps = 0; // when 0 fallback to fps + cfg.randomize_index = 0; + cfg.randomize_on_reload = 0; + cfg.movement_wait_factor = DEFAULT_MOVEMENT_WAIT_FACTOR; + cfg.screen_width = 0; + cfg.custom_sprite_sheet_filename = BONGOCAT_NULLPTR; + cfg.custom_sprite_sheet_settings = {}; + cfg._keep_old_animation_index = false; + cfg._strict = false; + cfg._custom = false; + cfg._animation_name = BONGOCAT_NULLPTR; + cfg._loaded_animation_fqname = BONGOCAT_NULLPTR; + + config = bongocat::move(cfg); +} + +static bongocat_error_t config_set_default_devices(config_t& config) { + if (config.num_keyboard_devices == 0) { + return config_add_keyboard_device(config, DEFAULT_DEVICE); + } + + return bongocat_error_t::BONGOCAT_SUCCESS; +} + +static void config_log_summary(const config_t& config) { + using namespace assets; + BONGOCAT_LOG_DEBUG("Configuration loaded successfully"); + BONGOCAT_LOG_DEBUG(" Overlay Height: %dpx", config.overlay_height); + switch (config.animation_sprite_sheet_layout) { + case config_animation_sprite_sheet_layout_t::None: + break; + case config_animation_sprite_sheet_layout_t::Bongocat: + // when loaded by default, _loaded_animation_fqname is not set + // assert(config._loaded_animation_fqname); + BONGOCAT_LOG_DEBUG(" Cat: '%s' %dx%d at offset (%d,%d)", + config._loaded_animation_fqname != nullptr ? config._loaded_animation_fqname : BONGOCAT_FQNAME, + config.cat_height, (config.cat_height * BONGOCAT_FRAME_WIDTH) / BONGOCAT_FRAME_HEIGHT, + config.cat_x_offset, config.cat_y_offset); + break; + case config_animation_sprite_sheet_layout_t::Dm: + assert(config._loaded_animation_fqname); + BONGOCAT_LOG_DEBUG(" dm: '%s' %03d/%03d (set=%d) at offset (%d,%d)", config._loaded_animation_fqname, + config.animation_index, DM_ANIMATIONS_COUNT, config.animation_dm_set, config.cat_x_offset, + config.cat_y_offset); + break; + case config_animation_sprite_sheet_layout_t::Pkmn: + assert(config._loaded_animation_fqname); + assert(PKMN_ANIMATIONS_COUNT <= INT32_MAX); + BONGOCAT_LOG_DEBUG(" pkmn: '%s' %03d/%03d at offset (%d,%d)", config._loaded_animation_fqname, + config.animation_index, PKMN_ANIMATIONS_COUNT, config.cat_x_offset, config.cat_y_offset); + break; + case config_animation_sprite_sheet_layout_t::MsAgent: + assert(config._loaded_animation_fqname); + assert(MS_AGENTS_ANIMATIONS_COUNT <= INT32_MAX); + BONGOCAT_LOG_DEBUG(" MS Agent: '%s' %02d/%02d at offset (%d,%d)", config._loaded_animation_fqname, + config.animation_index, MS_AGENTS_ANIMATIONS_COUNT, config.cat_x_offset, config.cat_y_offset); + break; + case config_animation_sprite_sheet_layout_t::Custom: + switch (config.animation_custom_set) { + case config_animation_custom_set_t::None: + break; + case config_animation_custom_set_t::misc: + assert(config._loaded_animation_fqname); + assert(MISC_ANIM_COUNT <= INT32_MAX); + BONGOCAT_LOG_DEBUG(" Misc: '%s' %03d/%03d at offset (%d,%d)", config._loaded_animation_fqname, + config.animation_index, MISC_ANIMATIONS_COUNT, config.cat_x_offset, config.cat_y_offset); + break; + case config_animation_custom_set_t::pmd: + assert(config._loaded_animation_fqname); + assert(PMD_ANIM_COUNT <= INT32_MAX); + BONGOCAT_LOG_DEBUG(" pkmn pmd: '%s' %04d/%04d at offset (%d,%d)", config._loaded_animation_fqname, + config.animation_index, PMD_ANIMATIONS_COUNT, config.cat_x_offset, config.cat_y_offset); + break; + case config_animation_custom_set_t::custom: + assert(config.custom_sprite_sheet_filename); + assert(config._custom); + BONGOCAT_LOG_DEBUG(" Custom: %s at offset (%d,%d)", config.custom_sprite_sheet_filename, config.cat_x_offset, + config.cat_y_offset); + break; + } + break; + } + BONGOCAT_LOG_DEBUG(" FPS: %d, Opacity: %d, Random: %d", config.fps, config.overlay_opacity, config.randomize_index); + BONGOCAT_LOG_DEBUG(" Mirror: X=%d, Y=%d", config.mirror_x, config.mirror_y); + BONGOCAT_LOG_DEBUG(" Anti-aliasing: %s", config.enable_antialiasing ? "enabled" : "disabled"); + BONGOCAT_LOG_DEBUG(" Position: %s", config.overlay_position == overlay_position_t::POSITION_TOP ? "top" : "bottom"); + BONGOCAT_LOG_DEBUG(" Alignment: %d", config.cat_align, + config.cat_align == align_type_t::ALIGN_CENTER ? "(center)" : ""); + BONGOCAT_LOG_DEBUG(" Layer: %s", config.layer == layer_type_t::LAYER_TOP ? "top" : "overlay"); + BONGOCAT_LOG_DEBUG(" Output Screen: %s", config.output_name); +} - // Log configuration summary - config_log_summary(ret); +// ============================================================================= +// PUBLIC API IMPLEMENTATION +// ============================================================================= - return ret; +created_result_t load(const char *config_file_path, load_config_overwrite_parameters_t overwrite_parameters) { + BONGOCAT_CHECK_NULL(config_file_path, bongocat_error_t::BONGOCAT_ERROR_INVALID_PARAM); + + config_t ret; + set_defaults(ret); + + // Parse config file and override defaults + bongocat_error_t result = bongocat_error_t::BONGOCAT_ERROR_CONFIG; + if (strcmp(config_file_path, "-") == 0) { + result = config_parse_stdin(ret, overwrite_parameters); + } else { + result = config_parse_file(ret, config_file_path, overwrite_parameters); + } + if (result != bongocat_error_t::BONGOCAT_SUCCESS) [[unlikely]] { + BONGOCAT_LOG_ERROR("Failed to parse configuration file: %s", bongocat::error_string(result)); + return result; + } + + if (overwrite_parameters.output_name != BONGOCAT_NULLPTR) { + if (ret.output_name != BONGOCAT_NULLPTR) { + ::free(ret.output_name); + ret.output_name = BONGOCAT_NULLPTR; } - - void reset(config_t& config) { - config_cleanup_devices(config); - set_defaults(config); + ret.output_name = strdup(overwrite_parameters.output_name); + } + if (overwrite_parameters.randomize_index >= 0) { + ret.randomize_index = overwrite_parameters.randomize_index >= 1 ? 1 : 0; + } + if (overwrite_parameters.strict >= 0) { + ret._strict = overwrite_parameters.strict >= 1; + } + + if (ret.input_fps <= 0) { + ret.input_fps = ret.fps; + } + + // Set default keyboard device if none specified + if (ret.num_keyboard_devices == 0) { + if (!ret._strict) { + result = config_set_default_devices(ret); + if (result != bongocat_error_t::BONGOCAT_SUCCESS) { + BONGOCAT_LOG_ERROR("Failed to set default keyboard devices: %s", bongocat::error_string(result)); + return result; + } } -} \ No newline at end of file + } + + // Validate and sanitize configuration + result = config_validate(ret); + if (result != bongocat_error_t::BONGOCAT_SUCCESS) { + BONGOCAT_LOG_ERROR("Configuration validation failed: %s", bongocat::error_string(result)); + return result; + } + + if (ret.num_keyboard_devices == 0) { + if (!ret._strict) { + // Set default keyboard device if none specified + result = config_set_default_devices(ret); + if (result != bongocat_error_t::BONGOCAT_SUCCESS) { + BONGOCAT_LOG_ERROR("Failed to set default keyboard devices: %s", bongocat::error_string(result)); + return result; + } else { + BONGOCAT_LOG_INFO("No device loaded, use default keyboard device: %s", DEFAULT_DEVICE); + } + } else { + BONGOCAT_LOG_INFO("No device loaded"); + } + } + + // Log configuration summary + config_log_summary(ret); + + return ret; +} + +void reset(config_t& config) { + config_cleanup_devices(config); + set_defaults(config); +} +} // namespace bongocat::config \ No newline at end of file diff --git a/src/config/config_watcher.cpp b/src/config/config_watcher.cpp index 501592f5..0df9b50b 100644 --- a/src/config/config_watcher.cpp +++ b/src/config/config_watcher.cpp @@ -1,285 +1,287 @@ #include "config/config_watcher.h" -#include "utils/time.h" + +#include "platform/wayland_thread_context.h" #include "utils/error.h" #include "utils/system_memory.h" -#include -#include +#include "utils/time.h" + #include -#include -#include +#include +#include #include -#include +#include #include -#include - -#include "platform/wayland_context.h" +#include +#include +#include namespace bongocat::config { - static inline constexpr int MAX_ATTEMPTS = 2048; - static inline constexpr int RECREATE_MAX_ATTEMPTS = 10; - static inline constexpr platform::time_ms_t RELOAD_DEBOUNCE_MS = 1000; - static inline constexpr platform::time_ms_t RELOAD_DELAY_MS = 100; - static inline constexpr platform::time_ms_t RECREATE_SLEEP_ATTEMPT_MS = 100; - static inline constexpr platform::time_ms_t TIMEOUT_MS = 100; - - static constexpr uint32_t FILE_MASK = IN_MODIFY | IN_CLOSE_WRITE | IN_ATTRIB | IN_MOVE_SELF | IN_DELETE_SELF; - static constexpr uint32_t DIR_MASK = IN_CREATE | IN_MOVED_TO | IN_DELETE | IN_MOVED_FROM; - - static bongocat_error_t reinitialize_inotify(config_watcher_t& watcher) { - if (watcher.inotify_fd._fd >= 0) { - platform::close_fd(watcher.inotify_fd); - } - - watcher.inotify_fd = platform::FileDescriptor(inotify_init1(IN_NONBLOCK | IN_CLOEXEC)); - if (watcher.inotify_fd._fd < 0) { - BONGOCAT_LOG_ERROR("config_watcher: Failed to reinit inotify: %s", strerror(errno)); - return bongocat_error_t::BONGOCAT_ERROR_FILE_IO; - } - - watcher.wd_dir = platform::FileDescriptor(inotify_add_watch(watcher.inotify_fd._fd, dirname(watcher.config_path), DIR_MASK)); - watcher.wd_file = platform::FileDescriptor(inotify_add_watch(watcher.inotify_fd._fd, watcher.config_path, FILE_MASK)); - - if (watcher.wd_dir._fd < 0 || watcher.wd_file._fd < 0) { - BONGOCAT_LOG_WARNING("config_watcher: partial reinit of inotify watches"); - } +static inline constexpr int MAX_ATTEMPTS = 2048; +static inline constexpr int RECREATE_MAX_ATTEMPTS = 10; +static inline constexpr platform::time_ms_t RELOAD_DEBOUNCE_MS = 1000; +static inline constexpr platform::time_ms_t RELOAD_DELAY_MS = 100; +static inline constexpr platform::time_ms_t RECREATE_SLEEP_ATTEMPT_MS = 100; +static inline constexpr platform::time_ms_t TIMEOUT_MS = 100; + +static constexpr uint32_t FILE_MASK = IN_MODIFY | IN_CLOSE_WRITE | IN_ATTRIB | IN_MOVE_SELF | IN_DELETE_SELF; +static constexpr uint32_t DIR_MASK = IN_CREATE | IN_MOVED_TO | IN_DELETE | IN_MOVED_FROM; + +static bongocat_error_t reinitialize_inotify(config_watcher_t& watcher) { + if (watcher.inotify_fd._fd >= 0) { + platform::close_fd(watcher.inotify_fd); + } + + watcher.inotify_fd = platform::FileDescriptor(inotify_init1(IN_NONBLOCK | IN_CLOEXEC)); + if (watcher.inotify_fd._fd < 0) { + BONGOCAT_LOG_ERROR("config_watcher: Failed to reinit inotify: %s", strerror(errno)); + return bongocat_error_t::BONGOCAT_ERROR_FILE_IO; + } + + watcher.wd_dir = + platform::FileDescriptor(inotify_add_watch(watcher.inotify_fd._fd, dirname(watcher.config_path), DIR_MASK)); + watcher.wd_file = platform::FileDescriptor(inotify_add_watch(watcher.inotify_fd._fd, watcher.config_path, FILE_MASK)); + + if (watcher.wd_dir._fd < 0 || watcher.wd_file._fd < 0) { + BONGOCAT_LOG_WARNING("config_watcher: partial reinit of inotify watches"); + } + + return bongocat_error_t::BONGOCAT_SUCCESS; +} - return bongocat_error_t::BONGOCAT_SUCCESS; +static void *config_watcher_thread(void *arg) { + assert(arg); + auto& watcher = *static_cast(arg); + + char buffer[INOTIFY_BUF_LEN] = {}; + platform::timestamp_ms_t last_reload_timestamp = platform::get_current_time_ms(); + + constexpr size_t fds_inotify_index = 0; + constexpr nfds_t fds_count = 1; + pollfd fds[fds_count] = { + {.fd = watcher.inotify_fd._fd, .events = POLLIN, .revents = 0}, + }; + constexpr int timeout_ms = TIMEOUT_MS; + + BONGOCAT_LOG_INFO("config_watcher: Config watcher started for: %s", watcher.config_path); + + atomic_store(&watcher._running, true); + while (atomic_load(&watcher._running)) { + int ret = poll(fds, fds_count, timeout_ms); + if (ret < 0) { + if (errno == EINTR) { + continue; + } + BONGOCAT_LOG_ERROR("config_watcher: Config watcher poll failed: %s", strerror(errno)); + break; } - - static void *config_watcher_thread(void *arg) { - assert(arg); - auto& watcher = *static_cast(arg); - - char buffer[INOTIFY_BUF_LEN] = {}; - platform::timestamp_ms_t last_reload_timestamp = platform::get_current_time_ms(); - - constexpr size_t fds_inotify_index = 0; - constexpr nfds_t fds_count = 1; - pollfd fds[fds_count] = { - { .fd = watcher.inotify_fd._fd, .events = POLLIN, .revents = 0 }, - }; - constexpr int timeout_ms = TIMEOUT_MS; - - BONGOCAT_LOG_INFO("config_watcher: Config watcher started for: %s", watcher.config_path); - - atomic_store(&watcher._running, true); - while (atomic_load(&watcher._running)) { - int ret = poll(fds, fds_count, timeout_ms); - if (ret < 0) { - if (errno == EINTR) continue; - BONGOCAT_LOG_ERROR("config_watcher: Config watcher poll failed: %s", strerror(errno)); - break; - } - if (!atomic_load(&watcher._running)) { - // draining pools - for (size_t i = 0; i < fds_count; i++) { - if (fds[i].revents & POLLIN) { - int attempts = 0; - uint64_t u; - while (read(fds[i].fd, &u, sizeof(uint64_t)) == sizeof(uint64_t) && attempts < MAX_ATTEMPTS) { - attempts++; - } - } - } - break; - } - - if (fds[fds_inotify_index].revents & POLLIN) { - const ssize_t length = read(watcher.inotify_fd._fd, buffer, config::INOTIFY_BUF_LEN); - if (length <= 0) { - if (errno == EINVAL || errno == EBADF) { - BONGOCAT_LOG_WARNING("config_watcher: inotify fd invalidated (likely after suspend). Reinitializing..."); - reinitialize_inotify(watcher); - continue; - } - BONGOCAT_LOG_ERROR("config_watcher: Config watcher read failed: %s", strerror(errno)); - continue; - } - - bool should_reload = false; - bool file_went_away = false; - bool file_recreated = false; - - ssize_t i = 0; - int attempts = 0; - while (i < length && attempts < MAX_ATTEMPTS) { - const auto *event = reinterpret_cast(&buffer[i]); - assert(event); - - BONGOCAT_LOG_VERBOSE("config_watcher: inotify event: wd=%d mask=0x%08X name=%s", - event->wd, event->mask, - event->len ? event->name : "(none)"); - - // File events - if (event->wd == watcher.wd_file._fd) { - should_reload |= event->mask & (IN_MODIFY | IN_CLOSE_WRITE | IN_ATTRIB); - file_went_away |= event->mask & (IN_MOVE_SELF | IN_DELETE_SELF); - } - // Directory events (watch for recreate) - else if (event->wd == watcher.wd_dir._fd) { - if ((event->mask & (IN_CREATE | IN_MOVED_TO)) && event->len > 0) { - file_recreated |= strcmp(event->name, basename(watcher.config_path)) == 0; - } - } - // Inotify queue overflow (critical) - else if (event->mask & IN_Q_OVERFLOW) { - BONGOCAT_LOG_WARNING("config_watcher: inotify event queue overflow, forcing full reload"); - should_reload = true; - } - - - assert(config::INOTIFY_EVENT_SIZE <= SSIZE_MAX); - i += static_cast(config::INOTIFY_EVENT_SIZE) + event->len; - attempts++; - } - - // Handle file disappearance - if (file_went_away && watcher.wd_file._fd >= 0) { - BONGOCAT_LOG_VERBOSE("config_watcher: Config file went away; removing file watch"); - inotify_rm_watch(watcher.inotify_fd._fd, watcher.wd_file._fd); - watcher.wd_file._fd = -1; - // the file is gone, wait for recreation - should_reload = false; - } - - // Handle recreation - if (file_recreated) { - BONGOCAT_LOG_VERBOSE("config_watcher: Config file recreated; re-adding file watch"); - int new_wd = inotify_add_watch(watcher.inotify_fd._fd, watcher.config_path, FILE_MASK); - if (new_wd >= 0) { - watcher.wd_file = platform::FileDescriptor(new_wd); - new_wd = -1; - should_reload = true; - } else { - BONGOCAT_LOG_ERROR("config_watcher: Failed to re-add file watch: %s", strerror(errno)); - should_reload = false; - } - } - - // ensure file exists (from IN_CLOSE_WRITE/IN_MODIFY) - if (should_reload) { - // If we don't currently have a file watch (it was removed), try stat the file - bool file_exists = false; - if (watcher.wd_file._fd >= 0) { - file_exists = true; - } else { - // small retry loop to handle race where create races with our handling - struct stat st{}; - for (int attempt = 0; attempt < RECREATE_MAX_ATTEMPTS; attempt++) { - if (stat(watcher.config_path, &st) == 0) { - file_exists = true; - break; - } - // small sleep before next stat - usleep(RECREATE_SLEEP_ATTEMPT_MS*1000); - } - } - - if (!file_exists) { - BONGOCAT_LOG_VERBOSE("config_watcher: Reload skipped: config file not present yet (will wait for recreate)"); - should_reload = false; - } - } - - // Debounce reloads - if (should_reload) { - // Debounce: only reload if at least some time have passed since last reload - const platform::timestamp_ms_t now = platform::get_current_time_ms(); - if (now - last_reload_timestamp >= RELOAD_DEBOUNCE_MS) { - // Small delay to ensure file write is complete - usleep(RELOAD_DELAY_MS*1000); - last_reload_timestamp = now; - - BONGOCAT_LOG_INFO("config_watcher: Config file changed, trigger reload"); - uint64_t u = 1; - if (write(watcher.reload_efd._fd, &u, sizeof(uint64_t)) >= 0) { - BONGOCAT_LOG_DEBUG("config_watcher: Write reload event in watcher"); - } else { - BONGOCAT_LOG_ERROR("config_watcher: Failed to write to notify pipe in watcher: %s", strerror(errno)); - } - } - } - } + if (!atomic_load(&watcher._running)) { + // draining pools + for (size_t i = 0; i < fds_count; i++) { + if (fds[i].revents & POLLIN) { + int attempts = 0; + uint64_t u; + while (read(fds[i].fd, &u, sizeof(uint64_t)) == sizeof(uint64_t) && attempts < MAX_ATTEMPTS) { + attempts++; + } } - atomic_store(&watcher._running, false); - - BONGOCAT_LOG_INFO("config_watcher: Config watcher stopped"); - return nullptr; + } + break; } - created_result_t> create_watcher(const char *config_path) { - BONGOCAT_CHECK_NULL(config_path, bongocat_error_t::BONGOCAT_ERROR_INVALID_PARAM); - AllocatedMemory ret = make_allocated_memory(); - assert(ret != nullptr); - if (ret == nullptr) [[unlikely]] { - return bongocat_error_t::BONGOCAT_ERROR_MEMORY; + if (fds[fds_inotify_index].revents & POLLIN) { + const ssize_t length = read(watcher.inotify_fd._fd, buffer, config::INOTIFY_BUF_LEN); + if (length <= 0) { + if (errno == EINVAL || errno == EBADF) { + BONGOCAT_LOG_WARNING("config_watcher: inotify fd invalidated (likely after suspend). Reinitializing..."); + reinitialize_inotify(watcher); + continue; } - - // Store config path - ret->config_path = strdup(config_path); - if (!ret->config_path) [[unlikely]] { - return bongocat_error_t::BONGOCAT_ERROR_MEMORY; + BONGOCAT_LOG_ERROR("config_watcher: Config watcher read failed: %s", strerror(errno)); + continue; + } + + bool should_reload = false; + bool file_went_away = false; + bool file_recreated = false; + + ssize_t i = 0; + int attempts = 0; + while (i < length && attempts < MAX_ATTEMPTS) { + const auto *event = reinterpret_cast(&buffer[i]); + assert(event); + + BONGOCAT_LOG_VERBOSE("config_watcher: inotify event: wd=%d mask=0x%08X name=%s", event->wd, event->mask, + event->len ? event->name : "(none)"); + + // File events + if (event->wd == watcher.wd_file._fd) { + should_reload |= event->mask & (IN_MODIFY | IN_CLOSE_WRITE | IN_ATTRIB); + file_went_away |= event->mask & (IN_MOVE_SELF | IN_DELETE_SELF); } - - // Initialize inotify - ret->inotify_fd = platform::FileDescriptor(inotify_init1(IN_NONBLOCK)); - if (ret->inotify_fd._fd < 0) [[unlikely]] { - BONGOCAT_LOG_ERROR("Failed to initialize inotify: %s", strerror(errno)); - return bongocat_error_t::BONGOCAT_ERROR_FILE_IO; + // Directory events (watch for recreate) + else if (event->wd == watcher.wd_dir._fd) { + if ((event->mask & (IN_CREATE | IN_MOVED_TO)) && event->len > 0) { + file_recreated |= strcmp(event->name, basename(watcher.config_path)) == 0; + } } - - constexpr size_t dirbuf_size = PATH_MAX; - char dirbuf[dirbuf_size] = {0}; - strncpy(dirbuf, config_path, dirbuf_size-1); - dirbuf[dirbuf_size-1] = '\0'; - const char* dir = dirname(dirbuf); - if (!dir) [[unlikely]] { - BONGOCAT_LOG_ERROR("dirname() failed for path %s", ret->config_path); - return bongocat_error_t::BONGOCAT_ERROR_FILE_IO; + // Inotify queue overflow (critical) + else if (event->mask & IN_Q_OVERFLOW) { + BONGOCAT_LOG_WARNING("config_watcher: inotify event queue overflow, forcing full reload"); + should_reload = true; } - ret->wd_file = platform::FileDescriptor(inotify_add_watch(ret->inotify_fd._fd, ret->config_path, FILE_MASK)); - if (ret->wd_file._fd < 0) { - BONGOCAT_LOG_ERROR("Failed to add inotify watch for file %s: %s", ret->config_path, strerror(errno)); - return bongocat_error_t::BONGOCAT_ERROR_FILE_IO; + assert(config::INOTIFY_EVENT_SIZE <= SSIZE_MAX); + i += static_cast(config::INOTIFY_EVENT_SIZE) + event->len; + attempts++; + } + + // Handle file disappearance + if (file_went_away && watcher.wd_file._fd >= 0) { + BONGOCAT_LOG_VERBOSE("config_watcher: Config file went away; removing file watch"); + inotify_rm_watch(watcher.inotify_fd._fd, watcher.wd_file._fd); + watcher.wd_file._fd = -1; + // the file is gone, wait for recreation + should_reload = false; + } + + // Handle recreation + if (file_recreated) { + BONGOCAT_LOG_VERBOSE("config_watcher: Config file recreated; re-adding file watch"); + int new_wd = inotify_add_watch(watcher.inotify_fd._fd, watcher.config_path, FILE_MASK); + if (new_wd >= 0) { + watcher.wd_file = platform::FileDescriptor(new_wd); + new_wd = -1; + should_reload = true; + } else { + BONGOCAT_LOG_ERROR("config_watcher: Failed to re-add file watch: %s", strerror(errno)); + should_reload = false; } - - ret->wd_dir = platform::FileDescriptor(inotify_add_watch(ret->inotify_fd._fd, dir, DIR_MASK)); - if (ret->wd_dir._fd < 0) { - BONGOCAT_LOG_ERROR("Failed to add inotify watch for dir %s: %s", dir, strerror(errno)); - return bongocat_error_t::BONGOCAT_ERROR_FILE_IO; + } + + // ensure file exists (from IN_CLOSE_WRITE/IN_MODIFY) + if (should_reload) { + // If we don't currently have a file watch (it was removed), try stat the file + bool file_exists = false; + if (watcher.wd_file._fd >= 0) { + file_exists = true; + } else { + // small retry loop to handle race where create races with our handling + struct stat st{}; + for (int attempt = 0; attempt < RECREATE_MAX_ATTEMPTS; attempt++) { + if (stat(watcher.config_path, &st) == 0) { + file_exists = true; + break; + } + // small sleep before next stat + usleep(RECREATE_SLEEP_ATTEMPT_MS * 1000); + } } - ret->reload_efd = platform::FileDescriptor(eventfd(0, EFD_NONBLOCK | EFD_CLOEXEC)); - if (ret->reload_efd._fd < 0) { - BONGOCAT_LOG_ERROR("Failed to create notify pipe for config reload: %s", strerror(errno)); - return bongocat_error_t::BONGOCAT_ERROR_FILE_IO; + if (!file_exists) { + BONGOCAT_LOG_VERBOSE("config_watcher: Reload skipped: config file not present yet (will wait for recreate)"); + should_reload = false; } - - return ret; + } + + // Debounce reloads + if (should_reload) { + // Debounce: only reload if at least some time have passed since last reload + const platform::timestamp_ms_t now = platform::get_current_time_ms(); + if (now - last_reload_timestamp >= RELOAD_DEBOUNCE_MS) { + // Small delay to ensure file write is complete + usleep(RELOAD_DELAY_MS * 1000); + last_reload_timestamp = now; + + BONGOCAT_LOG_INFO("config_watcher: Config file changed, trigger reload"); + uint64_t u = 1; + if (write(watcher.reload_efd._fd, &u, sizeof(uint64_t)) >= 0) { + BONGOCAT_LOG_DEBUG("config_watcher: Write reload event in watcher"); + } else { + BONGOCAT_LOG_ERROR("config_watcher: Failed to write to notify pipe in watcher: %s", strerror(errno)); + } + } + } } + } + atomic_store(&watcher._running, false); - void start_watcher(config_watcher_t& watcher) { - if (pthread_create(&watcher._watcher_thread, nullptr, config_watcher_thread, &watcher) != 0) { - atomic_store(&watcher._running, false); - BONGOCAT_LOG_ERROR("Failed to create config watcher thread: %s", strerror(errno)); - return; - } + BONGOCAT_LOG_INFO("config_watcher: Config watcher stopped"); + return BONGOCAT_NULLPTR; +} - BONGOCAT_LOG_INFO("Config watcher thread started"); - } +created_result_t> create_watcher(const char *config_path) { + BONGOCAT_CHECK_NULL(config_path, bongocat_error_t::BONGOCAT_ERROR_INVALID_PARAM); + AllocatedMemory ret = make_allocated_memory(); + assert(ret != BONGOCAT_NULLPTR); + if (ret == BONGOCAT_NULLPTR) [[unlikely]] { + return bongocat_error_t::BONGOCAT_ERROR_MEMORY; + } + + // Store config path + ret->config_path = strdup(config_path); + if (!ret->config_path) [[unlikely]] { + return bongocat_error_t::BONGOCAT_ERROR_MEMORY; + } + + // Initialize inotify + ret->inotify_fd = platform::FileDescriptor(inotify_init1(IN_NONBLOCK)); + if (ret->inotify_fd._fd < 0) [[unlikely]] { + BONGOCAT_LOG_ERROR("Failed to initialize inotify: %s", strerror(errno)); + return bongocat_error_t::BONGOCAT_ERROR_FILE_IO; + } + + constexpr size_t dirbuf_size = PATH_MAX; + char dirbuf[dirbuf_size] = {0}; + strncpy(dirbuf, config_path, dirbuf_size - 1); + dirbuf[dirbuf_size - 1] = '\0'; + const char *dir = dirname(dirbuf); + if (!dir) [[unlikely]] { + BONGOCAT_LOG_ERROR("dirname() failed for path %s", ret->config_path); + return bongocat_error_t::BONGOCAT_ERROR_FILE_IO; + } + + ret->wd_file = platform::FileDescriptor(inotify_add_watch(ret->inotify_fd._fd, ret->config_path, FILE_MASK)); + if (ret->wd_file._fd < 0) { + BONGOCAT_LOG_ERROR("Failed to add inotify watch for file %s: %s", ret->config_path, strerror(errno)); + return bongocat_error_t::BONGOCAT_ERROR_FILE_IO; + } + + ret->wd_dir = platform::FileDescriptor(inotify_add_watch(ret->inotify_fd._fd, dir, DIR_MASK)); + if (ret->wd_dir._fd < 0) { + BONGOCAT_LOG_ERROR("Failed to add inotify watch for dir %s: %s", dir, strerror(errno)); + return bongocat_error_t::BONGOCAT_ERROR_FILE_IO; + } + + ret->reload_efd = platform::FileDescriptor(eventfd(0, EFD_NONBLOCK | EFD_CLOEXEC)); + if (ret->reload_efd._fd < 0) { + BONGOCAT_LOG_ERROR("Failed to create notify pipe for config reload: %s", strerror(errno)); + return bongocat_error_t::BONGOCAT_ERROR_FILE_IO; + } + + return ret; +} - void stop_watcher(config_watcher_t& watcher) { - atomic_store(&watcher._running, false); - if (watcher._watcher_thread) { - BONGOCAT_LOG_DEBUG("Stopping config watcher thread"); - //pthread_cancel(watcher->_watcher_thread); - // Wait for thread to finish - if (platform::stop_thread_graceful_or_cancel(watcher._watcher_thread, watcher._running) != 0) { - BONGOCAT_LOG_ERROR("Failed to join config watcher thread: %s", strerror(errno)); - } - BONGOCAT_LOG_DEBUG("config watcher thread terminated"); - } - watcher._watcher_thread = 0; +void start_watcher(config_watcher_t& watcher) { + if (pthread_create(&watcher._watcher_thread, BONGOCAT_NULLPTR, config_watcher_thread, &watcher) != 0) { + atomic_store(&watcher._running, false); + BONGOCAT_LOG_ERROR("Failed to create config watcher thread: %s", strerror(errno)); + return; + } + + BONGOCAT_LOG_INFO("Config watcher thread started"); +} + +void stop_watcher(config_watcher_t& watcher) { + atomic_store(&watcher._running, false); + if (watcher._watcher_thread) { + BONGOCAT_LOG_DEBUG("Stopping config watcher thread"); + // pthread_cancel(watcher->_watcher_thread); + // Wait for thread to finish + if (platform::stop_thread_graceful_or_cancel(watcher._watcher_thread, watcher._running) != 0) { + BONGOCAT_LOG_ERROR("Failed to join config watcher thread: %s", strerror(errno)); } + BONGOCAT_LOG_DEBUG("config watcher thread terminated"); + } + watcher._watcher_thread = 0; } +} // namespace bongocat::config diff --git a/src/core/main.cpp b/src/core/main.cpp index e978b6a4..d96e048c 100644 --- a/src/core/main.cpp +++ b/src/core/main.cpp @@ -1,977 +1,1112 @@ +#include "config/config.h" #include "core/bongocat.h" -#include "platform/wayland.h" #include "graphics/animation.h" +#include "image_loader/load_images.h" #include "platform/input.h" #include "platform/update.h" -#include "config/config.h" +#include "platform/wayland.h" #include "utils/error.h" #include "utils/memory.h" + +#include +#include #include -#include -#include -#include -#include #include -#include +#include #include -#include #include -#include +#include +#include +#include #include -#include "image_loader/load_images.h" - // ============================================================================= // GLOBAL STATE AND CONFIGURATION // ============================================================================= namespace bongocat { - static inline constexpr platform::time_ms_t WAIT_FOR_SHUTDOWN_MS = 5000; - static inline constexpr platform::time_ms_t SLEEP_WAIT_FOR_SHUTDOWN_MS = 100; - static_assert(SLEEP_WAIT_FOR_SHUTDOWN_MS > 0); - - static inline constexpr platform::time_ms_t WAIT_FOR_SHUTDOWN_ANIMATION_THREAD_MS = 5000; - static inline constexpr platform::time_ms_t WAIT_FOR_SHUTDOWN_INPUT_THREAD_MS = 2000; - static inline constexpr platform::time_ms_t WAIT_FOR_SHUTDOWN_UPDATE_THREAD_MS = 5000; - static inline constexpr platform::time_ms_t WAIT_FOR_SHUTDOWN_CONFIG_WATCHER_THREAD_MS = 1000; - inline static constexpr platform::time_ms_t COND_RELOAD_CONFIG_TIMEOUT_MS = 5000; - inline static constexpr platform::time_ms_t COND_INIT_TIMEOUT_MS = 5000; - - inline static constexpr platform::time_ms_t WAIT_FOR_FLUSH_BEFORE_EXIT_MS = 100; - - struct main_context_t; - void stop_threads(main_context_t& context); - void cleanup(main_context_t& context); - - struct main_context_t { - volatile sig_atomic_t running {0}; - platform::FileDescriptor signal_fd {-1}; - - config::config_t config; - config::load_config_overwrite_parameters_t overwrite_config_parameters; - - AllocatedMemory config_watcher; - AllocatedMemory input; - AllocatedMemory update; - AllocatedMemory animation; - AllocatedMemory wayland; - - const char *signal_watch_path{nullptr}; - atomic_uint64_t config_generation{0}; - platform::CondVariable configs_reloaded_cond{}; - platform::Mutex sync_configs; - - char* pid_filename{nullptr}; - char* default_config_filename{nullptr}; - - main_context_t() = default; - ~main_context_t() { - cleanup(*this); - } - main_context_t(const main_context_t&) = delete; - main_context_t& operator=(const main_context_t&) = delete; - main_context_t(main_context_t&& other) noexcept = delete; - main_context_t& operator=(main_context_t&& other) noexcept = delete; - }; - inline void stop_threads(main_context_t& context) { - context.running = 0; - // stop threads - if (context.animation != nullptr) atomic_store(&context.animation->anim._animation_running, false); - if (context.input != nullptr) atomic_store(&context.input->_capture_input_running, false); - if (context.update != nullptr) atomic_store(&context.update->_running, false); - if (context.config_watcher != nullptr) atomic_store(&context.config_watcher->_running, false); - - // wait for threads - if (context.animation != nullptr) platform::join_thread_with_timeout(context.animation->anim._anim_thread, WAIT_FOR_SHUTDOWN_ANIMATION_THREAD_MS); - if (context.input != nullptr) platform::join_thread_with_timeout(context.input->_input_thread, WAIT_FOR_SHUTDOWN_INPUT_THREAD_MS); - if (context.update != nullptr) platform::join_thread_with_timeout(context.update->_update_thread, WAIT_FOR_SHUTDOWN_UPDATE_THREAD_MS); - if (context.config_watcher != nullptr) platform::join_thread_with_timeout(context.config_watcher->_watcher_thread, WAIT_FOR_SHUTDOWN_CONFIG_WATCHER_THREAD_MS); - - // stop threads - if (context.animation != nullptr) animation::stop(*context.animation); - if (context.input != nullptr) platform::input::stop(*context.input); - if (context.update != nullptr) platform::update::stop(*context.update); - if (context.config_watcher != nullptr) config::stop_watcher(*context.config_watcher); - - context.config_generation = 0; - } - void cleanup(main_context_t& context) { - stop_threads(context); - - // Cleanup Wayland - if (context.wayland != nullptr) cleanup_wayland(*context.wayland); - - // remove references (avoid dangling pointers) - if (context.wayland != nullptr) context.wayland->animation_trigger_context = nullptr; - if (context.animation != nullptr) context.animation->_input = nullptr; - - // Cleanup systems - if (context.animation != nullptr) cleanup(*context.animation); - if (context.input != nullptr) cleanup(*context.input); - if (context.update != nullptr) cleanup(*context.update); - if (context.signal_fd._fd >= 0) close_fd(context.signal_fd); - if (context.config_watcher != nullptr) cleanup_watcher(*context.config_watcher); - context.signal_watch_path = nullptr; - - release_allocated_memory(context.config_watcher); - release_allocated_memory(context.input); - release_allocated_memory(context.update); - release_allocated_memory(context.animation); - release_allocated_memory(context.wayland); - - // Cleanup configuration - cleanup(context.config); - context.overwrite_config_parameters.output_name = nullptr; - - // cleanup signals handler - platform::close_fd(context.signal_fd); - - if (context.pid_filename) ::free(context.pid_filename); - context.pid_filename = nullptr; - - if (context.default_config_filename) ::free(context.pid_filename); - context.default_config_filename = nullptr; - } - - inline main_context_t& get_main_context() { - static main_context_t g_instance; - return g_instance; - } - - // ============================================================================= - // COMMAND LINE ARGUMENTS STRUCTURE - // ============================================================================= - - struct cli_args_t { - const char *config_file{nullptr}; - bool watch_config{false}; - bool toggle_mode{false}; - bool show_help{false}; - bool show_version{false}; - const char *output_name{nullptr}; - int32_t randomize_index{-1}; - int32_t strict{-1}; - bool ignore_running{false}; - int64_t nr{-1}; - - bool nr_set{false}; - bool output_name_set{false}; - bool config_file_set{false}; - }; - - // ============================================================================= - // PROCESS MANAGEMENT MODULE - // ============================================================================= - - inline static constexpr size_t PID_STR_BUF = 64; - - inline static constexpr auto DEFAULT_PID_FILE = "/tmp/bongocat.pid"; - inline static constexpr auto PID_FILE_WITH_SUFFIX_TEMPLATE = "/tmp/bongocat-%s.pid"; - inline static constexpr auto PID_FILE_WITH_SUFFIX_MULTI_TEMPLATE = "/tmp/bongocat-%s.%" PRIu32 ".pid"; - inline static constexpr auto PID_FILE_WITH_SUFFIX_NR_TEMPLATE = "/tmp/bongocat-%" PRId64 ".pid"; - - inline static constexpr auto DEFAULT_CONF_FILENAME = "bongocat.conf"; - - static platform::FileDescriptor process_create_pid_file(const char *pid_filename) { - platform::FileDescriptor fd = platform::FileDescriptor(open(pid_filename, O_CREAT | O_WRONLY | O_TRUNC, 0644)); - if (fd._fd < 0) { - BONGOCAT_LOG_ERROR("Failed to create PID file: %s", strerror(errno)); - return platform::FileDescriptor(-1); - } - - if (flock(fd._fd, LOCK_EX | LOCK_NB) < 0) { - if (errno == EWOULDBLOCK) { - BONGOCAT_LOG_INFO("Another instance is already running"); - return platform::FileDescriptor(-2); // Already running - } - BONGOCAT_LOG_ERROR("Failed to lock PID file: %s", strerror(errno)); - return platform::FileDescriptor(-1); - } - - char pid_str[PID_STR_BUF] = {}; - snprintf(pid_str, sizeof(pid_str), "%d\n", getpid()); - if (write(fd._fd, pid_str, strlen(pid_str)) < 0) { - BONGOCAT_LOG_ERROR("Failed to write PID to file: %s", strerror(errno)); - return platform::FileDescriptor(-1); - } - - return fd; // Keep file descriptor open to maintain lock - } - - static void process_remove_pid_file(const char* pid_filename) { - assert(pid_filename); - unlink(pid_filename); - } +static inline constexpr platform::time_ms_t WAIT_FOR_SHUTDOWN_MS = 5000; +static inline constexpr platform::time_ms_t SLEEP_WAIT_FOR_SHUTDOWN_MS = 100; +static_assert(SLEEP_WAIT_FOR_SHUTDOWN_MS > 0); + +static inline constexpr platform::time_ms_t WAIT_FOR_SHUTDOWN_ANIMATION_THREAD_MS = 5000; +static inline constexpr platform::time_ms_t WAIT_FOR_SHUTDOWN_INPUT_THREAD_MS = 2000; +static inline constexpr platform::time_ms_t WAIT_FOR_SHUTDOWN_UPDATE_THREAD_MS = 5000; +static inline constexpr platform::time_ms_t WAIT_FOR_SHUTDOWN_CONFIG_WATCHER_THREAD_MS = 1000; +inline static constexpr platform::time_ms_t COND_RELOAD_CONFIG_TIMEOUT_MS = 5000; +inline static constexpr platform::time_ms_t COND_INIT_TIMEOUT_MS = 5000; + +inline static constexpr platform::time_ms_t WAIT_FOR_FLUSH_BEFORE_EXIT_MS = 100; + +struct main_context_t; +void stop_threads(main_context_t& context); +void cleanup(main_context_t& context); + +struct main_context_t { + volatile sig_atomic_t running{0}; + platform::FileDescriptor signal_fd{-1}; + + config::config_t config; + config::load_config_overwrite_parameters_t overwrite_config_parameters; + + AllocatedMemory config_watcher; + AllocatedMemory input; + AllocatedMemory update; + AllocatedMemory animation; + AllocatedMemory wayland; + + const char *signal_watch_path{BONGOCAT_NULLPTR}; + atomic_uint64_t config_generation{0}; + platform::CondVariable configs_reloaded_cond; + platform::Mutex sync_configs; + + char *pid_filename{BONGOCAT_NULLPTR}; + char *default_config_filename{BONGOCAT_NULLPTR}; + + main_context_t() = default; + ~main_context_t() { + cleanup(*this); + } + main_context_t(const main_context_t&) = delete; + main_context_t& operator=(const main_context_t&) = delete; + main_context_t(main_context_t&& other) noexcept = delete; + main_context_t& operator=(main_context_t&& other) noexcept = delete; +}; +inline void stop_threads(main_context_t& context) { + context.running = 0; + // stop threads + if (context.animation != BONGOCAT_NULLPTR) { + atomic_store(&context.animation->thread_context._animation_running, false); + } + if (context.input != BONGOCAT_NULLPTR) { + atomic_store(&context.input->_capture_input_running, false); + } + if (context.update != BONGOCAT_NULLPTR) { + atomic_store(&context.update->_running, false); + } + if (context.config_watcher != BONGOCAT_NULLPTR) { + atomic_store(&context.config_watcher->_running, false); + } + + // wait for threads + if (context.animation != BONGOCAT_NULLPTR) { + platform::join_thread_with_timeout(context.animation->thread_context._anim_thread, + WAIT_FOR_SHUTDOWN_ANIMATION_THREAD_MS); + } + if (context.input != BONGOCAT_NULLPTR) { + platform::join_thread_with_timeout(context.input->_input_thread, WAIT_FOR_SHUTDOWN_INPUT_THREAD_MS); + } + if (context.update != BONGOCAT_NULLPTR) { + platform::join_thread_with_timeout(context.update->_update_thread, WAIT_FOR_SHUTDOWN_UPDATE_THREAD_MS); + } + if (context.config_watcher != BONGOCAT_NULLPTR) { + platform::join_thread_with_timeout(context.config_watcher->_watcher_thread, + WAIT_FOR_SHUTDOWN_CONFIG_WATCHER_THREAD_MS); + } + + // stop threads + if (context.animation != BONGOCAT_NULLPTR) { + animation::stop(*context.animation); + } + if (context.input != BONGOCAT_NULLPTR) { + platform::input::stop(*context.input); + } + if (context.update != BONGOCAT_NULLPTR) { + platform::update::stop(*context.update); + } + if (context.config_watcher != BONGOCAT_NULLPTR) { + config::stop_watcher(*context.config_watcher); + } + + context.config_generation = 0; +} +void cleanup(main_context_t& context) { + stop_threads(context); + + // Cleanup Wayland + if (context.wayland != BONGOCAT_NULLPTR) { + cleanup_wayland(*context.wayland); + } + + // remove references (avoid dangling pointers) + if (context.wayland != BONGOCAT_NULLPTR) { + context.wayland->animation_context = BONGOCAT_NULLPTR; + } + if (context.animation != BONGOCAT_NULLPTR) { + context.animation->_input = BONGOCAT_NULLPTR; + } + + // Cleanup systems + if (context.animation != BONGOCAT_NULLPTR) { + cleanup(*context.animation); + } + if (context.input != BONGOCAT_NULLPTR) { + cleanup(*context.input); + } + if (context.update != BONGOCAT_NULLPTR) { + cleanup(*context.update); + } + if (context.signal_fd._fd >= 0) { + close_fd(context.signal_fd); + } + if (context.config_watcher != BONGOCAT_NULLPTR) { + cleanup_watcher(*context.config_watcher); + } + context.signal_watch_path = BONGOCAT_NULLPTR; + + release_allocated_memory(context.config_watcher); + release_allocated_memory(context.input); + release_allocated_memory(context.update); + release_allocated_memory(context.animation); + release_allocated_memory(context.wayland); + + // Cleanup configuration + cleanup(context.config); + context.overwrite_config_parameters.output_name = BONGOCAT_NULLPTR; + + // cleanup signals handler + platform::close_fd(context.signal_fd); + + if (context.pid_filename != BONGOCAT_NULLPTR) { + ::free(context.pid_filename); + context.pid_filename = BONGOCAT_NULLPTR; + } + + if (context.default_config_filename != BONGOCAT_NULLPTR) { + ::free(context.default_config_filename); + context.default_config_filename = BONGOCAT_NULLPTR; + } +} - static pid_t process_get_running_pid(const char* program_name, const char* pid_filename) { - assert(program_name); - assert(pid_filename); - platform::FileDescriptor fd = platform::FileDescriptor(::open(pid_filename, O_RDONLY)); - if (fd._fd < 0) { - return -1; // No PID file exists - } +inline main_context_t& get_main_context() { + static main_context_t g_instance; + return g_instance; +} - // Try to get a shared lock to read the file - if (flock(fd._fd, LOCK_SH | LOCK_NB) < 0) { - if (errno == EWOULDBLOCK) { - // File is locked by another process, so it's running - // We need to read the PID anyway, so let's try without lock - fd = platform::FileDescriptor(::open(pid_filename, O_RDONLY)); - if (fd._fd < 0) return -1; - } else { - return -1; - } - } +// ============================================================================= +// COMMAND LINE ARGUMENTS STRUCTURE +// ============================================================================= - char pid_str[PID_STR_BUF] = {0}; - const ssize_t bytes_read = read(fd._fd, pid_str, sizeof(pid_str) - 1); - platform::close_fd(fd); - if (bytes_read <= 0) { - return -1; - } - pid_str[bytes_read] = '\0'; - pid_str[strcspn(pid_str, "\r\n")] = '\0'; - for (char* p = pid_str; *p; ++p) { - if (*p == '\n' || *p == '\r' || *p == ' ' || *p == '\t') { - *p = '\0'; - break; - } - } +struct cli_args_t { + const char *config_file{BONGOCAT_NULLPTR}; + bool watch_config{false}; + bool toggle_mode{false}; + bool show_help{false}; + bool show_version{false}; + const char *output_name{BONGOCAT_NULLPTR}; + int32_t randomize_index{-1}; + int32_t strict{-1}; + bool ignore_running{false}; + int64_t nr{-1}; + + bool nr_set{false}; + bool output_name_set{false}; + bool config_file_set{false}; +}; +// ============================================================================= +// PROCESS MANAGEMENT MODULE +// ============================================================================= - char *endptr = nullptr; - errno = 0; // Reset errno before call - const auto pid = static_cast(strtol(pid_str, &endptr, 10)); - if (endptr == pid_str) { - return -1; // no digits at all - } - if ((errno == ERANGE) || pid < 0) { - BONGOCAT_LOG_ERROR("'%s' out of range for pid_t", pid_str); - return -1; - } +inline static constexpr size_t PID_STR_BUF = 64; - char exe_path[PATH_MAX] = {0}; - snprintf(exe_path, sizeof(exe_path), "/proc/%d/exe", pid); - char buf[PATH_MAX] = {0}; - ssize_t len = readlink(exe_path, buf, sizeof(buf) - 1); - if (len > 0) { - buf[len] = '\0'; +inline static constexpr auto DEFAULT_PID_FILE = "/tmp/bongocat.pid"; +inline static constexpr auto PID_FILE_WITH_SUFFIX_TEMPLATE = "/tmp/bongocat-%s.pid"; +inline static constexpr auto PID_FILE_WITH_SUFFIX_MULTI_TEMPLATE = "/tmp/bongocat-%s.%" PRIu32 ".pid"; +inline static constexpr auto PID_FILE_WITH_SUFFIX_NR_TEMPLATE = "/tmp/bongocat-%" PRId64 ".pid"; - const char* exe_basename = strrchr(buf, '/'); - exe_basename = exe_basename ? exe_basename + 1 : buf; +inline static constexpr auto DEFAULT_CONF_FILENAME = "bongocat.conf"; - const char* prog_basename = strrchr(program_name, '/'); - prog_basename = prog_basename ? prog_basename + 1 : program_name; +static platform::FileDescriptor process_create_pid_file(const char *pid_filename) { + platform::FileDescriptor fd = platform::FileDescriptor(open(pid_filename, O_CREAT | O_WRONLY | O_TRUNC, 0644)); + if (fd._fd < 0) { + BONGOCAT_LOG_ERROR("Failed to create PID file: %s", strerror(errno)); + return platform::FileDescriptor(-1); + } - if (strcmp(exe_basename, prog_basename) != 0) { - return -1; - } - } + if (flock(fd._fd, LOCK_EX | LOCK_NB) < 0) { + if (errno == EWOULDBLOCK) { + BONGOCAT_LOG_INFO("Another instance is already running"); + return platform::FileDescriptor(-2); // Already running + } + BONGOCAT_LOG_ERROR("Failed to lock PID file: %s", strerror(errno)); + return platform::FileDescriptor(-1); + } + + char pid_str[PID_STR_BUF] = {}; + snprintf(pid_str, sizeof(pid_str), "%d\n", getpid()); + if (write(fd._fd, pid_str, strlen(pid_str)) < 0) { + BONGOCAT_LOG_ERROR("Failed to write PID to file: %s", strerror(errno)); + return platform::FileDescriptor(-1); + } + + return fd; // Keep file descriptor open to maintain lock +} - // Check if process is actually running - if (kill(pid, 0) == 0) { - return pid; // Process is running - } +static void process_remove_pid_file(const char *pid_filename) { + assert(pid_filename); + unlink(pid_filename); +} +static pid_t process_get_running_pid(const char *program_name, const char *pid_filename) { + assert(program_name); + assert(pid_filename); + platform::FileDescriptor fd = platform::FileDescriptor(::open(pid_filename, O_RDONLY)); + if (fd._fd < 0) { + return -1; // No PID file exists + } + + // Try to get a shared lock to read the file + if (flock(fd._fd, LOCK_SH | LOCK_NB) < 0) { + if (errno == EWOULDBLOCK) { + // File is locked by another process, so it's running + // We need to read the PID anyway, so let's try without lock + fd = platform::FileDescriptor(::open(pid_filename, O_RDONLY)); + if (fd._fd < 0) { return -1; + } + } else { + return -1; } - - static int process_handle_toggle(const char* program_name, const char* pid_filename) { - const pid_t running_pid = process_get_running_pid(program_name, pid_filename); - if (running_pid < 0) { - // Process is not running, remove stale PID file - process_remove_pid_file(pid_filename); - } - - if (running_pid > 0) { - // Process is running, kill it - BONGOCAT_LOG_INFO("Stopping bongocat (PID: %d)", running_pid); - if (kill(running_pid, SIGTERM) == 0) { - // Wait a bit for graceful shutdown - for (int i = 0; i < WAIT_FOR_SHUTDOWN_MS/SLEEP_WAIT_FOR_SHUTDOWN_MS; i++) { - if (kill(running_pid, 0) != 0) { - BONGOCAT_LOG_INFO("Bongocat stopped successfully"); - return 0; - } - usleep(SLEEP_WAIT_FOR_SHUTDOWN_MS*1000); // 100ms - } - - // Force kill if still running - BONGOCAT_LOG_WARNING("Force killing bongocat"); - kill(running_pid, SIGKILL); - BONGOCAT_LOG_INFO("Bongocat force stopped"); - } else { - BONGOCAT_LOG_ERROR("Failed to stop bongocat: %s", strerror(errno)); - return 1; - } - } else { - BONGOCAT_LOG_INFO("Bongocat is not running, starting it now"); - return -1; // Signal to continue with normal startup - } - - return 0; + } + + char pid_str[PID_STR_BUF] = {0}; + const ssize_t bytes_read = read(fd._fd, pid_str, sizeof(pid_str) - 1); + platform::close_fd(fd); + if (bytes_read <= 0) { + return -1; + } + pid_str[bytes_read] = '\0'; + pid_str[strcspn(pid_str, "\r\n")] = '\0'; + for (char *p = pid_str; *p; ++p) { + if (*p == '\n' || *p == '\r' || *p == ' ' || *p == '\t') { + *p = '\0'; + break; } - - // ============================================================================= - // CONFIGURATION MANAGEMENT MODULE - // ============================================================================= - - static bool config_devices_changed(const config::config_t& old_config, const config::config_t& new_config) { - if (old_config.num_keyboard_devices != new_config.num_keyboard_devices) { - return true; - } - - // Check if any device paths changed - for (int i = 0; i < new_config.num_keyboard_devices; i++) { - bool found = false; - for (int j = 0; j < old_config.num_keyboard_devices; j++) { - if (strcmp(new_config.keyboard_devices[i], old_config.keyboard_devices[j]) == 0) { - found = true; - break; - } - } - if (!found) { - return true; - } - } - - return false; + } + + char *endptr = BONGOCAT_NULLPTR; + errno = 0; // Reset errno before call + const auto pid = static_cast(strtol(pid_str, &endptr, 10)); + if (endptr == pid_str) { + return -1; // no digits at all + } + if ((errno == ERANGE) || pid < 0) { + BONGOCAT_LOG_ERROR("'%s' out of range for pid_t", pid_str); + return -1; + } + + char exe_path[PATH_MAX] = {0}; + snprintf(exe_path, sizeof(exe_path), "/proc/%d/exe", pid); + char buf[PATH_MAX] = {0}; + const ssize_t len = readlink(exe_path, buf, sizeof(buf) - 1); + if (len > 0) { + buf[len] = '\0'; + + const char *exe_basename = strrchr(buf, '/'); + exe_basename = exe_basename != BONGOCAT_NULLPTR ? exe_basename + 1 : buf; + + const char *prog_basename = strrchr(program_name, '/'); + prog_basename = prog_basename != BONGOCAT_NULLPTR ? prog_basename + 1 : program_name; + + if (strcmp(exe_basename, prog_basename) != 0) { + return -1; } + } - static void config_reload_callback() { - assert(get_main_context().input != nullptr); - assert(get_main_context().animation != nullptr); - assert(get_main_context().signal_watch_path != nullptr); - BONGOCAT_LOG_INFO("Reloading configuration from: %s (config_watcher=%s)", get_main_context().signal_watch_path, (get_main_context().config_watcher) ? get_main_context().config_watcher->config_path : "OFF"); - assert(get_main_context().config_watcher == nullptr || strcmp(get_main_context().config_watcher->config_path, get_main_context().signal_watch_path) == 0); - - if (strcmp(get_main_context().signal_watch_path, "-") == 0) { - BONGOCAT_LOG_WARNING("No reload config for stdin"); - BONGOCAT_LOG_INFO("Keeping current configuration"); - return; - } - - // Create a temporary config to test loading - auto [new_config, error] = config::load(get_main_context().signal_watch_path, get_main_context().overwrite_config_parameters); - if (error != bongocat_error_t::BONGOCAT_SUCCESS) [[unlikely]] { - BONGOCAT_LOG_ERROR("Failed to reload config: %s", bongocat::error_string(error)); - BONGOCAT_LOG_INFO("Keeping current configuration"); - return; - } + // Check if process is actually running + if (kill(pid, 0) == 0) { + return pid; // Process is running + } - // If successful, update the global config - bool devices_changed = false; - bool update_needed = false; - { - platform::LockGuard guard (get_main_context().sync_configs); - config::config_t old_config = get_main_context().config; - // keep old animation, don't randomize - if (old_config.randomize_index && new_config.randomize_index && - old_config.animation_sprite_sheet_layout == new_config.animation_sprite_sheet_layout && - old_config.animation_dm_set == new_config.animation_dm_set) { - new_config._keep_old_animation_index = !new_config.randomize_on_reload; - } - // If successful, check if input devices changed before updating config - devices_changed = config_devices_changed(old_config, new_config); - // update features had been enabled - update_needed = (new_config.cpu_threshold > old_config.cpu_threshold && old_config.cpu_threshold < platform::ENABLED_MIN_CPU_PERCENT && new_config.cpu_threshold >= platform::ENABLED_MIN_CPU_PERCENT) || - (new_config.update_rate_ms > 0 && old_config.update_rate_ms <= 0); - get_main_context().config = bongocat::move(new_config); - /// @NOTE: don't use new_config after move - new_config = {}; - // Initialize error system with debug setting - bongocat::error_init(get_main_context().config.enable_debug); - - // Increment generation atomically - // Update the running systems with new config - update_config(get_main_context().wayland->wayland_context, get_main_context().config, *get_main_context().animation); - atomic_fetch_add(&get_main_context().config_generation, 1); - uint64_t new_gen{ atomic_load(&get_main_context().config_generation)}; - platform::input::trigger_update_config(*get_main_context().input, get_main_context().config, new_gen); - platform::update::trigger_update_config(*get_main_context().update, get_main_context().config, new_gen); - animation::trigger_update_config(*get_main_context().animation, get_main_context().config, new_gen); - - // Wait for both workers to catch up - int timedwait_result{0}; - timedwait_result |= get_main_context().input->config_updated.timedwait([&] { - return !atomic_load(&get_main_context().input->_capture_input_running) || atomic_load(&get_main_context().input->config_seen_generation) >= new_gen; - }, COND_RELOAD_CONFIG_TIMEOUT_MS); - timedwait_result |= get_main_context().update->config_updated.timedwait([&] { - return !atomic_load(&get_main_context().update->_running) || atomic_load(&get_main_context().update->config_seen_generation) >= new_gen; - }, COND_RELOAD_CONFIG_TIMEOUT_MS); - timedwait_result |= get_main_context().animation->anim.config_updated.timedwait([&] { - return !atomic_load(&get_main_context().animation->anim._animation_running) || atomic_load(&get_main_context().animation->anim.config_seen_generation) >= new_gen; - }, COND_RELOAD_CONFIG_TIMEOUT_MS); - - // reset config internal state - get_main_context().config._keep_old_animation_index = false; - if (timedwait_result != 0) { - // fallback when cond hits timeout (sync config generations) - if (atomic_load(&get_main_context().input->_capture_input_running)) { - atomic_store(&get_main_context().input->config_seen_generation, new_gen); - } - if (atomic_load(&get_main_context().update->_running)) { - atomic_store(&get_main_context().update->config_seen_generation, new_gen); - } - if (atomic_load(&get_main_context().animation->anim._animation_running)) { - atomic_store(&get_main_context().animation->anim.config_seen_generation, new_gen); - } - BONGOCAT_LOG_VERBOSE("timedwait timeouted, sync all config gen: %d", timedwait_result); - } - atomic_store(&get_main_context().config_generation, new_gen); - - BONGOCAT_LOG_VERBOSE("Input: config gen: %d", atomic_load(&get_main_context().input->config_seen_generation)); - BONGOCAT_LOG_VERBOSE("Update: config gen: %d", atomic_load(&get_main_context().update->config_seen_generation)); - BONGOCAT_LOG_VERBOSE("Animation: config gen: %d", atomic_load(&get_main_context().animation->anim.config_seen_generation)); - BONGOCAT_LOG_VERBOSE("Main: config gen: %d", atomic_load(&get_main_context().config_generation)); - } - // Tell workers they can continue - get_main_context().configs_reloaded_cond.notify_all(); - - BONGOCAT_LOG_INFO("Configuration reloaded successfully!"); - BONGOCAT_LOG_INFO("New screen dimensions: %dx%d", get_main_context().wayland->wayland_context._screen_width, get_main_context().wayland->wayland_context._bar_height); - - assert(get_main_context().animation != nullptr); - animation::trigger(*get_main_context().animation, animation::trigger_animation_cause_mask_t::UpdateConfig); - - // Check if input devices changed and restart monitoring if needed - if (devices_changed) { - BONGOCAT_LOG_INFO("Input devices changed, restarting input monitoring"); - const bongocat_error_t input_result = platform::input::restart(*get_main_context().input, *get_main_context().animation, get_main_context().config, get_main_context().configs_reloaded_cond, get_main_context().config_generation); - if (input_result != bongocat_error_t::BONGOCAT_SUCCESS) [[unlikely]] { - BONGOCAT_LOG_ERROR("Failed to restart input monitoring: %s", bongocat::error_string(input_result)); - } else { - BONGOCAT_LOG_INFO("Input monitoring restarted successfully"); - } - } + return -1; +} - // Check if update features are enabled and restart update if needed - if (update_needed) { - BONGOCAT_LOG_INFO("Update features enabled, restarting update thread"); - const bongocat_error_t update_result = platform::update::restart(*get_main_context().update, *get_main_context().animation, get_main_context().config, get_main_context().configs_reloaded_cond, get_main_context().config_generation); - if (update_result != bongocat_error_t::BONGOCAT_SUCCESS) [[unlikely]] { - BONGOCAT_LOG_ERROR("Failed to restart update thread: %s", bongocat::error_string(update_result)); - } else { - BONGOCAT_LOG_INFO("Update thread restarted successfully"); - } +static int process_handle_toggle(const char *program_name, const char *pid_filename) { + const pid_t running_pid = process_get_running_pid(program_name, pid_filename); + if (running_pid < 0) { + // Process is not running, remove stale PID file + process_remove_pid_file(pid_filename); + } + + if (running_pid > 0) { + // Process is running, kill it + BONGOCAT_LOG_INFO("Stopping bongocat (PID: %d)", running_pid); + if (kill(running_pid, SIGTERM) == 0) { + // Wait a bit for graceful shutdown + for (int i = 0; i < WAIT_FOR_SHUTDOWN_MS / SLEEP_WAIT_FOR_SHUTDOWN_MS; i++) { + if (kill(running_pid, 0) != 0) { + BONGOCAT_LOG_INFO("Bongocat stopped successfully"); + return 0; } + usleep(SLEEP_WAIT_FOR_SHUTDOWN_MS * 1000); // 100ms + } - // Wait for (new) threads to be ready - // wait for context - if (atomic_load(&get_main_context().animation->anim._animation_running)) { - get_main_context().animation->init_cond.timedwait([&]() { - return !atomic_load(&get_main_context().input->_capture_input_running) || atomic_load(&get_main_context().animation->ready); - }, COND_INIT_TIMEOUT_MS); - } - if (atomic_load(&get_main_context().input->_capture_input_running)) { - get_main_context().input->init_cond.timedwait([&]() { - return !atomic_load(&get_main_context().input->_capture_input_running) || atomic_load(&get_main_context().input->ready); - }, COND_INIT_TIMEOUT_MS); - } - if (atomic_load(&get_main_context().update->_running)) { - get_main_context().update->init_cond.timedwait([&]() { - return !atomic_load(&get_main_context().update->_running) || atomic_load(&get_main_context().update->ready); - }, COND_INIT_TIMEOUT_MS); - } - BONGOCAT_LOG_VERBOSE("Animation: running %d (ready=%d)", atomic_load(&get_main_context().animation->anim._animation_running), atomic_load(&get_main_context().animation->ready)); - BONGOCAT_LOG_VERBOSE("Input: running %d (ready=%d)", atomic_load(&get_main_context().input->_capture_input_running), atomic_load(&get_main_context().input->ready)); - BONGOCAT_LOG_VERBOSE("Update: running %d (ready=%d)", atomic_load(&get_main_context().update->_running), atomic_load(&get_main_context().update->ready)); - } - - static bongocat_error_t start_config_watcher(main_context_t& ctx, const char *config_file) { - auto [config_watcher, error] = config::create_watcher(config_file); - if (error == bongocat_error_t::BONGOCAT_SUCCESS) [[unlikely]] { - ctx.config_watcher = bongocat::move(config_watcher); - config::start_watcher(*ctx.config_watcher); - BONGOCAT_LOG_INFO("Config file watching enabled for: %s", config_file); - } else { - BONGOCAT_LOG_WARNING("Failed to initialize config watcher, continuing without hot-reload"); - } - return error; + // Force kill if still running + BONGOCAT_LOG_WARNING("Force killing bongocat"); + kill(running_pid, SIGKILL); + BONGOCAT_LOG_INFO("Bongocat force stopped"); + } else { + BONGOCAT_LOG_ERROR("Failed to stop bongocat: %s", strerror(errno)); + return 1; } + } else { + BONGOCAT_LOG_INFO("Bongocat is not running, starting it now"); + return -1; // Signal to continue with normal startup + } - static char *default_config_file_path() { - const char *xdg_config_home = getenv("XDG_CONFIG_HOME"); - const char *home = getenv("HOME"); - - if (xdg_config_home != nullptr) { - size_t len = strlen(xdg_config_home) + 1 + strlen(DEFAULT_CONF_FILENAME) + 1; - char *path = static_cast(::malloc(len)); - if (!path) return nullptr; - snprintf(path, len, "%s/%s", xdg_config_home, DEFAULT_CONF_FILENAME); - - if (access(path, F_OK) == 0) { - return path; // file exists - } - - free(path); - path = nullptr; - } - - if (home != nullptr) { - size_t len = strlen(home) + strlen("/.config/") + strlen(DEFAULT_CONF_FILENAME) + 1; - char *path = static_cast(::malloc(len)); - if (!path) return nullptr; - snprintf(path, len, "%s/.config/%s", home, DEFAULT_CONF_FILENAME); - - if (access(path, F_OK) == 0) { - return path; // file exists - } + return 0; +} - free(path); - path = nullptr; - } +// ============================================================================= +// CONFIGURATION MANAGEMENT MODULE +// ============================================================================= - // If neither env var is set, fallback to just filename in current dir - return strdup(DEFAULT_CONF_FILENAME); +static bool config_devices_changed(const config::config_t& old_config, const config::config_t& new_config) { + if (old_config.num_keyboard_devices != new_config.num_keyboard_devices) { + return true; + } + + // Check if any device paths changed + for (int i = 0; i < new_config.num_keyboard_devices; i++) { + bool found = false; + for (int j = 0; j < old_config.num_keyboard_devices; j++) { + if (strcmp(new_config.keyboard_devices[i], old_config.keyboard_devices[j]) == 0) { + found = true; + break; + } + } + if (!found) { + return true; } + } - // ============================================================================= - // SIGNAL HANDLING MODULE - // ============================================================================= + return false; +} - static bongocat_error_t signal_setup_handlers(main_context_t& ctx) { - sigset_t mask; - sigemptyset(&mask); - sigaddset(&mask, SIGINT); - sigaddset(&mask, SIGTERM); - sigaddset(&mask, SIGCHLD); - sigaddset(&mask, SIGUSR1); - sigaddset(&mask, SIGUSR2); +static void config_reload_callback() { + assert(get_main_context().input != BONGOCAT_NULLPTR); + assert(get_main_context().animation != BONGOCAT_NULLPTR); + assert(get_main_context().signal_watch_path != BONGOCAT_NULLPTR); + BONGOCAT_LOG_INFO("Reloading configuration from: %s (config_watcher=%s)", get_main_context().signal_watch_path, + (get_main_context().config_watcher) ? get_main_context().config_watcher->config_path : "OFF"); + assert(get_main_context().config_watcher == BONGOCAT_NULLPTR || + strcmp(get_main_context().config_watcher->config_path, get_main_context().signal_watch_path) == 0); + + if (strcmp(get_main_context().signal_watch_path, "-") == 0) { + BONGOCAT_LOG_WARNING("No reload config for stdin"); + BONGOCAT_LOG_INFO("Keeping current configuration"); + return; + } + + // Create a temporary config to test loading + auto [new_config, error] = + config::load(get_main_context().signal_watch_path, get_main_context().overwrite_config_parameters); + if (error != bongocat_error_t::BONGOCAT_SUCCESS) [[unlikely]] { + BONGOCAT_LOG_ERROR("Failed to reload config: %s", bongocat::error_string(error)); + BONGOCAT_LOG_INFO("Keeping current configuration"); + return; + } + + // If successful, update the global config + bool devices_changed = false; + bool update_needed = false; + { + platform::LockGuard guard(get_main_context().sync_configs); + config::config_t old_config = get_main_context().config; + // keep old animation, don't randomize + if (old_config.randomize_index >= 1 && new_config.randomize_index >= 1 && + old_config.animation_sprite_sheet_layout == new_config.animation_sprite_sheet_layout && + old_config.animation_dm_set == new_config.animation_dm_set) { + new_config._keep_old_animation_index = new_config.randomize_on_reload <= 0; + } + // If successful, check if input devices changed before updating config + devices_changed = config_devices_changed(old_config, new_config); + // update features had been enabled + update_needed = (new_config.cpu_threshold > old_config.cpu_threshold && + old_config.cpu_threshold < platform::ENABLED_MIN_CPU_PERCENT && + new_config.cpu_threshold >= platform::ENABLED_MIN_CPU_PERCENT) || + (new_config.update_rate_ms > 0 && old_config.update_rate_ms <= 0); + get_main_context().config = bongocat::move(new_config); + /// @NOTE: don't use new_config after move + new_config = {}; + // Initialize error system with debug setting + bongocat::error_init(get_main_context().config.enable_debug); + + // Increment generation atomically + // Update the running systems with new config + assert(get_main_context().wayland); + assert(get_main_context().animation); + assert(get_main_context().input); + assert(get_main_context().update); + update_config(*get_main_context().wayland, get_main_context().config, *get_main_context().animation); + atomic_fetch_add(&get_main_context().config_generation, 1); + uint64_t new_gen{atomic_load(&get_main_context().config_generation)}; + platform::input::trigger_update_config(*get_main_context().input, get_main_context().config, new_gen); + platform::update::trigger_update_config(*get_main_context().update, get_main_context().config, new_gen); + animation::trigger_update_config(*get_main_context().animation, get_main_context().config, new_gen); + + // Wait for both workers to catch up + int timedwait_result{0}; + timedwait_result |= get_main_context().input->config_updated.timedwait( + [&] { + return !atomic_load(&get_main_context().input->_capture_input_running) || + atomic_load(&get_main_context().input->config_seen_generation) >= new_gen; + }, + COND_RELOAD_CONFIG_TIMEOUT_MS); + timedwait_result |= get_main_context().update->config_updated.timedwait( + [&] { + return !atomic_load(&get_main_context().update->_running) || + atomic_load(&get_main_context().update->config_seen_generation) >= new_gen; + }, + COND_RELOAD_CONFIG_TIMEOUT_MS); + timedwait_result |= get_main_context().animation->thread_context.config_updated.timedwait( + [&] { + return !atomic_load(&get_main_context().animation->thread_context._animation_running) || + atomic_load(&get_main_context().animation->thread_context.config_seen_generation) >= new_gen; + }, + COND_RELOAD_CONFIG_TIMEOUT_MS); + + // reset config internal state + get_main_context().config._keep_old_animation_index = false; + if (timedwait_result != 0) { + // fallback when cond hits timeout (sync config generations) + if (atomic_load(&get_main_context().input->_capture_input_running)) { + atomic_store(&get_main_context().input->config_seen_generation, new_gen); + } + if (atomic_load(&get_main_context().update->_running)) { + atomic_store(&get_main_context().update->config_seen_generation, new_gen); + } + if (atomic_load(&get_main_context().animation->thread_context._animation_running)) { + atomic_store(&get_main_context().animation->thread_context.config_seen_generation, new_gen); + } + BONGOCAT_LOG_VERBOSE("timedwait timeouted, sync all config gen: %d", timedwait_result); + } + atomic_store(&get_main_context().config_generation, new_gen); + + BONGOCAT_LOG_VERBOSE("Input: config gen: %d", atomic_load(&get_main_context().input->config_seen_generation)); + BONGOCAT_LOG_VERBOSE("Update: config gen: %d", atomic_load(&get_main_context().update->config_seen_generation)); + BONGOCAT_LOG_VERBOSE("Animation: config gen: %d", + atomic_load(&get_main_context().animation->thread_context.config_seen_generation)); + BONGOCAT_LOG_VERBOSE("Main: config gen: %d", atomic_load(&get_main_context().config_generation)); + } + // Tell workers they can continue + get_main_context().configs_reloaded_cond.notify_all(); + + BONGOCAT_LOG_INFO("Configuration reloaded successfully!"); + BONGOCAT_LOG_INFO("New screen dimensions: %dx%d", get_main_context().wayland->thread_context._screen_width, + get_main_context().wayland->thread_context._bar_height); + + assert(get_main_context().animation != BONGOCAT_NULLPTR); + animation::trigger(*get_main_context().animation, animation::trigger_animation_cause_mask_t::UpdateConfig); + + // Check if input devices changed and restart monitoring if needed + if (devices_changed) { + BONGOCAT_LOG_INFO("Input devices changed, restarting input monitoring"); + const bongocat_error_t input_result = + platform::input::restart(*get_main_context().input, *get_main_context().animation, get_main_context().config, + get_main_context().configs_reloaded_cond, get_main_context().config_generation); + if (input_result != bongocat_error_t::BONGOCAT_SUCCESS) [[unlikely]] { + BONGOCAT_LOG_ERROR("Failed to restart input monitoring: %s", bongocat::error_string(input_result)); + } else { + BONGOCAT_LOG_INFO("Input monitoring restarted successfully"); + } + } + + // Check if update features are enabled and restart update if needed + if (update_needed) { + BONGOCAT_LOG_INFO("Update features enabled, restarting update thread"); + const bongocat_error_t update_result = + platform::update::restart(*get_main_context().update, *get_main_context().animation, get_main_context().config, + get_main_context().configs_reloaded_cond, get_main_context().config_generation); + if (update_result != bongocat_error_t::BONGOCAT_SUCCESS) [[unlikely]] { + BONGOCAT_LOG_ERROR("Failed to restart update thread: %s", bongocat::error_string(update_result)); + } else { + BONGOCAT_LOG_INFO("Update thread restarted successfully"); + } + } + + // Wait for (new) threads to be ready + // wait for context + if (atomic_load(&get_main_context().animation->thread_context._animation_running)) { + get_main_context().animation->init_cond.timedwait( + [&]() { + return !atomic_load(&get_main_context().input->_capture_input_running) || + atomic_load(&get_main_context().animation->ready); + }, + COND_INIT_TIMEOUT_MS); + } + if (atomic_load(&get_main_context().input->_capture_input_running)) { + get_main_context().input->init_cond.timedwait( + [&]() { + return !atomic_load(&get_main_context().input->_capture_input_running) || + atomic_load(&get_main_context().input->ready); + }, + COND_INIT_TIMEOUT_MS); + } + if (atomic_load(&get_main_context().update->_running)) { + get_main_context().update->init_cond.timedwait( + [&]() { + return !atomic_load(&get_main_context().update->_running) || atomic_load(&get_main_context().update->ready); + }, + COND_INIT_TIMEOUT_MS); + } + BONGOCAT_LOG_VERBOSE("Animation: running %d (ready=%d)", + atomic_load(&get_main_context().animation->thread_context._animation_running), + atomic_load(&get_main_context().animation->ready)); + BONGOCAT_LOG_VERBOSE("Input: running %d (ready=%d)", atomic_load(&get_main_context().input->_capture_input_running), + atomic_load(&get_main_context().input->ready)); + BONGOCAT_LOG_VERBOSE("Update: running %d (ready=%d)", atomic_load(&get_main_context().update->_running), + atomic_load(&get_main_context().update->ready)); +} - // Block signals globally so they are only delivered via signalfd - if (sigprocmask(SIG_BLOCK, &mask, nullptr) == -1) [[unlikely]] { - BONGOCAT_LOG_ERROR("Failed to block signals: %s", strerror(errno)); - return bongocat_error_t::BONGOCAT_ERROR_THREAD; - } +static bongocat_error_t start_config_watcher(main_context_t& ctx, const char *config_file) { + auto [config_watcher, error] = config::create_watcher(config_file); + if (error == bongocat_error_t::BONGOCAT_SUCCESS) [[unlikely]] { + ctx.config_watcher = bongocat::move(config_watcher); + config::start_watcher(*ctx.config_watcher); + BONGOCAT_LOG_INFO("Config file watching enabled for: %s", config_file); + } else { + BONGOCAT_LOG_WARNING("Failed to initialize config watcher, continuing without hot-reload"); + } + return error; +} - ctx.signal_fd = platform::FileDescriptor(signalfd(-1, &mask, SFD_NONBLOCK | SFD_CLOEXEC)); - if (ctx.signal_fd._fd == -1) [[unlikely]] { - BONGOCAT_LOG_ERROR("Failed to create signalfd: %s", strerror(errno)); - return bongocat_error_t::BONGOCAT_ERROR_THREAD; - } +static char *default_config_file_path() { + const char *xdg_config_home = getenv("XDG_CONFIG_HOME"); + const char *home = getenv("HOME"); - return bongocat_error_t::BONGOCAT_SUCCESS; + if (xdg_config_home != BONGOCAT_NULLPTR) { + const size_t len = strlen(xdg_config_home) + 1 + strlen(DEFAULT_CONF_FILENAME) + 1; + char *path = static_cast(::malloc(len)); + if (path == BONGOCAT_NULLPTR) [[unlikely]] { + return BONGOCAT_NULLPTR; } + snprintf(path, len, "%s/%s", xdg_config_home, DEFAULT_CONF_FILENAME); - // ============================================================================= - // SYSTEM INITIALIZATION AND CLEANUP MODULE - // ============================================================================= + if (access(path, F_OK) == 0) { + return path; // file exists + } - static bongocat_error_t system_initialize_components(main_context_t& ctx) { - // Initialize input system - { - auto [input, input_error] = platform::input::create(ctx.config); - if (input_error != bongocat_error_t::BONGOCAT_SUCCESS) [[unlikely]] { - BONGOCAT_LOG_ERROR("Failed to initialize input system: %s", bongocat::error_string(input_error)); - return input_error; - } - ctx.input = bongocat::move(input); - } + free(path); + path = BONGOCAT_NULLPTR; + } - // Initialize update system - { - auto [update, update_error] = platform::update::create(ctx.config); - if (update_error != bongocat_error_t::BONGOCAT_SUCCESS) [[unlikely]] { - BONGOCAT_LOG_ERROR("Failed to initialize updfate system: %s", bongocat::error_string(update_error)); - return update_error; - } - ctx.update = bongocat::move(update); - } + if (home != BONGOCAT_NULLPTR) { + const size_t len = strlen(home) + strlen("/.config/") + strlen(DEFAULT_CONF_FILENAME) + 1; + char *path = static_cast(::malloc(len)); + if (path == BONGOCAT_NULLPTR) [[unlikely]] { + return BONGOCAT_NULLPTR; + } + snprintf(path, len, "%s/.config/%s", home, DEFAULT_CONF_FILENAME); - // Initialize animation system - { - animation::init_image_loader(); - auto [animation, animation_error] = animation::create(ctx.config); - if (animation_error != bongocat_error_t::BONGOCAT_SUCCESS) [[unlikely]] { - BONGOCAT_LOG_ERROR("Failed to initialize animation system: %s", bongocat::error_string(animation_error)); - return animation_error; - } - ctx.animation = bongocat::move(animation); - } + if (access(path, F_OK) == 0) { + return path; // file exists + } - // Initialize Wayland - { - assert(ctx.animation != nullptr); - /// @NOTE: animation needed only for reference - auto [wayland, wayland_error] = platform::wayland::create(*ctx.animation, ctx.config); - if (wayland_error != bongocat_error_t::BONGOCAT_SUCCESS) [[unlikely]] { - BONGOCAT_LOG_ERROR("Failed to initialize Wayland: %s", bongocat::error_string(wayland_error)); - return wayland_error; - } - ctx.wayland = bongocat::move(wayland); - } + free(path); + path = BONGOCAT_NULLPTR; + } - // Setup wayland - { - assert(ctx.wayland != nullptr); - assert(ctx.animation != nullptr); - bongocat_error_t setup_wayland_result = setup(*ctx.wayland, *ctx.animation); - if (setup_wayland_result != bongocat_error_t::BONGOCAT_SUCCESS) [[unlikely]] { - BONGOCAT_LOG_ERROR("Failed to setup wayland: %s", bongocat::error_string(setup_wayland_result)); - return setup_wayland_result; - } - } + // If neither env var is set, fallback to just filename in current dir + return strdup(DEFAULT_CONF_FILENAME); +} - // Start animation thread - { - assert(ctx.animation != nullptr); - assert(ctx.input != nullptr); - assert(ctx.update != nullptr); - bongocat_error_t start_animation_result = animation::start(*ctx.animation, *ctx.input, *ctx.update, ctx.config, get_main_context().configs_reloaded_cond, get_main_context().config_generation); - if (start_animation_result != bongocat_error_t::BONGOCAT_SUCCESS) [[unlikely]] { - BONGOCAT_LOG_ERROR("Failed to start animation thread: %s", bongocat::error_string(start_animation_result)); - return start_animation_result; - } - } +// ============================================================================= +// SIGNAL HANDLING MODULE +// ============================================================================= - // Start input monitoring - { - assert(ctx.animation != nullptr); - assert(ctx.input != nullptr); - bongocat_error_t start_input_result = platform::input::start(*ctx.input, *ctx.animation, ctx.config, get_main_context().configs_reloaded_cond, get_main_context().config_generation); - if (start_input_result != bongocat_error_t::BONGOCAT_SUCCESS) [[unlikely]] { - BONGOCAT_LOG_ERROR("Failed to start input monitoring: %s", bongocat::error_string(start_input_result)); - return start_input_result; - } - } +static bongocat_error_t signal_setup_handlers(main_context_t& ctx) { + sigset_t mask; + sigemptyset(&mask); + sigaddset(&mask, SIGINT); + sigaddset(&mask, SIGTERM); + sigaddset(&mask, SIGCHLD); + sigaddset(&mask, SIGUSR1); + sigaddset(&mask, SIGUSR2); + sigaddset(&mask, SIGQUIT); + sigaddset(&mask, SIGHUP); + + // Block signals globally so they are only delivered via signalfd + if (sigprocmask(SIG_BLOCK, &mask, BONGOCAT_NULLPTR) == -1) [[unlikely]] { + BONGOCAT_LOG_ERROR("Failed to block signals: %s", strerror(errno)); + return bongocat_error_t::BONGOCAT_ERROR_THREAD; + } + + ctx.signal_fd = platform::FileDescriptor(signalfd(-1, &mask, SFD_NONBLOCK | SFD_CLOEXEC)); + if (ctx.signal_fd._fd == -1) [[unlikely]] { + BONGOCAT_LOG_ERROR("Failed to create signalfd: %s", strerror(errno)); + return bongocat_error_t::BONGOCAT_ERROR_THREAD; + } + + return bongocat_error_t::BONGOCAT_SUCCESS; +} - // Start update monitoring - { - assert(ctx.animation != nullptr); - assert(ctx.update != nullptr); - bongocat_error_t start_update_result = platform::update::start(*ctx.update, *ctx.animation, ctx.config, get_main_context().configs_reloaded_cond, get_main_context().config_generation); - if (start_update_result != bongocat_error_t::BONGOCAT_SUCCESS) [[unlikely]] { - BONGOCAT_LOG_ERROR("Failed to start update thread: %s", bongocat::error_string(start_update_result)); - return start_update_result; - } - } +// ============================================================================= +// SYSTEM INITIALIZATION AND CLEANUP MODULE +// ============================================================================= - return bongocat_error_t::BONGOCAT_SUCCESS; +static bongocat_error_t system_initialize_components(main_context_t& ctx) { + // Initialize input system + { + auto [input, input_error] = platform::input::create(ctx.config); + if (input_error != bongocat_error_t::BONGOCAT_SUCCESS) [[unlikely]] { + BONGOCAT_LOG_ERROR("Failed to initialize input system: %s", bongocat::error_string(input_error)); + return input_error; } - - [[ noreturn ]] static void system_cleanup_and_exit(main_context_t& ctx, int exit_code) { - BONGOCAT_LOG_INFO("Stop threads..."); - ctx.running = 0; - stop_threads(ctx); - - BONGOCAT_LOG_INFO("Performing cleanup..."); - process_remove_pid_file(ctx.pid_filename); - // clean up context before global cleanup (log mutex, etc.) - cleanup(ctx); - - BONGOCAT_LOG_INFO("Cleanup complete, exiting with code %d", exit_code); - usleep(WAIT_FOR_FLUSH_BEFORE_EXIT_MS*1000); - exit(exit_code); + ctx.input = bongocat::move(input); + } + + // Initialize update system + { + auto [update, update_error] = platform::update::create(ctx.config); + if (update_error != bongocat_error_t::BONGOCAT_SUCCESS) [[unlikely]] { + BONGOCAT_LOG_ERROR("Failed to initialize updfate system: %s", bongocat::error_string(update_error)); + return update_error; + } + ctx.update = bongocat::move(update); + } + + // Initialize animation system + { + animation::init_image_loader(); + auto [animation, animation_error] = animation::create(ctx.config); + if (animation_error != bongocat_error_t::BONGOCAT_SUCCESS) [[unlikely]] { + BONGOCAT_LOG_ERROR("Failed to initialize animation system: %s", bongocat::error_string(animation_error)); + return animation_error; + } + ctx.animation = bongocat::move(animation); + } + + // Initialize Wayland + { + assert(ctx.animation != BONGOCAT_NULLPTR); + /// @NOTE: animation needed only for reference + auto [wayland, wayland_error] = platform::wayland::create(*ctx.animation, ctx.config); + if (wayland_error != bongocat_error_t::BONGOCAT_SUCCESS) [[unlikely]] { + BONGOCAT_LOG_ERROR("Failed to initialize Wayland: %s", bongocat::error_string(wayland_error)); + return wayland_error; + } + ctx.wayland = bongocat::move(wayland); + } + + // Setup wayland + { + assert(ctx.wayland != BONGOCAT_NULLPTR); + assert(ctx.animation != BONGOCAT_NULLPTR); + bongocat_error_t setup_wayland_result = setup(*ctx.wayland, *ctx.animation); + if (setup_wayland_result != bongocat_error_t::BONGOCAT_SUCCESS) [[unlikely]] { + BONGOCAT_LOG_ERROR("Failed to setup wayland: %s", bongocat::error_string(setup_wayland_result)); + return setup_wayland_result; + } + } + + // Start animation thread + { + assert(ctx.animation != BONGOCAT_NULLPTR); + assert(ctx.input != BONGOCAT_NULLPTR); + assert(ctx.update != BONGOCAT_NULLPTR); + bongocat_error_t start_animation_result = + animation::start(*ctx.animation, *ctx.input, *ctx.update, ctx.config, get_main_context().configs_reloaded_cond, + get_main_context().config_generation); + if (start_animation_result != bongocat_error_t::BONGOCAT_SUCCESS) [[unlikely]] { + BONGOCAT_LOG_ERROR("Failed to start animation thread: %s", bongocat::error_string(start_animation_result)); + return start_animation_result; } + } + + // Start input monitoring + { + assert(ctx.animation != BONGOCAT_NULLPTR); + assert(ctx.input != BONGOCAT_NULLPTR); + bongocat_error_t start_input_result = + platform::input::start(*ctx.input, *ctx.animation, ctx.config, get_main_context().configs_reloaded_cond, + get_main_context().config_generation); + if (start_input_result != bongocat_error_t::BONGOCAT_SUCCESS) [[unlikely]] { + BONGOCAT_LOG_ERROR("Failed to start input monitoring: %s", bongocat::error_string(start_input_result)); + return start_input_result; + } + } + + // Start update monitoring + { + assert(ctx.animation != BONGOCAT_NULLPTR); + assert(ctx.update != BONGOCAT_NULLPTR); + bongocat_error_t start_update_result = + platform::update::start(*ctx.update, *ctx.animation, ctx.config, get_main_context().configs_reloaded_cond, + get_main_context().config_generation); + if (start_update_result != bongocat_error_t::BONGOCAT_SUCCESS) [[unlikely]] { + BONGOCAT_LOG_ERROR("Failed to start update thread: %s", bongocat::error_string(start_update_result)); + return start_update_result; + } + } - // ============================================================================= - // COMMAND LINE PROCESSING MODULE - // ============================================================================= + return bongocat_error_t::BONGOCAT_SUCCESS; +} - static void cli_show_help(const char *program_name) { - char *base_program_name = strdup(program_name); - if (!base_program_name) { - perror("strdup"); - return; - } +[[noreturn]] static void system_cleanup_and_exit(main_context_t& ctx, int exit_code) { + BONGOCAT_LOG_INFO("Stop threads..."); + ctx.running = 0; + stop_threads(ctx); - printf("Bongo Cat Wayland Overlay.\n\n"); - printf("Usage: %s [OPTIONS]\n\n", basename(base_program_name)); - printf("Options:\n"); - printf(" -h, --help Show this help message\n"); - printf(" -v, --version Show version information\n"); - printf(" -c, --config Specify config file (default: ~/.config/bongocat.conf)\n"); - printf(" -w, --watch-config Watch config file for changes and reload automatically\n"); - printf(" -t, --toggle Toggle bongocat on/off (start if not running, stop if running)\n"); - printf(" -o, --output-name NAME Specify output name (overwrite output_name from config)\n"); - printf(" --random Enable random animation_index, at start (overwrite random_index from config)\n"); - printf(" --strict Enable strict mode, only start up with a valid config and valid parameter\n"); - printf(" --nr NR Specify Nr. for PID file to avoid conflicting ruinning instances\n"); - printf(" --ignore-running Ignore current running instance\n"); - printf("\n"); - printf("Included sets:\n"); - if constexpr (features::EnableBongocatEmbeddedAssets) { - printf(" %8s - Classic Bongo cat\n", "bongocat"); - } - if constexpr (features::EnableDmEmbeddedAssets) { - if constexpr (features::EnableDmEmbeddedAssets) { - printf(" %8s - Digital Monster Original\n", "dm"); - } - if constexpr (features::EnableDm20EmbeddedAssets) { - printf(" %8s - Digital Monster Ver.20th\n", "dm20"); - } - if constexpr (features::EnableDmxEmbeddedAssets) { - printf(" %8s - Digital Monster X\n", "dmx"); - } - if constexpr (features::EnablePenEmbeddedAssets) { - printf(" %8s - Digimon Pendulum\n", "pen"); - } - if constexpr (features::EnablePen20EmbeddedAssets) { - printf(" %8s - Digimon Pendulum Ver.20th\n", "pen20"); - } - if constexpr (features::EnableDmcEmbeddedAssets) { - printf(" %8s - Digital Monster Color\n", "dmc"); - } - if constexpr (features::EnableDmAllEmbeddedAssets) { - printf(" %8s - Custom Digital Monster Colored (fan sprites)\n", "dmall"); - } - } - if constexpr (features::EnablePkmnEmbeddedAssets) { - printf(" %8s - Pokemon, up to Gen 5\n", "pkmn"); - } - if constexpr (features::EnablePmdEmbeddedAssets) { - printf(" %8s - Pokemon Mystery Dungeon, up to Gen 8 (fan sprites)\n", "pmd"); - } - if constexpr (features::EnableMsAgentEmbeddedAssets) { - printf(" %8s - MS Agent\n", "ms_agent"); - } - printf("\n"); - - ::free(base_program_name); - } - - static void cli_show_version() { - printf("bongocat version %s\n", BONGOCAT_VERSION); - } - - static created_result_t cli_parse_arguments(int argc, char *argv[]) { - cli_args_t args{}; - - for (int i = 1; i < argc; i++) { - if (strcmp(argv[i], "--help") == 0 || strcmp(argv[i], "-h") == 0) { - args.show_help = true; - } else if (strcmp(argv[i], "--version") == 0 || strcmp(argv[i], "-v") == 0) { - args.show_version = true; - } else if (strcmp(argv[i], "--config") == 0 || strcmp(argv[i], "-c") == 0) { - args.config_file_set = true; - if (i + 1 < argc) { - args.config_file = argv[i + 1]; - i++; // Skip the next argument since it's the config file path - } else { - BONGOCAT_LOG_ERROR("--config option requires a file path"); - return bongocat_error_t::BONGOCAT_ERROR_INVALID_PARAM; - } - } else if (strcmp(argv[i], "--watch-config") == 0 || strcmp(argv[i], "-w") == 0) { - args.watch_config = true; - } else if (strcmp(argv[i], "--toggle") == 0 || strcmp(argv[i], "-t") == 0) { - args.toggle_mode = true; - } else if (strcmp(argv[i], "--random") == 0) { - args.randomize_index = 1; - } else if (strcmp(argv[i], "--strict") == 0) { - args.strict = 1; - } else if (strcmp(argv[i], "--ignore-running") == 0) { - args.ignore_running = true; - } else if (strcmp(argv[i], "--nr") == 0) { - args.nr_set = true; - if (i + 1 < argc) { - char *endptr{nullptr}; - args.nr = strtoll(argv[i + 1], &endptr, 10); - if (*endptr != '\0' || errno == ERANGE) { - BONGOCAT_LOG_ERROR("--nr option requires a valid number"); - return bongocat_error_t::BONGOCAT_ERROR_INVALID_PARAM; - } - i++; // Skip the next argument since it's the nr value - } else { - BONGOCAT_LOG_ERROR("--nr option requires a number"); - return bongocat_error_t::BONGOCAT_ERROR_INVALID_PARAM; - } - } else if (strcmp(argv[i], "--output-name") == 0 || strcmp(argv[i], "-o") == 0) { - args.output_name_set = true; - if (i + 1 < argc) { - args.output_name = argv[i + 1]; - i++; // Skip the next argument since it's the output name - } else { - BONGOCAT_LOG_ERROR("--output-name option requires a output name"); - return bongocat_error_t::BONGOCAT_ERROR_INVALID_PARAM; - } - } else { - BONGOCAT_LOG_WARNING("Unknown argument: %s", argv[i]); - } - } + BONGOCAT_LOG_INFO("Performing cleanup..."); + process_remove_pid_file(ctx.pid_filename); + // clean up context before global cleanup (log mutex, etc.) + cleanup(ctx); - return args; - } + BONGOCAT_LOG_INFO("Cleanup complete, exiting with code %d", exit_code); + usleep(WAIT_FOR_FLUSH_BEFORE_EXIT_MS * 1000); + exit(exit_code); } // ============================================================================= -// MAIN APPLICATION ENTRY POINT +// COMMAND LINE PROCESSING MODULE // ============================================================================= -int main(int argc, char *argv[]) { - using namespace bongocat; - // Initialize error system early - bongocat::error_init(true); // Enable debug initially - - // Parse command line arguments - const auto [args, args_result] = cli_parse_arguments(argc, argv); - if (args_result != bongocat_error_t::BONGOCAT_SUCCESS) { - return EXIT_FAILURE; +static void cli_show_help(const char *program_name) { + char *base_program_name = strdup(program_name); + if (base_program_name == BONGOCAT_NULLPTR) [[unlikely]] { + perror("strdup"); + return; + } + + printf("Bongo Cat Wayland Overlay.\n\n"); + printf("Usage: %s [OPTIONS]\n\n", basename(base_program_name)); + printf("Options:\n"); + printf(" -h, --help Show this help message\n"); + printf(" -v, --version Show version information\n"); + printf(" -c, --config Specify config file (default: ~/.config/bongocat.conf)\n"); + printf(" -w, --watch-config Watch config file for changes and reload automatically\n"); + printf(" -t, --toggle Toggle bongocat on/off (start if not running, stop if running)\n"); + printf(" -o, --output-name NAME Specify output name (overwrite output_name from config)\n"); + printf( + " --random Enable random animation_index, at start (overwrite random_index from config)\n"); + printf(" --strict Enable strict mode, only start up with a valid config and valid parameter\n"); + printf(" --nr NR Specify Nr. for PID file to avoid conflicting ruinning instances\n"); + printf(" --ignore-running Ignore current running instance\n"); + printf("\n"); + printf("Included sets:\n"); + if constexpr (features::EnableBongocatEmbeddedAssets) { + printf(" %8s - Classic Bongo cat\n", "bongocat"); + } + if constexpr (features::EnableDmEmbeddedAssets) { + if constexpr (features::EnableDmEmbeddedAssets) { + printf(" %8s - Digital Monster Original\n", "dm"); } - - // Handle help and version requests - if (args.show_help) { - cli_show_help(argv[0]); - return EXIT_SUCCESS; + if constexpr (features::EnableDm20EmbeddedAssets) { + printf(" %8s - Digital Monster Ver.20th\n", "dm20"); } - if (args.show_version) { - cli_show_version(); - return EXIT_SUCCESS; + if constexpr (features::EnableDmxEmbeddedAssets) { + printf(" %8s - Digital Monster X\n", "dmx"); } - - BONGOCAT_LOG_INFO("Starting Bongo Cat Overlay v%s", BONGOCAT_VERSION); - - main_context_t& ctx = get_main_context(); - - // Load configuration - ctx.overwrite_config_parameters = { - .output_name = args.output_name, - .randomize_index = args.randomize_index, - .strict = args.strict, - }; - if (args.config_file == nullptr) { - get_main_context().default_config_filename = default_config_file_path(); + if constexpr (features::EnablePenEmbeddedAssets) { + printf(" %8s - Digimon Pendulum\n", "pen"); } - const char* config_file = args.config_file == nullptr ? get_main_context().default_config_filename : args.config_file; - if (args.strict) { - if (strcmp(config_file, "-") != 0 && access(config_file, F_OK) != 0) { - BONGOCAT_LOG_ERROR("Configuration file required: %s", config_file); - return EXIT_FAILURE; - } + if constexpr (features::EnablePen20EmbeddedAssets) { + printf(" %8s - Digimon Pendulum Ver.20th\n", "pen20"); } - - auto [config, config_error] = config::load(config_file, ctx.overwrite_config_parameters); - if (config_error != bongocat_error_t::BONGOCAT_SUCCESS) { - BONGOCAT_LOG_ERROR("Failed to load configuration: %s", bongocat::error_string(config_error)); - return EXIT_FAILURE; + if constexpr (features::EnableDmcEmbeddedAssets) { + printf(" %8s - Digital Monster Color\n", "dmc"); } - ctx.config = bongocat::move(config); - bongocat::error_init(ctx.config.enable_debug); - - // validate args - if (config._strict) { - if (args.nr_set && args.nr < 0) { - BONGOCAT_LOG_ERROR("--nr needs to be a positive number"); - return EXIT_FAILURE; - } - if (args.output_name_set && (!args.output_name || strlen(args.output_name) <= 0)) { - BONGOCAT_LOG_ERROR("--output_name value is missing"); - return EXIT_FAILURE; - } + if constexpr (features::EnableDmAllEmbeddedAssets) { + printf(" %8s - Custom Digital Monster Colored (fan sprites)\n", "dmall"); } + } + if constexpr (features::EnablePkmnEmbeddedAssets) { + printf(" %8s - Pokemon, up to Gen 5\n", "pkmn"); + } + if constexpr (features::EnablePmdEmbeddedAssets) { + printf(" %8s - Pokemon Mystery Dungeon, up to Gen 8 (fan sprites)\n", "pmd"); + } + if constexpr (features::EnableMsAgentEmbeddedAssets) { + printf(" %8s - MS Agent\n", "ms_agent"); + } + printf("\n"); + + ::free(base_program_name); +} - if (args.nr >= 0) { - // set pid file, based on nr - const int needed_size = snprintf(nullptr, 0, PID_FILE_WITH_SUFFIX_NR_TEMPLATE, args.nr) + 1; - assert(needed_size >= 0); - ctx.pid_filename = static_cast(::malloc(static_cast(needed_size))); - if (ctx.pid_filename != nullptr) { - snprintf(ctx.pid_filename, static_cast(needed_size), PID_FILE_WITH_SUFFIX_NR_TEMPLATE, args.nr); - } - } else if (ctx.config.output_name && ctx.config.output_name[0] != '\0') { - // set pid file, based on output_name - if (!args.ignore_running) { - const int needed_size = snprintf(nullptr, 0, PID_FILE_WITH_SUFFIX_TEMPLATE, ctx.config.output_name) + 1; - assert(needed_size >= 0); - ctx.pid_filename = static_cast(::malloc(static_cast(needed_size))); - if (ctx.pid_filename != nullptr) { - snprintf(ctx.pid_filename, static_cast(needed_size), PID_FILE_WITH_SUFFIX_TEMPLATE, ctx.config.output_name); - } - } else { - const int needed_size = snprintf(nullptr, 0, PID_FILE_WITH_SUFFIX_MULTI_TEMPLATE, ctx.config.output_name, platform::slow_rand()) + 1; - assert(needed_size >= 0); - ctx.pid_filename = static_cast(::malloc(static_cast(needed_size))); - if (ctx.pid_filename != nullptr) { - snprintf(ctx.pid_filename, static_cast(needed_size), PID_FILE_WITH_SUFFIX_MULTI_TEMPLATE, ctx.config.output_name, platform::slow_rand()); - } - } +static void cli_show_version() { + printf("bongocat version %s\n", BONGOCAT_VERSION); +} - if (ctx.pid_filename == nullptr) { - BONGOCAT_LOG_ERROR("Failed to allocate PID filename"); - return EXIT_FAILURE; +static created_result_t cli_parse_arguments(int argc, char *argv[]) { + cli_args_t args{}; + + for (int i = 1; i < argc; i++) { + if (strcmp(argv[i], "--help") == 0 || strcmp(argv[i], "-h") == 0) { + args.show_help = true; + } else if (strcmp(argv[i], "--version") == 0 || strcmp(argv[i], "-v") == 0) { + args.show_version = true; + } else if (strcmp(argv[i], "--config") == 0 || strcmp(argv[i], "-c") == 0) { + args.config_file_set = true; + if (i + 1 < argc) { + args.config_file = argv[i + 1]; + i++; // Skip the next argument since it's the config file path + } else { + BONGOCAT_LOG_ERROR("--config option requires a file path"); + return bongocat_error_t::BONGOCAT_ERROR_INVALID_PARAM; + } + } else if (strcmp(argv[i], "--watch-config") == 0 || strcmp(argv[i], "-w") == 0) { + args.watch_config = true; + } else if (strcmp(argv[i], "--toggle") == 0 || strcmp(argv[i], "-t") == 0) { + args.toggle_mode = true; + } else if (strcmp(argv[i], "--random") == 0) { + args.randomize_index = 1; + } else if (strcmp(argv[i], "--strict") == 0) { + args.strict = 1; + } else if (strcmp(argv[i], "--ignore-running") == 0) { + args.ignore_running = true; + } else if (strcmp(argv[i], "--nr") == 0) { + args.nr_set = true; + if (i + 1 < argc) { + char *endptr{BONGOCAT_NULLPTR}; + args.nr = strtoll(argv[i + 1], &endptr, 10); + if (*endptr != '\0' || errno == ERANGE) { + BONGOCAT_LOG_ERROR("--nr option requires a valid number"); + return bongocat_error_t::BONGOCAT_ERROR_INVALID_PARAM; } + i++; // Skip the next argument since it's the nr value + } else { + BONGOCAT_LOG_ERROR("--nr option requires a number"); + return bongocat_error_t::BONGOCAT_ERROR_INVALID_PARAM; + } + } else if (strcmp(argv[i], "--output-name") == 0 || strcmp(argv[i], "-o") == 0) { + args.output_name_set = true; + if (i + 1 < argc) { + args.output_name = argv[i + 1]; + i++; // Skip the next argument since it's the output name + } else { + BONGOCAT_LOG_ERROR("--output-name option requires a output name"); + return bongocat_error_t::BONGOCAT_ERROR_INVALID_PARAM; + } } else { - ctx.pid_filename = strdup(DEFAULT_PID_FILE); + BONGOCAT_LOG_WARNING("Unknown argument: %s", argv[i]); } + } - // Handle toggle mode - if (args.toggle_mode) { - BONGOCAT_LOG_INFO("Toggle... (pid=%s)", ctx.pid_filename); - if (const int toggle_result = process_handle_toggle(argv[0], ctx.pid_filename); toggle_result >= 0) { - return toggle_result; // Either successfully toggled off or error - } - // toggle_result == -1 means continue with startup - } + return args; +} +} // namespace bongocat - // Create PID file to track this instance - const platform::FileDescriptor pid_fd = process_create_pid_file(ctx.pid_filename); - if (pid_fd._fd < 0) { - BONGOCAT_LOG_ERROR("Failed to create PID file"); - return EXIT_FAILURE; +// ============================================================================= +// MAIN APPLICATION ENTRY POINT +// ============================================================================= + +int main(int argc, char *argv[]) { + using namespace bongocat; + // Initialize error system early + bongocat::error_init(true); // Enable debug initially + + // Parse command line arguments + const auto [args, args_result] = cli_parse_arguments(argc, argv); + if (args_result != bongocat_error_t::BONGOCAT_SUCCESS) { + return EXIT_FAILURE; + } + + // Handle help and version requests + if (args.show_help) { + cli_show_help(argv[0]); + return EXIT_SUCCESS; + } + if (args.show_version) { + cli_show_version(); + return EXIT_SUCCESS; + } + + BONGOCAT_LOG_INFO("Starting Bongo Cat Overlay v%s", BONGOCAT_VERSION); + + main_context_t& ctx = get_main_context(); + + // Load configuration + ctx.overwrite_config_parameters = { + .output_name = args.output_name, + .randomize_index = args.randomize_index, + .strict = args.strict, + }; + if (args.config_file == BONGOCAT_NULLPTR) { + /// @TODO: RAII default_config_filename string + get_main_context().default_config_filename = default_config_file_path(); + } + const char *config_file = + args.config_file == BONGOCAT_NULLPTR ? get_main_context().default_config_filename : args.config_file; + if (args.strict >= 1) { + if (strcmp(config_file, "-") != 0 && access(config_file, F_OK) != 0) { + BONGOCAT_LOG_ERROR("Configuration file required: %s", config_file); + if (args.config_file == BONGOCAT_NULLPTR && get_main_context().default_config_filename != BONGOCAT_NULLPTR) { + ::free(get_main_context().default_config_filename); + get_main_context().default_config_filename = BONGOCAT_NULLPTR; + } + return EXIT_FAILURE; } - if (!args.ignore_running) { - if (pid_fd._fd == -2) { - BONGOCAT_LOG_ERROR("Another instance of bongocat is already running"); - return EXIT_FAILURE; - } + } + + auto [config, config_error] = config::load(config_file, ctx.overwrite_config_parameters); + if (config_error != bongocat_error_t::BONGOCAT_SUCCESS) { + BONGOCAT_LOG_ERROR("Failed to load configuration: %s", bongocat::error_string(config_error)); + if (args.config_file == BONGOCAT_NULLPTR && get_main_context().default_config_filename != BONGOCAT_NULLPTR) { + ::free(get_main_context().default_config_filename); + get_main_context().default_config_filename = BONGOCAT_NULLPTR; } - BONGOCAT_LOG_INFO("PID file created: %s", ctx.pid_filename); - BONGOCAT_LOG_INFO("bongocat PID: %d", getpid()); - - // Setup signal handlers - ctx.signal_watch_path = config_file; - bongocat_error_t signal_result = signal_setup_handlers(ctx); - if (signal_result != bongocat_error_t::BONGOCAT_SUCCESS) { - BONGOCAT_LOG_ERROR("Failed to setup signal handlers: %s", bongocat::error_string(signal_result)); - return EXIT_FAILURE; - } - BONGOCAT_LOG_INFO("Signal handler configure (fd=%i)", ctx.signal_fd._fd); - - // Initialize config watcher if requested - if (args.watch_config) { - if (strcmp(config_file, "-") != 0) { - start_config_watcher(ctx, config_file); - } else { - BONGOCAT_LOG_INFO("Skip config watcher, no config watcher for stdin"); - } - } else { - BONGOCAT_LOG_INFO("No config watcher, continuing without hot-reload"); + return EXIT_FAILURE; + } + ctx.config = bongocat::move(config); + bongocat::error_init(ctx.config.enable_debug >= 1); + + // validate args + if (config._strict) { + if (args.nr_set && args.nr < 0) { + if (args.config_file == BONGOCAT_NULLPTR && get_main_context().default_config_filename != BONGOCAT_NULLPTR) { + ::free(get_main_context().default_config_filename); + get_main_context().default_config_filename = BONGOCAT_NULLPTR; + } + BONGOCAT_LOG_ERROR("--nr needs to be a positive number"); + return EXIT_FAILURE; } - - // Initialize all system components - bongocat_error_t result = system_initialize_components(ctx); - if (result != bongocat_error_t::BONGOCAT_SUCCESS) { - system_cleanup_and_exit(ctx, EXIT_FAILURE); + if (args.output_name_set && (args.output_name == BONGOCAT_NULLPTR || strlen(args.output_name) <= 0)) { + if (args.config_file == BONGOCAT_NULLPTR && get_main_context().default_config_filename != BONGOCAT_NULLPTR) { + ::free(get_main_context().default_config_filename); + get_main_context().default_config_filename = BONGOCAT_NULLPTR; + } + BONGOCAT_LOG_ERROR("--output_name value is missing"); + return EXIT_FAILURE; } - - assert(ctx.input != nullptr); - assert(ctx.animation != nullptr); - assert(ctx.wayland != nullptr); - - if (abs(ctx.config.cat_x_offset) > ctx.wayland->wayland_context._screen_width) { - BONGOCAT_LOG_WARNING("cat_x_offset %d may position cat off-screen (screen width: %d)", - ctx.config.cat_x_offset, ctx.wayland->wayland_context._screen_width); + } + + if (args.nr >= 0) { + // set pid file, based on nr + const int needed_size = snprintf(BONGOCAT_NULLPTR, 0, PID_FILE_WITH_SUFFIX_NR_TEMPLATE, args.nr) + 1; + assert(needed_size >= 0); + ctx.pid_filename = static_cast(::malloc(static_cast(needed_size))); + if (ctx.pid_filename != BONGOCAT_NULLPTR) { + snprintf(ctx.pid_filename, static_cast(needed_size), PID_FILE_WITH_SUFFIX_NR_TEMPLATE, args.nr); } - - BONGOCAT_LOG_INFO("Bar dimensions: %dx%d", ctx.wayland->wayland_context._screen_width, ctx.config.overlay_height); - - BONGOCAT_LOG_INFO("Bongo Cat Overlay configured successfully"); - - // trigger initial rendering - platform::wayland::request_render(*ctx.animation); - // Main Wayland event loop with graceful shutdown - assert(ctx.wayland != nullptr); - assert(ctx.input != nullptr); - /// @NOTE: config_watcher os optional - result = run(*ctx.wayland, ctx.running, ctx.signal_fd._fd, *ctx.input, ctx.config, ctx.config_watcher.ptr, config_reload_callback); - if (result != bongocat_error_t::BONGOCAT_SUCCESS) { - BONGOCAT_LOG_ERROR("Wayland event loop error: %s", bongocat::error_string(result)); - system_cleanup_and_exit(ctx, EXIT_FAILURE); + } else if (ctx.config.output_name != BONGOCAT_NULLPTR && ctx.config.output_name[0] != '\0') { + // set pid file, based on output_name + if (!args.ignore_running) { + const int needed_size = snprintf(BONGOCAT_NULLPTR, 0, PID_FILE_WITH_SUFFIX_TEMPLATE, ctx.config.output_name) + 1; + assert(needed_size >= 0); + ctx.pid_filename = static_cast(::malloc(static_cast(needed_size))); + if (ctx.pid_filename != BONGOCAT_NULLPTR) { + snprintf(ctx.pid_filename, static_cast(needed_size), PID_FILE_WITH_SUFFIX_TEMPLATE, + ctx.config.output_name); + } + } else { + const int needed_size = snprintf(BONGOCAT_NULLPTR, 0, PID_FILE_WITH_SUFFIX_MULTI_TEMPLATE, ctx.config.output_name, + platform::slow_rand()) + + 1; + assert(needed_size >= 0); + ctx.pid_filename = static_cast(::malloc(static_cast(needed_size))); + if (ctx.pid_filename != BONGOCAT_NULLPTR) { + snprintf(ctx.pid_filename, static_cast(needed_size), PID_FILE_WITH_SUFFIX_MULTI_TEMPLATE, + ctx.config.output_name, platform::slow_rand()); + } } - - BONGOCAT_LOG_INFO("Main loop exited, shutting down"); - system_cleanup_and_exit(ctx, EXIT_SUCCESS); - // Never reached - //return EXIT_SUCCESS; + if (ctx.pid_filename == BONGOCAT_NULLPTR) { + if (args.config_file == BONGOCAT_NULLPTR && get_main_context().default_config_filename != BONGOCAT_NULLPTR) { + ::free(get_main_context().default_config_filename); + get_main_context().default_config_filename = BONGOCAT_NULLPTR; + } + BONGOCAT_LOG_ERROR("Failed to allocate PID filename"); + return EXIT_FAILURE; + } + } else { + ctx.pid_filename = strdup(DEFAULT_PID_FILE); + } + + // Handle toggle mode + if (args.toggle_mode) { + BONGOCAT_LOG_INFO("Toggle... (pid=%s)", ctx.pid_filename); + if (const int toggle_result = process_handle_toggle(argv[0], ctx.pid_filename); toggle_result >= 0) { + return toggle_result; // Either successfully toggled off or error + } + // toggle_result == -1 means continue with startup + } + + // Create PID file to track this instance + const platform::FileDescriptor pid_fd = process_create_pid_file(ctx.pid_filename); + if (pid_fd._fd < 0) { + if (args.config_file == BONGOCAT_NULLPTR && get_main_context().default_config_filename != BONGOCAT_NULLPTR) { + ::free(get_main_context().default_config_filename); + get_main_context().default_config_filename = BONGOCAT_NULLPTR; + } + BONGOCAT_LOG_ERROR("Failed to create PID file"); + return EXIT_FAILURE; + } + if (!args.ignore_running) { + if (pid_fd._fd == -2) { + if (args.config_file == BONGOCAT_NULLPTR && get_main_context().default_config_filename != BONGOCAT_NULLPTR) { + ::free(get_main_context().default_config_filename); + get_main_context().default_config_filename = BONGOCAT_NULLPTR; + } + BONGOCAT_LOG_ERROR("Another instance of bongocat is already running"); + return EXIT_FAILURE; + } + } + BONGOCAT_LOG_INFO("PID file created: %s", ctx.pid_filename); + BONGOCAT_LOG_INFO("bongocat PID: %d", getpid()); + + // Setup signal handlers + ctx.signal_watch_path = config_file; + const bongocat_error_t signal_result = signal_setup_handlers(ctx); + if (signal_result != bongocat_error_t::BONGOCAT_SUCCESS) { + if (args.config_file == BONGOCAT_NULLPTR && get_main_context().default_config_filename != BONGOCAT_NULLPTR) { + ::free(get_main_context().default_config_filename); + get_main_context().default_config_filename = BONGOCAT_NULLPTR; + } + BONGOCAT_LOG_ERROR("Failed to setup signal handlers: %s", bongocat::error_string(signal_result)); + return EXIT_FAILURE; + } + BONGOCAT_LOG_INFO("Signal handler configure (fd=%i)", ctx.signal_fd._fd); + + // Initialize config watcher if requested + if (args.watch_config) { + if (strcmp(config_file, "-") != 0) { + start_config_watcher(ctx, config_file); + } else { + BONGOCAT_LOG_INFO("Skip config watcher, no config watcher for stdin"); + } + } else { + BONGOCAT_LOG_INFO("No config watcher, continuing without hot-reload"); + } + + // Initialize all system components + bongocat_error_t result = system_initialize_components(ctx); + if (result != bongocat_error_t::BONGOCAT_SUCCESS) { + system_cleanup_and_exit(ctx, EXIT_FAILURE); + } + + assert(ctx.input != BONGOCAT_NULLPTR); + assert(ctx.animation != BONGOCAT_NULLPTR); + assert(ctx.wayland != BONGOCAT_NULLPTR); + + if (abs(ctx.config.cat_x_offset) > ctx.wayland->thread_context._screen_width) { + BONGOCAT_LOG_WARNING("cat_x_offset %d may position cat off-screen (screen width: %d)", ctx.config.cat_x_offset, + ctx.wayland->thread_context._screen_width); + } + + BONGOCAT_LOG_INFO("Bar dimensions: %dx%d", ctx.wayland->thread_context._screen_width, ctx.config.overlay_height); + + BONGOCAT_LOG_INFO("Bongo Cat Overlay configured successfully"); + + // trigger initial rendering + platform::wayland::request_render(*ctx.animation); + // Main Wayland event loop with graceful shutdown + assert(ctx.wayland != BONGOCAT_NULLPTR); + assert(ctx.input != BONGOCAT_NULLPTR); + /// @NOTE: config_watcher os optional + result = run(*ctx.wayland, ctx.running, ctx.signal_fd._fd, *ctx.input, ctx.config, ctx.config_watcher.ptr, + config_reload_callback); + if (result != bongocat_error_t::BONGOCAT_SUCCESS) { + BONGOCAT_LOG_ERROR("Wayland event loop error: %s", bongocat::error_string(result)); + system_cleanup_and_exit(ctx, EXIT_FAILURE); + } + + BONGOCAT_LOG_INFO("Main loop exited, shutting down"); + system_cleanup_and_exit(ctx, EXIT_SUCCESS); + + BONGOCAT_UNREACHABLE(); + // Never reached + // return EXIT_SUCCESS; } \ No newline at end of file diff --git a/src/embedded_assets/.clang-format b/src/embedded_assets/.clang-format new file mode 100644 index 00000000..15f9b6fc --- /dev/null +++ b/src/embedded_assets/.clang-format @@ -0,0 +1,2 @@ +DisableFormat: true +SortIncludes: Never \ No newline at end of file diff --git a/src/embedded_assets/bongocat/bongocat_get_sprite_sheet.cpp b/src/embedded_assets/bongocat/bongocat_get_sprite_sheet.cpp index be25d90b..5aff3b8c 100644 --- a/src/embedded_assets/bongocat/bongocat_get_sprite_sheet.cpp +++ b/src/embedded_assets/bongocat/bongocat_get_sprite_sheet.cpp @@ -1,17 +1,22 @@ -#include "embedded_assets/embedded_image.h" #include "embedded_assets/bongocat/bongocat.h" #include "embedded_assets/bongocat/bongocat_images.h" +#include "embedded_assets/embedded_image.h" namespace bongocat::assets { - embedded_image_t get_bongocat_sprite(size_t i) { - using namespace assets; - switch (i) { - case BONGOCAT_FRAME_BOTH_UP: return {bongo_cat_both_up_png, bongo_cat_both_up_png_size, "bongo-cat-both-up"}; - case BONGOCAT_FRAME_LEFT_DOWN: return {bongo_cat_left_down_png, bongo_cat_left_down_png_size, "bongo-cat-left-down"}; - case BONGOCAT_FRAME_RIGHT_DOWN: return {bongo_cat_right_down_png, bongo_cat_right_down_png_size, "bongo-cat-right-down"}; - case BONGOCAT_FRAME_BOTH_DOWN: return {bongo_cat_both_down_png, bongo_cat_both_down_png_size, "bongo-cat-both-down"}; - default: return {}; - } - return { }; - } -} \ No newline at end of file +embedded_image_t get_bongocat_sprite(size_t i) { + using namespace assets; + switch (i) { + case BONGOCAT_FRAME_BOTH_UP: + return {bongo_cat_both_up_png, bongo_cat_both_up_png_size, "bongo-cat-both-up"}; + case BONGOCAT_FRAME_LEFT_DOWN: + return {bongo_cat_left_down_png, bongo_cat_left_down_png_size, "bongo-cat-left-down"}; + case BONGOCAT_FRAME_RIGHT_DOWN: + return {bongo_cat_right_down_png, bongo_cat_right_down_png_size, "bongo-cat-right-down"}; + case BONGOCAT_FRAME_BOTH_DOWN: + return {bongo_cat_both_down_png, bongo_cat_both_down_png_size, "bongo-cat-both-down"}; + default: + return {}; + } + return {}; +} +} // namespace bongocat::assets \ No newline at end of file diff --git a/src/embedded_assets/bongocat/bongocat_images.c b/src/embedded_assets/bongocat/bongocat_images.c index b83e1dd8..a5768bb6 100644 --- a/src/embedded_assets/bongocat/bongocat_images.c +++ b/src/embedded_assets/bongocat/bongocat_images.c @@ -1,6 +1,7 @@ -#include #include "embedded_assets/bongocat/bongocat_images.h" +#include + // Embedded asset data const unsigned char bongo_cat_both_up_png[] = { #embed "../../../assets/bongo-cat-both-up.png" diff --git a/src/embedded_assets/min_dm/min_dm_get_sprite_sheet.cpp b/src/embedded_assets/min_dm/min_dm_get_sprite_sheet.cpp index a585255d..be7d4955 100644 --- a/src/embedded_assets/min_dm/min_dm_get_sprite_sheet.cpp +++ b/src/embedded_assets/min_dm/min_dm_get_sprite_sheet.cpp @@ -4,25 +4,40 @@ #include "embedded_assets/min_dm/min_dm_sprite.h" namespace bongocat::assets { - embedded_image_t get_min_dm_sprite_sheet(size_t i) { - using namespace assets; - switch (i) { - case DM_BOTAMON_ANIM_INDEX: return {dm_botamon_png, dm_botamon_png_size, "botamon"}; - case DM_KOROMON_ANIM_INDEX: return {dm_koromon_png, dm_koromon_png_size, "koromon"}; - case DM_AGUMON_ANIM_INDEX: return {dm_agumon_png, dm_agumon_png_size, "agumon"}; - case DM_BETAMON_ANIM_INDEX: return {dm_betamon_png, dm_betamon_png_size, "betamon"}; - case DM_GREYMON_ANIM_INDEX: return {dm_greymon_png, dm_greymon_png_size, "greymon"}; - case DM_TYRANOMON_ANIM_INDEX: return {dm_tyranomon_png, dm_tyranomon_png_size, "tyranomon"}; - case DM_DEVIMON_ANIM_INDEX: return {dm_devimon_png, dm_devimon_png_size, "devimon"}; - case DM_MERAMON_ANIM_INDEX: return {dm_meramon_png, dm_meramon_png_size, "meramon"}; - case DM_AIRDRAMON_ANIM_INDEX: return {dm_airdramon_png, dm_airdramon_png_size, "airdramon"}; - case DM_SEADRAMON_ANIM_INDEX: return {dm_seadramon_png, dm_seadramon_png_size, "seadramon"}; - case DM_NUMEMON_ANIM_INDEX: return {dm_numemon_png, dm_numemon_png_size, "numemon"}; - case DM_METAL_GREYMON_ANIM_INDEX: return {dm_metal_greymon_png, dm_metal_greymon_png_size, "metal_greymon"}; - case DM_MAMEMON_ANIM_INDEX: return {dm_mamemon_png, dm_mamemon_png_size, "mamemon"}; - case DM_MONZAEMON_ANIM_INDEX: return {dm_monzaemon_png, dm_monzaemon_png_size, "monzaemon"}; - default: return { nullptr, 0, "" }; - } - return { nullptr, 0, "" }; - } -} \ No newline at end of file +embedded_image_t get_min_dm_sprite_sheet(size_t i) { + using namespace assets; + switch (i) { + case DM_BOTAMON_ANIM_INDEX: + return {dm_botamon_png, dm_botamon_png_size, "botamon"}; + case DM_KOROMON_ANIM_INDEX: + return {dm_koromon_png, dm_koromon_png_size, "koromon"}; + case DM_AGUMON_ANIM_INDEX: + return {dm_agumon_png, dm_agumon_png_size, "agumon"}; + case DM_BETAMON_ANIM_INDEX: + return {dm_betamon_png, dm_betamon_png_size, "betamon"}; + case DM_GREYMON_ANIM_INDEX: + return {dm_greymon_png, dm_greymon_png_size, "greymon"}; + case DM_TYRANOMON_ANIM_INDEX: + return {dm_tyranomon_png, dm_tyranomon_png_size, "tyranomon"}; + case DM_DEVIMON_ANIM_INDEX: + return {dm_devimon_png, dm_devimon_png_size, "devimon"}; + case DM_MERAMON_ANIM_INDEX: + return {dm_meramon_png, dm_meramon_png_size, "meramon"}; + case DM_AIRDRAMON_ANIM_INDEX: + return {dm_airdramon_png, dm_airdramon_png_size, "airdramon"}; + case DM_SEADRAMON_ANIM_INDEX: + return {dm_seadramon_png, dm_seadramon_png_size, "seadramon"}; + case DM_NUMEMON_ANIM_INDEX: + return {dm_numemon_png, dm_numemon_png_size, "numemon"}; + case DM_METAL_GREYMON_ANIM_INDEX: + return {dm_metal_greymon_png, dm_metal_greymon_png_size, "metal_greymon"}; + case DM_MAMEMON_ANIM_INDEX: + return {dm_mamemon_png, dm_mamemon_png_size, "mamemon"}; + case DM_MONZAEMON_ANIM_INDEX: + return {dm_monzaemon_png, dm_monzaemon_png_size, "monzaemon"}; + default: + return {nullptr, 0, ""}; + } + return {nullptr, 0, ""}; +} +} // namespace bongocat::assets \ No newline at end of file diff --git a/src/embedded_assets/min_dm/min_dm_images.c b/src/embedded_assets/min_dm/min_dm_images.c index d536406e..b2d1cc45 100644 --- a/src/embedded_assets/min_dm/min_dm_images.c +++ b/src/embedded_assets/min_dm/min_dm_images.c @@ -1,4 +1,5 @@ #include "embedded_assets/min_dm/min_dm_images.h" + #include // Botamon diff --git a/src/embedded_assets/misc/misc_get_sprite_sheet.cpp b/src/embedded_assets/misc/misc_get_sprite_sheet.cpp index d795f1ea..d44df681 100644 --- a/src/embedded_assets/misc/misc_get_sprite_sheet.cpp +++ b/src/embedded_assets/misc/misc_get_sprite_sheet.cpp @@ -4,21 +4,25 @@ #include "embedded_assets/misc/misc_sprite.h" namespace bongocat::assets { - embedded_image_t get_misc_sprite_sheet(size_t i) { - using namespace assets; - switch (i) { - case MISC_NEKO_ANIM_INDEX: return {misc_neko_png, misc_neko_png_size, "neko"}; - default: return { nullptr, 0, "" }; - } - return { nullptr, 0, "" }; - } +embedded_image_t get_misc_sprite_sheet(size_t i) { + using namespace assets; + switch (i) { + case MISC_NEKO_ANIM_INDEX: + return {misc_neko_png, misc_neko_png_size, "neko"}; + default: + return {nullptr, 0, ""}; + } + return {nullptr, 0, ""}; +} - custom_animation_settings_t get_misc_sprite_sheet_columns(size_t i) { - using namespace assets; - switch (i) { - case MISC_NEKO_ANIM_INDEX: return MISC_NEKO_SPRITE_SHEET_SETTINGS; - default: return {}; - } - return {}; - } -} \ No newline at end of file +custom_animation_settings_t get_misc_sprite_sheet_columns(size_t i) { + using namespace assets; + switch (i) { + case MISC_NEKO_ANIM_INDEX: + return MISC_NEKO_SPRITE_SHEET_SETTINGS; + default: + return {}; + } + return {}; +} +} // namespace bongocat::assets \ No newline at end of file diff --git a/src/embedded_assets/misc/misc_images.c b/src/embedded_assets/misc/misc_images.c index b0d78d65..05ebfd71 100644 --- a/src/embedded_assets/misc/misc_images.c +++ b/src/embedded_assets/misc/misc_images.c @@ -1,4 +1,5 @@ #include "embedded_assets/misc/misc_images.h" + #include // neko @@ -6,4 +7,3 @@ const unsigned char misc_neko_png[] = { #embed "../../../assets/misc/neko.png" }; const size_t misc_neko_png_size = sizeof(misc_neko_png); - diff --git a/src/embedded_assets/ms_agent/embedded_assets_ms_agent.cpp b/src/embedded_assets/ms_agent/embedded_assets_ms_agent.cpp index d2cd8626..afca7952 100644 --- a/src/embedded_assets/ms_agent/embedded_assets_ms_agent.cpp +++ b/src/embedded_assets/ms_agent/embedded_assets_ms_agent.cpp @@ -1,116 +1,126 @@ #include "embedded_assets/embedded_image.h" #include "embedded_assets/ms_agent/ms_agent.hpp" -#include "embedded_assets/ms_agent/ms_agent_sprite.h" #include "embedded_assets/ms_agent/ms_agent_images.h" +#include "embedded_assets/ms_agent/ms_agent_sprite.h" namespace bongocat::assets { - embedded_image_t get_ms_agent_sprite_sheet(size_t i) { - switch (i) { - case CLIPPY_ANIM_INDEX: return {clippy_png, clippy_png_size, "clippy"}; +embedded_image_t get_ms_agent_sprite_sheet(size_t i) { + switch (i) { + case CLIPPY_ANIM_INDEX: + return {clippy_png, clippy_png_size, "clippy"}; #ifdef FEATURE_MORE_MS_AGENT_EMBEDDED_ASSETS - case LINKS_ANIM_INDEX: return {links_png, links_png_size, "links"}; - case ROVER_ANIM_INDEX: return {rover_png, rover_png_size, "rover"}; - case MERLIN_ANIM_INDEX: return {merlin_png, merlin_png_size, "merlin"}; + case LINKS_ANIM_INDEX: + return {links_png, links_png_size, "links"}; + case ROVER_ANIM_INDEX: + return {rover_png, rover_png_size, "rover"}; + case MERLIN_ANIM_INDEX: + return {merlin_png, merlin_png_size, "merlin"}; #endif - default: return {}; - } - return {}; - } - - ms_agent_animation_indices_t get_ms_agent_animation_indices(size_t i) { - switch (i) { - case CLIPPY_ANIM_INDEX: return { - .start_index_frame_idle = 0, - .end_index_frame_idle = CLIPPY_FRAMES_IDLE-1, - - .start_index_frame_boring = 0, - .end_index_frame_boring = CLIPPY_FRAMES_BORING-1, - - .start_index_frame_start_writing = 0, - .end_index_frame_start_writing = CLIPPY_FRAMES_START_WRITING-1, - - .start_index_frame_writing = 0, - .end_index_frame_writing = CLIPPY_FRAMES_WRITING-1, - - .start_index_frame_end_writing = 0, - .end_index_frame_end_writing = CLIPPY_FRAMES_END_WRITING-1, - - .start_index_frame_sleep = 0, - .end_index_frame_sleep = CLIPPY_FRAMES_SLEEP-1, - - .start_index_frame_wake_up = 0, - .end_index_frame_wake_up = CLIPPY_FRAMES_WAKE_UP-1, - }; + default: + return {}; + } + return {}; +} + +ms_agent_animation_indices_t get_ms_agent_animation_indices(size_t i) { + switch (i) { + case CLIPPY_ANIM_INDEX: + return { + .start_index_frame_idle = 0, + .end_index_frame_idle = CLIPPY_FRAMES_IDLE - 1, + + .start_index_frame_boring = 0, + .end_index_frame_boring = CLIPPY_FRAMES_BORING - 1, + + .start_index_frame_start_writing = 0, + .end_index_frame_start_writing = CLIPPY_FRAMES_START_WRITING - 1, + + .start_index_frame_writing = 0, + .end_index_frame_writing = CLIPPY_FRAMES_WRITING - 1, + + .start_index_frame_end_writing = 0, + .end_index_frame_end_writing = CLIPPY_FRAMES_END_WRITING - 1, + + .start_index_frame_sleep = 0, + .end_index_frame_sleep = CLIPPY_FRAMES_SLEEP - 1, + + .start_index_frame_wake_up = 0, + .end_index_frame_wake_up = CLIPPY_FRAMES_WAKE_UP - 1, + }; #ifdef FEATURE_MORE_MS_AGENT_EMBEDDED_ASSETS - case LINKS_ANIM_INDEX: return { - .start_index_frame_idle = 0, - .end_index_frame_idle = LINKS_FRAMES_IDLE-1, - - .start_index_frame_boring = 0, - .end_index_frame_boring = LINKS_FRAMES_BORING-1, - - .start_index_frame_start_writing = 0, - .end_index_frame_start_writing = LINKS_FRAMES_START_WRITING-1, - - .start_index_frame_writing = 0, - .end_index_frame_writing = LINKS_FRAMES_WRITING-1, - - .start_index_frame_end_writing = 0, - .end_index_frame_end_writing = LINKS_FRAMES_END_WRITING-1, - - .start_index_frame_sleep = 0, - .end_index_frame_sleep = LINKS_FRAMES_SLEEP-1, - - .start_index_frame_wake_up = 0, - .end_index_frame_wake_up = LINKS_FRAMES_WAKE_UP-1, - }; - case ROVER_ANIM_INDEX: return { - .start_index_frame_idle = 0, - .end_index_frame_idle = ROVER_FRAMES_IDLE-1, - - .start_index_frame_boring = 0, - .end_index_frame_boring = ROVER_FRAMES_BORING-1, - - .start_index_frame_start_writing = 0, - .end_index_frame_start_writing = ROVER_FRAMES_START_WRITING-1, - - .start_index_frame_writing = 0, - .end_index_frame_writing = ROVER_FRAMES_WRITING-1, - - .start_index_frame_end_writing = 0, - .end_index_frame_end_writing = ROVER_FRAMES_END_WRITING-1, - - .start_index_frame_sleep = 0, - .end_index_frame_sleep = ROVER_FRAMES_SLEEP-1, - - .start_index_frame_wake_up = 0, - .end_index_frame_wake_up = ROVER_FRAMES_WAKE_UP-1, - }; - case MERLIN_ANIM_INDEX: return { - .start_index_frame_idle = 0, - .end_index_frame_idle = MERLIN_FRAMES_IDLE-1, - - .start_index_frame_boring = 0, - .end_index_frame_boring = MERLIN_FRAMES_BORING-1, - - .start_index_frame_start_writing = 0, - .end_index_frame_start_writing = MERLIN_FRAMES_START_WRITING-1, - - .start_index_frame_writing = 0, - .end_index_frame_writing = MERLIN_FRAMES_WRITING-1, - - .start_index_frame_end_writing = 0, - .end_index_frame_end_writing = MERLIN_FRAMES_END_WRITING-1, - - .start_index_frame_sleep = 0, - .end_index_frame_sleep = MERLIN_FRAMES_SLEEP-1, - - .start_index_frame_wake_up = 0, - .end_index_frame_wake_up = MERLIN_FRAMES_WAKE_UP-1, - }; + case LINKS_ANIM_INDEX: + return { + .start_index_frame_idle = 0, + .end_index_frame_idle = LINKS_FRAMES_IDLE - 1, + + .start_index_frame_boring = 0, + .end_index_frame_boring = LINKS_FRAMES_BORING - 1, + + .start_index_frame_start_writing = 0, + .end_index_frame_start_writing = LINKS_FRAMES_START_WRITING - 1, + + .start_index_frame_writing = 0, + .end_index_frame_writing = LINKS_FRAMES_WRITING - 1, + + .start_index_frame_end_writing = 0, + .end_index_frame_end_writing = LINKS_FRAMES_END_WRITING - 1, + + .start_index_frame_sleep = 0, + .end_index_frame_sleep = LINKS_FRAMES_SLEEP - 1, + + .start_index_frame_wake_up = 0, + .end_index_frame_wake_up = LINKS_FRAMES_WAKE_UP - 1, + }; + case ROVER_ANIM_INDEX: + return { + .start_index_frame_idle = 0, + .end_index_frame_idle = ROVER_FRAMES_IDLE - 1, + + .start_index_frame_boring = 0, + .end_index_frame_boring = ROVER_FRAMES_BORING - 1, + + .start_index_frame_start_writing = 0, + .end_index_frame_start_writing = ROVER_FRAMES_START_WRITING - 1, + + .start_index_frame_writing = 0, + .end_index_frame_writing = ROVER_FRAMES_WRITING - 1, + + .start_index_frame_end_writing = 0, + .end_index_frame_end_writing = ROVER_FRAMES_END_WRITING - 1, + + .start_index_frame_sleep = 0, + .end_index_frame_sleep = ROVER_FRAMES_SLEEP - 1, + + .start_index_frame_wake_up = 0, + .end_index_frame_wake_up = ROVER_FRAMES_WAKE_UP - 1, + }; + case MERLIN_ANIM_INDEX: + return { + .start_index_frame_idle = 0, + .end_index_frame_idle = MERLIN_FRAMES_IDLE - 1, + + .start_index_frame_boring = 0, + .end_index_frame_boring = MERLIN_FRAMES_BORING - 1, + + .start_index_frame_start_writing = 0, + .end_index_frame_start_writing = MERLIN_FRAMES_START_WRITING - 1, + + .start_index_frame_writing = 0, + .end_index_frame_writing = MERLIN_FRAMES_WRITING - 1, + + .start_index_frame_end_writing = 0, + .end_index_frame_end_writing = MERLIN_FRAMES_END_WRITING - 1, + + .start_index_frame_sleep = 0, + .end_index_frame_sleep = MERLIN_FRAMES_SLEEP - 1, + + .start_index_frame_wake_up = 0, + .end_index_frame_wake_up = MERLIN_FRAMES_WAKE_UP - 1, + }; #endif - default: return {}; - } - return {}; - } -} \ No newline at end of file + default: + return {}; + } + return {}; +} +} // namespace bongocat::assets \ No newline at end of file diff --git a/src/embedded_assets/ms_agent/ms_agent_images.c b/src/embedded_assets/ms_agent/ms_agent_images.c index 168aa572..65b1f68f 100644 --- a/src/embedded_assets/ms_agent/ms_agent_images.c +++ b/src/embedded_assets/ms_agent/ms_agent_images.c @@ -1,4 +1,5 @@ #include "embedded_assets/ms_agent/ms_agent_images.h" + #include // Embedded asset data @@ -7,20 +8,19 @@ const unsigned char clippy_png[] = { }; const size_t clippy_png_size = sizeof(clippy_png); - #ifdef FEATURE_MORE_MS_AGENT_EMBEDDED_ASSETS const unsigned char links_png[] = { -#embed "../../../assets/ms_agent/links.png" +# embed "../../../assets/ms_agent/links.png" }; const size_t links_png_size = sizeof(links_png); const unsigned char rover_png[] = { -#embed "../../../assets/ms_agent/rover.png" +# embed "../../../assets/ms_agent/rover.png" }; const size_t rover_png_size = sizeof(rover_png); const unsigned char merlin_png[] = { -#embed "../../../assets/ms_agent/merlin.png" +# embed "../../../assets/ms_agent/merlin.png" }; const size_t merlin_png_size = sizeof(merlin_png); #endif diff --git a/src/embedded_assets/pmd/include/pmd_config_parse_animation_name.h b/src/embedded_assets/pmd/include/pmd_config_parse_animation_name.h index 2c8b545a..78f74b8f 100644 --- a/src/embedded_assets/pmd/include/pmd_config_parse_animation_name.h +++ b/src/embedded_assets/pmd/include/pmd_config_parse_animation_name.h @@ -5,7 +5,7 @@ #include "embedded_assets/embedded_image.h" namespace bongocat::assets { - [[nodiscard]] extern config_custom_animation_entry_t get_config_animation_name_pmd(size_t i); + BONGOCAT_NODISCARD extern config_custom_animation_entry_t get_config_animation_name_pmd(size_t i); extern int config_parse_animation_name_pmd(config::config_t& config, const char *value); } diff --git a/src/graphics/animation.cpp b/src/graphics/animation.cpp index 01d2487d..5ba4e133 100644 --- a/src/graphics/animation.cpp +++ b/src/graphics/animation.cpp @@ -1,5033 +1,5229 @@ -#include "graphics/animation_context.h" #include "graphics/animation.h" -#include "platform/wayland.h" -#include "utils/time.h" -#include -#include -#include -#include -#include -#include -#include +#include "embedded_assets/bongocat/bongocat.h" #include "embedded_assets/bongocat/bongocat.hpp" +#include "embedded_assets/custom/custom_sprite.h" +#include "embedded_assets/min_dm/min_dm_sprite.h" +#include "embedded_assets/misc/misc.hpp" +#include "embedded_assets/misc/misc_sprite.h" +#include "embedded_assets/ms_agent/ms_agent.hpp" +#include "embedded_assets/ms_agent/ms_agent_sprite.h" +#include "embedded_assets/pkmn/pkmn_sprite.h" +#include "graphics/animation_thread_context.h" #include "graphics/embedded_assets_dms.h" +#include "graphics/embedded_assets_pkmn.h" #include "image_loader/bongocat/load_images_bongocat.h" -#include "image_loader/ms_agent/load_images_ms_agent.h" #include "image_loader/dm/load_images_dm.h" #include "image_loader/dm20/load_images_dm20.h" +#include "image_loader/dmc/load_images_dmc.h" #include "image_loader/dmx/load_images_dmx.h" +#include "image_loader/ms_agent/load_images_ms_agent.h" #include "image_loader/pen/load_images_pen.h" #include "image_loader/pen20/load_images_pen20.h" -#include "image_loader/dmc/load_images_dmc.h" -#include "embedded_assets/bongocat/bongocat.h" -#include "embedded_assets/custom/custom_sprite.h" -#include "embedded_assets/ms_agent/ms_agent.hpp" -#include "embedded_assets/ms_agent/ms_agent_sprite.h" -#include "embedded_assets/min_dm/min_dm_sprite.h" -#include "embedded_assets/misc/misc.hpp" -#include "graphics/embedded_assets_pkmn.h" -#include "embedded_assets/pkmn/pkmn_sprite.h" -#include "embedded_assets/misc/misc.hpp" -#include "embedded_assets/misc/misc_sprite.h" +#include "platform/wayland.h" +#include "utils/time.h" + +#include +#include +#include +#include +#include +#include +#include #if defined(FEATURE_CUSTOM_SPRITE_SHEETS) || defined(FEATURE_MISC_EMBEDDED_ASSETS) -#define FEATURE_CUSTOM_SPRITE_SHEETS_ANIMATION +# define FEATURE_CUSTOM_SPRITE_SHEETS_ANIMATION #endif namespace bongocat::animation { - // ============================================================================= - // GLOBAL STATE AND CONFIGURATION - // ============================================================================= - - inline static constexpr platform::time_ms_t POOL_MIN_TIMEOUT_MS = 5; - inline static constexpr platform::time_ms_t POOL_MAX_TIMEOUT_MS = 1000; - inline static constexpr int MAX_ATTEMPTS = 2048; - static_assert(POOL_MAX_TIMEOUT_MS >= POOL_MIN_TIMEOUT_MS); - - inline static constexpr platform::time_ms_t COND_RELOAD_CONFIGS_TIMEOUT_MS = 5000; - - inline static constexpr int CHANCE_FOR_SKIPPING_MOVEMENT_PERCENT = 30; // in percent - inline static constexpr int SMALL_MAX_DISTANCE_PER_MOVEMENT_PART = 3; // 1/3 of movement_radius (for small areas) - inline static constexpr int MAX_DISTANCE_PER_MOVEMENT_PART = 5; // 1/5 of movement_radius - static_assert(SMALL_MAX_DISTANCE_PER_MOVEMENT_PART > 0); - static_assert(MAX_DISTANCE_PER_MOVEMENT_PART > 0); - inline static constexpr int MAX_MOVEMENT_RADIUS_SMALL = 100; - inline static constexpr int FLIP_DIRECTION_NEAR_WALL_PERCENT = 70; - inline static constexpr int SMALL_FLIP_DIRECTION_NEAR_WALL_PERCENT = 40; - - inline static constexpr int SLEEP_BORING_PART = 2; // 1/2 of sleep time for triggering boring animation - - // ============================================================================= - // ANIMATION STATE MANAGEMENT MODULE - // ============================================================================= - - static bool is_sleep_time(const config::config_t& config) { - time_t raw_time; - tm time_info{}; - time(&raw_time); - localtime_r(&raw_time, &time_info); - - const int now_minutes = time_info.tm_hour * 60 + time_info.tm_min; - const int begin = config.sleep_begin.hour * 60 + config.sleep_begin.min; - const int end = config.sleep_end.hour * 60 + config.sleep_end.min; - - // Normal range (e.g., 10:00–22:00): begin < end && (now_minutes >= begin && now_minutes < end) - // Overnight range (e.g., 22:00–06:00): begin > end && (now_minutes >= begin || now_minutes < end) - - return (begin == end) || - (begin < end ? (now_minutes >= begin && now_minutes < end) - : (now_minutes >= begin || now_minutes < end)); - } - - enum class animation_state_row_t : uint8_t { - Idle, - StartWriting, - Writing, - EndWriting, - Happy, - FallASleep, - Sleep, - WakeUp, - Boring, - Test, - StartWorking, - Working, - EndWorking, - StartMoving, - Moving, - EndMoving, - StartRunning, - Running, - EndRunning, - }; - - struct animation_state_t { - // animation timing - platform::time_ms_t frame_delta_ms_counter{0}; - platform::time_ms_t update_delta_ms_counter{0}; - platform::time_ns_t frame_time_ns{0}; - platform::time_ms_t frame_time_ms{0}; - platform::time_ms_t hold_frame_ms{0}; - platform::timestamp_ms_t last_frame_update_ms{0}; - platform::timestamp_ms_t time_until_next_frame_ms{0}; - - // state - bool hold_frame_after_release{false}; - bool show_boring_animation_once{false}; - bool is_idle_sleep{false}; - - // moving - float anim_velocity{0.0}; - float anim_distance{0.0}; - float anim_last_direction{0.0}; - int32_t anim_pause_after_movement_ms{0}; - - // animation player data - animation_state_row_t row_state{animation_state_row_t::Idle}; - // for ms agent and sprite sheets - int32_t start_col_index{0}; - int32_t end_col_index{0}; - // for am/bongocat/pkmn (cached animation frames) - int32_t animations_index{0}; // for sprite_sheet.frames (col indices) array - }; - - struct anim_next_frame_result_t { - bool moved{false}; - bool frame_changed{false}; - bool rerender{false}; - int32_t new_col{0}; - int32_t new_row{0}; - }; - - - struct animation_trigger_t { - trigger_animation_cause_mask_t anim_cause{trigger_animation_cause_mask_t::NONE}; - int any_key_press_counter{0}; - }; - struct anim_handle_key_press_result_t { - animation_trigger_t trigger; - anim_next_frame_result_t update_frame_result; - }; - - struct anim_conditions_t { - platform::timestamp_ms_t last_key_pressed_timestamp{0}; - - // trigger (start animation) - bool any_key_pressed{false}; - bool trigger_test_animation{false}; - bool check_for_idle_sleep{false}; - bool process_movement{false}; - - // trigger (back to idle) - bool release_test_frame{false}; - bool release_frame_after_press{false}; - bool release_frame_after_update{false}; - - // for continues animations - bool process_idle_animation{false}; - bool process_movement_animation{false}; - bool process_working_animation{false}; - bool process_running_animation{false}; - bool go_next_frame{false}; - bool go_next_frame_running{false}; - bool release_frame_for_non_idle{false}; - - // current animation - bool is_writing{false}; - bool is_moving{false}; - bool is_working{false}; - bool is_running{false}; - bool continue_writing{false}; - }; - static anim_conditions_t get_anim_conditions([[maybe_unused]] const animation_context_t& ctx, - const platform::input::input_context_t& input, - [[maybe_unused]] const platform::update::update_context_t& upd, - const animation_state_t& current_state, - const animation_trigger_t& trigger, - const config::config_t& current_config) { - assert(input.shm != nullptr); - const auto& input_shm = *input.shm; - const auto& update_shm = *upd.shm; - const platform::timestamp_ms_t last_key_pressed_timestamp = input_shm.last_key_pressed_timestamp; - const platform::timestamp_ms_t fps_ms = 1000/current_config.fps; - - const bool any_key_pressed = has_flag(trigger.anim_cause, trigger_animation_cause_mask_t::KeyPress) && trigger.any_key_press_counter > 0; - - const bool process_idle_animation_by_animation_speed = current_config.idle_animation && current_config.animation_speed_ms > 0 && current_state.frame_delta_ms_counter > current_config.animation_speed_ms; - const bool process_idle_animation_by_fps = current_config.idle_animation && current_config.animation_speed_ms <= 0 && current_state.frame_delta_ms_counter > fps_ms; - const bool process_idle_animation = process_idle_animation_by_animation_speed || process_idle_animation_by_fps; - - const bool release_frame_by_animation_speed = !current_config.idle_animation && current_config.animation_speed_ms > 0 && current_state.hold_frame_ms > current_config.animation_speed_ms; - const bool release_frame_animation_by_fps = !current_config.idle_animation && current_config.animation_speed_ms <= 0 && current_state.hold_frame_ms > fps_ms; - const bool release_frame_for_non_idle = release_frame_by_animation_speed || release_frame_animation_by_fps; - const bool go_next_frame = (current_config.animation_speed_ms > 0 && current_state.frame_delta_ms_counter > current_config.animation_speed_ms) || (current_config.animation_speed_ms <= 0 && current_state.frame_delta_ms_counter > fps_ms); - - const bool process_movement_animation_by_animation_speed = current_config.movement_speed > 0 && current_config.movement_radius > 0 && current_config.animation_speed_ms > 0 && current_state.frame_delta_ms_counter > current_config.animation_speed_ms; - const bool process_movement_animation_by_fps = current_config.movement_speed > 0 && current_config.movement_radius > 0 && current_config.animation_speed_ms <= 0 && current_state.frame_delta_ms_counter > fps_ms; - const bool process_movement_animation = process_movement_animation_by_animation_speed || process_movement_animation_by_fps; - - const bool process_working_animation_by_animation_speed = current_config.cpu_running_factor < 1.0 && current_config.cpu_threshold > 0 && current_config.update_rate_ms > 0 && current_config.animation_speed_ms > 0 && current_state.frame_delta_ms_counter > current_config.animation_speed_ms; - const bool process_working_animation_by_fps = current_config.cpu_running_factor < 1.0 && current_config.cpu_threshold > 0 && current_config.update_rate_ms > 0 && current_config.animation_speed_ms <= 0 && current_state.frame_delta_ms_counter > fps_ms; - const bool process_working_animation = process_working_animation_by_animation_speed || process_working_animation_by_fps; - - const bool process_running_animation_by_animation_speed = current_config.cpu_running_factor >= 1.0 && current_config.cpu_threshold > 0 && current_config.update_rate_ms > 0 && current_config.animation_speed_ms > 0 && current_state.frame_delta_ms_counter > current_config.animation_speed_ms; - const bool process_running_animation_by_fps = current_config.cpu_running_factor >= 1.0 && current_config.cpu_threshold > 0 && current_config.update_rate_ms > 0 && current_config.animation_speed_ms <= 0 && current_state.frame_delta_ms_counter > fps_ms; - const bool process_running_animation = process_running_animation_by_animation_speed || process_running_animation_by_fps; - - const bool process_movement = current_config.movement_speed > 0 && current_config.movement_radius > 0 && current_state.update_delta_ms_counter > current_config.animation_speed_ms; - - const bool is_writing = current_state.row_state == animation_state_row_t::StartWriting || current_state.row_state == animation_state_row_t::Writing || current_state.row_state == animation_state_row_t::EndWriting; - const bool release_frame_after_press = !any_key_pressed && current_state.hold_frame_ms > current_config.keypress_duration_ms; - - const bool is_running = current_state.row_state == animation_state_row_t::StartRunning || current_state.row_state == animation_state_row_t::Running; - double running_animation_speed_factor = 1.0; - if (current_config.cpu_running_factor >= 1.0 && update_shm.cpu_active) { - running_animation_speed_factor = update_shm.avg_cpu_usage > 0 ? 1.0 / ((update_shm.avg_cpu_usage/100.0) * (current_config.cpu_running_factor)) : 0; - } - const double running_animation_speed_ms = running_animation_speed_factor * current_config.animation_speed_ms; - const bool go_next_frame_running = current_config.cpu_running_factor >= 1.0 && ((running_animation_speed_ms > 0 && static_cast(current_state.frame_delta_ms_counter) > running_animation_speed_ms) || (running_animation_speed_ms <= 0 && (running_animation_speed_factor*static_cast(current_state.frame_delta_ms_counter)) > static_cast(fps_ms))); - +// ============================================================================= +// GLOBAL STATE AND CONFIGURATION +// ============================================================================= + +inline static constexpr platform::time_ms_t POOL_MIN_TIMEOUT_MS = 5; +inline static constexpr platform::time_ms_t POOL_MAX_TIMEOUT_MS = 1000; +inline static constexpr int MAX_ATTEMPTS = 2048; +static_assert(POOL_MAX_TIMEOUT_MS >= POOL_MIN_TIMEOUT_MS); + +inline static constexpr platform::time_ms_t COND_RELOAD_CONFIGS_TIMEOUT_MS = 5000; + +inline static constexpr int CHANCE_FOR_SKIPPING_MOVEMENT_PERCENT = 30; // in percent +inline static constexpr int SMALL_MAX_DISTANCE_PER_MOVEMENT_PART = 3; // 1/3 of movement_radius (for small areas) +inline static constexpr int MAX_DISTANCE_PER_MOVEMENT_PART = 5; // 1/5 of movement_radius +static_assert(SMALL_MAX_DISTANCE_PER_MOVEMENT_PART > 0); +static_assert(MAX_DISTANCE_PER_MOVEMENT_PART > 0); +inline static constexpr int MAX_MOVEMENT_RADIUS_SMALL = 100; +inline static constexpr int FLIP_DIRECTION_NEAR_WALL_PERCENT = 70; +inline static constexpr int SMALL_FLIP_DIRECTION_NEAR_WALL_PERCENT = 40; + +inline static constexpr int SLEEP_BORING_PART = 2; // 1/2 of sleep time for triggering boring animation + +// ============================================================================= +// ANIMATION STATE MANAGEMENT MODULE +// ============================================================================= + +static bool is_sleep_time(const config::config_t& config) { + time_t raw_time; + tm time_info{}; + time(&raw_time); + localtime_r(&raw_time, &time_info); + + const int now_minutes = (time_info.tm_hour * 60) + time_info.tm_min; + const int begin = (config.sleep_begin.hour * 60) + config.sleep_begin.min; + const int end = (config.sleep_end.hour * 60) + config.sleep_end.min; + + // Normal range (e.g., 10:00–22:00): begin < end && (now_minutes >= begin && now_minutes < end) + // Overnight range (e.g., 22:00–06:00): begin > end && (now_minutes >= begin || now_minutes < end) + + return (begin == end) || + (begin < end ? (now_minutes >= begin && now_minutes < end) : (now_minutes >= begin || now_minutes < end)); +} - assert(SMALL_MAX_DISTANCE_PER_MOVEMENT_PART > 0); - assert(MAX_DISTANCE_PER_MOVEMENT_PART > 0); +enum class animation_state_row_t : uint8_t { + Idle, + StartWriting, + Writing, + EndWriting, + Happy, + FallASleep, + Sleep, + WakeUp, + Boring, + Test, + StartWorking, + Working, + EndWorking, + StartMoving, + Moving, + EndMoving, + StartRunning, + Running, + EndRunning, +}; + +struct animation_state_t { + // animation timing + platform::time_ms_t frame_delta_ms_counter{0}; + platform::time_ms_t update_delta_ms_counter{0}; + platform::time_ns_t frame_time_ns{0}; + platform::time_ms_t frame_time_ms{0}; + platform::time_ms_t hold_frame_ms{0}; + platform::timestamp_ms_t last_frame_update_ms{0}; + platform::timestamp_ms_t time_until_next_frame_ms{0}; + + // state + bool hold_frame_after_release{false}; + bool show_boring_animation_once{false}; + bool is_idle_sleep{false}; + + // moving + float anim_velocity{0.0}; + float anim_distance{0.0}; + float anim_last_direction{0.0}; + int32_t anim_pause_after_movement_ms{0}; + + // animation player data + animation_state_row_t row_state{animation_state_row_t::Idle}; + // for ms agent and sprite sheets + int32_t start_col_index{0}; + int32_t end_col_index{0}; + // for am/bongocat/pkmn (cached animation frames) + int32_t animations_index{0}; // for sprite_sheet.frames (col indices) array +}; + +struct anim_next_frame_result_t { + bool moved{false}; + bool frame_changed{false}; + bool rerender{false}; + int32_t new_col{0}; + int32_t new_row{0}; +}; + +struct animation_trigger_t { + trigger_animation_cause_mask_t anim_cause{trigger_animation_cause_mask_t::NONE}; + int any_key_press_counter{0}; +}; +struct anim_handle_key_press_result_t { + animation_trigger_t trigger; + anim_next_frame_result_t update_frame_result; +}; + +struct anim_conditions_t { + platform::timestamp_ms_t last_key_pressed_timestamp{0}; + + // trigger (start animation) + bool any_key_pressed{false}; + bool trigger_test_animation{false}; + bool check_for_idle_sleep{false}; + bool process_movement{false}; + + // trigger (back to idle) + bool release_test_frame{false}; + bool release_frame_after_press{false}; + bool release_frame_after_update{false}; + + // for continues animations + bool process_idle_animation{false}; + bool process_movement_animation{false}; + bool process_working_animation{false}; + bool process_running_animation{false}; + bool go_next_frame{false}; + bool go_next_frame_running{false}; + bool release_frame_for_non_idle{false}; + + // current animation + bool is_writing{false}; + bool is_moving{false}; + bool is_working{false}; + bool is_running{false}; + bool continue_writing{false}; +}; +static anim_conditions_t get_anim_conditions([[maybe_unused]] const animation_thread_context_t& ctx, + const platform::input::input_context_t& input, + [[maybe_unused]] const platform::update::update_context_t& upd, + const animation_state_t& current_state, const animation_trigger_t& trigger, + const config::config_t& current_config) { + assert(input.shm); + const auto& input_shm = *input.shm; + const auto& update_shm = *upd.shm; + const platform::timestamp_ms_t last_key_pressed_timestamp = input_shm.last_key_pressed_timestamp; + const platform::timestamp_ms_t fps_ms = 1000 / current_config.fps; + + const bool any_key_pressed = + has_flag(trigger.anim_cause, trigger_animation_cause_mask_t::KeyPress) && trigger.any_key_press_counter > 0; + + const bool process_idle_animation_by_animation_speed = + current_config.idle_animation >= 1 && current_config.animation_speed_ms > 0 && + current_state.frame_delta_ms_counter > current_config.animation_speed_ms; + const bool process_idle_animation_by_fps = current_config.idle_animation >= 1 && + current_config.animation_speed_ms <= 0 && + current_state.frame_delta_ms_counter > fps_ms; + const bool process_idle_animation = process_idle_animation_by_animation_speed || process_idle_animation_by_fps; + + const bool release_frame_by_animation_speed = current_config.idle_animation <= 0 && + current_config.animation_speed_ms > 0 && + current_state.hold_frame_ms > current_config.animation_speed_ms; + const bool release_frame_animation_by_fps = current_config.idle_animation <= 0 && + current_config.animation_speed_ms <= 0 && + current_state.hold_frame_ms > fps_ms; + const bool release_frame_for_non_idle = release_frame_by_animation_speed || release_frame_animation_by_fps; + const bool go_next_frame = (current_config.animation_speed_ms > 0 && + current_state.frame_delta_ms_counter > current_config.animation_speed_ms) || + (current_config.animation_speed_ms <= 0 && current_state.frame_delta_ms_counter > fps_ms); + + const bool process_movement_animation_by_animation_speed = + current_config.movement_speed > 0 && current_config.movement_radius > 0 && + current_config.animation_speed_ms > 0 && current_state.frame_delta_ms_counter > current_config.animation_speed_ms; + const bool process_movement_animation_by_fps = + current_config.movement_speed > 0 && current_config.movement_radius > 0 && + current_config.animation_speed_ms <= 0 && current_state.frame_delta_ms_counter > fps_ms; + const bool process_movement_animation = + process_movement_animation_by_animation_speed || process_movement_animation_by_fps; + + const bool process_working_animation_by_animation_speed = + current_config.cpu_running_factor < 1.0 && current_config.cpu_threshold > 0 && + current_config.update_rate_ms > 0 && current_config.animation_speed_ms > 0 && + current_state.frame_delta_ms_counter > current_config.animation_speed_ms; + const bool process_working_animation_by_fps = current_config.cpu_running_factor < 1.0 && + current_config.cpu_threshold > 0 && current_config.update_rate_ms > 0 && + current_config.animation_speed_ms <= 0 && + current_state.frame_delta_ms_counter > fps_ms; + const bool process_working_animation = + process_working_animation_by_animation_speed || process_working_animation_by_fps; + + const bool process_running_animation_by_animation_speed = + current_config.cpu_running_factor >= 1.0 && current_config.cpu_threshold > 0 && + current_config.update_rate_ms > 0 && current_config.animation_speed_ms > 0 && + current_state.frame_delta_ms_counter > current_config.animation_speed_ms; + const bool process_running_animation_by_fps = current_config.cpu_running_factor >= 1.0 && + current_config.cpu_threshold > 0 && current_config.update_rate_ms > 0 && + current_config.animation_speed_ms <= 0 && + current_state.frame_delta_ms_counter > fps_ms; + const bool process_running_animation = + process_running_animation_by_animation_speed || process_running_animation_by_fps; + + const bool process_movement = current_config.movement_speed > 0 && current_config.movement_radius > 0 && + current_state.update_delta_ms_counter > current_config.animation_speed_ms; + + const bool is_writing = current_state.row_state == animation_state_row_t::StartWriting || + current_state.row_state == animation_state_row_t::Writing || + current_state.row_state == animation_state_row_t::EndWriting; + const bool release_frame_after_press = + !any_key_pressed && current_state.hold_frame_ms > current_config.keypress_duration_ms; + + const bool is_running = current_state.row_state == animation_state_row_t::StartRunning || + current_state.row_state == animation_state_row_t::Running; + double running_animation_speed_factor = 1.0; + if (current_config.cpu_running_factor >= 1.0 && update_shm.cpu_active) { + running_animation_speed_factor = + update_shm.avg_cpu_usage > 0 ? 1.0 / ((update_shm.avg_cpu_usage / 100.0) * (current_config.cpu_running_factor)) + : 0; + } + const double running_animation_speed_ms = running_animation_speed_factor * current_config.animation_speed_ms; + const bool go_next_frame_running = + current_config.cpu_running_factor >= 1.0 && + ((running_animation_speed_ms > 0 && + static_cast(current_state.frame_delta_ms_counter) > running_animation_speed_ms) || + (running_animation_speed_ms <= 0 && + (running_animation_speed_factor * static_cast(current_state.frame_delta_ms_counter)) > + static_cast(fps_ms))); + + assert(SMALL_MAX_DISTANCE_PER_MOVEMENT_PART > 0); + assert(MAX_DISTANCE_PER_MOVEMENT_PART > 0); + + // @TODO: reduce duplicated condition for when updating frames vs. movement vs. working animation + + return { + .last_key_pressed_timestamp = last_key_pressed_timestamp, + + .any_key_pressed = any_key_pressed, + .trigger_test_animation = + current_config.test_animation_interval_sec > 0 && + current_state.frame_delta_ms_counter > current_config.test_animation_interval_sec * 1000L, + .check_for_idle_sleep = + current_config.idle_sleep_timeout_sec > 0 && + ((SLEEP_BORING_PART > 0 && + current_state.frame_delta_ms_counter > current_config.idle_sleep_timeout_sec * 1000L / SLEEP_BORING_PART && + last_key_pressed_timestamp > 0) || + process_idle_animation || process_movement), + .process_movement = process_movement, + + .release_test_frame = current_config.test_animation_duration_ms > 0 && + current_state.frame_delta_ms_counter > current_config.test_animation_duration_ms, + .release_frame_after_press = release_frame_after_press, + .release_frame_after_update = + (!any_key_pressed && has_flag(trigger.anim_cause, trigger_animation_cause_mask_t::CpuUpdate)) && + current_state.hold_frame_ms > current_config.keypress_duration_ms, + + .process_idle_animation = process_idle_animation, + .process_movement_animation = process_movement_animation, + .process_working_animation = process_working_animation, + .process_running_animation = process_running_animation, + .go_next_frame = go_next_frame, + .go_next_frame_running = go_next_frame_running, + .release_frame_for_non_idle = + release_frame_for_non_idle || go_next_frame || (is_running && go_next_frame_running), + + .is_writing = is_writing, + .is_moving = current_state.row_state == animation_state_row_t::StartMoving || + current_state.row_state == animation_state_row_t::Moving || + current_state.row_state == animation_state_row_t::EndMoving, + .is_working = current_state.row_state == animation_state_row_t::StartWorking || + current_state.row_state == animation_state_row_t::Working, + .is_running = is_running, + .continue_writing = ((!any_key_pressed && current_state.hold_frame_ms < current_config.keypress_duration_ms) || + !release_frame_after_press) && + is_writing, + }; +} - // @TODO: reduce duplicated condition for when updating frames vs. movement vs. working animation - - return { - .last_key_pressed_timestamp = last_key_pressed_timestamp, - - .any_key_pressed = any_key_pressed, - .trigger_test_animation = current_config.test_animation_interval_sec > 0 && current_state.frame_delta_ms_counter > current_config.test_animation_interval_sec*1000, - .check_for_idle_sleep = current_config.idle_sleep_timeout_sec > 0 && ((SLEEP_BORING_PART > 0 && current_state.frame_delta_ms_counter > current_config.idle_sleep_timeout_sec*1000/SLEEP_BORING_PART && last_key_pressed_timestamp > 0) || process_idle_animation || process_movement), - .process_movement = process_movement, - - .release_test_frame = current_config.test_animation_duration_ms > 0 && current_state.frame_delta_ms_counter > current_config.test_animation_duration_ms, - .release_frame_after_press = release_frame_after_press, - .release_frame_after_update = (!any_key_pressed && has_flag(trigger.anim_cause, trigger_animation_cause_mask_t::CpuUpdate)) && current_state.hold_frame_ms > current_config.keypress_duration_ms, - - .process_idle_animation = process_idle_animation, - .process_movement_animation = process_movement_animation, - .process_working_animation = process_working_animation, - .process_running_animation = process_running_animation, - .go_next_frame = go_next_frame, - .go_next_frame_running = go_next_frame_running, - .release_frame_for_non_idle = release_frame_for_non_idle || go_next_frame || (is_running && go_next_frame_running), - - .is_writing = is_writing, - .is_moving = current_state.row_state == animation_state_row_t::StartMoving || current_state.row_state == animation_state_row_t::Moving || current_state.row_state == animation_state_row_t::EndMoving, - .is_working = current_state.row_state == animation_state_row_t::StartWorking || current_state.row_state == animation_state_row_t::Working, - .is_running = is_running, - .continue_writing = ((!any_key_pressed && current_state.hold_frame_ms < current_config.keypress_duration_ms) || !release_frame_after_press) && is_writing, - }; +static anim_next_frame_result_t +anim_update_animation_state(animation_shared_memory_t& anim_shm, animation_state_t& state, + const animation_player_result_t& new_animation_result, const animation_state_t& new_state, + const animation_player_result_t& current_animation_result, + const animation_state_t& current_state, const config::config_t& current_config) { + const bool moved = static_cast(current_state.anim_distance) != static_cast(new_state.anim_distance); + const bool frame_changed = new_animation_result.sprite_sheet_col != current_animation_result.sprite_sheet_col || + new_animation_result.sprite_sheet_row != current_animation_result.sprite_sheet_row; + const bool rerender = frame_changed || current_state.row_state != new_state.row_state || moved; + state = new_state; + if (new_state.animations_index != current_state.animations_index || rerender || moved) { + anim_shm.animation_player_result = new_animation_result; + if (current_config.enable_debug >= 1) { + BONGOCAT_LOG_VERBOSE("Animation frame change: %d", new_animation_result.sprite_sheet_col); } - - static anim_next_frame_result_t anim_update_animation_state(animation_shared_memory_t& anim_shm, - animation_state_t& state, - const animation_player_result_t& new_animation_result, - const animation_state_t& new_state, - const animation_player_result_t& current_animation_result, - const animation_state_t& current_state, - const config::config_t& current_config) { - const bool moved = static_cast(current_state.anim_distance) != static_cast(new_state.anim_distance); - const bool frame_changed = new_animation_result.sprite_sheet_col != current_animation_result.sprite_sheet_col || new_animation_result.sprite_sheet_row != current_animation_result.sprite_sheet_row; - const bool rerender = frame_changed || current_state.row_state != new_state.row_state || moved; - state = new_state; - if (new_state.animations_index != current_state.animations_index || rerender || moved) { - anim_shm.animation_player_result = new_animation_result; - if (current_config.enable_debug) { - BONGOCAT_LOG_VERBOSE("Animation frame change: %d", new_animation_result.sprite_sheet_col); - } - if (frame_changed) { - state.frame_delta_ms_counter = 0; - } - state.update_delta_ms_counter = 0; - } - - return { .moved = moved, .frame_changed = frame_changed, .rerender = rerender, .new_col = new_animation_result.sprite_sheet_col, .new_row = new_animation_result.sprite_sheet_row }; + if (frame_changed) { + state.frame_delta_ms_counter = 0; } + state.update_delta_ms_counter = 0; + } + + return {.moved = moved, + .frame_changed = frame_changed, + .rerender = rerender, + .new_col = new_animation_result.sprite_sheet_col, + .new_row = new_animation_result.sprite_sheet_row}; +} - /// @TODO: move anim_..._animation into own cpp files per asset set, make it more modular +/// @TODO: move anim_..._animation into own cpp files per asset set, make it more modular #ifdef FEATURE_BONGOCAT_EMBEDDED_ASSETS - enum class anim_bongocat_process_animation_result_status_t : uint8_t { None, Started, Updated, End, Looped, NextAnimationStarted }; - struct anim_bongocat_process_animation_result_t { - animation_state_row_t row_state; - anim_bongocat_process_animation_result_status_t status{anim_bongocat_process_animation_result_status_t::None}; - }; - static anim_bongocat_process_animation_result_t anim_bongocat_process_animation(animation_player_result_t& new_animation_result, - animation_state_t& new_state, - const animation_state_t& current_state, - const bongocat_sprite_sheet_t& current_frames) { - assert(MAX_ANIMATION_FRAMES > 0); - assert(MAX_ANIMATION_FRAMES <= INT_MAX); - - anim_bongocat_process_animation_result_t ret {.row_state = new_state.row_state, .status = anim_bongocat_process_animation_result_status_t::Updated}; - // forward animation - new_state.animations_index = current_state.animations_index + 1; - if (new_state.animations_index > static_cast(MAX_ANIMATION_FRAMES-1)) { - ret.status = anim_bongocat_process_animation_result_status_t::Looped; - new_state.animations_index = 0; - } - /* - // backwards animation - else if (new_state.animations_index < 0) { - ret = anim_bongocat_process_animation_result_t::End; - new_state.animations_index = MAX_ANIMATION_FRAMES-1; - } - */ - switch (new_state.row_state) { - case animation_state_row_t::Idle: - new_animation_result.sprite_sheet_col = current_frames.animations.idle[new_state.animations_index]; - break; - case animation_state_row_t::StartWriting: - case animation_state_row_t::Writing: - case animation_state_row_t::EndWriting: - new_animation_result.sprite_sheet_col = current_frames.animations.writing[new_state.animations_index]; - break; - case animation_state_row_t::Happy: - new_animation_result.sprite_sheet_col = current_frames.animations.happy[new_state.animations_index]; - break; - case animation_state_row_t::FallASleep: - // not supported - new_animation_result.sprite_sheet_col = current_frames.animations.sleep[new_state.animations_index]; - break; - case animation_state_row_t::Sleep: - new_animation_result.sprite_sheet_col = current_frames.animations.sleep[new_state.animations_index]; - break; - case animation_state_row_t::WakeUp: - new_animation_result.sprite_sheet_col = current_frames.animations.wake_up[new_state.animations_index]; - break; - case animation_state_row_t::Boring: - new_animation_result.sprite_sheet_col = current_frames.animations.boring[new_state.animations_index]; - break; - case animation_state_row_t::Test: - new_animation_result.sprite_sheet_col = current_frames.animations.writing[new_state.animations_index]; - break; - case animation_state_row_t::StartWorking: - case animation_state_row_t::Working: - case animation_state_row_t::EndWorking: - new_animation_result.sprite_sheet_col = current_frames.animations.working[new_state.animations_index]; - break; - case animation_state_row_t::StartMoving: - case animation_state_row_t::Moving: - case animation_state_row_t::EndMoving: - new_animation_result.sprite_sheet_col = current_frames.animations.moving[new_state.animations_index]; - break; - case animation_state_row_t::StartRunning: - case animation_state_row_t::Running: - case animation_state_row_t::EndRunning: - new_animation_result.sprite_sheet_col = current_frames.animations.running[new_state.animations_index]; - break; - } - return ret; +enum class anim_bongocat_process_animation_result_status_t : uint8_t { + None, + Started, + Updated, + End, + Looped, + NextAnimationStarted +}; +struct anim_bongocat_process_animation_result_t { + animation_state_row_t row_state; + anim_bongocat_process_animation_result_status_t status{anim_bongocat_process_animation_result_status_t::None}; +}; +static anim_bongocat_process_animation_result_t +anim_bongocat_process_animation(const platform::input::input_context_t& input, + animation_player_result_t& new_animation_result, animation_state_t& new_state, + const animation_state_t& current_state, const bongocat_sprite_sheet_t& current_frames) { + assert(MAX_ANIMATION_FRAMES > 0); + assert(MAX_ANIMATION_FRAMES <= INT_MAX); + + // read-only config + assert(input._local_copy_config); + const config::config_t& current_config = *input._local_copy_config; + + anim_bongocat_process_animation_result_t ret{.row_state = new_state.row_state, + .status = anim_bongocat_process_animation_result_status_t::Updated}; + // forward animation + new_state.animations_index = current_state.animations_index + 1; + if (new_state.animations_index > static_cast(MAX_ANIMATION_FRAMES - 1)) { + ret.status = anim_bongocat_process_animation_result_status_t::Looped; + new_state.animations_index = 0; + } + /* + // backwards animation + else if (new_state.animations_index < 0) { + ret = anim_bongocat_process_animation_result_t::End; + new_state.animations_index = MAX_ANIMATION_FRAMES-1; + } + */ + switch (new_state.row_state) { + case animation_state_row_t::Idle: + new_animation_result.sprite_sheet_col = current_frames.animations.idle[new_state.animations_index]; + break; + case animation_state_row_t::StartWriting: + case animation_state_row_t::Writing: + case animation_state_row_t::EndWriting: + if (current_config.enable_hand_mapping >= 1) { + switch (input.shm->hand_mapping) { + case platform::input::input_hand_mapping_t::None: + new_animation_result.sprite_sheet_col = current_frames.animations.writing[new_state.animations_index]; + break; + case platform::input::input_hand_mapping_t::Left: + new_animation_result.sprite_sheet_col = + (current_config.mirror_x >= 1) ? current_frames.animations.right_writing[new_state.animations_index] + : current_frames.animations.left_writing[new_state.animations_index]; + break; + case platform::input::input_hand_mapping_t::Right: + new_animation_result.sprite_sheet_col = + (current_config.mirror_x >= 1) ? current_frames.animations.left_writing[new_state.animations_index] + : current_frames.animations.right_writing[new_state.animations_index]; + break; + } + } else { + new_animation_result.sprite_sheet_col = current_frames.animations.writing[new_state.animations_index]; } - static anim_bongocat_process_animation_result_t anim_bongocat_restart_animation(animation_context_t& ctx, - animation_state_row_t new_row_state, - animation_player_result_t& new_animation_result, - animation_state_t& new_state, - [[maybe_unused]] const animation_state_t& current_state, - const bongocat_sprite_sheet_t& current_frames, - const config::config_t& current_config) { - using namespace assets; - assert(MAX_ANIMATION_FRAMES > 0); - assert(MAX_ANIMATION_FRAMES <= INT_MAX); - - new_state.row_state = new_row_state; - new_animation_result.sprite_sheet_row = BONGOCAT_SPRITE_SHEET_ROW; - new_state.animations_index = 0; - - anim_bongocat_process_animation_result_t ret {.row_state = new_state.row_state, .status = anim_bongocat_process_animation_result_status_t::Started}; - switch (new_state.row_state) { - case animation_state_row_t::Idle: - new_animation_result.sprite_sheet_col = current_frames.animations.idle[new_state.animations_index]; - if (current_config.idle_frame) { - new_animation_result.sprite_sheet_col = current_config.idle_frame; - } - break; - case animation_state_row_t::StartWriting: - new_state.animations_index = static_cast(ctx._rng.range(0, (MAX_ANIMATION_FRAMES-1) / 2)); - [[fallthrough]]; - case animation_state_row_t::Writing: - case animation_state_row_t::EndWriting: - new_animation_result.sprite_sheet_col = current_frames.animations.writing[new_state.animations_index]; - break; - case animation_state_row_t::Happy: - new_animation_result.sprite_sheet_col = current_frames.animations.happy[new_state.animations_index]; - break; - case animation_state_row_t::FallASleep: - // not supported - new_animation_result.sprite_sheet_col = current_frames.animations.sleep[new_state.animations_index]; - break; - case animation_state_row_t::Sleep: - new_animation_result.sprite_sheet_col = current_frames.animations.sleep[new_state.animations_index]; - break; - case animation_state_row_t::WakeUp: - new_animation_result.sprite_sheet_col = current_frames.animations.wake_up[new_state.animations_index]; - break; - case animation_state_row_t::Boring: - new_animation_result.sprite_sheet_col = current_frames.animations.boring[new_state.animations_index]; - break; - case animation_state_row_t::Test: - new_animation_result.sprite_sheet_col = current_frames.animations.writing[new_state.animations_index]; - break; - case animation_state_row_t::StartWorking: - case animation_state_row_t::Working: - case animation_state_row_t::EndWorking: - new_animation_result.sprite_sheet_col = current_frames.animations.working[new_state.animations_index]; - break; - case animation_state_row_t::StartMoving: - case animation_state_row_t::Moving: - case animation_state_row_t::EndMoving: - new_animation_result.sprite_sheet_col = current_frames.animations.moving[new_state.animations_index]; - break; - case animation_state_row_t::StartRunning: - case animation_state_row_t::Running: - case animation_state_row_t::EndRunning: - new_animation_result.sprite_sheet_col = current_frames.animations.running[new_state.animations_index]; - break; - } - return ret; + break; + case animation_state_row_t::Happy: + new_animation_result.sprite_sheet_col = current_frames.animations.happy[new_state.animations_index]; + break; + case animation_state_row_t::FallASleep: + // not supported + new_animation_result.sprite_sheet_col = current_frames.animations.sleep[new_state.animations_index]; + break; + case animation_state_row_t::Sleep: + new_animation_result.sprite_sheet_col = current_frames.animations.sleep[new_state.animations_index]; + break; + case animation_state_row_t::WakeUp: + new_animation_result.sprite_sheet_col = current_frames.animations.wake_up[new_state.animations_index]; + break; + case animation_state_row_t::Boring: + new_animation_result.sprite_sheet_col = current_frames.animations.boring[new_state.animations_index]; + break; + case animation_state_row_t::Test: + new_animation_result.sprite_sheet_col = current_frames.animations.writing[new_state.animations_index]; + break; + case animation_state_row_t::StartWorking: + case animation_state_row_t::Working: + case animation_state_row_t::EndWorking: + new_animation_result.sprite_sheet_col = current_frames.animations.working[new_state.animations_index]; + break; + case animation_state_row_t::StartMoving: + case animation_state_row_t::Moving: + case animation_state_row_t::EndMoving: + new_animation_result.sprite_sheet_col = current_frames.animations.moving[new_state.animations_index]; + break; + case animation_state_row_t::StartRunning: + case animation_state_row_t::Running: + case animation_state_row_t::EndRunning: + new_animation_result.sprite_sheet_col = current_frames.animations.running[new_state.animations_index]; + break; + } + return ret; +} +static anim_bongocat_process_animation_result_t +anim_bongocat_restart_animation(animation_thread_context_t& ctx, const platform::input::input_context_t& input, + animation_state_row_t new_row_state, animation_player_result_t& new_animation_result, + animation_state_t& new_state, [[maybe_unused]] const animation_state_t& current_state, + const bongocat_sprite_sheet_t& current_frames) { + using namespace assets; + assert(MAX_ANIMATION_FRAMES > 0); + assert(MAX_ANIMATION_FRAMES <= INT_MAX); + + // read-only config + assert(input._local_copy_config); + const config::config_t& current_config = *input._local_copy_config; + + new_state.row_state = new_row_state; + new_animation_result.sprite_sheet_row = BONGOCAT_SPRITE_SHEET_ROW; + new_state.animations_index = 0; + + anim_bongocat_process_animation_result_t ret{.row_state = new_state.row_state, + .status = anim_bongocat_process_animation_result_status_t::Started}; + switch (new_state.row_state) { + case animation_state_row_t::Idle: + new_animation_result.sprite_sheet_col = current_frames.animations.idle[new_state.animations_index]; + if (current_config.idle_frame >= 1) { + new_animation_result.sprite_sheet_col = current_config.idle_frame; } - /* - static anim_bongocat_process_animation_result_t anim_bongocat_show_single_frame(animation_context_t& ctx, - animation_state_row_t new_row_state, - animation_player_result_t& new_animation_result, - animation_state_t& new_state, - [[maybe_unused]] const animation_state_t& current_state, - [[maybe_unused]] const bongocat_sprite_sheet_t& current_frames, - const config::config_t& current_config) { - using namespace assets; - assert(MAX_ANIMATION_FRAMES > 0); - assert(MAX_ANIMATION_FRAMES <= INT_MAX); - anim_bongocat_process_animation_result_t ret = anim_bongocat_process_animation_result_t::Started; - new_state.row_state = new_row_state; - new_animation_result.sprite_sheet_row = BONGOCAT_SPRITE_SHEET_ROW; + break; + case animation_state_row_t::StartWriting: { + if (current_config.enable_hand_mapping >= 1) { + switch (input.shm->hand_mapping) { + case platform::input::input_hand_mapping_t::None: + new_state.animations_index = static_cast(ctx._rng.range(0, (MAX_ANIMATION_FRAMES - 1) / 2)); + break; + case platform::input::input_hand_mapping_t::Left: + case platform::input::input_hand_mapping_t::Right: new_state.animations_index = 0; - switch (new_state.row_state) { - case animation_state_row_t::Idle: - new_animation_result.sprite_sheet_col = BONGOCAT_FRAME_BOTH_UP; - if (current_config.idle_frame) { - new_animation_result.sprite_sheet_col = current_config.idle_frame; - } - break; - case animation_state_row_t::StartWriting: - new_animation_result.sprite_sheet_col = ctx._rng.range(0, 100) <= 50 ? BONGOCAT_FRAME_LEFT_DOWN : BONGOCAT_FRAME_RIGHT_DOWN; - [[fallthrough]]; - case animation_state_row_t::Writing: - case animation_state_row_t::EndWriting: - // toggle frame - if (new_animation_result.sprite_sheet_col == BONGOCAT_FRAME_LEFT_DOWN) { - new_animation_result.sprite_sheet_col = BONGOCAT_FRAME_RIGHT_DOWN; - } else if (new_animation_result.sprite_sheet_col == BONGOCAT_FRAME_RIGHT_DOWN) { - new_animation_result.sprite_sheet_col = BONGOCAT_FRAME_LEFT_DOWN; - } else { - new_animation_result.sprite_sheet_col = ctx._rng.range(0, 100) <= 50 ? BONGOCAT_FRAME_LEFT_DOWN : BONGOCAT_FRAME_RIGHT_DOWN; - } - break; - case animation_state_row_t::Happy: - new_animation_result.sprite_sheet_col = BONGOCAT_FRAME_BOTH_UP; - break; - case animation_state_row_t::Sleep: + break; + } + } else { + new_state.animations_index = static_cast(ctx._rng.range(0, (MAX_ANIMATION_FRAMES - 1) / 2)); + } + [[fallthrough]]; + } + case animation_state_row_t::Writing: + case animation_state_row_t::EndWriting: + if (current_config.enable_hand_mapping >= 1) { + switch (input.shm->hand_mapping) { + case platform::input::input_hand_mapping_t::None: + new_animation_result.sprite_sheet_col = current_frames.animations.writing[new_state.animations_index]; + break; + case platform::input::input_hand_mapping_t::Left: + new_animation_result.sprite_sheet_col = + (current_config.mirror_x >= 1) ? current_frames.animations.right_writing[new_state.animations_index] + : current_frames.animations.left_writing[new_state.animations_index]; + break; + case platform::input::input_hand_mapping_t::Right: + new_animation_result.sprite_sheet_col = + (current_config.mirror_x >= 1) ? current_frames.animations.left_writing[new_state.animations_index] + : current_frames.animations.right_writing[new_state.animations_index]; + break; + } + } else { + new_animation_result.sprite_sheet_col = current_frames.animations.writing[new_state.animations_index]; + } + break; + case animation_state_row_t::Happy: + new_animation_result.sprite_sheet_col = current_frames.animations.happy[new_state.animations_index]; + break; + case animation_state_row_t::FallASleep: + // not supported + new_animation_result.sprite_sheet_col = current_frames.animations.sleep[new_state.animations_index]; + break; + case animation_state_row_t::Sleep: + new_animation_result.sprite_sheet_col = current_frames.animations.sleep[new_state.animations_index]; + break; + case animation_state_row_t::WakeUp: + new_animation_result.sprite_sheet_col = current_frames.animations.wake_up[new_state.animations_index]; + break; + case animation_state_row_t::Boring: + new_animation_result.sprite_sheet_col = current_frames.animations.boring[new_state.animations_index]; + break; + case animation_state_row_t::Test: + new_animation_result.sprite_sheet_col = current_frames.animations.writing[new_state.animations_index]; + break; + case animation_state_row_t::StartWorking: + case animation_state_row_t::Working: + case animation_state_row_t::EndWorking: + new_animation_result.sprite_sheet_col = current_frames.animations.working[new_state.animations_index]; + break; + case animation_state_row_t::StartMoving: + case animation_state_row_t::Moving: + case animation_state_row_t::EndMoving: + new_animation_result.sprite_sheet_col = current_frames.animations.moving[new_state.animations_index]; + break; + case animation_state_row_t::StartRunning: + case animation_state_row_t::Running: + case animation_state_row_t::EndRunning: + new_animation_result.sprite_sheet_col = current_frames.animations.running[new_state.animations_index]; + break; + } + return ret; +} +/* +static anim_bongocat_process_animation_result_t anim_bongocat_show_single_frame(animation_context_t& ctx, + animation_state_row_t new_row_state, + animation_player_result_t& +new_animation_result, animation_state_t& new_state, + [[maybe_unused]] const +animation_state_t& current_state, + [[maybe_unused]] const +bongocat_sprite_sheet_t& current_frames, const config::config_t& current_config) { using namespace assets; + assert(MAX_ANIMATION_FRAMES > 0); + assert(MAX_ANIMATION_FRAMES <= INT_MAX); + anim_bongocat_process_animation_result_t ret = anim_bongocat_process_animation_result_t::Started; + new_state.row_state = new_row_state; + new_animation_result.sprite_sheet_row = BONGOCAT_SPRITE_SHEET_ROW; + new_state.animations_index = 0; + switch (new_state.row_state) { + case animation_state_row_t::Idle: + new_animation_result.sprite_sheet_col = BONGOCAT_FRAME_BOTH_UP; + if (current_config.idle_frame) { + new_animation_result.sprite_sheet_col = current_config.idle_frame; + } + break; + case animation_state_row_t::StartWriting: + new_animation_result.sprite_sheet_col = ctx._rng.range(0, 100) <= 50 ? BONGOCAT_FRAME_LEFT_DOWN : +BONGOCAT_FRAME_RIGHT_DOWN; + [[fallthrough]]; + case animation_state_row_t::Writing: + case animation_state_row_t::EndWriting: + // toggle frame + if (new_animation_result.sprite_sheet_col == BONGOCAT_FRAME_LEFT_DOWN) { + new_animation_result.sprite_sheet_col = BONGOCAT_FRAME_RIGHT_DOWN; + } else if (new_animation_result.sprite_sheet_col == BONGOCAT_FRAME_RIGHT_DOWN) { + new_animation_result.sprite_sheet_col = BONGOCAT_FRAME_LEFT_DOWN; + } else { + new_animation_result.sprite_sheet_col = ctx._rng.range(0, 100) <= 50 ? BONGOCAT_FRAME_LEFT_DOWN : +BONGOCAT_FRAME_RIGHT_DOWN; + } + break; + case animation_state_row_t::Happy: + new_animation_result.sprite_sheet_col = BONGOCAT_FRAME_BOTH_UP; + break; + case animation_state_row_t::Sleep: + new_animation_result.sprite_sheet_col = BONGOCAT_FRAME_BOTH_DOWN; + break; + case animation_state_row_t::WakeUp: + new_animation_result.sprite_sheet_col = BONGOCAT_FRAME_BOTH_UP; + break; + case animation_state_row_t::Boring: + new_animation_result.sprite_sheet_col = BONGOCAT_FRAME_BOTH_DOWN; + break; + case animation_state_row_t::Test: + // toggle frame (same as writing?) + if (new_animation_result.sprite_sheet_col == BONGOCAT_FRAME_LEFT_DOWN) { + new_animation_result.sprite_sheet_col = BONGOCAT_FRAME_RIGHT_DOWN; + } else if (new_animation_result.sprite_sheet_col == BONGOCAT_FRAME_RIGHT_DOWN) { + new_animation_result.sprite_sheet_col = BONGOCAT_FRAME_LEFT_DOWN; + } else { + new_animation_result.sprite_sheet_col = ctx._rng.range(0, 100) <= 50 ? BONGOCAT_FRAME_LEFT_DOWN : +BONGOCAT_FRAME_RIGHT_DOWN; + } + break; + case animation_state_row_t::StartWorking: + case animation_state_row_t::Working: + case animation_state_row_t::EndWorking: + // toggle frame (unused) + if (new_animation_result.sprite_sheet_col == BONGOCAT_FRAME_LEFT_DOWN) { + new_animation_result.sprite_sheet_col = BONGOCAT_FRAME_RIGHT_DOWN; + } else if (new_animation_result.sprite_sheet_col == BONGOCAT_FRAME_RIGHT_DOWN) { + new_animation_result.sprite_sheet_col = BONGOCAT_FRAME_LEFT_DOWN; + } else { + new_animation_result.sprite_sheet_col = ctx._rng.range(0, 100) <= 50 ? BONGOCAT_FRAME_LEFT_DOWN : +BONGOCAT_FRAME_RIGHT_DOWN; + } + break; + case animation_state_row_t::StartMoving: + case animation_state_row_t::Moving: + case animation_state_row_t::EndMoving: + // toggle frame (unused) + if (new_animation_result.sprite_sheet_col == BONGOCAT_FRAME_BOTH_UP) { new_animation_result.sprite_sheet_col = BONGOCAT_FRAME_BOTH_DOWN; - break; - case animation_state_row_t::WakeUp: - new_animation_result.sprite_sheet_col = BONGOCAT_FRAME_BOTH_UP; - break; - case animation_state_row_t::Boring: + } else if (new_animation_result.sprite_sheet_col == BONGOCAT_FRAME_BOTH_UP) { new_animation_result.sprite_sheet_col = BONGOCAT_FRAME_BOTH_DOWN; - break; - case animation_state_row_t::Test: - // toggle frame (same as writing?) - if (new_animation_result.sprite_sheet_col == BONGOCAT_FRAME_LEFT_DOWN) { - new_animation_result.sprite_sheet_col = BONGOCAT_FRAME_RIGHT_DOWN; - } else if (new_animation_result.sprite_sheet_col == BONGOCAT_FRAME_RIGHT_DOWN) { - new_animation_result.sprite_sheet_col = BONGOCAT_FRAME_LEFT_DOWN; - } else { - new_animation_result.sprite_sheet_col = ctx._rng.range(0, 100) <= 50 ? BONGOCAT_FRAME_LEFT_DOWN : BONGOCAT_FRAME_RIGHT_DOWN; - } - break; - case animation_state_row_t::StartWorking: - case animation_state_row_t::Working: - case animation_state_row_t::EndWorking: - // toggle frame (unused) - if (new_animation_result.sprite_sheet_col == BONGOCAT_FRAME_LEFT_DOWN) { - new_animation_result.sprite_sheet_col = BONGOCAT_FRAME_RIGHT_DOWN; - } else if (new_animation_result.sprite_sheet_col == BONGOCAT_FRAME_RIGHT_DOWN) { - new_animation_result.sprite_sheet_col = BONGOCAT_FRAME_LEFT_DOWN; - } else { - new_animation_result.sprite_sheet_col = ctx._rng.range(0, 100) <= 50 ? BONGOCAT_FRAME_LEFT_DOWN : BONGOCAT_FRAME_RIGHT_DOWN; - } - break; - case animation_state_row_t::StartMoving: - case animation_state_row_t::Moving: - case animation_state_row_t::EndMoving: - // toggle frame (unused) - if (new_animation_result.sprite_sheet_col == BONGOCAT_FRAME_BOTH_UP) { - new_animation_result.sprite_sheet_col = BONGOCAT_FRAME_BOTH_DOWN; - } else if (new_animation_result.sprite_sheet_col == BONGOCAT_FRAME_BOTH_UP) { - new_animation_result.sprite_sheet_col = BONGOCAT_FRAME_BOTH_DOWN; - } else { - new_animation_result.sprite_sheet_col = ctx._rng.range(0, 100) <= 50 ? BONGOCAT_FRAME_BOTH_UP : BONGOCAT_FRAME_BOTH_DOWN; - } - break; - } - return ret; - } - */ - - static anim_bongocat_process_animation_result_t anim_bongocat_start_or_process_animation(animation_context_t& ctx, - animation_state_row_t new_row_state, - animation_player_result_t& new_animation_result, - animation_state_t& new_state, - const animation_state_t& current_state, - const bongocat_sprite_sheet_t& current_frames, - const config::config_t& current_config) { - if (current_state.row_state != new_row_state) { - auto result = anim_bongocat_process_animation(new_animation_result, new_state, current_state, current_frames); - if (result.status == anim_bongocat_process_animation_result_status_t::Looped || result.status == anim_bongocat_process_animation_result_status_t::End) { - result = anim_bongocat_restart_animation(ctx, new_row_state, new_animation_result, new_state, current_state, current_frames, current_config); - result.status = anim_bongocat_process_animation_result_status_t::NextAnimationStarted; + } else { + new_animation_result.sprite_sheet_col = ctx._rng.range(0, 100) <= 50 ? BONGOCAT_FRAME_BOTH_UP : +BONGOCAT_FRAME_BOTH_DOWN; } - return result; - } - - return anim_bongocat_restart_animation(ctx, new_row_state, new_animation_result, new_state, - current_state, current_frames, current_config); + break; } + return ret; +} +*/ + +static anim_bongocat_process_animation_result_t anim_bongocat_start_or_process_animation( + animation_thread_context_t& ctx, const platform::input::input_context_t& input, animation_state_row_t new_row_state, + animation_player_result_t& new_animation_result, animation_state_t& new_state, + const animation_state_t& current_state, const bongocat_sprite_sheet_t& current_frames) { + if (current_state.row_state != new_row_state) { + auto result = + anim_bongocat_process_animation(input, new_animation_result, new_state, current_state, current_frames); + if (result.status == anim_bongocat_process_animation_result_status_t::Looped || + result.status == anim_bongocat_process_animation_result_status_t::End) { + result = anim_bongocat_restart_animation(ctx, input, new_row_state, new_animation_result, new_state, + current_state, current_frames); + result.status = anim_bongocat_process_animation_result_status_t::NextAnimationStarted; + } + return result; + } - static anim_next_frame_result_t anim_bongocat_idle_next_frame(animation_context_t& ctx, - const platform::input::input_context_t& input, - [[maybe_unused]] const platform::update::update_context_t& upd, - animation_state_t& state, const anim_handle_key_press_result_t& trigger_result) { - using namespace assets; - // read-only config - assert(ctx._local_copy_config != nullptr); - const config::config_t& current_config = *ctx._local_copy_config; - - assert(ctx.shm != nullptr); - assert(input.shm != nullptr); - animation_shared_memory_t& anim_shm = *ctx.shm; - const auto& input_shm = *input.shm; - const auto current_state = state; - const auto& current_animation_result = anim_shm.animation_player_result; - [[maybe_unused]] const int anim_index = anim_shm.anim_index; - const platform::timestamp_ms_t last_key_pressed_timestamp = input_shm.last_key_pressed_timestamp; - assert(anim_shm.anim_type == config::config_animation_sprite_sheet_layout_t::Bongocat); - assert(get_current_animation(ctx).type == animation_t::Type::Bongocat); - const auto& current_frames = get_current_animation(ctx).bongocat; - - auto new_animation_result = anim_shm.animation_player_result; - auto new_state = state; - - const auto conditions = get_anim_conditions(ctx, input, upd, current_state, trigger_result.trigger, current_config); - - // Idle Animation - const bool stop_writing = conditions.is_writing && conditions.release_frame_after_press; - const bool stop_test_animation = conditions.trigger_test_animation && current_state.row_state == animation_state_row_t::Test && conditions.release_test_frame; - if (stop_writing || stop_test_animation) { - // back to idle - anim_bongocat_restart_animation(ctx, animation_state_row_t::Idle, - new_animation_result, new_state, - current_state, current_frames, current_config); - } + return anim_bongocat_restart_animation(ctx, input, new_row_state, new_animation_result, new_state, current_state, + current_frames); +} - if constexpr (features::BongocatIdleAnimation) { - if (!stop_writing && !stop_test_animation && conditions.process_idle_animation) { - if (current_state.row_state == animation_state_row_t::Idle) { - // loop idle animation - anim_bongocat_process_animation(new_animation_result, new_state, current_state, current_frames); - } else if (current_state.row_state == animation_state_row_t::WakeUp) { - // process wake up animation - anim_bongocat_start_or_process_animation(ctx, animation_state_row_t::Idle, // back to idle, when animation ended - new_animation_result, new_state, - current_state, current_frames, current_config); - } +static anim_next_frame_result_t +anim_bongocat_idle_next_frame(animation_thread_context_t& ctx, const platform::input::input_context_t& input, + [[maybe_unused]] const platform::update::update_context_t& upd, animation_state_t& state, + const anim_handle_key_press_result_t& trigger_result) { + using namespace assets; + // read-only config + assert(ctx._local_copy_config); + const config::config_t& current_config = *ctx._local_copy_config; + + assert(ctx.shm); + assert(input.shm); + animation_shared_memory_t& anim_shm = *ctx.shm; + const auto& input_shm = *input.shm; + const auto current_state = state; + const auto& current_animation_result = anim_shm.animation_player_result; + [[maybe_unused]] const int anim_index = anim_shm.anim_index; + const platform::timestamp_ms_t last_key_pressed_timestamp = input_shm.last_key_pressed_timestamp; + assert(anim_shm.anim_type == config::config_animation_sprite_sheet_layout_t::Bongocat); + assert(get_current_animation(ctx).type == animation_t::type_t::Bongocat); + const auto& current_frames = get_current_animation(ctx).bongocat; + + auto new_animation_result = anim_shm.animation_player_result; + auto new_state = state; + + const auto conditions = get_anim_conditions(ctx, input, upd, current_state, trigger_result.trigger, current_config); + + // Idle Animation + const bool stop_writing = conditions.is_writing && conditions.release_frame_after_press; + const bool stop_test_animation = conditions.trigger_test_animation && + current_state.row_state == animation_state_row_t::Test && + conditions.release_test_frame; + if (stop_writing || stop_test_animation) { + // back to idle + anim_bongocat_restart_animation(ctx, input, animation_state_row_t::Idle, new_animation_result, new_state, + current_state, current_frames); + } + + if constexpr (features::BongocatIdleAnimation) { + if (!stop_writing && !stop_test_animation && conditions.process_idle_animation) { + if (current_state.row_state == animation_state_row_t::Idle) { + // loop idle animation + anim_bongocat_process_animation(input, new_animation_result, new_state, current_state, current_frames); + } else if (current_state.row_state == animation_state_row_t::WakeUp) { + // process wake up animation + anim_bongocat_start_or_process_animation(ctx, input, + animation_state_row_t::Idle, // back to idle, when animation ended + new_animation_result, new_state, current_state, current_frames); + } + } + } else { + if (current_state.row_state == animation_state_row_t::WakeUp && conditions.release_frame_for_non_idle) { + // back to idle + anim_bongocat_restart_animation(ctx, input, animation_state_row_t::Idle, new_animation_result, new_state, + current_state, current_frames); + } + } + + // Start Test animation + if (!conditions.any_key_pressed && conditions.trigger_test_animation && + current_state.row_state == animation_state_row_t::Idle) { + anim_bongocat_restart_animation(ctx, input, animation_state_row_t::Test, new_animation_result, new_state, + current_state, current_frames); + } else if (!conditions.any_key_pressed && conditions.trigger_test_animation && + current_state.row_state == animation_state_row_t::Test) { + // loop test animation + anim_bongocat_process_animation(input, new_animation_result, new_state, current_state, current_frames); + } + + const bool is_sleeping_time = current_config.enable_scheduled_sleep >= 1 && is_sleep_time(current_config); + + // Idle Sleep + if (conditions.check_for_idle_sleep) { + if (!is_sleeping_time) { + const platform::timestamp_ms_t now = platform::get_current_time_ms(); + const platform::time_ms_t idle_sleep_timeout_ms = current_config.idle_sleep_timeout_sec * 1000L; + assert(now >= last_key_pressed_timestamp); + const auto sleep_timeout = now - last_key_pressed_timestamp; + + if constexpr (features::BongocatBoringAnimation) { + const bool start_boring = SLEEP_BORING_PART > 0 && sleep_timeout >= idle_sleep_timeout_ms / SLEEP_BORING_PART; + if (current_state.row_state == animation_state_row_t::Idle) { + // start boring animation + if (start_boring && !current_state.show_boring_animation_once) { + anim_bongocat_restart_animation(ctx, input, animation_state_row_t::Boring, new_animation_result, new_state, + current_state, current_frames); + new_state.show_boring_animation_once = true; + } + } else if (current_state.row_state == animation_state_row_t::Boring) { + if constexpr (features::BongocatIdleAnimation) { + if (conditions.process_idle_animation) { + const auto animation_result = anim_bongocat_start_or_process_animation( + ctx, input, animation_state_row_t::Idle, // back to idle, when animation ended + new_animation_result, new_state, current_state, current_frames); + if (!start_boring && animation_result.row_state == animation_state_row_t::Idle) { + new_state.show_boring_animation_once = false; + } } - } else { - if (current_state.row_state == animation_state_row_t::WakeUp && conditions.release_frame_for_non_idle) { + } else { + if (!start_boring || (start_boring && current_state.show_boring_animation_once)) { + if (conditions.release_frame_for_non_idle) { // back to idle - anim_bongocat_restart_animation(ctx, animation_state_row_t::Idle, - new_animation_result, new_state, - current_state, current_frames, current_config); - } - } - - // Start Test animation - if (!conditions.any_key_pressed && conditions.trigger_test_animation && current_state.row_state == animation_state_row_t::Idle) { - anim_bongocat_restart_animation(ctx, animation_state_row_t::Test, - new_animation_result, new_state, - current_state, current_frames, current_config); - } else if (!conditions.any_key_pressed && conditions.trigger_test_animation && current_state.row_state == animation_state_row_t::Test) { - // loop test animation - anim_bongocat_process_animation(new_animation_result, new_state, current_state, current_frames); - } - - const bool is_sleeping_time = current_config.enable_scheduled_sleep && is_sleep_time(current_config); - - // Idle Sleep - if (conditions.check_for_idle_sleep) { - if (!is_sleeping_time) { - const platform::timestamp_ms_t now = platform::get_current_time_ms(); - const platform::time_ms_t idle_sleep_timeout_ms = current_config.idle_sleep_timeout_sec*1000; - assert(now >= last_key_pressed_timestamp); - const auto sleep_timeout = now - last_key_pressed_timestamp; - - if constexpr (features::BongocatBoringAnimation) { - const bool start_boring = SLEEP_BORING_PART > 0 && sleep_timeout >= idle_sleep_timeout_ms/SLEEP_BORING_PART; - if (current_state.row_state == animation_state_row_t::Idle) { - // start boring animation - if (start_boring && !current_state.show_boring_animation_once) { - anim_bongocat_restart_animation(ctx, animation_state_row_t::Boring, - new_animation_result, new_state, - current_state, current_frames, current_config); - new_state.show_boring_animation_once = true; - } - } else if (current_state.row_state == animation_state_row_t::Boring) { - if constexpr (features::BongocatIdleAnimation) { - if (conditions.process_idle_animation) { - const auto animation_result = anim_bongocat_start_or_process_animation(ctx, animation_state_row_t::Idle, // back to idle, when animation ended - new_animation_result, new_state, - current_state, current_frames, current_config); - if (!start_boring && animation_result.row_state == animation_state_row_t::Idle) { - new_state.show_boring_animation_once = false; - } - } - } else { - if (!start_boring || (start_boring && current_state.show_boring_animation_once)) { - if (conditions.release_frame_for_non_idle) { - // back to idle - anim_bongocat_restart_animation(ctx, animation_state_row_t::Idle, - new_animation_result, new_state, - current_state, current_frames, current_config); - if (!start_boring) { - new_state.show_boring_animation_once = false; - } - } - } - } - } - } - - // idle sleep - if (sleep_timeout >= idle_sleep_timeout_ms) { - if (current_state.row_state == animation_state_row_t::Idle) { - anim_bongocat_restart_animation(ctx, animation_state_row_t::Sleep, - new_animation_result, new_state, - current_state, current_frames, current_config); - new_state.is_idle_sleep = true; - new_state.show_boring_animation_once = false; - } else if (current_state.is_idle_sleep) { - if constexpr (features::BongocatIdleAnimation) { - if (current_state.row_state == animation_state_row_t::Sleep) { - if (conditions.process_idle_animation) { - // loop sleep animation - anim_bongocat_process_animation(new_animation_result, new_state, current_state, current_frames); - } - } - } - } - } else if (current_state.row_state == animation_state_row_t::Sleep && current_state.is_idle_sleep) { - if constexpr (features::BongocatIdleAnimation) { - if (conditions.process_idle_animation) { - const auto animation_result = anim_bongocat_start_or_process_animation(ctx, animation_state_row_t::Idle, // back to idle, when animation ended - new_animation_result, new_state, - current_state, current_frames, current_config); - if (animation_result.row_state == animation_state_row_t::Idle) { - new_state.is_idle_sleep = false; - } - } - } else { - if (conditions.release_frame_for_non_idle) { - // back to idle - anim_bongocat_restart_animation(ctx, animation_state_row_t::Idle, - new_animation_result, new_state, - current_state, current_frames, current_config); - new_state.is_idle_sleep = false; - } - } + anim_bongocat_restart_animation(ctx, input, animation_state_row_t::Idle, new_animation_result, + new_state, current_state, current_frames); + if (!start_boring) { + new_state.show_boring_animation_once = false; } + } } + } } - - // Sleep Mode - if (current_config.enable_scheduled_sleep) { - if (is_sleeping_time) { - if (current_state.row_state == animation_state_row_t::Idle) { - anim_bongocat_restart_animation(ctx, animation_state_row_t::Sleep, - new_animation_result, new_state, - current_state, current_frames, current_config); - new_state.is_idle_sleep = false; - } else { - if constexpr (features::BongocatIdleAnimation) { - if (current_state.row_state == animation_state_row_t::Sleep && conditions.process_idle_animation) { - // loop sleep animation - anim_bongocat_process_animation(new_animation_result, new_state, current_state, current_frames); - } - } - } - } else { - if (current_state.row_state == animation_state_row_t::Sleep && !current_state.is_idle_sleep) { - if (conditions.release_frame_for_non_idle) { - // back to idle - anim_bongocat_restart_animation(ctx, animation_state_row_t::Idle, - new_animation_result, new_state, - current_state, current_frames, current_config); - } - } - } - } else { - if (current_state.row_state == animation_state_row_t::Sleep && !current_state.is_idle_sleep) { - if (conditions.release_frame_for_non_idle) { - // back to idle - anim_bongocat_restart_animation(ctx, animation_state_row_t::Idle, - new_animation_result, new_state, - current_state, current_frames, current_config); - } - } - } - - return anim_update_animation_state(anim_shm, state, - new_animation_result, new_state, - current_animation_result, current_state, - current_config); - } - - static anim_next_frame_result_t anim_bongocat_key_pressed_next_frame(animation_context_t& ctx, animation_state_t& state, - const platform::input::input_context_t& input, - [[maybe_unused]] const platform::update::update_context_t& upd, - const animation_trigger_t& trigger) { - using namespace assets; - - // read-only config - assert(ctx._local_copy_config != nullptr); - const config::config_t& current_config = *ctx._local_copy_config; - - assert(ctx.shm != nullptr); - assert(input.shm != nullptr); - animation_shared_memory_t& anim_shm = *ctx.shm; - //const auto& input_shm = *input.shm; - const auto current_state = state; - const auto& current_animation_result = anim_shm.animation_player_result; - [[maybe_unused]] const int anim_index = anim_shm.anim_index; - //const platform::timestamp_ms_t last_key_pressed_timestamp = input_shm.last_key_pressed_timestamp; - assert(get_current_animation(ctx).type == animation_t::Type::Bongocat); - const auto& current_frames = get_current_animation(ctx).bongocat; - - auto new_animation_result = anim_shm.animation_player_result; - auto new_state = state; - - const auto conditions = get_anim_conditions(ctx, input, upd, current_state, trigger, current_config); - - /// @TODO: use state machine for animation (states) - - // in Writing mode/start writing - if (!conditions.is_writing) { - if (state.row_state == animation_state_row_t::Sleep && current_state.is_idle_sleep) { - // wake up - anim_bongocat_restart_animation(ctx, animation_state_row_t::WakeUp, - new_animation_result, new_state, - current_state, current_frames, current_config); - } else if (state.row_state == animation_state_row_t::Idle || conditions.is_moving) { - // start writing - anim_bongocat_restart_animation(ctx, animation_state_row_t::StartWriting, - new_animation_result, new_state, - current_state, current_frames, current_config); + } + + // idle sleep + if (sleep_timeout >= idle_sleep_timeout_ms) { + if (current_state.row_state == animation_state_row_t::Idle) { + anim_bongocat_restart_animation(ctx, input, animation_state_row_t::Sleep, new_animation_result, new_state, + current_state, current_frames); + new_state.is_idle_sleep = true; + new_state.show_boring_animation_once = false; + } else if (current_state.is_idle_sleep) { + if constexpr (features::BongocatIdleAnimation) { + if (current_state.row_state == animation_state_row_t::Sleep) { + if (conditions.process_idle_animation) { + // loop sleep animation + anim_bongocat_process_animation(input, new_animation_result, new_state, current_state, current_frames); + } } - } else if (state.row_state == animation_state_row_t::StartWriting) { - anim_bongocat_start_or_process_animation(ctx, animation_state_row_t::Writing, - new_animation_result, new_state, - current_state, current_frames, current_config); + } } - + } else if (current_state.row_state == animation_state_row_t::Sleep && current_state.is_idle_sleep) { if constexpr (features::BongocatIdleAnimation) { - if (conditions.is_writing && conditions.process_idle_animation) { - if (conditions.release_frame_after_press && current_state.row_state == animation_state_row_t::Writing) { - anim_bongocat_start_or_process_animation(ctx, animation_state_row_t::EndWriting, - new_animation_result, new_state, - current_state, current_frames, current_config); - } else if (conditions.release_frame_after_press && (current_state.row_state == animation_state_row_t::EndWriting || current_state.row_state == animation_state_row_t::WakeUp)) { - anim_bongocat_start_or_process_animation(ctx, animation_state_row_t::Idle, // back to idle - new_animation_result, new_state, - current_state, current_frames, current_config); - } + if (conditions.process_idle_animation) { + const auto animation_result = anim_bongocat_start_or_process_animation( + ctx, input, animation_state_row_t::Idle, // back to idle, when animation ended + new_animation_result, new_state, current_state, current_frames); + if (animation_result.row_state == animation_state_row_t::Idle) { + new_state.is_idle_sleep = false; } + } } else { - if (conditions.is_writing) { - if (conditions.continue_writing) { - // keep writing - anim_bongocat_process_animation(new_animation_result, new_state, current_state, current_frames); - } - } else if (((!conditions.is_writing && state.row_state == animation_state_row_t::Idle) || state.row_state == animation_state_row_t::WakeUp) && conditions.release_frame_after_press) { - // back to idle - anim_bongocat_restart_animation(ctx, animation_state_row_t::Idle, - new_animation_result, new_state, - current_state, current_frames, current_config); - } + if (conditions.release_frame_for_non_idle) { + // back to idle + anim_bongocat_restart_animation(ctx, input, animation_state_row_t::Idle, new_animation_result, new_state, + current_state, current_frames); + new_state.is_idle_sleep = false; + } } - - return anim_update_animation_state(anim_shm, state, - new_animation_result, new_state, - current_animation_result, current_state, - current_config); + } } -#endif - -#ifdef FEATURE_ENABLE_DM_EMBEDDED_ASSETS - enum class anim_dm_process_animation_result_status_t : uint8_t { None, Started, Updated, End, Looped, NextAnimationStarted, SkipMovement, Moved, Stop }; - struct anim_dm_process_animation_result_t { - animation_state_row_t row_state; - anim_dm_process_animation_result_status_t status{anim_dm_process_animation_result_status_t::None}; - }; - static anim_dm_process_animation_result_t anim_dm_process_animation(animation_player_result_t& new_animation_result, - animation_state_t& new_state, - const animation_state_t& current_state, - const dm_sprite_sheet_t& current_frames) { - assert(MAX_ANIMATION_FRAMES > 0); - assert(MAX_ANIMATION_FRAMES <= INT_MAX); - - anim_dm_process_animation_result_t ret {.row_state = new_state.row_state, .status = anim_dm_process_animation_result_status_t::Updated}; - // forward animation - new_state.animations_index = current_state.animations_index + 1; - if (new_state.animations_index > static_cast(MAX_ANIMATION_FRAMES-1)) { - ret.status = anim_dm_process_animation_result_status_t::Looped; - new_state.animations_index = 0; - } - /* - // backwards animation - else if (new_state.animations_index < 0) { - ret = anim_bongocat_process_animation_result_t::End; - new_state.animations_index = MAX_ANIMATION_FRAMES-1; + } + + // Sleep Mode + if (current_config.enable_scheduled_sleep >= 1) { + if (is_sleeping_time) { + if (current_state.row_state == animation_state_row_t::Idle) { + anim_bongocat_restart_animation(ctx, input, animation_state_row_t::Sleep, new_animation_result, new_state, + current_state, current_frames); + new_state.is_idle_sleep = false; + } else { + if constexpr (features::BongocatIdleAnimation) { + if (current_state.row_state == animation_state_row_t::Sleep && conditions.process_idle_animation) { + // loop sleep animation + anim_bongocat_process_animation(input, new_animation_result, new_state, current_state, current_frames); + } } - */ - switch (new_state.row_state) { - case animation_state_row_t::Idle: - new_animation_result.sprite_sheet_col = current_frames.animations.idle[new_state.animations_index]; - break; - case animation_state_row_t::StartWriting: - case animation_state_row_t::Writing: - case animation_state_row_t::EndWriting: - new_animation_result.sprite_sheet_col = current_frames.animations.writing[new_state.animations_index]; - break; - case animation_state_row_t::Happy: - new_animation_result.sprite_sheet_col = current_frames.animations.happy[new_state.animations_index]; - break; - case animation_state_row_t::FallASleep: - // not supported - new_animation_result.sprite_sheet_col = current_frames.animations.sleep[new_state.animations_index]; - break; - case animation_state_row_t::Sleep: - new_animation_result.sprite_sheet_col = current_frames.animations.sleep[new_state.animations_index]; - break; - case animation_state_row_t::WakeUp: - new_animation_result.sprite_sheet_col = current_frames.animations.wake_up[new_state.animations_index]; - break; - case animation_state_row_t::Boring: - new_animation_result.sprite_sheet_col = current_frames.animations.boring[new_state.animations_index]; - break; - case animation_state_row_t::Test: - new_animation_result.sprite_sheet_col = current_frames.animations.writing[new_state.animations_index]; - break; - case animation_state_row_t::StartWorking: - case animation_state_row_t::Working: - case animation_state_row_t::EndWorking: - new_animation_result.sprite_sheet_col = current_frames.animations.working[new_state.animations_index]; - break; - case animation_state_row_t::StartMoving: - case animation_state_row_t::Moving: - case animation_state_row_t::EndMoving: - new_animation_result.sprite_sheet_col = current_frames.animations.moving[new_state.animations_index]; - break; - case animation_state_row_t::StartRunning: - case animation_state_row_t::Running: - case animation_state_row_t::EndRunning: - new_animation_result.sprite_sheet_col = current_frames.animations.running[new_state.animations_index]; - break; + } + } else { + if (current_state.row_state == animation_state_row_t::Sleep && !current_state.is_idle_sleep) { + if (conditions.release_frame_for_non_idle) { + // back to idle + anim_bongocat_restart_animation(ctx, input, animation_state_row_t::Idle, new_animation_result, new_state, + current_state, current_frames); } - return ret; + } } - static anim_dm_process_animation_result_t anim_dm_restart_animation([[maybe_unused]] animation_context_t& ctx, - animation_state_row_t new_row_state, - animation_player_result_t& new_animation_result, - animation_state_t& new_state, - [[maybe_unused]] const animation_state_t& current_state, - const dm_sprite_sheet_t& current_frames, - const config::config_t& current_config) { - using namespace assets; - assert(MAX_ANIMATION_FRAMES > 0); - assert(MAX_ANIMATION_FRAMES <= INT_MAX); - - new_state.row_state = new_row_state; - new_animation_result.sprite_sheet_row = DM_SPRITE_SHEET_ROW; - new_state.animations_index = 0; - - anim_dm_process_animation_result_t ret {.row_state = new_state.row_state, .status = anim_dm_process_animation_result_status_t::Started}; - switch (new_state.row_state) { - case animation_state_row_t::Idle: - new_animation_result.sprite_sheet_col = current_frames.animations.idle[new_state.animations_index]; - if (current_config.idle_frame) { - new_animation_result.sprite_sheet_col = current_config.idle_frame; - } - break; - case animation_state_row_t::StartWriting: - case animation_state_row_t::Writing: - case animation_state_row_t::EndWriting: - new_animation_result.sprite_sheet_col = current_frames.animations.writing[new_state.animations_index]; - break; - case animation_state_row_t::Happy: - new_animation_result.sprite_sheet_col = current_frames.animations.happy[new_state.animations_index]; - break; - case animation_state_row_t::FallASleep: - // not supported - new_animation_result.sprite_sheet_col = current_frames.animations.sleep[new_state.animations_index]; - break; - case animation_state_row_t::Sleep: - new_animation_result.sprite_sheet_col = current_frames.animations.sleep[new_state.animations_index]; - break; - case animation_state_row_t::WakeUp: - new_animation_result.sprite_sheet_col = current_frames.animations.wake_up[new_state.animations_index]; - break; - case animation_state_row_t::Boring: - new_animation_result.sprite_sheet_col = current_frames.animations.boring[new_state.animations_index]; - break; - case animation_state_row_t::Test: - new_animation_result.sprite_sheet_col = current_frames.animations.writing[new_state.animations_index]; - break; - case animation_state_row_t::StartWorking: - case animation_state_row_t::Working: - case animation_state_row_t::EndWorking: - new_animation_result.sprite_sheet_col = current_frames.animations.working[new_state.animations_index]; - break; - case animation_state_row_t::StartMoving: - case animation_state_row_t::Moving: - case animation_state_row_t::EndMoving: - new_animation_result.sprite_sheet_col = current_frames.animations.moving[new_state.animations_index]; - break; - case animation_state_row_t::StartRunning: - case animation_state_row_t::Running: - case animation_state_row_t::EndRunning: - new_animation_result.sprite_sheet_col = current_frames.animations.running[new_state.animations_index]; - break; - } - return ret; - } - static anim_dm_process_animation_result_t anim_dm_show_single_frame([[maybe_unused]] animation_context_t& ctx, - animation_state_row_t new_row_state, - animation_player_result_t& new_animation_result, - animation_state_t& new_state, - [[maybe_unused]] const animation_state_t& current_state, - [[maybe_unused]] const dm_sprite_sheet_t& current_frames, - const config::config_t& current_config) { - using namespace assets; - assert(MAX_ANIMATION_FRAMES > 0); - assert(MAX_ANIMATION_FRAMES <= INT_MAX); - - new_state.row_state = new_row_state; - new_animation_result.sprite_sheet_row = DM_SPRITE_SHEET_ROW; - new_state.animations_index = 0; - - anim_dm_process_animation_result_t ret {.row_state = new_state.row_state, .status = anim_dm_process_animation_result_status_t::Started}; - switch (new_state.row_state) { - case animation_state_row_t::Idle: - new_animation_result.sprite_sheet_col = DM_FRAME_IDLE1; - if (current_config.idle_frame) { - new_animation_result.sprite_sheet_col = current_config.idle_frame; - } - break; - case animation_state_row_t::StartWriting: - case animation_state_row_t::Writing: - case animation_state_row_t::EndWriting: - // toggle frame - if (new_animation_result.sprite_sheet_col == DM_FRAME_IDLE1) { - new_animation_result.sprite_sheet_col = DM_FRAME_IDLE2; - } else if (new_animation_result.sprite_sheet_col == DM_FRAME_IDLE2) { - new_animation_result.sprite_sheet_col = DM_FRAME_IDLE1; - } else { - new_animation_result.sprite_sheet_col = ctx._rng.range(0, 100) <= 50 ? DM_FRAME_IDLE1 : DM_FRAME_IDLE2; - } - break; - case animation_state_row_t::Happy: - if (current_frames.frames.happy.valid) { - new_animation_result.sprite_sheet_col = current_frames.frames.happy.col; - } else { - // toggle frame - if (new_animation_result.sprite_sheet_col == DM_FRAME_IDLE1) { - new_animation_result.sprite_sheet_col = DM_FRAME_IDLE2; - } else if (new_animation_result.sprite_sheet_col == DM_FRAME_IDLE2) { - new_animation_result.sprite_sheet_col = DM_FRAME_IDLE1; - } else { - new_animation_result.sprite_sheet_col = ctx._rng.range(0, 100) <= 50 ? DM_FRAME_IDLE1 : DM_FRAME_IDLE2; - } - } - break; - case animation_state_row_t::FallASleep: - // not fully supported - if (current_frames.frames.sleep.valid) { - new_animation_result.sprite_sheet_col = current_frames.frames.sleep.col; - } else if (current_frames.frames.down.valid) { - new_animation_result.sprite_sheet_col = current_frames.frames.down.col; - } else { - new_animation_result.sprite_sheet_col = DM_FRAME_IDLE1; - } - break; - case animation_state_row_t::Sleep: - if (current_frames.frames.sleep.valid) { - new_animation_result.sprite_sheet_col = current_frames.frames.sleep.col; - } else if (current_frames.frames.down.valid) { - new_animation_result.sprite_sheet_col = current_frames.frames.down.col; - } else { - new_animation_result.sprite_sheet_col = DM_FRAME_IDLE1; - } - break; - case animation_state_row_t::WakeUp: - if (current_frames.frames.happy.valid) { - new_animation_result.sprite_sheet_col = current_frames.frames.sleep.col; - } else { - new_animation_result.sprite_sheet_col = DM_FRAME_IDLE2; - } - break; - case animation_state_row_t::Boring: - if (current_frames.frames.sad.valid) { - new_animation_result.sprite_sheet_col = current_frames.frames.sad.col; - } else if (current_frames.frames.angry.valid) { - new_animation_result.sprite_sheet_col = current_frames.frames.angry.col; - } else if (current_frames.frames.down.valid) { - new_animation_result.sprite_sheet_col = current_frames.frames.down.col; - } else { - // toggle frame - if (new_animation_result.sprite_sheet_col == DM_FRAME_IDLE1) { - new_animation_result.sprite_sheet_col = DM_FRAME_IDLE2; - } else if (new_animation_result.sprite_sheet_col == DM_FRAME_IDLE2) { - new_animation_result.sprite_sheet_col = DM_FRAME_IDLE1; - } else { - new_animation_result.sprite_sheet_col = ctx._rng.range(0, 100) <= 50 ? DM_FRAME_IDLE1 : DM_FRAME_IDLE2; - } - } - break; - case animation_state_row_t::Test: - // toggle frame - if (new_animation_result.sprite_sheet_col == DM_FRAME_IDLE1) { - new_animation_result.sprite_sheet_col = DM_FRAME_IDLE2; - } else if (new_animation_result.sprite_sheet_col == DM_FRAME_IDLE2) { - new_animation_result.sprite_sheet_col = DM_FRAME_IDLE1; - } else { - new_animation_result.sprite_sheet_col = ctx._rng.range(0, 100) <= 50 ? DM_FRAME_IDLE1 : DM_FRAME_IDLE2; - } - break; - case animation_state_row_t::StartWorking: - case animation_state_row_t::Working: - case animation_state_row_t::EndWorking: - if (current_frames.frames.attack_1.valid) { - new_animation_result.sprite_sheet_col = current_frames.frames.attack_1.col; - } else if (current_frames.frames.angry.valid) { - new_animation_result.sprite_sheet_col = current_frames.frames.angry.col; - } else { - // toggle frame - if (new_animation_result.sprite_sheet_col == DM_FRAME_IDLE1) { - new_animation_result.sprite_sheet_col = DM_FRAME_IDLE2; - } else if (new_animation_result.sprite_sheet_col == DM_FRAME_IDLE2) { - new_animation_result.sprite_sheet_col = DM_FRAME_IDLE1; - } else { - new_animation_result.sprite_sheet_col = ctx._rng.range(0, 100) <= 50 ? DM_FRAME_IDLE1 : DM_FRAME_IDLE2; - } - } - break; - case animation_state_row_t::StartMoving: - case animation_state_row_t::Moving: - if (current_frames.frames.movement_1.valid) { - new_animation_result.sprite_sheet_col = current_frames.frames.movement_1.col; - } else { - // toggle frame - if (new_animation_result.sprite_sheet_col == DM_FRAME_IDLE1) { - new_animation_result.sprite_sheet_col = DM_FRAME_IDLE2; - } else if (new_animation_result.sprite_sheet_col == DM_FRAME_IDLE2) { - new_animation_result.sprite_sheet_col = DM_FRAME_IDLE1; - } else { - new_animation_result.sprite_sheet_col = ctx._rng.range(0, 100) <= 50 ? DM_FRAME_IDLE1 : DM_FRAME_IDLE2; - } - } - break; - case animation_state_row_t::EndMoving: - if (current_frames.frames.movement_2.valid) { - new_animation_result.sprite_sheet_col = current_frames.frames.movement_2.col; - } else { - // toggle frame - if (new_animation_result.sprite_sheet_col == DM_FRAME_IDLE1) { - new_animation_result.sprite_sheet_col = DM_FRAME_IDLE2; - } else if (new_animation_result.sprite_sheet_col == DM_FRAME_IDLE2) { - new_animation_result.sprite_sheet_col = DM_FRAME_IDLE1; - } else { - new_animation_result.sprite_sheet_col = ctx._rng.range(0, 100) <= 50 ? DM_FRAME_IDLE1 : DM_FRAME_IDLE2; - } - } - break; - case animation_state_row_t::StartRunning: - case animation_state_row_t::Running: - if (current_frames.frames.movement_1.valid) { - new_animation_result.sprite_sheet_col = current_frames.frames.movement_1.col; - } else { - // toggle frame - if (new_animation_result.sprite_sheet_col == DM_FRAME_IDLE1) { - new_animation_result.sprite_sheet_col = DM_FRAME_IDLE2; - } else if (new_animation_result.sprite_sheet_col == DM_FRAME_IDLE2) { - new_animation_result.sprite_sheet_col = DM_FRAME_IDLE1; - } else { - new_animation_result.sprite_sheet_col = ctx._rng.range(0, 100) <= 50 ? DM_FRAME_IDLE1 : DM_FRAME_IDLE2; - } - } - break; - case animation_state_row_t::EndRunning: - if (current_frames.frames.movement_2.valid) { - new_animation_result.sprite_sheet_col = current_frames.frames.movement_2.col; - } else { - // toggle frame - if (new_animation_result.sprite_sheet_col == DM_FRAME_IDLE1) { - new_animation_result.sprite_sheet_col = DM_FRAME_IDLE2; - } else if (new_animation_result.sprite_sheet_col == DM_FRAME_IDLE2) { - new_animation_result.sprite_sheet_col = DM_FRAME_IDLE1; - } else { - new_animation_result.sprite_sheet_col = ctx._rng.range(0, 100) <= 50 ? DM_FRAME_IDLE1 : DM_FRAME_IDLE2; - } - } - break; - } - return ret; + } else { + if (current_state.row_state == animation_state_row_t::Sleep && !current_state.is_idle_sleep) { + if (conditions.release_frame_for_non_idle) { + // back to idle + anim_bongocat_restart_animation(ctx, input, animation_state_row_t::Idle, new_animation_result, new_state, + current_state, current_frames); + } } + } - static anim_dm_process_animation_result_t anim_dm_start_or_process_animation(animation_context_t& ctx, - animation_state_row_t new_row_state, - animation_player_result_t& new_animation_result, - animation_state_t& new_state, - const animation_state_t& current_state, - const dm_sprite_sheet_t& current_frames, - const config::config_t& current_config) { - - if (current_state.row_state != new_row_state) { - auto result = anim_dm_process_animation(new_animation_result, new_state, current_state, current_frames); - if (result.status == anim_dm_process_animation_result_status_t::End || result.status == anim_dm_process_animation_result_status_t::Looped) { - result = anim_dm_restart_animation(ctx, new_row_state, new_animation_result, new_state, - current_state, current_frames, current_config); - result.status = anim_dm_process_animation_result_status_t::NextAnimationStarted; - } - return result; - } + return anim_update_animation_state(anim_shm, state, new_animation_result, new_state, current_animation_result, + current_state, current_config); +} - return anim_dm_restart_animation(ctx, new_row_state, new_animation_result, new_state, - current_state, current_frames, current_config); +static anim_next_frame_result_t anim_bongocat_key_pressed_next_frame( + animation_thread_context_t& ctx, animation_state_t& state, const platform::input::input_context_t& input, + [[maybe_unused]] const platform::update::update_context_t& upd, const animation_trigger_t& trigger) { + using namespace assets; + + // read-only config + assert(ctx._local_copy_config); + const config::config_t& current_config = *ctx._local_copy_config; + + assert(ctx.shm); + assert(input.shm); + animation_shared_memory_t& anim_shm = *ctx.shm; + // const auto& input_shm = *input.shm; + const auto current_state = state; + const auto& current_animation_result = anim_shm.animation_player_result; + [[maybe_unused]] const int anim_index = anim_shm.anim_index; + // const platform::timestamp_ms_t last_key_pressed_timestamp = input_shm.last_key_pressed_timestamp; + assert(get_current_animation(ctx).type == animation_t::type_t::Bongocat); + const auto& current_frames = get_current_animation(ctx).bongocat; + + auto new_animation_result = anim_shm.animation_player_result; + auto new_state = state; + + const auto conditions = get_anim_conditions(ctx, input, upd, current_state, trigger, current_config); + + /// @TODO: use state machine for animation (states) + + // in Writing mode/start writing + if (!conditions.is_writing) { + if (state.row_state == animation_state_row_t::Sleep && current_state.is_idle_sleep) { + // wake up + anim_bongocat_restart_animation(ctx, input, animation_state_row_t::WakeUp, new_animation_result, new_state, + current_state, current_frames); + } else if (state.row_state == animation_state_row_t::Idle || conditions.is_moving) { + // start writing + anim_bongocat_restart_animation(ctx, input, animation_state_row_t::StartWriting, new_animation_result, new_state, + current_state, current_frames); } - - - static anim_dm_process_animation_result_t anim_dm_handle_movement(animation_context_t& ctx, - const platform::input::input_context_t& input, - [[maybe_unused]] const platform::update::update_context_t& upd, - animation_player_result_t& new_animation_result, - animation_state_t& new_state, - const anim_handle_key_press_result_t& trigger_result, - const animation_state_t& current_state, - const dm_sprite_sheet_t& current_frames, - const config::config_t& current_config) { - using namespace assets; - - assert(ctx.shm != nullptr); - animation_shared_memory_t& anim_shm = *ctx.shm; - - const auto conditions = get_anim_conditions(ctx, input, upd, current_state, trigger_result.trigger, current_config); - - anim_dm_process_animation_result_t ret {.row_state = new_state.row_state, .status = anim_dm_process_animation_result_status_t::None}; - if (!conditions.is_writing && current_config.movement_speed > 0 && current_config.movement_radius > 0 && current_config.fps > 0) { - if (conditions.process_idle_animation || conditions.process_movement || conditions.process_movement_animation) { - const float delta = 1.0f / static_cast(current_config.fps); - const auto delta_ms = static_cast(delta * 1000); - const auto frame_height = current_config.cat_height; - const auto frame_width = static_cast(static_cast(frame_height) * (static_cast(current_frames.frame_width) / static_cast(current_frames.frame_height))); - const auto fmovement_radius = static_cast(current_config.movement_radius); - - assert(current_config.movement_radius > 0); - float max_movement_offset_x_left = 0.0f; - float max_movement_offset_x_right = 0.0f; - float wall_distance = 0.0f; - switch (current_config.cat_align) { - case config::align_type_t::ALIGN_CENTER: - // range: [-r, +r] - max_movement_offset_x_left = -static_cast(current_config.movement_radius) + static_cast(frame_width) / 2.0f; - max_movement_offset_x_right = static_cast(current_config.movement_radius - frame_width); - wall_distance = anim_shm.movement_offset_x / fmovement_radius; - break; - case config::align_type_t::ALIGN_LEFT: - // range: [0, +2r] - max_movement_offset_x_left = 0.0f; - max_movement_offset_x_right = static_cast(current_config.movement_radius) * 2.0f - static_cast(frame_width); - wall_distance = (anim_shm.movement_offset_x / (fmovement_radius * 2.0f)) * 2.0f - 1.0f; - break; - case config::align_type_t::ALIGN_RIGHT: - // range: [-2r, 0] - max_movement_offset_x_left = -(static_cast(current_config.movement_radius) * 2.0f) + static_cast(frame_width); - max_movement_offset_x_right = 0.0f; - wall_distance = (anim_shm.movement_offset_x / (fmovement_radius * 2.0f)) * 2.0f + 1.0f; // normalize to -1 → 1 - break; - } - - if (current_state.row_state == animation_state_row_t::Idle && (current_state.anim_pause_after_movement_ms > 0 || (ctx._rng.range(0, 100) <= CHANCE_FOR_SKIPPING_MOVEMENT_PERCENT))) { - // skip movement - new_state.anim_velocity = 0.0f; - new_state.anim_distance = 0.0f; - if (new_state.anim_pause_after_movement_ms > 0) { - new_state.anim_pause_after_movement_ms -= delta_ms; - if (new_state.anim_pause_after_movement_ms <= 0) { - new_state.anim_pause_after_movement_ms = 0; - } - } - ret.status = anim_dm_process_animation_result_status_t::SkipMovement; - } else { - // moving animation - constexpr float DIR_EPSILON = 1e-3f; - assert(MAX_DISTANCE_PER_MOVEMENT_PART > 0); - const int movement_part = current_config.movement_radius <= MAX_MOVEMENT_RADIUS_SMALL ? SMALL_MAX_DISTANCE_PER_MOVEMENT_PART : MAX_DISTANCE_PER_MOVEMENT_PART; - const float fmovement_part = static_cast(movement_part); - if (current_state.row_state == animation_state_row_t::Idle) { - // start movement - const auto min_movement = current_config.movement_radius <= MAX_MOVEMENT_RADIUS_SMALL ? (fmovement_radius / fmovement_part / 2) + 1 : (fmovement_radius / fmovement_part / fmovement_part) + 1; - float max_move_distance = fmovement_radius / fmovement_part; - max_move_distance = max_move_distance <= min_movement ? min_movement : max_move_distance; - - assert(max_move_distance >= 0); - assert(min_movement >= 0); - new_state.anim_distance = static_cast(ctx._rng.range(static_cast(min_movement), static_cast(max_move_distance))); - - if (anim_shm.movement_offset_x >= max_movement_offset_x_right) { - // run against wall, change direction - anim_shm.anim_direction = -1.0; - anim_shm.movement_offset_x = max_movement_offset_x_right; - } else if (anim_shm.movement_offset_x <= max_movement_offset_x_left) { - // run against wall, change direction - anim_shm.anim_direction = 1.0; - anim_shm.movement_offset_x = max_movement_offset_x_left; - } else { - float toward_wall_bias = fabs(wall_distance); - toward_wall_bias = toward_wall_bias >= 1.0f ? 1.0f : toward_wall_bias; - toward_wall_bias = toward_wall_bias <= 0.0f ? 0.0f : toward_wall_bias; - - const int flip_direction_chance = current_config.movement_radius <= MAX_MOVEMENT_RADIUS_SMALL ? SMALL_FLIP_DIRECTION_NEAR_WALL_PERCENT : FLIP_DIRECTION_NEAR_WALL_PERCENT; - - assert(toward_wall_bias >= 0.0f); - // change direction: chance drops at center, changes falloff steeper near walls - const auto dir_chance = static_cast(static_cast(100 - flip_direction_chance + 10) * (1.0f - pow(toward_wall_bias, 1.5f))); - - if (fabs(new_state.anim_last_direction) >= DIR_EPSILON) { - anim_shm.anim_direction = (ctx._rng.range(0, 100) < dir_chance) ? -new_state.anim_last_direction : new_state.anim_last_direction; - } else if (fabs(anim_shm.anim_direction) < DIR_EPSILON) { - // idle: choose random start direction - anim_shm.anim_direction = (ctx._rng.range(0, 100) < dir_chance) ? 1.0f : -1.0f; - } else { - if (wall_distance > (100.0f / static_cast(flip_direction_chance))) { - anim_shm.anim_direction = (ctx._rng.range(0, 100) < dir_chance) ? -1.0f : 1.0f; - } else if (wall_distance < -(100.0f / static_cast(flip_direction_chance))) { - anim_shm.anim_direction = (ctx._rng.range(0, 100) < dir_chance) ? 1.0f : -1.0f; - } else { - anim_shm.anim_direction = (ctx._rng.range(0, 100) < dir_chance) ? anim_shm.anim_direction : -anim_shm.anim_direction; - } - } - } - // start moving animation - new_state.anim_velocity = anim_shm.anim_direction * static_cast(current_config.movement_speed); - ret = anim_dm_restart_animation(ctx, animation_state_row_t::StartMoving, - new_animation_result, new_state, - current_state, current_frames, current_config); - } else if (current_state.row_state == animation_state_row_t::StartMoving) { - if (conditions.process_idle_animation) { - ret = anim_dm_start_or_process_animation(ctx, animation_state_row_t::Moving, // continue with moving - new_animation_result, new_state, - current_state, current_frames, current_config); - ret.status = anim_dm_process_animation_result_status_t::Updated; - } else if (conditions.process_movement_animation) { - ret = anim_dm_restart_animation(ctx, animation_state_row_t::Moving, - new_animation_result, new_state, - current_state, current_frames, current_config); - ret.status = anim_dm_process_animation_result_status_t::Updated; - } - } else if (current_state.row_state == animation_state_row_t::Moving) { - if (conditions.process_movement) { - ret = anim_dm_process_animation(new_animation_result, new_state, current_state, current_frames); - ret.status = anim_dm_process_animation_result_status_t::Moved; - - new_state.anim_distance -= fabs(new_state.anim_velocity); - anim_shm.movement_offset_x += new_state.anim_velocity; - // clamp walking/movement area - if (anim_shm.movement_offset_x > max_movement_offset_x_right) { - anim_shm.movement_offset_x = max_movement_offset_x_right; - ret = anim_dm_restart_animation(ctx, animation_state_row_t::EndMoving, - new_animation_result, new_state, - current_state, current_frames, current_config); - ret.status = anim_dm_process_animation_result_status_t::Stop; - } else if (anim_shm.movement_offset_x < max_movement_offset_x_left) { - anim_shm.movement_offset_x = max_movement_offset_x_left; - ret = anim_dm_restart_animation(ctx, animation_state_row_t::EndMoving, - new_animation_result, new_state, - current_state, current_frames, current_config); - ret.status = anim_dm_process_animation_result_status_t::Stop; - } - - if (new_state.anim_distance <= 0 || (fabs(new_state.anim_distance) < DIR_EPSILON || fabs(new_state.anim_velocity) < DIR_EPSILON)) { - ret = anim_dm_restart_animation(ctx, animation_state_row_t::EndMoving, - new_animation_result, new_state, - current_state, current_frames, current_config); - ret.status = anim_dm_process_animation_result_status_t::Stop; - } - } - } else if (current_state.row_state == animation_state_row_t::EndMoving) { - if (conditions.process_idle_animation) { - ret = anim_dm_start_or_process_animation(ctx, animation_state_row_t::Idle, - new_animation_result, new_state, - current_state, current_frames, current_config); - } else { - ret = anim_dm_restart_animation(ctx, animation_state_row_t::Idle, - new_animation_result, new_state, - current_state, current_frames, current_config); - } - new_state.anim_last_direction = anim_shm.anim_direction; - new_state.anim_velocity = 0.0f; - anim_shm.anim_direction = 0.0; - if (ret.row_state == animation_state_row_t::Idle && new_state.anim_distance <= 0) { - assert(current_config.animation_speed_ms >= 0); - assert(movement_part >= 0); - const auto min_wait = current_config.animation_speed_ms * (current_config.movement_wait_factor / 2); - const auto max_wait = current_config.animation_speed_ms * current_config.movement_wait_factor; - assert(min_wait >= 0); - assert(max_wait >= 0); - new_state.anim_pause_after_movement_ms = static_cast(ctx._rng.range(static_cast(min_wait), static_cast(max_wait))); - ret.status = anim_dm_process_animation_result_status_t::End; - } - } - } - } - } else { - // movement got disabled, back to idle - if ((current_config.movement_speed <= 0 || current_config.movement_radius <= 0) && conditions.is_moving) { - // back to idle - ret = anim_dm_restart_animation(ctx, animation_state_row_t::Idle, - new_animation_result, new_state, - current_state, current_frames, current_config); - new_state.anim_velocity = 0.0f; - ret.status = anim_dm_process_animation_result_status_t::Stop; - } - } - - return ret; + } else if (state.row_state == animation_state_row_t::StartWriting) { + anim_bongocat_start_or_process_animation(ctx, input, animation_state_row_t::Writing, new_animation_result, + new_state, current_state, current_frames); + } + + if constexpr (features::BongocatIdleAnimation) { + if (conditions.is_writing && conditions.process_idle_animation) { + if (conditions.release_frame_after_press && current_state.row_state == animation_state_row_t::Writing) { + anim_bongocat_start_or_process_animation(ctx, input, animation_state_row_t::EndWriting, new_animation_result, + new_state, current_state, current_frames); + } else if (conditions.release_frame_after_press && + (current_state.row_state == animation_state_row_t::EndWriting || + current_state.row_state == animation_state_row_t::WakeUp)) { + anim_bongocat_start_or_process_animation(ctx, input, animation_state_row_t::Idle, // back to idle + new_animation_result, new_state, current_state, current_frames); + } } + } else { + if (conditions.is_writing) { + if (conditions.continue_writing) { + // keep writing + anim_bongocat_process_animation(input, new_animation_result, new_state, current_state, current_frames); + } + } else if (((!conditions.is_writing && state.row_state == animation_state_row_t::Idle) || + state.row_state == animation_state_row_t::WakeUp) && + conditions.release_frame_after_press) { + // back to idle + anim_bongocat_restart_animation(ctx, input, animation_state_row_t::Idle, new_animation_result, new_state, + current_state, current_frames); + } + } - static anim_next_frame_result_t anim_dm_idle_next_frame(animation_context_t& ctx, const platform::input::input_context_t& input, - [[maybe_unused]] const platform::update::update_context_t& upd, - animation_state_t& state, const anim_handle_key_press_result_t& trigger_result) { - using namespace assets; - - // read-only config - assert(ctx._local_copy_config != nullptr); - const config::config_t& current_config = *ctx._local_copy_config; - - assert(ctx.shm != nullptr); - assert(input.shm != nullptr); - assert(upd.shm != nullptr); - animation_shared_memory_t& anim_shm = *ctx.shm; - const auto& input_shm = *input.shm; - const auto& update_shm = *upd.shm; - const auto current_state = state; - const auto& current_animation_result = anim_shm.animation_player_result; - [[maybe_unused]] const int anim_index = anim_shm.anim_index; - const platform::timestamp_ms_t last_key_pressed_timestamp = input_shm.last_key_pressed_timestamp; - assert(get_current_animation(ctx).type == animation_t::Type::Dm); - const auto& current_frames = get_current_animation(ctx).dm; - - auto new_animation_result = anim_shm.animation_player_result; - auto new_state = state; - - const auto conditions = get_anim_conditions(ctx, input, upd, current_state, trigger_result.trigger, current_config); - - /// @TODO: make animation state machine ? - - if (!conditions.is_moving) { - // Idle Animation - const bool stop_writing = conditions.is_writing && conditions.release_frame_after_press; - const bool stop_happy_kpm = current_state.row_state == animation_state_row_t::Happy && conditions.release_frame_after_press && !conditions.process_idle_animation; - const bool stop_test_animation = conditions.trigger_test_animation && current_state.row_state == animation_state_row_t::Test && conditions.release_test_frame; - if (stop_writing || stop_test_animation || stop_happy_kpm) { - // back to idle - anim_dm_restart_animation(ctx, animation_state_row_t::Idle, - new_animation_result, new_state, - current_state, current_frames, current_config); - } else { - // Test Animation - if (!conditions.any_key_pressed && conditions.trigger_test_animation && current_state.row_state == animation_state_row_t::Test) { - anim_dm_process_animation(new_animation_result, new_state, current_state, current_frames); - } else { - // continues animations - if (current_state.row_state == animation_state_row_t::Idle) { - if (conditions.process_idle_animation) { - // Idle Animation - anim_dm_process_animation(new_animation_result, new_state, current_state, current_frames); - } - } - // finish animations, back to idle - if (current_state.row_state == animation_state_row_t::Happy) { - if (conditions.release_frame_after_press) { - if (conditions.process_idle_animation) { - // finish last happy animation - anim_dm_start_or_process_animation(ctx, animation_state_row_t::Idle, // back to idle - new_animation_result, new_state, - current_state, current_frames, current_config); - } else { - if (conditions.release_frame_for_non_idle) { - anim_dm_restart_animation(ctx, animation_state_row_t::Idle, - new_animation_result, new_state, - current_state, current_frames, current_config); - } - } - } - } - // Finsh wakeup - if (current_state.row_state == animation_state_row_t::WakeUp) { - // back to idle - if (conditions.process_idle_animation) { - // end current sleep animation - const auto animation_result = anim_dm_start_or_process_animation(ctx, animation_state_row_t::Idle, - new_animation_result, new_state, - current_state, current_frames, current_config); - if (animation_result.row_state == animation_state_row_t::Idle) { - new_state.is_idle_sleep = false; - } - } else { - if (conditions.release_frame_for_non_idle) { - anim_dm_restart_animation(ctx, animation_state_row_t::Idle, - new_animation_result, new_state, - current_state, current_frames, current_config); - new_state.is_idle_sleep = false; - } - } - } - // Finish Working - if (current_state.row_state == animation_state_row_t::EndWorking && (conditions.release_frame_after_update || conditions.release_frame_for_non_idle) && !update_shm.cpu_active) { - if (conditions.process_idle_animation) { - anim_dm_start_or_process_animation(ctx, animation_state_row_t::Idle, // back to idle, when animation ended - new_animation_result, new_state, - current_state, current_frames, current_config); - } else { - if (conditions.release_frame_for_non_idle) { - // back to idle - anim_dm_restart_animation(ctx, animation_state_row_t::Idle, - new_animation_result, new_state, - current_state, current_frames, current_config); - } - } - } else if (conditions.is_working && current_state.row_state != animation_state_row_t::EndWorking && !update_shm.cpu_active) { - // Cancel Working - if (conditions.process_idle_animation) { - // back to idle - anim_dm_start_or_process_animation(ctx, animation_state_row_t::EndWorking, - new_animation_result, new_state, - current_state, current_frames, current_config); - } else { - if (conditions.release_frame_for_non_idle) { - anim_dm_show_single_frame(ctx, animation_state_row_t::EndWorking, - new_animation_result, new_state, - current_state, current_frames, current_config); - } - } - } - } - } - } - - - /* - // Start Test animation - if (!conditions.any_key_pressed && conditions.trigger_test_animation && current_state.row_state == animation_state_row_t::Idle) { - anim_dm_restart_animation(ctx, animation_state_row_t::Test, - new_animation_result, new_state, - current_state, current_frames, current_config); - } else if (!conditions.any_key_pressed && conditions.trigger_test_animation && current_state.row_state == animation_state_row_t::Test) { - // loop test animation - anim_dm_process_animation(new_animation_result, new_state, current_state, current_frames); - } - */ - - /// @TODO: move moving handle into own anim_handle_moving_animation - // Move Animation - anim_dm_handle_movement(ctx, input, upd, new_animation_result, new_state, trigger_result, current_state, current_frames, current_config); - - const bool is_sleeping_time = current_config.enable_scheduled_sleep && is_sleep_time(current_config); - - // Idle Sleep - if (conditions.check_for_idle_sleep) { - if (!is_sleeping_time) { - const platform::timestamp_ms_t now = platform::get_current_time_ms(); - const platform::time_ms_t idle_sleep_timeout_ms = current_config.idle_sleep_timeout_sec*1000; - assert(now >= last_key_pressed_timestamp); - const auto sleep_timeout = now - last_key_pressed_timestamp; - - const bool start_boring = SLEEP_BORING_PART > 0 && sleep_timeout >= idle_sleep_timeout_ms/SLEEP_BORING_PART; - if (current_state.row_state == animation_state_row_t::Idle) { - // start boring animation - if (start_boring && !current_state.show_boring_animation_once) { - if (conditions.process_idle_animation) { - anim_dm_restart_animation(ctx, animation_state_row_t::Boring, - new_animation_result, new_state, - current_state, current_frames, current_config); - } else { - anim_dm_show_single_frame(ctx, animation_state_row_t::Boring, - new_animation_result, new_state, - current_state, current_frames, current_config); - } - new_state.show_boring_animation_once = true; - new_state.anim_last_direction = 0.0f; - } - } else if (current_state.row_state == animation_state_row_t::Boring) { - if (conditions.process_idle_animation) { - const auto animation_result = anim_dm_start_or_process_animation(ctx, animation_state_row_t::Idle, // back to idle, when animation ended - new_animation_result, new_state, - current_state, current_frames, current_config); - if (!start_boring && animation_result.row_state == animation_state_row_t::Idle) { - new_state.show_boring_animation_once = false; - } - } else { - if (!start_boring || (start_boring && current_state.show_boring_animation_once)) { - if (conditions.release_frame_for_non_idle) { - // back to idle - anim_dm_restart_animation(ctx, animation_state_row_t::Idle, - new_animation_result, new_state, - current_state, current_frames, current_config); - if (!start_boring) { - new_state.show_boring_animation_once = false; - } - } - } - } - } - - // idle sleep - if (sleep_timeout >= idle_sleep_timeout_ms) { - if (current_state.row_state == animation_state_row_t::Idle || conditions.is_moving) { - if (conditions.process_idle_animation && conditions.is_moving) { - anim_dm_start_or_process_animation(ctx, animation_state_row_t::Sleep, // wait for moving to end - new_animation_result, new_state, - current_state, current_frames, current_config); - } else { - anim_dm_restart_animation(ctx, animation_state_row_t::Sleep, - new_animation_result, new_state, - current_state, current_frames, current_config); - } - new_state.anim_last_direction = 0.0f; - new_state.is_idle_sleep = true; - } else if (current_state.row_state == animation_state_row_t::Sleep) { - if (conditions.process_idle_animation) { - // loop sleep animation - anim_dm_process_animation(new_animation_result, new_state, current_state, current_frames); - } - } - } else { - if (current_state.row_state == animation_state_row_t::Sleep && current_state.is_idle_sleep) { - // wake up - if (conditions.process_idle_animation) { - // end current sleep animation - const auto animation_result = anim_dm_start_or_process_animation(ctx, animation_state_row_t::WakeUp, - new_animation_result, new_state, - current_state, current_frames, current_config); - if (animation_result.row_state == animation_state_row_t::WakeUp) { - new_state.is_idle_sleep = false; - new_state.show_boring_animation_once = false; - } - } else { - if (conditions.release_frame_for_non_idle) { - anim_dm_show_single_frame(ctx, animation_state_row_t::WakeUp, - new_animation_result, new_state, - current_state, current_frames, current_config); - new_state.is_idle_sleep = false; - new_state.show_boring_animation_once = false; - } - } - } - } - } - } - - // Sleep Mode - if (current_config.enable_scheduled_sleep) { - if (is_sleeping_time) { - if (new_state.row_state == animation_state_row_t::Idle) { - anim_dm_restart_animation(ctx, animation_state_row_t::Sleep, - new_animation_result, new_state, - current_state, current_frames, current_config); - new_state.anim_last_direction = 0.0f; - new_state.is_idle_sleep = false; - } else if (state.row_state == animation_state_row_t::Sleep && !new_state.is_idle_sleep) { - if (conditions.process_idle_animation) { - anim_dm_process_animation(new_animation_result, new_state, current_state, current_frames); - } - } - } else { - if (current_state.row_state == animation_state_row_t::Sleep && !new_state.is_idle_sleep) { - // wake up - if (conditions.process_idle_animation) { - // end current sleep animation - const auto animation_result = anim_dm_start_or_process_animation(ctx, animation_state_row_t::WakeUp, - new_animation_result, new_state, - current_state, current_frames, current_config); - if (animation_result.row_state == animation_state_row_t::WakeUp) { - new_state.is_idle_sleep = false; - new_state.show_boring_animation_once = false; - } - } else { - if (conditions.release_frame_for_non_idle) { - anim_dm_show_single_frame(ctx, animation_state_row_t::WakeUp, - new_animation_result, new_state, - current_state, current_frames, current_config); - new_state.is_idle_sleep = false; - new_state.show_boring_animation_once = false; - } - } - } - } - } else { - if (current_state.row_state == animation_state_row_t::Sleep && !new_state.is_idle_sleep) { - // wake up - if (conditions.process_idle_animation) { - // end current sleep animation - const auto animation_result = anim_dm_start_or_process_animation(ctx, animation_state_row_t::WakeUp, - new_animation_result, new_state, - current_state, current_frames, current_config); - if (animation_result.row_state == animation_state_row_t::WakeUp) { - new_state.is_idle_sleep = false; - new_state.show_boring_animation_once = false; - } - } else { - if (conditions.release_frame_for_non_idle) { - anim_dm_show_single_frame(ctx, animation_state_row_t::WakeUp, - new_animation_result, new_state, - current_state, current_frames, current_config); - new_state.is_idle_sleep = false; - new_state.show_boring_animation_once = false; - } - } - } - } + return anim_update_animation_state(anim_shm, state, new_animation_result, new_state, current_animation_result, + current_state, current_config); +} +#endif - return anim_update_animation_state(anim_shm, state, - new_animation_result, new_state, - current_animation_result, current_state, - current_config); +#ifdef FEATURE_ENABLE_DM_EMBEDDED_ASSETS +enum class anim_dm_process_animation_result_status_t : uint8_t { + None, + Started, + Updated, + End, + Looped, + NextAnimationStarted, + SkipMovement, + Moved, + Stop +}; +struct anim_dm_process_animation_result_t { + animation_state_row_t row_state; + anim_dm_process_animation_result_status_t status{anim_dm_process_animation_result_status_t::None}; +}; +static anim_dm_process_animation_result_t anim_dm_process_animation(animation_player_result_t& new_animation_result, + animation_state_t& new_state, + const animation_state_t& current_state, + const dm_sprite_sheet_t& current_frames) { + assert(MAX_ANIMATION_FRAMES > 0); + assert(MAX_ANIMATION_FRAMES <= INT_MAX); + + anim_dm_process_animation_result_t ret{.row_state = new_state.row_state, + .status = anim_dm_process_animation_result_status_t::Updated}; + // forward animation + new_state.animations_index = current_state.animations_index + 1; + if (new_state.animations_index > static_cast(MAX_ANIMATION_FRAMES - 1)) { + ret.status = anim_dm_process_animation_result_status_t::Looped; + new_state.animations_index = 0; + } + /* + // backwards animation + else if (new_state.animations_index < 0) { + ret = anim_bongocat_process_animation_result_t::End; + new_state.animations_index = MAX_ANIMATION_FRAMES-1; + } + */ + switch (new_state.row_state) { + case animation_state_row_t::Idle: + new_animation_result.sprite_sheet_col = current_frames.animations.idle[new_state.animations_index]; + break; + case animation_state_row_t::StartWriting: + case animation_state_row_t::Writing: + case animation_state_row_t::EndWriting: + new_animation_result.sprite_sheet_col = current_frames.animations.writing[new_state.animations_index]; + break; + case animation_state_row_t::Happy: + new_animation_result.sprite_sheet_col = current_frames.animations.happy[new_state.animations_index]; + break; + case animation_state_row_t::FallASleep: + // not supported + new_animation_result.sprite_sheet_col = current_frames.animations.sleep[new_state.animations_index]; + break; + case animation_state_row_t::Sleep: + new_animation_result.sprite_sheet_col = current_frames.animations.sleep[new_state.animations_index]; + break; + case animation_state_row_t::WakeUp: + new_animation_result.sprite_sheet_col = current_frames.animations.wake_up[new_state.animations_index]; + break; + case animation_state_row_t::Boring: + new_animation_result.sprite_sheet_col = current_frames.animations.boring[new_state.animations_index]; + break; + case animation_state_row_t::Test: + new_animation_result.sprite_sheet_col = current_frames.animations.writing[new_state.animations_index]; + break; + case animation_state_row_t::StartWorking: + case animation_state_row_t::Working: + case animation_state_row_t::EndWorking: + new_animation_result.sprite_sheet_col = current_frames.animations.working[new_state.animations_index]; + break; + case animation_state_row_t::StartMoving: + case animation_state_row_t::Moving: + case animation_state_row_t::EndMoving: + new_animation_result.sprite_sheet_col = current_frames.animations.moving[new_state.animations_index]; + break; + case animation_state_row_t::StartRunning: + case animation_state_row_t::Running: + case animation_state_row_t::EndRunning: + new_animation_result.sprite_sheet_col = current_frames.animations.running[new_state.animations_index]; + break; + } + return ret; +} +static anim_dm_process_animation_result_t +anim_dm_restart_animation([[maybe_unused]] animation_thread_context_t& ctx, animation_state_row_t new_row_state, + animation_player_result_t& new_animation_result, animation_state_t& new_state, + [[maybe_unused]] const animation_state_t& current_state, + const dm_sprite_sheet_t& current_frames, const config::config_t& current_config) { + using namespace assets; + assert(MAX_ANIMATION_FRAMES > 0); + assert(MAX_ANIMATION_FRAMES <= INT_MAX); + + new_state.row_state = new_row_state; + new_animation_result.sprite_sheet_row = DM_SPRITE_SHEET_ROW; + new_state.animations_index = 0; + + anim_dm_process_animation_result_t ret{.row_state = new_state.row_state, + .status = anim_dm_process_animation_result_status_t::Started}; + switch (new_state.row_state) { + case animation_state_row_t::Idle: + new_animation_result.sprite_sheet_col = current_frames.animations.idle[new_state.animations_index]; + if (current_config.idle_frame) { + new_animation_result.sprite_sheet_col = current_config.idle_frame; + } + break; + case animation_state_row_t::StartWriting: + case animation_state_row_t::Writing: + case animation_state_row_t::EndWriting: + new_animation_result.sprite_sheet_col = current_frames.animations.writing[new_state.animations_index]; + break; + case animation_state_row_t::Happy: + new_animation_result.sprite_sheet_col = current_frames.animations.happy[new_state.animations_index]; + break; + case animation_state_row_t::FallASleep: + // not supported + new_animation_result.sprite_sheet_col = current_frames.animations.sleep[new_state.animations_index]; + break; + case animation_state_row_t::Sleep: + new_animation_result.sprite_sheet_col = current_frames.animations.sleep[new_state.animations_index]; + break; + case animation_state_row_t::WakeUp: + new_animation_result.sprite_sheet_col = current_frames.animations.wake_up[new_state.animations_index]; + break; + case animation_state_row_t::Boring: + new_animation_result.sprite_sheet_col = current_frames.animations.boring[new_state.animations_index]; + break; + case animation_state_row_t::Test: + new_animation_result.sprite_sheet_col = current_frames.animations.writing[new_state.animations_index]; + break; + case animation_state_row_t::StartWorking: + case animation_state_row_t::Working: + case animation_state_row_t::EndWorking: + new_animation_result.sprite_sheet_col = current_frames.animations.working[new_state.animations_index]; + break; + case animation_state_row_t::StartMoving: + case animation_state_row_t::Moving: + case animation_state_row_t::EndMoving: + new_animation_result.sprite_sheet_col = current_frames.animations.moving[new_state.animations_index]; + break; + case animation_state_row_t::StartRunning: + case animation_state_row_t::Running: + case animation_state_row_t::EndRunning: + new_animation_result.sprite_sheet_col = current_frames.animations.running[new_state.animations_index]; + break; + } + return ret; +} +static anim_dm_process_animation_result_t +anim_dm_show_single_frame([[maybe_unused]] animation_thread_context_t& ctx, animation_state_row_t new_row_state, + animation_player_result_t& new_animation_result, animation_state_t& new_state, + [[maybe_unused]] const animation_state_t& current_state, + [[maybe_unused]] const dm_sprite_sheet_t& current_frames, + const config::config_t& current_config) { + using namespace assets; + assert(MAX_ANIMATION_FRAMES > 0); + assert(MAX_ANIMATION_FRAMES <= INT_MAX); + + new_state.row_state = new_row_state; + new_animation_result.sprite_sheet_row = DM_SPRITE_SHEET_ROW; + new_state.animations_index = 0; + + anim_dm_process_animation_result_t ret{.row_state = new_state.row_state, + .status = anim_dm_process_animation_result_status_t::Started}; + switch (new_state.row_state) { + case animation_state_row_t::Idle: + new_animation_result.sprite_sheet_col = DM_FRAME_IDLE1; + if (current_config.idle_frame) { + new_animation_result.sprite_sheet_col = current_config.idle_frame; + } + break; + case animation_state_row_t::StartWriting: + case animation_state_row_t::Writing: + case animation_state_row_t::EndWriting: + // toggle frame + if (new_animation_result.sprite_sheet_col == DM_FRAME_IDLE1) { + new_animation_result.sprite_sheet_col = DM_FRAME_IDLE2; + } else if (new_animation_result.sprite_sheet_col == DM_FRAME_IDLE2) { + new_animation_result.sprite_sheet_col = DM_FRAME_IDLE1; + } else { + new_animation_result.sprite_sheet_col = ctx._rng.range(0, 100) <= 50 ? DM_FRAME_IDLE1 : DM_FRAME_IDLE2; + } + break; + case animation_state_row_t::Happy: + if (current_frames.frames.happy.valid) { + new_animation_result.sprite_sheet_col = current_frames.frames.happy.col; + } else { + // toggle frame + if (new_animation_result.sprite_sheet_col == DM_FRAME_IDLE1) { + new_animation_result.sprite_sheet_col = DM_FRAME_IDLE2; + } else if (new_animation_result.sprite_sheet_col == DM_FRAME_IDLE2) { + new_animation_result.sprite_sheet_col = DM_FRAME_IDLE1; + } else { + new_animation_result.sprite_sheet_col = ctx._rng.range(0, 100) <= 50 ? DM_FRAME_IDLE1 : DM_FRAME_IDLE2; + } + } + break; + case animation_state_row_t::FallASleep: + // not fully supported + if (current_frames.frames.sleep.valid) { + new_animation_result.sprite_sheet_col = current_frames.frames.sleep.col; + } else if (current_frames.frames.down.valid) { + new_animation_result.sprite_sheet_col = current_frames.frames.down.col; + } else { + new_animation_result.sprite_sheet_col = DM_FRAME_IDLE1; + } + break; + case animation_state_row_t::Sleep: + if (current_frames.frames.sleep.valid) { + new_animation_result.sprite_sheet_col = current_frames.frames.sleep.col; + } else if (current_frames.frames.down.valid) { + new_animation_result.sprite_sheet_col = current_frames.frames.down.col; + } else { + new_animation_result.sprite_sheet_col = DM_FRAME_IDLE1; + } + break; + case animation_state_row_t::WakeUp: + if (current_frames.frames.happy.valid) { + new_animation_result.sprite_sheet_col = current_frames.frames.sleep.col; + } else { + new_animation_result.sprite_sheet_col = DM_FRAME_IDLE2; } + break; + case animation_state_row_t::Boring: + if (current_frames.frames.sad.valid) { + new_animation_result.sprite_sheet_col = current_frames.frames.sad.col; + } else if (current_frames.frames.angry.valid) { + new_animation_result.sprite_sheet_col = current_frames.frames.angry.col; + } else if (current_frames.frames.down.valid) { + new_animation_result.sprite_sheet_col = current_frames.frames.down.col; + } else { + // toggle frame + if (new_animation_result.sprite_sheet_col == DM_FRAME_IDLE1) { + new_animation_result.sprite_sheet_col = DM_FRAME_IDLE2; + } else if (new_animation_result.sprite_sheet_col == DM_FRAME_IDLE2) { + new_animation_result.sprite_sheet_col = DM_FRAME_IDLE1; + } else { + new_animation_result.sprite_sheet_col = ctx._rng.range(0, 100) <= 50 ? DM_FRAME_IDLE1 : DM_FRAME_IDLE2; + } + } + break; + case animation_state_row_t::Test: + // toggle frame + if (new_animation_result.sprite_sheet_col == DM_FRAME_IDLE1) { + new_animation_result.sprite_sheet_col = DM_FRAME_IDLE2; + } else if (new_animation_result.sprite_sheet_col == DM_FRAME_IDLE2) { + new_animation_result.sprite_sheet_col = DM_FRAME_IDLE1; + } else { + new_animation_result.sprite_sheet_col = ctx._rng.range(0, 100) <= 50 ? DM_FRAME_IDLE1 : DM_FRAME_IDLE2; + } + break; + case animation_state_row_t::StartWorking: + case animation_state_row_t::Working: + case animation_state_row_t::EndWorking: + if (current_frames.frames.attack_1.valid) { + new_animation_result.sprite_sheet_col = current_frames.frames.attack_1.col; + } else if (current_frames.frames.angry.valid) { + new_animation_result.sprite_sheet_col = current_frames.frames.angry.col; + } else { + // toggle frame + if (new_animation_result.sprite_sheet_col == DM_FRAME_IDLE1) { + new_animation_result.sprite_sheet_col = DM_FRAME_IDLE2; + } else if (new_animation_result.sprite_sheet_col == DM_FRAME_IDLE2) { + new_animation_result.sprite_sheet_col = DM_FRAME_IDLE1; + } else { + new_animation_result.sprite_sheet_col = ctx._rng.range(0, 100) <= 50 ? DM_FRAME_IDLE1 : DM_FRAME_IDLE2; + } + } + break; + case animation_state_row_t::StartMoving: + case animation_state_row_t::Moving: + if (current_frames.frames.movement_1.valid) { + new_animation_result.sprite_sheet_col = current_frames.frames.movement_1.col; + } else { + // toggle frame + if (new_animation_result.sprite_sheet_col == DM_FRAME_IDLE1) { + new_animation_result.sprite_sheet_col = DM_FRAME_IDLE2; + } else if (new_animation_result.sprite_sheet_col == DM_FRAME_IDLE2) { + new_animation_result.sprite_sheet_col = DM_FRAME_IDLE1; + } else { + new_animation_result.sprite_sheet_col = ctx._rng.range(0, 100) <= 50 ? DM_FRAME_IDLE1 : DM_FRAME_IDLE2; + } + } + break; + case animation_state_row_t::EndMoving: + if (current_frames.frames.movement_2.valid) { + new_animation_result.sprite_sheet_col = current_frames.frames.movement_2.col; + } else { + // toggle frame + if (new_animation_result.sprite_sheet_col == DM_FRAME_IDLE1) { + new_animation_result.sprite_sheet_col = DM_FRAME_IDLE2; + } else if (new_animation_result.sprite_sheet_col == DM_FRAME_IDLE2) { + new_animation_result.sprite_sheet_col = DM_FRAME_IDLE1; + } else { + new_animation_result.sprite_sheet_col = ctx._rng.range(0, 100) <= 50 ? DM_FRAME_IDLE1 : DM_FRAME_IDLE2; + } + } + break; + case animation_state_row_t::StartRunning: + case animation_state_row_t::Running: + if (current_frames.frames.movement_1.valid) { + new_animation_result.sprite_sheet_col = current_frames.frames.movement_1.col; + } else { + // toggle frame + if (new_animation_result.sprite_sheet_col == DM_FRAME_IDLE1) { + new_animation_result.sprite_sheet_col = DM_FRAME_IDLE2; + } else if (new_animation_result.sprite_sheet_col == DM_FRAME_IDLE2) { + new_animation_result.sprite_sheet_col = DM_FRAME_IDLE1; + } else { + new_animation_result.sprite_sheet_col = ctx._rng.range(0, 100) <= 50 ? DM_FRAME_IDLE1 : DM_FRAME_IDLE2; + } + } + break; + case animation_state_row_t::EndRunning: + if (current_frames.frames.movement_2.valid) { + new_animation_result.sprite_sheet_col = current_frames.frames.movement_2.col; + } else { + // toggle frame + if (new_animation_result.sprite_sheet_col == DM_FRAME_IDLE1) { + new_animation_result.sprite_sheet_col = DM_FRAME_IDLE2; + } else if (new_animation_result.sprite_sheet_col == DM_FRAME_IDLE2) { + new_animation_result.sprite_sheet_col = DM_FRAME_IDLE1; + } else { + new_animation_result.sprite_sheet_col = ctx._rng.range(0, 100) <= 50 ? DM_FRAME_IDLE1 : DM_FRAME_IDLE2; + } + } + break; + } + return ret; +} - static anim_next_frame_result_t anim_dm_key_pressed_next_frame(animation_context_t& ctx, - const platform::input::input_context_t& input, - [[maybe_unused]] const platform::update::update_context_t& upd, - animation_state_t& state, - const animation_trigger_t& trigger) { - using namespace assets; - - // read-only config - assert(ctx._local_copy_config != nullptr); - const config::config_t& current_config = *ctx._local_copy_config; - - assert(ctx.shm != nullptr); - assert(input.shm != nullptr); - animation_shared_memory_t& anim_shm = *ctx.shm; - const auto& input_shm = *input.shm; - const auto current_state = state; - const auto& current_animation_result = anim_shm.animation_player_result; - [[maybe_unused]] const int anim_index = anim_shm.anim_index; - assert(get_current_animation(ctx).type == animation_t::Type::Dm); - const auto& current_frames = get_current_animation(ctx).dm; - - auto new_animation_result = anim_shm.animation_player_result; - auto new_state = state; - - const auto conditions = get_anim_conditions(ctx, input, upd, current_state, trigger, current_config); - - bool show_happy = false; - if (input_shm.kpm > 0) { - if (current_config.happy_kpm > 0 && input_shm.kpm >= current_config.happy_kpm) { - show_happy = DM_HAPPY_CHANCE_PERCENT >= 100 || ctx._rng.range(0, 99) < DM_HAPPY_CHANCE_PERCENT; - } - } +static anim_dm_process_animation_result_t +anim_dm_start_or_process_animation(animation_thread_context_t& ctx, animation_state_row_t new_row_state, + animation_player_result_t& new_animation_result, animation_state_t& new_state, + const animation_state_t& current_state, const dm_sprite_sheet_t& current_frames, + const config::config_t& current_config) { + if (current_state.row_state != new_row_state) { + auto result = anim_dm_process_animation(new_animation_result, new_state, current_state, current_frames); + if (result.status == anim_dm_process_animation_result_status_t::End || + result.status == anim_dm_process_animation_result_status_t::Looped) { + result = anim_dm_restart_animation(ctx, new_row_state, new_animation_result, new_state, current_state, + current_frames, current_config); + result.status = anim_dm_process_animation_result_status_t::NextAnimationStarted; + } + return result; + } - /// @TODO: use state machine for animation (states) + return anim_dm_restart_animation(ctx, new_row_state, new_animation_result, new_state, current_state, current_frames, + current_config); +} - if (show_happy && (conditions.is_writing || current_state.row_state == animation_state_row_t::Idle)) { - // show happy animation (KPM) - anim_dm_restart_animation(ctx, animation_state_row_t::Happy, - new_animation_result, new_state, - current_state, current_frames, current_config); - } else if (conditions.is_writing || conditions.is_moving || current_state.row_state == animation_state_row_t::Idle) { - const bool end_writing = !conditions.release_frame_after_press && conditions.is_writing; - // in Writing mode/start writing - if (!conditions.is_writing || conditions.is_moving) { - anim_dm_restart_animation(ctx, animation_state_row_t::StartWriting, - new_animation_result, new_state, - current_state, current_frames, current_config); - new_state.show_boring_animation_once = false; +static anim_dm_process_animation_result_t +anim_dm_handle_movement(animation_thread_context_t& ctx, const platform::input::input_context_t& input, + [[maybe_unused]] const platform::update::update_context_t& upd, + animation_player_result_t& new_animation_result, animation_state_t& new_state, + const anim_handle_key_press_result_t& trigger_result, const animation_state_t& current_state, + const dm_sprite_sheet_t& current_frames, const config::config_t& current_config) { + using namespace assets; + + assert(ctx.shm); + animation_shared_memory_t& anim_shm = *ctx.shm; + + const auto conditions = get_anim_conditions(ctx, input, upd, current_state, trigger_result.trigger, current_config); + + anim_dm_process_animation_result_t ret{.row_state = new_state.row_state, + .status = anim_dm_process_animation_result_status_t::None}; + if (!conditions.is_writing && current_config.movement_speed > 0 && current_config.movement_radius > 0 && + current_config.fps > 0) { + if (conditions.process_idle_animation || conditions.process_movement || conditions.process_movement_animation) { + const float delta = 1.0f / static_cast(current_config.fps); + const auto delta_ms = static_cast(delta * 1000); + const auto frame_height = current_config.cat_height; + const auto frame_width = + static_cast(static_cast(frame_height) * (static_cast(current_frames.frame_width) / + static_cast(current_frames.frame_height))); + const auto fmovement_radius = static_cast(current_config.movement_radius); + + assert(current_config.movement_radius > 0); + float max_movement_offset_x_left = 0.0f; + float max_movement_offset_x_right = 0.0f; + float wall_distance = 0.0f; + switch (current_config.cat_align) { + case config::align_type_t::ALIGN_CENTER: + // range: [-r, +r] + max_movement_offset_x_left = + -static_cast(current_config.movement_radius) + static_cast(frame_width) / 2.0f; + max_movement_offset_x_right = static_cast(current_config.movement_radius - frame_width); + wall_distance = anim_shm.movement_offset_x / fmovement_radius; + break; + case config::align_type_t::ALIGN_LEFT: + // range: [0, +2r] + max_movement_offset_x_left = 0.0f; + max_movement_offset_x_right = + static_cast(current_config.movement_radius) * 2.0f - static_cast(frame_width); + wall_distance = (anim_shm.movement_offset_x / (fmovement_radius * 2.0f)) * 2.0f - 1.0f; + break; + case config::align_type_t::ALIGN_RIGHT: + // range: [-2r, 0] + max_movement_offset_x_left = + -(static_cast(current_config.movement_radius) * 2.0f) + static_cast(frame_width); + max_movement_offset_x_right = 0.0f; + wall_distance = (anim_shm.movement_offset_x / (fmovement_radius * 2.0f)) * 2.0f + 1.0f; // normalize to -1 → 1 + break; + } + + if (current_state.row_state == animation_state_row_t::Idle && + (current_state.anim_pause_after_movement_ms > 0 || + (ctx._rng.range(0, 100) <= CHANCE_FOR_SKIPPING_MOVEMENT_PERCENT))) { + // skip movement + new_state.anim_velocity = 0.0f; + new_state.anim_distance = 0.0f; + if (new_state.anim_pause_after_movement_ms > 0) { + new_state.anim_pause_after_movement_ms -= delta_ms; + if (new_state.anim_pause_after_movement_ms <= 0) { + new_state.anim_pause_after_movement_ms = 0; + } + } + ret.status = anim_dm_process_animation_result_status_t::SkipMovement; + } else { + // moving animation + constexpr float DIR_EPSILON = 1e-3f; + assert(MAX_DISTANCE_PER_MOVEMENT_PART > 0); + const int movement_part = current_config.movement_radius <= MAX_MOVEMENT_RADIUS_SMALL + ? SMALL_MAX_DISTANCE_PER_MOVEMENT_PART + : MAX_DISTANCE_PER_MOVEMENT_PART; + const float fmovement_part = static_cast(movement_part); + if (current_state.row_state == animation_state_row_t::Idle) { + // start movement + const auto min_movement = current_config.movement_radius <= MAX_MOVEMENT_RADIUS_SMALL + ? (fmovement_radius / fmovement_part / 2) + 1 + : (fmovement_radius / fmovement_part / fmovement_part) + 1; + float max_move_distance = fmovement_radius / fmovement_part; + max_move_distance = max_move_distance <= min_movement ? min_movement : max_move_distance; + + assert(max_move_distance >= 0); + assert(min_movement >= 0); + new_state.anim_distance = static_cast( + ctx._rng.range(static_cast(min_movement), static_cast(max_move_distance))); + + if (anim_shm.movement_offset_x >= max_movement_offset_x_right) { + // run against wall, change direction + anim_shm.anim_direction = -1.0; + anim_shm.movement_offset_x = max_movement_offset_x_right; + } else if (anim_shm.movement_offset_x <= max_movement_offset_x_left) { + // run against wall, change direction + anim_shm.anim_direction = 1.0; + anim_shm.movement_offset_x = max_movement_offset_x_left; + } else { + float toward_wall_bias = fabs(wall_distance); + toward_wall_bias = toward_wall_bias >= 1.0f ? 1.0f : toward_wall_bias; + toward_wall_bias = toward_wall_bias <= 0.0f ? 0.0f : toward_wall_bias; + + const int flip_direction_chance = current_config.movement_radius <= MAX_MOVEMENT_RADIUS_SMALL + ? SMALL_FLIP_DIRECTION_NEAR_WALL_PERCENT + : FLIP_DIRECTION_NEAR_WALL_PERCENT; + + assert(toward_wall_bias >= 0.0f); + // change direction: chance drops at center, changes falloff steeper near walls + const auto dir_chance = static_cast(static_cast(100 - flip_direction_chance + 10) * + (1.0f - pow(toward_wall_bias, 1.5f))); + + if (fabs(new_state.anim_last_direction) >= DIR_EPSILON) { + anim_shm.anim_direction = (ctx._rng.range(0, 100) < dir_chance) ? -new_state.anim_last_direction + : new_state.anim_last_direction; + } else if (fabs(anim_shm.anim_direction) < DIR_EPSILON) { + // idle: choose random start direction + anim_shm.anim_direction = (ctx._rng.range(0, 100) < dir_chance) ? 1.0f : -1.0f; } else { - if (conditions.process_idle_animation) { - // transition writing animations - if (current_state.row_state == animation_state_row_t::StartWriting) { - anim_dm_start_or_process_animation(ctx, animation_state_row_t::Writing, - new_animation_result, new_state, - current_state, current_frames, current_config); - } else if (conditions.release_frame_after_press && current_state.row_state == animation_state_row_t::Writing) { - anim_dm_start_or_process_animation(ctx, animation_state_row_t::EndWriting, - new_animation_result, new_state, - current_state, current_frames, current_config); - } else if (conditions.release_frame_after_press && current_state.row_state == animation_state_row_t::EndWriting) { - anim_dm_start_or_process_animation(ctx, animation_state_row_t::Idle, // back to idle - new_animation_result, new_state, - current_state, current_frames, current_config); - } else if (end_writing) { - // keep writing - anim_dm_process_animation(new_animation_result, new_state, current_state, current_frames); - } - } else { - if (current_state.row_state == animation_state_row_t::StartWriting) { - anim_dm_start_or_process_animation(ctx, animation_state_row_t::Writing, - new_animation_result, new_state, - current_state, current_frames, current_config); - } else if (end_writing) { - // back to idle - anim_dm_restart_animation(ctx, animation_state_row_t::Idle, - new_animation_result, new_state, - current_state, current_frames, current_config); - } else if (conditions.continue_writing) { - // keep writing - anim_dm_process_animation(new_animation_result, new_state, current_state, current_frames); - } - } + if (wall_distance > (100.0f / static_cast(flip_direction_chance))) { + anim_shm.anim_direction = (ctx._rng.range(0, 100) < dir_chance) ? -1.0f : 1.0f; + } else if (wall_distance < -(100.0f / static_cast(flip_direction_chance))) { + anim_shm.anim_direction = (ctx._rng.range(0, 100) < dir_chance) ? 1.0f : -1.0f; + } else { + anim_shm.anim_direction = + (ctx._rng.range(0, 100) < dir_chance) ? anim_shm.anim_direction : -anim_shm.anim_direction; + } } - } else if (current_state.row_state == animation_state_row_t::Happy) { - if (conditions.release_frame_after_press) { - if (conditions.process_idle_animation) { - // finish last happy animation - anim_dm_start_or_process_animation(ctx, conditions.continue_writing ? animation_state_row_t::Writing : animation_state_row_t::Idle, // back to idle - new_animation_result, new_state, - current_state, current_frames, current_config); - } else { - // back to idle - anim_dm_restart_animation(ctx, conditions.continue_writing ? animation_state_row_t::Writing : animation_state_row_t::Idle, - new_animation_result, new_state, - current_state, current_frames, current_config); - } - } else { - if (!conditions.process_idle_animation) { - // back to idle - anim_dm_restart_animation(ctx, conditions.continue_writing ? animation_state_row_t::Writing : animation_state_row_t::Idle, - new_animation_result, new_state, - current_state, current_frames, current_config); - } else { - // finish last happy animation - anim_dm_start_or_process_animation(ctx, conditions.continue_writing ? animation_state_row_t::Writing : animation_state_row_t::Idle, // back to idle - new_animation_result, new_state, - current_state, current_frames, current_config); - } + } + // start moving animation + new_state.anim_velocity = anim_shm.anim_direction * static_cast(current_config.movement_speed); + ret = anim_dm_restart_animation(ctx, animation_state_row_t::StartMoving, new_animation_result, new_state, + current_state, current_frames, current_config); + } else if (current_state.row_state == animation_state_row_t::StartMoving) { + if (conditions.process_idle_animation) { + ret = anim_dm_start_or_process_animation(ctx, animation_state_row_t::Moving, // continue with moving + new_animation_result, new_state, current_state, current_frames, + current_config); + ret.status = anim_dm_process_animation_result_status_t::Updated; + } else if (conditions.process_movement_animation) { + ret = anim_dm_restart_animation(ctx, animation_state_row_t::Moving, new_animation_result, new_state, + current_state, current_frames, current_config); + ret.status = anim_dm_process_animation_result_status_t::Updated; + } + } else if (current_state.row_state == animation_state_row_t::Moving) { + if (conditions.process_movement) { + ret = anim_dm_process_animation(new_animation_result, new_state, current_state, current_frames); + ret.status = anim_dm_process_animation_result_status_t::Moved; + + new_state.anim_distance -= fabs(new_state.anim_velocity); + anim_shm.movement_offset_x += new_state.anim_velocity; + // clamp walking/movement area + if (anim_shm.movement_offset_x > max_movement_offset_x_right) { + anim_shm.movement_offset_x = max_movement_offset_x_right; + ret = anim_dm_restart_animation(ctx, animation_state_row_t::EndMoving, new_animation_result, new_state, + current_state, current_frames, current_config); + ret.status = anim_dm_process_animation_result_status_t::Stop; + } else if (anim_shm.movement_offset_x < max_movement_offset_x_left) { + anim_shm.movement_offset_x = max_movement_offset_x_left; + ret = anim_dm_restart_animation(ctx, animation_state_row_t::EndMoving, new_animation_result, new_state, + current_state, current_frames, current_config); + ret.status = anim_dm_process_animation_result_status_t::Stop; } - } else if (state.row_state == animation_state_row_t::Sleep && current_state.is_idle_sleep) { - // wake up - anim_dm_restart_animation(ctx, animation_state_row_t::WakeUp, - new_animation_result, new_state, + + if (new_state.anim_distance <= 0 || + (fabs(new_state.anim_distance) < DIR_EPSILON || fabs(new_state.anim_velocity) < DIR_EPSILON)) { + ret = anim_dm_restart_animation(ctx, animation_state_row_t::EndMoving, new_animation_result, new_state, + current_state, current_frames, current_config); + ret.status = anim_dm_process_animation_result_status_t::Stop; + } + } + } else if (current_state.row_state == animation_state_row_t::EndMoving) { + if (conditions.process_idle_animation) { + ret = anim_dm_start_or_process_animation(ctx, animation_state_row_t::Idle, new_animation_result, new_state, + current_state, current_frames, current_config); + } else { + ret = anim_dm_restart_animation(ctx, animation_state_row_t::Idle, new_animation_result, new_state, current_state, current_frames, current_config); + } + new_state.anim_last_direction = anim_shm.anim_direction; + new_state.anim_velocity = 0.0f; + anim_shm.anim_direction = 0.0; + if (ret.row_state == animation_state_row_t::Idle && new_state.anim_distance <= 0) { + assert(current_config.animation_speed_ms >= 0); + assert(movement_part >= 0); + const auto min_wait = current_config.animation_speed_ms * (current_config.movement_wait_factor / 2); + const auto max_wait = current_config.animation_speed_ms * current_config.movement_wait_factor; + assert(min_wait >= 0); + assert(max_wait >= 0); + new_state.anim_pause_after_movement_ms = + static_cast(ctx._rng.range(static_cast(min_wait), static_cast(max_wait))); + ret.status = anim_dm_process_animation_result_status_t::End; + } } - - return anim_update_animation_state(anim_shm, state, - new_animation_result, new_state, - current_animation_result, current_state, - current_config); + } } + } else { + // movement got disabled, back to idle + if ((current_config.movement_speed <= 0 || current_config.movement_radius <= 0) && conditions.is_moving) { + // back to idle + ret = anim_dm_restart_animation(ctx, animation_state_row_t::Idle, new_animation_result, new_state, current_state, + current_frames, current_config); + new_state.anim_velocity = 0.0f; + ret.status = anim_dm_process_animation_result_status_t::Stop; + } + } - static anim_next_frame_result_t anim_dm_working_next_frame(animation_context_t& ctx, - const platform::input::input_context_t& input, - const platform::update::update_context_t& upd, - animation_state_t& state, - const animation_trigger_t& trigger) { - using namespace assets; - - // read-only config - assert(ctx._local_copy_config != nullptr); - const config::config_t& current_config = *ctx._local_copy_config; - - assert(ctx.shm != nullptr); - //assert(input.shm != nullptr); - assert(upd.shm != nullptr); - animation_shared_memory_t& anim_shm = *ctx.shm; - //const auto& input_shm = *input.shm; - const auto& update_shm = *upd.shm; - const auto current_state = state; - const auto& current_animation_result = anim_shm.animation_player_result; - [[maybe_unused]] const int anim_index = anim_shm.anim_index; - //const platform::timestamp_ms_t last_key_pressed_timestamp = input_shm.last_key_pressed_timestamp; - assert(get_current_animation(ctx).type == animation_t::Type::Dm); - const auto& current_frames = get_current_animation(ctx).dm; - - auto new_animation_result = anim_shm.animation_player_result; - auto new_state = state; - - const auto conditions = get_anim_conditions(ctx, input, upd, current_state, trigger, current_config); - - /// @TODO: use state machine for animation (states) - - if (conditions.process_working_animation) { - // toggle frame, show attack animation - const bool above_threshold = current_config.cpu_threshold >= platform::ENABLED_MIN_CPU_PERCENT && (update_shm.avg_cpu_usage >= current_config.cpu_threshold || update_shm.max_cpu_usage >= current_config.cpu_threshold); - const bool lower_threshold = current_config.cpu_threshold >= platform::ENABLED_MIN_CPU_PERCENT && (update_shm.avg_cpu_usage < current_config.cpu_threshold && update_shm.max_cpu_usage < current_config.cpu_threshold); - - if (above_threshold) { - if (current_state.row_state == animation_state_row_t::Idle || conditions.is_moving) { - anim_dm_restart_animation(ctx, animation_state_row_t::StartWorking, - new_animation_result, new_state, - current_state, current_frames, current_config); - BONGOCAT_LOG_VERBOSE("Start Working: %d %d; %d%%", above_threshold, lower_threshold, update_shm.avg_cpu_usage); - } else if (conditions.is_working) { - // continues animations - if (conditions.process_idle_animation) { - if (update_shm.cpu_active && current_state.row_state == animation_state_row_t::StartWorking) { - anim_dm_start_or_process_animation(ctx, animation_state_row_t::Working, // continue working - new_animation_result, new_state, - current_state, current_frames, current_config); - } else if (!update_shm.cpu_active && (current_state.row_state == animation_state_row_t::StartWorking || state.row_state == animation_state_row_t::Working)) { - // end working, cool down - anim_dm_start_or_process_animation(ctx, animation_state_row_t::EndWorking, - new_animation_result, new_state, - current_state, current_frames, current_config); - } else if (!update_shm.cpu_active && current_state.row_state == animation_state_row_t::EndWorking) { - // back to idle - anim_dm_start_or_process_animation(ctx, animation_state_row_t::Idle, - new_animation_result, new_state, - current_state, current_frames, current_config); - } else if (update_shm.cpu_active && current_state.row_state == animation_state_row_t::Working) { - // keep working - anim_dm_process_animation(new_animation_result, new_state, current_state, current_frames); - } - } else { - if (update_shm.cpu_active) { - if (update_shm.cpu_active && current_state.row_state == animation_state_row_t::StartWorking) { - anim_dm_show_single_frame(ctx, animation_state_row_t::Working, - new_animation_result, new_state, - current_state, current_frames, current_config); - } else if (!update_shm.cpu_active && (current_state.row_state == animation_state_row_t::StartWorking || state.row_state == animation_state_row_t::Working)) { - // end working - anim_dm_show_single_frame(ctx, animation_state_row_t::EndWorking, - new_animation_result, new_state, - current_state, current_frames, current_config); - } else if (!update_shm.cpu_active && current_state.row_state == animation_state_row_t::EndWorking) { - // back to idle - anim_dm_restart_animation(ctx, animation_state_row_t::Idle, - new_animation_result, new_state, - current_state, current_frames, current_config); - } else if (update_shm.cpu_active && current_state.row_state == animation_state_row_t::Working) { - // keep working - anim_dm_process_animation(new_animation_result, new_state, current_state, current_frames); - } - } - } - } - } else if (lower_threshold) { - // End Working - if (conditions.is_working && current_state.row_state != animation_state_row_t::EndMoving) { - if (conditions.process_idle_animation) { - // end working, cool down - anim_dm_start_or_process_animation(ctx, animation_state_row_t::EndWorking, - new_animation_result, new_state, - current_state, current_frames, current_config); - } else { - anim_dm_show_single_frame(ctx, animation_state_row_t::EndWorking, - new_animation_result, new_state, - current_state, current_frames, current_config); - } - BONGOCAT_LOG_VERBOSE("Stop Working: %d %d; %d%%", above_threshold, lower_threshold, update_shm.avg_cpu_usage); - } + return ret; +} + +static anim_next_frame_result_t anim_dm_idle_next_frame(animation_thread_context_t& ctx, + const platform::input::input_context_t& input, + [[maybe_unused]] const platform::update::update_context_t& upd, + animation_state_t& state, + const anim_handle_key_press_result_t& trigger_result) { + using namespace assets; + + // read-only config + assert(ctx._local_copy_config); + const config::config_t& current_config = *ctx._local_copy_config; + + assert(ctx.shm); + assert(input.shm); + assert(upd.shm); + animation_shared_memory_t& anim_shm = *ctx.shm; + const auto& input_shm = *input.shm; + const auto& update_shm = *upd.shm; + const auto current_state = state; + const auto& current_animation_result = anim_shm.animation_player_result; + [[maybe_unused]] const int anim_index = anim_shm.anim_index; + const platform::timestamp_ms_t last_key_pressed_timestamp = input_shm.last_key_pressed_timestamp; + assert(get_current_animation(ctx).type == animation_t::type_t::Dm); + const auto& current_frames = get_current_animation(ctx).dm; + + auto new_animation_result = anim_shm.animation_player_result; + auto new_state = state; + + const auto conditions = get_anim_conditions(ctx, input, upd, current_state, trigger_result.trigger, current_config); + + /// @TODO: make animation state machine ? + + if (!conditions.is_moving) { + // Idle Animation + const bool stop_writing = conditions.is_writing && conditions.release_frame_after_press; + const bool stop_happy_kpm = current_state.row_state == animation_state_row_t::Happy && + conditions.release_frame_after_press && !conditions.process_idle_animation; + const bool stop_test_animation = conditions.trigger_test_animation && + current_state.row_state == animation_state_row_t::Test && + conditions.release_test_frame; + if (stop_writing || stop_test_animation || stop_happy_kpm) { + // back to idle + anim_dm_restart_animation(ctx, animation_state_row_t::Idle, new_animation_result, new_state, current_state, + current_frames, current_config); + } else { + // Test Animation + if (!conditions.any_key_pressed && conditions.trigger_test_animation && + current_state.row_state == animation_state_row_t::Test) { + anim_dm_process_animation(new_animation_result, new_state, current_state, current_frames); + } else { + // continues animations + if (current_state.row_state == animation_state_row_t::Idle) { + if (conditions.process_idle_animation) { + // Idle Animation + anim_dm_process_animation(new_animation_result, new_state, current_state, current_frames); + } + } + // finish animations, back to idle + if (current_state.row_state == animation_state_row_t::Happy) { + if (conditions.release_frame_after_press) { + if (conditions.process_idle_animation) { + // finish last happy animation + anim_dm_start_or_process_animation(ctx, animation_state_row_t::Idle, // back to idle + new_animation_result, new_state, current_state, current_frames, + current_config); } else { - // Cancel Working - if (conditions.is_working && current_state.row_state != animation_state_row_t::EndWorking && !update_shm.cpu_active) { - if (conditions.process_idle_animation) { - // back to idle - anim_dm_start_or_process_animation(ctx, animation_state_row_t::EndWorking, - new_animation_result, new_state, - current_state, current_frames, current_config); - } else { - anim_dm_show_single_frame(ctx, animation_state_row_t::EndMoving, - new_animation_result, new_state, - current_state, current_frames, current_config); - } - BONGOCAT_LOG_VERBOSE("Stop Working: %d %d; %d%%", above_threshold, lower_threshold, update_shm.avg_cpu_usage); - } + if (conditions.release_frame_for_non_idle) { + anim_dm_restart_animation(ctx, animation_state_row_t::Idle, new_animation_result, new_state, + current_state, current_frames, current_config); + } } + } } - - return anim_update_animation_state(anim_shm, state, - new_animation_result, new_state, - current_animation_result, current_state, - current_config); + // Finsh wakeup + if (current_state.row_state == animation_state_row_t::WakeUp) { + // back to idle + if (conditions.process_idle_animation) { + // end current sleep animation + const auto animation_result = + anim_dm_start_or_process_animation(ctx, animation_state_row_t::Idle, new_animation_result, new_state, + current_state, current_frames, current_config); + if (animation_result.row_state == animation_state_row_t::Idle) { + new_state.is_idle_sleep = false; + } + } else { + if (conditions.release_frame_for_non_idle) { + anim_dm_restart_animation(ctx, animation_state_row_t::Idle, new_animation_result, new_state, + current_state, current_frames, current_config); + new_state.is_idle_sleep = false; + } + } + } + // Finish Working + if (current_state.row_state == animation_state_row_t::EndWorking && + (conditions.release_frame_after_update || conditions.release_frame_for_non_idle) && + !update_shm.cpu_active) { + if (conditions.process_idle_animation) { + anim_dm_start_or_process_animation(ctx, animation_state_row_t::Idle, // back to idle, when animation ended + new_animation_result, new_state, current_state, current_frames, + current_config); + } else { + if (conditions.release_frame_for_non_idle) { + // back to idle + anim_dm_restart_animation(ctx, animation_state_row_t::Idle, new_animation_result, new_state, + current_state, current_frames, current_config); + } + } + } else if (conditions.is_working && current_state.row_state != animation_state_row_t::EndWorking && + !update_shm.cpu_active) { + // Cancel Working + if (conditions.process_idle_animation) { + // back to idle + anim_dm_start_or_process_animation(ctx, animation_state_row_t::EndWorking, new_animation_result, new_state, + current_state, current_frames, current_config); + } else { + if (conditions.release_frame_for_non_idle) { + anim_dm_show_single_frame(ctx, animation_state_row_t::EndWorking, new_animation_result, new_state, + current_state, current_frames, current_config); + } + } + } + } } -#endif - - -#ifdef FEATURE_PKMN_EMBEDDED_ASSETS - enum class anim_pkmn_process_animation_result_status_t : uint8_t { None, Started, Updated, End, Looped, NextAnimationStarted }; - struct anim_pkmn_process_animation_result_t { - animation_state_row_t row_state; - anim_pkmn_process_animation_result_status_t status{anim_pkmn_process_animation_result_status_t::None}; - }; - static anim_pkmn_process_animation_result_t anim_pkmn_process_animation(animation_player_result_t& new_animation_result, - animation_state_t& new_state, - const animation_state_t& current_state, - const pkmn_sprite_sheet_t& current_frames) { - assert(MAX_ANIMATION_FRAMES > 0); - assert(MAX_ANIMATION_FRAMES <= INT_MAX); - - anim_pkmn_process_animation_result_t ret {.row_state = new_state.row_state, .status = anim_pkmn_process_animation_result_status_t::Updated}; - // forward animation - new_state.animations_index = current_state.animations_index + 1; - if (new_state.animations_index > static_cast(MAX_ANIMATION_FRAMES-1)) { - ret.status = anim_pkmn_process_animation_result_status_t::Looped; - new_state.animations_index = 0; + } + + /* + // Start Test animation + if (!conditions.any_key_pressed && conditions.trigger_test_animation && current_state.row_state == + animation_state_row_t::Idle) { anim_dm_restart_animation(ctx, animation_state_row_t::Test, new_animation_result, + new_state, current_state, current_frames, current_config); } else if (!conditions.any_key_pressed && + conditions.trigger_test_animation && current_state.row_state == animation_state_row_t::Test) { + // loop test animation + anim_dm_process_animation(new_animation_result, new_state, current_state, current_frames); + } + */ + + /// @TODO: move moving handle into own anim_handle_moving_animation + // Move Animation + anim_dm_handle_movement(ctx, input, upd, new_animation_result, new_state, trigger_result, current_state, + current_frames, current_config); + + const bool is_sleeping_time = current_config.enable_scheduled_sleep && is_sleep_time(current_config); + + // Idle Sleep + if (conditions.check_for_idle_sleep) { + if (!is_sleeping_time) { + const platform::timestamp_ms_t now = platform::get_current_time_ms(); + const platform::time_ms_t idle_sleep_timeout_ms = current_config.idle_sleep_timeout_sec * 1000; + assert(now >= last_key_pressed_timestamp); + const auto sleep_timeout = now - last_key_pressed_timestamp; + + const bool start_boring = SLEEP_BORING_PART > 0 && sleep_timeout >= idle_sleep_timeout_ms / SLEEP_BORING_PART; + if (current_state.row_state == animation_state_row_t::Idle) { + // start boring animation + if (start_boring && !current_state.show_boring_animation_once) { + if (conditions.process_idle_animation) { + anim_dm_restart_animation(ctx, animation_state_row_t::Boring, new_animation_result, new_state, + current_state, current_frames, current_config); + } else { + anim_dm_show_single_frame(ctx, animation_state_row_t::Boring, new_animation_result, new_state, + current_state, current_frames, current_config); + } + new_state.show_boring_animation_once = true; + new_state.anim_last_direction = 0.0f; } - /* - // backwards animation - else if (new_state.animations_index < 0) { - ret = anim_bongocat_process_animation_result_t::End; - new_state.animations_index = MAX_ANIMATION_FRAMES-1; + } else if (current_state.row_state == animation_state_row_t::Boring) { + if (conditions.process_idle_animation) { + const auto animation_result = anim_dm_start_or_process_animation( + ctx, animation_state_row_t::Idle, // back to idle, when animation ended + new_animation_result, new_state, current_state, current_frames, current_config); + if (!start_boring && animation_result.row_state == animation_state_row_t::Idle) { + new_state.show_boring_animation_once = false; + } + } else { + if (!start_boring || (start_boring && current_state.show_boring_animation_once)) { + if (conditions.release_frame_for_non_idle) { + // back to idle + anim_dm_restart_animation(ctx, animation_state_row_t::Idle, new_animation_result, new_state, + current_state, current_frames, current_config); + if (!start_boring) { + new_state.show_boring_animation_once = false; + } + } + } } - */ - switch (new_state.row_state) { - case animation_state_row_t::Idle: - new_animation_result.sprite_sheet_col = current_frames.animations.idle[new_state.animations_index]; - break; - case animation_state_row_t::StartWriting: - case animation_state_row_t::Writing: - case animation_state_row_t::EndWriting: - new_animation_result.sprite_sheet_col = current_frames.animations.writing[new_state.animations_index]; - break; - case animation_state_row_t::Happy: - new_animation_result.sprite_sheet_col = current_frames.animations.happy[new_state.animations_index]; - break; - case animation_state_row_t::FallASleep: - // not supported - new_animation_result.sprite_sheet_col = current_frames.animations.sleep[new_state.animations_index]; - break; - case animation_state_row_t::Sleep: - new_animation_result.sprite_sheet_col = current_frames.animations.sleep[new_state.animations_index]; - break; - case animation_state_row_t::WakeUp: - new_animation_result.sprite_sheet_col = current_frames.animations.wake_up[new_state.animations_index]; - break; - case animation_state_row_t::Boring: - new_animation_result.sprite_sheet_col = current_frames.animations.boring[new_state.animations_index]; - break; - case animation_state_row_t::Test: - new_animation_result.sprite_sheet_col = current_frames.animations.writing[new_state.animations_index]; - break; - case animation_state_row_t::StartWorking: - case animation_state_row_t::Working: - case animation_state_row_t::EndWorking: - new_animation_result.sprite_sheet_col = current_frames.animations.working[new_state.animations_index]; - break; - case animation_state_row_t::StartMoving: - case animation_state_row_t::Moving: - case animation_state_row_t::EndMoving: - new_animation_result.sprite_sheet_col = current_frames.animations.moving[new_state.animations_index]; - break; - case animation_state_row_t::StartRunning: - case animation_state_row_t::Running: - case animation_state_row_t::EndRunning: - new_animation_result.sprite_sheet_col = current_frames.animations.moving[new_state.animations_index]; - break; + } + + // idle sleep + if (sleep_timeout >= idle_sleep_timeout_ms) { + if (current_state.row_state == animation_state_row_t::Idle || conditions.is_moving) { + if (conditions.process_idle_animation && conditions.is_moving) { + anim_dm_start_or_process_animation(ctx, animation_state_row_t::Sleep, // wait for moving to end + new_animation_result, new_state, current_state, current_frames, + current_config); + } else { + anim_dm_restart_animation(ctx, animation_state_row_t::Sleep, new_animation_result, new_state, current_state, + current_frames, current_config); + } + new_state.anim_last_direction = 0.0f; + new_state.is_idle_sleep = true; + } else if (current_state.row_state == animation_state_row_t::Sleep) { + if (conditions.process_idle_animation) { + // loop sleep animation + anim_dm_process_animation(new_animation_result, new_state, current_state, current_frames); + } } - return ret; - } - static anim_pkmn_process_animation_result_t anim_pkmn_restart_animation([[maybe_unused]] animation_context_t& ctx, - animation_state_row_t new_row_state, - animation_player_result_t& new_animation_result, - animation_state_t& new_state, - [[maybe_unused]] const animation_state_t& current_state, - const pkmn_sprite_sheet_t& current_frames, - const config::config_t& current_config) { - using namespace assets; - assert(MAX_ANIMATION_FRAMES > 0); - assert(MAX_ANIMATION_FRAMES <= INT_MAX); - - new_state.row_state = new_row_state; - new_animation_result.sprite_sheet_row = PKMN_SPRITE_SHEET_ROW; - new_state.animations_index = 0; - - anim_pkmn_process_animation_result_t ret {.row_state = new_state.row_state, .status = anim_pkmn_process_animation_result_status_t::Started}; - switch (new_state.row_state) { - case animation_state_row_t::Idle: - new_animation_result.sprite_sheet_col = current_frames.animations.idle[new_state.animations_index]; - if (current_config.idle_frame) { - new_animation_result.sprite_sheet_col = current_config.idle_frame; - } - break; - case animation_state_row_t::StartWriting: - case animation_state_row_t::Writing: - case animation_state_row_t::EndWriting: - new_animation_result.sprite_sheet_col = current_frames.animations.writing[new_state.animations_index]; - break; - case animation_state_row_t::Happy: - new_animation_result.sprite_sheet_col = current_frames.animations.happy[new_state.animations_index]; - break; - case animation_state_row_t::FallASleep: - // not supported - new_animation_result.sprite_sheet_col = current_frames.animations.sleep[new_state.animations_index]; - break; - case animation_state_row_t::Sleep: - new_animation_result.sprite_sheet_col = current_frames.animations.sleep[new_state.animations_index]; - break; - case animation_state_row_t::WakeUp: - new_animation_result.sprite_sheet_col = current_frames.animations.wake_up[new_state.animations_index]; - break; - case animation_state_row_t::Boring: - new_animation_result.sprite_sheet_col = current_frames.animations.boring[new_state.animations_index]; - break; - case animation_state_row_t::Test: - new_animation_result.sprite_sheet_col = current_frames.animations.writing[new_state.animations_index]; - break; - case animation_state_row_t::StartWorking: - case animation_state_row_t::Working: - case animation_state_row_t::EndWorking: - new_animation_result.sprite_sheet_col = current_frames.animations.working[new_state.animations_index]; - break; - case animation_state_row_t::StartMoving: - case animation_state_row_t::Moving: - case animation_state_row_t::EndMoving: - new_animation_result.sprite_sheet_col = current_frames.animations.running[new_state.animations_index]; - break; - case animation_state_row_t::StartRunning: - case animation_state_row_t::Running: - case animation_state_row_t::EndRunning: - new_animation_result.sprite_sheet_col = current_frames.animations.running[new_state.animations_index]; - break; + } else { + if (current_state.row_state == animation_state_row_t::Sleep && current_state.is_idle_sleep) { + // wake up + if (conditions.process_idle_animation) { + // end current sleep animation + const auto animation_result = + anim_dm_start_or_process_animation(ctx, animation_state_row_t::WakeUp, new_animation_result, new_state, + current_state, current_frames, current_config); + if (animation_result.row_state == animation_state_row_t::WakeUp) { + new_state.is_idle_sleep = false; + new_state.show_boring_animation_once = false; + } + } else { + if (conditions.release_frame_for_non_idle) { + anim_dm_show_single_frame(ctx, animation_state_row_t::WakeUp, new_animation_result, new_state, + current_state, current_frames, current_config); + new_state.is_idle_sleep = false; + new_state.show_boring_animation_once = false; + } + } } - return ret; + } } - /* - static anim_pkmn_process_animation_result_t anim_pkmn_show_single_frame(animation_context_t& ctx, - animation_state_row_t new_row_state, - animation_player_result_t& new_animation_result, - animation_state_t& new_state, - [[maybe_unused]] const animation_state_t& current_state, - [[maybe_unused]] const pkmn_sprite_sheet_t& current_frames, - const config::config_t& current_config) { - using namespace assets; - assert(MAX_ANIMATION_FRAMES > 0); - assert(MAX_ANIMATION_FRAMES <= INT_MAX); - - new_state.row_state = new_row_state; - new_animation_result.sprite_sheet_row = PKMN_SPRITE_SHEET_ROW; - new_state.animations_index = 0; - - anim_pkmn_process_animation_result_t ret {.row_state = new_state.row_state, .status = anim_pkmn_process_animation_result_status_t::Started}; - switch (new_state.row_state) { - case animation_state_row_t::Idle: - new_animation_result.sprite_sheet_col = PKMN_FRAME_IDLE1; - if (current_config.idle_frame) { - new_animation_result.sprite_sheet_col = current_config.idle_frame; - } - break; - case animation_state_row_t::StartWriting: - case animation_state_row_t::Writing: - case animation_state_row_t::EndWriting: - // toggle frame - if (new_animation_result.sprite_sheet_col == PKMN_FRAME_IDLE1) { - new_animation_result.sprite_sheet_col = PKMN_FRAME_IDLE2; - } else if (new_animation_result.sprite_sheet_col == PKMN_FRAME_IDLE2) { - new_animation_result.sprite_sheet_col = PKMN_FRAME_IDLE1; - } else { - new_animation_result.sprite_sheet_col = ctx._rng.range(0, 100) <= 50 ? PKMN_FRAME_IDLE1 : PKMN_FRAME_IDLE2; - } - break; - case animation_state_row_t::Happy: - // toggle frame - if (new_animation_result.sprite_sheet_col == PKMN_FRAME_IDLE1) { - new_animation_result.sprite_sheet_col = PKMN_FRAME_IDLE2; - } else if (new_animation_result.sprite_sheet_col == PKMN_FRAME_IDLE2) { - new_animation_result.sprite_sheet_col = PKMN_FRAME_IDLE1; - } else { - new_animation_result.sprite_sheet_col = ctx._rng.range(0, 100) <= 50 ? PKMN_FRAME_IDLE1 : PKMN_FRAME_IDLE2; - } - break; - case animation_state_row_t::Sleep: - new_animation_result.sprite_sheet_col = PKMN_FRAME_IDLE2; - break; - case animation_state_row_t::WakeUp: - new_animation_result.sprite_sheet_col = PKMN_FRAME_IDLE1; - break; - case animation_state_row_t::Boring: - new_animation_result.sprite_sheet_col = PKMN_FRAME_IDLE2; - break; - case animation_state_row_t::Test: - // toggle frame (same as writing?) - if (new_animation_result.sprite_sheet_col == PKMN_FRAME_IDLE1) { - new_animation_result.sprite_sheet_col = PKMN_FRAME_IDLE2; - } else if (new_animation_result.sprite_sheet_col == PKMN_FRAME_IDLE2) { - new_animation_result.sprite_sheet_col = PKMN_FRAME_IDLE1; - } else { - new_animation_result.sprite_sheet_col = ctx._rng.range(0, 100) <= 50 ? PKMN_FRAME_IDLE1 : PKMN_FRAME_IDLE2; - } - break; - case animation_state_row_t::StartWorking: - case animation_state_row_t::Working: - case animation_state_row_t::EndWorking: - // toggle frame (unused) - if (new_animation_result.sprite_sheet_col == PKMN_FRAME_IDLE1) { - new_animation_result.sprite_sheet_col = PKMN_FRAME_IDLE2; - } else if (new_animation_result.sprite_sheet_col == PKMN_FRAME_IDLE2) { - new_animation_result.sprite_sheet_col = PKMN_FRAME_IDLE1; - } else { - new_animation_result.sprite_sheet_col = ctx._rng.range(0, 100) <= 50 ? PKMN_FRAME_IDLE1 : PKMN_FRAME_IDLE2; - } - break; - case animation_state_row_t::StartMoving: - case animation_state_row_t::Moving: - case animation_state_row_t::EndMoving: - // toggle frame (unused) - if (new_animation_result.sprite_sheet_col == PKMN_FRAME_IDLE1) { - new_animation_result.sprite_sheet_col = PKMN_FRAME_IDLE2; - } else if (new_animation_result.sprite_sheet_col == PKMN_FRAME_IDLE2) { - new_animation_result.sprite_sheet_col = PKMN_FRAME_IDLE1; - } else { - new_animation_result.sprite_sheet_col = ctx._rng.range(0, 100) <= 50 ? PKMN_FRAME_IDLE1 : PKMN_FRAME_IDLE2; - } - break; + } + + // Sleep Mode + if (current_config.enable_scheduled_sleep) { + if (is_sleeping_time) { + if (new_state.row_state == animation_state_row_t::Idle) { + anim_dm_restart_animation(ctx, animation_state_row_t::Sleep, new_animation_result, new_state, current_state, + current_frames, current_config); + new_state.anim_last_direction = 0.0f; + new_state.is_idle_sleep = false; + } else if (state.row_state == animation_state_row_t::Sleep && !new_state.is_idle_sleep) { + if (conditions.process_idle_animation) { + anim_dm_process_animation(new_animation_result, new_state, current_state, current_frames); } - return ret; - } - */ - - static anim_pkmn_process_animation_result_t anim_pkmn_start_or_process_animation(animation_context_t& ctx, - animation_state_row_t new_row_state, - animation_player_result_t& new_animation_result, - animation_state_t& new_state, - const animation_state_t& current_state, - const pkmn_sprite_sheet_t& current_frames, - const config::config_t& current_config) { - if (current_state.row_state != new_row_state) { - auto result = anim_pkmn_process_animation(new_animation_result, new_state, current_state, current_frames); - if (result.status == anim_pkmn_process_animation_result_status_t::Looped || result.status == anim_pkmn_process_animation_result_status_t::End) { - result = anim_pkmn_restart_animation(ctx, new_row_state, new_animation_result, new_state, current_state, current_frames, current_config); - result.status = anim_pkmn_process_animation_result_status_t::NextAnimationStarted; - } - return result; + } + } else { + if (current_state.row_state == animation_state_row_t::Sleep && !new_state.is_idle_sleep) { + // wake up + if (conditions.process_idle_animation) { + // end current sleep animation + const auto animation_result = + anim_dm_start_or_process_animation(ctx, animation_state_row_t::WakeUp, new_animation_result, new_state, + current_state, current_frames, current_config); + if (animation_result.row_state == animation_state_row_t::WakeUp) { + new_state.is_idle_sleep = false; + new_state.show_boring_animation_once = false; + } + } else { + if (conditions.release_frame_for_non_idle) { + anim_dm_show_single_frame(ctx, animation_state_row_t::WakeUp, new_animation_result, new_state, + current_state, current_frames, current_config); + new_state.is_idle_sleep = false; + new_state.show_boring_animation_once = false; + } } - - return anim_pkmn_restart_animation(ctx, new_row_state, new_animation_result, new_state, + } + } + } else { + if (current_state.row_state == animation_state_row_t::Sleep && !new_state.is_idle_sleep) { + // wake up + if (conditions.process_idle_animation) { + // end current sleep animation + const auto animation_result = + anim_dm_start_or_process_animation(ctx, animation_state_row_t::WakeUp, new_animation_result, new_state, current_state, current_frames, current_config); + if (animation_result.row_state == animation_state_row_t::WakeUp) { + new_state.is_idle_sleep = false; + new_state.show_boring_animation_once = false; + } + } else { + if (conditions.release_frame_for_non_idle) { + anim_dm_show_single_frame(ctx, animation_state_row_t::WakeUp, new_animation_result, new_state, current_state, + current_frames, current_config); + new_state.is_idle_sleep = false; + new_state.show_boring_animation_once = false; + } + } } + } - static anim_next_frame_result_t anim_pkmn_idle_next_frame(animation_context_t& ctx, - [[maybe_unused]] const platform::input::input_context_t& input, - [[maybe_unused]] const platform::update::update_context_t& upd, - animation_state_t& state, const anim_handle_key_press_result_t& trigger_result) { - using namespace assets; - // read-only config - assert(ctx._local_copy_config != nullptr); - const config::config_t& current_config = *ctx._local_copy_config; - - assert(ctx.shm != nullptr); - assert(input.shm != nullptr); - animation_shared_memory_t& anim_shm = *ctx.shm; - //const auto& input_shm = *input.shm; - const auto current_state = state; - const auto& current_animation_result = anim_shm.animation_player_result; - [[maybe_unused]] const int anim_index = anim_shm.anim_index; - assert(get_current_animation(ctx).type == animation_t::Type::Pkmn); - const auto& current_frames = get_current_animation(ctx).pkmn; - - auto new_animation_result = anim_shm.animation_player_result; - auto new_state = state; - - const auto conditions = get_anim_conditions(ctx, input, upd, current_state, trigger_result.trigger, current_config); + return anim_update_animation_state(anim_shm, state, new_animation_result, new_state, current_animation_result, + current_state, current_config); +} - // Idle Animation - const bool stop_writing = conditions.is_writing && conditions.release_frame_after_press; - if (stop_writing) { +static anim_next_frame_result_t +anim_dm_key_pressed_next_frame(animation_thread_context_t& ctx, const platform::input::input_context_t& input, + [[maybe_unused]] const platform::update::update_context_t& upd, animation_state_t& state, + const animation_trigger_t& trigger) { + using namespace assets; + + // read-only config + assert(ctx._local_copy_config); + const config::config_t& current_config = *ctx._local_copy_config; + + assert(ctx.shm); + assert(input.shm); + animation_shared_memory_t& anim_shm = *ctx.shm; + const auto& input_shm = *input.shm; + const auto current_state = state; + const auto& current_animation_result = anim_shm.animation_player_result; + [[maybe_unused]] const int anim_index = anim_shm.anim_index; + assert(get_current_animation(ctx).type == animation_t::type_t::Dm); + const auto& current_frames = get_current_animation(ctx).dm; + + auto new_animation_result = anim_shm.animation_player_result; + auto new_state = state; + + const auto conditions = get_anim_conditions(ctx, input, upd, current_state, trigger, current_config); + + bool show_happy = false; + if (input_shm.kpm > 0) { + if (current_config.happy_kpm > 0 && input_shm.kpm >= current_config.happy_kpm) { + show_happy = DM_HAPPY_CHANCE_PERCENT >= 100 || ctx._rng.range(0, 99) < DM_HAPPY_CHANCE_PERCENT; + } + } + + /// @TODO: use state machine for animation (states) + + if (show_happy && (conditions.is_writing || current_state.row_state == animation_state_row_t::Idle)) { + // show happy animation (KPM) + anim_dm_restart_animation(ctx, animation_state_row_t::Happy, new_animation_result, new_state, current_state, + current_frames, current_config); + } else if (conditions.is_writing || conditions.is_moving || current_state.row_state == animation_state_row_t::Idle) { + const bool end_writing = !conditions.release_frame_after_press && conditions.is_writing; + // in Writing mode/start writing + if (!conditions.is_writing || conditions.is_moving) { + anim_dm_restart_animation(ctx, animation_state_row_t::StartWriting, new_animation_result, new_state, + current_state, current_frames, current_config); + new_state.show_boring_animation_once = false; + } else { + if (conditions.process_idle_animation) { + // transition writing animations + if (current_state.row_state == animation_state_row_t::StartWriting) { + anim_dm_start_or_process_animation(ctx, animation_state_row_t::Writing, new_animation_result, new_state, + current_state, current_frames, current_config); + } else if (conditions.release_frame_after_press && current_state.row_state == animation_state_row_t::Writing) { + anim_dm_start_or_process_animation(ctx, animation_state_row_t::EndWriting, new_animation_result, new_state, + current_state, current_frames, current_config); + } else if (conditions.release_frame_after_press && + current_state.row_state == animation_state_row_t::EndWriting) { + anim_dm_start_or_process_animation(ctx, animation_state_row_t::Idle, // back to idle + new_animation_result, new_state, current_state, current_frames, + current_config); + } else if (end_writing) { + // keep writing + anim_dm_process_animation(new_animation_result, new_state, current_state, current_frames); + } + } else { + if (current_state.row_state == animation_state_row_t::StartWriting) { + anim_dm_start_or_process_animation(ctx, animation_state_row_t::Writing, new_animation_result, new_state, + current_state, current_frames, current_config); + } else if (end_writing) { + // back to idle + anim_dm_restart_animation(ctx, animation_state_row_t::Idle, new_animation_result, new_state, current_state, + current_frames, current_config); + } else if (conditions.continue_writing) { + // keep writing + anim_dm_process_animation(new_animation_result, new_state, current_state, current_frames); + } + } + } + } else if (current_state.row_state == animation_state_row_t::Happy) { + if (conditions.release_frame_after_press) { + if (conditions.process_idle_animation) { + // finish last happy animation + anim_dm_start_or_process_animation( + ctx, + conditions.continue_writing ? animation_state_row_t::Writing : animation_state_row_t::Idle, // back to idle + new_animation_result, new_state, current_state, current_frames, current_config); + } else { + // back to idle + anim_dm_restart_animation( + ctx, conditions.continue_writing ? animation_state_row_t::Writing : animation_state_row_t::Idle, + new_animation_result, new_state, current_state, current_frames, current_config); + } + } else { + if (!conditions.process_idle_animation) { + // back to idle + anim_dm_restart_animation( + ctx, conditions.continue_writing ? animation_state_row_t::Writing : animation_state_row_t::Idle, + new_animation_result, new_state, current_state, current_frames, current_config); + } else { + // finish last happy animation + anim_dm_start_or_process_animation( + ctx, + conditions.continue_writing ? animation_state_row_t::Writing : animation_state_row_t::Idle, // back to idle + new_animation_result, new_state, current_state, current_frames, current_config); + } + } + } else if (state.row_state == animation_state_row_t::Sleep && current_state.is_idle_sleep) { + // wake up + anim_dm_restart_animation(ctx, animation_state_row_t::WakeUp, new_animation_result, new_state, current_state, + current_frames, current_config); + } + + return anim_update_animation_state(anim_shm, state, new_animation_result, new_state, current_animation_result, + current_state, current_config); +} + +static anim_next_frame_result_t anim_dm_working_next_frame(animation_thread_context_t& ctx, + const platform::input::input_context_t& input, + const platform::update::update_context_t& upd, + animation_state_t& state, + const animation_trigger_t& trigger) { + using namespace assets; + + // read-only config + assert(ctx._local_copy_config); + const config::config_t& current_config = *ctx._local_copy_config; + + assert(ctx.shm); + // assert(input.shm != nullptr); + assert(upd.shm); + animation_shared_memory_t& anim_shm = *ctx.shm; + // const auto& input_shm = *input.shm; + const auto& update_shm = *upd.shm; + const auto current_state = state; + const auto& current_animation_result = anim_shm.animation_player_result; + [[maybe_unused]] const int anim_index = anim_shm.anim_index; + // const platform::timestamp_ms_t last_key_pressed_timestamp = input_shm.last_key_pressed_timestamp; + assert(get_current_animation(ctx).type == animation_t::type_t::Dm); + const auto& current_frames = get_current_animation(ctx).dm; + + auto new_animation_result = anim_shm.animation_player_result; + auto new_state = state; + + const auto conditions = get_anim_conditions(ctx, input, upd, current_state, trigger, current_config); + + /// @TODO: use state machine for animation (states) + + if (conditions.process_working_animation) { + // toggle frame, show attack animation + const bool above_threshold = current_config.cpu_threshold >= platform::ENABLED_MIN_CPU_PERCENT && + (update_shm.avg_cpu_usage >= current_config.cpu_threshold || + update_shm.max_cpu_usage >= current_config.cpu_threshold); + const bool lower_threshold = current_config.cpu_threshold >= platform::ENABLED_MIN_CPU_PERCENT && + (update_shm.avg_cpu_usage < current_config.cpu_threshold && + update_shm.max_cpu_usage < current_config.cpu_threshold); + + if (above_threshold) { + if (current_state.row_state == animation_state_row_t::Idle || conditions.is_moving) { + anim_dm_restart_animation(ctx, animation_state_row_t::StartWorking, new_animation_result, new_state, + current_state, current_frames, current_config); + BONGOCAT_LOG_VERBOSE("Start Working: %d %d; %d%%", above_threshold, lower_threshold, update_shm.avg_cpu_usage); + } else if (conditions.is_working) { + // continues animations + if (conditions.process_idle_animation) { + if (update_shm.cpu_active && current_state.row_state == animation_state_row_t::StartWorking) { + anim_dm_start_or_process_animation(ctx, animation_state_row_t::Working, // continue working + new_animation_result, new_state, current_state, current_frames, + current_config); + } else if (!update_shm.cpu_active && (current_state.row_state == animation_state_row_t::StartWorking || + state.row_state == animation_state_row_t::Working)) { + // end working, cool down + anim_dm_start_or_process_animation(ctx, animation_state_row_t::EndWorking, new_animation_result, new_state, + current_state, current_frames, current_config); + } else if (!update_shm.cpu_active && current_state.row_state == animation_state_row_t::EndWorking) { // back to idle - anim_pkmn_restart_animation(ctx, animation_state_row_t::Idle, - new_animation_result, new_state, - current_state, current_frames, current_config); + anim_dm_start_or_process_animation(ctx, animation_state_row_t::Idle, new_animation_result, new_state, + current_state, current_frames, current_config); + } else if (update_shm.cpu_active && current_state.row_state == animation_state_row_t::Working) { + // keep working + anim_dm_process_animation(new_animation_result, new_state, current_state, current_frames); + } } else { - // continues animations - if (current_state.row_state == animation_state_row_t::Idle) { - if (conditions.process_idle_animation) { - // Idle Animation - anim_pkmn_process_animation(new_animation_result, new_state, current_state, current_frames); - } + if (update_shm.cpu_active) { + if (update_shm.cpu_active && current_state.row_state == animation_state_row_t::StartWorking) { + anim_dm_show_single_frame(ctx, animation_state_row_t::Working, new_animation_result, new_state, + current_state, current_frames, current_config); + } else if (!update_shm.cpu_active && (current_state.row_state == animation_state_row_t::StartWorking || + state.row_state == animation_state_row_t::Working)) { + // end working + anim_dm_show_single_frame(ctx, animation_state_row_t::EndWorking, new_animation_result, new_state, + current_state, current_frames, current_config); + } else if (!update_shm.cpu_active && current_state.row_state == animation_state_row_t::EndWorking) { + // back to idle + anim_dm_restart_animation(ctx, animation_state_row_t::Idle, new_animation_result, new_state, + current_state, current_frames, current_config); + } else if (update_shm.cpu_active && current_state.row_state == animation_state_row_t::Working) { + // keep working + anim_dm_process_animation(new_animation_result, new_state, current_state, current_frames); } + } + } + } + } else if (lower_threshold) { + // End Working + if (conditions.is_working && current_state.row_state != animation_state_row_t::EndMoving) { + if (conditions.process_idle_animation) { + // end working, cool down + anim_dm_start_or_process_animation(ctx, animation_state_row_t::EndWorking, new_animation_result, new_state, + current_state, current_frames, current_config); + } else { + anim_dm_show_single_frame(ctx, animation_state_row_t::EndWorking, new_animation_result, new_state, + current_state, current_frames, current_config); + } + BONGOCAT_LOG_VERBOSE("Stop Working: %d %d; %d%%", above_threshold, lower_threshold, update_shm.avg_cpu_usage); + } + } else { + // Cancel Working + if (conditions.is_working && current_state.row_state != animation_state_row_t::EndWorking && + !update_shm.cpu_active) { + if (conditions.process_idle_animation) { + // back to idle + anim_dm_start_or_process_animation(ctx, animation_state_row_t::EndWorking, new_animation_result, new_state, + current_state, current_frames, current_config); + } else { + anim_dm_show_single_frame(ctx, animation_state_row_t::EndMoving, new_animation_result, new_state, + current_state, current_frames, current_config); } + BONGOCAT_LOG_VERBOSE("Stop Working: %d %d; %d%%", above_threshold, lower_threshold, update_shm.avg_cpu_usage); + } + } + } - const bool is_sleeping_time = current_config.enable_scheduled_sleep && is_sleep_time(current_config); + return anim_update_animation_state(anim_shm, state, new_animation_result, new_state, current_animation_result, + current_state, current_config); +} +#endif - // Sleep Mode - if (current_config.enable_scheduled_sleep) { - if (is_sleeping_time) { - if (current_state.row_state == animation_state_row_t::Idle) { - anim_pkmn_restart_animation(ctx, animation_state_row_t::Sleep, - new_animation_result, new_state, - current_state, current_frames, current_config); - new_state.is_idle_sleep = false; - } +#ifdef FEATURE_PKMN_EMBEDDED_ASSETS +enum class anim_pkmn_process_animation_result_status_t : uint8_t { + None, + Started, + Updated, + End, + Looped, + NextAnimationStarted +}; +struct anim_pkmn_process_animation_result_t { + animation_state_row_t row_state; + anim_pkmn_process_animation_result_status_t status{anim_pkmn_process_animation_result_status_t::None}; +}; +static anim_pkmn_process_animation_result_t anim_pkmn_process_animation(animation_player_result_t& new_animation_result, + animation_state_t& new_state, + const animation_state_t& current_state, + const pkmn_sprite_sheet_t& current_frames) { + assert(MAX_ANIMATION_FRAMES > 0); + assert(MAX_ANIMATION_FRAMES <= INT_MAX); + + anim_pkmn_process_animation_result_t ret{.row_state = new_state.row_state, + .status = anim_pkmn_process_animation_result_status_t::Updated}; + // forward animation + new_state.animations_index = current_state.animations_index + 1; + if (new_state.animations_index > static_cast(MAX_ANIMATION_FRAMES - 1)) { + ret.status = anim_pkmn_process_animation_result_status_t::Looped; + new_state.animations_index = 0; + } + /* + // backwards animation + else if (new_state.animations_index < 0) { + ret = anim_bongocat_process_animation_result_t::End; + new_state.animations_index = MAX_ANIMATION_FRAMES-1; + } + */ + switch (new_state.row_state) { + case animation_state_row_t::Idle: + new_animation_result.sprite_sheet_col = current_frames.animations.idle[new_state.animations_index]; + break; + case animation_state_row_t::StartWriting: + case animation_state_row_t::Writing: + case animation_state_row_t::EndWriting: + new_animation_result.sprite_sheet_col = current_frames.animations.writing[new_state.animations_index]; + break; + case animation_state_row_t::Happy: + new_animation_result.sprite_sheet_col = current_frames.animations.happy[new_state.animations_index]; + break; + case animation_state_row_t::FallASleep: + // not supported + new_animation_result.sprite_sheet_col = current_frames.animations.sleep[new_state.animations_index]; + break; + case animation_state_row_t::Sleep: + new_animation_result.sprite_sheet_col = current_frames.animations.sleep[new_state.animations_index]; + break; + case animation_state_row_t::WakeUp: + new_animation_result.sprite_sheet_col = current_frames.animations.wake_up[new_state.animations_index]; + break; + case animation_state_row_t::Boring: + new_animation_result.sprite_sheet_col = current_frames.animations.boring[new_state.animations_index]; + break; + case animation_state_row_t::Test: + new_animation_result.sprite_sheet_col = current_frames.animations.writing[new_state.animations_index]; + break; + case animation_state_row_t::StartWorking: + case animation_state_row_t::Working: + case animation_state_row_t::EndWorking: + new_animation_result.sprite_sheet_col = current_frames.animations.working[new_state.animations_index]; + break; + case animation_state_row_t::StartMoving: + case animation_state_row_t::Moving: + case animation_state_row_t::EndMoving: + new_animation_result.sprite_sheet_col = current_frames.animations.moving[new_state.animations_index]; + break; + case animation_state_row_t::StartRunning: + case animation_state_row_t::Running: + case animation_state_row_t::EndRunning: + new_animation_result.sprite_sheet_col = current_frames.animations.moving[new_state.animations_index]; + break; + } + return ret; +} +static anim_pkmn_process_animation_result_t +anim_pkmn_restart_animation([[maybe_unused]] animation_thread_context_t& ctx, animation_state_row_t new_row_state, + animation_player_result_t& new_animation_result, animation_state_t& new_state, + [[maybe_unused]] const animation_state_t& current_state, + const pkmn_sprite_sheet_t& current_frames, const config::config_t& current_config) { + using namespace assets; + assert(MAX_ANIMATION_FRAMES > 0); + assert(MAX_ANIMATION_FRAMES <= INT_MAX); + + new_state.row_state = new_row_state; + new_animation_result.sprite_sheet_row = PKMN_SPRITE_SHEET_ROW; + new_state.animations_index = 0; + + anim_pkmn_process_animation_result_t ret{.row_state = new_state.row_state, + .status = anim_pkmn_process_animation_result_status_t::Started}; + switch (new_state.row_state) { + case animation_state_row_t::Idle: + new_animation_result.sprite_sheet_col = current_frames.animations.idle[new_state.animations_index]; + if (current_config.idle_frame) { + new_animation_result.sprite_sheet_col = current_config.idle_frame; + } + break; + case animation_state_row_t::StartWriting: + case animation_state_row_t::Writing: + case animation_state_row_t::EndWriting: + new_animation_result.sprite_sheet_col = current_frames.animations.writing[new_state.animations_index]; + break; + case animation_state_row_t::Happy: + new_animation_result.sprite_sheet_col = current_frames.animations.happy[new_state.animations_index]; + break; + case animation_state_row_t::FallASleep: + // not supported + new_animation_result.sprite_sheet_col = current_frames.animations.sleep[new_state.animations_index]; + break; + case animation_state_row_t::Sleep: + new_animation_result.sprite_sheet_col = current_frames.animations.sleep[new_state.animations_index]; + break; + case animation_state_row_t::WakeUp: + new_animation_result.sprite_sheet_col = current_frames.animations.wake_up[new_state.animations_index]; + break; + case animation_state_row_t::Boring: + new_animation_result.sprite_sheet_col = current_frames.animations.boring[new_state.animations_index]; + break; + case animation_state_row_t::Test: + new_animation_result.sprite_sheet_col = current_frames.animations.writing[new_state.animations_index]; + break; + case animation_state_row_t::StartWorking: + case animation_state_row_t::Working: + case animation_state_row_t::EndWorking: + new_animation_result.sprite_sheet_col = current_frames.animations.working[new_state.animations_index]; + break; + case animation_state_row_t::StartMoving: + case animation_state_row_t::Moving: + case animation_state_row_t::EndMoving: + new_animation_result.sprite_sheet_col = current_frames.animations.running[new_state.animations_index]; + break; + case animation_state_row_t::StartRunning: + case animation_state_row_t::Running: + case animation_state_row_t::EndRunning: + new_animation_result.sprite_sheet_col = current_frames.animations.running[new_state.animations_index]; + break; + } + return ret; +} +/* +static anim_pkmn_process_animation_result_t anim_pkmn_show_single_frame(animation_context_t& ctx, + animation_state_row_t new_row_state, + animation_player_result_t& +new_animation_result, animation_state_t& new_state, + [[maybe_unused]] const +animation_state_t& current_state, + [[maybe_unused]] const +pkmn_sprite_sheet_t& current_frames, const config::config_t& current_config) { using namespace assets; + assert(MAX_ANIMATION_FRAMES > 0); + assert(MAX_ANIMATION_FRAMES <= INT_MAX); + + new_state.row_state = new_row_state; + new_animation_result.sprite_sheet_row = PKMN_SPRITE_SHEET_ROW; + new_state.animations_index = 0; + + anim_pkmn_process_animation_result_t ret {.row_state = new_state.row_state, .status = +anim_pkmn_process_animation_result_status_t::Started}; switch (new_state.row_state) { case animation_state_row_t::Idle: + new_animation_result.sprite_sheet_col = PKMN_FRAME_IDLE1; + if (current_config.idle_frame) { + new_animation_result.sprite_sheet_col = current_config.idle_frame; + } + break; + case animation_state_row_t::StartWriting: + case animation_state_row_t::Writing: + case animation_state_row_t::EndWriting: + // toggle frame + if (new_animation_result.sprite_sheet_col == PKMN_FRAME_IDLE1) { + new_animation_result.sprite_sheet_col = PKMN_FRAME_IDLE2; + } else if (new_animation_result.sprite_sheet_col == PKMN_FRAME_IDLE2) { + new_animation_result.sprite_sheet_col = PKMN_FRAME_IDLE1; } else { - if (current_state.row_state == animation_state_row_t::Sleep && !current_state.is_idle_sleep) { - if (conditions.release_frame_for_non_idle) { - // back to idle - anim_pkmn_restart_animation(ctx, animation_state_row_t::Idle, - new_animation_result, new_state, - current_state, current_frames, current_config); - } - } + new_animation_result.sprite_sheet_col = ctx._rng.range(0, 100) <= 50 ? PKMN_FRAME_IDLE1 : +PKMN_FRAME_IDLE2; } - } else { - if (current_state.row_state == animation_state_row_t::Sleep && !current_state.is_idle_sleep) { - if (conditions.release_frame_for_non_idle) { - // back to idle - anim_pkmn_restart_animation(ctx, animation_state_row_t::Idle, - new_animation_result, new_state, - current_state, current_frames, current_config); - } + break; + case animation_state_row_t::Happy: + // toggle frame + if (new_animation_result.sprite_sheet_col == PKMN_FRAME_IDLE1) { + new_animation_result.sprite_sheet_col = PKMN_FRAME_IDLE2; + } else if (new_animation_result.sprite_sheet_col == PKMN_FRAME_IDLE2) { + new_animation_result.sprite_sheet_col = PKMN_FRAME_IDLE1; + } else { + new_animation_result.sprite_sheet_col = ctx._rng.range(0, 100) <= 50 ? PKMN_FRAME_IDLE1 : +PKMN_FRAME_IDLE2; } - } - - return anim_update_animation_state(anim_shm, state, - new_animation_result, new_state, - current_animation_result, current_state, - current_config); + break; + case animation_state_row_t::Sleep: + new_animation_result.sprite_sheet_col = PKMN_FRAME_IDLE2; + break; + case animation_state_row_t::WakeUp: + new_animation_result.sprite_sheet_col = PKMN_FRAME_IDLE1; + break; + case animation_state_row_t::Boring: + new_animation_result.sprite_sheet_col = PKMN_FRAME_IDLE2; + break; + case animation_state_row_t::Test: + // toggle frame (same as writing?) + if (new_animation_result.sprite_sheet_col == PKMN_FRAME_IDLE1) { + new_animation_result.sprite_sheet_col = PKMN_FRAME_IDLE2; + } else if (new_animation_result.sprite_sheet_col == PKMN_FRAME_IDLE2) { + new_animation_result.sprite_sheet_col = PKMN_FRAME_IDLE1; + } else { + new_animation_result.sprite_sheet_col = ctx._rng.range(0, 100) <= 50 ? PKMN_FRAME_IDLE1 : +PKMN_FRAME_IDLE2; + } + break; + case animation_state_row_t::StartWorking: + case animation_state_row_t::Working: + case animation_state_row_t::EndWorking: + // toggle frame (unused) + if (new_animation_result.sprite_sheet_col == PKMN_FRAME_IDLE1) { + new_animation_result.sprite_sheet_col = PKMN_FRAME_IDLE2; + } else if (new_animation_result.sprite_sheet_col == PKMN_FRAME_IDLE2) { + new_animation_result.sprite_sheet_col = PKMN_FRAME_IDLE1; + } else { + new_animation_result.sprite_sheet_col = ctx._rng.range(0, 100) <= 50 ? PKMN_FRAME_IDLE1 : +PKMN_FRAME_IDLE2; + } + break; + case animation_state_row_t::StartMoving: + case animation_state_row_t::Moving: + case animation_state_row_t::EndMoving: + // toggle frame (unused) + if (new_animation_result.sprite_sheet_col == PKMN_FRAME_IDLE1) { + new_animation_result.sprite_sheet_col = PKMN_FRAME_IDLE2; + } else if (new_animation_result.sprite_sheet_col == PKMN_FRAME_IDLE2) { + new_animation_result.sprite_sheet_col = PKMN_FRAME_IDLE1; + } else { + new_animation_result.sprite_sheet_col = ctx._rng.range(0, 100) <= 50 ? PKMN_FRAME_IDLE1 : +PKMN_FRAME_IDLE2; + } + break; } + return ret; +} +*/ + +static anim_pkmn_process_animation_result_t +anim_pkmn_start_or_process_animation(animation_thread_context_t& ctx, animation_state_row_t new_row_state, + animation_player_result_t& new_animation_result, animation_state_t& new_state, + const animation_state_t& current_state, const pkmn_sprite_sheet_t& current_frames, + const config::config_t& current_config) { + if (current_state.row_state != new_row_state) { + auto result = anim_pkmn_process_animation(new_animation_result, new_state, current_state, current_frames); + if (result.status == anim_pkmn_process_animation_result_status_t::Looped || + result.status == anim_pkmn_process_animation_result_status_t::End) { + result = anim_pkmn_restart_animation(ctx, new_row_state, new_animation_result, new_state, current_state, + current_frames, current_config); + result.status = anim_pkmn_process_animation_result_status_t::NextAnimationStarted; + } + return result; + } - static anim_next_frame_result_t anim_pkmn_key_pressed_next_frame(animation_context_t& ctx, - [[maybe_unused]] const platform::input::input_context_t& input, - [[maybe_unused]] const platform::update::update_context_t& upd, - animation_state_t& state, - const animation_trigger_t& trigger) { - using namespace assets; - - // read-only config - assert(ctx._local_copy_config != nullptr); - const config::config_t& current_config = *ctx._local_copy_config; - - assert(ctx.shm != nullptr); - assert(input.shm != nullptr); - animation_shared_memory_t& anim_shm = *ctx.shm; - //const auto& input_shm = *input.shm; - const auto current_state = state; - const auto& current_animation_result = anim_shm.animation_player_result; - [[maybe_unused]] const int anim_index = anim_shm.anim_index; - //const platform::timestamp_ms_t last_key_pressed_timestamp = input_shm.last_key_pressed_timestamp; - assert(get_current_animation(ctx).type == animation_t::Type::Pkmn); - const auto& current_frames = get_current_animation(ctx).pkmn; - - auto new_animation_result = anim_shm.animation_player_result; - auto new_state = state; - - const auto conditions = get_anim_conditions(ctx, input, upd, current_state, trigger, current_config); - - // in Writing mode/start writing - if (!conditions.is_writing) { - if (state.row_state == animation_state_row_t::Sleep && current_state.is_idle_sleep) { - // wake up - anim_pkmn_restart_animation(ctx, animation_state_row_t::WakeUp, - new_animation_result, new_state, - current_state, current_frames, current_config); - } else if (state.row_state == animation_state_row_t::Idle || conditions.is_moving) { - // start writing - anim_pkmn_restart_animation(ctx, animation_state_row_t::StartWriting, - new_animation_result, new_state, - current_state, current_frames, current_config); - } - } else if (state.row_state == animation_state_row_t::StartWriting) { - anim_pkmn_start_or_process_animation(ctx, animation_state_row_t::Writing, - new_animation_result, new_state, - current_state, current_frames, current_config); - } + return anim_pkmn_restart_animation(ctx, new_row_state, new_animation_result, new_state, current_state, current_frames, + current_config); +} - if (conditions.is_writing) { - if (conditions.continue_writing) { - // keep writing - anim_pkmn_process_animation(new_animation_result, new_state, current_state, current_frames); - } - } else if (((!conditions.is_writing && state.row_state == animation_state_row_t::Idle) || state.row_state == animation_state_row_t::WakeUp) && conditions.release_frame_after_press) { - // back to idle - anim_pkmn_restart_animation(ctx, animation_state_row_t::Idle, - new_animation_result, new_state, - current_state, current_frames, current_config); +static anim_next_frame_result_t +anim_pkmn_idle_next_frame(animation_thread_context_t& ctx, + [[maybe_unused]] const platform::input::input_context_t& input, + [[maybe_unused]] const platform::update::update_context_t& upd, animation_state_t& state, + const anim_handle_key_press_result_t& trigger_result) { + using namespace assets; + // read-only config + assert(ctx._local_copy_config); + const config::config_t& current_config = *ctx._local_copy_config; + + assert(ctx.shm); + assert(input.shm); + animation_shared_memory_t& anim_shm = *ctx.shm; + // const auto& input_shm = *input.shm; + const auto current_state = state; + const auto& current_animation_result = anim_shm.animation_player_result; + [[maybe_unused]] const int anim_index = anim_shm.anim_index; + assert(get_current_animation(ctx).type == animation_t::type_t::Pkmn); + const auto& current_frames = get_current_animation(ctx).pkmn; + + auto new_animation_result = anim_shm.animation_player_result; + auto new_state = state; + + const auto conditions = get_anim_conditions(ctx, input, upd, current_state, trigger_result.trigger, current_config); + + // Idle Animation + const bool stop_writing = conditions.is_writing && conditions.release_frame_after_press; + if (stop_writing) { + // back to idle + anim_pkmn_restart_animation(ctx, animation_state_row_t::Idle, new_animation_result, new_state, current_state, + current_frames, current_config); + } else { + // continues animations + if (current_state.row_state == animation_state_row_t::Idle) { + if (conditions.process_idle_animation) { + // Idle Animation + anim_pkmn_process_animation(new_animation_result, new_state, current_state, current_frames); + } + } + } + + const bool is_sleeping_time = current_config.enable_scheduled_sleep && is_sleep_time(current_config); + + // Sleep Mode + if (current_config.enable_scheduled_sleep) { + if (is_sleeping_time) { + if (current_state.row_state == animation_state_row_t::Idle) { + anim_pkmn_restart_animation(ctx, animation_state_row_t::Sleep, new_animation_result, new_state, current_state, + current_frames, current_config); + new_state.is_idle_sleep = false; + } + } else { + if (current_state.row_state == animation_state_row_t::Sleep && !current_state.is_idle_sleep) { + if (conditions.release_frame_for_non_idle) { + // back to idle + anim_pkmn_restart_animation(ctx, animation_state_row_t::Idle, new_animation_result, new_state, current_state, + current_frames, current_config); } + } + } + } else { + if (current_state.row_state == animation_state_row_t::Sleep && !current_state.is_idle_sleep) { + if (conditions.release_frame_for_non_idle) { + // back to idle + anim_pkmn_restart_animation(ctx, animation_state_row_t::Idle, new_animation_result, new_state, current_state, + current_frames, current_config); + } + } + } - return anim_update_animation_state(anim_shm, state, - new_animation_result, new_state, - current_animation_result, current_state, - current_config); + return anim_update_animation_state(anim_shm, state, new_animation_result, new_state, current_animation_result, + current_state, current_config); +} + +static anim_next_frame_result_t +anim_pkmn_key_pressed_next_frame(animation_thread_context_t& ctx, + [[maybe_unused]] const platform::input::input_context_t& input, + [[maybe_unused]] const platform::update::update_context_t& upd, + animation_state_t& state, const animation_trigger_t& trigger) { + using namespace assets; + + // read-only config + assert(ctx._local_copy_config); + const config::config_t& current_config = *ctx._local_copy_config; + + assert(ctx.shm); + assert(input.shm); + animation_shared_memory_t& anim_shm = *ctx.shm; + // const auto& input_shm = *input.shm; + const auto current_state = state; + const auto& current_animation_result = anim_shm.animation_player_result; + [[maybe_unused]] const int anim_index = anim_shm.anim_index; + // const platform::timestamp_ms_t last_key_pressed_timestamp = input_shm.last_key_pressed_timestamp; + assert(get_current_animation(ctx).type == animation_t::type_t::Pkmn); + const auto& current_frames = get_current_animation(ctx).pkmn; + + auto new_animation_result = anim_shm.animation_player_result; + auto new_state = state; + + const auto conditions = get_anim_conditions(ctx, input, upd, current_state, trigger, current_config); + + // in Writing mode/start writing + if (!conditions.is_writing) { + if (state.row_state == animation_state_row_t::Sleep && current_state.is_idle_sleep) { + // wake up + anim_pkmn_restart_animation(ctx, animation_state_row_t::WakeUp, new_animation_result, new_state, current_state, + current_frames, current_config); + } else if (state.row_state == animation_state_row_t::Idle || conditions.is_moving) { + // start writing + anim_pkmn_restart_animation(ctx, animation_state_row_t::StartWriting, new_animation_result, new_state, + current_state, current_frames, current_config); + } + } else if (state.row_state == animation_state_row_t::StartWriting) { + anim_pkmn_start_or_process_animation(ctx, animation_state_row_t::Writing, new_animation_result, new_state, + current_state, current_frames, current_config); + } + + if (conditions.is_writing) { + if (conditions.continue_writing) { + // keep writing + anim_pkmn_process_animation(new_animation_result, new_state, current_state, current_frames); } + } else if (((!conditions.is_writing && state.row_state == animation_state_row_t::Idle) || + state.row_state == animation_state_row_t::WakeUp) && + conditions.release_frame_after_press) { + // back to idle + anim_pkmn_restart_animation(ctx, animation_state_row_t::Idle, new_animation_result, new_state, current_state, + current_frames, current_config); + } + + return anim_update_animation_state(anim_shm, state, new_animation_result, new_state, current_animation_result, + current_state, current_config); +} #endif #ifdef FEATURE_MS_AGENT_EMBEDDED_ASSETS - enum class anim_ms_agent_process_animation_result_status_t : uint8_t { None, Started, Updated, End, Looped, NextAnimationStarted }; - struct anim_ms_agent_process_animation_result_t { - animation_state_row_t row_state; - anim_ms_agent_process_animation_result_status_t status{anim_ms_agent_process_animation_result_status_t::None}; - }; - static anim_ms_agent_process_animation_result_t anim_ms_agent_process_animation(animation_player_result_t& new_animation_result, - animation_state_t& new_state, - [[maybe_unused]] const animation_state_t& current_state, - const ms_agent_sprite_sheet_t& current_frames) { - using namespace assets; - assert(MAX_ANIMATION_FRAMES > 0); - assert(MAX_ANIMATION_FRAMES <= INT_MAX); - - const ms_agent_sprite_sheet_animation_section_t *section = nullptr; - switch (new_state.row_state) { - case animation_state_row_t::Idle: - section = current_frames.idle.valid ? ¤t_frames.idle : nullptr; - break; - case animation_state_row_t::StartWriting: - section = current_frames.start_writing.valid ? ¤t_frames.start_writing : nullptr; - break; - case animation_state_row_t::Writing: - section = current_frames.writing.valid ? ¤t_frames.writing : nullptr; - break; - case animation_state_row_t::EndWriting: - section = current_frames.end_writing.valid ? ¤t_frames.end_writing : nullptr; - break; - case animation_state_row_t::Happy: - section = current_frames.happy.valid ? ¤t_frames.happy : nullptr; - break; - case animation_state_row_t::FallASleep: - // use sleep animation for ms agent - section = current_frames.sleep.valid ? ¤t_frames.sleep : nullptr; - break; - case animation_state_row_t::Sleep: - section = current_frames.sleep.valid ? ¤t_frames.sleep : nullptr; - break; - case animation_state_row_t::WakeUp: - section = current_frames.wake_up.valid ? ¤t_frames.wake_up : nullptr; - break; - case animation_state_row_t::Boring: - section = current_frames.boring.valid ? ¤t_frames.boring : nullptr; - break; - case animation_state_row_t::Test: - section = current_frames.writing.valid ? ¤t_frames.writing : nullptr; - break; - case animation_state_row_t::StartWorking: - section = current_frames.start_working.valid ? ¤t_frames.start_working : nullptr; - break; - case animation_state_row_t::Working: - section = current_frames.working.valid ? ¤t_frames.working : nullptr; - break; - case animation_state_row_t::EndWorking: - section = current_frames.end_working.valid ? ¤t_frames.end_working : nullptr; - break; - case animation_state_row_t::StartMoving: - section = current_frames.start_moving.valid ? ¤t_frames.start_moving : nullptr; - break; - case animation_state_row_t::Moving: - section = current_frames.moving.valid ? ¤t_frames.moving : nullptr; - break; - case animation_state_row_t::EndMoving: - section = current_frames.end_moving.valid ? ¤t_frames.end_moving : nullptr; - break; - case animation_state_row_t::StartRunning: - section = current_frames.start_running.valid ? ¤t_frames.start_running : nullptr; - break; - case animation_state_row_t::Running: - section = current_frames.running.valid ? ¤t_frames.running : nullptr; - break; - case animation_state_row_t::EndRunning: - section = current_frames.end_running.valid ? ¤t_frames.end_running : nullptr; - break; - } - - anim_ms_agent_process_animation_result_t ret {.row_state = new_state.row_state, .status = anim_ms_agent_process_animation_result_status_t::None}; - if (section && section->valid) { - new_animation_result.sprite_sheet_row = section->row; - new_animation_result.sprite_sheet_col = new_animation_result.sprite_sheet_col + 1; - ret.status = anim_ms_agent_process_animation_result_status_t::Updated; - - if (new_animation_result.sprite_sheet_col <= section->start_col) { - // start animation - new_animation_result.sprite_sheet_col = section->start_col; - ret.status = anim_ms_agent_process_animation_result_status_t::Started; - } else if (new_animation_result.sprite_sheet_col == section->end_col) { - // last frame - new_animation_result.sprite_sheet_col = section->end_col; - ret.status = anim_ms_agent_process_animation_result_status_t::Updated; - } else if (new_animation_result.sprite_sheet_col > section->end_col) { - // don't loop at sleep, show last frame - if (new_state.row_state == animation_state_row_t::Sleep) { - // end animation - new_animation_result.sprite_sheet_col = section->end_col; - ret.status = anim_ms_agent_process_animation_result_status_t::End; - } else { - // loop animation - new_animation_result.sprite_sheet_col = section->start_col; - ret.status = anim_ms_agent_process_animation_result_status_t::Looped; - } - } - - } +enum class anim_ms_agent_process_animation_result_status_t : uint8_t { + None, + Started, + Updated, + End, + Looped, + NextAnimationStarted +}; +struct anim_ms_agent_process_animation_result_t { + animation_state_row_t row_state; + anim_ms_agent_process_animation_result_status_t status{anim_ms_agent_process_animation_result_status_t::None}; +}; +static anim_ms_agent_process_animation_result_t +anim_ms_agent_process_animation(animation_player_result_t& new_animation_result, animation_state_t& new_state, + [[maybe_unused]] const animation_state_t& current_state, + const ms_agent_sprite_sheet_t& current_frames) { + using namespace assets; + assert(MAX_ANIMATION_FRAMES > 0); + assert(MAX_ANIMATION_FRAMES <= INT_MAX); + + const ms_agent_sprite_sheet_animation_section_t *section = BONGOCAT_NULLPTR; + switch (new_state.row_state) { + case animation_state_row_t::Idle: + section = current_frames.idle.valid ? ¤t_frames.idle : BONGOCAT_NULLPTR; + break; + case animation_state_row_t::StartWriting: + section = current_frames.start_writing.valid ? ¤t_frames.start_writing : BONGOCAT_NULLPTR; + break; + case animation_state_row_t::Writing: + section = current_frames.writing.valid ? ¤t_frames.writing : BONGOCAT_NULLPTR; + break; + case animation_state_row_t::EndWriting: + section = current_frames.end_writing.valid ? ¤t_frames.end_writing : BONGOCAT_NULLPTR; + break; + case animation_state_row_t::Happy: + section = current_frames.happy.valid ? ¤t_frames.happy : BONGOCAT_NULLPTR; + break; + case animation_state_row_t::FallASleep: + // use sleep animation for ms agent + section = current_frames.sleep.valid ? ¤t_frames.sleep : BONGOCAT_NULLPTR; + break; + case animation_state_row_t::Sleep: + section = current_frames.sleep.valid ? ¤t_frames.sleep : BONGOCAT_NULLPTR; + break; + case animation_state_row_t::WakeUp: + section = current_frames.wake_up.valid ? ¤t_frames.wake_up : BONGOCAT_NULLPTR; + break; + case animation_state_row_t::Boring: + section = current_frames.boring.valid ? ¤t_frames.boring : BONGOCAT_NULLPTR; + break; + case animation_state_row_t::Test: + section = current_frames.writing.valid ? ¤t_frames.writing : BONGOCAT_NULLPTR; + break; + case animation_state_row_t::StartWorking: + section = current_frames.start_working.valid ? ¤t_frames.start_working : BONGOCAT_NULLPTR; + break; + case animation_state_row_t::Working: + section = current_frames.working.valid ? ¤t_frames.working : BONGOCAT_NULLPTR; + break; + case animation_state_row_t::EndWorking: + section = current_frames.end_working.valid ? ¤t_frames.end_working : BONGOCAT_NULLPTR; + break; + case animation_state_row_t::StartMoving: + section = current_frames.start_moving.valid ? ¤t_frames.start_moving : BONGOCAT_NULLPTR; + break; + case animation_state_row_t::Moving: + section = current_frames.moving.valid ? ¤t_frames.moving : BONGOCAT_NULLPTR; + break; + case animation_state_row_t::EndMoving: + section = current_frames.end_moving.valid ? ¤t_frames.end_moving : BONGOCAT_NULLPTR; + break; + case animation_state_row_t::StartRunning: + section = current_frames.start_running.valid ? ¤t_frames.start_running : BONGOCAT_NULLPTR; + break; + case animation_state_row_t::Running: + section = current_frames.running.valid ? ¤t_frames.running : BONGOCAT_NULLPTR; + break; + case animation_state_row_t::EndRunning: + section = current_frames.end_running.valid ? ¤t_frames.end_running : BONGOCAT_NULLPTR; + break; + } + + anim_ms_agent_process_animation_result_t ret{.row_state = new_state.row_state, + .status = anim_ms_agent_process_animation_result_status_t::None}; + if (section && section->valid) { + new_animation_result.sprite_sheet_row = section->row; + new_animation_result.sprite_sheet_col = new_animation_result.sprite_sheet_col + 1; + ret.status = anim_ms_agent_process_animation_result_status_t::Updated; + + if (new_animation_result.sprite_sheet_col <= section->start_col) { + // start animation + new_animation_result.sprite_sheet_col = section->start_col; + ret.status = anim_ms_agent_process_animation_result_status_t::Started; + } else if (new_animation_result.sprite_sheet_col == section->end_col) { + // last frame + new_animation_result.sprite_sheet_col = section->end_col; + ret.status = anim_ms_agent_process_animation_result_status_t::Updated; + } else if (new_animation_result.sprite_sheet_col > section->end_col) { + // don't loop at sleep, show last frame + if (new_state.row_state == animation_state_row_t::Sleep) { + // end animation + new_animation_result.sprite_sheet_col = section->end_col; + ret.status = anim_ms_agent_process_animation_result_status_t::End; + } else { + // loop animation + new_animation_result.sprite_sheet_col = section->start_col; + ret.status = anim_ms_agent_process_animation_result_status_t::Looped; + } + } + } - return ret; + return ret; +} +static anim_ms_agent_process_animation_result_t +anim_ms_agent_restart_animation([[maybe_unused]] animation_thread_context_t& ctx, animation_state_row_t new_row_state, + animation_player_result_t& new_animation_result, animation_state_t& new_state, + [[maybe_unused]] const animation_state_t& current_state, + const ms_agent_sprite_sheet_t& current_frames, + [[maybe_unused]] const config::config_t& current_config) { + using namespace assets; + assert(MAX_ANIMATION_FRAMES > 0); + assert(MAX_ANIMATION_FRAMES <= INT_MAX); + + const ms_agent_sprite_sheet_animation_section_t *section = BONGOCAT_NULLPTR; + switch (new_row_state) { + case animation_state_row_t::Idle: + section = current_frames.idle.valid ? ¤t_frames.idle : BONGOCAT_NULLPTR; + break; + case animation_state_row_t::StartWriting: + section = current_frames.start_writing.valid ? ¤t_frames.start_writing : BONGOCAT_NULLPTR; + break; + case animation_state_row_t::Writing: + section = current_frames.writing.valid ? ¤t_frames.writing : BONGOCAT_NULLPTR; + break; + case animation_state_row_t::EndWriting: + section = current_frames.end_writing.valid ? ¤t_frames.end_writing : BONGOCAT_NULLPTR; + break; + case animation_state_row_t::Happy: + section = current_frames.happy.valid ? ¤t_frames.happy : BONGOCAT_NULLPTR; + break; + case animation_state_row_t::FallASleep: + section = current_frames.sleep.valid ? ¤t_frames.sleep : BONGOCAT_NULLPTR; + break; + case animation_state_row_t::Sleep: + section = current_frames.sleep.valid ? ¤t_frames.sleep : BONGOCAT_NULLPTR; + break; + case animation_state_row_t::WakeUp: + section = current_frames.wake_up.valid ? ¤t_frames.wake_up : BONGOCAT_NULLPTR; + break; + case animation_state_row_t::Boring: + section = current_frames.boring.valid ? ¤t_frames.boring : BONGOCAT_NULLPTR; + break; + case animation_state_row_t::Test: + section = current_frames.writing.valid ? ¤t_frames.writing : BONGOCAT_NULLPTR; + break; + case animation_state_row_t::StartWorking: + section = current_frames.start_working.valid ? ¤t_frames.start_working : BONGOCAT_NULLPTR; + break; + case animation_state_row_t::Working: + section = current_frames.working.valid ? ¤t_frames.working : BONGOCAT_NULLPTR; + break; + case animation_state_row_t::EndWorking: + section = current_frames.end_working.valid ? ¤t_frames.end_working : BONGOCAT_NULLPTR; + break; + case animation_state_row_t::StartMoving: + section = current_frames.start_moving.valid ? ¤t_frames.start_moving : BONGOCAT_NULLPTR; + break; + case animation_state_row_t::Moving: + section = current_frames.moving.valid ? ¤t_frames.moving : BONGOCAT_NULLPTR; + break; + case animation_state_row_t::EndMoving: + section = current_frames.end_moving.valid ? ¤t_frames.end_moving : BONGOCAT_NULLPTR; + break; + case animation_state_row_t::StartRunning: + section = current_frames.start_running.valid ? ¤t_frames.start_running : BONGOCAT_NULLPTR; + break; + case animation_state_row_t::Running: + section = current_frames.running.valid ? ¤t_frames.running : BONGOCAT_NULLPTR; + break; + case animation_state_row_t::EndRunning: + section = current_frames.end_running.valid ? ¤t_frames.end_running : BONGOCAT_NULLPTR; + break; + } + + anim_ms_agent_process_animation_result_t ret{.row_state = new_state.row_state, + .status = anim_ms_agent_process_animation_result_status_t::None}; + if (section && section->valid) { + new_state.row_state = new_row_state; + new_animation_result.sprite_sheet_row = section->row; + new_animation_result.sprite_sheet_col = section->start_col; + if (new_state.row_state == animation_state_row_t::Idle) { + if (current_config.idle_frame) { + new_animation_result.sprite_sheet_col = current_config.idle_frame; + } } - static anim_ms_agent_process_animation_result_t anim_ms_agent_restart_animation([[maybe_unused]] animation_context_t& ctx, - animation_state_row_t new_row_state, - animation_player_result_t& new_animation_result, - animation_state_t& new_state, - [[maybe_unused]] const animation_state_t& current_state, - const ms_agent_sprite_sheet_t& current_frames, - [[maybe_unused]] const config::config_t& current_config) { - using namespace assets; - assert(MAX_ANIMATION_FRAMES > 0); - assert(MAX_ANIMATION_FRAMES <= INT_MAX); - - const ms_agent_sprite_sheet_animation_section_t *section = nullptr; - switch (new_row_state) { - case animation_state_row_t::Idle: - section = current_frames.idle.valid ? ¤t_frames.idle : nullptr; - break; - case animation_state_row_t::StartWriting: - section = current_frames.start_writing.valid ? ¤t_frames.start_writing : nullptr; - break; - case animation_state_row_t::Writing: - section = current_frames.writing.valid ? ¤t_frames.writing : nullptr; - break; - case animation_state_row_t::EndWriting: - section = current_frames.end_writing.valid ? ¤t_frames.end_writing : nullptr; - break; - case animation_state_row_t::Happy: - section = current_frames.happy.valid ? ¤t_frames.happy : nullptr; - break; - case animation_state_row_t::FallASleep: - section = current_frames.sleep.valid ? ¤t_frames.sleep : nullptr; - break; - case animation_state_row_t::Sleep: - section = current_frames.sleep.valid ? ¤t_frames.sleep : nullptr; - break; - case animation_state_row_t::WakeUp: - section = current_frames.wake_up.valid ? ¤t_frames.wake_up : nullptr; - break; - case animation_state_row_t::Boring: - section = current_frames.boring.valid ? ¤t_frames.boring : nullptr; - break; - case animation_state_row_t::Test: - section = current_frames.writing.valid ? ¤t_frames.writing : nullptr; - break; - case animation_state_row_t::StartWorking: - section = current_frames.start_working.valid ? ¤t_frames.start_working : nullptr; - break; - case animation_state_row_t::Working: - section = current_frames.working.valid ? ¤t_frames.working : nullptr; - break; - case animation_state_row_t::EndWorking: - section = current_frames.end_working.valid ? ¤t_frames.end_working : nullptr; - break; - case animation_state_row_t::StartMoving: - section = current_frames.start_moving.valid ? ¤t_frames.start_moving : nullptr; - break; - case animation_state_row_t::Moving: - section = current_frames.moving.valid ? ¤t_frames.moving : nullptr; - break; - case animation_state_row_t::EndMoving: - section = current_frames.end_moving.valid ? ¤t_frames.end_moving : nullptr; - break; - case animation_state_row_t::StartRunning: - section = current_frames.start_running.valid ? ¤t_frames.start_running : nullptr; - break; - case animation_state_row_t::Running: - section = current_frames.running.valid ? ¤t_frames.running : nullptr; - break; - case animation_state_row_t::EndRunning: - section = current_frames.end_running.valid ? ¤t_frames.end_running : nullptr; - break; - } + ret.row_state = new_state.row_state; + ret.status = anim_ms_agent_process_animation_result_status_t::Started; + } - anim_ms_agent_process_animation_result_t ret {.row_state = new_state.row_state, .status = anim_ms_agent_process_animation_result_status_t::None}; - if (section && section->valid) { - new_state.row_state = new_row_state; - new_animation_result.sprite_sheet_row = section->row; - new_animation_result.sprite_sheet_col = section->start_col; - if (new_state.row_state == animation_state_row_t::Idle) { - if (current_config.idle_frame) { - new_animation_result.sprite_sheet_col = current_config.idle_frame; - } - } - ret.row_state = new_state.row_state; - ret.status = anim_ms_agent_process_animation_result_status_t::Started; - } + return ret; +} - return ret; +static anim_ms_agent_process_animation_result_t +anim_ms_agent_start_or_process_animation(animation_thread_context_t& ctx, animation_state_row_t new_row_state, + animation_player_result_t& new_animation_result, animation_state_t& new_state, + const animation_state_t& current_state, + const ms_agent_sprite_sheet_t& current_frames, + const config::config_t& current_config) { + if (current_state.row_state != new_row_state) { + auto result = anim_ms_agent_process_animation(new_animation_result, new_state, current_state, current_frames); + if (result.status == anim_ms_agent_process_animation_result_status_t::Looped || + result.status == anim_ms_agent_process_animation_result_status_t::End) { + result = anim_ms_agent_restart_animation(ctx, new_row_state, new_animation_result, new_state, current_state, + current_frames, current_config); + result.status = anim_ms_agent_process_animation_result_status_t::NextAnimationStarted; } + return result; + } - static anim_ms_agent_process_animation_result_t anim_ms_agent_start_or_process_animation(animation_context_t& ctx, - animation_state_row_t new_row_state, - animation_player_result_t& new_animation_result, - animation_state_t& new_state, - const animation_state_t& current_state, - const ms_agent_sprite_sheet_t& current_frames, - const config::config_t& current_config) { - - if (current_state.row_state != new_row_state) { - auto result = anim_ms_agent_process_animation(new_animation_result, new_state, current_state, current_frames); - if (result.status == anim_ms_agent_process_animation_result_status_t::Looped || result.status == anim_ms_agent_process_animation_result_status_t::End) { - result = anim_ms_agent_restart_animation(ctx, new_row_state, new_animation_result, new_state, - current_state, current_frames, current_config); - result.status = anim_ms_agent_process_animation_result_status_t::NextAnimationStarted; - } - return result; - } + return anim_ms_agent_restart_animation(ctx, new_row_state, new_animation_result, new_state, current_state, + current_frames, current_config); +} - return anim_ms_agent_restart_animation(ctx, new_row_state, new_animation_result, new_state, - current_state, current_frames, current_config); +static anim_next_frame_result_t +anim_ms_agent_idle_next_frame(animation_thread_context_t& ctx, const platform::input::input_context_t& input, + [[maybe_unused]] const platform::update::update_context_t& upd, animation_state_t& state, + const anim_handle_key_press_result_t& trigger_result) { + using namespace assets; + + // read-only config + assert(ctx._local_copy_config); + const config::config_t& current_config = *ctx._local_copy_config; + + assert(ctx.shm); + assert(input.shm); + // assert(upd.shm); + animation_shared_memory_t& anim_shm = *ctx.shm; + const auto& input_shm = *input.shm; + // const auto& update_shm = *upd.shm; + const auto current_state = state; + const auto& current_animation_result = anim_shm.animation_player_result; + [[maybe_unused]] const int anim_index = anim_shm.anim_index; + const platform::timestamp_ms_t last_key_pressed_timestamp = input_shm.last_key_pressed_timestamp; + assert(get_current_animation(ctx).type == animation_t::type_t::MsAgent); + const auto& current_frames = get_current_animation(ctx).ms_agent; + + auto new_animation_result = anim_shm.animation_player_result; + auto new_state = state; + + const auto conditions = get_anim_conditions(ctx, input, upd, current_state, trigger_result.trigger, current_config); + + /// @TODO: make animation fsm + + const platform::timestamp_ms_t now = platform::get_current_time_ms(); + const platform::time_ms_t idle_sleep_timeout_ms = current_config.idle_sleep_timeout_sec * 1000; + assert(now >= last_key_pressed_timestamp); + const auto sleep_timeout = now - last_key_pressed_timestamp; + + const bool start_boring = SLEEP_BORING_PART > 0 && sleep_timeout >= idle_sleep_timeout_ms / SLEEP_BORING_PART; + + switch (current_state.row_state) { + case animation_state_row_t::Test: + case animation_state_row_t::Happy: + case animation_state_row_t::StartMoving: + case animation_state_row_t::Moving: + case animation_state_row_t::EndMoving: + case animation_state_row_t::StartRunning: + case animation_state_row_t::Running: + case animation_state_row_t::EndRunning: + // not supported, same as idle + break; + case animation_state_row_t::StartWorking: + case animation_state_row_t::Working: + case animation_state_row_t::EndWorking: + // @TODO: add working (CPU state) animation + break; + case animation_state_row_t::Idle: { + if (current_config.idle_animation && conditions.go_next_frame) { + anim_ms_agent_process_animation(new_animation_result, new_state, current_state, current_frames); } - static anim_next_frame_result_t anim_ms_agent_idle_next_frame(animation_context_t& ctx, const platform::input::input_context_t& input, - [[maybe_unused]] const platform::update::update_context_t& upd, - animation_state_t& state, const anim_handle_key_press_result_t& trigger_result) { - using namespace assets; - - // read-only config - assert(ctx._local_copy_config != nullptr); - const config::config_t& current_config = *ctx._local_copy_config; - - assert(ctx.shm != nullptr); - assert(input.shm != nullptr); - //assert(upd.shm != nullptr); - animation_shared_memory_t& anim_shm = *ctx.shm; - const auto& input_shm = *input.shm; - //const auto& update_shm = *upd.shm; - const auto current_state = state; - const auto& current_animation_result = anim_shm.animation_player_result; - [[maybe_unused]] const int anim_index = anim_shm.anim_index; - const platform::timestamp_ms_t last_key_pressed_timestamp = input_shm.last_key_pressed_timestamp; - assert(get_current_animation(ctx).type == animation_t::Type::MsAgent); - const auto& current_frames = get_current_animation(ctx).ms_agent; - - auto new_animation_result = anim_shm.animation_player_result; - auto new_state = state; - - const auto conditions = get_anim_conditions(ctx, input, upd, current_state, trigger_result.trigger, current_config); - - /// @TODO: make animation fsm - - const platform::timestamp_ms_t now = platform::get_current_time_ms(); - const platform::time_ms_t idle_sleep_timeout_ms = current_config.idle_sleep_timeout_sec*1000; - assert(now >= last_key_pressed_timestamp); - const auto sleep_timeout = now - last_key_pressed_timestamp; - - const bool start_boring = SLEEP_BORING_PART > 0 && sleep_timeout >= idle_sleep_timeout_ms/SLEEP_BORING_PART; - - switch (current_state.row_state) { - case animation_state_row_t::Test: - case animation_state_row_t::Happy: - case animation_state_row_t::StartMoving: - case animation_state_row_t::Moving: - case animation_state_row_t::EndMoving: - case animation_state_row_t::StartRunning: - case animation_state_row_t::Running: - case animation_state_row_t::EndRunning: - // not supported, same as idle - break; - case animation_state_row_t::StartWorking: - case animation_state_row_t::Working: - case animation_state_row_t::EndWorking: - // @TODO: add working (CPU state) animation - break; - case animation_state_row_t::Idle: { - if (current_config.idle_animation && conditions.go_next_frame) { - anim_ms_agent_process_animation(new_animation_result, new_state, current_state, current_frames); - } - - // handle sleep - const bool is_sleeping_time = current_config.enable_scheduled_sleep && is_sleep_time(current_config); - - // Idle Sleep - if (conditions.check_for_idle_sleep) { - if (!is_sleeping_time) { - if (current_state.row_state == animation_state_row_t::Idle) { - // start boring animation - if (start_boring && !current_state.show_boring_animation_once) { - anim_ms_agent_restart_animation(ctx, animation_state_row_t::Boring, - new_animation_result, new_state, - current_state, current_frames, current_config); - new_state.show_boring_animation_once = true; - new_state.anim_last_direction = 0.0f; - } - } - - // idle sleep - if (sleep_timeout >= idle_sleep_timeout_ms) { - if (current_state.row_state == animation_state_row_t::Idle || conditions.is_moving) { - anim_ms_agent_process_animation_result_t animation_result { .row_state = current_state.row_state }; - if (conditions.is_moving) { - animation_result = anim_ms_agent_start_or_process_animation(ctx, animation_state_row_t::Sleep, // wait for moving to end - new_animation_result, new_state, - current_state, current_frames, current_config); - } else { - animation_result = anim_ms_agent_restart_animation(ctx, animation_state_row_t::Sleep, - new_animation_result, new_state, - current_state, current_frames, current_config); - } - if (animation_result.row_state == animation_state_row_t::Sleep) { - new_state.anim_last_direction = 0.0f; - new_state.is_idle_sleep = true; - new_state.show_boring_animation_once = false; - } - } else if (current_state.row_state == animation_state_row_t::Sleep) { - if (conditions.go_next_frame) { - // process sleep animation - anim_ms_agent_process_animation(new_animation_result, new_state, current_state, current_frames); - } - } - } else { - if (current_state.row_state == animation_state_row_t::Sleep && current_state.is_idle_sleep) { - // wake up - if (conditions.go_next_frame) { - // end current sleep animation - const auto animation_result = anim_ms_agent_start_or_process_animation(ctx, animation_state_row_t::WakeUp, - new_animation_result, new_state, - current_state, current_frames, current_config); - if (animation_result.row_state == animation_state_row_t::WakeUp) { - new_state.is_idle_sleep = false; - } - } - } - } - } - } + // handle sleep + const bool is_sleeping_time = current_config.enable_scheduled_sleep && is_sleep_time(current_config); - // Sleep Mode - if (current_config.enable_scheduled_sleep) { - if (is_sleeping_time) { - if (new_state.row_state == animation_state_row_t::Idle) { - anim_ms_agent_restart_animation(ctx, animation_state_row_t::Sleep, - new_animation_result, new_state, - current_state, current_frames, current_config); - new_state.anim_last_direction = 0.0f; - new_state.is_idle_sleep = false; - } else if (state.row_state == animation_state_row_t::Sleep && !new_state.is_idle_sleep) { - if (conditions.go_next_frame) { - anim_ms_agent_process_animation(new_animation_result, new_state, current_state, current_frames); - } - } - } else { - if (current_state.row_state == animation_state_row_t::Sleep && !new_state.is_idle_sleep) { - // wake up - if (conditions.go_next_frame) { - // end current sleep animation - anim_ms_agent_start_or_process_animation(ctx, animation_state_row_t::WakeUp, - new_animation_result, new_state, - current_state, current_frames, current_config); - } - } - } - } else { - if (current_state.row_state == animation_state_row_t::Sleep && !new_state.is_idle_sleep) { - // wake up - if (conditions.go_next_frame) { - // end current sleep animation - anim_ms_agent_start_or_process_animation(ctx, animation_state_row_t::WakeUp, - new_animation_result, new_state, - current_state, current_frames, current_config); - } - } - } - }break; - case animation_state_row_t::Writing: { - if (conditions.continue_writing) { - if (conditions.go_next_frame) { - // loop writing animation - anim_ms_agent_process_animation(new_animation_result, new_state, current_state, current_frames); - } - } else { - if (conditions.release_frame_after_press) { - // cancel writing - anim_ms_agent_start_or_process_animation(ctx, animation_state_row_t::EndWriting, - new_animation_result, new_state, - current_state, current_frames, current_config); - } - } - }break; - case animation_state_row_t::StartWriting: - if (conditions.go_next_frame) { - const auto animation_result = anim_ms_agent_start_or_process_animation(ctx, animation_state_row_t::Writing, - new_animation_result, new_state, - current_state, current_frames, current_config); - if (animation_result.row_state == animation_state_row_t::Writing) { - // reset release counter after writing is started (for real) - new_state.hold_frame_after_release = true; - new_state.hold_frame_ms = 0; - } - } - break; - case animation_state_row_t::EndWriting: - if (conditions.go_next_frame) { - // finish animation - anim_ms_agent_start_or_process_animation(ctx, animation_state_row_t::Idle, // back to idle - new_animation_result, new_state, - current_state, current_frames, current_config); - } - break; - case animation_state_row_t::FallASleep: - if (conditions.go_next_frame) { - // finish animation - anim_ms_agent_start_or_process_animation(ctx, animation_state_row_t::Sleep, - new_animation_result, new_state, - current_state, current_frames, current_config); - } - break; - case animation_state_row_t::Sleep: - if (conditions.go_next_frame) { - anim_ms_agent_process_animation(new_animation_result, new_state, current_state, current_frames); - } - break; - case animation_state_row_t::WakeUp: - if (conditions.go_next_frame) { - // finish animation - anim_ms_agent_start_or_process_animation(ctx, animation_state_row_t::Idle, // back to idle - new_animation_result, new_state, - current_state, current_frames, current_config); - } - break; - case animation_state_row_t::Boring: - if (conditions.go_next_frame) { - anim_ms_agent_start_or_process_animation(ctx, animation_state_row_t::Idle, // back to idle, when animation ended - new_animation_result, new_state, - current_state, current_frames, current_config); - if (!start_boring) { - new_state.show_boring_animation_once = false; - } - } - break; + // Idle Sleep + if (conditions.check_for_idle_sleep) { + if (!is_sleeping_time) { + if (current_state.row_state == animation_state_row_t::Idle) { + // start boring animation + if (start_boring && !current_state.show_boring_animation_once) { + anim_ms_agent_restart_animation(ctx, animation_state_row_t::Boring, new_animation_result, new_state, + current_state, current_frames, current_config); + new_state.show_boring_animation_once = true; + new_state.anim_last_direction = 0.0f; + } } - return anim_update_animation_state(anim_shm, state, - new_animation_result, new_state, - current_animation_result, current_state, - current_config); + // idle sleep + if (sleep_timeout >= idle_sleep_timeout_ms) { + if (current_state.row_state == animation_state_row_t::Idle || conditions.is_moving) { + anim_ms_agent_process_animation_result_t animation_result{.row_state = current_state.row_state}; + if (conditions.is_moving) { + animation_result = anim_ms_agent_start_or_process_animation( + ctx, animation_state_row_t::Sleep, // wait for moving to end + new_animation_result, new_state, current_state, current_frames, current_config); + } else { + animation_result = + anim_ms_agent_restart_animation(ctx, animation_state_row_t::Sleep, new_animation_result, new_state, + current_state, current_frames, current_config); + } + if (animation_result.row_state == animation_state_row_t::Sleep) { + new_state.anim_last_direction = 0.0f; + new_state.is_idle_sleep = true; + new_state.show_boring_animation_once = false; + } + } else if (current_state.row_state == animation_state_row_t::Sleep) { + if (conditions.go_next_frame) { + // process sleep animation + anim_ms_agent_process_animation(new_animation_result, new_state, current_state, current_frames); + } + } + } else { + if (current_state.row_state == animation_state_row_t::Sleep && current_state.is_idle_sleep) { + // wake up + if (conditions.go_next_frame) { + // end current sleep animation + const auto animation_result = + anim_ms_agent_start_or_process_animation(ctx, animation_state_row_t::WakeUp, new_animation_result, + new_state, current_state, current_frames, current_config); + if (animation_result.row_state == animation_state_row_t::WakeUp) { + new_state.is_idle_sleep = false; + } + } + } + } + } } - static anim_next_frame_result_t anim_ms_agent_key_pressed_next_frame(animation_context_t& ctx, - animation_state_t& state, - [[maybe_unused]] const platform::input::input_context_t& input, - [[maybe_unused]] const platform::update::update_context_t& upd, - [[maybe_unused]] const animation_trigger_t& trigger) { - using namespace assets; - - // read-only config - assert(ctx._local_copy_config != nullptr); - const config::config_t& current_config = *ctx._local_copy_config; - - assert(ctx.shm != nullptr); - assert(input.shm != nullptr); - animation_shared_memory_t& anim_shm = *ctx.shm; - //const auto& input_shm = *input.shm; - const auto current_state = state; - const auto& current_animation_result = anim_shm.animation_player_result; - [[maybe_unused]] const int anim_index = anim_shm.anim_index; - assert(get_current_animation(ctx).type == animation_t::Type::MsAgent); - const auto& current_frames = get_current_animation(ctx).ms_agent; - - auto new_animation_result = anim_shm.animation_player_result; - auto new_state = state; - - //const auto conditions = get_anim_conditions(ctx, input, current_state, trigger, current_config); - - // in Writing mode/start writing - switch (state.row_state) { - case animation_state_row_t::StartMoving: - case animation_state_row_t::Moving: - case animation_state_row_t::EndMoving: - case animation_state_row_t::StartRunning: - case animation_state_row_t::Running: - case animation_state_row_t::EndRunning: - // moving not supported - case animation_state_row_t::Test: - case animation_state_row_t::Happy: - case animation_state_row_t::Idle: - anim_ms_agent_restart_animation(ctx, animation_state_row_t::StartWriting, - new_animation_result, new_state, - current_state, current_frames, current_config); - break; - case animation_state_row_t::StartWriting: - // start end writing and process animation in anim_ms_agent_idle_next_frame - break; - case animation_state_row_t::Writing: - // reset hold frame so we can continue writing - new_state.hold_frame_ms = 0; - // start end writing and process animation in anim_ms_agent_idle_next_frame - break; - case animation_state_row_t::EndWriting: - // start end writing and process animation in anim_ms_agent_idle_next_frame - break; - case animation_state_row_t::FallASleep: - // process animation in anim_ms_agent_idle_next_frame - break; - case animation_state_row_t::Sleep: - if (current_state.is_idle_sleep) { - // wake up, end current sleep animation - anim_ms_agent_start_or_process_animation(ctx, animation_state_row_t::WakeUp, - new_animation_result, new_state, - current_state, current_frames, current_config); - new_state.is_idle_sleep = false; - } - case animation_state_row_t::WakeUp: - // process animation in anim_ms_agent_idle_next_frame - break; - case animation_state_row_t::Boring: - // process animation in anim_ms_agent_idle_next_frame - break; - case animation_state_row_t::StartWorking: - case animation_state_row_t::Working: - case animation_state_row_t::EndWorking: - // start end writing and process animation in anim_ms_agent_idle_next_frame - break; + // Sleep Mode + if (current_config.enable_scheduled_sleep) { + if (is_sleeping_time) { + if (new_state.row_state == animation_state_row_t::Idle) { + anim_ms_agent_restart_animation(ctx, animation_state_row_t::Sleep, new_animation_result, new_state, + current_state, current_frames, current_config); + new_state.anim_last_direction = 0.0f; + new_state.is_idle_sleep = false; + } else if (state.row_state == animation_state_row_t::Sleep && !new_state.is_idle_sleep) { + if (conditions.go_next_frame) { + anim_ms_agent_process_animation(new_animation_result, new_state, current_state, current_frames); + } } - - return anim_update_animation_state(anim_shm, state, - new_animation_result, new_state, - current_animation_result, current_state, - current_config); + } else { + if (current_state.row_state == animation_state_row_t::Sleep && !new_state.is_idle_sleep) { + // wake up + if (conditions.go_next_frame) { + // end current sleep animation + anim_ms_agent_start_or_process_animation(ctx, animation_state_row_t::WakeUp, new_animation_result, + new_state, current_state, current_frames, current_config); + } + } + } + } else { + if (current_state.row_state == animation_state_row_t::Sleep && !new_state.is_idle_sleep) { + // wake up + if (conditions.go_next_frame) { + // end current sleep animation + anim_ms_agent_start_or_process_animation(ctx, animation_state_row_t::WakeUp, new_animation_result, new_state, + current_state, current_frames, current_config); + } + } + } + } break; + case animation_state_row_t::Writing: { + if (conditions.continue_writing) { + if (conditions.go_next_frame) { + // loop writing animation + anim_ms_agent_process_animation(new_animation_result, new_state, current_state, current_frames); + } + } else { + if (conditions.release_frame_after_press) { + // cancel writing + anim_ms_agent_start_or_process_animation(ctx, animation_state_row_t::EndWriting, new_animation_result, + new_state, current_state, current_frames, current_config); + } + } + } break; + case animation_state_row_t::StartWriting: + if (conditions.go_next_frame) { + const auto animation_result = + anim_ms_agent_start_or_process_animation(ctx, animation_state_row_t::Writing, new_animation_result, new_state, + current_state, current_frames, current_config); + if (animation_result.row_state == animation_state_row_t::Writing) { + // reset release counter after writing is started (for real) + new_state.hold_frame_after_release = true; + new_state.hold_frame_ms = 0; + } + } + break; + case animation_state_row_t::EndWriting: + if (conditions.go_next_frame) { + // finish animation + anim_ms_agent_start_or_process_animation(ctx, animation_state_row_t::Idle, // back to idle + new_animation_result, new_state, current_state, current_frames, + current_config); + } + break; + case animation_state_row_t::FallASleep: + if (conditions.go_next_frame) { + // finish animation + anim_ms_agent_start_or_process_animation(ctx, animation_state_row_t::Sleep, new_animation_result, new_state, + current_state, current_frames, current_config); + } + break; + case animation_state_row_t::Sleep: + if (conditions.go_next_frame) { + anim_ms_agent_process_animation(new_animation_result, new_state, current_state, current_frames); + } + break; + case animation_state_row_t::WakeUp: + if (conditions.go_next_frame) { + // finish animation + anim_ms_agent_start_or_process_animation(ctx, animation_state_row_t::Idle, // back to idle + new_animation_result, new_state, current_state, current_frames, + current_config); + } + break; + case animation_state_row_t::Boring: + if (conditions.go_next_frame) { + anim_ms_agent_start_or_process_animation(ctx, animation_state_row_t::Idle, // back to idle, when animation ended + new_animation_result, new_state, current_state, current_frames, + current_config); + if (!start_boring) { + new_state.show_boring_animation_once = false; + } } + break; + } + return anim_update_animation_state(anim_shm, state, new_animation_result, new_state, current_animation_result, + current_state, current_config); +} + +static anim_next_frame_result_t +anim_ms_agent_key_pressed_next_frame(animation_thread_context_t& ctx, animation_state_t& state, + [[maybe_unused]] const platform::input::input_context_t& input, + [[maybe_unused]] const platform::update::update_context_t& upd, + [[maybe_unused]] const animation_trigger_t& trigger) { + using namespace assets; + + // read-only config + assert(ctx._local_copy_config); + const config::config_t& current_config = *ctx._local_copy_config; + + assert(ctx.shm); + assert(input.shm); + animation_shared_memory_t& anim_shm = *ctx.shm; + // const auto& input_shm = *input.shm; + const auto current_state = state; + const auto& current_animation_result = anim_shm.animation_player_result; + [[maybe_unused]] const int anim_index = anim_shm.anim_index; + assert(get_current_animation(ctx).type == animation_t::type_t::MsAgent); + const auto& current_frames = get_current_animation(ctx).ms_agent; + + auto new_animation_result = anim_shm.animation_player_result; + auto new_state = state; + + // const auto conditions = get_anim_conditions(ctx, input, current_state, trigger, current_config); + + // in Writing mode/start writing + switch (state.row_state) { + case animation_state_row_t::StartMoving: + case animation_state_row_t::Moving: + case animation_state_row_t::EndMoving: + case animation_state_row_t::StartRunning: + case animation_state_row_t::Running: + case animation_state_row_t::EndRunning: + // moving not supported + case animation_state_row_t::Test: + case animation_state_row_t::Happy: + case animation_state_row_t::Idle: + anim_ms_agent_restart_animation(ctx, animation_state_row_t::StartWriting, new_animation_result, new_state, + current_state, current_frames, current_config); + break; + case animation_state_row_t::StartWriting: + // start end writing and process animation in anim_ms_agent_idle_next_frame + break; + case animation_state_row_t::Writing: + // reset hold frame so we can continue writing + new_state.hold_frame_ms = 0; + // start end writing and process animation in anim_ms_agent_idle_next_frame + break; + case animation_state_row_t::EndWriting: + // start end writing and process animation in anim_ms_agent_idle_next_frame + break; + case animation_state_row_t::FallASleep: + // process animation in anim_ms_agent_idle_next_frame + break; + case animation_state_row_t::Sleep: + if (current_state.is_idle_sleep) { + // wake up, end current sleep animation + anim_ms_agent_start_or_process_animation(ctx, animation_state_row_t::WakeUp, new_animation_result, new_state, + current_state, current_frames, current_config); + new_state.is_idle_sleep = false; + } + case animation_state_row_t::WakeUp: + // process animation in anim_ms_agent_idle_next_frame + break; + case animation_state_row_t::Boring: + // process animation in anim_ms_agent_idle_next_frame + break; + case animation_state_row_t::StartWorking: + case animation_state_row_t::Working: + case animation_state_row_t::EndWorking: + // start end writing and process animation in anim_ms_agent_idle_next_frame + break; + } + + return anim_update_animation_state(anim_shm, state, new_animation_result, new_state, current_animation_result, + current_state, current_config); +} - /* - static anim_next_frame_result_t anim_ms_agent_working_next_frame(animation_context_t& ctx, const platform::update::update_context_t& upd, - animation_state_t& state) { - using namespace assets; +/* +static anim_next_frame_result_t anim_ms_agent_working_next_frame(animation_context_t& ctx, const +platform::update::update_context_t& upd, animation_state_t& state) { using namespace assets; - // read-only config - assert(ctx._local_copy_config != nullptr); - const config::config_t& current_config = *ctx._local_copy_config; + // read-only config + assert(ctx._local_copy_config != nullptr); + const config::config_t& current_config = *ctx._local_copy_config; - assert(ctx.shm != nullptr); - assert(upd.shm != nullptr); - //assert(input.shm != nullptr); - animation_shared_memory_t& anim_shm = *ctx.shm; - //const auto& input_shm = *input.shm; - const auto& update_shm = *upd.shm; - const auto current_state = state; - const auto& current_animation_result = anim_shm.animation_player_result; - [[maybe_unused]] const int anim_index = anim_shm.anim_index; - //const platform::timestamp_ms_t last_key_pressed_timestamp = input_shm.last_key_pressed_timestamp; - assert(get_current_animation(ctx).type == animation_t::Type::MsAgent); - const auto& current_frames = get_current_animation(ctx).ms_agent; + assert(ctx.shm != nullptr); + assert(upd.shm != nullptr); + //assert(input.shm != nullptr); + animation_shared_memory_t& anim_shm = *ctx.shm; + //const auto& input_shm = *input.shm; + const auto& update_shm = *upd.shm; + const auto current_state = state; + const auto& current_animation_result = anim_shm.animation_player_result; + [[maybe_unused]] const int anim_index = anim_shm.anim_index; + //const platform::timestamp_ms_t last_key_pressed_timestamp = input_shm.last_key_pressed_timestamp; + assert(get_current_animation(ctx).type == animation_t::type_t::MsAgent); + const auto& current_frames = get_current_animation(ctx).ms_agent; - auto new_animation_result = anim_shm.animation_player_result; - auto new_state = state; + auto new_animation_result = anim_shm.animation_player_result; + auto new_state = state; - assert(anim_shm.anim_type == config::config_animation_sprite_sheet_layout_t::MsAgent); + assert(anim_shm.anim_type == config::config_animation_sprite_sheet_layout_t::MsAgent); - /// @TODO: use state machine for animation (states) + /// @TODO: use state machine for animation (states) - return anim_update_animation_state(anim_shm, state, - new_animation_result, new_state, - current_animation_result, current_state, - current_config); - } - */ + return anim_update_animation_state(anim_shm, state, + new_animation_result, new_state, + current_animation_result, current_state, + current_config); +} +*/ #endif #ifdef FEATURE_CUSTOM_SPRITE_SHEETS_ANIMATION - enum class anim_custom_process_animation_result_status_t : uint8_t { None, Started, Updated, End, Looped, NextAnimationStarted, SkipMovement, Moved, Stop }; - struct anim_custom_process_animation_result_t { - animation_state_row_t row_state; - anim_custom_process_animation_result_status_t status{anim_custom_process_animation_result_status_t::None}; - }; - static anim_custom_process_animation_result_t anim_custom_process_animation(animation_player_result_t& new_animation_result, - animation_state_t& new_state, - [[maybe_unused]] const animation_state_t& current_state, - const custom_sprite_sheet_t& current_frames) { - using namespace assets; - - const custom_sprite_sheet_animation_section_t *section = nullptr; - switch (new_state.row_state) { - case animation_state_row_t::Idle: - section = current_frames.idle.valid ? ¤t_frames.idle : nullptr; - break; - case animation_state_row_t::StartWriting: - section = current_frames.start_writing.valid ? ¤t_frames.start_writing : nullptr; - break; - case animation_state_row_t::Writing: - section = current_frames.writing.valid ? ¤t_frames.writing : nullptr; - break; - case animation_state_row_t::EndWriting: - section = current_frames.end_writing.valid ? ¤t_frames.end_writing : nullptr; - break; - case animation_state_row_t::Happy: - section = current_frames.happy.valid ? ¤t_frames.happy : nullptr; - break; - case animation_state_row_t::FallASleep: - section = current_frames.fall_asleep.valid ? ¤t_frames.fall_asleep : nullptr; - break; - case animation_state_row_t::Sleep: - section = current_frames.sleep.valid ? ¤t_frames.sleep : nullptr; - break; - case animation_state_row_t::WakeUp: - section = current_frames.wake_up.valid ? ¤t_frames.wake_up : nullptr; - break; - case animation_state_row_t::Boring: - section = current_frames.boring.valid ? ¤t_frames.boring : nullptr; - break; - case animation_state_row_t::Test: - section = current_frames.writing.valid ? ¤t_frames.writing : nullptr; - break; - case animation_state_row_t::StartWorking: - section = current_frames.start_working.valid ? ¤t_frames.start_working : nullptr; - break; - case animation_state_row_t::Working: - section = current_frames.working.valid ? ¤t_frames.working : nullptr; - break; - case animation_state_row_t::EndWorking: - section = current_frames.end_working.valid ? ¤t_frames.end_working : nullptr; - break; - case animation_state_row_t::StartMoving: - section = current_frames.start_moving.valid ? ¤t_frames.start_moving : nullptr; - break; - case animation_state_row_t::Moving: - section = current_frames.moving.valid ? ¤t_frames.moving : nullptr; - break; - case animation_state_row_t::EndMoving: - section = current_frames.end_moving.valid ? ¤t_frames.end_moving : nullptr; - break; - case animation_state_row_t::StartRunning: - section = current_frames.start_running.valid ? ¤t_frames.start_running : nullptr; - break; - case animation_state_row_t::Running: - section = current_frames.running.valid ? ¤t_frames.running : nullptr; - break; - case animation_state_row_t::EndRunning: - section = current_frames.end_running.valid ? ¤t_frames.end_running : nullptr; - break; - } - - anim_custom_process_animation_result_t ret {.row_state = new_state.row_state, .status = anim_custom_process_animation_result_status_t::None}; - if (section && section->valid) { - new_animation_result.sprite_sheet_row = section->row; - new_animation_result.sprite_sheet_col = new_animation_result.sprite_sheet_col + 1; - ret.status = anim_custom_process_animation_result_status_t::Updated; - - if (new_animation_result.sprite_sheet_col <= section->start_col) { - // start animation - new_animation_result.sprite_sheet_col = section->start_col; - ret.status = anim_custom_process_animation_result_status_t::Started; - } else if (new_animation_result.sprite_sheet_col == section->end_col) { - // last frame - new_animation_result.sprite_sheet_col = section->end_col; - ret.status = anim_custom_process_animation_result_status_t::Updated; - } else if (new_animation_result.sprite_sheet_col > section->end_col) { - // loop animation - new_animation_result.sprite_sheet_col = section->start_col; - ret.status = anim_custom_process_animation_result_status_t::Looped; - } - } - - return ret; - } - static anim_custom_process_animation_result_t anim_custom_restart_animation([[maybe_unused]] animation_context_t& ctx, - animation_state_row_t new_row_state, - animation_player_result_t& new_animation_result, - animation_state_t& new_state, - [[maybe_unused]] const animation_state_t& current_state, - const custom_sprite_sheet_t& current_frames, - [[maybe_unused]] const config::config_t& current_config) { - using namespace assets; - - const custom_sprite_sheet_animation_section_t *section = nullptr; - switch (new_row_state) { - case animation_state_row_t::Idle: - section = current_frames.idle.valid ? ¤t_frames.idle : nullptr; - break; - case animation_state_row_t::StartWriting: - section = current_frames.start_writing.valid ? ¤t_frames.start_writing : nullptr; - break; - case animation_state_row_t::Writing: - section = current_frames.writing.valid ? ¤t_frames.writing : nullptr; - break; - case animation_state_row_t::EndWriting: - section = current_frames.end_writing.valid ? ¤t_frames.end_writing : nullptr; - break; - case animation_state_row_t::Happy: - section = current_frames.happy.valid ? ¤t_frames.happy : nullptr; - break; - case animation_state_row_t::FallASleep: - section = current_frames.fall_asleep.valid ? ¤t_frames.fall_asleep : nullptr; - break; - case animation_state_row_t::Sleep: - section = current_frames.sleep.valid ? ¤t_frames.sleep : nullptr; - break; - case animation_state_row_t::WakeUp: - section = current_frames.wake_up.valid ? ¤t_frames.wake_up : nullptr; - break; - case animation_state_row_t::Boring: - section = current_frames.boring.valid ? ¤t_frames.boring : nullptr; - break; - case animation_state_row_t::Test: - section = current_frames.writing.valid ? ¤t_frames.writing : nullptr; - break; - case animation_state_row_t::StartWorking: - section = current_frames.start_working.valid ? ¤t_frames.start_working : nullptr; - break; - case animation_state_row_t::Working: - section = current_frames.working.valid ? ¤t_frames.working : nullptr; - break; - case animation_state_row_t::EndWorking: - section = current_frames.end_working.valid ? ¤t_frames.end_working : nullptr; - break; - case animation_state_row_t::StartMoving: - section = current_frames.start_moving.valid ? ¤t_frames.start_moving : nullptr; - break; - case animation_state_row_t::Moving: - section = current_frames.moving.valid ? ¤t_frames.moving : nullptr; - break; - case animation_state_row_t::EndMoving: - section = current_frames.end_moving.valid ? ¤t_frames.end_moving : nullptr; - break; - case animation_state_row_t::StartRunning: - section = current_frames.start_running.valid ? ¤t_frames.start_running : nullptr; - break; - case animation_state_row_t::Running: - section = current_frames.running.valid ? ¤t_frames.running : nullptr; - break; - case animation_state_row_t::EndRunning: - section = current_frames.end_running.valid ? ¤t_frames.end_running : nullptr; - break; - } - - anim_custom_process_animation_result_t ret {.row_state = new_state.row_state, .status = anim_custom_process_animation_result_status_t::None}; - if (section && section->valid) { - new_state.row_state = new_row_state; - new_animation_result.sprite_sheet_row = section->row; - new_animation_result.sprite_sheet_col = section->start_col; - new_animation_result.overwrite_mirror_x = animation_player_custom_overwrite_mirror_x::None; - if (new_state.row_state == animation_state_row_t::Idle) { - assert(current_frames.idle.end_col >= 0); - if (current_config.idle_frame) { - new_animation_result.sprite_sheet_col = current_config.idle_frame % (current_frames.idle.end_col+1); - } - } else if (new_state.row_state == animation_state_row_t::StartMoving || new_state.row_state == animation_state_row_t::Moving || new_state.row_state == animation_state_row_t::EndMoving) { - // flip movement frame when configured - if (current_config.custom_sprite_sheet_settings.feature_mirror_x_moving >= 0) { - if (current_config.custom_sprite_sheet_settings.feature_mirror_x_moving == 1) { - new_animation_result.overwrite_mirror_x = animation_player_custom_overwrite_mirror_x::Mirror; - } else if (current_config.custom_sprite_sheet_settings.feature_mirror_x_moving == 0) { - new_animation_result.overwrite_mirror_x = animation_player_custom_overwrite_mirror_x::NoMirror; - } - } - } - - ret.row_state = new_state.row_state; - ret.status = anim_custom_process_animation_result_status_t::Started; - } - - return ret; +enum class anim_custom_process_animation_result_status_t : uint8_t { + None, + Started, + Updated, + End, + Looped, + NextAnimationStarted, + SkipMovement, + Moved, + Stop +}; +struct anim_custom_process_animation_result_t { + animation_state_row_t row_state; + anim_custom_process_animation_result_status_t status{anim_custom_process_animation_result_status_t::None}; +}; +static anim_custom_process_animation_result_t +anim_custom_process_animation(animation_player_result_t& new_animation_result, animation_state_t& new_state, + [[maybe_unused]] const animation_state_t& current_state, + const custom_sprite_sheet_t& current_frames) { + using namespace assets; + + const custom_sprite_sheet_animation_section_t *section = BONGOCAT_NULLPTR; + switch (new_state.row_state) { + case animation_state_row_t::Idle: + section = current_frames.idle.valid ? ¤t_frames.idle : BONGOCAT_NULLPTR; + break; + case animation_state_row_t::StartWriting: + section = current_frames.start_writing.valid ? ¤t_frames.start_writing : BONGOCAT_NULLPTR; + break; + case animation_state_row_t::Writing: + section = current_frames.writing.valid ? ¤t_frames.writing : BONGOCAT_NULLPTR; + break; + case animation_state_row_t::EndWriting: + section = current_frames.end_writing.valid ? ¤t_frames.end_writing : BONGOCAT_NULLPTR; + break; + case animation_state_row_t::Happy: + section = current_frames.happy.valid ? ¤t_frames.happy : BONGOCAT_NULLPTR; + break; + case animation_state_row_t::FallASleep: + section = current_frames.fall_asleep.valid ? ¤t_frames.fall_asleep : BONGOCAT_NULLPTR; + break; + case animation_state_row_t::Sleep: + section = current_frames.sleep.valid ? ¤t_frames.sleep : BONGOCAT_NULLPTR; + break; + case animation_state_row_t::WakeUp: + section = current_frames.wake_up.valid ? ¤t_frames.wake_up : BONGOCAT_NULLPTR; + break; + case animation_state_row_t::Boring: + section = current_frames.boring.valid ? ¤t_frames.boring : BONGOCAT_NULLPTR; + break; + case animation_state_row_t::Test: + section = current_frames.writing.valid ? ¤t_frames.writing : BONGOCAT_NULLPTR; + break; + case animation_state_row_t::StartWorking: + section = current_frames.start_working.valid ? ¤t_frames.start_working : BONGOCAT_NULLPTR; + break; + case animation_state_row_t::Working: + section = current_frames.working.valid ? ¤t_frames.working : BONGOCAT_NULLPTR; + break; + case animation_state_row_t::EndWorking: + section = current_frames.end_working.valid ? ¤t_frames.end_working : BONGOCAT_NULLPTR; + break; + case animation_state_row_t::StartMoving: + section = current_frames.start_moving.valid ? ¤t_frames.start_moving : BONGOCAT_NULLPTR; + break; + case animation_state_row_t::Moving: + section = current_frames.moving.valid ? ¤t_frames.moving : BONGOCAT_NULLPTR; + break; + case animation_state_row_t::EndMoving: + section = current_frames.end_moving.valid ? ¤t_frames.end_moving : BONGOCAT_NULLPTR; + break; + case animation_state_row_t::StartRunning: + section = current_frames.start_running.valid ? ¤t_frames.start_running : BONGOCAT_NULLPTR; + break; + case animation_state_row_t::Running: + section = current_frames.running.valid ? ¤t_frames.running : BONGOCAT_NULLPTR; + break; + case animation_state_row_t::EndRunning: + section = current_frames.end_running.valid ? ¤t_frames.end_running : BONGOCAT_NULLPTR; + break; + } + + anim_custom_process_animation_result_t ret{.row_state = new_state.row_state, + .status = anim_custom_process_animation_result_status_t::None}; + if (section != BONGOCAT_NULLPTR && section->valid) { + new_animation_result.sprite_sheet_row = section->row; + new_animation_result.sprite_sheet_col = new_animation_result.sprite_sheet_col + 1; + ret.status = anim_custom_process_animation_result_status_t::Updated; + + if (new_animation_result.sprite_sheet_col <= section->start_col) { + // start animation + new_animation_result.sprite_sheet_col = section->start_col; + ret.status = anim_custom_process_animation_result_status_t::Started; + } else if (new_animation_result.sprite_sheet_col == section->end_col) { + // last frame + new_animation_result.sprite_sheet_col = section->end_col; + ret.status = anim_custom_process_animation_result_status_t::Updated; + } else if (new_animation_result.sprite_sheet_col > section->end_col) { + // loop animation + new_animation_result.sprite_sheet_col = section->start_col; + ret.status = anim_custom_process_animation_result_status_t::Looped; } - static anim_custom_process_animation_result_t anim_custom_restart_animation([[maybe_unused]] animation_context_t& ctx, - animation_state_row_t new_row_state, - const animation_state_row_t fallback_row_state, - const animation_state_row_t end_fallback_row_state, - animation_player_result_t& new_animation_result, - animation_state_t& new_state, - [[maybe_unused]] const animation_state_t& current_state, - const custom_sprite_sheet_t& current_frames, - [[maybe_unused]] const config::config_t& current_config) { - using namespace assets; - - constexpr size_t new_row_states_count = 3; - const animation_state_row_t new_row_states[new_row_states_count] = { new_row_state, fallback_row_state, end_fallback_row_state}; - - const custom_sprite_sheet_animation_section_t *section = nullptr; - for (size_t i = 0;i < new_row_states_count && section == nullptr;i++) { - new_row_state = new_row_states[i]; - switch (new_row_state) { - case animation_state_row_t::Idle: - section = current_frames.idle.valid ? ¤t_frames.idle : nullptr; - break; - case animation_state_row_t::StartWriting: - section = current_frames.start_writing.valid ? ¤t_frames.start_writing : nullptr; - break; - case animation_state_row_t::Writing: - section = current_frames.writing.valid ? ¤t_frames.writing : nullptr; - break; - case animation_state_row_t::EndWriting: - section = current_frames.end_writing.valid ? ¤t_frames.end_writing : nullptr; - break; - case animation_state_row_t::Happy: - section = current_frames.happy.valid ? ¤t_frames.happy : nullptr; - break; - case animation_state_row_t::FallASleep: - section = current_frames.fall_asleep.valid ? ¤t_frames.fall_asleep : nullptr; - break; - case animation_state_row_t::Sleep: - section = current_frames.sleep.valid ? ¤t_frames.sleep : nullptr; - break; - case animation_state_row_t::WakeUp: - section = current_frames.wake_up.valid ? ¤t_frames.wake_up : nullptr; - break; - case animation_state_row_t::Boring: - section = current_frames.boring.valid ? ¤t_frames.boring : nullptr; - break; - case animation_state_row_t::Test: - section = current_frames.writing.valid ? ¤t_frames.writing : nullptr; - break; - case animation_state_row_t::StartWorking: - section = current_frames.start_working.valid ? ¤t_frames.start_working : nullptr; - break; - case animation_state_row_t::Working: - section = current_frames.working.valid ? ¤t_frames.working : nullptr; - break; - case animation_state_row_t::EndWorking: - section = current_frames.end_working.valid ? ¤t_frames.end_working : nullptr; - break; - case animation_state_row_t::StartMoving: - section = current_frames.start_moving.valid ? ¤t_frames.start_moving : nullptr; - break; - case animation_state_row_t::Moving: - section = current_frames.moving.valid ? ¤t_frames.moving : nullptr; - break; - case animation_state_row_t::EndMoving: - section = current_frames.end_moving.valid ? ¤t_frames.end_moving : nullptr; - break; - case animation_state_row_t::StartRunning: - section = current_frames.start_running.valid ? ¤t_frames.start_running : nullptr; - break; - case animation_state_row_t::Running: - section = current_frames.running.valid ? ¤t_frames.running : nullptr; - break; - case animation_state_row_t::EndRunning: - section = current_frames.end_running.valid ? ¤t_frames.end_running : nullptr; - break; - } - } + } - anim_custom_process_animation_result_t ret {.row_state = new_state.row_state, .status = anim_custom_process_animation_result_status_t::None}; - if (section && section->valid) { - new_state.row_state = new_row_state; - new_animation_result.sprite_sheet_row = section->row; - new_animation_result.sprite_sheet_col = section->start_col; - new_animation_result.overwrite_mirror_x = animation_player_custom_overwrite_mirror_x::None; - if (new_state.row_state == animation_state_row_t::Idle) { - assert(current_frames.idle.end_col >= 0); - if (current_config.idle_frame) { - new_animation_result.sprite_sheet_col = current_config.idle_frame % (current_frames.idle.end_col+1); - } - } else if (new_state.row_state == animation_state_row_t::StartMoving || new_state.row_state == animation_state_row_t::Moving || new_state.row_state == animation_state_row_t::EndMoving) { - // flip movement frame when configured - if (current_config.custom_sprite_sheet_settings.feature_mirror_x_moving >= 0) { - if (current_config.custom_sprite_sheet_settings.feature_mirror_x_moving == 1) { - new_animation_result.overwrite_mirror_x = animation_player_custom_overwrite_mirror_x::Mirror; - } else if (current_config.custom_sprite_sheet_settings.feature_mirror_x_moving == 0) { - new_animation_result.overwrite_mirror_x = animation_player_custom_overwrite_mirror_x::NoMirror; - } - } - } - ret.row_state = new_state.row_state; - ret.status = anim_custom_process_animation_result_status_t::Started; + return ret; +} +static anim_custom_process_animation_result_t +anim_custom_restart_animation([[maybe_unused]] animation_thread_context_t& ctx, animation_state_row_t new_row_state, + animation_player_result_t& new_animation_result, animation_state_t& new_state, + [[maybe_unused]] const animation_state_t& current_state, + const custom_sprite_sheet_t& current_frames, + [[maybe_unused]] const config::config_t& current_config) { + using namespace assets; + + const custom_sprite_sheet_animation_section_t *section = BONGOCAT_NULLPTR; + switch (new_row_state) { + case animation_state_row_t::Idle: + section = current_frames.idle.valid ? ¤t_frames.idle : BONGOCAT_NULLPTR; + break; + case animation_state_row_t::StartWriting: + section = current_frames.start_writing.valid ? ¤t_frames.start_writing : BONGOCAT_NULLPTR; + break; + case animation_state_row_t::Writing: + section = current_frames.writing.valid ? ¤t_frames.writing : BONGOCAT_NULLPTR; + break; + case animation_state_row_t::EndWriting: + section = current_frames.end_writing.valid ? ¤t_frames.end_writing : BONGOCAT_NULLPTR; + break; + case animation_state_row_t::Happy: + section = current_frames.happy.valid ? ¤t_frames.happy : BONGOCAT_NULLPTR; + break; + case animation_state_row_t::FallASleep: + section = current_frames.fall_asleep.valid ? ¤t_frames.fall_asleep : BONGOCAT_NULLPTR; + break; + case animation_state_row_t::Sleep: + section = current_frames.sleep.valid ? ¤t_frames.sleep : BONGOCAT_NULLPTR; + break; + case animation_state_row_t::WakeUp: + section = current_frames.wake_up.valid ? ¤t_frames.wake_up : BONGOCAT_NULLPTR; + break; + case animation_state_row_t::Boring: + section = current_frames.boring.valid ? ¤t_frames.boring : BONGOCAT_NULLPTR; + break; + case animation_state_row_t::Test: + section = current_frames.writing.valid ? ¤t_frames.writing : BONGOCAT_NULLPTR; + break; + case animation_state_row_t::StartWorking: + section = current_frames.start_working.valid ? ¤t_frames.start_working : BONGOCAT_NULLPTR; + break; + case animation_state_row_t::Working: + section = current_frames.working.valid ? ¤t_frames.working : BONGOCAT_NULLPTR; + break; + case animation_state_row_t::EndWorking: + section = current_frames.end_working.valid ? ¤t_frames.end_working : BONGOCAT_NULLPTR; + break; + case animation_state_row_t::StartMoving: + section = current_frames.start_moving.valid ? ¤t_frames.start_moving : BONGOCAT_NULLPTR; + break; + case animation_state_row_t::Moving: + section = current_frames.moving.valid ? ¤t_frames.moving : BONGOCAT_NULLPTR; + break; + case animation_state_row_t::EndMoving: + section = current_frames.end_moving.valid ? ¤t_frames.end_moving : BONGOCAT_NULLPTR; + break; + case animation_state_row_t::StartRunning: + section = current_frames.start_running.valid ? ¤t_frames.start_running : BONGOCAT_NULLPTR; + break; + case animation_state_row_t::Running: + section = current_frames.running.valid ? ¤t_frames.running : BONGOCAT_NULLPTR; + break; + case animation_state_row_t::EndRunning: + section = current_frames.end_running.valid ? ¤t_frames.end_running : BONGOCAT_NULLPTR; + break; + } + + anim_custom_process_animation_result_t ret{.row_state = new_state.row_state, + .status = anim_custom_process_animation_result_status_t::None}; + if (section != BONGOCAT_NULLPTR && section->valid) { + new_state.row_state = new_row_state; + new_animation_result.sprite_sheet_row = section->row; + new_animation_result.sprite_sheet_col = section->start_col; + new_animation_result.overwrite_mirror_x = animation_player_custom_overwrite_mirror_x::None; + if (new_state.row_state == animation_state_row_t::Idle) { + assert(current_frames.idle.end_col >= 0); + if (current_config.idle_frame >= 1) { + new_animation_result.sprite_sheet_col = current_config.idle_frame % (current_frames.idle.end_col + 1); + } + } else if (new_state.row_state == animation_state_row_t::StartMoving || + new_state.row_state == animation_state_row_t::Moving || + new_state.row_state == animation_state_row_t::EndMoving) { + // flip movement frame when configured + if (current_config.custom_sprite_sheet_settings.feature_mirror_x_moving >= 0) { + if (current_config.custom_sprite_sheet_settings.feature_mirror_x_moving == 1) { + new_animation_result.overwrite_mirror_x = animation_player_custom_overwrite_mirror_x::Mirror; + } else if (current_config.custom_sprite_sheet_settings.feature_mirror_x_moving == 0) { + new_animation_result.overwrite_mirror_x = animation_player_custom_overwrite_mirror_x::NoMirror; } - - return ret; + } } - static anim_custom_process_animation_result_t anim_custom_start_or_process_animation(animation_context_t& ctx, - animation_state_row_t new_row_state, - animation_player_result_t& new_animation_result, - animation_state_t& new_state, - const animation_state_t& current_state, - const custom_sprite_sheet_t& current_frames, - const config::config_t& current_config) { - - if (current_state.row_state != new_row_state) { - auto result = anim_custom_process_animation(new_animation_result, new_state, current_state, current_frames); - if (result.status == anim_custom_process_animation_result_status_t::Looped || result.status == anim_custom_process_animation_result_status_t::End) { - result = anim_custom_restart_animation(ctx, new_row_state, new_animation_result, new_state, - current_state, current_frames, current_config); - result.status = result.status != anim_custom_process_animation_result_status_t::None ? anim_custom_process_animation_result_status_t::NextAnimationStarted : anim_custom_process_animation_result_status_t::None; - } - return result; - } + ret.row_state = new_state.row_state; + ret.status = anim_custom_process_animation_result_status_t::Started; + } - return anim_custom_restart_animation(ctx, new_row_state, new_animation_result, new_state, - current_state, current_frames, current_config); + return ret; +} +static anim_custom_process_animation_result_t anim_custom_restart_animation( + [[maybe_unused]] animation_thread_context_t& ctx, animation_state_row_t new_row_state, + const animation_state_row_t fallback_row_state, const animation_state_row_t end_fallback_row_state, + animation_player_result_t& new_animation_result, animation_state_t& new_state, + [[maybe_unused]] const animation_state_t& current_state, const custom_sprite_sheet_t& current_frames, + [[maybe_unused]] const config::config_t& current_config) { + using namespace assets; + + constexpr size_t new_row_states_count = 3; + const animation_state_row_t new_row_states[new_row_states_count] = {new_row_state, fallback_row_state, + end_fallback_row_state}; + + const custom_sprite_sheet_animation_section_t *section = BONGOCAT_NULLPTR; + for (size_t i = 0; i < new_row_states_count && section == BONGOCAT_NULLPTR; i++) { + new_row_state = new_row_states[i]; + switch (new_row_state) { + case animation_state_row_t::Idle: + section = current_frames.idle.valid ? ¤t_frames.idle : BONGOCAT_NULLPTR; + break; + case animation_state_row_t::StartWriting: + section = current_frames.start_writing.valid ? ¤t_frames.start_writing : BONGOCAT_NULLPTR; + break; + case animation_state_row_t::Writing: + section = current_frames.writing.valid ? ¤t_frames.writing : BONGOCAT_NULLPTR; + break; + case animation_state_row_t::EndWriting: + section = current_frames.end_writing.valid ? ¤t_frames.end_writing : BONGOCAT_NULLPTR; + break; + case animation_state_row_t::Happy: + section = current_frames.happy.valid ? ¤t_frames.happy : BONGOCAT_NULLPTR; + break; + case animation_state_row_t::FallASleep: + section = current_frames.fall_asleep.valid ? ¤t_frames.fall_asleep : BONGOCAT_NULLPTR; + break; + case animation_state_row_t::Sleep: + section = current_frames.sleep.valid ? ¤t_frames.sleep : BONGOCAT_NULLPTR; + break; + case animation_state_row_t::WakeUp: + section = current_frames.wake_up.valid ? ¤t_frames.wake_up : BONGOCAT_NULLPTR; + break; + case animation_state_row_t::Boring: + section = current_frames.boring.valid ? ¤t_frames.boring : BONGOCAT_NULLPTR; + break; + case animation_state_row_t::Test: + section = current_frames.writing.valid ? ¤t_frames.writing : BONGOCAT_NULLPTR; + break; + case animation_state_row_t::StartWorking: + section = current_frames.start_working.valid ? ¤t_frames.start_working : BONGOCAT_NULLPTR; + break; + case animation_state_row_t::Working: + section = current_frames.working.valid ? ¤t_frames.working : BONGOCAT_NULLPTR; + break; + case animation_state_row_t::EndWorking: + section = current_frames.end_working.valid ? ¤t_frames.end_working : BONGOCAT_NULLPTR; + break; + case animation_state_row_t::StartMoving: + section = current_frames.start_moving.valid ? ¤t_frames.start_moving : BONGOCAT_NULLPTR; + break; + case animation_state_row_t::Moving: + section = current_frames.moving.valid ? ¤t_frames.moving : BONGOCAT_NULLPTR; + break; + case animation_state_row_t::EndMoving: + section = current_frames.end_moving.valid ? ¤t_frames.end_moving : BONGOCAT_NULLPTR; + break; + case animation_state_row_t::StartRunning: + section = current_frames.start_running.valid ? ¤t_frames.start_running : BONGOCAT_NULLPTR; + break; + case animation_state_row_t::Running: + section = current_frames.running.valid ? ¤t_frames.running : BONGOCAT_NULLPTR; + break; + case animation_state_row_t::EndRunning: + section = current_frames.end_running.valid ? ¤t_frames.end_running : BONGOCAT_NULLPTR; + break; } - static anim_custom_process_animation_result_t anim_custom_start_or_process_animation(animation_context_t& ctx, - animation_state_row_t new_row_state, - animation_state_row_t fallback_row_state, - animation_player_result_t& new_animation_result, - animation_state_t& new_state, - const animation_state_t& current_state, - const custom_sprite_sheet_t& current_frames, - const config::config_t& current_config) { - - if (current_state.row_state != new_row_state) { - auto result = anim_custom_process_animation(new_animation_result, new_state, current_state, current_frames); - if (result.status == anim_custom_process_animation_result_status_t::Looped || result.status == anim_custom_process_animation_result_status_t::End) { - result = anim_custom_restart_animation(ctx, new_row_state, new_animation_result, new_state, - current_state, current_frames, current_config); - if (result.status == anim_custom_process_animation_result_status_t::None) { - result = anim_custom_restart_animation(ctx, fallback_row_state, new_animation_result, new_state, - current_state, current_frames, current_config); - result.status = result.status != anim_custom_process_animation_result_status_t::None ? anim_custom_process_animation_result_status_t::NextAnimationStarted : anim_custom_process_animation_result_status_t::None; - } else { - result.status = anim_custom_process_animation_result_status_t::NextAnimationStarted; - } - } - return result; + } + + anim_custom_process_animation_result_t ret{.row_state = new_state.row_state, + .status = anim_custom_process_animation_result_status_t::None}; + if (section != BONGOCAT_NULLPTR && section->valid) { + new_state.row_state = new_row_state; + new_animation_result.sprite_sheet_row = section->row; + new_animation_result.sprite_sheet_col = section->start_col; + new_animation_result.overwrite_mirror_x = animation_player_custom_overwrite_mirror_x::None; + if (new_state.row_state == animation_state_row_t::Idle) { + assert(current_frames.idle.end_col >= 0); + if (current_config.idle_frame >= 1) { + new_animation_result.sprite_sheet_col = current_config.idle_frame % (current_frames.idle.end_col + 1); + } + } else if (new_state.row_state == animation_state_row_t::StartMoving || + new_state.row_state == animation_state_row_t::Moving || + new_state.row_state == animation_state_row_t::EndMoving) { + // flip movement frame when configured + if (current_config.custom_sprite_sheet_settings.feature_mirror_x_moving >= 0) { + if (current_config.custom_sprite_sheet_settings.feature_mirror_x_moving == 1) { + new_animation_result.overwrite_mirror_x = animation_player_custom_overwrite_mirror_x::Mirror; + } else if (current_config.custom_sprite_sheet_settings.feature_mirror_x_moving == 0) { + new_animation_result.overwrite_mirror_x = animation_player_custom_overwrite_mirror_x::NoMirror; } + } + } + ret.row_state = new_state.row_state; + ret.status = anim_custom_process_animation_result_status_t::Started; + } + + return ret; +} - return anim_custom_restart_animation(ctx, new_row_state, new_animation_result, new_state, - current_state, current_frames, current_config); +static anim_custom_process_animation_result_t +anim_custom_start_or_process_animation(animation_thread_context_t& ctx, animation_state_row_t new_row_state, + animation_player_result_t& new_animation_result, animation_state_t& new_state, + const animation_state_t& current_state, + const custom_sprite_sheet_t& current_frames, + const config::config_t& current_config) { + if (current_state.row_state != new_row_state) { + auto result = anim_custom_process_animation(new_animation_result, new_state, current_state, current_frames); + if (result.status == anim_custom_process_animation_result_status_t::Looped || + result.status == anim_custom_process_animation_result_status_t::End) { + result = anim_custom_restart_animation(ctx, new_row_state, new_animation_result, new_state, current_state, + current_frames, current_config); + result.status = result.status != anim_custom_process_animation_result_status_t::None + ? anim_custom_process_animation_result_status_t::NextAnimationStarted + : anim_custom_process_animation_result_status_t::None; } + return result; + } + return anim_custom_restart_animation(ctx, new_row_state, new_animation_result, new_state, current_state, + current_frames, current_config); +} +static anim_custom_process_animation_result_t anim_custom_start_or_process_animation( + animation_thread_context_t& ctx, animation_state_row_t new_row_state, animation_state_row_t fallback_row_state, + animation_player_result_t& new_animation_result, animation_state_t& new_state, + const animation_state_t& current_state, const custom_sprite_sheet_t& current_frames, + const config::config_t& current_config) { + if (current_state.row_state != new_row_state) { + auto result = anim_custom_process_animation(new_animation_result, new_state, current_state, current_frames); + if (result.status == anim_custom_process_animation_result_status_t::Looped || + result.status == anim_custom_process_animation_result_status_t::End) { + result = anim_custom_restart_animation(ctx, new_row_state, new_animation_result, new_state, current_state, + current_frames, current_config); + if (result.status == anim_custom_process_animation_result_status_t::None) { + result = anim_custom_restart_animation(ctx, fallback_row_state, new_animation_result, new_state, current_state, + current_frames, current_config); + result.status = result.status != anim_custom_process_animation_result_status_t::None + ? anim_custom_process_animation_result_status_t::NextAnimationStarted + : anim_custom_process_animation_result_status_t::None; + } else { + result.status = anim_custom_process_animation_result_status_t::NextAnimationStarted; + } + } + return result; + } - static anim_custom_process_animation_result_t anim_custom_handle_movement(animation_context_t& ctx, - const platform::input::input_context_t& input, - [[maybe_unused]] const platform::update::update_context_t& upd, - animation_player_result_t& new_animation_result, - animation_state_t& new_state, - const anim_handle_key_press_result_t& trigger_result, - const animation_state_t& current_state, - const custom_sprite_sheet_t& current_frames, - const config::config_t& current_config) { - using namespace assets; - - assert(ctx.shm != nullptr); - animation_shared_memory_t& anim_shm = *ctx.shm; - - const auto conditions = get_anim_conditions(ctx, input, upd, current_state, trigger_result.trigger, current_config); - - anim_custom_process_animation_result_t ret {.row_state = new_state.row_state, .status = anim_custom_process_animation_result_status_t::None}; - if (!conditions.is_writing && current_config.movement_speed > 0 && current_config.movement_radius > 0 && current_config.fps > 0) { - if ((conditions.go_next_frame || conditions.process_idle_animation) || conditions.process_movement || conditions.process_movement_animation) { - const float delta = 1.0f / static_cast(current_config.fps); - const auto delta_ms = static_cast(delta * 1000); - const auto frame_height = current_config.cat_height; - const auto frame_width = static_cast(static_cast(frame_height) * (static_cast(current_frames.frame_width) / static_cast(current_frames.frame_height))); - const auto fmovement_radius = static_cast(current_config.movement_radius); - - assert(current_config.movement_radius > 0); - float max_movement_offset_x_left = 0.0f; - float max_movement_offset_x_right = 0.0f; - float wall_distance = 0.0f; - switch (current_config.cat_align) { - case config::align_type_t::ALIGN_CENTER: - // range: [-r, +r] - max_movement_offset_x_left = -static_cast(current_config.movement_radius) + static_cast(frame_width) / 2.0f; - max_movement_offset_x_right = static_cast(current_config.movement_radius - frame_width); - wall_distance = anim_shm.movement_offset_x / fmovement_radius; - break; - case config::align_type_t::ALIGN_LEFT: - // range: [0, +2r] - max_movement_offset_x_left = 0.0f; - max_movement_offset_x_right = static_cast(current_config.movement_radius) * 2.0f - static_cast(frame_width); - wall_distance = (anim_shm.movement_offset_x / (fmovement_radius * 2.0f)) * 2.0f - 1.0f; - break; - case config::align_type_t::ALIGN_RIGHT: - // range: [-2r, 0] - max_movement_offset_x_left = -(static_cast(current_config.movement_radius) * 2.0f) + static_cast(frame_width); - max_movement_offset_x_right = 0.0f; - wall_distance = (anim_shm.movement_offset_x / (fmovement_radius * 2.0f)) * 2.0f + 1.0f; // normalize to -1 → 1 - break; - } + return anim_custom_restart_animation(ctx, new_row_state, new_animation_result, new_state, current_state, + current_frames, current_config); +} - if (current_state.row_state == animation_state_row_t::Idle && (current_state.anim_pause_after_movement_ms > 0 || (ctx._rng.range(0, 100) <= CHANCE_FOR_SKIPPING_MOVEMENT_PERCENT))) { - // skip movement - new_state.anim_velocity = 0.0f; - new_state.anim_distance = 0.0f; - if (new_state.anim_pause_after_movement_ms > 0) { - new_state.anim_pause_after_movement_ms -= delta_ms; - if (new_state.anim_pause_after_movement_ms <= 0) { - new_state.anim_pause_after_movement_ms = 0; - } - } - ret.status = anim_custom_process_animation_result_status_t::SkipMovement; - } else { - // moving animation - constexpr float DIR_EPSILON = 1e-3f; - assert(MAX_DISTANCE_PER_MOVEMENT_PART > 0); - const int movement_part = current_config.movement_radius <= MAX_MOVEMENT_RADIUS_SMALL ? SMALL_MAX_DISTANCE_PER_MOVEMENT_PART : MAX_DISTANCE_PER_MOVEMENT_PART; - const float fmovement_part = static_cast(movement_part); - bool end_movement = false; - if (current_state.row_state == animation_state_row_t::Idle) { - // start movement - const auto min_movement = current_config.movement_radius <= MAX_MOVEMENT_RADIUS_SMALL ? (fmovement_radius / fmovement_part / 2) + 1 : (fmovement_radius / fmovement_part / fmovement_part) + 1; - float max_move_distance = fmovement_radius / fmovement_part; - max_move_distance = max_move_distance <= min_movement ? min_movement : max_move_distance; - - assert(max_move_distance >= 0); - assert(min_movement >= 0); - new_state.anim_distance = static_cast(ctx._rng.range(static_cast(min_movement), static_cast(max_move_distance))); - - if (anim_shm.movement_offset_x >= max_movement_offset_x_right) { - // run against wall, change direction - anim_shm.anim_direction = -1.0; - anim_shm.movement_offset_x = max_movement_offset_x_right; - } else if (anim_shm.movement_offset_x <= max_movement_offset_x_left) { - // run against wall, change direction - anim_shm.anim_direction = 1.0; - anim_shm.movement_offset_x = max_movement_offset_x_left; - } else { - float toward_wall_bias = fabs(wall_distance); - toward_wall_bias = toward_wall_bias >= 1.0f ? 1.0f : toward_wall_bias; - toward_wall_bias = toward_wall_bias <= 0.0f ? 0.0f : toward_wall_bias; - - const int flip_direction_chance = current_config.movement_radius <= MAX_MOVEMENT_RADIUS_SMALL ? SMALL_FLIP_DIRECTION_NEAR_WALL_PERCENT : FLIP_DIRECTION_NEAR_WALL_PERCENT; - - assert(toward_wall_bias >= 0.0f); - // change direction: chance drops at center, changes falloff steeper near walls - const auto dir_chance = static_cast(static_cast(100 - flip_direction_chance + 10) * (1.0f - pow(toward_wall_bias, 1.5f))); - - if (fabs(new_state.anim_last_direction) >= DIR_EPSILON) { - anim_shm.anim_direction = (ctx._rng.range(0, 100) < dir_chance) ? -new_state.anim_last_direction : new_state.anim_last_direction; - } else if (fabs(anim_shm.anim_direction) < DIR_EPSILON) { - // idle: choose random start direction - anim_shm.anim_direction = (ctx._rng.range(0, 100) < dir_chance) ? 1.0f : -1.0f; - } else { - if (wall_distance > (100.0f / static_cast(flip_direction_chance))) { - anim_shm.anim_direction = (ctx._rng.range(0, 100) < dir_chance) ? -1.0f : 1.0f; - } else if (wall_distance < -(100.0f / static_cast(flip_direction_chance))) { - anim_shm.anim_direction = (ctx._rng.range(0, 100) < dir_chance) ? 1.0f : -1.0f; - } else { - anim_shm.anim_direction = (ctx._rng.range(0, 100) < dir_chance) ? anim_shm.anim_direction : -anim_shm.anim_direction; - } - } - } - // start moving animation - new_state.anim_velocity = anim_shm.anim_direction * static_cast(current_config.movement_speed); - ret = anim_custom_restart_animation(ctx, animation_state_row_t::StartMoving, animation_state_row_t::Moving, animation_state_row_t::EndMoving, - new_animation_result, new_state, - current_state, current_frames, current_config); - } else if (current_state.row_state == animation_state_row_t::StartMoving) { - if (conditions.process_idle_animation) { - ret = anim_custom_start_or_process_animation(ctx, animation_state_row_t::Moving, animation_state_row_t::EndMoving, - new_animation_result, new_state, - current_state, current_frames, current_config); - ret.status = anim_custom_process_animation_result_status_t::Updated; - } else if (conditions.process_movement_animation) { - ret = anim_custom_restart_animation(ctx, animation_state_row_t::Moving, animation_state_row_t::EndMoving, animation_state_row_t::Idle, - new_animation_result, new_state, - current_state, current_frames, current_config); - ret.status = anim_custom_process_animation_result_status_t::Updated; - } - } else if (current_state.row_state == animation_state_row_t::Moving) { - if (conditions.process_movement) { - ret = anim_custom_process_animation(new_animation_result, new_state, current_state, current_frames); - ret.status = anim_custom_process_animation_result_status_t::Moved; - - new_state.anim_distance -= fabs(new_state.anim_velocity); - anim_shm.movement_offset_x += new_state.anim_velocity; - // clamp walking/movement area - if (anim_shm.movement_offset_x > max_movement_offset_x_right) { - anim_shm.movement_offset_x = max_movement_offset_x_right; - ret = anim_custom_restart_animation(ctx, animation_state_row_t::EndMoving, animation_state_row_t::Idle, animation_state_row_t::Idle, - new_animation_result, new_state, - current_state, current_frames, current_config); - ret.status = anim_custom_process_animation_result_status_t::Stop; - } else if (anim_shm.movement_offset_x < max_movement_offset_x_left) { - anim_shm.movement_offset_x = max_movement_offset_x_left; - ret = anim_custom_restart_animation(ctx, animation_state_row_t::EndMoving, animation_state_row_t::Idle, animation_state_row_t::Idle, - new_animation_result, new_state, - current_state, current_frames, current_config); - ret.status = anim_custom_process_animation_result_status_t::Stop; - } - - if (new_state.anim_distance <= 0 || (fabs(new_state.anim_distance) < DIR_EPSILON || fabs(new_state.anim_velocity) < DIR_EPSILON)) { - ret = anim_custom_restart_animation(ctx, animation_state_row_t::EndMoving, animation_state_row_t::Idle, animation_state_row_t::Idle, - new_animation_result, new_state, - current_state, current_frames, current_config); - ret.status = anim_custom_process_animation_result_status_t::Stop; - } - - end_movement = ret.status == anim_custom_process_animation_result_status_t::Stop; - } - } else if (current_state.row_state == animation_state_row_t::EndMoving) { - end_movement = true; - } - - if (end_movement) { - if (conditions.process_idle_animation) { - ret = anim_custom_start_or_process_animation(ctx, animation_state_row_t::Idle, - new_animation_result, new_state, - current_state, current_frames, current_config); - } else { - ret = anim_custom_restart_animation(ctx, animation_state_row_t::Idle, - new_animation_result, new_state, - current_state, current_frames, current_config); - } - new_state.anim_last_direction = anim_shm.anim_direction; - new_state.anim_velocity = 0.0f; - anim_shm.anim_direction = 0.0; - if (ret.row_state == animation_state_row_t::Idle && new_state.anim_distance <= 0) { - assert(current_config.animation_speed_ms >= 0); - assert(movement_part >= 0); - const auto min_wait = current_config.animation_speed_ms * (current_config.movement_wait_factor / 2); - const auto max_wait = current_config.animation_speed_ms * current_config.movement_wait_factor; - assert(min_wait >= 0); - assert(max_wait >= 0); - new_state.anim_pause_after_movement_ms = static_cast(ctx._rng.range(static_cast(min_wait), static_cast(max_wait))); - ret.status = anim_custom_process_animation_result_status_t::End; - } - } - } +static anim_custom_process_animation_result_t +anim_custom_handle_movement(animation_thread_context_t& ctx, const platform::input::input_context_t& input, + [[maybe_unused]] const platform::update::update_context_t& upd, + animation_player_result_t& new_animation_result, animation_state_t& new_state, + const anim_handle_key_press_result_t& trigger_result, + const animation_state_t& current_state, const custom_sprite_sheet_t& current_frames, + const config::config_t& current_config) { + using namespace assets; + + assert(ctx.shm != BONGOCAT_NULLPTR); + animation_shared_memory_t& anim_shm = *ctx.shm; + + const auto conditions = get_anim_conditions(ctx, input, upd, current_state, trigger_result.trigger, current_config); + + anim_custom_process_animation_result_t ret{.row_state = new_state.row_state, + .status = anim_custom_process_animation_result_status_t::None}; + if (!conditions.is_writing && current_config.movement_speed > 0 && current_config.movement_radius > 0 && + current_config.fps > 0) { + if ((conditions.go_next_frame || conditions.process_idle_animation) || conditions.process_movement || + conditions.process_movement_animation) { + const float delta = 1.0f / static_cast(current_config.fps); + const auto delta_ms = static_cast(delta * 1000); + const auto frame_height = current_config.cat_height; + const auto frame_width = + static_cast(static_cast(frame_height) * (static_cast(current_frames.frame_width) / + static_cast(current_frames.frame_height))); + const auto fmovement_radius = static_cast(current_config.movement_radius); + + assert(current_config.movement_radius > 0); + float max_movement_offset_x_left = 0.0f; + float max_movement_offset_x_right = 0.0f; + float wall_distance = 0.0f; + switch (current_config.cat_align) { + case config::align_type_t::ALIGN_CENTER: + // range: [-r, +r] + max_movement_offset_x_left = + -static_cast(current_config.movement_radius) + static_cast(frame_width) / 2.0f; + max_movement_offset_x_right = static_cast(current_config.movement_radius - frame_width); + wall_distance = anim_shm.movement_offset_x / fmovement_radius; + break; + case config::align_type_t::ALIGN_LEFT: + // range: [0, +2r] + max_movement_offset_x_left = 0.0f; + max_movement_offset_x_right = + static_cast(current_config.movement_radius) * 2.0f - static_cast(frame_width); + wall_distance = (anim_shm.movement_offset_x / (fmovement_radius * 2.0f)) * 2.0f - 1.0f; + break; + case config::align_type_t::ALIGN_RIGHT: + // range: [-2r, 0] + max_movement_offset_x_left = + -(static_cast(current_config.movement_radius) * 2.0f) + static_cast(frame_width); + max_movement_offset_x_right = 0.0f; + wall_distance = (anim_shm.movement_offset_x / (fmovement_radius * 2.0f)) * 2.0f + 1.0f; // normalize to -1 → 1 + break; + } + + if (current_state.row_state == animation_state_row_t::Idle && + (current_state.anim_pause_after_movement_ms > 0 || + (ctx._rng.range(0, 100) <= CHANCE_FOR_SKIPPING_MOVEMENT_PERCENT))) { + // skip movement + new_state.anim_velocity = 0.0f; + new_state.anim_distance = 0.0f; + if (new_state.anim_pause_after_movement_ms > 0) { + new_state.anim_pause_after_movement_ms -= delta_ms; + if (new_state.anim_pause_after_movement_ms <= 0) { + new_state.anim_pause_after_movement_ms = 0; + } + } + ret.status = anim_custom_process_animation_result_status_t::SkipMovement; + } else { + // moving animation + constexpr float DIR_EPSILON = 1e-3f; + assert(MAX_DISTANCE_PER_MOVEMENT_PART > 0); + const int movement_part = current_config.movement_radius <= MAX_MOVEMENT_RADIUS_SMALL + ? SMALL_MAX_DISTANCE_PER_MOVEMENT_PART + : MAX_DISTANCE_PER_MOVEMENT_PART; + const float fmovement_part = static_cast(movement_part); + bool end_movement = false; + if (current_state.row_state == animation_state_row_t::Idle) { + // start movement + const auto min_movement = current_config.movement_radius <= MAX_MOVEMENT_RADIUS_SMALL + ? (fmovement_radius / fmovement_part / 2) + 1 + : (fmovement_radius / fmovement_part / fmovement_part) + 1; + float max_move_distance = fmovement_radius / fmovement_part; + max_move_distance = max_move_distance <= min_movement ? min_movement : max_move_distance; + + assert(max_move_distance >= 0); + assert(min_movement >= 0); + new_state.anim_distance = static_cast( + ctx._rng.range(static_cast(min_movement), static_cast(max_move_distance))); + + if (anim_shm.movement_offset_x >= max_movement_offset_x_right) { + // run against wall, change direction + anim_shm.anim_direction = -1.0; + anim_shm.movement_offset_x = max_movement_offset_x_right; + } else if (anim_shm.movement_offset_x <= max_movement_offset_x_left) { + // run against wall, change direction + anim_shm.anim_direction = 1.0; + anim_shm.movement_offset_x = max_movement_offset_x_left; + } else { + float toward_wall_bias = fabs(wall_distance); + toward_wall_bias = toward_wall_bias >= 1.0f ? 1.0f : toward_wall_bias; + toward_wall_bias = toward_wall_bias <= 0.0f ? 0.0f : toward_wall_bias; + + const int flip_direction_chance = current_config.movement_radius <= MAX_MOVEMENT_RADIUS_SMALL + ? SMALL_FLIP_DIRECTION_NEAR_WALL_PERCENT + : FLIP_DIRECTION_NEAR_WALL_PERCENT; + + assert(toward_wall_bias >= 0.0f); + // change direction: chance drops at center, changes falloff steeper near walls + const auto dir_chance = static_cast(static_cast(100 - flip_direction_chance + 10) * + (1.0f - pow(toward_wall_bias, 1.5f))); + + if (fabs(new_state.anim_last_direction) >= DIR_EPSILON) { + anim_shm.anim_direction = (ctx._rng.range(0, 100) < dir_chance) ? -new_state.anim_last_direction + : new_state.anim_last_direction; + } else if (fabs(anim_shm.anim_direction) < DIR_EPSILON) { + // idle: choose random start direction + anim_shm.anim_direction = (ctx._rng.range(0, 100) < dir_chance) ? 1.0f : -1.0f; + } else { + if (wall_distance > (100.0f / static_cast(flip_direction_chance))) { + anim_shm.anim_direction = (ctx._rng.range(0, 100) < dir_chance) ? -1.0f : 1.0f; + } else if (wall_distance < -(100.0f / static_cast(flip_direction_chance))) { + anim_shm.anim_direction = (ctx._rng.range(0, 100) < dir_chance) ? 1.0f : -1.0f; + } else { + anim_shm.anim_direction = + (ctx._rng.range(0, 100) < dir_chance) ? anim_shm.anim_direction : -anim_shm.anim_direction; + } } - } else { - // movement got disabled, back to idle - if ((current_config.movement_speed <= 0 || current_config.movement_radius <= 0) && conditions.is_moving) { - // back to idle - ret = anim_custom_restart_animation(ctx, animation_state_row_t::Idle, - new_animation_result, new_state, - current_state, current_frames, current_config); - new_state.anim_velocity = 0.0f; - ret.status = anim_custom_process_animation_result_status_t::Stop; + } + // start moving animation + new_state.anim_velocity = anim_shm.anim_direction * static_cast(current_config.movement_speed); + ret = anim_custom_restart_animation(ctx, animation_state_row_t::StartMoving, animation_state_row_t::Moving, + animation_state_row_t::EndMoving, new_animation_result, new_state, + current_state, current_frames, current_config); + } else if (current_state.row_state == animation_state_row_t::StartMoving) { + if (conditions.process_idle_animation) { + ret = anim_custom_start_or_process_animation(ctx, animation_state_row_t::Moving, + animation_state_row_t::EndMoving, new_animation_result, + new_state, current_state, current_frames, current_config); + ret.status = anim_custom_process_animation_result_status_t::Updated; + } else if (conditions.process_movement_animation) { + ret = anim_custom_restart_animation(ctx, animation_state_row_t::Moving, animation_state_row_t::EndMoving, + animation_state_row_t::Idle, new_animation_result, new_state, + current_state, current_frames, current_config); + ret.status = anim_custom_process_animation_result_status_t::Updated; + } + } else if (current_state.row_state == animation_state_row_t::Moving) { + if (conditions.process_movement) { + ret = anim_custom_process_animation(new_animation_result, new_state, current_state, current_frames); + ret.status = anim_custom_process_animation_result_status_t::Moved; + + new_state.anim_distance -= fabs(new_state.anim_velocity); + anim_shm.movement_offset_x += new_state.anim_velocity; + // clamp walking/movement area + if (anim_shm.movement_offset_x > max_movement_offset_x_right) { + anim_shm.movement_offset_x = max_movement_offset_x_right; + ret = anim_custom_restart_animation(ctx, animation_state_row_t::EndMoving, animation_state_row_t::Idle, + animation_state_row_t::Idle, new_animation_result, new_state, + current_state, current_frames, current_config); + ret.status = anim_custom_process_animation_result_status_t::Stop; + } else if (anim_shm.movement_offset_x < max_movement_offset_x_left) { + anim_shm.movement_offset_x = max_movement_offset_x_left; + ret = anim_custom_restart_animation(ctx, animation_state_row_t::EndMoving, animation_state_row_t::Idle, + animation_state_row_t::Idle, new_animation_result, new_state, + current_state, current_frames, current_config); + ret.status = anim_custom_process_animation_result_status_t::Stop; + } + + if (new_state.anim_distance <= 0 || + (fabs(new_state.anim_distance) < DIR_EPSILON || fabs(new_state.anim_velocity) < DIR_EPSILON)) { + ret = anim_custom_restart_animation(ctx, animation_state_row_t::EndMoving, animation_state_row_t::Idle, + animation_state_row_t::Idle, new_animation_result, new_state, + current_state, current_frames, current_config); + ret.status = anim_custom_process_animation_result_status_t::Stop; } + + end_movement = ret.status == anim_custom_process_animation_result_status_t::Stop; + } + } else if (current_state.row_state == animation_state_row_t::EndMoving) { + end_movement = true; } + if (end_movement) { + if (conditions.process_idle_animation) { + ret = anim_custom_start_or_process_animation(ctx, animation_state_row_t::Idle, new_animation_result, + new_state, current_state, current_frames, current_config); + } else { + ret = anim_custom_restart_animation(ctx, animation_state_row_t::Idle, new_animation_result, new_state, + current_state, current_frames, current_config); + } + new_state.anim_last_direction = anim_shm.anim_direction; + new_state.anim_velocity = 0.0f; + anim_shm.anim_direction = 0.0; + if (ret.row_state == animation_state_row_t::Idle && new_state.anim_distance <= 0) { + assert(current_config.animation_speed_ms >= 0); + assert(movement_part >= 0); + const auto min_wait = current_config.animation_speed_ms * (current_config.movement_wait_factor / 2); + const auto max_wait = current_config.animation_speed_ms * current_config.movement_wait_factor; + assert(min_wait >= 0); + assert(max_wait >= 0); + new_state.anim_pause_after_movement_ms = + static_cast(ctx._rng.range(static_cast(min_wait), static_cast(max_wait))); + ret.status = anim_custom_process_animation_result_status_t::End; + } + } + } + } + } else { + // movement got disabled, back to idle + if ((current_config.movement_speed <= 0 || current_config.movement_radius <= 0) && conditions.is_moving) { + // back to idle + ret = anim_custom_restart_animation(ctx, animation_state_row_t::Idle, new_animation_result, new_state, + current_state, current_frames, current_config); + new_state.anim_velocity = 0.0f; + ret.status = anim_custom_process_animation_result_status_t::Stop; + } + } - return ret; + return ret; +} +static anim_next_frame_result_t +anim_custom_idle_next_frame(animation_thread_context_t& ctx, const platform::input::input_context_t& input, + [[maybe_unused]] const platform::update::update_context_t& upd, animation_state_t& state, + const anim_handle_key_press_result_t& trigger_result) { + using namespace assets; + + // read-only config + assert(ctx._local_copy_config); + const config::config_t& current_config = *ctx._local_copy_config; + + assert(ctx.shm != BONGOCAT_NULLPTR); + assert(input.shm != BONGOCAT_NULLPTR); + assert(upd.shm != BONGOCAT_NULLPTR); + animation_shared_memory_t& anim_shm = *ctx.shm; + const auto& input_shm = *input.shm; + const auto& update_shm = *upd.shm; + const auto current_state = state; + const auto& current_animation_result = anim_shm.animation_player_result; + [[maybe_unused]] const int anim_index = anim_shm.anim_index; + const platform::timestamp_ms_t last_key_pressed_timestamp = input_shm.last_key_pressed_timestamp; + assert(get_current_animation(ctx).type == animation_t::type_t::Custom); + const auto& current_frames = get_current_animation(ctx).custom; + + auto new_animation_result = anim_shm.animation_player_result; + auto new_state = state; + + const auto conditions = get_anim_conditions(ctx, input, upd, current_state, trigger_result.trigger, current_config); + + /// @TODO: make animation fsm + + const platform::timestamp_ms_t now = platform::get_current_time_ms(); + const platform::time_ms_t idle_sleep_timeout_ms = current_config.idle_sleep_timeout_sec * 1000L; + assert(now >= last_key_pressed_timestamp); + const auto sleep_timeout = now - last_key_pressed_timestamp; + + const bool start_boring = current_frames.feature_boring && SLEEP_BORING_PART > 0 && + sleep_timeout >= idle_sleep_timeout_ms / SLEEP_BORING_PART; + + switch (current_state.row_state) { + case animation_state_row_t::Happy: + if (current_frames.feature_writing_happy) { + if (current_frames.feature_writing_toggle_frames && current_frames.happy.end_col == 0) { + // animation for + const bool stop_happy_kpm = current_state.row_state == animation_state_row_t::Happy && + conditions.release_frame_after_press && !conditions.process_idle_animation; + if (stop_happy_kpm) { + // back to idle + anim_custom_restart_animation(ctx, animation_state_row_t::Idle, new_animation_result, new_state, + current_state, current_frames, current_config); + } + } + if (new_state.row_state == animation_state_row_t::Happy) { + if (conditions.go_next_frame) { + if (conditions.continue_writing) { + anim_custom_start_or_process_animation(ctx, animation_state_row_t::StartWorking, + animation_state_row_t::Working, new_animation_result, new_state, + current_state, current_frames, current_config); + } else { + anim_custom_start_or_process_animation(ctx, animation_state_row_t::Idle, new_animation_result, new_state, + current_state, current_frames, current_config); + } + } + } + } else { + anim_custom_restart_animation(ctx, animation_state_row_t::Idle, new_animation_result, new_state, current_state, + current_frames, current_config); + } + break; + case animation_state_row_t::StartMoving: + case animation_state_row_t::Moving: + case animation_state_row_t::EndMoving: + if (current_frames.feature_moving) { + anim_custom_handle_movement(ctx, input, upd, new_animation_result, new_state, trigger_result, current_state, + current_frames, current_config); + } + break; + case animation_state_row_t::StartWorking: + if (current_frames.feature_working) { + if (conditions.go_next_frame) { + anim_custom_start_or_process_animation(ctx, animation_state_row_t::Working, animation_state_row_t::EndWorking, + new_animation_result, new_state, current_state, current_frames, + current_config); + } + } else { + anim_custom_restart_animation(ctx, animation_state_row_t::Idle, new_animation_result, new_state, current_state, + current_frames, current_config); + } + break; + case animation_state_row_t::Working: + if (current_frames.feature_working) { + if (conditions.go_next_frame) { + if (update_shm.cpu_active) { + anim_custom_process_animation(new_animation_result, new_state, current_state, current_frames); + } else { + anim_custom_start_or_process_animation(ctx, animation_state_row_t::EndWorking, animation_state_row_t::Idle, + new_animation_result, new_state, current_state, current_frames, + current_config); + } + } + } else { + anim_custom_restart_animation(ctx, animation_state_row_t::Idle, new_animation_result, new_state, current_state, + current_frames, current_config); + } + break; + case animation_state_row_t::EndWorking: + if (current_frames.feature_working) { + if (conditions.go_next_frame) { + anim_custom_start_or_process_animation(ctx, animation_state_row_t::Idle, new_animation_result, new_state, + current_state, current_frames, current_config); + } + } else { + anim_custom_restart_animation(ctx, animation_state_row_t::Idle, new_animation_result, new_state, current_state, + current_frames, current_config); + } + break; + case animation_state_row_t::Test: + if (current_frames.feature_writing_toggle_frames) { + const bool stop_test_animation = conditions.trigger_test_animation && + current_state.row_state == animation_state_row_t::Test && + conditions.release_test_frame; + if (stop_test_animation) { + // back to idle + anim_custom_restart_animation(ctx, animation_state_row_t::Idle, new_animation_result, new_state, current_state, + current_frames, current_config); + } + } else { + if (current_config.idle_animation >= 1 && conditions.go_next_frame) { + anim_custom_process_animation(new_animation_result, new_state, current_state, current_frames); + } + } + break; + case animation_state_row_t::Idle: { + if (current_config.idle_animation >= 1 && conditions.go_next_frame) { + anim_custom_process_animation(new_animation_result, new_state, current_state, current_frames); } - static anim_next_frame_result_t anim_custom_idle_next_frame(animation_context_t& ctx, const platform::input::input_context_t& input, - [[maybe_unused]] const platform::update::update_context_t& upd, - animation_state_t& state, const anim_handle_key_press_result_t& trigger_result) { - using namespace assets; - - // read-only config - assert(ctx._local_copy_config != nullptr); - const config::config_t& current_config = *ctx._local_copy_config; - - assert(ctx.shm != nullptr); - assert(input.shm != nullptr); - assert(upd.shm != nullptr); - animation_shared_memory_t& anim_shm = *ctx.shm; - const auto& input_shm = *input.shm; - const auto& update_shm = *upd.shm; - const auto current_state = state; - const auto& current_animation_result = anim_shm.animation_player_result; - [[maybe_unused]] const int anim_index = anim_shm.anim_index; - const platform::timestamp_ms_t last_key_pressed_timestamp = input_shm.last_key_pressed_timestamp; - assert(get_current_animation(ctx).type == animation_t::Type::Custom); - const auto& current_frames = get_current_animation(ctx).custom; - - auto new_animation_result = anim_shm.animation_player_result; - auto new_state = state; - - const auto conditions = get_anim_conditions(ctx, input, upd, current_state, trigger_result.trigger, current_config); - - /// @TODO: make animation fsm - - const platform::timestamp_ms_t now = platform::get_current_time_ms(); - const platform::time_ms_t idle_sleep_timeout_ms = current_config.idle_sleep_timeout_sec*1000; - assert(now >= last_key_pressed_timestamp); - const auto sleep_timeout = now - last_key_pressed_timestamp; - - const bool start_boring = current_frames.feature_boring && SLEEP_BORING_PART > 0 && sleep_timeout >= idle_sleep_timeout_ms/SLEEP_BORING_PART; - - switch (current_state.row_state) { - case animation_state_row_t::Happy: - if (current_frames.feature_writing_happy) { - if (current_frames.feature_writing_toggle_frames && current_frames.happy.end_col == 0) { - // animation for - const bool stop_happy_kpm = current_state.row_state == animation_state_row_t::Happy && conditions.release_frame_after_press && !conditions.process_idle_animation; - if (stop_happy_kpm) { - // back to idle - anim_custom_restart_animation(ctx, animation_state_row_t::Idle, - new_animation_result, new_state, - current_state, current_frames, current_config); - } - } - if (new_state.row_state == animation_state_row_t::Happy) { - if (conditions.go_next_frame) { - if (conditions.continue_writing) { - anim_custom_start_or_process_animation(ctx, animation_state_row_t::StartWorking, animation_state_row_t::Working, - new_animation_result, new_state, - current_state, current_frames, current_config); - } else { - anim_custom_start_or_process_animation(ctx, animation_state_row_t::Idle, - new_animation_result, new_state, - current_state, current_frames, current_config); - } - } - } - } else { - anim_custom_restart_animation(ctx, animation_state_row_t::Idle, - new_animation_result, new_state, - current_state, current_frames, current_config); - } - break; - case animation_state_row_t::StartMoving: - case animation_state_row_t::Moving: - case animation_state_row_t::EndMoving: - if (current_frames.feature_moving) { - anim_custom_handle_movement(ctx, input, upd, new_animation_result, new_state, trigger_result, current_state, current_frames, current_config); - } - break; - case animation_state_row_t::StartWorking: - if (current_frames.feature_working) { - if (conditions.go_next_frame) { - anim_custom_start_or_process_animation(ctx, animation_state_row_t::Working, animation_state_row_t::EndWorking, - new_animation_result, new_state, - current_state, current_frames, current_config); - } - } else { - anim_custom_restart_animation(ctx, animation_state_row_t::Idle, - new_animation_result, new_state, - current_state, current_frames, current_config); - } - break; - case animation_state_row_t::Working: - if (current_frames.feature_working) { - if (conditions.go_next_frame) { - if (update_shm.cpu_active) { - anim_custom_process_animation(new_animation_result, new_state, current_state, current_frames); - } else { - anim_custom_start_or_process_animation(ctx, animation_state_row_t::EndWorking, animation_state_row_t::Idle, - new_animation_result, new_state, - current_state, current_frames, current_config); - } - } - } else { - anim_custom_restart_animation(ctx, animation_state_row_t::Idle, - new_animation_result, new_state, - current_state, current_frames, current_config); - } - break; - case animation_state_row_t::EndWorking: - if (current_frames.feature_working) { - if (conditions.go_next_frame) { - anim_custom_start_or_process_animation(ctx, animation_state_row_t::Idle, - new_animation_result, new_state, - current_state, current_frames, current_config); - } - } else { - anim_custom_restart_animation(ctx, animation_state_row_t::Idle, - new_animation_result, new_state, - current_state, current_frames, current_config); - } - break; - case animation_state_row_t::Test: - if (current_frames.feature_writing_toggle_frames) { - const bool stop_test_animation = conditions.trigger_test_animation && current_state.row_state == animation_state_row_t::Test && conditions.release_test_frame; - if (stop_test_animation) { - // back to idle - anim_custom_restart_animation(ctx, animation_state_row_t::Idle, - new_animation_result, new_state, - current_state, current_frames, current_config); - } - } else { - if (current_config.idle_animation && conditions.go_next_frame) { - anim_custom_process_animation(new_animation_result, new_state, current_state, current_frames); - } - } - break; - case animation_state_row_t::Idle: { - if (current_config.idle_animation && conditions.go_next_frame) { - anim_custom_process_animation(new_animation_result, new_state, current_state, current_frames); - } - if (current_frames.feature_moving) { - // Move Animation - anim_custom_handle_movement(ctx, input, upd, new_animation_result, new_state, trigger_result, current_state, current_frames, current_config); - } + if (current_frames.feature_moving) { + // Move Animation + anim_custom_handle_movement(ctx, input, upd, new_animation_result, new_state, trigger_result, current_state, + current_frames, current_config); + } - if (current_frames.feature_sleep || current_frames.feature_boring) { - // handle sleep - const bool is_sleeping_time = current_config.enable_scheduled_sleep && is_sleep_time(current_config); - - // Idle Sleep - if (conditions.check_for_idle_sleep) { - if (!is_sleeping_time) { - if (current_state.row_state == animation_state_row_t::Idle) { - // start boring animation - if (current_frames.feature_boring) { - if (start_boring && !current_state.show_boring_animation_once) { - anim_custom_restart_animation(ctx, animation_state_row_t::Boring, - new_animation_result, new_state, - current_state, current_frames, current_config); - new_state.show_boring_animation_once = true; - new_state.anim_last_direction = 0.0f; - } - } - } - - // idle sleep - if (current_frames.feature_sleep) { - if (sleep_timeout >= idle_sleep_timeout_ms) { - if (current_state.row_state == animation_state_row_t::Idle || conditions.is_moving) { - anim_custom_process_animation_result_t animation_result{ .row_state = current_state.row_state }; - if (conditions.is_moving) { - if (conditions.go_next_frame) { - animation_result = anim_custom_start_or_process_animation(ctx, animation_state_row_t::FallASleep, animation_state_row_t::Sleep, - new_animation_result, new_state, - current_state, current_frames, current_config); - } - } else { - animation_result = anim_custom_restart_animation(ctx, animation_state_row_t::FallASleep, animation_state_row_t::Sleep, animation_state_row_t::WakeUp, - new_animation_result, new_state, - current_state, current_frames, current_config); - } - if (animation_result.row_state == animation_state_row_t::FallASleep || animation_result.row_state == animation_state_row_t::Sleep || animation_result.row_state == animation_state_row_t::WakeUp) { - new_state.anim_last_direction = 0.0f; - new_state.is_idle_sleep = true; - new_state.show_boring_animation_once = false; - } - } else if (current_state.row_state == animation_state_row_t::Sleep) { - if (conditions.go_next_frame) { - // process sleep animation - anim_custom_process_animation(new_animation_result, new_state, current_state, current_frames); - } - } - } else { - if (current_state.row_state == animation_state_row_t::Sleep && current_state.is_idle_sleep) { - // wake up - if (conditions.go_next_frame) { - // end current sleep animation - anim_custom_process_animation_result_t animation_result{ .row_state = current_state.row_state }; - if (current_frames.feature_sleep_wake_up) { - animation_result = anim_custom_start_or_process_animation(ctx, animation_state_row_t::WakeUp, animation_state_row_t::Idle, - new_animation_result, new_state, - current_state, current_frames, current_config); - } else { - // no wake up animation - animation_result = anim_custom_start_or_process_animation(ctx, animation_state_row_t::Idle, - new_animation_result, new_state, - current_state, current_frames, current_config); - } - if (animation_result.row_state == animation_state_row_t::WakeUp || animation_result.row_state == animation_state_row_t::Idle) { - new_state.is_idle_sleep = false; - } - } - } - } - } - } - } - - // Sleep Mode - if (current_frames.feature_sleep) { - if (current_config.enable_scheduled_sleep) { - if (is_sleeping_time) { - if (new_state.row_state == animation_state_row_t::Idle) { - anim_custom_restart_animation(ctx, animation_state_row_t::FallASleep, animation_state_row_t::Sleep, animation_state_row_t::WakeUp, - new_animation_result, new_state, - current_state, current_frames, current_config); - new_state.anim_last_direction = 0.0f; - new_state.is_idle_sleep = false; - } else if (state.row_state == animation_state_row_t::Sleep && !new_state.is_idle_sleep) { - if (conditions.go_next_frame) { - anim_custom_process_animation(new_animation_result, new_state, current_state, current_frames); - } - } - } else { - if (current_state.row_state == animation_state_row_t::Sleep && !new_state.is_idle_sleep) { - // wake up - if (conditions.go_next_frame) { - // end current sleep animation - anim_custom_start_or_process_animation(ctx, animation_state_row_t::WakeUp, animation_state_row_t::Idle, - new_animation_result, new_state, - current_state, current_frames, current_config); - } - } - } - } else { - if (current_state.row_state == animation_state_row_t::Sleep && !new_state.is_idle_sleep) { - // wake up - if (conditions.go_next_frame) { - if (current_frames.feature_sleep_wake_up) { - // end current sleep animation - anim_custom_start_or_process_animation(ctx, animation_state_row_t::WakeUp, animation_state_row_t::Idle, - new_animation_result, new_state, - current_state, current_frames, current_config); - } else { - // no wake up animation - anim_custom_start_or_process_animation(ctx, animation_state_row_t::Idle, - new_animation_result, new_state, - current_state, current_frames, current_config); - } - } - } - } - } - } - }break; - case animation_state_row_t::Writing: { - if (current_frames.feature_writing) { - if (conditions.continue_writing) { - if (current_frames.feature_writing_toggle_frames) { - // back to Idle Animation (after writing) - const bool stop_writing = conditions.is_writing && conditions.release_frame_after_press; - const bool stop_happy_kpm = current_state.row_state == animation_state_row_t::Happy && conditions.release_frame_after_press && !conditions.process_idle_animation; - if (stop_writing || stop_happy_kpm) { - // back to idle - anim_custom_restart_animation(ctx, animation_state_row_t::Idle, - new_animation_result, new_state, - current_state, current_frames, current_config); - } - } else { - if (conditions.go_next_frame) { - // loop writing animation - const auto animation_result = anim_custom_process_animation(new_animation_result, new_state, current_state, current_frames); - if ((animation_result.status == anim_custom_process_animation_result_status_t::End || animation_result.status == anim_custom_process_animation_result_status_t::Looped) && !conditions.any_key_pressed) { - // end writing - anim_custom_restart_animation(ctx, animation_state_row_t::EndWriting, animation_state_row_t::Idle, animation_state_row_t::Idle, - new_animation_result, new_state, - current_state, current_frames, current_config); - } - } - } - } else { - // cancel writing - if (current_frames.feature_writing_toggle_frames) { - if (conditions.release_frame_after_press) { - anim_custom_restart_animation(ctx, animation_state_row_t::EndWriting, animation_state_row_t::Idle, animation_state_row_t::Idle, - new_animation_result, new_state, - current_state, current_frames, current_config); - } - } else { - if (conditions.go_next_frame) { - anim_custom_start_or_process_animation(ctx, animation_state_row_t::EndWriting, animation_state_row_t::Idle, - new_animation_result, new_state, - current_state, current_frames, current_config); - } - } - } - } else { - anim_custom_restart_animation(ctx, animation_state_row_t::Idle, - new_animation_result, new_state, - current_state, current_frames, current_config); - } - }break; - case animation_state_row_t::StartWriting: - if (current_frames.feature_writing) { - if (conditions.go_next_frame) { - const auto animation_result = anim_custom_start_or_process_animation(ctx, animation_state_row_t::Writing, animation_state_row_t::EndWriting, - new_animation_result, new_state, - current_state, current_frames, current_config); - if (animation_result.row_state == animation_state_row_t::Writing || animation_result.row_state == animation_state_row_t::EndWriting) { - // reset release counter after writing is started (for real) - new_state.hold_frame_after_release = true; - new_state.hold_frame_ms = 0; - } - } - } else { - anim_custom_restart_animation(ctx, animation_state_row_t::Idle, - new_animation_result, new_state, - current_state, current_frames, current_config); - } - break; - case animation_state_row_t::EndWriting: - if (current_frames.feature_writing) { - if (conditions.go_next_frame) { - // finish animation - anim_custom_start_or_process_animation(ctx, animation_state_row_t::Idle, // back to idle - new_animation_result, new_state, - current_state, current_frames, current_config); - } - } else { - anim_custom_restart_animation(ctx, animation_state_row_t::Idle, - new_animation_result, new_state, - current_state, current_frames, current_config); - } - break; - case animation_state_row_t::Sleep: - if (current_frames.feature_sleep) { - if (conditions.go_next_frame) { - anim_custom_process_animation(new_animation_result, new_state, current_state, current_frames); - } - } else { - anim_custom_restart_animation(ctx, animation_state_row_t::Idle, - new_animation_result, new_state, - current_state, current_frames, current_config); - } - break; - case animation_state_row_t::FallASleep: - if (current_frames.feature_sleep) { - if (conditions.go_next_frame) { - anim_custom_start_or_process_animation(ctx, animation_state_row_t::Sleep, animation_state_row_t::WakeUp, - new_animation_result, new_state, - current_state, current_frames, current_config); - } - } else { - anim_custom_restart_animation(ctx, animation_state_row_t::Idle, - new_animation_result, new_state, - current_state, current_frames, current_config); - } - break; - case animation_state_row_t::WakeUp: - if (current_frames.feature_sleep_wake_up) { - if (conditions.go_next_frame) { - // finish animation - anim_custom_start_or_process_animation(ctx, animation_state_row_t::Idle, // back to idle - new_animation_result, new_state, - current_state, current_frames, current_config); - } + if (current_frames.feature_sleep || current_frames.feature_boring) { + // handle sleep + const bool is_sleeping_time = current_config.enable_scheduled_sleep >= 1 && is_sleep_time(current_config); + + // Idle Sleep + if (conditions.check_for_idle_sleep) { + if (!is_sleeping_time) { + if (current_state.row_state == animation_state_row_t::Idle) { + // start boring animation + if (current_frames.feature_boring) { + if (start_boring && !current_state.show_boring_animation_once) { + anim_custom_restart_animation(ctx, animation_state_row_t::Boring, new_animation_result, new_state, + current_state, current_frames, current_config); + new_state.show_boring_animation_once = true; + new_state.anim_last_direction = 0.0f; + } + } + } + + // idle sleep + if (current_frames.feature_sleep) { + if (sleep_timeout >= idle_sleep_timeout_ms) { + if (current_state.row_state == animation_state_row_t::Idle || conditions.is_moving) { + anim_custom_process_animation_result_t animation_result{.row_state = current_state.row_state}; + if (conditions.is_moving) { + if (conditions.go_next_frame) { + animation_result = anim_custom_start_or_process_animation( + ctx, animation_state_row_t::FallASleep, animation_state_row_t::Sleep, new_animation_result, + new_state, current_state, current_frames, current_config); + } } else { - anim_custom_restart_animation(ctx, animation_state_row_t::Idle, - new_animation_result, new_state, - current_state, current_frames, current_config); - } - break; - case animation_state_row_t::Boring: - if (current_frames.feature_boring) { - if (conditions.go_next_frame) { - anim_custom_start_or_process_animation(ctx, animation_state_row_t::Idle, // back to idle, when animation ended - new_animation_result, new_state, - current_state, current_frames, current_config); - if (!start_boring) { - new_state.show_boring_animation_once = false; - } - } + animation_result = anim_custom_restart_animation( + ctx, animation_state_row_t::FallASleep, animation_state_row_t::Sleep, + animation_state_row_t::WakeUp, new_animation_result, new_state, current_state, current_frames, + current_config); } - break; - case animation_state_row_t::StartRunning: - if (current_frames.feature_running) { - if (conditions.go_next_frame_running) { - anim_custom_start_or_process_animation(ctx, animation_state_row_t::Running, animation_state_row_t::EndRunning, - new_animation_result, new_state, - current_state, current_frames, current_config); - } - } else { - anim_custom_restart_animation(ctx, animation_state_row_t::Idle, - new_animation_result, new_state, - current_state, current_frames, current_config); + if (animation_result.row_state == animation_state_row_t::FallASleep || + animation_result.row_state == animation_state_row_t::Sleep || + animation_result.row_state == animation_state_row_t::WakeUp) { + new_state.anim_last_direction = 0.0f; + new_state.is_idle_sleep = true; + new_state.show_boring_animation_once = false; } - break; - case animation_state_row_t::Running: - if (current_frames.feature_running) { - if (conditions.go_next_frame_running) { - if (update_shm.cpu_active) { - anim_custom_process_animation(new_animation_result, new_state, current_state, current_frames); - } else { - anim_custom_start_or_process_animation(ctx, animation_state_row_t::EndRunning, animation_state_row_t::Idle, - new_animation_result, new_state, - current_state, current_frames, current_config); - } - } - } else { - anim_custom_restart_animation(ctx, animation_state_row_t::Idle, - new_animation_result, new_state, - current_state, current_frames, current_config); + } else if (current_state.row_state == animation_state_row_t::Sleep) { + if (conditions.go_next_frame) { + // process sleep animation + anim_custom_process_animation(new_animation_result, new_state, current_state, current_frames); } - break; - case animation_state_row_t::EndRunning: - if (current_frames.feature_running) { - if (conditions.go_next_frame_running) { - anim_custom_start_or_process_animation(ctx, animation_state_row_t::Idle, - new_animation_result, new_state, - current_state, current_frames, current_config); - } - } else { - anim_custom_restart_animation(ctx, animation_state_row_t::Idle, - new_animation_result, new_state, - current_state, current_frames, current_config); + } + } else { + if (current_state.row_state == animation_state_row_t::Sleep && current_state.is_idle_sleep) { + // wake up + if (conditions.go_next_frame) { + // end current sleep animation + anim_custom_process_animation_result_t animation_result{.row_state = current_state.row_state}; + if (current_frames.feature_sleep_wake_up) { + animation_result = anim_custom_start_or_process_animation( + ctx, animation_state_row_t::WakeUp, animation_state_row_t::Idle, new_animation_result, + new_state, current_state, current_frames, current_config); + } else { + // no wake up animation + animation_result = anim_custom_start_or_process_animation( + ctx, animation_state_row_t::Idle, new_animation_result, new_state, current_state, + current_frames, current_config); + } + if (animation_result.row_state == animation_state_row_t::WakeUp || + animation_result.row_state == animation_state_row_t::Idle) { + new_state.is_idle_sleep = false; + } } - break; + } + } + } } + } - return anim_update_animation_state(anim_shm, state, - new_animation_result, new_state, - current_animation_result, current_state, - current_config); + // Sleep Mode + if (current_frames.feature_sleep) { + if (current_config.enable_scheduled_sleep >= 1) { + if (is_sleeping_time) { + if (new_state.row_state == animation_state_row_t::Idle) { + anim_custom_restart_animation(ctx, animation_state_row_t::FallASleep, animation_state_row_t::Sleep, + animation_state_row_t::WakeUp, new_animation_result, new_state, + current_state, current_frames, current_config); + new_state.anim_last_direction = 0.0f; + new_state.is_idle_sleep = false; + } else if (state.row_state == animation_state_row_t::Sleep && !new_state.is_idle_sleep) { + if (conditions.go_next_frame) { + anim_custom_process_animation(new_animation_result, new_state, current_state, current_frames); + } + } + } else { + if (current_state.row_state == animation_state_row_t::Sleep && !new_state.is_idle_sleep) { + // wake up + if (conditions.go_next_frame) { + // end current sleep animation + anim_custom_start_or_process_animation(ctx, animation_state_row_t::WakeUp, animation_state_row_t::Idle, + new_animation_result, new_state, current_state, current_frames, + current_config); + } + } + } + } else { + if (current_state.row_state == animation_state_row_t::Sleep && !new_state.is_idle_sleep) { + // wake up + if (conditions.go_next_frame) { + if (current_frames.feature_sleep_wake_up) { + // end current sleep animation + anim_custom_start_or_process_animation(ctx, animation_state_row_t::WakeUp, animation_state_row_t::Idle, + new_animation_result, new_state, current_state, current_frames, + current_config); + } else { + // no wake up animation + anim_custom_start_or_process_animation(ctx, animation_state_row_t::Idle, new_animation_result, + new_state, current_state, current_frames, current_config); + } + } + } + } + } } - static anim_next_frame_result_t anim_custom_key_pressed_next_frame(animation_context_t& ctx, - animation_state_t& state, - [[maybe_unused]] const platform::input::input_context_t& input, - [[maybe_unused]] const platform::update::update_context_t& upd, - [[maybe_unused]] const animation_trigger_t& trigger) { - using namespace assets; - - // read-only config - assert(ctx._local_copy_config != nullptr); - const config::config_t& current_config = *ctx._local_copy_config; - - assert(ctx.shm != nullptr); - assert(input.shm != nullptr); - animation_shared_memory_t& anim_shm = *ctx.shm; - const auto& input_shm = *input.shm; - const auto current_state = state; - const auto& current_animation_result = anim_shm.animation_player_result; - [[maybe_unused]] const int anim_index = anim_shm.anim_index; - assert(get_current_animation(ctx).type == animation_t::Type::Custom); - const auto& current_frames = get_current_animation(ctx).custom; - - auto new_animation_result = anim_shm.animation_player_result; - auto new_state = state; - - const auto conditions = get_anim_conditions(ctx, input, upd, current_state, trigger, current_config); - - bool show_happy = false; - if (current_frames.feature_writing_happy) { - if (input_shm.kpm > 0) { - if (current_config.happy_kpm > 0 && input_shm.kpm >= current_config.happy_kpm) { - show_happy = CUSTOM_HAPPY_CHANCE_PERCENT >= 100 || ctx._rng.range(0, 99) < CUSTOM_HAPPY_CHANCE_PERCENT; - } + } break; + case animation_state_row_t::Writing: { + if (current_frames.feature_writing) { + if (conditions.continue_writing) { + if (current_frames.feature_writing_toggle_frames) { + // back to Idle Animation (after writing) + const bool stop_writing = conditions.is_writing && conditions.release_frame_after_press; + const bool stop_happy_kpm = current_state.row_state == animation_state_row_t::Happy && + conditions.release_frame_after_press && !conditions.process_idle_animation; + if (stop_writing || stop_happy_kpm) { + // back to idle + anim_custom_restart_animation(ctx, animation_state_row_t::Idle, new_animation_result, new_state, + current_state, current_frames, current_config); + } + } else { + if (conditions.go_next_frame) { + // loop writing animation + const auto animation_result = + anim_custom_process_animation(new_animation_result, new_state, current_state, current_frames); + if ((animation_result.status == anim_custom_process_animation_result_status_t::End || + animation_result.status == anim_custom_process_animation_result_status_t::Looped) && + !conditions.any_key_pressed) { + // end writing + anim_custom_restart_animation(ctx, animation_state_row_t::EndWriting, animation_state_row_t::Idle, + animation_state_row_t::Idle, new_animation_result, new_state, current_state, + current_frames, current_config); } + } } - - // in Writing mode/start writing - switch (current_state.row_state) { - case animation_state_row_t::StartMoving: - case animation_state_row_t::Moving: - case animation_state_row_t::EndMoving: - // moving is handle in anim_custom_handle_movement - break; - case animation_state_row_t::Happy: - // end happy animation in idle - break; - case animation_state_row_t::Test: - case animation_state_row_t::Idle: - if (current_frames.feature_writing_happy && show_happy) { - // show happy animation (KPM) - anim_custom_restart_animation(ctx, animation_state_row_t::Happy, animation_state_row_t::Writing, animation_state_row_t::Idle, - new_animation_result, new_state, - current_state, current_frames, current_config); - } else if (current_frames.feature_writing) { - // start writing - const auto animation_result = anim_custom_restart_animation(ctx, animation_state_row_t::StartWriting, animation_state_row_t::Writing, animation_state_row_t::EndWriting, - new_animation_result, new_state, - current_state, current_frames, current_config); - if (current_frames.feature_writing_toggle_frames_random && - animation_result.row_state == animation_state_row_t::Writing && (animation_result.status == anim_custom_process_animation_result_status_t::Started || animation_result.status == anim_custom_process_animation_result_status_t::NextAnimationStarted || animation_result.status == anim_custom_process_animation_result_status_t::Looped) && - current_frames.writing.valid && current_frames.writing.start_col >= 0 && current_frames.writing.end_col >= 0) { - new_animation_result.sprite_sheet_col = static_cast(ctx._rng.range(static_cast(current_frames.writing.start_col), static_cast(current_frames.writing.end_col))); - } - if (animation_result.row_state == animation_state_row_t::Writing || animation_result.row_state == animation_state_row_t::EndWriting) { - // reset release counter after writing is started (for real) - new_state.hold_frame_after_release = true; - new_state.hold_frame_ms = 0; - } - } - break; - case animation_state_row_t::StartWriting: - // start end writing and process animation in anim_custom_idle_next_frame - break; - case animation_state_row_t::Writing: - if (current_frames.feature_writing) { - if (current_frames.feature_writing_happy && show_happy && conditions.is_writing) { - // show happy animation (KPM) - anim_custom_restart_animation(ctx, animation_state_row_t::Happy, animation_state_row_t::Writing, animation_state_row_t::Idle, - new_animation_result, new_state, - current_state, current_frames, current_config); - new_state.hold_frame_ms = 0; - } else if (current_frames.feature_writing_happy && current_state.row_state == animation_state_row_t::Happy) { - // wait for happy animation to end - new_state.hold_frame_ms = 0; - } else if (current_frames.feature_writing_toggle_frames) { - // keep writing - anim_custom_process_animation(new_animation_result, new_state, current_state, current_frames); - } else { - // reset hold frame so we can continue writing - new_state.hold_frame_ms = 0; - // start end writing and process animation in anim_custom_idle_next_frame - } - } - break; - case animation_state_row_t::EndWriting: - // start end writing and process animation in anim_custom_idle_next_frame - break; - case animation_state_row_t::FallASleep: - case animation_state_row_t::Sleep: - if (current_state.is_idle_sleep) { - anim_custom_process_animation_result_t animation_result{.row_state = current_state.row_state}; - if (current_frames.feature_sleep_wake_up) { - // wake up, end current sleep animation - animation_result = anim_custom_restart_animation(ctx, animation_state_row_t::WakeUp, animation_state_row_t::Idle, animation_state_row_t::Idle, - new_animation_result, new_state, - current_state, current_frames, current_config); - } else { - // no wake up animation - animation_result = anim_custom_restart_animation(ctx, animation_state_row_t::Idle, - new_animation_result, new_state, - current_state, current_frames, current_config); - } - if (animation_result.row_state != animation_state_row_t::Sleep) { - new_state.is_idle_sleep = false; - } - } - case animation_state_row_t::WakeUp: - // process animation in anim_custom_idle_next_frame - break; - case animation_state_row_t::Boring: - // process animation in anim_custom_idle_next_frame - break; - case animation_state_row_t::StartWorking: - case animation_state_row_t::Working: - case animation_state_row_t::EndWorking: - // start end working and process animation in anim_custom_idle_next_frame - break; - case animation_state_row_t::StartRunning: - case animation_state_row_t::Running: - case animation_state_row_t::EndRunning: - // start end running and process animation in anim_custom_idle_next_frame - break; + } else { + // cancel writing + if (current_frames.feature_writing_toggle_frames) { + if (conditions.release_frame_after_press) { + anim_custom_restart_animation(ctx, animation_state_row_t::EndWriting, animation_state_row_t::Idle, + animation_state_row_t::Idle, new_animation_result, new_state, current_state, + current_frames, current_config); + } + } else { + if (conditions.go_next_frame) { + anim_custom_start_or_process_animation(ctx, animation_state_row_t::EndWriting, animation_state_row_t::Idle, + new_animation_result, new_state, current_state, current_frames, + current_config); + } + } + } + } else { + anim_custom_restart_animation(ctx, animation_state_row_t::Idle, new_animation_result, new_state, current_state, + current_frames, current_config); + } + } break; + case animation_state_row_t::StartWriting: + if (current_frames.feature_writing) { + if (conditions.go_next_frame) { + const auto animation_result = anim_custom_start_or_process_animation( + ctx, animation_state_row_t::Writing, animation_state_row_t::EndWriting, new_animation_result, new_state, + current_state, current_frames, current_config); + if (animation_result.row_state == animation_state_row_t::Writing || + animation_result.row_state == animation_state_row_t::EndWriting) { + // reset release counter after writing is started (for real) + new_state.hold_frame_after_release = true; + new_state.hold_frame_ms = 0; + } + } + } else { + anim_custom_restart_animation(ctx, animation_state_row_t::Idle, new_animation_result, new_state, current_state, + current_frames, current_config); + } + break; + case animation_state_row_t::EndWriting: + if (current_frames.feature_writing) { + if (conditions.go_next_frame) { + // finish animation + anim_custom_start_or_process_animation(ctx, animation_state_row_t::Idle, // back to idle + new_animation_result, new_state, current_state, current_frames, + current_config); + } + } else { + anim_custom_restart_animation(ctx, animation_state_row_t::Idle, new_animation_result, new_state, current_state, + current_frames, current_config); + } + break; + case animation_state_row_t::Sleep: + if (current_frames.feature_sleep) { + if (conditions.go_next_frame) { + anim_custom_process_animation(new_animation_result, new_state, current_state, current_frames); + } + } else { + anim_custom_restart_animation(ctx, animation_state_row_t::Idle, new_animation_result, new_state, current_state, + current_frames, current_config); + } + break; + case animation_state_row_t::FallASleep: + if (current_frames.feature_sleep) { + if (conditions.go_next_frame) { + anim_custom_start_or_process_animation(ctx, animation_state_row_t::Sleep, animation_state_row_t::WakeUp, + new_animation_result, new_state, current_state, current_frames, + current_config); + } + } else { + anim_custom_restart_animation(ctx, animation_state_row_t::Idle, new_animation_result, new_state, current_state, + current_frames, current_config); + } + break; + case animation_state_row_t::WakeUp: + if (current_frames.feature_sleep_wake_up) { + if (conditions.go_next_frame) { + // finish animation + anim_custom_start_or_process_animation(ctx, animation_state_row_t::Idle, // back to idle + new_animation_result, new_state, current_state, current_frames, + current_config); + } + } else { + anim_custom_restart_animation(ctx, animation_state_row_t::Idle, new_animation_result, new_state, current_state, + current_frames, current_config); + } + break; + case animation_state_row_t::Boring: + if (current_frames.feature_boring) { + if (conditions.go_next_frame) { + anim_custom_start_or_process_animation(ctx, animation_state_row_t::Idle, // back to idle, when animation ended + new_animation_result, new_state, current_state, current_frames, + current_config); + if (!start_boring) { + new_state.show_boring_animation_once = false; + } + } + } + break; + case animation_state_row_t::StartRunning: + if (current_frames.feature_running) { + if (conditions.go_next_frame_running) { + anim_custom_start_or_process_animation(ctx, animation_state_row_t::Running, animation_state_row_t::EndRunning, + new_animation_result, new_state, current_state, current_frames, + current_config); + } + } else { + anim_custom_restart_animation(ctx, animation_state_row_t::Idle, new_animation_result, new_state, current_state, + current_frames, current_config); + } + break; + case animation_state_row_t::Running: + if (current_frames.feature_running) { + if (conditions.go_next_frame_running) { + if (update_shm.cpu_active) { + anim_custom_process_animation(new_animation_result, new_state, current_state, current_frames); + } else { + anim_custom_start_or_process_animation(ctx, animation_state_row_t::EndRunning, animation_state_row_t::Idle, + new_animation_result, new_state, current_state, current_frames, + current_config); } + } + } else { + anim_custom_restart_animation(ctx, animation_state_row_t::Idle, new_animation_result, new_state, current_state, + current_frames, current_config); + } + break; + case animation_state_row_t::EndRunning: + if (current_frames.feature_running) { + if (conditions.go_next_frame_running) { + anim_custom_start_or_process_animation(ctx, animation_state_row_t::Idle, new_animation_result, new_state, + current_state, current_frames, current_config); + } + } else { + anim_custom_restart_animation(ctx, animation_state_row_t::Idle, new_animation_result, new_state, current_state, + current_frames, current_config); + } + break; + } - return anim_update_animation_state(anim_shm, state, - new_animation_result, new_state, - current_animation_result, current_state, - current_config); + return anim_update_animation_state(anim_shm, state, new_animation_result, new_state, current_animation_result, + current_state, current_config); +} +static anim_next_frame_result_t +anim_custom_key_pressed_next_frame(animation_thread_context_t& ctx, animation_state_t& state, + [[maybe_unused]] const platform::input::input_context_t& input, + [[maybe_unused]] const platform::update::update_context_t& upd, + [[maybe_unused]] const animation_trigger_t& trigger) { + using namespace assets; + + // read-only config + assert(ctx._local_copy_config); + const config::config_t& current_config = *ctx._local_copy_config; + + assert(ctx.shm != BONGOCAT_NULLPTR); + assert(input.shm != BONGOCAT_NULLPTR); + animation_shared_memory_t& anim_shm = *ctx.shm; + const auto& input_shm = *input.shm; + const auto current_state = state; + const auto& current_animation_result = anim_shm.animation_player_result; + [[maybe_unused]] const int anim_index = anim_shm.anim_index; + assert(get_current_animation(ctx).type == animation_t::type_t::Custom); + const auto& current_frames = get_current_animation(ctx).custom; + + auto new_animation_result = anim_shm.animation_player_result; + auto new_state = state; + + const auto conditions = get_anim_conditions(ctx, input, upd, current_state, trigger, current_config); + + bool show_happy = false; + if (current_frames.feature_writing_happy) { + if (input_shm.kpm > 0) { + if (current_config.happy_kpm > 0 && input_shm.kpm >= current_config.happy_kpm) { + show_happy = CUSTOM_HAPPY_CHANCE_PERCENT >= 100 || ctx._rng.range(0, 99) < CUSTOM_HAPPY_CHANCE_PERCENT; + } + } + } + + // in Writing mode/start writing + switch (current_state.row_state) { + case animation_state_row_t::StartMoving: + case animation_state_row_t::Moving: + case animation_state_row_t::EndMoving: + // moving is handle in anim_custom_handle_movement + break; + case animation_state_row_t::Happy: + // end happy animation in idle + break; + case animation_state_row_t::Test: + case animation_state_row_t::Idle: + if (current_frames.feature_writing_happy && show_happy) { + // show happy animation (KPM) + anim_custom_restart_animation(ctx, animation_state_row_t::Happy, animation_state_row_t::Writing, + animation_state_row_t::Idle, new_animation_result, new_state, current_state, + current_frames, current_config); + } else if (current_frames.feature_writing) { + // start writing + const auto animation_result = anim_custom_restart_animation( + ctx, animation_state_row_t::StartWriting, animation_state_row_t::Writing, animation_state_row_t::EndWriting, + new_animation_result, new_state, current_state, current_frames, current_config); + if (current_frames.feature_writing_toggle_frames_random && + animation_result.row_state == animation_state_row_t::Writing && + (animation_result.status == anim_custom_process_animation_result_status_t::Started || + animation_result.status == anim_custom_process_animation_result_status_t::NextAnimationStarted || + animation_result.status == anim_custom_process_animation_result_status_t::Looped) && + current_frames.writing.valid && current_frames.writing.start_col >= 0 && + current_frames.writing.end_col >= 0) { + new_animation_result.sprite_sheet_col = + static_cast(ctx._rng.range(static_cast(current_frames.writing.start_col), + static_cast(current_frames.writing.end_col))); + } + if (animation_result.row_state == animation_state_row_t::Writing || + animation_result.row_state == animation_state_row_t::EndWriting) { + // reset release counter after writing is started (for real) + new_state.hold_frame_after_release = true; + new_state.hold_frame_ms = 0; + } + } + break; + case animation_state_row_t::StartWriting: + // start end writing and process animation in anim_custom_idle_next_frame + break; + case animation_state_row_t::Writing: + if (current_frames.feature_writing) { + if (current_frames.feature_writing_happy && show_happy && conditions.is_writing) { + // show happy animation (KPM) + anim_custom_restart_animation(ctx, animation_state_row_t::Happy, animation_state_row_t::Writing, + animation_state_row_t::Idle, new_animation_result, new_state, current_state, + current_frames, current_config); + new_state.hold_frame_ms = 0; + } else if (current_frames.feature_writing_happy && current_state.row_state == animation_state_row_t::Happy) { + // wait for happy animation to end + new_state.hold_frame_ms = 0; + } else if (current_frames.feature_writing_toggle_frames) { + // keep writing + anim_custom_process_animation(new_animation_result, new_state, current_state, current_frames); + } else { + // reset hold frame so we can continue writing + new_state.hold_frame_ms = 0; + // start end writing and process animation in anim_custom_idle_next_frame + } } + break; + case animation_state_row_t::EndWriting: + // start end writing and process animation in anim_custom_idle_next_frame + break; + case animation_state_row_t::FallASleep: + case animation_state_row_t::Sleep: + if (current_state.is_idle_sleep) { + anim_custom_process_animation_result_t animation_result{.row_state = current_state.row_state}; + if (current_frames.feature_sleep_wake_up) { + // wake up, end current sleep animation + animation_result = anim_custom_restart_animation( + ctx, animation_state_row_t::WakeUp, animation_state_row_t::Idle, animation_state_row_t::Idle, + new_animation_result, new_state, current_state, current_frames, current_config); + } else { + // no wake up animation + animation_result = anim_custom_restart_animation(ctx, animation_state_row_t::Idle, new_animation_result, + new_state, current_state, current_frames, current_config); + } + if (animation_result.row_state != animation_state_row_t::Sleep) { + new_state.is_idle_sleep = false; + } + } + case animation_state_row_t::WakeUp: + // process animation in anim_custom_idle_next_frame + break; + case animation_state_row_t::Boring: + // process animation in anim_custom_idle_next_frame + break; + case animation_state_row_t::StartWorking: + case animation_state_row_t::Working: + case animation_state_row_t::EndWorking: + // start end working and process animation in anim_custom_idle_next_frame + break; + case animation_state_row_t::StartRunning: + case animation_state_row_t::Running: + case animation_state_row_t::EndRunning: + // start end running and process animation in anim_custom_idle_next_frame + break; + } + + return anim_update_animation_state(anim_shm, state, new_animation_result, new_state, current_animation_result, + current_state, current_config); +} - static anim_next_frame_result_t anim_custom_working_next_frame(animation_context_t& ctx, +static anim_next_frame_result_t anim_custom_working_next_frame(animation_thread_context_t& ctx, const platform::input::input_context_t& input, const platform::update::update_context_t& upd, animation_state_t& state, const animation_trigger_t& trigger) { - using namespace assets; - - // read-only config - assert(ctx._local_copy_config != nullptr); - const config::config_t& current_config = *ctx._local_copy_config; - - assert(ctx.shm != nullptr); - //assert(input.shm != nullptr); - assert(upd.shm != nullptr); - animation_shared_memory_t& anim_shm = *ctx.shm; - //const auto& input_shm = *input.shm; - const auto& update_shm = *upd.shm; - const auto current_state = state; - const auto& current_animation_result = anim_shm.animation_player_result; - [[maybe_unused]] const int anim_index = anim_shm.anim_index; - //const platform::timestamp_ms_t last_key_pressed_timestamp = input_shm.last_key_pressed_timestamp; - assert(get_current_animation(ctx).type == animation_t::Type::Custom); - const auto& current_frames = get_current_animation(ctx).custom; - - auto new_animation_result = anim_shm.animation_player_result; - auto new_state = state; - - const auto conditions = get_anim_conditions(ctx, input, upd, current_state, trigger, current_config); - - /// @TODO: use state machine for animation (states) - - if (current_frames.feature_working) { - if (conditions.process_working_animation) { - // toggle frame, show attack animation - const bool above_threshold = current_config.cpu_threshold >= platform::ENABLED_MIN_CPU_PERCENT && (update_shm.avg_cpu_usage >= current_config.cpu_threshold || update_shm.max_cpu_usage >= current_config.cpu_threshold); - const bool lower_threshold = current_config.cpu_threshold >= platform::ENABLED_MIN_CPU_PERCENT && (update_shm.avg_cpu_usage < current_config.cpu_threshold && update_shm.max_cpu_usage < current_config.cpu_threshold); - - if (above_threshold) { - if (current_state.row_state == animation_state_row_t::Idle || conditions.is_moving) { - anim_custom_restart_animation(ctx, animation_state_row_t::StartWorking, animation_state_row_t::Working, animation_state_row_t::EndWorking, - new_animation_result, new_state, - current_state, current_frames, current_config); - BONGOCAT_LOG_VERBOSE("Start Working: %d %d; %d%%", above_threshold, lower_threshold, update_shm.avg_cpu_usage); - } else if (conditions.is_working) { - if (update_shm.cpu_active && current_state.row_state == animation_state_row_t::StartWorking) { - anim_custom_start_or_process_animation(ctx, animation_state_row_t::Working, animation_state_row_t::EndWorking, - new_animation_result, new_state, - current_state, current_frames, current_config); - } else if (!update_shm.cpu_active && (current_state.row_state == animation_state_row_t::StartWorking || state.row_state == animation_state_row_t::Working)) { - // end working, cool down - anim_custom_start_or_process_animation(ctx, animation_state_row_t::EndWorking, animation_state_row_t::Idle, - new_animation_result, new_state, - current_state, current_frames, current_config); - } else if (!update_shm.cpu_active && current_state.row_state == animation_state_row_t::EndWorking) { - // back to idle - anim_custom_start_or_process_animation(ctx, animation_state_row_t::Idle, - new_animation_result, new_state, - current_state, current_frames, current_config); - } - } - } else if (lower_threshold) { - // End Working - if (conditions.is_working && current_state.row_state != animation_state_row_t::EndMoving) { - if (conditions.go_next_frame) { - // end working, cool down - anim_custom_start_or_process_animation(ctx, animation_state_row_t::EndWorking, animation_state_row_t::Idle, - new_animation_result, new_state, - current_state, current_frames, current_config); - BONGOCAT_LOG_VERBOSE("Stop Working: %d %d; %d%%", above_threshold, lower_threshold, update_shm.avg_cpu_usage); - } - } - } else { - // Cancel Working - if (conditions.is_working && current_state.row_state != animation_state_row_t::EndMoving && !update_shm.cpu_active) { - // back to idle - anim_custom_start_or_process_animation(ctx, animation_state_row_t::EndMoving, animation_state_row_t::Idle, - new_animation_result, new_state, - current_state, current_frames, current_config); - BONGOCAT_LOG_VERBOSE("Stop Working: %d %d; %d%%", above_threshold, lower_threshold, update_shm.avg_cpu_usage); - } - } - } - } else { - // Cancel Working - if (conditions.is_working && !update_shm.cpu_active) { - // back to idle - anim_custom_start_or_process_animation(ctx, animation_state_row_t::Idle, - new_animation_result, new_state, + using namespace assets; + + // read-only config + assert(ctx._local_copy_config); + const config::config_t& current_config = *ctx._local_copy_config; + + assert(ctx.shm != BONGOCAT_NULLPTR); + // assert(input.shm != nullptr); + assert(upd.shm != BONGOCAT_NULLPTR); + animation_shared_memory_t& anim_shm = *ctx.shm; + // const auto& input_shm = *input.shm; + const auto& update_shm = *upd.shm; + const auto current_state = state; + const auto& current_animation_result = anim_shm.animation_player_result; + [[maybe_unused]] const int anim_index = anim_shm.anim_index; + // const platform::timestamp_ms_t last_key_pressed_timestamp = input_shm.last_key_pressed_timestamp; + assert(get_current_animation(ctx).type == animation_t::type_t::Custom); + const auto& current_frames = get_current_animation(ctx).custom; + + auto new_animation_result = anim_shm.animation_player_result; + auto new_state = state; + + const auto conditions = get_anim_conditions(ctx, input, upd, current_state, trigger, current_config); + + /// @TODO: use state machine for animation (states) + + if (current_frames.feature_working) { + if (conditions.process_working_animation) { + // toggle frame, show attack animation + const bool above_threshold = current_config.cpu_threshold >= platform::ENABLED_MIN_CPU_PERCENT && + (update_shm.avg_cpu_usage >= current_config.cpu_threshold || + update_shm.max_cpu_usage >= current_config.cpu_threshold); + const bool lower_threshold = current_config.cpu_threshold >= platform::ENABLED_MIN_CPU_PERCENT && + (update_shm.avg_cpu_usage < current_config.cpu_threshold && + update_shm.max_cpu_usage < current_config.cpu_threshold); + + if (above_threshold) { + if (current_state.row_state == animation_state_row_t::Idle || conditions.is_moving) { + anim_custom_restart_animation(ctx, animation_state_row_t::StartWorking, animation_state_row_t::Working, + animation_state_row_t::EndWorking, new_animation_result, new_state, current_state, current_frames, current_config); - } + BONGOCAT_LOG_VERBOSE("Start Working: %d %d; %d%%", above_threshold, lower_threshold, + update_shm.avg_cpu_usage); + } else if (conditions.is_working) { + if (update_shm.cpu_active && current_state.row_state == animation_state_row_t::StartWorking) { + anim_custom_start_or_process_animation(ctx, animation_state_row_t::Working, + animation_state_row_t::EndWorking, new_animation_result, new_state, + current_state, current_frames, current_config); + } else if (!update_shm.cpu_active && (current_state.row_state == animation_state_row_t::StartWorking || + state.row_state == animation_state_row_t::Working)) { + // end working, cool down + anim_custom_start_or_process_animation(ctx, animation_state_row_t::EndWorking, animation_state_row_t::Idle, + new_animation_result, new_state, current_state, current_frames, + current_config); + } else if (!update_shm.cpu_active && current_state.row_state == animation_state_row_t::EndWorking) { + // back to idle + anim_custom_start_or_process_animation(ctx, animation_state_row_t::Idle, new_animation_result, new_state, + current_state, current_frames, current_config); + } } - - return anim_update_animation_state(anim_shm, state, - new_animation_result, new_state, - current_animation_result, current_state, - current_config); + } else if (lower_threshold) { + // End Working + if (conditions.is_working && current_state.row_state != animation_state_row_t::EndMoving) { + if (conditions.go_next_frame) { + // end working, cool down + anim_custom_start_or_process_animation(ctx, animation_state_row_t::EndWorking, animation_state_row_t::Idle, + new_animation_result, new_state, current_state, current_frames, + current_config); + BONGOCAT_LOG_VERBOSE("Stop Working: %d %d; %d%%", above_threshold, lower_threshold, + update_shm.avg_cpu_usage); + } + } + } else { + // Cancel Working + if (conditions.is_working && current_state.row_state != animation_state_row_t::EndMoving && + !update_shm.cpu_active) { + // back to idle + anim_custom_start_or_process_animation(ctx, animation_state_row_t::EndMoving, animation_state_row_t::Idle, + new_animation_result, new_state, current_state, current_frames, + current_config); + BONGOCAT_LOG_VERBOSE("Stop Working: %d %d; %d%%", above_threshold, lower_threshold, update_shm.avg_cpu_usage); + } + } } + } else { + // Cancel Working + if (conditions.is_working && !update_shm.cpu_active) { + // back to idle + anim_custom_start_or_process_animation(ctx, animation_state_row_t::Idle, new_animation_result, new_state, + current_state, current_frames, current_config); + } + } - static anim_next_frame_result_t anim_custom_running_next_frame(animation_context_t& ctx, - const platform::input::input_context_t& input, - const platform::update::update_context_t& upd, - animation_state_t& state, - const animation_trigger_t& trigger) { - using namespace assets; - - // read-only config - assert(ctx._local_copy_config != nullptr); - const config::config_t& current_config = *ctx._local_copy_config; - - assert(ctx.shm != nullptr); - //assert(input.shm != nullptr); - assert(upd.shm != nullptr); - animation_shared_memory_t& anim_shm = *ctx.shm; - //const auto& input_shm = *input.shm; - const auto& update_shm = *upd.shm; - const auto current_state = state; - const auto& current_animation_result = anim_shm.animation_player_result; - [[maybe_unused]] const int anim_index = anim_shm.anim_index; - //const platform::timestamp_ms_t last_key_pressed_timestamp = input_shm.last_key_pressed_timestamp; - assert(get_current_animation(ctx).type == animation_t::Type::Custom); - const auto& current_frames = get_current_animation(ctx).custom; - - auto new_animation_result = anim_shm.animation_player_result; - auto new_state = state; - - const auto conditions = get_anim_conditions(ctx, input, upd, current_state, trigger, current_config); - - /// @TODO: use state machine for animation (states) - - if (current_frames.feature_running) { - if (conditions.process_running_animation) { - // toggle frame, show running animation - const bool above_threshold = current_config.cpu_threshold >= platform::ENABLED_MIN_CPU_PERCENT && (update_shm.avg_cpu_usage >= current_config.cpu_threshold || update_shm.max_cpu_usage >= current_config.cpu_threshold); - const bool lower_threshold = current_config.cpu_threshold >= platform::ENABLED_MIN_CPU_PERCENT && (update_shm.avg_cpu_usage < current_config.cpu_threshold && update_shm.max_cpu_usage < current_config.cpu_threshold); - - if (above_threshold) { - if (current_state.row_state == animation_state_row_t::Idle || conditions.is_moving) { - anim_custom_restart_animation(ctx, animation_state_row_t::StartRunning, animation_state_row_t::Running, animation_state_row_t::EndRunning, - new_animation_result, new_state, - current_state, current_frames, current_config); - BONGOCAT_LOG_VERBOSE("Start Running: %d %d; %d%%", above_threshold, lower_threshold, update_shm.avg_cpu_usage); - } else if (conditions.is_running) { - if (update_shm.cpu_active && current_state.row_state == animation_state_row_t::StartRunning) { - anim_custom_start_or_process_animation(ctx, animation_state_row_t::Running, animation_state_row_t::EndRunning, - new_animation_result, new_state, - current_state, current_frames, current_config); - } else if (!update_shm.cpu_active && (current_state.row_state == animation_state_row_t::StartRunning || state.row_state == animation_state_row_t::Running)) { - // end running, cool down - anim_custom_start_or_process_animation(ctx, animation_state_row_t::EndRunning, animation_state_row_t::Running, - new_animation_result, new_state, - current_state, current_frames, current_config); - } else if (!update_shm.cpu_active && current_state.row_state == animation_state_row_t::EndRunning) { - // back to idle - anim_custom_start_or_process_animation(ctx, animation_state_row_t::Idle, - new_animation_result, new_state, - current_state, current_frames, current_config); - } - } - } else if (lower_threshold) { - // End Running - if (conditions.is_running && current_state.row_state != animation_state_row_t::EndRunning) { - if (conditions.go_next_frame_running) { - // end running, cool down - anim_custom_start_or_process_animation(ctx, animation_state_row_t::EndRunning, animation_state_row_t::Idle, - new_animation_result, new_state, - current_state, current_frames, current_config); - BONGOCAT_LOG_VERBOSE("Stop Running: %d %d; %d%%", above_threshold, lower_threshold, update_shm.avg_cpu_usage); - } - } - } else { - // Cancel Running - if (conditions.is_running && current_state.row_state != animation_state_row_t::EndRunning && !update_shm.cpu_active) { - // back to idle - anim_custom_start_or_process_animation(ctx, animation_state_row_t::EndRunning, animation_state_row_t::Idle, - new_animation_result, new_state, - current_state, current_frames, current_config); - BONGOCAT_LOG_VERBOSE("Stop Running: %d %d; %d%%", above_threshold, lower_threshold, update_shm.avg_cpu_usage); - } - } - } - } else { - // Cancel Running - if (conditions.is_running && !update_shm.cpu_active) { - // back to idle - anim_custom_start_or_process_animation(ctx, animation_state_row_t::Idle, - new_animation_result, new_state, + return anim_update_animation_state(anim_shm, state, new_animation_result, new_state, current_animation_result, + current_state, current_config); +} + +static anim_next_frame_result_t anim_custom_running_next_frame(animation_thread_context_t& ctx, + const platform::input::input_context_t& input, + const platform::update::update_context_t& upd, + animation_state_t& state, + const animation_trigger_t& trigger) { + using namespace assets; + + // read-only config + assert(ctx._local_copy_config); + const config::config_t& current_config = *ctx._local_copy_config; + + assert(ctx.shm != BONGOCAT_NULLPTR); + // assert(input.shm != nullptr); + assert(upd.shm != BONGOCAT_NULLPTR); + animation_shared_memory_t& anim_shm = *ctx.shm; + // const auto& input_shm = *input.shm; + const auto& update_shm = *upd.shm; + const auto current_state = state; + const auto& current_animation_result = anim_shm.animation_player_result; + [[maybe_unused]] const int anim_index = anim_shm.anim_index; + // const platform::timestamp_ms_t last_key_pressed_timestamp = input_shm.last_key_pressed_timestamp; + assert(get_current_animation(ctx).type == animation_t::type_t::Custom); + const auto& current_frames = get_current_animation(ctx).custom; + + auto new_animation_result = anim_shm.animation_player_result; + auto new_state = state; + + const auto conditions = get_anim_conditions(ctx, input, upd, current_state, trigger, current_config); + + /// @TODO: use state machine for animation (states) + + if (current_frames.feature_running) { + if (conditions.process_running_animation) { + // toggle frame, show running animation + const bool above_threshold = current_config.cpu_threshold >= platform::ENABLED_MIN_CPU_PERCENT && + (update_shm.avg_cpu_usage >= current_config.cpu_threshold || + update_shm.max_cpu_usage >= current_config.cpu_threshold); + const bool lower_threshold = current_config.cpu_threshold >= platform::ENABLED_MIN_CPU_PERCENT && + (update_shm.avg_cpu_usage < current_config.cpu_threshold && + update_shm.max_cpu_usage < current_config.cpu_threshold); + + if (above_threshold) { + if (current_state.row_state == animation_state_row_t::Idle || conditions.is_moving) { + anim_custom_restart_animation(ctx, animation_state_row_t::StartRunning, animation_state_row_t::Running, + animation_state_row_t::EndRunning, new_animation_result, new_state, current_state, current_frames, current_config); - } + BONGOCAT_LOG_VERBOSE("Start Running: %d %d; %d%%", above_threshold, lower_threshold, + update_shm.avg_cpu_usage); + } else if (conditions.is_running) { + if (update_shm.cpu_active && current_state.row_state == animation_state_row_t::StartRunning) { + anim_custom_start_or_process_animation(ctx, animation_state_row_t::Running, + animation_state_row_t::EndRunning, new_animation_result, new_state, + current_state, current_frames, current_config); + } else if (!update_shm.cpu_active && (current_state.row_state == animation_state_row_t::StartRunning || + state.row_state == animation_state_row_t::Running)) { + // end running, cool down + anim_custom_start_or_process_animation(ctx, animation_state_row_t::EndRunning, + animation_state_row_t::Running, new_animation_result, new_state, + current_state, current_frames, current_config); + } else if (!update_shm.cpu_active && current_state.row_state == animation_state_row_t::EndRunning) { + // back to idle + anim_custom_start_or_process_animation(ctx, animation_state_row_t::Idle, new_animation_result, new_state, + current_state, current_frames, current_config); + } } - - return anim_update_animation_state(anim_shm, state, - new_animation_result, new_state, - current_animation_result, current_state, - current_config); + } else if (lower_threshold) { + // End Running + if (conditions.is_running && current_state.row_state != animation_state_row_t::EndRunning) { + if (conditions.go_next_frame_running) { + // end running, cool down + anim_custom_start_or_process_animation(ctx, animation_state_row_t::EndRunning, animation_state_row_t::Idle, + new_animation_result, new_state, current_state, current_frames, + current_config); + BONGOCAT_LOG_VERBOSE("Stop Running: %d %d; %d%%", above_threshold, lower_threshold, + update_shm.avg_cpu_usage); + } + } + } else { + // Cancel Running + if (conditions.is_running && current_state.row_state != animation_state_row_t::EndRunning && + !update_shm.cpu_active) { + // back to idle + anim_custom_start_or_process_animation(ctx, animation_state_row_t::EndRunning, animation_state_row_t::Idle, + new_animation_result, new_state, current_state, current_frames, + current_config); + BONGOCAT_LOG_VERBOSE("Stop Running: %d %d; %d%%", above_threshold, lower_threshold, update_shm.avg_cpu_usage); + } + } + } + } else { + // Cancel Running + if (conditions.is_running && !update_shm.cpu_active) { + // back to idle + anim_custom_start_or_process_animation(ctx, animation_state_row_t::Idle, new_animation_result, new_state, + current_state, current_frames, current_config); } + } + + return anim_update_animation_state(anim_shm, state, new_animation_result, new_state, current_animation_result, + current_state, current_config); +} #endif - static anim_next_frame_result_t anim_handle_idle_animation(animation_context_t& ctx, - [[maybe_unused]] const platform::input::input_context_t& input, - [[maybe_unused]] const platform::update::update_context_t& upd, - animation_state_t& state, const anim_handle_key_press_result_t& trigger_result) { - using namespace assets; - - // read-only config - assert(ctx._local_copy_config != nullptr); - //const config::config_t& current_config = *ctx._local_copy_config; - - assert(input.shm != nullptr); - assert(ctx.shm != nullptr); - animation_shared_memory_t& anim_shm = *ctx.shm; - //const auto& input_shm = *input.shm; - //auto& animation_player_data = anim_shm.animation_player_data; - //const int current_frame = animation_player_data.frame_index; - //const int current_row = animation_player_data.sprite_sheet_row; - //const animation_state_row_t current_row_state = state.row_state; - //const int anim_index = anim_shm.anim_index; - - switch (anim_shm.anim_type) { - case config::config_animation_sprite_sheet_layout_t::None: - break; - case config::config_animation_sprite_sheet_layout_t::Bongocat: { +static anim_next_frame_result_t +anim_handle_idle_animation(animation_thread_context_t& ctx, + [[maybe_unused]] const platform::input::input_context_t& input, + [[maybe_unused]] const platform::update::update_context_t& upd, animation_state_t& state, + const anim_handle_key_press_result_t& trigger_result) { + using namespace assets; + + // read-only config + assert(ctx._local_copy_config); + // const config::config_t& current_config = *ctx._local_copy_config; + + assert(input.shm != BONGOCAT_NULLPTR); + assert(ctx.shm != BONGOCAT_NULLPTR); + const animation_shared_memory_t& anim_shm = *ctx.shm; + // const auto& input_shm = *input.shm; + // auto& animation_player_data = anim_shm.animation_player_data; + // const int current_frame = animation_player_data.frame_index; + // const int current_row = animation_player_data.sprite_sheet_row; + // const animation_state_row_t current_row_state = state.row_state; + // const int anim_index = anim_shm.anim_index; + + switch (anim_shm.anim_type) { + case config::config_animation_sprite_sheet_layout_t::None: + break; + case config::config_animation_sprite_sheet_layout_t::Bongocat: { #ifdef FEATURE_BONGOCAT_EMBEDDED_ASSETS - return anim_bongocat_idle_next_frame(ctx, input, upd, state, trigger_result); + return anim_bongocat_idle_next_frame(ctx, input, upd, state, trigger_result); #endif - }break; - case config::config_animation_sprite_sheet_layout_t::Dm: { + } break; + case config::config_animation_sprite_sheet_layout_t::Dm: { #ifdef FEATURE_ENABLE_DM_EMBEDDED_ASSETS - return anim_dm_idle_next_frame(ctx, input, upd, state, trigger_result); + return anim_dm_idle_next_frame(ctx, input, upd, state, trigger_result); #endif - }break; - case config::config_animation_sprite_sheet_layout_t::Pkmn: { + } break; + case config::config_animation_sprite_sheet_layout_t::Pkmn: { #ifdef FEATURE_PKMN_EMBEDDED_ASSETS - return anim_pkmn_idle_next_frame(ctx, input, upd, state, trigger_result); + return anim_pkmn_idle_next_frame(ctx, input, upd, state, trigger_result); #endif - }break; - case config::config_animation_sprite_sheet_layout_t::MsAgent: { + } break; + case config::config_animation_sprite_sheet_layout_t::MsAgent: { #ifdef FEATURE_MS_AGENT_EMBEDDED_ASSETS - return anim_ms_agent_idle_next_frame(ctx, input, upd, state, trigger_result); + return anim_ms_agent_idle_next_frame(ctx, input, upd, state, trigger_result); #endif - }break; - case config::config_animation_sprite_sheet_layout_t::Custom: { + } break; + case config::config_animation_sprite_sheet_layout_t::Custom: { #ifdef FEATURE_CUSTOM_SPRITE_SHEETS_ANIMATION - return anim_custom_idle_next_frame(ctx, input, upd, state, trigger_result); + return anim_custom_idle_next_frame(ctx, input, upd, state, trigger_result); #endif - }break; - } - return {}; - } + } break; + } + return {}; +} - static anim_handle_key_press_result_t anim_handle_animation_trigger(animation_session_t& animation_trigger_ctx, animation_state_t& state, const animation_trigger_t& trigger) { - using namespace assets; - - assert(animation_trigger_ctx._input != nullptr); - assert(animation_trigger_ctx._input->shm != nullptr); - assert(animation_trigger_ctx._update != nullptr); - assert(animation_trigger_ctx._update->shm != nullptr); - animation_context_t& ctx = animation_trigger_ctx.anim; - [[maybe_unused]] const platform::input::input_context_t& input = *animation_trigger_ctx._input; - [[maybe_unused]] const platform::update::update_context_t& upd = *animation_trigger_ctx._update; - - // read-only config - //assert(ctx._local_copy_config != nullptr); - //assert(ctx.shm != nullptr); - //assert(animation_trigger_ctx._config != nullptr); - //const config::config_t& current_config = *ctx._local_copy_config; - - assert(input.shm != nullptr); - assert(upd.shm != nullptr); - assert(ctx.shm != nullptr); - animation_shared_memory_t& anim_shm = *ctx.shm; - //const auto& input_shm = *input.shm; - //const auto current_state = state; - //const auto& current_animation_result = anim_shm.animation_player_result; - [[maybe_unused]] const int anim_index = anim_shm.anim_index; - - anim_next_frame_result_t update_frame_result{}; - - if (has_flag(trigger.anim_cause, trigger_animation_cause_mask_t::CpuUpdate)) { - // handle working animation - switch (anim_shm.anim_type) { - case config::config_animation_sprite_sheet_layout_t::None: - break; - case config::config_animation_sprite_sheet_layout_t::Bongocat: - break; - case config::config_animation_sprite_sheet_layout_t::Dm: { +static anim_handle_key_press_result_t anim_handle_animation_trigger(animation_context_t& animation_ctx, + animation_state_t& state, + const animation_trigger_t& trigger) { + using namespace assets; + + assert(animation_ctx._input != BONGOCAT_NULLPTR); + assert(animation_ctx._input->shm != BONGOCAT_NULLPTR); + assert(animation_ctx._update != BONGOCAT_NULLPTR); + assert(animation_ctx._update->shm != BONGOCAT_NULLPTR); + animation_thread_context_t& ctx = animation_ctx.thread_context; + [[maybe_unused]] const platform::input::input_context_t& input = *animation_ctx._input; + [[maybe_unused]] const platform::update::update_context_t& upd = *animation_ctx._update; + + // read-only config + // assert(ctx._local_copy_config != nullptr); + // assert(ctx.shm != nullptr); + // assert(animation_trigger_ctx._config != nullptr); + // const config::config_t& current_config = *ctx._local_copy_config; + + assert(input.shm != BONGOCAT_NULLPTR); + assert(upd.shm != BONGOCAT_NULLPTR); + assert(ctx.shm != BONGOCAT_NULLPTR); + const animation_shared_memory_t& anim_shm = *ctx.shm; + // const auto& input_shm = *input.shm; + // const auto current_state = state; + // const auto& current_animation_result = anim_shm.animation_player_result; + [[maybe_unused]] const int anim_index = anim_shm.anim_index; + + anim_next_frame_result_t update_frame_result{}; + + if (has_flag(trigger.anim_cause, trigger_animation_cause_mask_t::CpuUpdate)) { + // handle working animation + switch (anim_shm.anim_type) { + case config::config_animation_sprite_sheet_layout_t::None: + break; + case config::config_animation_sprite_sheet_layout_t::Bongocat: + break; + case config::config_animation_sprite_sheet_layout_t::Dm: { #ifdef FEATURE_ENABLE_DM_EMBEDDED_ASSETS - update_frame_result = anim_dm_working_next_frame(ctx, input, upd, state, trigger); + update_frame_result = anim_dm_working_next_frame(ctx, input, upd, state, trigger); #endif - }break; - case config::config_animation_sprite_sheet_layout_t::Pkmn: - break; - case config::config_animation_sprite_sheet_layout_t::MsAgent: { + } break; + case config::config_animation_sprite_sheet_layout_t::Pkmn: + break; + case config::config_animation_sprite_sheet_layout_t::MsAgent: { #ifdef FEATURE_MS_AGENT_EMBEDDED_ASSETS - /// @TODO: add working animation for MS agents - //update_frame_result = anim_ms_agent_working_next_frame(ctx, input, upd, state, trigger); + /// @TODO: add working animation for MS agents + // update_frame_result = anim_ms_agent_working_next_frame(ctx, input, upd, state, trigger); #endif - }break; - case config::config_animation_sprite_sheet_layout_t::Custom: { + } break; + case config::config_animation_sprite_sheet_layout_t::Custom: { #ifdef FEATURE_CUSTOM_SPRITE_SHEETS_ANIMATION - update_frame_result = anim_custom_working_next_frame(ctx, input, upd, state, trigger); + update_frame_result = anim_custom_working_next_frame(ctx, input, upd, state, trigger); #endif - }break; - } - // handle running animation - switch (anim_shm.anim_type) { - case config::config_animation_sprite_sheet_layout_t::None: - break; - case config::config_animation_sprite_sheet_layout_t::Bongocat: - break; - case config::config_animation_sprite_sheet_layout_t::Dm: - break; - case config::config_animation_sprite_sheet_layout_t::Pkmn: - break; - case config::config_animation_sprite_sheet_layout_t::MsAgent: - break; - case config::config_animation_sprite_sheet_layout_t::Custom: { + } break; + } + // handle running animation + switch (anim_shm.anim_type) { + case config::config_animation_sprite_sheet_layout_t::None: + break; + case config::config_animation_sprite_sheet_layout_t::Bongocat: + break; + case config::config_animation_sprite_sheet_layout_t::Dm: + break; + case config::config_animation_sprite_sheet_layout_t::Pkmn: + break; + case config::config_animation_sprite_sheet_layout_t::MsAgent: + break; + case config::config_animation_sprite_sheet_layout_t::Custom: { #ifdef FEATURE_CUSTOM_SPRITE_SHEETS_ANIMATION - update_frame_result = anim_custom_running_next_frame(ctx, input, upd, state, trigger); + update_frame_result = anim_custom_running_next_frame(ctx, input, upd, state, trigger); #endif - }break; - } - BONGOCAT_LOG_VERBOSE("CPU update detected - switching to frame %d (%zu)", update_frame_result.new_col, trigger.anim_cause); - } - - // handle key press animation - if (has_flag(trigger.anim_cause, trigger_animation_cause_mask_t::KeyPress)) { - switch (anim_shm.anim_type) { - case config::config_animation_sprite_sheet_layout_t::None: - break; - case config::config_animation_sprite_sheet_layout_t::Bongocat: { + } break; + } + BONGOCAT_LOG_VERBOSE("CPU update detected - switching to frame %d (%zu)", update_frame_result.new_col, + trigger.anim_cause); + } + + // handle key press animation + if (has_flag(trigger.anim_cause, trigger_animation_cause_mask_t::KeyPress)) { + switch (anim_shm.anim_type) { + case config::config_animation_sprite_sheet_layout_t::None: + break; + case config::config_animation_sprite_sheet_layout_t::Bongocat: { #ifdef FEATURE_BONGOCAT_EMBEDDED_ASSETS - update_frame_result = anim_bongocat_key_pressed_next_frame(ctx, state, input, upd, trigger); + update_frame_result = anim_bongocat_key_pressed_next_frame(ctx, state, input, upd, trigger); #endif - }break; - case config::config_animation_sprite_sheet_layout_t::Dm: { + } break; + case config::config_animation_sprite_sheet_layout_t::Dm: { #ifdef FEATURE_ENABLE_DM_EMBEDDED_ASSETS - update_frame_result = anim_dm_key_pressed_next_frame(ctx, input, upd, state, trigger); + update_frame_result = anim_dm_key_pressed_next_frame(ctx, input, upd, state, trigger); #endif - }break; - case config::config_animation_sprite_sheet_layout_t::Pkmn: { + } break; + case config::config_animation_sprite_sheet_layout_t::Pkmn: { #ifdef FEATURE_PKMN_EMBEDDED_ASSETS - update_frame_result = anim_pkmn_key_pressed_next_frame(ctx, input, upd, state, trigger); + update_frame_result = anim_pkmn_key_pressed_next_frame(ctx, input, upd, state, trigger); #endif - }break; - case config::config_animation_sprite_sheet_layout_t::MsAgent: { + } break; + case config::config_animation_sprite_sheet_layout_t::MsAgent: { #ifdef FEATURE_MS_AGENT_EMBEDDED_ASSETS - update_frame_result = anim_ms_agent_key_pressed_next_frame(ctx, state, input, upd, trigger); + update_frame_result = anim_ms_agent_key_pressed_next_frame(ctx, state, input, upd, trigger); #endif - }break; - case config::config_animation_sprite_sheet_layout_t::Custom: { + } break; + case config::config_animation_sprite_sheet_layout_t::Custom: { #ifdef FEATURE_CUSTOM_SPRITE_SHEETS_ANIMATION - update_frame_result = anim_custom_key_pressed_next_frame(ctx, state, input, upd, trigger); + update_frame_result = anim_custom_key_pressed_next_frame(ctx, state, input, upd, trigger); #endif - }break; - } - BONGOCAT_LOG_VERBOSE("Key press detected - switching to frame %d (%zu)", update_frame_result.new_col, trigger.anim_cause); - } - - return { .trigger = trigger, .update_frame_result = update_frame_result}; + } break; } + BONGOCAT_LOG_VERBOSE("Key press detected - switching to frame %d (%zu)", update_frame_result.new_col, + trigger.anim_cause); + } - static bool anim_update_state(animation_session_t& animation_trigger_ctx, animation_state_t& state, const animation_trigger_t& trigger) { - assert(animation_trigger_ctx._input); - platform::input::input_context_t& input = *animation_trigger_ctx._input; - platform::update::update_context_t& upd = *animation_trigger_ctx._update; - animation_context_t& ctx = animation_trigger_ctx.anim; - // read-only config - assert(ctx._local_copy_config != nullptr); - const config::config_t& current_config = *ctx._local_copy_config; - - bool ret = false; - { - state.frame_delta_ms_counter += state.frame_time_ms; - state.update_delta_ms_counter += state.frame_time_ms; - - anim_handle_key_press_result_t trigger_result; - anim_next_frame_result_t idle_update_result; - bool hold_frame = false; - bool key_pressed = false; - { - platform::LockGuard input_guard (input.input_lock); - platform::LockGuard update_guard (upd.update_lock); - trigger_result = anim_handle_animation_trigger(animation_trigger_ctx, state, trigger); - idle_update_result = anim_handle_idle_animation(ctx, input, upd, state, trigger_result); - key_pressed = has_flag(trigger_result.trigger.anim_cause, trigger_animation_cause_mask_t::KeyPress) && trigger_result.trigger.any_key_press_counter > 0; - hold_frame = key_pressed || idle_update_result.frame_changed || idle_update_result.rerender; - } - ret = idle_update_result.rerender || trigger_result.update_frame_result.rerender; - if (key_pressed) { - BONGOCAT_LOG_VERBOSE("Trigger key press animation"); - } - if (idle_update_result.frame_changed) { - BONGOCAT_LOG_VERBOSE("Trigger frame changed"); - } - if (idle_update_result.moved) { - BONGOCAT_LOG_VERBOSE("Trigger movement animation"); - } - if (idle_update_result.rerender) { - BONGOCAT_LOG_VERBOSE("Trigger rerender"); - } - - if (!state.hold_frame_after_release && hold_frame) { - state.hold_frame_after_release = true; - } - if (state.hold_frame_after_release && (!key_pressed && !hold_frame) && state.hold_frame_ms > current_config.keypress_duration_ms) { - state.hold_frame_after_release = false; - state.hold_frame_ms = 0; - } - if (state.hold_frame_after_release) { - state.hold_frame_ms += state.frame_time_ms; - } - } + return {.trigger = trigger, .update_frame_result = update_frame_result}; +} - return ret; +static bool anim_update_state(animation_context_t& animation_ctx, animation_state_t& state, + const animation_trigger_t& trigger) { + assert(animation_ctx._input); + platform::input::input_context_t& input = *animation_ctx._input; + platform::update::update_context_t& upd = *animation_ctx._update; + animation_thread_context_t& ctx = animation_ctx.thread_context; + // read-only config + assert(ctx._local_copy_config); + const config::config_t& current_config = *ctx._local_copy_config; + + bool ret = false; + { + state.frame_delta_ms_counter += state.frame_time_ms; + state.update_delta_ms_counter += state.frame_time_ms; + + anim_handle_key_press_result_t trigger_result; + anim_next_frame_result_t idle_update_result; + bool hold_frame = false; + bool key_pressed = false; + { + platform::LockGuard input_guard(input.input_lock); + platform::LockGuard update_guard(upd.update_lock); + trigger_result = anim_handle_animation_trigger(animation_ctx, state, trigger); + idle_update_result = anim_handle_idle_animation(ctx, input, upd, state, trigger_result); + key_pressed = has_flag(trigger_result.trigger.anim_cause, trigger_animation_cause_mask_t::KeyPress) && + trigger_result.trigger.any_key_press_counter > 0; + hold_frame = key_pressed || idle_update_result.frame_changed || idle_update_result.rerender; + } + ret = idle_update_result.rerender || trigger_result.update_frame_result.rerender; + if (key_pressed) { + BONGOCAT_LOG_VERBOSE("Trigger key press animation"); + } + if (idle_update_result.frame_changed) { + BONGOCAT_LOG_VERBOSE("Trigger frame changed"); + } + if (idle_update_result.moved) { + BONGOCAT_LOG_VERBOSE("Trigger movement animation"); + } + if (idle_update_result.rerender) { + BONGOCAT_LOG_VERBOSE("Trigger rerender"); } - // ============================================================================= - // ANIMATION THREAD MANAGEMENT MODULE - // ============================================================================= - - static void anim_init_state(animation_context_t& ctx, animation_state_t& state) { - // read-only config - assert(ctx._local_copy_config != nullptr); - const config::config_t& current_config = *ctx._local_copy_config; - assert(current_config.fps > 0); - - state.hold_frame_ms = 0; - state.frame_delta_ms_counter = 0; - state.update_delta_ms_counter = 0; - state.frame_time_ns = 1000000000LL / current_config.fps; - state.frame_time_ms = state.frame_time_ns/1000000LL; - state.last_frame_update_ms = platform::get_current_time_ms(); - state.row_state = animation_state_row_t::Idle; + if (!state.hold_frame_after_release && hold_frame) { + state.hold_frame_after_release = true; } + if (state.hold_frame_after_release && (!key_pressed && !hold_frame) && + state.hold_frame_ms > current_config.keypress_duration_ms) { + state.hold_frame_after_release = false; + state.hold_frame_ms = 0; + } + if (state.hold_frame_after_release) { + state.hold_frame_ms += state.frame_time_ms; + } + } + + return ret; +} - static void cleanup_anim_thread(void* arg) { - assert(arg); - animation_session_t& trigger_ctx = *static_cast(arg); +// ============================================================================= +// ANIMATION THREAD MANAGEMENT MODULE +// ============================================================================= + +static void anim_init_state(animation_thread_context_t& ctx, animation_state_t& state) { + // read-only config + assert(ctx._local_copy_config); + const config::config_t& current_config = *ctx._local_copy_config; + assert(current_config.fps > 0); + + state.hold_frame_ms = 0; + state.frame_delta_ms_counter = 0; + state.update_delta_ms_counter = 0; + state.frame_time_ns = 1000000000LL / current_config.fps; + state.frame_time_ms = state.frame_time_ns / 1000000LL; + state.last_frame_update_ms = platform::get_current_time_ms(); + state.row_state = animation_state_row_t::Idle; +} - atomic_store(&trigger_ctx.anim._animation_running, false); +static void cleanup_anim_thread(void *arg) { + assert(arg); + animation_context_t& animation_context = *static_cast(arg); - trigger_ctx.anim.config_updated.notify_all(); + atomic_store(&animation_context.thread_context._animation_running, false); - BONGOCAT_LOG_INFO("Animation thread cleanup completed (via pthread_cancel)"); - } + animation_context.thread_context.config_updated.notify_all(); + + BONGOCAT_LOG_INFO("Animation thread cleanup completed (via pthread_cancel)"); +} - static void *anim_thread(void *arg) { - using namespace assets; - - assert(arg); - auto& trigger_ctx = *static_cast(arg); - - // sanity checks - assert(trigger_ctx._config != nullptr); - assert(trigger_ctx._input != nullptr); - assert(trigger_ctx._update != nullptr); - assert(trigger_ctx._configs_reloaded_cond != nullptr); - assert(trigger_ctx.anim.shm != nullptr); - assert(trigger_ctx.trigger_efd._fd >= 0); - assert(trigger_ctx.render_efd._fd >= 0); - assert(trigger_ctx.anim.update_config_efd._fd >= 0); - - // init animation state - animation_state_t state; - { - platform::LockGuard guard (trigger_ctx.anim.anim_lock); - animation_context_t& ctx = trigger_ctx.anim; - - assert(ctx.shm != nullptr); - //assert(input.shm != nullptr); - animation_shared_memory_t& anim_shm = *ctx.shm; - //const auto& input_shm = *input.shm; - //auto& current_state = state; - auto& current_animation_result = anim_shm.animation_player_result; - [[maybe_unused]] const int anim_index = anim_shm.anim_index; - - // read-only config - assert(ctx._local_copy_config != nullptr); - const config::config_t& current_config = *ctx._local_copy_config; - - anim_init_state(ctx, state); - - // setup animation player - switch (current_config.animation_sprite_sheet_layout) { - case config::config_animation_sprite_sheet_layout_t::None: - break; - case config::config_animation_sprite_sheet_layout_t::Bongocat: +static void *anim_thread(void *arg) { + using namespace assets; + + assert(arg); + auto& trigger_ctx = *static_cast(arg); + + // sanity checks + assert(trigger_ctx._config != BONGOCAT_NULLPTR); + assert(trigger_ctx._input != BONGOCAT_NULLPTR); + assert(trigger_ctx._update != BONGOCAT_NULLPTR); + assert(trigger_ctx._configs_reloaded_cond != BONGOCAT_NULLPTR); + assert(trigger_ctx.thread_context.shm); + assert(trigger_ctx.trigger_efd._fd >= 0); + assert(trigger_ctx.render_efd._fd >= 0); + assert(trigger_ctx.thread_context.update_config_efd._fd >= 0); + + // init animation state + animation_state_t state; + { + platform::LockGuard guard(trigger_ctx.thread_context.anim_lock); + animation_thread_context_t& ctx = trigger_ctx.thread_context; + + assert(ctx.shm); + // assert(input.shm); + animation_shared_memory_t& anim_shm = *ctx.shm; + // const auto& input_shm = *input.shm; + // auto& current_state = state; + auto& current_animation_result = anim_shm.animation_player_result; + [[maybe_unused]] const int anim_index = anim_shm.anim_index; + + // read-only config + assert(ctx._local_copy_config); + const config::config_t& current_config = *ctx._local_copy_config; + + anim_init_state(ctx, state); + + // setup animation player + switch (current_config.animation_sprite_sheet_layout) { + case config::config_animation_sprite_sheet_layout_t::None: + break; + case config::config_animation_sprite_sheet_layout_t::Bongocat: #ifdef FEATURE_BONGOCAT_EMBEDDED_ASSETS - current_animation_result.sprite_sheet_col = current_config.idle_frame; - current_animation_result.sprite_sheet_row = BONGOCAT_SPRITE_SHEET_ROW; - state.animations_index = 0; - state.row_state = animation_state_row_t::Idle; + current_animation_result.sprite_sheet_col = current_config.idle_frame; + current_animation_result.sprite_sheet_row = BONGOCAT_SPRITE_SHEET_ROW; + state.animations_index = 0; + state.row_state = animation_state_row_t::Idle; #endif - break; - case config::config_animation_sprite_sheet_layout_t::Dm: + break; + case config::config_animation_sprite_sheet_layout_t::Dm: #ifdef FEATURE_ENABLE_DM_EMBEDDED_ASSETS - current_animation_result.sprite_sheet_col = current_config.idle_frame; - current_animation_result.sprite_sheet_row = DM_SPRITE_SHEET_ROW; - state.animations_index = 0; - state.row_state = animation_state_row_t::Idle; + current_animation_result.sprite_sheet_col = current_config.idle_frame; + current_animation_result.sprite_sheet_row = DM_SPRITE_SHEET_ROW; + state.animations_index = 0; + state.row_state = animation_state_row_t::Idle; #endif - break; - case config::config_animation_sprite_sheet_layout_t::Pkmn: + break; + case config::config_animation_sprite_sheet_layout_t::Pkmn: #ifdef FEATURE_PKMN_EMBEDDED_ASSETS - current_animation_result.sprite_sheet_col = current_config.idle_frame; - current_animation_result.sprite_sheet_row = PKMN_SPRITE_SHEET_ROW; - state.animations_index = 0; - state.row_state = animation_state_row_t::Idle; + current_animation_result.sprite_sheet_col = current_config.idle_frame; + current_animation_result.sprite_sheet_row = PKMN_SPRITE_SHEET_ROW; + state.animations_index = 0; + state.row_state = animation_state_row_t::Idle; #endif - break; - case config::config_animation_sprite_sheet_layout_t::MsAgent: { + break; + case config::config_animation_sprite_sheet_layout_t::MsAgent: { #ifdef FEATURE_MS_AGENT_EMBEDDED_ASSETS - assert(anim_shm.anim_index >= 0); - const ms_agent_animation_indices_t animation_indices = get_ms_agent_animation_indices(static_cast(anim_shm.anim_index)); - current_animation_result.sprite_sheet_col = current_config.idle_frame; - current_animation_result.sprite_sheet_row = MS_AGENT_SPRITE_SHEET_ROW_IDLE; - state.start_col_index = animation_indices.start_index_frame_idle; - state.end_col_index = animation_indices.end_index_frame_idle; - state.row_state = animation_state_row_t::Idle; + assert(anim_shm.anim_index >= 0); + const ms_agent_animation_indices_t animation_indices = + get_ms_agent_animation_indices(static_cast(anim_shm.anim_index)); + current_animation_result.sprite_sheet_col = current_config.idle_frame; + current_animation_result.sprite_sheet_row = MS_AGENT_SPRITE_SHEET_ROW_IDLE; + state.start_col_index = animation_indices.start_index_frame_idle; + state.end_col_index = animation_indices.end_index_frame_idle; + state.row_state = animation_state_row_t::Idle; #endif - }break; - case config::config_animation_sprite_sheet_layout_t::Custom: { + } break; + case config::config_animation_sprite_sheet_layout_t::Custom: { #ifdef FEATURE_MISC_EMBEDDED_ASSETS - assert(anim_shm.anim_index >= 0); - if (static_cast(anim_shm.anim_index) <= MAX_MISC_ANIM_INDEX) { - const custom_animation_settings_t custom_columns = get_misc_sprite_sheet_columns(static_cast(anim_shm.anim_index)); - current_animation_result.sprite_sheet_col = current_config.idle_frame; - current_animation_result.sprite_sheet_row = CUSTOM_SPRITE_SHEET_ROW_IDLE; - state.start_col_index = 0; - state.end_col_index = custom_columns.idle_frames > 0 ? custom_columns.idle_frames-1 : 0; - state.row_state = animation_state_row_t::Idle; - } + assert(anim_shm.anim_index >= 0); + if (static_cast(anim_shm.anim_index) <= MAX_MISC_ANIM_INDEX) { + const custom_animation_settings_t custom_columns = + get_misc_sprite_sheet_columns(static_cast(anim_shm.anim_index)); + current_animation_result.sprite_sheet_col = current_config.idle_frame; + current_animation_result.sprite_sheet_row = CUSTOM_SPRITE_SHEET_ROW_IDLE; + state.start_col_index = 0; + state.end_col_index = custom_columns.idle_frames > 0 ? custom_columns.idle_frames - 1 : 0; + state.row_state = animation_state_row_t::Idle; + } #endif #ifdef FEATURE_CUSTOM_SPRITE_SHEETS - assert(anim_shm.anim_index >= 0); - if (static_cast(anim_shm.anim_index) == CUSTOM_ANIM_INDEX) { - current_animation_result.sprite_sheet_col = current_config.idle_frame; - current_animation_result.sprite_sheet_row = current_config.custom_sprite_sheet_settings.idle_row_index >= 0 ? current_config.custom_sprite_sheet_settings.idle_row_index : static_cast(CUSTOM_SPRITE_SHEET_ROW_IDLE); - state.start_col_index = 0; - state.end_col_index = current_config.custom_sprite_sheet_settings.idle_frames > 0 ? current_config.custom_sprite_sheet_settings.idle_frames-1 : 0; - state.row_state = animation_state_row_t::Idle; - } + assert(anim_shm.anim_index >= 0); + if (static_cast(anim_shm.anim_index) == CUSTOM_ANIM_INDEX) { + current_animation_result.sprite_sheet_col = current_config.idle_frame; + current_animation_result.sprite_sheet_row = current_config.custom_sprite_sheet_settings.idle_row_index >= 0 + ? current_config.custom_sprite_sheet_settings.idle_row_index + : static_cast(CUSTOM_SPRITE_SHEET_ROW_IDLE); + state.start_col_index = 0; + state.end_col_index = current_config.custom_sprite_sheet_settings.idle_frames > 0 + ? current_config.custom_sprite_sheet_settings.idle_frames - 1 + : 0; + state.row_state = animation_state_row_t::Idle; + } #endif - }break; - } - } + } break; + } + } - BONGOCAT_LOG_DEBUG("Animation thread main loop started"); + BONGOCAT_LOG_DEBUG("Animation thread main loop started"); - // trigger initial render - platform::wayland::request_render(trigger_ctx); + // trigger initial render + platform::wayland::request_render(trigger_ctx); - pthread_cleanup_push(cleanup_anim_thread, arg); + pthread_cleanup_push(cleanup_anim_thread, arg); - // local thread context - timespec next_frame_time{}; - clock_gettime(CLOCK_MONOTONIC, &next_frame_time); + // local thread context + timespec next_frame_time{}; + clock_gettime(CLOCK_MONOTONIC, &next_frame_time); - atomic_store(&trigger_ctx.anim._animation_running, true); - while (atomic_load(&trigger_ctx.anim._animation_running)) { - pthread_testcancel(); // optional, but makes cancellation more responsive + atomic_store(&trigger_ctx.thread_context._animation_running, true); + while (atomic_load(&trigger_ctx.thread_context._animation_running)) { + pthread_testcancel(); // optional, but makes cancellation more responsive - animation_context_t& ctx = trigger_ctx.anim; + animation_thread_context_t& ctx = trigger_ctx.thread_context; - // read from config - platform::time_ms_t timeout_ms; - int32_t fps = 1; - { - assert(ctx._local_copy_config != nullptr); - const config::config_t& current_config = *ctx._local_copy_config; + // read from config + platform::time_ms_t timeout_ms; + int32_t fps = 1; + { + assert(ctx._local_copy_config); + const config::config_t& current_config = *ctx._local_copy_config; - fps = current_config.fps; - timeout_ms = current_config.fps > 0 ? 1000 / current_config.fps / 3 : POOL_MIN_TIMEOUT_MS; - timeout_ms = timeout_ms < POOL_MIN_TIMEOUT_MS ? POOL_MIN_TIMEOUT_MS : timeout_ms; - timeout_ms = timeout_ms > POOL_MAX_TIMEOUT_MS ? POOL_MAX_TIMEOUT_MS : timeout_ms; - } + fps = current_config.fps; + timeout_ms = current_config.fps > 0 ? 1000 / current_config.fps / 3 : POOL_MIN_TIMEOUT_MS; + timeout_ms = timeout_ms < POOL_MIN_TIMEOUT_MS ? POOL_MIN_TIMEOUT_MS : timeout_ms; + timeout_ms = timeout_ms > POOL_MAX_TIMEOUT_MS ? POOL_MAX_TIMEOUT_MS : timeout_ms; + } - trigger_animation_cause_mask_t triggered_anim_cause = trigger_animation_cause_mask_t::NONE; - int any_key_press_counter = 0; - - bool reload_config = false; - uint64_t new_gen{atomic_load(trigger_ctx._config_generation)}; - - /// event poll - constexpr size_t fds_update_config_index = 0; - constexpr size_t fds_animation_trigger_index = 1; - constexpr nfds_t fds_count = 2; - pollfd fds[fds_count] = { - { .fd = trigger_ctx.anim.update_config_efd._fd, .events = POLLIN, .revents = 0 }, - { .fd = trigger_ctx.trigger_efd._fd, .events = POLLIN, .revents = 0 }, - }; - - assert(timeout_ms <= INT_MAX); - const int poll_result = poll(fds, fds_count, static_cast(timeout_ms)); - if (poll_result < 0) { - if (errno == EINTR) continue; // Interrupted by signal - BONGOCAT_LOG_ERROR("animation: Poll error: %s", strerror(errno)); - break; - } - if (poll_result > 0) { - // cancel pooling (when not running anymore) - if (!atomic_load(&ctx._animation_running)) { - // draining pools - for (size_t i = 0; i < fds_count; i++) { - platform::drain_event(fds[i], MAX_ATTEMPTS); - } - break; - } + trigger_animation_cause_mask_t triggered_anim_cause = trigger_animation_cause_mask_t::NONE; + int any_key_press_counter = 0; - // Handle config update - if (fds[fds_update_config_index].revents & POLLIN) { - BONGOCAT_LOG_DEBUG("animation: Receive update config event"); - platform::drain_event(fds[fds_update_config_index], MAX_ATTEMPTS, "update config eventfd"); - reload_config = new_gen > 0; - } + bool reload_config = false; + uint64_t new_gen{atomic_load(trigger_ctx._config_generation)}; - // animation trigger event - if (fds[fds_animation_trigger_index].revents & POLLIN) { - BONGOCAT_LOG_VERBOSE("Receive animation trigger event"); - uint64_t u; - ssize_t rc; - int attempts = 0; - assert(MAX_ATTEMPTS <= INT_MAX); - while ((rc = read(trigger_ctx.trigger_efd._fd, &u, sizeof(u))) == sizeof(u) && - attempts < static_cast(MAX_ATTEMPTS)) - { - attempts++; - auto cause = static_cast(u); - switch (cause) { - case trigger_animation_cause_mask_t::NONE: - break; - case trigger_animation_cause_mask_t::Init: - triggered_anim_cause = flag_add(triggered_anim_cause, cause); - break; - case trigger_animation_cause_mask_t::KeyPress: - any_key_press_counter++; - triggered_anim_cause = flag_add(triggered_anim_cause, cause); - break; - case trigger_animation_cause_mask_t::IdleUpdate: - triggered_anim_cause = flag_add(triggered_anim_cause, cause); - break; - case trigger_animation_cause_mask_t::CpuUpdate: - triggered_anim_cause = flag_add(triggered_anim_cause, cause); - break; - case trigger_animation_cause_mask_t::UpdateConfig: - triggered_anim_cause = flag_add(triggered_anim_cause, cause); - break; - case trigger_animation_cause_mask_t::Timeout: - triggered_anim_cause = flag_add(triggered_anim_cause, cause); - break; - } - } - if (rc < 0) { - check_errno("animation trigger eventfd"); - } - - BONGOCAT_LOG_VERBOSE("animation trigger: %zu", triggered_anim_cause); - } else { - triggered_anim_cause = flag_add(triggered_anim_cause, trigger_animation_cause_mask_t::Timeout); - triggered_anim_cause = flag_add(triggered_anim_cause, trigger_animation_cause_mask_t::CpuUpdate); - } - } + /// event poll + constexpr size_t fds_update_config_index = 0; + constexpr size_t fds_animation_trigger_index = 1; + constexpr nfds_t fds_count = 2; + pollfd fds[fds_count] = { + {.fd = trigger_ctx.thread_context.update_config_efd._fd, .events = POLLIN, .revents = 0}, + {.fd = trigger_ctx.trigger_efd._fd, .events = POLLIN, .revents = 0}, + }; - // Update Animations - { - platform::LockGuard guard (trigger_ctx.anim.anim_lock); - assert(ctx.shm != nullptr); - const bool frame_changed = anim_update_state(trigger_ctx, state, { - .anim_cause = triggered_anim_cause, - .any_key_press_counter = any_key_press_counter, - }); - if (frame_changed) { - uint64_t u = 1; - if (write(trigger_ctx.render_efd._fd, &u, sizeof(uint64_t)) >= 0) { - BONGOCAT_LOG_VERBOSE("animation: Write animation render event"); - } else { - BONGOCAT_LOG_ERROR("animation: Failed to write to notify pipe in animation: %s", strerror(errno)); - } - } + assert(timeout_ms <= INT_MAX); + const int poll_result = poll(fds, fds_count, static_cast(timeout_ms)); + if (poll_result < 0) { + if (errno == EINTR) { + continue; // Interrupted by signal + } + BONGOCAT_LOG_ERROR("animation: Poll error: %s", strerror(errno)); + break; + } + if (poll_result > 0) { + // cancel pooling (when not running anymore) + if (!atomic_load(&ctx._animation_running)) { + // draining pools + for (size_t i = 0; i < fds_count; i++) { + platform::drain_event(fds[i], MAX_ATTEMPTS); + } + break; + } + + // Handle config update + if (fds[fds_update_config_index].revents & POLLIN) { + BONGOCAT_LOG_DEBUG("animation: Receive update config event"); + platform::drain_event(fds[fds_update_config_index], MAX_ATTEMPTS, "update config eventfd"); + reload_config = new_gen > 0; + } + + // animation trigger event + if (fds[fds_animation_trigger_index].revents & POLLIN) { + BONGOCAT_LOG_VERBOSE("Receive animation trigger event"); + uint64_t u; + ssize_t rc; + int attempts = 0; + assert(MAX_ATTEMPTS <= INT_MAX); + while ((rc = read(trigger_ctx.trigger_efd._fd, &u, sizeof(u))) == sizeof(u) && + attempts < static_cast(MAX_ATTEMPTS)) { + attempts++; + auto cause = static_cast(u); + switch (cause) { + case trigger_animation_cause_mask_t::NONE: + break; + case trigger_animation_cause_mask_t::Init: + triggered_anim_cause = flag_add(triggered_anim_cause, cause); + break; + case trigger_animation_cause_mask_t::KeyPress: + any_key_press_counter++; + triggered_anim_cause = flag_add(triggered_anim_cause, cause); + break; + case trigger_animation_cause_mask_t::IdleUpdate: + triggered_anim_cause = flag_add(triggered_anim_cause, cause); + break; + case trigger_animation_cause_mask_t::CpuUpdate: + triggered_anim_cause = flag_add(triggered_anim_cause, cause); + break; + case trigger_animation_cause_mask_t::UpdateConfig: + triggered_anim_cause = flag_add(triggered_anim_cause, cause); + break; + case trigger_animation_cause_mask_t::Timeout: + triggered_anim_cause = flag_add(triggered_anim_cause, cause); + break; + } + } + if (rc < 0) { + check_errno("animation trigger eventfd"); + } - // Advance next frame time by exactly one frame - next_frame_time.tv_nsec += state.frame_time_ns; - while (next_frame_time.tv_nsec >= 1000000000LL) { - next_frame_time.tv_nsec -= 1000000000LL; - next_frame_time.tv_sec += 1; - } + BONGOCAT_LOG_VERBOSE("animation trigger: %zu", triggered_anim_cause); + } else { + triggered_anim_cause = flag_add(triggered_anim_cause, trigger_animation_cause_mask_t::Timeout); + triggered_anim_cause = flag_add(triggered_anim_cause, trigger_animation_cause_mask_t::CpuUpdate); + } + } - timespec now{}; - clock_gettime(CLOCK_MONOTONIC, &now); - - // If we're already past the next frame time, catch up (skip missed frames) - if ((now.tv_sec > next_frame_time.tv_sec) || - (now.tv_sec == next_frame_time.tv_sec && now.tv_nsec > next_frame_time.tv_nsec)) { - // Skip ahead until next_frame_time is >= now - do { - next_frame_time.tv_nsec += state.frame_time_ns; - while (next_frame_time.tv_nsec >= 1000000000LL) { - next_frame_time.tv_nsec -= 1000000000LL; - next_frame_time.tv_sec += 1; - } - } while ((now.tv_sec > next_frame_time.tv_sec) || - (now.tv_sec == next_frame_time.tv_sec && now.tv_nsec > next_frame_time.tv_nsec)); - - state.time_until_next_frame_ms = 0; - //BONGOCAT_LOG_VERBOSE("Animation skipped frame(s) to catch up"); - } else { - // Sleep until the next frame using absolute time - const auto sec_diff = next_frame_time.tv_sec - now.tv_sec; - const auto nsec_diff = next_frame_time.tv_nsec - now.tv_nsec; - state.time_until_next_frame_ms = static_cast(sec_diff * 1000L + (nsec_diff + 999999LL) / 1000000LL); - - if (clock_nanosleep(CLOCK_MONOTONIC, TIMER_ABSTIME, &next_frame_time, nullptr) != 0) { - // Interrupted, just continue - } - } + // Update Animations + { + platform::LockGuard guard(trigger_ctx.thread_context.anim_lock); + assert(ctx.shm); + const bool frame_changed = anim_update_state(trigger_ctx, state, + { + .anim_cause = triggered_anim_cause, + .any_key_press_counter = any_key_press_counter, + }); + if (frame_changed) { + uint64_t u = 1; + if (write(trigger_ctx.render_efd._fd, &u, sizeof(uint64_t)) >= 0) { + BONGOCAT_LOG_VERBOSE("animation: Write animation render event"); + } else { + BONGOCAT_LOG_ERROR("animation: Failed to write to notify pipe in animation: %s", strerror(errno)); + } + } + + // Advance next frame time by exactly one frame + next_frame_time.tv_nsec += state.frame_time_ns; + while (next_frame_time.tv_nsec >= 1000000000LL) { + next_frame_time.tv_nsec -= 1000000000LL; + next_frame_time.tv_sec += 1; + } + + timespec now{}; + clock_gettime(CLOCK_MONOTONIC, &now); + + // If we're already past the next frame time, catch up (skip missed frames) + if ((now.tv_sec > next_frame_time.tv_sec) || + (now.tv_sec == next_frame_time.tv_sec && now.tv_nsec > next_frame_time.tv_nsec)) { + // Skip ahead until next_frame_time is >= now + do { + next_frame_time.tv_nsec += state.frame_time_ns; + while (next_frame_time.tv_nsec >= 1000000000LL) { + next_frame_time.tv_nsec -= 1000000000LL; + next_frame_time.tv_sec += 1; + } + } while ((now.tv_sec > next_frame_time.tv_sec) || + (now.tv_sec == next_frame_time.tv_sec && now.tv_nsec > next_frame_time.tv_nsec)); + + state.time_until_next_frame_ms = 0; + // BONGOCAT_LOG_VERBOSE("Animation skipped frame(s) to catch up"); + } else { + // Sleep until the next frame using absolute time + const auto sec_diff = next_frame_time.tv_sec - now.tv_sec; + const auto nsec_diff = next_frame_time.tv_nsec - now.tv_nsec; + state.time_until_next_frame_ms = + static_cast((sec_diff * 1000L) + (nsec_diff + 999999LL) / 1000000LL); + + if (clock_nanosleep(CLOCK_MONOTONIC, TIMER_ABSTIME, &next_frame_time, BONGOCAT_NULLPTR) != 0) { + // Interrupted, just continue + } + } - // Update variables from config in case FPS changed - state.frame_time_ns = (fps > 0) ? 1000000000LL / fps : 1000000000LL / DEFAULT_FPS; - state.frame_time_ms = state.frame_time_ns / 1000000LL; - } + // Update variables from config in case FPS changed + state.frame_time_ns = (fps > 0) ? 1000000000LL / fps : 1000000000LL / DEFAULT_FPS; + state.frame_time_ms = state.frame_time_ns / 1000000LL; + } - // handle update config - if (reload_config) { - assert(trigger_ctx._config_generation != nullptr); - assert(trigger_ctx._configs_reloaded_cond != nullptr); - assert(trigger_ctx._config != nullptr); + // handle update config + if (reload_config) { + assert(trigger_ctx._config_generation != BONGOCAT_NULLPTR); + assert(trigger_ctx._configs_reloaded_cond != BONGOCAT_NULLPTR); + assert(trigger_ctx._config != BONGOCAT_NULLPTR); + + update_config(ctx, *trigger_ctx._config, new_gen); + + // wait for reload config to be done (all configs) + const int rc = trigger_ctx._configs_reloaded_cond->timedwait( + [&] { return atomic_load(trigger_ctx._config_generation) >= new_gen; }, COND_RELOAD_CONFIGS_TIMEOUT_MS); + if (rc == ETIMEDOUT) { + BONGOCAT_LOG_WARNING("animation: Timed out waiting for reload eventfd: %s", strerror(errno)); + } + if constexpr (features::Debug) { + if (atomic_load(&trigger_ctx.thread_context.config_seen_generation) < + atomic_load(trigger_ctx._config_generation)) { + BONGOCAT_LOG_VERBOSE( + "animation: trigger_ctx.anim.config_seen_generation < trigger_ctx._config_generation; %d < %d", + atomic_load(&trigger_ctx.thread_context.config_seen_generation), + atomic_load(trigger_ctx._config_generation)); + } + } + // assert(atomic_load(&trigger_ctx.anim.config_seen_generation) >= atomic_load(trigger_ctx._config_generation)); + atomic_store(&trigger_ctx.thread_context.config_seen_generation, atomic_load(trigger_ctx._config_generation)); + BONGOCAT_LOG_INFO("animation: Animation config reloaded (gen=%u)", new_gen); + } + } - update_config(ctx, *trigger_ctx._config, new_gen); + // Will run only on normal return + pthread_cleanup_pop(1); // 1 = call cleanup even if not canceled - // wait for reload config to be done (all configs) - const int rc = trigger_ctx._configs_reloaded_cond->timedwait([&] { - return atomic_load(trigger_ctx._config_generation) >= new_gen; - }, COND_RELOAD_CONFIGS_TIMEOUT_MS); - if (rc == ETIMEDOUT) { - BONGOCAT_LOG_WARNING("animation: Timed out waiting for reload eventfd: %s", strerror(errno)); - } - if constexpr (features::Debug) { - if (atomic_load(&trigger_ctx.anim.config_seen_generation) < atomic_load(trigger_ctx._config_generation)) { - BONGOCAT_LOG_VERBOSE("animation: trigger_ctx.anim.config_seen_generation < trigger_ctx._config_generation; %d < %d", atomic_load(&trigger_ctx.anim.config_seen_generation), atomic_load(trigger_ctx._config_generation)); - } - } - //assert(atomic_load(&trigger_ctx.anim.config_seen_generation) >= atomic_load(trigger_ctx._config_generation)); - atomic_store(&trigger_ctx.anim.config_seen_generation, atomic_load(trigger_ctx._config_generation)); - BONGOCAT_LOG_INFO("animation: Animation config reloaded (gen=%u)", new_gen); - } - } + // done when callback cleanup_anim_context + // cleanup_anim_context(arg); - // Will run only on normal return - pthread_cleanup_pop(1); // 1 = call cleanup even if not canceled + BONGOCAT_LOG_INFO("Animation thread main loop exited"); - // done when callback cleanup_anim_context - //cleanup_anim_context(arg); + return BONGOCAT_NULLPTR; +} - BONGOCAT_LOG_INFO("Animation thread main loop exited"); +// ============================================================================= +// PUBLIC API IMPLEMENTATION +// ============================================================================= + +bongocat_error_t start(animation_context_t& animation_ctx, platform::input::input_context_t& input, + platform::update::update_context_t& upd, const config::config_t& config, + platform::CondVariable& configs_reloaded_cond, atomic_uint64_t& config_generation) { + BONGOCAT_LOG_INFO("Starting animation thread"); + + // Initialize shared memory for local config + animation_ctx.thread_context._local_copy_config = platform::make_allocated_mmap(); + if (!animation_ctx.thread_context._local_copy_config) [[unlikely]] { + BONGOCAT_LOG_ERROR("Failed to create shared memory for input monitoring: %s", strerror(errno)); + return bongocat_error_t::BONGOCAT_ERROR_MEMORY; + } + assert(animation_ctx.thread_context._local_copy_config); + update_config(animation_ctx.thread_context, config, atomic_load(&config_generation)); + + // set extern/global references + animation_ctx._input = &input; + animation_ctx._update = &upd; + animation_ctx._config = &config; + animation_ctx._configs_reloaded_cond = &configs_reloaded_cond; + animation_ctx._config_generation = &config_generation; + atomic_store(&animation_ctx.ready, true); + animation_ctx.init_cond.notify_all(); + + animation_ctx._configs_reloaded_cond->notify_all(); + + // start animation thread + const int result = + pthread_create(&animation_ctx.thread_context._anim_thread, BONGOCAT_NULLPTR, anim_thread, &animation_ctx); + if (result != 0) { + BONGOCAT_LOG_ERROR("Failed to create animation thread: %s", strerror(result)); + return bongocat_error_t::BONGOCAT_ERROR_THREAD; + } + + BONGOCAT_LOG_DEBUG("Animation thread started successfully"); + return bongocat_error_t::BONGOCAT_SUCCESS; +} - return nullptr; - } +void trigger(animation_context_t& animation_ctx, trigger_animation_cause_mask_t cause) { + const auto u = static_cast(cause); + if (write(animation_ctx.trigger_efd._fd, &u, sizeof(uint64_t)) >= 0) { + BONGOCAT_LOG_VERBOSE("Write animation trigger event: %zu", cause); + } else { + BONGOCAT_LOG_ERROR("Failed to write to notify pipe in animation: %s", strerror(errno)); + } +} - // ============================================================================= - // PUBLIC API IMPLEMENTATION - // ============================================================================= +void trigger_update_config(animation_context_t& animation_ctx, const config::config_t& config, + uint64_t config_generation) { + // assert(trigger_ctx.anim._local_copy_config != nullptr); + // assert(trigger_ctx.anim.shm != nullptr); + + animation_ctx._config = &config; + if (write(animation_ctx.thread_context.update_config_efd._fd, &config_generation, sizeof(uint64_t)) >= 0) { + BONGOCAT_LOG_VERBOSE("Write animation trigger update config"); + } else { + BONGOCAT_LOG_ERROR("Failed to write to notify pipe in animation: %s", strerror(errno)); + } +} - bongocat_error_t start(animation_session_t& trigger_ctx, platform::input::input_context_t& input, platform::update::update_context_t& upd, const config::config_t& config, platform::CondVariable& configs_reloaded_cond, atomic_uint64_t& config_generation) { - BONGOCAT_LOG_INFO("Starting animation thread"); +BONGOCAT_NODISCARD static int rand_animation_index(animation_thread_context_t& ctx, const config::config_t& config) { + using namespace assets; + assert(ctx._local_copy_config); + assert(ctx.shm); + platform::random_xoshiro128& rng = ctx._rng; - // Initialize shared memory for local config - trigger_ctx.anim._local_copy_config = platform::make_allocated_mmap(); - if (!trigger_ctx.anim._local_copy_config.ptr) { - BONGOCAT_LOG_ERROR("Failed to create shared memory for input monitoring: %s", strerror(errno)); - return bongocat_error_t::BONGOCAT_ERROR_MEMORY; + if (config.randomize_index >= 1) { + if constexpr (features::EnableLazyLoadAssets) { + switch (config.animation_sprite_sheet_layout) { + case config::config_animation_sprite_sheet_layout_t::None: + return config.animation_index; + case config::config_animation_sprite_sheet_layout_t::Bongocat: + assert(BONGOCAT_ANIM_COUNT <= INT32_MAX && BONGOCAT_ANIM_COUNT <= UINT32_MAX); + return BONGOCAT_ANIM_COUNT > 0 ? static_cast(rng.range(0, BONGOCAT_ANIM_COUNT - 1)) : 0; + case config::config_animation_sprite_sheet_layout_t::Dm: + switch (ctx.shm->anim_dm_set) { + case config::config_animation_dm_set_t::None: + return config.animation_index; + case config::config_animation_dm_set_t::min_dm: + assert(MIN_DM_ANIM_COUNT <= INT32_MAX && MIN_DM_ANIM_COUNT <= UINT32_MAX); + return MIN_DM_ANIM_COUNT > 0 ? static_cast(rng.range(0, MIN_DM_ANIM_COUNT - 1)) : 0; + case config::config_animation_dm_set_t::dm: + assert(DM_ANIM_COUNT <= INT32_MAX && DM_ANIM_COUNT <= UINT32_MAX); + return DM_ANIM_COUNT > 0 ? static_cast(rng.range(0, DM_ANIM_COUNT - 1)) : 0; + case config::config_animation_dm_set_t::dm20: + assert(DM20_ANIM_COUNT <= INT32_MAX && DM20_ANIM_COUNT <= UINT32_MAX); + return DM20_ANIM_COUNT > 0 ? static_cast(rng.range(0, DM20_ANIM_COUNT - 1)) : 0; + case config::config_animation_dm_set_t::dmx: + assert(DMX_ANIM_COUNT <= INT32_MAX && DMX_ANIM_COUNT <= UINT32_MAX); + return DMX_ANIM_COUNT > 0 ? static_cast(rng.range(0, DMX_ANIM_COUNT - 1)) : 0; + case config::config_animation_dm_set_t::pen: + assert(PEN_ANIM_COUNT <= INT32_MAX && PEN_ANIM_COUNT <= UINT32_MAX); + return PEN_ANIM_COUNT > 0 ? static_cast(rng.range(0, PEN_ANIM_COUNT - 1)) : 0; + case config::config_animation_dm_set_t::pen20: + assert(PEN20_ANIM_COUNT <= INT32_MAX && PEN20_ANIM_COUNT <= UINT32_MAX); + return PEN20_ANIM_COUNT > 0 ? static_cast(rng.range(0, PEN20_ANIM_COUNT - 1)) : 0; + case config::config_animation_dm_set_t::dmc: + assert(DMC_ANIM_COUNT <= INT32_MAX && DMC_ANIM_COUNT <= UINT32_MAX); + return DMC_ANIM_COUNT > 0 ? static_cast(rng.range(0, DMC_ANIM_COUNT - 1)) : 0; + case config::config_animation_dm_set_t::dmall: + assert(DMALL_ANIM_COUNT <= INT32_MAX && DMALL_ANIM_COUNT <= UINT32_MAX); + return DMALL_ANIM_COUNT > 0 ? static_cast(rng.range(0, DMALL_ANIM_COUNT - 1)) : 0; } - assert(trigger_ctx.anim._local_copy_config != nullptr); - update_config(trigger_ctx.anim, config, atomic_load(&config_generation)); - - // set extern/global references - trigger_ctx._input = &input; - trigger_ctx._update = &upd; - trigger_ctx._config = &config; - trigger_ctx._configs_reloaded_cond = &configs_reloaded_cond; - trigger_ctx._config_generation = &config_generation; - atomic_store(&trigger_ctx.ready, true); - trigger_ctx.init_cond.notify_all(); - - trigger_ctx._configs_reloaded_cond->notify_all(); - - // start animation thread - const int result = pthread_create(&trigger_ctx.anim._anim_thread, nullptr, anim_thread, &trigger_ctx); - if (result != 0) { - BONGOCAT_LOG_ERROR("Failed to create animation thread: %s", strerror(result)); - return bongocat_error_t::BONGOCAT_ERROR_THREAD; + break; + case config::config_animation_sprite_sheet_layout_t::Pkmn: + assert(PKMN_ANIM_COUNT <= INT32_MAX && PKMN_ANIM_COUNT <= UINT32_MAX); + return PKMN_ANIM_COUNT > 0 ? static_cast(rng.range(0, PKMN_ANIM_COUNT - 1)) : 0; + case config::config_animation_sprite_sheet_layout_t::MsAgent: + assert(MS_AGENTS_ANIM_COUNT <= INT32_MAX && MS_AGENTS_ANIM_COUNT <= UINT32_MAX); + return MS_AGENTS_ANIM_COUNT > 0 ? static_cast(rng.range(0, MS_AGENTS_ANIM_COUNT - 1)) : 0; + case config::config_animation_sprite_sheet_layout_t::Custom: + switch (ctx.shm->anim_custom_set) { + case config::config_animation_custom_set_t::None: + break; + case config::config_animation_custom_set_t::misc: + assert(MISC_ANIM_COUNT <= INT32_MAX && MISC_ANIM_COUNT <= UINT32_MAX); + return MISC_ANIM_COUNT > 0 ? static_cast(rng.range(0, MISC_ANIM_COUNT - 1)) : 0; + case config::config_animation_custom_set_t::pmd: + assert(PMD_ANIM_COUNT <= INT32_MAX && PMD_ANIM_COUNT <= UINT32_MAX); + return PMD_ANIM_COUNT > 0 ? static_cast(rng.range(0, PMD_ANIM_COUNT - 1)) : 0; + case config::config_animation_custom_set_t::custom: + if (config.animation_index == CUSTOM_ANIM_INDEX) { + return config.animation_index; + } + break; } - - BONGOCAT_LOG_DEBUG("Animation thread started successfully"); - return bongocat_error_t::BONGOCAT_SUCCESS; + } } - - - void trigger(animation_session_t& trigger_ctx, trigger_animation_cause_mask_t cause) { - const auto u = static_cast(cause); - if (write(trigger_ctx.trigger_efd._fd, &u, sizeof(uint64_t)) >= 0) { - BONGOCAT_LOG_VERBOSE("Write animation trigger event: %zu", cause); - } else { - BONGOCAT_LOG_ERROR("Failed to write to notify pipe in animation: %s", strerror(errno)); + if constexpr (!features::EnableLazyLoadAssets || features::EnablePreloadAssets) { + switch (config.animation_sprite_sheet_layout) { + case config::config_animation_sprite_sheet_layout_t::None: + return config.animation_index; + case config::config_animation_sprite_sheet_layout_t::Bongocat: + assert(ctx.shm->bongocat_anims.count > 0); + assert(ctx.shm->bongocat_anims.count <= INT32_MAX && ctx.shm->bongocat_anims.count <= UINT32_MAX); + return ctx.shm->bongocat_anims.count > 0 + ? static_cast(rng.range(0, static_cast(ctx.shm->bongocat_anims.count - 1))) + : 0; + case config::config_animation_sprite_sheet_layout_t::Dm: + switch (ctx.shm->anim_dm_set) { + case config::config_animation_dm_set_t::None: + return config.animation_index; + case config::config_animation_dm_set_t::min_dm: + assert(ctx.shm->min_dm_anims.count > 0); + assert(ctx.shm->min_dm_anims.count <= INT32_MAX && ctx.shm->min_dm_anims.count <= UINT32_MAX); + return ctx.shm->min_dm_anims.count > 0 + ? static_cast(rng.range(0, static_cast(ctx.shm->min_dm_anims.count - 1))) + : 0; + case config::config_animation_dm_set_t::dm: + assert(ctx.shm->dm_anims.count > 0); + assert(ctx.shm->dm_anims.count <= INT32_MAX && ctx.shm->dm_anims.count <= UINT32_MAX); + return ctx.shm->dm_anims.count > 0 + ? static_cast(rng.range(0, static_cast(ctx.shm->dm_anims.count - 1))) + : 0; + case config::config_animation_dm_set_t::dm20: + assert(ctx.shm->dm20_anims.count > 0); + assert(ctx.shm->dm20_anims.count <= INT32_MAX && ctx.shm->dm20_anims.count <= UINT32_MAX); + return ctx.shm->dm20_anims.count > 0 + ? static_cast(rng.range(0, static_cast(ctx.shm->dm20_anims.count - 1))) + : 0; + case config::config_animation_dm_set_t::dmx: + assert(ctx.shm->dmx_anims.count > 0); + assert(ctx.shm->dmx_anims.count <= INT32_MAX && ctx.shm->dmx_anims.count <= UINT32_MAX); + return ctx.shm->dmx_anims.count > 0 + ? static_cast(rng.range(0, static_cast(ctx.shm->dmx_anims.count - 1))) + : 0; + case config::config_animation_dm_set_t::pen: + assert(ctx.shm->pen_anims.count > 0); + assert(ctx.shm->pen_anims.count <= INT32_MAX && ctx.shm->pen_anims.count <= UINT32_MAX); + return ctx.shm->pen_anims.count > 0 + ? static_cast(rng.range(0, static_cast(ctx.shm->pen_anims.count - 1))) + : 0; + case config::config_animation_dm_set_t::pen20: + assert(ctx.shm->pen20_anims.count > 0); + assert(ctx.shm->pen20_anims.count <= INT32_MAX && ctx.shm->pen20_anims.count <= UINT32_MAX); + return ctx.shm->pen20_anims.count > 0 + ? static_cast(rng.range(0, static_cast(ctx.shm->pen20_anims.count - 1))) + : 0; + case config::config_animation_dm_set_t::dmc: + assert(ctx.shm->dmc_anims.count > 0); + assert(ctx.shm->dmc_anims.count <= INT32_MAX && ctx.shm->dmc_anims.count <= UINT32_MAX); + return ctx.shm->dmc_anims.count > 0 + ? static_cast(rng.range(0, static_cast(ctx.shm->dmc_anims.count - 1))) + : 0; + case config::config_animation_dm_set_t::dmall: + assert(ctx.shm->dmall_anims.count > 0); + assert(ctx.shm->dmall_anims.count <= INT32_MAX && ctx.shm->dmall_anims.count <= UINT32_MAX); + return ctx.shm->dmall_anims.count > 0 + ? static_cast(rng.range(0, static_cast(ctx.shm->dmall_anims.count - 1))) + : 0; } - } - - void trigger_update_config(animation_session_t& trigger_ctx, const config::config_t& config, uint64_t config_generation) { - //assert(trigger_ctx.anim._local_copy_config != nullptr); - //assert(trigger_ctx.anim.shm != nullptr); - - trigger_ctx._config = &config; - if (write(trigger_ctx.anim.update_config_efd._fd, &config_generation, sizeof(uint64_t)) >= 0) { - BONGOCAT_LOG_VERBOSE("Write animation trigger update config"); - } else { - BONGOCAT_LOG_ERROR("Failed to write to notify pipe in animation: %s", strerror(errno)); + break; + case config::config_animation_sprite_sheet_layout_t::Pkmn: + assert(ctx.shm->pkmn_anims.count > 0); + assert(ctx.shm->pkmn_anims.count <= INT32_MAX && ctx.shm->pkmn_anims.count <= UINT32_MAX); + return ctx.shm->pkmn_anims.count > 0 + ? static_cast(rng.range(0, static_cast(ctx.shm->pkmn_anims.count - 1))) + : 0; + case config::config_animation_sprite_sheet_layout_t::MsAgent: + assert(ctx.shm->ms_anims.count > 0); + assert(ctx.shm->ms_anims.count <= INT32_MAX && ctx.shm->ms_anims.count <= UINT32_MAX); + return ctx.shm->ms_anims.count > 0 + ? static_cast(rng.range(0, static_cast(ctx.shm->ms_anims.count - 1))) + : 0; + case config::config_animation_sprite_sheet_layout_t::Custom: + switch (ctx.shm->anim_custom_set) { + case config::config_animation_custom_set_t::None: + break; + case config::config_animation_custom_set_t::misc: + assert(ctx.shm->misc_anims.count > 0); + assert(ctx.shm->misc_anims.count <= INT32_MAX && ctx.shm->misc_anims.count <= UINT32_MAX); + return ctx.shm->misc_anims.count > 0 + ? static_cast(rng.range(0, static_cast(ctx.shm->misc_anims.count - 1))) + : 0; + case config::config_animation_custom_set_t::pmd: + assert(ctx.shm->pmd_anims.count > 0); + assert(ctx.shm->pmd_anims.count <= INT32_MAX && ctx.shm->pmd_anims.count <= UINT32_MAX); + return ctx.shm->pmd_anims.count > 0 + ? static_cast(rng.range(0, static_cast(ctx.shm->pmd_anims.count - 1))) + : 0; + case config::config_animation_custom_set_t::custom: + if (config.animation_index == CUSTOM_ANIM_INDEX) { + return config.animation_index; + } + break; } + } } + } - [[nodiscard]] static int rand_animation_index(animation_context_t& ctx, const config::config_t& config) { - using namespace assets; - assert(ctx._local_copy_config != nullptr); - assert(ctx.shm != nullptr); - platform::random_xoshiro128& rng = ctx._rng; - - if (config.randomize_index) { - if constexpr (features::EnableLazyLoadAssets) { - switch (config.animation_sprite_sheet_layout) { - case config::config_animation_sprite_sheet_layout_t::None: - return config.animation_index; - case config::config_animation_sprite_sheet_layout_t::Bongocat: - assert(BONGOCAT_ANIM_COUNT <= INT32_MAX && BONGOCAT_ANIM_COUNT <= UINT32_MAX); - return BONGOCAT_ANIM_COUNT > 0 ? static_cast(rng.range(0, BONGOCAT_ANIM_COUNT-1)) : 0; - case config::config_animation_sprite_sheet_layout_t::Dm: - switch (ctx.shm->anim_dm_set) { - case config::config_animation_dm_set_t::None: - return config.animation_index; - case config::config_animation_dm_set_t::min_dm: - assert(MIN_DM_ANIM_COUNT <= INT32_MAX && MIN_DM_ANIM_COUNT<= UINT32_MAX); - return MIN_DM_ANIM_COUNT > 0 ? static_cast(rng.range(0, MIN_DM_ANIM_COUNT-1)) : 0; - case config::config_animation_dm_set_t::dm: - assert(DM_ANIM_COUNT <= INT32_MAX && DM_ANIM_COUNT <= UINT32_MAX); - return DM_ANIM_COUNT > 0 ? static_cast(rng.range(0, DM_ANIM_COUNT-1)) : 0; - case config::config_animation_dm_set_t::dm20: - assert(DM20_ANIM_COUNT <= INT32_MAX && DM20_ANIM_COUNT <= UINT32_MAX); - return DM20_ANIM_COUNT > 0 ? static_cast(rng.range(0, DM20_ANIM_COUNT-1)) : 0; - case config::config_animation_dm_set_t::dmx: - assert(DMX_ANIM_COUNT<= INT32_MAX && DMX_ANIM_COUNT <= UINT32_MAX); - return DMX_ANIM_COUNT > 0 ? static_cast(rng.range(0, DMX_ANIM_COUNT-1)) : 0; - case config::config_animation_dm_set_t::pen: - assert(PEN_ANIM_COUNT <= INT32_MAX && PEN_ANIM_COUNT <= UINT32_MAX); - return PEN_ANIM_COUNT > 0 ? static_cast(rng.range(0, PEN_ANIM_COUNT-1)) : 0; - case config::config_animation_dm_set_t::pen20: - assert(PEN20_ANIM_COUNT <= INT32_MAX && PEN20_ANIM_COUNT <= UINT32_MAX); - return PEN20_ANIM_COUNT > 0 ? static_cast(rng.range(0, PEN20_ANIM_COUNT-1)) : 0; - case config::config_animation_dm_set_t::dmc: - assert(DMC_ANIM_COUNT <= INT32_MAX && DMC_ANIM_COUNT <= UINT32_MAX); - return DMC_ANIM_COUNT > 0 ? static_cast(rng.range(0, DMC_ANIM_COUNT-1)) : 0; - case config::config_animation_dm_set_t::dmall: - assert(DMALL_ANIM_COUNT <= INT32_MAX && DMALL_ANIM_COUNT <= UINT32_MAX); - return DMALL_ANIM_COUNT > 0 ? static_cast(rng.range(0, DMALL_ANIM_COUNT-1)) : 0; - } - break; - case config::config_animation_sprite_sheet_layout_t::Pkmn: - assert(PKMN_ANIM_COUNT <= INT32_MAX && PKMN_ANIM_COUNT <= UINT32_MAX); - return PKMN_ANIM_COUNT > 0 ? static_cast(rng.range(0, PKMN_ANIM_COUNT-1)) : 0; - case config::config_animation_sprite_sheet_layout_t::MsAgent: - assert(MS_AGENTS_ANIM_COUNT <= INT32_MAX && MS_AGENTS_ANIM_COUNT <= UINT32_MAX); - return MS_AGENTS_ANIM_COUNT > 0 ? static_cast(rng.range(0, MS_AGENTS_ANIM_COUNT-1)) : 0; - case config::config_animation_sprite_sheet_layout_t::Custom: - switch (ctx.shm->anim_custom_set) { - case config::config_animation_custom_set_t::None: - break; - case config::config_animation_custom_set_t::misc: - assert(MISC_ANIM_COUNT <= INT32_MAX && MISC_ANIM_COUNT <= UINT32_MAX); - return MISC_ANIM_COUNT > 0 ? static_cast(rng.range(0, MISC_ANIM_COUNT-1)) : 0; - case config::config_animation_custom_set_t::pmd: - assert(PMD_ANIM_COUNT <= INT32_MAX && PMD_ANIM_COUNT <= UINT32_MAX); - return PMD_ANIM_COUNT > 0 ? static_cast(rng.range(0, PMD_ANIM_COUNT-1)) : 0; - case config::config_animation_custom_set_t::custom: - if (config.animation_index == CUSTOM_ANIM_INDEX) { - return config.animation_index; - } - break; - } - } - } - if constexpr (!features::EnableLazyLoadAssets || features::EnablePreloadAssets) { - switch (config.animation_sprite_sheet_layout) { - case config::config_animation_sprite_sheet_layout_t::None: - return config.animation_index; - case config::config_animation_sprite_sheet_layout_t::Bongocat: - assert(ctx.shm->bongocat_anims.count > 0); - assert(ctx.shm->bongocat_anims.count <= INT32_MAX && ctx.shm->bongocat_anims.count <= UINT32_MAX); - return ctx.shm->bongocat_anims.count > 0 ? static_cast(rng.range(0, static_cast(ctx.shm->bongocat_anims.count-1))) : 0; - case config::config_animation_sprite_sheet_layout_t::Dm: - switch (ctx.shm->anim_dm_set) { - case config::config_animation_dm_set_t::None: - return config.animation_index; - case config::config_animation_dm_set_t::min_dm: - assert(ctx.shm->min_dm_anims.count > 0); - assert(ctx.shm->min_dm_anims.count <= INT32_MAX && ctx.shm->min_dm_anims.count <= UINT32_MAX); - return ctx.shm->min_dm_anims.count > 0 ? static_cast(rng.range(0, static_cast(ctx.shm->min_dm_anims.count-1))) : 0; - case config::config_animation_dm_set_t::dm: - assert(ctx.shm->dm_anims.count > 0); - assert(ctx.shm->dm_anims.count <= INT32_MAX && ctx.shm->dm_anims.count <= UINT32_MAX); - return ctx.shm->dm_anims.count > 0 ? static_cast(rng.range(0, static_cast(ctx.shm->dm_anims.count-1))) : 0; - case config::config_animation_dm_set_t::dm20: - assert(ctx.shm->dm20_anims.count > 0); - assert(ctx.shm->dm20_anims.count <= INT32_MAX && ctx.shm->dm20_anims.count <= UINT32_MAX); - return ctx.shm->dm20_anims.count > 0 ? static_cast(rng.range(0, static_cast(ctx.shm->dm20_anims.count-1))) : 0; - case config::config_animation_dm_set_t::dmx: - assert(ctx.shm->dmx_anims.count > 0); - assert(ctx.shm->dmx_anims.count <= INT32_MAX && ctx.shm->dmx_anims.count <= UINT32_MAX); - return ctx.shm->dmx_anims.count > 0 ? static_cast(rng.range(0, static_cast(ctx.shm->dmx_anims.count-1))) : 0; - case config::config_animation_dm_set_t::pen: - assert(ctx.shm->pen_anims.count > 0); - assert(ctx.shm->pen_anims.count <= INT32_MAX && ctx.shm->pen_anims.count <= UINT32_MAX); - return ctx.shm->pen_anims.count > 0 ? static_cast(rng.range(0, static_cast(ctx.shm->pen_anims.count-1))) : 0; - case config::config_animation_dm_set_t::pen20: - assert(ctx.shm->pen20_anims.count > 0); - assert(ctx.shm->pen20_anims.count <= INT32_MAX && ctx.shm->pen20_anims.count <= UINT32_MAX); - return ctx.shm->pen20_anims.count > 0 ? static_cast(rng.range(0, static_cast(ctx.shm->pen20_anims.count-1))) : 0; - case config::config_animation_dm_set_t::dmc: - assert(ctx.shm->dmc_anims.count > 0); - assert(ctx.shm->dmc_anims.count <= INT32_MAX && ctx.shm->dmc_anims.count <= UINT32_MAX); - return ctx.shm->dmc_anims.count > 0 ? static_cast(rng.range(0, static_cast(ctx.shm->dmc_anims.count-1))) : 0; - case config::config_animation_dm_set_t::dmall: - assert(ctx.shm->dmall_anims.count > 0); - assert(ctx.shm->dmall_anims.count <= INT32_MAX && ctx.shm->dmall_anims.count <= UINT32_MAX); - return ctx.shm->dmall_anims.count > 0 ? static_cast(rng.range(0, static_cast(ctx.shm->dmall_anims.count-1))) : 0; - } - break; - case config::config_animation_sprite_sheet_layout_t::Pkmn: - assert(ctx.shm->pkmn_anims.count > 0); - assert(ctx.shm->pkmn_anims.count <= INT32_MAX && ctx.shm->pkmn_anims.count <= UINT32_MAX); - return ctx.shm->pkmn_anims.count > 0 ? static_cast(rng.range(0, static_cast(ctx.shm->pkmn_anims.count-1))) : 0; - case config::config_animation_sprite_sheet_layout_t::MsAgent: - assert(ctx.shm->ms_anims.count > 0); - assert(ctx.shm->ms_anims.count <= INT32_MAX && ctx.shm->ms_anims.count <= UINT32_MAX); - return ctx.shm->ms_anims.count > 0 ? static_cast(rng.range(0, static_cast(ctx.shm->ms_anims.count-1))) : 0; - case config::config_animation_sprite_sheet_layout_t::Custom: - switch(ctx.shm->anim_custom_set) { - case config::config_animation_custom_set_t::None: - break; - case config::config_animation_custom_set_t::misc: - assert(ctx.shm->misc_anims.count > 0); - assert(ctx.shm->misc_anims.count <= INT32_MAX && ctx.shm->misc_anims.count <= UINT32_MAX); - return ctx.shm->misc_anims.count > 0 ? static_cast(rng.range(0, static_cast(ctx.shm->misc_anims.count-1))) : 0; - case config::config_animation_custom_set_t::pmd: - assert(ctx.shm->pmd_anims.count > 0); - assert(ctx.shm->pmd_anims.count <= INT32_MAX && ctx.shm->pmd_anims.count <= UINT32_MAX); - return ctx.shm->pmd_anims.count > 0 ? static_cast(rng.range(0, static_cast(ctx.shm->pmd_anims.count-1))) : 0; - case config::config_animation_custom_set_t::custom: - if (config.animation_index == CUSTOM_ANIM_INDEX) { - return config.animation_index; - } - break; - } - } - } - } + return config.animation_index; +} - return config.animation_index; +static void update_config_reload_sprite_sheet(animation_thread_context_t& ctx) { + using namespace assets; + assert(ctx._local_copy_config); + + platform::LockGuard guard(ctx.anim_lock); + const auto old_anim_type = ctx.shm->anim_type; + const auto old_anim_dm_set = ctx.shm->anim_dm_set; + const auto old_anim_custom_set = ctx.shm->anim_custom_set; + const auto old_anim_index = ctx.shm->anim_index; + + ctx.shm->anim_type = ctx._local_copy_config->animation_sprite_sheet_layout; + ctx.shm->anim_dm_set = ctx._local_copy_config->animation_dm_set; + ctx.shm->anim_custom_set = ctx._local_copy_config->animation_custom_set; + /// @NOTE: set dm_set, etc. first so rand_animation_index works + ctx.shm->anim_index = !ctx._local_copy_config->_keep_old_animation_index + ? rand_animation_index(ctx, *ctx._local_copy_config) + : old_anim_index; + + [[maybe_unused]] const auto t0 = platform::get_current_time_us(); + if constexpr (features::EnableLazyLoadAssets) { + auto [result, error] = hot_load_animation(ctx); + if (error != bongocat_error_t::BONGOCAT_SUCCESS) { + // rollback + ctx.shm->anim_type = old_anim_type; + ctx.shm->anim_dm_set = old_anim_dm_set; + ctx.shm->anim_custom_set = old_anim_custom_set; + ctx.shm->anim_index = old_anim_index; } - - static void update_config_reload_sprite_sheet(animation_context_t& ctx) { - using namespace assets; - assert(ctx._local_copy_config != nullptr); - - platform::LockGuard guard (ctx.anim_lock); - const auto old_anim_type = ctx.shm->anim_type; - const auto old_anim_dm_set = ctx.shm->anim_dm_set; - const auto old_anim_custom_set = ctx.shm->anim_custom_set; - const auto old_anim_index = ctx.shm->anim_index; - - ctx.shm->anim_type = ctx._local_copy_config->animation_sprite_sheet_layout; - ctx.shm->anim_dm_set = ctx._local_copy_config->animation_dm_set; - ctx.shm->anim_custom_set = ctx._local_copy_config->animation_custom_set; - /// @NOTE: set dm_set, etc. first so rand_animation_index works - ctx.shm->anim_index = !ctx._local_copy_config->_keep_old_animation_index ? rand_animation_index(ctx, *ctx._local_copy_config) : old_anim_index; - - [[maybe_unused]] const auto t0 = platform::get_current_time_us(); - if constexpr (features::EnableLazyLoadAssets) { - auto [result, error] = hot_load_animation(ctx); - if (error != bongocat_error_t::BONGOCAT_SUCCESS) { - // rollback - ctx.shm->anim_type = old_anim_type; - ctx.shm->anim_dm_set = old_anim_dm_set; - ctx.shm->anim_custom_set = old_anim_custom_set; - ctx.shm->anim_index = old_anim_index; - } + } else { + if constexpr (features::EnableCustomSpriteSheetsAssets) { + if (ctx._local_copy_config->_custom && static_cast(ctx.shm->anim_index) == CUSTOM_ANIM_INDEX) { + auto [result, error] = details::anim_load_custom_animation(ctx, *ctx._local_copy_config); + if (error == bongocat_error_t::BONGOCAT_SUCCESS) { + ctx.shm->anim = bongocat::move(result); } else { - if constexpr (features::EnableCustomSpriteSheetsAssets) { - if (ctx._local_copy_config->_custom && static_cast(ctx.shm->anim_index) == CUSTOM_ANIM_INDEX) { - auto [result, error] = details::anim_load_custom_animation(ctx, *ctx._local_copy_config); - if (error == bongocat_error_t::BONGOCAT_SUCCESS) { - ctx.shm->anim = bongocat::move(result); - } else { - // rollback - ctx.shm->anim_type = old_anim_type; - ctx.shm->anim_dm_set = old_anim_dm_set; - ctx.shm->anim_custom_set = old_anim_custom_set; - ctx.shm->anim_index = old_anim_index; - } - } - } + // rollback + ctx.shm->anim_type = old_anim_type; + ctx.shm->anim_dm_set = old_anim_dm_set; + ctx.shm->anim_custom_set = old_anim_custom_set; + ctx.shm->anim_index = old_anim_index; } + } + } + } - // initial frame - ctx.shm->animation_player_result.sprite_sheet_col = ctx._local_copy_config->idle_frame ? ctx._local_copy_config->idle_frame : 0; - ctx.shm->animation_player_result.sprite_sheet_row = features::EnableCustomSpriteSheetsAssets && ctx._local_copy_config->_custom && ctx._local_copy_config->custom_sprite_sheet_settings.idle_row_index > 0 ? ctx._local_copy_config->custom_sprite_sheet_settings.idle_row_index : 0; + // initial frame + ctx.shm->animation_player_result.sprite_sheet_col = + ctx._local_copy_config->idle_frame ? ctx._local_copy_config->idle_frame : 0; + ctx.shm->animation_player_result.sprite_sheet_row = + features::EnableCustomSpriteSheetsAssets && ctx._local_copy_config->_custom && + ctx._local_copy_config->custom_sprite_sheet_settings.idle_row_index > 0 + ? ctx._local_copy_config->custom_sprite_sheet_settings.idle_row_index + : 0; - [[maybe_unused]] const auto t1 = platform::get_current_time_us(); + [[maybe_unused]] const auto t1 = platform::get_current_time_us(); - BONGOCAT_LOG_DEBUG("Update sprite sheet; load assets in %.3fms (%.6fsec)", static_cast(t1 - t0) / 1000.0, static_cast(t1 - t0) / 1000000.0); - } - void update_config(animation_context_t& ctx, const config::config_t& config, uint64_t new_gen) { - assert(ctx._local_copy_config != nullptr); - assert(ctx.shm != nullptr); + BONGOCAT_LOG_DEBUG("Update sprite sheet; load assets in %.3fms (%.6fsec)", static_cast(t1 - t0) / 1000.0, + static_cast(t1 - t0) / 1000000.0); +} +void update_config(animation_thread_context_t& ctx, const config::config_t& config, uint64_t new_gen) { + assert(ctx._local_copy_config); + assert(ctx.shm); - *ctx._local_copy_config = config; + *ctx._local_copy_config = config; - update_config_reload_sprite_sheet(ctx); + update_config_reload_sprite_sheet(ctx); - atomic_store(&ctx.config_seen_generation, new_gen); - // Signal main that reload is done - ctx.config_updated.notify_all(); - } + atomic_store(&ctx.config_seen_generation, new_gen); + // Signal main that reload is done + ctx.config_updated.notify_all(); } +} // namespace bongocat::animation diff --git a/src/graphics/animation_init.cpp b/src/graphics/animation_init.cpp index 6d67fae4..b0655d60 100644 --- a/src/graphics/animation_init.cpp +++ b/src/graphics/animation_init.cpp @@ -1,648 +1,699 @@ -#include "graphics/animation_context.h" #include "graphics/animation.h" +#include "graphics/animation_thread_context.h" #include "platform/wayland.h" #include "utils/memory.h" + +#include #include #include -#include -#include #include +#include // assets -#include "graphics/embedded_assets_dms.h" -#include "graphics/embedded_assets_pkmn.h" -#include "embedded_assets/bongocat/bongocat.hpp" -#include "embedded_assets/ms_agent/ms_agent.hpp" -#include "embedded_assets/misc/misc.hpp" #include "embedded_assets/bongocat/bongocat.h" -#include "embedded_assets/ms_agent/ms_agent_sprite.h" +#include "embedded_assets/bongocat/bongocat.hpp" #include "embedded_assets/dm/dm_sprite.h" -#include "embedded_assets/min_dm/min_dm_sprite.h" #include "embedded_assets/dm20/dm20_sprite.h" +#include "embedded_assets/dmall/dmall_sprite.h" +#include "embedded_assets/dmc/dmc_sprite.h" #include "embedded_assets/dmx/dmx_sprite.h" +#include "embedded_assets/min_dm/min_dm_sprite.h" +#include "embedded_assets/misc/misc.hpp" +#include "embedded_assets/misc/misc_sprite.h" +#include "embedded_assets/ms_agent/ms_agent.hpp" +#include "embedded_assets/ms_agent/ms_agent_sprite.h" #include "embedded_assets/pen/pen_sprite.h" #include "embedded_assets/pen20/pen20_sprite.h" -#include "embedded_assets/dmc/dmc_sprite.h" -#include "embedded_assets/dmall/dmall_sprite.h" -#include "embedded_assets/misc/misc_sprite.h" #include "embedded_assets/pkmn/pkmn_sprite.h" #include "embedded_assets/pmd/pmd_sprite.h" +#include "graphics/embedded_assets_dms.h" +#include "graphics/embedded_assets_pkmn.h" // image loader #include "image_loader/bongocat/load_images_bongocat.h" #include "image_loader/custom/load_custom.h" -#include "image_loader/ms_agent/load_images_ms_agent.h" #include "image_loader/dm/load_images_dm.h" -#include "image_loader/min_dm/load_images_min_dm.h" #include "image_loader/dm20/load_images_dm20.h" +#include "image_loader/dmall/load_images_dmall.h" +#include "image_loader/dmc/load_images_dmc.h" #include "image_loader/dmx/load_images_dmx.h" +#include "image_loader/min_dm/load_images_min_dm.h" +#include "image_loader/misc/load_images_misc.h" +#include "image_loader/ms_agent/load_images_ms_agent.h" #include "image_loader/pen/load_images_pen.h" #include "image_loader/pen20/load_images_pen20.h" -#include "image_loader/dmc/load_images_dmc.h" -#include "image_loader/dmall/load_images_dmall.h" #include "image_loader/pkmn/load_images_pkmn.h" -#include "image_loader/misc/load_images_misc.h" #include "image_loader/pmd/load_images_pmd.h" - namespace bongocat::animation { - [[maybe_unused]] static constexpr bool should_load_bongocat([[maybe_unused]] const config::config_t& config) { - return features::EnablePreloadAssets || config.animation_sprite_sheet_layout == config::config_animation_sprite_sheet_layout_t::Bongocat; +[[maybe_unused]] static constexpr bool should_load_bongocat([[maybe_unused]] const config::config_t& config) { + return features::EnablePreloadAssets || + config.animation_sprite_sheet_layout == config::config_animation_sprite_sheet_layout_t::Bongocat; +} +[[maybe_unused]] static constexpr bool should_load_dm([[maybe_unused]] const config::config_t& config) { + return features::EnablePreloadAssets || + (config.animation_sprite_sheet_layout == config::config_animation_sprite_sheet_layout_t::Dm && + config.animation_dm_set != config::config_animation_dm_set_t::None); +} +[[maybe_unused]] static constexpr bool should_load_ms_agent([[maybe_unused]] const config::config_t& config) { + return features::EnablePreloadAssets || + config.animation_sprite_sheet_layout == config::config_animation_sprite_sheet_layout_t::MsAgent; +} +[[maybe_unused]] static constexpr bool should_load_pkmn([[maybe_unused]] const config::config_t& config) { + return features::EnablePreloadAssets || + config.animation_sprite_sheet_layout == config::config_animation_sprite_sheet_layout_t::Pkmn; +} +[[maybe_unused]] static constexpr bool should_load_misc([[maybe_unused]] const config::config_t& config) { + return features::EnablePreloadAssets || + (config.animation_sprite_sheet_layout == config::config_animation_sprite_sheet_layout_t::Custom && + config.animation_custom_set == config::config_animation_custom_set_t::misc); +} +[[maybe_unused]] static constexpr bool should_load_custom([[maybe_unused]] const config::config_t& config) { + return (features::EnablePreloadAssets && config._custom) || + (config._custom && + config.animation_sprite_sheet_layout == config::config_animation_sprite_sheet_layout_t::Custom && + config.animation_custom_set == config::config_animation_custom_set_t::custom); +} +[[maybe_unused]] static constexpr bool should_load_pmd([[maybe_unused]] const config::config_t& config) { + return features::EnablePreloadAssets || + (config._custom && + config.animation_sprite_sheet_layout == config::config_animation_sprite_sheet_layout_t::Custom && + config.animation_custom_set == config::config_animation_custom_set_t::pmd); +} + +namespace details { + created_result_t anim_load_custom_animation(animation_thread_context_t& ctx, + const config::config_t& config) { + BONGOCAT_CHECK_NULL(config.custom_sprite_sheet_filename, bongocat_error_t::BONGOCAT_ERROR_INVALID_PARAM); + if (strlen(config.custom_sprite_sheet_filename) <= 0) { + return bongocat_error_t::BONGOCAT_ERROR_INVALID_PARAM; + } + + assert(ctx.shm.ptr); + assert(ctx._local_copy_config.ptr); + + BONGOCAT_LOG_VERBOSE("Load custom Animation: %s ...", config.custom_sprite_sheet_filename); + auto sprite_sheet_image_result = load_custom_sprite_sheet_file(config.custom_sprite_sheet_filename); + if (sprite_sheet_image_result.error != bongocat_error_t::BONGOCAT_SUCCESS) [[unlikely]] { + BONGOCAT_LOG_ERROR("Load custom Animation failed: %s", config.custom_sprite_sheet_filename); + return bongocat_error_t::BONGOCAT_ERROR_ANIMATION; } - [[maybe_unused]] static constexpr bool should_load_dm([[maybe_unused]] const config::config_t& config) { - return features::EnablePreloadAssets || (config.animation_sprite_sheet_layout == config::config_animation_sprite_sheet_layout_t::Dm && config.animation_dm_set != config::config_animation_dm_set_t::None); + + auto result = load_custom_anim(ctx, sprite_sheet_image_result.result, config.custom_sprite_sheet_settings); + free_custom_sprite_sheet_file(sprite_sheet_image_result.result); + if (result.error != bongocat_error_t::BONGOCAT_SUCCESS) [[unlikely]] { + BONGOCAT_LOG_ERROR("Load custom Animation failed: %s", config.custom_sprite_sheet_filename); + return bongocat_error_t::BONGOCAT_ERROR_ANIMATION; } - [[maybe_unused]] static constexpr bool should_load_ms_agent([[maybe_unused]] const config::config_t& config) { - return features::EnablePreloadAssets || config.animation_sprite_sheet_layout == config::config_animation_sprite_sheet_layout_t::MsAgent; + + return result; + } +} // namespace details + +created_result_t hot_load_animation(animation_thread_context_t& ctx) { + // read-only config + assert(ctx._local_copy_config); + const config::config_t& current_config = *ctx._local_copy_config; + assert(ctx.shm); + animation_shared_memory_t& anim_shm = *ctx.shm; + const int anim_index = anim_shm.anim_index; + + switch (anim_shm.anim_type) { + case config::config_animation_sprite_sheet_layout_t::None: + // unload other sprite sheets + cleanup_animation(anim_shm.anim); + break; + case config::config_animation_sprite_sheet_layout_t::Bongocat: { + if constexpr (features::EnableBongocatEmbeddedAssets) { + auto [result, error] = load_bongocat_sprite_sheet(ctx, anim_index); + if (error != bongocat_error_t::BONGOCAT_SUCCESS) [[unlikely]] { + return error; + } + anim_shm.anim = bongocat::move(result); } - [[maybe_unused]] static constexpr bool should_load_pkmn([[maybe_unused]] const config::config_t& config) { - return features::EnablePreloadAssets || config.animation_sprite_sheet_layout == config::config_animation_sprite_sheet_layout_t::Pkmn; + } break; + case config::config_animation_sprite_sheet_layout_t::Dm: { + bongocat_error_t error = bongocat_error_t::BONGOCAT_ERROR_INVALID_PARAM; + dm_sprite_sheet_t result; + switch (anim_shm.anim_dm_set) { + case config::config_animation_dm_set_t::None: + cleanup_animation(result); + error = bongocat_error_t::BONGOCAT_SUCCESS; + break; + case config::config_animation_dm_set_t::min_dm: { + if constexpr (features::EnableMinDmEmbeddedAssets) { + auto [l_result, l_error] = load_min_dm_sprite_sheet(ctx, anim_index); + result = bongocat::move(l_result); + error = bongocat::move(l_error); + } + } break; + case config::config_animation_dm_set_t::dm: { + if constexpr (features::EnableFullDmEmbeddedAssets) { + auto [l_result, l_error] = load_dm_sprite_sheet(ctx, anim_index); + result = bongocat::move(l_result); + error = bongocat::move(l_error); + } + } break; + case config::config_animation_dm_set_t::dm20: { + if constexpr (features::EnableDm20EmbeddedAssets) { + auto [l_result, l_error] = load_dm20_sprite_sheet(ctx, anim_index); + result = bongocat::move(l_result); + error = bongocat::move(l_error); + } + } break; + case config::config_animation_dm_set_t::dmx: { + if constexpr (features::EnableDmxEmbeddedAssets) { + auto [l_result, l_error] = load_dmx_sprite_sheet(ctx, anim_index); + result = bongocat::move(l_result); + error = bongocat::move(l_error); + } + } break; + case config::config_animation_dm_set_t::pen: { + if constexpr (features::EnablePenEmbeddedAssets) { + auto [l_result, l_error] = load_pen_sprite_sheet(ctx, anim_index); + result = bongocat::move(l_result); + error = bongocat::move(l_error); + } + } break; + case config::config_animation_dm_set_t::pen20: { + if constexpr (features::EnablePen20EmbeddedAssets) { + auto [l_result, l_error] = load_pen20_sprite_sheet(ctx, anim_index); + result = bongocat::move(l_result); + error = bongocat::move(l_error); + } + } break; + case config::config_animation_dm_set_t::dmc: { + if constexpr (features::EnableDmcEmbeddedAssets) { + auto [l_result, l_error] = load_dmc_sprite_sheet(ctx, anim_index); + result = bongocat::move(l_result); + error = bongocat::move(l_error); + } + } break; + case config::config_animation_dm_set_t::dmall: { + if constexpr (features::EnableDmAllEmbeddedAssets) { + auto [l_result, l_error] = load_dmall_sprite_sheet(ctx, anim_index); + result = bongocat::move(l_result); + error = bongocat::move(l_error); + } + } break; } - [[maybe_unused]] static constexpr bool should_load_misc([[maybe_unused]] const config::config_t& config) { - return features::EnablePreloadAssets || (config.animation_sprite_sheet_layout == config::config_animation_sprite_sheet_layout_t::Custom && config.animation_custom_set == config::config_animation_custom_set_t::misc); + if (error != bongocat_error_t::BONGOCAT_SUCCESS) [[unlikely]] { + return error; } - [[maybe_unused]] static constexpr bool should_load_custom([[maybe_unused]] const config::config_t& config) { - return (features::EnablePreloadAssets && config._custom) || (config._custom && config.animation_sprite_sheet_layout == config::config_animation_sprite_sheet_layout_t::Custom && config.animation_custom_set == config::config_animation_custom_set_t::custom); + anim_shm.anim = bongocat::move(result); + } break; + case config::config_animation_sprite_sheet_layout_t::Pkmn: + if constexpr (features::EnablePkmnEmbeddedAssets) { + auto [result, error] = load_pkmn_sprite_sheet(ctx, anim_index); + if (error != bongocat_error_t::BONGOCAT_SUCCESS) [[unlikely]] { + return error; + } + anim_shm.anim = bongocat::move(result); } - [[maybe_unused]] static constexpr bool should_load_pmd([[maybe_unused]] const config::config_t& config) { - return features::EnablePreloadAssets || (config._custom && config.animation_sprite_sheet_layout == config::config_animation_sprite_sheet_layout_t::Custom && config.animation_custom_set == config::config_animation_custom_set_t::pmd); + break; + case config::config_animation_sprite_sheet_layout_t::MsAgent: + if constexpr (features::EnableMsAgentEmbeddedAssets) { + auto [result, error] = load_ms_agent_sprite_sheet(ctx, anim_index); + if (error != bongocat_error_t::BONGOCAT_SUCCESS) [[unlikely]] { + return error; + } + anim_shm.anim = bongocat::move(result); } - - namespace details { - created_result_t anim_load_custom_animation(animation_context_t& ctx, const config::config_t& config) { - BONGOCAT_CHECK_NULL(config.custom_sprite_sheet_filename, bongocat_error_t::BONGOCAT_ERROR_INVALID_PARAM); - if (strlen(config.custom_sprite_sheet_filename) <= 0) { - return bongocat_error_t::BONGOCAT_ERROR_INVALID_PARAM; - } - - assert(ctx.shm.ptr); - assert(ctx._local_copy_config.ptr); - - BONGOCAT_LOG_VERBOSE("Load custom Animation: %s ...", config.custom_sprite_sheet_filename); - auto sprite_sheet_image_result = load_custom_sprite_sheet_file(config.custom_sprite_sheet_filename); - if (sprite_sheet_image_result.error != bongocat_error_t::BONGOCAT_SUCCESS) [[unlikely]] { - BONGOCAT_LOG_ERROR("Load custom Animation failed: %s", config.custom_sprite_sheet_filename); - return bongocat_error_t::BONGOCAT_ERROR_ANIMATION; - } - - auto result = load_custom_anim(ctx, sprite_sheet_image_result.result, config.custom_sprite_sheet_settings); - free_custom_sprite_sheet_file(sprite_sheet_image_result.result); - if (result.error != bongocat_error_t::BONGOCAT_SUCCESS) [[unlikely]] { - BONGOCAT_LOG_ERROR("Load custom Animation failed: %s", config.custom_sprite_sheet_filename); - return bongocat_error_t::BONGOCAT_ERROR_ANIMATION; - } - - return result; - } + break; + case config::config_animation_sprite_sheet_layout_t::Custom: + assert(anim_index >= 0); + if constexpr (features::EnableCustomSpriteSheetsAssets && features::EnableMiscEmbeddedAssets) { + assert(assets::CUSTOM_ANIM_INDEX > assets::MAX_MISC_ANIM_INDEX); } - - created_result_t hot_load_animation(animation_context_t& ctx) { - // read-only config - assert(ctx._local_copy_config != nullptr); - const config::config_t& current_config = *ctx._local_copy_config; - assert(ctx.shm != nullptr); - animation_shared_memory_t& anim_shm = *ctx.shm; - const int anim_index = anim_shm.anim_index; - - switch (anim_shm.anim_type) { - case config::config_animation_sprite_sheet_layout_t::None: - // unload other sprite sheets - cleanup_animation(anim_shm.anim); - break; - case config::config_animation_sprite_sheet_layout_t::Bongocat: { - if constexpr (features::EnableBongocatEmbeddedAssets) { - auto [result, error] = load_bongocat_sprite_sheet(ctx, anim_index); - if (error != bongocat_error_t::BONGOCAT_SUCCESS) [[unlikely]] { - return error; - } - anim_shm.anim = bongocat::move(result); - } - }break; - case config::config_animation_sprite_sheet_layout_t::Dm: { - bongocat_error_t error = bongocat_error_t::BONGOCAT_ERROR_INVALID_PARAM; - dm_sprite_sheet_t result; - switch (anim_shm.anim_dm_set) { - case config::config_animation_dm_set_t::None: - cleanup_animation(result); - error = bongocat_error_t::BONGOCAT_SUCCESS; - break; - case config::config_animation_dm_set_t::min_dm:{ - if constexpr (features::EnableMinDmEmbeddedAssets) { - auto [l_result, l_error] = load_min_dm_sprite_sheet(ctx, anim_index); - result = bongocat::move(l_result); - error = bongocat::move(l_error); - } - }break; - case config::config_animation_dm_set_t::dm:{ - if constexpr (features::EnableFullDmEmbeddedAssets) { - auto [l_result, l_error] = load_dm_sprite_sheet(ctx, anim_index); - result = bongocat::move(l_result); - error = bongocat::move(l_error); - } - }break; - case config::config_animation_dm_set_t::dm20:{ - if constexpr (features::EnableDm20EmbeddedAssets) { - auto [l_result, l_error] = load_dm20_sprite_sheet(ctx, anim_index); - result = bongocat::move(l_result); - error = bongocat::move(l_error); - } - }break; - case config::config_animation_dm_set_t::dmx:{ - if constexpr (features::EnableDmxEmbeddedAssets) { - auto [l_result, l_error] = load_dmx_sprite_sheet(ctx, anim_index); - result = bongocat::move(l_result); - error = bongocat::move(l_error); - } - }break; - case config::config_animation_dm_set_t::pen:{ - if constexpr (features::EnablePenEmbeddedAssets) { - auto [l_result, l_error] = load_pen_sprite_sheet(ctx, anim_index); - result = bongocat::move(l_result); - error = bongocat::move(l_error); - } - }break; - case config::config_animation_dm_set_t::pen20:{ - if constexpr (features::EnablePen20EmbeddedAssets) { - auto [l_result, l_error] = load_pen20_sprite_sheet(ctx, anim_index); - result = bongocat::move(l_result); - error = bongocat::move(l_error); - } - }break; - case config::config_animation_dm_set_t::dmc:{ - if constexpr (features::EnableDmcEmbeddedAssets) { - auto [l_result, l_error] = load_dmc_sprite_sheet(ctx, anim_index); - result = bongocat::move(l_result); - error = bongocat::move(l_error); - } - }break; - case config::config_animation_dm_set_t::dmall:{ - if constexpr (features::EnableDmAllEmbeddedAssets) { - auto [l_result, l_error] = load_dmall_sprite_sheet(ctx, anim_index); - result = bongocat::move(l_result); - error = bongocat::move(l_error); - } - }break; - } - if (error != bongocat_error_t::BONGOCAT_SUCCESS) [[unlikely]] { - return error; - } - anim_shm.anim = bongocat::move(result); - }break; - case config::config_animation_sprite_sheet_layout_t::Pkmn: - if constexpr (features::EnablePkmnEmbeddedAssets) { - auto [result, error] = load_pkmn_sprite_sheet(ctx, anim_index); - if (error != bongocat_error_t::BONGOCAT_SUCCESS) [[unlikely]] { - return error; - } - anim_shm.anim = bongocat::move(result); - } - break; - case config::config_animation_sprite_sheet_layout_t::MsAgent: - if constexpr (features::EnableMsAgentEmbeddedAssets) { - auto [result, error] = load_ms_agent_sprite_sheet(ctx, anim_index); - if (error != bongocat_error_t::BONGOCAT_SUCCESS) [[unlikely]] { - return error; - } - anim_shm.anim = bongocat::move(result); - } - break; - case config::config_animation_sprite_sheet_layout_t::Custom: - assert(anim_index >= 0); - if constexpr (features::EnableCustomSpriteSheetsAssets && features::EnableMiscEmbeddedAssets) { - assert(assets::CUSTOM_ANIM_INDEX > assets::MAX_MISC_ANIM_INDEX); - } - if constexpr (features::EnableCustomSpriteSheetsAssets) { - if (current_config._custom && anim_index == assets::CUSTOM_ANIM_INDEX) { - auto [result, error] = details::anim_load_custom_animation(ctx, current_config); - if (error != bongocat_error_t::BONGOCAT_SUCCESS) { - return error; - } - anim_shm.anim = bongocat::move(result); - } - } - if constexpr (features::EnableMiscEmbeddedAssets) { - assert(anim_index >= 0); - if (current_config.animation_custom_set == config::config_animation_custom_set_t::misc && static_cast(anim_index) < assets::MISC_ANIM_COUNT) { - auto [result, error] = load_misc_sprite_sheet(ctx, anim_index); - if (error != bongocat_error_t::BONGOCAT_SUCCESS) [[unlikely]] { - return error; - } - anim_shm.anim = bongocat::move(result); - } - } - if constexpr (features::EnablePmdEmbeddedAssets) { - assert(anim_index >= 0); - if (current_config.animation_custom_set == config::config_animation_custom_set_t::pmd && static_cast(anim_index) < assets::PMD_ANIM_COUNT) { - auto [result, error] = load_pmd_sprite_sheet(ctx, anim_index); - if (error != bongocat_error_t::BONGOCAT_SUCCESS) [[unlikely]] { - return error; - } - anim_shm.anim = bongocat::move(result); - } - } - - break; - /// @NOTE(assets): 6. add hot reload asset + if constexpr (features::EnableCustomSpriteSheetsAssets) { + if (current_config._custom && anim_index == assets::CUSTOM_ANIM_INDEX) { + auto [result, error] = details::anim_load_custom_animation(ctx, current_config); + if (error != bongocat_error_t::BONGOCAT_SUCCESS) { + return error; } - - created_result_t ret; - ret.result = &get_current_animation(ctx); - ret.error = bongocat_error_t::BONGOCAT_SUCCESS; - return ret; + anim_shm.anim = bongocat::move(result); + } } - - animation_t& get_current_animation(animation_context_t& ctx) { - using namespace assets; - // fallback sprite - static animation_t none_sprite_sheet{}; - - // read-only config - assert(ctx._local_copy_config != nullptr); - //const config::config_t& current_config = *ctx._local_copy_config; - assert(ctx.shm != nullptr); - animation_shared_memory_t& anim_shm = *ctx.shm; - const int anim_index = anim_shm.anim_index; - - switch (anim_shm.anim_type) { - case config::config_animation_sprite_sheet_layout_t::None: - return none_sprite_sheet; - case config::config_animation_sprite_sheet_layout_t::Bongocat: { - if (features::EnableLazyLoadAssets) { - assert(anim_shm.anim.type == animation_t::Type::Bongocat); - return anim_shm.anim; - } - assert(anim_index >= 0); - return static_cast(anim_index) < anim_shm.bongocat_anims.count ? anim_shm.bongocat_anims[static_cast(anim_index)] : none_sprite_sheet; - } - case config::config_animation_sprite_sheet_layout_t::Dm: { - switch (anim_shm.anim_dm_set) { - case config::config_animation_dm_set_t::None: - return none_sprite_sheet; - case config::config_animation_dm_set_t::min_dm: - if (features::EnableLazyLoadAssets) { - assert(anim_shm.anim.type == animation_t::Type::Dm); - return anim_shm.anim; - } - assert(anim_index >= 0); - return static_cast(anim_index) < anim_shm.min_dm_anims.count ? anim_shm.min_dm_anims[static_cast(anim_index)] : none_sprite_sheet; - case config::config_animation_dm_set_t::dm: - if (features::EnableLazyLoadAssets) { - assert(anim_shm.anim.type == animation_t::Type::Dm); - return anim_shm.anim; - } - return static_cast(anim_index) < anim_shm.dm_anims.count ? anim_shm.dm_anims[static_cast(anim_index)] : none_sprite_sheet; - case config::config_animation_dm_set_t::dm20: - if (features::EnableLazyLoadAssets) { - assert(anim_shm.anim.type == animation_t::Type::Dm); - return anim_shm.anim; - } - assert(anim_index >= 0); - return static_cast(anim_index) < anim_shm.dm20_anims.count ? anim_shm.dm20_anims[static_cast(anim_index)] : none_sprite_sheet; - case config::config_animation_dm_set_t::dmx: - if (features::EnableLazyLoadAssets) { - assert(anim_shm.anim.type == animation_t::Type::Dm); - return anim_shm.anim; - } - assert(anim_index >= 0); - return static_cast(anim_index) < anim_shm.dmx_anims.count ? anim_shm.dmx_anims[static_cast(anim_index)] : none_sprite_sheet; - case config::config_animation_dm_set_t::pen: - if (features::EnableLazyLoadAssets) { - assert(anim_shm.anim.type == animation_t::Type::Dm); - return anim_shm.anim; - } - return static_cast(anim_index) < anim_shm.pen_anims.count ? anim_shm.pen_anims[static_cast(anim_index)] : none_sprite_sheet; - case config::config_animation_dm_set_t::pen20: - if (features::EnableLazyLoadAssets) { - assert(anim_shm.anim.type == animation_t::Type::Dm); - return anim_shm.anim; - } - assert(anim_index >= 0); - return static_cast(anim_index) < anim_shm.pen20_anims.count ? anim_shm.pen20_anims[static_cast(anim_index)] : none_sprite_sheet; - case config::config_animation_dm_set_t::dmc: - if (features::EnableLazyLoadAssets) { - assert(anim_shm.anim.type == animation_t::Type::Dm); - return anim_shm.anim; - } - assert(anim_index >= 0); - return static_cast(anim_index) < anim_shm.dmc_anims.count ? anim_shm.dmc_anims[static_cast(anim_index)] : none_sprite_sheet; - case config::config_animation_dm_set_t::dmall: - if (features::EnableLazyLoadAssets) { - assert(anim_shm.anim.type == animation_t::Type::Dm); - return anim_shm.anim; - } - assert(anim_index >= 0); - return static_cast(anim_index) < anim_shm.dmall_anims.count ? anim_shm.dmall_anims[static_cast(anim_index)] : none_sprite_sheet; - } - }break; - case config::config_animation_sprite_sheet_layout_t::Pkmn: - if (features::EnableLazyLoadAssets) { - assert(anim_shm.anim.type == animation_t::Type::Pkmn); - return anim_shm.anim; - } - assert(anim_index >= 0); - return static_cast(anim_index) < anim_shm.pkmn_anims.count ? anim_shm.pkmn_anims[static_cast(anim_index)] : none_sprite_sheet; - case config::config_animation_sprite_sheet_layout_t::MsAgent: - if (features::EnableLazyLoadAssets) { - assert(anim_shm.anim.type == animation_t::Type::MsAgent); - return anim_shm.anim; - } - assert(anim_index >= 0); - return static_cast(anim_index) < anim_shm.ms_anims.count ? anim_shm.ms_anims[static_cast(anim_index)] : none_sprite_sheet; - case config::config_animation_sprite_sheet_layout_t::Custom: - switch (anim_shm.anim_custom_set) { - case config::config_animation_custom_set_t::None: - break; - case config::config_animation_custom_set_t::misc: - if (features::EnableLazyLoadAssets) { - assert(anim_shm.anim.type == animation_t::Type::Custom); - return anim_shm.anim; - } - assert(anim_index >= 0); - return static_cast(anim_index) < anim_shm.misc_anims.count ? anim_shm.misc_anims[static_cast(anim_index)] : none_sprite_sheet; - break; - case config::config_animation_custom_set_t::pmd: - if (features::EnableLazyLoadAssets) { - assert(anim_shm.anim.type == animation_t::Type::Custom); - return anim_shm.anim; - } - assert(anim_index >= 0); - return static_cast(anim_index) < anim_shm.pmd_anims.count ? anim_shm.pmd_anims[static_cast(anim_index)] : none_sprite_sheet; - break; - case config::config_animation_custom_set_t::custom: - if (features::EnableLazyLoadAssets) { - assert(anim_shm.anim.type == animation_t::Type::Custom); - return anim_shm.anim; - } - if (static_cast(anim_index) == CUSTOM_ANIM_INDEX) { - return anim_shm.anim; - } - break; - } + if constexpr (features::EnableMiscEmbeddedAssets) { + assert(anim_index >= 0); + if (current_config.animation_custom_set == config::config_animation_custom_set_t::misc && + static_cast(anim_index) < assets::MISC_ANIM_COUNT) { + auto [result, error] = load_misc_sprite_sheet(ctx, anim_index); + if (error != bongocat_error_t::BONGOCAT_SUCCESS) [[unlikely]] { + return error; } - - return none_sprite_sheet; + anim_shm.anim = bongocat::move(result); + } } - - // ============================================================================= - // PUBLIC API IMPLEMENTATION - // ============================================================================= - - created_result_t> create(const config::config_t& config) { - using namespace assets; - BONGOCAT_LOG_INFO("Initializing animation system"); - AllocatedMemory ret = make_allocated_memory(); - assert(ret != nullptr); - if (ret == nullptr) { - return bongocat_error_t::BONGOCAT_ERROR_MEMORY; + if constexpr (features::EnablePmdEmbeddedAssets) { + assert(anim_index >= 0); + if (current_config.animation_custom_set == config::config_animation_custom_set_t::pmd && + static_cast(anim_index) < assets::PMD_ANIM_COUNT) { + auto [result, error] = load_pmd_sprite_sheet(ctx, anim_index); + if (error != bongocat_error_t::BONGOCAT_SUCCESS) [[unlikely]] { + return error; } + anim_shm.anim = bongocat::move(result); + } + } - ret->_config = &config; + break; + /// @NOTE(assets): 6. add hot reload asset + } - // Initialize shared memory - ret->anim.shm = platform::make_allocated_mmap(); - if (!ret->anim.shm) { - BONGOCAT_LOG_ERROR("Failed to create shared memory for animation system: %s", strerror(errno)); - return bongocat_error_t::BONGOCAT_ERROR_MEMORY; - } - assert(ret->anim.shm != nullptr); + created_result_t ret; + ret.result = &get_current_animation(ctx); + ret.error = bongocat_error_t::BONGOCAT_SUCCESS; + return ret; +} - // Initialize shared memory for local config - ret->anim._local_copy_config = platform::make_allocated_mmap(); - if (!ret->anim._local_copy_config) { - BONGOCAT_LOG_ERROR("Failed to create shared memory for animation system: %s", strerror(errno)); - return bongocat_error_t::BONGOCAT_ERROR_MEMORY; - } - assert(ret->anim._local_copy_config != nullptr); - //config_set_defaults(*ctx._local_copy_config); - *ret->anim._local_copy_config = config; - ret->anim.shm->animation_player_result.sprite_sheet_col = config.idle_frame; // initial frame - - ret->trigger_efd = platform::FileDescriptor(eventfd(0, EFD_NONBLOCK | EFD_CLOEXEC)); - if (ret->trigger_efd._fd < 0) { - BONGOCAT_LOG_ERROR("Failed to create notify pipe for animation trigger: %s", strerror(errno)); - return bongocat_error_t::BONGOCAT_ERROR_FILE_IO; - } +animation_t& get_current_animation(animation_thread_context_t& ctx) { + using namespace assets; + // fallback sprite + static animation_t none_sprite_sheet{}; + + // read-only config + assert(ctx._local_copy_config); + // const config::config_t& current_config = *ctx._local_copy_config; + assert(ctx.shm); + animation_shared_memory_t& anim_shm = *ctx.shm; + const int anim_index = anim_shm.anim_index; + + switch (anim_shm.anim_type) { + case config::config_animation_sprite_sheet_layout_t::None: + return none_sprite_sheet; + case config::config_animation_sprite_sheet_layout_t::Bongocat: { + if (features::EnableLazyLoadAssets) { + assert(anim_shm.anim.type == animation_t::type_t::Bongocat); + return anim_shm.anim; + } + assert(anim_index >= 0); + return static_cast(anim_index) < anim_shm.bongocat_anims.count + ? anim_shm.bongocat_anims[static_cast(anim_index)] + : none_sprite_sheet; + } + case config::config_animation_sprite_sheet_layout_t::Dm: { + switch (anim_shm.anim_dm_set) { + case config::config_animation_dm_set_t::None: + return none_sprite_sheet; + case config::config_animation_dm_set_t::min_dm: + if (features::EnableLazyLoadAssets) { + assert(anim_shm.anim.type == animation_t::type_t::Dm); + return anim_shm.anim; + } + assert(anim_index >= 0); + return static_cast(anim_index) < anim_shm.min_dm_anims.count + ? anim_shm.min_dm_anims[static_cast(anim_index)] + : none_sprite_sheet; + case config::config_animation_dm_set_t::dm: + if (features::EnableLazyLoadAssets) { + assert(anim_shm.anim.type == animation_t::type_t::Dm); + return anim_shm.anim; + } + return static_cast(anim_index) < anim_shm.dm_anims.count + ? anim_shm.dm_anims[static_cast(anim_index)] + : none_sprite_sheet; + case config::config_animation_dm_set_t::dm20: + if (features::EnableLazyLoadAssets) { + assert(anim_shm.anim.type == animation_t::type_t::Dm); + return anim_shm.anim; + } + assert(anim_index >= 0); + return static_cast(anim_index) < anim_shm.dm20_anims.count + ? anim_shm.dm20_anims[static_cast(anim_index)] + : none_sprite_sheet; + case config::config_animation_dm_set_t::dmx: + if (features::EnableLazyLoadAssets) { + assert(anim_shm.anim.type == animation_t::type_t::Dm); + return anim_shm.anim; + } + assert(anim_index >= 0); + return static_cast(anim_index) < anim_shm.dmx_anims.count + ? anim_shm.dmx_anims[static_cast(anim_index)] + : none_sprite_sheet; + case config::config_animation_dm_set_t::pen: + if (features::EnableLazyLoadAssets) { + assert(anim_shm.anim.type == animation_t::type_t::Dm); + return anim_shm.anim; + } + return static_cast(anim_index) < anim_shm.pen_anims.count + ? anim_shm.pen_anims[static_cast(anim_index)] + : none_sprite_sheet; + case config::config_animation_dm_set_t::pen20: + if (features::EnableLazyLoadAssets) { + assert(anim_shm.anim.type == animation_t::type_t::Dm); + return anim_shm.anim; + } + assert(anim_index >= 0); + return static_cast(anim_index) < anim_shm.pen20_anims.count + ? anim_shm.pen20_anims[static_cast(anim_index)] + : none_sprite_sheet; + case config::config_animation_dm_set_t::dmc: + if (features::EnableLazyLoadAssets) { + assert(anim_shm.anim.type == animation_t::type_t::Dm); + return anim_shm.anim; + } + assert(anim_index >= 0); + return static_cast(anim_index) < anim_shm.dmc_anims.count + ? anim_shm.dmc_anims[static_cast(anim_index)] + : none_sprite_sheet; + case config::config_animation_dm_set_t::dmall: + if (features::EnableLazyLoadAssets) { + assert(anim_shm.anim.type == animation_t::type_t::Dm); + return anim_shm.anim; + } + assert(anim_index >= 0); + return static_cast(anim_index) < anim_shm.dmall_anims.count + ? anim_shm.dmall_anims[static_cast(anim_index)] + : none_sprite_sheet; + } + } break; + case config::config_animation_sprite_sheet_layout_t::Pkmn: + if (features::EnableLazyLoadAssets) { + assert(anim_shm.anim.type == animation_t::type_t::Pkmn); + return anim_shm.anim; + } + assert(anim_index >= 0); + return static_cast(anim_index) < anim_shm.pkmn_anims.count + ? anim_shm.pkmn_anims[static_cast(anim_index)] + : none_sprite_sheet; + case config::config_animation_sprite_sheet_layout_t::MsAgent: + if (features::EnableLazyLoadAssets) { + assert(anim_shm.anim.type == animation_t::type_t::MsAgent); + return anim_shm.anim; + } + assert(anim_index >= 0); + return static_cast(anim_index) < anim_shm.ms_anims.count + ? anim_shm.ms_anims[static_cast(anim_index)] + : none_sprite_sheet; + case config::config_animation_sprite_sheet_layout_t::Custom: + switch (anim_shm.anim_custom_set) { + case config::config_animation_custom_set_t::None: + break; + case config::config_animation_custom_set_t::misc: + if (features::EnableLazyLoadAssets) { + assert(anim_shm.anim.type == animation_t::type_t::Custom); + return anim_shm.anim; + } + assert(anim_index >= 0); + return static_cast(anim_index) < anim_shm.misc_anims.count + ? anim_shm.misc_anims[static_cast(anim_index)] + : none_sprite_sheet; + break; + case config::config_animation_custom_set_t::pmd: + if (features::EnableLazyLoadAssets) { + assert(anim_shm.anim.type == animation_t::type_t::Custom); + return anim_shm.anim; + } + assert(anim_index >= 0); + return static_cast(anim_index) < anim_shm.pmd_anims.count + ? anim_shm.pmd_anims[static_cast(anim_index)] + : none_sprite_sheet; + break; + case config::config_animation_custom_set_t::custom: + if (features::EnableLazyLoadAssets) { + assert(anim_shm.anim.type == animation_t::type_t::Custom); + return anim_shm.anim; + } + if (static_cast(anim_index) == CUSTOM_ANIM_INDEX) { + return anim_shm.anim; + } + break; + } + } - ret->render_efd = platform::FileDescriptor(eventfd(0, EFD_NONBLOCK | EFD_CLOEXEC)); - if (ret->render_efd._fd < 0) { - BONGOCAT_LOG_ERROR("Failed to create notify pipe for animation render: %s", strerror(errno)); - return bongocat_error_t::BONGOCAT_ERROR_FILE_IO; - } + return none_sprite_sheet; +} - ret->anim.update_config_efd = platform::FileDescriptor(eventfd(0, EFD_NONBLOCK | EFD_CLOEXEC)); - if (ret->anim.update_config_efd._fd < 0) { - BONGOCAT_LOG_ERROR("Failed to create notify pipe for input update config: %s", strerror(errno)); - return bongocat_error_t::BONGOCAT_ERROR_FILE_IO; - } +// ============================================================================= +// PUBLIC API IMPLEMENTATION +// ============================================================================= + +created_result_t> create(const config::config_t& config) { + using namespace assets; + BONGOCAT_LOG_INFO("Initializing animation system"); + AllocatedMemory ret = make_allocated_memory(); + assert(ret); + if (!ret) [[unlikely]] { + return bongocat_error_t::BONGOCAT_ERROR_MEMORY; + } + + ret->_config = &config; + + // Initialize shared memory + ret->thread_context.shm = platform::make_allocated_mmap(); + if (!ret->thread_context.shm) { + BONGOCAT_LOG_ERROR("Failed to create shared memory for animation system: %s", strerror(errno)); + return bongocat_error_t::BONGOCAT_ERROR_MEMORY; + } + assert(ret->thread_context.shm); + + // Initialize shared memory for local config + ret->thread_context._local_copy_config = platform::make_allocated_mmap(); + if (!ret->thread_context._local_copy_config) { + BONGOCAT_LOG_ERROR("Failed to create shared memory for animation system: %s", strerror(errno)); + return bongocat_error_t::BONGOCAT_ERROR_MEMORY; + } + assert(ret->thread_context._local_copy_config); + // config_set_defaults(*ctx._local_copy_config); + *ret->thread_context._local_copy_config = config; + ret->thread_context.shm->animation_player_result.sprite_sheet_col = config.idle_frame; // initial frame + + ret->trigger_efd = platform::FileDescriptor(eventfd(0, EFD_NONBLOCK | EFD_CLOEXEC)); + if (ret->trigger_efd._fd < 0) { + BONGOCAT_LOG_ERROR("Failed to create notify pipe for animation trigger: %s", strerror(errno)); + return bongocat_error_t::BONGOCAT_ERROR_FILE_IO; + } + + ret->render_efd = platform::FileDescriptor(eventfd(0, EFD_NONBLOCK | EFD_CLOEXEC)); + if (ret->render_efd._fd < 0) { + BONGOCAT_LOG_ERROR("Failed to create notify pipe for animation render: %s", strerror(errno)); + return bongocat_error_t::BONGOCAT_ERROR_FILE_IO; + } + + ret->thread_context.update_config_efd = platform::FileDescriptor(eventfd(0, EFD_NONBLOCK | EFD_CLOEXEC)); + if (ret->thread_context.update_config_efd._fd < 0) { + BONGOCAT_LOG_ERROR("Failed to create notify pipe for input update config: %s", strerror(errno)); + return bongocat_error_t::BONGOCAT_ERROR_FILE_IO; + } + + [[maybe_unused]] const auto t0 = platform::get_current_time_us(); + // Load embedded images/animations + if constexpr (features::EnableLazyLoadAssets) { + hot_load_animation(ret->thread_context); + } + + /// @TODO: async assets load + // Initialize embedded images/animations + if constexpr (!features::EnableLazyLoadAssets || features::EnablePreloadAssets) { + assert(ret->thread_context._local_copy_config.ptr); + // preload assets + if constexpr (features::EnableBongocatEmbeddedAssets) { + // Load Bongocat + if (should_load_bongocat(*ret->thread_context._local_copy_config)) { + BONGOCAT_LOG_INFO("Load bongocat sprite sheet frames: %d", BONGOCAT_EMBEDDED_IMAGES_COUNT); + assert(ret->thread_context.shm); + animation_thread_context_t& ctx = ret->thread_context; // alias for inits in includes + + ctx.shm->bongocat_anims = platform::make_allocated_mmap_array(BONGOCAT_ANIM_COUNT); + + init_bongocat_anim(ctx, BONGOCAT_ANIM_INDEX, get_bongocat_sprite, BONGOCAT_EMBEDDED_IMAGES_COUNT); + } + } - [[maybe_unused]] const auto t0 = platform::get_current_time_us(); - // Load embedded images/animations - if constexpr (features::EnableLazyLoadAssets) { - hot_load_animation(ret->anim); - } + if constexpr (features::EnableDmEmbeddedAssets) { + // Load dm + if (should_load_dm(*ret->thread_context._local_copy_config)) { + BONGOCAT_LOG_INFO("Load dm sprite sheets: %d", DM_ANIMATIONS_COUNT); + assert(ret->thread_context.shm); + animation_thread_context_t& ctx = ret->thread_context; // alias for inits in includes - /// @TODO: async assets load - // Initialize embedded images/animations - if constexpr (!features::EnableLazyLoadAssets || features::EnablePreloadAssets) { - assert(ret->anim._local_copy_config.ptr); - // preload assets - if constexpr (features::EnableBongocatEmbeddedAssets) { - // Load Bongocat - if (should_load_bongocat(*ret->anim._local_copy_config)) { - BONGOCAT_LOG_INFO("Load bongocat sprite sheet frames: %d", BONGOCAT_EMBEDDED_IMAGES_COUNT); - assert(ret->anim.shm != nullptr); - animation_context_t& ctx = ret->anim; // alias for inits in includes - - ctx.shm->bongocat_anims = platform::make_allocated_mmap_array(BONGOCAT_ANIM_COUNT); - - init_bongocat_anim(ctx, BONGOCAT_ANIM_INDEX, get_bongocat_sprite, BONGOCAT_EMBEDDED_IMAGES_COUNT); - } - } - - if constexpr (features::EnableDmEmbeddedAssets) { - // Load dm - if (should_load_dm(*ret->anim._local_copy_config)) { - BONGOCAT_LOG_INFO("Load dm sprite sheets: %d", DM_ANIMATIONS_COUNT); - assert(ret->anim.shm != nullptr); - animation_context_t& ctx = ret->anim; // alias for inits in includes - - if constexpr (features::EnableMinDmEmbeddedAssets) { - BONGOCAT_LOG_INFO("Init min_dm sprite sheets: %d", MIN_DM_ANIM_COUNT); - ctx.shm->min_dm_anims = platform::make_allocated_mmap_array(MIN_DM_ANIM_COUNT); + if constexpr (features::EnableMinDmEmbeddedAssets) { + BONGOCAT_LOG_INFO("Init min_dm sprite sheets: %d", MIN_DM_ANIM_COUNT); + ctx.shm->min_dm_anims = platform::make_allocated_mmap_array(MIN_DM_ANIM_COUNT); #ifdef FEATURE_MIN_DM_EMBEDDED_ASSETS - // init minimal dm - //init_dm_anim(ctx, DM_AGUMON_ANIM_INDEX, get_dm_sprite_sheet(DM_AGUMON_ANIM_INDEX), DM_AGUMON_SPRITE_SHEET_COLS, DM_AGUMON_SPRITE_SHEET_ROWS); -#include "min_dm_init_dm_anim.cpp.inl" + // init minimal dm + // init_dm_anim(ctx, DM_AGUMON_ANIM_INDEX, get_dm_sprite_sheet(DM_AGUMON_ANIM_INDEX), + // DM_AGUMON_SPRITE_SHEET_COLS, DM_AGUMON_SPRITE_SHEET_ROWS); +# include "min_dm_init_dm_anim.cpp.inl" #endif - } - if constexpr (features::EnableFullDmEmbeddedAssets) { - BONGOCAT_LOG_INFO("Init dm sprite sheets: %d", DM_ANIM_COUNT); - ctx.shm->dm_anims = platform::make_allocated_mmap_array(DM_ANIM_COUNT); + } + if constexpr (features::EnableFullDmEmbeddedAssets) { + BONGOCAT_LOG_INFO("Init dm sprite sheets: %d", DM_ANIM_COUNT); + ctx.shm->dm_anims = platform::make_allocated_mmap_array(DM_ANIM_COUNT); #ifdef FEATURE_DM_EMBEDDED_ASSETS - // dm -#include "dm_init_dm_anim.cpp.inl" + // dm +# include "dm_init_dm_anim.cpp.inl" #endif - } - if constexpr (features::EnableDm20EmbeddedAssets) { - BONGOCAT_LOG_INFO("Init dm20 sprite sheets: %d", DM20_ANIM_COUNT); - ctx.shm->dm20_anims = platform::make_allocated_mmap_array(DM20_ANIM_COUNT); + } + if constexpr (features::EnableDm20EmbeddedAssets) { + BONGOCAT_LOG_INFO("Init dm20 sprite sheets: %d", DM20_ANIM_COUNT); + ctx.shm->dm20_anims = platform::make_allocated_mmap_array(DM20_ANIM_COUNT); #ifdef FEATURE_DM20_EMBEDDED_ASSETS - // dm20 -#include "dm20_init_dm_anim.cpp.inl" + // dm20 +# include "dm20_init_dm_anim.cpp.inl" #endif - } - if constexpr (features::EnableDmxEmbeddedAssets) { - BONGOCAT_LOG_INFO("Init dmx sprite sheets: %d", DMX_ANIM_COUNT); - ctx.shm->dmx_anims = platform::make_allocated_mmap_array(DMX_ANIM_COUNT); + } + if constexpr (features::EnableDmxEmbeddedAssets) { + BONGOCAT_LOG_INFO("Init dmx sprite sheets: %d", DMX_ANIM_COUNT); + ctx.shm->dmx_anims = platform::make_allocated_mmap_array(DMX_ANIM_COUNT); #ifdef FEATURE_DMX_EMBEDDED_ASSETS - // dmx -#include "dmx_init_dm_anim.cpp.inl" + // dmx +# include "dmx_init_dm_anim.cpp.inl" #endif - } - if constexpr (features::EnablePenEmbeddedAssets) { - BONGOCAT_LOG_INFO("Init pen sprite sheets: %d", PEN20_ANIM_COUNT); - ctx.shm->pen_anims = platform::make_allocated_mmap_array(PEN_ANIM_COUNT); + } + if constexpr (features::EnablePenEmbeddedAssets) { + BONGOCAT_LOG_INFO("Init pen sprite sheets: %d", PEN20_ANIM_COUNT); + ctx.shm->pen_anims = platform::make_allocated_mmap_array(PEN_ANIM_COUNT); #ifdef FEATURE_PEN_EMBEDDED_ASSETS - // pen -#include "pen_init_dm_anim.cpp.inl" + // pen +# include "pen_init_dm_anim.cpp.inl" #endif - } - if constexpr (features::EnablePen20EmbeddedAssets) { - BONGOCAT_LOG_INFO("Init pen20 sprite sheets: %d", PEN20_ANIM_COUNT); - ctx.shm->pen20_anims = platform::make_allocated_mmap_array(PEN20_ANIM_COUNT); + } + if constexpr (features::EnablePen20EmbeddedAssets) { + BONGOCAT_LOG_INFO("Init pen20 sprite sheets: %d", PEN20_ANIM_COUNT); + ctx.shm->pen20_anims = platform::make_allocated_mmap_array(PEN20_ANIM_COUNT); #ifdef FEATURE_PEN20_EMBEDDED_ASSETS - // pen20 -#include "pen20_init_dm_anim.cpp.inl" + // pen20 +# include "pen20_init_dm_anim.cpp.inl" #endif - } - if constexpr (features::EnableDmcEmbeddedAssets) { - BONGOCAT_LOG_INFO("Init dmc sprite sheets: %d", DMC_ANIM_COUNT); - ctx.shm->dmc_anims = platform::make_allocated_mmap_array(DMC_ANIM_COUNT); + } + if constexpr (features::EnableDmcEmbeddedAssets) { + BONGOCAT_LOG_INFO("Init dmc sprite sheets: %d", DMC_ANIM_COUNT); + ctx.shm->dmc_anims = platform::make_allocated_mmap_array(DMC_ANIM_COUNT); #ifdef FEATURE_DMC_EMBEDDED_ASSETS - // dmc -#include "dmc_init_dm_anim.cpp.inl" + // dmc +# include "dmc_init_dm_anim.cpp.inl" #endif - } - if constexpr (features::EnableDmAllEmbeddedAssets) { - BONGOCAT_LOG_INFO("Init dmall sprite sheets: %d", DMALL_ANIM_COUNT); - ctx.shm->dmall_anims = platform::make_allocated_mmap_array(DMALL_ANIM_COUNT); + } + if constexpr (features::EnableDmAllEmbeddedAssets) { + BONGOCAT_LOG_INFO("Init dmall sprite sheets: %d", DMALL_ANIM_COUNT); + ctx.shm->dmall_anims = platform::make_allocated_mmap_array(DMALL_ANIM_COUNT); #ifdef FEATURE_DMALL_EMBEDDED_ASSETS - // dmall -#include "dmall_init_dm_anim.cpp.inl" + // dmall +# include "dmall_init_dm_anim.cpp.inl" #endif - } - } - } + } + } + } - if constexpr (features::EnableMsAgentEmbeddedAssets) { - // Load Ms Pets (Clippy) - if (should_load_ms_agent(*ret->anim._local_copy_config)) { - BONGOCAT_LOG_INFO("Load MS agent sprite sheets: %d", MS_AGENTS_ANIM_COUNT); - assert(ret->anim.shm != nullptr); - animation_context_t& ctx = ret->anim; // alias for inits in includes + if constexpr (features::EnableMsAgentEmbeddedAssets) { + // Load Ms Pets (Clippy) + if (should_load_ms_agent(*ret->thread_context._local_copy_config)) { + BONGOCAT_LOG_INFO("Load MS agent sprite sheets: %d", MS_AGENTS_ANIM_COUNT); + assert(ret->thread_context.shm); + animation_thread_context_t& ctx = ret->thread_context; // alias for inits in includes - ctx.shm->ms_anims = platform::make_allocated_mmap_array(MS_AGENTS_ANIM_COUNT); + ctx.shm->ms_anims = platform::make_allocated_mmap_array(MS_AGENTS_ANIM_COUNT); - // clippy - init_ms_agent_anim(ctx, CLIPPY_ANIM_INDEX, get_ms_agent_sprite_sheet(CLIPPY_ANIM_INDEX), CLIPPY_SPRITE_SHEET_COLS, CLIPPY_SPRITE_SHEET_ROWS, get_ms_agent_animation_indices(CLIPPY_ANIM_INDEX)); + // clippy + init_ms_agent_anim(ctx, CLIPPY_ANIM_INDEX, get_ms_agent_sprite_sheet(CLIPPY_ANIM_INDEX), + CLIPPY_SPRITE_SHEET_COLS, CLIPPY_SPRITE_SHEET_ROWS, + get_ms_agent_animation_indices(CLIPPY_ANIM_INDEX)); #ifdef FEATURE_MORE_MS_AGENT_EMBEDDED_ASSETS - /// @NOTE(config): add more MS Pets here - init_ms_agent_anim(ctx, LINKS_ANIM_INDEX, get_ms_agent_sprite_sheet(LINKS_ANIM_INDEX), LINKS_SPRITE_SHEET_COLS, LINKS_SPRITE_SHEET_ROWS, get_ms_agent_animation_indices(LINKS_ANIM_INDEX)); - init_ms_agent_anim(ctx, ROVER_ANIM_INDEX, get_ms_agent_sprite_sheet(ROVER_ANIM_INDEX), ROVER_SPRITE_SHEET_COLS, ROVER_SPRITE_SHEET_ROWS, get_ms_agent_animation_indices(ROVER_ANIM_INDEX)); - init_ms_agent_anim(ctx, MERLIN_ANIM_INDEX, get_ms_agent_sprite_sheet(MERLIN_ANIM_INDEX), MERLIN_SPRITE_SHEET_COLS, MERLIN_SPRITE_SHEET_ROWS, get_ms_agent_animation_indices(MERLIN_ANIM_INDEX)); + /// @NOTE(config): add more MS Pets here + init_ms_agent_anim(ctx, LINKS_ANIM_INDEX, get_ms_agent_sprite_sheet(LINKS_ANIM_INDEX), LINKS_SPRITE_SHEET_COLS, + LINKS_SPRITE_SHEET_ROWS, get_ms_agent_animation_indices(LINKS_ANIM_INDEX)); + init_ms_agent_anim(ctx, ROVER_ANIM_INDEX, get_ms_agent_sprite_sheet(ROVER_ANIM_INDEX), ROVER_SPRITE_SHEET_COLS, + ROVER_SPRITE_SHEET_ROWS, get_ms_agent_animation_indices(ROVER_ANIM_INDEX)); + init_ms_agent_anim(ctx, MERLIN_ANIM_INDEX, get_ms_agent_sprite_sheet(MERLIN_ANIM_INDEX), + MERLIN_SPRITE_SHEET_COLS, MERLIN_SPRITE_SHEET_ROWS, + get_ms_agent_animation_indices(MERLIN_ANIM_INDEX)); #endif - } - } + } + } - if constexpr (features::EnablePkmnEmbeddedAssets) { - // Load pkmn - if (should_load_pkmn(*ret->anim._local_copy_config)) { - BONGOCAT_LOG_INFO("Load pkmn sprite sheets: %d", PKMN_ANIM_COUNT); - assert(ret->anim.shm != nullptr); - animation_context_t& ctx = ret->anim; // alias for inits in includes + if constexpr (features::EnablePkmnEmbeddedAssets) { + // Load pkmn + if (should_load_pkmn(*ret->thread_context._local_copy_config)) { + BONGOCAT_LOG_INFO("Load pkmn sprite sheets: %d", PKMN_ANIM_COUNT); + assert(ret->thread_context.shm); + animation_thread_context_t& ctx = ret->thread_context; // alias for inits in includes - ctx.shm->pkmn_anims = platform::make_allocated_mmap_array(PKMN_ANIM_COUNT); + ctx.shm->pkmn_anims = platform::make_allocated_mmap_array(PKMN_ANIM_COUNT); #ifdef FEATURE_PKMN_EMBEDDED_ASSETS - // pkmn -#include "pkmn_init_pkmn_anim.cpp.inl" + // pkmn +# include "pkmn_init_pkmn_anim.cpp.inl" #endif - } - } - if constexpr (features::EnablePmdEmbeddedAssets) { - // Load pmd (pkmn) - if (should_load_pkmn(*ret->anim._local_copy_config)) { - BONGOCAT_LOG_INFO("Load pmd sprite sheets: %d", PKMN_ANIM_COUNT); - assert(ret->anim.shm != nullptr); - animation_context_t& ctx = ret->anim; // alias for inits in includes - - ctx.shm->pmd_anims = platform::make_allocated_mmap_array(PMD_ANIM_COUNT); + } + } + if constexpr (features::EnablePmdEmbeddedAssets) { + // Load pmd (pkmn) + if (should_load_pkmn(*ret->thread_context._local_copy_config)) { + BONGOCAT_LOG_INFO("Load pmd sprite sheets: %d", PKMN_ANIM_COUNT); + assert(ret->thread_context.shm); + animation_thread_context_t& ctx = ret->thread_context; // alias for inits in includes + + ctx.shm->pmd_anims = platform::make_allocated_mmap_array(PMD_ANIM_COUNT); #ifdef FEATURE_PMD_EMBEDDED_ASSETS - // pmd (pkmn) -#include "pmd_init_custom_anim.cpp.inl" + // pmd (pkmn) +# include "pmd_init_custom_anim.cpp.inl" #endif - } - } - - if constexpr (features::EnableMiscEmbeddedAssets) { - // Load Misc Pets (neko) - if (should_load_misc(*ret->anim._local_copy_config)) { - BONGOCAT_LOG_INFO("Load Misc sprite sheets: %d", MISC_ANIM_COUNT); - assert(ret->anim.shm != nullptr); - animation_context_t& ctx = ret->anim; // alias for inits in includes - - ctx.shm->misc_anims = platform::make_allocated_mmap_array(MISC_ANIM_COUNT); - - // neko - init_misc_anim(ctx, MISC_NEKO_ANIM_INDEX, get_misc_sprite_sheet(MISC_NEKO_ANIM_INDEX), get_misc_sprite_sheet_columns(MISC_NEKO_ANIM_INDEX)); - } - } - - if constexpr (features::EnableCustomSpriteSheetsAssets) { - assert(ret->anim._local_copy_config.ptr); - // Load custom sprite sheet - if (should_load_custom(*ret->anim._local_copy_config)) { - assert(ret->anim.shm != nullptr); - animation_context_t& ctx = ret->anim; // alias for inits in includes - assert(ctx.shm.ptr); - assert(ctx._local_copy_config.ptr); - - if (ctx._local_copy_config->_custom) { - BONGOCAT_LOG_INFO("Load custom sprite sheets: %s", ctx._local_copy_config->custom_sprite_sheet_filename); - - auto result = details::anim_load_custom_animation(ctx, *ctx._local_copy_config); - if (result.error != bongocat_error_t::BONGOCAT_SUCCESS) { - return bongocat_error_t::BONGOCAT_ERROR_ANIMATION; - } - - ctx.shm->anim = bongocat::move(result.result); - } - } - } - - /// @NOTE(assets): 7. add pre-load asset - } - [[maybe_unused]] const auto t1 = platform::get_current_time_us(); + } + } + + if constexpr (features::EnableMiscEmbeddedAssets) { + // Load Misc Pets (neko) + if (should_load_misc(*ret->thread_context._local_copy_config)) { + BONGOCAT_LOG_INFO("Load Misc sprite sheets: %d", MISC_ANIM_COUNT); + assert(ret->thread_context.shm); + animation_thread_context_t& ctx = ret->thread_context; // alias for inits in includes - // init anim - ret->anim._rng = platform::random_xoshiro128(platform::slow_rand()); + ctx.shm->misc_anims = platform::make_allocated_mmap_array(MISC_ANIM_COUNT); - BONGOCAT_LOG_INFO("Animation system initialized successfully with embedded assets; load assets in %.3fms (%.6fsec)", static_cast(t1 - t0) / 1000.0, static_cast(t1 - t0) / 1000000.0); - return ret; + // neko + init_misc_anim(ctx, MISC_NEKO_ANIM_INDEX, get_misc_sprite_sheet(MISC_NEKO_ANIM_INDEX), + get_misc_sprite_sheet_columns(MISC_NEKO_ANIM_INDEX)); + } } - void stop(animation_context_t& ctx) { - atomic_store(&ctx._animation_running, false); - if (ctx._anim_thread) { - BONGOCAT_LOG_DEBUG("Stopping animation thread"); - // Wait for thread to finish gracefully - //pthread_cancel(ctx->_anim_thread); - if (platform::stop_thread_graceful_or_cancel(ctx._anim_thread, ctx._animation_running) != 0) { - BONGOCAT_LOG_ERROR("Failed to join animation thread: %s", strerror(errno)); - } - BONGOCAT_LOG_DEBUG("Animation thread terminated"); + if constexpr (features::EnableCustomSpriteSheetsAssets) { + assert(ret->thread_context._local_copy_config.ptr); + // Load custom sprite sheet + if (should_load_custom(*ret->thread_context._local_copy_config)) { + assert(ret->thread_context.shm); + animation_thread_context_t& ctx = ret->thread_context; // alias for inits in includes + assert(ctx.shm.ptr); + assert(ctx._local_copy_config.ptr); + + if (ctx._local_copy_config->_custom) { + BONGOCAT_LOG_INFO("Load custom sprite sheets: %s", ctx._local_copy_config->custom_sprite_sheet_filename); + + auto result = details::anim_load_custom_animation(ctx, *ctx._local_copy_config); + if (result.error != bongocat_error_t::BONGOCAT_SUCCESS) { + return bongocat_error_t::BONGOCAT_ERROR_ANIMATION; + } + + ctx.shm->anim = bongocat::move(result.result); } - ctx._anim_thread = 0; + } + } - ctx.config_updated.notify_all(); + /// @NOTE(assets): 7. add pre-load asset + } + [[maybe_unused]] const auto t1 = platform::get_current_time_us(); + + // init anim + ret->thread_context._rng = platform::random_xoshiro128(platform::slow_rand()); + + BONGOCAT_LOG_INFO("Animation system initialized successfully with embedded assets; load assets in %.3fms (%.6fsec)", + static_cast(t1 - t0) / 1000.0, static_cast(t1 - t0) / 1000000.0); + return ret; +} + +void stop(animation_thread_context_t& ctx) { + atomic_store(&ctx._animation_running, false); + if (ctx._anim_thread) { + BONGOCAT_LOG_DEBUG("Stopping animation thread"); + // Wait for thread to finish gracefully + // pthread_cancel(ctx->_anim_thread); + if (platform::stop_thread_graceful_or_cancel(ctx._anim_thread, ctx._animation_running) != 0) { + BONGOCAT_LOG_ERROR("Failed to join animation thread: %s", strerror(errno)); } + BONGOCAT_LOG_DEBUG("Animation thread terminated"); + } + ctx._anim_thread = 0; + + ctx.config_updated.notify_all(); } +} // namespace bongocat::animation diff --git a/src/graphics/bar.cpp b/src/graphics/bar.cpp index b92d340b..015b985c 100644 --- a/src/graphics/bar.cpp +++ b/src/graphics/bar.cpp @@ -1,829 +1,926 @@ -#include "platform/wayland-protocols.hpp" - #include "bar.h" -#include "graphics/drawing.h" -#include "graphics/animation_context.h" -#include "platform/wayland.h" -#include "platform/wayland_callbacks.h" -#include "graphics/animation.h" -#include -#include #include "embedded_assets/bongocat/bongocat.h" #include "embedded_assets/misc/misc.hpp" +#include "graphics/animation.h" +#include "graphics/animation_thread_context.h" +#include "graphics/drawing.h" #include "graphics/embedded_assets_dms.h" #include "graphics/embedded_assets_pkmn.h" +#include "platform/wayland-protocols.hpp" +#include "platform/wayland.h" +#include "platform/wayland_callbacks.h" -namespace bongocat::animation { - inline static uint32_t DEFAULT_FILL_COLOR = 0x00000000; // ARGB - inline static uint32_t DEBUG_MOVEMENT_BAR_COLOR = 0xFFFF0000; // ARGB - - // ============================================================================= - // DRAWING MANAGEMENT - // ============================================================================= - - struct cat_rect_t { - int x; - int y; - int width; - int height; - }; - - template - /// @TODO: required SpriteSheet must be _sprite_sheet_t - cat_rect_t get_position(const platform::wayland::wayland_context_t& wayland_ctx, const SpriteSheet& sheet, const config::config_t& config) { - const int cat_height = config.cat_height; - const int cat_width = static_cast(static_cast(cat_height) * (static_cast(sheet.frame_width) / static_cast(sheet.frame_height))); - - int cat_x = 0; - switch (config.cat_align) { - case config::align_type_t::ALIGN_CENTER: - cat_x = (wayland_ctx._screen_width - cat_width) / 2 + config.cat_x_offset; - break; - case config::align_type_t::ALIGN_LEFT: - cat_x = config.cat_x_offset; - break; - case config::align_type_t::ALIGN_RIGHT: - cat_x = wayland_ctx._screen_width - cat_width - config.cat_x_offset; - break; - default: - BONGOCAT_LOG_VERBOSE("Invalid cat_align %d", config.cat_align); - break; - } - const int cat_y = (wayland_ctx._bar_height - cat_height) / 2 + config.cat_y_offset; - - return { .x = cat_x, .y = cat_y, .width = cat_width, .height = cat_height }; - } - - /// @TODO: make draw_sprite more generic (template?) - void draw_sprite(platform::wayland::wayland_session_t& ctx, platform::wayland::wayland_shm_buffer_t& shm_buffer, const bongocat_sprite_sheet_t& sheet, blit_image_color_option_flags_t extra_drawing_option = blit_image_color_option_flags_t::Normal) { - using namespace assets; - if (sheet.frame_width <= 0 || sheet.frame_height <= 0) { - return; - } - - platform::wayland::wayland_context_t& wayland_ctx = ctx.wayland_context; - animation_context_t& anim = ctx.animation_trigger_context->anim; - //animation_trigger_context_t *trigger_ctx = ctx.animation_trigger_context; - //platform::wayland::wayland_shared_memory_t *wayland_ctx_shm = wayland_ctx.ctx_shm.ptr; - - assert(wayland_ctx._local_copy_config != nullptr); - assert(anim.shm != nullptr); - const config::config_t& current_config = *wayland_ctx._local_copy_config.ptr; - const animation_shared_memory_t& anim_shm = *anim.shm; - - uint8_t *pixels = shm_buffer.pixels.data; - const size_t pixels_size = shm_buffer.pixels._size_bytes; - - const sprite_sheet_animation_frame_t* region = nullptr; - switch (anim_shm.animation_player_result.sprite_sheet_col) { - case BONGOCAT_FRAME_BOTH_UP: - region = &sheet.both_up; - break; - case BONGOCAT_FRAME_LEFT_DOWN: - region = &sheet.left_down; - break; - case BONGOCAT_FRAME_RIGHT_DOWN: - region = &sheet.right_down; - break; - case BONGOCAT_FRAME_BOTH_DOWN: - region = &sheet.both_down; - break; - default: - assert(anim_shm.animation_player_result.sprite_sheet_col >= 0 && static_cast(anim_shm.animation_player_result.sprite_sheet_col) < BONGOCAT_SPRITE_SHEET_COLS); - break; - } - - auto [cat_x, cat_y, cat_width, cat_height] = get_position(wayland_ctx, sheet, current_config); - auto cat_x_with_offset = cat_x + static_cast(anim_shm.movement_offset_x); - - if (region) { - // draw debug rectangle - if (current_config.enable_movement_debug && current_config.movement_radius > 0) { - cat_rect_t movement_debug_bar{}; - switch (current_config.cat_align) { - case config::align_type_t::ALIGN_CENTER: - movement_debug_bar = { .x = cat_x + cat_width/2 - current_config.movement_radius, .y = 0, .width = current_config.movement_radius * 2, .height = wayland_ctx._bar_height }; - break; - case config::align_type_t::ALIGN_LEFT: - movement_debug_bar = { .x = cat_x, .y = 0, .width = current_config.movement_radius * 2, .height = wayland_ctx._bar_height }; - break; - case config::align_type_t::ALIGN_RIGHT: - movement_debug_bar = { .x = cat_x + cat_width - current_config.movement_radius*2, .y = 0, .width = current_config.movement_radius * 2, .height = wayland_ctx._bar_height }; - break; - } - - const bool bar_visible = !wayland_ctx._fullscreen_detected && wayland_ctx.bar_visibility == platform::wayland::bar_visibility_t::Show; - const int effective_opacity = bar_visible ? current_config.overlay_opacity : 0; - const uint32_t fill = DEBUG_MOVEMENT_BAR_COLOR & (0x00FFFFFF | (static_cast(effective_opacity) << 24u)); // RGBA, little-endian - auto *p = reinterpret_cast(pixels); - assert(wayland_ctx._screen_width >= 0); - [[maybe_unused]] const size_t total_pixels = static_cast(wayland_ctx._screen_width) * static_cast(wayland_ctx._bar_height); - for (int32_t y = movement_debug_bar.y;y < movement_debug_bar.y + movement_debug_bar.height && y < wayland_ctx._bar_height; y++) { - for (int32_t x = movement_debug_bar.x;x < movement_debug_bar.x + movement_debug_bar.width && x < wayland_ctx._screen_width; x++) { - if (x >= 0 && y >= 0) { - size_t pi = static_cast(x) + static_cast(y) * static_cast(wayland_ctx._screen_width); - assert(pi < total_pixels); - p[pi] = fill; - } - } - } - } +#include +#include - blit_image_color_option_flags_t drawing_option = blit_image_color_option_flags_t::Normal; - if (current_config.invert_color) { - drawing_option = flag_add(drawing_option, blit_image_color_option_flags_t::Invert); - } - if (anim_shm.anim_direction >= 1.0f) { - if (!current_config.mirror_x) { - drawing_option = flag_add(drawing_option, blit_image_color_option_flags_t::MirrorX); - } - } else { - if (current_config.mirror_x) { - drawing_option = flag_add(drawing_option, blit_image_color_option_flags_t::MirrorX); - } - } - if (current_config.mirror_y) { - drawing_option = flag_add(drawing_option, blit_image_color_option_flags_t::MirrorY); - } - if (current_config.enable_antialiasing) { - drawing_option = flag_add(drawing_option, blit_image_color_option_flags_t::BilinearInterpolation); - } - if (extra_drawing_option != blit_image_color_option_flags_t::Normal) { - drawing_option = flag_add(drawing_option, extra_drawing_option); - } +namespace bongocat::animation { +inline static constexpr uint32_t DEFAULT_FILL_COLOR = 0x00000000; // ARGB +inline static constexpr uint32_t DEBUG_MOVEMENT_BAR_COLOR = 0xFFFF0000; // ARGB + +// ============================================================================= +// DRAWING MANAGEMENT +// ============================================================================= + +struct cat_rect_t { + int x; + int y; + int width; + int height; +}; + +template +/// @TODO: required SpriteSheet must be _sprite_sheet_t +cat_rect_t get_position(const platform::wayland::wayland_thread_context& wayland_ctx, const SpriteSheet& sheet, + const config::config_t& config) { + const int cat_height = config.cat_height; + const int cat_width = static_cast(static_cast(cat_height) * (static_cast(sheet.frame_width) / + static_cast(sheet.frame_height))); + + int cat_x = 0; + switch (config.cat_align) { + case config::align_type_t::ALIGN_CENTER: + cat_x = (wayland_ctx._screen_width - cat_width) / 2 + config.cat_x_offset; + break; + case config::align_type_t::ALIGN_LEFT: + cat_x = config.cat_x_offset; + break; + case config::align_type_t::ALIGN_RIGHT: + cat_x = wayland_ctx._screen_width - cat_width - config.cat_x_offset; + break; + default: + BONGOCAT_LOG_VERBOSE("Invalid cat_align %d", config.cat_align); + break; + } + const int cat_y = ((wayland_ctx._bar_height - cat_height) / 2) + config.cat_y_offset; + + return {.x = cat_x, .y = cat_y, .width = cat_width, .height = cat_height}; +} - blit_image_scaled(pixels, pixels_size, - wayland_ctx._screen_width, wayland_ctx._bar_height, BGRA_CHANNELS, - sheet.image.pixels.data, sheet.image.pixels._size_bytes, sheet.image.sprite_sheet_width, sheet.image.sprite_sheet_height, sheet.image.channels, - region->col * sheet.frame_width, region->row * sheet.frame_height, - sheet.frame_width, sheet.frame_height, - cat_x_with_offset, cat_y, cat_width, cat_height, - blit_image_color_order_t::BGRA, blit_image_color_order_t::RGBA, drawing_option); +/// @TODO: make draw_sprite more generic (template?) +void draw_sprite(platform::wayland::wayland_context_t& ctx, platform::wayland::wayland_shm_buffer_t& shm_buffer, + const bongocat_sprite_sheet_t& sheet, + blit_image_color_option_flags_t extra_drawing_option = blit_image_color_option_flags_t::Normal) { + using namespace assets; + if (sheet.frame_width <= 0 || sheet.frame_height <= 0) { + return; + } + + const platform::wayland::wayland_thread_context& wayland_ctx = ctx.thread_context; + animation_thread_context_t& anim = ctx.animation_context->thread_context; + // animation_trigger_context_t *trigger_ctx = ctx.animation_trigger_context; + // platform::wayland::wayland_shared_memory_t *wayland_ctx_shm = wayland_ctx.ctx_shm.ptr; + + assert(wayland_ctx._local_copy_config); + assert(anim.shm); + const config::config_t& current_config = *wayland_ctx._local_copy_config.ptr; + const animation_shared_memory_t& anim_shm = *anim.shm; + + uint8_t *pixels = shm_buffer.pixels.data; + const size_t pixels_size = shm_buffer.pixels._size_bytes; + + const sprite_sheet_animation_frame_t *region = BONGOCAT_NULLPTR; + switch (anim_shm.animation_player_result.sprite_sheet_col) { + case BONGOCAT_FRAME_BOTH_UP: + region = &sheet.both_up; + break; + case BONGOCAT_FRAME_LEFT_DOWN: + region = &sheet.left_down; + break; + case BONGOCAT_FRAME_RIGHT_DOWN: + region = &sheet.right_down; + break; + case BONGOCAT_FRAME_BOTH_DOWN: + region = &sheet.both_down; + break; + default: + assert(anim_shm.animation_player_result.sprite_sheet_col >= 0 && + static_cast(anim_shm.animation_player_result.sprite_sheet_col) < BONGOCAT_SPRITE_SHEET_COLS); + break; + } + + auto [cat_x, cat_y, cat_width, cat_height] = get_position(wayland_ctx, sheet, current_config); + auto cat_x_with_offset = cat_x + static_cast(anim_shm.movement_offset_x); + + if (region != BONGOCAT_NULLPTR) { + // draw debug rectangle + if (current_config.enable_movement_debug >= 1 && current_config.movement_radius > 0) { + cat_rect_t movement_debug_bar{}; + switch (current_config.cat_align) { + case config::align_type_t::ALIGN_CENTER: + movement_debug_bar = {.x = cat_x + (cat_width / 2) - current_config.movement_radius, + .y = 0, + .width = current_config.movement_radius * 2, + .height = wayland_ctx._bar_height}; + break; + case config::align_type_t::ALIGN_LEFT: + movement_debug_bar = { + .x = cat_x, .y = 0, .width = current_config.movement_radius * 2, .height = wayland_ctx._bar_height}; + break; + case config::align_type_t::ALIGN_RIGHT: + movement_debug_bar = {.x = cat_x + cat_width - (current_config.movement_radius * 2), + .y = 0, + .width = current_config.movement_radius * 2, + .height = wayland_ctx._bar_height}; + break; + } + + // Skip fullscreen hiding when layer is LAYER_OVERLAY (always visible) + const bool is_overlay_layer = current_config.layer == config::layer_type_t::LAYER_OVERLAY; + const bool is_fullscreen = !is_overlay_layer && wayland_ctx._fullscreen_detected; + const bool bar_visible = + !is_fullscreen && wayland_ctx.bar_visibility == platform::wayland::bar_visibility_t::Show; + const int effective_opacity = bar_visible ? current_config.overlay_opacity : 0; + const uint32_t fill = DEBUG_MOVEMENT_BAR_COLOR & + (0x00FFFFFF | (static_cast(effective_opacity) << 24u)); // RGBA, little-endian + auto *p = reinterpret_cast(pixels); + assert(wayland_ctx._screen_width >= 0); + [[maybe_unused]] const size_t total_pixels = + static_cast(wayland_ctx._screen_width) * static_cast(wayland_ctx._bar_height); + for (int32_t y = movement_debug_bar.y; + y < movement_debug_bar.y + movement_debug_bar.height && y < wayland_ctx._bar_height; y++) { + for (int32_t x = movement_debug_bar.x; + x < movement_debug_bar.x + movement_debug_bar.width && x < wayland_ctx._screen_width; x++) { + if (x >= 0 && y >= 0) { + const size_t pi = + static_cast(x) + (static_cast(y) * static_cast(wayland_ctx._screen_width)); + assert(pi < total_pixels); + p[pi] = fill; + } } + } } - void draw_sprite(platform::wayland::wayland_session_t& ctx, platform::wayland::wayland_shm_buffer_t& shm_buffer, const dm_sprite_sheet_t& sheet, blit_image_color_option_flags_t extra_drawing_option = blit_image_color_option_flags_t::Normal) { - using namespace assets; - if (sheet.frame_width <= 0 || sheet.frame_height <= 0) { - return; - } - - platform::wayland::wayland_context_t& wayland_ctx = ctx.wayland_context; - animation_context_t& anim = ctx.animation_trigger_context->anim; - //animation_trigger_context_t *trigger_ctx = ctx.animation_trigger_context; - //platform::wayland::wayland_shared_memory_t *wayland_ctx_shm = wayland_ctx.ctx_shm.ptr; - - assert(wayland_ctx._local_copy_config != nullptr); - assert(anim.shm != nullptr); - const config::config_t& current_config = *wayland_ctx._local_copy_config.ptr; - const animation_shared_memory_t& anim_shm = *anim.shm; - - uint8_t *pixels = shm_buffer.pixels.data; - const size_t pixels_size = shm_buffer.pixels._size_bytes; - - const sprite_sheet_animation_frame_t* region = nullptr; - switch (anim_shm.animation_player_result.sprite_sheet_col) { - case DM_FRAME_IDLE1: - region = &sheet.frames.idle_1; - break; - case DM_FRAME_IDLE2: - region = &sheet.frames.idle_2; - break; - case DM_FRAME_ANGRY: - region = &sheet.frames.angry; - break; - case DM_FRAME_DOWN: - region = &sheet.frames.down; - break; - case DM_FRAME_HAPPY: - region = &sheet.frames.happy; - break; - case DM_FRAME_EAT1: - region = &sheet.frames.eat_1; - break; - case DM_FRAME_SLEEP: - region = &sheet.frames.sleep; - break; - case DM_FRAME_REFUSE: - region = &sheet.frames.refuse; - break; - case DM_FRAME_SAD: - region = &sheet.frames.sad; - break; - case DM_FRAME_LOSE1: - region = &sheet.frames.lose_1; - break; - case DM_FRAME_EAT2: - region = &sheet.frames.eat_2; - break; - case DM_FRAME_LOSE2: - region = &sheet.frames.lose_2; - break; - case DM_FRAME_ATTACK: - region = &sheet.frames.attack_1; - break; - case DM_FRAME_MOVEMENT1: - region = &sheet.frames.movement_1; - break; - case DM_FRAME_MOVEMENT2: - region = &sheet.frames.movement_2; - break; - default: - assert(anim_shm.animation_player_result.sprite_sheet_col >= 0 && static_cast(anim_shm.animation_player_result.sprite_sheet_col) < DM_SPRITE_SHEET_MAX_COLS); - break; - } - - auto [cat_x, cat_y, cat_width, cat_height] = get_position(wayland_ctx, sheet, current_config); - auto cat_x_with_offset = cat_x + static_cast(anim_shm.movement_offset_x); - - if (region) { - // draw debug rectangle - if (current_config.enable_movement_debug && current_config.movement_radius > 0) { - cat_rect_t movement_debug_bar{}; - switch (current_config.cat_align) { - case config::align_type_t::ALIGN_CENTER: - movement_debug_bar = { .x = cat_x + cat_width/2 - current_config.movement_radius, .y = 0, .width = current_config.movement_radius * 2, .height = wayland_ctx._bar_height }; - break; - case config::align_type_t::ALIGN_LEFT: - movement_debug_bar = { .x = cat_x, .y = 0, .width = current_config.movement_radius * 2, .height = wayland_ctx._bar_height }; - break; - case config::align_type_t::ALIGN_RIGHT: - movement_debug_bar = { .x = cat_x + cat_width - current_config.movement_radius*2, .y = 0, .width = current_config.movement_radius * 2, .height = wayland_ctx._bar_height }; - break; - } - - const bool bar_visible = !wayland_ctx._fullscreen_detected && wayland_ctx.bar_visibility == platform::wayland::bar_visibility_t::Show; - const int effective_opacity = bar_visible ? current_config.overlay_opacity : 0; - const uint32_t fill = DEBUG_MOVEMENT_BAR_COLOR & (0x00FFFFFF | (static_cast(effective_opacity) << 24u)); // RGBA, little-endian - auto *p = reinterpret_cast(pixels); - assert(wayland_ctx._screen_width >= 0); - [[maybe_unused]] const size_t total_pixels = static_cast(wayland_ctx._screen_width) * static_cast(wayland_ctx._bar_height); - for (int32_t y = movement_debug_bar.y;y < movement_debug_bar.y + movement_debug_bar.height && y < wayland_ctx._bar_height; y++) { - for (int32_t x = movement_debug_bar.x;x < movement_debug_bar.x + movement_debug_bar.width && x < wayland_ctx._screen_width; x++) { - if (x >= 0 && y >= 0) { - size_t pi = static_cast(x) + static_cast(y) * static_cast(wayland_ctx._screen_width); - assert(pi < total_pixels); - p[pi] = fill; - } - } - } - } - - blit_image_color_option_flags_t drawing_option = blit_image_color_option_flags_t::Normal; - if (current_config.invert_color) { - drawing_option = flag_add(drawing_option, blit_image_color_option_flags_t::Invert); - } - if (anim_shm.anim_direction >= 1.0f) { - if (!current_config.mirror_x) { - drawing_option = flag_add(drawing_option, blit_image_color_option_flags_t::MirrorX); - } - } else { - if (current_config.mirror_x) { - drawing_option = flag_add(drawing_option, blit_image_color_option_flags_t::MirrorX); - } - } - if (current_config.mirror_y) { - drawing_option = flag_add(drawing_option, blit_image_color_option_flags_t::MirrorY); - } - if (extra_drawing_option != blit_image_color_option_flags_t::Normal) { - drawing_option = flag_add(drawing_option, extra_drawing_option); - } - - blit_image_scaled(pixels, pixels_size, - wayland_ctx._screen_width, wayland_ctx._bar_height, BGRA_CHANNELS, - sheet.image.pixels.data, sheet.image.pixels._size_bytes, sheet.image.sprite_sheet_width, sheet.image.sprite_sheet_height, sheet.image.channels, - region->col * sheet.frame_width, region->row * sheet.frame_height, - sheet.frame_width, sheet.frame_height, - cat_x_with_offset, cat_y, cat_width, cat_height, - blit_image_color_order_t::BGRA, blit_image_color_order_t::RGBA, drawing_option); - } + blit_image_color_option_flags_t drawing_option = blit_image_color_option_flags_t::Normal; + if (current_config.invert_color >= 1) { + drawing_option = flag_add(drawing_option, blit_image_color_option_flags_t::Invert); + } + if (anim_shm.anim_direction >= 1.0f) { + if (current_config.mirror_x <= 0) { + drawing_option = flag_add(drawing_option, blit_image_color_option_flags_t::MirrorX); + } + } else { + if (current_config.mirror_x >= 1) { + drawing_option = flag_add(drawing_option, blit_image_color_option_flags_t::MirrorX); + } + } + if (current_config.mirror_y >= 1) { + drawing_option = flag_add(drawing_option, blit_image_color_option_flags_t::MirrorY); + } + if (current_config.enable_antialiasing >= 1) { + drawing_option = flag_add(drawing_option, blit_image_color_option_flags_t::BilinearInterpolation); + } + if (extra_drawing_option != blit_image_color_option_flags_t::Normal) { + drawing_option = flag_add(drawing_option, extra_drawing_option); } - void draw_sprite(platform::wayland::wayland_session_t& ctx, platform::wayland::wayland_shm_buffer_t& shm_buffer, const pkmn_sprite_sheet_t& sheet, blit_image_color_option_flags_t extra_drawing_option = blit_image_color_option_flags_t::Normal) { - using namespace assets; - if (sheet.frame_width <= 0 || sheet.frame_height <= 0) { - return; - } - - platform::wayland::wayland_context_t& wayland_ctx = ctx.wayland_context; - animation_context_t& anim = ctx.animation_trigger_context->anim; - //animation_trigger_context_t *trigger_ctx = ctx.animation_trigger_context; - //platform::wayland::wayland_shared_memory_t *wayland_ctx_shm = wayland_ctx.ctx_shm.ptr; - - assert(wayland_ctx._local_copy_config != nullptr); - assert(anim.shm != nullptr); - const config::config_t& current_config = *wayland_ctx._local_copy_config.ptr; - const animation_shared_memory_t& anim_shm = *anim.shm; - - uint8_t *pixels = shm_buffer.pixels.data; - const size_t pixels_size = shm_buffer.pixels._size_bytes; - - const sprite_sheet_animation_frame_t* region = nullptr; - switch (anim_shm.animation_player_result.sprite_sheet_col) { - case PKMN_FRAME_IDLE1: - region = &sheet.idle_1; - break; - case PKMN_FRAME_IDLE2: - region = &sheet.idle_2; - break; - default: - assert(anim_shm.animation_player_result.sprite_sheet_col >= 0 && static_cast(anim_shm.animation_player_result.sprite_sheet_col) < PKMN_SPRITE_SHEET_COLS); - break; - } - - auto [cat_x, cat_y, cat_width, cat_height] = get_position(wayland_ctx, sheet, current_config); - auto cat_x_with_offset = cat_x + static_cast(anim_shm.movement_offset_x); - - if (region) { - // draw debug rectangle - if (current_config.enable_movement_debug && current_config.movement_radius > 0) { - cat_rect_t movement_debug_bar{}; - switch (current_config.cat_align) { - case config::align_type_t::ALIGN_CENTER: - movement_debug_bar = { .x = cat_x + cat_width/2 - current_config.movement_radius, .y = 0, .width = current_config.movement_radius * 2, .height = wayland_ctx._bar_height }; - break; - case config::align_type_t::ALIGN_LEFT: - movement_debug_bar = { .x = cat_x, .y = 0, .width = current_config.movement_radius * 2, .height = wayland_ctx._bar_height }; - break; - case config::align_type_t::ALIGN_RIGHT: - movement_debug_bar = { .x = cat_x + cat_width - current_config.movement_radius*2, .y = 0, .width = current_config.movement_radius * 2, .height = wayland_ctx._bar_height }; - break; - } - - const bool bar_visible = !wayland_ctx._fullscreen_detected && wayland_ctx.bar_visibility == platform::wayland::bar_visibility_t::Show; - const int effective_opacity = bar_visible ? current_config.overlay_opacity : 0; - const uint32_t fill = DEBUG_MOVEMENT_BAR_COLOR & (0x00FFFFFF | (static_cast(effective_opacity) << 24u)); // RGBA, little-endian - auto *p = reinterpret_cast(pixels); - assert(wayland_ctx._screen_width >= 0); - [[maybe_unused]] const size_t total_pixels = static_cast(wayland_ctx._screen_width) * static_cast(wayland_ctx._bar_height); - for (int32_t y = movement_debug_bar.y;y < movement_debug_bar.y + movement_debug_bar.height && y < wayland_ctx._bar_height; y++) { - for (int32_t x = movement_debug_bar.x;x < movement_debug_bar.x + movement_debug_bar.width && x < wayland_ctx._screen_width; x++) { - if (x >= 0 && y >= 0) { - size_t pi = static_cast(x) + static_cast(y) * static_cast(wayland_ctx._screen_width); - assert(pi < total_pixels); - p[pi] = fill; - } - } - } - } - - blit_image_color_option_flags_t drawing_option = blit_image_color_option_flags_t::Normal; - if (current_config.invert_color) { - drawing_option = flag_add(drawing_option, blit_image_color_option_flags_t::Invert); - } - if (anim_shm.anim_direction >= 1.0f) { - if (!current_config.mirror_x) { - drawing_option = flag_add(drawing_option, blit_image_color_option_flags_t::MirrorX); - } - } else { - if (current_config.mirror_x) { - drawing_option = flag_add(drawing_option, blit_image_color_option_flags_t::MirrorX); - } - } - if (current_config.mirror_y) { - drawing_option = flag_add(drawing_option, blit_image_color_option_flags_t::MirrorY); - } - if (extra_drawing_option != blit_image_color_option_flags_t::Normal) { - drawing_option = flag_add(drawing_option, extra_drawing_option); - } + blit_image_scaled(pixels, pixels_size, wayland_ctx._screen_width, wayland_ctx._bar_height, BGRA_CHANNELS, + sheet.image.pixels.data, sheet.image.pixels._size_bytes, sheet.image.sprite_sheet_width, + sheet.image.sprite_sheet_height, sheet.image.channels, region->col * sheet.frame_width, + region->row * sheet.frame_height, sheet.frame_width, sheet.frame_height, cat_x_with_offset, cat_y, + cat_width, cat_height, blit_image_color_order_t::BGRA, blit_image_color_order_t::RGBA, + drawing_option); + } +} - blit_image_scaled(pixels, pixels_size, - wayland_ctx._screen_width, wayland_ctx._bar_height, BGRA_CHANNELS, - sheet.image.pixels.data, sheet.image.pixels._size_bytes, sheet.image.sprite_sheet_width, sheet.image.sprite_sheet_height, sheet.image.channels, - region->col * sheet.frame_width, region->row * sheet.frame_height, - sheet.frame_width, sheet.frame_height, - cat_x_with_offset, cat_y, cat_width, cat_height, - blit_image_color_order_t::BGRA, blit_image_color_order_t::RGBA, drawing_option); +void draw_sprite(platform::wayland::wayland_context_t& ctx, platform::wayland::wayland_shm_buffer_t& shm_buffer, + const dm_sprite_sheet_t& sheet, + blit_image_color_option_flags_t extra_drawing_option = blit_image_color_option_flags_t::Normal) { + using namespace assets; + if (sheet.frame_width <= 0 || sheet.frame_height <= 0) { + return; + } + + const platform::wayland::wayland_thread_context& wayland_ctx = ctx.thread_context; + animation_thread_context_t& anim = ctx.animation_context->thread_context; + // animation_trigger_context_t *trigger_ctx = ctx.animation_trigger_context; + // platform::wayland::wayland_shared_memory_t *wayland_ctx_shm = wayland_ctx.ctx_shm.ptr; + + assert(wayland_ctx._local_copy_config); + assert(anim.shm); + const config::config_t& current_config = *wayland_ctx._local_copy_config.ptr; + const animation_shared_memory_t& anim_shm = *anim.shm; + + uint8_t *pixels = shm_buffer.pixels.data; + const size_t pixels_size = shm_buffer.pixels._size_bytes; + + const sprite_sheet_animation_frame_t *region = BONGOCAT_NULLPTR; + switch (anim_shm.animation_player_result.sprite_sheet_col) { + case DM_FRAME_IDLE1: + region = &sheet.frames.idle_1; + break; + case DM_FRAME_IDLE2: + region = &sheet.frames.idle_2; + break; + case DM_FRAME_ANGRY: + region = &sheet.frames.angry; + break; + case DM_FRAME_DOWN: + region = &sheet.frames.down; + break; + case DM_FRAME_HAPPY: + region = &sheet.frames.happy; + break; + case DM_FRAME_EAT1: + region = &sheet.frames.eat_1; + break; + case DM_FRAME_SLEEP: + region = &sheet.frames.sleep; + break; + case DM_FRAME_REFUSE: + region = &sheet.frames.refuse; + break; + case DM_FRAME_SAD: + region = &sheet.frames.sad; + break; + case DM_FRAME_LOSE1: + region = &sheet.frames.lose_1; + break; + case DM_FRAME_EAT2: + region = &sheet.frames.eat_2; + break; + case DM_FRAME_LOSE2: + region = &sheet.frames.lose_2; + break; + case DM_FRAME_ATTACK: + region = &sheet.frames.attack_1; + break; + case DM_FRAME_MOVEMENT1: + region = &sheet.frames.movement_1; + break; + case DM_FRAME_MOVEMENT2: + region = &sheet.frames.movement_2; + break; + default: + assert(anim_shm.animation_player_result.sprite_sheet_col >= 0 && + static_cast(anim_shm.animation_player_result.sprite_sheet_col) < DM_SPRITE_SHEET_MAX_COLS); + break; + } + + auto [cat_x, cat_y, cat_width, cat_height] = get_position(wayland_ctx, sheet, current_config); + auto cat_x_with_offset = cat_x + static_cast(anim_shm.movement_offset_x); + + if (region != BONGOCAT_NULLPTR) { + // draw debug rectangle + if (current_config.enable_movement_debug >= 1 && current_config.movement_radius > 0) { + cat_rect_t movement_debug_bar{}; + switch (current_config.cat_align) { + case config::align_type_t::ALIGN_CENTER: + movement_debug_bar = {.x = cat_x + (cat_width / 2) - current_config.movement_radius, + .y = 0, + .width = current_config.movement_radius * 2, + .height = wayland_ctx._bar_height}; + break; + case config::align_type_t::ALIGN_LEFT: + movement_debug_bar = { + .x = cat_x, .y = 0, .width = current_config.movement_radius * 2, .height = wayland_ctx._bar_height}; + break; + case config::align_type_t::ALIGN_RIGHT: + movement_debug_bar = {.x = cat_x + cat_width - (current_config.movement_radius * 2), + .y = 0, + .width = current_config.movement_radius * 2, + .height = wayland_ctx._bar_height}; + break; + } + + const bool bar_visible = + !wayland_ctx._fullscreen_detected && wayland_ctx.bar_visibility == platform::wayland::bar_visibility_t::Show; + const int effective_opacity = bar_visible ? current_config.overlay_opacity : 0; + const uint32_t fill = DEBUG_MOVEMENT_BAR_COLOR & + (0x00FFFFFF | (static_cast(effective_opacity) << 24u)); // RGBA, little-endian + auto *p = reinterpret_cast(pixels); + assert(wayland_ctx._screen_width >= 0); + [[maybe_unused]] const size_t total_pixels = + static_cast(wayland_ctx._screen_width) * static_cast(wayland_ctx._bar_height); + for (int32_t y = movement_debug_bar.y; + y < movement_debug_bar.y + movement_debug_bar.height && y < wayland_ctx._bar_height; y++) { + for (int32_t x = movement_debug_bar.x; + x < movement_debug_bar.x + movement_debug_bar.width && x < wayland_ctx._screen_width; x++) { + if (x >= 0 && y >= 0) { + const size_t pi = + static_cast(x) + (static_cast(y) * static_cast(wayland_ctx._screen_width)); + assert(pi < total_pixels); + p[pi] = fill; + } } + } } - void draw_sprite(platform::wayland::wayland_session_t& ctx, platform::wayland::wayland_shm_buffer_t& shm_buffer, const ms_agent_sprite_sheet_t& sheet, int col, int row) { - if (sheet.frame_width <= 0 || sheet.frame_height <= 0) { - return; - } - - platform::wayland::wayland_context_t& wayland_ctx = ctx.wayland_context; - //animation_context_t& anim = ctx.animation_trigger_context->anim; - //animation_trigger_context_t *trigger_ctx = ctx.animation_trigger_context; - //platform::wayland::wayland_shared_memory_t *wayland_ctx_shm = wayland_ctx.ctx_shm.ptr; - - assert(wayland_ctx._local_copy_config != nullptr); - //assert(anim.shm != nullptr); - const config::config_t& current_config = *wayland_ctx._local_copy_config.ptr; - //const animation_shared_memory_t& anim_shm = *anim.shm; - - uint8_t *pixels = shm_buffer.pixels.data; - const size_t pixels_size = shm_buffer.pixels._size_bytes; + blit_image_color_option_flags_t drawing_option = blit_image_color_option_flags_t::Normal; + if (current_config.invert_color >= 1) { + drawing_option = flag_add(drawing_option, blit_image_color_option_flags_t::Invert); + } + if (anim_shm.anim_direction >= 1.0f) { + if (current_config.mirror_x <= 0) { + drawing_option = flag_add(drawing_option, blit_image_color_option_flags_t::MirrorX); + } + } else { + if (current_config.mirror_x >= 1) { + drawing_option = flag_add(drawing_option, blit_image_color_option_flags_t::MirrorX); + } + } + if (current_config.mirror_y >= 1) { + drawing_option = flag_add(drawing_option, blit_image_color_option_flags_t::MirrorY); + } + if (extra_drawing_option != blit_image_color_option_flags_t::Normal) { + drawing_option = flag_add(drawing_option, extra_drawing_option); + } - auto [cat_x, cat_y, cat_width, cat_height] = get_position(wayland_ctx, sheet, current_config); + blit_image_scaled(pixels, pixels_size, wayland_ctx._screen_width, wayland_ctx._bar_height, BGRA_CHANNELS, + sheet.image.pixels.data, sheet.image.pixels._size_bytes, sheet.image.sprite_sheet_width, + sheet.image.sprite_sheet_height, sheet.image.channels, region->col * sheet.frame_width, + region->row * sheet.frame_height, sheet.frame_width, sheet.frame_height, cat_x_with_offset, cat_y, + cat_width, cat_height, blit_image_color_order_t::BGRA, blit_image_color_order_t::RGBA, + drawing_option); + } +} - blit_image_color_option_flags_t drawing_option = blit_image_color_option_flags_t::Normal; - if (current_config.invert_color) { - drawing_option = flag_add(drawing_option, blit_image_color_option_flags_t::Invert); - } - if (current_config.mirror_x) { - drawing_option = flag_add(drawing_option, blit_image_color_option_flags_t::MirrorX); +void draw_sprite(platform::wayland::wayland_context_t& ctx, platform::wayland::wayland_shm_buffer_t& shm_buffer, + const pkmn_sprite_sheet_t& sheet, + blit_image_color_option_flags_t extra_drawing_option = blit_image_color_option_flags_t::Normal) { + using namespace assets; + if (sheet.frame_width <= 0 || sheet.frame_height <= 0) { + return; + } + + const platform::wayland::wayland_thread_context& wayland_ctx = ctx.thread_context; + animation_thread_context_t& anim = ctx.animation_context->thread_context; + // animation_trigger_context_t *trigger_ctx = ctx.animation_trigger_context; + // platform::wayland::wayland_shared_memory_t *wayland_ctx_shm = wayland_ctx.ctx_shm.ptr; + + assert(wayland_ctx._local_copy_config); + assert(anim.shm); + const config::config_t& current_config = *wayland_ctx._local_copy_config.ptr; + const animation_shared_memory_t& anim_shm = *anim.shm; + + uint8_t *pixels = shm_buffer.pixels.data; + const size_t pixels_size = shm_buffer.pixels._size_bytes; + + const sprite_sheet_animation_frame_t *region = BONGOCAT_NULLPTR; + switch (anim_shm.animation_player_result.sprite_sheet_col) { + case PKMN_FRAME_IDLE1: + region = &sheet.idle_1; + break; + case PKMN_FRAME_IDLE2: + region = &sheet.idle_2; + break; + default: + assert(anim_shm.animation_player_result.sprite_sheet_col >= 0 && + static_cast(anim_shm.animation_player_result.sprite_sheet_col) < PKMN_SPRITE_SHEET_COLS); + break; + } + + auto [cat_x, cat_y, cat_width, cat_height] = get_position(wayland_ctx, sheet, current_config); + auto cat_x_with_offset = cat_x + static_cast(anim_shm.movement_offset_x); + + if (region != BONGOCAT_NULLPTR) { + // draw debug rectangle + if (current_config.enable_movement_debug >= 1 && current_config.movement_radius > 0) { + cat_rect_t movement_debug_bar{}; + switch (current_config.cat_align) { + case config::align_type_t::ALIGN_CENTER: + movement_debug_bar = {.x = cat_x + (cat_width / 2) - current_config.movement_radius, + .y = 0, + .width = current_config.movement_radius * 2, + .height = wayland_ctx._bar_height}; + break; + case config::align_type_t::ALIGN_LEFT: + movement_debug_bar = { + .x = cat_x, .y = 0, .width = current_config.movement_radius * 2, .height = wayland_ctx._bar_height}; + break; + case config::align_type_t::ALIGN_RIGHT: + movement_debug_bar = {.x = cat_x + cat_width - (current_config.movement_radius * 2), + .y = 0, + .width = current_config.movement_radius * 2, + .height = wayland_ctx._bar_height}; + break; + } + + const bool bar_visible = + !wayland_ctx._fullscreen_detected && wayland_ctx.bar_visibility == platform::wayland::bar_visibility_t::Show; + const int effective_opacity = bar_visible ? current_config.overlay_opacity : 0; + const uint32_t fill = DEBUG_MOVEMENT_BAR_COLOR & + (0x00FFFFFF | (static_cast(effective_opacity) << 24u)); // RGBA, little-endian + auto *p = reinterpret_cast(pixels); + assert(wayland_ctx._screen_width >= 0); + [[maybe_unused]] const size_t total_pixels = + static_cast(wayland_ctx._screen_width) * static_cast(wayland_ctx._bar_height); + for (int32_t y = movement_debug_bar.y; + y < movement_debug_bar.y + movement_debug_bar.height && y < wayland_ctx._bar_height; y++) { + for (int32_t x = movement_debug_bar.x; + x < movement_debug_bar.x + movement_debug_bar.width && x < wayland_ctx._screen_width; x++) { + if (x >= 0 && y >= 0) { + const size_t pi = + static_cast(x) + (static_cast(y) * static_cast(wayland_ctx._screen_width)); + assert(pi < total_pixels); + p[pi] = fill; + } } - if (current_config.mirror_y) { - drawing_option = flag_add(drawing_option, blit_image_color_option_flags_t::MirrorY); - } - if (current_config.enable_antialiasing) { - drawing_option = flag_add(drawing_option, blit_image_color_option_flags_t::BilinearInterpolation); - } - - blit_image_scaled(pixels, pixels_size, - wayland_ctx._screen_width, wayland_ctx._bar_height, BGRA_CHANNELS, - sheet.image.pixels.data, sheet.image.pixels._size_bytes, sheet.image.sprite_sheet_width, sheet.image.sprite_sheet_height, sheet.image.channels, - col * sheet.frame_width, row * sheet.frame_height, - sheet.frame_width, sheet.frame_height, - cat_x, cat_y, cat_width, cat_height, - blit_image_color_order_t::BGRA, blit_image_color_order_t::RGBA, drawing_option); + } } - enum class draw_sprite_overwrite_option_t : uint32_t { - None = 0, - MovementNoMirror = (1 << 0), - MovementMirror = (1 << 1), - }; - void draw_sprite(platform::wayland::wayland_session_t& ctx, platform::wayland::wayland_shm_buffer_t& shm_buffer, const custom_sprite_sheet_t& sheet, int col, int row, draw_sprite_overwrite_option_t overwrite_option = draw_sprite_overwrite_option_t::None) { - if (sheet.frame_width <= 0 || sheet.frame_height <= 0) { - return; - } - - platform::wayland::wayland_context_t& wayland_ctx = ctx.wayland_context; - animation_context_t& anim = ctx.animation_trigger_context->anim; - //animation_trigger_context_t *trigger_ctx = ctx.animation_trigger_context; - //platform::wayland::wayland_shared_memory_t *wayland_ctx_shm = wayland_ctx.ctx_shm.ptr; - - assert(wayland_ctx._local_copy_config != nullptr); - assert(anim.shm != nullptr); - const config::config_t& current_config = *wayland_ctx._local_copy_config.ptr; - const animation_shared_memory_t& anim_shm = *anim.shm; - - uint8_t *pixels = shm_buffer.pixels.data; - const size_t pixels_size = shm_buffer.pixels._size_bytes; + blit_image_color_option_flags_t drawing_option = blit_image_color_option_flags_t::Normal; + if (current_config.invert_color >= 1) { + drawing_option = flag_add(drawing_option, blit_image_color_option_flags_t::Invert); + } + if (anim_shm.anim_direction >= 1.0f) { + if (current_config.mirror_x <= 0) { + drawing_option = flag_add(drawing_option, blit_image_color_option_flags_t::MirrorX); + } + } else { + if (current_config.mirror_x >= 1) { + drawing_option = flag_add(drawing_option, blit_image_color_option_flags_t::MirrorX); + } + } + if (current_config.mirror_y >= 1) { + drawing_option = flag_add(drawing_option, blit_image_color_option_flags_t::MirrorY); + } + if (extra_drawing_option != blit_image_color_option_flags_t::Normal) { + drawing_option = flag_add(drawing_option, extra_drawing_option); + } - auto [cat_x, cat_y, cat_width, cat_height] = get_position(wayland_ctx, sheet, current_config); - auto cat_x_with_offset = cat_x + static_cast(anim_shm.movement_offset_x); + blit_image_scaled(pixels, pixels_size, wayland_ctx._screen_width, wayland_ctx._bar_height, BGRA_CHANNELS, + sheet.image.pixels.data, sheet.image.pixels._size_bytes, sheet.image.sprite_sheet_width, + sheet.image.sprite_sheet_height, sheet.image.channels, region->col * sheet.frame_width, + region->row * sheet.frame_height, sheet.frame_width, sheet.frame_height, cat_x_with_offset, cat_y, + cat_width, cat_height, blit_image_color_order_t::BGRA, blit_image_color_order_t::RGBA, + drawing_option); + } +} - // draw debug rectangle - if (current_config.enable_movement_debug && current_config.movement_radius > 0) { - cat_rect_t movement_debug_bar{}; - switch (current_config.cat_align) { - case config::align_type_t::ALIGN_CENTER: - movement_debug_bar = { .x = cat_x + cat_width/2 - current_config.movement_radius, .y = 0, .width = current_config.movement_radius * 2, .height = wayland_ctx._bar_height }; - break; - case config::align_type_t::ALIGN_LEFT: - movement_debug_bar = { .x = cat_x, .y = 0, .width = current_config.movement_radius * 2, .height = wayland_ctx._bar_height }; - break; - case config::align_type_t::ALIGN_RIGHT: - movement_debug_bar = { .x = cat_x + cat_width - current_config.movement_radius*2, .y = 0, .width = current_config.movement_radius * 2, .height = wayland_ctx._bar_height }; - break; - } +void draw_sprite(platform::wayland::wayland_context_t& ctx, platform::wayland::wayland_shm_buffer_t& shm_buffer, + const ms_agent_sprite_sheet_t& sheet, int col, int row) { + if (sheet.frame_width <= 0 || sheet.frame_height <= 0) { + return; + } + + const platform::wayland::wayland_thread_context& wayland_ctx = ctx.thread_context; + // animation_context_t& anim = ctx.animation_trigger_context->anim; + // animation_trigger_context_t *trigger_ctx = ctx.animation_trigger_context; + // platform::wayland::wayland_shared_memory_t *wayland_ctx_shm = wayland_ctx.ctx_shm.ptr; + + assert(wayland_ctx._local_copy_config); + // assert(anim.shm); + const config::config_t& current_config = *wayland_ctx._local_copy_config.ptr; + // const animation_shared_memory_t& anim_shm = *anim.shm; + + uint8_t *pixels = shm_buffer.pixels.data; + const size_t pixels_size = shm_buffer.pixels._size_bytes; + + auto [cat_x, cat_y, cat_width, cat_height] = get_position(wayland_ctx, sheet, current_config); + + blit_image_color_option_flags_t drawing_option = blit_image_color_option_flags_t::Normal; + if (current_config.invert_color >= 1) { + drawing_option = flag_add(drawing_option, blit_image_color_option_flags_t::Invert); + } + if (current_config.mirror_x >= 1) { + drawing_option = flag_add(drawing_option, blit_image_color_option_flags_t::MirrorX); + } + if (current_config.mirror_y >= 1) { + drawing_option = flag_add(drawing_option, blit_image_color_option_flags_t::MirrorY); + } + if (current_config.enable_antialiasing >= 1) { + drawing_option = flag_add(drawing_option, blit_image_color_option_flags_t::BilinearInterpolation); + } + + blit_image_scaled(pixels, pixels_size, wayland_ctx._screen_width, wayland_ctx._bar_height, BGRA_CHANNELS, + sheet.image.pixels.data, sheet.image.pixels._size_bytes, sheet.image.sprite_sheet_width, + sheet.image.sprite_sheet_height, sheet.image.channels, col * sheet.frame_width, + row * sheet.frame_height, sheet.frame_width, sheet.frame_height, cat_x, cat_y, cat_width, + cat_height, blit_image_color_order_t::BGRA, blit_image_color_order_t::RGBA, drawing_option); +} - const bool bar_visible = !wayland_ctx._fullscreen_detected && wayland_ctx.bar_visibility == platform::wayland::bar_visibility_t::Show; - const int effective_opacity = bar_visible ? current_config.overlay_opacity : 0; - const uint32_t fill = DEBUG_MOVEMENT_BAR_COLOR & (0x00FFFFFF | (static_cast(effective_opacity) << 24u)); // RGBA, little-endian - auto *p = reinterpret_cast(pixels); - assert(wayland_ctx._screen_width >= 0); - [[maybe_unused]] const size_t total_pixels = static_cast(wayland_ctx._screen_width) * static_cast(wayland_ctx._bar_height); - for (int32_t y = movement_debug_bar.y;y < movement_debug_bar.y + movement_debug_bar.height && y < wayland_ctx._bar_height; y++) { - for (int32_t x = movement_debug_bar.x;x < movement_debug_bar.x + movement_debug_bar.width && x < wayland_ctx._screen_width; x++) { - if (x >= 0 && y >= 0) { - size_t pi = static_cast(x) + static_cast(y) * static_cast(wayland_ctx._screen_width); - assert(pi < total_pixels); - p[pi] = fill; - } - } - } - } +enum class draw_sprite_overwrite_option_t : uint32_t { + None = 0, + MovementNoMirror = (1 << 0), + MovementMirror = (1 << 1), + DisableThresholdAlpha = (1 << 2), +}; +void draw_sprite(platform::wayland::wayland_context_t& ctx, platform::wayland::wayland_shm_buffer_t& shm_buffer, + const custom_sprite_sheet_t& sheet, int col, int row, + draw_sprite_overwrite_option_t overwrite_option = draw_sprite_overwrite_option_t::None) { + if (sheet.frame_width <= 0 || sheet.frame_height <= 0) { + return; + } + + const platform::wayland::wayland_thread_context& wayland_ctx = ctx.thread_context; + animation_thread_context_t& anim = ctx.animation_context->thread_context; + // animation_trigger_context_t *trigger_ctx = ctx.animation_trigger_context; + // platform::wayland::wayland_shared_memory_t *wayland_ctx_shm = wayland_ctx.ctx_shm.ptr; + + assert(wayland_ctx._local_copy_config); + assert(anim.shm); + const config::config_t& current_config = *wayland_ctx._local_copy_config.ptr; + const animation_shared_memory_t& anim_shm = *anim.shm; + + uint8_t *pixels = shm_buffer.pixels.data; + const size_t pixels_size = shm_buffer.pixels._size_bytes; + + auto [cat_x, cat_y, cat_width, cat_height] = get_position(wayland_ctx, sheet, current_config); + auto cat_x_with_offset = cat_x + static_cast(anim_shm.movement_offset_x); + + // draw debug rectangle + if (current_config.enable_movement_debug >= 1 && current_config.movement_radius > 0) { + cat_rect_t movement_debug_bar{}; + switch (current_config.cat_align) { + case config::align_type_t::ALIGN_CENTER: + movement_debug_bar = {.x = cat_x + (cat_width / 2) - current_config.movement_radius, + .y = 0, + .width = current_config.movement_radius * 2, + .height = wayland_ctx._bar_height}; + break; + case config::align_type_t::ALIGN_LEFT: + movement_debug_bar = { + .x = cat_x, .y = 0, .width = current_config.movement_radius * 2, .height = wayland_ctx._bar_height}; + break; + case config::align_type_t::ALIGN_RIGHT: + movement_debug_bar = {.x = cat_x + cat_width - (current_config.movement_radius * 2), + .y = 0, + .width = current_config.movement_radius * 2, + .height = wayland_ctx._bar_height}; + break; + } - blit_image_color_option_flags_t drawing_option = blit_image_color_option_flags_t::Normal; - if (current_config.invert_color) { - drawing_option = flag_add(drawing_option, blit_image_color_option_flags_t::Invert); - } - if (current_config.mirror_y) { - drawing_option = flag_add(drawing_option, blit_image_color_option_flags_t::MirrorY); - } - if (current_config.enable_antialiasing) { - drawing_option = flag_add(drawing_option, blit_image_color_option_flags_t::BilinearInterpolation); - } - switch (overwrite_option) { - case draw_sprite_overwrite_option_t::None: - if (anim_shm.anim_direction >= 1.0f) { - if (!current_config.mirror_x) { - drawing_option = flag_add(drawing_option, blit_image_color_option_flags_t::MirrorX); - } - } else { - if (current_config.mirror_x) { - drawing_option = flag_add(drawing_option, blit_image_color_option_flags_t::MirrorX); - } - } - break; - case draw_sprite_overwrite_option_t::MovementNoMirror: - if (anim_shm.anim_direction >= 1.0f) { - drawing_option = flag_remove(drawing_option, blit_image_color_option_flags_t::MirrorX); - } - break; - case draw_sprite_overwrite_option_t::MovementMirror: - if (anim_shm.anim_direction < 0.0f) { - drawing_option = flag_add(drawing_option, blit_image_color_option_flags_t::MirrorX); - } - break; + const bool bar_visible = + !wayland_ctx._fullscreen_detected && wayland_ctx.bar_visibility == platform::wayland::bar_visibility_t::Show; + const int effective_opacity = bar_visible ? current_config.overlay_opacity : 0; + const uint32_t fill = DEBUG_MOVEMENT_BAR_COLOR & + (0x00FFFFFF | (static_cast(effective_opacity) << 24u)); // RGBA, little-endian + auto *p = reinterpret_cast(pixels); + assert(wayland_ctx._screen_width >= 0); + [[maybe_unused]] const size_t total_pixels = + static_cast(wayland_ctx._screen_width) * static_cast(wayland_ctx._bar_height); + for (int32_t y = movement_debug_bar.y; + y < movement_debug_bar.y + movement_debug_bar.height && y < wayland_ctx._bar_height; y++) { + for (int32_t x = movement_debug_bar.x; + x < movement_debug_bar.x + movement_debug_bar.width && x < wayland_ctx._screen_width; x++) { + if (x >= 0 && y >= 0) { + const size_t pi = + static_cast(x) + (static_cast(y) * static_cast(wayland_ctx._screen_width)); + assert(pi < total_pixels); + p[pi] = fill; } - - blit_image_scaled(pixels, pixels_size, - wayland_ctx._screen_width, wayland_ctx._bar_height, BGRA_CHANNELS, - sheet.image.pixels.data, sheet.image.pixels._size_bytes, sheet.image.sprite_sheet_width, sheet.image.sprite_sheet_height, sheet.image.channels, - col * sheet.frame_width, row * sheet.frame_height, - sheet.frame_width, sheet.frame_height, - cat_x_with_offset, cat_y, cat_width, cat_height, - blit_image_color_order_t::BGRA, blit_image_color_order_t::RGBA, drawing_option); + } + } + } + + blit_image_color_option_flags_t drawing_option = blit_image_color_option_flags_t::Normal; + if (current_config.invert_color >= 1) { + drawing_option = flag_add(drawing_option, blit_image_color_option_flags_t::Invert); + } + if (current_config.mirror_y >= 1) { + drawing_option = flag_add(drawing_option, blit_image_color_option_flags_t::MirrorY); + } + if (current_config.enable_antialiasing >= 1) { + drawing_option = flag_add(drawing_option, blit_image_color_option_flags_t::BilinearInterpolation); + } + switch (overwrite_option) { + case draw_sprite_overwrite_option_t::None: + if (anim_shm.anim_direction >= 1.0f) { + if (current_config.mirror_x <= 0) { + drawing_option = flag_add(drawing_option, blit_image_color_option_flags_t::MirrorX); + } + } else { + if (current_config.mirror_x >= 1) { + drawing_option = flag_add(drawing_option, blit_image_color_option_flags_t::MirrorX); + } + } + break; + case draw_sprite_overwrite_option_t::MovementNoMirror: + if (anim_shm.anim_direction >= 1.0f) { + drawing_option = flag_remove(drawing_option, blit_image_color_option_flags_t::MirrorX); + } + break; + case draw_sprite_overwrite_option_t::MovementMirror: + if (anim_shm.anim_direction < 0.0f) { + drawing_option = flag_add(drawing_option, blit_image_color_option_flags_t::MirrorX); } + break; + case draw_sprite_overwrite_option_t::DisableThresholdAlpha: + drawing_option = flag_add(drawing_option, blit_image_color_option_flags_t::DisableThresholdAlpha); + break; + } + + blit_image_scaled(pixels, pixels_size, wayland_ctx._screen_width, wayland_ctx._bar_height, BGRA_CHANNELS, + sheet.image.pixels.data, sheet.image.pixels._size_bytes, sheet.image.sprite_sheet_width, + sheet.image.sprite_sheet_height, sheet.image.channels, col * sheet.frame_width, + row * sheet.frame_height, sheet.frame_width, sheet.frame_height, cat_x_with_offset, cat_y, + cat_width, cat_height, blit_image_color_order_t::BGRA, blit_image_color_order_t::RGBA, + drawing_option); +} - static bool draw_bar_on_buffer(platform::wayland::wayland_session_t& ctx, platform::wayland::wayland_shm_buffer_t& shm_buffer) { - platform::wayland::wayland_context_t& wayland_ctx = ctx.wayland_context; - animation_context_t& anim = ctx.animation_trigger_context->anim; - //animation_trigger_context_t *trigger_ctx = ctx.animation_trigger_context; - //platform::wayland::wayland_shared_memory_t *wayland_ctx_shm = wayland_ctx.ctx_shm.ptr; - - // read-only - assert(wayland_ctx._local_copy_config != nullptr); - assert(anim.shm != nullptr); - const config::config_t& current_config = *wayland_ctx._local_copy_config.ptr; - - uint8_t *pixels = shm_buffer.pixels.data; - const size_t pixels_size = shm_buffer.pixels._size_bytes; - - const bool bar_visible = !wayland_ctx._fullscreen_detected && wayland_ctx.bar_visibility == platform::wayland::bar_visibility_t::Show; - const int effective_opacity = bar_visible ? current_config.overlay_opacity : 0; - - assert(wayland_ctx._screen_width >= 0); - assert(wayland_ctx._bar_height >= 0); - assert(effective_opacity >= 0); - - // Fast clear with 32-bit fill - const uint32_t fill = DEFAULT_FILL_COLOR | (static_cast(effective_opacity) << 24u); // RGBA, little-endian - auto *p = reinterpret_cast(pixels); - const size_t total_pixels = static_cast(wayland_ctx._screen_width) * static_cast(wayland_ctx._bar_height); - if (current_config.enable_debug) { - if (const size_t expected_bytes = total_pixels * sizeof(uint32_t); expected_bytes > pixels_size) { - BONGOCAT_LOG_VERBOSE("draw_bar: pixel write would overflow buffer (expected %zu bytes, have %zu). Aborting draw.", - expected_bytes, pixels_size); - return false; - } +static bool draw_bar_on_buffer(platform::wayland::wayland_context_t& ctx, + platform::wayland::wayland_shm_buffer_t& shm_buffer) { + const platform::wayland::wayland_thread_context& wayland_ctx = ctx.thread_context; + animation_thread_context_t& anim = ctx.animation_context->thread_context; + // animation_trigger_context_t *trigger_ctx = ctx.animation_trigger_context; + // platform::wayland::wayland_shared_memory_t *wayland_ctx_shm = wayland_ctx.ctx_shm.ptr; + + // read-only + assert(wayland_ctx._local_copy_config); + assert(anim.shm); + const config::config_t& current_config = *wayland_ctx._local_copy_config.ptr; + + assert(shm_buffer.pixels.data); + uint8_t *pixels = shm_buffer.pixels.data; + const size_t pixels_size = shm_buffer.pixels._size_bytes; + + const bool bar_visible = + !wayland_ctx._fullscreen_detected && wayland_ctx.bar_visibility == platform::wayland::bar_visibility_t::Show; + const int effective_opacity = bar_visible ? current_config.overlay_opacity : 0; + + assert(wayland_ctx._screen_width >= 0); + assert(wayland_ctx._bar_height >= 0); + assert(effective_opacity >= 0); + + // Fast clear with 32-bit fill + const uint32_t fill = DEFAULT_FILL_COLOR | (static_cast(effective_opacity) << 24u); // RGBA, little-endian + auto *p = reinterpret_cast(pixels); + const size_t total_pixels = + static_cast(wayland_ctx._screen_width) * static_cast(wayland_ctx._bar_height); + if (current_config.enable_debug >= 1) { + if (const size_t expected_bytes = total_pixels * sizeof(uint32_t); expected_bytes > pixels_size) { + BONGOCAT_LOG_VERBOSE("draw_bar: pixel write would overflow buffer (expected %zu bytes, have %zu). Aborting draw.", + expected_bytes, pixels_size); + return false; + } + } + for (size_t i = 0; i < total_pixels; i++) { + p[i] = fill; + } + + { + platform::LockGuard guard(anim.anim_lock); + const animation_shared_memory_t& anim_shm = *anim.shm; + + if (bar_visible) { + switch (anim_shm.anim_type) { + case config::config_animation_sprite_sheet_layout_t::None: + break; + case config::config_animation_sprite_sheet_layout_t::Bongocat: { + if constexpr (!features::EnableLazyLoadAssets || features::EnablePreloadAssets) { + assert(anim_shm.anim_index >= 0 && static_cast(anim_shm.anim_index) < anim_shm.bongocat_anims.count); } - for (size_t i = 0; i < total_pixels; i++) { - p[i] = fill; + const animation_t& cat_anim = get_current_animation(anim); + assert(cat_anim.type == animation_t::type_t::Bongocat); + const bongocat_sprite_sheet_t& sheet = cat_anim.bongocat; + draw_sprite(ctx, shm_buffer, sheet, + current_config.enable_antialiasing >= 1 ? blit_image_color_option_flags_t::BilinearInterpolation + : blit_image_color_option_flags_t::Normal); + } break; + case config::config_animation_sprite_sheet_layout_t::Dm: { + if constexpr (!features::EnableLazyLoadAssets || features::EnablePreloadAssets) { + switch (anim_shm.anim_dm_set) { + case config::config_animation_dm_set_t::None: + break; + case config::config_animation_dm_set_t::min_dm: { + assert(anim_shm.anim_index >= 0 && static_cast(anim_shm.anim_index) < anim_shm.min_dm_anims.count); + } break; + case config::config_animation_dm_set_t::dm: { + assert(anim_shm.anim_index >= 0 && static_cast(anim_shm.anim_index) < anim_shm.dm_anims.count); + } break; + case config::config_animation_dm_set_t::dm20: { + assert(anim_shm.anim_index >= 0 && static_cast(anim_shm.anim_index) < anim_shm.dm20_anims.count); + } break; + case config::config_animation_dm_set_t::dmx: { + assert(anim_shm.anim_index >= 0 && static_cast(anim_shm.anim_index) < anim_shm.dmx_anims.count); + } break; + case config::config_animation_dm_set_t::pen: { + assert(anim_shm.anim_index >= 0 && static_cast(anim_shm.anim_index) < anim_shm.pen_anims.count); + } break; + case config::config_animation_dm_set_t::pen20: { + assert(anim_shm.anim_index >= 0 && static_cast(anim_shm.anim_index) < anim_shm.pen20_anims.count); + } break; + case config::config_animation_dm_set_t::dmc: { + assert(anim_shm.anim_index >= 0 && static_cast(anim_shm.anim_index) < anim_shm.dmc_anims.count); + } break; + case config::config_animation_dm_set_t::dmall: { + assert(anim_shm.anim_index >= 0 && static_cast(anim_shm.anim_index) < anim_shm.dmall_anims.count); + } break; + } } - - { - platform::LockGuard guard (anim.anim_lock); - const animation_shared_memory_t& anim_shm = *anim.shm; - - if (bar_visible) { - switch (anim_shm.anim_type) { - case config::config_animation_sprite_sheet_layout_t::None: - break; - case config::config_animation_sprite_sheet_layout_t::Bongocat: { - if constexpr (!features::EnableLazyLoadAssets || features::EnablePreloadAssets) { - assert(anim_shm.anim_index >= 0 && static_cast(anim_shm.anim_index) < anim_shm.bongocat_anims.count); - } - const animation_t& cat_anim = get_current_animation(anim); - assert(cat_anim.type == animation_t::Type::Bongocat); - const bongocat_sprite_sheet_t& sheet = cat_anim.bongocat; - draw_sprite(ctx, shm_buffer, sheet, current_config.enable_antialiasing ? blit_image_color_option_flags_t::BilinearInterpolation : blit_image_color_option_flags_t::Normal); - }break; - case config::config_animation_sprite_sheet_layout_t::Dm: { - if constexpr (!features::EnableLazyLoadAssets || features::EnablePreloadAssets) { - switch (anim_shm.anim_dm_set) { - case config::config_animation_dm_set_t::None: - break; - case config::config_animation_dm_set_t::min_dm: { - assert(anim_shm.anim_index >= 0 && static_cast(anim_shm.anim_index) < anim_shm.min_dm_anims.count); - }break; - case config::config_animation_dm_set_t::dm: { - assert(anim_shm.anim_index >= 0 && static_cast(anim_shm.anim_index) < anim_shm.dm_anims.count); - }break; - case config::config_animation_dm_set_t::dm20: { - assert(anim_shm.anim_index >= 0 && static_cast(anim_shm.anim_index) < anim_shm.dm20_anims.count); - }break; - case config::config_animation_dm_set_t::dmx: { - assert(anim_shm.anim_index >= 0 && static_cast(anim_shm.anim_index) < anim_shm.dmx_anims.count); - }break; - case config::config_animation_dm_set_t::pen: { - assert(anim_shm.anim_index >= 0 && static_cast(anim_shm.anim_index) < anim_shm.pen_anims.count); - }break; - case config::config_animation_dm_set_t::pen20: { - assert(anim_shm.anim_index >= 0 && static_cast(anim_shm.anim_index) < anim_shm.pen20_anims.count); - }break; - case config::config_animation_dm_set_t::dmc: { - assert(anim_shm.anim_index >= 0 && static_cast(anim_shm.anim_index) < anim_shm.dmc_anims.count); - }break; - case config::config_animation_dm_set_t::dmall: { - assert(anim_shm.anim_index >= 0 && static_cast(anim_shm.anim_index) < anim_shm.dmall_anims.count); - }break; - } - } - const animation_t& dm_anim = get_current_animation(anim); - assert(dm_anim.type == animation_t::Type::Dm); - const dm_sprite_sheet_t& sheet = dm_anim.dm; - draw_sprite(ctx, shm_buffer, sheet); - }break; - case config::config_animation_sprite_sheet_layout_t::Pkmn: { - if constexpr (!features::EnableLazyLoadAssets || features::EnablePreloadAssets) { - assert(anim_shm.anim_index >= 0 && static_cast(anim_shm.anim_index) < anim_shm.pkmn_anims.count); - } - const animation_t& pkmn_anim = get_current_animation(anim); - assert(pkmn_anim.type == animation_t::Type::Pkmn); - const auto& sheet = pkmn_anim.pkmn; - draw_sprite(ctx, shm_buffer, sheet); - }break; - case config::config_animation_sprite_sheet_layout_t::MsAgent:{ - if constexpr (!features::EnableLazyLoadAssets || features::EnablePreloadAssets) { - assert(anim_shm.anim_index >= 0 && static_cast(anim_shm.anim_index) < anim_shm.ms_anims.count); - } - const animation_t& ms_anim = get_current_animation(anim); - assert(ms_anim.type == animation_t::Type::MsAgent); - const ms_agent_sprite_sheet_t& sheet = ms_anim.ms_agent; - const int col = anim_shm.animation_player_result.sprite_sheet_col; - const int row = anim_shm.animation_player_result.sprite_sheet_row; - draw_sprite(ctx, shm_buffer, sheet, col, row); - }break; - case config::config_animation_sprite_sheet_layout_t::Custom:{ - const int col = anim_shm.animation_player_result.sprite_sheet_col; - const int row = anim_shm.animation_player_result.sprite_sheet_row; - assert(anim_shm.anim_index >= 0); - switch (anim_shm.anim_custom_set) { - case config::config_animation_custom_set_t::None: - break; - case config::config_animation_custom_set_t::misc: - if (features::EnableMiscEmbeddedAssets) { - if constexpr (!features::EnableLazyLoadAssets || features::EnablePreloadAssets) { - assert(anim_shm.anim_index >= 0 && static_cast(anim_shm.anim_index) < anim_shm.misc_anims.count); - } - const animation_t& custom_anim = get_current_animation(anim); - assert(custom_anim.type == animation_t::Type::Custom); - const custom_sprite_sheet_t& sheet = custom_anim.custom; - draw_sprite(ctx, shm_buffer, sheet, col, row); - } - break; - case config::config_animation_custom_set_t::pmd: - if (features::EnableMiscEmbeddedAssets) { - if constexpr (!features::EnableLazyLoadAssets || features::EnablePreloadAssets) { - assert(anim_shm.anim_index >= 0 && static_cast(anim_shm.anim_index) < anim_shm.pmd_anims.count); - } - const animation_t& custom_anim = get_current_animation(anim); - assert(custom_anim.type == animation_t::Type::Custom); - const custom_sprite_sheet_t& sheet = custom_anim.custom; - draw_sprite_overwrite_option_t overwrite_mirror_x {draw_sprite_overwrite_option_t::None}; - /* - switch (anim_shm.animation_player_result.overwrite_mirror_x) { - case animation_player_custom_overwrite_mirror_x::None: - break; - case animation_player_custom_overwrite_mirror_x::NoMirror: - overwrite_mirror_x = draw_sprite_overwrite_option_t::MovementNoMirror; - break; - case animation_player_custom_overwrite_mirror_x::Mirror: - overwrite_mirror_x = draw_sprite_overwrite_option_t::MovementMirror; - break; - } - */ - draw_sprite(ctx, shm_buffer, sheet, col, row, overwrite_mirror_x); - } - break; - case config::config_animation_custom_set_t::custom: - if (features::EnableCustomSpriteSheetsAssets && anim_shm.anim_index >= 0 && static_cast(anim_shm.anim_index) == assets::CUSTOM_ANIM_INDEX) { - const animation_t& custom_anim = get_current_animation(anim); - assert(custom_anim.type == animation_t::Type::Custom); - const custom_sprite_sheet_t& sheet = custom_anim.custom; - draw_sprite_overwrite_option_t overwrite_mirror_x {draw_sprite_overwrite_option_t::None}; - switch (anim_shm.animation_player_result.overwrite_mirror_x) { - case animation_player_custom_overwrite_mirror_x::None: - break; - case animation_player_custom_overwrite_mirror_x::NoMirror: - overwrite_mirror_x = draw_sprite_overwrite_option_t::MovementNoMirror; - break; - case animation_player_custom_overwrite_mirror_x::Mirror: - overwrite_mirror_x = draw_sprite_overwrite_option_t::MovementMirror; - break; - } - draw_sprite(ctx, shm_buffer, sheet, col, row, overwrite_mirror_x); - } - break; - } - }break; - } - } else { - BONGOCAT_LOG_VERBOSE("skip drawing, keep buffer clean: fullscreen=%d, visibility=%d", wayland_ctx._fullscreen_detected, wayland_ctx.bar_visibility); - } + const animation_t& dm_anim = get_current_animation(anim); + assert(dm_anim.type == animation_t::type_t::Dm); + const dm_sprite_sheet_t& sheet = dm_anim.dm; + draw_sprite(ctx, shm_buffer, sheet); + } break; + case config::config_animation_sprite_sheet_layout_t::Pkmn: { + if constexpr (!features::EnableLazyLoadAssets || features::EnablePreloadAssets) { + assert(anim_shm.anim_index >= 0 && static_cast(anim_shm.anim_index) < anim_shm.pkmn_anims.count); } - - return true; - } - - draw_bar_result_t draw_bar(platform::wayland::wayland_session_t& ctx) { - platform::wayland::wayland_context_t& wayland_ctx = ctx.wayland_context; - //animation_context_t& anim = ctx.animation_trigger_context->anim; - //animation_trigger_context_t *trigger_ctx = ctx.animation_trigger_context; - platform::wayland::wayland_shared_memory_t *wayland_ctx_shm = wayland_ctx.ctx_shm.ptr; - - // read-only - assert(wayland_ctx._local_copy_config != nullptr); - //assert(anim.shm != nullptr); - const config::config_t& current_config = *wayland_ctx._local_copy_config.ptr; - - if (!atomic_load(&wayland_ctx_shm->configured)) { - BONGOCAT_LOG_VERBOSE("Surface not configured yet, skipping draw"); - return draw_bar_result_t::Skip; + const animation_t& pkmn_anim = get_current_animation(anim); + assert(pkmn_anim.type == animation_t::type_t::Pkmn); + const auto& sheet = pkmn_anim.pkmn; + draw_sprite(ctx, shm_buffer, sheet); + } break; + case config::config_animation_sprite_sheet_layout_t::MsAgent: { + if constexpr (!features::EnableLazyLoadAssets || features::EnablePreloadAssets) { + assert(anim_shm.anim_index >= 0 && static_cast(anim_shm.anim_index) < anim_shm.ms_anims.count); } - - //assert(wayland_ctx_shm->current_buffer_index >= 0); - assert(platform::wayland::WAYLAND_NUM_BUFFERS > 0); - assert(platform::wayland::WAYLAND_NUM_BUFFERS <= INT_MAX); - [[maybe_unused]] const size_t current_buffer_index = wayland_ctx_shm->current_buffer_index; - [[maybe_unused]] size_t next_buffer_index = (wayland_ctx_shm->current_buffer_index + 1) % platform::wayland::WAYLAND_NUM_BUFFERS; - - platform::wayland::wayland_shm_buffer_t *shm_buffer = nullptr; - if constexpr (platform::wayland::WAYLAND_NUM_BUFFERS == 1) { - shm_buffer = &wayland_ctx_shm->buffers[0]; - if (atomic_load(&shm_buffer->busy)) { - BONGOCAT_LOG_VERBOSE("Wayland: single buffer still busy, skip draw"); - atomic_store(&wayland_ctx._redraw_after_frame, true); - return draw_bar_result_t::Busy; + const animation_t& ms_anim = get_current_animation(anim); + assert(ms_anim.type == animation_t::type_t::MsAgent); + const ms_agent_sprite_sheet_t& sheet = ms_anim.ms_agent; + const int col = anim_shm.animation_player_result.sprite_sheet_col; + const int row = anim_shm.animation_player_result.sprite_sheet_row; + draw_sprite(ctx, shm_buffer, sheet, col, row); + } break; + case config::config_animation_sprite_sheet_layout_t::Custom: { + const int col = anim_shm.animation_player_result.sprite_sheet_col; + const int row = anim_shm.animation_player_result.sprite_sheet_row; + assert(anim_shm.anim_index >= 0); + switch (anim_shm.anim_custom_set) { + case config::config_animation_custom_set_t::None: + break; + case config::config_animation_custom_set_t::misc: + if (features::EnableMiscEmbeddedAssets) { + if constexpr (!features::EnableLazyLoadAssets || features::EnablePreloadAssets) { + assert(anim_shm.anim_index >= 0 && static_cast(anim_shm.anim_index) < anim_shm.misc_anims.count); } - } else { - for (size_t i = 0; i < platform::wayland::WAYLAND_NUM_BUFFERS; i++) { - //assert(next_buffer_index >= 0); - auto *buf = &wayland_ctx_shm->buffers[next_buffer_index]; - if (!atomic_load(&buf->busy)) { - shm_buffer = buf; - next_buffer_index = i; + const animation_t& custom_anim = get_current_animation(anim); + assert(custom_anim.type == animation_t::type_t::Custom); + const custom_sprite_sheet_t& sheet = custom_anim.custom; + draw_sprite(ctx, shm_buffer, sheet, col, row); + } + break; + case config::config_animation_custom_set_t::pmd: + if (features::EnablePmdEmbeddedAssets) { + if constexpr (!features::EnableLazyLoadAssets || features::EnablePreloadAssets) { + assert(anim_shm.anim_index >= 0 && static_cast(anim_shm.anim_index) < anim_shm.pmd_anims.count); + } + const animation_t& custom_anim = get_current_animation(anim); + assert(custom_anim.type == animation_t::type_t::Custom); + const custom_sprite_sheet_t& sheet = custom_anim.custom; + draw_sprite_overwrite_option_t overwrite_drawing_options{draw_sprite_overwrite_option_t::None}; + // Mirroring for pmd sprite sheets not needed + /* + switch (anim_shm.animation_player_result.overwrite_mirror_x) { + case animation_player_custom_overwrite_mirror_x::None: break; - } - next_buffer_index = (next_buffer_index + 1) % platform::wayland::WAYLAND_NUM_BUFFERS; + case animation_player_custom_overwrite_mirror_x::NoMirror: + overwrite_drawing_options = + flag_add(overwrite_drawing_options,draw_sprite_overwrite_option_t::MovementNoMirror); break; case + animation_player_custom_overwrite_mirror_x::Mirror: overwrite_drawing_options = + flag_add(overwrite_drawing_options,draw_sprite_overwrite_option_t::MovementMirror); break; } - - if (!shm_buffer) { - BONGOCAT_LOG_VERBOSE("draw_bar: All buffers busy, skip drawing"); - atomic_store(&wayland_ctx._redraw_after_frame, true); - return draw_bar_result_t::Busy; + */ + + draw_sprite(ctx, shm_buffer, sheet, col, row, overwrite_drawing_options); + } + break; + case config::config_animation_custom_set_t::custom: + if (features::EnableCustomSpriteSheetsAssets && anim_shm.anim_index >= 0 && + static_cast(anim_shm.anim_index) == assets::CUSTOM_ANIM_INDEX) { + const animation_t& custom_anim = get_current_animation(anim); + assert(custom_anim.type == animation_t::type_t::Custom); + const custom_sprite_sheet_t& sheet = custom_anim.custom; + + draw_sprite_overwrite_option_t overwrite_drawing_options{draw_sprite_overwrite_option_t::None}; + switch (anim_shm.animation_player_result.overwrite_mirror_x) { + case animation_player_custom_overwrite_mirror_x::None: + break; + case animation_player_custom_overwrite_mirror_x::NoMirror: + overwrite_drawing_options = + flag_add(overwrite_drawing_options, draw_sprite_overwrite_option_t::MovementNoMirror); + break; + case animation_player_custom_overwrite_mirror_x::Mirror: + overwrite_drawing_options = + flag_add(overwrite_drawing_options, draw_sprite_overwrite_option_t::MovementMirror); + break; } - } + overwrite_drawing_options = + flag_add(overwrite_drawing_options, draw_sprite_overwrite_option_t::DisableThresholdAlpha); - BONGOCAT_LOG_VERBOSE("draw_bar: using buffer %zu", next_buffer_index); - if constexpr (platform::wayland::WAYLAND_NUM_BUFFERS > 1) { - wayland_ctx_shm->current_buffer_index = next_buffer_index; - BONGOCAT_LOG_VERBOSE("draw_bar: new current_buffer_index: %i", next_buffer_index); - } - assert(wayland_ctx_shm->current_buffer_index < platform::wayland::WAYLAND_NUM_BUFFERS); - - assert(shm_buffer); - draw_bar_on_buffer(ctx, *shm_buffer); - - assert(shm_buffer->buffer); - wl_surface_attach(wayland_ctx.surface, shm_buffer->buffer, 0, 0); - wl_surface_damage_buffer(wayland_ctx.surface, 0, 0, wayland_ctx._screen_width, wayland_ctx._bar_height); - - { - platform::LockGuard guard (wayland_ctx._frame_cb_lock); - if (!atomic_load(&wayland_ctx._frame_pending) && !wayland_ctx._frame_cb) { - wayland_ctx._frame_cb = wl_surface_frame(wayland_ctx.surface); - wl_callback_add_listener(wayland_ctx._frame_cb, &platform::wayland::details::frame_listener, &ctx); - atomic_store(&wayland_ctx._frame_pending, true); - BONGOCAT_LOG_VERBOSE("draw_bar: Set frame pending"); - } else { - // Frame callback is pending: queue redraw after current frame - atomic_store(&wayland_ctx._redraw_after_frame, true); - BONGOCAT_LOG_VERBOSE("draw_bar: Queued redraw after pending frame"); - } + draw_sprite(ctx, shm_buffer, sheet, col, row, overwrite_drawing_options); + } + break; } + } break; + } + } else { + BONGOCAT_LOG_VERBOSE("skip drawing, keep buffer clean: fullscreen=%d, visibility=%d", + wayland_ctx._fullscreen_detected, wayland_ctx.bar_visibility); + } + } - wl_surface_commit(wayland_ctx.surface); + return true; +} - atomic_store(&shm_buffer->busy, true); +draw_bar_result_t draw_bar(platform::wayland::wayland_context_t& ctx) { + platform::wayland::wayland_thread_context& wayland_ctx = ctx.thread_context; + // animation_context_t& anim = ctx.animation_trigger_context->anim; + // animation_trigger_context_t *trigger_ctx = ctx.animation_trigger_context; + platform::wayland::wayland_shared_memory_t& wayland_ctx_shm = *wayland_ctx.ctx_shm.ptr; + + // read-only + assert(wayland_ctx._local_copy_config); + // assert(anim.shm); + const config::config_t& current_config = *wayland_ctx._local_copy_config.ptr; + + if (!atomic_load(&wayland_ctx_shm.configured)) { + BONGOCAT_LOG_VERBOSE("Surface not configured yet, skipping draw"); + return draw_bar_result_t::Skip; + } + + // assert(wayland_ctx_shm->current_buffer_index >= 0); + assert(platform::wayland::WAYLAND_NUM_BUFFERS > 0); + assert(platform::wayland::WAYLAND_NUM_BUFFERS <= INT_MAX); + [[maybe_unused]] const size_t current_buffer_index = wayland_ctx_shm.current_buffer_index; + [[maybe_unused]] size_t next_buffer_index = + (wayland_ctx_shm.current_buffer_index + 1) % platform::wayland::WAYLAND_NUM_BUFFERS; + + platform::wayland::wayland_shm_buffer_t *shm_buffer = BONGOCAT_NULLPTR; + if constexpr (platform::wayland::WAYLAND_NUM_BUFFERS == 1) { + shm_buffer = &wayland_ctx_shm.buffers[0]; + if (atomic_load(&shm_buffer->busy)) { + BONGOCAT_LOG_VERBOSE("Wayland: single buffer still busy, skip draw"); + atomic_store(&wayland_ctx._redraw_after_frame, true); + return draw_bar_result_t::Busy; + } + } else { + for (size_t i = 0; i < platform::wayland::WAYLAND_NUM_BUFFERS; i++) { + // assert(next_buffer_index >= 0); + auto *buf = &wayland_ctx_shm.buffers[next_buffer_index]; + if (!atomic_load(&buf->busy)) { + shm_buffer = buf; + next_buffer_index = i; + break; + } + next_buffer_index = (next_buffer_index + 1) % platform::wayland::WAYLAND_NUM_BUFFERS; + } - /// @TODO: flush here or on main ??? - const int flush_ret = wl_display_flush(wayland_ctx.display); - if (flush_ret == -1 && errno == EAGAIN) { - // send buffer full; need to make progress by reading pending events first - wl_display_cancel_read(wayland_ctx.display); - if (wl_display_dispatch_pending(wayland_ctx.display) == -1) { - BONGOCAT_LOG_ERROR("draw_bar: wl_display_dispatch_pending failed after EAGAIN"); - } - } else if (flush_ret == -1) { - BONGOCAT_LOG_ERROR("draw_bar: wl_display_flush failed: %s", strerror(errno)); - wl_display_cancel_read(wayland_ctx.display); - wl_display_dispatch_pending(wayland_ctx.display); - } + if (shm_buffer == BONGOCAT_NULLPTR) { + BONGOCAT_LOG_VERBOSE("draw_bar: All buffers busy, skip drawing"); + atomic_store(&wayland_ctx._redraw_after_frame, true); + return draw_bar_result_t::Busy; + } + } + + assert(shm_buffer); + if (!shm_buffer->pixels) { + BONGOCAT_LOG_VERBOSE("draw_bar: Config or pixels not ready, skipping draw"); + return draw_bar_result_t::Skip; + } + + // for calc render time + [[maybe_unused]] const auto t0 = platform::get_current_time_us(); + + BONGOCAT_LOG_VERBOSE("draw_bar: using buffer %zu", next_buffer_index); + if constexpr (platform::wayland::WAYLAND_NUM_BUFFERS > 1) { + wayland_ctx_shm.current_buffer_index = next_buffer_index; + BONGOCAT_LOG_VERBOSE("draw_bar: new current_buffer_index: %i", next_buffer_index); + } + assert(wayland_ctx_shm.current_buffer_index < platform::wayland::WAYLAND_NUM_BUFFERS); + + assert(shm_buffer); + draw_bar_on_buffer(ctx, *shm_buffer); + + assert(shm_buffer->buffer); + wl_surface_attach(wayland_ctx.surface, shm_buffer->buffer, 0, 0); + wl_surface_damage_buffer(wayland_ctx.surface, 0, 0, wayland_ctx._screen_width, wayland_ctx._bar_height); + + { + platform::LockGuard guard(wayland_ctx._frame_cb_lock); + if (!atomic_load(&wayland_ctx._frame_pending) && wayland_ctx._frame_cb == BONGOCAT_NULLPTR) { + wayland_ctx._frame_cb = wl_surface_frame(wayland_ctx.surface); + wl_callback_add_listener(wayland_ctx._frame_cb, &platform::wayland::details::frame_listener, &ctx); + atomic_store(&wayland_ctx._frame_pending, true); + BONGOCAT_LOG_VERBOSE("draw_bar: Set frame pending"); + } else { + // Frame callback is pending: queue redraw after current frame + atomic_store(&wayland_ctx._redraw_after_frame, true); + BONGOCAT_LOG_VERBOSE("draw_bar: Queued redraw after pending frame"); + } + } - const platform::timestamp_ms_t now = platform::get_current_time_ms(); - assert(current_config.fps > 0); - if (const platform::time_ms_t frame_interval_ms = 1000 / current_config.fps; wayland_ctx._last_frame_timestamp_ms <= 0 || (now - wayland_ctx._last_frame_timestamp_ms) >= frame_interval_ms) { - wayland_ctx._last_frame_timestamp_ms = now; - } + wl_surface_commit(wayland_ctx.surface); + + atomic_store(&shm_buffer->busy, true); - return draw_bar_result_t::NoFlushNeeded; + /// @TODO: flush here or on main ??? + const int flush_ret = wl_display_flush(wayland_ctx.display); + if (flush_ret == -1 && errno == EAGAIN) { + // send buffer full; need to make progress by reading pending events first + wl_display_cancel_read(wayland_ctx.display); + if (wl_display_dispatch_pending(wayland_ctx.display) == -1) { + BONGOCAT_LOG_ERROR("draw_bar: wl_display_dispatch_pending failed after EAGAIN"); } + } else if (flush_ret == -1) { + BONGOCAT_LOG_ERROR("draw_bar: wl_display_flush failed: %s", strerror(errno)); + wl_display_cancel_read(wayland_ctx.display); + wl_display_dispatch_pending(wayland_ctx.display); + } + + const platform::timestamp_ms_t now = platform::get_current_time_ms(); + assert(current_config.fps > 0); + if (const platform::time_ms_t frame_interval_ms = 1000 / current_config.fps; + wayland_ctx._last_frame_timestamp_ms <= 0 || (now - wayland_ctx._last_frame_timestamp_ms) >= frame_interval_ms) { + wayland_ctx._last_frame_timestamp_ms = now; + } + + [[maybe_unused]] const auto t1 = platform::get_current_time_us(); + BONGOCAT_LOG_VERBOSE("draw_bar: time %.3fms (%.6fsec)", static_cast(t1 - t0) / 1000.0, + static_cast(t1 - t0) / 1000000.0); + + return draw_bar_result_t::NoFlushNeeded; } +} // namespace bongocat::animation diff --git a/src/graphics/bar.h b/src/graphics/bar.h index 5d9a15ae..f8f43bfc 100644 --- a/src/graphics/bar.h +++ b/src/graphics/bar.h @@ -1,16 +1,18 @@ #ifndef BONGOCAT_ANIMATION_BAR_H #define BONGOCAT_ANIMATION_BAR_H -#include "platform/global_wayland_session.h" +#include "platform/wayland_context.h" namespace bongocat::animation { - enum class draw_bar_result_t { - Skip, - Busy, - FlushNeeded, - NoFlushNeeded, - }; - draw_bar_result_t draw_bar(platform::wayland::wayland_session_t& ctx); -} +enum class draw_bar_result_t : uint8_t { + Skip, + Busy, + FlushNeeded, + NoFlushNeeded, +}; -#endif // BONGOCAT_ANIMATION_BAR_H \ No newline at end of file +// Draw the overlay bar +draw_bar_result_t draw_bar(platform::wayland::wayland_context_t& ctx); +} // namespace bongocat::animation + +#endif // BONGOCAT_ANIMATION_BAR_H \ No newline at end of file diff --git a/src/graphics/drawing_images.cpp b/src/graphics/drawing_images.cpp index e85b6688..f435854b 100644 --- a/src/graphics/drawing_images.cpp +++ b/src/graphics/drawing_images.cpp @@ -1,265 +1,487 @@ +#include "graphics/animation_thread_context.h" #include "graphics/drawing.h" -#include "graphics/animation_context.h" + #include #include namespace bongocat::animation { - // ============================================================================= - // GLOBAL STATE AND CONFIGURATION - // ============================================================================= - - static inline constexpr uint8_t THRESHOLD_ALPHA = 120; - static inline constexpr unsigned int FIXED_SHIFT = 16; - static inline constexpr unsigned int FIXED_ONE = (1u << FIXED_SHIFT); - - // ============================================================================= - // DRAWING OPERATIONS MODULE - // ============================================================================= - - /* - static bool drawing_is_pixel_in_bounds(int x, int y, int width, int height) { - return x >= 0 && y >= 0 && x < width && y < height; +// ============================================================================= +// GLOBAL STATE AND CONFIGURATION +// ============================================================================= + +static inline constexpr uint8_t THRESHOLD_ALPHA = 120; +static inline constexpr unsigned int FIXED_SHIFT = 16; +static inline constexpr unsigned int FIXED_ONE = (1u << FIXED_SHIFT); + +// ============================================================================= +// DRAWING OPERATIONS MODULE +// ============================================================================= + +/* +static bool drawing_is_pixel_in_bounds(int x, int y, int width, int height) { + return x >= 0 && y >= 0 && x < width && y < height; +} +*/ + +constexpr static uint8_t apply_invert(uint8_t v, bool invert) { + return v ^ (invert ? 0xFF : 0x00); // branchless invert +} + +static void drawing_copy_pixel_rgba(uint8_t *dest, int dest_channels, int dest_idx, blit_image_color_order_t dest_order, + uint8_t r, uint8_t g, uint8_t b, uint8_t a) { + // Map destination channel indices + const int dr = (dest_order == blit_image_color_order_t::RGBA) ? 0 : 2; + constexpr int dg = 1; + const int db = (dest_order == blit_image_color_order_t::RGBA) ? 2 : 0; + + // Store without branching + if (dest_channels >= 1) { + dest[dest_idx + dr] = r; + } + if (dest_channels >= 2) { + dest[dest_idx + dg] = g; + } + if (dest_channels >= 3) { + dest[dest_idx + db] = b; + } + if (dest_channels >= 4) { + dest[dest_idx + 3] = a; + } +} + +void drawing_copy_pixel(uint8_t *dest, int dest_channels, int dest_idx, const unsigned char *src, int src_channels, + int src_idx, blit_image_color_option_flags_t options, blit_image_color_order_t dest_order, + blit_image_color_order_t src_order) { + if (has_flag(options, blit_image_color_option_flags_t::Invisible)) { + return; + } + const bool invert = has_flag(options, blit_image_color_option_flags_t::Invert); + + // Map source channel indices for RGB + const int sr = (src_order == blit_image_color_order_t::RGBA) ? 0 : 2; + const int sg = 1; + const int sb = (src_order == blit_image_color_order_t::RGBA) ? 2 : 0; + + // Load into RGBA without branches + uint8_t r{0}; + uint8_t g{0}; + uint8_t b{0}; + uint8_t a{0}; + if (src_channels == 1) { + // 1-channel grayscale -> fill all channels with 0/255 + uint8_t v = src[src_idx] > 0 ? 255 : 0; + v = apply_invert(v, invert); + r = g = b = a = v; + } else if (src_channels == 2) { + // 2-channel grayscale + alpha (alpha ignored in original) + const uint8_t gray = apply_invert(src[src_idx], invert); + r = g = b = gray; + a = 255; + } else { + // RGB / RGBA + r = apply_invert(src[src_idx + sr], invert); + g = apply_invert(src[src_idx + sg], invert); + b = apply_invert(src[src_idx + sb], invert); + a = (src_channels >= 4) ? src[src_idx + 3] : 255; // Alpha not inverted + } + + drawing_copy_pixel_rgba(dest, dest_channels, dest_idx, dest_order, r, g, b, a); +} + +void drawing_blend_pixel(uint8_t *dest, int dest_channels, int dest_idx, uint8_t src_r, uint8_t src_g, uint8_t src_b, + uint8_t src_a, int src_channels, blit_image_color_option_flags_t options, + blit_image_color_order_t dest_order, blit_image_color_order_t src_order) { + if (has_flag(options, blit_image_color_option_flags_t::Invisible)) { + return; + } + const bool invert = has_flag(options, blit_image_color_option_flags_t::Invert); + + // Map source channel indices for RGB + const uint8_t sr = (src_order == blit_image_color_order_t::RGBA) ? src_r : src_b; + const uint8_t sg = src_g; + const uint8_t sb = (src_order == blit_image_color_order_t::RGBA) ? src_b : src_r; + const uint8_t sa = src_a; + + // Skip fully transparent pixels + if (sa == 0) { + return; + } + + // Load into RGBA without branches + uint8_t r{0}; + uint8_t g{0}; + uint8_t b{0}; + uint8_t a{0}; + if (src_channels == 1) { + // 1-channel grayscale -> fill all channels with 0/255 + uint8_t v = sr > 0 ? 255 : 0; + v = apply_invert(v, invert); + r = g = b = a = v; + } else if (src_channels == 2) { + // 2-channel grayscale + alpha (alpha ignored in original) + const uint8_t gray = apply_invert(sr, invert); + r = g = b = gray; + a = 255; + } else { + // Fully opaque - direct copy (fast path) + if (src_a == 255) { + // RGB / RGBA + r = apply_invert(sr, invert); + g = apply_invert(sg, invert); + b = apply_invert(sb, invert); + a = 255; // Alpha not inverted + } else { + // Alpha blend: out = src * alpha + dest * (1 - alpha) + const float alpha = static_cast(sa) / 255.0f; + const float inv_alpha = 1.0f - alpha; + + // Map source channel indices for RGB + const uint8_t dr = (dest_order == blit_image_color_order_t::RGBA) ? dest[dest_idx + 0] : dest[dest_idx + 2]; + const uint8_t dg = dest[dest_idx + 1]; + const uint8_t db = (dest_order == blit_image_color_order_t::RGBA) ? dest[dest_idx + 2] : dest[dest_idx + 0]; + // const uint8_t da = dest[dest_idx + 3]; + + r = static_cast(lroundf((sr * alpha) + (dr * inv_alpha))); + g = static_cast(lroundf((sg * alpha) + (dg * inv_alpha))); + b = static_cast(lroundf((sb * alpha) + (db * inv_alpha))); + a = 255; // Alpha not inverted } - */ - - constexpr static uint8_t apply_invert(uint8_t v, bool invert) { - return v ^ (invert ? 0xFF : 0x00); // branchless invert + } + + drawing_copy_pixel_rgba(dest, dest_channels, dest_idx, dest_order, r, g, b, a); +} + +struct drawing_get_pixel_result_t { + unsigned char r{0}; + unsigned char g{0}; + unsigned char b{0}; + unsigned char a{0}; +}; +// Bilinear interpolation for smooth scaling +static drawing_get_pixel_result_t drawing_get_interpolated_pixel(const unsigned char *src, size_t src_size, int src_w, + int src_h, int src_channels, float fx, float fy) { + // Clamp coordinates to image bounds + if (fx < 0) { + fx = 0; + } + if (fy < 0) { + fy = 0; + } + if (fx >= static_cast(src_w - 1)) { + fx = static_cast(src_w - 1); + } + if (fy >= static_cast(src_h - 1)) { + fy = static_cast(src_h - 1); + } + + const int x1 = static_cast(fx); + const int y1 = static_cast(fy); + int x2 = x1 + 1; + int y2 = y1 + 1; + + // Clamp to bounds + if (x2 >= src_w) { + x2 = src_w - 1; + } + if (y2 >= src_h) { + y2 = src_h - 1; + } + + const float dx = fx - static_cast(x1); + const float dy = fy - static_cast(y1); + + // Get the four surrounding pixels + const int idx_tl = (y1 * src_w + x1) * src_channels; // top-left + const int idx_tr = (y1 * src_w + x2) * src_channels; // top-right + const int idx_bl = (y2 * src_w + x1) * src_channels; // bottom-left + const int idx_br = (y2 * src_w + x2) * src_channels; // bottom-right + + // Interpolate each channel + drawing_get_pixel_result_t ret; + for (int c = 0; c < src_channels; c++) { + assert(idx_tl >= 0); + assert(idx_tr >= 0); + assert(idx_bl >= 0); + assert(idx_br >= 0); + const size_t tl_idx_c = static_cast(idx_tl) + static_cast(c); + const size_t tr_idx_c = static_cast(idx_tr) + static_cast(c); + const size_t bl_idx_c = static_cast(idx_bl) + static_cast(c); + const size_t br_idx_c = static_cast(idx_br) + static_cast(c); + + if (tl_idx_c < src_size && tr_idx_c < src_size && bl_idx_c < src_size && br_idx_c < src_size) { + const float top = (static_cast(src[tl_idx_c]) * (1.0f - dx)) + (static_cast(src[tr_idx_c]) * dx); + const float bottom = (static_cast(src[bl_idx_c]) * (1.0f - dx)) + (static_cast(src[br_idx_c]) * dx); + const float result = (top * (1.0f - dy)) + (bottom * dy); + + switch (c) { + case 0: + ret.r = static_cast(lroundf(result)); + break; // R + case 1: + ret.g = static_cast(lroundf(result)); + break; // G + case 2: + ret.b = static_cast(lroundf(result)); + break; // B + case 3: + ret.a = static_cast(lroundf(result)); + break; // A + default: + break; + } } - - static void drawing_copy_pixel_rgba(uint8_t *dest, int dest_channels, int dest_idx, - blit_image_color_order_t dest_order, - uint8_t r, uint8_t g, uint8_t b, uint8_t a) { - // Map destination channel indices - const int dr = (dest_order == blit_image_color_order_t::RGBA) ? 0 : 2; - constexpr int dg = 1; - const int db = (dest_order == blit_image_color_order_t::RGBA) ? 2 : 0; - - // Store without branching - if (dest_channels >= 1) dest[dest_idx + dr] = r; - if (dest_channels >= 2) dest[dest_idx + dg] = g; - if (dest_channels >= 3) dest[dest_idx + db] = b; - if (dest_channels >= 4) dest[dest_idx + 3] = a; + } + return ret; +} + +// Box filter for high-quality downscaling - averages all source pixels that +// map to a destination pixel. Produces much smoother results than bilinear +// when shrinking images significantly. +static drawing_get_pixel_result_t drawing_get_box_filtered_pixel(const unsigned char *src, size_t src_size, int src_w, + int src_h, int src_channels, float fx, float fy) { + // Clamp coordinates to image bounds + if (fx < 0) { + fx = 0; + } + if (fy < 0) { + fy = 0; + } + if (fx >= static_cast(src_w - 1)) { + fx = static_cast(src_w - 1); + } + if (fy >= static_cast(src_h - 1)) { + fy = static_cast(src_h - 1); + } + + const int x1 = static_cast(fx); + const int y1 = static_cast(fy); + int x2 = x1 + 1; + int y2 = y1 + 1; + + // Clamp to bounds + if (x2 >= src_w) { + x2 = src_w - 1; + } + if (y2 >= src_h) { + y2 = src_h - 1; + } + + // Indices of 2×2 block + const int idx_tl = (y1 * src_w + x1) * src_channels; + const int idx_tr = (y1 * src_w + x2) * src_channels; + const int idx_bl = (y2 * src_w + x1) * src_channels; + const int idx_br = (y2 * src_w + x2) * src_channels; + + float sum_r = 0.0f; + float sum_g = 0.0f; + float sum_b = 0.0f; + float sum_a = 0.0f; + int count = 0; + + // Accumulate function (manual inline) + const int base_indices[4] = {idx_tl, idx_tr, idx_bl, idx_br}; + + assert(src_channels >= 0); + for (int i = 0; i < src_channels; i++) { + const int base = base_indices[i]; + if (base < 0) { + continue; } - void drawing_copy_pixel(uint8_t *dest, int dest_channels, int dest_idx, - const unsigned char *src, int src_channels, int src_idx, - blit_image_color_option_flags_t options, - blit_image_color_order_t dest_order, - blit_image_color_order_t src_order) - { - if (has_flag(options, blit_image_color_option_flags_t::Invisible)) return; - const bool invert = has_flag(options, blit_image_color_option_flags_t::Invert); - - // Map source channel indices for RGB - const int sr = (src_order == blit_image_color_order_t::RGBA) ? 0 : 2; - const int sg = 1; - const int sb = (src_order == blit_image_color_order_t::RGBA) ? 2 : 0; - - // Load into RGBA without branches - uint8_t r, g, b, a; - if (src_channels == 1) { - // 1-channel grayscale -> fill all channels with 0/255 - uint8_t v = src[src_idx] ? 255 : 0; - v = apply_invert(v, invert); - r = g = b = a = v; - } - else if (src_channels == 2) { - // 2-channel grayscale + alpha (alpha ignored in original) - const uint8_t gray = apply_invert(src[src_idx], invert); - r = g = b = gray; - a = 255; - } - else { - // RGB / RGBA - r = apply_invert(src[src_idx + sr], invert); - g = apply_invert(src[src_idx + sg], invert); - b = apply_invert(src[src_idx + sb], invert); - a = (src_channels >= 4) ? src[src_idx + 3] : 255; // Alpha not inverted - } - drawing_copy_pixel_rgba(dest, dest_channels, dest_idx, dest_order, r, g, b, a); + for (int c = 0; c < src_channels; c++) { + const size_t idx_c = static_cast(base) + static_cast(c); + if (idx_c >= src_size) { + continue; + } + + const float v = src[idx_c]; + switch (c) { + case 0: + sum_r += v; + break; + case 1: + sum_g += v; + break; + case 2: + sum_b += v; + break; + case 3: + sum_a += v; + break; + } } - - - struct drawing_get_interpolated_pixel_result_t { unsigned char r{0}; unsigned char g{0}; unsigned char b{0}; unsigned char a{0}; }; - // Bilinear interpolation for smooth scaling - static drawing_get_interpolated_pixel_result_t drawing_get_interpolated_pixel(const unsigned char *src, size_t src_size, int src_w, int src_h, int src_channels, float fx, float fy) { - // Clamp coordinates to image bounds - if (fx < 0) fx = 0; - if (fy < 0) fy = 0; - if (fx >= static_cast(src_w - 1)) fx = static_cast(src_w - 1); - if (fy >= static_cast(src_h - 1)) fy = static_cast(src_h - 1); - - int x1 = static_cast(fx); - int y1 = static_cast(fy); - int x2 = x1 + 1; - int y2 = y1 + 1; - - // Clamp to bounds - if (x2 >= src_w) x2 = src_w - 1; - if (y2 >= src_h) y2 = src_h - 1; - - const float dx = fx - static_cast(x1); - const float dy = fy - static_cast(y1); - - // Get the four surrounding pixels - const int idx_tl = (y1 * src_w + x1) * src_channels; // top-left - const int idx_tr = (y1 * src_w + x2) * src_channels; // top-right - const int idx_bl = (y2 * src_w + x1) * src_channels; // bottom-left - const int idx_br = (y2 * src_w + x2) * src_channels; // bottom-right - - // Interpolate each channel - drawing_get_interpolated_pixel_result_t ret; - for (int c = 0; c < src_channels; c++) { - assert(idx_tl >= 0); - assert(idx_tr >= 0); - assert(idx_bl >= 0); - assert(idx_br >= 0); - const size_t tl_idx_c = static_cast(idx_tl) + static_cast(c); - const size_t tr_idx_c = static_cast(idx_tr) + static_cast(c); - const size_t bl_idx_c = static_cast(idx_bl) + static_cast(c); - const size_t br_idx_c = static_cast(idx_br) + static_cast(c); - - if (tl_idx_c < src_size && tr_idx_c < src_size && bl_idx_c < src_size && br_idx_c < src_size) { - float top = static_cast(src[tl_idx_c]) * (1.0f - dx) + static_cast(src[tr_idx_c]) * dx; - float bottom = static_cast(src[bl_idx_c]) * (1.0f - dx) + static_cast(src[br_idx_c]) * dx; - float result = top * (1.0f - dy) + bottom * dy; - - switch (c) { - case 0: ret.r = static_cast(result + 0.5f); break; // R - case 1: ret.g = static_cast(result + 0.5f); break; // G - case 2: ret.b = static_cast(result + 0.5f); break; // B - case 3: ret.a = static_cast(result + 0.5f); break; // A - default: break; - } - } - } - return ret; + count++; + } + + if (count == 0) { + count = 1; + } + + drawing_get_pixel_result_t ret; + // Average + ret.r = static_cast(lroundf(sum_r / static_cast(count))); + ret.g = static_cast(lroundf(sum_g / static_cast(count))); + ret.b = static_cast(lroundf(sum_b / static_cast(count))); + if (src_channels == 4) { + ret.a = static_cast(lroundf(sum_a / static_cast(count))); + } else { + ret.a = 255; + } + return ret; +} + +void blit_image_scaled(uint8_t *dest, size_t dest_size, int dest_w, int dest_h, int dest_channels, + const unsigned char *src, size_t src_size, int src_w, int src_h, int src_channels, int src_x, + int src_y, int frame_w, int frame_h, int offset_x, int offset_y, int target_w, int target_h, + blit_image_color_order_t dest_order, blit_image_color_order_t src_order, + blit_image_color_option_flags_t options) { + if (dest == BONGOCAT_NULLPTR || src == BONGOCAT_NULLPTR) { + return; + } + if (dest_w <= 0 || dest_h <= 0 || src_w <= 0 || src_h <= 0) { + return; + } + if (dest_channels <= 0 || src_channels <= 0) { + return; + } + if (target_w <= 0 || target_h <= 0) { + return; + } + if (frame_w <= 0 || frame_h <= 0) { + return; + } + if (has_flag(options, blit_image_color_option_flags_t::Invisible)) { + return; + } + + assert(dest_w >= 0); + assert(dest_h >= 0); + assert(dest_channels >= 0); + assert(src_w >= 0); + assert(src_h >= 0); + assert(src_channels >= 0); + // Verify buffers are large enough + const size_t needed_dest = + static_cast(dest_w) * static_cast(dest_h) * static_cast(dest_channels); + const size_t needed_src = static_cast(src_w) * static_cast(src_h) * static_cast(src_channels); + if (dest_size < needed_dest || src_size < needed_src) { + return; + } + + // Clip destination rectangle + const int dst_left = offset_x; + const int dst_top = offset_y; + const int dst_right = offset_x + target_w; + const int dst_bottom = offset_y + target_h; + + int x0 = 0; + int x1 = target_w; + if (dst_left < 0) { + x0 = -dst_left; + } + if (dst_right > dest_w) { + x1 = target_w - (dst_right - dest_w); + } + if (x0 >= x1) { + return; + } + + int y0 = 0; + int y1 = target_h; + if (dst_top < 0) { + y0 = -dst_top; + } + if (dst_bottom > dest_h) { + y1 = target_h - (dst_bottom - dest_h); + } + if (y0 >= y1) { + return; + } + + // Fixed-point increments + assert(target_w > 0); + assert(target_h > 0); + int32_t inc_x = static_cast((static_cast(frame_w) << FIXED_SHIFT) / target_w); + int32_t inc_y = static_cast((static_cast(frame_h) << FIXED_SHIFT) / target_h); + + int32_t src_x_start = src_x << FIXED_SHIFT; + int32_t src_y_start = src_y << FIXED_SHIFT; + + const bool use_bilinear_interpolation = has_flag(options, blit_image_color_option_flags_t::BilinearInterpolation); + const bool mirror_x = has_flag(options, blit_image_color_option_flags_t::MirrorX); + const bool mirror_y = has_flag(options, blit_image_color_option_flags_t::MirrorY); + const bool disable_threshold_alpha = + use_bilinear_interpolation || has_flag(options, blit_image_color_option_flags_t::DisableThresholdAlpha); + + // MirrorX / MirrorY affect direction and start point + if (mirror_x) { + src_x_start = (src_x + frame_w - 1) << FIXED_SHIFT; + inc_x = -inc_x; + } + if (mirror_y) { + src_y_start = (src_y + frame_h - 1) << FIXED_SHIFT; + inc_y = -inc_y; + } + + const size_t src_row_bytes = static_cast(src_w) * static_cast(src_channels); + const size_t dest_row_bytes = static_cast(dest_w) * static_cast(dest_channels); + + const bool is_downscaling = (target_w < src_w) || (target_h < src_h); + + for (int ty = y0; ty < y1; ++ty) { + const int dy = offset_y + ty; + assert(dy < dest_h); + + const int32_t sy_fixed = src_y_start + static_cast(static_cast(ty) * inc_y); + const int sy = sy_fixed >> FIXED_SHIFT; + + if (static_cast(sy) >= static_cast(src_h)) { + continue; } - void blit_image_scaled(uint8_t *dest, size_t dest_size, int dest_w, int dest_h, int dest_channels, - const unsigned char *src, size_t src_size, int src_w, int src_h, int src_channels, - int src_x, int src_y, - int frame_w, int frame_h, - int offset_x, int offset_y, int target_w, int target_h, - blit_image_color_order_t dest_order, - blit_image_color_order_t src_order, - blit_image_color_option_flags_t options) - { - if (!dest || !src) return; - if (dest_w <= 0 || dest_h <= 0 || src_w <= 0 || src_h <= 0) return; - if (dest_channels <= 0 || src_channels <= 0) return; - if (target_w <= 0 || target_h <= 0) return; - if (frame_w <= 0 || frame_h <= 0) return; - if (has_flag(options, blit_image_color_option_flags_t::Invisible)) return; - - assert(dest_w >= 0); - assert(dest_h >= 0); - assert(dest_channels >= 0); - assert(src_w >= 0); - assert(src_h >= 0); - assert(src_channels >= 0); - // Verify buffers are large enough - const size_t needed_dest = static_cast(dest_w) * static_cast(dest_h) * static_cast(dest_channels); - const size_t needed_src = static_cast(src_w) * static_cast(src_h) * static_cast(src_channels); - if (dest_size < needed_dest || src_size < needed_src) return; - - // Clip destination rectangle - const int dst_left = offset_x; - const int dst_top = offset_y; - const int dst_right = offset_x + target_w; - const int dst_bottom = offset_y + target_h; - - int x0 = 0; - int x1 = target_w; - if (dst_left < 0) x0 = -dst_left; - if (dst_right > dest_w) x1 = target_w - (dst_right - dest_w); - if (x0 >= x1) return; - - int y0 = 0; - int y1 = target_h; - if (dst_top < 0) y0 = -dst_top; - if (dst_bottom > dest_h) y1 = target_h - (dst_bottom - dest_h); - if (y0 >= y1) return; - - // Fixed-point increments - assert(target_w > 0); - assert(target_h > 0); - int32_t inc_x = static_cast((static_cast(frame_w) << FIXED_SHIFT) / target_w); - int32_t inc_y = static_cast((static_cast(frame_h) << FIXED_SHIFT) / target_h); - - int32_t src_x_start = src_x << FIXED_SHIFT; - int32_t src_y_start = src_y << FIXED_SHIFT; - - // MirrorX / MirrorY affect direction and start point - if (has_flag(options, blit_image_color_option_flags_t::MirrorX)) { - src_x_start = (src_x + frame_w - 1) << FIXED_SHIFT; - inc_x = -inc_x; - } - if (has_flag(options, blit_image_color_option_flags_t::MirrorY)) { - src_y_start = (src_y + frame_h - 1) << FIXED_SHIFT; - inc_y = -inc_y; - } - - const size_t src_row_bytes = static_cast(src_w) * static_cast(src_channels); - const size_t dest_row_bytes = static_cast(dest_w) * static_cast(dest_channels); - - const bool use_bilinear_interpolation = has_flag(options, blit_image_color_option_flags_t::BilinearInterpolation); - - for (int ty = y0; ty < y1; ++ty) { - const int dy = offset_y + ty; - assert(dy < dest_h); - - int32_t sy_fixed = src_y_start + static_cast(static_cast(ty) * inc_y); - const int sy = sy_fixed >> FIXED_SHIFT; - - if (static_cast(sy) >= static_cast(src_h)) continue; - - uint8_t *dest_row = dest + static_cast(dy) * dest_row_bytes; - const unsigned char *src_row = src + static_cast(sy) * src_row_bytes; - - int32_t sx_fixed = src_x_start + static_cast(static_cast(x0) * inc_x); - uint8_t *dest_ptr = dest_row + static_cast(offset_x + x0) * static_cast(dest_channels); - - for (int tx = x0; tx < x1; ++tx) { - const int sx = sx_fixed >> FIXED_SHIFT; - - if (static_cast(sx) < static_cast(src_w)) { - const uint8_t *src_pixel = src_row + static_cast(sx) * static_cast(src_channels); - int dest_idx = static_cast(dest_ptr - dest); - int src_idx = static_cast(src_pixel - src); - - if (use_bilinear_interpolation) { - // Use bilinear interpolation for smooth scaling - float fx = static_cast(sx_fixed) / static_cast(1 << FIXED_SHIFT); - float fy = static_cast(sy_fixed) / static_cast(1 << FIXED_SHIFT); - - auto pixel = drawing_get_interpolated_pixel(src, src_size, src_w, src_h, src_channels, fx, fy); - if (src_channels >= 4) { - if (src_pixel[3] > THRESHOLD_ALPHA) { - drawing_copy_pixel_rgba(dest, dest_channels, dest_idx, dest_order, pixel.r, pixel.g, pixel.b, pixel.a); - } - } else { - drawing_copy_pixel_rgba(dest, dest_channels, dest_idx, dest_order, pixel.r, pixel.g, pixel.b, 255); - } - } else { - // Use nearest-neighbor scaling (original behavior) - if (src_channels >= 4) { - if (src_pixel[3] > THRESHOLD_ALPHA) { - drawing_copy_pixel(dest, dest_channels, dest_idx, - src, src_channels, src_idx, - options, dest_order, src_order); - } - } else { - drawing_copy_pixel(dest, dest_channels, dest_idx, - src, src_channels, src_idx, - options, dest_order, src_order); - } - } - } - - sx_fixed += inc_x; - dest_ptr += dest_channels; + const uint8_t *dest_row = dest + (static_cast(dy) * dest_row_bytes); + const unsigned char *src_row = src + (static_cast(sy) * src_row_bytes); + + int32_t sx_fixed = src_x_start + static_cast(static_cast(x0) * inc_x); + const uint8_t *dest_ptr = dest_row + (static_cast(offset_x + x0) * static_cast(dest_channels)); + + for (int tx = x0; tx < x1; ++tx) { + // const int dx = offset_x + tx; + // assert(dx < dest_w); + const int sx = sx_fixed >> FIXED_SHIFT; + + if (static_cast(sx) < static_cast(src_w)) { + const uint8_t *src_pixel = src_row + (static_cast(sx) * static_cast(src_channels)); + const int dest_idx = static_cast(dest_ptr - dest); + const int src_idx = static_cast(src_pixel - src); + + if (use_bilinear_interpolation) { + // Use bilinear interpolation for smooth scaling + const float fx = static_cast(sx_fixed) / static_cast(1 << FIXED_SHIFT); + const float fy = static_cast(sy_fixed) / static_cast(1 << FIXED_SHIFT); + + const drawing_get_pixel_result_t pixel = + (is_downscaling) ? drawing_get_box_filtered_pixel(src, src_size, src_w, src_h, src_channels, fx, fy) + : drawing_get_interpolated_pixel(src, src_size, src_w, src_h, src_channels, fx, fy); + drawing_blend_pixel(dest, dest_channels, dest_idx, pixel.r, pixel.g, pixel.b, pixel.a, 4, options, dest_order, + blit_image_color_order_t::RGBA); + } else { + // Use nearest-neighbor scaling (original behavior) + if (src_channels >= 4) { + if (disable_threshold_alpha || src_pixel[3] > THRESHOLD_ALPHA) { + drawing_copy_pixel(dest, dest_channels, dest_idx, src, src_channels, src_idx, options, dest_order, + src_order); } + } else { + drawing_copy_pixel(dest, dest_channels, dest_idx, src, src_channels, src_idx, options, dest_order, + src_order); + } } + } + + sx_fixed += inc_x; + dest_ptr += dest_channels; } -} \ No newline at end of file + } +} +} // namespace bongocat::animation \ No newline at end of file diff --git a/src/image_loader/.clang-format b/src/image_loader/.clang-format new file mode 100644 index 00000000..15f9b6fc --- /dev/null +++ b/src/image_loader/.clang-format @@ -0,0 +1,2 @@ +DisableFormat: true +SortIncludes: Never \ No newline at end of file diff --git a/src/image_loader/CMakeLists.txt b/src/image_loader/CMakeLists.txt index 4ae91eea..3b829a7f 100644 --- a/src/image_loader/CMakeLists.txt +++ b/src/image_loader/CMakeLists.txt @@ -1,57 +1,53 @@ # stb_image lib add_library(assets_image_loader STATIC) -target_include_directories(assets_image_loader - PRIVATE ${INCLUDE_DIR}/image_loader - PUBLIC ${INCLUDE_DIR}) +target_include_directories( + assets_image_loader + PRIVATE ${INCLUDE_DIR}/image_loader + PUBLIC ${INCLUDE_DIR}) target_include_directories(assets_image_loader SYSTEM PRIVATE ${PROJECT_SOURCE_DIR}/lib) target_link_libraries(assets_image_loader PRIVATE bongocat_options) -if (FEATURE_USE_HYBRID_IMAGE_BACKEND) +if(FEATURE_USE_HYBRID_IMAGE_BACKEND) + add_library(pngle STATIC) + target_include_directories(pngle SYSTEM PRIVATE ${PROJECT_SOURCE_DIR}/lib) + target_sources(pngle PRIVATE pngle.c miniz.c) + target_compile_definitions(pngle PUBLIC PNGLE_NO_GAMMA_CORRECTION) + + add_library(stb_image STATIC) + target_include_directories(stb_image SYSTEM PRIVATE ${PROJECT_SOURCE_DIR}/lib) + target_sources(stb_image PRIVATE stb_image.c) + target_compile_definitions(stb_image PUBLIC STBI_NO_STDIO STBI_ONLY_PNG) + target_compile_definitions(stb_image PUBLIC $<$:STBI_NO_LINEAR>) + + target_sources(assets_image_loader PRIVATE load_images.cpp load_images_hybrid.cpp) + target_link_libraries(assets_image_loader PRIVATE pngle stb_image) + target_compile_definitions(assets_image_loader PUBLIC FEATURE_USE_HYBRID_IMAGE_BACKEND) + message(STATUS "Use hybrid image backend (stb_image + pngle)") +else() + if(FEATURE_USE_PNGLE) add_library(pngle STATIC) target_include_directories(pngle SYSTEM PRIVATE ${PROJECT_SOURCE_DIR}/lib) target_sources(pngle PRIVATE pngle.c miniz.c) target_compile_definitions(pngle PUBLIC PNGLE_NO_GAMMA_CORRECTION) + target_sources(assets_image_loader PRIVATE load_images.cpp load_images_pngle.cpp) + target_link_libraries(assets_image_loader PRIVATE pngle) + target_compile_definitions(assets_image_loader PUBLIC FEATURE_USE_PNGLE) + message(STATUS "Use pngle image backend") + else() add_library(stb_image STATIC) target_include_directories(stb_image SYSTEM PRIVATE ${PROJECT_SOURCE_DIR}/lib) target_sources(stb_image PRIVATE stb_image.c) target_compile_definitions(stb_image PUBLIC STBI_NO_STDIO STBI_ONLY_PNG) - target_compile_definitions(stb_image PUBLIC - $<$:STBI_NO_LINEAR> - ) + target_compile_definitions(stb_image PUBLIC $<$:STBI_NO_LINEAR>) - target_sources(assets_image_loader PRIVATE load_images.cpp load_images_hybrid.cpp) - target_link_libraries(assets_image_loader PRIVATE pngle stb_image) - target_compile_definitions(assets_image_loader PUBLIC FEATURE_USE_HYBRID_IMAGE_BACKEND) - message(STATUS "Use hybrid image backend (stb_image + pngle)") -else() - if (FEATURE_USE_PNGLE) - add_library(pngle STATIC) - target_include_directories(pngle SYSTEM PRIVATE ${PROJECT_SOURCE_DIR}/lib) - target_sources(pngle PRIVATE pngle.c miniz.c) - target_compile_definitions(pngle PUBLIC PNGLE_NO_GAMMA_CORRECTION) - - target_sources(assets_image_loader PRIVATE load_images.cpp load_images_pngle.cpp) - target_link_libraries(assets_image_loader PRIVATE pngle) - target_compile_definitions(assets_image_loader PUBLIC FEATURE_USE_PNGLE) - message(STATUS "Use pngle image backend") - else() - add_library(stb_image STATIC) - target_include_directories(stb_image SYSTEM PRIVATE ${PROJECT_SOURCE_DIR}/lib) - target_sources(stb_image PRIVATE stb_image.c) - target_compile_definitions(stb_image PUBLIC STBI_NO_STDIO STBI_ONLY_PNG) - target_compile_definitions(stb_image PUBLIC - $<$:STBI_NO_LINEAR> - ) - - target_sources(assets_image_loader PRIVATE load_images.cpp load_images_stb_image.cpp) - target_link_libraries(assets_image_loader PRIVATE stb_image) - target_compile_definitions(assets_image_loader PUBLIC FEATURE_USE_STB_IMAGE) - message(STATUS "Use stb_image backend") - endif() + target_sources(assets_image_loader PRIVATE load_images.cpp load_images_stb_image.cpp) + target_link_libraries(assets_image_loader PRIVATE stb_image) + target_compile_definitions(assets_image_loader PUBLIC FEATURE_USE_STB_IMAGE) + message(STATUS "Use stb_image backend") + endif() endif() - # @NOTE(assets): 3. add image_loader sub directory add_subdirectory(bongocat) add_subdirectory(base_dm) @@ -69,5 +65,5 @@ add_subdirectory(custom) add_subdirectory(misc) add_subdirectory(pmd) -# @NOTE(assets): 3.1. add image_loader in include/image_loader (see other load_images_...h as reference) -# @NOTE(assets): 3.2. add image_loader in src/image_loader/xxx (see other load_images_...cpp as reference) \ No newline at end of file +# @NOTE(assets): 3.1. add image_loader in include/image_loader (see other load_images_...h as reference) @NOTE(assets): 3.2. add +# image_loader in src/image_loader/xxx (see other load_images_...cpp as reference) diff --git a/src/image_loader/base_dm/CMakeLists.txt b/src/image_loader/base_dm/CMakeLists.txt index 71be2290..8670ab22 100644 --- a/src/image_loader/base_dm/CMakeLists.txt +++ b/src/image_loader/base_dm/CMakeLists.txt @@ -1,5 +1,11 @@ add_library(assets_base_dm_loader STATIC) target_sources(assets_base_dm_loader PRIVATE load_dm.cpp) target_compile_options(assets_base_dm_loader PRIVATE -ffunction-sections -fdata-sections) -target_include_directories(assets_base_dm_loader PRIVATE ${INCLUDE_DIR}/image_loader/base_dm PUBLIC ${INCLUDE_DIR}) -target_link_libraries(assets_base_dm_loader PUBLIC assets_image_loader PRIVATE bongocat_options) \ No newline at end of file +target_include_directories( + assets_base_dm_loader + PRIVATE ${INCLUDE_DIR}/image_loader/base_dm + PUBLIC ${INCLUDE_DIR}) +target_link_libraries( + assets_base_dm_loader + PUBLIC assets_image_loader + PRIVATE bongocat_options) diff --git a/src/image_loader/base_dm/load_dm.cpp b/src/image_loader/base_dm/load_dm.cpp index 6ac1e5c3..22f556f3 100644 --- a/src/image_loader/base_dm/load_dm.cpp +++ b/src/image_loader/base_dm/load_dm.cpp @@ -1,149 +1,154 @@ #include "image_loader/base_dm/load_dm.h" -#include "graphics/animation_context.h" + +#include "graphics/animation_thread_context.h" #include "image_loader/load_images.h" namespace bongocat::animation { - created_result_t load_dm_anim(const animation_context_t& ctx, [[maybe_unused]] int anim_index, const assets::embedded_image_t& sprite_sheet_image, int sprite_sheet_cols, int sprite_sheet_rows) { - using namespace assets; - BONGOCAT_CHECK_NULL(ctx._local_copy_config.ptr, bongocat_error_t::BONGOCAT_ERROR_INVALID_PARAM); - - auto result = load_sprite_sheet_anim(*ctx._local_copy_config, sprite_sheet_image, sprite_sheet_cols, sprite_sheet_rows); - if (result.error != bongocat_error_t::BONGOCAT_SUCCESS) [[unlikely]] { - BONGOCAT_LOG_ERROR("Load dm Animation failed: %s, index: %d", sprite_sheet_image.name, anim_index); - return bongocat_error_t::BONGOCAT_ERROR_ANIMATION; - } - assert(result.result.total_frames > 0); ///< this SHOULD always work if it's an valid EMBEDDED image - assert(MAX_NUM_FRAMES <= INT_MAX); - assert(result.result.total_frames <= static_cast(MAX_NUM_FRAMES)); - if (result.result.total_frames > static_cast(MAX_NUM_FRAMES)) { - BONGOCAT_LOG_ERROR("Sprite Sheet does not fit in out_frames: %d, total_frames: %d", MAX_NUM_FRAMES, result.result.total_frames); - return bongocat_error_t::BONGOCAT_ERROR_INVALID_PARAM; - } - - dm_sprite_sheet_t ret; - ret.image = bongocat::move(result.result.image); - ret.frame_width = bongocat::move(result.result.frame_width); - ret.frame_height = bongocat::move(result.result.frame_height); - ret.total_frames = bongocat::move(result.result.total_frames); - - ret.frames.idle_1 = bongocat::move(result.result.frames[0]); - ret.frames.idle_2 = bongocat::move(result.result.frames[1]); - ret.frames.angry = bongocat::move(result.result.frames[2]); - ret.frames.down = bongocat::move(result.result.frames[3]); - ret.frames.happy = bongocat::move(result.result.frames[4]); - ret.frames.eat_1 = bongocat::move(result.result.frames[5]); - ret.frames.sleep = bongocat::move(result.result.frames[6]); - ret.frames.refuse = bongocat::move(result.result.frames[7]); - ret.frames.sad = bongocat::move(result.result.frames[8]); - - ret.frames.lose_1 = bongocat::move(result.result.frames[9]); - ret.frames.eat_2 = bongocat::move(result.result.frames[10]); - ret.frames.lose_2 = bongocat::move(result.result.frames[11]); - ret.frames.attack_1 = bongocat::move(result.result.frames[12]); - - ret.frames.movement_1 = bongocat::move(result.result.frames[13]); - ret.frames.movement_2 = bongocat::move(result.result.frames[14]); - - // setup animations - using namespace assets; - - // minimal frames existing - assert(ret.frames.idle_1.valid); - assert(ret.frames.idle_2.valid); - assert(ret.frames.angry.valid); - assert(ret.frames.down.valid); - - assert(MAX_ANIMATION_FRAMES >= 4); - ret.animations.idle[0] = ret.frames.idle_1.col; - ret.animations.idle[1] = ret.frames.idle_2.col; - ret.animations.idle[2] = ret.frames.idle_1.col; - ret.animations.idle[3] = ret.frames.idle_2.col; - - ret.animations.boring[0] = ret.frames.sad.col ? ret.frames.sad.col : ret.frames.idle_1.col; - ret.animations.boring[1] = ret.frames.lose_1.col ? ret.frames.lose_1.col : ret.frames.idle_2.col; - ret.animations.boring[2] = ret.frames.lose_2.col ? ret.frames.lose_2.col : ret.frames.idle_1.col; - ret.animations.boring[3] = ret.frames.idle_2.col; - - ret.animations.writing[0] = ret.frames.idle_2.col; - ret.animations.writing[1] = ret.frames.idle_1.col; - ret.animations.writing[2] = ret.frames.idle_2.col; - ret.animations.writing[3] = ret.frames.idle_1.col; - - // sleep animation - if (ret.frames.sleep.valid) { - ret.animations.sleep[0] = ret.frames.sleep.col; - ret.animations.sleep[1] = ret.frames.sleep.col; - ret.animations.sleep[2] = ret.frames.sleep.col; - ret.animations.sleep[3] = ret.frames.sleep.col; - } else if (ret.frames.down.valid) { - ret.animations.sleep[0] = ret.frames.down.col; - ret.animations.sleep[1] = ret.frames.down.col; - ret.animations.sleep[2] = ret.frames.down.col; - ret.animations.sleep[3] = ret.frames.down.col; - } else { - // fallback - ret.animations.sleep[0] = ret.frames.idle_2.col; - ret.animations.sleep[1] = ret.frames.idle_2.col; - ret.animations.sleep[2] = ret.frames.idle_2.col; - ret.animations.sleep[3] = ret.frames.idle_2.col; - } - - ret.animations.wake_up[0] = ret.frames.idle_1.col; - ret.animations.wake_up[1] = ret.frames.idle_2.col; - ret.animations.wake_up[2] = ret.frames.idle_1.col; - ret.animations.wake_up[3] = ret.frames.idle_2.col; - - // working/attack animation - ret.animations.working[0] = ret.frames.idle_1.col; - ret.animations.working[1] = ret.frames.idle_2.col; - ret.animations.working[2] = ret.frames.angry.valid ? ret.frames.angry.col : ret.frames.idle_1.col; - ret.animations.working[3] = ret.frames.idle_2.col; - ret.animations.working[2] = ret.frames.attack_1.valid ? ret.frames.attack_1.col : ret.animations.working[2]; - ret.animations.working[3] = ret.frames.attack_2.valid ? ret.frames.attack_2.col : ret.animations.working[3]; - - // moving/walking animation - if (ret.frames.movement_1.valid || ret.frames.movement_2.valid) { - ret.animations.moving[0] = ret.frames.movement_1.valid ? ret.frames.movement_1.col : ret.frames.idle_1.col; - ret.animations.moving[1] = ret.frames.movement_1.valid ? ret.frames.movement_1.col : ret.frames.idle_1.col; - ret.animations.moving[2] = ret.frames.movement_2.valid ? ret.frames.movement_2.col : ret.frames.idle_2.col; - ret.animations.moving[3] = ret.frames.movement_2.valid ? ret.frames.movement_2.col : ret.frames.idle_2.col; - } else { - // fallback - ret.animations.moving[0] = ret.frames.idle_1.col; - ret.animations.moving[1] = ret.frames.idle_1.col; - ret.animations.moving[2] = ret.frames.idle_2.col; - ret.animations.moving[3] = ret.frames.idle_2.col; - } - // running animation (same as moving) - if (ret.frames.movement_1.valid || ret.frames.movement_2.valid) { - ret.animations.running[0] = ret.frames.movement_1.valid ? ret.frames.movement_1.col : ret.frames.idle_1.col; - ret.animations.running[1] = ret.frames.movement_1.valid ? ret.frames.movement_1.col : ret.frames.idle_1.col; - ret.animations.running[2] = ret.frames.movement_2.valid ? ret.frames.movement_2.col : ret.frames.idle_2.col; - ret.animations.running[3] = ret.frames.movement_2.valid ? ret.frames.movement_2.col : ret.frames.idle_2.col; - } else { - // fallback - ret.animations.running[0] = ret.frames.idle_1.col; - ret.animations.running[1] = ret.frames.idle_1.col; - ret.animations.running[2] = ret.frames.idle_2.col; - ret.animations.running[3] = ret.frames.idle_2.col; - } - - // happy animation - if (ret.frames.happy.valid) { - ret.animations.happy[0] = ret.frames.happy.valid ? ret.frames.happy.col : ret.frames.idle_2.col; - ret.animations.happy[1] = ret.frames.idle_1.col; - ret.animations.happy[2] = ret.frames.happy.valid ? ret.frames.happy.col : ret.frames.idle_2.col; - ret.animations.happy[3] = ret.frames.idle_1.col; - } else { - // fallback - ret.animations.happy[0] = ret.frames.idle_1.col; - ret.animations.happy[1] = ret.frames.idle_2.col; - ret.animations.happy[2] = ret.frames.idle_1.col; - ret.animations.happy[3] = ret.frames.idle_2.col; - } - - return ret; - } - +created_result_t load_dm_anim(const animation_thread_context_t& ctx, [[maybe_unused]] int anim_index, + const assets::embedded_image_t& sprite_sheet_image, + int sprite_sheet_cols, int sprite_sheet_rows) { + using namespace assets; + BONGOCAT_CHECK_NULL(ctx._local_copy_config.ptr, bongocat_error_t::BONGOCAT_ERROR_INVALID_PARAM); + + auto result = + load_sprite_sheet_anim(*ctx._local_copy_config, sprite_sheet_image, sprite_sheet_cols, sprite_sheet_rows); + if (result.error != bongocat_error_t::BONGOCAT_SUCCESS) [[unlikely]] { + BONGOCAT_LOG_ERROR("Load dm Animation failed: %s, index: %d", sprite_sheet_image.name, anim_index); + return bongocat_error_t::BONGOCAT_ERROR_ANIMATION; + } + assert(result.result.total_frames > 0); ///< this SHOULD always work if it's an valid EMBEDDED image + assert(MAX_NUM_FRAMES <= INT_MAX); + assert(result.result.total_frames <= static_cast(MAX_NUM_FRAMES)); + if (result.result.total_frames > static_cast(MAX_NUM_FRAMES)) { + BONGOCAT_LOG_ERROR("Sprite Sheet does not fit in out_frames: %d, total_frames: %d", MAX_NUM_FRAMES, + result.result.total_frames); + return bongocat_error_t::BONGOCAT_ERROR_INVALID_PARAM; + } + + dm_sprite_sheet_t ret; + ret.image = bongocat::move(result.result.image); + ret.frame_width = bongocat::move(result.result.frame_width); + ret.frame_height = bongocat::move(result.result.frame_height); + ret.total_frames = bongocat::move(result.result.total_frames); + + ret.frames.idle_1 = bongocat::move(result.result.frames[0]); + ret.frames.idle_2 = bongocat::move(result.result.frames[1]); + ret.frames.angry = bongocat::move(result.result.frames[2]); + ret.frames.down = bongocat::move(result.result.frames[3]); + ret.frames.happy = bongocat::move(result.result.frames[4]); + ret.frames.eat_1 = bongocat::move(result.result.frames[5]); + ret.frames.sleep = bongocat::move(result.result.frames[6]); + ret.frames.refuse = bongocat::move(result.result.frames[7]); + ret.frames.sad = bongocat::move(result.result.frames[8]); + + ret.frames.lose_1 = bongocat::move(result.result.frames[9]); + ret.frames.eat_2 = bongocat::move(result.result.frames[10]); + ret.frames.lose_2 = bongocat::move(result.result.frames[11]); + ret.frames.attack_1 = bongocat::move(result.result.frames[12]); + + ret.frames.movement_1 = bongocat::move(result.result.frames[13]); + ret.frames.movement_2 = bongocat::move(result.result.frames[14]); + + // setup animations + using namespace assets; + + // minimal frames existing + assert(ret.frames.idle_1.valid); + assert(ret.frames.idle_2.valid); + assert(ret.frames.angry.valid); + assert(ret.frames.down.valid); + + assert(MAX_ANIMATION_FRAMES >= 4); + ret.animations.idle[0] = ret.frames.idle_1.col; + ret.animations.idle[1] = ret.frames.idle_2.col; + ret.animations.idle[2] = ret.frames.idle_1.col; + ret.animations.idle[3] = ret.frames.idle_2.col; + + ret.animations.boring[0] = ret.frames.sad.col ? ret.frames.sad.col : ret.frames.idle_1.col; + ret.animations.boring[1] = ret.frames.lose_1.col ? ret.frames.lose_1.col : ret.frames.idle_2.col; + ret.animations.boring[2] = ret.frames.lose_2.col ? ret.frames.lose_2.col : ret.frames.idle_1.col; + ret.animations.boring[3] = ret.frames.idle_2.col; + + ret.animations.writing[0] = ret.frames.idle_2.col; + ret.animations.writing[1] = ret.frames.idle_1.col; + ret.animations.writing[2] = ret.frames.idle_2.col; + ret.animations.writing[3] = ret.frames.idle_1.col; + + // sleep animation + if (ret.frames.sleep.valid) { + ret.animations.sleep[0] = ret.frames.sleep.col; + ret.animations.sleep[1] = ret.frames.sleep.col; + ret.animations.sleep[2] = ret.frames.sleep.col; + ret.animations.sleep[3] = ret.frames.sleep.col; + } else if (ret.frames.down.valid) { + ret.animations.sleep[0] = ret.frames.down.col; + ret.animations.sleep[1] = ret.frames.down.col; + ret.animations.sleep[2] = ret.frames.down.col; + ret.animations.sleep[3] = ret.frames.down.col; + } else { + // fallback + ret.animations.sleep[0] = ret.frames.idle_2.col; + ret.animations.sleep[1] = ret.frames.idle_2.col; + ret.animations.sleep[2] = ret.frames.idle_2.col; + ret.animations.sleep[3] = ret.frames.idle_2.col; + } + + ret.animations.wake_up[0] = ret.frames.idle_1.col; + ret.animations.wake_up[1] = ret.frames.idle_2.col; + ret.animations.wake_up[2] = ret.frames.idle_1.col; + ret.animations.wake_up[3] = ret.frames.idle_2.col; + + // working/attack animation + ret.animations.working[0] = ret.frames.idle_1.col; + ret.animations.working[1] = ret.frames.idle_2.col; + ret.animations.working[2] = ret.frames.angry.valid ? ret.frames.angry.col : ret.frames.idle_1.col; + ret.animations.working[3] = ret.frames.idle_2.col; + ret.animations.working[2] = ret.frames.attack_1.valid ? ret.frames.attack_1.col : ret.animations.working[2]; + ret.animations.working[3] = ret.frames.attack_2.valid ? ret.frames.attack_2.col : ret.animations.working[3]; + + // moving/walking animation + if (ret.frames.movement_1.valid || ret.frames.movement_2.valid) { + ret.animations.moving[0] = ret.frames.movement_1.valid ? ret.frames.movement_1.col : ret.frames.idle_1.col; + ret.animations.moving[1] = ret.frames.movement_1.valid ? ret.frames.movement_1.col : ret.frames.idle_1.col; + ret.animations.moving[2] = ret.frames.movement_2.valid ? ret.frames.movement_2.col : ret.frames.idle_2.col; + ret.animations.moving[3] = ret.frames.movement_2.valid ? ret.frames.movement_2.col : ret.frames.idle_2.col; + } else { + // fallback + ret.animations.moving[0] = ret.frames.idle_1.col; + ret.animations.moving[1] = ret.frames.idle_1.col; + ret.animations.moving[2] = ret.frames.idle_2.col; + ret.animations.moving[3] = ret.frames.idle_2.col; + } + // running animation (same as moving) + if (ret.frames.movement_1.valid || ret.frames.movement_2.valid) { + ret.animations.running[0] = ret.frames.movement_1.valid ? ret.frames.movement_1.col : ret.frames.idle_1.col; + ret.animations.running[1] = ret.frames.movement_1.valid ? ret.frames.movement_1.col : ret.frames.idle_1.col; + ret.animations.running[2] = ret.frames.movement_2.valid ? ret.frames.movement_2.col : ret.frames.idle_2.col; + ret.animations.running[3] = ret.frames.movement_2.valid ? ret.frames.movement_2.col : ret.frames.idle_2.col; + } else { + // fallback + ret.animations.running[0] = ret.frames.idle_1.col; + ret.animations.running[1] = ret.frames.idle_1.col; + ret.animations.running[2] = ret.frames.idle_2.col; + ret.animations.running[3] = ret.frames.idle_2.col; + } + + // happy animation + if (ret.frames.happy.valid) { + ret.animations.happy[0] = ret.frames.happy.valid ? ret.frames.happy.col : ret.frames.idle_2.col; + ret.animations.happy[1] = ret.frames.idle_1.col; + ret.animations.happy[2] = ret.frames.happy.valid ? ret.frames.happy.col : ret.frames.idle_2.col; + ret.animations.happy[3] = ret.frames.idle_1.col; + } else { + // fallback + ret.animations.happy[0] = ret.frames.idle_1.col; + ret.animations.happy[1] = ret.frames.idle_2.col; + ret.animations.happy[2] = ret.frames.idle_1.col; + ret.animations.happy[3] = ret.frames.idle_2.col; + } + + return ret; } + +} // namespace bongocat::animation \ No newline at end of file diff --git a/src/image_loader/bongocat/CMakeLists.txt b/src/image_loader/bongocat/CMakeLists.txt index 40b1af9e..29256a3f 100644 --- a/src/image_loader/bongocat/CMakeLists.txt +++ b/src/image_loader/bongocat/CMakeLists.txt @@ -1,9 +1,11 @@ add_library(assets_bongocat_loader STATIC) target_sources(assets_bongocat_loader PRIVATE load_images_bongocat.cpp) target_compile_options(assets_bongocat_loader PRIVATE -ffunction-sections -fdata-sections) -target_include_directories(assets_bongocat_loader - PRIVATE ${INCLUDE_DIR}/embedded_assets/bongocat ${INCLUDE_DIR}/image_loader/bongocat - PUBLIC ${INCLUDE_DIR}) -target_link_libraries(assets_bongocat_loader - PUBLIC assets_image_loader assets_bongocat - PRIVATE assets_bongocat_interface assets_bongocat_feature bongocat_options) \ No newline at end of file +target_include_directories( + assets_bongocat_loader + PRIVATE ${INCLUDE_DIR}/embedded_assets/bongocat ${INCLUDE_DIR}/image_loader/bongocat + PUBLIC ${INCLUDE_DIR}) +target_link_libraries( + assets_bongocat_loader + PUBLIC assets_image_loader assets_bongocat + PRIVATE assets_bongocat_interface assets_bongocat_feature bongocat_options) diff --git a/src/image_loader/bongocat/load_images_bongocat.cpp b/src/image_loader/bongocat/load_images_bongocat.cpp index bf5c764f..1f82c8c7 100644 --- a/src/image_loader/bongocat/load_images_bongocat.cpp +++ b/src/image_loader/bongocat/load_images_bongocat.cpp @@ -1,122 +1,138 @@ -#include "graphics/drawing.h" -#include "graphics/animation_context.h" +#include "embedded_assets/bongocat/bongocat.h" +#include "embedded_assets/bongocat/bongocat.hpp" #include "graphics/animation.h" -#include "utils/memory.h" +#include "graphics/animation_thread_context.h" +#include "graphics/drawing.h" #include "image_loader/load_images.h" -#include "embedded_assets/bongocat/bongocat.hpp" -#include "embedded_assets/bongocat/bongocat.h" -#include +#include "utils/memory.h" +#include namespace bongocat::animation { - created_result_t load_bongocat_anim([[maybe_unused]] int anim_index, get_sprite_callback_t get_sprite, size_t embedded_images_count) { - BONGOCAT_LOG_VERBOSE("Load bongocat Animation(index=%d) ...", anim_index); - auto result = anim_sprite_sheet_from_embedded_images(get_sprite, embedded_images_count); - if (result.error != bongocat_error_t::BONGOCAT_SUCCESS) [[unlikely]] { - return result.error; - } - - bongocat_sprite_sheet_t ret; - ret.image = bongocat::move(result.result.image); - ret.frame_width = bongocat::move(result.result.frame_width); - ret.frame_height = bongocat::move(result.result.frame_height); - ret.total_frames = bongocat::move(result.result.total_frames); - ret.both_up = bongocat::move(result.result.frames[0]); - ret.left_down = bongocat::move(result.result.frames[1]); - ret.right_down = bongocat::move(result.result.frames[2]); - ret.both_down = bongocat::move(result.result.frames[3]); - - // setup animations (cache) - using namespace assets; - - assert(ret.both_up.valid); - assert(ret.left_down.valid); - assert(ret.right_down.valid); - assert(ret.both_down.valid); - - assert(MAX_ANIMATION_FRAMES >= 4); - ret.animations.idle[0] = BONGOCAT_FRAME_BOTH_UP; - ret.animations.idle[1] = BONGOCAT_FRAME_BOTH_UP; - ret.animations.idle[2] = BONGOCAT_FRAME_BOTH_UP; - ret.animations.idle[3] = BONGOCAT_FRAME_BOTH_UP; - - ret.animations.boring[0] = BONGOCAT_FRAME_BOTH_DOWN; - ret.animations.boring[1] = BONGOCAT_FRAME_BOTH_UP; - ret.animations.boring[2] = BONGOCAT_FRAME_BOTH_UP; - ret.animations.boring[3] = BONGOCAT_FRAME_BOTH_UP; - - ret.animations.writing[0] = BONGOCAT_FRAME_LEFT_DOWN; - ret.animations.writing[1] = BONGOCAT_FRAME_RIGHT_DOWN; - ret.animations.writing[2] = BONGOCAT_FRAME_LEFT_DOWN; - ret.animations.writing[3] = BONGOCAT_FRAME_RIGHT_DOWN; - - ret.animations.sleep[0] = BONGOCAT_FRAME_BOTH_DOWN; - ret.animations.sleep[1] = BONGOCAT_FRAME_BOTH_DOWN; - ret.animations.sleep[2] = BONGOCAT_FRAME_BOTH_DOWN; - ret.animations.sleep[3] = BONGOCAT_FRAME_BOTH_DOWN; - - ret.animations.wake_up[0] = BONGOCAT_FRAME_BOTH_UP; - ret.animations.wake_up[1] = BONGOCAT_FRAME_BOTH_UP; - ret.animations.wake_up[2] = BONGOCAT_FRAME_BOTH_UP; - ret.animations.wake_up[3] = BONGOCAT_FRAME_BOTH_UP; - - ret.animations.working[0] = BONGOCAT_FRAME_LEFT_DOWN; - ret.animations.working[1] = BONGOCAT_FRAME_RIGHT_DOWN; - ret.animations.working[2] = BONGOCAT_FRAME_LEFT_DOWN; - ret.animations.working[3] = BONGOCAT_FRAME_RIGHT_DOWN; - - ret.animations.moving[0] = BONGOCAT_FRAME_BOTH_UP; - ret.animations.moving[1] = BONGOCAT_FRAME_BOTH_DOWN; - ret.animations.moving[2] = BONGOCAT_FRAME_BOTH_UP; - ret.animations.moving[3] = BONGOCAT_FRAME_BOTH_DOWN; - - ret.animations.happy[0] = BONGOCAT_FRAME_LEFT_DOWN; - ret.animations.happy[1] = BONGOCAT_FRAME_RIGHT_DOWN; - ret.animations.happy[2] = BONGOCAT_FRAME_LEFT_DOWN; - ret.animations.happy[3] = BONGOCAT_FRAME_RIGHT_DOWN; - - ret.animations.running[0] = BONGOCAT_FRAME_BOTH_UP; - ret.animations.running[1] = BONGOCAT_FRAME_BOTH_DOWN; - ret.animations.running[2] = BONGOCAT_FRAME_BOTH_UP; - ret.animations.running[3] = BONGOCAT_FRAME_BOTH_DOWN; - - return ret; - } - - bongocat_error_t init_bongocat_anim(animation_context_t& ctx, int anim_index, get_sprite_callback_t get_sprite, size_t embedded_images_count) { - using namespace assets; - BONGOCAT_CHECK_NULL(ctx.shm.ptr, bongocat_error_t::BONGOCAT_ERROR_INVALID_PARAM); - BONGOCAT_CHECK_NULL(ctx._local_copy_config.ptr, bongocat_error_t::BONGOCAT_ERROR_INVALID_PARAM); - - assert(anim_index >= 0 && static_cast(anim_index) < BONGOCAT_ANIM_COUNT); - BONGOCAT_LOG_VERBOSE("Load bongocat Animation (%d/%d): %s ...", anim_index, BONGOCAT_ANIM_COUNT, get_sprite(embedded_images_count).name); - auto result = load_bongocat_anim(anim_index, get_sprite, embedded_images_count); - if (result.error != bongocat_error_t::BONGOCAT_SUCCESS) [[unlikely]] { - BONGOCAT_LOG_ERROR("Load bongocat Animation failed: index: %d", anim_index); - return result.error; - } - assert(result.result.total_frames > 0); ///< this SHOULD always work if it's an valid EMBEDDED image - assert(MAX_NUM_FRAMES <= INT_MAX); - assert(result.result.total_frames <= static_cast(MAX_NUM_FRAMES)); - if (result.result.total_frames > static_cast(MAX_NUM_FRAMES)) { - BONGOCAT_LOG_ERROR("Sprite Sheet does not fit in out_frames: %d, total_frames: %d", MAX_NUM_FRAMES, result.result.total_frames); - return bongocat_error_t::BONGOCAT_ERROR_INVALID_PARAM; - } - - assert(anim_index >= 0); - ctx.shm->bongocat_anims[static_cast(anim_index)] = bongocat::move(result.result); - assert(ctx.shm->bongocat_anims[static_cast(anim_index)].type == animation_t::Type::Bongocat); - - return bongocat_error_t::BONGOCAT_SUCCESS; - } - - created_result_t load_bongocat_sprite_sheet(const animation_context_t& /*ctx*/, int index) { - using namespace assets; - using namespace animation; - switch (index) { - case BONGOCAT_ANIM_INDEX: return load_bongocat_anim(BONGOCAT_ANIM_INDEX, get_bongocat_sprite, BONGOCAT_EMBEDDED_IMAGES_COUNT); - default: return bongocat_error_t::BONGOCAT_ERROR_INVALID_PARAM; - } - return bongocat_error_t::BONGOCAT_ERROR_INVALID_PARAM; - } +created_result_t +load_bongocat_anim([[maybe_unused]] int anim_index, get_sprite_callback_t get_sprite, size_t embedded_images_count) { + BONGOCAT_LOG_VERBOSE("Load bongocat Animation(index=%d) ...", anim_index); + auto result = anim_sprite_sheet_from_embedded_images(get_sprite, embedded_images_count); + if (result.error != bongocat_error_t::BONGOCAT_SUCCESS) [[unlikely]] { + return result.error; + } + + bongocat_sprite_sheet_t ret; + ret.image = bongocat::move(result.result.image); + ret.frame_width = bongocat::move(result.result.frame_width); + ret.frame_height = bongocat::move(result.result.frame_height); + ret.total_frames = bongocat::move(result.result.total_frames); + ret.both_up = bongocat::move(result.result.frames[0]); + ret.left_down = bongocat::move(result.result.frames[1]); + ret.right_down = bongocat::move(result.result.frames[2]); + ret.both_down = bongocat::move(result.result.frames[3]); + + // setup animations (cache) + using namespace assets; + + assert(ret.both_up.valid); + assert(ret.left_down.valid); + assert(ret.right_down.valid); + assert(ret.both_down.valid); + + assert(MAX_ANIMATION_FRAMES >= 4); + ret.animations.idle[0] = BONGOCAT_FRAME_BOTH_UP; + ret.animations.idle[1] = BONGOCAT_FRAME_BOTH_UP; + ret.animations.idle[2] = BONGOCAT_FRAME_BOTH_UP; + ret.animations.idle[3] = BONGOCAT_FRAME_BOTH_UP; + + ret.animations.boring[0] = BONGOCAT_FRAME_BOTH_DOWN; + ret.animations.boring[1] = BONGOCAT_FRAME_BOTH_UP; + ret.animations.boring[2] = BONGOCAT_FRAME_BOTH_UP; + ret.animations.boring[3] = BONGOCAT_FRAME_BOTH_UP; + + ret.animations.writing[0] = BONGOCAT_FRAME_LEFT_DOWN; + ret.animations.writing[1] = BONGOCAT_FRAME_RIGHT_DOWN; + ret.animations.writing[2] = BONGOCAT_FRAME_LEFT_DOWN; + ret.animations.writing[3] = BONGOCAT_FRAME_RIGHT_DOWN; + + ret.animations.sleep[0] = BONGOCAT_FRAME_BOTH_DOWN; + ret.animations.sleep[1] = BONGOCAT_FRAME_BOTH_DOWN; + ret.animations.sleep[2] = BONGOCAT_FRAME_BOTH_DOWN; + ret.animations.sleep[3] = BONGOCAT_FRAME_BOTH_DOWN; + + ret.animations.wake_up[0] = BONGOCAT_FRAME_BOTH_UP; + ret.animations.wake_up[1] = BONGOCAT_FRAME_BOTH_UP; + ret.animations.wake_up[2] = BONGOCAT_FRAME_BOTH_UP; + ret.animations.wake_up[3] = BONGOCAT_FRAME_BOTH_UP; + + ret.animations.working[0] = BONGOCAT_FRAME_LEFT_DOWN; + ret.animations.working[1] = BONGOCAT_FRAME_RIGHT_DOWN; + ret.animations.working[2] = BONGOCAT_FRAME_LEFT_DOWN; + ret.animations.working[3] = BONGOCAT_FRAME_RIGHT_DOWN; + + ret.animations.moving[0] = BONGOCAT_FRAME_BOTH_UP; + ret.animations.moving[1] = BONGOCAT_FRAME_BOTH_DOWN; + ret.animations.moving[2] = BONGOCAT_FRAME_BOTH_UP; + ret.animations.moving[3] = BONGOCAT_FRAME_BOTH_DOWN; + + ret.animations.happy[0] = BONGOCAT_FRAME_LEFT_DOWN; + ret.animations.happy[1] = BONGOCAT_FRAME_RIGHT_DOWN; + ret.animations.happy[2] = BONGOCAT_FRAME_LEFT_DOWN; + ret.animations.happy[3] = BONGOCAT_FRAME_RIGHT_DOWN; + + ret.animations.running[0] = BONGOCAT_FRAME_BOTH_UP; + ret.animations.running[1] = BONGOCAT_FRAME_BOTH_DOWN; + ret.animations.running[2] = BONGOCAT_FRAME_BOTH_UP; + ret.animations.running[3] = BONGOCAT_FRAME_BOTH_DOWN; + + ret.animations.left_writing[0] = BONGOCAT_FRAME_LEFT_DOWN; + ret.animations.left_writing[1] = BONGOCAT_FRAME_BOTH_UP; + ret.animations.left_writing[2] = BONGOCAT_FRAME_LEFT_DOWN; + ret.animations.left_writing[3] = BONGOCAT_FRAME_BOTH_UP; + + ret.animations.right_writing[0] = BONGOCAT_FRAME_RIGHT_DOWN; + ret.animations.right_writing[1] = BONGOCAT_FRAME_BOTH_UP; + ret.animations.right_writing[2] = BONGOCAT_FRAME_RIGHT_DOWN; + ret.animations.right_writing[3] = BONGOCAT_FRAME_BOTH_UP; + + return ret; +} + +bongocat_error_t init_bongocat_anim(animation_thread_context_t& ctx, int anim_index, get_sprite_callback_t get_sprite, + size_t embedded_images_count) { + using namespace assets; + BONGOCAT_CHECK_NULL(ctx.shm.ptr, bongocat_error_t::BONGOCAT_ERROR_INVALID_PARAM); + BONGOCAT_CHECK_NULL(ctx._local_copy_config.ptr, bongocat_error_t::BONGOCAT_ERROR_INVALID_PARAM); + + assert(anim_index >= 0 && static_cast(anim_index) < BONGOCAT_ANIM_COUNT); + BONGOCAT_LOG_VERBOSE("Load bongocat Animation (%d/%d): %s ...", anim_index, BONGOCAT_ANIM_COUNT, + get_sprite(embedded_images_count).name); + auto result = load_bongocat_anim(anim_index, get_sprite, embedded_images_count); + if (result.error != bongocat_error_t::BONGOCAT_SUCCESS) [[unlikely]] { + BONGOCAT_LOG_ERROR("Load bongocat Animation failed: index: %d", anim_index); + return result.error; + } + assert(result.result.total_frames > 0); ///< this SHOULD always work if it's an valid EMBEDDED image + assert(MAX_NUM_FRAMES <= INT_MAX); + assert(result.result.total_frames <= static_cast(MAX_NUM_FRAMES)); + if (result.result.total_frames > static_cast(MAX_NUM_FRAMES)) { + BONGOCAT_LOG_ERROR("Sprite Sheet does not fit in out_frames: %d, total_frames: %d", MAX_NUM_FRAMES, + result.result.total_frames); + return bongocat_error_t::BONGOCAT_ERROR_INVALID_PARAM; + } + + assert(anim_index >= 0); + ctx.shm->bongocat_anims[static_cast(anim_index)] = bongocat::move(result.result); + assert(ctx.shm->bongocat_anims[static_cast(anim_index)].type == animation_t::type_t::Bongocat); + + return bongocat_error_t::BONGOCAT_SUCCESS; +} + +created_result_t load_bongocat_sprite_sheet(const animation_thread_context_t& /*ctx*/, int index) { + using namespace assets; + using namespace animation; + switch (index) { + case BONGOCAT_ANIM_INDEX: + return load_bongocat_anim(BONGOCAT_ANIM_INDEX, get_bongocat_sprite, BONGOCAT_EMBEDDED_IMAGES_COUNT); + default: + return bongocat_error_t::BONGOCAT_ERROR_INVALID_PARAM; + } + return bongocat_error_t::BONGOCAT_ERROR_INVALID_PARAM; } +} // namespace bongocat::animation diff --git a/src/image_loader/custom/CMakeLists.txt b/src/image_loader/custom/CMakeLists.txt index 59bd254c..a67071a3 100644 --- a/src/image_loader/custom/CMakeLists.txt +++ b/src/image_loader/custom/CMakeLists.txt @@ -1,5 +1,11 @@ add_library(assets_custom_loader STATIC) target_sources(assets_custom_loader PRIVATE load_custom.cpp) target_compile_options(assets_custom_loader PRIVATE -ffunction-sections -fdata-sections) -target_include_directories(assets_custom_loader PRIVATE ${INCLUDE_DIR}/image_loader/custom PUBLIC ${INCLUDE_DIR}) -target_link_libraries(assets_custom_loader PUBLIC assets_image_loader PRIVATE bongocat_options) \ No newline at end of file +target_include_directories( + assets_custom_loader + PRIVATE ${INCLUDE_DIR}/image_loader/custom + PUBLIC ${INCLUDE_DIR}) +target_link_libraries( + assets_custom_loader + PUBLIC assets_image_loader + PRIVATE bongocat_options) diff --git a/src/image_loader/custom/load_custom.cpp b/src/image_loader/custom/load_custom.cpp index 9629bc13..383e312d 100644 --- a/src/image_loader/custom/load_custom.cpp +++ b/src/image_loader/custom/load_custom.cpp @@ -1,218 +1,327 @@ #include "image_loader/custom/load_custom.h" -#include "graphics/animation_context.h" + +#include "graphics/animation_thread_context.h" #include "image_loader/load_images.h" namespace bongocat::animation { - created_result_t load_custom_sprite_sheet_file(const char* filename) { - using namespace assets; - BONGOCAT_CHECK_NULL(filename, bongocat_error_t::BONGOCAT_ERROR_INVALID_PARAM); - - auto file_content_result = platform::make_allocated_mmap_file_content_open(filename); - if (file_content_result.error != bongocat_error_t::BONGOCAT_SUCCESS) { - return file_content_result.error; - } - - assets::custom_image_t ret; - ret.data = bongocat::move(file_content_result.result); - ret.name = ::strdup(filename); - return ret; - } - void free_custom_sprite_sheet_file(assets::custom_image_t& image) noexcept { - platform::release_allocated_mmap_file_content(image.data); - if (image.name) ::free(image.name); - image.name = nullptr; - } - - created_result_t load_custom_anim(const animation_context_t& ctx, const assets::custom_image_t& sprite_sheet_image, const assets::custom_animation_settings_t& sprite_sheet_settings) { - assert(sprite_sheet_image.data._size_bytes >= 0); - return load_custom_anim(ctx, assets::embedded_image_t{ .data = sprite_sheet_image.data.data, .size = static_cast(sprite_sheet_image.data._size_bytes), .name = sprite_sheet_image.name ? sprite_sheet_image.name : "" }, sprite_sheet_settings); - } - created_result_t load_custom_anim(const animation_context_t& ctx, const assets::embedded_image_t& sprite_sheet_image, const assets::custom_animation_settings_t& sprite_sheet_settings) { - using namespace assets; - BONGOCAT_CHECK_NULL(ctx._local_copy_config.ptr, bongocat_error_t::BONGOCAT_ERROR_INVALID_PARAM); - - const int sprite_sheet_cols = get_custom_animation_settings_max_cols(sprite_sheet_settings); - const int sprite_sheet_rows = get_custom_animation_settings_rows_count(sprite_sheet_settings); - - if (sprite_sheet_cols == 0 || sprite_sheet_rows == 0) [[unlikely]] { - BONGOCAT_LOG_ERROR("Load custom Animation failed, no cols and rows: %s; %i, %i", sprite_sheet_image.name, sprite_sheet_cols, sprite_sheet_rows); - return bongocat_error_t::BONGOCAT_ERROR_ANIMATION; - } - - auto result = load_sprite_sheet_anim(*ctx._local_copy_config, sprite_sheet_image, sprite_sheet_cols, sprite_sheet_rows); - if (result.error != bongocat_error_t::BONGOCAT_SUCCESS) [[unlikely]] { - BONGOCAT_LOG_ERROR("Load custom Animation failed: %s", sprite_sheet_image.name); - return bongocat_error_t::BONGOCAT_ERROR_ANIMATION; - } - - custom_sprite_sheet_t ret; - ret.image = bongocat::move(result.result.image); - ret.frame_width = bongocat::move(result.result.frame_width); - ret.frame_height = bongocat::move(result.result.frame_height); - - // setup animations - if (sprite_sheet_rows > 0) { - int row = 0; - - if (sprite_sheet_settings.idle_frames > 0) { - ret.idle = { .valid = true, .start_col = 0, .end_col = sprite_sheet_settings.idle_frames-1, .row = sprite_sheet_settings.idle_row_index >= 0 ? sprite_sheet_settings.idle_row_index : row }; - ret.idle.row = ret.idle.row >= 0 ? ret.idle.row : 0; - ret.idle.row = ret.idle.row < sprite_sheet_rows ? ret.idle.row : sprite_sheet_rows-1; - row++; - } - - if (sprite_sheet_settings.boring_frames > 0) { - ret.boring = { .valid = true, .start_col = 0, .end_col = sprite_sheet_settings.boring_frames-1, .row = sprite_sheet_settings.boring_row_index >= 0 ? sprite_sheet_settings.boring_row_index : row }; - ret.boring.row = ret.boring.row >= 0 ? ret.boring.row : 0; - ret.boring.row = ret.boring.row < sprite_sheet_rows ? ret.boring.row : sprite_sheet_rows-1; - row++; - } - - if (sprite_sheet_settings.start_writing_frames > 0) { - ret.start_writing = { .valid = true, .start_col = 0, .end_col = sprite_sheet_settings.start_writing_frames-1, .row = sprite_sheet_settings.start_writing_row_index >= 0 ? sprite_sheet_settings.start_writing_row_index : row }; - ret.start_writing.row = ret.start_writing.row >= 0 ? ret.start_writing.row : 0; - ret.start_writing.row = ret.start_writing.row < sprite_sheet_rows ? ret.start_writing.row : sprite_sheet_rows-1; - row++; - } - if (sprite_sheet_settings.writing_frames > 0) { - ret.writing = { .valid = true, .start_col = 0, .end_col = sprite_sheet_settings.writing_frames-1, .row = sprite_sheet_settings.writing_row_index >= 0 ? sprite_sheet_settings.writing_row_index : row }; - ret.writing.row = ret.writing.row >= 0 ? ret.writing.row : 0; - ret.writing.row = ret.writing.row < sprite_sheet_rows ? ret.writing.row : sprite_sheet_rows-1; - row++; - } - if (sprite_sheet_settings.end_writing_frames > 0) { - ret.end_writing = { .valid = true, .start_col = 0, .end_col = sprite_sheet_settings.end_writing_frames-1, .row = sprite_sheet_settings.end_writing_row_index >= 0 ? sprite_sheet_settings.end_writing_row_index : row }; - ret.end_writing.row = ret.end_writing.row >= 0 ? ret.end_writing.row : 0; - ret.end_writing.row = ret.end_writing.row < sprite_sheet_rows ? ret.end_writing.row : sprite_sheet_rows-1; - row++; - } - - if (sprite_sheet_settings.happy_frames > 0) { - ret.happy = { .valid = true, .start_col = 0, .end_col = sprite_sheet_settings.happy_frames-1, .row = sprite_sheet_settings.happy_row_index >= 0 ? sprite_sheet_settings.happy_row_index : row }; - ret.happy.row = ret.happy.row >= 0 ? ret.happy.row : 0; - ret.happy.row = ret.happy.row < sprite_sheet_rows ? ret.happy.row : sprite_sheet_rows-1; - row++; - } - - if (sprite_sheet_settings.asleep_frames > 0) { - ret.fall_asleep = { .valid = true, .start_col = 0, .end_col = sprite_sheet_settings.asleep_frames-1, .row = sprite_sheet_settings.asleep_row_index >= 0 ? sprite_sheet_settings.asleep_row_index : row }; - ret.fall_asleep.row = ret.fall_asleep.row >= 0 ? ret.fall_asleep.row : 0; - ret.fall_asleep.row = ret.fall_asleep.row < sprite_sheet_rows ? ret.fall_asleep.row : sprite_sheet_rows-1; - row++; - } - if (sprite_sheet_settings.sleep_frames > 0) { - ret.sleep = { .valid = true, .start_col = 0, .end_col = sprite_sheet_settings.sleep_frames-1, .row = sprite_sheet_settings.sleep_row_index >= 0 ? sprite_sheet_settings.sleep_row_index : row }; - ret.sleep.row = ret.sleep.row >= 0 ? ret.sleep.row : 0; - ret.sleep.row = ret.sleep.row < sprite_sheet_rows ? ret.sleep.row : sprite_sheet_rows-1; - row++; - } - if (sprite_sheet_settings.wake_up_frames > 0) { - ret.wake_up = { .valid = true, .start_col = 0, .end_col = sprite_sheet_settings.wake_up_frames-1, .row = sprite_sheet_settings.wake_up_row_index >= 0 ? sprite_sheet_settings.wake_up_row_index : row }; - ret.wake_up.row = ret.wake_up.row >= 0 ? ret.wake_up.row : 0; - ret.wake_up.row = ret.wake_up.row < sprite_sheet_rows ? ret.wake_up.row : sprite_sheet_rows-1; - row++; - } - - if (sprite_sheet_settings.start_working_frames > 0) { - ret.start_working = { .valid = true, .start_col = 0, .end_col = sprite_sheet_settings.start_working_frames-1, .row = sprite_sheet_settings.start_working_row_index >= 0 ? sprite_sheet_settings.start_working_row_index : row }; - ret.start_working.row = ret.start_working.row >= 0 ? ret.start_working.row : 0; - ret.start_working.row = ret.start_working.row < sprite_sheet_rows ? ret.start_working.row : sprite_sheet_rows-1; - row++; - } - if (sprite_sheet_settings.working_frames > 0) { - ret.working = { .valid = true, .start_col = 0, .end_col = sprite_sheet_settings.working_frames-1, .row = sprite_sheet_settings.working_row_index >= 0 ? sprite_sheet_settings.working_row_index : row }; - ret.working.row = ret.working.row >= 0 ? ret.working.row : 0; - ret.working.row = ret.working.row < sprite_sheet_rows ? ret.working.row : sprite_sheet_rows-1; - row++; - } - if (sprite_sheet_settings.end_working_frames > 0) { - ret.end_working = { .valid = true, .start_col = 0, .end_col = sprite_sheet_settings.end_working_frames-1, .row = sprite_sheet_settings.end_working_row_index >= 0 ? sprite_sheet_settings.end_working_row_index : row }; - ret.end_working.row = ret.end_working.row >= 0 ? ret.end_working.row : 0; - ret.end_working.row = ret.end_working.row < sprite_sheet_rows ? ret.end_working.row : sprite_sheet_rows-1; - row++; - } - - if (sprite_sheet_settings.start_moving_frames > 0) { - ret.start_moving = { .valid = true, .start_col = 0, .end_col = sprite_sheet_settings.start_moving_frames-1, .row = sprite_sheet_settings.start_moving_row_index >= 0 ? sprite_sheet_settings.start_moving_row_index : row }; - ret.start_moving.row = ret.start_moving.row >= 0 ? ret.start_moving.row : 0; - ret.start_moving.row = ret.start_moving.row < sprite_sheet_rows ? ret.start_moving.row : sprite_sheet_rows-1; - row++; - } - if (sprite_sheet_settings.moving_frames > 0) { - ret.moving = { .valid = true, .start_col = 0, .end_col = sprite_sheet_settings.moving_frames-1, .row = sprite_sheet_settings.moving_row_index >= 0 ? sprite_sheet_settings.moving_row_index : row }; - ret.moving.row = ret.moving.row >= 0 ? ret.moving.row : 0; - ret.moving.row = ret.moving.row < sprite_sheet_rows ? ret.moving.row : sprite_sheet_rows-1; - row++; - } - if (sprite_sheet_settings.end_moving_frames > 0) { - ret.end_moving = { .valid = true, .start_col = 0, .end_col = sprite_sheet_settings.end_moving_frames-1, .row = sprite_sheet_settings.end_moving_row_index >= 0 ? sprite_sheet_settings.end_moving_row_index : row }; - ret.end_moving.row = ret.end_moving.row >= 0 ? ret.end_moving.row : 0; - ret.end_moving.row = ret.end_moving.row < sprite_sheet_rows ? ret.end_moving.row : sprite_sheet_rows-1; - row++; - } - - if (sprite_sheet_settings.start_running_frames > 0) { - ret.start_running = { .valid = true, .start_col = 0, .end_col = sprite_sheet_settings.start_running_frames-1, .row = sprite_sheet_settings.start_running_row_index >= 0 ? sprite_sheet_settings.start_running_row_index : row }; - if (ret.start_moving.valid) { - ret.start_running.row = ret.start_moving.row; - } else { - ret.start_running.row = ret.start_running.row >= 0 ? ret.start_running.row : 0; - ret.start_running.row = ret.start_running.row < sprite_sheet_rows ? ret.start_running.row : sprite_sheet_rows-1; - } - row++; - } else if (ret.start_moving.valid) { - ret.start_running = { .valid = true, .start_col = ret.start_moving.start_col, .end_col = ret.start_moving.end_col, .row = sprite_sheet_settings.start_running_row_index >= 0 ? sprite_sheet_settings.start_running_row_index : ret.start_moving.row }; - } - if (sprite_sheet_settings.running_frames > 0) { - ret.running = { .valid = true, .start_col = 0, .end_col = sprite_sheet_settings.running_frames-1, .row = sprite_sheet_settings.running_row_index >= 0 ? sprite_sheet_settings.running_row_index : row }; - if (ret.moving.valid) { - ret.running.row = ret.moving.row; - } else { - ret.running.row = ret.running.row >= 0 ? ret.running.row : 0; - ret.running.row = ret.running.row < sprite_sheet_rows ? ret.running.row : sprite_sheet_rows-1; - } - row++; - } else if (ret.moving.valid) { - ret.running = { .valid = true, .start_col = ret.moving.start_col, .end_col = ret.moving.end_col, .row = sprite_sheet_settings.running_row_index >= 0 ? sprite_sheet_settings.running_row_index : ret.moving.row }; - } - if (sprite_sheet_settings.end_running_frames > 0) { - ret.end_running = { .valid = true, .start_col = 0, .end_col = sprite_sheet_settings.end_running_frames-1, .row = sprite_sheet_settings.end_running_row_index >= 0 ? sprite_sheet_settings.end_running_row_index : row }; - if (ret.end_moving.valid) { - ret.end_running.row = ret.end_moving.row; - } else { - ret.end_running.row = ret.end_running.row >= 0 ? ret.end_running.row : 0; - ret.end_running.row = ret.end_running.row < sprite_sheet_rows ? ret.end_running.row : sprite_sheet_rows-1; - } - row++; - } else if (ret.end_moving.valid) { - ret.end_running = { .valid = true, .start_col = ret.end_moving.start_col, .end_col = ret.end_moving.end_col, .row = sprite_sheet_settings.end_running_row_index >= 0 ? sprite_sheet_settings.end_running_row_index : ret.end_moving.row }; - } - } - - // features - ret.feature_idle = ret.idle.valid; - ret.feature_boring = ret.boring.valid; - ret.feature_writing = ret.writing.valid || ret.start_writing.valid || ret.end_writing.valid; - ret.feature_writing_happy = ret.feature_writing && ret.happy.valid; - ret.feature_sleep = ret.sleep.valid || ret.fall_asleep.valid; - ret.feature_sleep_wake_up = ret.feature_sleep && ret.wake_up.valid; - ret.feature_working = ret.working.valid || ret.start_working.valid || ret.end_working.valid; - ret.feature_moving = ret.moving.valid || ret.start_moving.valid || ret.end_moving.valid; - ret.feature_running = ret.running.valid || ret.start_running.valid || ret.end_running.valid; - // is feature_toggle_writing_frames enabled or writing has only 2 frames (default) - ret.feature_writing_toggle_frames = ret.working.valid && (sprite_sheet_settings.feature_toggle_writing_frames >= 1 || (sprite_sheet_settings.feature_toggle_writing_frames < 0 && !ret.start_moving.valid && !ret.end_working.valid && ret.working.valid && sprite_sheet_settings.working_frames == 2)); - ret.feature_writing_toggle_frames_random = ret.working.valid && (sprite_sheet_settings.feature_toggle_writing_frames_random >= 1 || (sprite_sheet_settings.feature_toggle_writing_frames_random < 0 && !ret.start_moving.valid && !ret.end_working.valid && ret.working.valid && sprite_sheet_settings.working_frames == 2)); - - if (!ret.feature_idle) [[unlikely]] { - BONGOCAT_LOG_WARNING("Custom Animation without idle animation: %s", sprite_sheet_image.name); - // default to first frame - ret.idle = { .valid = true, .start_col = 0, .end_col = 0, .row = sprite_sheet_settings.idle_row_index >= 0 ? sprite_sheet_settings.idle_row_index : 0 }; - ret.idle.row = ret.idle.row >= 0 ? ret.idle.row : 0; - ret.idle.row = ret.idle.row < sprite_sheet_rows ? ret.idle.row : sprite_sheet_rows-1; - } - - return ret; +created_result_t load_custom_sprite_sheet_file(const char *filename) { + using namespace assets; + BONGOCAT_CHECK_NULL(filename, bongocat_error_t::BONGOCAT_ERROR_INVALID_PARAM); + + auto file_content_result = platform::make_allocated_mmap_file_content_open(filename); + if (file_content_result.error != bongocat_error_t::BONGOCAT_SUCCESS) { + return file_content_result.error; + } + + assets::custom_image_t ret; + ret.data = bongocat::move(file_content_result.result); + ret.name = ::strdup(filename); + return ret; +} +void free_custom_sprite_sheet_file(assets::custom_image_t& image) noexcept { + platform::release_allocated_mmap_file_content(image.data); + if (image.name != BONGOCAT_NULLPTR) { + ::free(image.name); + image.name = BONGOCAT_NULLPTR; + } +} + +created_result_t +load_custom_anim(const animation_thread_context_t& ctx, const assets::custom_image_t& sprite_sheet_image, + const assets::custom_animation_settings_t& sprite_sheet_settings) { + assert(sprite_sheet_image.data._size_bytes >= 0); + return load_custom_anim(ctx, + assets::embedded_image_t{.data = sprite_sheet_image.data.data, + .size = static_cast(sprite_sheet_image.data._size_bytes), + .name = sprite_sheet_image.name != BONGOCAT_NULLPTR ? sprite_sheet_image.name : ""}, + sprite_sheet_settings); +} +created_result_t +load_custom_anim(const animation_thread_context_t& ctx, const assets::embedded_image_t& sprite_sheet_image, + const assets::custom_animation_settings_t& sprite_sheet_settings) { + using namespace assets; + BONGOCAT_CHECK_NULL(ctx._local_copy_config.ptr, bongocat_error_t::BONGOCAT_ERROR_INVALID_PARAM); + + const int sprite_sheet_cols = get_custom_animation_settings_max_cols(sprite_sheet_settings); + const int sprite_sheet_rows = get_custom_animation_settings_rows_count(sprite_sheet_settings); + + if (sprite_sheet_cols == 0 || sprite_sheet_rows == 0) [[unlikely]] { + BONGOCAT_LOG_ERROR("Load custom Animation failed, no cols and rows: %s; %i, %i", sprite_sheet_image.name, + sprite_sheet_cols, sprite_sheet_rows); + return bongocat_error_t::BONGOCAT_ERROR_ANIMATION; + } + + auto result = + load_sprite_sheet_anim(*ctx._local_copy_config, sprite_sheet_image, sprite_sheet_cols, sprite_sheet_rows); + if (result.error != bongocat_error_t::BONGOCAT_SUCCESS) [[unlikely]] { + BONGOCAT_LOG_ERROR("Load custom Animation failed: %s", sprite_sheet_image.name); + return bongocat_error_t::BONGOCAT_ERROR_ANIMATION; + } + + custom_sprite_sheet_t ret; + ret.image = bongocat::move(result.result.image); + ret.frame_width = bongocat::move(result.result.frame_width); + ret.frame_height = bongocat::move(result.result.frame_height); + + // setup animations + if (sprite_sheet_rows > 0) { + int row = 0; + + if (sprite_sheet_settings.idle_frames > 0) { + ret.idle = {.valid = true, + .start_col = 0, + .end_col = sprite_sheet_settings.idle_frames - 1, + .row = sprite_sheet_settings.idle_row_index >= 0 ? sprite_sheet_settings.idle_row_index : row}; + ret.idle.row = ret.idle.row >= 0 ? ret.idle.row : 0; + ret.idle.row = ret.idle.row < sprite_sheet_rows ? ret.idle.row : sprite_sheet_rows - 1; + row++; + } + + if (sprite_sheet_settings.boring_frames > 0) { + ret.boring = {.valid = true, + .start_col = 0, + .end_col = sprite_sheet_settings.boring_frames - 1, + .row = sprite_sheet_settings.boring_row_index >= 0 ? sprite_sheet_settings.boring_row_index : row}; + ret.boring.row = ret.boring.row >= 0 ? ret.boring.row : 0; + ret.boring.row = ret.boring.row < sprite_sheet_rows ? ret.boring.row : sprite_sheet_rows - 1; + row++; + } + + if (sprite_sheet_settings.start_writing_frames > 0) { + ret.start_writing = {.valid = true, + .start_col = 0, + .end_col = sprite_sheet_settings.start_writing_frames - 1, + .row = sprite_sheet_settings.start_writing_row_index >= 0 + ? sprite_sheet_settings.start_writing_row_index + : row}; + ret.start_writing.row = ret.start_writing.row >= 0 ? ret.start_writing.row : 0; + ret.start_writing.row = ret.start_writing.row < sprite_sheet_rows ? ret.start_writing.row : sprite_sheet_rows - 1; + row++; + } + if (sprite_sheet_settings.writing_frames > 0) { + ret.writing = {.valid = true, + .start_col = 0, + .end_col = sprite_sheet_settings.writing_frames - 1, + .row = + sprite_sheet_settings.writing_row_index >= 0 ? sprite_sheet_settings.writing_row_index : row}; + ret.writing.row = ret.writing.row >= 0 ? ret.writing.row : 0; + ret.writing.row = ret.writing.row < sprite_sheet_rows ? ret.writing.row : sprite_sheet_rows - 1; + row++; + } + if (sprite_sheet_settings.end_writing_frames > 0) { + ret.end_writing = { + .valid = true, + .start_col = 0, + .end_col = sprite_sheet_settings.end_writing_frames - 1, + .row = sprite_sheet_settings.end_writing_row_index >= 0 ? sprite_sheet_settings.end_writing_row_index : row}; + ret.end_writing.row = ret.end_writing.row >= 0 ? ret.end_writing.row : 0; + ret.end_writing.row = ret.end_writing.row < sprite_sheet_rows ? ret.end_writing.row : sprite_sheet_rows - 1; + row++; + } + + if (sprite_sheet_settings.happy_frames > 0) { + ret.happy = {.valid = true, + .start_col = 0, + .end_col = sprite_sheet_settings.happy_frames - 1, + .row = sprite_sheet_settings.happy_row_index >= 0 ? sprite_sheet_settings.happy_row_index : row}; + ret.happy.row = ret.happy.row >= 0 ? ret.happy.row : 0; + ret.happy.row = ret.happy.row < sprite_sheet_rows ? ret.happy.row : sprite_sheet_rows - 1; + row++; + } + + if (sprite_sheet_settings.asleep_frames > 0) { + ret.fall_asleep = {.valid = true, + .start_col = 0, + .end_col = sprite_sheet_settings.asleep_frames - 1, + .row = sprite_sheet_settings.asleep_row_index >= 0 ? sprite_sheet_settings.asleep_row_index + : row}; + ret.fall_asleep.row = ret.fall_asleep.row >= 0 ? ret.fall_asleep.row : 0; + ret.fall_asleep.row = ret.fall_asleep.row < sprite_sheet_rows ? ret.fall_asleep.row : sprite_sheet_rows - 1; + row++; + } + if (sprite_sheet_settings.sleep_frames > 0) { + ret.sleep = {.valid = true, + .start_col = 0, + .end_col = sprite_sheet_settings.sleep_frames - 1, + .row = sprite_sheet_settings.sleep_row_index >= 0 ? sprite_sheet_settings.sleep_row_index : row}; + ret.sleep.row = ret.sleep.row >= 0 ? ret.sleep.row : 0; + ret.sleep.row = ret.sleep.row < sprite_sheet_rows ? ret.sleep.row : sprite_sheet_rows - 1; + row++; + } + if (sprite_sheet_settings.wake_up_frames > 0) { + ret.wake_up = {.valid = true, + .start_col = 0, + .end_col = sprite_sheet_settings.wake_up_frames - 1, + .row = + sprite_sheet_settings.wake_up_row_index >= 0 ? sprite_sheet_settings.wake_up_row_index : row}; + ret.wake_up.row = ret.wake_up.row >= 0 ? ret.wake_up.row : 0; + ret.wake_up.row = ret.wake_up.row < sprite_sheet_rows ? ret.wake_up.row : sprite_sheet_rows - 1; + row++; + } + + if (sprite_sheet_settings.start_working_frames > 0) { + ret.start_working = {.valid = true, + .start_col = 0, + .end_col = sprite_sheet_settings.start_working_frames - 1, + .row = sprite_sheet_settings.start_working_row_index >= 0 + ? sprite_sheet_settings.start_working_row_index + : row}; + ret.start_working.row = ret.start_working.row >= 0 ? ret.start_working.row : 0; + ret.start_working.row = ret.start_working.row < sprite_sheet_rows ? ret.start_working.row : sprite_sheet_rows - 1; + row++; + } + if (sprite_sheet_settings.working_frames > 0) { + ret.working = {.valid = true, + .start_col = 0, + .end_col = sprite_sheet_settings.working_frames - 1, + .row = + sprite_sheet_settings.working_row_index >= 0 ? sprite_sheet_settings.working_row_index : row}; + ret.working.row = ret.working.row >= 0 ? ret.working.row : 0; + ret.working.row = ret.working.row < sprite_sheet_rows ? ret.working.row : sprite_sheet_rows - 1; + row++; + } + if (sprite_sheet_settings.end_working_frames > 0) { + ret.end_working = { + .valid = true, + .start_col = 0, + .end_col = sprite_sheet_settings.end_working_frames - 1, + .row = sprite_sheet_settings.end_working_row_index >= 0 ? sprite_sheet_settings.end_working_row_index : row}; + ret.end_working.row = ret.end_working.row >= 0 ? ret.end_working.row : 0; + ret.end_working.row = ret.end_working.row < sprite_sheet_rows ? ret.end_working.row : sprite_sheet_rows - 1; + row++; + } + + if (sprite_sheet_settings.start_moving_frames > 0) { + ret.start_moving = {.valid = true, + .start_col = 0, + .end_col = sprite_sheet_settings.start_moving_frames - 1, + .row = sprite_sheet_settings.start_moving_row_index >= 0 + ? sprite_sheet_settings.start_moving_row_index + : row}; + ret.start_moving.row = ret.start_moving.row >= 0 ? ret.start_moving.row : 0; + ret.start_moving.row = ret.start_moving.row < sprite_sheet_rows ? ret.start_moving.row : sprite_sheet_rows - 1; + row++; + } + if (sprite_sheet_settings.moving_frames > 0) { + ret.moving = {.valid = true, + .start_col = 0, + .end_col = sprite_sheet_settings.moving_frames - 1, + .row = sprite_sheet_settings.moving_row_index >= 0 ? sprite_sheet_settings.moving_row_index : row}; + ret.moving.row = ret.moving.row >= 0 ? ret.moving.row : 0; + ret.moving.row = ret.moving.row < sprite_sheet_rows ? ret.moving.row : sprite_sheet_rows - 1; + row++; + } + if (sprite_sheet_settings.end_moving_frames > 0) { + ret.end_moving = { + .valid = true, + .start_col = 0, + .end_col = sprite_sheet_settings.end_moving_frames - 1, + .row = sprite_sheet_settings.end_moving_row_index >= 0 ? sprite_sheet_settings.end_moving_row_index : row}; + ret.end_moving.row = ret.end_moving.row >= 0 ? ret.end_moving.row : 0; + ret.end_moving.row = ret.end_moving.row < sprite_sheet_rows ? ret.end_moving.row : sprite_sheet_rows - 1; + row++; + } + + if (sprite_sheet_settings.start_running_frames > 0) { + ret.start_running = {.valid = true, + .start_col = 0, + .end_col = sprite_sheet_settings.start_running_frames - 1, + .row = sprite_sheet_settings.start_running_row_index >= 0 + ? sprite_sheet_settings.start_running_row_index + : row}; + if (ret.start_moving.valid) { + ret.start_running.row = ret.start_moving.row; + } else { + ret.start_running.row = ret.start_running.row >= 0 ? ret.start_running.row : 0; + ret.start_running.row = + ret.start_running.row < sprite_sheet_rows ? ret.start_running.row : sprite_sheet_rows - 1; + } + row++; + } else if (ret.start_moving.valid) { + ret.start_running = {.valid = true, + .start_col = ret.start_moving.start_col, + .end_col = ret.start_moving.end_col, + .row = sprite_sheet_settings.start_running_row_index >= 0 + ? sprite_sheet_settings.start_running_row_index + : ret.start_moving.row}; } + if (sprite_sheet_settings.running_frames > 0) { + ret.running = {.valid = true, + .start_col = 0, + .end_col = sprite_sheet_settings.running_frames - 1, + .row = + sprite_sheet_settings.running_row_index >= 0 ? sprite_sheet_settings.running_row_index : row}; + if (ret.moving.valid) { + ret.running.row = ret.moving.row; + } else { + ret.running.row = ret.running.row >= 0 ? ret.running.row : 0; + ret.running.row = ret.running.row < sprite_sheet_rows ? ret.running.row : sprite_sheet_rows - 1; + } + row++; + } else if (ret.moving.valid) { + ret.running = {.valid = true, + .start_col = ret.moving.start_col, + .end_col = ret.moving.end_col, + .row = sprite_sheet_settings.running_row_index >= 0 ? sprite_sheet_settings.running_row_index + : ret.moving.row}; + } + if (sprite_sheet_settings.end_running_frames > 0) { + ret.end_running = { + .valid = true, + .start_col = 0, + .end_col = sprite_sheet_settings.end_running_frames - 1, + .row = sprite_sheet_settings.end_running_row_index >= 0 ? sprite_sheet_settings.end_running_row_index : row}; + if (ret.end_moving.valid) { + ret.end_running.row = ret.end_moving.row; + } else { + ret.end_running.row = ret.end_running.row >= 0 ? ret.end_running.row : 0; + ret.end_running.row = ret.end_running.row < sprite_sheet_rows ? ret.end_running.row : sprite_sheet_rows - 1; + } + row++; + } else if (ret.end_moving.valid) { + ret.end_running = {.valid = true, + .start_col = ret.end_moving.start_col, + .end_col = ret.end_moving.end_col, + .row = sprite_sheet_settings.end_running_row_index >= 0 + ? sprite_sheet_settings.end_running_row_index + : ret.end_moving.row}; + } + } + + // features + ret.feature_idle = ret.idle.valid; + ret.feature_boring = ret.boring.valid; + ret.feature_writing = ret.writing.valid || ret.start_writing.valid || ret.end_writing.valid; + ret.feature_writing_happy = ret.feature_writing && ret.happy.valid; + ret.feature_sleep = ret.sleep.valid || ret.fall_asleep.valid; + ret.feature_sleep_wake_up = ret.feature_sleep && ret.wake_up.valid; + ret.feature_working = ret.working.valid || ret.start_working.valid || ret.end_working.valid; + ret.feature_moving = ret.moving.valid || ret.start_moving.valid || ret.end_moving.valid; + ret.feature_running = ret.running.valid || ret.start_running.valid || ret.end_running.valid; + // is feature_toggle_writing_frames enabled or writing has only 2 frames (default) + ret.feature_writing_toggle_frames = + ret.working.valid && (sprite_sheet_settings.feature_toggle_writing_frames >= 1 || + (sprite_sheet_settings.feature_toggle_writing_frames < 0 && !ret.start_moving.valid && + !ret.end_working.valid && ret.working.valid && sprite_sheet_settings.working_frames == 2)); + ret.feature_writing_toggle_frames_random = + ret.working.valid && + (sprite_sheet_settings.feature_toggle_writing_frames_random >= 1 || + (sprite_sheet_settings.feature_toggle_writing_frames_random < 0 && !ret.start_moving.valid && + !ret.end_working.valid && ret.working.valid && sprite_sheet_settings.working_frames == 2)); + if (!ret.feature_idle) [[unlikely]] { + BONGOCAT_LOG_WARNING("Custom Animation without idle animation: %s", sprite_sheet_image.name); + // default to first frame + ret.idle = {.valid = true, + .start_col = 0, + .end_col = 0, + .row = sprite_sheet_settings.idle_row_index >= 0 ? sprite_sheet_settings.idle_row_index : 0}; + ret.idle.row = ret.idle.row >= 0 ? ret.idle.row : 0; + ret.idle.row = ret.idle.row < sprite_sheet_rows ? ret.idle.row : sprite_sheet_rows - 1; + } + + return ret; } + +} // namespace bongocat::animation diff --git a/src/image_loader/dm/CMakeLists.txt b/src/image_loader/dm/CMakeLists.txt index 508077cf..038f8490 100644 --- a/src/image_loader/dm/CMakeLists.txt +++ b/src/image_loader/dm/CMakeLists.txt @@ -1,9 +1,11 @@ add_library(assets_dm_loader STATIC) target_sources(assets_dm_loader PRIVATE dm_load_sprite_sheet.cpp load_images_dm.cpp) target_compile_options(assets_dm_loader PRIVATE -ffunction-sections -fdata-sections) -target_include_directories(assets_dm_loader - PRIVATE ${INCLUDE_DIR}/embedded_assets/dm ${INCLUDE_DIR}/image_loader/dm - PUBLIC ${INCLUDE_DIR}) -target_link_libraries(assets_dm_loader - PUBLIC assets_image_loader assets_base_dm_loader assets_dm - PRIVATE assets_dm_interface assets_dm_feature bongocat_options) \ No newline at end of file +target_include_directories( + assets_dm_loader + PRIVATE ${INCLUDE_DIR}/embedded_assets/dm ${INCLUDE_DIR}/image_loader/dm + PUBLIC ${INCLUDE_DIR}) +target_link_libraries( + assets_dm_loader + PUBLIC assets_image_loader assets_base_dm_loader assets_dm + PRIVATE assets_dm_interface assets_dm_feature bongocat_options) diff --git a/src/image_loader/dm/dm_load_sprite_sheet.cpp b/src/image_loader/dm/dm_load_sprite_sheet.cpp index 4932f310..c941b296 100644 --- a/src/image_loader/dm/dm_load_sprite_sheet.cpp +++ b/src/image_loader/dm/dm_load_sprite_sheet.cpp @@ -1,5 +1,5 @@ #include "core/bongocat.h" -#include "graphics/animation_context.h" +#include "graphics/animation_thread_context.h" #include "graphics/sprite_sheet.h" #include "image_loader/base_dm/load_dm.h" #include "embedded_assets/dm/dm.hpp" @@ -8,7 +8,7 @@ #include "image_loader/dm/load_images_dm.h" namespace bongocat::animation { - created_result_t load_dm_sprite_sheet(const animation_context_t& ctx, int index) { + created_result_t load_dm_sprite_sheet(const animation_thread_context_t& ctx, int index) { using namespace assets; switch (index) { case DM_AGUMON_ANIM_INDEX: return load_dm_anim(ctx, DM_AGUMON_ANIM_INDEX, get_dm_sprite_sheet(DM_AGUMON_ANIM_INDEX), DM_AGUMON_SPRITE_SHEET_COLS, DM_AGUMON_SPRITE_SHEET_ROWS); diff --git a/src/image_loader/dm/load_images_dm.cpp b/src/image_loader/dm/load_images_dm.cpp index 84ef43aa..74ba4dda 100644 --- a/src/image_loader/dm/load_images_dm.cpp +++ b/src/image_loader/dm/load_images_dm.cpp @@ -1,11 +1,11 @@ #include "load_images_dm.h" #include "embedded_assets/dm/dm.hpp" -#include "graphics/animation_context.h" +#include "graphics/animation_thread_context.h" #include "image_loader/load_images.h" #include "image_loader/base_dm/load_dm.h" namespace bongocat::animation { -bongocat_error_t init_dm_anim(animation_context_t& ctx, int anim_index, const assets::embedded_image_t& sprite_sheet_image, int sprite_sheet_cols, int sprite_sheet_rows) { +bongocat_error_t init_dm_anim(animation_thread_context_t& ctx, int anim_index, const assets::embedded_image_t& sprite_sheet_image, int sprite_sheet_cols, int sprite_sheet_rows) { using namespace assets; BONGOCAT_CHECK_NULL(ctx.shm.ptr, bongocat_error_t::BONGOCAT_ERROR_INVALID_PARAM); BONGOCAT_CHECK_NULL(ctx._local_copy_config.ptr, bongocat_error_t::BONGOCAT_ERROR_INVALID_PARAM); @@ -21,7 +21,7 @@ bongocat_error_t init_dm_anim(animation_context_t& ctx, int anim_index, const as assert(anim_index >= 0); ctx.shm->dm_anims[static_cast(anim_index)] = bongocat::move(result.result); - assert(ctx.shm->dm_anims[static_cast(anim_index)].type == animation_t::Type::Dm); + assert(ctx.shm->dm_anims[static_cast(anim_index)].type == animation_t::type_t::Dm); return bongocat_error_t::BONGOCAT_SUCCESS; } diff --git a/src/image_loader/dm20/CMakeLists.txt b/src/image_loader/dm20/CMakeLists.txt index 36af11e6..010a7dcf 100644 --- a/src/image_loader/dm20/CMakeLists.txt +++ b/src/image_loader/dm20/CMakeLists.txt @@ -1,9 +1,11 @@ add_library(assets_dm20_loader STATIC) target_sources(assets_dm20_loader PRIVATE dm20_load_sprite_sheet.cpp load_images_dm20.cpp) target_compile_options(assets_dm20_loader PRIVATE -ffunction-sections -fdata-sections) -target_include_directories(assets_dm20_loader - PRIVATE ${INCLUDE_DIR}/embedded_assets/dm20 ${INCLUDE_DIR}/image_loader/dm20 - PUBLIC ${INCLUDE_DIR}) -target_link_libraries(assets_dm20_loader - PUBLIC assets_image_loader assets_base_dm_loader assets_dm20 - PRIVATE assets_dm20_interface assets_dm20_feature bongocat_options) \ No newline at end of file +target_include_directories( + assets_dm20_loader + PRIVATE ${INCLUDE_DIR}/embedded_assets/dm20 ${INCLUDE_DIR}/image_loader/dm20 + PUBLIC ${INCLUDE_DIR}) +target_link_libraries( + assets_dm20_loader + PUBLIC assets_image_loader assets_base_dm_loader assets_dm20 + PRIVATE assets_dm20_interface assets_dm20_feature bongocat_options) diff --git a/src/image_loader/dm20/dm20_load_sprite_sheet.cpp b/src/image_loader/dm20/dm20_load_sprite_sheet.cpp index 2c5a06c9..e6a38039 100644 --- a/src/image_loader/dm20/dm20_load_sprite_sheet.cpp +++ b/src/image_loader/dm20/dm20_load_sprite_sheet.cpp @@ -1,5 +1,5 @@ #include "core/bongocat.h" -#include "graphics/animation_context.h" +#include "graphics/animation_thread_context.h" #include "graphics/sprite_sheet.h" #include "image_loader/base_dm/load_dm.h" #include "embedded_assets/dm20/dm20.hpp" @@ -8,7 +8,7 @@ #include "image_loader/dm20/load_images_dm20.h" namespace bongocat::animation { - created_result_t load_dm20_sprite_sheet(const animation_context_t& ctx, int index) { + created_result_t load_dm20_sprite_sheet(const animation_thread_context_t& ctx, int index) { using namespace assets; switch (index) { case DM20_AEGISDRAMON_ANIM_INDEX: return load_dm_anim(ctx, DM20_AEGISDRAMON_ANIM_INDEX, get_dm20_sprite_sheet(DM20_AEGISDRAMON_ANIM_INDEX), DM20_AEGISDRAMON_SPRITE_SHEET_COLS, DM20_AEGISDRAMON_SPRITE_SHEET_ROWS); diff --git a/src/image_loader/dm20/load_images_dm20.cpp b/src/image_loader/dm20/load_images_dm20.cpp index 05813d07..1c31899e 100644 --- a/src/image_loader/dm20/load_images_dm20.cpp +++ b/src/image_loader/dm20/load_images_dm20.cpp @@ -1,11 +1,11 @@ #include "load_images_dm20.h" -#include "graphics/animation_context.h" +#include "graphics/animation_thread_context.h" #include "image_loader/base_dm/load_dm.h" #include "embedded_assets/embedded_image.h" #include "embedded_assets/dm20/dm20.hpp" namespace bongocat::animation { - bongocat_error_t init_dm20_anim(animation_context_t& ctx, int anim_index, const assets::embedded_image_t& sprite_sheet_image, int sprite_sheet_cols, int sprite_sheet_rows) { + bongocat_error_t init_dm20_anim(animation_thread_context_t& ctx, int anim_index, const assets::embedded_image_t& sprite_sheet_image, int sprite_sheet_cols, int sprite_sheet_rows) { using namespace assets; BONGOCAT_CHECK_NULL(ctx.shm.ptr, bongocat_error_t::BONGOCAT_ERROR_INVALID_PARAM); BONGOCAT_CHECK_NULL(ctx._local_copy_config.ptr, bongocat_error_t::BONGOCAT_ERROR_INVALID_PARAM); @@ -21,7 +21,7 @@ namespace bongocat::animation { assert(anim_index >= 0); ctx.shm->dm20_anims[static_cast(anim_index)] = bongocat::move(result.result); - assert(ctx.shm->dm20_anims[static_cast(anim_index)].type == animation_t::Type::Dm); + assert(ctx.shm->dm20_anims[static_cast(anim_index)].type == animation_t::type_t::Dm); return bongocat_error_t::BONGOCAT_SUCCESS; } diff --git a/src/image_loader/dmall/CMakeLists.txt b/src/image_loader/dmall/CMakeLists.txt index 65ad38cb..202f229a 100644 --- a/src/image_loader/dmall/CMakeLists.txt +++ b/src/image_loader/dmall/CMakeLists.txt @@ -1,9 +1,11 @@ add_library(assets_dmall_loader STATIC) target_sources(assets_dmall_loader PRIVATE dmall_load_sprite_sheet.cpp load_images_dmall.cpp) target_compile_options(assets_dmall_loader PRIVATE -ffunction-sections -fdata-sections) -target_include_directories(assets_dmall_loader - PRIVATE ${INCLUDE_DIR}/embedded_assets/dmall ${INCLUDE_DIR}/image_loader/dmall - PUBLIC ${INCLUDE_DIR}) -target_link_libraries(assets_dmall_loader - PUBLIC assets_image_loader assets_base_dm_loader assets_dmall - PRIVATE assets_dmall_interface assets_dmall_feature bongocat_options) \ No newline at end of file +target_include_directories( + assets_dmall_loader + PRIVATE ${INCLUDE_DIR}/embedded_assets/dmall ${INCLUDE_DIR}/image_loader/dmall + PUBLIC ${INCLUDE_DIR}) +target_link_libraries( + assets_dmall_loader + PUBLIC assets_image_loader assets_base_dm_loader assets_dmall + PRIVATE assets_dmall_interface assets_dmall_feature bongocat_options) diff --git a/src/image_loader/dmall/dmall_load_sprite_sheet.cpp b/src/image_loader/dmall/dmall_load_sprite_sheet.cpp index 154a57a4..ad50beb5 100644 --- a/src/image_loader/dmall/dmall_load_sprite_sheet.cpp +++ b/src/image_loader/dmall/dmall_load_sprite_sheet.cpp @@ -1,5 +1,5 @@ #include "core/bongocat.h" -#include "graphics/animation_context.h" +#include "graphics/animation_thread_context.h" #include "graphics/sprite_sheet.h" #include "image_loader/base_dm/load_dm.h" #include "embedded_assets/dmall/dmall.hpp" @@ -8,7 +8,7 @@ #include "image_loader/dmall/load_images_dmall.h" namespace bongocat::animation { - created_result_t load_dmall_sprite_sheet(const animation_context_t& ctx, int index) { + created_result_t load_dmall_sprite_sheet(const animation_thread_context_t& ctx, int index) { using namespace assets; switch (index) { case DMALL_AEGISDRAMON_ANIM_INDEX: return load_dm_anim(ctx, DMALL_AEGISDRAMON_ANIM_INDEX, get_dmall_sprite_sheet(DMALL_AEGISDRAMON_ANIM_INDEX), DMALL_AEGISDRAMON_SPRITE_SHEET_COLS, DMALL_AEGISDRAMON_SPRITE_SHEET_ROWS); diff --git a/src/image_loader/dmall/load_images_dmall.cpp b/src/image_loader/dmall/load_images_dmall.cpp index 5393ed14..54c3ae4f 100644 --- a/src/image_loader/dmall/load_images_dmall.cpp +++ b/src/image_loader/dmall/load_images_dmall.cpp @@ -1,11 +1,11 @@ #include "load_images_dmall.h" -#include "graphics/animation_context.h" +#include "graphics/animation_thread_context.h" #include "image_loader/base_dm/load_dm.h" #include "embedded_assets/embedded_image.h" #include "embedded_assets/dmall/dmall.hpp" namespace bongocat::animation { - bongocat_error_t init_dmall_anim(animation_context_t& ctx, int anim_index, const assets::embedded_image_t& sprite_sheet_image, int sprite_sheet_cols, int sprite_sheet_rows) { + bongocat_error_t init_dmall_anim(animation_thread_context_t& ctx, int anim_index, const assets::embedded_image_t& sprite_sheet_image, int sprite_sheet_cols, int sprite_sheet_rows) { using namespace assets; BONGOCAT_CHECK_NULL(ctx.shm.ptr, bongocat_error_t::BONGOCAT_ERROR_INVALID_PARAM); BONGOCAT_CHECK_NULL(ctx._local_copy_config.ptr, bongocat_error_t::BONGOCAT_ERROR_INVALID_PARAM); @@ -21,7 +21,7 @@ namespace bongocat::animation { assert(anim_index >= 0); ctx.shm->dmall_anims[static_cast(anim_index)] = bongocat::move(result.result); - assert(ctx.shm->dmall_anims[static_cast(anim_index)].type == animation_t::Type::Dm); + assert(ctx.shm->dmall_anims[static_cast(anim_index)].type == animation_t::type_t::Dm); return bongocat_error_t::BONGOCAT_SUCCESS; } diff --git a/src/image_loader/dmc/CMakeLists.txt b/src/image_loader/dmc/CMakeLists.txt index 98be355c..a9881535 100644 --- a/src/image_loader/dmc/CMakeLists.txt +++ b/src/image_loader/dmc/CMakeLists.txt @@ -1,9 +1,11 @@ add_library(assets_dmc_loader STATIC) target_sources(assets_dmc_loader PRIVATE dmc_load_sprite_sheet.cpp load_images_dmc.cpp) target_compile_options(assets_dmc_loader PRIVATE -ffunction-sections -fdata-sections) -target_include_directories(assets_dmc_loader - PRIVATE ${INCLUDE_DIR}/embedded_assets/dmc ${INCLUDE_DIR}/image_loader/dmc - PUBLIC ${INCLUDE_DIR}) -target_link_libraries(assets_dmc_loader - PUBLIC assets_image_loader assets_base_dm_loader assets_dmc - PRIVATE assets_dmc_interface assets_dmc_feature bongocat_options) \ No newline at end of file +target_include_directories( + assets_dmc_loader + PRIVATE ${INCLUDE_DIR}/embedded_assets/dmc ${INCLUDE_DIR}/image_loader/dmc + PUBLIC ${INCLUDE_DIR}) +target_link_libraries( + assets_dmc_loader + PUBLIC assets_image_loader assets_base_dm_loader assets_dmc + PRIVATE assets_dmc_interface assets_dmc_feature bongocat_options) diff --git a/src/image_loader/dmc/dmc_load_sprite_sheet.cpp b/src/image_loader/dmc/dmc_load_sprite_sheet.cpp index 337a1ad7..2ccbac6b 100644 --- a/src/image_loader/dmc/dmc_load_sprite_sheet.cpp +++ b/src/image_loader/dmc/dmc_load_sprite_sheet.cpp @@ -1,5 +1,5 @@ #include "core/bongocat.h" -#include "graphics/animation_context.h" +#include "graphics/animation_thread_context.h" #include "graphics/sprite_sheet.h" #include "image_loader/base_dm/load_dm.h" #include "embedded_assets/dmc/dmc.hpp" @@ -8,7 +8,7 @@ #include "image_loader/dmc/load_images_dmc.h" namespace bongocat::animation { - created_result_t load_dmc_sprite_sheet(const animation_context_t& ctx, int index) { + created_result_t load_dmc_sprite_sheet(const animation_thread_context_t& ctx, int index) { using namespace assets; switch (index) { case DMC_AGUMON_ANIM_INDEX: return load_dm_anim(ctx, DMC_AGUMON_ANIM_INDEX, get_dmc_sprite_sheet(DMC_AGUMON_ANIM_INDEX), DMC_AGUMON_SPRITE_SHEET_COLS, DMC_AGUMON_SPRITE_SHEET_ROWS); diff --git a/src/image_loader/dmc/load_images_dmc.cpp b/src/image_loader/dmc/load_images_dmc.cpp index d219179c..d3622439 100644 --- a/src/image_loader/dmc/load_images_dmc.cpp +++ b/src/image_loader/dmc/load_images_dmc.cpp @@ -1,11 +1,11 @@ #include "load_images_dmc.h" -#include "graphics/animation_context.h" +#include "graphics/animation_thread_context.h" #include "image_loader/base_dm/load_dm.h" #include "embedded_assets/embedded_image.h" #include "embedded_assets/dmc/dmc.hpp" namespace bongocat::animation { - bongocat_error_t init_dmc_anim(animation_context_t& ctx, int anim_index, const assets::embedded_image_t& sprite_sheet_image, int sprite_sheet_cols, int sprite_sheet_rows) { + bongocat_error_t init_dmc_anim(animation_thread_context_t& ctx, int anim_index, const assets::embedded_image_t& sprite_sheet_image, int sprite_sheet_cols, int sprite_sheet_rows) { using namespace assets; BONGOCAT_CHECK_NULL(ctx.shm.ptr, bongocat_error_t::BONGOCAT_ERROR_INVALID_PARAM); BONGOCAT_CHECK_NULL(ctx._local_copy_config.ptr, bongocat_error_t::BONGOCAT_ERROR_INVALID_PARAM); @@ -21,7 +21,7 @@ namespace bongocat::animation { assert(anim_index >= 0); ctx.shm->dmc_anims[static_cast(anim_index)] = bongocat::move(result.result); - assert(ctx.shm->dmc_anims[static_cast(anim_index)].type == animation_t::Type::Dm); + assert(ctx.shm->dmc_anims[static_cast(anim_index)].type == animation_t::type_t::Dm); return bongocat_error_t::BONGOCAT_SUCCESS; } diff --git a/src/image_loader/dmx/CMakeLists.txt b/src/image_loader/dmx/CMakeLists.txt index 3fa89ba7..8c4cde37 100644 --- a/src/image_loader/dmx/CMakeLists.txt +++ b/src/image_loader/dmx/CMakeLists.txt @@ -1,9 +1,11 @@ add_library(assets_dmx_loader STATIC) target_sources(assets_dmx_loader PRIVATE dmx_load_sprite_sheet.cpp load_images_dmx.cpp) target_compile_options(assets_dmx_loader PRIVATE -ffunction-sections -fdata-sections) -target_include_directories(assets_dmx_loader - PRIVATE ${INCLUDE_DIR}/embedded_assets/dmx ${INCLUDE_DIR}/image_loader/dmx - PUBLIC ${INCLUDE_DIR}) -target_link_libraries(assets_dmx_loader - PUBLIC assets_image_loader assets_base_dm_loader assets_dmx - PRIVATE assets_dmx_interface assets_dmx_feature bongocat_options) \ No newline at end of file +target_include_directories( + assets_dmx_loader + PRIVATE ${INCLUDE_DIR}/embedded_assets/dmx ${INCLUDE_DIR}/image_loader/dmx + PUBLIC ${INCLUDE_DIR}) +target_link_libraries( + assets_dmx_loader + PUBLIC assets_image_loader assets_base_dm_loader assets_dmx + PRIVATE assets_dmx_interface assets_dmx_feature bongocat_options) diff --git a/src/image_loader/dmx/dmx_load_sprite_sheet.cpp b/src/image_loader/dmx/dmx_load_sprite_sheet.cpp index 3fd03ef8..c068f93b 100644 --- a/src/image_loader/dmx/dmx_load_sprite_sheet.cpp +++ b/src/image_loader/dmx/dmx_load_sprite_sheet.cpp @@ -1,5 +1,5 @@ #include "core/bongocat.h" -#include "graphics/animation_context.h" +#include "graphics/animation_thread_context.h" #include "graphics/sprite_sheet.h" #include "image_loader/base_dm/load_dm.h" #include "embedded_assets/dmx/dmx.hpp" @@ -8,7 +8,7 @@ #include "image_loader/dmx/load_images_dmx.h" namespace bongocat::animation { - created_result_t load_dmx_sprite_sheet(const animation_context_t& ctx, int index) { + created_result_t load_dmx_sprite_sheet(const animation_thread_context_t& ctx, int index) { using namespace assets; switch (index) { case DMX_AGUMON_BLACK_X_ANIM_INDEX: return load_dm_anim(ctx, DMX_AGUMON_BLACK_X_ANIM_INDEX, get_dmx_sprite_sheet(DMX_AGUMON_BLACK_X_ANIM_INDEX), DMX_AGUMON_BLACK_X_SPRITE_SHEET_COLS, DMX_AGUMON_BLACK_X_SPRITE_SHEET_ROWS); diff --git a/src/image_loader/dmx/load_images_dmx.cpp b/src/image_loader/dmx/load_images_dmx.cpp index 092f5106..0c4f1059 100644 --- a/src/image_loader/dmx/load_images_dmx.cpp +++ b/src/image_loader/dmx/load_images_dmx.cpp @@ -1,12 +1,12 @@ #include "load_images_dmx.h" -#include "graphics/animation_context.h" +#include "graphics/animation_thread_context.h" #include "image_loader/base_dm/load_dm.h" #include "embedded_assets/embedded_image.h" #include "embedded_assets/dmx/dmx.hpp" #include "utils/error.h" namespace bongocat::animation { - bongocat_error_t init_dmx_anim(animation_context_t& ctx, int anim_index, const assets::embedded_image_t& sprite_sheet_image, int sprite_sheet_cols, int sprite_sheet_rows) { + bongocat_error_t init_dmx_anim(animation_thread_context_t& ctx, int anim_index, const assets::embedded_image_t& sprite_sheet_image, int sprite_sheet_cols, int sprite_sheet_rows) { using namespace assets; BONGOCAT_CHECK_NULL(ctx.shm.ptr, bongocat_error_t::BONGOCAT_ERROR_INVALID_PARAM); BONGOCAT_CHECK_NULL(ctx._local_copy_config.ptr, bongocat_error_t::BONGOCAT_ERROR_INVALID_PARAM); @@ -22,7 +22,7 @@ namespace bongocat::animation { assert(anim_index >= 0); ctx.shm->dmx_anims[static_cast(anim_index)] = bongocat::move(result.result); - assert(ctx.shm->dmx_anims[static_cast(anim_index)].type == animation_t::Type::Dm); + assert(ctx.shm->dmx_anims[static_cast(anim_index)].type == animation_t::type_t::Dm); return bongocat_error_t::BONGOCAT_SUCCESS; } diff --git a/src/image_loader/load_images.cpp b/src/image_loader/load_images.cpp index ab149eb6..26a863c6 100644 --- a/src/image_loader/load_images.cpp +++ b/src/image_loader/load_images.cpp @@ -1,236 +1,255 @@ -#include "graphics/drawing.h" -#include "graphics/animation_context.h" +#include "image_loader/load_images.h" + #include "graphics/animation.h" +#include "graphics/animation_thread_context.h" +#include "graphics/drawing.h" #include "utils/memory.h" -#include "image_loader/load_images.h" + #include namespace bongocat::animation { - // ============================================================================= - // IMAGE LOADING MODULE - // ============================================================================= - - [[nodiscard]] static created_result_t load_sprite_sheet_from_memory(const uint8_t* sprite_data, size_t sprite_data_size, - int frame_columns, int frame_rows, - int padding_x, int padding_y) { - generic_sprite_sheet_t ret; - - auto [sprite_sheet, sprite_sheet_error] = load_image(sprite_data, sprite_data_size, RGBA_CHANNELS); - if (sprite_sheet_error != bongocat_error_t::BONGOCAT_SUCCESS) [[unlikely]] { - BONGOCAT_LOG_ERROR("Failed to load sprite sheet. %dx%d", sprite_sheet.width, sprite_sheet.height); - return sprite_sheet_error; - } - - assert(frame_columns != 0 && frame_rows != 0 && sprite_sheet.width % frame_columns == 0 && sprite_sheet.height % frame_rows == 0); - if (frame_columns == 0 || frame_rows == 0 || sprite_sheet.width % frame_columns != 0 || sprite_sheet.height % frame_rows != 0) { - BONGOCAT_LOG_ERROR("Sprite sheet dimensions not divisible by frame grid; frame_columns=%d, frame_rows=%d vs %dx%d sprite size", frame_columns, frame_rows, sprite_sheet.width, sprite_sheet.height); - return bongocat_error_t::BONGOCAT_ERROR_INVALID_PARAM; - } - - const auto frame_width = sprite_sheet.width / frame_columns; - const auto frame_height = sprite_sheet.height / frame_rows; - const auto total_frames = frame_columns * frame_rows; - - const auto dest_frame_width = frame_width + padding_x*2; - const auto dest_frame_height = frame_height + padding_y*2; - const auto dest_pixels_width = dest_frame_width * frame_columns; - const auto dest_pixels_height = dest_frame_height * frame_rows; - assert(dest_pixels_width >= 0); - assert(dest_pixels_height >= 0); - assert(sprite_sheet.channels >= 0); - const size_t dest_pixels_size = static_cast(dest_pixels_width) * static_cast(dest_pixels_height) * static_cast(sprite_sheet.channels); - auto dest_pixels = make_allocated_array(dest_pixels_size); - if (!dest_pixels) { - BONGOCAT_LOG_ERROR("Failed to allocate memory for dest_pixels (%zu bytes)\n", dest_pixels_size); - return bongocat_error_t::BONGOCAT_ERROR_MEMORY; - } - //memset(dest_pixels.data, 0, dest_pixels_size); - - const auto src_frame_width = frame_width; - const auto src_frame_height = frame_height; - const auto src_pixels_width = sprite_sheet.width; - const auto src_pixels_height = sprite_sheet.height; - assert(src_pixels_width >= 0); - assert(src_pixels_height >= 0); - assert(sprite_sheet.channels >= 0); - const size_t src_pixels_size = static_cast(src_pixels_width) * static_cast(src_pixels_height) * static_cast(sprite_sheet.channels); - size_t frame_index = 0; - for (int row = 0; row < frame_rows; ++row) { - for (int col = 0; col < frame_columns; ++col) { - const auto src_x = col * src_frame_width; - const auto src_y = row * src_frame_height; - const auto dst_x = col * dest_frame_width + padding_x; - const auto dst_y = row * dest_frame_height + padding_y; - [[maybe_unused]] const auto src_idx = (src_y * src_pixels_width + src_x) * sprite_sheet.channels; - [[maybe_unused]] const auto dst_idx = (dst_y * dest_pixels_width + dst_x) * sprite_sheet.channels; - assert(src_idx >= 0); - assert(dst_idx >= 0); - - bool set_frames = false; - for (int fy = 0; fy < src_frame_height; fy++) { - for (int fx = 0; fx < src_frame_width; fx++) { - const auto src_px_idx = ((src_y + fy) * src_pixels_width + (src_x + fx)) * sprite_sheet.channels; - const auto dst_px_idx = ((dst_y + fy) * dest_pixels_width + (dst_x + fx)) * sprite_sheet.channels; - - if (src_px_idx >= 0 && dst_px_idx >= 0 && - static_cast(src_px_idx) < src_pixels_size && - static_cast(dst_px_idx) < dest_pixels_size) { - drawing_copy_pixel(dest_pixels.data, sprite_sheet.channels, dst_px_idx, - sprite_sheet.pixels, sprite_sheet.channels, src_px_idx, - blit_image_color_option_flags_t::Normal, - blit_image_color_order_t::RGBA, - blit_image_color_order_t::RGBA); - if (!set_frames && frame_index < MAX_NUM_FRAMES) { - set_frames = true; - } - } - } - } - if (frame_index < MAX_NUM_FRAMES) { - if (set_frames) { - ret.frames[frame_index] = { .valid = true, .col = col, .row = row }; - } else { - ret.frames[frame_index] = { .valid = false, .col = 0, .row = 0, }; - } - } - - frame_index++; - } - } - - ret.image.sprite_sheet_width = sprite_sheet.width; - ret.image.sprite_sheet_height = sprite_sheet.height; - ret.image.channels = sprite_sheet.channels; - // move pixels ownership into out_frames - ret.image.pixels = bongocat::move(dest_pixels); - dest_pixels = nullptr; - ret.frame_width = dest_frame_width; - ret.frame_height = dest_frame_height; - ret.total_frames = total_frames; - - return ret; - } - - created_result_t anim_sprite_sheet_from_embedded_images(get_sprite_callback_t get_sprite, size_t embedded_images_count) { - generic_sprite_sheet_t ret; - - int total_frames = 0; - int max_frame_width = 0; - int max_frame_height = 0; - int max_channels = 0; - auto loaded_images = make_allocated_array(embedded_images_count); - for (size_t i = 0;i < embedded_images_count && i < loaded_images.count; i++) { - const assets::embedded_image_t img = get_sprite(i); - - BONGOCAT_LOG_DEBUG("Loading embedded image: %s", img.name); - auto [loaded_image, image_error] = load_image(img.data, img.size, RGBA_CHANNELS); - if (image_error != bongocat_error_t::BONGOCAT_SUCCESS) [[unlikely]] { - BONGOCAT_LOG_ERROR("Failed to load embedded image: %s (%d)", img.name, image_error); - continue; +// ============================================================================= +// IMAGE LOADING MODULE +// ============================================================================= + +BONGOCAT_NODISCARD static created_result_t +load_sprite_sheet_from_memory(const uint8_t *sprite_data, size_t sprite_data_size, int frame_columns, int frame_rows, + int padding_x, int padding_y) { + generic_sprite_sheet_t ret; + + auto [sprite_sheet, sprite_sheet_error] = load_image(sprite_data, sprite_data_size, RGBA_CHANNELS); + if (sprite_sheet_error != bongocat_error_t::BONGOCAT_SUCCESS) [[unlikely]] { + BONGOCAT_LOG_ERROR("Failed to load sprite sheet. %dx%d", sprite_sheet.width, sprite_sheet.height); + return sprite_sheet_error; + } + + assert(frame_columns != 0 && frame_rows != 0 && sprite_sheet.width % frame_columns == 0 && + sprite_sheet.height % frame_rows == 0); + if (frame_columns == 0 || frame_rows == 0 || sprite_sheet.width % frame_columns != 0 || + sprite_sheet.height % frame_rows != 0) { + BONGOCAT_LOG_ERROR( + "Sprite sheet dimensions not divisible by frame grid; frame_columns=%d, frame_rows=%d vs %dx%d sprite size", + frame_columns, frame_rows, sprite_sheet.width, sprite_sheet.height); + return bongocat_error_t::BONGOCAT_ERROR_INVALID_PARAM; + } + + const auto frame_width = sprite_sheet.width / frame_columns; + const auto frame_height = sprite_sheet.height / frame_rows; + const auto total_frames = frame_columns * frame_rows; + + const auto dest_frame_width = frame_width + (padding_x * 2); + const auto dest_frame_height = frame_height + (padding_y * 2); + const auto dest_pixels_width = dest_frame_width * frame_columns; + const auto dest_pixels_height = dest_frame_height * frame_rows; + assert(dest_pixels_width >= 0); + assert(dest_pixels_height >= 0); + assert(sprite_sheet.channels >= 0); + const size_t dest_pixels_size = static_cast(dest_pixels_width) * static_cast(dest_pixels_height) * + static_cast(sprite_sheet.channels); + auto dest_pixels = make_allocated_array(dest_pixels_size); + if (!dest_pixels) { + BONGOCAT_LOG_ERROR("Failed to allocate memory for dest_pixels (%zu bytes)\n", dest_pixels_size); + return bongocat_error_t::BONGOCAT_ERROR_MEMORY; + } + // memset(dest_pixels.data, 0, dest_pixels_size); + + const auto src_frame_width = frame_width; + const auto src_frame_height = frame_height; + const auto src_pixels_width = sprite_sheet.width; + const auto src_pixels_height = sprite_sheet.height; + assert(src_pixels_width >= 0); + assert(src_pixels_height >= 0); + assert(sprite_sheet.channels >= 0); + const size_t src_pixels_size = static_cast(src_pixels_width) * static_cast(src_pixels_height) * + static_cast(sprite_sheet.channels); + size_t frame_index = 0; + for (int row = 0; row < frame_rows; ++row) { + for (int col = 0; col < frame_columns; ++col) { + const auto src_x = col * src_frame_width; + const auto src_y = row * src_frame_height; + const auto dst_x = (col * dest_frame_width) + padding_x; + const auto dst_y = (row * dest_frame_height) + padding_y; + [[maybe_unused]] const auto src_idx = (src_y * src_pixels_width + src_x) * sprite_sheet.channels; + [[maybe_unused]] const auto dst_idx = (dst_y * dest_pixels_width + dst_x) * sprite_sheet.channels; + assert(src_idx >= 0); + assert(dst_idx >= 0); + + bool set_frames = false; + for (int fy = 0; fy < src_frame_height; fy++) { + for (int fx = 0; fx < src_frame_width; fx++) { + const auto src_px_idx = ((src_y + fy) * src_pixels_width + (src_x + fx)) * sprite_sheet.channels; + const auto dst_px_idx = ((dst_y + fy) * dest_pixels_width + (dst_x + fx)) * sprite_sheet.channels; + + if (src_px_idx >= 0 && dst_px_idx >= 0 && static_cast(src_px_idx) < src_pixels_size && + static_cast(dst_px_idx) < dest_pixels_size) { + drawing_copy_pixel(dest_pixels.data, sprite_sheet.channels, dst_px_idx, sprite_sheet.pixels, + sprite_sheet.channels, src_px_idx, blit_image_color_option_flags_t::Normal, + blit_image_color_order_t::RGBA, blit_image_color_order_t::RGBA); + if (!set_frames && frame_index < MAX_NUM_FRAMES) { + set_frames = true; } - loaded_images[i] = bongocat::move(loaded_image); - assert(loaded_images[i].width >= 0); - assert(loaded_images[i].height >= 0); - assert(loaded_images[i].channels >= 0); - - // update image properties - max_frame_width = loaded_images[i].width > max_frame_width ? loaded_images[i].width : max_frame_width; - max_frame_height = loaded_images[i].height > max_frame_height ? loaded_images[i].height : max_frame_height; - max_channels = loaded_images[i].channels > max_channels ? loaded_images[i].channels : max_channels; - - BONGOCAT_LOG_DEBUG("Loaded %dx%d embedded image", loaded_images[i].width, loaded_images[i].height); - total_frames++; + } } - - ret.frame_width = max_frame_width; - ret.frame_height = max_frame_height; - ret.total_frames = total_frames; - ret.image.sprite_sheet_width = max_frame_width * total_frames; - ret.image.sprite_sheet_height = max_frame_height; - ret.image.channels = max_channels; - // create sprite sheet - assert(ret.image.sprite_sheet_width >= 0); - assert(ret.image.sprite_sheet_height >= 0); - assert(ret.image.channels >= 0); - ret.image.pixels = make_allocated_array(static_cast(ret.image.sprite_sheet_width) * static_cast(ret.image.sprite_sheet_height) * static_cast(ret.image.channels)); - if (!ret.image.pixels) { - ret.frame_width = 0; - ret.frame_height = 0; - ret.total_frames = 0; - ret.image.sprite_sheet_width = 0; - ret.image.sprite_sheet_height = 0; - ret.image.channels = 0; - - for (size_t i = 0; i < loaded_images.count; i++) { - if (loaded_images[i].pixels) ::free(loaded_images[i].pixels); - loaded_images[i].pixels = nullptr; - } - return bongocat_error_t::BONGOCAT_ERROR_MEMORY; - } - // reset frames - //memset(anim->pixels.data, 0, anim->pixels.count * sizeof(uint8_t)); - for (size_t i = 0;i < MAX_NUM_FRAMES;i++) { - ret.frames[i] = {}; - } - // append images into one sprite sheet - assert(max_frame_width >= 0); - assert(max_channels >= 0); - assert(ret.image.sprite_sheet_width >= 0); - for (size_t frame = 0; frame < loaded_images.count; frame++) { - const auto& src = loaded_images[frame]; - assert(src.channels >= 0); - assert(src.width >= 0); - assert(src.height >= 0); - if (src.pixels && src.height > 0) { - // copy pixel data of sub-region - assert(src.height >= 0); - for (size_t y = 0; y < static_cast(src.height); y++) { - unsigned char* dest_row = ret.image.pixels.data + - ((y) * static_cast(ret.image.sprite_sheet_width) + (frame * static_cast(max_frame_width))) * static_cast(max_channels); - const unsigned char* src_row = src.pixels + (y * static_cast(src.width) * static_cast(src.channels)); - memcpy(dest_row, src_row, static_cast(src.width) * static_cast(max_channels)); - } - - // update sub-region - if (frame < MAX_NUM_FRAMES) { - ret.frames[frame] = { .valid = true, .col = static_cast(frame), .row = 0 }; - } - } else { - if (frame < MAX_NUM_FRAMES) { - ret.frames[frame] = { .valid = false, .col = static_cast(frame), .row = 0 }; - } - } + } + if (frame_index < MAX_NUM_FRAMES) { + if (set_frames) { + ret.frames[frame_index] = {.valid = true, .col = col, .row = row}; + } else { + ret.frames[frame_index] = { + .valid = false, + .col = 0, + .row = 0, + }; } + } - - for (size_t i = 0; i < loaded_images.count; i++) { - if (loaded_images[i].pixels) ::free(loaded_images[i].pixels); - loaded_images[i].pixels = nullptr; - } - return ret; + frame_index++; } + } + + ret.image.sprite_sheet_width = sprite_sheet.width; + ret.image.sprite_sheet_height = sprite_sheet.height; + ret.image.channels = sprite_sheet.channels; + // move pixels ownership into out_frames + ret.image.pixels = bongocat::move(dest_pixels); + dest_pixels = BONGOCAT_NULLPTR; + ret.frame_width = dest_frame_width; + ret.frame_height = dest_frame_height; + ret.total_frames = total_frames; + + return ret; +} + +created_result_t anim_sprite_sheet_from_embedded_images(get_sprite_callback_t get_sprite, + size_t embedded_images_count) { + generic_sprite_sheet_t ret; + + int total_frames = 0; + int max_frame_width = 0; + int max_frame_height = 0; + int max_channels = 0; + auto loaded_images = make_allocated_array(embedded_images_count); + for (size_t i = 0; i < embedded_images_count && i < loaded_images.count; i++) { + const assets::embedded_image_t img = get_sprite(i); + + BONGOCAT_LOG_DEBUG("Loading embedded image: %s", img.name); + auto [loaded_image, image_error] = load_image(img.data, img.size, RGBA_CHANNELS); + if (image_error != bongocat_error_t::BONGOCAT_SUCCESS) [[unlikely]] { + BONGOCAT_LOG_ERROR("Failed to load embedded image: %s (%d)", img.name, image_error); + continue; + } + loaded_images[i] = bongocat::move(loaded_image); + assert(loaded_images[i].width >= 0); + assert(loaded_images[i].height >= 0); + assert(loaded_images[i].channels >= 0); + + // update image properties + max_frame_width = loaded_images[i].width > max_frame_width ? loaded_images[i].width : max_frame_width; + max_frame_height = loaded_images[i].height > max_frame_height ? loaded_images[i].height : max_frame_height; + max_channels = loaded_images[i].channels > max_channels ? loaded_images[i].channels : max_channels; + + BONGOCAT_LOG_DEBUG("Loaded %dx%d embedded image", loaded_images[i].width, loaded_images[i].height); + total_frames++; + } + + ret.frame_width = max_frame_width; + ret.frame_height = max_frame_height; + ret.total_frames = total_frames; + ret.image.sprite_sheet_width = max_frame_width * total_frames; + ret.image.sprite_sheet_height = max_frame_height; + ret.image.channels = max_channels; + // create sprite sheet + assert(ret.image.sprite_sheet_width >= 0); + assert(ret.image.sprite_sheet_height >= 0); + assert(ret.image.channels >= 0); + ret.image.pixels = make_allocated_array(static_cast(ret.image.sprite_sheet_width) * + static_cast(ret.image.sprite_sheet_height) * + static_cast(ret.image.channels)); + if (!ret.image.pixels) { + ret.frame_width = 0; + ret.frame_height = 0; + ret.total_frames = 0; + ret.image.sprite_sheet_width = 0; + ret.image.sprite_sheet_height = 0; + ret.image.channels = 0; + + for (size_t i = 0; i < loaded_images.count; i++) { + if (loaded_images[i].pixels != BONGOCAT_NULLPTR) { + ::free(loaded_images[i].pixels); + loaded_images[i].pixels = BONGOCAT_NULLPTR; + } + } + return bongocat_error_t::BONGOCAT_ERROR_MEMORY; + } + // reset frames + // memset(anim->pixels.data, 0, anim->pixels.count * sizeof(uint8_t)); + for (size_t i = 0; i < MAX_NUM_FRAMES; i++) { + ret.frames[i] = {}; + } + // append images into one sprite sheet + assert(max_frame_width >= 0); + assert(max_channels >= 0); + assert(ret.image.sprite_sheet_width >= 0); + for (size_t frame = 0; frame < loaded_images.count; frame++) { + const auto& src = loaded_images[frame]; + assert(src.channels >= 0); + assert(src.width >= 0); + assert(src.height >= 0); + if (src.pixels != BONGOCAT_NULLPTR && src.height > 0) { + // copy pixel data of sub-region + assert(src.height >= 0); + for (size_t y = 0; y < static_cast(src.height); y++) { + unsigned char *dest_row = ret.image.pixels.data + ((y * static_cast(ret.image.sprite_sheet_width)) + + (frame * static_cast(max_frame_width))) * + static_cast(max_channels); + const unsigned char *src_row = + src.pixels + (y * static_cast(src.width) * static_cast(src.channels)); + memcpy(dest_row, src_row, static_cast(src.width) * static_cast(max_channels)); + } + + // update sub-region + if (frame < MAX_NUM_FRAMES) { + ret.frames[frame] = {.valid = true, .col = static_cast(frame), .row = 0}; + } + } else { + if (frame < MAX_NUM_FRAMES) { + ret.frames[frame] = {.valid = false, .col = static_cast(frame), .row = 0}; + } + } + } - created_result_t load_sprite_sheet_anim(const config::config_t& config, const assets::embedded_image_t& sprite_sheet_image, int sprite_sheet_cols, int sprite_sheet_rows) { - if (sprite_sheet_cols < 0 || sprite_sheet_rows < 0) { - return bongocat_error_t::BONGOCAT_ERROR_INVALID_PARAM; - } - - assert(sprite_sheet_image.size <= INT_MAX); - auto result = load_sprite_sheet_from_memory(sprite_sheet_image.data, sprite_sheet_image.size, - sprite_sheet_cols, sprite_sheet_rows, - config.padding_x, config.padding_y); - if (result.error != bongocat_error_t::BONGOCAT_SUCCESS) [[unlikely]] { - BONGOCAT_LOG_ERROR("Sprite Sheet load failed: %s", sprite_sheet_image.name); - return result.error; - } - if (result.result.total_frames <= 0) { - BONGOCAT_LOG_ERROR("Sprite Sheet is empty: %s", sprite_sheet_image.name); - return bongocat_error_t::BONGOCAT_ERROR_ANIMATION; - } - - // assume every frame is the same size, pick first frame - BONGOCAT_LOG_DEBUG("Loaded %dx%d sprite sheet with %d frames", result.result.image.sprite_sheet_width, result.result.image.sprite_sheet_height, result.result.total_frames); - - return result; + for (size_t i = 0; i < loaded_images.count; i++) { + if (loaded_images[i].pixels != BONGOCAT_NULLPTR) { + ::free(loaded_images[i].pixels); + loaded_images[i].pixels = BONGOCAT_NULLPTR; } -} \ No newline at end of file + } + return ret; +} + +created_result_t load_sprite_sheet_anim(const config::config_t& config, + const assets::embedded_image_t& sprite_sheet_image, + int sprite_sheet_cols, int sprite_sheet_rows) { + if (sprite_sheet_cols < 0 || sprite_sheet_rows < 0) { + return bongocat_error_t::BONGOCAT_ERROR_INVALID_PARAM; + } + + assert(sprite_sheet_image.size <= INT_MAX); + auto result = load_sprite_sheet_from_memory(sprite_sheet_image.data, sprite_sheet_image.size, sprite_sheet_cols, + sprite_sheet_rows, config.padding_x, config.padding_y); + if (result.error != bongocat_error_t::BONGOCAT_SUCCESS) [[unlikely]] { + BONGOCAT_LOG_ERROR("Sprite Sheet load failed: %s", sprite_sheet_image.name); + return result.error; + } + if (result.result.total_frames <= 0) { + BONGOCAT_LOG_ERROR("Sprite Sheet is empty: %s", sprite_sheet_image.name); + return bongocat_error_t::BONGOCAT_ERROR_ANIMATION; + } + + // assume every frame is the same size, pick first frame + BONGOCAT_LOG_DEBUG("Loaded %dx%d sprite sheet with %d frames", result.result.image.sprite_sheet_width, + result.result.image.sprite_sheet_height, result.result.total_frames); + + return result; +} +} // namespace bongocat::animation \ No newline at end of file diff --git a/src/image_loader/load_images_hybrid.cpp b/src/image_loader/load_images_hybrid.cpp index 4e07bc7a..a5fe4d3b 100644 --- a/src/image_loader/load_images_hybrid.cpp +++ b/src/image_loader/load_images_hybrid.cpp @@ -1,149 +1,159 @@ #include "image_loader/load_images.h" + #include #include #include // include pngle #if defined(__GNUC__) || defined(__clang__) -#pragma GCC diagnostic push -#pragma GCC diagnostic ignored "-Wdouble-promotion" -#pragma GCC diagnostic ignored "-Wsign-compare" -#pragma GCC diagnostic ignored "-Wunused-function" -#pragma GCC diagnostic ignored "-Wold-style-cast" -#pragma GCC diagnostic ignored "-Wsign-conversion" -#pragma GCC diagnostic ignored "-Wcast-align" -#pragma GCC diagnostic ignored "-Wconversion" -#if defined(__GNUC__) && !defined(__clang__) -#pragma GCC diagnostic ignored "-Wduplicated-branches" -#pragma GCC diagnostic ignored "-Wuseless-cast" -//#pragma GCC diagnostic ignored "-Wimplicit-int-conversion" -#endif +# pragma GCC diagnostic push +# pragma GCC diagnostic ignored "-Wdouble-promotion" +# pragma GCC diagnostic ignored "-Wsign-compare" +# pragma GCC diagnostic ignored "-Wunused-function" +# pragma GCC diagnostic ignored "-Wold-style-cast" +# pragma GCC diagnostic ignored "-Wsign-conversion" +# pragma GCC diagnostic ignored "-Wcast-align" +# pragma GCC diagnostic ignored "-Wconversion" +# if defined(__GNUC__) && !defined(__clang__) +# pragma GCC diagnostic ignored "-Wduplicated-branches" +# pragma GCC diagnostic ignored "-Wuseless-cast" +// #pragma GCC diagnostic ignored "-Wimplicit-int-conversion" +# endif #endif #include "pngle.h" #if defined(__GNUC__) || defined(__clang__) -#pragma GCC diagnostic pop +# pragma GCC diagnostic pop #endif // include stb_image #if defined(__GNUC__) || defined(__clang__) -#pragma GCC diagnostic push -#pragma GCC diagnostic ignored "-Wdouble-promotion" -#pragma GCC diagnostic ignored "-Wsign-compare" -#pragma GCC diagnostic ignored "-Wunused-function" -#pragma GCC diagnostic ignored "-Wold-style-cast" -#pragma GCC diagnostic ignored "-Wsign-conversion" -#pragma GCC diagnostic ignored "-Wcast-align" -#pragma GCC diagnostic ignored "-Wconversion" -#if defined(__GNUC__) && !defined(__clang__) -#pragma GCC diagnostic ignored "-Wduplicated-branches" -#pragma GCC diagnostic ignored "-Wuseless-cast" -//#pragma GCC diagnostic ignored "-Wimplicit-int-conversion" -#endif +# pragma GCC diagnostic push +# pragma GCC diagnostic ignored "-Wdouble-promotion" +# pragma GCC diagnostic ignored "-Wsign-compare" +# pragma GCC diagnostic ignored "-Wunused-function" +# pragma GCC diagnostic ignored "-Wold-style-cast" +# pragma GCC diagnostic ignored "-Wsign-conversion" +# pragma GCC diagnostic ignored "-Wcast-align" +# pragma GCC diagnostic ignored "-Wconversion" +# if defined(__GNUC__) && !defined(__clang__) +# pragma GCC diagnostic ignored "-Wduplicated-branches" +# pragma GCC diagnostic ignored "-Wuseless-cast" +// #pragma GCC diagnostic ignored "-Wimplicit-int-conversion" +# endif #endif #include "stb_image.h" #if defined(__GNUC__) || defined(__clang__) -#pragma GCC diagnostic pop +# pragma GCC diagnostic pop #endif namespace bongocat::animation { - inline static constexpr size_t HybridImageBackendPngleThresholdBytes = 192 * 1024; // 192kb - - struct decode_state_t { - Image *image{nullptr}; - int desired_channels{RGBA_CHANNELS}; - }; - [[nodiscard]] static created_result_t load_image_pngle(const unsigned char *data, size_t size, int desired_channels) { - Image ret; - pngle_t *pngle = pngle_new(); - if (!pngle) { - return bongocat_error_t::BONGOCAT_ERROR_IMAGE; - } - - decode_state_t state { .image = &ret, .desired_channels = desired_channels }; - - // Pixel callback: pngle calls this for each RGBA pixel - pngle_set_draw_callback(pngle, - [](pngle_t *p_pngle, uint32_t x, uint32_t y, [[maybe_unused]] uint32_t w, [[maybe_unused]] uint32_t h, const uint8_t rgba[4]) { - auto *st = static_cast(pngle_get_user_data(p_pngle)); - Image *img = st->image; - - constexpr uint32_t channels = 4; - if (!img->pixels) { - // Allocate buffer on first pixel - img->width = static_cast(pngle_get_width(p_pngle)); - img->height = static_cast(pngle_get_height(p_pngle)); - img->channels = channels; // pngle always gives RGBA - size_t buf_size = pngle_get_width(p_pngle) * pngle_get_height(p_pngle) * channels; - img->pixels = static_cast(::malloc(buf_size)); - if (!img->pixels) return; - } - - unsigned char *dst = &img->pixels[(y * pngle_get_width(p_pngle) + x) * channels]; - dst[0] = rgba[0]; - dst[1] = rgba[1]; - dst[2] = rgba[2]; - dst[3] = rgba[3]; - }); - - pngle_set_user_data(pngle, &state); - - // Feed the PNG data - const int fed = pngle_feed(pngle, data, size); - if (fed < 0) { - pngle_destroy(pngle); - if (ret.pixels) ::free(ret.pixels); - ret.pixels = nullptr; - return bongocat_error_t::BONGOCAT_ERROR_IMAGE; - } - - pngle_destroy(pngle); - - if (!ret.pixels) { - return bongocat_error_t::BONGOCAT_ERROR_IMAGE; - } - - // Handle desired_channels (pngle gives RGBA only) - if (desired_channels > 0 && desired_channels != 4) { - // Optionally strip alpha or replicate grayscale here - // For now, just return RGBA - ret.channels = 4; - } - - assert(ret.width > 0); - assert(ret.height > 0); - return ret; +inline static constexpr size_t HybridImageBackendPngleThresholdBytes = 192zu * 1024zu; // 192kb + +struct decode_state_t { + Image *image{BONGOCAT_NULLPTR}; + int desired_channels{RGBA_CHANNELS}; +}; +BONGOCAT_NODISCARD static created_result_t load_image_pngle(const unsigned char *data, size_t size, + int desired_channels) { + Image ret; + pngle_t *pngle = pngle_new(); + if (pngle == BONGOCAT_NULLPTR) { + return bongocat_error_t::BONGOCAT_ERROR_IMAGE; + } + + decode_state_t state{.image = &ret, .desired_channels = desired_channels}; + + // Pixel callback: pngle calls this for each RGBA pixel + pngle_set_draw_callback(pngle, [](pngle_t *p_pngle, uint32_t x, uint32_t y, [[maybe_unused]] uint32_t w, + [[maybe_unused]] uint32_t h, const uint8_t rgba[4]) { + auto *st = static_cast(pngle_get_user_data(p_pngle)); + Image *img = st->image; + + constexpr uint32_t channels = 4; + if (!img->pixels) { + // Allocate buffer on first pixel + img->width = static_cast(pngle_get_width(p_pngle)); + img->height = static_cast(pngle_get_height(p_pngle)); + img->channels = channels; // pngle always gives RGBA + const size_t buf_size = pngle_get_width(p_pngle) * pngle_get_height(p_pngle) * channels; + img->pixels = static_cast(::malloc(buf_size)); + if (!img->pixels) { + return; + } } - [[nodiscard]] static created_result_t load_image_stb_image(const unsigned char *data, size_t size, int desired_channels) { - Image ret; - assert(size <= INT_MAX); - int channels_in_file; - ret.pixels = stbi_load_from_memory(data, static_cast(size), &ret.width, &ret.height, &channels_in_file, desired_channels); - if (ret.pixels == nullptr) { - ret.pixels = nullptr; - return bongocat_error_t::BONGOCAT_ERROR_IMAGE; - } - assert(ret.width > 0); - assert(ret.height > 0); - assert(channels_in_file > 0); - ret.channels = desired_channels; - return ret; - } - - created_result_t load_image(const unsigned char *data, size_t size, int desired_channels) { - if (size >= HybridImageBackendPngleThresholdBytes) { - return load_image_pngle(data, size, desired_channels); - } - - return load_image_stb_image(data, size, desired_channels); - } - - void cleanup_image(Image& image) { - if (image.pixels) ::free(image.pixels); - image.pixels = nullptr; - } - - void init_image_loader() { - + unsigned char *dst = &img->pixels[(y * pngle_get_width(p_pngle) + x) * channels]; + dst[0] = rgba[0]; + dst[1] = rgba[1]; + dst[2] = rgba[2]; + dst[3] = rgba[3]; + }); + + pngle_set_user_data(pngle, &state); + + // Feed the PNG data + const int fed = pngle_feed(pngle, data, size); + if (fed < 0) { + pngle_destroy(pngle); + pngle = BONGOCAT_NULLPTR; + if (ret.pixels != BONGOCAT_NULLPTR) { + ::free(ret.pixels); + ret.pixels = BONGOCAT_NULLPTR; } -} \ No newline at end of file + return bongocat_error_t::BONGOCAT_ERROR_IMAGE; + } + + pngle_destroy(pngle); + pngle = BONGOCAT_NULLPTR; + + if (ret.pixels == BONGOCAT_NULLPTR) { + return bongocat_error_t::BONGOCAT_ERROR_IMAGE; + } + + // Handle desired_channels (pngle gives RGBA only) + if (desired_channels > 0 && desired_channels != 4) { + // Optionally strip alpha or replicate grayscale here + // For now, just return RGBA + ret.channels = 4; + } + + assert(ret.width > 0); + assert(ret.height > 0); + return ret; +} + +BONGOCAT_NODISCARD static created_result_t load_image_stb_image(const unsigned char *data, size_t size, + int desired_channels) { + Image ret; + assert(size <= INT_MAX); + int channels_in_file; + ret.pixels = + stbi_load_from_memory(data, static_cast(size), &ret.width, &ret.height, &channels_in_file, desired_channels); + if (ret.pixels == BONGOCAT_NULLPTR) { + ret.pixels = BONGOCAT_NULLPTR; + return bongocat_error_t::BONGOCAT_ERROR_IMAGE; + } + assert(ret.width > 0); + assert(ret.height > 0); + assert(channels_in_file > 0); + ret.channels = desired_channels; + return ret; +} + +created_result_t load_image(const unsigned char *data, size_t size, int desired_channels) { + if (size >= HybridImageBackendPngleThresholdBytes) { + return load_image_pngle(data, size, desired_channels); + } + + return load_image_stb_image(data, size, desired_channels); +} + +void cleanup_image(Image& image) { + if (image.pixels != BONGOCAT_NULLPTR) { + ::free(image.pixels); + image.pixels = BONGOCAT_NULLPTR; + } +} + +void init_image_loader() {} +} // namespace bongocat::animation \ No newline at end of file diff --git a/src/image_loader/load_images_pngle.cpp b/src/image_loader/load_images_pngle.cpp index f3de2d3f..c2711d6a 100644 --- a/src/image_loader/load_images_pngle.cpp +++ b/src/image_loader/load_images_pngle.cpp @@ -1,102 +1,109 @@ #include "image_loader/load_images.h" + #include #include #include // include pngle #if defined(__GNUC__) || defined(__clang__) -#pragma GCC diagnostic push -#pragma GCC diagnostic ignored "-Wdouble-promotion" -#pragma GCC diagnostic ignored "-Wsign-compare" -#pragma GCC diagnostic ignored "-Wunused-function" -#pragma GCC diagnostic ignored "-Wold-style-cast" -#pragma GCC diagnostic ignored "-Wsign-conversion" -#pragma GCC diagnostic ignored "-Wcast-align" -#pragma GCC diagnostic ignored "-Wconversion" -#if defined(__GNUC__) && !defined(__clang__) -#pragma GCC diagnostic ignored "-Wduplicated-branches" -#pragma GCC diagnostic ignored "-Wuseless-cast" -//#pragma GCC diagnostic ignored "-Wimplicit-int-conversion" -#endif +# pragma GCC diagnostic push +# pragma GCC diagnostic ignored "-Wdouble-promotion" +# pragma GCC diagnostic ignored "-Wsign-compare" +# pragma GCC diagnostic ignored "-Wunused-function" +# pragma GCC diagnostic ignored "-Wold-style-cast" +# pragma GCC diagnostic ignored "-Wsign-conversion" +# pragma GCC diagnostic ignored "-Wcast-align" +# pragma GCC diagnostic ignored "-Wconversion" +# if defined(__GNUC__) && !defined(__clang__) +# pragma GCC diagnostic ignored "-Wduplicated-branches" +# pragma GCC diagnostic ignored "-Wuseless-cast" +// #pragma GCC diagnostic ignored "-Wimplicit-int-conversion" +# endif #endif #include "pngle.h" #if defined(__GNUC__) || defined(__clang__) -#pragma GCC diagnostic pop +# pragma GCC diagnostic pop #endif namespace bongocat::animation { - struct decode_state_t { - Image *image{nullptr}; - int desired_channels{RGBA_CHANNELS}; - }; - created_result_t load_image(const unsigned char *data, size_t size, int desired_channels) { - Image ret; - pngle_t *pngle = pngle_new(); - if (!pngle) { - return bongocat_error_t::BONGOCAT_ERROR_IMAGE; - } +struct decode_state_t { + Image *image{BONGOCAT_NULLPTR}; + int desired_channels{RGBA_CHANNELS}; +}; +created_result_t load_image(const unsigned char *data, size_t size, int desired_channels) { + Image ret; + pngle_t *pngle = pngle_new(); + if (pngle == BONGOCAT_NULLPTR) { + return bongocat_error_t::BONGOCAT_ERROR_IMAGE; + } - decode_state_t state { .image = &ret, .desired_channels = desired_channels }; + decode_state_t state{.image = &ret, .desired_channels = desired_channels}; - // Pixel callback: pngle calls this for each RGBA pixel - pngle_set_draw_callback(pngle, - [](pngle_t *p_pngle, uint32_t x, uint32_t y, [[maybe_unused]] uint32_t w, [[maybe_unused]] uint32_t h, const uint8_t rgba[4]) { - auto *st = static_cast(pngle_get_user_data(p_pngle)); - Image *img = st->image; + // Pixel callback: pngle calls this for each RGBA pixel + pngle_set_draw_callback(pngle, [](pngle_t *p_pngle, uint32_t x, uint32_t y, [[maybe_unused]] uint32_t w, + [[maybe_unused]] uint32_t h, const uint8_t rgba[4]) { + auto *st = static_cast(pngle_get_user_data(p_pngle)); + Image *img = st->image; - constexpr uint32_t channels = 4; - if (!img->pixels) { - // Allocate buffer on first pixel - img->width = static_cast(pngle_get_width(p_pngle)); - img->height = static_cast(pngle_get_height(p_pngle)); - img->channels = channels; // pngle always gives RGBA - size_t buf_size = pngle_get_width(p_pngle) * pngle_get_height(p_pngle) * channels; - img->pixels = static_cast(::malloc(buf_size)); - if (!img->pixels) return; - } - - unsigned char *dst = &img->pixels[(y * pngle_get_width(p_pngle) + x) * channels]; - dst[0] = rgba[0]; - dst[1] = rgba[1]; - dst[2] = rgba[2]; - dst[3] = rgba[3]; - }); + constexpr uint32_t channels = 4; + if (!img->pixels) { + // Allocate buffer on first pixel + img->width = static_cast(pngle_get_width(p_pngle)); + img->height = static_cast(pngle_get_height(p_pngle)); + img->channels = channels; // pngle always gives RGBA + const size_t buf_size = pngle_get_width(p_pngle) * pngle_get_height(p_pngle) * channels; + img->pixels = static_cast(::malloc(buf_size)); + if (img->pixels == BONGOCAT_NULLPTR) { + return; + } + } - pngle_set_user_data(pngle, &state); + unsigned char *dst = &img->pixels[(y * pngle_get_width(p_pngle) + x) * channels]; + dst[0] = rgba[0]; + dst[1] = rgba[1]; + dst[2] = rgba[2]; + dst[3] = rgba[3]; + }); - // Feed the PNG data - const int fed = pngle_feed(pngle, data, size); - if (fed < 0) { - pngle_destroy(pngle); - if (ret.pixels) ::free(ret.pixels); - ret.pixels = nullptr; - return bongocat_error_t::BONGOCAT_ERROR_IMAGE; - } + pngle_set_user_data(pngle, &state); - pngle_destroy(pngle); + // Feed the PNG data + const int fed = pngle_feed(pngle, data, size); + if (fed < 0) { + pngle_destroy(pngle); + pngle = BONGOCAT_NULLPTR; + if (ret.pixels != BONGOCAT_NULLPTR) { + ::free(ret.pixels); + ret.pixels = BONGOCAT_NULLPTR; + } + return bongocat_error_t::BONGOCAT_ERROR_IMAGE; + } - if (!ret.pixels) { - return bongocat_error_t::BONGOCAT_ERROR_IMAGE; - } + pngle_destroy(pngle); + pngle = BONGOCAT_NULLPTR; - // Handle desired_channels (pngle gives RGBA only) - if (desired_channels > 0 && desired_channels != 4) { - // Optionally strip alpha or replicate grayscale here - // For now, just return RGBA - ret.channels = 4; - } + if (ret.pixels == BONGOCAT_NULLPTR) { + return bongocat_error_t::BONGOCAT_ERROR_IMAGE; + } - assert(ret.width > 0); - assert(ret.height > 0); - return ret; - } + // Handle desired_channels (pngle gives RGBA only) + if (desired_channels > 0 && desired_channels != 4) { + // Optionally strip alpha or replicate grayscale here + // For now, just return RGBA + ret.channels = 4; + } - void cleanup_image(Image& image) { - if (image.pixels) ::free(image.pixels); - image.pixels = nullptr; - } + assert(ret.width > 0); + assert(ret.height > 0); + return ret; +} - void init_image_loader() { +void cleanup_image(Image& image) { + if (image.pixels != BONGOCAT_NULLPTR) { + ::free(image.pixels); + image.pixels = BONGOCAT_NULLPTR; + } +} - } -} \ No newline at end of file +void init_image_loader() {} +} // namespace bongocat::animation \ No newline at end of file diff --git a/src/image_loader/load_images_stb_image.cpp b/src/image_loader/load_images_stb_image.cpp index f521e92b..6c775363 100644 --- a/src/image_loader/load_images_stb_image.cpp +++ b/src/image_loader/load_images_stb_image.cpp @@ -27,8 +27,8 @@ namespace bongocat::animation { assert(size <= INT_MAX); int channels_in_file; ret.pixels = stbi_load_from_memory(data, static_cast(size), &ret.width, &ret.height, &channels_in_file, desired_channels); - if (ret.pixels == nullptr) [[unlikely]] { - ret.pixels = nullptr; + if (ret.pixels == BONGOCAT_NULLPTR) [[unlikely]] { + ret.pixels = BONGOCAT_NULLPTR; return bongocat_error_t::BONGOCAT_ERROR_IMAGE; } assert(ret.width > 0); @@ -39,8 +39,10 @@ namespace bongocat::animation { } void cleanup_image(Image& image) { - if (image.pixels) stbi_image_free(image.pixels); - image.pixels = nullptr; + if (image.pixels) { + stbi_image_free(image.pixels); + } + image.pixels = BONGOCAT_NULLPTR; } void init_image_loader() { diff --git a/src/image_loader/min_dm/CMakeLists.txt b/src/image_loader/min_dm/CMakeLists.txt index 4536ac83..2e26abee 100644 --- a/src/image_loader/min_dm/CMakeLists.txt +++ b/src/image_loader/min_dm/CMakeLists.txt @@ -1,9 +1,11 @@ add_library(assets_min_dm_loader STATIC) target_sources(assets_min_dm_loader PRIVATE load_images_min_dm.cpp min_dm_load_sprite_sheet.cpp) target_compile_options(assets_min_dm_loader PRIVATE -ffunction-sections -fdata-sections) -target_include_directories(assets_min_dm_loader - PRIVATE ${INCLUDE_DIR}/embedded_assets/min_dm ${INCLUDE_DIR}/image_loader/min_dm - PUBLIC ${INCLUDE_DIR}) -target_link_libraries(assets_min_dm_loader - PUBLIC assets_image_loader assets_base_dm_loader assets_min_dm - PRIVATE assets_min_dm_interface assets_min_dm_feature bongocat_options) \ No newline at end of file +target_include_directories( + assets_min_dm_loader + PRIVATE ${INCLUDE_DIR}/embedded_assets/min_dm ${INCLUDE_DIR}/image_loader/min_dm + PUBLIC ${INCLUDE_DIR}) +target_link_libraries( + assets_min_dm_loader + PUBLIC assets_image_loader assets_base_dm_loader assets_min_dm + PRIVATE assets_min_dm_interface assets_min_dm_feature bongocat_options) diff --git a/src/image_loader/min_dm/load_images_min_dm.cpp b/src/image_loader/min_dm/load_images_min_dm.cpp index 3d8b2a0e..b91b7374 100644 --- a/src/image_loader/min_dm/load_images_min_dm.cpp +++ b/src/image_loader/min_dm/load_images_min_dm.cpp @@ -1,28 +1,31 @@ #include "load_images_min_dm.h" -#include "graphics/animation_context.h" -#include "image_loader/base_dm/load_dm.h" + #include "embedded_assets/embedded_image.h" #include "embedded_assets/min_dm/min_dm.hpp" +#include "graphics/animation_thread_context.h" +#include "image_loader/base_dm/load_dm.h" namespace bongocat::animation { - bongocat_error_t init_min_dm_anim(animation_context_t& ctx, int anim_index, const assets::embedded_image_t& sprite_sheet_image, int sprite_sheet_cols, int sprite_sheet_rows) { - using namespace assets; - BONGOCAT_CHECK_NULL(ctx.shm.ptr, bongocat_error_t::BONGOCAT_ERROR_INVALID_PARAM); - BONGOCAT_CHECK_NULL(ctx._local_copy_config.ptr, bongocat_error_t::BONGOCAT_ERROR_INVALID_PARAM); +bongocat_error_t init_min_dm_anim(animation_thread_context_t& ctx, int anim_index, + const assets::embedded_image_t& sprite_sheet_image, int sprite_sheet_cols, + int sprite_sheet_rows) { + using namespace assets; + BONGOCAT_CHECK_NULL(ctx.shm.ptr, bongocat_error_t::BONGOCAT_ERROR_INVALID_PARAM); + BONGOCAT_CHECK_NULL(ctx._local_copy_config.ptr, bongocat_error_t::BONGOCAT_ERROR_INVALID_PARAM); - assert(anim_index >= 0 && static_cast(anim_index) < MIN_DM_ANIM_COUNT); - BONGOCAT_LOG_VERBOSE("Load min_dm Animation (%d/%d): %s ...", anim_index, MIN_DM_ANIM_COUNT, sprite_sheet_image.name); - auto result = load_dm_anim(ctx, anim_index, sprite_sheet_image, sprite_sheet_cols, sprite_sheet_rows); - if (result.error != bongocat_error_t::BONGOCAT_SUCCESS) [[unlikely]] { - BONGOCAT_LOG_ERROR("Load dm20 Animation failed: %s, index: %d", sprite_sheet_image.name, anim_index); - return bongocat_error_t::BONGOCAT_ERROR_ANIMATION; - } - assert(result.result.total_frames > 0); ///< this SHOULD always work if it's an valid EMBEDDED image + assert(anim_index >= 0 && static_cast(anim_index) < MIN_DM_ANIM_COUNT); + BONGOCAT_LOG_VERBOSE("Load min_dm Animation (%d/%d): %s ...", anim_index, MIN_DM_ANIM_COUNT, sprite_sheet_image.name); + auto result = load_dm_anim(ctx, anim_index, sprite_sheet_image, sprite_sheet_cols, sprite_sheet_rows); + if (result.error != bongocat_error_t::BONGOCAT_SUCCESS) [[unlikely]] { + BONGOCAT_LOG_ERROR("Load dm20 Animation failed: %s, index: %d", sprite_sheet_image.name, anim_index); + return bongocat_error_t::BONGOCAT_ERROR_ANIMATION; + } + assert(result.result.total_frames > 0); ///< this SHOULD always work if it's an valid EMBEDDED image - assert(anim_index >= 0); - ctx.shm->min_dm_anims[static_cast(anim_index)] = bongocat::move(result.result); - assert(ctx.shm->min_dm_anims[static_cast(anim_index)].type == animation_t::Type::Dm); + assert(anim_index >= 0); + ctx.shm->min_dm_anims[static_cast(anim_index)] = bongocat::move(result.result); + assert(ctx.shm->min_dm_anims[static_cast(anim_index)].type == animation_t::type_t::Dm); - return bongocat_error_t::BONGOCAT_SUCCESS; - } + return bongocat_error_t::BONGOCAT_SUCCESS; } +} // namespace bongocat::animation diff --git a/src/image_loader/min_dm/min_dm_load_sprite_sheet.cpp b/src/image_loader/min_dm/min_dm_load_sprite_sheet.cpp index 229df1e9..ca68650e 100644 --- a/src/image_loader/min_dm/min_dm_load_sprite_sheet.cpp +++ b/src/image_loader/min_dm/min_dm_load_sprite_sheet.cpp @@ -1,33 +1,62 @@ #include "core/bongocat.h" -#include "graphics/animation_context.h" -#include "graphics/sprite_sheet.h" -#include "image_loader/base_dm/load_dm.h" -#include "embedded_assets/min_dm/min_dm.hpp" #include "embedded_assets/embedded_image.h" +#include "embedded_assets/min_dm/min_dm.hpp" #include "embedded_assets/min_dm/min_dm_sprite.h" +#include "graphics/animation_thread_context.h" +#include "graphics/sprite_sheet.h" +#include "image_loader/base_dm/load_dm.h" #include "image_loader/min_dm/load_images_min_dm.h" namespace bongocat::animation { - created_result_t load_min_dm_sprite_sheet(const animation_context_t& ctx, int index) { - using namespace animation; - using namespace assets; - switch (index) { - case DM_BOTAMON_ANIM_INDEX: return load_dm_anim(ctx, DM_BOTAMON_ANIM_INDEX, get_min_dm_sprite_sheet(DM_BOTAMON_ANIM_INDEX), DM_BOTAMON_SPRITE_SHEET_COLS, DM_BOTAMON_SPRITE_SHEET_ROWS); - case DM_KOROMON_ANIM_INDEX: return load_dm_anim(ctx, DM_KOROMON_ANIM_INDEX, get_min_dm_sprite_sheet(DM_KOROMON_ANIM_INDEX), DM_KOROMON_SPRITE_SHEET_COLS, DM_KOROMON_SPRITE_SHEET_ROWS); - case DM_AGUMON_ANIM_INDEX: return load_dm_anim(ctx, DM_AGUMON_ANIM_INDEX, get_min_dm_sprite_sheet(DM_AGUMON_ANIM_INDEX), DM_AGUMON_SPRITE_SHEET_COLS, DM_AGUMON_SPRITE_SHEET_ROWS); - case DM_BETAMON_ANIM_INDEX: return load_dm_anim(ctx, DM_BETAMON_ANIM_INDEX, get_min_dm_sprite_sheet(DM_BETAMON_ANIM_INDEX), DM_BETAMON_SPRITE_SHEET_COLS, DM_BETAMON_SPRITE_SHEET_ROWS); - case DM_GREYMON_ANIM_INDEX: return load_dm_anim(ctx, DM_GREYMON_ANIM_INDEX, get_min_dm_sprite_sheet(DM_GREYMON_ANIM_INDEX), DM_GREYMON_SPRITE_SHEET_COLS, DM_GREYMON_SPRITE_SHEET_ROWS); - case DM_TYRANOMON_ANIM_INDEX: return load_dm_anim(ctx, DM_TYRANOMON_ANIM_INDEX, get_min_dm_sprite_sheet(DM_TYRANOMON_ANIM_INDEX), DM_TYRANOMON_SPRITE_SHEET_COLS, DM_TYRANOMON_SPRITE_SHEET_ROWS); - case DM_DEVIMON_ANIM_INDEX: return load_dm_anim(ctx, DM_DEVIMON_ANIM_INDEX, get_min_dm_sprite_sheet(DM_DEVIMON_ANIM_INDEX), DM_DEVIMON_SPRITE_SHEET_COLS, DM_DEVIMON_SPRITE_SHEET_ROWS); - case DM_MERAMON_ANIM_INDEX: return load_dm_anim(ctx, DM_MERAMON_ANIM_INDEX, get_min_dm_sprite_sheet(DM_MERAMON_ANIM_INDEX), DM_MERAMON_SPRITE_SHEET_COLS, DM_MERAMON_SPRITE_SHEET_ROWS); - case DM_AIRDRAMON_ANIM_INDEX: return load_dm_anim(ctx, DM_AIRDRAMON_ANIM_INDEX, get_min_dm_sprite_sheet(DM_AIRDRAMON_ANIM_INDEX), DM_AIRDRAMON_SPRITE_SHEET_COLS, DM_AIRDRAMON_SPRITE_SHEET_ROWS); - case DM_SEADRAMON_ANIM_INDEX: return load_dm_anim(ctx, DM_SEADRAMON_ANIM_INDEX, get_min_dm_sprite_sheet(DM_SEADRAMON_ANIM_INDEX), DM_SEADRAMON_SPRITE_SHEET_COLS, DM_SEADRAMON_SPRITE_SHEET_ROWS); - case DM_NUMEMON_ANIM_INDEX: return load_dm_anim(ctx, DM_NUMEMON_ANIM_INDEX, get_min_dm_sprite_sheet(DM_NUMEMON_ANIM_INDEX), DM_NUMEMON_SPRITE_SHEET_COLS, DM_NUMEMON_SPRITE_SHEET_ROWS); - case DM_METAL_GREYMON_ANIM_INDEX: return load_dm_anim(ctx, DM_METAL_GREYMON_ANIM_INDEX, get_min_dm_sprite_sheet(DM_METAL_GREYMON_ANIM_INDEX), DM_METAL_GREYMON_SPRITE_SHEET_COLS, DM_METAL_GREYMON_SPRITE_SHEET_ROWS); - case DM_MAMEMON_ANIM_INDEX: return load_dm_anim(ctx, DM_MAMEMON_ANIM_INDEX, get_min_dm_sprite_sheet(DM_MAMEMON_ANIM_INDEX), DM_MAMEMON_SPRITE_SHEET_COLS, DM_MAMEMON_SPRITE_SHEET_ROWS); - case DM_MONZAEMON_ANIM_INDEX: return load_dm_anim(ctx, DM_MONZAEMON_ANIM_INDEX, get_min_dm_sprite_sheet(DM_MONZAEMON_ANIM_INDEX), DM_MONZAEMON_SPRITE_SHEET_COLS, DM_MONZAEMON_SPRITE_SHEET_ROWS); - default: return bongocat_error_t::BONGOCAT_ERROR_INVALID_PARAM; - } - return bongocat_error_t::BONGOCAT_ERROR_INVALID_PARAM; - } -} \ No newline at end of file +created_result_t load_min_dm_sprite_sheet(const animation_thread_context_t& ctx, int index) { + using namespace animation; + using namespace assets; + switch (index) { + case DM_BOTAMON_ANIM_INDEX: + return load_dm_anim(ctx, DM_BOTAMON_ANIM_INDEX, get_min_dm_sprite_sheet(DM_BOTAMON_ANIM_INDEX), + DM_BOTAMON_SPRITE_SHEET_COLS, DM_BOTAMON_SPRITE_SHEET_ROWS); + case DM_KOROMON_ANIM_INDEX: + return load_dm_anim(ctx, DM_KOROMON_ANIM_INDEX, get_min_dm_sprite_sheet(DM_KOROMON_ANIM_INDEX), + DM_KOROMON_SPRITE_SHEET_COLS, DM_KOROMON_SPRITE_SHEET_ROWS); + case DM_AGUMON_ANIM_INDEX: + return load_dm_anim(ctx, DM_AGUMON_ANIM_INDEX, get_min_dm_sprite_sheet(DM_AGUMON_ANIM_INDEX), + DM_AGUMON_SPRITE_SHEET_COLS, DM_AGUMON_SPRITE_SHEET_ROWS); + case DM_BETAMON_ANIM_INDEX: + return load_dm_anim(ctx, DM_BETAMON_ANIM_INDEX, get_min_dm_sprite_sheet(DM_BETAMON_ANIM_INDEX), + DM_BETAMON_SPRITE_SHEET_COLS, DM_BETAMON_SPRITE_SHEET_ROWS); + case DM_GREYMON_ANIM_INDEX: + return load_dm_anim(ctx, DM_GREYMON_ANIM_INDEX, get_min_dm_sprite_sheet(DM_GREYMON_ANIM_INDEX), + DM_GREYMON_SPRITE_SHEET_COLS, DM_GREYMON_SPRITE_SHEET_ROWS); + case DM_TYRANOMON_ANIM_INDEX: + return load_dm_anim(ctx, DM_TYRANOMON_ANIM_INDEX, get_min_dm_sprite_sheet(DM_TYRANOMON_ANIM_INDEX), + DM_TYRANOMON_SPRITE_SHEET_COLS, DM_TYRANOMON_SPRITE_SHEET_ROWS); + case DM_DEVIMON_ANIM_INDEX: + return load_dm_anim(ctx, DM_DEVIMON_ANIM_INDEX, get_min_dm_sprite_sheet(DM_DEVIMON_ANIM_INDEX), + DM_DEVIMON_SPRITE_SHEET_COLS, DM_DEVIMON_SPRITE_SHEET_ROWS); + case DM_MERAMON_ANIM_INDEX: + return load_dm_anim(ctx, DM_MERAMON_ANIM_INDEX, get_min_dm_sprite_sheet(DM_MERAMON_ANIM_INDEX), + DM_MERAMON_SPRITE_SHEET_COLS, DM_MERAMON_SPRITE_SHEET_ROWS); + case DM_AIRDRAMON_ANIM_INDEX: + return load_dm_anim(ctx, DM_AIRDRAMON_ANIM_INDEX, get_min_dm_sprite_sheet(DM_AIRDRAMON_ANIM_INDEX), + DM_AIRDRAMON_SPRITE_SHEET_COLS, DM_AIRDRAMON_SPRITE_SHEET_ROWS); + case DM_SEADRAMON_ANIM_INDEX: + return load_dm_anim(ctx, DM_SEADRAMON_ANIM_INDEX, get_min_dm_sprite_sheet(DM_SEADRAMON_ANIM_INDEX), + DM_SEADRAMON_SPRITE_SHEET_COLS, DM_SEADRAMON_SPRITE_SHEET_ROWS); + case DM_NUMEMON_ANIM_INDEX: + return load_dm_anim(ctx, DM_NUMEMON_ANIM_INDEX, get_min_dm_sprite_sheet(DM_NUMEMON_ANIM_INDEX), + DM_NUMEMON_SPRITE_SHEET_COLS, DM_NUMEMON_SPRITE_SHEET_ROWS); + case DM_METAL_GREYMON_ANIM_INDEX: + return load_dm_anim(ctx, DM_METAL_GREYMON_ANIM_INDEX, get_min_dm_sprite_sheet(DM_METAL_GREYMON_ANIM_INDEX), + DM_METAL_GREYMON_SPRITE_SHEET_COLS, DM_METAL_GREYMON_SPRITE_SHEET_ROWS); + case DM_MAMEMON_ANIM_INDEX: + return load_dm_anim(ctx, DM_MAMEMON_ANIM_INDEX, get_min_dm_sprite_sheet(DM_MAMEMON_ANIM_INDEX), + DM_MAMEMON_SPRITE_SHEET_COLS, DM_MAMEMON_SPRITE_SHEET_ROWS); + case DM_MONZAEMON_ANIM_INDEX: + return load_dm_anim(ctx, DM_MONZAEMON_ANIM_INDEX, get_min_dm_sprite_sheet(DM_MONZAEMON_ANIM_INDEX), + DM_MONZAEMON_SPRITE_SHEET_COLS, DM_MONZAEMON_SPRITE_SHEET_ROWS); + default: + return bongocat_error_t::BONGOCAT_ERROR_INVALID_PARAM; + } + return bongocat_error_t::BONGOCAT_ERROR_INVALID_PARAM; +} +} // namespace bongocat::animation \ No newline at end of file diff --git a/src/image_loader/misc/CMakeLists.txt b/src/image_loader/misc/CMakeLists.txt index 027a7b68..6b6c0cd0 100644 --- a/src/image_loader/misc/CMakeLists.txt +++ b/src/image_loader/misc/CMakeLists.txt @@ -1,9 +1,11 @@ add_library(assets_misc_loader STATIC) target_sources(assets_misc_loader PRIVATE load_images_misc.cpp misc_load_sprite_sheet.cpp) target_compile_options(assets_misc_loader PRIVATE -ffunction-sections -fdata-sections) -target_include_directories(assets_misc_loader - PRIVATE ${INCLUDE_DIR}/embedded_assets/misc ${INCLUDE_DIR}/image_loader/misc - PUBLIC ${INCLUDE_DIR}) -target_link_libraries(assets_misc_loader - PUBLIC assets_image_loader assets_custom_loader assets_misc - PRIVATE assets_misc_interface assets_misc_feature bongocat_options) \ No newline at end of file +target_include_directories( + assets_misc_loader + PRIVATE ${INCLUDE_DIR}/embedded_assets/misc ${INCLUDE_DIR}/image_loader/misc + PUBLIC ${INCLUDE_DIR}) +target_link_libraries( + assets_misc_loader + PUBLIC assets_image_loader assets_custom_loader assets_misc + PRIVATE assets_misc_interface assets_misc_feature bongocat_options) diff --git a/src/image_loader/misc/load_images_misc.cpp b/src/image_loader/misc/load_images_misc.cpp index 4bf78ff2..837b4524 100644 --- a/src/image_loader/misc/load_images_misc.cpp +++ b/src/image_loader/misc/load_images_misc.cpp @@ -1,27 +1,30 @@ #include "load_images_misc.h" -#include "graphics/animation_context.h" -#include "image_loader/custom/load_custom.h" + #include "embedded_assets/embedded_image.h" #include "embedded_assets/misc/misc.hpp" +#include "graphics/animation_thread_context.h" +#include "image_loader/custom/load_custom.h" namespace bongocat::animation { - bongocat_error_t init_misc_anim(animation_context_t& ctx, int anim_index, const assets::embedded_image_t& sprite_sheet_image, const assets::custom_animation_settings_t& sprite_sheet_settings) { - using namespace assets; - BONGOCAT_CHECK_NULL(ctx.shm.ptr, bongocat_error_t::BONGOCAT_ERROR_INVALID_PARAM); - BONGOCAT_CHECK_NULL(ctx._local_copy_config.ptr, bongocat_error_t::BONGOCAT_ERROR_INVALID_PARAM); +bongocat_error_t init_misc_anim(animation_thread_context_t& ctx, int anim_index, + const assets::embedded_image_t& sprite_sheet_image, + const assets::custom_animation_settings_t& sprite_sheet_settings) { + using namespace assets; + BONGOCAT_CHECK_NULL(ctx.shm.ptr, bongocat_error_t::BONGOCAT_ERROR_INVALID_PARAM); + BONGOCAT_CHECK_NULL(ctx._local_copy_config.ptr, bongocat_error_t::BONGOCAT_ERROR_INVALID_PARAM); - assert(anim_index >= 0 && static_cast(anim_index) < MISC_ANIM_COUNT); - BONGOCAT_LOG_VERBOSE("Load misc Animation (%d/%d): %s ...", anim_index, MISC_ANIM_COUNT, sprite_sheet_image.name); - auto result = load_custom_anim(ctx, sprite_sheet_image, sprite_sheet_settings); - if (result.error != bongocat_error_t::BONGOCAT_SUCCESS) [[unlikely]] { - BONGOCAT_LOG_ERROR("Load misc Animation failed: %s, index: %d", sprite_sheet_image.name, anim_index); - return bongocat_error_t::BONGOCAT_ERROR_ANIMATION; - } + assert(anim_index >= 0 && static_cast(anim_index) < MISC_ANIM_COUNT); + BONGOCAT_LOG_VERBOSE("Load misc Animation (%d/%d): %s ...", anim_index, MISC_ANIM_COUNT, sprite_sheet_image.name); + auto result = load_custom_anim(ctx, sprite_sheet_image, sprite_sheet_settings); + if (result.error != bongocat_error_t::BONGOCAT_SUCCESS) [[unlikely]] { + BONGOCAT_LOG_ERROR("Load misc Animation failed: %s, index: %d", sprite_sheet_image.name, anim_index); + return bongocat_error_t::BONGOCAT_ERROR_ANIMATION; + } - assert(anim_index >= 0); - ctx.shm->misc_anims[static_cast(anim_index)] = bongocat::move(result.result); - assert(ctx.shm->misc_anims[static_cast(anim_index)].type == animation_t::Type::Custom); + assert(anim_index >= 0); + ctx.shm->misc_anims[static_cast(anim_index)] = bongocat::move(result.result); + assert(ctx.shm->misc_anims[static_cast(anim_index)].type == animation_t::type_t::Custom); - return bongocat_error_t::BONGOCAT_SUCCESS; - } + return bongocat_error_t::BONGOCAT_SUCCESS; } +} // namespace bongocat::animation diff --git a/src/image_loader/misc/misc_load_sprite_sheet.cpp b/src/image_loader/misc/misc_load_sprite_sheet.cpp index 4fe51725..16e1c2b3 100644 --- a/src/image_loader/misc/misc_load_sprite_sheet.cpp +++ b/src/image_loader/misc/misc_load_sprite_sheet.cpp @@ -1,20 +1,21 @@ #include "core/bongocat.h" -#include "graphics/animation_context.h" -#include "graphics/sprite_sheet.h" -#include "image_loader/custom/load_custom.h" -#include "embedded_assets/misc/misc.hpp" #include "embedded_assets/embedded_image.h" +#include "embedded_assets/misc/misc.hpp" #include "embedded_assets/misc/misc_sprite.h" +#include "graphics/animation_thread_context.h" +#include "graphics/sprite_sheet.h" #include "image_loader/custom/load_custom.h" namespace bongocat::animation { - created_result_t load_misc_sprite_sheet(const animation_context_t& ctx, int index) { - using namespace animation; - using namespace assets; - switch (index) { - case MISC_NEKO_ANIM_INDEX: return load_custom_anim(ctx, get_misc_sprite_sheet(MISC_NEKO_ANIM_INDEX), MISC_NEKO_SPRITE_SHEET_SETTINGS); - default: return bongocat_error_t::BONGOCAT_ERROR_INVALID_PARAM; - } - return bongocat_error_t::BONGOCAT_ERROR_INVALID_PARAM; - } -} \ No newline at end of file +created_result_t load_misc_sprite_sheet(const animation_thread_context_t& ctx, int index) { + using namespace animation; + using namespace assets; + switch (index) { + case MISC_NEKO_ANIM_INDEX: + return load_custom_anim(ctx, get_misc_sprite_sheet(MISC_NEKO_ANIM_INDEX), MISC_NEKO_SPRITE_SHEET_SETTINGS); + default: + return bongocat_error_t::BONGOCAT_ERROR_INVALID_PARAM; + } + return bongocat_error_t::BONGOCAT_ERROR_INVALID_PARAM; +} +} // namespace bongocat::animation \ No newline at end of file diff --git a/src/image_loader/ms_agent/CMakeLists.txt b/src/image_loader/ms_agent/CMakeLists.txt index 851f3660..d88a0d7e 100644 --- a/src/image_loader/ms_agent/CMakeLists.txt +++ b/src/image_loader/ms_agent/CMakeLists.txt @@ -1,21 +1,23 @@ add_library(assets_ms_agent_loader STATIC) target_sources(assets_ms_agent_loader PRIVATE load_images_ms_agent.cpp) target_compile_options(assets_ms_agent_loader PRIVATE -ffunction-sections -fdata-sections) -target_include_directories(assets_ms_agent_loader - PRIVATE ${INCLUDE_DIR}/embedded_assets/ms_agent ${INCLUDE_DIR}/image_loader/ms_agent - PUBLIC ${INCLUDE_DIR}) -target_link_libraries(assets_ms_agent_loader - PUBLIC assets_image_loader assets_ms_agent - PRIVATE assets_ms_agent_interface assets_ms_agent_feature bongocat_options) - - +target_include_directories( + assets_ms_agent_loader + PRIVATE ${INCLUDE_DIR}/embedded_assets/ms_agent ${INCLUDE_DIR}/image_loader/ms_agent + PUBLIC ${INCLUDE_DIR}) +target_link_libraries( + assets_ms_agent_loader + PUBLIC assets_image_loader assets_ms_agent + PRIVATE assets_ms_agent_interface assets_ms_agent_feature bongocat_options) add_library(assets_more_ms_agent_loader STATIC) target_sources(assets_more_ms_agent_loader PRIVATE load_images_ms_agent.cpp) target_compile_options(assets_more_ms_agent_loader PRIVATE -ffunction-sections -fdata-sections) -target_include_directories(assets_more_ms_agent_loader - PRIVATE ${INCLUDE_DIR}/embedded_assets/ms_agent ${INCLUDE_DIR}/image_loader/ms_agent - PUBLIC ${INCLUDE_DIR}) -target_link_libraries(assets_more_ms_agent_loader - PUBLIC assets_image_loader assets_more_ms_agent - PRIVATE assets_more_ms_agent_interface assets_more_ms_agent_feature bongocat_options) \ No newline at end of file +target_include_directories( + assets_more_ms_agent_loader + PRIVATE ${INCLUDE_DIR}/embedded_assets/ms_agent ${INCLUDE_DIR}/image_loader/ms_agent + PUBLIC ${INCLUDE_DIR}) +target_link_libraries( + assets_more_ms_agent_loader + PUBLIC assets_image_loader assets_more_ms_agent + PRIVATE assets_more_ms_agent_interface assets_more_ms_agent_feature bongocat_options) diff --git a/src/image_loader/ms_agent/load_images_ms_agent.cpp b/src/image_loader/ms_agent/load_images_ms_agent.cpp index 92529310..e0facc72 100644 --- a/src/image_loader/ms_agent/load_images_ms_agent.cpp +++ b/src/image_loader/ms_agent/load_images_ms_agent.cpp @@ -1,229 +1,297 @@ -#include "graphics/drawing.h" -#include "graphics/animation_context.h" +#include "embedded_assets/ms_agent/ms_agent.hpp" +#include "embedded_assets/ms_agent/ms_agent_sprite.h" #include "graphics/animation.h" -#include "utils/memory.h" +#include "graphics/animation_thread_context.h" +#include "graphics/drawing.h" #include "image_loader/load_images.h" -#include "embedded_assets/ms_agent/ms_agent_sprite.h" -#include "embedded_assets/ms_agent/ms_agent.hpp" -#include +#include "utils/memory.h" +#include namespace bongocat::animation { - [[nodiscard]] static created_result_t load_ms_agent_sprite_sheet_from_memory(const uint8_t* sprite_data, size_t sprite_data_size, - int frame_columns, int frame_rows, - int padding_x, int padding_y) { - auto [sprite_sheet, sprite_sheet_error] = load_image(sprite_data, sprite_data_size, RGBA_CHANNELS); // Force RGBA - if (sprite_sheet_error != bongocat_error_t::BONGOCAT_SUCCESS) [[unlikely]] { - BONGOCAT_LOG_ERROR("Failed to load sprite sheet."); - return sprite_sheet_error; - } +BONGOCAT_NODISCARD static created_result_t +load_ms_agent_sprite_sheet_from_memory(const uint8_t *sprite_data, size_t sprite_data_size, int frame_columns, + int frame_rows, int padding_x, int padding_y) { + auto [sprite_sheet, sprite_sheet_error] = load_image(sprite_data, sprite_data_size, RGBA_CHANNELS); // Force RGBA + if (sprite_sheet_error != bongocat_error_t::BONGOCAT_SUCCESS) [[unlikely]] { + BONGOCAT_LOG_ERROR("Failed to load sprite sheet."); + return sprite_sheet_error; + } - assert(frame_columns != 0 && frame_rows != 0 && sprite_sheet.width % frame_columns == 0 && sprite_sheet.height % frame_rows == 0); - if (frame_columns == 0 || frame_rows == 0 || sprite_sheet.width % frame_columns != 0 || sprite_sheet.height % frame_rows != 0) { - BONGOCAT_LOG_ERROR("Sprite sheet dimensions not divisible by frame grid; frame_columns=%d, frame_rows=%d vs %dx%d sprite size", frame_columns, frame_rows, sprite_sheet.width, sprite_sheet.height); - return bongocat_error_t::BONGOCAT_ERROR_INVALID_PARAM; - } + assert(frame_columns != 0 && frame_rows != 0 && sprite_sheet.width % frame_columns == 0 && + sprite_sheet.height % frame_rows == 0); + if (frame_columns == 0 || frame_rows == 0 || sprite_sheet.width % frame_columns != 0 || + sprite_sheet.height % frame_rows != 0) { + BONGOCAT_LOG_ERROR( + "Sprite sheet dimensions not divisible by frame grid; frame_columns=%d, frame_rows=%d vs %dx%d sprite size", + frame_columns, frame_rows, sprite_sheet.width, sprite_sheet.height); + return bongocat_error_t::BONGOCAT_ERROR_INVALID_PARAM; + } - const auto frame_width = sprite_sheet.width / frame_columns; - const auto frame_height = sprite_sheet.height / frame_rows; + const auto frame_width = sprite_sheet.width / frame_columns; + const auto frame_height = sprite_sheet.height / frame_rows; - /* - assert(MAX_NUM_FRAMES <= INT_MAX); - if (total_frames > (int)MAX_NUM_FRAMES) { - BONGOCAT_LOG_ERROR("Sprite Sheet does not fit in out_frames: %d, total_frames: %d", MAX_NUM_FRAMES, total_frames); - return BONGOCAT_ERROR_INVALID_PARAM; - } - */ - - const auto dest_frame_width = frame_width + padding_x*2; - const auto dest_frame_height = frame_height + padding_y*2; - const auto dest_pixels_width = dest_frame_width * frame_columns; - const auto dest_pixels_height = dest_frame_height * frame_rows; - assert(dest_pixels_width >= 0); - assert(dest_pixels_height >= 0); - assert(sprite_sheet.channels >= 0); - const size_t dest_pixels_size = static_cast(dest_pixels_width) * static_cast(dest_pixels_height) * static_cast(sprite_sheet.channels); - auto dest_pixels = make_allocated_array(dest_pixels_size); - if (!dest_pixels) { - BONGOCAT_LOG_ERROR("Failed to allocate memory for dest_pixels (%zu bytes)\n", dest_pixels_size); - return bongocat_error_t::BONGOCAT_ERROR_MEMORY; - } - //memset(dest_pixels.data, 0, dest_pixels_size); - - const auto src_frame_width = frame_width; - const auto src_frame_height = frame_height; - const auto src_pixels_width = sprite_sheet.width; - const auto src_pixels_height = sprite_sheet.height; - assert(src_pixels_width >= 0); - assert(src_pixels_height >= 0); - assert(sprite_sheet.channels >= 0); - const size_t src_pixels_size = static_cast(src_pixels_width) * static_cast(src_pixels_height) * static_cast(sprite_sheet.channels); - size_t frame_index = 0; - for (int row = 0; row < frame_rows; ++row) { - for (int col = 0; col < frame_columns; ++col) { - const auto src_x = col * src_frame_width; - const auto src_y = row * src_frame_height; - const auto dst_x = col * dest_frame_width + padding_x; - const auto dst_y = row * dest_frame_height + padding_y; - [[maybe_unused]] const auto src_idx = (src_y * src_pixels_width + src_x) * sprite_sheet.channels; - [[maybe_unused]] const auto dst_idx = (dst_y * dest_pixels_width + dst_x) * sprite_sheet.channels; - assert(src_idx >= 0); - assert(dst_idx >= 0); - - bool set_frames = false; - for (int fy = 0; fy < src_frame_height; fy++) { - for (int fx = 0; fx < src_frame_width; fx++) { - const auto src_px_idx = ((src_y + fy) * src_pixels_width + (src_x + fx)) * sprite_sheet.channels; - const auto dst_px_idx = ((dst_y + fy) * dest_pixels_width + (dst_x + fx)) * sprite_sheet.channels; - - if (src_px_idx >= 0 && dst_px_idx >= 0 && - static_cast(src_px_idx) < src_pixels_size && - static_cast(dst_px_idx) < dest_pixels_size) { - drawing_copy_pixel(dest_pixels.data, sprite_sheet.channels, dst_px_idx, - sprite_sheet.pixels, sprite_sheet.channels, src_px_idx, - blit_image_color_option_flags_t::Normal, - blit_image_color_order_t::RGBA, - blit_image_color_order_t::RGBA); - if (!set_frames && frame_index < MAX_NUM_FRAMES) { - set_frames = true; - } - } - } - } - frame_index++; + /* + assert(MAX_NUM_FRAMES <= INT_MAX); + if (total_frames > (int)MAX_NUM_FRAMES) { + BONGOCAT_LOG_ERROR("Sprite Sheet does not fit in out_frames: %d, total_frames: %d", MAX_NUM_FRAMES, total_frames); + return BONGOCAT_ERROR_INVALID_PARAM; + } + */ + + const auto dest_frame_width = frame_width + (padding_x * 2); + const auto dest_frame_height = frame_height + (padding_y * 2); + const auto dest_pixels_width = dest_frame_width * frame_columns; + const auto dest_pixels_height = dest_frame_height * frame_rows; + assert(dest_pixels_width >= 0); + assert(dest_pixels_height >= 0); + assert(sprite_sheet.channels >= 0); + const size_t dest_pixels_size = static_cast(dest_pixels_width) * static_cast(dest_pixels_height) * + static_cast(sprite_sheet.channels); + auto dest_pixels = make_allocated_array(dest_pixels_size); + if (!dest_pixels) { + BONGOCAT_LOG_ERROR("Failed to allocate memory for dest_pixels (%zu bytes)\n", dest_pixels_size); + return bongocat_error_t::BONGOCAT_ERROR_MEMORY; + } + // memset(dest_pixels.data, 0, dest_pixels_size); + + const auto src_frame_width = frame_width; + const auto src_frame_height = frame_height; + const auto src_pixels_width = sprite_sheet.width; + const auto src_pixels_height = sprite_sheet.height; + assert(src_pixels_width >= 0); + assert(src_pixels_height >= 0); + assert(sprite_sheet.channels >= 0); + const size_t src_pixels_size = static_cast(src_pixels_width) * static_cast(src_pixels_height) * + static_cast(sprite_sheet.channels); + size_t frame_index = 0; + for (int row = 0; row < frame_rows; ++row) { + for (int col = 0; col < frame_columns; ++col) { + const auto src_x = col * src_frame_width; + const auto src_y = row * src_frame_height; + const auto dst_x = (col * dest_frame_width) + padding_x; + const auto dst_y = (row * dest_frame_height) + padding_y; + [[maybe_unused]] const auto src_idx = (src_y * src_pixels_width + src_x) * sprite_sheet.channels; + [[maybe_unused]] const auto dst_idx = (dst_y * dest_pixels_width + dst_x) * sprite_sheet.channels; + assert(src_idx >= 0); + assert(dst_idx >= 0); + + bool set_frames = false; + for (int fy = 0; fy < src_frame_height; fy++) { + for (int fx = 0; fx < src_frame_width; fx++) { + const auto src_px_idx = ((src_y + fy) * src_pixels_width + (src_x + fx)) * sprite_sheet.channels; + const auto dst_px_idx = ((dst_y + fy) * dest_pixels_width + (dst_x + fx)) * sprite_sheet.channels; + + if (src_px_idx >= 0 && dst_px_idx >= 0 && static_cast(src_px_idx) < src_pixels_size && + static_cast(dst_px_idx) < dest_pixels_size) { + drawing_copy_pixel(dest_pixels.data, sprite_sheet.channels, dst_px_idx, sprite_sheet.pixels, + sprite_sheet.channels, src_px_idx, blit_image_color_option_flags_t::Normal, + blit_image_color_order_t::RGBA, blit_image_color_order_t::RGBA); + if (!set_frames && frame_index < MAX_NUM_FRAMES) { + set_frames = true; } + } } - - ms_agent_sprite_sheet_t ret; - ret.image.sprite_sheet_width = sprite_sheet.width; - ret.image.sprite_sheet_height = sprite_sheet.height; - ret.image.channels = sprite_sheet.channels; - // move pixels ownership into out_frames - ret.image.pixels = move(dest_pixels); - dest_pixels = nullptr; - ret.frame_width = dest_frame_width; - ret.frame_height = dest_frame_height; - - return ret; + } + frame_index++; } + } - created_result_t load_ms_agent_sprite_sheet(const config::config_t& config, const assets::embedded_image_t& sprite_sheet_image, int sprite_sheet_cols, int sprite_sheet_rows) { - if (sprite_sheet_cols < 0 || sprite_sheet_rows < 0) { - return bongocat_error_t::BONGOCAT_ERROR_INVALID_PARAM; - } + ms_agent_sprite_sheet_t ret; + ret.image.sprite_sheet_width = sprite_sheet.width; + ret.image.sprite_sheet_height = sprite_sheet.height; + ret.image.channels = sprite_sheet.channels; + // move pixels ownership into out_frames + ret.image.pixels = move(dest_pixels); + dest_pixels = BONGOCAT_NULLPTR; + ret.frame_width = dest_frame_width; + ret.frame_height = dest_frame_height; - assert(sprite_sheet_image.size <= INT_MAX); - auto result = load_ms_agent_sprite_sheet_from_memory(sprite_sheet_image.data, sprite_sheet_image.size, - sprite_sheet_cols, sprite_sheet_rows, - config.padding_x, config.padding_y); - if (result.error != bongocat_error_t::BONGOCAT_SUCCESS) [[unlikely]] { - BONGOCAT_LOG_ERROR("Sprite Sheet load failed: %s", sprite_sheet_image.name); - return result.error; - } + return ret; +} - // assume every frame is the same size, pick first frame - BONGOCAT_LOG_DEBUG("Loaded %dx%d sprite sheet", result.result.image.sprite_sheet_width, result.result.image.sprite_sheet_height); +created_result_t load_ms_agent_sprite_sheet(const config::config_t& config, + const assets::embedded_image_t& sprite_sheet_image, + int sprite_sheet_cols, int sprite_sheet_rows) { + if (sprite_sheet_cols < 0 || sprite_sheet_rows < 0) { + return bongocat_error_t::BONGOCAT_ERROR_INVALID_PARAM; + } - return result; - } + assert(sprite_sheet_image.size <= INT_MAX); + auto result = + load_ms_agent_sprite_sheet_from_memory(sprite_sheet_image.data, sprite_sheet_image.size, sprite_sheet_cols, + sprite_sheet_rows, config.padding_x, config.padding_y); + if (result.error != bongocat_error_t::BONGOCAT_SUCCESS) [[unlikely]] { + BONGOCAT_LOG_ERROR("Sprite Sheet load failed: %s", sprite_sheet_image.name); + return result.error; + } - created_result_t load_ms_agent_anim(const animation_context_t& ctx, [[maybe_unused]] int anim_index, const assets::embedded_image_t& sprite_sheet_image, int sprite_sheet_cols, int sprite_sheet_rows, const assets::ms_agent_animation_indices_t& animation_data) { - using namespace assets; - BONGOCAT_CHECK_NULL(ctx._local_copy_config.ptr, bongocat_error_t::BONGOCAT_ERROR_INVALID_PARAM); + // assume every frame is the same size, pick first frame + BONGOCAT_LOG_DEBUG("Loaded %dx%d sprite sheet", result.result.image.sprite_sheet_width, + result.result.image.sprite_sheet_height); - BONGOCAT_LOG_VERBOSE("Load MS agent Animation(index=%d) ...", anim_index); - auto result = load_ms_agent_sprite_sheet(*ctx._local_copy_config, sprite_sheet_image, sprite_sheet_cols, sprite_sheet_rows); - if (result.error != bongocat_error_t::BONGOCAT_SUCCESS) [[unlikely]] { - return result.error; - } + return result; +} +created_result_t +load_ms_agent_anim(const animation_thread_context_t& ctx, [[maybe_unused]] int anim_index, + const assets::embedded_image_t& sprite_sheet_image, int sprite_sheet_cols, int sprite_sheet_rows, + const assets::ms_agent_animation_indices_t& animation_data) { + using namespace assets; + BONGOCAT_CHECK_NULL(ctx._local_copy_config.ptr, bongocat_error_t::BONGOCAT_ERROR_INVALID_PARAM); - // setup animation frames data - if (result.result.frame_height > 0 && sprite_sheet_rows > 0) { - [[maybe_unused]] const auto rows = result.result.image.sprite_sheet_height / result.result.frame_height; - assert(rows == sprite_sheet_rows); + BONGOCAT_LOG_VERBOSE("Load MS agent Animation(index=%d) ...", anim_index); + auto result = + load_ms_agent_sprite_sheet(*ctx._local_copy_config, sprite_sheet_image, sprite_sheet_cols, sprite_sheet_rows); + if (result.error != bongocat_error_t::BONGOCAT_SUCCESS) [[unlikely]] { + return result.error; + } - assert(sprite_sheet_rows > 0); - if (static_cast(sprite_sheet_rows - 1) >= MS_AGENT_SPRITE_SHEET_ROW_IDLE) { - result.result.idle = { .valid = true, .start_col = animation_data.start_index_frame_idle, .end_col = animation_data.end_index_frame_idle, .row = MS_AGENT_SPRITE_SHEET_ROW_IDLE }; - } - if (static_cast(sprite_sheet_rows - 1) >= MS_AGENT_SPRITE_SHEET_ROW_BORING) { - result.result.boring = { .valid = true, .start_col = animation_data.start_index_frame_boring, .end_col = animation_data.end_index_frame_boring, .row = MS_AGENT_SPRITE_SHEET_ROW_BORING }; - } - if (static_cast(sprite_sheet_rows - 1) >= MS_AGENT_SPRITE_SHEET_ROW_START_WRITING) { - result.result.start_writing = { .valid = true, .start_col = animation_data.start_index_frame_start_writing, .end_col = animation_data.end_index_frame_start_writing, .row = MS_AGENT_SPRITE_SHEET_ROW_START_WRITING }; - } - if (static_cast(sprite_sheet_rows - 1) >= MS_AGENT_SPRITE_SHEET_ROW_WRITING) { - result.result.writing = { .valid = true, .start_col = animation_data.start_index_frame_writing, .end_col = animation_data.end_index_frame_writing, .row = MS_AGENT_SPRITE_SHEET_ROW_WRITING }; - } - if (static_cast(sprite_sheet_rows - 1) >= MS_AGENT_SPRITE_SHEET_ROW_END_WRITING) { - result.result.end_writing = { .valid = true, .start_col = animation_data.start_index_frame_end_writing, .end_col = animation_data.end_index_frame_end_writing, .row = MS_AGENT_SPRITE_SHEET_ROW_END_WRITING }; - } - if (static_cast(sprite_sheet_rows - 1) >= MS_AGENT_SPRITE_SHEET_ROW_SLEEP) { - result.result.sleep = { .valid = true, .start_col = animation_data.start_index_frame_sleep, .end_col = animation_data.end_index_frame_sleep, .row = MS_AGENT_SPRITE_SHEET_ROW_SLEEP }; - } - if (static_cast(sprite_sheet_rows - 1) >= MS_AGENT_SPRITE_SHEET_ROW_WAKE_UP) { - result.result.wake_up = { .valid = true, .start_col = animation_data.start_index_frame_wake_up, .end_col = animation_data.end_index_frame_wake_up, .row = MS_AGENT_SPRITE_SHEET_ROW_WAKE_UP }; - } - if (static_cast(sprite_sheet_rows - 1) >= MS_AGENT_SPRITE_SHEET_ROW_START_WORKING) { - result.result.start_working = { .valid = true, .start_col = animation_data.start_index_frame_start_working, .end_col = animation_data.end_index_frame_start_working, .row = MS_AGENT_SPRITE_SHEET_ROW_START_WORKING }; - } - if (static_cast(sprite_sheet_rows - 1) >= MS_AGENT_SPRITE_SHEET_ROW_WORKING) { - result.result.working = { .valid = true, .start_col = animation_data.start_index_frame_working, .end_col = animation_data.end_index_frame_working, .row = MS_AGENT_SPRITE_SHEET_ROW_WORKING }; - } - if (static_cast(sprite_sheet_rows - 1) >= MS_AGENT_SPRITE_SHEET_ROW_END_WORKING) { - result.result.end_working = { .valid = true, .start_col = animation_data.start_index_frame_end_working, .end_col = animation_data.end_index_frame_end_working, .row = MS_AGENT_SPRITE_SHEET_ROW_END_WORKING }; - } - if (static_cast(sprite_sheet_rows - 1) >= MS_AGENT_SPRITE_SHEET_ROW_START_MOVING) { - result.result.start_moving = { .valid = true, .start_col = animation_data.start_index_frame_start_moving, .end_col = animation_data.end_index_frame_start_moving, .row = MS_AGENT_SPRITE_SHEET_ROW_START_MOVING }; - } - if (static_cast(sprite_sheet_rows - 1) >= MS_AGENT_SPRITE_SHEET_ROW_MOVING) { - result.result.moving = { .valid = true, .start_col = animation_data.start_index_frame_moving, .end_col = animation_data.end_index_frame_moving, .row = MS_AGENT_SPRITE_SHEET_ROW_MOVING }; - } - if (static_cast(sprite_sheet_rows - 1) >= MS_AGENT_SPRITE_SHEET_ROW_END_MOVING) { - result.result.end_moving = { .valid = true, .start_col = animation_data.start_index_frame_end_moving, .end_col = animation_data.end_index_frame_end_moving, .row = MS_AGENT_SPRITE_SHEET_ROW_END_MOVING }; - } - //if (static_cast(sprite_sheet_rows - 1) >= MS_AGENT_SPRITE_SHEET_ROW_HAPPY) { - // result.result.happy = { .valid = true, .start_col = animation_data.start_index_frame_happy), .end_col = animation_data.end_index_happy_moving), .row = MS_AGENT_SPRITE_SHEET_ROW_HAPPY }; - //} - } + // setup animation frames data + if (result.result.frame_height > 0 && sprite_sheet_rows > 0) { + [[maybe_unused]] const auto rows = result.result.image.sprite_sheet_height / result.result.frame_height; + assert(rows == sprite_sheet_rows); - return bongocat::move(result.result); + assert(sprite_sheet_rows > 0); + if (static_cast(sprite_sheet_rows - 1) >= MS_AGENT_SPRITE_SHEET_ROW_IDLE) { + result.result.idle = {.valid = true, + .start_col = animation_data.start_index_frame_idle, + .end_col = animation_data.end_index_frame_idle, + .row = MS_AGENT_SPRITE_SHEET_ROW_IDLE}; + } + if (static_cast(sprite_sheet_rows - 1) >= MS_AGENT_SPRITE_SHEET_ROW_BORING) { + result.result.boring = {.valid = true, + .start_col = animation_data.start_index_frame_boring, + .end_col = animation_data.end_index_frame_boring, + .row = MS_AGENT_SPRITE_SHEET_ROW_BORING}; + } + if (static_cast(sprite_sheet_rows - 1) >= MS_AGENT_SPRITE_SHEET_ROW_START_WRITING) { + result.result.start_writing = {.valid = true, + .start_col = animation_data.start_index_frame_start_writing, + .end_col = animation_data.end_index_frame_start_writing, + .row = MS_AGENT_SPRITE_SHEET_ROW_START_WRITING}; + } + if (static_cast(sprite_sheet_rows - 1) >= MS_AGENT_SPRITE_SHEET_ROW_WRITING) { + result.result.writing = {.valid = true, + .start_col = animation_data.start_index_frame_writing, + .end_col = animation_data.end_index_frame_writing, + .row = MS_AGENT_SPRITE_SHEET_ROW_WRITING}; + } + if (static_cast(sprite_sheet_rows - 1) >= MS_AGENT_SPRITE_SHEET_ROW_END_WRITING) { + result.result.end_writing = {.valid = true, + .start_col = animation_data.start_index_frame_end_writing, + .end_col = animation_data.end_index_frame_end_writing, + .row = MS_AGENT_SPRITE_SHEET_ROW_END_WRITING}; + } + if (static_cast(sprite_sheet_rows - 1) >= MS_AGENT_SPRITE_SHEET_ROW_SLEEP) { + result.result.sleep = {.valid = true, + .start_col = animation_data.start_index_frame_sleep, + .end_col = animation_data.end_index_frame_sleep, + .row = MS_AGENT_SPRITE_SHEET_ROW_SLEEP}; + } + if (static_cast(sprite_sheet_rows - 1) >= MS_AGENT_SPRITE_SHEET_ROW_WAKE_UP) { + result.result.wake_up = {.valid = true, + .start_col = animation_data.start_index_frame_wake_up, + .end_col = animation_data.end_index_frame_wake_up, + .row = MS_AGENT_SPRITE_SHEET_ROW_WAKE_UP}; } + if (static_cast(sprite_sheet_rows - 1) >= MS_AGENT_SPRITE_SHEET_ROW_START_WORKING) { + result.result.start_working = {.valid = true, + .start_col = animation_data.start_index_frame_start_working, + .end_col = animation_data.end_index_frame_start_working, + .row = MS_AGENT_SPRITE_SHEET_ROW_START_WORKING}; + } + if (static_cast(sprite_sheet_rows - 1) >= MS_AGENT_SPRITE_SHEET_ROW_WORKING) { + result.result.working = {.valid = true, + .start_col = animation_data.start_index_frame_working, + .end_col = animation_data.end_index_frame_working, + .row = MS_AGENT_SPRITE_SHEET_ROW_WORKING}; + } + if (static_cast(sprite_sheet_rows - 1) >= MS_AGENT_SPRITE_SHEET_ROW_END_WORKING) { + result.result.end_working = {.valid = true, + .start_col = animation_data.start_index_frame_end_working, + .end_col = animation_data.end_index_frame_end_working, + .row = MS_AGENT_SPRITE_SHEET_ROW_END_WORKING}; + } + if (static_cast(sprite_sheet_rows - 1) >= MS_AGENT_SPRITE_SHEET_ROW_START_MOVING) { + result.result.start_moving = {.valid = true, + .start_col = animation_data.start_index_frame_start_moving, + .end_col = animation_data.end_index_frame_start_moving, + .row = MS_AGENT_SPRITE_SHEET_ROW_START_MOVING}; + } + if (static_cast(sprite_sheet_rows - 1) >= MS_AGENT_SPRITE_SHEET_ROW_MOVING) { + result.result.moving = {.valid = true, + .start_col = animation_data.start_index_frame_moving, + .end_col = animation_data.end_index_frame_moving, + .row = MS_AGENT_SPRITE_SHEET_ROW_MOVING}; + } + if (static_cast(sprite_sheet_rows - 1) >= MS_AGENT_SPRITE_SHEET_ROW_END_MOVING) { + result.result.end_moving = {.valid = true, + .start_col = animation_data.start_index_frame_end_moving, + .end_col = animation_data.end_index_frame_end_moving, + .row = MS_AGENT_SPRITE_SHEET_ROW_END_MOVING}; + } + // if (static_cast(sprite_sheet_rows - 1) >= MS_AGENT_SPRITE_SHEET_ROW_HAPPY) { + // result.result.happy = { .valid = true, .start_col = animation_data.start_index_frame_happy), .end_col = + // animation_data.end_index_happy_moving), .row = MS_AGENT_SPRITE_SHEET_ROW_HAPPY }; + // } + } - created_result_t init_ms_agent_anim(animation_context_t& ctx, int anim_index, const assets::embedded_image_t& sprite_sheet_image, int sprite_sheet_cols, int sprite_sheet_rows, const assets::ms_agent_animation_indices_t& animation_data) { - using namespace assets; - BONGOCAT_CHECK_NULL(ctx.shm.ptr, bongocat_error_t::BONGOCAT_ERROR_INVALID_PARAM); - BONGOCAT_CHECK_NULL(ctx._local_copy_config.ptr, bongocat_error_t::BONGOCAT_ERROR_INVALID_PARAM); - - assert(anim_index >= 0 && static_cast(anim_index) < MS_AGENTS_ANIM_COUNT); - BONGOCAT_LOG_VERBOSE("Load MS agent Animation (%d/%d): %s ...", anim_index, MS_AGENTS_ANIM_COUNT, sprite_sheet_image.name); - auto result = load_ms_agent_anim(ctx, anim_index, sprite_sheet_image, sprite_sheet_cols, sprite_sheet_rows, animation_data); - if (result.error != bongocat_error_t::BONGOCAT_SUCCESS) { - BONGOCAT_LOG_ERROR("Load MS agent Animation failed: %s, index: %d", sprite_sheet_image.name, anim_index); - return bongocat_error_t::BONGOCAT_ERROR_ANIMATION; - } - assert(result.error == bongocat_error_t::BONGOCAT_SUCCESS); ///< this SHOULD always work if it's an valid EMBEDDED image + return bongocat::move(result.result); +} - assert(anim_index >= 0); - ctx.shm->ms_anims[static_cast(anim_index)] = bongocat::move(result.result); - assert(ctx.shm->ms_anims[static_cast(anim_index)].type == animation_t::Type::MsAgent); +created_result_t +init_ms_agent_anim(animation_thread_context_t& ctx, int anim_index, const assets::embedded_image_t& sprite_sheet_image, + int sprite_sheet_cols, int sprite_sheet_rows, + const assets::ms_agent_animation_indices_t& animation_data) { + using namespace assets; + BONGOCAT_CHECK_NULL(ctx.shm.ptr, bongocat_error_t::BONGOCAT_ERROR_INVALID_PARAM); + BONGOCAT_CHECK_NULL(ctx._local_copy_config.ptr, bongocat_error_t::BONGOCAT_ERROR_INVALID_PARAM); - return bongocat_error_t::BONGOCAT_SUCCESS; - } + assert(anim_index >= 0 && static_cast(anim_index) < MS_AGENTS_ANIM_COUNT); + BONGOCAT_LOG_VERBOSE("Load MS agent Animation (%d/%d): %s ...", anim_index, MS_AGENTS_ANIM_COUNT, + sprite_sheet_image.name); + auto result = + load_ms_agent_anim(ctx, anim_index, sprite_sheet_image, sprite_sheet_cols, sprite_sheet_rows, animation_data); + if (result.error != bongocat_error_t::BONGOCAT_SUCCESS) { + BONGOCAT_LOG_ERROR("Load MS agent Animation failed: %s, index: %d", sprite_sheet_image.name, anim_index); + return bongocat_error_t::BONGOCAT_ERROR_ANIMATION; + } + assert(result.error == + bongocat_error_t::BONGOCAT_SUCCESS); ///< this SHOULD always work if it's an valid EMBEDDED image - created_result_t load_ms_agent_sprite_sheet(const animation_context_t& ctx, int index) { - using namespace assets; - using namespace animation; - switch (index) { - case CLIPPY_ANIM_INDEX: return load_ms_agent_anim(ctx, CLIPPY_ANIM_INDEX, get_ms_agent_sprite_sheet(CLIPPY_ANIM_INDEX), CLIPPY_SPRITE_SHEET_COLS, CLIPPY_SPRITE_SHEET_ROWS, get_ms_agent_animation_indices(CLIPPY_ANIM_INDEX)); + assert(anim_index >= 0); + ctx.shm->ms_anims[static_cast(anim_index)] = bongocat::move(result.result); + assert(ctx.shm->ms_anims[static_cast(anim_index)].type == animation_t::type_t::MsAgent); + + return bongocat_error_t::BONGOCAT_SUCCESS; +} + +created_result_t load_ms_agent_sprite_sheet(const animation_thread_context_t& ctx, int index) { + using namespace assets; + using namespace animation; + switch (index) { + case CLIPPY_ANIM_INDEX: + return load_ms_agent_anim(ctx, CLIPPY_ANIM_INDEX, get_ms_agent_sprite_sheet(CLIPPY_ANIM_INDEX), + CLIPPY_SPRITE_SHEET_COLS, CLIPPY_SPRITE_SHEET_ROWS, + get_ms_agent_animation_indices(CLIPPY_ANIM_INDEX)); #ifdef FEATURE_MORE_MS_AGENT_EMBEDDED_ASSETS - case LINKS_ANIM_INDEX: return load_ms_agent_anim(ctx, LINKS_ANIM_INDEX, get_ms_agent_sprite_sheet(LINKS_ANIM_INDEX), LINKS_SPRITE_SHEET_COLS, LINKS_SPRITE_SHEET_ROWS, get_ms_agent_animation_indices(LINKS_ANIM_INDEX)); - case ROVER_ANIM_INDEX: return load_ms_agent_anim(ctx, ROVER_ANIM_INDEX, get_ms_agent_sprite_sheet(ROVER_ANIM_INDEX), ROVER_SPRITE_SHEET_COLS, ROVER_SPRITE_SHEET_ROWS, get_ms_agent_animation_indices(ROVER_ANIM_INDEX)); - case MERLIN_ANIM_INDEX: return load_ms_agent_anim(ctx, MERLIN_ANIM_INDEX, get_ms_agent_sprite_sheet(MERLIN_ANIM_INDEX), MERLIN_SPRITE_SHEET_COLS, MERLIN_SPRITE_SHEET_ROWS, get_ms_agent_animation_indices(MERLIN_ANIM_INDEX)); + case LINKS_ANIM_INDEX: + return load_ms_agent_anim(ctx, LINKS_ANIM_INDEX, get_ms_agent_sprite_sheet(LINKS_ANIM_INDEX), + LINKS_SPRITE_SHEET_COLS, LINKS_SPRITE_SHEET_ROWS, + get_ms_agent_animation_indices(LINKS_ANIM_INDEX)); + case ROVER_ANIM_INDEX: + return load_ms_agent_anim(ctx, ROVER_ANIM_INDEX, get_ms_agent_sprite_sheet(ROVER_ANIM_INDEX), + ROVER_SPRITE_SHEET_COLS, ROVER_SPRITE_SHEET_ROWS, + get_ms_agent_animation_indices(ROVER_ANIM_INDEX)); + case MERLIN_ANIM_INDEX: + return load_ms_agent_anim(ctx, MERLIN_ANIM_INDEX, get_ms_agent_sprite_sheet(MERLIN_ANIM_INDEX), + MERLIN_SPRITE_SHEET_COLS, MERLIN_SPRITE_SHEET_ROWS, + get_ms_agent_animation_indices(MERLIN_ANIM_INDEX)); #endif - default: return bongocat_error_t::BONGOCAT_ERROR_INVALID_PARAM; - } - return bongocat_error_t::BONGOCAT_ERROR_INVALID_PARAM; - } + default: + return bongocat_error_t::BONGOCAT_ERROR_INVALID_PARAM; + } + return bongocat_error_t::BONGOCAT_ERROR_INVALID_PARAM; } +} // namespace bongocat::animation diff --git a/src/image_loader/pen/CMakeLists.txt b/src/image_loader/pen/CMakeLists.txt index 073b6ee9..190352a2 100644 --- a/src/image_loader/pen/CMakeLists.txt +++ b/src/image_loader/pen/CMakeLists.txt @@ -1,9 +1,11 @@ add_library(assets_pen_loader STATIC) target_sources(assets_pen_loader PRIVATE pen_load_sprite_sheet.cpp load_images_pen.cpp) target_compile_options(assets_pen_loader PRIVATE -ffunction-sections -fdata-sections) -target_include_directories(assets_pen_loader - PRIVATE ${INCLUDE_DIR}/embedded_assets/pen ${INCLUDE_DIR}/image_loader/pen - PUBLIC ${INCLUDE_DIR}) -target_link_libraries(assets_pen_loader - PUBLIC assets_image_loader assets_base_dm_loader assets_pen - PRIVATE assets_pen_interface assets_pen_feature bongocat_options) \ No newline at end of file +target_include_directories( + assets_pen_loader + PRIVATE ${INCLUDE_DIR}/embedded_assets/pen ${INCLUDE_DIR}/image_loader/pen + PUBLIC ${INCLUDE_DIR}) +target_link_libraries( + assets_pen_loader + PUBLIC assets_image_loader assets_base_dm_loader assets_pen + PRIVATE assets_pen_interface assets_pen_feature bongocat_options) diff --git a/src/image_loader/pen/load_images_pen.cpp b/src/image_loader/pen/load_images_pen.cpp index b8d74ba5..e3767f6e 100644 --- a/src/image_loader/pen/load_images_pen.cpp +++ b/src/image_loader/pen/load_images_pen.cpp @@ -1,11 +1,11 @@ #include "load_images_pen.h" #include "embedded_assets/pen/pen.hpp" -#include "graphics/animation_context.h" +#include "graphics/animation_thread_context.h" #include "image_loader/load_images.h" #include "image_loader/base_dm/load_dm.h" namespace bongocat::animation { -bongocat_error_t init_pen_anim(animation_context_t& ctx, int anim_index, const assets::embedded_image_t& sprite_sheet_image, int sprite_sheet_cols, int sprite_sheet_rows) { +bongocat_error_t init_pen_anim(animation_thread_context_t& ctx, int anim_index, const assets::embedded_image_t& sprite_sheet_image, int sprite_sheet_cols, int sprite_sheet_rows) { using namespace assets; BONGOCAT_CHECK_NULL(ctx.shm.ptr, bongocat_error_t::BONGOCAT_ERROR_INVALID_PARAM); BONGOCAT_CHECK_NULL(ctx._local_copy_config.ptr, bongocat_error_t::BONGOCAT_ERROR_INVALID_PARAM); @@ -21,7 +21,7 @@ bongocat_error_t init_pen_anim(animation_context_t& ctx, int anim_index, const a assert(anim_index >= 0); ctx.shm->pen_anims[static_cast(anim_index)] = bongocat::move(result.result); - assert(ctx.shm->pen_anims[static_cast(anim_index)].type == animation_t::Type::Dm); + assert(ctx.shm->pen_anims[static_cast(anim_index)].type == animation_t::type_t::Dm); return bongocat_error_t::BONGOCAT_SUCCESS; } diff --git a/src/image_loader/pen/pen_load_sprite_sheet.cpp b/src/image_loader/pen/pen_load_sprite_sheet.cpp index fc8864aa..b8e9cd46 100644 --- a/src/image_loader/pen/pen_load_sprite_sheet.cpp +++ b/src/image_loader/pen/pen_load_sprite_sheet.cpp @@ -1,5 +1,5 @@ #include "core/bongocat.h" -#include "graphics/animation_context.h" +#include "graphics/animation_thread_context.h" #include "graphics/sprite_sheet.h" #include "image_loader/base_dm/load_dm.h" #include "embedded_assets/pen/pen.hpp" @@ -8,7 +8,7 @@ #include "image_loader/pen/load_images_pen.h" namespace bongocat::animation { - created_result_t load_pen_sprite_sheet(const animation_context_t& ctx, int index) { + created_result_t load_pen_sprite_sheet(const animation_thread_context_t& ctx, int index) { using namespace assets; switch (index) { case PEN_AERO_V_DRAMON_ANIM_INDEX: return load_dm_anim(ctx, PEN_AERO_V_DRAMON_ANIM_INDEX, get_pen_sprite_sheet(PEN_AERO_V_DRAMON_ANIM_INDEX), PEN_AERO_V_DRAMON_SPRITE_SHEET_COLS, PEN_AERO_V_DRAMON_SPRITE_SHEET_ROWS); diff --git a/src/image_loader/pen20/CMakeLists.txt b/src/image_loader/pen20/CMakeLists.txt index c7dcb0df..e56d9d24 100644 --- a/src/image_loader/pen20/CMakeLists.txt +++ b/src/image_loader/pen20/CMakeLists.txt @@ -1,9 +1,11 @@ add_library(assets_pen20_loader STATIC) target_sources(assets_pen20_loader PRIVATE pen20_load_sprite_sheet.cpp load_images_pen20.cpp) target_compile_options(assets_pen20_loader PRIVATE -ffunction-sections -fdata-sections) -target_include_directories(assets_pen20_loader - PRIVATE ${INCLUDE_DIR}/embedded_assets/pen20 ${INCLUDE_DIR}/image_loader/pen20 - PUBLIC ${INCLUDE_DIR}) -target_link_libraries(assets_pen20_loader - PUBLIC assets_image_loader assets_base_dm_loader assets_pen20 - PRIVATE assets_pen20_interface assets_pen20_feature bongocat_options) \ No newline at end of file +target_include_directories( + assets_pen20_loader + PRIVATE ${INCLUDE_DIR}/embedded_assets/pen20 ${INCLUDE_DIR}/image_loader/pen20 + PUBLIC ${INCLUDE_DIR}) +target_link_libraries( + assets_pen20_loader + PUBLIC assets_image_loader assets_base_dm_loader assets_pen20 + PRIVATE assets_pen20_interface assets_pen20_feature bongocat_options) diff --git a/src/image_loader/pen20/load_images_pen20.cpp b/src/image_loader/pen20/load_images_pen20.cpp index a089b1ce..a509301b 100644 --- a/src/image_loader/pen20/load_images_pen20.cpp +++ b/src/image_loader/pen20/load_images_pen20.cpp @@ -1,11 +1,11 @@ #include "load_images_pen20.h" #include "embedded_assets/pen20/pen20.hpp" -#include "graphics/animation_context.h" +#include "graphics/animation_thread_context.h" #include "image_loader/load_images.h" #include "image_loader/base_dm/load_dm.h" namespace bongocat::animation { -bongocat_error_t init_pen20_anim(animation_context_t& ctx, int anim_index, const assets::embedded_image_t& sprite_sheet_image, int sprite_sheet_cols, int sprite_sheet_rows) { +bongocat_error_t init_pen20_anim(animation_thread_context_t& ctx, int anim_index, const assets::embedded_image_t& sprite_sheet_image, int sprite_sheet_cols, int sprite_sheet_rows) { using namespace assets; BONGOCAT_CHECK_NULL(ctx.shm.ptr, bongocat_error_t::BONGOCAT_ERROR_INVALID_PARAM); BONGOCAT_CHECK_NULL(ctx._local_copy_config.ptr, bongocat_error_t::BONGOCAT_ERROR_INVALID_PARAM); @@ -21,7 +21,7 @@ bongocat_error_t init_pen20_anim(animation_context_t& ctx, int anim_index, const assert(anim_index >= 0); ctx.shm->pen20_anims[static_cast(anim_index)] = bongocat::move(result.result); - assert(ctx.shm->pen20_anims[static_cast(anim_index)].type == animation_t::Type::Dm); + assert(ctx.shm->pen20_anims[static_cast(anim_index)].type == animation_t::type_t::Dm); return bongocat_error_t::BONGOCAT_SUCCESS; } diff --git a/src/image_loader/pen20/pen20_load_sprite_sheet.cpp b/src/image_loader/pen20/pen20_load_sprite_sheet.cpp index cd7f02f7..b94aac08 100644 --- a/src/image_loader/pen20/pen20_load_sprite_sheet.cpp +++ b/src/image_loader/pen20/pen20_load_sprite_sheet.cpp @@ -1,5 +1,5 @@ #include "core/bongocat.h" -#include "graphics/animation_context.h" +#include "graphics/animation_thread_context.h" #include "graphics/sprite_sheet.h" #include "image_loader/base_dm/load_dm.h" #include "embedded_assets/pen20/pen20.hpp" @@ -8,7 +8,7 @@ #include "image_loader/pen20/load_images_pen20.h" namespace bongocat::animation { - created_result_t load_pen20_sprite_sheet(const animation_context_t& ctx, int index) { + created_result_t load_pen20_sprite_sheet(const animation_thread_context_t& ctx, int index) { using namespace assets; switch (index) { case PEN20_AERO_V_DRAMON_ANIM_INDEX: return load_dm_anim(ctx, PEN20_AERO_V_DRAMON_ANIM_INDEX, get_pen20_sprite_sheet(PEN20_AERO_V_DRAMON_ANIM_INDEX), PEN20_AERO_V_DRAMON_SPRITE_SHEET_COLS, PEN20_AERO_V_DRAMON_SPRITE_SHEET_ROWS); diff --git a/src/image_loader/pkmn/CMakeLists.txt b/src/image_loader/pkmn/CMakeLists.txt index e0414b8a..798a242b 100644 --- a/src/image_loader/pkmn/CMakeLists.txt +++ b/src/image_loader/pkmn/CMakeLists.txt @@ -1,9 +1,11 @@ add_library(assets_pkmn_loader STATIC) target_sources(assets_pkmn_loader PRIVATE pkmn_load_sprite_sheet.cpp load_images_pkmn.cpp) target_compile_options(assets_pkmn_loader PRIVATE -ffunction-sections -fdata-sections) -target_include_directories(assets_pkmn_loader - PRIVATE ${INCLUDE_DIR}/embedded_assets/pkmn ${INCLUDE_DIR}/image_loader/pkmn - PUBLIC ${INCLUDE_DIR}) -target_link_libraries(assets_pkmn_loader - PUBLIC assets_image_loader assets_pkmn - PRIVATE assets_pkmn_interface assets_pkmn_feature bongocat_options) \ No newline at end of file +target_include_directories( + assets_pkmn_loader + PRIVATE ${INCLUDE_DIR}/embedded_assets/pkmn ${INCLUDE_DIR}/image_loader/pkmn + PUBLIC ${INCLUDE_DIR}) +target_link_libraries( + assets_pkmn_loader + PUBLIC assets_image_loader assets_pkmn + PRIVATE assets_pkmn_interface assets_pkmn_feature bongocat_options) diff --git a/src/image_loader/pkmn/load_images_pkmn.cpp b/src/image_loader/pkmn/load_images_pkmn.cpp index 15f01fe6..89248d83 100644 --- a/src/image_loader/pkmn/load_images_pkmn.cpp +++ b/src/image_loader/pkmn/load_images_pkmn.cpp @@ -1,5 +1,5 @@ #include "load_images_pkmn.h" -#include "graphics/animation_context.h" +#include "graphics/animation_thread_context.h" #include "image_loader/base_dm/load_dm.h" #include "embedded_assets/embedded_image.h" #include "embedded_assets/pkmn/pkmn.hpp" @@ -7,7 +7,7 @@ #include "utils/error.h" namespace bongocat::animation { - created_result_t load_pkmn_anim(const animation_context_t& ctx, [[maybe_unused]] int anim_index, const assets::embedded_image_t& sprite_sheet_image, int sprite_sheet_cols, int sprite_sheet_rows) { + created_result_t load_pkmn_anim(const animation_thread_context_t& ctx, [[maybe_unused]] int anim_index, const assets::embedded_image_t& sprite_sheet_image, int sprite_sheet_cols, int sprite_sheet_rows) { using namespace assets; BONGOCAT_CHECK_NULL(ctx._local_copy_config.ptr, bongocat_error_t::BONGOCAT_ERROR_INVALID_PARAM); @@ -89,7 +89,7 @@ namespace bongocat::animation { return ret; } - bongocat_error_t init_pkmn_anim(animation_context_t& ctx, int anim_index, const assets::embedded_image_t& sprite_sheet_image, int sprite_sheet_cols, int sprite_sheet_rows) { + bongocat_error_t init_pkmn_anim(animation_thread_context_t& ctx, int anim_index, const assets::embedded_image_t& sprite_sheet_image, int sprite_sheet_cols, int sprite_sheet_rows) { using namespace assets; BONGOCAT_CHECK_NULL(ctx.shm.ptr, bongocat_error_t::BONGOCAT_ERROR_INVALID_PARAM); BONGOCAT_CHECK_NULL(ctx._local_copy_config.ptr, bongocat_error_t::BONGOCAT_ERROR_INVALID_PARAM); @@ -105,7 +105,7 @@ namespace bongocat::animation { assert(anim_index >= 0); ctx.shm->pkmn_anims[static_cast(anim_index)] = bongocat::move(result.result); - assert(ctx.shm->pkmn_anims[static_cast(anim_index)].type == animation_t::Type::Pkmn); + assert(ctx.shm->pkmn_anims[static_cast(anim_index)].type == animation_t::type_t::Pkmn); return bongocat_error_t::BONGOCAT_SUCCESS; } diff --git a/src/image_loader/pkmn/pkmn_load_sprite_sheet.cpp b/src/image_loader/pkmn/pkmn_load_sprite_sheet.cpp index 56934d9c..d272fa3c 100644 --- a/src/image_loader/pkmn/pkmn_load_sprite_sheet.cpp +++ b/src/image_loader/pkmn/pkmn_load_sprite_sheet.cpp @@ -1,5 +1,5 @@ #include "core/bongocat.h" -#include "graphics/animation_context.h" +#include "graphics/animation_thread_context.h" #include "graphics/sprite_sheet.h" #include "embedded_assets/pkmn/pkmn.hpp" #include "embedded_assets/embedded_image.h" @@ -7,7 +7,7 @@ #include "image_loader/pkmn/load_images_pkmn.h" namespace bongocat::animation { - created_result_t load_pkmn_sprite_sheet(const animation_context_t& ctx, int index) { + created_result_t load_pkmn_sprite_sheet(const animation_thread_context_t& ctx, int index) { using namespace assets; switch (index) { case PKMN_BULBASAUR_ANIM_INDEX: return load_pkmn_anim(ctx, PKMN_BULBASAUR_ANIM_INDEX, get_pkmn_sprite_sheet(PKMN_BULBASAUR_ANIM_INDEX), PKMN_BULBASAUR_SPRITE_SHEET_COLS, PKMN_BULBASAUR_SPRITE_SHEET_ROWS); diff --git a/src/image_loader/pmd/CMakeLists.txt b/src/image_loader/pmd/CMakeLists.txt index 921ff213..09849fba 100644 --- a/src/image_loader/pmd/CMakeLists.txt +++ b/src/image_loader/pmd/CMakeLists.txt @@ -1,9 +1,11 @@ add_library(assets_pmd_loader STATIC) target_sources(assets_pmd_loader PRIVATE load_images_pmd.cpp pmd_load_sprite_sheet.cpp) target_compile_options(assets_pmd_loader PRIVATE -ffunction-sections -fdata-sections) -target_include_directories(assets_pmd_loader - PRIVATE ${INCLUDE_DIR}/embedded_assets/misc ${INCLUDE_DIR}/image_loader/pmd - PUBLIC ${INCLUDE_DIR}) -target_link_libraries(assets_pmd_loader - PUBLIC assets_image_loader assets_custom_loader assets_pmd - PRIVATE assets_pmd_interface assets_pmd_feature bongocat_options) \ No newline at end of file +target_include_directories( + assets_pmd_loader + PRIVATE ${INCLUDE_DIR}/embedded_assets/misc ${INCLUDE_DIR}/image_loader/pmd + PUBLIC ${INCLUDE_DIR}) +target_link_libraries( + assets_pmd_loader + PUBLIC assets_image_loader assets_custom_loader assets_pmd + PRIVATE assets_pmd_interface assets_pmd_feature bongocat_options) diff --git a/src/image_loader/pmd/load_images_pmd.cpp b/src/image_loader/pmd/load_images_pmd.cpp index 6072e5f6..564bcb45 100644 --- a/src/image_loader/pmd/load_images_pmd.cpp +++ b/src/image_loader/pmd/load_images_pmd.cpp @@ -1,11 +1,11 @@ #include "load_images_pmd.h" -#include "graphics/animation_context.h" +#include "graphics/animation_thread_context.h" #include "image_loader/custom/load_custom.h" #include "embedded_assets/embedded_image.h" #include "embedded_assets/pmd/pmd.hpp" namespace bongocat::animation { - bongocat_error_t init_pmd_anim(animation_context_t& ctx, int anim_index, const assets::embedded_image_t& sprite_sheet_image, const assets::custom_animation_settings_t& sprite_sheet_settings) { + bongocat_error_t init_pmd_anim(animation_thread_context_t& ctx, int anim_index, const assets::embedded_image_t& sprite_sheet_image, const assets::custom_animation_settings_t& sprite_sheet_settings) { using namespace assets; BONGOCAT_CHECK_NULL(ctx.shm.ptr, bongocat_error_t::BONGOCAT_ERROR_INVALID_PARAM); BONGOCAT_CHECK_NULL(ctx._local_copy_config.ptr, bongocat_error_t::BONGOCAT_ERROR_INVALID_PARAM); @@ -20,7 +20,7 @@ namespace bongocat::animation { assert(anim_index >= 0); ctx.shm->pmd_anims[static_cast(anim_index)] = bongocat::move(result.result); - assert(ctx.shm->pmd_anims[static_cast(anim_index)].type == animation_t::Type::Custom); + assert(ctx.shm->pmd_anims[static_cast(anim_index)].type == animation_t::type_t::Custom); return bongocat_error_t::BONGOCAT_SUCCESS; } diff --git a/src/image_loader/pmd/pmd_load_sprite_sheet.cpp b/src/image_loader/pmd/pmd_load_sprite_sheet.cpp index aa69ea58..1dc59b40 100644 --- a/src/image_loader/pmd/pmd_load_sprite_sheet.cpp +++ b/src/image_loader/pmd/pmd_load_sprite_sheet.cpp @@ -1,5 +1,5 @@ #include "core/bongocat.h" -#include "graphics/animation_context.h" +#include "graphics/animation_thread_context.h" #include "graphics/sprite_sheet.h" #include "image_loader/custom/load_custom.h" #include "embedded_assets/pmd/pmd.hpp" @@ -8,7 +8,7 @@ #include "image_loader/pmd/load_images_pmd.h" namespace bongocat::animation { - created_result_t load_pmd_sprite_sheet(const animation_context_t& ctx, int index) { + created_result_t load_pmd_sprite_sheet(const animation_thread_context_t& ctx, int index) { using namespace assets; switch (index) { case PMD_BULBASAUR_ANIM_INDEX: return load_custom_anim(ctx, get_pmd_sprite_sheet(PMD_BULBASAUR_ANIM_INDEX), get_pmd_sprite_sheet_settings(PMD_BULBASAUR_ANIM_INDEX)); diff --git a/src/platform/input.cpp b/src/platform/input.cpp index d2c9c39c..c61a9df5 100644 --- a/src/platform/input.cpp +++ b/src/platform/input.cpp @@ -1,1211 +1,1298 @@ #include "platform/input.h" + #include "graphics/animation.h" -#include "utils/memory.h" #include "platform/wayland.h" -#include -#include -#include -#include -#include -#include -#include +#include "utils/memory.h" + #include +#include #include +#include +#include #include +#include #include -#include -#include +#include +#include +#include +#include +#include namespace bongocat::platform::input { - static inline constexpr size_t INPUT_EVENT_BUF = 128; - static inline constexpr size_t MAX_DEVICE_FDS = 256; - inline static constexpr int MAX_ATTEMPTS = 2048; +static inline constexpr size_t INPUT_EVENT_BUF = 128; +static inline constexpr size_t MAX_DEVICE_FDS = 256; +inline static constexpr int MAX_ATTEMPTS = 2048; + +static inline constexpr auto INPUT_POOL_TIMEOUT_MS = 10; + +static inline constexpr time_sec_t START_ADAPTIVE_CHECK_INTERVAL_SEC = 5; +static inline constexpr time_sec_t MID_ADAPTIVE_CHECK_INTERVAL_SEC = 15; +static inline constexpr time_sec_t MAX_ADAPTIVE_CHECK_INTERVAL_SEC = 30; + +static inline constexpr time_ms_t RESET_KPM_TIMEOUT_MS = 5 * 1000; + +inline static constexpr time_ms_t COND_ANIMATION_TRIGGER_INIT_TIMEOUT_MS = 5000; +inline static constexpr time_ms_t COND_RELOAD_CONFIGS_TIMEOUT_MS = 5000; + +inline static constexpr size_t TEST_STDIN_BUF_LEN = 256; + +// Uses Linux input keycodes from +// Left-hand keys on QWERTY keyboard +// clang-format off + inline static constexpr int INPUT_LEFT_KEYS[] = { + // Number row left half (1-6) + 2, 3, 4, 5, 6, 7, // KEY_1 to KEY_6 + // QWERTY row left half + 16, 17, 18, 19, 20, // KEY_Q, KEY_W, KEY_E, KEY_R, KEY_T + // Home row left half + 30, 31, 32, 33, 34, // KEY_A, KEY_S, KEY_D, KEY_F, KEY_G + // Bottom row left half + 44, 45, 46, 47, 48, // KEY_Z, KEY_X, KEY_C, KEY_V, KEY_B + // Modifiers and special keys (left side) + 1, // KEY_ESC + 15, // KEY_TAB + 58, // KEY_CAPSLOCK + 42, // KEY_LEFTSHIFT + 29, // KEY_LEFTCTRL + 56, // KEY_LEFTALT + 41, // KEY_GRAVE (backtick) + 125, // KEY_LEFTMETA (super) + }; +// clang-format on - static inline constexpr auto INPUT_POOL_TIMEOUT_MS = 10; +static input_hand_mapping_t get_hand_mapping_form_keycode([[maybe_unused]] const input_context_t& input, int keycode) { + // read-only config + assert(input._local_copy_config); + // const config::config_t& current_config = *input._local_copy_config; - static inline constexpr time_sec_t START_ADAPTIVE_CHECK_INTERVAL_SEC = 5; - static inline constexpr time_sec_t MID_ADAPTIVE_CHECK_INTERVAL_SEC = 15; - static inline constexpr time_sec_t MAX_ADAPTIVE_CHECK_INTERVAL_SEC = 30; + for (size_t i = 0; i < LEN_ARRAY(INPUT_LEFT_KEYS); i++) { + if (keycode == INPUT_LEFT_KEYS[i]) { + return input_hand_mapping_t::Left; // Left hand + } + } + return input_hand_mapping_t::Right; // Right hand (default for all other keys) +} - static inline constexpr time_ms_t RESET_KPM_TIMEOUT_MS = 5 * 1000; +static void cleanup_input_devices_paths(input_context_t& input, size_t device_paths_count) { + for (size_t i = 0; i < device_paths_count; i++) { + if (input._device_paths[i] != BONGOCAT_NULLPTR) { + ::free(input._device_paths[i]); + input._device_paths[i] = BONGOCAT_NULLPTR; + } + } + release_allocated_array(input._device_paths); +} - inline static constexpr time_ms_t COND_ANIMATION_TRIGGER_INIT_TIMEOUT_MS = 5000; - inline static constexpr time_ms_t COND_RELOAD_CONFIGS_TIMEOUT_MS = 5000; +static void cleanup_input_thread_context(input_context_t& input) { + cleanup_input_devices_paths(input, input._device_paths.count); + release_allocated_array(input._device_paths); + release_allocated_array(input._unique_paths_indices); + input._unique_paths_indices_capacity = 0; + release_allocated_array(input._unique_devices); + if (input._udev_mon != BONGOCAT_NULLPTR) { + udev_monitor_unref(input._udev_mon); + input._udev_mon = BONGOCAT_NULLPTR; + } + if (input._udev != BONGOCAT_NULLPTR) { + udev_unref(input._udev); + input._udev = BONGOCAT_NULLPTR; + } + input._udev_fd = -1; +} - inline static constexpr size_t TEST_STDIN_BUF_LEN = 256; +static void cleanup_input_thread(void *arg) { + assert(arg); + const animation::animation_context_t& animation_context = *static_cast(arg); + assert(animation_context._input); + input_context_t& input = *animation_context._input; - static void cleanup_input_devices_paths(input_context_t& input, size_t device_paths_count) { - for (size_t i = 0; i < device_paths_count; i++) { - if (input._device_paths[i]) ::free(input._device_paths[i]); - input._device_paths[i] = nullptr; - } - release_allocated_array(input._device_paths); - } + atomic_store(&input._capture_input_running, false); - static void cleanup_input_thread_context(input_context_t& input) { - cleanup_input_devices_paths(input, input._device_paths.count); - release_allocated_array(input._device_paths); - release_allocated_array(input._unique_paths_indices); - input._unique_paths_indices_capacity = 0; - release_allocated_array(input._unique_devices); - if (input._udev_mon) udev_monitor_unref(input._udev_mon); - if (input._udev) udev_unref(input._udev); - input._udev_mon = nullptr; - input._udev = nullptr; - input._udev_fd = -1; - } + input.config_updated.notify_all(); - static void cleanup_input_thread(void* arg) { - assert(arg); - animation::animation_session_t& trigger_ctx = *static_cast(arg); - assert(trigger_ctx._input); - input_context_t& input = *trigger_ctx._input; + cleanup_input_thread_context(*animation_context._input); - atomic_store(&input._capture_input_running, false); + BONGOCAT_LOG_INFO("Input thread cleanup completed (via pthread_cancel)"); +} - input.config_updated.notify_all(); +inline static bool is_device_valid(const char *path) { + struct stat fd_st{}; + return stat(path, &fd_st) == 0 && (S_ISCHR(fd_st.st_mode) && !S_ISLNK(fd_st.st_mode)); +} +inline static bool is_open_device_valid(int fd) { + struct stat fd_st{}; + return fd >= 0 && fstat(fd, &fd_st) == 0 && (S_ISCHR(fd_st.st_mode) && !S_ISLNK(fd_st.st_mode)); +} +inline static void trigger_key_press(animation::animation_context_t& animation_ctx, int keycode = 0) { + assert(animation_ctx._input); + // animation_context_t& anim = trigger_ctx.anim; + input_context_t& input = *animation_ctx._input; + + // read-only config + assert(input._local_copy_config); + const config::config_t& current_config = *input._local_copy_config; + + int timeout = INPUT_POOL_TIMEOUT_MS; + if (current_config.input_fps > 0) { + timeout = 1000 / current_config.input_fps; + } else if (current_config.fps > 0) { + timeout = 1000 / current_config.fps / 3; + } + + const timestamp_ms_t now = get_current_time_ms(); + const time_ms_t duration_ms = now - input._latest_kpm_update_ms; + time_ms_t min_key_press_check_time_ms = timeout * 2L; + if (current_config.input_fps > 0) { + min_key_press_check_time_ms = 2000 / current_config.input_fps; + } else if (current_config.fps > 0) { + min_key_press_check_time_ms = 2000 / current_config.fps; + } + if (duration_ms >= min_key_press_check_time_ms) { + const int input_kpm_counter = atomic_load(&input._input_kpm_counter); + if (input_kpm_counter > 0) { + if (duration_ms > 0) { + const double duration_min = static_cast(duration_ms) / 60000.0; + assert(duration_min > 0.0); + input.shm->kpm = static_cast(static_cast(input_kpm_counter) / duration_min); + } else { + input.shm->kpm = 0; + } + atomic_store(&input._input_kpm_counter, 0); + input._latest_kpm_update_ms = now; + } + } + input.shm->any_key_pressed = keycode != 0 ? 1 : 0; + input.shm->last_key_pressed_timestamp = now; + atomic_fetch_add(&input.shm->input_counter, 1); + atomic_fetch_add(&input._input_kpm_counter, 1); + if (current_config.enable_hand_mapping >= 1 && keycode != 0) { + input.shm->hand_mapping = get_hand_mapping_form_keycode(input, keycode); + } else { + input.shm->hand_mapping = input_hand_mapping_t::None; + } + animation::trigger(animation_ctx, animation::trigger_animation_cause_mask_t::KeyPress); +} - cleanup_input_thread_context(*trigger_ctx._input); +// for testing +static FileDescriptor open_tty_nonblocking() { + int fd = dup(STDIN_FILENO); + if (fd < 0) { + BONGOCAT_LOG_ERROR("dup stdin"); + return FileDescriptor(fd); + } + const int flags = fcntl(fd, F_GETFL, 0); + if (flags < 0) { + BONGOCAT_LOG_ERROR("fcntl getfl"); + close(fd); + fd = -1; + return FileDescriptor(fd); + } + if (fcntl(fd, F_SETFL, flags | O_NONBLOCK) < 0) { + BONGOCAT_LOG_ERROR("fcntl setfl"); + close(fd); + fd = -1; + return FileDescriptor(fd); + } + return FileDescriptor(fd); +} - BONGOCAT_LOG_INFO("Input thread cleanup completed (via pthread_cancel)"); +struct sync_devices_options_t { + bool reload_devices_needed{false}; +}; +struct sync_devices_options_result_t { + size_t valid_devices{0}; + size_t broken_symlink{0}; + size_t failed{0}; + size_t ignore{0}; +}; +BONGOCAT_NODISCARD static created_result_t +sync_devices(input_context_t& input, sync_devices_options_t options = {}) { + assert(input.shm); + + size_t valid_devices = 0; + // Ensure buffer size + if (input._unique_paths_indices_capacity < input._device_paths.count || options.reload_devices_needed) { + auto new_unique_devices = make_allocated_array(input._device_paths.count); + if (!new_unique_devices) { + BONGOCAT_LOG_ERROR("Failed to allocate memory for unique devices"); + return bongocat_error_t::BONGOCAT_ERROR_MEMORY; } - - inline static bool is_device_valid(const char* path) { - struct stat fd_st{}; - return stat(path, &fd_st) == 0 && (S_ISCHR(fd_st.st_mode) && !S_ISLNK(fd_st.st_mode)); + auto new_unique_paths_indices = make_allocated_array(input._device_paths.count); + if (!new_unique_paths_indices) { + release_allocated_array(input._unique_devices); + BONGOCAT_LOG_ERROR("Failed to allocate memory for unique path indices"); + return bongocat_error_t::BONGOCAT_ERROR_MEMORY; } - inline static bool is_open_device_valid(int fd) { - struct stat fd_st{}; - return fd >= 0 && fstat(fd, &fd_st) == 0 && (S_ISCHR(fd_st.st_mode) && !S_ISLNK(fd_st.st_mode)); + input._unique_devices = bongocat::move(new_unique_devices); + input._unique_paths_indices = bongocat::move(new_unique_paths_indices); + input._unique_paths_indices_capacity = input._device_paths.count; + } + + size_t broken_symlink = 0; + size_t failed = 0; + size_t ignore = 0; + + // recover real size for syncing + input._unique_devices.count = input._unique_paths_indices_capacity; + input._unique_paths_indices.count = input._unique_paths_indices_capacity; + size_t num_unique_devices = 0; + for (size_t i = 0; i < input._device_paths.count; i++) { + const char *device_path = input._device_paths[i]; + + // Resolve to canonical path + char resolved[PATH_MAX]; + const char *candidate = BONGOCAT_NULLPTR; + input_unique_file_type_t new_type = input_unique_file_type_t::File; + + struct stat lst{}; + if (lstat(device_path, &lst) == 0 && S_ISLNK(lst.st_mode)) { + new_type = input_unique_file_type_t::Symlink; + if (realpath(device_path, resolved) != BONGOCAT_NULLPTR) { + candidate = resolved; + } else { + BONGOCAT_LOG_WARNING("Broken symlink: %s", device_path); + broken_symlink++; + continue; + } + } else { + candidate = device_path; } - inline static void trigger_key_press(animation::animation_session_t& trigger_ctx) { - assert(trigger_ctx._input); - //animation_context_t& anim = trigger_ctx.anim; - input_context_t& input = *trigger_ctx._input; - - // read-only config - assert(input._local_copy_config != nullptr); - const config::config_t& current_config = *input._local_copy_config; - - int timeout = INPUT_POOL_TIMEOUT_MS; - if (current_config.input_fps > 0) { - timeout = 1000 / current_config.input_fps; - } else if (current_config.fps > 0) { - timeout = 1000 / current_config.fps / 3; - } - const timestamp_ms_t now = get_current_time_ms(); - const time_ms_t duration_ms = now - input._latest_kpm_update_ms; - time_ms_t min_key_press_check_time_ms = timeout*2; - if (current_config.input_fps > 0) { - min_key_press_check_time_ms = 2000 / current_config.input_fps; - } else if (current_config.fps > 0) { - min_key_press_check_time_ms = 2000 / current_config.fps; - } - if (duration_ms >= min_key_press_check_time_ms) { - const int input_kpm_counter = atomic_load(&input._input_kpm_counter); - if (input_kpm_counter > 0) { - if (duration_ms > 0) { - const double duration_min = static_cast(duration_ms) / 60000.0; - assert(duration_min > 0.0); - input.shm->kpm = static_cast(static_cast(input_kpm_counter) / duration_min); - } else { - input.shm->kpm = 0; - } - atomic_store(&input._input_kpm_counter, 0); - input._latest_kpm_update_ms = now; - } - } - input.shm->last_key_pressed_timestamp = now; - atomic_fetch_add(&input.shm->input_counter, 1); - atomic_fetch_add(&input._input_kpm_counter, 1); - animation::trigger(trigger_ctx, animation::trigger_animation_cause_mask_t::KeyPress); + // Skip if non-existent + if (access(candidate, F_OK) != 0) { + failed++; + BONGOCAT_LOG_WARNING("Device missing: %s", candidate); + continue; } - // for testing - static FileDescriptor open_tty_nonblocking() { - int fd = dup(STDIN_FILENO); - if (fd < 0) { - BONGOCAT_LOG_ERROR("dup stdin"); - return FileDescriptor(fd); - } - int flags = fcntl(fd, F_GETFL, 0); - if (flags < 0) { - BONGOCAT_LOG_ERROR("fcntl getfl"); - close(fd); - fd = -1; - return FileDescriptor(fd); - } - if (fcntl(fd, F_SETFL, flags | O_NONBLOCK) < 0) { - BONGOCAT_LOG_ERROR("fcntl setfl"); - close(fd); - fd = -1; - return FileDescriptor(fd); - } - return FileDescriptor(fd); + // Check if we already added this canonical path + bool duplicate = false; + for (size_t j = 0; j < num_unique_devices; j++) { + const input_unique_file_t& prev = input._unique_devices[j]; + if (prev.canonical_path != BONGOCAT_NULLPTR && strcmp(prev.canonical_path, candidate) == 0) { + duplicate = true; + break; + } + } + if (duplicate) { + continue; } - struct sync_devices_options_t { - bool reload_devices_needed{false}; - }; - struct sync_devices_options_result_t { - size_t valid_devices{0}; - size_t broken_symlink{0}; - size_t failed{0}; - size_t ignore{0}; - }; - [[nodiscard]] static created_result_t sync_devices(input_context_t& input, sync_devices_options_t options = {}) { - assert(input.shm != nullptr); - - size_t valid_devices = 0; - // Ensure buffer size - if (input._unique_paths_indices_capacity < input._device_paths.count || options.reload_devices_needed) { - auto new_unique_devices = make_allocated_array(input._device_paths.count); - if (!new_unique_devices) { - BONGOCAT_LOG_ERROR("Failed to allocate memory for unique devices"); - return bongocat_error_t::BONGOCAT_ERROR_MEMORY; - } - auto new_unique_paths_indices = make_allocated_array(input._device_paths.count); - if (!new_unique_paths_indices) { - release_allocated_array(input._unique_devices); - BONGOCAT_LOG_ERROR("Failed to allocate memory for unique path indices"); - return bongocat_error_t::BONGOCAT_ERROR_MEMORY; - } - input._unique_devices = bongocat::move(new_unique_devices); - input._unique_paths_indices = bongocat::move(new_unique_paths_indices); - input._unique_paths_indices_capacity = input._device_paths.count; - } - - size_t broken_symlink = 0; - size_t failed = 0; - size_t ignore = 0; - - // recover real size for syncing - input._unique_devices.count = input._unique_paths_indices_capacity; - input._unique_paths_indices.count = input._unique_paths_indices_capacity; - size_t num_unique_devices = 0; - for (size_t i = 0; i < input._device_paths.count; i++) { - const char* device_path = input._device_paths[i]; - - // Resolve to canonical path - char resolved[PATH_MAX]; - const char* candidate = nullptr; - input_unique_file_type_t new_type = input_unique_file_type_t::File; - - struct stat lst{}; - if (lstat(device_path, &lst) == 0 && S_ISLNK(lst.st_mode)) { - new_type = input_unique_file_type_t::Symlink; - if (realpath(device_path, resolved)) { - candidate = resolved; - } else { - BONGOCAT_LOG_WARNING("Broken symlink: %s", device_path); - broken_symlink++; - continue; - } - } else { - candidate = device_path; - } - - // Skip if non-existent - if (access(candidate, F_OK) != 0) { - failed++; - BONGOCAT_LOG_WARNING("Device missing: %s", candidate); - continue; - } - - // Check if we already added this canonical path - bool duplicate = false; - for (size_t j = 0; j < num_unique_devices; j++) { - const input_unique_file_t& prev = input._unique_devices[j]; - if (prev.canonical_path && strcmp(prev.canonical_path, candidate) == 0) { - duplicate = true; - break; - } - } - if (duplicate) continue; + input_unique_file_t& cur = input._unique_devices[num_unique_devices]; + input._unique_paths_indices[num_unique_devices] = i; - input_unique_file_t& cur = input._unique_devices[num_unique_devices]; - input._unique_paths_indices[num_unique_devices] = i; + // Decide if we need to replace real_device_path + bool need_reopen = false; + bool need_replace = false; + if (cur.canonical_path != BONGOCAT_NULLPTR) { + need_replace = strcmp(cur.canonical_path, candidate) != 0; + } + if (static_cast(cur.canonical_path) != static_cast(candidate)) { + need_reopen = cur.canonical_path == BONGOCAT_NULLPTR && candidate != BONGOCAT_NULLPTR; + need_replace = true; + } + if (!need_reopen && cur.type != new_type) { + need_reopen = true; + } - // Decide if we need to replace real_device_path - bool need_reopen = false; - bool need_replace = false; - if (cur.canonical_path) { - need_replace = strcmp(cur.canonical_path, candidate) != 0; - } - if (static_cast(cur.canonical_path) != static_cast(candidate)) { - need_reopen = !cur.canonical_path && candidate; - need_replace = true; - } - if (!need_reopen && cur.type != new_type) { - need_reopen = true; - } + // Check existing FD + if (!need_reopen && cur.fd._fd >= 0 && is_open_device_valid(cur.fd._fd)) { + valid_devices++; + num_unique_devices++; + continue; + } - // Check existing FD - if (!need_reopen && cur.fd._fd >= 0 && is_open_device_valid(cur.fd._fd)) { - valid_devices++; - num_unique_devices++; - continue; + // Reopen + if (need_reopen || need_replace) { + close_fd(cur.fd); + struct stat st{}; + if (stat(candidate, &st) == 0 && S_ISCHR(st.st_mode)) { + if (need_reopen) { + int fd = open(candidate, O_RDONLY | O_NONBLOCK | O_CLOEXEC); + if (fd >= 0 && is_open_device_valid(fd)) { + cur.fd = FileDescriptor(fd); + BONGOCAT_LOG_INFO("Opened input device: %s (fd=%d)", candidate, cur.fd._fd); + } + } + if (cur.fd._fd >= 0 && is_open_device_valid(cur.fd._fd)) { + // Replace canonical path if changed + if (need_replace) { + if (cur.canonical_path != BONGOCAT_NULLPTR) { + ::free(cur.canonical_path); + cur.canonical_path = BONGOCAT_NULLPTR; } + cur.canonical_path = ::strdup(candidate); + } + cur.type = new_type; + cur._device_path = device_path; + valid_devices++; + } else { + cleanup(cur); + failed++; + BONGOCAT_LOG_WARNING("Failed to open %s", candidate); + } + } else { + cleanup(cur); + ignore++; + BONGOCAT_LOG_WARNING("Ignoring non-char device: %s", candidate); + } + } - // Reopen - if (need_reopen || need_replace) { - close_fd(cur.fd); - struct stat st{}; - if (stat(candidate, &st) == 0 && S_ISCHR(st.st_mode)) { - if (need_reopen) { - int fd = open(candidate, O_RDONLY | O_NONBLOCK | O_CLOEXEC); - if (fd >= 0 && is_open_device_valid(fd)) { - cur.fd = FileDescriptor(fd); - BONGOCAT_LOG_INFO("Opened input device: %s (fd=%d)", candidate, cur.fd._fd); - } - } - if (cur.fd._fd >= 0 && is_open_device_valid(cur.fd._fd)) { - // Replace canonical path if changed - if (need_replace) { - if (cur.canonical_path) ::free(cur.canonical_path); - cur.canonical_path = ::strdup(candidate); - } - cur.type = new_type; - cur._device_path = device_path; - valid_devices++; - } else { - cleanup(cur); - failed++; - BONGOCAT_LOG_WARNING("Failed to open %s", candidate); - } - } else { - cleanup(cur); - ignore++; - BONGOCAT_LOG_WARNING("Ignoring non-char device: %s", candidate); - } - } + num_unique_devices++; + } + + assert(num_unique_devices <= input._device_paths.count); + // shrink size, @NOTE: don't do this with mmap array + input._unique_devices.count = num_unique_devices; + input._unique_paths_indices.count = num_unique_devices; + return sync_devices_options_result_t{ + .valid_devices = valid_devices, + .broken_symlink = broken_symlink, + .failed = failed, + .ignore = ignore, + }; +} - num_unique_devices++; - } +static bongocat_error_t setup_udev_monitor(input_context_t& input) { + if (input._udev_mon != BONGOCAT_NULLPTR) { + udev_monitor_unref(input._udev_mon); + input._udev_mon = BONGOCAT_NULLPTR; + } + if (input._udev != BONGOCAT_NULLPTR) { + udev_unref(input._udev); + input._udev = BONGOCAT_NULLPTR; + } + input._udev_fd = -1; + + input._udev = udev_new(); + if (input._udev == BONGOCAT_NULLPTR) { + BONGOCAT_LOG_ERROR("Failed to init udev\n"); + return bongocat_error_t::BONGOCAT_ERROR_MEMORY; + } + + input._udev_mon = udev_monitor_new_from_netlink(input._udev, "udev"); + if (input._udev_mon == BONGOCAT_NULLPTR) { + BONGOCAT_LOG_ERROR("Failed to create udev monitor\n"); + udev_unref(input._udev); + input._udev = BONGOCAT_NULLPTR; + return bongocat_error_t::BONGOCAT_ERROR_MEMORY; + } + + // only care about input subsystem + udev_monitor_filter_add_match_subsystem_devtype(input._udev_mon, "input", BONGOCAT_NULLPTR); + udev_monitor_enable_receiving(input._udev_mon); + + input._udev_fd = udev_monitor_get_fd(input._udev_mon); + return bongocat_error_t::BONGOCAT_SUCCESS; +} - assert(num_unique_devices <= input._device_paths.count); - // shrink size, @NOTE: don't do this with mmap array - input._unique_devices.count = num_unique_devices; - input._unique_paths_indices.count = num_unique_devices; - return sync_devices_options_result_t { - .valid_devices = valid_devices, - .broken_symlink = broken_symlink, - .failed = failed, - .ignore = ignore, - }; +static void *input_thread(void *arg) { + assert(arg); + animation::animation_context_t& animation_ctx = *static_cast(arg); + + // from thread context + // animation_context_t& anim = trigger_ctx.anim; + // wait for input context (in animation start) + animation_ctx.init_cond.timedwait([&]() { return atomic_load(&animation_ctx.ready); }, + COND_ANIMATION_TRIGGER_INIT_TIMEOUT_MS); + assert(animation_ctx._input != BONGOCAT_NULLPTR); + input_context_t& input = *animation_ctx._input; + + // sanity checks + assert(input._config != BONGOCAT_NULLPTR); + assert(input._configs_reloaded_cond != BONGOCAT_NULLPTR); + assert(!input._capture_input_running); + assert(input.shm); + assert(input._local_copy_config); + assert(input.update_config_efd._fd >= 0); + + // keep local copies of device_paths + { + // read-only config + assert(input._local_copy_config); + const config::config_t& current_config = *input._local_copy_config; + + assert(current_config.num_keyboard_devices >= 0); + const int device_paths_count = current_config.num_keyboard_devices; + const char *const *device_paths = current_config.keyboard_devices; + assert(device_paths_count >= 0); + input._device_paths = make_allocated_array(static_cast(device_paths_count)); + for (size_t i = 0; i < input._device_paths.count; i++) { + input._device_paths[i] = strdup(device_paths[i]); + if (input._device_paths[i] == BONGOCAT_NULLPTR) { + atomic_store(&input._capture_input_running, false); + cleanup_input_devices_paths(input, i); + cleanup_input_thread_context(input); + BONGOCAT_LOG_ERROR("input: Failed to allocate memory for device_paths"); + return BONGOCAT_NULLPTR; + } + } + } + + BONGOCAT_LOG_DEBUG("input: Starting input capture on %d devices", input._device_paths.count); + + // init unique devices + size_t track_valid_devices = 0; + { + [[maybe_unused]] const auto t0 = platform::get_current_time_us(); + auto [sync_devices_result, init_devices_result] = sync_devices(input); + if (init_devices_result != bongocat_error_t::BONGOCAT_SUCCESS) [[unlikely]] { + atomic_store(&input._capture_input_running, false); + cleanup_input_thread_context(input); + BONGOCAT_LOG_ERROR("input: Failed to init devices and file descriptors"); + return BONGOCAT_NULLPTR; + } + [[maybe_unused]] const auto t1 = platform::get_current_time_us(); + + BONGOCAT_LOG_INFO("input: Successfully opened %d/%d input devices; init time %.3fms (%.6fsec)", + sync_devices_result.valid_devices, input._device_paths.count, + static_cast(t1 - t0) / 1000.0, static_cast(t1 - t0) / 1000000.0); + track_valid_devices = sync_devices_result.valid_devices; + } + + // udev monitoring + { + const bongocat_error_t udev_result = setup_udev_monitor(input); + if (udev_result != bongocat_error_t::BONGOCAT_SUCCESS) [[unlikely]] { + BONGOCAT_LOG_WARNING("Can't create udev monitoring"); + } else { + BONGOCAT_LOG_INFO("Start udev monitoring"); + } + } + + // trigger initial render + wayland::request_render(animation_ctx); + + pthread_cleanup_push(cleanup_input_thread, arg); + + // local thread context + int check_counter = 0; // check is done periodically + time_sec_t adaptive_check_interval_sec = START_ADAPTIVE_CHECK_INTERVAL_SEC; + /// event poll + // 0: reload config event + // 1: udev monitor fd + // 2 - n: device events + // last: stdin (optional) + constexpr size_t MAX_PFDS = 2 + MAX_DEVICE_FDS + ((features::Debug) ? 1 : 0); + pollfd pfds[MAX_PFDS]; + input_event ev[INPUT_EVENT_BUF]; + + constexpr bool include_stdin = features::Debug; + FileDescriptor tty_fd; + if constexpr (include_stdin) { + tty_fd = open_tty_nonblocking(); + BONGOCAT_LOG_INFO("input: Open stdin for testing (fd=%d)", tty_fd._fd); + } + + atomic_store(&input._capture_input_running, true); + while (atomic_load(&input._capture_input_running)) { + pthread_testcancel(); // optional, but makes cancellation more responsive + + // read from config + int timeout = INPUT_POOL_TIMEOUT_MS; + bool enable_debug = false; + { + // read-only config + assert(input._local_copy_config); + const config::config_t& current_config = *input._local_copy_config; + + enable_debug = current_config.enable_debug >= 1; + + if (current_config.input_fps > 0) { + timeout = 1000 / current_config.input_fps; + } else if (current_config.fps > 0) { + timeout = 1000 / current_config.fps / 3; + } } - static bongocat_error_t setup_udev_monitor(input_context_t& input) { - if (input._udev_mon) udev_monitor_unref(input._udev_mon); - if (input._udev) udev_unref(input._udev); - input._udev_mon = nullptr; - input._udev = nullptr; - input._udev_fd = -1; - - input._udev = udev_new(); - if (!input._udev) { - BONGOCAT_LOG_ERROR("Failed to init udev\n"); - return bongocat_error_t::BONGOCAT_ERROR_MEMORY; - } - - input._udev_mon = udev_monitor_new_from_netlink(input._udev, "udev"); - if (!input._udev_mon) { - BONGOCAT_LOG_ERROR("Failed to create udev monitor\n"); - udev_unref(input._udev); - input._udev = nullptr; - return bongocat_error_t::BONGOCAT_ERROR_MEMORY; + // only map valid fds into pfds + constexpr size_t fds_update_config_index = 0; + constexpr size_t fds_udev_monitor_index = 1; + constexpr size_t fds_device_potential_start_index = 2; + pfds[fds_update_config_index] = {.fd = input.update_config_efd._fd, .events = POLLIN, .revents = 0}; + pfds[fds_udev_monitor_index] = {.fd = input._udev_fd, .events = POLLIN, .revents = 0}; + + assert(fds_device_potential_start_index < SSIZE_MAX); + const ssize_t fds_device_start_index = + input._unique_devices.count > 0 ? static_cast(fds_device_potential_start_index) : -1; + ssize_t fds_device_end_index = input._unique_devices.count > 0 ? fds_device_start_index : -1; + nfds_t nfds = fds_device_potential_start_index; + nfds_t device_nfds = 0; + for (size_t i = 0; i < input._unique_devices.count && i < MAX_DEVICE_FDS; i++) { + if (input._unique_devices[i].fd._fd >= 0) { + pfds[nfds].fd = input._unique_devices[i].fd._fd; + pfds[nfds].events = POLLIN; + pfds[nfds].revents = 0; + nfds++; + device_nfds++; + } + } + assert(device_nfds <= input._unique_devices.count); + assert(input._unique_devices.count <= SSIZE_MAX); + fds_device_end_index = (fds_device_start_index >= 0 && device_nfds > 0) + ? fds_device_start_index + static_cast(device_nfds) + : fds_device_start_index; + + ssize_t fds_stdin_index = -1; + if constexpr (include_stdin) { + if (fds_device_end_index >= 0) { + fds_stdin_index = fds_device_end_index + 1; + } else { + fds_stdin_index = 1; + } + } + if (device_nfds == 0 && !include_stdin) { + BONGOCAT_LOG_ERROR("input: All input devices became unavailable"); + break; + } else if (device_nfds > MAX_DEVICE_FDS) { + device_nfds = MAX_DEVICE_FDS; + fds_stdin_index = -1; + } + if (nfds > MAX_PFDS) { + nfds = MAX_PFDS - ((include_stdin) ? 2 : 1); + } + if (fds_stdin_index >= 0) { + pfds[fds_stdin_index] = {.fd = tty_fd._fd, .events = POLLIN, .revents = 0}; + nfds++; + } + { + // read-only config + assert(input._local_copy_config); + const config::config_t& current_config = *input._local_copy_config; + if (device_nfds > MAX_DEVICE_FDS) { + if (current_config._strict) { + BONGOCAT_LOG_ERROR("input: Max input devices fds: %d/%d (%d)", device_nfds, MAX_DEVICE_FDS, + input._unique_devices.count); + break; + } else { + BONGOCAT_LOG_WARNING("input: Max input devices fds: %d/%d (%d)", device_nfds, MAX_DEVICE_FDS, + input._unique_devices.count); } + } + } - // only care about input subsystem - udev_monitor_filter_add_match_subsystem_devtype(input._udev_mon, "input", nullptr); - udev_monitor_enable_receiving(input._udev_mon); - - input._udev_fd = udev_monitor_get_fd(input._udev_mon); - return bongocat_error_t::BONGOCAT_SUCCESS; + /// @TODO: move to tests + // check indices + if constexpr (features::Debug) { + // const bool has_update_config = fds_update_config_index >= 0; + // const bool has_udev = fds_udev_monitor_index >= 0; + const bool has_std_in = fds_stdin_index >= 0; + const bool has_devices = input._unique_devices.count > 0; + + // include every fd + if (has_devices && has_std_in) { + assert(fds_update_config_index == 0); + assert(fds_udev_monitor_index == 1); + assert(fds_device_start_index >= 0); + assert(fds_device_end_index >= 0); + assert(fds_stdin_index >= 0); + + assert(fds_update_config_index == 0); + assert(static_cast(fds_device_start_index) > fds_update_config_index); + assert(static_cast(fds_device_end_index) > fds_update_config_index); + assert(fds_device_end_index >= fds_device_start_index); + assert(fds_stdin_index > fds_device_end_index); + + // assert(device_nfds >= 0); + // assert(nfds >= 0); + assert(device_nfds <= SSIZE_MAX); + assert(nfds <= SSIZE_MAX); + assert(static_cast(device_nfds) == fds_device_end_index - fds_device_start_index); + assert(static_cast(nfds) == fds_device_end_index - fds_device_start_index + 3); + } + // only update + devices + if (has_devices && !has_std_in) { + assert(fds_update_config_index == 0); + assert(fds_udev_monitor_index == 1); + assert(fds_device_start_index >= 0); + assert(fds_device_end_index >= 0); + assert(fds_stdin_index == -1); + + assert(fds_update_config_index == 0); + assert(fds_device_end_index >= 0); + assert(static_cast(fds_device_end_index) > fds_update_config_index); + assert(fds_device_end_index >= fds_device_start_index); + + // assert(device_nfds >= 0); + // assert(nfds >= 0); + assert(device_nfds <= SSIZE_MAX); + assert(nfds <= SSIZE_MAX); + assert(static_cast(device_nfds) == fds_device_end_index - fds_device_start_index); + assert(static_cast(nfds) == fds_device_end_index - fds_device_start_index + 3); + } + // only devices + if (has_devices && !has_std_in) { + assert(fds_update_config_index == 0); + assert(fds_udev_monitor_index == 1); + assert(fds_device_start_index >= 0); + assert(fds_device_end_index >= 0); + assert(fds_stdin_index == -1); + + assert(fds_device_end_index >= 0 && static_cast(fds_device_end_index) <= SIZE_MAX); + assert(static_cast(fds_device_end_index) > fds_update_config_index); + assert(fds_device_end_index >= fds_device_start_index); + + // assert(device_nfds >= 0); + // assert(nfds >= 0); + assert(device_nfds <= SSIZE_MAX); + assert(nfds <= SSIZE_MAX); + assert(static_cast(device_nfds) == fds_device_end_index - fds_device_start_index); + assert(static_cast(nfds) == fds_device_end_index - fds_device_start_index); + } + // nothing (empty) + if (!has_devices && !has_std_in) { + assert(fds_update_config_index == 0); + assert(fds_udev_monitor_index == 1); + assert(fds_device_start_index == -1); + assert(fds_device_end_index == -1); + assert(fds_stdin_index == -1); + + assert(fds_device_end_index >= 0 && static_cast(fds_device_end_index) <= SIZE_MAX); + assert(static_cast(fds_device_end_index) > fds_update_config_index); + assert(fds_device_end_index >= fds_device_start_index); + + assert(device_nfds == 0); + assert(nfds == 2); + } + // no devices, only config + if (!has_devices && !has_std_in) { + assert(fds_update_config_index == 0); + assert(fds_udev_monitor_index == 1); + assert(fds_udev_monitor_index == 1); + assert(fds_device_start_index == -1); + assert(fds_device_end_index == -1); + assert(fds_stdin_index == -1); + + assert(device_nfds == 0); + assert(nfds == 2); + } + // no devices, only config + stdin + if (!has_devices && has_std_in) { + assert(fds_update_config_index == 0); + assert(fds_udev_monitor_index == 1); + assert(fds_device_start_index == -1); + assert(fds_device_end_index == -1); + assert(fds_stdin_index == 1); + + assert(device_nfds == 0); + assert(nfds == 3); + } } - static void* input_thread(void* arg) { - assert(arg); - animation::animation_session_t& trigger_ctx = *static_cast(arg); - - // from thread context - //animation_context_t& anim = trigger_ctx.anim; - // wait for input context (in animation start) - trigger_ctx.init_cond.timedwait([&]() { - return atomic_load(&trigger_ctx.ready); - }, COND_ANIMATION_TRIGGER_INIT_TIMEOUT_MS); - assert(trigger_ctx._input != nullptr); - input_context_t& input = *trigger_ctx._input; - - // sanity checks - assert(input._config != nullptr); - assert(input._configs_reloaded_cond != nullptr); - assert(!input._capture_input_running); - assert(input.shm != nullptr); - assert(input._local_copy_config != nullptr); - assert(input.update_config_efd._fd >= 0); - - // keep local copies of device_paths - { - // read-only config - assert(input._local_copy_config != nullptr); - const config::config_t& current_config = *input._local_copy_config; - - assert(current_config.num_keyboard_devices >= 0); - int device_paths_count = current_config.num_keyboard_devices; - const char *const *device_paths = current_config.keyboard_devices; - assert(device_paths_count >= 0); - input._device_paths = make_allocated_array(static_cast(device_paths_count)); - for (size_t i = 0; i < input._device_paths.count; i++) { - input._device_paths[i] = strdup(device_paths[i]); - if (!input._device_paths[i]) { - atomic_store(&input._capture_input_running, false); - cleanup_input_devices_paths(input, i); - cleanup_input_thread_context(input); - BONGOCAT_LOG_ERROR("input: Failed to allocate memory for device_paths"); - return nullptr; + // poll events + const int poll_result = poll(pfds, nfds, timeout); + if (poll_result < 0) { + if (errno == EINTR) { + continue; // Interrupted by signal + } + BONGOCAT_LOG_ERROR("input: Poll error: %s", strerror(errno)); + break; + } + if (poll_result == 0) { + // Timeout — adaptive device checking + check_counter++; + if (check_counter >= (adaptive_check_interval_sec * (1000 / 100))) { + check_counter = 0; + bool found_new_device = false; + for (size_t i = 0; i < input._unique_devices.count; i++) { + const char *device_path = input._unique_devices[i].canonical_path; + bool need_reopen = false; + if (device_path == BONGOCAT_NULLPTR) { + continue; + } + // If an fd is already open, check if it is still valid + if (input._unique_devices[i].fd._fd >= 0) { + if (!is_open_device_valid(input._unique_devices[i].fd._fd)) { + // fd no longer valid + need_reopen = true; + } else { + // check if device node changed + struct stat old_st{}; + if (input._unique_devices[i].fd._fd >= 0 && fstat(input._unique_devices[i].fd._fd, &old_st) == 0) { + struct stat new_st{}; + if (stat(device_path, &new_st) == 0) { + if (old_st.st_rdev != new_st.st_rdev) { + need_reopen = true; + } } + } + } + } else { + // FD never opened + need_reopen = true; + } + + if (need_reopen) { + // Close old FD if still open + if (input._unique_devices[i].fd._fd >= 0) { + close_fd(input._unique_devices[i].fd); } - } - BONGOCAT_LOG_DEBUG("input: Starting input capture on %d devices", input._device_paths.count); - - // init unique devices - size_t track_valid_devices = 0; - { - [[maybe_unused]] const auto t0 = platform::get_current_time_us(); - auto [sync_devices_result, init_devices_result] = sync_devices(input); - if (init_devices_result != bongocat_error_t::BONGOCAT_SUCCESS) [[unlikely]] { - atomic_store(&input._capture_input_running, false); - cleanup_input_thread_context(input); - BONGOCAT_LOG_ERROR("input: Failed to init devices and file descriptors"); - return nullptr; + if (int new_fd = open(device_path, O_RDONLY | O_NONBLOCK | O_CLOEXEC); new_fd >= 0) { + if (is_open_device_valid(new_fd)) { + input._unique_devices[i].fd = FileDescriptor(new_fd); + new_fd = -1; + found_new_device = true; + BONGOCAT_LOG_INFO("input: New input device detected and opened: %s (fd=%d)", device_path, + input._unique_devices[i].fd._fd); + } else { + // Not a valid char device — close immediately + close(new_fd); + BONGOCAT_LOG_VERBOSE("input: vFile opened but not a char device: %s", device_path); + } + } else { + BONGOCAT_LOG_VERBOSE("input: Failed to open input device: %s (%s)", device_path, strerror(errno)); } - [[maybe_unused]] const auto t1 = platform::get_current_time_us(); + } + } - BONGOCAT_LOG_INFO("input: Successfully opened %d/%d input devices; init time %.3fms (%.6fsec)", sync_devices_result.valid_devices, input._device_paths.count, static_cast(t1 - t0) / 1000.0, static_cast(t1 - t0) / 1000000.0); - track_valid_devices = sync_devices_result.valid_devices; + if (!found_new_device && adaptive_check_interval_sec < MAX_ADAPTIVE_CHECK_INTERVAL_SEC) { + adaptive_check_interval_sec = (adaptive_check_interval_sec < MID_ADAPTIVE_CHECK_INTERVAL_SEC) + ? MID_ADAPTIVE_CHECK_INTERVAL_SEC + : MAX_ADAPTIVE_CHECK_INTERVAL_SEC; + BONGOCAT_LOG_DEBUG("input: Increased device check interval to %d seconds", adaptive_check_interval_sec); + } else if (found_new_device && adaptive_check_interval_sec > START_ADAPTIVE_CHECK_INTERVAL_SEC) { + adaptive_check_interval_sec = START_ADAPTIVE_CHECK_INTERVAL_SEC; + BONGOCAT_LOG_DEBUG("input: Reset device check interval to %d seconds", START_ADAPTIVE_CHECK_INTERVAL_SEC); } + } + continue; + } - // udev monitoring - { - const bongocat_error_t udev_result = setup_udev_monitor(input); - if (udev_result != bongocat_error_t::BONGOCAT_SUCCESS) [[unlikely]] { - BONGOCAT_LOG_WARNING("Can't create udev monitoring"); + // cancel pooling (when not running anymore) + if (!atomic_load(&input._capture_input_running)) { + // draining pools + if (pfds[fds_update_config_index].revents & POLLIN) { + drain_event(pfds[fds_update_config_index], MAX_ATTEMPTS); + } + if (pfds[fds_udev_monitor_index].revents & POLLIN) { + drain_event(pfds[fds_udev_monitor_index], MAX_ATTEMPTS); + } + if (fds_device_start_index >= 0) { + assert(fds_device_start_index >= 0); + assert(fds_device_end_index >= 0); + for (nfds_t p = static_cast(fds_device_start_index); p <= static_cast(fds_device_end_index); + p++) { + // Handle ready devices + if (pfds[p].revents & POLLIN) { + // discard evdev input + [[maybe_unused]] auto discard_result = read(pfds[p].fd, ev, sizeof(ev)); + ((void)discard_result); + } + } + } + if (fds_stdin_index >= 0) { + if (pfds[fds_stdin_index].revents & POLLIN) { + char buf[TEST_STDIN_BUF_LEN] = {0}; + // Drain stdin until empty (EAGAIN) + int attempts = 0; + ssize_t rd = 0; + while (attempts < MAX_ATTEMPTS) { + rd = read(pfds[fds_stdin_index].fd, buf, TEST_STDIN_BUF_LEN); + if (rd > 0) { + continue; + } +#if EAGAIN != EWOULDBLOCK + if (rd == -1 && (errno == EAGAIN || errno == EWOULDBLOCK)) { +#else + if (rd == -1 && errno == EAGAIN) { +#endif + break; // drained completely } else { - BONGOCAT_LOG_INFO("Start udev monitoring"); + break; // EOF or error } + } } + } + break; + } - // trigger initial render - wayland::request_render(trigger_ctx); - - pthread_cleanup_push(cleanup_input_thread, arg); - - // local thread context - int check_counter = 0; // check is done periodically - time_sec_t adaptive_check_interval_sec = START_ADAPTIVE_CHECK_INTERVAL_SEC; - /// event poll - // 0: reload config event - // 1: udev monitor fd - // 2 - n: device events - // last: stdin (optional) - constexpr size_t MAX_PFDS = 2 + MAX_DEVICE_FDS + ((features::Debug) ? 1 : 0); - pollfd pfds[MAX_PFDS]; - input_event ev[INPUT_EVENT_BUF]; - - constexpr bool include_stdin = features::Debug; - FileDescriptor tty_fd; - if constexpr (include_stdin) { - tty_fd = open_tty_nonblocking(); - BONGOCAT_LOG_INFO("input: Open stdin for testing (fd=%d)", tty_fd._fd); + // handle hotplug + bool sync_devices_needed = false; + bool reload_devices_needed = false; + if (pfds[fds_udev_monitor_index].revents & POLLIN) { + BONGOCAT_LOG_DEBUG("input: Receive udev event"); + size_t attempts = 0; + udev_device *dev = BONGOCAT_NULLPTR; + while ((dev = udev_monitor_receive_device(input._udev_mon)) != BONGOCAT_NULLPTR && attempts < MAX_ATTEMPTS) { + const char *action = udev_device_get_action(dev); + const char *node = udev_device_get_devnode(dev); + + if (action != BONGOCAT_NULLPTR && node != BONGOCAT_NULLPTR) { + BONGOCAT_LOG_VERBOSE("input: udev %s: %s", action, node); + if (strcmp(action, "add") == 0 || strcmp(action, "remove") == 0) { + sync_devices_needed = true; + reload_devices_needed = true; + } } - atomic_store(&input._capture_input_running, true); - while (atomic_load(&input._capture_input_running)) { - pthread_testcancel(); // optional, but makes cancellation more responsive - - // read from config - int timeout = INPUT_POOL_TIMEOUT_MS; - bool enable_debug = false; - { - // read-only config - assert(input._local_copy_config != nullptr); - const config::config_t& current_config = *input._local_copy_config; - - enable_debug = current_config.enable_debug; + udev_device_unref(dev); + attempts++; + } + drain_event(pfds[fds_udev_monitor_index], MAX_ATTEMPTS, "udev monitor fd"); + } - if (current_config.input_fps > 0) { - timeout = 1000 / current_config.input_fps; - } else if (current_config.fps > 0) { - timeout = 1000 / current_config.fps / 3; - } - } + // Handle config update + assert(input._config_generation != BONGOCAT_NULLPTR); + bool reload_config = false; + uint64_t new_gen{atomic_load(input._config_generation)}; + if (pfds[fds_update_config_index].revents & POLLIN) { + BONGOCAT_LOG_DEBUG("input: Receive update config event"); + drain_event(pfds[fds_update_config_index], MAX_ATTEMPTS, "update config eventfd"); + reload_config = new_gen > 0; + sync_devices_needed |= reload_config; + } - // only map valid fds into pfds - constexpr size_t fds_update_config_index = 0; - constexpr size_t fds_udev_monitor_index = 1; - constexpr size_t fds_device_potential_start_index = 2; - pfds[fds_update_config_index] = { .fd = input.update_config_efd._fd, .events = POLLIN, .revents = 0 }; - pfds[fds_udev_monitor_index] = { .fd = input._udev_fd, .events = POLLIN, .revents = 0 }; - - assert(fds_device_potential_start_index < SSIZE_MAX); - ssize_t fds_device_start_index = input._unique_devices.count > 0 ? static_cast(fds_device_potential_start_index) : -1; - ssize_t fds_device_end_index = input._unique_devices.count > 0 ? fds_device_start_index : -1; - nfds_t nfds = fds_device_potential_start_index; - nfds_t device_nfds = 0; - for (size_t i = 0; i < input._unique_devices.count && i < MAX_DEVICE_FDS; i++) { - if (input._unique_devices[i].fd._fd >= 0) { - pfds[nfds].fd = input._unique_devices[i].fd._fd; - pfds[nfds].events = POLLIN; - pfds[nfds].revents = 0; - nfds++; - device_nfds++; - } - } - assert(device_nfds <= input._unique_devices.count); - assert(input._unique_devices.count <= SSIZE_MAX); - fds_device_end_index = (fds_device_start_index >= 0 && device_nfds > 0)? fds_device_start_index + static_cast(device_nfds) : fds_device_start_index; - - ssize_t fds_stdin_index = -1; - if constexpr (include_stdin) { - if (fds_device_end_index >= 0) { - fds_stdin_index = fds_device_end_index+1; - } else { - fds_stdin_index = 1; + // Handle device events + { + platform::LockGuard guard(input.input_lock); + assert(input.shm); + // auto& input_shm = *input.shm; + + if (fds_device_start_index >= 0) { + assert(fds_device_start_index >= 0); + assert(fds_device_end_index >= 0); + for (nfds_t p = static_cast(fds_device_start_index); p <= static_cast(fds_device_end_index); + p++) { + // Handle ready devices + if (pfds[p].revents & POLLIN) { + // handle evdev input + const ssize_t rd = read(pfds[p].fd, ev, sizeof(ev)); + if (rd < 0) { + if (errno == ENODEV) { + sync_devices_needed = true; + } + if (errno == EAGAIN) { + continue; + } + BONGOCAT_LOG_WARNING("input: Read error on fd=%d: %s", pfds[p].fd, strerror(errno)); + close(pfds[p].fd); + // pfds[p].fd is only a reference, reset also the owner (unique_fd) + for (size_t i = 0; i < input._unique_devices.count; i++) { + if (input._unique_devices[i].fd._fd == pfds[p].fd) { + input._unique_devices[i].fd._fd = -1; + pfds[i].fd = -1; + break; } + } + pfds[p].fd = -1; + sync_devices_needed = true; + continue; } - if (device_nfds == 0 && !include_stdin) { - BONGOCAT_LOG_ERROR("input: All input devices became unavailable"); - break; - } else if (device_nfds > MAX_DEVICE_FDS) { - device_nfds = MAX_DEVICE_FDS; - fds_stdin_index = -1; - } - if (nfds > MAX_PFDS) { - nfds = MAX_PFDS - ((include_stdin) ? 2 : 1); - } - if (fds_stdin_index >= 0) { - pfds[fds_stdin_index] = { .fd = tty_fd._fd, .events = POLLIN, .revents = 0 }; - nfds++; - } - { - // read-only config - assert(input._local_copy_config != nullptr); - const config::config_t& current_config = *input._local_copy_config; - if (device_nfds > MAX_DEVICE_FDS) { - if (current_config._strict) { - BONGOCAT_LOG_ERROR("input: Max input devices fds: %d/%d (%d)", device_nfds, MAX_DEVICE_FDS, input._unique_devices.count); - break; - } else { - BONGOCAT_LOG_WARNING("input: Max input devices fds: %d/%d (%d)", device_nfds, MAX_DEVICE_FDS, input._unique_devices.count); - } + assert(rd >= 0); + if (rd == 0 || static_cast(rd) % sizeof(input_event) != 0) { + BONGOCAT_LOG_WARNING("input: EOF or partial read on fd=%d", pfds[p].fd); + close(pfds[p].fd); + // pfds[p].fd is only a reference, reset also the owner (unique_fd) + for (size_t i = 0; i < input._unique_devices.count; i++) { + if (input._unique_devices[i].fd._fd == pfds[p].fd) { + input._unique_devices[i].fd._fd = -1; + pfds[i].fd = -1; + break; } + } + pfds[p].fd = -1; + sync_devices_needed = true; + continue; } - /// @TODO: move to tests - // check indices - if constexpr (features::Debug) { - //const bool has_update_config = fds_update_config_index >= 0; - //const bool has_udev = fds_udev_monitor_index >= 0; - const bool has_std_in = fds_stdin_index >= 0; - const bool has_devices = input._unique_devices.count > 0; - - // include every fd - if (has_devices && has_std_in) { - assert(fds_update_config_index == 0); - assert(fds_udev_monitor_index == 1); - assert(fds_device_start_index >= 0); - assert(fds_device_end_index >= 0); - assert(fds_stdin_index >= 0); - - assert(fds_update_config_index == 0); - assert(static_cast(fds_device_start_index) > fds_update_config_index); - assert(static_cast(fds_device_end_index) > fds_update_config_index); - assert(fds_device_end_index >= fds_device_start_index); - assert(fds_stdin_index > fds_device_end_index); - - //assert(device_nfds >= 0); - //assert(nfds >= 0); - assert(device_nfds <= SSIZE_MAX); - assert(nfds <= SSIZE_MAX); - assert(static_cast(device_nfds) == fds_device_end_index-fds_device_start_index); - assert(static_cast(nfds) == fds_device_end_index-fds_device_start_index + 3); - } - // only update + devices - if (has_devices && !has_std_in) { - assert(fds_update_config_index == 0); - assert(fds_udev_monitor_index == 1); - assert(fds_device_start_index >= 0); - assert(fds_device_end_index >= 0); - assert(fds_stdin_index == -1); - - assert(fds_update_config_index == 0); - assert(fds_device_end_index >= 0); - assert(static_cast(fds_device_end_index) > fds_update_config_index); - assert(fds_device_end_index >= fds_device_start_index); - - //assert(device_nfds >= 0); - //assert(nfds >= 0); - assert(device_nfds <= SSIZE_MAX); - assert(nfds <= SSIZE_MAX); - assert(static_cast(device_nfds) == fds_device_end_index-fds_device_start_index); - assert(static_cast(nfds) == fds_device_end_index-fds_device_start_index + 3); - } - // only devices - if (has_devices && !has_std_in) { - assert(fds_update_config_index == 0); - assert(fds_udev_monitor_index == 1); - assert(fds_device_start_index >= 0); - assert(fds_device_end_index >= 0); - assert(fds_stdin_index == -1); - - assert(fds_device_end_index >= 0 && static_cast(fds_device_end_index) <= SIZE_MAX); - assert(static_cast(fds_device_end_index) > fds_update_config_index); - assert(fds_device_end_index >= fds_device_start_index); - - //assert(device_nfds >= 0); - //assert(nfds >= 0); - assert(device_nfds <= SSIZE_MAX); - assert(nfds <= SSIZE_MAX); - assert(static_cast(device_nfds) == fds_device_end_index-fds_device_start_index); - assert(static_cast(nfds) == fds_device_end_index-fds_device_start_index); - } - // nothing (empty) - if (!has_devices && !has_std_in) { - assert(fds_update_config_index == 0); - assert(fds_udev_monitor_index == 1); - assert(fds_device_start_index == -1); - assert(fds_device_end_index == -1); - assert(fds_stdin_index == -1); - - assert(fds_device_end_index >= 0 && static_cast(fds_device_end_index) <= SIZE_MAX); - assert(static_cast(fds_device_end_index) > fds_update_config_index); - assert(fds_device_end_index >= fds_device_start_index); - - assert(device_nfds == 0); - assert(nfds == 2); - } - // no devices, only config - if (!has_devices && !has_std_in) { - assert(fds_update_config_index == 0); - assert(fds_udev_monitor_index == 1); - assert(fds_udev_monitor_index == 1); - assert(fds_device_start_index == -1); - assert(fds_device_end_index == -1); - assert(fds_stdin_index == -1); - - assert(device_nfds == 0); - assert(nfds == 2); - } - // no devices, only config + stdin - if (!has_devices && has_std_in) { - assert(fds_update_config_index == 0); - assert(fds_udev_monitor_index == 1); - assert(fds_device_start_index == -1); - assert(fds_device_end_index == -1); - assert(fds_stdin_index == 1); - - assert(device_nfds == 0); - assert(nfds == 3); + bool key_pressed = false; + // keep captured keycode short-lived + int captured_keycode = 0; + assert(rd >= 0); + assert(sizeof(input_event) > 0); + const auto num_events = static_cast(static_cast(rd) / sizeof(input_event)); + for (ssize_t j = 0; j < num_events; j++) { + if (ev[j].type == EV_KEY && ev[j].value == 1) { + key_pressed = true; + captured_keycode = ev[j].code; // Store for hand mapping + if (enable_debug) { + BONGOCAT_LOG_VERBOSE("input: Key event: fd=%d, code=%d, time=%lld.%06lld", pfds[p].fd, ev[j].code, + ev[j].time.tv_sec, ev[j].time.tv_usec); + } else { + // break loop early, when no debug (no print needed for every key press) + break; } + } } - // poll events - const int poll_result = poll(pfds, nfds, timeout); - if (poll_result < 0) { - if (errno == EINTR) continue; // Interrupted by signal - BONGOCAT_LOG_ERROR("input: Poll error: %s", strerror(errno)); - break; - } - if (poll_result == 0) { - // Timeout — adaptive device checking - check_counter++; - if (check_counter >= (adaptive_check_interval_sec * (1000 / 100))) { - check_counter = 0; - bool found_new_device = false; - for (size_t i = 0; i < input._unique_devices.count; i++) { - const char* device_path = input._unique_devices[i].canonical_path; - bool need_reopen = false; - if (device_path == nullptr) continue; - // If an fd is already open, check if it is still valid - if (input._unique_devices[i].fd._fd >= 0) { - if (!is_open_device_valid(input._unique_devices[i].fd._fd)) { - // fd no longer valid - need_reopen = true; - } else { - // check if device node changed - struct stat old_st{}; - if (input._unique_devices[i].fd._fd >= 0 && - fstat(input._unique_devices[i].fd._fd, &old_st) == 0) { - struct stat new_st{}; - if (stat(device_path, &new_st) == 0) { - if (old_st.st_rdev != new_st.st_rdev) { - need_reopen = true; - } - } - } - } - } else { - // FD never opened - need_reopen = true; - } - - if (need_reopen) { - // Close old FD if still open - if (input._unique_devices[i].fd._fd >= 0) { - close_fd(input._unique_devices[i].fd); - } - - if (int new_fd = open(device_path, O_RDONLY | O_NONBLOCK | O_CLOEXEC); new_fd >= 0) { - if (is_open_device_valid(new_fd)) { - input._unique_devices[i].fd = FileDescriptor(new_fd); - new_fd = -1; - found_new_device = true; - BONGOCAT_LOG_INFO("input: New input device detected and opened: %s (fd=%d)", device_path, input._unique_devices[i].fd._fd); - } else { - // Not a valid char device — close immediately - close(new_fd); - BONGOCAT_LOG_VERBOSE("input: vFile opened but not a char device: %s", device_path); - } - } else { - BONGOCAT_LOG_VERBOSE("input: Failed to open input device: %s (%s)", device_path, strerror(errno)); - } - } - } - - if (!found_new_device && adaptive_check_interval_sec < MAX_ADAPTIVE_CHECK_INTERVAL_SEC) { - adaptive_check_interval_sec = - (adaptive_check_interval_sec < MID_ADAPTIVE_CHECK_INTERVAL_SEC) - ? MID_ADAPTIVE_CHECK_INTERVAL_SEC - : MAX_ADAPTIVE_CHECK_INTERVAL_SEC; - BONGOCAT_LOG_DEBUG("input: Increased device check interval to %d seconds", adaptive_check_interval_sec); - } else if (found_new_device && adaptive_check_interval_sec > START_ADAPTIVE_CHECK_INTERVAL_SEC) { - adaptive_check_interval_sec = START_ADAPTIVE_CHECK_INTERVAL_SEC; - BONGOCAT_LOG_DEBUG("input: Reset device check interval to %d seconds", START_ADAPTIVE_CHECK_INTERVAL_SEC); - } - } - continue; + const timestamp_ms_t now = get_current_time_ms(); + if (key_pressed) { + trigger_key_press(animation_ctx, captured_keycode); + } else { + input.shm->any_key_pressed = 0; + input.shm->hand_mapping = input_hand_mapping_t::None; + if (input.shm->kpm > 0 && now - input._latest_kpm_update_ms >= RESET_KPM_TIMEOUT_MS) { + input.shm->kpm = 0; + atomic_store(&input._input_kpm_counter, 0); + input._latest_kpm_update_ms = now; + } } - - // cancel pooling (when not running anymore) - if (!atomic_load(&input._capture_input_running)) { - // draining pools - if (pfds[fds_update_config_index].revents & POLLIN) { - drain_event(pfds[fds_update_config_index], MAX_ATTEMPTS); - } - if (pfds[fds_udev_monitor_index].revents & POLLIN) { - drain_event(pfds[fds_udev_monitor_index], MAX_ATTEMPTS); - } - if (fds_device_start_index >= 0) { - assert(fds_device_start_index >= 0); - assert(fds_device_end_index >= 0); - for (nfds_t p = static_cast(fds_device_start_index); p <= static_cast(fds_device_end_index); p++) { - // Handle ready devices - if (pfds[p].revents & POLLIN) { - // discard evdev input - [[maybe_unused]] auto discard_result = read(pfds[p].fd, ev, sizeof(ev)); - ((void)discard_result); - } - } - } - if (fds_stdin_index >= 0) { - if (pfds[fds_stdin_index].revents & POLLIN) { - char buf[TEST_STDIN_BUF_LEN] = {0}; - // Drain stdin until empty (EAGAIN) - int attempts = 0; - ssize_t rd = 0; - while (attempts < MAX_ATTEMPTS) { - rd = read(pfds[fds_stdin_index].fd, buf, TEST_STDIN_BUF_LEN); - if (rd > 0) { - continue; - } -#if EAGAIN != EWOULDBLOCK - if (rd == -1 && (errno == EAGAIN || errno == EWOULDBLOCK)) { -#else - if (rd == -1 && errno == EAGAIN) { -#endif - break; // drained completely - } else { - break; // EOF or error - } - } - } - } + } + + if (pfds[p].revents & (POLLERR | POLLHUP | POLLNVAL)) { + close(pfds[p].fd); + // pfds[p].fd is only a reference, reset also the owner (unique_fd) + for (size_t i = 0; i < input._unique_devices.count; i++) { + if (input._unique_devices[i].fd._fd == pfds[p].fd) { + input._unique_devices[i].fd._fd = -1; + pfds[i].fd = -1; break; + } } - - // handle hotplug - bool sync_devices_needed = false; - bool reload_devices_needed = false; - if (pfds[fds_udev_monitor_index].revents & POLLIN) { - BONGOCAT_LOG_DEBUG("input: Receive udev event"); - size_t attempts = 0; - udev_device *dev = nullptr; - while ((dev = udev_monitor_receive_device(input._udev_mon)) != nullptr && attempts < MAX_ATTEMPTS) { - const char* action = udev_device_get_action(dev); - const char* node = udev_device_get_devnode(dev); - - if (action && node) { - BONGOCAT_LOG_VERBOSE("input: udev %s: %s", action, node); - if (strcmp(action, "add") == 0 || strcmp(action, "remove") == 0) { - sync_devices_needed = true; - reload_devices_needed = true; - } - } - - udev_device_unref(dev); - attempts++; - } - drain_event(pfds[fds_udev_monitor_index], MAX_ATTEMPTS, "udev monitor fd"); - } - - // Handle config update - assert(input._config_generation != nullptr); - bool reload_config = false; - uint64_t new_gen{atomic_load(input._config_generation)}; - if (pfds[fds_update_config_index].revents & POLLIN) { - BONGOCAT_LOG_DEBUG("input: Receive update config event"); - drain_event(pfds[fds_update_config_index], MAX_ATTEMPTS, "update config eventfd"); - reload_config = new_gen > 0; - sync_devices_needed |= reload_config; + pfds[p].fd = -1; + sync_devices_needed = true; + } + } + } + + // simulate "any key pressed" + if (fds_stdin_index >= 0) { + if (pfds[fds_stdin_index].revents & POLLIN) { + char buf[TEST_STDIN_BUF_LEN] = {0}; + bool got_key = false; + + // Drain stdin until empty (EAGAIN) + int attempts = 0; + ssize_t rd = 0; + while (attempts < MAX_ATTEMPTS) { + rd = read(pfds[fds_stdin_index].fd, buf, TEST_STDIN_BUF_LEN); + if (rd > 0) { + got_key = true; } - - // Handle device events - { - platform::LockGuard guard (input.input_lock); - assert(input.shm != nullptr); - //auto& input_shm = *input.shm; - - if (fds_device_start_index >= 0) { - assert(fds_device_start_index >= 0); - assert(fds_device_end_index >= 0); - for (nfds_t p = static_cast(fds_device_start_index); p <= static_cast(fds_device_end_index); p++) { - // Handle ready devices - if (pfds[p].revents & POLLIN) { - // handle evdev input - const ssize_t rd = read(pfds[p].fd, ev, sizeof(ev)); - if (rd < 0) { - if (errno == ENODEV) sync_devices_needed = true; - if (errno == EAGAIN) continue; - BONGOCAT_LOG_WARNING("input: Read error on fd=%d: %s", pfds[p].fd, strerror(errno)); - close(pfds[p].fd); - // pfds[p].fd is only a reference, reset also the owner (unique_fd) - for (size_t i = 0; i < input._unique_devices.count; i++) { - if (input._unique_devices[i].fd._fd == pfds[p].fd) { - input._unique_devices[i].fd._fd = -1; - pfds[i].fd = -1; - break; - } - } - pfds[p].fd = -1; - sync_devices_needed = true; - continue; - } - assert(rd >= 0); - if (rd == 0 || static_cast(rd) % sizeof(input_event) != 0) { - BONGOCAT_LOG_WARNING("input: EOF or partial read on fd=%d", pfds[p].fd); - close(pfds[p].fd); - // pfds[p].fd is only a reference, reset also the owner (unique_fd) - for (size_t i = 0; i < input._unique_devices.count; i++) { - if (input._unique_devices[i].fd._fd == pfds[p].fd) { - input._unique_devices[i].fd._fd = -1; - pfds[i].fd = -1; - break; - } - } - pfds[p].fd = -1; - sync_devices_needed = true; - continue; - } - - bool key_pressed = false; - assert(rd >= 0); - assert(sizeof(input_event) > 0); - const auto num_events = static_cast(static_cast(rd) / sizeof(input_event)); - for (ssize_t j = 0; j < num_events; j++) { - if (ev[j].type == EV_KEY && ev[j].value == 1) { - key_pressed = true; - if (enable_debug) { - BONGOCAT_LOG_VERBOSE("input: Key event: fd=%d, code=%d, time=%lld.%06lld", - pfds[p].fd, ev[j].code, - ev[j].time.tv_sec, ev[j].time.tv_usec); - } else { - // break loop early, when no debug (no print needed for every key press) - break; - } - } - } - - - const timestamp_ms_t now = get_current_time_ms(); - if (key_pressed) { - trigger_key_press(trigger_ctx); - } else { - if (input.shm->kpm > 0 && now - input._latest_kpm_update_ms >= RESET_KPM_TIMEOUT_MS) { - input.shm->kpm = 0; - atomic_store(&input._input_kpm_counter, 0); - input._latest_kpm_update_ms = now; - } - } - } - - if (pfds[p].revents & (POLLERR | POLLHUP | POLLNVAL)) { - close(pfds[p].fd); - // pfds[p].fd is only a reference, reset also the owner (unique_fd) - for (size_t i = 0; i < input._unique_devices.count; i++) { - if (input._unique_devices[i].fd._fd == pfds[p].fd) { - input._unique_devices[i].fd._fd = -1; - pfds[i].fd = -1; - break; - } - } - pfds[p].fd = -1; - sync_devices_needed = true; - } - } - } - - // simulate "any key pressed" - if (fds_stdin_index >= 0) { - if (pfds[fds_stdin_index].revents & POLLIN) { - char buf[TEST_STDIN_BUF_LEN] = {0}; - bool got_key = false; - - // Drain stdin until empty (EAGAIN) - int attempts = 0; - ssize_t rd = 0; - while (attempts < MAX_ATTEMPTS) { - rd = read(pfds[fds_stdin_index].fd, buf, TEST_STDIN_BUF_LEN); - if (rd > 0) { - got_key = true; - } #if EAGAIN != EWOULDBLOCK - if (rd == -1 && (errno == EAGAIN || errno == EWOULDBLOCK)) { + if (rd == -1 && (errno == EAGAIN || errno == EWOULDBLOCK)) { #else - if (rd == -1 && errno == EAGAIN) { + if (rd == -1 && errno == EAGAIN) { #endif - break; // drained completely - } else { - break; // EOF or error - } - } - - if (got_key) { - trigger_key_press(trigger_ctx); - if (enable_debug) { - size_t len = (rd > 0) ? (static_cast(rd) < TEST_STDIN_BUF_LEN ? static_cast(rd) : TEST_STDIN_BUF_LEN-1) : 0; - buf[len] = '\0'; - BONGOCAT_LOG_VERBOSE("input: stdin input: %s", buf); - } - } - } - } + break; // drained completely + } else { + break; // EOF or error } - - // Revalidate valid devices - if (sync_devices_needed) { - platform::LockGuard guard (input.input_lock); - - auto [sync_devices_result, revalid_devices_result] = sync_devices(input, { .reload_devices_needed = reload_devices_needed }); - if (revalid_devices_result != bongocat_error_t::BONGOCAT_SUCCESS) { - BONGOCAT_LOG_ERROR("input: Failed to revalidate devices and file descriptors"); - } else { - if (sync_devices_result.valid_devices == 0) { - BONGOCAT_LOG_VERBOSE("input: All input devices became unavailable"); - } - } - track_valid_devices = sync_devices_result.valid_devices; - } - - // handle update config - if (reload_config) { - assert(input._config_generation != nullptr); - assert(input._configs_reloaded_cond != nullptr); - assert(input._config != nullptr); - - update_config(input, *input._config, new_gen); - - // wait for reload config to be done (all configs) - const int rc = input._configs_reloaded_cond->timedwait([&] { - return atomic_load(input._config_generation) >= new_gen; - }, COND_RELOAD_CONFIGS_TIMEOUT_MS); - if (rc == ETIMEDOUT) { - BONGOCAT_LOG_WARNING("input: Timed out waiting for reload eventfd: %s", strerror(errno)); - } - if constexpr (features::Debug) { - if (atomic_load(&input.config_seen_generation) < atomic_load(input._config_generation)) { - BONGOCAT_LOG_VERBOSE("input: input.config_seen_generation < input._config_generation; %d < %d", atomic_load(&input.config_seen_generation), atomic_load(input._config_generation)); - } - } - //assert(atomic_load(&input.config_seen_generation) >= atomic_load(input._config_generation)); - atomic_store(&input.config_seen_generation, atomic_load(input._config_generation)); - BONGOCAT_LOG_INFO("input: Input config reloaded (gen=%u)", new_gen); + } + + if (got_key) { + trigger_key_press(animation_ctx); + if (enable_debug) { + const size_t len = (rd > 0) ? (static_cast(rd) < TEST_STDIN_BUF_LEN ? static_cast(rd) + : TEST_STDIN_BUF_LEN - 1) + : 0; + buf[len] = '\0'; + BONGOCAT_LOG_VERBOSE("input: stdin input: %s", buf); } + } } - - close_fd(tty_fd); - - atomic_store(&input._capture_input_running, false); - if (track_valid_devices == 0) { - BONGOCAT_LOG_ERROR("input: All input devices are unavailable"); - } - - // Will run only on normal return - pthread_cleanup_pop(1); // 1 = call cleanup even if not canceled - - // done when callback cleanup_input_thread - //cleanup_input_thread_context(arg); - // sanity check for clean up - assert(input._device_paths == nullptr); - assert(input._unique_devices == nullptr); - assert(input._unique_paths_indices == nullptr); - assert(input._unique_paths_indices_capacity == 0); - - BONGOCAT_LOG_INFO("Input monitoring stopped"); - - return nullptr; + } } - created_result_t> create(const config::config_t& config) { - AllocatedMemory ret = make_allocated_memory(); - assert(ret != nullptr); - if (ret == nullptr) { - return bongocat_error_t::BONGOCAT_ERROR_MEMORY; - } - if (config.num_keyboard_devices <= 0) { - BONGOCAT_LOG_WARNING("No input devices specified"); - } - - const timestamp_ms_t now = get_current_time_ms(); - ret->_capture_input_running = false; - ret->_input_kpm_counter = 0; - ret->_latest_kpm_update_ms = now; - - BONGOCAT_LOG_INFO("Initializing input monitoring system for %d devices", config.num_keyboard_devices); - - // Initialize shared memory for key press flag - ret->shm = make_allocated_mmap(); - if (!ret->shm.ptr) { - BONGOCAT_LOG_ERROR("Failed to create shared memory for input monitoring: %s", strerror(errno)); - return bongocat_error_t::BONGOCAT_ERROR_MEMORY; + // Revalidate valid devices + if (sync_devices_needed) { + platform::LockGuard guard(input.input_lock); + + auto [sync_devices_result, revalid_devices_result] = + sync_devices(input, {.reload_devices_needed = reload_devices_needed}); + if (revalid_devices_result != bongocat_error_t::BONGOCAT_SUCCESS) { + BONGOCAT_LOG_ERROR("input: Failed to revalidate devices and file descriptors"); + } else { + if (sync_devices_result.valid_devices == 0) { + BONGOCAT_LOG_VERBOSE("input: All input devices became unavailable"); } - ret->shm->any_key_pressed = 0; - ret->shm->kpm = 0; - ret->shm->input_counter = 0; - ret->shm->last_key_pressed_timestamp = 0; - - // Initialize shared memory for local config - ret->_local_copy_config = make_allocated_mmap(); - if (!ret->_local_copy_config) { - BONGOCAT_LOG_ERROR("Failed to create shared memory for input monitoring: %s", strerror(errno)); - return bongocat_error_t::BONGOCAT_ERROR_MEMORY; - } - assert(ret->_local_copy_config != nullptr); - *ret->_local_copy_config = config; + } + track_valid_devices = sync_devices_result.valid_devices; + } - ret->update_config_efd = platform::FileDescriptor(eventfd(0, EFD_NONBLOCK | EFD_CLOEXEC)); - if (ret->update_config_efd._fd < 0) { - BONGOCAT_LOG_ERROR("Failed to create notify pipe for input update config: %s", strerror(errno)); - return bongocat_error_t::BONGOCAT_ERROR_FILE_IO; + // handle update config + if (reload_config) { + assert(input._config_generation != BONGOCAT_NULLPTR); + assert(input._configs_reloaded_cond != BONGOCAT_NULLPTR); + assert(input._config != BONGOCAT_NULLPTR); + + update_config(input, *input._config, new_gen); + + // wait for reload config to be done (all configs) + const int rc = input._configs_reloaded_cond->timedwait( + [&] { return atomic_load(input._config_generation) >= new_gen; }, COND_RELOAD_CONFIGS_TIMEOUT_MS); + if (rc == ETIMEDOUT) { + BONGOCAT_LOG_WARNING("input: Timed out waiting for reload eventfd: %s", strerror(errno)); + } + if constexpr (features::Debug) { + if (atomic_load(&input.config_seen_generation) < atomic_load(input._config_generation)) { + BONGOCAT_LOG_VERBOSE("input: input.config_seen_generation < input._config_generation; %d < %d", + atomic_load(&input.config_seen_generation), atomic_load(input._config_generation)); } - - BONGOCAT_LOG_INFO("Input monitoring started"); - return ret; + } + // assert(atomic_load(&input.config_seen_generation) >= atomic_load(input._config_generation)); + atomic_store(&input.config_seen_generation, atomic_load(input._config_generation)); + BONGOCAT_LOG_INFO("input: Input config reloaded (gen=%u)", new_gen); } + } - bongocat_error_t start(input_context_t& input, animation::animation_session_t& trigger_ctx, const config::config_t& config, CondVariable& configs_reloaded_cond, atomic_uint64_t& config_generation) { - if (config.num_keyboard_devices < 0) { - BONGOCAT_LOG_ERROR("No input devices specified"); - return bongocat_error_t::BONGOCAT_ERROR_INVALID_PARAM; - } else if (config.num_keyboard_devices == 0) { - BONGOCAT_LOG_WARNING("No input devices specified"); - } + close_fd(tty_fd); - const timestamp_ms_t now = get_current_time_ms(); - input._latest_kpm_update_ms = now; + atomic_store(&input._capture_input_running, false); + if (track_valid_devices == 0) { + BONGOCAT_LOG_ERROR("input: All input devices are unavailable"); + } - BONGOCAT_LOG_INFO("Initializing input monitoring system for %d devices", config.num_keyboard_devices); + // Will run only on normal return + pthread_cleanup_pop(1); // 1 = call cleanup even if not canceled - // Initialize shared memory for key press flag - input.shm = make_allocated_mmap(); - if (!input.shm.ptr) { - BONGOCAT_LOG_ERROR("Failed to create shared memory for input monitoring: %s", strerror(errno)); - return bongocat_error_t::BONGOCAT_ERROR_MEMORY; - } - input.shm->any_key_pressed = 0; - input.shm->kpm = 0; - input.shm->input_counter = 0; - input.shm->last_key_pressed_timestamp = now; // for idle check timestamp should be zero - - // Initialize shared memory for local config - input._local_copy_config = make_allocated_mmap(); - if (!input._local_copy_config) { - BONGOCAT_LOG_ERROR("Failed to create shared memory for input monitoring: %s", strerror(errno)); - return bongocat_error_t::BONGOCAT_ERROR_MEMORY; - } - assert(input._local_copy_config != nullptr); - update_config(input, config, atomic_load(&config_generation)); - - // wait for animation trigger to be ready (input should be the same) - int cond_ret = trigger_ctx.init_cond.timedwait([&]() { - return atomic_load(&trigger_ctx.ready); - }, COND_ANIMATION_TRIGGER_INIT_TIMEOUT_MS); - if (cond_ret == ETIMEDOUT) { - BONGOCAT_LOG_ERROR("Failed to initialize input monitoring: waiting for animation thread to start in time"); - } else { - assert(trigger_ctx._input == &input); - } - // set extern/global references - { - // guard for anim_update_state - LockGuard guard (trigger_ctx.anim.anim_lock); - trigger_ctx._input = &input; - } - trigger_ctx.init_cond.notify_all(); - input._config = &config; - input._configs_reloaded_cond = &configs_reloaded_cond; - input._config_generation = &config_generation; - atomic_store(&input.ready, true); - input.init_cond.notify_all(); - - input._configs_reloaded_cond->notify_all(); - - // start input monitoring thread - const int result = pthread_create(&input._input_thread, nullptr, input_thread, &trigger_ctx); - if (result != 0) { - BONGOCAT_LOG_ERROR("Failed to start input monitoring thread: %s", strerror(errno)); - return bongocat_error_t::BONGOCAT_ERROR_THREAD; - } + // done when callback cleanup_input_thread + // cleanup_input_thread_context(arg); + // sanity check for clean up + assert(input._device_paths == BONGOCAT_NULLPTR); + assert(input._unique_devices == BONGOCAT_NULLPTR); + assert(input._unique_paths_indices == BONGOCAT_NULLPTR); + assert(input._unique_paths_indices_capacity == 0); - BONGOCAT_LOG_INFO("Input monitoring started"); - return bongocat_error_t::BONGOCAT_SUCCESS; - } + BONGOCAT_LOG_INFO("Input monitoring stopped"); - bongocat_error_t restart(input_context_t& input, animation::animation_session_t& trigger_ctx, const config::config_t& config, CondVariable& configs_reloaded_cond, atomic_uint64_t& config_generation) { - BONGOCAT_LOG_INFO("Restarting input monitoring system"); - // Stop current monitoring - if (input._input_thread) { - BONGOCAT_LOG_DEBUG("Input monitoring thread"); - atomic_store(&input._capture_input_running, false); - //pthread_cancel(ctx->_input_thread); - if (stop_thread_graceful_or_cancel(input._input_thread, input._capture_input_running) != 0) { - BONGOCAT_LOG_ERROR("Failed to join input thread: %s", strerror(errno)); - } - BONGOCAT_LOG_DEBUG("Input monitoring thread terminated"); - } - - // already done when stop current input thread - //cleanup_input_thread_context(ctx); - assert(!input._device_paths); - assert(!input._unique_paths_indices); - assert(!input._unique_devices); - assert(input._unique_paths_indices_capacity == 0); - - // reset stats - //ret._latest_kpm_update_ms = get_current_time_ms(); - - /// @TODO: re-create context with create(), avoid duplicate code - - // Start new monitoring (reuse shared memory if it exists) - { - LockGuard guard (input.input_lock); - if (input.shm == nullptr) { - input.shm = make_allocated_mmap(); - if (input.shm.ptr == MAP_FAILED) { - BONGOCAT_LOG_ERROR("Failed to create shared memory for input monitoring: %s", strerror(errno)); - return bongocat_error_t::BONGOCAT_ERROR_MEMORY; - } - } - } + return BONGOCAT_NULLPTR; +} - if (!input._local_copy_config) { - input._local_copy_config = make_unallocated_mmap_value(config); - if (!input._local_copy_config) { - BONGOCAT_LOG_ERROR("Failed to create shared memory for input monitoring: %s", strerror(errno)); - return bongocat_error_t::BONGOCAT_ERROR_MEMORY; - } - } - assert(input._local_copy_config != nullptr); - update_config(input, config, atomic_load(&config_generation)); - - if (input.update_config_efd._fd < 0) { - input.update_config_efd = platform::FileDescriptor(eventfd(0, EFD_NONBLOCK | EFD_CLOEXEC)); - if (input.update_config_efd._fd < 0) { - BONGOCAT_LOG_ERROR("Failed to create notify pipe for input, update config: %s", strerror(errno)); - return bongocat_error_t::BONGOCAT_ERROR_FILE_IO; - } - } +created_result_t> create(const config::config_t& config) { + AllocatedMemory ret = make_allocated_memory(); + assert(ret); + if (ret == BONGOCAT_NULLPTR) [[unlikely]] { + return bongocat_error_t::BONGOCAT_ERROR_MEMORY; + } + if (config.num_keyboard_devices <= 0) { + BONGOCAT_LOG_WARNING("No input devices specified"); + } + + const timestamp_ms_t now = get_current_time_ms(); + ret->_capture_input_running = false; + ret->_input_kpm_counter = 0; + ret->_latest_kpm_update_ms = now; + + BONGOCAT_LOG_INFO("Initializing input monitoring system for %d devices", config.num_keyboard_devices); + + // Initialize shared memory for key press flag + ret->shm = make_allocated_mmap(); + if (!ret->shm.ptr) { + BONGOCAT_LOG_ERROR("Failed to create shared memory for input monitoring: %s", strerror(errno)); + return bongocat_error_t::BONGOCAT_ERROR_MEMORY; + } + ret->shm->any_key_pressed = 0; + ret->shm->hand_mapping = input_hand_mapping_t::None; + ret->shm->kpm = 0; + ret->shm->input_counter = 0; + ret->shm->last_key_pressed_timestamp = 0; + + // Initialize shared memory for local config + ret->_local_copy_config = make_allocated_mmap(); + if (!ret->_local_copy_config) { + BONGOCAT_LOG_ERROR("Failed to create shared memory for input monitoring: %s", strerror(errno)); + return bongocat_error_t::BONGOCAT_ERROR_MEMORY; + } + assert(ret->_local_copy_config); + *ret->_local_copy_config = config; + + ret->update_config_efd = platform::FileDescriptor(eventfd(0, EFD_NONBLOCK | EFD_CLOEXEC)); + if (ret->update_config_efd._fd < 0) { + BONGOCAT_LOG_ERROR("Failed to create notify pipe for input update config: %s", strerror(errno)); + return bongocat_error_t::BONGOCAT_ERROR_FILE_IO; + } + + BONGOCAT_LOG_INFO("Input monitoring started"); + return ret; +} - //if (trigger_ctx._input != ctx._input) { - // BONGOCAT_LOG_DEBUG("Input context in animation differs from animation trigger input context"); - //} +bongocat_error_t start(input_context_t& input, animation::animation_context_t& animation_ctx, + const config::config_t& config, CondVariable& configs_reloaded_cond, + atomic_uint64_t& config_generation) { + if (config.num_keyboard_devices < 0) { + BONGOCAT_LOG_ERROR("No input devices specified"); + return bongocat_error_t::BONGOCAT_ERROR_INVALID_PARAM; + } else if (config.num_keyboard_devices == 0) { + BONGOCAT_LOG_WARNING("No input devices specified"); + } + + const timestamp_ms_t now = get_current_time_ms(); + input._latest_kpm_update_ms = now; + + BONGOCAT_LOG_INFO("Initializing input monitoring system for %d devices", config.num_keyboard_devices); + + // Initialize shared memory for key press flag + input.shm = make_allocated_mmap(); + if (!input.shm.ptr) { + BONGOCAT_LOG_ERROR("Failed to create shared memory for input monitoring: %s", strerror(errno)); + return bongocat_error_t::BONGOCAT_ERROR_MEMORY; + } + input.shm->any_key_pressed = 0; + input.shm->hand_mapping = input_hand_mapping_t::None; + input.shm->kpm = 0; + input.shm->input_counter = 0; + input.shm->last_key_pressed_timestamp = now; // for idle check timestamp should be zero + + // Initialize shared memory for local config + input._local_copy_config = make_allocated_mmap(); + if (!input._local_copy_config) { + BONGOCAT_LOG_ERROR("Failed to create shared memory for input monitoring: %s", strerror(errno)); + return bongocat_error_t::BONGOCAT_ERROR_MEMORY; + } + assert(input._local_copy_config); + update_config(input, config, atomic_load(&config_generation)); + + // wait for animation trigger to be ready (input should be the same) + int cond_ret = animation_ctx.init_cond.timedwait([&]() { return atomic_load(&animation_ctx.ready); }, + COND_ANIMATION_TRIGGER_INIT_TIMEOUT_MS); + if (cond_ret == ETIMEDOUT) { + BONGOCAT_LOG_ERROR("Failed to initialize input monitoring: waiting for animation thread to start in time"); + } else { + assert(animation_ctx._input == &input); + } + // set extern/global references + { + // guard for anim_update_state + LockGuard guard(animation_ctx.thread_context.anim_lock); + animation_ctx._input = &input; + } + animation_ctx.init_cond.notify_all(); + input._config = &config; + input._configs_reloaded_cond = &configs_reloaded_cond; + input._config_generation = &config_generation; + atomic_store(&input.ready, true); + input.init_cond.notify_all(); + + input._configs_reloaded_cond->notify_all(); + + // start input monitoring thread + const int result = pthread_create(&input._input_thread, BONGOCAT_NULLPTR, input_thread, &animation_ctx); + if (result != 0) { + BONGOCAT_LOG_ERROR("Failed to start input monitoring thread: %s", strerror(errno)); + return bongocat_error_t::BONGOCAT_ERROR_THREAD; + } + + BONGOCAT_LOG_INFO("Input monitoring started"); + return bongocat_error_t::BONGOCAT_SUCCESS; +} - // set extern/global references - { - // guard for anim_update_state - LockGuard guard (trigger_ctx.anim.anim_lock); - trigger_ctx._input = &input; - } - input._config = &config; - input._configs_reloaded_cond = &configs_reloaded_cond; - input._config_generation = &config_generation; - atomic_store(&input.config_seen_generation, atomic_load(&config_generation)); - input.init_cond.notify_all(); - input._configs_reloaded_cond->notify_all(); - - // start input monitoring - const int result = pthread_create(&input._input_thread, nullptr, input_thread, &trigger_ctx); - if (result != 0) { - BONGOCAT_LOG_ERROR("Failed to start input monitoring thread: %s", strerror(errno)); - return bongocat_error_t::BONGOCAT_ERROR_THREAD; - } +bongocat_error_t restart(input_context_t& input, animation::animation_context_t& animation_ctx, + const config::config_t& config, CondVariable& configs_reloaded_cond, + atomic_uint64_t& config_generation) { + BONGOCAT_LOG_INFO("Restarting input monitoring system"); + // Stop current monitoring + if (input._input_thread) { + BONGOCAT_LOG_DEBUG("Input monitoring thread"); + atomic_store(&input._capture_input_running, false); + // pthread_cancel(ctx->_input_thread); + if (stop_thread_graceful_or_cancel(input._input_thread, input._capture_input_running) != 0) { + BONGOCAT_LOG_ERROR("Failed to join input thread: %s", strerror(errno)); + } + BONGOCAT_LOG_DEBUG("Input monitoring thread terminated"); + } + + // already done when stop current input thread + // cleanup_input_thread_context(ctx); + assert(!input._device_paths); + assert(!input._unique_paths_indices); + assert(!input._unique_devices); + assert(input._unique_paths_indices_capacity == 0); + + // reset stats + // ret._latest_kpm_update_ms = get_current_time_ms(); + + /// @TODO: re-create context with create(), avoid duplicate code + + // Start new monitoring (reuse shared memory if it exists) + { + LockGuard guard(input.input_lock); + if (!input.shm) { + input.shm = make_allocated_mmap(); + if (input.shm.ptr == MAP_FAILED) { + BONGOCAT_LOG_ERROR("Failed to create shared memory for input monitoring: %s", strerror(errno)); + return bongocat_error_t::BONGOCAT_ERROR_MEMORY; + } + } + } - BONGOCAT_LOG_INFO("Input monitoring restarted"); - return bongocat_error_t::BONGOCAT_SUCCESS; + if (!input._local_copy_config) { + input._local_copy_config = make_unallocated_mmap_value(config); + if (!input._local_copy_config) { + BONGOCAT_LOG_ERROR("Failed to create shared memory for input monitoring: %s", strerror(errno)); + return bongocat_error_t::BONGOCAT_ERROR_MEMORY; + } + } + assert(input._local_copy_config); + update_config(input, config, atomic_load(&config_generation)); + + if (input.update_config_efd._fd < 0) { + input.update_config_efd = platform::FileDescriptor(eventfd(0, EFD_NONBLOCK | EFD_CLOEXEC)); + if (input.update_config_efd._fd < 0) { + BONGOCAT_LOG_ERROR("Failed to create notify pipe for input, update config: %s", strerror(errno)); + return bongocat_error_t::BONGOCAT_ERROR_FILE_IO; } + } + + // if (trigger_ctx._input != ctx._input) { + // BONGOCAT_LOG_DEBUG("Input context in animation differs from animation trigger input context"); + // } + + // set extern/global references + { + // guard for anim_update_state + LockGuard guard(animation_ctx.thread_context.anim_lock); + animation_ctx._input = &input; + } + input._config = &config; + input._configs_reloaded_cond = &configs_reloaded_cond; + input._config_generation = &config_generation; + atomic_store(&input.config_seen_generation, atomic_load(&config_generation)); + input.init_cond.notify_all(); + input._configs_reloaded_cond->notify_all(); + + // start input monitoring + const int result = pthread_create(&input._input_thread, BONGOCAT_NULLPTR, input_thread, &animation_ctx); + if (result != 0) { + BONGOCAT_LOG_ERROR("Failed to start input monitoring thread: %s", strerror(errno)); + return bongocat_error_t::BONGOCAT_ERROR_THREAD; + } + + BONGOCAT_LOG_INFO("Input monitoring restarted"); + return bongocat_error_t::BONGOCAT_SUCCESS; +} - void stop(input_context_t& ctx) { - atomic_store(&ctx._capture_input_running, false); - if (ctx._input_thread) { - BONGOCAT_LOG_DEBUG("Stopping input thread"); - //pthread_cancel(ctx->_input_thread); - if (stop_thread_graceful_or_cancel(ctx._input_thread, ctx._capture_input_running) != 0) { - BONGOCAT_LOG_ERROR("Failed to join input thread: %s", strerror(errno)); - } - BONGOCAT_LOG_DEBUG("Input monitoring thread terminated"); - } - ctx._input_thread = 0; +void stop(input_context_t& ctx) { + atomic_store(&ctx._capture_input_running, false); + if (ctx._input_thread) { + BONGOCAT_LOG_DEBUG("Stopping input thread"); + // pthread_cancel(ctx->_input_thread); + if (stop_thread_graceful_or_cancel(ctx._input_thread, ctx._capture_input_running) != 0) { + BONGOCAT_LOG_ERROR("Failed to join input thread: %s", strerror(errno)); + } + BONGOCAT_LOG_DEBUG("Input monitoring thread terminated"); + } + ctx._input_thread = 0; - ctx._config = nullptr; - ctx._configs_reloaded_cond = nullptr; - ctx._config_generation = nullptr; + ctx._config = BONGOCAT_NULLPTR; + ctx._configs_reloaded_cond = BONGOCAT_NULLPTR; + ctx._config_generation = BONGOCAT_NULLPTR; - ctx.config_updated.notify_all(); - atomic_store(&ctx.ready, false); - ctx.init_cond.notify_all(); - } + ctx.config_updated.notify_all(); + atomic_store(&ctx.ready, false); + ctx.init_cond.notify_all(); +} - void trigger_update_config(input_context_t& input, const config::config_t& config, uint64_t config_generation) { - //assert(input.anim._local_copy_config != nullptr); - //assert(input.anim.shm != nullptr); +void trigger_update_config(input_context_t& input, const config::config_t& config, uint64_t config_generation) { + // assert(input.anim._local_copy_config != nullptr); + // assert(input.anim.shm != nullptr); - input._config = &config; - if (write(input.update_config_efd._fd, &config_generation, sizeof(uint64_t)) >= 0) { - BONGOCAT_LOG_VERBOSE("Write input trigger update config"); - } else { - BONGOCAT_LOG_ERROR("Failed to write to notify pipe in input: %s", strerror(errno)); - } - } + input._config = &config; + if (write(input.update_config_efd._fd, &config_generation, sizeof(uint64_t)) >= 0) { + BONGOCAT_LOG_VERBOSE("Write input trigger update config"); + } else { + BONGOCAT_LOG_ERROR("Failed to write to notify pipe in input: %s", strerror(errno)); + } +} - void update_config(input_context_t& input, const config::config_t& config, uint64_t new_gen) { - assert(input._local_copy_config != nullptr); +void update_config(input_context_t& input, const config::config_t& config, uint64_t new_gen) { + assert(input._local_copy_config); - *input._local_copy_config = config; + *input._local_copy_config = config; - atomic_store(&input.config_seen_generation, new_gen); - // Signal main that reload is done - input.config_updated.notify_all(); - } + atomic_store(&input.config_seen_generation, new_gen); + // Signal main that reload is done + input.config_updated.notify_all(); } +} // namespace bongocat::platform::input diff --git a/src/platform/update.cpp b/src/platform/update.cpp index f0d244aa..12780099 100644 --- a/src/platform/update.cpp +++ b/src/platform/update.cpp @@ -1,692 +1,724 @@ #include "platform/update.h" + #include "graphics/animation.h" -#include "utils/memory.h" #include "platform/wayland.h" -#include -#include -#include -#include -#include -#include -#include +#include "utils/memory.h" + #include #include +#include #include +#include #include -#include -#include +#include #include +#include +#include +#include +#include +#include +#include namespace bongocat::platform::update { - inline static constexpr int MAX_ATTEMPTS = 2048; - inline static constexpr size_t GET_CPU_PRESENT_LAST_BUF = 256; - inline static constexpr size_t CPU_INFO_BUF = 8192; +inline static constexpr int MAX_ATTEMPTS = 2048; +inline static constexpr size_t GET_CPU_PRESENT_LAST_BUF = 256; +inline static constexpr size_t CPU_INFO_BUF = 8192; - static inline constexpr auto UPDATE_POOL_TIMEOUT_MS = 1000; - static inline constexpr auto COND_STORED_TIMEOUT_MS = 1000; +static inline constexpr auto UPDATE_POOL_TIMEOUT_MS = 1000; +static inline constexpr auto COND_STORED_TIMEOUT_MS = 1000; - inline static constexpr time_ms_t COND_ANIMATION_TRIGGER_INIT_TIMEOUT_MS = 5000; - inline static constexpr time_ms_t COND_RELOAD_CONFIGS_TIMEOUT_MS = 5000; +inline static constexpr time_ms_t COND_ANIMATION_TRIGGER_INIT_TIMEOUT_MS = 5000; +inline static constexpr time_ms_t COND_RELOAD_CONFIGS_TIMEOUT_MS = 5000; - inline static constexpr const char* FILENAME_CPU_PRESET = "/sys/devices/system/cpu/present"; - inline static constexpr const char* FILENAME_PROC_STAT = "/proc/stat"; +inline static constexpr const char *FILENAME_CPU_PRESET = "/sys/devices/system/cpu/present"; +inline static constexpr const char *FILENAME_PROC_STAT = "/proc/stat"; - static void cleanup_update_thread(void* arg) { - assert(arg); - animation::animation_session_t& trigger_ctx = *static_cast(arg); - assert(trigger_ctx._update); - update_context_t& input = *trigger_ctx._update; +static void cleanup_update_thread(void *arg) { + assert(arg); + const animation::animation_context_t& animation_context = *static_cast(arg); + assert(animation_context._update); + update_context_t& input = *animation_context._update; - atomic_store(&input._running, false); + atomic_store(&input._running, false); - input.config_updated.notify_all(); + input.config_updated.notify_all(); - BONGOCAT_LOG_INFO("Update thread cleanup completed (via pthread_cancel)"); - } + BONGOCAT_LOG_INFO("Update thread cleanup completed (via pthread_cancel)"); +} +static int set_nonblocking(int fd) { + const int flags = fcntl(fd, F_GETFL, 0); + if (flags == -1) { + return -1; + } + return fcntl(fd, F_SETFL, flags | O_NONBLOCK); +} - static int set_nonblocking(int fd) { - int flags = fcntl(fd, F_GETFL, 0); - if (flags == -1) return -1; - return fcntl(fd, F_SETFL, flags | O_NONBLOCK); +/* +static size_t get_cpu_present_last(int fd) { + lseek(fd, 0, SEEK_SET); + char buf[GET_CPU_PRESENT_LAST_BUF] = {0}; + ssize_t len = read(fd, buf, sizeof(buf)-1); + if (len <= 0) return 0; + assert(len >= 0 && static_cast(len) < GET_CPU_PRESENT_LAST_BUF); + buf[len] = '\0'; + + const char* last_sep = strpbrk(buf, "-,"); + if (last_sep) { + return strtoul(last_sep + 1, nullptr, 10); } + return 0; +} +*/ + +static const cpu_snapshot_t& get_latest_snapshot_unlocked(update_context_t& ctx) { + assert(ctx.shm); + auto& update_shm = *ctx.shm; + ctx.update_cond.timedwait([&]() { return update_shm.cpu_snapshots.stored > 0; }, COND_STORED_TIMEOUT_MS); + static_assert(CpuSnapshotRingBufferMaxHistory > 0); + const size_t latest = + (update_shm.cpu_snapshots.head + CpuSnapshotRingBufferMaxHistory - 1) % CpuSnapshotRingBufferMaxHistory; + assert(latest < CpuSnapshotRingBufferMaxHistory); + return update_shm.cpu_snapshots.history[latest]; +} - /* - static size_t get_cpu_present_last(int fd) { - lseek(fd, 0, SEEK_SET); - char buf[GET_CPU_PRESENT_LAST_BUF] = {0}; - ssize_t len = read(fd, buf, sizeof(buf)-1); - if (len <= 0) return 0; - assert(len >= 0 && static_cast(len) < GET_CPU_PRESENT_LAST_BUF); - buf[len] = '\0'; - - const char* last_sep = strpbrk(buf, "-,"); - if (last_sep) { - return strtoul(last_sep + 1, nullptr, 10); - } - return 0; - } - */ - - static const cpu_snapshot_t& get_latest_snapshot_unlocked(update_context_t& ctx) { - assert(ctx.shm != nullptr); - auto& update_shm = *ctx.shm; - ctx.update_cond.timedwait([&]() { - return update_shm.cpu_snapshots.stored > 0; - }, COND_STORED_TIMEOUT_MS); - assert(CpuSnapshotRingBufferMaxHistory > 0); - const size_t latest = (update_shm.cpu_snapshots.head + CpuSnapshotRingBufferMaxHistory - 1) % CpuSnapshotRingBufferMaxHistory; - assert(latest < CpuSnapshotRingBufferMaxHistory); - return update_shm.cpu_snapshots.history[latest]; - } +const cpu_snapshot_t& get_latest_snapshot(update_context_t& ctx) { + LockGuard guard(ctx.update_lock); + return get_latest_snapshot_unlocked(ctx); +} - const cpu_snapshot_t& get_latest_snapshot(update_context_t& ctx) { - LockGuard guard (ctx.update_lock); - return get_latest_snapshot_unlocked(ctx); +static size_t parse_cpuinfo_fd(int fd, size_t cpu_present_last, cpu_stat_t *out, size_t out_size) { + lseek(fd, 0, SEEK_SET); + char buf[CPU_INFO_BUF] = {0}; + const ssize_t len = read(fd, buf, sizeof(buf) - 1); + if (len <= 0) { + return 0; + } + assert(len >= 0 && static_cast(len) < CPU_INFO_BUF); + buf[len] = '\0'; + + ssize_t current_cpu_number = -1; + size_t used = 0; + + char *saveptr = BONGOCAT_NULLPTR; + char *line = strtok_r(buf, "\n", &saveptr); + while (line) { + if (strncmp(line, "cpu", 3) != 0) { + break; } - static size_t parse_cpuinfo_fd(int fd, size_t cpu_present_last, cpu_stat_t* out, size_t out_size) { - lseek(fd, 0, SEEK_SET); - char buf[CPU_INFO_BUF] = {0}; - const ssize_t len = read(fd, buf, sizeof(buf)-1); - if (len <= 0) return 0; - assert(len >= 0 && static_cast(len) < CPU_INFO_BUF); - buf[len] = '\0'; - - ssize_t current_cpu_number = -1; - size_t used = 0; - - char* saveptr = nullptr; - char* line = strtok_r(buf, "\n", &saveptr); - while (line) { - if (strncmp(line, "cpu", 3) != 0) break; - - size_t line_cpu_number = 0; - if (current_cpu_number >= 0) { - line_cpu_number = strtoul(line + 3, nullptr, 10); - assert(current_cpu_number >= 0); - //assert(current_cpu_number <= SIZE_MAX); - while (line_cpu_number > static_cast(current_cpu_number) && used < out_size) { - out[used] = {}; - used++; - current_cpu_number++; - } - } - - char* p = line; - while (*p && !isspace(static_cast(*p))) p++; - while (*p && isspace(static_cast(*p))) p++; - - constexpr size_t times_size = 16; - size_t times[times_size] = {0}; - size_t times_count = 0; - char* tok = strtok(p, " \t"); - while (tok && times_count < times_size) { - times[times_count++] = strtoull(tok, nullptr, 10); - tok = strtok(nullptr, " \t"); - } - - size_t idle_time = 0; - size_t total_time = 0; - assert(times_size >= 5); - if (times_count >= 5) { - idle_time = times[3] + times[4]; - for (size_t i = 0; i < times_count; i++) total_time += times[i]; - } - - if (used < MaxCpus) { - out[used] = cpu_stat_t{.idle_time = idle_time, .total_time = total_time}; - used++; - } - current_cpu_number++; - - line = strtok_r(nullptr, "\n", &saveptr); - } - - if (current_cpu_number >= 0) { - assert(current_cpu_number >= 0); - //assert(current_cpu_number <= SIZE_MAX); - while (static_cast(current_cpu_number) <= cpu_present_last && used < out_size) { - out[used] = {}; - used++; - current_cpu_number++; - } - } - - return used; + size_t line_cpu_number = 0; + if (current_cpu_number >= 0) { + line_cpu_number = strtoul(line + 3, BONGOCAT_NULLPTR, 10); + assert(current_cpu_number >= 0); + // assert(current_cpu_number <= SIZE_MAX); + while (line_cpu_number > static_cast(current_cpu_number) && used < out_size) { + out[used] = {}; + used++; + current_cpu_number++; + } } - static double compute_avg_cpu_usage(const cpu_snapshot_t& prev, const cpu_snapshot_t& curr) { - assert(curr.count == prev.count); // both snapshots must have same CPU count - - size_t total_delta = 0; - size_t idle_delta = 0; - for (size_t i = 0; i < curr.count; i++) { - const cpu_stat_t& p = prev.stats[i]; - const cpu_stat_t& c = curr.stats[i]; - - const size_t d_total = (c.total_time > p.total_time) ? (c.total_time - p.total_time) : 0; - const size_t d_idle = (c.idle_time > p.idle_time) ? (c.idle_time - p.idle_time) : 0; - - total_delta += d_total; - idle_delta += d_idle; - } - - if (total_delta == 0) return 0.0; - return 100.0 * static_cast(total_delta - idle_delta) / static_cast(total_delta); + char *p = line; + while (*p && !isspace(static_cast(*p))) { + p++; + } + while (*p && isspace(static_cast(*p))) { + p++; } - static double compute_max_cpu_usage(const cpu_snapshot_t& prev, const cpu_snapshot_t& curr) { - assert(curr.count == prev.count); // both snapshots must have same CPU count - - double max_usage = 0.0; - - for (size_t i = 0; i < curr.count; i++) { - const cpu_stat_t* p = &prev.stats[i]; - const cpu_stat_t* c = &curr.stats[i]; - - const size_t d_total = (c->total_time > p->total_time) ? (c->total_time - p->total_time) : 0; - const size_t d_idle = (c->idle_time > p->idle_time) ? (c->idle_time - p->idle_time) : 0; - if (d_total == 0) { - continue; // skip if no change - } + constexpr size_t times_size = 16; + size_t times[times_size] = {0}; + size_t times_count = 0; + char *tok = strtok(p, " \t"); + while (tok && times_count < times_size) { + times[times_count++] = strtoull(tok, BONGOCAT_NULLPTR, 10); + tok = strtok(BONGOCAT_NULLPTR, " \t"); + } - const double usage = 100.0 * static_cast(d_total - d_idle) / static_cast(d_total); - if (usage > max_usage) { - max_usage = usage; - } - } + size_t idle_time = 0; + size_t total_time = 0; + assert(times_size >= 5); + if (times_count >= 5) { + idle_time = times[3] + times[4]; + for (size_t i = 0; i < times_count; i++) { + total_time += times[i]; + } + } - return max_usage; + if (used < MaxCpus) { + out[used] = cpu_stat_t{.idle_time = idle_time, .total_time = total_time}; + used++; + } + current_cpu_number++; + + line = strtok_r(BONGOCAT_NULLPTR, "\n", &saveptr); + } + + if (current_cpu_number >= 0) { + assert(current_cpu_number >= 0); + // assert(current_cpu_number <= SIZE_MAX); + while (static_cast(current_cpu_number) <= cpu_present_last && used < out_size) { + out[used] = {}; + used++; + current_cpu_number++; } + } - static void* update_thread(void* arg) { - assert(arg); - animation::animation_session_t& trigger_ctx = *static_cast(arg); - - // from thread context - //animation_context_t& anim = trigger_ctx.anim; - // wait for input context (in animation start) - trigger_ctx.init_cond.timedwait([&]() { - return atomic_load(&trigger_ctx.ready); - }, COND_ANIMATION_TRIGGER_INIT_TIMEOUT_MS); - assert(trigger_ctx._input != nullptr); - update_context_t& upd = *trigger_ctx._update; - - // sanity checks - assert(upd._config != nullptr); - assert(upd._configs_reloaded_cond != nullptr); - assert(!upd._running); - assert(upd.shm != nullptr); - assert(upd._local_copy_config != nullptr); - assert(upd.update_config_efd._fd >= 0); - assert(upd.fd_present._fd >= 0); - assert(upd.fd_stat._fd >= 0); - - // trigger initial render - wayland::request_render(trigger_ctx); - - pthread_cleanup_push(cleanup_update_thread, arg); - - // local thread context - /// event poll - // 0: reload config event - // 1: fd_stat - // 2: fd_present - constexpr size_t nfds = 3; - pollfd pfds[nfds]; - - atomic_store(&upd._running, true); - while (atomic_load(&upd._running)) { - pthread_testcancel(); // optional, but makes cancellation more responsive - - // read from config - time_ms_t timeout = UPDATE_POOL_TIMEOUT_MS; - time_ms_t update_rate_ms = 0; - time_ms_t animation_speed_ms = 0; - double cpu_threshold = 0; - int fps = 0; - //bool enable_debug = false; - { - // read-only config - assert(upd._local_copy_config != nullptr); - const config::config_t& current_config = *upd._local_copy_config; - - //enable_debug = current_config.enable_debug; - - cpu_threshold = current_config.cpu_threshold; - update_rate_ms = current_config.update_rate_ms; - animation_speed_ms = current_config.animation_speed_ms; - fps = current_config.fps; - if (update_rate_ms > 0) { - timeout = update_rate_ms; - } else if (animation_speed_ms > 0) { - timeout = animation_speed_ms; - } else if (current_config.fps > 0) { - timeout = 1000 / current_config.fps / 2; - } - } + return used; +} - // init pfds - constexpr size_t fds_update_config_index = 0; - constexpr size_t fds_stat_index = 1; - constexpr size_t fds_preset_index = 2; - pfds[fds_update_config_index] = { .fd = upd.update_config_efd._fd, .events = POLLIN, .revents = 0 }; - pfds[fds_stat_index] = { .fd = upd.fd_stat._fd, .events = POLLIN, .revents = 0 }; - pfds[fds_preset_index] = { .fd = upd.fd_present._fd, .events = POLLIN, .revents = 0 }; - - // poll events - const int poll_result = poll(pfds, nfds, static_cast(timeout)); - if (poll_result < 0) { - if (errno == EINTR) continue; // Interrupted by signal - BONGOCAT_LOG_ERROR("update: Poll error: %s", strerror(errno)); - break; - } +static double compute_avg_cpu_usage(const cpu_snapshot_t& prev, const cpu_snapshot_t& curr) { + assert(curr.count == prev.count); // both snapshots must have same CPU count - // cancel pooling (when not running anymore) - if (!atomic_load(&upd._running)) { - // draining pools - for (size_t i = 0;i < nfds;i++) { - if (pfds[i].revents & POLLIN) { - drain_event(pfds[i], MAX_ATTEMPTS); - } - } - break; - } + size_t total_delta = 0; + size_t idle_delta = 0; + for (size_t i = 0; i < curr.count; i++) { + const cpu_stat_t& p = prev.stats[i]; + const cpu_stat_t& c = curr.stats[i]; - // Handle config update - assert(upd._config_generation != nullptr); - bool reload_config = false; - uint64_t new_gen{atomic_load(upd._config_generation)}; - if (pfds[fds_update_config_index].revents & POLLIN) { - BONGOCAT_LOG_DEBUG("update: Receive update config event"); - drain_event(pfds[fds_update_config_index], MAX_ATTEMPTS, "update config eventfd"); - reload_config = new_gen > 0; - } + const size_t d_total = (c.total_time > p.total_time) ? (c.total_time - p.total_time) : 0; + const size_t d_idle = (c.idle_time > p.idle_time) ? (c.idle_time - p.idle_time) : 0; - // Handle stat - if (pfds[fds_stat_index].revents & POLLIN) { - BONGOCAT_LOG_VERBOSE("update: Receive update CPU stats event"); - drain_event(pfds[fds_stat_index], MAX_ATTEMPTS, FILENAME_PROC_STAT); - - if (update_rate_ms > 0) { - platform::LockGuard guard (upd.update_lock); - assert(upd.shm != nullptr); - auto& update_shm = *upd.shm; - - const size_t count = parse_cpuinfo_fd(upd.fd_stat._fd, 0, update_shm.cpu_snapshots.history[update_shm.cpu_snapshots.head].stats, MaxCpus); - update_shm.cpu_snapshots.history[update_shm.cpu_snapshots.head].count = count; - - { - LockGuard guard_cond (upd.update_cond._mutex); - update_shm.cpu_snapshots.head = (update_shm.cpu_snapshots.head + 1) % CpuSnapshotRingBufferMaxHistory; - if (update_shm.cpu_snapshots.stored < CpuSnapshotRingBufferMaxHistory) update_shm.cpu_snapshots.stored++; - pthread_cond_broadcast(&upd.update_cond._cond); - } - } - } + total_delta += d_total; + idle_delta += d_idle; + } - // cpu_present file changed - if (pfds[fds_preset_index].revents & POLLIN) { - BONGOCAT_LOG_VERBOSE("update: Receive update CPU present event"); - drain_event(pfds[fds_preset_index], MAX_ATTEMPTS, FILENAME_CPU_PRESET); + if (total_delta == 0) { + return 0.0; + } + return 100.0 * static_cast(total_delta - idle_delta) / static_cast(total_delta); +} - if (update_rate_ms > 0) { - BONGOCAT_LOG_VERBOSE("update: cpu_present file changed (hotplug)"); - } - } +static double compute_max_cpu_usage(const cpu_snapshot_t& prev, const cpu_snapshot_t& curr) { + assert(curr.count == prev.count); // both snapshots must have same CPU count - // trigger animation - bool animation_triggered = false; - if (update_rate_ms > 0) { - LockGuard guard (upd.update_lock); - assert(upd.shm != nullptr); - auto& update_shm = *upd.shm; - - // read-only config - assert(upd._local_copy_config != nullptr); - const config::config_t& current_config = *upd._local_copy_config; - - update_shm.latest_snapshot = &get_latest_snapshot_unlocked(upd); - - if (update_shm.cpu_snapshots.stored >= 2) { - const size_t latest = (update_shm.cpu_snapshots.head + CpuSnapshotRingBufferMaxHistory - 1) % CpuSnapshotRingBufferMaxHistory; - const size_t prev_i = (latest + CpuSnapshotRingBufferMaxHistory - 1) % CpuSnapshotRingBufferMaxHistory; - - const cpu_snapshot_t& curr = update_shm.cpu_snapshots.history[latest]; - const cpu_snapshot_t& prev = update_shm.cpu_snapshots.history[prev_i]; - - update_shm.avg_cpu_usage = compute_avg_cpu_usage(prev, curr); - update_shm.max_cpu_usage = compute_max_cpu_usage(prev, curr); - - const bool above_threshold = current_config.cpu_threshold >= ENABLED_MIN_CPU_PERCENT && (update_shm.avg_cpu_usage >= current_config.cpu_threshold || update_shm.max_cpu_usage >= current_config.cpu_threshold); - const bool lower_threshold = current_config.cpu_threshold >= ENABLED_MIN_CPU_PERCENT && (update_shm.last_avg_cpu_usage >= current_config.cpu_threshold || update_shm.last_max_cpu_usage >= current_config.cpu_threshold) && (update_shm.avg_cpu_usage < current_config.cpu_threshold || update_shm.max_cpu_usage < current_config.cpu_threshold); - const bool crossed_delta = fabs(update_shm.avg_cpu_usage - update_shm.last_avg_cpu_usage) >= TRIGGER_ANIMATION_CPU_DIFF_PERCENT || fabs(update_shm.max_cpu_usage - update_shm.last_max_cpu_usage) >= TRIGGER_ANIMATION_CPU_DIFF_PERCENT; - if (above_threshold || lower_threshold) { - if (!update_shm.cpu_active || crossed_delta) { - BONGOCAT_LOG_VERBOSE("update: avg. CPU: %.2f (max: %.2f)", update_shm.avg_cpu_usage, update_shm.max_cpu_usage); - animation::trigger(trigger_ctx, animation::trigger_animation_cause_mask_t::CpuUpdate); - if (lower_threshold) { - animation::trigger(trigger_ctx, animation::trigger_animation_cause_mask_t::IdleUpdate); - } - animation_triggered = true; - } - update_shm.cpu_active = true; - } else { - if (update_shm.cpu_active) { - update_shm.cpu_active = false; - animation::trigger(trigger_ctx, animation::trigger_animation_cause_mask_t::CpuUpdate); - animation::trigger(trigger_ctx, animation::trigger_animation_cause_mask_t::IdleUpdate); - } - } - } - - update_shm.last_avg_cpu_usage = update_shm.avg_cpu_usage; - update_shm.last_max_cpu_usage = update_shm.max_cpu_usage; - } + double max_usage = 0.0; - // handle update config - if (reload_config) { - assert(upd._config_generation != nullptr); - assert(upd._configs_reloaded_cond != nullptr); - assert(upd._config != nullptr); - - update_config(upd, *upd._config, new_gen); - - // wait for reload config to be done (all configs) - const int rc = upd._configs_reloaded_cond->timedwait([&] { - return atomic_load(upd._config_generation) >= new_gen; - }, COND_RELOAD_CONFIGS_TIMEOUT_MS); - if (rc == ETIMEDOUT) { - BONGOCAT_LOG_WARNING("update: Timed out waiting for reload eventfd: %s", strerror(errno)); - } - if constexpr (features::Debug) { - if (atomic_load(&upd.config_seen_generation) < atomic_load(upd._config_generation)) { - BONGOCAT_LOG_VERBOSE("update: update.config_seen_generation < update._config_generation; %d < %d", atomic_load(&upd.config_seen_generation), atomic_load(upd._config_generation)); - } - } - //assert(atomic_load(&upd.config_seen_generation) >= atomic_load(upd._config_generation)); - atomic_store(&upd.config_seen_generation, atomic_load(upd._config_generation)); - BONGOCAT_LOG_INFO("update: Update config reloaded (gen=%u)", new_gen); - } + for (size_t i = 0; i < curr.count; i++) { + const cpu_stat_t *p = &prev.stats[i]; + const cpu_stat_t *c = &curr.stats[i]; - if (update_rate_ms) { - // sleep - timespec ts; - ts.tv_sec = 0; - assert(fps > 0); - if (animation_triggered && animation_speed_ms > 0) { - // when animation were triggered, wait for animation to end (assumption), before triggering a possible new animation - ts.tv_nsec = animation_speed_ms * 1000 * 1000; - } else if (animation_triggered) { - ts.tv_nsec = (1000 / fps) * 1000 * 1000; - } else { - ts.tv_nsec = timeout * 1000 * 1000; - } - while (ts.tv_nsec >= 1000000000LL) { - ts.tv_nsec -= 1000000000LL; - ts.tv_sec += 1; - } - nanosleep(&ts, nullptr); - { - LockGuard guard (upd.update_lock); - assert(upd.shm != nullptr); - auto& update_shm = *upd.shm; - update_shm.cpu_active = false; - } - } else { - if (update_rate_ms == 0 || cpu_threshold < ENABLED_MIN_CPU_PERCENT) { - atomic_store(&upd._running, false); - BONGOCAT_LOG_WARNING("update: Stop update thread, not needed"); - } - } - } + const size_t d_total = (c->total_time > p->total_time) ? (c->total_time - p->total_time) : 0; + const size_t d_idle = (c->idle_time > p->idle_time) ? (c->idle_time - p->idle_time) : 0; + if (d_total == 0) { + continue; // skip if no change + } - atomic_store(&upd._running, false); + const double usage = 100.0 * static_cast(d_total - d_idle) / static_cast(d_total); + if (usage > max_usage) { + max_usage = usage; + } + } - // Will run only on normal return - pthread_cleanup_pop(1); // 1 = call cleanup even if not canceled + return max_usage; +} - BONGOCAT_LOG_INFO("Update monitoring stopped"); +static void *update_thread(void *arg) { + assert(arg); + animation::animation_context_t& animation_ctx = *static_cast(arg); + + // from thread context + // animation_context_t& anim = trigger_ctx.anim; + // wait for input context (in animation start) + animation_ctx.init_cond.timedwait([&]() { return atomic_load(&animation_ctx.ready); }, + COND_ANIMATION_TRIGGER_INIT_TIMEOUT_MS); + assert(animation_ctx._input != BONGOCAT_NULLPTR); + update_context_t& upd = *animation_ctx._update; + + // sanity checks + assert(upd._config != BONGOCAT_NULLPTR); + assert(upd._configs_reloaded_cond != BONGOCAT_NULLPTR); + assert(!upd._running); + assert(upd.shm); + assert(upd._local_copy_config); + assert(upd.update_config_efd._fd >= 0); + assert(upd.fd_present._fd >= 0); + assert(upd.fd_stat._fd >= 0); + + // trigger initial render + wayland::request_render(animation_ctx); + + pthread_cleanup_push(cleanup_update_thread, arg); + + // local thread context + /// event poll + // 0: reload config event + // 1: fd_stat + // 2: fd_present + constexpr size_t nfds = 3; + pollfd pfds[nfds]; + + atomic_store(&upd._running, true); + while (atomic_load(&upd._running)) { + pthread_testcancel(); // optional, but makes cancellation more responsive + + // read from config + time_ms_t timeout = UPDATE_POOL_TIMEOUT_MS; + time_ms_t update_rate_ms = 0; + time_ms_t animation_speed_ms = 0; + double cpu_threshold = 0; + int fps = 0; + // bool enable_debug = false; + { + // read-only config + assert(upd._local_copy_config); + const config::config_t& current_config = *upd._local_copy_config; + + // enable_debug = current_config.enable_debug; + + cpu_threshold = current_config.cpu_threshold; + update_rate_ms = current_config.update_rate_ms; + animation_speed_ms = current_config.animation_speed_ms; + fps = current_config.fps; + if (update_rate_ms > 0) { + timeout = update_rate_ms; + } else if (animation_speed_ms > 0) { + timeout = animation_speed_ms; + } else if (current_config.fps > 0) { + timeout = 1000 / current_config.fps / 2; + } + } - return nullptr; + // init pfds + constexpr size_t fds_update_config_index = 0; + constexpr size_t fds_stat_index = 1; + constexpr size_t fds_preset_index = 2; + pfds[fds_update_config_index] = {.fd = upd.update_config_efd._fd, .events = POLLIN, .revents = 0}; + pfds[fds_stat_index] = {.fd = upd.fd_stat._fd, .events = POLLIN, .revents = 0}; + pfds[fds_preset_index] = {.fd = upd.fd_present._fd, .events = POLLIN, .revents = 0}; + + // poll events + const int poll_result = poll(pfds, nfds, static_cast(timeout)); + if (poll_result < 0) { + if (errno == EINTR) { + continue; // Interrupted by signal + } + BONGOCAT_LOG_ERROR("update: Poll error: %s", strerror(errno)); + break; } - created_result_t> create(const config::config_t& config) { - AllocatedMemory ret = make_allocated_memory(); - assert(ret != nullptr); - if (ret == nullptr) { - return bongocat_error_t::BONGOCAT_ERROR_MEMORY; + // cancel pooling (when not running anymore) + if (!atomic_load(&upd._running)) { + // draining pools + for (size_t i = 0; i < nfds; i++) { + if (pfds[i].revents & POLLIN) { + drain_event(pfds[i], MAX_ATTEMPTS); } + } + break; + } - ret->_running = false; - - BONGOCAT_LOG_INFO("Initializing update monitoring system"); + // Handle config update + assert(upd._config_generation != BONGOCAT_NULLPTR); + bool reload_config = false; + uint64_t new_gen{atomic_load(upd._config_generation)}; + if (pfds[fds_update_config_index].revents & POLLIN) { + BONGOCAT_LOG_DEBUG("update: Receive update config event"); + drain_event(pfds[fds_update_config_index], MAX_ATTEMPTS, "update config eventfd"); + reload_config = new_gen > 0; + } - // Initialize shared memory - ret->shm = make_allocated_mmap(); - if (!ret->shm.ptr) { - BONGOCAT_LOG_ERROR("Failed to create shared memory for update monitoring: %s", strerror(errno)); - return bongocat_error_t::BONGOCAT_ERROR_MEMORY; - } + // Handle stat + if (pfds[fds_stat_index].revents & POLLIN) { + BONGOCAT_LOG_VERBOSE("update: Receive update CPU stats event"); + drain_event(pfds[fds_stat_index], MAX_ATTEMPTS, FILENAME_PROC_STAT); - // Initialize shared memory for local config - ret->_local_copy_config = make_allocated_mmap(); - if (!ret->_local_copy_config) { - BONGOCAT_LOG_ERROR("Failed to create shared memory for update monitoring: %s", strerror(errno)); - return bongocat_error_t::BONGOCAT_ERROR_MEMORY; - } - assert(ret->_local_copy_config != nullptr); - *ret->_local_copy_config = config; + if (update_rate_ms > 0) { + platform::LockGuard guard(upd.update_lock); + assert(upd.shm); + auto& update_shm = *upd.shm; - ret->update_config_efd = platform::FileDescriptor(eventfd(0, EFD_NONBLOCK | EFD_CLOEXEC)); - if (ret->update_config_efd._fd < 0) { - BONGOCAT_LOG_ERROR("Failed to create notify pipe for update update config: %s", strerror(errno)); - return bongocat_error_t::BONGOCAT_ERROR_FILE_IO; - } + const size_t count = parse_cpuinfo_fd( + upd.fd_stat._fd, 0, update_shm.cpu_snapshots.history[update_shm.cpu_snapshots.head].stats, MaxCpus); + update_shm.cpu_snapshots.history[update_shm.cpu_snapshots.head].count = count; - // open files - ret->fd_present = FileDescriptor(open(FILENAME_CPU_PRESET, O_RDONLY)); - if (ret->fd_present._fd < 0) { - BONGOCAT_LOG_ERROR("Failed to open cpu_present"); - return bongocat_error_t::BONGOCAT_ERROR_FILE_IO; - } - set_nonblocking(ret->fd_present._fd); - - ret->fd_stat = FileDescriptor(open(FILENAME_PROC_STAT, O_RDONLY)); - if (ret->fd_stat._fd < 0) { - BONGOCAT_LOG_ERROR("Failed to open proc stat"); - return bongocat_error_t::BONGOCAT_ERROR_FILE_IO; + { + LockGuard guard_cond(upd.update_cond._mutex); + update_shm.cpu_snapshots.head = (update_shm.cpu_snapshots.head + 1) % CpuSnapshotRingBufferMaxHistory; + if (update_shm.cpu_snapshots.stored < CpuSnapshotRingBufferMaxHistory) { + update_shm.cpu_snapshots.stored++; + } + pthread_cond_broadcast(&upd.update_cond._cond); } - set_nonblocking(ret->fd_stat._fd); - - BONGOCAT_LOG_INFO("Input monitoring started"); - return ret; + } } - bongocat_error_t start(update_context_t& upd, animation::animation_session_t& trigger_ctx, const config::config_t& config, CondVariable& configs_reloaded_cond, atomic_uint64_t& config_generation) { - BONGOCAT_LOG_INFO("Initializing update monitoring"); + // cpu_present file changed + if (pfds[fds_preset_index].revents & POLLIN) { + BONGOCAT_LOG_VERBOSE("update: Receive update CPU present event"); + drain_event(pfds[fds_preset_index], MAX_ATTEMPTS, FILENAME_CPU_PRESET); - // Initialize shared memory for key press flag - upd.shm = make_allocated_mmap(); - if (!upd.shm.ptr) { - BONGOCAT_LOG_ERROR("Failed to create shared memory for input monitoring: %s", strerror(errno)); - return bongocat_error_t::BONGOCAT_ERROR_MEMORY; - } + if (update_rate_ms > 0) { + BONGOCAT_LOG_VERBOSE("update: cpu_present file changed (hotplug)"); + } + } - // Initialize shared memory for local config - upd._local_copy_config = make_allocated_mmap(); - if (!upd._local_copy_config) { - BONGOCAT_LOG_ERROR("Failed to create shared memory for input monitoring: %s", strerror(errno)); - return bongocat_error_t::BONGOCAT_ERROR_MEMORY; - } - assert(upd._local_copy_config != nullptr); - update_config(upd, config, atomic_load(&config_generation)); - - // wait for animation trigger to be ready (input should be the same) - int cond_ret = trigger_ctx.init_cond.timedwait([&]() { - return atomic_load(&trigger_ctx.ready); - }, COND_ANIMATION_TRIGGER_INIT_TIMEOUT_MS); - if (cond_ret == ETIMEDOUT) { - BONGOCAT_LOG_ERROR("Failed to initialize input monitoring: waiting for animation thread to start in time"); + // trigger animation + bool animation_triggered = false; + if (update_rate_ms > 0) { + LockGuard guard(upd.update_lock); + assert(upd.shm); + auto& update_shm = *upd.shm; + + // read-only config + assert(upd._local_copy_config); + const config::config_t& current_config = *upd._local_copy_config; + + update_shm.latest_snapshot = &get_latest_snapshot_unlocked(upd); + + if (update_shm.cpu_snapshots.stored >= 2) { + const size_t latest = + (update_shm.cpu_snapshots.head + CpuSnapshotRingBufferMaxHistory - 1) % CpuSnapshotRingBufferMaxHistory; + const size_t prev_i = (latest + CpuSnapshotRingBufferMaxHistory - 1) % CpuSnapshotRingBufferMaxHistory; + + const cpu_snapshot_t& curr = update_shm.cpu_snapshots.history[latest]; + const cpu_snapshot_t& prev = update_shm.cpu_snapshots.history[prev_i]; + + update_shm.avg_cpu_usage = compute_avg_cpu_usage(prev, curr); + update_shm.max_cpu_usage = compute_max_cpu_usage(prev, curr); + + const bool above_threshold = current_config.cpu_threshold >= ENABLED_MIN_CPU_PERCENT && + (update_shm.avg_cpu_usage >= current_config.cpu_threshold || + update_shm.max_cpu_usage >= current_config.cpu_threshold); + const bool lower_threshold = current_config.cpu_threshold >= ENABLED_MIN_CPU_PERCENT && + (update_shm.last_avg_cpu_usage >= current_config.cpu_threshold || + update_shm.last_max_cpu_usage >= current_config.cpu_threshold) && + (update_shm.avg_cpu_usage < current_config.cpu_threshold || + update_shm.max_cpu_usage < current_config.cpu_threshold); + const bool crossed_delta = + fabs(update_shm.avg_cpu_usage - update_shm.last_avg_cpu_usage) >= TRIGGER_ANIMATION_CPU_DIFF_PERCENT || + fabs(update_shm.max_cpu_usage - update_shm.last_max_cpu_usage) >= TRIGGER_ANIMATION_CPU_DIFF_PERCENT; + if (above_threshold || lower_threshold) { + if (!update_shm.cpu_active || crossed_delta) { + BONGOCAT_LOG_VERBOSE("update: avg. CPU: %.2f (max: %.2f)", update_shm.avg_cpu_usage, + update_shm.max_cpu_usage); + animation::trigger(animation_ctx, animation::trigger_animation_cause_mask_t::CpuUpdate); + if (lower_threshold) { + animation::trigger(animation_ctx, animation::trigger_animation_cause_mask_t::IdleUpdate); + } + animation_triggered = true; + } + update_shm.cpu_active = true; } else { - assert(trigger_ctx._update == &upd); - } - // set extern/global references - { - // guard for anim_update_state - LockGuard guard (trigger_ctx.anim.anim_lock); - trigger_ctx._update = &upd; - } - trigger_ctx.init_cond.notify_all(); - upd._config = &config; - upd._configs_reloaded_cond = &configs_reloaded_cond; - upd._config_generation = &config_generation; - atomic_store(&upd.ready, true); - upd.init_cond.notify_all(); - - upd._configs_reloaded_cond->notify_all(); - - // start update thread - const int result = pthread_create(&upd._update_thread, nullptr, update_thread, &trigger_ctx); - if (result != 0) { - BONGOCAT_LOG_ERROR("Failed to start update thread: %s", strerror(errno)); - return bongocat_error_t::BONGOCAT_ERROR_THREAD; + if (update_shm.cpu_active) { + update_shm.cpu_active = false; + animation::trigger(animation_ctx, animation::trigger_animation_cause_mask_t::CpuUpdate); + animation::trigger(animation_ctx, animation::trigger_animation_cause_mask_t::IdleUpdate); + } } + } - BONGOCAT_LOG_INFO("Update monitoring started"); - return bongocat_error_t::BONGOCAT_SUCCESS; + update_shm.last_avg_cpu_usage = update_shm.avg_cpu_usage; + update_shm.last_max_cpu_usage = update_shm.max_cpu_usage; } - bongocat_error_t restart(update_context_t& upd, animation::animation_session_t& trigger_ctx, const config::config_t& config, CondVariable& configs_reloaded_cond, atomic_uint64_t& config_generation) { - BONGOCAT_LOG_INFO("Restarting update system"); - // Stop current monitoring - if (upd._update_thread) { - BONGOCAT_LOG_DEBUG("Update thread"); - atomic_store(&upd._running, false); - //pthread_cancel(ctx->_update_thread); - if (stop_thread_graceful_or_cancel(upd._update_thread, upd._running) != 0) { - BONGOCAT_LOG_ERROR("Failed to join update thread: %s", strerror(errno)); - } - BONGOCAT_LOG_DEBUG("Update thread terminated"); + // handle update config + if (reload_config) { + assert(upd._config_generation != BONGOCAT_NULLPTR); + assert(upd._configs_reloaded_cond != BONGOCAT_NULLPTR); + assert(upd._config != BONGOCAT_NULLPTR); + + update_config(upd, *upd._config, new_gen); + + // wait for reload config to be done (all configs) + const int rc = upd._configs_reloaded_cond->timedwait( + [&] { return atomic_load(upd._config_generation) >= new_gen; }, COND_RELOAD_CONFIGS_TIMEOUT_MS); + if (rc == ETIMEDOUT) { + BONGOCAT_LOG_WARNING("update: Timed out waiting for reload eventfd: %s", strerror(errno)); + } + if constexpr (features::Debug) { + if (atomic_load(&upd.config_seen_generation) < atomic_load(upd._config_generation)) { + BONGOCAT_LOG_VERBOSE("update: update.config_seen_generation < update._config_generation; %d < %d", + atomic_load(&upd.config_seen_generation), atomic_load(upd._config_generation)); } + } + // assert(atomic_load(&upd.config_seen_generation) >= atomic_load(upd._config_generation)); + atomic_store(&upd.config_seen_generation, atomic_load(upd._config_generation)); + BONGOCAT_LOG_INFO("update: Update config reloaded (gen=%u)", new_gen); + } - /// @TODO: re-create context with create(), avoid duplicate code - { - LockGuard guard (upd.update_lock); - // Start new monitoring (reuse shared memory if it exists) - if (upd.shm == nullptr) { - upd.shm = make_allocated_mmap(); - if (upd.shm.ptr == MAP_FAILED) { - BONGOCAT_LOG_ERROR("Failed to create shared memory for update thread: %s", strerror(errno)); - return bongocat_error_t::BONGOCAT_ERROR_MEMORY; - } - } - } + if (update_rate_ms > 0) { + // sleep + timespec ts; + ts.tv_sec = 0; + assert(fps > 0); + if (animation_triggered && animation_speed_ms > 0) { + // when animation were triggered, wait for animation to end (assumption), before triggering a possible new + // animation + ts.tv_nsec = animation_speed_ms * 1000 * 1000; + } else if (animation_triggered) { + ts.tv_nsec = (1000L / fps) * 1000 * 1000; + } else { + ts.tv_nsec = timeout * 1000 * 1000; + } + while (ts.tv_nsec >= 1000000000LL) { + ts.tv_nsec -= 1000000000LL; + ts.tv_sec += 1; + } + nanosleep(&ts, BONGOCAT_NULLPTR); + { + LockGuard guard(upd.update_lock); + assert(upd.shm); + auto& update_shm = *upd.shm; + update_shm.cpu_active = false; + } + } else { + if (update_rate_ms == 0 || cpu_threshold < ENABLED_MIN_CPU_PERCENT) { + atomic_store(&upd._running, false); + BONGOCAT_LOG_WARNING("update: Stop update thread, not needed"); + } + } + } - if (!upd._local_copy_config) { - upd._local_copy_config = make_unallocated_mmap_value(config); - if (!upd._local_copy_config) { - BONGOCAT_LOG_ERROR("Failed to create shared memory for update: %s", strerror(errno)); - return bongocat_error_t::BONGOCAT_ERROR_MEMORY; - } - } - assert(upd._local_copy_config != nullptr); - update_config(upd, config, atomic_load(&config_generation)); - - if (upd.update_config_efd._fd < 0) { - upd.update_config_efd = platform::FileDescriptor(eventfd(0, EFD_NONBLOCK | EFD_CLOEXEC)); - if (upd.update_config_efd._fd < 0) { - BONGOCAT_LOG_ERROR("Failed to create notify pipe for update, update config: %s", strerror(errno)); - return bongocat_error_t::BONGOCAT_ERROR_FILE_IO; - } - } + atomic_store(&upd._running, false); - // re-open files - /// @TODO: healthcheck of fd before re-start + // Will run only on normal return + pthread_cleanup_pop(1); // 1 = call cleanup even if not canceled - if (upd.fd_present._fd < 0) { - upd.fd_present = FileDescriptor(open(FILENAME_CPU_PRESET, O_RDONLY)); - if (upd.fd_present._fd < 0) { - BONGOCAT_LOG_ERROR("Failed to open cpu_present"); - return bongocat_error_t::BONGOCAT_ERROR_FILE_IO; - } - set_nonblocking(upd.fd_present._fd); - } - if (upd.fd_stat._fd < 0) { - upd.fd_stat = FileDescriptor(open(FILENAME_PROC_STAT, O_RDONLY)); - if (upd.fd_stat._fd < 0) { - BONGOCAT_LOG_ERROR("Failed to open proc stat"); - return bongocat_error_t::BONGOCAT_ERROR_FILE_IO; - } - set_nonblocking(upd.fd_stat._fd); - } + BONGOCAT_LOG_INFO("Update monitoring stopped"); - //if (trigger_ctx._update != ctx._update) { - // BONGOCAT_LOG_DEBUG("Update context in animation differs from animation trigger update context"); - //} + return BONGOCAT_NULLPTR; +} - // set extern/global references - { - // guard for anim_update_state - LockGuard guard (trigger_ctx.anim.anim_lock); - trigger_ctx._update = &upd; - } - upd._config = &config; - upd._configs_reloaded_cond = &configs_reloaded_cond; - upd._config_generation = &config_generation; - atomic_store(&upd.config_seen_generation, atomic_load(&config_generation)); - upd.init_cond.notify_all(); - upd._configs_reloaded_cond->notify_all(); - - // start update monitoring - const int result = pthread_create(&upd._update_thread, nullptr, update_thread, &trigger_ctx); - if (result != 0) { - BONGOCAT_LOG_ERROR("Failed to start update thread: %s", strerror(errno)); - return bongocat_error_t::BONGOCAT_ERROR_THREAD; - } +created_result_t> create(const config::config_t& config) { + AllocatedMemory ret = make_allocated_memory(); + // assert(ret != nullptr); + if (!ret) [[unlikely]] { + return bongocat_error_t::BONGOCAT_ERROR_MEMORY; + } + + ret->_running = false; + + BONGOCAT_LOG_INFO("Initializing update monitoring system"); + + // Initialize shared memory + ret->shm = make_allocated_mmap(); + if (!ret->shm) [[unlikely]] { + BONGOCAT_LOG_ERROR("Failed to create shared memory for update monitoring: %s", strerror(errno)); + return bongocat_error_t::BONGOCAT_ERROR_MEMORY; + } + + // Initialize shared memory for local config + ret->_local_copy_config = make_allocated_mmap(); + if (!ret->_local_copy_config) [[unlikely]] { + BONGOCAT_LOG_ERROR("Failed to create shared memory for update monitoring: %s", strerror(errno)); + return bongocat_error_t::BONGOCAT_ERROR_MEMORY; + } + assert(ret->_local_copy_config); + *ret->_local_copy_config = config; + + ret->update_config_efd = platform::FileDescriptor(eventfd(0, EFD_NONBLOCK | EFD_CLOEXEC)); + if (ret->update_config_efd._fd < 0) [[unlikely]] { + BONGOCAT_LOG_ERROR("Failed to create notify pipe for update update config: %s", strerror(errno)); + return bongocat_error_t::BONGOCAT_ERROR_FILE_IO; + } + + // open files + ret->fd_present = FileDescriptor(open(FILENAME_CPU_PRESET, O_RDONLY)); + if (ret->fd_present._fd < 0) { + BONGOCAT_LOG_ERROR("Failed to open cpu_present"); + return bongocat_error_t::BONGOCAT_ERROR_FILE_IO; + } + set_nonblocking(ret->fd_present._fd); + + ret->fd_stat = FileDescriptor(open(FILENAME_PROC_STAT, O_RDONLY)); + if (ret->fd_stat._fd < 0) { + BONGOCAT_LOG_ERROR("Failed to open proc stat"); + return bongocat_error_t::BONGOCAT_ERROR_FILE_IO; + } + set_nonblocking(ret->fd_stat._fd); + + BONGOCAT_LOG_INFO("Input monitoring started"); + return ret; +} + +bongocat_error_t start(update_context_t& upd, animation::animation_context_t& animation_ctx, + const config::config_t& config, CondVariable& configs_reloaded_cond, + atomic_uint64_t& config_generation) { + BONGOCAT_LOG_INFO("Initializing update monitoring"); + + // Initialize shared memory for key press flag + upd.shm = make_allocated_mmap(); + if (!upd.shm) [[unlikely]] { + BONGOCAT_LOG_ERROR("Failed to create shared memory for input monitoring: %s", strerror(errno)); + return bongocat_error_t::BONGOCAT_ERROR_MEMORY; + } + + // Initialize shared memory for local config + upd._local_copy_config = make_allocated_mmap(); + if (!upd._local_copy_config) [[unlikely]] { + BONGOCAT_LOG_ERROR("Failed to create shared memory for input monitoring: %s", strerror(errno)); + return bongocat_error_t::BONGOCAT_ERROR_MEMORY; + } + assert(upd._local_copy_config); + update_config(upd, config, atomic_load(&config_generation)); + + // wait for animation trigger to be ready (input should be the same) + const int cond_ret = animation_ctx.init_cond.timedwait([&]() { return atomic_load(&animation_ctx.ready); }, + COND_ANIMATION_TRIGGER_INIT_TIMEOUT_MS); + if (cond_ret == ETIMEDOUT) { + BONGOCAT_LOG_ERROR("Failed to initialize input monitoring: waiting for animation thread to start in time"); + } else { + assert(animation_ctx._update == &upd); + } + // set extern/global references + { + // guard for anim_update_state + LockGuard guard(animation_ctx.thread_context.anim_lock); + animation_ctx._update = &upd; + } + animation_ctx.init_cond.notify_all(); + upd._config = &config; + upd._configs_reloaded_cond = &configs_reloaded_cond; + upd._config_generation = &config_generation; + atomic_store(&upd.ready, true); + upd.init_cond.notify_all(); + + upd._configs_reloaded_cond->notify_all(); + + // start update thread + const int result = pthread_create(&upd._update_thread, BONGOCAT_NULLPTR, update_thread, &animation_ctx); + if (result != 0) { + BONGOCAT_LOG_ERROR("Failed to start update thread: %s", strerror(errno)); + return bongocat_error_t::BONGOCAT_ERROR_THREAD; + } + + BONGOCAT_LOG_INFO("Update monitoring started"); + return bongocat_error_t::BONGOCAT_SUCCESS; +} - BONGOCAT_LOG_INFO("Update thread restarted"); - return bongocat_error_t::BONGOCAT_SUCCESS; +bongocat_error_t restart(update_context_t& upd, animation::animation_context_t& animation_ctx, + const config::config_t& config, CondVariable& configs_reloaded_cond, + atomic_uint64_t& config_generation) { + BONGOCAT_LOG_INFO("Restarting update system"); + // Stop current monitoring + if (upd._update_thread) { + BONGOCAT_LOG_DEBUG("Update thread"); + atomic_store(&upd._running, false); + // pthread_cancel(ctx->_update_thread); + if (stop_thread_graceful_or_cancel(upd._update_thread, upd._running) != 0) { + BONGOCAT_LOG_ERROR("Failed to join update thread: %s", strerror(errno)); + } + BONGOCAT_LOG_DEBUG("Update thread terminated"); + } + + /// @TODO: re-create context with create(), avoid duplicate code + { + LockGuard guard(upd.update_lock); + // Start new monitoring (reuse shared memory if it exists) + if (!upd.shm) { + upd.shm = make_allocated_mmap(); + if (upd.shm.ptr == MAP_FAILED) { + BONGOCAT_LOG_ERROR("Failed to create shared memory for update thread: %s", strerror(errno)); + return bongocat_error_t::BONGOCAT_ERROR_MEMORY; + } } + } - void stop(update_context_t& ctx) { - atomic_store(&ctx._running, false); - if (ctx._update_thread) { - BONGOCAT_LOG_DEBUG("Stopping update thread"); - //pthread_cancel(ctx->_update_thread); - if (stop_thread_graceful_or_cancel(ctx._update_thread, ctx._running) != 0) { - BONGOCAT_LOG_ERROR("Failed to join update thread: %s", strerror(errno)); - } - BONGOCAT_LOG_DEBUG("Update thread terminated"); - } - ctx._update_thread = 0; + if (!upd._local_copy_config) { + upd._local_copy_config = make_unallocated_mmap_value(config); + if (!upd._local_copy_config) { + BONGOCAT_LOG_ERROR("Failed to create shared memory for update: %s", strerror(errno)); + return bongocat_error_t::BONGOCAT_ERROR_MEMORY; + } + } + assert(upd._local_copy_config); + update_config(upd, config, atomic_load(&config_generation)); + + if (upd.update_config_efd._fd < 0) { + upd.update_config_efd = platform::FileDescriptor(eventfd(0, EFD_NONBLOCK | EFD_CLOEXEC)); + if (upd.update_config_efd._fd < 0) { + BONGOCAT_LOG_ERROR("Failed to create notify pipe for update, update config: %s", strerror(errno)); + return bongocat_error_t::BONGOCAT_ERROR_FILE_IO; + } + } - ctx._config = nullptr; - ctx._configs_reloaded_cond = nullptr; - ctx._config_generation = nullptr; + // re-open files + /// @TODO: healthcheck of fd before re-start - ctx.config_updated.notify_all(); - atomic_store(&ctx.ready, false); - ctx.init_cond.notify_all(); + if (upd.fd_present._fd < 0) { + upd.fd_present = FileDescriptor(open(FILENAME_CPU_PRESET, O_RDONLY)); + if (upd.fd_present._fd < 0) { + BONGOCAT_LOG_ERROR("Failed to open cpu_present"); + return bongocat_error_t::BONGOCAT_ERROR_FILE_IO; } + set_nonblocking(upd.fd_present._fd); + } + if (upd.fd_stat._fd < 0) { + upd.fd_stat = FileDescriptor(open(FILENAME_PROC_STAT, O_RDONLY)); + if (upd.fd_stat._fd < 0) { + BONGOCAT_LOG_ERROR("Failed to open proc stat"); + return bongocat_error_t::BONGOCAT_ERROR_FILE_IO; + } + set_nonblocking(upd.fd_stat._fd); + } + + // if (trigger_ctx._update != ctx._update) { + // BONGOCAT_LOG_DEBUG("Update context in animation differs from animation trigger update context"); + // } + + // set extern/global references + { + // guard for anim_update_state + LockGuard guard(animation_ctx.thread_context.anim_lock); + animation_ctx._update = &upd; + } + upd._config = &config; + upd._configs_reloaded_cond = &configs_reloaded_cond; + upd._config_generation = &config_generation; + atomic_store(&upd.config_seen_generation, atomic_load(&config_generation)); + upd.init_cond.notify_all(); + upd._configs_reloaded_cond->notify_all(); + + // start update monitoring + const int result = pthread_create(&upd._update_thread, BONGOCAT_NULLPTR, update_thread, &animation_ctx); + if (result != 0) { + BONGOCAT_LOG_ERROR("Failed to start update thread: %s", strerror(errno)); + return bongocat_error_t::BONGOCAT_ERROR_THREAD; + } + + BONGOCAT_LOG_INFO("Update thread restarted"); + return bongocat_error_t::BONGOCAT_SUCCESS; +} - void trigger_update_config(update_context_t& upd, const config::config_t& config, uint64_t config_generation) { - upd._config = &config; - if (write(upd.update_config_efd._fd, &config_generation, sizeof(uint64_t)) >= 0) { - BONGOCAT_LOG_VERBOSE("Write update trigger update config"); - } else { - BONGOCAT_LOG_ERROR("Failed to write to notify pipe in update: %s", strerror(errno)); - } +void stop(update_context_t& ctx) { + atomic_store(&ctx._running, false); + if (ctx._update_thread) { + BONGOCAT_LOG_DEBUG("Stopping update thread"); + // pthread_cancel(ctx->_update_thread); + if (stop_thread_graceful_or_cancel(ctx._update_thread, ctx._running) != 0) { + BONGOCAT_LOG_ERROR("Failed to join update thread: %s", strerror(errno)); } + BONGOCAT_LOG_DEBUG("Update thread terminated"); + } + ctx._update_thread = 0; - void update_config(update_context_t& upd, const config::config_t& config, uint64_t new_gen) { - assert(upd._local_copy_config != nullptr); + ctx._config = BONGOCAT_NULLPTR; + ctx._configs_reloaded_cond = BONGOCAT_NULLPTR; + ctx._config_generation = BONGOCAT_NULLPTR; - *upd._local_copy_config = config; + ctx.config_updated.notify_all(); + atomic_store(&ctx.ready, false); + ctx.init_cond.notify_all(); +} - atomic_store(&upd.config_seen_generation, new_gen); - // Signal main that reload is done - upd.config_updated.notify_all(); - } +void trigger_update_config(update_context_t& upd, const config::config_t& config, uint64_t config_generation) { + upd._config = &config; + if (write(upd.update_config_efd._fd, &config_generation, sizeof(uint64_t)) >= 0) { + BONGOCAT_LOG_VERBOSE("Write update trigger update config"); + } else { + BONGOCAT_LOG_ERROR("Failed to write to notify pipe in update: %s", strerror(errno)); + } +} + +void update_config(update_context_t& upd, const config::config_t& config, uint64_t new_gen) { + assert(upd._local_copy_config); + + *upd._local_copy_config = config; + + atomic_store(&upd.config_seen_generation, new_gen); + // Signal main that reload is done + upd.config_updated.notify_all(); } +} // namespace bongocat::platform::update diff --git a/src/platform/wayland.cpp b/src/platform/wayland.cpp index dfa58646..9c9a3282 100644 --- a/src/platform/wayland.cpp +++ b/src/platform/wayland.cpp @@ -1,762 +1,555 @@ -#include "platform/wayland-protocols.hpp" +#include "platform/wayland.h" +#include "../graphics/bar.h" #include "graphics/animation.h" -#include "platform/wayland.h" +#include "platform/wayland-protocols.hpp" +#include "platform/wayland_context.h" #include "platform/wayland_shared_memory.h" -#include "platform/global_wayland_session.h" #include "utils/memory.h" -#include "../graphics/bar.h" +#include "wayland_hyprland.h" + #include -#include -#include -#include #include #include -#include +#include #include +#include +#include +#include +#include #include +#include #include #include -#include -#include -#include -#include #include - -#include "wayland_hyprland.h" -//#include "wayland_sway.h" +#include +#include +// #include "wayland_sway.h" #include "platform/wayland_callbacks.h" +#include "platform/wayland_setups.h" namespace bongocat::platform::wayland { - // ============================================================================= - // GLOBAL STATE AND CONFIGURATION - // ============================================================================= - - static inline constexpr int CREATE_SHM_MAX_ATTEMPTS = 100; - static inline constexpr time_ms_t CHECK_INTERVAL_MS = 100; - static inline constexpr time_ms_t POOL_MIN_TIMEOUT_MS = 5; - static inline constexpr time_ms_t POOL_MAX_TIMEOUT_MS = 1000; - static_assert(POOL_MAX_TIMEOUT_MS >= POOL_MIN_TIMEOUT_MS); - - inline static constexpr time_ms_t COND_INIT_TIMEOUT_MS = 5000; - - static inline constexpr auto WAYLAND_LAYER_NAME = "OVERLAY"; - static inline constexpr auto WAYLAND_LAYER_NAMESPACE = "bongocat-overlay"; - - static inline constexpr size_t CREATE_SHM_NAME_SUFFIX_LEN = 8; - static inline constexpr char CREATE_SHM_NAME_TEMPLATE[] = "/bongocat-bar-shm-XXXXXXXX"; - static inline constexpr size_t CREATE_SHM_NAME_PREFIX_LEN = LEN_ARRAY(CREATE_SHM_NAME_TEMPLATE)-1 - CREATE_SHM_NAME_SUFFIX_LEN; - static_assert((CREATE_SHM_NAME_PREFIX_LEN + CREATE_SHM_NAME_SUFFIX_LEN) == LEN_ARRAY(CREATE_SHM_NAME_TEMPLATE)-1); - - // ============================================================================= - // BUFFER AND DRAWING MANAGEMENT - // ============================================================================= - - FileDescriptor create_shm(off_t size) { - char* name = strdup(CREATE_SHM_NAME_TEMPLATE); - constexpr char charset_arr[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; - constexpr size_t charset_len = sizeof(charset_arr) - 1; - int fd = -1; - - random_xoshiro128 rng (slow_rand()); - for (int i = 0; i < CREATE_SHM_MAX_ATTEMPTS; i++) { - for (size_t j = 0; j < CREATE_SHM_NAME_SUFFIX_LEN; j++) { - assert(sizeof(charset_arr) - 1 > 0); - name[CREATE_SHM_NAME_PREFIX_LEN + j] = charset_arr[rng.range(0, charset_len-1)]; - } - fd = shm_open(name, O_RDWR | O_CREAT | O_EXCL, 0600); - if (fd >= 0) { - shm_unlink(name); - break; - } - } - - if (fd < 0 || ftruncate(fd, size) < 0) { - close(fd); - fd = -1; - perror("shm"); - } - - ::free(name); - return FileDescriptor(fd); +// ============================================================================= +// GLOBAL STATE AND CONFIGURATION +// ============================================================================= + +static inline constexpr time_ms_t CHECK_INTERVAL_MS = 100; +static inline constexpr time_ms_t POOL_MIN_TIMEOUT_MS = 5; +static inline constexpr time_ms_t POOL_MAX_TIMEOUT_MS = 1000; +static_assert(POOL_MAX_TIMEOUT_MS >= POOL_MIN_TIMEOUT_MS); + +inline static constexpr time_ms_t COND_INIT_TIMEOUT_MS = 5000; + +static inline constexpr auto WAYLAND_LAYER_NAME = "OVERLAY"; + +// ============================================================================= +// MAIN WAYLAND INTERFACE IMPLEMENTATION +// ============================================================================= + +created_result_t> create(animation::animation_context_t& animation_ctx, + const config::config_t& config) { + AllocatedMemory ret = make_allocated_memory(); + assert(ret); + if (!ret) [[unlikely]] { + return bongocat_error_t::BONGOCAT_ERROR_MEMORY; + } + + ret->animation_context = &animation_ctx; + ret->thread_context._bar_height = DEFAULT_BAR_HEIGHT; + + // Initialize shared memory + ret->thread_context.ctx_shm = make_allocated_mmap(); + if (!ret->thread_context.ctx_shm) [[unlikely]] { + BONGOCAT_LOG_ERROR("Failed to create shared memory for animation system: %s", strerror(errno)); + return bongocat_error_t::BONGOCAT_ERROR_MEMORY; + } + if (ret->thread_context.ctx_shm) [[unlikely]] { + static_assert(WAYLAND_NUM_BUFFERS <= INT_MAX); + for (size_t i = 0; i < WAYLAND_NUM_BUFFERS; i++) { + ret->thread_context.ctx_shm->buffers[i] = {}; } + atomic_store(&ret->thread_context.ctx_shm->configured, false); + } + + // Initialize shared memory for local config + ret->thread_context._local_copy_config = make_allocated_mmap(); + if (!ret->thread_context._local_copy_config) [[unlikely]] { + BONGOCAT_LOG_ERROR("Failed to create shared memory for animation system: %s", strerror(errno)); + return bongocat_error_t::BONGOCAT_ERROR_MEMORY; + } + assert(ret->thread_context._local_copy_config); + *ret->thread_context._local_copy_config = config; + ret->thread_context._bar_height = config.overlay_height; + + return ret; +} - // ============================================================================= - // MAIN WAYLAND INTERFACE IMPLEMENTATION - // ============================================================================= - - static bongocat_error_t wayland_setup_protocols(wayland_session_t& ctx) { - wayland_context_t& wayland_ctx = ctx.wayland_context; - //animation_context_t& anim = *ctx.animation_context; - //animation_trigger_context_t& trigger_ctx = *ctx.animation_trigger_context; - - // read-only config - assert(wayland_ctx._local_copy_config != nullptr); - const config::config_t& current_config = *wayland_ctx._local_copy_config; - - /// @TODO: add RAII wrapper for wl_registry - wl_registry *registry = wl_display_get_registry(wayland_ctx.display); - if (!registry) { - BONGOCAT_LOG_ERROR("Failed to get Wayland registry"); - return bongocat_error_t::BONGOCAT_ERROR_WAYLAND; - } - - wl_registry_add_listener(registry, &details::reg_listener, &ctx); - wl_display_roundtrip(wayland_ctx.display); - - if (ctx.xdg_output_manager) { - for (size_t i = 0; i < ctx.output_count && i < MAX_OUTPUTS; i++) { - ctx.outputs[i].xdg_output = zxdg_output_manager_v1_get_xdg_output(ctx.xdg_output_manager, ctx.outputs[i].wl_output); - zxdg_output_v1_add_listener(ctx.outputs[i].xdg_output, &details::xdg_output_listener, &ctx.outputs[i]); - ctx.screen_infos[i] = {}; - assert(ctx.outputs[i].wl_output); - ctx.screen_infos[i].wl_output = ctx.outputs[i].wl_output; - } - - // Wait for all xdg_output events - wl_display_roundtrip(wayland_ctx.display); // Process initial events - wl_display_roundtrip(wayland_ctx.display); // Ensure all `done` events arrive - BONGOCAT_LOG_DEBUG("Listener bound for xdg_output and foreign toplevel handle"); - - // DE specific inits - hyprland::update_outputs_with_monitor_ids(ctx); - } - - wayland_ctx.output = nullptr; - if (current_config.output_name) { - for (size_t i = 0; i < ctx.output_count; ++i) { - if (static_cast(ctx.outputs[i].received) & static_cast(output_ref_received_flags_t::Name) && - strcmp(ctx.outputs[i].name_str, current_config.output_name) == 0) { - wayland_ctx.output = ctx.outputs[i].wl_output; - wayland_ctx._output_name_str = ctx.outputs[i].name_str; - wayland_ctx._screen_info = &ctx.screen_infos[i]; - BONGOCAT_LOG_INFO("Matched output: %s", wayland_ctx._output_name_str); - break; - } - } - - if (!wayland_ctx.output) { - if (current_config._strict) { - BONGOCAT_LOG_ERROR("Could not find output named '%s'", current_config.output_name); - wl_registry_destroy(registry); - return bongocat_error_t::BONGOCAT_ERROR_INVALID_PARAM; - } else { - BONGOCAT_LOG_ERROR("Could not find output named '%s', defaulting to first output", current_config.output_name); - } - } - } - - // Fallback - if (!wayland_ctx.output && ctx.output_count > 0) { - wayland_ctx.output = ctx.outputs[0].wl_output; - wayland_ctx._output_name_str = ctx.outputs[0].name_str; - wayland_ctx._screen_info = &ctx.screen_infos[0]; - BONGOCAT_LOG_WARNING("Falling back to first output: %s", wayland_ctx._output_name_str); - } - - if (!wayland_ctx.compositor || !wayland_ctx.shm || !wayland_ctx.layer_shell) { - BONGOCAT_LOG_ERROR("Missing required Wayland protocols"); - wl_registry_destroy(registry); - return bongocat_error_t::BONGOCAT_ERROR_WAYLAND; - } - - // Configure screen dimensions - int screen_width {DEFAULT_SCREEN_WIDTH}; - if (current_config.screen_width > 0) { - BONGOCAT_LOG_WARNING("Use screen width from config: %d", current_config.screen_width); - screen_width = current_config.screen_width; - } else { - // auto-detect screen width - if (wayland_ctx.output) { - wl_display_roundtrip(wayland_ctx.display); - if (wayland_ctx._screen_info && wayland_ctx._screen_info->screen_width > 0) { - BONGOCAT_LOG_INFO("Detected screen width: %d", wayland_ctx._screen_info->screen_width); - screen_width = wayland_ctx._screen_info->screen_width; - } else { - BONGOCAT_LOG_WARNING("Using default screen width: %d", DEFAULT_SCREEN_WIDTH); - screen_width = DEFAULT_SCREEN_WIDTH; - } - } else { - BONGOCAT_LOG_WARNING("No output found, using default screen width: %d", DEFAULT_SCREEN_WIDTH); - screen_width = DEFAULT_SCREEN_WIDTH; - if (current_config._strict) { - wl_registry_destroy(registry); - return bongocat_error_t::BONGOCAT_ERROR_INVALID_PARAM; - } - } - - } - wayland_ctx._screen_width = screen_width; +bongocat_error_t setup(wayland_context_t& ctx, animation::animation_context_t& animation_ctx) { + ctx.animation_context = &animation_ctx; + + if (!ctx.thread_context.ctx_shm) [[unlikely]] { + BONGOCAT_LOG_ERROR("Failed to create shared memory for animation system: %s", strerror(errno)); + return bongocat_error_t::BONGOCAT_ERROR_MEMORY; + } + if (!ctx.thread_context._local_copy_config) [[unlikely]] { + BONGOCAT_LOG_ERROR("Failed to create shared memory for animation system: %s", strerror(errno)); + return bongocat_error_t::BONGOCAT_ERROR_MEMORY; + } + + BONGOCAT_LOG_INFO("Initializing Wayland connection"); + + ctx.thread_context.display = wl_display_connect(BONGOCAT_NULLPTR); + if (ctx.thread_context.display == BONGOCAT_NULLPTR) { + BONGOCAT_LOG_ERROR("Failed to connect to Wayland display"); + return bongocat_error_t::BONGOCAT_ERROR_WAYLAND; + } + + bongocat_error_t result = details::wayland_setup_protocols(ctx); + if (result != bongocat_error_t::BONGOCAT_SUCCESS) { + return result; + } + result = details::wayland_setup_surface(ctx); + if (result != bongocat_error_t::BONGOCAT_SUCCESS) { + return result; + } + result = details::wayland_setup_buffer(ctx.thread_context, *ctx.animation_context); + if (result != bongocat_error_t::BONGOCAT_SUCCESS) { + return result; + } + + atomic_store(&ctx.ready, true); + + BONGOCAT_LOG_INFO("Wayland initialization complete (%dx%d buffer)", ctx.thread_context._screen_width, + ctx.thread_context._bar_height); + return bongocat_error_t::BONGOCAT_SUCCESS; +} - wl_registry_destroy(registry); - return bongocat_error_t::BONGOCAT_SUCCESS; +bongocat_error_t run(wayland_context_t& ctx, volatile sig_atomic_t& running, int signal_fd, + input::input_context_t& input, const config::config_t& config, + const config::config_watcher_t *config_watcher, config_reload_callback_t config_reload_callback) { + BONGOCAT_CHECK_NULL(config_reload_callback, bongocat_error_t::BONGOCAT_ERROR_INVALID_PARAM); + BONGOCAT_CHECK_NULL(ctx.animation_context, bongocat_error_t::BONGOCAT_ERROR_INVALID_PARAM); + + // from thread context + wayland_thread_context& wayland_ctx = ctx.thread_context; + // animation_context_t& anim = trigger_ctx.anim; + // wait for context + ctx.animation_context->init_cond.timedwait([&]() { return atomic_load(&ctx.animation_context->ready); }, + COND_INIT_TIMEOUT_MS); + input.init_cond.timedwait([&]() { return atomic_load(&ctx.animation_context->ready); }, COND_INIT_TIMEOUT_MS); + animation::animation_context_t& animation_ctx = *ctx.animation_context; + assert(animation_ctx._input != BONGOCAT_NULLPTR); + assert(animation_ctx._input == &input); + // wayland_shared_memory_t *wayland_ctx_shm = wayland_ctx.ctx_shm; + + BONGOCAT_LOG_INFO("Starting Wayland event loop"); + + running = 1; + while (running >= 1 && wayland_ctx.display != BONGOCAT_NULLPTR) { + const time_ms_t frame_based_timeout = config.fps > 0 ? 1000 / config.fps : 0; + // Periodic fullscreen check for fallback fullscreen detection + timeval now{}; + gettimeofday(&now, BONGOCAT_NULLPTR); + const time_ms_t elapsed_ms = ((now.tv_sec - ctx.fs_detector.last_check.tv_sec) * 1000L) + + ((now.tv_usec - ctx.fs_detector.last_check.tv_usec) / 1000L); + time_ms_t fullscreen_check_interval = frame_based_timeout; + if (fullscreen_check_interval < CHECK_INTERVAL_MS) { + fullscreen_check_interval = CHECK_INTERVAL_MS; + } + if (elapsed_ms >= fullscreen_check_interval) { + details::fs_update_state_fallback(ctx); + ctx.fs_detector.last_check = now; } - static bongocat_error_t wayland_setup_surface(wayland_session_t& ctx) { - wayland_context_t& wayland_ctx = ctx.wayland_context; - //animation_context_t& anim = *ctx.animation_context; - //animation_trigger_context_t& trigger_ctx = *ctx.animation_trigger_context; - - // read-only config - assert(wayland_ctx._local_copy_config != nullptr); - const config::config_t& current_config = *wayland_ctx._local_copy_config; - - wayland_ctx.surface = wl_compositor_create_surface(wayland_ctx.compositor); - if (!wayland_ctx.surface) { - BONGOCAT_LOG_ERROR("Failed to create surface"); - return bongocat_error_t::BONGOCAT_ERROR_WAYLAND; - } - - zwlr_layer_shell_v1_layer layer = ZWLR_LAYER_SHELL_V1_LAYER_OVERLAY; - switch (current_config.layer) { - case config::layer_type_t::LAYER_BACKGROUND: - layer = ZWLR_LAYER_SHELL_V1_LAYER_BACKGROUND; - break; - case config::layer_type_t::LAYER_BOTTOM: - layer = ZWLR_LAYER_SHELL_V1_LAYER_BOTTOM; - break; - case config::layer_type_t::LAYER_TOP: - layer = ZWLR_LAYER_SHELL_V1_LAYER_TOP; - break; - case config::layer_type_t::LAYER_OVERLAY: - layer = ZWLR_LAYER_SHELL_V1_LAYER_OVERLAY; - break; - } - wayland_ctx.layer_surface = zwlr_layer_shell_v1_get_layer_surface(wayland_ctx.layer_shell, wayland_ctx.surface, wayland_ctx.output, - layer, WAYLAND_LAYER_NAMESPACE); - - if (!wayland_ctx.layer_surface) { - BONGOCAT_LOG_ERROR("Failed to create layer surface"); - return bongocat_error_t::BONGOCAT_ERROR_WAYLAND; - } - - // Configure layer surface - uint32_t anchor = ZWLR_LAYER_SURFACE_V1_ANCHOR_LEFT | ZWLR_LAYER_SURFACE_V1_ANCHOR_RIGHT; - switch (current_config.overlay_position) { - case config::overlay_position_t::POSITION_TOP: - anchor |= ZWLR_LAYER_SURFACE_V1_ANCHOR_TOP; - break; - case config::overlay_position_t::POSITION_BOTTOM: - anchor |= ZWLR_LAYER_SURFACE_V1_ANCHOR_BOTTOM; - break; - default: - BONGOCAT_LOG_ERROR("Invalid overlay_position %d for layer surface, set to top (default)"); - anchor |= ZWLR_LAYER_SURFACE_V1_ANCHOR_TOP; - break; - } + // Handle Wayland events + constexpr size_t fds_signals_index = 0; + constexpr size_t fds_config_reload_index = 1; + constexpr size_t fds_animation_render_index = 2; + constexpr size_t fds_wayland_index = 3; + constexpr nfds_t fds_count = 4; + pollfd fds[fds_count] = { + {.fd = signal_fd, .events = POLLIN, .revents = 0}, + {.fd = config_watcher != BONGOCAT_NULLPTR ? config_watcher->reload_efd._fd : -1, + .events = POLLIN, + .revents = 0 }, + {.fd = animation_ctx.render_efd._fd, .events = POLLIN, .revents = 0}, + {.fd = wl_display_get_fd(wayland_ctx.display), .events = POLLIN, .revents = 0}, + }; + static_assert(fds_count == LEN_ARRAY(fds)); + + // compute desired timeout + time_ms_t timeout_ms = frame_based_timeout; + if (timeout_ms < POOL_MIN_TIMEOUT_MS) { + timeout_ms = POOL_MIN_TIMEOUT_MS; + } + if (timeout_ms > POOL_MAX_TIMEOUT_MS) { + timeout_ms = POOL_MAX_TIMEOUT_MS; + } - assert(wayland_ctx._bar_height >= 0); - //assert(current_config.bar_height <= UINT32_MAX); - zwlr_layer_surface_v1_set_anchor(wayland_ctx.layer_surface, anchor); - zwlr_layer_surface_v1_set_size(wayland_ctx.layer_surface, 0, static_cast(wayland_ctx._bar_height)); - zwlr_layer_surface_v1_set_exclusive_zone(wayland_ctx.layer_surface, -1); - zwlr_layer_surface_v1_set_keyboard_interactivity(wayland_ctx.layer_surface, - ZWLR_LAYER_SURFACE_V1_KEYBOARD_INTERACTIVITY_NONE); - zwlr_layer_surface_v1_add_listener(wayland_ctx.layer_surface, &details::layer_listener, &ctx); - - // Make surface click-through - wl_region *input_region = wl_compositor_create_region(wayland_ctx.compositor); - if (input_region) { - wl_surface_set_input_region(wayland_ctx.surface, input_region); - wl_region_destroy(input_region); - } + // avoid reloading twice, by signal OR watcher + bool config_reload_requested = false; + bool render_requested = false; + bool needs_flush = false; + bool toggle_visibility_requested = false; + + bool prepared_read = false; + { + int attempts = 0; + while (wl_display_prepare_read(wayland_ctx.display) != 0 && attempts < MAX_ATTEMPTS) { + wl_display_dispatch_pending(wayland_ctx.display); + attempts++; + } + prepared_read = attempts < MAX_ATTEMPTS; + } - wl_surface_commit(wayland_ctx.surface); - if constexpr (WAYLAND_NUM_BUFFERS == 1) { - wl_display_roundtrip(wayland_ctx.display); - } - return bongocat_error_t::BONGOCAT_SUCCESS; + if (prepared_read) { + // Try to flush queued requests to the compositor so it can process them and send replies. + // If flush would block (EAGAIN), cancel the prepared read and dispatch pending events to make progress. + const int flush_ret = wl_display_flush(wayland_ctx.display); + if (flush_ret == -1 && errno == EAGAIN) { + // send buffer full; need to make progress by reading pending events first + wl_display_cancel_read(wayland_ctx.display); + if (wl_display_dispatch_pending(wayland_ctx.display) == -1) { + BONGOCAT_LOG_ERROR("wl_display_dispatch_pending failed after EAGAIN"); + running = 0; + } + } else if (flush_ret == -1) { + BONGOCAT_LOG_ERROR("wl_display_flush failed: %s", strerror(errno)); + wl_display_cancel_read(wayland_ctx.display); + if (wl_display_dispatch_pending(wayland_ctx.display) == -1) { + running = 0; + } + } } - static bongocat_error_t wayland_setup_buffer(wayland_context_t& wayland_context, animation::animation_session_t& anim) { - // read-only config - assert(wayland_context._local_copy_config != nullptr); - //const config::config_t& current_config = *wayland_context._local_copy_config; - - wayland_shared_memory_t& wayland_ctx_shm = *wayland_context.ctx_shm; - - /// @TODO: limit screen_width and bar_height for buffer_size - const int32_t buffer_width = wayland_context._screen_width; - const int32_t buffer_height = wayland_context._bar_height; - assert(buffer_width >= 0); - assert(buffer_height >= 0); - assert(RGBA_CHANNELS >= 0); - const size_t buffer_size = static_cast(buffer_width) * static_cast(buffer_height) * RGBA_CHANNELS; - if (buffer_size <= 0) { - BONGOCAT_LOG_ERROR("Invalid buffer size: %d", buffer_size); + assert(timeout_ms <= INT_MAX); + const int poll_result = poll(fds, fds_count, static_cast(timeout_ms)); + if (poll_result > 0) { + // signal events + if (fds[fds_signals_index].revents & POLLIN) { + signalfd_siginfo fdsi{}; + const ssize_t s = read(fds[fds_signals_index].fd, &fdsi, sizeof(fdsi)); + if (s != sizeof(fdsi)) { + BONGOCAT_LOG_ERROR("Failed to read signal fd"); + } else { + switch (fdsi.ssi_signo) { + case SIGINT: + case SIGTERM: + case SIGQUIT: // Handle Ctrl+\ for graceful shutdown + case SIGHUP: // Handle terminal hangup + BONGOCAT_LOG_INFO("Received signal %d, shutting down gracefully", fdsi.ssi_signo); + running = 0; + break; + case SIGCHLD: + // Handle child process termination - reap zombies + while (waitpid(-1, BONGOCAT_NULLPTR, WNOHANG) > 0) {} + break; + case SIGUSR1: + BONGOCAT_LOG_INFO("Received SIGUSR1, toggle bar visibility"); + toggle_visibility_requested = true; + break; + case SIGUSR2: + BONGOCAT_LOG_INFO("Received SIGUSR2, reloading config"); + config_reload_requested = true; + break; + default: + BONGOCAT_LOG_WARNING("Received unexpected signal %d", fdsi.ssi_signo); + break; + } + } + } + if (!running) { + // draining pools + for (size_t i = 0; i < fds_count; i++) { + platform::drain_event(fds[i], MAX_ATTEMPTS); + } + if (prepared_read) { + wl_display_cancel_read(wayland_ctx.display); + } + render_requested = false; + toggle_visibility_requested = false; + break; + } + + // reload config event + if (fds[fds_config_reload_index].revents & POLLIN) { + BONGOCAT_LOG_DEBUG("Receive reload event"); + if (config_watcher != BONGOCAT_NULLPTR) { + platform::drain_event(fds[fds_config_reload_index], MAX_ATTEMPTS, "update config eventfd"); + } + config_reload_requested = true; + } + + // render event + if (fds[fds_animation_render_index].revents & POLLIN) { + BONGOCAT_LOG_VERBOSE("Receive render event"); + platform::drain_event(fds[fds_animation_render_index], MAX_ATTEMPTS, "render eventfd"); + if (atomic_load(&wayland_ctx.ctx_shm->configured)) { + render_requested = true; + } + } + + // wayland events + if (prepared_read) { + if (fds[fds_wayland_index].revents & POLLIN) { + if (wl_display_read_events(wayland_ctx.display) == -1 || + wl_display_dispatch_pending(wayland_ctx.display) == -1) { + BONGOCAT_LOG_ERROR("Failed to handle Wayland events: %s", strerror(errno)); + running = 0; return bongocat_error_t::BONGOCAT_ERROR_WAYLAND; + } + } else { + wl_display_cancel_read(wayland_ctx.display); } - - static_assert(WAYLAND_NUM_BUFFERS > 0); - static_assert(WAYLAND_NUM_BUFFERS <= INT32_MAX); - if (buffer_size > INT32_MAX / static_cast(WAYLAND_NUM_BUFFERS)) { - BONGOCAT_LOG_ERROR("Buffer size too large for SHM pool offset"); + } else { + if (fds[fds_wayland_index].revents & POLLIN) { + if (wl_display_dispatch_pending(wayland_ctx.display) == -1) { + BONGOCAT_LOG_ERROR("Failed to dispatch pending events: %s", strerror(errno)); + running = 0; return bongocat_error_t::BONGOCAT_ERROR_WAYLAND; - } - assert(buffer_size <= INT32_MAX / static_cast(WAYLAND_NUM_BUFFERS)); - const size_t total_size = buffer_size * WAYLAND_NUM_BUFFERS; - - assert(total_size <= INT32_MAX); - FileDescriptor fd = create_shm(static_cast(total_size)); - if (fd._fd < 0) { - return bongocat_error_t::BONGOCAT_ERROR_FILE_IO; - } - - wl_shm_pool *pool = wl_shm_create_pool(wayland_context.shm, fd._fd, static_cast(total_size)); - if (!pool) { - BONGOCAT_LOG_ERROR("Failed to create shared memory pool"); + } + } else { + // dispatch any events already read + if (wl_display_dispatch_pending(wayland_ctx.display) == -1) { + BONGOCAT_LOG_ERROR("Failed to dispatch pending Wayland events: %s", strerror(errno)); + running = 0; return bongocat_error_t::BONGOCAT_ERROR_WAYLAND; - } - - static_assert(WAYLAND_NUM_BUFFERS > 0); - static_assert(WAYLAND_NUM_BUFFERS <= INT32_MAX); - for (size_t i = 0; i < WAYLAND_NUM_BUFFERS; i++) { - //assert(buffer_size >= 0); - assert(i <= INT32_MAX); - assert(buffer_size <= INT32_MAX); - assert(buffer_size <= static_cast(INT32_MAX)); - assert(buffer_size <= static_cast(INT32_MAX) / WAYLAND_NUM_BUFFERS); - const off_t offset = static_cast(i) * static_cast(buffer_size); - - assert(static_cast(buffer_size) <= SIZE_MAX); - wayland_ctx_shm.buffers[i].pixels = make_allocated_mmap_file_buffer_value(0, buffer_size, fd._fd, offset); - if (wayland_ctx_shm.buffers[i].pixels == nullptr) { - BONGOCAT_LOG_ERROR("Failed to map shared memory: %s", strerror(errno)); - for (size_t j = 0; j < i; j++) { - cleanup_shm_buffer(wayland_ctx_shm.buffers[j]); - } - wl_shm_pool_destroy(pool); - return bongocat_error_t::BONGOCAT_ERROR_MEMORY; - } - - //assert(buffer_size >= 0); - assert(i <= INT32_MAX); - assert(buffer_size <= INT32_MAX); - assert(offset <= INT32_MAX); - wayland_ctx_shm.buffers[i].buffer = wl_shm_pool_create_buffer(pool, static_cast(offset), wayland_context._screen_width, - wayland_context._bar_height, - wayland_context._screen_width * RGBA_CHANNELS, - WL_SHM_FORMAT_ARGB8888); - if (wayland_ctx_shm.buffers[i].buffer == nullptr) { - BONGOCAT_LOG_ERROR("Failed to create buffer"); - for (size_t j = 0; j < i; j++) { - cleanup_shm_buffer(wayland_ctx_shm.buffers[j]); - } - wl_shm_pool_destroy(pool); - return bongocat_error_t::BONGOCAT_ERROR_WAYLAND; - } - - // created buffer successfully, set other properties - assert(i <= INT_MAX); - wayland_ctx_shm.buffers[i].index = i; - atomic_store(&wayland_ctx_shm.buffers[i].busy, false); - atomic_store(&wayland_ctx_shm.buffers[i].pending, false); - wayland_ctx_shm.buffers[i]._animation_trigger_context = &anim; - wayland_ctx_shm.buffers[i]._wayland_context = &wayland_context; - wl_buffer_add_listener(wayland_ctx_shm.buffers[i].buffer, &details::buffer_listener, &wayland_ctx_shm.buffers[i]); - } - - wl_shm_pool_destroy(pool); - - wayland_ctx_shm.current_buffer_index = 0; - - return bongocat_error_t::BONGOCAT_SUCCESS; + } + } + } + + if (render_requested) { + if (!atomic_load(&wayland_ctx.ctx_shm->configured)) { + BONGOCAT_LOG_VERBOSE("Surface not configured yet, skip drawing"); + render_requested = false; + } + } + + BONGOCAT_LOG_VERBOSE("Poll revents: poll_result=%d; signal=%x, reload=%x, render=%x, wayland=%x", poll_result, + fds[fds_signals_index].revents, fds[fds_config_reload_index].revents, + fds[fds_animation_render_index].revents, fds[fds_wayland_index].revents); + } else if (poll_result == 0) { + if (prepared_read) { + wl_display_cancel_read(wayland_ctx.display); + } + if (wl_display_dispatch_pending(wayland_ctx.display) == -1) { + BONGOCAT_LOG_ERROR("Failed to dispatch pending events"); + running = 0; + return bongocat_error_t::BONGOCAT_ERROR_WAYLAND; + } + } else { + if (prepared_read) { + wl_display_cancel_read(wayland_ctx.display); + } + if (errno != EINTR) { + BONGOCAT_LOG_ERROR("Poll error: %s", strerror(errno)); + running = 0; + return bongocat_error_t::BONGOCAT_ERROR_WAYLAND; + } } - created_result_t> create(animation::animation_session_t& anim, const config::config_t& config) { - AllocatedMemory ret = make_allocated_memory(); - assert(ret != nullptr); - if (ret == nullptr) { - return bongocat_error_t::BONGOCAT_ERROR_MEMORY; - } - - ret->animation_trigger_context = &anim; - ret->wayland_context._bar_height = DEFAULT_BAR_HEIGHT; - - // Initialize shared memory - ret->wayland_context.ctx_shm = make_allocated_mmap(); - if (ret->wayland_context.ctx_shm == nullptr) { - BONGOCAT_LOG_ERROR("Failed to create shared memory for animation system: %s", strerror(errno)); - return bongocat_error_t::BONGOCAT_ERROR_MEMORY; - } - if (ret->wayland_context.ctx_shm != nullptr) { - static_assert(WAYLAND_NUM_BUFFERS <= INT_MAX); - for (size_t i = 0;i < WAYLAND_NUM_BUFFERS;i++) { - ret->wayland_context.ctx_shm->buffers[i] = {}; - } - atomic_store(&ret->wayland_context.ctx_shm->configured, false); - } - - // Initialize shared memory for local config - ret->wayland_context._local_copy_config = make_allocated_mmap(); - if (ret->wayland_context._local_copy_config == nullptr) { - BONGOCAT_LOG_ERROR("Failed to create shared memory for animation system: %s", strerror(errno)); - return bongocat_error_t::BONGOCAT_ERROR_MEMORY; - } - assert(ret->wayland_context._local_copy_config != nullptr); - *ret->wayland_context._local_copy_config = config; - ret->wayland_context._bar_height = config.overlay_height; - - return ret; + // do reload once + if (config_reload_requested && config_reload_callback) { + config_reload_callback(); + render_requested = true; + } + if (toggle_visibility_requested) { + wayland_ctx.bar_visibility = + wayland_ctx.bar_visibility == bar_visibility_t::Show ? bar_visibility_t::Hide : bar_visibility_t::Show; + render_requested = true; } - bongocat_error_t setup(wayland_session_t& ctx, animation::animation_session_t& anim) { - ctx.animation_trigger_context = &anim; - - if (ctx.wayland_context.ctx_shm == nullptr) { - BONGOCAT_LOG_ERROR("Failed to create shared memory for animation system: %s", strerror(errno)); - return bongocat_error_t::BONGOCAT_ERROR_MEMORY; - } - if (!ctx.wayland_context._local_copy_config) { - BONGOCAT_LOG_ERROR("Failed to create shared memory for animation system: %s", strerror(errno)); - return bongocat_error_t::BONGOCAT_ERROR_MEMORY; - } - - BONGOCAT_LOG_INFO("Initializing Wayland connection"); - - ctx.wayland_context.display = wl_display_connect(nullptr); - if (!ctx.wayland_context.display) { - BONGOCAT_LOG_ERROR("Failed to connect to Wayland display"); - return bongocat_error_t::BONGOCAT_ERROR_WAYLAND; - } - - - bongocat_error_t result = wayland_setup_protocols(ctx); - if (result != bongocat_error_t::BONGOCAT_SUCCESS) { - return result; - } - result = wayland_setup_surface(ctx); - if (result != bongocat_error_t::BONGOCAT_SUCCESS) { - return result; + /// @TODO: release buffer fallback after timeout, fallback + /* + const auto now_ms = platform::get_current_time_ms(); + if (now_ms - wayland_ctx._last_frame_timestamp_ms > 500 && + all_buffers_busy(wayland_ctx.ctx_shm.ptr)) + { + for (auto& buf : wayland_ctx.ctx_shm->buffers) { + atomic_store(&buf.busy, false); } - result = wayland_setup_buffer(ctx.wayland_context, *ctx.animation_trigger_context); - if (result != bongocat_error_t::BONGOCAT_SUCCESS) { - return result; - } - - atomic_store(&ctx.ready, true); - - BONGOCAT_LOG_INFO("Wayland initialization complete (%dx%d buffer)", - ctx.wayland_context._screen_width, - ctx.wayland_context._bar_height); - return bongocat_error_t::BONGOCAT_SUCCESS; + BONGOCAT_LOG_WARNING("Missed frame_done fallback: forcibly releasing stuck buffers"); } - - bongocat_error_t run(wayland_session_t& ctx, volatile sig_atomic_t& running, int signal_fd, input::input_context_t& input, const config::config_t& config, const config::config_watcher_t* config_watcher, config_reload_callback_t config_reload_callback) { - BONGOCAT_CHECK_NULL(config_reload_callback, bongocat_error_t::BONGOCAT_ERROR_INVALID_PARAM); - BONGOCAT_CHECK_NULL(ctx.animation_trigger_context, bongocat_error_t::BONGOCAT_ERROR_INVALID_PARAM); - - // from thread context - wayland_context_t& wayland_ctx = ctx.wayland_context; - //animation_context_t& anim = trigger_ctx.anim; - // wait for context - ctx.animation_trigger_context->init_cond.timedwait([&]() { - return atomic_load(&ctx.animation_trigger_context->ready); - }, COND_INIT_TIMEOUT_MS); - input.init_cond.timedwait([&]() { - return atomic_load(&ctx.animation_trigger_context->ready); - }, COND_INIT_TIMEOUT_MS); - animation::animation_session_t& trigger_ctx = *ctx.animation_trigger_context; - assert(trigger_ctx._input != nullptr); - assert(trigger_ctx._input == &input); - //wayland_shared_memory_t *wayland_ctx_shm = wayland_ctx.ctx_shm; - - BONGOCAT_LOG_INFO("Starting Wayland event loop"); - - running = 1; - while (running && wayland_ctx.display) { - const time_ms_t frame_based_timeout = config.fps > 0 ? 1000 / config.fps : 0; - // Periodic fullscreen check for fallback fullscreen detection - timeval now{}; - gettimeofday(&now, nullptr); - const time_ms_t elapsed_ms = (now.tv_sec - ctx.fs_detector.last_check.tv_sec) * 1000L + (now.tv_usec - ctx.fs_detector.last_check.tv_usec) / 1000L; - time_ms_t fullscreen_check_interval = frame_based_timeout; - if (fullscreen_check_interval < CHECK_INTERVAL_MS) fullscreen_check_interval = CHECK_INTERVAL_MS; - if (elapsed_ms >= fullscreen_check_interval) { - details::fs_update_state_fallback(ctx); - ctx.fs_detector.last_check = now; - } - - // Handle Wayland events - constexpr size_t fds_signals_index = 0; - constexpr size_t fds_config_reload_index = 1; - constexpr size_t fds_animation_render_index = 2; - constexpr size_t fds_wayland_index = 3; - constexpr nfds_t fds_count = 4; - pollfd fds[fds_count] = { - { .fd = signal_fd, .events = POLLIN, .revents = 0 }, - { .fd = config_watcher ? config_watcher->reload_efd._fd : -1, .events = POLLIN, .revents = 0 }, - { .fd = trigger_ctx.render_efd._fd, .events = POLLIN, .revents = 0 }, - { .fd = wl_display_get_fd(wayland_ctx.display), .events = POLLIN, .revents = 0 }, - }; - static_assert(fds_count == LEN_ARRAY(fds)); - - // compute desired timeout - time_ms_t timeout_ms = frame_based_timeout; - if (timeout_ms < POOL_MIN_TIMEOUT_MS) timeout_ms = POOL_MIN_TIMEOUT_MS; - if (timeout_ms > POOL_MAX_TIMEOUT_MS) timeout_ms = POOL_MAX_TIMEOUT_MS; - - // avoid reloading twice, by signal OR watcher - bool config_reload_requested = false; - bool render_requested = false; - bool needs_flush = false; - bool toggle_visibility_requested = false; - - bool prepared_read = false; - { - int attempts = 0; - while (wl_display_prepare_read(wayland_ctx.display) != 0 && attempts < MAX_ATTEMPTS) { - wl_display_dispatch_pending(wayland_ctx.display); - attempts++; - } - prepared_read = attempts < MAX_ATTEMPTS; - } - - - if (prepared_read) { - // Try to flush queued requests to the compositor so it can process them and send replies. - // If flush would block (EAGAIN), cancel the prepared read and dispatch pending events to make progress. - const int flush_ret = wl_display_flush(wayland_ctx.display); - if (flush_ret == -1 && errno == EAGAIN) { - // send buffer full; need to make progress by reading pending events first - wl_display_cancel_read(wayland_ctx.display); - if (wl_display_dispatch_pending(wayland_ctx.display) == -1) { - BONGOCAT_LOG_ERROR("wl_display_dispatch_pending failed after EAGAIN"); - running = 0; - } - } else if (flush_ret == -1) { - BONGOCAT_LOG_ERROR("wl_display_flush failed: %s", strerror(errno)); - wl_display_cancel_read(wayland_ctx.display); - if (wl_display_dispatch_pending(wayland_ctx.display) == -1) { - running = 0; - } - } - } - - assert(timeout_ms <= INT_MAX); - const int poll_result = poll(fds, fds_count, static_cast(timeout_ms)); - if (poll_result > 0) { - // signal events - if (fds[fds_signals_index].revents & POLLIN) { - signalfd_siginfo fdsi{}; - ssize_t s = read(fds[fds_signals_index].fd, &fdsi, sizeof(fdsi)); - if (s != sizeof(fdsi)) { - BONGOCAT_LOG_ERROR("Failed to read signal fd"); - } else { - switch (fdsi.ssi_signo) { - case SIGINT: - case SIGTERM: - BONGOCAT_LOG_INFO("Received signal %d, shutting down gracefully", fdsi.ssi_signo); - running = 0; - break; - case SIGCHLD: - while (waitpid(-1, nullptr, WNOHANG) > 0){} - break; - case SIGUSR1: - BONGOCAT_LOG_INFO("Received SIGUSR1, toggle bar visibility"); - toggle_visibility_requested = true; - break; - case SIGUSR2: - BONGOCAT_LOG_INFO("Received SIGUSR2, reloading config"); - config_reload_requested = true; - break; - default: - BONGOCAT_LOG_WARNING("Received unexpected signal %d", fdsi.ssi_signo); - break; - } - } - } - if (!running) { - // draining pools - for (size_t i = 0; i < fds_count; i++) { - platform::drain_event(fds[i], MAX_ATTEMPTS); - } - if (prepared_read) wl_display_cancel_read(wayland_ctx.display); - render_requested = false; - toggle_visibility_requested = false; - break; - } - - // reload config event - if (fds[fds_config_reload_index].revents & POLLIN) { - BONGOCAT_LOG_DEBUG("Receive reload event"); - if (config_watcher) { - platform::drain_event(fds[fds_config_reload_index], MAX_ATTEMPTS, "update config eventfd"); - } - config_reload_requested = true; - } - - // render event - if (fds[fds_animation_render_index].revents & POLLIN) { - BONGOCAT_LOG_VERBOSE("Receive render event"); - platform::drain_event(fds[fds_animation_render_index], MAX_ATTEMPTS, "render eventfd"); - if (atomic_load(&wayland_ctx.ctx_shm->configured)) { - render_requested = true; - } - } - - // wayland events - if (prepared_read) { - if (fds[fds_wayland_index].revents & POLLIN) { - if (wl_display_read_events(wayland_ctx.display) == -1 || - wl_display_dispatch_pending(wayland_ctx.display) == -1) { - BONGOCAT_LOG_ERROR("Failed to handle Wayland events: %s", strerror(errno)); - running = 0; - return bongocat_error_t::BONGOCAT_ERROR_WAYLAND; - } - } else { - wl_display_cancel_read(wayland_ctx.display); - } - } else { - if (fds[fds_wayland_index].revents & POLLIN) { - if (wl_display_dispatch_pending(wayland_ctx.display) == -1) { - BONGOCAT_LOG_ERROR("Failed to dispatch pending events: %s", strerror(errno)); - running = 0; - return bongocat_error_t::BONGOCAT_ERROR_WAYLAND; - } - } else { - // dispatch any events already read - if (wl_display_dispatch_pending(wayland_ctx.display) == -1) { - BONGOCAT_LOG_ERROR("Failed to dispatch pending Wayland events: %s", strerror(errno)); - running = 0; - return bongocat_error_t::BONGOCAT_ERROR_WAYLAND; - } - } - } - - if (render_requested) { - if (!atomic_load(&wayland_ctx.ctx_shm->configured)) { - BONGOCAT_LOG_VERBOSE("Surface not configured yet, skip drawing"); - render_requested = false; - } - } - - BONGOCAT_LOG_VERBOSE("Poll revents: poll_result=%d; signal=%x, reload=%x, render=%x, wayland=%x", - poll_result, - fds[fds_signals_index].revents, - fds[fds_config_reload_index].revents, - fds[fds_animation_render_index].revents, - fds[fds_wayland_index].revents); - } else if (poll_result == 0) { - if (prepared_read) wl_display_cancel_read(wayland_ctx.display); - if (wl_display_dispatch_pending(wayland_ctx.display) == -1) { - BONGOCAT_LOG_ERROR("Failed to dispatch pending events"); - running = 0; - return bongocat_error_t::BONGOCAT_ERROR_WAYLAND; - } - } else { - if (prepared_read) wl_display_cancel_read(wayland_ctx.display); - if (errno != EINTR) { - BONGOCAT_LOG_ERROR("Poll error: %s", strerror(errno)); - running = 0; - return bongocat_error_t::BONGOCAT_ERROR_WAYLAND; - } - } - - // do reload once - if (config_reload_requested && config_reload_callback) { - config_reload_callback(); - render_requested = true; - } - if (toggle_visibility_requested) { - wayland_ctx.bar_visibility = wayland_ctx.bar_visibility == bar_visibility_t::Show ? bar_visibility_t::Hide : bar_visibility_t::Show; - render_requested = true; - } - - /// @TODO: release buffer fallback after timeout, fallback - /* - const auto now_ms = platform::get_current_time_ms(); - if (now_ms - wayland_ctx._last_frame_timestamp_ms > 500 && - all_buffers_busy(wayland_ctx.ctx_shm.ptr)) - { - for (auto& buf : wayland_ctx.ctx_shm->buffers) { - atomic_store(&buf.busy, false); - } - BONGOCAT_LOG_WARNING("Missed frame_done fallback: forcibly releasing stuck buffers"); - } - */ - - if (render_requested) { - BONGOCAT_LOG_VERBOSE("Receive render event"); - BONGOCAT_LOG_VERBOSE("Try to draw_bar in wayland_run"); - - if (!atomic_load(&wayland_ctx._frame_pending)) { - wl_display_dispatch_pending(wayland_ctx.display); - const auto draw_bar_result = animation::draw_bar(ctx); - needs_flush = draw_bar_result == animation::draw_bar_result_t::FlushNeeded; - } else { - if (!atomic_exchange(&wayland_ctx._redraw_after_frame, true)) { - BONGOCAT_LOG_VERBOSE("Queued redraw after frame"); - request_render(trigger_ctx); - } else { - const auto draw_bar_result = animation::draw_bar(ctx); - needs_flush = draw_bar_result == animation::draw_bar_result_t::FlushNeeded; - } - } - render_requested = false; - } - toggle_visibility_requested = false; - - if (needs_flush) { - const int flush_ret = wl_display_flush(wayland_ctx.display); - if (flush_ret == -1 && errno == EAGAIN) { - // send buffer full; need to make progress by reading pending events first - wl_display_cancel_read(wayland_ctx.display); - if (wl_display_dispatch_pending(wayland_ctx.display) == -1) { - BONGOCAT_LOG_ERROR("wl_display_dispatch_pending failed after EAGAIN"); - running = 0; - } - } else if (flush_ret == -1) { - BONGOCAT_LOG_ERROR("wl_display_flush failed: %s", strerror(errno)); - wl_display_cancel_read(wayland_ctx.display); - if (wl_display_dispatch_pending(wayland_ctx.display) == -1) { - running = 0; - } - } - } + */ + + if (render_requested) { + BONGOCAT_LOG_VERBOSE("Receive render event"); + BONGOCAT_LOG_VERBOSE("Try to draw_bar in wayland_run"); + + if (!atomic_load(&wayland_ctx._frame_pending)) { + wl_display_dispatch_pending(wayland_ctx.display); + const auto draw_bar_result = animation::draw_bar(ctx); + needs_flush = draw_bar_result == animation::draw_bar_result_t::FlushNeeded; + } else { + if (!atomic_exchange(&wayland_ctx._redraw_after_frame, true)) { + BONGOCAT_LOG_VERBOSE("Queued redraw after frame"); + request_render(animation_ctx); + } else { + const auto draw_bar_result = animation::draw_bar(ctx); + needs_flush = draw_bar_result == animation::draw_bar_result_t::FlushNeeded; } - running = 0; - - BONGOCAT_LOG_INFO("Wayland event loop exited"); - return bongocat_error_t::BONGOCAT_SUCCESS; + } + render_requested = false; } - - // ============================================================================= - // PUBLIC API IMPLEMENTATION - // ============================================================================= - - int get_screen_width(const wayland_session_t& ctx) { - return (ctx.wayland_context._screen_info) ? ctx.wayland_context._screen_info->screen_width : 0; + toggle_visibility_requested = false; + + if (needs_flush) { + const int flush_ret = wl_display_flush(wayland_ctx.display); + if (flush_ret == -1 && errno == EAGAIN) { + // send buffer full; need to make progress by reading pending events first + wl_display_cancel_read(wayland_ctx.display); + if (wl_display_dispatch_pending(wayland_ctx.display) == -1) { + BONGOCAT_LOG_ERROR("wl_display_dispatch_pending failed after EAGAIN"); + running = 0; + } + } else if (flush_ret == -1) { + BONGOCAT_LOG_ERROR("wl_display_flush failed: %s", strerror(errno)); + wl_display_cancel_read(wayland_ctx.display); + if (wl_display_dispatch_pending(wayland_ctx.display) == -1) { + running = 0; + } + } } + } + running = 0; - void update_config(wayland_context_t& ctx, const config::config_t& config, animation::animation_session_t& trigger_ctx) { - assert(ctx._local_copy_config != nullptr && ctx._local_copy_config.ptr != MAP_FAILED); + BONGOCAT_LOG_INFO("Wayland event loop exited"); + return bongocat_error_t::BONGOCAT_SUCCESS; +} - *ctx._local_copy_config = config; +// ============================================================================= +// PUBLIC API IMPLEMENTATION +// ============================================================================= - /// @NOTE: assume animation has the same local copy as wayland config - //animation_update_config(anim, config); - if (atomic_load(&ctx.ctx_shm->configured)) { - request_render(trigger_ctx); - } - } +int get_screen_width(const wayland_context_t& ctx) { + return ctx.thread_context._screen_info != BONGOCAT_NULLPTR ? ctx.thread_context._screen_info->screen_width : 0; +} - const char* get_current_layer_name() { - return WAYLAND_LAYER_NAME; +void update_config(wayland_context_t& ctx, const config::config_t& config, + animation::animation_context_t& animation_ctx) { + assert(ctx.thread_context._local_copy_config); + + // Check if dimensions changed - requires buffer/surface recreation + const auto old_height = + ctx.thread_context._local_copy_config ? ctx.thread_context._local_copy_config->overlay_height : 0; + const auto old_width = ctx.thread_context._screen_width; + char *old_screen_name = ctx.thread_context._output_name_str != BONGOCAT_NULLPTR + ? strdup(ctx.thread_context._output_name_str) + : BONGOCAT_NULLPTR; + + // update old config + *ctx.thread_context._local_copy_config = config; + + const bool dimensions_changed = (old_height != ctx.thread_context._local_copy_config->overlay_height) || + (ctx.thread_context._local_copy_config->screen_width > 0 && + old_width != ctx.thread_context._local_copy_config->screen_width); + const bool change_screen = + ctx.thread_context._local_copy_config->output_name != BONGOCAT_NULLPTR && + (strcmp(old_screen_name, ctx.thread_context._local_copy_config->output_name) != 0 || + strcmp(ctx.thread_context._output_name_str, ctx.thread_context._local_copy_config->output_name) != 0); + + if (((dimensions_changed && old_height > 0 && old_width > 0) || change_screen) && ctx.thread_context.ctx_shm) { + // ~~Lock animation mutex to prevent draw_bar() during config update + // This is critical - animation thread must not access buffer while we + // recreate it~~ + // not sure if this is needed, animation thread don't touch the bar directly, + // it just triggers a rerender event + platform::LockGuard anim_guard(animation_ctx.thread_context.anim_lock); + + BONGOCAT_LOG_INFO("Dimensions changed (%dx%d -> %dx%d), recreating buffer...", old_width, old_height, + ctx.thread_context._local_copy_config->screen_width, + ctx.thread_context._local_copy_config->overlay_height); + + // Mark as not configured first + assert(ctx.thread_context.ctx_shm); + atomic_store(&ctx.thread_context.ctx_shm->configured, false); + + if (details::wayland_update_screen_info(ctx) != bongocat_error_t::BONGOCAT_SUCCESS) { + if (old_screen_name != BONGOCAT_NULLPTR) { + ::free(old_screen_name); + old_screen_name = BONGOCAT_NULLPTR; + } + BONGOCAT_LOG_ERROR("Failed to update width for bar"); + return; } + ctx.thread_context._bar_height = config.overlay_height; + + if (ctx.thread_context._screen_width > 0 && ctx.thread_context._bar_height > 0) { + // Cleanup old buffer + cleanup_wayland_context_buffer(ctx.thread_context); + + // Cleanup old surface + cleanup_wayland_context_surface(ctx.thread_context); + + if (details::wayland_update_screen_info(ctx) != bongocat_error_t::BONGOCAT_SUCCESS) { + if (old_screen_name != BONGOCAT_NULLPTR) { + ::free(old_screen_name); + old_screen_name = BONGOCAT_NULLPTR; + } + BONGOCAT_LOG_ERROR("Failed to update width for bar"); + return; + } + ctx.thread_context._bar_height = config.overlay_height; + + // Recreate surface and buffer with new dimensions + if (details::wayland_setup_surface(ctx) != bongocat_error_t::BONGOCAT_SUCCESS) { + if (old_screen_name != BONGOCAT_NULLPTR) { + ::free(old_screen_name); + old_screen_name = BONGOCAT_NULLPTR; + } + BONGOCAT_LOG_ERROR("Failed to recreate surface after config change"); + return; + } + if (details::wayland_setup_buffer(ctx.thread_context, animation_ctx) != bongocat_error_t::BONGOCAT_SUCCESS) { + if (old_screen_name != BONGOCAT_NULLPTR) { + ::free(old_screen_name); + old_screen_name = BONGOCAT_NULLPTR; + } + BONGOCAT_LOG_ERROR("Failed to recreate buffer after config change"); + return; + } + + atomic_store(&ctx.thread_context.ctx_shm->configured, true); + + // Wait for new configure event + wl_display_roundtrip(ctx.thread_context.display); + + BONGOCAT_LOG_INFO("Buffer recreated successfully (%dx%d)", ctx.thread_context._local_copy_config->screen_width, + ctx.thread_context._local_copy_config->overlay_height); + } else { + BONGOCAT_LOG_ERROR("Buffer recreated failed (%dx%d)", ctx.thread_context._local_copy_config->screen_width, + ctx.thread_context._local_copy_config->overlay_height); + } + } + + if (old_screen_name != nullptr) { + ::free(old_screen_name); + old_screen_name = nullptr; + } + + /// @NOTE: assume animation has the same local copy as wayland config + // animation_update_config(anim, config); + if (atomic_load(&ctx.thread_context.ctx_shm->configured)) { + request_render(animation_ctx); + } +} - bongocat_error_t request_render(animation::animation_session_t& trigger_ctx) { - if (trigger_ctx.render_efd._fd < 0) { - return bongocat_error_t::BONGOCAT_ERROR_INVALID_PARAM; - } +const char *get_current_layer_name() { + return WAYLAND_LAYER_NAME; +} - constexpr uint64_t u = 1; - const ssize_t s = write(trigger_ctx.render_efd._fd, &u, sizeof(u)); - if (s != sizeof(u)) { - BONGOCAT_LOG_WARNING("Failed to write render eventfd: %s", strerror(errno)); - return bongocat_error_t::BONGOCAT_ERROR_FILE_IO; - } +bongocat_error_t request_render(animation::animation_context_t& animation_ctx) { + if (animation_ctx.render_efd._fd < 0) { + return bongocat_error_t::BONGOCAT_ERROR_INVALID_PARAM; + } - return bongocat_error_t::BONGOCAT_SUCCESS; - } + constexpr uint64_t u = 1; + const ssize_t s = write(animation_ctx.render_efd._fd, &u, sizeof(u)); + if (s != sizeof(u)) { + BONGOCAT_LOG_WARNING("Failed to write render eventfd: %s", strerror(errno)); + return bongocat_error_t::BONGOCAT_ERROR_FILE_IO; + } + + return bongocat_error_t::BONGOCAT_SUCCESS; } +} // namespace bongocat::platform::wayland diff --git a/src/platform/wayland_callbacks.cpp b/src/platform/wayland_callbacks.cpp index e11c042f..1091e9f2 100644 --- a/src/platform/wayland_callbacks.cpp +++ b/src/platform/wayland_callbacks.cpp @@ -1,768 +1,929 @@ -#include "platform/wayland-protocols.hpp" +#include "platform/wayland_callbacks.h" +#include "../graphics/bar.h" #include "graphics/animation.h" +#include "platform/wayland-protocols.hpp" #include "platform/wayland.h" +#include "platform/wayland_context.h" +#include "platform/wayland_setups.h" #include "platform/wayland_shared_memory.h" -#include "platform/global_wayland_session.h" -#include "platform/wayland_callbacks.h" #include "utils/memory.h" +#include "wayland_hyprland.h" +#include "wayland_sway.h" + #include -#include -#include -#include -#include #include -#include -#include +#include +#include +#include #include -#include #include +#include +#include #include - -#include "wayland_hyprland.h" -#include "wayland_sway.h" -#include "../graphics/bar.h" +#include +#include namespace bongocat::platform::wayland::details { #ifdef __cplusplus -#define wl_array_for_each_typed(pos, array, type) \ -for (type *pos = reinterpret_cast((array)->data); \ - reinterpret_cast(pos) < (reinterpret_cast((array)->data) + (array)->size); \ - ++pos) +# define wl_array_for_each_typed(pos, array, type) \ + for (type *pos = reinterpret_cast((array)->data); \ + reinterpret_cast(pos) < (reinterpret_cast((array)->data) + (array)->size); ++pos) #endif - // ============================================================================= - // ZXDG LISTENER IMPLEMENTATION - // ============================================================================= - - void handle_xdg_output_name(void *data, [[maybe_unused]] zxdg_output_v1 *xdg_output, - const char *name) { - if (!data) { - BONGOCAT_LOG_VERBOSE("Handler called with null data (ignored)"); - return; - } - auto *oref = static_cast(data); - - snprintf(oref->name_str, sizeof(oref->name_str), "%s", name); - oref->received = static_cast(static_cast(oref->received) | static_cast( - output_ref_received_flags_t::Name)); - - BONGOCAT_LOG_DEBUG("xdg_output.name: xdg-output name received: %s", name); - } - - void handle_xdg_output_logical_position(void *data, [[maybe_unused]] zxdg_output_v1 *xdg_output, int32_t x, int32_t y) { - if (!data) { - BONGOCAT_LOG_VERBOSE("Handler called with null data (ignored)"); - return; - } - auto *oref = static_cast(data); - - oref->x = x; - oref->y = y; - oref->received = static_cast(static_cast(oref->received) | static_cast( - output_ref_received_flags_t::LogicalPosition)); - - BONGOCAT_LOG_VERBOSE("xdg_output.logical_position: %d,%d received", x, y); - } - void handle_xdg_output_logical_size(void *data, [[maybe_unused]] zxdg_output_v1 *xdg_output, - int32_t width, int32_t height) { - if (!data) { - BONGOCAT_LOG_VERBOSE("Handler called with null data (ignored)"); - return; - } - auto *oref = static_cast(data); - - oref->width = width; - oref->height = height; - oref->received = static_cast(static_cast(oref->received) | static_cast( - output_ref_received_flags_t::LogicalSize)); - - BONGOCAT_LOG_VERBOSE("xdg_output.logical_size: %dx%d received", width, height); - } - void handle_xdg_output_done(void *data, [[maybe_unused]] zxdg_output_v1 *xdg_output) { - if (!data) { - BONGOCAT_LOG_VERBOSE("Handler called with null data (ignored)"); - return; - } - //auto *oref = static_cast(data); - - BONGOCAT_LOG_VERBOSE("xdg_output.done: done received"); - } - - void handle_xdg_output_description(void *data, [[maybe_unused]] zxdg_output_v1 *xdg_output, [[maybe_unused]] const char *description) { - if (!data) { - BONGOCAT_LOG_VERBOSE("Handler called with null data (ignored)"); - return; - } - //auto *oref = static_cast(data); - - BONGOCAT_LOG_VERBOSE("xdg_output.description: description received"); - } +// ============================================================================= +// ZXDG LISTENER IMPLEMENTATION +// ============================================================================= + +void handle_xdg_output_name(void *data, [[maybe_unused]] zxdg_output_v1 *xdg_output, const char *name) { + if (data == BONGOCAT_NULLPTR || name == BONGOCAT_NULLPTR) { + BONGOCAT_LOG_VERBOSE("Handler called with null data (ignored)"); + return; + } + auto *oref = static_cast(data); + + /// name received + { + snprintf(oref->name_str, sizeof(oref->name_str), "%s", name); + oref->received = flag_add(oref->received, output_ref_received_flags_t::Name); + BONGOCAT_LOG_DEBUG("xdg_output.name: xdg-output name received: %s", name); + } + + // @NOTE: this should always be set ? + // assert(oref->wayland); + + /// Reconnection handling + if (oref->wayland != BONGOCAT_NULLPTR) { + wayland_thread_context& wayland_ctx = oref->wayland->thread_context; + // animation_context_t& anim = *ctx.animation_context; + // animation_trigger_context_t& trigger_ctx = *ctx.animation_trigger_context; + + // read-only config + // assert(wayland_ctx._local_copy_config != nullptr); + // const config::config_t& current_config = *wayland_ctx._local_copy_config; + + // Check if this is the output we're waiting for (reconnection case) + if (!atomic_load(&oref->wayland->_output_lost)) { + return; + } + + bool should_reconnect = false; + // Case 1: User specified an output name - match exactly + if (wayland_ctx.using_named_output && wayland_ctx._output_name_str != BONGOCAT_NULLPTR) { + should_reconnect = (strcmp(name, wayland_ctx._output_name_str) == 0); + } + // Case 2: Using fallback (first output) - reconnect to any output + else if (!wayland_ctx.using_named_output) { + should_reconnect = true; + BONGOCAT_LOG_DEBUG("Using fallback output, accepting '%s'", name); + } + + if (should_reconnect) { + BONGOCAT_LOG_INFO("Target output '%s' reconnected!", name); + + // Clean up old surface if it exists + cleanup_wayland_context_surface(wayland_ctx); + + // Set new output + wayland_ctx.output = oref->wl_output; + wayland_ctx.bound_output_name = oref->name; + atomic_store(&oref->wayland->_output_lost, false); + + // Recreate surface on new output + // Note: wayland_setup_surface already commits, triggering a configure + // event. The layer_surface_configure callback will ack and call draw_bar() + // to render. + if (wayland_setup_surface(*oref->wayland) == bongocat_error_t::BONGOCAT_SUCCESS) { + assert(wayland_ctx.ctx_shm); + if (wayland_ctx.ctx_shm) { + atomic_store(&wayland_ctx.ctx_shm->configured, true); + } + if constexpr (WAYLAND_NUM_BUFFERS != 1) { + // Wait for configure event to be processed + wl_display_roundtrip(wayland_ctx.display); + } + if (oref->wayland->animation_context != BONGOCAT_NULLPTR) { + request_render(*oref->wayland->animation_context); + } + BONGOCAT_LOG_INFO("Surface recreated, configure event processed"); + } else { + BONGOCAT_LOG_ERROR("Failed to recreate surface on reconnected output"); + } + } + } +} +void handle_xdg_output_logical_position(void *data, [[maybe_unused]] zxdg_output_v1 *xdg_output, int32_t x, int32_t y) { + if (data == BONGOCAT_NULLPTR) { + BONGOCAT_LOG_VERBOSE("Handler called with null data (ignored)"); + return; + } + auto *oref = static_cast(data); - // ============================================================================= - // FULLSCREEN DETECTION IMPLEMENTATION - // ============================================================================= + oref->x = x; + oref->y = y; + oref->received = flag_add(oref->received, output_ref_received_flags_t::LogicalPosition); - static bool fs_update_state(wayland_session_t& ctx, bool new_state) { - if (!atomic_load(&ctx.ready)) { - BONGOCAT_LOG_VERBOSE("Wayland configured yet, skipping update"); - return false; - } - if (!ctx.animation_trigger_context) { - BONGOCAT_LOG_VERBOSE("Wayland not configured yet"); - return false; - } + BONGOCAT_LOG_VERBOSE("xdg_output.logical_position: %d,%d received", x, y); +} +void handle_xdg_output_logical_size(void *data, [[maybe_unused]] zxdg_output_v1 *xdg_output, int32_t width, + int32_t height) { + if (data == BONGOCAT_NULLPTR) { + BONGOCAT_LOG_VERBOSE("Handler called with null data (ignored)"); + return; + } + auto *oref = static_cast(data); + + oref->width = width; + oref->height = height; + oref->received = flag_add(oref->received, output_ref_received_flags_t::LogicalSize); + + BONGOCAT_LOG_VERBOSE("xdg_output.logical_size: %dx%d received", width, height); +} +void handle_xdg_output_done(void *data, [[maybe_unused]] zxdg_output_v1 *xdg_output) { + if (data == BONGOCAT_NULLPTR) { + BONGOCAT_LOG_VERBOSE("Handler called with null data (ignored)"); + return; + } + // auto *oref = static_cast(data); + + BONGOCAT_LOG_VERBOSE("xdg_output.done: done received"); +} - //const wayland_shared_memory_t& wayland_ctx_shm = *ctx.wayland_context.ctx_shm; +void handle_xdg_output_description(void *data, [[maybe_unused]] zxdg_output_v1 *xdg_output, + [[maybe_unused]] const char *description) { + if (data == BONGOCAT_NULLPTR) { + BONGOCAT_LOG_VERBOSE("Handler called with null data (ignored)"); + return; + } + // auto *oref = static_cast(data); - if (new_state != ctx.fs_detector.has_fullscreen_toplevel) { - ctx.fs_detector.has_fullscreen_toplevel = new_state; - ctx.wayland_context._fullscreen_detected = new_state; + BONGOCAT_LOG_VERBOSE("xdg_output.description: description received"); +} - BONGOCAT_LOG_INFO("Fullscreen state changed: %s", - ctx.wayland_context._fullscreen_detected ? "detected" : "cleared"); +// ============================================================================= +// FULLSCREEN DETECTION IMPLEMENTATION +// ============================================================================= - if (ctx.wayland_context.ctx_shm != nullptr && atomic_load(&ctx.wayland_context.ctx_shm->configured)) { - request_render(*ctx.animation_trigger_context); - } else { - BONGOCAT_LOG_VERBOSE("Wayland not configured yet, skipping request rendering"); - } +static bool fs_update_state(wayland_context_t& ctx, bool new_state) { + if (!atomic_load(&ctx.ready)) { + BONGOCAT_LOG_VERBOSE("Wayland configured yet, skipping update"); + return false; + } + if (ctx.animation_context == BONGOCAT_NULLPTR) { + BONGOCAT_LOG_VERBOSE("Wayland not configured yet"); + return false; + } - return true; - } + // const wayland_shared_memory_t& wayland_ctx_shm = *ctx.wayland_context.ctx_shm; - return false; - } + if (new_state != ctx.fs_detector.has_fullscreen_toplevel) { + ctx.fs_detector.has_fullscreen_toplevel = new_state; + ctx.thread_context._fullscreen_detected = new_state; - namespace hyprland { - static int fs_update_state(wayland_session_t& ctx) { - if (wayland::hyprland::window_info_t win; wayland::hyprland::get_active_window(win)) { - bool fullscreen_on_same_output = false; - for (size_t i = 0; i < ctx.output_count; i++) { - if (ctx.outputs[i].hypr_id == win.monitor_id) { - if (ctx.wayland_context.output == ctx.outputs[i].wl_output) { - fullscreen_on_same_output = true; - break; - } - } - } - if (fullscreen_on_same_output) { - details::fs_update_state(ctx, win.fullscreen); - return win.fullscreen ? 1 : 0; - } - - details::fs_update_state(ctx, false); - return 0; - } - - return -1; - } - } + BONGOCAT_LOG_INFO("Fullscreen state changed: %s", ctx.thread_context._fullscreen_detected ? "detected" : "cleared"); - static bool fs_check_compositor_fallback() { - //BONGOCAT_LOG_VERBOSE("Using compositor-specific fullscreen detection"); - - // Try Hyprland first - if (const int result = wayland::hyprland::fs_check_compositor_fallback(); result >= 0) { - return result == 1; - } - - // Try Sway as fallback - if (const int result = wayland::sway::fs_check_compositor_fallback(); result >= 0) { - return result == 1; - } - - BONGOCAT_LOG_DEBUG("No supported compositor found for fullscreen detection"); - return false; + if (ctx.thread_context.ctx_shm && atomic_load(&ctx.thread_context.ctx_shm->configured)) { + request_render(*ctx.animation_context); + } else { + BONGOCAT_LOG_VERBOSE("Wayland not configured yet, skipping request rendering"); } - static bool fs_check_status(wayland_session_t& ctx) { - if (!atomic_load(&ctx.ready)) { - BONGOCAT_LOG_VERBOSE("Wayland configured yet, skipping handling"); - return false; - } + return true; + } - if (ctx.fs_detector.manager) { - return ctx.fs_detector.has_fullscreen_toplevel; - } - - return fs_check_compositor_fallback(); - } - - void fs_update_state_fallback(wayland_session_t& ctx) { - for (size_t i = 0; i < ctx.num_toplevels; ++i) { - const tracked_toplevel_t& tracked = ctx.tracked_toplevels[i]; - // Skip handles that are not mapped or destroyed - if (!tracked.handle) continue; - if (tracked.is_fullscreen) { - // Only update overlay if on our output - if (tracked.output == ctx.wayland_context.output) { - fs_update_state(ctx, true); - return; - } - } - } + return false; +} - const bool new_state = fs_check_status(ctx); - if (new_state != ctx.wayland_context._fullscreen_detected) { - fs_update_state(ctx, new_state); - } - } +namespace hyprland { + static int fs_update_state(wayland_context_t& ctx) { + if (wayland::hyprland::window_info_t win; wayland::hyprland::get_active_window(win)) { + bool fullscreen_on_same_output = false; + for (size_t i = 0; i < ctx.output_count; i++) { + if (ctx.outputs[i].hypr_id == win.monitor_id) { + if (ctx.thread_context.output == ctx.outputs[i].wl_output) { + fullscreen_on_same_output = true; + break; + } + } + } + if (fullscreen_on_same_output) { + details::fs_update_state(ctx, win.fullscreen); + return win.fullscreen ? 1 : 0; + } + + details::fs_update_state(ctx, false); + return 0; + } + + return -1; + } +} // namespace hyprland + +static bool fs_check_compositor_fallback() { + // BONGOCAT_LOG_VERBOSE("Using compositor-specific fullscreen detection"); + + // Try Hyprland first + if (const int result = wayland::hyprland::fs_check_compositor_fallback(); result >= 0) { + return result == 1; + } + + // Try Sway as fallback + if (const int result = wayland::sway::fs_check_compositor_fallback(); result >= 0) { + return result == 1; + } + + BONGOCAT_LOG_DEBUG("No supported compositor found for fullscreen detection"); + return false; +} - struct update_fullscreen_state_toplevel_result_t { bool output_found{false}; bool changed{false}; }; - static update_fullscreen_state_toplevel_result_t update_fullscreen_state_toplevel(wayland_session_t& ctx, tracked_toplevel_t& tracked, bool is_fullscreen) { - bool state_changed = tracked.is_fullscreen != is_fullscreen; - tracked.is_fullscreen = is_fullscreen; - - /// @NOTE: tracked.output can always be NULL when no output.enter/output.leave event were triggert - // Only trigger overlay update if this fullscreen window is on our output - if (tracked.output == ctx.wayland_context.output && state_changed) { - state_changed = fs_update_state(ctx, is_fullscreen); - BONGOCAT_LOG_VERBOSE("Fullscreen state updated for window %p: %d", - static_cast(tracked.handle), - is_fullscreen); - return { .output_found = true, .changed = state_changed }; - } +static bool fs_check_status(wayland_context_t& ctx) { + if (!atomic_load(&ctx.ready)) { + BONGOCAT_LOG_VERBOSE("Wayland configured yet, skipping handling"); + return false; + } - return { .output_found = false, .changed = state_changed }; - } + if (ctx.fs_detector.manager != BONGOCAT_NULLPTR) { + return ctx.fs_detector.has_fullscreen_toplevel; + } - // Foreign toplevel protocol event handlers - void fs_handle_toplevel_state(void *data, [[maybe_unused]] zwlr_foreign_toplevel_handle_v1 *handle, - wl_array *state) { - if (!data) { - BONGOCAT_LOG_VERBOSE("Handler called with null data (ignored)"); - return; - } - wayland_session_t& ctx = *static_cast(data); - if (!atomic_load(&ctx.ready)) { - BONGOCAT_LOG_VERBOSE("Wayland configured yet, skipping handling"); - return; - } - // only check for state changes when everything is ready, no need to do something before like fullscreen check - - // check if fullscreen state event change - bool is_fullscreen = false; - wl_array_for_each_typed(state_ptr, state, uint32_t) { - if (*state_ptr == ZWLR_FOREIGN_TOPLEVEL_HANDLE_V1_STATE_FULLSCREEN) { - is_fullscreen = true; - break; - } - } - - /// @NOTE: tracked.output can always be NULL when no output.enter/output.leave event were triggert - for (size_t i = 0; i < ctx.num_toplevels; ++i) { - if (ctx.tracked_toplevels[i].handle == handle) { - auto [output_found, changed] = update_fullscreen_state_toplevel(ctx, ctx.tracked_toplevels[i], is_fullscreen); - if (output_found) { - if (changed) { - BONGOCAT_LOG_VERBOSE("fs_handle_toplevel.state: Update fullscreen state: %d", is_fullscreen); - } - return; - } - } - } + return fs_check_compositor_fallback(); +} +void fs_update_state_fallback(wayland_context_t& ctx) { + for (size_t i = 0; i < ctx.num_toplevels; ++i) { + const tracked_toplevel_t& tracked = ctx.tracked_toplevels[i]; + // Skip handles that are not mapped or destroyed + if (tracked.handle == BONGOCAT_NULLPTR) { + continue; + } + if (tracked.is_fullscreen) { + // Only update overlay if on our output + if (tracked.output == ctx.thread_context.output) { + fs_update_state(ctx, true); + return; + } + } + } + + const bool new_state = fs_check_status(ctx); + if (new_state != ctx.thread_context._fullscreen_detected) { + fs_update_state(ctx, new_state); + } +} - // check for hyprland - if (const int result = hyprland::fs_update_state(ctx); result >= 0) { - BONGOCAT_LOG_VERBOSE("fs_handle_toplevel.state: Update fullscreen state: %d (hyprland)", result); - return; - } +struct update_fullscreen_state_toplevel_result_t { + bool output_found{false}; + bool changed{false}; +}; +static update_fullscreen_state_toplevel_result_t +update_fullscreen_state_toplevel(wayland_context_t& ctx, tracked_toplevel_t& tracked, bool is_fullscreen) { + bool state_changed = tracked.is_fullscreen != is_fullscreen; + tracked.is_fullscreen = is_fullscreen; + + /// @NOTE: tracked.output can always be NULL when no output.enter/output.leave event were triggert + // Only trigger overlay update if this fullscreen window is on our output + if (tracked.output == ctx.thread_context.output && state_changed) { + state_changed = fs_update_state(ctx, is_fullscreen); + BONGOCAT_LOG_VERBOSE("Fullscreen state updated for window %p: %d", static_cast(tracked.handle), + is_fullscreen); + return {.output_found = true, .changed = state_changed}; + } + + return {.output_found = false, .changed = state_changed}; +} - // Fallback for when no toplevel was found - const bool changed = fs_update_state(ctx, is_fullscreen); +// Foreign toplevel protocol event handlers +void fs_handle_toplevel_state(void *data, [[maybe_unused]] zwlr_foreign_toplevel_handle_v1 *handle, wl_array *state) { + if (data == BONGOCAT_NULLPTR) { + BONGOCAT_LOG_VERBOSE("Handler called with null data (ignored)"); + return; + } + wayland_context_t& ctx = *static_cast(data); + if (!atomic_load(&ctx.ready)) { + BONGOCAT_LOG_VERBOSE("Wayland configured yet, skipping handling"); + return; + } + // only check for state changes when everything is ready, no need to do something before like fullscreen check + + // check if fullscreen state event change + bool is_fullscreen = false; + wl_array_for_each_typed(state_ptr, state, uint32_t) { + if (*state_ptr == ZWLR_FOREIGN_TOPLEVEL_HANDLE_V1_STATE_FULLSCREEN) { + is_fullscreen = true; + break; + } + } + + /// @NOTE: tracked.output can always be NULL when no output.enter/output.leave event were triggert + for (size_t i = 0; i < ctx.num_toplevels; ++i) { + if (ctx.tracked_toplevels[i].handle == handle) { + auto [output_found, changed] = update_fullscreen_state_toplevel(ctx, ctx.tracked_toplevels[i], is_fullscreen); + if (output_found) { if (changed) { - BONGOCAT_LOG_VERBOSE("fs_handle_toplevel.state: Update fullscreen state: %d", is_fullscreen); + BONGOCAT_LOG_VERBOSE("fs_handle_toplevel.state: Update fullscreen state: %d", is_fullscreen); } + return; + } } + } - void fs_handle_toplevel_closed(void *data, zwlr_foreign_toplevel_handle_v1 *handle) { - if (!data) { - BONGOCAT_LOG_VERBOSE("Handler called with null data (ignored)"); - return; - } - wayland_session_t& ctx = *static_cast(data); - if (!atomic_load(&ctx.ready)) { - BONGOCAT_LOG_VERBOSE("Wayland configured yet, skipping handling"); - return; - } + // check for hyprland + if (const int result = hyprland::fs_update_state(ctx); result >= 0) { + BONGOCAT_LOG_VERBOSE("fs_handle_toplevel.state: Update fullscreen state: %d (hyprland)", result); + return; + } - if (handle) zwlr_foreign_toplevel_handle_v1_destroy(handle); - - // remove from tracked_toplevels if present - for (size_t i = 0; i < ctx.num_toplevels; ++i) { - if (ctx.tracked_toplevels[i].handle == handle) { - ctx.tracked_toplevels[i].handle = nullptr; - // compact array to keep contiguous - for (size_t j = i; j + 1 < ctx.num_toplevels; ++j) { - ctx.tracked_toplevels[j] = ctx.tracked_toplevels[j+1]; - } - ctx.tracked_toplevels[ctx.num_toplevels - 1].handle = {}; - ctx.num_toplevels--; - break; - } - } - - BONGOCAT_LOG_DEBUG("fs_handle_toplevel.closed: Close toplevel handle"); - } - - // Minimal event handlers for unused events - void fs_handle_title(void *data, [[maybe_unused]] zwlr_foreign_toplevel_handle_v1 *handle, [[maybe_unused]] const char *title) { - if (!data) { - BONGOCAT_LOG_VERBOSE("Handler called with null data (ignored)"); - return; - } - //wayland_session_t& ctx = *static_cast(data); - - BONGOCAT_LOG_VERBOSE("fs_toplevel_listener.title: title received"); - } - - void fs_handle_app_id(void *data, [[maybe_unused]] zwlr_foreign_toplevel_handle_v1 *handle, [[maybe_unused]] const char *app_id) { - if (!data) { - BONGOCAT_LOG_VERBOSE("Handler called with null data (ignored)"); - return; - } - //wayland_session_t& ctx = *static_cast(data); - - BONGOCAT_LOG_VERBOSE("fs_toplevel_listener.app_id: app_id received"); - } - - void fs_handle_output_enter(void *data, [[maybe_unused]] zwlr_foreign_toplevel_handle_v1 *handle, [[maybe_unused]] wl_output *output) { - if (!data) { - BONGOCAT_LOG_VERBOSE("Handler called with null data (ignored)"); - return; - } - wayland_session_t& ctx = *static_cast(data); - - for (size_t i = 0; i < ctx.num_toplevels; i++) { - auto &tracked = ctx.tracked_toplevels[i]; - if (tracked.handle == handle) { - BONGOCAT_LOG_VERBOSE("fs_toplevel_listener.output_enter: update tracked_toplevels[%i] output", i); - tracked.output = output; - if (tracked.is_fullscreen) { - if (tracked.output == ctx.wayland_context.output) { - fs_update_state(ctx, true); - } - } - break; - } - } - - BONGOCAT_LOG_VERBOSE("fs_toplevel_listener.output_enter: output received"); - } - - void fs_handle_output_leave(void *data, [[maybe_unused]] zwlr_foreign_toplevel_handle_v1 *handle, [[maybe_unused]] wl_output *output) { - if (!data) { - BONGOCAT_LOG_VERBOSE("Handler called with null data (ignored)"); - return; - } - wayland_session_t& ctx = *static_cast(data); - - for (size_t i = 0; i < ctx.num_toplevels; i++) { - auto &tracked = ctx.tracked_toplevels[i]; - if (tracked.handle == handle && tracked.output == output) { - BONGOCAT_LOG_VERBOSE("fs_toplevel_listener.output_leave: update tracked_toplevels[%i] output", i); - if (tracked.is_fullscreen && tracked.output == ctx.wayland_context.output) { - fs_update_state(ctx, false); - } - tracked.output = nullptr; - break; - } - } - - BONGOCAT_LOG_VERBOSE("fs_toplevel_listener.output_leave: output received"); - } + // Fallback for when no toplevel was found + const bool changed = fs_update_state(ctx, is_fullscreen); + if (changed) { + BONGOCAT_LOG_VERBOSE("fs_handle_toplevel.state: Update fullscreen state: %d", is_fullscreen); + } +} - void fs_handle_done(void *data, [[maybe_unused]] zwlr_foreign_toplevel_handle_v1 *handle) { - if (!data) { - BONGOCAT_LOG_VERBOSE("Handler called with null data (ignored)"); - return; - } - //wayland_session_t& ctx = *static_cast(data); +void fs_handle_toplevel_closed(void *data, zwlr_foreign_toplevel_handle_v1 *handle) { + if (data == BONGOCAT_NULLPTR) { + BONGOCAT_LOG_VERBOSE("Handler called with null data (ignored)"); + return; + } + wayland_context_t& ctx = *static_cast(data); + if (!atomic_load(&ctx.ready)) { + BONGOCAT_LOG_VERBOSE("Wayland configured yet, skipping handling"); + return; + } + + // remove from tracked_toplevels if present + for (size_t i = 0; i < ctx.num_toplevels; ++i) { + if (ctx.tracked_toplevels[i].handle == handle) { + if (ctx.tracked_toplevels[i].handle != BONGOCAT_NULLPTR) { + zwlr_foreign_toplevel_handle_v1_destroy(ctx.tracked_toplevels[i].handle); + ctx.tracked_toplevels[i].handle = BONGOCAT_NULLPTR; + } + // compact array to keep contiguous + for (size_t j = i; j + 1 < ctx.num_toplevels; ++j) { + ctx.tracked_toplevels[j] = ctx.tracked_toplevels[j + 1]; + } + ctx.tracked_toplevels[ctx.num_toplevels - 1].handle = {}; + ctx.num_toplevels--; + break; + } + } + + BONGOCAT_LOG_DEBUG("fs_handle_toplevel.closed: Close toplevel handle"); +} - BONGOCAT_LOG_VERBOSE("fs_toplevel_listener.done: done received"); - } +// Minimal event handlers for unused events +void fs_handle_title(void *data, [[maybe_unused]] zwlr_foreign_toplevel_handle_v1 *handle, + [[maybe_unused]] const char *title) { + if (data == BONGOCAT_NULLPTR) { + BONGOCAT_LOG_VERBOSE("Handler called with null data (ignored)"); + return; + } + // wayland_session_t& ctx = *static_cast(data); - void fs_handle_parent(void *data, [[maybe_unused]] zwlr_foreign_toplevel_handle_v1 *handle, [[maybe_unused]] zwlr_foreign_toplevel_handle_v1 *parent) { - if (!data) { - BONGOCAT_LOG_VERBOSE("Handler called with null data (ignored)"); - return; - } - //wayland_session_t& ctx = *static_cast(data); + BONGOCAT_LOG_VERBOSE("fs_toplevel_listener.title: title received"); +} - BONGOCAT_LOG_VERBOSE("fs_toplevel_listener.parent: parent received"); - } +void fs_handle_app_id(void *data, [[maybe_unused]] zwlr_foreign_toplevel_handle_v1 *handle, + [[maybe_unused]] const char *app_id) { + if (data == BONGOCAT_NULLPTR) { + BONGOCAT_LOG_VERBOSE("Handler called with null data (ignored)"); + return; + } + // wayland_session_t& ctx = *static_cast(data); + BONGOCAT_LOG_VERBOSE("fs_toplevel_listener.app_id: app_id received"); +} - void fs_handle_manager_toplevel(void *data, [[maybe_unused]] zwlr_foreign_toplevel_manager_v1 *manager, - zwlr_foreign_toplevel_handle_v1 *toplevel) { - if (!data) { - BONGOCAT_LOG_WARNING("Handler called with null data (ignored)"); - return; - } - wayland_session_t& ctx = *static_cast(data); - - BONGOCAT_LOG_VERBOSE("fs_toplevel_manager_listener.toplevel: toplevel received"); - - zwlr_foreign_toplevel_handle_v1_add_listener(toplevel, &fs_toplevel_listener, &ctx); - if (ctx.num_toplevels < MAX_TOP_LEVELS) { - bool already_tracked = false; - for (size_t i = 0; i < ctx.num_toplevels; i++) { - if (ctx.tracked_toplevels[i].handle == toplevel) { - already_tracked = true; - break; - } - } - if (!already_tracked) { - ctx.tracked_toplevels[ctx.num_toplevels].handle = toplevel; - ctx.num_toplevels++; - } - } else { - BONGOCAT_LOG_ERROR("fs_toplevel_manager_listener.toplevel: toplevel tracker is full, %zu max: %d", ctx.num_toplevels, MAX_TOP_LEVELS); - } +void fs_handle_output_enter(void *data, [[maybe_unused]] zwlr_foreign_toplevel_handle_v1 *handle, + [[maybe_unused]] wl_output *output) { + if (data == BONGOCAT_NULLPTR) { + BONGOCAT_LOG_VERBOSE("Handler called with null data (ignored)"); + return; + } + wayland_context_t& ctx = *static_cast(data); + + for (size_t i = 0; i < ctx.num_toplevels; i++) { + auto& tracked = ctx.tracked_toplevels[i]; + if (tracked.handle == handle) { + BONGOCAT_LOG_VERBOSE("fs_toplevel_listener.output_enter: update tracked_toplevels[%i] output", i); + tracked.output = output; + if (tracked.is_fullscreen) { + if (tracked.output == ctx.thread_context.output) { + fs_update_state(ctx, true); + } + } + break; + } + } + + BONGOCAT_LOG_VERBOSE("fs_toplevel_listener.output_enter: output received"); +} - BONGOCAT_LOG_DEBUG("fs_toplevel_manager_listener.toplevel: New toplevel registered for fullscreen monitoring: %zu", ctx.num_toplevels); - } +void fs_handle_output_leave(void *data, [[maybe_unused]] zwlr_foreign_toplevel_handle_v1 *handle, + [[maybe_unused]] wl_output *output) { + if (data == BONGOCAT_NULLPTR) { + BONGOCAT_LOG_VERBOSE("Handler called with null data (ignored)"); + return; + } + wayland_context_t& ctx = *static_cast(data); + + for (size_t i = 0; i < ctx.num_toplevels; i++) { + auto& tracked = ctx.tracked_toplevels[i]; + if (tracked.handle == handle && tracked.output == output) { + BONGOCAT_LOG_VERBOSE("fs_toplevel_listener.output_leave: update tracked_toplevels[%i] output", i); + if (tracked.is_fullscreen && tracked.output == ctx.thread_context.output) { + fs_update_state(ctx, false); + } + tracked.output = BONGOCAT_NULLPTR; + break; + } + } + + BONGOCAT_LOG_VERBOSE("fs_toplevel_listener.output_leave: output received"); +} - void fs_handle_manager_finished(void *data, zwlr_foreign_toplevel_manager_v1 *manager) { - if (!data) { - BONGOCAT_LOG_VERBOSE("Handler called with null data (ignored)"); - return; - } - wayland_session_t& ctx = *static_cast(data); - if (!atomic_load(&ctx.ready)) { - BONGOCAT_LOG_VERBOSE("Wayland configured yet, skipping handling"); - return; - } +void fs_handle_done(void *data, [[maybe_unused]] zwlr_foreign_toplevel_handle_v1 *handle) { + if (data == BONGOCAT_NULLPTR) { + BONGOCAT_LOG_VERBOSE("Handler called with null data (ignored)"); + return; + } + // wayland_session_t& ctx = *static_cast(data); - BONGOCAT_LOG_INFO("fs_toplevel_manager_listener.finished: Foreign toplevel manager finished"); - if (manager) zwlr_foreign_toplevel_manager_v1_destroy(manager); - ctx.fs_detector.manager = nullptr; - } + BONGOCAT_LOG_VERBOSE("fs_toplevel_listener.done: done received"); +} - // ============================================================================= - // SCREEN DIMENSION MANAGEMENT - // ============================================================================= +void fs_handle_parent(void *data, [[maybe_unused]] zwlr_foreign_toplevel_handle_v1 *handle, + [[maybe_unused]] zwlr_foreign_toplevel_handle_v1 *parent) { + if (data == BONGOCAT_NULLPTR) { + BONGOCAT_LOG_VERBOSE("Handler called with null data (ignored)"); + return; + } + // wayland_session_t& ctx = *static_cast(data); - static void screen_calculate_dimensions(screen_info_t& screen_info) { - if (screen_info.received == screen_info_received_flags_t::None || - (static_cast(screen_info.received) & static_cast(screen_info_received_flags_t::Geometry)) == 0 || - (static_cast(screen_info.received) & static_cast(screen_info_received_flags_t::Mode)) == 0 || - screen_info.screen_width > 0) { - return; - } + BONGOCAT_LOG_VERBOSE("fs_toplevel_listener.parent: parent received"); +} - const bool is_rotated = (screen_info.transform == WL_OUTPUT_TRANSFORM_90 || - screen_info.transform == WL_OUTPUT_TRANSFORM_270 || - screen_info.transform == WL_OUTPUT_TRANSFORM_FLIPPED_90 || - screen_info.transform == WL_OUTPUT_TRANSFORM_FLIPPED_270); - - if (is_rotated) { - screen_info.screen_width = screen_info.raw_height; - screen_info.screen_height = screen_info.raw_width; - BONGOCAT_LOG_INFO("Detected rotated screen: %dx%d (transform: %d)", - screen_info.raw_height, screen_info.raw_width, screen_info.transform); - } else { - screen_info.screen_width = screen_info.raw_width; - screen_info.screen_height = screen_info.raw_height; - BONGOCAT_LOG_INFO("Detected screen: %dx%d (transform: %d)", - screen_info.raw_width, screen_info.raw_height, screen_info.transform); - } - } +void fs_handle_manager_toplevel(void *data, [[maybe_unused]] zwlr_foreign_toplevel_manager_v1 *manager, + zwlr_foreign_toplevel_handle_v1 *toplevel) { + if (data == BONGOCAT_NULLPTR) { + BONGOCAT_LOG_WARNING("Handler called with null data (ignored)"); + return; + } + wayland_context_t& ctx = *static_cast(data); + + BONGOCAT_LOG_VERBOSE("fs_toplevel_manager_listener.toplevel: toplevel received"); + + zwlr_foreign_toplevel_handle_v1_add_listener(toplevel, &fs_toplevel_listener, &ctx); + if (ctx.num_toplevels < MAX_TOP_LEVELS) { + bool already_tracked = false; + for (size_t i = 0; i < ctx.num_toplevels; i++) { + if (ctx.tracked_toplevels[i].handle == toplevel) { + already_tracked = true; + break; + } + } + if (!already_tracked) { + ctx.tracked_toplevels[ctx.num_toplevels].handle = toplevel; + ctx.num_toplevels++; + } + } else { + BONGOCAT_LOG_ERROR("fs_toplevel_manager_listener.toplevel: toplevel tracker is full, %zu max: %d", + ctx.num_toplevels, MAX_TOP_LEVELS); + } + + BONGOCAT_LOG_DEBUG("fs_toplevel_manager_listener.toplevel: New toplevel registered for fullscreen monitoring: %zu", + ctx.num_toplevels); +} +void fs_handle_manager_finished(void *data, zwlr_foreign_toplevel_manager_v1 *manager) { + if (data == BONGOCAT_NULLPTR) { + BONGOCAT_LOG_VERBOSE("Handler called with null data (ignored)"); + return; + } + wayland_context_t& ctx = *static_cast(data); + if (!atomic_load(&ctx.ready)) { + BONGOCAT_LOG_VERBOSE("Wayland configured yet, skipping handling"); + return; + } + + BONGOCAT_LOG_INFO("fs_toplevel_manager_listener.finished: Foreign toplevel manager finished"); + if (manager != BONGOCAT_NULLPTR) { + zwlr_foreign_toplevel_manager_v1_destroy(manager); + manager = BONGOCAT_NULLPTR; + } + ctx.fs_detector.manager = BONGOCAT_NULLPTR; +} - // ============================================================================= - // WAYLAND EVENT HANDLERS - // ============================================================================= +// ============================================================================= +// SCREEN DIMENSION MANAGEMENT +// ============================================================================= + +static void screen_calculate_dimensions(screen_info_t& screen_info) { + if (screen_info.received == screen_info_received_flags_t::None || + (static_cast(screen_info.received) & static_cast(screen_info_received_flags_t::Geometry)) == + 0 || + (static_cast(screen_info.received) & static_cast(screen_info_received_flags_t::Mode)) == 0 || + screen_info.screen_width > 0) { + return; + } + + const bool is_rotated = + (screen_info.transform == WL_OUTPUT_TRANSFORM_90 || screen_info.transform == WL_OUTPUT_TRANSFORM_270 || + screen_info.transform == WL_OUTPUT_TRANSFORM_FLIPPED_90 || + screen_info.transform == WL_OUTPUT_TRANSFORM_FLIPPED_270); + + if (is_rotated) { + screen_info.screen_width = screen_info.raw_height; + screen_info.screen_height = screen_info.raw_width; + BONGOCAT_LOG_INFO("Detected rotated screen: %dx%d (transform: %d)", screen_info.raw_height, screen_info.raw_width, + screen_info.transform); + } else { + screen_info.screen_width = screen_info.raw_width; + screen_info.screen_height = screen_info.raw_height; + BONGOCAT_LOG_INFO("Detected screen: %dx%d (transform: %d)", screen_info.raw_width, screen_info.raw_height, + screen_info.transform); + } +} - void layer_surface_configure(void *data, - zwlr_layer_surface_v1 *ls, - uint32_t serial, [[maybe_unused]] uint32_t w, [[maybe_unused]] uint32_t h) { - if (!data) { - BONGOCAT_LOG_VERBOSE("Handler called with null data (ignored)"); - return; - } - wayland_session_t& ctx = *static_cast(data); +// ============================================================================= +// WAYLAND EVENT HANDLERS +// ============================================================================= + +void layer_surface_configure(void *data, zwlr_layer_surface_v1 *ls, uint32_t serial, [[maybe_unused]] uint32_t w, + [[maybe_unused]] uint32_t h) { + if (data == BONGOCAT_NULLPTR) { + BONGOCAT_LOG_VERBOSE("Handler called with null data (ignored)"); + return; + } + wayland_context_t& ctx = *static_cast(data); + + assert(ctx.animation_context != BONGOCAT_NULLPTR); + assert(ctx.thread_context.ctx_shm); + wayland_shared_memory_t& wayland_ctx_shm = *ctx.thread_context.ctx_shm; + + zwlr_layer_surface_v1_ack_configure(ls, serial); + atomic_store(&wayland_ctx_shm.configured, true); + if (atomic_load(&ctx.ready)) { + // trigger initial rendering + request_render(*ctx.animation_context); + } + + BONGOCAT_LOG_DEBUG("layer_surface.configure: Layer surface configured: %dx%d", w, h); +} +void layer_surface_closed(void *data, [[maybe_unused]] zwlr_layer_surface_v1 *ls) { + if (data == BONGOCAT_NULLPTR) { + BONGOCAT_LOG_VERBOSE("Handler called with null data (ignored)"); + return; + } + const wayland_context_t& ctx = *static_cast(data); + if (!atomic_load(&ctx.ready)) { + BONGOCAT_LOG_VERBOSE("Wayland configured yet, skipping handling"); + return; + } + + BONGOCAT_LOG_VERBOSE("layer_surface.closed: Layer surface closed"); +} - assert(ctx.animation_trigger_context != nullptr); - assert(ctx.wayland_context.ctx_shm != nullptr); - wayland_shared_memory_t& wayland_ctx_shm = *ctx.wayland_context.ctx_shm; +void xdg_wm_base_ping(void *data, xdg_wm_base *wm_base, uint32_t serial) { + assert(data); + [[maybe_unused]] const wayland_context_t& ctx = *static_cast(data); - zwlr_layer_surface_v1_ack_configure(ls, serial); - atomic_store(&wayland_ctx_shm.configured, true); - if (atomic_load(&ctx.ready)) { - // trigger initial rendering - request_render(*ctx.animation_trigger_context); - } + BONGOCAT_LOG_VERBOSE("xdg_wm_base.ping: base pong %x", serial); + xdg_wm_base_pong(wm_base, serial); +} - BONGOCAT_LOG_DEBUG("layer_surface.configure: Layer surface configured: %dx%d", w, h); - } - void layer_surface_closed(void *data, [[maybe_unused]] zwlr_layer_surface_v1 *ls) { - if (!data) { - BONGOCAT_LOG_VERBOSE("Handler called with null data (ignored)"); - return; - } - wayland_session_t& ctx = *static_cast(data); - if (!atomic_load(&ctx.ready)) { - BONGOCAT_LOG_VERBOSE("Wayland configured yet, skipping handling"); - return; - } +void output_geometry(void *data, [[maybe_unused]] wl_output *wl_output, [[maybe_unused]] int32_t x, + [[maybe_unused]] int32_t y, [[maybe_unused]] int32_t physical_width, + [[maybe_unused]] int32_t physical_height, [[maybe_unused]] int32_t subpixel, + [[maybe_unused]] const char *make, [[maybe_unused]] const char *model, int32_t transform) { + if (data == BONGOCAT_NULLPTR) { + BONGOCAT_LOG_VERBOSE("Handler called with null data (ignored)"); + return; + } + wayland_context_t& ctx = *static_cast(data); + + for (size_t i = 0; i < MAX_OUTPUTS; i++) { + if (ctx.screen_infos[i].wl_output == wl_output) { + ctx.screen_infos[i].transform = transform; + ctx.screen_infos[i].received = + static_cast(static_cast(ctx.screen_infos[i].received) | + static_cast(screen_info_received_flags_t::Geometry)); + screen_calculate_dimensions(ctx.screen_infos[i]); + } + } + BONGOCAT_LOG_DEBUG("wl_output.geometry: Output transform: %d", transform); +} - BONGOCAT_LOG_VERBOSE("layer_surface.closed: Layer surface closed"); - } +void output_mode(void *data, [[maybe_unused]] wl_output *wl_output, uint32_t flags, int32_t width, int32_t height, + [[maybe_unused]] int32_t refresh) { + if (data == BONGOCAT_NULLPTR) { + BONGOCAT_LOG_VERBOSE("Handler called with null data (ignored)"); + return; + } + wayland_context_t& ctx = *static_cast(data); + + BONGOCAT_LOG_VERBOSE("wl_output.mode: mode received: %u", flags); + + if (flags & WL_OUTPUT_MODE_CURRENT) { + for (size_t i = 0; i < MAX_OUTPUTS; i++) { + if (ctx.screen_infos[i].wl_output == wl_output) { + ctx.screen_infos[i].raw_width = width; + ctx.screen_infos[i].raw_height = height; + ctx.screen_infos[i].received = + static_cast(static_cast(ctx.screen_infos[i].received) | + static_cast(screen_info_received_flags_t::Mode)); + BONGOCAT_LOG_DEBUG("wl_output.mode: Received raw screen mode: %dx%d", width, height); + screen_calculate_dimensions(ctx.screen_infos[i]); + } + } + } +} - void xdg_wm_base_ping(void *data, xdg_wm_base *wm_base, uint32_t serial) { - assert(data); - [[maybe_unused]] wayland_session_t& ctx = *static_cast(data); +void output_done(void *data, [[maybe_unused]] wl_output *wl_output) { + if (data == BONGOCAT_NULLPTR) { + BONGOCAT_LOG_VERBOSE("Handler called with null data (ignored)"); + return; + } + wayland_context_t& ctx = *static_cast(data); - BONGOCAT_LOG_VERBOSE("xdg_wm_base.ping: base pong %x", serial); - xdg_wm_base_pong(wm_base, serial); + for (size_t i = 0; i < MAX_OUTPUTS; i++) { + if (ctx.screen_infos[i].wl_output == wl_output) { + screen_calculate_dimensions(ctx.screen_infos[i]); } + } + BONGOCAT_LOG_DEBUG("wl_output.done: Output configuration complete"); +} - void output_geometry(void *data, - [[maybe_unused]] wl_output *wl_output, - [[maybe_unused]] int32_t x, - [[maybe_unused]] int32_t y, - [[maybe_unused]] int32_t physical_width, - [[maybe_unused]] int32_t physical_height, - [[maybe_unused]] int32_t subpixel, - [[maybe_unused]] const char *make, - [[maybe_unused]] const char *model, - int32_t transform) { - if (!data) { - BONGOCAT_LOG_VERBOSE("Handler called with null data (ignored)"); - return; - } - wayland_session_t& ctx = *static_cast(data); - - for (size_t i = 0; i < MAX_OUTPUTS; i++) { - if (ctx.screen_infos[i].wl_output == wl_output) { - ctx.screen_infos[i].transform = transform; - ctx.screen_infos[i].received = static_cast(static_cast(ctx.screen_infos[i].received) | static_cast( - screen_info_received_flags_t::Geometry)); - screen_calculate_dimensions(ctx.screen_infos[i]); - } - } - BONGOCAT_LOG_DEBUG("wl_output.geometry: Output transform: %d", transform); - } +void output_scale(void *data, [[maybe_unused]] wl_output *wl_output, [[maybe_unused]] int32_t factor) { + if (data == BONGOCAT_NULLPTR) { + BONGOCAT_LOG_VERBOSE("Handler called with null data (ignored)"); + return; + } + // wayland_session_t& ctx = *static_cast(data); - void output_mode(void *data , - [[maybe_unused]] wl_output *wl_output, - uint32_t flags, int32_t width, int32_t height, - [[maybe_unused]] int32_t refresh) { - if (!data) { - BONGOCAT_LOG_VERBOSE("Handler called with null data (ignored)"); - return; - } - wayland_session_t& ctx = *static_cast(data); - - BONGOCAT_LOG_VERBOSE("wl_output.mode: mode received: %u", flags); - - if (flags & WL_OUTPUT_MODE_CURRENT) { - for (size_t i = 0; i < MAX_OUTPUTS; i++) { - if (ctx.screen_infos[i].wl_output == wl_output) { - ctx.screen_infos[i].raw_width = width; - ctx.screen_infos[i].raw_height = height; - ctx.screen_infos[i].received = static_cast(static_cast(ctx.screen_infos[i].received) | static_cast( - screen_info_received_flags_t::Mode)); - BONGOCAT_LOG_DEBUG("wl_output.mode: Received raw screen mode: %dx%d", width, height); - screen_calculate_dimensions(ctx.screen_infos[i]); - } - } - } - } + // Scale not needed for our use case + BONGOCAT_LOG_VERBOSE("wl_output.scale: factor received"); +} - void output_done(void *data, [[maybe_unused]] wl_output *wl_output) { - if (!data) { - BONGOCAT_LOG_VERBOSE("Handler called with null data (ignored)"); - return; - } - wayland_session_t& ctx = *static_cast(data); +void output_name(void *data, [[maybe_unused]] wl_output *wl_output, [[maybe_unused]] const char *name) { + if (data == BONGOCAT_NULLPTR) { + BONGOCAT_LOG_VERBOSE("Handler called with null data (ignored)"); + return; + } + // wayland_session_t& ctx = *static_cast(data); - for (size_t i = 0; i < MAX_OUTPUTS; i++) { - if (ctx.screen_infos[i].wl_output == wl_output) { - screen_calculate_dimensions(ctx.screen_infos[i]); - } - } - BONGOCAT_LOG_DEBUG("wl_output.done: Output configuration complete"); - } + BONGOCAT_LOG_VERBOSE("wl_output.name: name received"); +} - void output_scale(void *data, - [[maybe_unused]] wl_output *wl_output, - [[maybe_unused]] int32_t factor) { - if (!data) { - BONGOCAT_LOG_VERBOSE("Handler called with null data (ignored)"); - return; - } - //wayland_session_t& ctx = *static_cast(data); +void output_description(void *data, [[maybe_unused]] wl_output *wl_output, [[maybe_unused]] const char *name) { + if (data == BONGOCAT_NULLPTR) { + BONGOCAT_LOG_VERBOSE("Handler called with null data (ignored)"); + return; + } + // wayland_session_t& ctx = *static_cast(data); - // Scale not needed for our use case - BONGOCAT_LOG_VERBOSE("wl_output.scale: factor received"); - } + BONGOCAT_LOG_VERBOSE("wl_output.description: description received"); +} - void output_name(void *data, [[maybe_unused]] wl_output *wl_output, [[maybe_unused]] const char *name) { - if (!data) { - BONGOCAT_LOG_VERBOSE("Handler called with null data (ignored)"); - return; - } - //wayland_session_t& ctx = *static_cast(data); +void buffer_release(void *data, wl_buffer *buffer) { + if (data == BONGOCAT_NULLPTR) { + BONGOCAT_LOG_VERBOSE("Handler called with null data (ignored)"); + return; + } + wayland_shm_buffer_t& wayland_shm_buffer = *static_cast(data); + + if (wayland_shm_buffer.buffer == buffer) { + atomic_store(&wayland_shm_buffer.busy, false); + BONGOCAT_LOG_VERBOSE("wl_buffer.release: buffer %d released", wayland_shm_buffer.index); + + /* + if (atomic_exchange(&wayland_shm_buffer._wayland_context->_redraw_after_frame, false)) { + // @TODO: render immediately (optional) + } + */ + } else { + BONGOCAT_LOG_VERBOSE("wl_buffer.release: buffer is not matching with data.buffer"); + } +} - BONGOCAT_LOG_VERBOSE("wl_output.name: name received"); - } +void frame_done(void *data, wl_callback *cb, [[maybe_unused]] uint32_t time) { + if (data == BONGOCAT_NULLPTR) { + BONGOCAT_LOG_WARNING("Handler called with null data (ignored)"); + return; + } + auto& ctx = *static_cast(data); + + if (!atomic_load(&ctx.ready)) { + BONGOCAT_LOG_WARNING("Wayland configured yet, skipping handling"); + return; + } + if (!ctx.animation_context) { + BONGOCAT_LOG_WARNING("Wayland configured yet, skipping handling"); + return; + } + + platform::wayland::wayland_thread_context& wayland_ctx = ctx.thread_context; + // animation_context_t& anim = *ctx->animation_context; + // animation::animation_session_t& trigger_ctx = *ctx.animation_trigger_context; + // wayland_shared_memory_t& wayland_ctx_shm = wayland_ctx->ctx_shm; + // read-only + assert(wayland_ctx._local_copy_config); + // const config::config_t& current_config = *wayland_ctx._local_copy_config; + // const animation_shared_memory_t *const anim_shm = anim->shm; + // assert(anim_shm); + + // Clear frame callback under lock + { + platform::LockGuard guard(wayland_ctx._frame_cb_lock); + if (wayland_ctx._frame_cb == cb) { + wl_callback_destroy(wayland_ctx._frame_cb); + wayland_ctx._frame_cb = BONGOCAT_NULLPTR; + atomic_store(&wayland_ctx._frame_pending, false); + BONGOCAT_LOG_VERBOSE("wl_callback.done: frame done"); + } else { + BONGOCAT_LOG_VERBOSE("wl_callback.done: cb is not matching"); + } + } + + // Fallback for missed buffer.release + platform::wayland::wayland_shared_memory_t *shm_ctx = wayland_ctx.ctx_shm.ptr; + assert(shm_ctx); + for (size_t i = 0; i < platform::wayland::WAYLAND_NUM_BUFFERS; i++) { + auto& buf = shm_ctx->buffers[i]; + if (atomic_load(&buf.busy)) { + atomic_store(&buf.busy, false); + BONGOCAT_LOG_WARNING("wl_callback.done: fallback released stuck buffer %zu (missed wl_buffer.release)", i); + } + } + + if (atomic_exchange(&wayland_ctx._redraw_after_frame, false)) { + // render immediately + BONGOCAT_LOG_VERBOSE("wl_callback.done: redraw"); + animation::draw_bar(ctx); + } +} - void output_description(void *data, [[maybe_unused]] wl_output *wl_output, [[maybe_unused]] const char *name) { - if (!data) { - BONGOCAT_LOG_VERBOSE("Handler called with null data (ignored)"); - return; - } - //wayland_session_t& ctx = *static_cast(data); +// ============================================================================= +// WAYLAND PROTOCOL REGISTRY +// ============================================================================= + +void registry_global(void *data, wl_registry *reg, uint32_t name, const char *iface, [[maybe_unused]] uint32_t ver) { + if (data == BONGOCAT_NULLPTR) { + BONGOCAT_LOG_VERBOSE("Handler called with null data (ignored)"); + return; + } + wayland_context_t& ctx = *static_cast(data); + + BONGOCAT_LOG_VERBOSE("wl_registry.global: registry received: %s", iface); + + if (strcmp(iface, wl_compositor_interface.name) == 0) { + ctx.thread_context.compositor = + static_cast(wl_registry_bind(reg, name, &wl_compositor_interface, 4)); + BONGOCAT_LOG_VERBOSE("wl_registry.global: compositor registry bind"); + } else if (strcmp(iface, wl_shm_interface.name) == 0) { + ctx.thread_context.shm = static_cast(wl_registry_bind(reg, name, &wl_shm_interface, 1)); + BONGOCAT_LOG_VERBOSE("wl_registry.global: shm registry bind"); + } else if (strcmp(iface, zwlr_layer_shell_v1_interface.name) == 0) { + ctx.thread_context.layer_shell = + static_cast(wl_registry_bind(reg, name, &zwlr_layer_shell_v1_interface, 1)); + BONGOCAT_LOG_VERBOSE("wl_registry.global: layer_shell registry bind"); + } else if (strcmp(iface, xdg_wm_base_interface.name) == 0) { + ctx.thread_context.xdg_wm_base = static_cast(wl_registry_bind(reg, name, &xdg_wm_base_interface, 1)); + BONGOCAT_LOG_VERBOSE("wl_registry.global: xdg_wm_base registry bind"); + if (ctx.thread_context.xdg_wm_base != BONGOCAT_NULLPTR) { + xdg_wm_base_add_listener(ctx.thread_context.xdg_wm_base, &xdg_wm_base_listener, &ctx); + } + } else if (strcmp(iface, zxdg_output_manager_v1_interface.name) == 0) { + ctx.xdg_output_manager = + static_cast(wl_registry_bind(reg, name, &zxdg_output_manager_v1_interface, 3)); + BONGOCAT_LOG_VERBOSE("wl_registry.global: xdg_output_manager registry bind"); + } else if (strcmp(iface, wl_output_interface.name) == 0) { + if (ctx.output_count < MAX_OUTPUTS) { + ctx.outputs[ctx.output_count].name = name; + ctx.outputs[ctx.output_count].wl_output = + static_cast(wl_registry_bind(reg, name, &wl_output_interface, 2)); + wl_output_add_listener(ctx.outputs[ctx.output_count].wl_output, &output_listener, &ctx); + BONGOCAT_LOG_VERBOSE("wl_registry.global: wl_output registry bind: %i", ctx.output_count); + + // If we lost our output, get xdg_output to check if this is the one + // reconnecting + if (atomic_load(&ctx._output_lost) && ctx.xdg_output_manager != BONGOCAT_NULLPTR) { + ctx.outputs[ctx.output_count].xdg_output = + zxdg_output_manager_v1_get_xdg_output(ctx.xdg_output_manager, ctx.outputs[ctx.output_count].wl_output); + ctx.outputs[ctx.output_count].received = flag_remove( + ctx.outputs[ctx.output_count].received, output_ref_received_flags_t::Name); + ctx.outputs[ctx.output_count].wayland = &ctx; + zxdg_output_v1_add_listener(ctx.outputs[ctx.output_count].xdg_output, &xdg_output_listener, + &ctx.outputs[ctx.output_count]); + BONGOCAT_LOG_VERBOSE("wl_registry.global: New output appeared while output_lost, checking name..."); + } + + ctx.output_count++; + } + } else if (strcmp(iface, zwlr_foreign_toplevel_manager_v1_interface.name) == 0) { + ctx.fs_detector.manager = static_cast( + wl_registry_bind(reg, name, &zwlr_foreign_toplevel_manager_v1_interface, 3)); + BONGOCAT_LOG_VERBOSE("wl_registry.global: foreign_toplevel_manager (fs_detector.manager) registry bind"); + if (ctx.fs_detector.manager != BONGOCAT_NULLPTR) { + zwlr_foreign_toplevel_manager_v1_add_listener(ctx.fs_detector.manager, &fs_manager_listener, &ctx); + BONGOCAT_LOG_INFO( + "wl_registry.global: Foreign toplevel manager bound - using Wayland protocol for fullscreen detection"); + } + } +} - BONGOCAT_LOG_VERBOSE("wl_output.description: description received"); - } +void registry_remove(void *data, [[maybe_unused]] wl_registry *registry, [[maybe_unused]] uint32_t name) { + if (data == BONGOCAT_NULLPTR || registry == BONGOCAT_NULLPTR) { + BONGOCAT_LOG_WARNING("Handler called with null data (ignored)"); + return; + } + wayland_context_t& ctx = *static_cast(data); + platform::wayland::wayland_thread_context& wayland_ctx = ctx.thread_context; + // animation_context_t& anim = ctx.animation_trigger_context->anim; + // animation_trigger_context_t *trigger_ctx = ctx.animation_trigger_context; + + if (!wayland_ctx.ctx_shm) { + BONGOCAT_LOG_WARNING("Handler called with null wayland_ctx.ctx_shm (ignored)"); + return; + } + assert(wayland_ctx.ctx_shm.ptr); + platform::wayland::wayland_shared_memory_t& wayland_ctx_shm = *wayland_ctx.ctx_shm; + + BONGOCAT_LOG_VERBOSE("wl_registry.global_remove: registry received"); + + // Check if the removed global is our bound output + if (name == ctx.thread_context.bound_output_name && ctx.thread_context.bound_output_name != 0) { + BONGOCAT_LOG_VERBOSE("Bound output disconnected (registry name %u)", name); + atomic_store(&ctx._output_lost, true); + atomic_store(&wayland_ctx_shm.configured, false); + + // Clean up the old output reference + wayland_ctx.output = BONGOCAT_NULLPTR; + + // Remove from outputs array + for (size_t i = 0; i < ctx.output_count; ++i) { + if (ctx.outputs[i].name == name) { + if (ctx.outputs[i].xdg_output != BONGOCAT_NULLPTR) { + zxdg_output_v1_destroy(ctx.outputs[i].xdg_output); + ctx.outputs[i].xdg_output = BONGOCAT_NULLPTR; + } + if (ctx.outputs[i].wl_output != BONGOCAT_NULLPTR) { + wl_output_destroy(ctx.outputs[i].wl_output); + ctx.outputs[i].wl_output = BONGOCAT_NULLPTR; + } + // Shift remaining outputs + for (size_t j = i; j < ctx.output_count - 1; ++j) { + ctx.outputs[j] = ctx.outputs[j + 1]; + } + // Zero out the now-unused slot + ctx.outputs[ctx.output_count - 1] = {}; + ctx.output_count--; + break; + } + } + } +} +// Helper to handle output reconnection +void wayland_handle_output_reconnect(output_ref_t *oref, struct wl_output *new_output, uint32_t registry_name, + [[maybe_unused]] const char *output_name) { + assert(oref->wayland); - void buffer_release(void *data, wl_buffer *buffer) { - if (!data) { - BONGOCAT_LOG_VERBOSE("Handler called with null data (ignored)"); - return; - } - wayland_shm_buffer_t& wayland_shm_buffer = *static_cast(data); - - if (wayland_shm_buffer.buffer == buffer) { - atomic_store(&wayland_shm_buffer.busy, false); - BONGOCAT_LOG_VERBOSE("wl_buffer.release: buffer %d released", wayland_shm_buffer.index); - - /* - if (atomic_exchange(&wayland_shm_buffer._wayland_context->_redraw_after_frame, false)) { - // @TODO: render immediately (optional) - } - */ - } else { - BONGOCAT_LOG_VERBOSE("wl_buffer.release: buffer is not matching with data.buffer"); - } - } + wayland_thread_context& wayland_ctx = oref->wayland->thread_context; + // animation_context_t& anim = *ctx.animation_context; + // animation_trigger_context_t& trigger_ctx = *ctx.animation_trigger_context; - void frame_done(void *data, wl_callback *cb, [[maybe_unused]] uint32_t time) { - if (!data) { - BONGOCAT_LOG_WARNING("Handler called with null data (ignored)"); - return; - } - auto& ctx = *static_cast(data); + // read-only config + // assert(wayland_ctx._local_copy_config != nullptr); + // const config::config_t& current_config = *wayland_ctx._local_copy_config; - if (!atomic_load(&ctx.ready)) { - BONGOCAT_LOG_WARNING("Wayland configured yet, skipping handling"); - return; - } - if (!ctx.animation_trigger_context) { - BONGOCAT_LOG_WARNING("Wayland configured yet, skipping handling"); - return; - } + BONGOCAT_LOG_INFO("Output '%s' reconnected (registry name %u)", output_name, registry_name); - platform::wayland::wayland_context_t& wayland_ctx = ctx.wayland_context; - //animation_context_t& anim = *ctx->animation_context; - //animation::animation_session_t& trigger_ctx = *ctx.animation_trigger_context; - //wayland_shared_memory_t& wayland_ctx_shm = wayland_ctx->ctx_shm; - // read-only - assert(wayland_ctx._local_copy_config != nullptr); - //const config::config_t& current_config = *wayland_ctx._local_copy_config; - //const animation_shared_memory_t *const anim_shm = anim->shm; - //assert(anim_shm); - - // Clear frame callback under lock - { - platform::LockGuard guard (wayland_ctx._frame_cb_lock); - if (wayland_ctx._frame_cb == cb) { - wl_callback_destroy(wayland_ctx._frame_cb); - wayland_ctx._frame_cb = nullptr; - atomic_store(&wayland_ctx._frame_pending, false); - BONGOCAT_LOG_VERBOSE("wl_callback.done: frame done"); - } else { - BONGOCAT_LOG_VERBOSE("wl_callback.done: cb is not matching"); - } - } + // Clean up old surface if it exists + cleanup_wayland_context_surface(wayland_ctx); - // Fallback for missed buffer.release - platform::wayland::wayland_shared_memory_t* shm_ctx = wayland_ctx.ctx_shm.ptr; - assert(shm_ctx); - for (size_t i = 0; i < platform::wayland::WAYLAND_NUM_BUFFERS; i++) { - auto& buf = shm_ctx->buffers[i]; - if (atomic_load(&buf.busy)) { - atomic_store(&buf.busy, false); - BONGOCAT_LOG_WARNING("wl_callback.done: fallback released stuck buffer %zu (missed wl_buffer.release)", i); - } - } + // Set new output + wayland_ctx.output = new_output; + wayland_ctx.bound_output_name = registry_name; + atomic_store(&oref->wayland->_output_lost, false); - if (atomic_exchange(&wayland_ctx._redraw_after_frame, false)) { - // render immediately - BONGOCAT_LOG_VERBOSE("wl_callback.done: redraw"); - animation::draw_bar(ctx); - } + // Recreate surface on new output + if (wayland_setup_surface(*oref->wayland) == bongocat_error_t::BONGOCAT_SUCCESS) { + assert(wayland_ctx.ctx_shm.ptr && wayland_ctx.ctx_shm.ptr != MAP_FAILED); + if (wayland_ctx.ctx_shm) { + atomic_store(&wayland_ctx.ctx_shm->configured, true); } - - // ============================================================================= - // WAYLAND PROTOCOL REGISTRY - // ============================================================================= - - void registry_global(void *data , wl_registry *reg, - uint32_t name, const char *iface, - [[maybe_unused]] uint32_t ver) { - if (!data) { - BONGOCAT_LOG_VERBOSE("Handler called with null data (ignored)"); - return; - } - wayland_session_t& ctx = *static_cast(data); - - BONGOCAT_LOG_VERBOSE("wl_registry.global: registry received: %s", iface); - - if (strcmp(iface, wl_compositor_interface.name) == 0) { - ctx.wayland_context.compositor = static_cast(wl_registry_bind(reg, name, &wl_compositor_interface, 4)); - BONGOCAT_LOG_VERBOSE("wl_registry.global: compositor registry bind"); - } else if (strcmp(iface, wl_shm_interface.name) == 0) { - ctx.wayland_context.shm = static_cast(wl_registry_bind(reg, name, &wl_shm_interface, 1)); - BONGOCAT_LOG_VERBOSE("wl_registry.global: shm registry bind"); - } else if (strcmp(iface, zwlr_layer_shell_v1_interface.name) == 0) { - ctx.wayland_context.layer_shell = static_cast(wl_registry_bind(reg, name, &zwlr_layer_shell_v1_interface, 1)); - BONGOCAT_LOG_VERBOSE("wl_registry.global: layer_shell registry bind"); - } else if (strcmp(iface, xdg_wm_base_interface.name) == 0) { - ctx.wayland_context.xdg_wm_base = static_cast(wl_registry_bind(reg, name, &xdg_wm_base_interface, 1)); - BONGOCAT_LOG_VERBOSE("wl_registry.global: xdg_wm_base registry bind"); - if (ctx.wayland_context.xdg_wm_base) { - xdg_wm_base_add_listener(ctx.wayland_context.xdg_wm_base, &xdg_wm_base_listener, &ctx); - } - } else if (strcmp(iface, zxdg_output_manager_v1_interface.name) == 0) { - ctx.xdg_output_manager = static_cast(wl_registry_bind(reg, name, &zxdg_output_manager_v1_interface, 3)); - BONGOCAT_LOG_VERBOSE("wl_registry.global: xdg_output_manager registry bind"); - } else if (strcmp(iface, wl_output_interface.name) == 0) { - if (ctx.output_count < MAX_OUTPUTS) { - ctx.outputs[ctx.output_count].name = name; - ctx.outputs[ctx.output_count].wl_output = static_cast(wl_registry_bind(reg, name, &wl_output_interface, 2)); - wl_output_add_listener(ctx.outputs[ctx.output_count].wl_output, &output_listener, &ctx); - BONGOCAT_LOG_VERBOSE("wl_registry.global: wl_output registry bind: %i", ctx.output_count); - ctx.output_count++; - } - } else if (strcmp(iface, zwlr_foreign_toplevel_manager_v1_interface.name) == 0) { - ctx.fs_detector.manager = static_cast(wl_registry_bind( - reg, name, &zwlr_foreign_toplevel_manager_v1_interface, 3)); - BONGOCAT_LOG_VERBOSE("wl_registry.global: foreign_toplevel_manager (fs_detector.manager) registry bind"); - if (ctx.fs_detector.manager) { - zwlr_foreign_toplevel_manager_v1_add_listener(ctx.fs_detector.manager, &fs_manager_listener, &ctx); - BONGOCAT_LOG_INFO("wl_registry.global: Foreign toplevel manager bound - using Wayland protocol for fullscreen detection"); - } - } + BONGOCAT_LOG_INFO("Surface recreated on reconnected output"); + if constexpr (WAYLAND_NUM_BUFFERS != 1) { + wl_display_roundtrip(wayland_ctx.display); } - - void registry_remove(void *data, - [[maybe_unused]] wl_registry *registry, - [[maybe_unused]] uint32_t name) { - if (!data) { - BONGOCAT_LOG_WARNING("Handler called with null data (ignored)"); - return; - } - //wayland_session_t& ctx = *static_cast(data); - - BONGOCAT_LOG_VERBOSE("wl_registry.global_remove: registry received"); + if (oref->wayland->animation_context != BONGOCAT_NULLPTR) { + request_render(*oref->wayland->animation_context); } + } else { + BONGOCAT_LOG_ERROR("Failed to recreate surface on reconnected output"); + } } +} // namespace bongocat::platform::wayland::details diff --git a/src/platform/wayland_hyprland.cpp b/src/platform/wayland_hyprland.cpp index 178d2ea7..996496dd 100644 --- a/src/platform/wayland_hyprland.cpp +++ b/src/platform/wayland_hyprland.cpp @@ -1,110 +1,118 @@ #include "wayland_hyprland.h" + #include "utils/error.h" + #include #include -#include #include -#include -#include +#include #include -#include #include +#include +#include #include +#include namespace bongocat::platform::wayland::hyprland { int fs_check_compositor_fallback() { - FILE *fp = popen("hyprctl activewindow 2>/dev/null", "r"); - if (fp) { - bool is_fullscreen = false; - - char line[LINE_BUF]; - while (fgets(line, LINE_BUF, fp)) { - const size_t len = strlen(line); - if (len > 0 && line[len-1] == '\n') { - line[len-1] = '\0'; - } - - if (strstr(line, "fullscreen: 1") || strstr(line, "fullscreen: 2") || strstr(line, "fullscreen: true")) { - is_fullscreen = true; - BONGOCAT_LOG_DEBUG("Fullscreen detected in Hyprland"); - break; - } - } + FILE *fp = popen("hyprctl activewindow 2>/dev/null", "r"); + if (fp != BONGOCAT_NULLPTR) { + bool is_fullscreen = false; + + char line[LINE_BUF]; + while (fgets(line, LINE_BUF, fp)) { + const size_t len = strlen(line); + if (len > 0 && line[len - 1] == '\n') { + line[len - 1] = '\0'; + } - pclose(fp); - return is_fullscreen ? 1 : 0; + if (strstr(line, "fullscreen: 1") != BONGOCAT_NULLPTR || strstr(line, "fullscreen: 2") != BONGOCAT_NULLPTR || + strstr(line, "fullscreen: true") != BONGOCAT_NULLPTR) { + is_fullscreen = true; + BONGOCAT_LOG_DEBUG("Fullscreen detected in Hyprland"); + break; + } } - return -1; + pclose(fp); + return is_fullscreen ? 1 : 0; + } + + return -1; } -void update_outputs_with_monitor_ids(wayland_session_t& ctx) { - FILE *fp = popen("hyprctl monitors 2>/dev/null", "r"); - if (!fp) return; +void update_outputs_with_monitor_ids(wayland_context_t& ctx) { + FILE *fp = popen("hyprctl monitors 2>/dev/null", "r"); + if (fp == BONGOCAT_NULLPTR) { + return; + } - char line[LINE_BUF]; - while (fgets(line, LINE_BUF, fp)) { - int id = -1; - char name[256] = {0}; - int result = sscanf(line, "Monitor %d \"%255[^\"]\"", &id, name); - if (result < 2) { - result = sscanf(line, "Monitor %255s (ID %d)", name, &id); - } + char line[LINE_BUF]; + while (fgets(line, LINE_BUF, fp)) { + int id = -1; + char name[256] = {0}; + int result = sscanf(line, "Monitor %d \"%255[^\"]\"", &id, name); + if (result < 2) { + result = sscanf(line, "Monitor %255s (ID %d)", name, &id); + } - if (result == 2) { - for (size_t i = 0; i < ctx.output_count; i++) { - // match by xdg-output name - if ((static_cast(ctx.outputs[i].received) & static_cast(output_ref_received_flags_t::Name)) && strcmp(ctx.outputs[i].name_str, name) == 0) { - ctx.outputs[i].hypr_id = id; - BONGOCAT_LOG_DEBUG("Mapped xdg-output '%s' to Hyprland ID %d", name, id); - break; - } - } + if (result == 2) { + for (size_t i = 0; i < ctx.output_count; i++) { + // match by xdg-output name + if (has_flag(ctx.outputs[i].received, output_ref_received_flags_t::Name) && + strcmp(ctx.outputs[i].name_str, name) == 0) { + ctx.outputs[i].hypr_id = id; + BONGOCAT_LOG_DEBUG("Mapped xdg-output '%s' to Hyprland ID %d", name, id); + break; } + } } + } - pclose(fp); + pclose(fp); } bool get_active_window(window_info_t& win) { - FILE *fp = popen("hyprctl activewindow 2>/dev/null", "r"); - if (!fp) return false; + FILE *fp = popen("hyprctl activewindow 2>/dev/null", "r"); + if (fp == BONGOCAT_NULLPTR) { + return false; + } - bool has_window = false; - win.monitor_id = -1; - win.fullscreen = false; + bool has_window = false; + win.monitor_id = -1; + win.fullscreen = false; - char line[LINE_BUF]; - while (fgets(line, LINE_BUF, fp)) { - // monitor: 0 - if (strstr(line, "monitor:")) { - sscanf(line, "%*[\t ]monitor: %d", &win.monitor_id); - has_window = true; - } - // fullscreen: 0/1/2 - if (strstr(line, "fullscreen:")) { - int val; - if (sscanf(line, "%*[\t ]fullscreen: %d", &val) == 1) { - win.fullscreen = (val != 0); - } - } - // at: X,Y - if (strstr(line, "at:")) { - if (sscanf(line, "%*[\t ]at: [%d, %d]", &win.x, &win.y) < 2) { - sscanf(line, "%*[\t ]at: %d,%d", &win.x, &win.y); - } - } - // size: W,H - if (strstr(line, "size:")) { - if (sscanf(line, "%*[\t ]size: [%d, %d]", &win.width, &win.height) < 2) { - sscanf(line, "%*[\t ]size: %d,%d", &win.width, &win.height); - } - } + char line[LINE_BUF]; + while (fgets(line, LINE_BUF, fp)) { + // monitor: 0 + if (strstr(line, "monitor:") != BONGOCAT_NULLPTR) { + sscanf(line, "%*[\t ]monitor: %d", &win.monitor_id); + has_window = true; + } + // fullscreen: 0/1/2 + if (strstr(line, "fullscreen:") != BONGOCAT_NULLPTR) { + int val; + if (sscanf(line, "%*[\t ]fullscreen: %d", &val) == 1) { + win.fullscreen = (val != 0); + } } + // at: X,Y + if (strstr(line, "at:") != BONGOCAT_NULLPTR) { + if (sscanf(line, "%*[\t ]at: [%d, %d]", &win.x, &win.y) < 2) { + sscanf(line, "%*[\t ]at: %d,%d", &win.x, &win.y); + } + } + // size: W,H + if (strstr(line, "size:") != BONGOCAT_NULLPTR) { + if (sscanf(line, "%*[\t ]size: [%d, %d]", &win.width, &win.height) < 2) { + sscanf(line, "%*[\t ]size: %d,%d", &win.width, &win.height); + } + } + } - pclose(fp); - return has_window; + pclose(fp); + return has_window; } -} \ No newline at end of file +} // namespace bongocat::platform::wayland::hyprland \ No newline at end of file diff --git a/src/platform/wayland_hyprland.h b/src/platform/wayland_hyprland.h index 70e2cbf0..7667dbdc 100644 --- a/src/platform/wayland_hyprland.h +++ b/src/platform/wayland_hyprland.h @@ -1,23 +1,23 @@ #pragma once -#include "platform/global_wayland_session.h" +#include "platform/wayland_context.h" namespace bongocat::platform::wayland::hyprland { static inline constexpr size_t LINE_BUF = 512; struct window_info_t { - int monitor_id{-1}; // monitor number in Hyprland - int x{0}; - int y{0}; - int width{0}; - int height{0}; - bool fullscreen{false}; + int monitor_id{-1}; // monitor number in Hyprland + int x{0}; + int y{0}; + int width{0}; + int height{0}; + bool fullscreen{false}; }; extern int fs_check_compositor_fallback(); -extern void update_outputs_with_monitor_ids(wayland_session_t& ctx); +extern void update_outputs_with_monitor_ids(wayland_context_t& ctx); extern bool get_active_window(window_info_t& win); -} +} // namespace bongocat::platform::wayland::hyprland diff --git a/src/platform/wayland_setups.cpp b/src/platform/wayland_setups.cpp new file mode 100644 index 00000000..99db2f20 --- /dev/null +++ b/src/platform/wayland_setups.cpp @@ -0,0 +1,402 @@ +#include "platform/wayland_setups.h" + +#include "../graphics/bar.h" +#include "graphics/animation.h" +#include "platform/wayland-protocols.hpp" +#include "platform/wayland.h" +#include "platform/wayland_context.h" +#include "platform/wayland_shared_memory.h" +#include "utils/memory.h" +#include "wayland_hyprland.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +// #include "wayland_sway.h" +#include "platform/wayland_callbacks.h" + +namespace bongocat::platform::wayland::details { +// ============================================================================= +// GLOBAL STATE AND CONFIGURATION +// ============================================================================= + +static inline constexpr int CREATE_SHM_MAX_ATTEMPTS = 100; + +static inline constexpr auto WAYLAND_LAYER_NAMESPACE = "bongocat-overlay"; + +static inline constexpr size_t CREATE_SHM_NAME_SUFFIX_LEN = 8; +static inline constexpr char CREATE_SHM_NAME_TEMPLATE[] = "/bongocat-bar-shm-XXXXXXXX"; +static inline constexpr size_t CREATE_SHM_NAME_PREFIX_LEN = + LEN_ARRAY(CREATE_SHM_NAME_TEMPLATE) - 1 - CREATE_SHM_NAME_SUFFIX_LEN; +static_assert((CREATE_SHM_NAME_PREFIX_LEN + CREATE_SHM_NAME_SUFFIX_LEN) == LEN_ARRAY(CREATE_SHM_NAME_TEMPLATE) - 1); + +// ============================================================================= +// BUFFER AND DRAWING MANAGEMENT +// ============================================================================= + +FileDescriptor create_shm(off_t size) { + char *name = strdup(CREATE_SHM_NAME_TEMPLATE); + constexpr char charset_arr[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; + constexpr size_t charset_len = sizeof(charset_arr) - 1; + int fd = -1; + + random_xoshiro128 rng(slow_rand()); + for (int i = 0; i < CREATE_SHM_MAX_ATTEMPTS; i++) { + for (size_t j = 0; j < CREATE_SHM_NAME_SUFFIX_LEN; j++) { + assert(sizeof(charset_arr) - 1 > 0); + name[CREATE_SHM_NAME_PREFIX_LEN + j] = charset_arr[rng.range(0, charset_len - 1)]; + } + fd = shm_open(name, O_RDWR | O_CREAT | O_EXCL, 0600); + if (fd >= 0) { + shm_unlink(name); + break; + } + } + + if (fd < 0 || ftruncate(fd, size) < 0) { + close(fd); + fd = -1; + perror("shm"); + } + + ::free(name); + return FileDescriptor(fd); +} + +// ============================================================================= +// MAIN WAYLAND INTERFACE IMPLEMENTATION +// ============================================================================= + +bongocat_error_t wayland_update_screen_info(wayland_context_t& ctx) { + wayland_thread_context& wayland_ctx = ctx.thread_context; + + // read-only config + assert(wayland_ctx._local_copy_config); + const config::config_t& current_config = *wayland_ctx._local_copy_config; + + wayland_ctx.output = BONGOCAT_NULLPTR; + wayland_ctx.bound_output_name = 0; + wayland_ctx.using_named_output = false; + if (current_config.output_name != BONGOCAT_NULLPTR) { + for (size_t i = 0; i < ctx.output_count; ++i) { + if (has_flag(ctx.outputs[i].received, output_ref_received_flags_t::Name) && + strcmp(ctx.outputs[i].name_str, current_config.output_name) == 0) { + wayland_ctx.output = ctx.outputs[i].wl_output; + wayland_ctx._output_name_str = ctx.outputs[i].name_str; + wayland_ctx._screen_info = &ctx.screen_infos[i]; + wayland_ctx.bound_output_name = ctx.outputs[i].name; // Store registry name for tracking + wayland_ctx.using_named_output = true; // User specified this output + BONGOCAT_LOG_INFO("Matched output: %s", wayland_ctx._output_name_str); + break; + } + } + + if (wayland_ctx.output == BONGOCAT_NULLPTR) { + if (current_config._strict) { + BONGOCAT_LOG_ERROR("Could not find output named '%s'", current_config.output_name); + return bongocat_error_t::BONGOCAT_ERROR_INVALID_PARAM; + } else { + BONGOCAT_LOG_ERROR("Could not find output named '%s', defaulting to first output", current_config.output_name); + } + } + } + + // Fallback + if (wayland_ctx.output == BONGOCAT_NULLPTR && ctx.output_count > 0) { + wayland_ctx.output = ctx.outputs[0].wl_output; + wayland_ctx._output_name_str = ctx.outputs[0].name_str; + wayland_ctx._screen_info = &ctx.screen_infos[0]; + wayland_ctx.bound_output_name = ctx.outputs[0].name; + wayland_ctx.using_named_output = false; // Using fallback, not a named output + BONGOCAT_LOG_WARNING("Falling back to first output: %s", wayland_ctx._output_name_str); + } + + if (wayland_ctx.compositor == BONGOCAT_NULLPTR || wayland_ctx.shm == BONGOCAT_NULLPTR || + wayland_ctx.layer_shell == BONGOCAT_NULLPTR) { + BONGOCAT_LOG_ERROR("Missing required Wayland protocols"); + return bongocat_error_t::BONGOCAT_ERROR_WAYLAND; + } + + // Configure screen dimensions + int screen_width{DEFAULT_SCREEN_WIDTH}; + if (current_config.screen_width > 0) { + BONGOCAT_LOG_WARNING("Use screen width from config: %d", current_config.screen_width); + screen_width = current_config.screen_width; + } else { + // auto-detect screen width + if (wayland_ctx.output != BONGOCAT_NULLPTR) { + wl_display_roundtrip(wayland_ctx.display); + if (wayland_ctx._screen_info != BONGOCAT_NULLPTR && wayland_ctx._screen_info->screen_width > 0) { + BONGOCAT_LOG_INFO("Detected screen width: %d", wayland_ctx._screen_info->screen_width); + screen_width = wayland_ctx._screen_info->screen_width; + } else { + BONGOCAT_LOG_WARNING("Using default screen width: %d", DEFAULT_SCREEN_WIDTH); + screen_width = DEFAULT_SCREEN_WIDTH; + } + } else { + BONGOCAT_LOG_WARNING("No output found, using default screen width: %d", DEFAULT_SCREEN_WIDTH); + screen_width = DEFAULT_SCREEN_WIDTH; + if (current_config._strict) { + return bongocat_error_t::BONGOCAT_ERROR_INVALID_PARAM; + } + } + } + wayland_ctx._screen_width = screen_width; + + return bongocat_error_t::BONGOCAT_SUCCESS; +} +bongocat_error_t wayland_setup_protocols(wayland_context_t& ctx) { + wayland_thread_context& wayland_ctx = ctx.thread_context; + // animation_context_t& anim = *ctx.animation_context; + // animation_trigger_context_t& trigger_ctx = *ctx.animation_trigger_context; + + // read-only config + // assert(wayland_ctx._local_copy_config != nullptr); + // const config::config_t& current_config = *wayland_ctx._local_copy_config; + + /// @TODO: add RAII wrapper for wl_registry + wl_registry *registry = wl_display_get_registry(wayland_ctx.display); + if (registry == BONGOCAT_NULLPTR) { + BONGOCAT_LOG_ERROR("Failed to get Wayland registry"); + return bongocat_error_t::BONGOCAT_ERROR_WAYLAND; + } + + wl_registry_add_listener(registry, &details::reg_listener, &ctx); + wl_display_roundtrip(wayland_ctx.display); + + if (ctx.xdg_output_manager != BONGOCAT_NULLPTR) { + for (size_t i = 0; i < ctx.output_count && i < MAX_OUTPUTS; i++) { + ctx.outputs[i].wayland = &ctx; + ctx.outputs[i].xdg_output = + zxdg_output_manager_v1_get_xdg_output(ctx.xdg_output_manager, ctx.outputs[i].wl_output); + zxdg_output_v1_add_listener(ctx.outputs[i].xdg_output, &details::xdg_output_listener, &ctx.outputs[i]); + ctx.screen_infos[i] = {}; + assert(ctx.outputs[i].wl_output); + ctx.screen_infos[i].wl_output = ctx.outputs[i].wl_output; + } + + // Wait for all xdg_output events + wl_display_roundtrip(wayland_ctx.display); // Process initial events + wl_display_roundtrip(wayland_ctx.display); // Ensure all `done` events arrive + BONGOCAT_LOG_DEBUG("Listener bound for xdg_output and foreign toplevel handle"); + + // DE specific inits + hyprland::update_outputs_with_monitor_ids(ctx); + } + + if (wayland_ctx.compositor == BONGOCAT_NULLPTR || wayland_ctx.shm == BONGOCAT_NULLPTR || + wayland_ctx.layer_shell == BONGOCAT_NULLPTR) { + BONGOCAT_LOG_ERROR("Missing required Wayland protocols"); + wl_registry_destroy(registry); + return bongocat_error_t::BONGOCAT_ERROR_WAYLAND; + } + + const bongocat_error_t result_update_screen_width = wayland_update_screen_info(ctx); + if (result_update_screen_width != bongocat_error_t::BONGOCAT_SUCCESS) { + wl_registry_destroy(registry); + return result_update_screen_width; + } + + // move new registry + if (wayland_ctx.registry != BONGOCAT_NULLPTR) { + wl_registry_destroy(wayland_ctx.registry); + } + wayland_ctx.registry = registry; + registry = BONGOCAT_NULLPTR; + + for (size_t i = 0; i < ctx.output_count && i < MAX_OUTPUTS; i++) { + ctx.outputs[i].wayland = &ctx; + } + + return bongocat_error_t::BONGOCAT_SUCCESS; +} + +bongocat_error_t wayland_setup_surface(wayland_context_t& ctx) { + wayland_thread_context& wayland_ctx = ctx.thread_context; + // animation_context_t& anim = *ctx.animation_context; + // animation_trigger_context_t& trigger_ctx = *ctx.animation_trigger_context; + + // read-only config + assert(wayland_ctx._local_copy_config); + const config::config_t& current_config = *wayland_ctx._local_copy_config; + + wayland_ctx.surface = wl_compositor_create_surface(wayland_ctx.compositor); + if (wayland_ctx.surface == BONGOCAT_NULLPTR) { + BONGOCAT_LOG_ERROR("Failed to create surface"); + return bongocat_error_t::BONGOCAT_ERROR_WAYLAND; + } + + zwlr_layer_shell_v1_layer layer = ZWLR_LAYER_SHELL_V1_LAYER_OVERLAY; + switch (current_config.layer) { + case config::layer_type_t::LAYER_BACKGROUND: + layer = ZWLR_LAYER_SHELL_V1_LAYER_BACKGROUND; + break; + case config::layer_type_t::LAYER_BOTTOM: + layer = ZWLR_LAYER_SHELL_V1_LAYER_BOTTOM; + break; + case config::layer_type_t::LAYER_TOP: + layer = ZWLR_LAYER_SHELL_V1_LAYER_TOP; + break; + case config::layer_type_t::LAYER_OVERLAY: + layer = ZWLR_LAYER_SHELL_V1_LAYER_OVERLAY; + break; + } + wayland_ctx.layer_surface = zwlr_layer_shell_v1_get_layer_surface(wayland_ctx.layer_shell, wayland_ctx.surface, + wayland_ctx.output, layer, WAYLAND_LAYER_NAMESPACE); + if (wayland_ctx.layer_surface == BONGOCAT_NULLPTR) { + BONGOCAT_LOG_ERROR("Failed to create layer surface"); + return bongocat_error_t::BONGOCAT_ERROR_WAYLAND; + } + + // Configure layer surface + uint32_t anchor = ZWLR_LAYER_SURFACE_V1_ANCHOR_LEFT | ZWLR_LAYER_SURFACE_V1_ANCHOR_RIGHT; + switch (current_config.overlay_position) { + case config::overlay_position_t::POSITION_TOP: + anchor |= ZWLR_LAYER_SURFACE_V1_ANCHOR_TOP; + break; + case config::overlay_position_t::POSITION_BOTTOM: + anchor |= ZWLR_LAYER_SURFACE_V1_ANCHOR_BOTTOM; + break; + default: + BONGOCAT_LOG_ERROR("Invalid overlay_position %d for layer surface, set to top (default)"); + anchor |= ZWLR_LAYER_SURFACE_V1_ANCHOR_TOP; + break; + } + + assert(wayland_ctx._bar_height >= 0); + // assert(current_config.bar_height <= UINT32_MAX); + if (wayland_ctx._bar_height == 0) { + BONGOCAT_LOG_ERROR("Can not set anchor with bar_height=0"); + zwlr_layer_surface_v1_destroy(wayland_ctx.layer_surface); + return bongocat_error_t::BONGOCAT_ERROR_WAYLAND; + } + zwlr_layer_surface_v1_set_anchor(wayland_ctx.layer_surface, anchor); + zwlr_layer_surface_v1_set_size(wayland_ctx.layer_surface, 0, static_cast(wayland_ctx._bar_height)); + zwlr_layer_surface_v1_set_exclusive_zone(wayland_ctx.layer_surface, -1); + zwlr_layer_surface_v1_set_keyboard_interactivity(wayland_ctx.layer_surface, + ZWLR_LAYER_SURFACE_V1_KEYBOARD_INTERACTIVITY_NONE); + zwlr_layer_surface_v1_add_listener(wayland_ctx.layer_surface, &details::layer_listener, &ctx); + + // Make surface click-through + wl_region *input_region = wl_compositor_create_region(wayland_ctx.compositor); + if (input_region != BONGOCAT_NULLPTR) { + wl_surface_set_input_region(wayland_ctx.surface, input_region); + wl_region_destroy(input_region); + input_region = BONGOCAT_NULLPTR; + } + + wl_surface_commit(wayland_ctx.surface); + if constexpr (WAYLAND_NUM_BUFFERS == 1) { + wl_display_roundtrip(wayland_ctx.display); + } + return bongocat_error_t::BONGOCAT_SUCCESS; +} + +bongocat_error_t wayland_setup_buffer(wayland_thread_context& wayland_context, + animation::animation_context_t& animation_ctx) { + // read-only config + assert(wayland_context._local_copy_config); + // const config::config_t& current_config = *wayland_context._local_copy_config; + + wayland_shared_memory_t& wayland_ctx_shm = *wayland_context.ctx_shm; + + /// @TODO: limit screen_width and bar_height for buffer_size + const int32_t buffer_width = wayland_context._screen_width; + const int32_t buffer_height = wayland_context._bar_height; + assert(buffer_width >= 0); + assert(buffer_height >= 0); + assert(RGBA_CHANNELS >= 0); + const size_t buffer_size = static_cast(buffer_width) * static_cast(buffer_height) * RGBA_CHANNELS; + if (buffer_size <= 0) { + BONGOCAT_LOG_ERROR("Invalid buffer size: %d", buffer_size); + return bongocat_error_t::BONGOCAT_ERROR_WAYLAND; + } + + static_assert(WAYLAND_NUM_BUFFERS > 0); + static_assert(WAYLAND_NUM_BUFFERS <= INT32_MAX); + if (buffer_size > INT32_MAX / static_cast(WAYLAND_NUM_BUFFERS)) { + BONGOCAT_LOG_ERROR("Buffer size too large for SHM pool offset"); + return bongocat_error_t::BONGOCAT_ERROR_WAYLAND; + } + assert(buffer_size <= INT32_MAX / static_cast(WAYLAND_NUM_BUFFERS)); + const size_t total_size = buffer_size * WAYLAND_NUM_BUFFERS; + + assert(total_size <= INT32_MAX); + FileDescriptor fd = create_shm(static_cast(total_size)); + if (fd._fd < 0) { + return bongocat_error_t::BONGOCAT_ERROR_FILE_IO; + } + + wl_shm_pool *pool = wl_shm_create_pool(wayland_context.shm, fd._fd, static_cast(total_size)); + if (pool == BONGOCAT_NULLPTR) { + BONGOCAT_LOG_ERROR("Failed to create shared memory pool"); + return bongocat_error_t::BONGOCAT_ERROR_WAYLAND; + } + + static_assert(WAYLAND_NUM_BUFFERS > 0); + static_assert(WAYLAND_NUM_BUFFERS <= INT32_MAX); + for (size_t i = 0; i < WAYLAND_NUM_BUFFERS; i++) { + // assert(buffer_size >= 0); + assert(i <= INT32_MAX); + assert(buffer_size <= INT32_MAX); + assert(buffer_size <= static_cast(INT32_MAX)); + assert(buffer_size <= static_cast(INT32_MAX) / WAYLAND_NUM_BUFFERS); + const off_t offset = static_cast(i) * static_cast(buffer_size); + + assert(static_cast(buffer_size) <= SIZE_MAX); + wayland_ctx_shm.buffers[i].pixels = make_allocated_mmap_file_buffer_value(0, buffer_size, fd._fd, offset); + if (!wayland_ctx_shm.buffers[i].pixels) { + BONGOCAT_LOG_ERROR("Failed to map shared memory: %s", strerror(errno)); + for (size_t j = 0; j < i; j++) { + cleanup_shm_buffer(wayland_ctx_shm.buffers[j]); + } + wl_shm_pool_destroy(pool); + return bongocat_error_t::BONGOCAT_ERROR_MEMORY; + } + + // assert(buffer_size >= 0); + assert(i <= INT32_MAX); + assert(buffer_size <= INT32_MAX); + assert(offset <= INT32_MAX); + wayland_ctx_shm.buffers[i].buffer = wl_shm_pool_create_buffer( + pool, static_cast(offset), wayland_context._screen_width, wayland_context._bar_height, + wayland_context._screen_width * RGBA_CHANNELS, WL_SHM_FORMAT_ARGB8888); + if (wayland_ctx_shm.buffers[i].buffer == BONGOCAT_NULLPTR) { + BONGOCAT_LOG_ERROR("Failed to create buffer"); + for (size_t j = 0; j < i; j++) { + cleanup_shm_buffer(wayland_ctx_shm.buffers[j]); + } + wl_shm_pool_destroy(pool); + return bongocat_error_t::BONGOCAT_ERROR_WAYLAND; + } + + // created buffer successfully, set other properties + assert(i <= INT_MAX); + wayland_ctx_shm.buffers[i].index = i; + atomic_store(&wayland_ctx_shm.buffers[i].busy, false); + atomic_store(&wayland_ctx_shm.buffers[i].pending, false); + wayland_ctx_shm.buffers[i]._animation_context = &animation_ctx; + wayland_ctx_shm.buffers[i]._wayland_thread_context = &wayland_context; + wl_buffer_add_listener(wayland_ctx_shm.buffers[i].buffer, &details::buffer_listener, &wayland_ctx_shm.buffers[i]); + } + + wl_shm_pool_destroy(pool); + + wayland_ctx_shm.current_buffer_index = 0; + + return bongocat_error_t::BONGOCAT_SUCCESS; +} +} // namespace bongocat::platform::wayland::details diff --git a/src/platform/wayland_sway.cpp b/src/platform/wayland_sway.cpp index 744a1af8..fb386562 100644 --- a/src/platform/wayland_sway.cpp +++ b/src/platform/wayland_sway.cpp @@ -1,36 +1,38 @@ #include "wayland_sway.h" + #include "utils/error.h" + #include -#include -#include #include -#include -#include +#include #include -#include +#include +#include #include +#include +#include namespace bongocat::platform::wayland::sway { int fs_check_compositor_fallback() { - FILE *fp = popen("swaymsg -t get_tree 2>/dev/null", "r"); - if (fp) { - bool is_fullscreen = false; - - char sway_buffer[SWAY_BUF] = {0}; - while (fgets(sway_buffer, SWAY_BUF, fp)) { - if (strstr(sway_buffer, "\"fullscreen_mode\":1")) { - is_fullscreen = true; - BONGOCAT_LOG_DEBUG("Fullscreen detected in Sway"); - break; - } - } - - pclose(fp); - return is_fullscreen ? 1 : 0; + FILE *fp = popen("swaymsg -t get_tree 2>/dev/null", "r"); + if (fp != BONGOCAT_NULLPTR) { + bool is_fullscreen = false; + + char sway_buffer[SWAY_BUF] = {0}; + while (fgets(sway_buffer, SWAY_BUF, fp)) { + if (strstr(sway_buffer, "\"fullscreen_mode\":1")) { + is_fullscreen = true; + BONGOCAT_LOG_DEBUG("Fullscreen detected in Sway"); + break; + } } - return -1; -} + pclose(fp); + return is_fullscreen ? 1 : 0; + } + return -1; } + +} // namespace bongocat::platform::wayland::sway diff --git a/src/platform/wayland_sway.h b/src/platform/wayland_sway.h index 3319ecd6..3169d37b 100644 --- a/src/platform/wayland_sway.h +++ b/src/platform/wayland_sway.h @@ -1,6 +1,6 @@ #pragma once -#include "platform/global_wayland_session.h" +#include "platform/wayland_context.h" namespace bongocat::platform::wayland::sway { @@ -8,4 +8,4 @@ static inline constexpr size_t SWAY_BUF = 4096; extern int fs_check_compositor_fallback(); -} +} // namespace bongocat::platform::wayland::sway diff --git a/src/utils/error.cpp b/src/utils/error.cpp index e991966d..b38e9a70 100644 --- a/src/utils/error.cpp +++ b/src/utils/error.cpp @@ -1,95 +1,127 @@ #include "utils/error.h" + #include "utils/system_memory.h" + #include #include -#include -#include #include -#include #include +#include +#include +#include namespace bongocat { - namespace details { - inline atomic_bool& get_debug_enabled() { - static atomic_bool g_instance = true; - return g_instance; - } - } +namespace details { + inline atomic_bool& get_debug_enabled() { + static atomic_bool g_instance = true; + return g_instance; + } +} // namespace details - void error_init(bool enable_debug) { - atomic_store(&details::get_debug_enabled(), enable_debug); - } +void error_init(bool enable_debug) { + atomic_store(&details::get_debug_enabled(), enable_debug); +} #if !defined(BONGOCAT_DISABLE_LOGGER) || defined(BONGOCAT_ENABLE_LOGGER) - namespace details { - inline platform::Mutex& get_log_mutex() { - static platform::Mutex g_instance; - return g_instance; - } - - inline void log_timestamp(FILE *stream) { - timeval tv{}; - tm tm_info{}; - char timestamp[64] = {0}; - - gettimeofday(&tv, nullptr); - localtime_r(&tv.tv_sec, &tm_info); // Thread-safe version - - strftime(timestamp, sizeof(timestamp), "%Y-%m-%d %H:%M:%S", &tm_info); - fprintf(stream, "[%s.%03ld] ", timestamp, tv.tv_usec / 1000); - } - - // Core log function using va_list - inline void log_vprintf(const char* name, const char* format, va_list args) { - const int name_len = static_cast(strlen(name)); - assert(name_len > 0); - - platform::LockGuard guard (get_log_mutex()); - log_timestamp(stdout); - fprintf(stdout, "%.*s: ", name_len, name); - vfprintf(stdout, format, args); - fprintf(stdout, "\n"); - fflush(stdout); - } - - // Convenience inline functions - void log_error(const char* fmt, ...) { - va_list args; va_start(args, fmt); log_vprintf("ERROR", fmt, args); va_end(args); - } - - void log_warning(const char* fmt, ...) { - va_list args; va_start(args, fmt); log_vprintf("WARNING", fmt, args); va_end(args); - } - - void log_info(const char* fmt, ...) { - va_list args; va_start(args, fmt); log_vprintf("INFO", fmt, args); va_end(args); - } - - void log_debug(const char* fmt, ...) { - if (!atomic_load(&get_debug_enabled())) return; - va_list args; va_start(args, fmt); log_vprintf("DEBUG", fmt, args); va_end(args); - } - - void log_verbose(const char* fmt, ...) { - if (!atomic_load(&get_debug_enabled())) return; - va_list args; va_start(args, fmt); log_vprintf("VERBOSE", fmt, args); va_end(args); - } +namespace details { + inline platform::Mutex& get_log_mutex() { + static platform::Mutex g_instance; + return g_instance; + } + + inline void log_timestamp(FILE *stream) { + timeval tv{}; + tm tm_info{}; + char timestamp[64] = {0}; + + gettimeofday(&tv, BONGOCAT_NULLPTR); + localtime_r(&tv.tv_sec, &tm_info); // Thread-safe version + + strftime(timestamp, sizeof(timestamp), "%Y-%m-%d %H:%M:%S", &tm_info); + fprintf(stream, "[%s.%03ld] ", timestamp, tv.tv_usec / 1000); + } + + // Core log function using va_list + inline void log_vprintf(const char *name, const char *format, va_list args) { + const int name_len = static_cast(strlen(name)); + assert(name_len > 0); + + platform::LockGuard guard(get_log_mutex()); + log_timestamp(stdout); + fprintf(stdout, "%.*s: ", name_len, name); + vfprintf(stdout, format, args); + fprintf(stdout, "\n"); + fflush(stdout); + } + + // Convenience inline functions + void log_error(const char *fmt, ...) { + va_list args; + va_start(args, fmt); + log_vprintf("ERROR", fmt, args); + va_end(args); + } + + void log_warning(const char *fmt, ...) { + va_list args; + va_start(args, fmt); + log_vprintf("WARNING", fmt, args); + va_end(args); + } + + void log_info(const char *fmt, ...) { + va_list args; + va_start(args, fmt); + log_vprintf("INFO", fmt, args); + va_end(args); + } + + void log_debug(const char *fmt, ...) { + if (!atomic_load(&get_debug_enabled())) { + return; } -#endif + va_list args; + va_start(args, fmt); + log_vprintf("DEBUG", fmt, args); + va_end(args); + } - const char* error_string(bongocat_error_t error) { - switch (error) { - case bongocat_error_t::BONGOCAT_SUCCESS: return "Success"; - case bongocat_error_t::BONGOCAT_ERROR_MEMORY: return "Memory allocation error"; - case bongocat_error_t::BONGOCAT_ERROR_FILE_IO: return "File I/O error"; - case bongocat_error_t::BONGOCAT_ERROR_WAYLAND: return "Wayland error"; - case bongocat_error_t::BONGOCAT_ERROR_CONFIG: return "Configuration error"; - case bongocat_error_t::BONGOCAT_ERROR_INPUT: return "Input error"; - case bongocat_error_t::BONGOCAT_ERROR_ANIMATION: return "Animation error"; - case bongocat_error_t::BONGOCAT_ERROR_THREAD: return "Thread error"; - case bongocat_error_t::BONGOCAT_ERROR_INVALID_PARAM: return "Invalid parameter"; - case bongocat_error_t::BONGOCAT_ERROR_IMAGE: return "Load image error"; - default: return "Unknown error"; - } + void log_verbose(const char *fmt, ...) { + if (!atomic_load(&get_debug_enabled())) { + return; } + va_list args; + va_start(args, fmt); + log_vprintf("VERBOSE", fmt, args); + va_end(args); + } +} // namespace details +#endif + +const char *error_string(bongocat_error_t error) { + switch (error) { + case bongocat_error_t::BONGOCAT_SUCCESS: + return "Success"; + case bongocat_error_t::BONGOCAT_ERROR_MEMORY: + return "Memory allocation error"; + case bongocat_error_t::BONGOCAT_ERROR_FILE_IO: + return "File I/O error"; + case bongocat_error_t::BONGOCAT_ERROR_WAYLAND: + return "Wayland error"; + case bongocat_error_t::BONGOCAT_ERROR_CONFIG: + return "Configuration error"; + case bongocat_error_t::BONGOCAT_ERROR_INPUT: + return "Input error"; + case bongocat_error_t::BONGOCAT_ERROR_ANIMATION: + return "Animation error"; + case bongocat_error_t::BONGOCAT_ERROR_THREAD: + return "Thread error"; + case bongocat_error_t::BONGOCAT_ERROR_INVALID_PARAM: + return "Invalid parameter"; + case bongocat_error_t::BONGOCAT_ERROR_IMAGE: + return "Load image error"; + default: + return "Unknown error"; + } } +} // namespace bongocat diff --git a/src/utils/memory.cpp b/src/utils/memory.cpp index 487f4330..cc83a0e9 100644 --- a/src/utils/memory.cpp +++ b/src/utils/memory.cpp @@ -1,309 +1,333 @@ #include "utils/memory.h" -#include "utils/system_memory.h" -#include "utils/error.h" + #include "core/bongocat.h" -#include +#include "utils/error.h" +#include "utils/system_memory.h" + #include -#include +#include #include +#include namespace bongocat { - namespace details { +namespace details { #if !defined(BONGOCAT_DISABLE_MEMORY_STATISTICS) || defined(BONGOCAT_ENABLE_MEMORY_STATISTICS) - inline memory_stats_t& get_memory_stats() { - static memory_stats_t g_instance{}; - return g_instance; - } - inline platform::Mutex& get_memory_mutex() { - static platform::Mutex g_instance; - return g_instance; - } + inline memory_stats_t& get_memory_stats() { + static memory_stats_t g_instance{}; + return g_instance; + } + inline platform::Mutex& get_memory_mutex() { + static platform::Mutex g_instance; + return g_instance; + } #endif #ifndef NDEBUG - struct allocation_record_t { - void *ptr{nullptr}; - size_t size{0}; - const char *file{}; - int line{0}; - allocation_record_t *next{nullptr}; - }; - - inline allocation_record_t*& get_allocations() { - static allocation_record_t *g_instance = nullptr; - return g_instance; - } + struct allocation_record_t { + void *ptr{BONGOCAT_NULLPTR}; + size_t size{0}; + const char *file{}; + int line{0}; + allocation_record_t *next{BONGOCAT_NULLPTR}; + }; + + inline allocation_record_t *& get_allocations() { + static allocation_record_t *g_instance = BONGOCAT_NULLPTR; + return g_instance; + } #endif - } - +} // namespace details - void* malloc(size_t size) { - if (size == 0) { - BONGOCAT_LOG_WARNING("Attempted to allocate 0 bytes"); - return nullptr; - } +void *malloc(size_t size) { + if (size == 0) { + BONGOCAT_LOG_WARNING("Attempted to allocate 0 bytes"); + return BONGOCAT_NULLPTR; + } - void *ptr = ::malloc(size); - if (!ptr) { - BONGOCAT_LOG_ERROR("Failed to allocate %zu bytes", size); - return nullptr; - } + void *ptr = ::malloc(size); + if (ptr == BONGOCAT_NULLPTR) [[unlikely]] { + BONGOCAT_LOG_ERROR("Failed to allocate %zu bytes", size); + return BONGOCAT_NULLPTR; + } #if !defined(BONGOCAT_DISABLE_MEMORY_STATISTICS) || defined(BONGOCAT_ENABLE_MEMORY_STATISTICS) - { - using namespace details; - platform::LockGuard guard(get_memory_mutex()); - get_memory_stats().total_allocated += size; - get_memory_stats().current_allocated += size; - if (get_memory_stats().current_allocated > get_memory_stats().peak_allocated) { - atomic_store(&get_memory_stats().peak_allocated, atomic_load(&get_memory_stats().current_allocated)); - } - ++get_memory_stats().allocation_count; - } + { + using namespace details; + platform::LockGuard guard(get_memory_mutex()); + get_memory_stats().total_allocated += size; + get_memory_stats().current_allocated += size; + if (get_memory_stats().current_allocated > get_memory_stats().peak_allocated) { + atomic_store(&get_memory_stats().peak_allocated, atomic_load(&get_memory_stats().current_allocated)); + } + ++get_memory_stats().allocation_count; + } #endif - return ptr; - } + return ptr; +} - void* calloc(size_t count, size_t size) { - if (count == 0 || size == 0) { - BONGOCAT_LOG_WARNING("Attempted to allocate 0 bytes"); - return nullptr; - } - - // Check for overflow - assert(size > 0); - if (count > SIZE_MAX / size) { - BONGOCAT_LOG_ERROR("Integer overflow in calloc"); - return nullptr; - } - - void *ptr = ::calloc(count, size); - if (!ptr) { - BONGOCAT_LOG_ERROR("Failed to allocate %zu bytes", count * size); - return nullptr; - } +void *calloc(size_t count, size_t size) { + if (count == 0 || size == 0) { + BONGOCAT_LOG_WARNING("Attempted to allocate 0 bytes"); + return BONGOCAT_NULLPTR; + } + + // Check for overflow + assert(size > 0); + if (count > SIZE_MAX / size) { + BONGOCAT_LOG_ERROR("Integer overflow in calloc"); + return BONGOCAT_NULLPTR; + } + + void *ptr = ::calloc(count, size); + if (ptr == BONGOCAT_NULLPTR) [[unlikely]] { + BONGOCAT_LOG_ERROR("Failed to allocate %zu bytes", count * size); + return BONGOCAT_NULLPTR; + } #if !defined(BONGOCAT_DISABLE_MEMORY_STATISTICS) || defined(BONGOCAT_ENABLE_MEMORY_STATISTICS) - { - using namespace details; - platform::LockGuard guard(get_memory_mutex()); - const size_t total_size = count * size; - get_memory_stats().total_allocated += total_size; - get_memory_stats().current_allocated += total_size; - if (get_memory_stats().current_allocated > get_memory_stats().peak_allocated) { - atomic_store(&get_memory_stats().peak_allocated, atomic_load(&get_memory_stats().current_allocated)); - } - ++get_memory_stats().allocation_count; - } + { + using namespace details; + platform::LockGuard guard(get_memory_mutex()); + const size_t total_size = count * size; + get_memory_stats().total_allocated += total_size; + get_memory_stats().current_allocated += total_size; + if (get_memory_stats().current_allocated > get_memory_stats().peak_allocated) { + atomic_store(&get_memory_stats().peak_allocated, atomic_load(&get_memory_stats().current_allocated)); + } + ++get_memory_stats().allocation_count; + } #endif - return ptr; - } + return ptr; +} - void* bongocat_realloc(void *ptr, size_t size) { - if (size == 0) { - bongocat::free(ptr); - return nullptr; - } +void *bongocat_realloc(void *ptr, size_t size) { + if (size == 0) { + bongocat::free(ptr); + return BONGOCAT_NULLPTR; + } - void *new_ptr = ::realloc(ptr, size); - if (!new_ptr) { - BONGOCAT_LOG_ERROR("Failed to reallocate to %zu bytes", size); - return nullptr; - } + void *new_ptr = ::realloc(ptr, size); + if (new_ptr == BONGOCAT_NULLPTR) [[unlikely]] { + BONGOCAT_LOG_ERROR("Failed to reallocate to %zu bytes", size); + return BONGOCAT_NULLPTR; + } - // Note: We can't track size changes accurately without storing original sizes - // This is a limitation of this simple tracking system + // Note: We can't track size changes accurately without storing original sizes + // This is a limitation of this simple tracking system - return new_ptr; - } + return new_ptr; +} - void free(void *ptr) { - if (!ptr) return; +void free(void *ptr) { + if (ptr == BONGOCAT_NULLPTR) { + return; + } - ::free(ptr); + ::free(ptr); #if !defined(BONGOCAT_DISABLE_MEMORY_STATISTICS) || defined(BONGOCAT_ENABLE_MEMORY_STATISTICS) - { - using namespace details; - platform::LockGuard guard(get_memory_mutex()); - ++get_memory_stats().free_count; - /* - if (static_cast(g_memory_stats.free_count) > static_cast(g_memory_stats.allocation_count)) { - BONGOCAT_LOG_VERBOSE("Potential double free: %d/%d", atomic_load(&g_memory_stats.allocation_count), atomic_load(&g_memory_stats.free_count)); - } - */ - } -#endif + { + using namespace details; + platform::LockGuard guard(get_memory_mutex()); + ++get_memory_stats().free_count; + /* + if (static_cast(g_memory_stats.free_count) > static_cast(g_memory_stats.allocation_count)) { + BONGOCAT_LOG_VERBOSE("Potential double free: %d/%d", atomic_load(&g_memory_stats.allocation_count), + atomic_load(&g_memory_stats.free_count)); } + */ + } +#endif +} - memory_pool_t* memory_pool_create(size_t size, size_t alignment) { - if (size == 0 || alignment == 0) { - BONGOCAT_LOG_ERROR("Invalid memory pool parameters"); - return nullptr; - } - - auto *pool = static_cast(bongocat::malloc(sizeof(memory_pool_t))); - if (!pool) return nullptr; - - pool->data = bongocat::malloc(size); - if (!pool->data) { - bongocat::free(pool); - return nullptr; - } - - pool->size = size; - pool->used = 0; - pool->alignment = alignment; - - return pool; - } +memory_pool_t *memory_pool_create(size_t size, size_t alignment) { + if (size == 0 || alignment == 0) { + BONGOCAT_LOG_ERROR("Invalid memory pool parameters"); + return BONGOCAT_NULLPTR; + } + + // Validate alignment is a power of 2 + if ((alignment & (alignment - 1)) != 0) { + BONGOCAT_LOG_ERROR("Memory pool alignment must be a power of 2, got %zu", alignment); + return NULL; + } + + auto *pool = static_cast(bongocat::malloc(sizeof(memory_pool_t))); + if (pool == BONGOCAT_NULLPTR) [[unlikely]] { + return BONGOCAT_NULLPTR; + } + + pool->data = bongocat::malloc(size); + if (pool->data == BONGOCAT_NULLPTR) [[unlikely]] { + bongocat::free(pool); + return BONGOCAT_NULLPTR; + } + + pool->size = size; + pool->used = 0; + pool->alignment = alignment; + + return pool; +} - void* memory_pool_alloc(memory_pool_t& pool, size_t size) { - if (size == 0) return nullptr; +void *memory_pool_alloc(memory_pool_t& pool, size_t size) { + if (size == 0) { + return BONGOCAT_NULLPTR; + } - // Align the size - const size_t aligned_size = (size + pool.alignment - 1) & ~(pool.alignment - 1); + // Align the size + const size_t aligned_size = (size + pool.alignment - 1) & ~(pool.alignment - 1); - if (pool.used + aligned_size > pool.size) { - BONGOCAT_LOG_ERROR("Memory pool exhausted"); - return nullptr; - } + if (pool.used + aligned_size > pool.size) { + BONGOCAT_LOG_ERROR("Memory pool exhausted"); + return BONGOCAT_NULLPTR; + } - void *ptr = static_cast(pool.data) + pool.used; - pool.used += aligned_size; + void *ptr = static_cast(pool.data) + pool.used; + pool.used += aligned_size; - return ptr; - } + return ptr; +} - void memory_pool_reset(memory_pool_t *pool) { - if (pool) { - pool->used = 0; - } - } +void memory_pool_reset(memory_pool_t *pool) { + if (pool != BONGOCAT_NULLPTR) { + pool->used = 0; + } +} - void memory_pool_destroy(memory_pool_t *pool) { - if (pool) { - bongocat::free(pool->data); - bongocat::free(pool); - } - } +void memory_pool_destroy(memory_pool_t *pool) { + if (pool != BONGOCAT_NULLPTR) { + bongocat::free(pool->data); + bongocat::free(pool); + } +} #if !defined(BONGOCAT_DISABLE_MEMORY_STATISTICS) || defined(BONGOCAT_ENABLE_MEMORY_STATISTICS) - void memory_get_stats(memory_stats_t *stats) { - if (!stats) return; - - { - using namespace details; - platform::LockGuard guard(get_memory_mutex()); - stats->total_allocated = atomic_load(&get_memory_stats().total_allocated); - stats->current_allocated = atomic_load(&get_memory_stats().current_allocated); - stats->peak_allocated = atomic_load(&get_memory_stats().peak_allocated); - stats->allocation_count = atomic_load(&get_memory_stats().allocation_count); - stats->free_count = atomic_load(&get_memory_stats().free_count); - } - } +void memory_get_stats(memory_stats_t *stats) { + if (stats == BONGOCAT_NULLPTR) { + return; + } + + { + using namespace details; + platform::LockGuard guard(get_memory_mutex()); + stats->total_allocated = atomic_load(&get_memory_stats().total_allocated); + stats->current_allocated = atomic_load(&get_memory_stats().current_allocated); + stats->peak_allocated = atomic_load(&get_memory_stats().peak_allocated); + stats->allocation_count = atomic_load(&get_memory_stats().allocation_count); + stats->free_count = atomic_load(&get_memory_stats().free_count); + } +} - void memory_print_stats() { - memory_stats_t stats; - memory_get_stats(&stats); - - { - using namespace details; - platform::LockGuard guard(get_memory_mutex()); - const size_t total_allocated = atomic_load(&stats.total_allocated); - const size_t current_allocated = atomic_load(&stats.current_allocated); - const size_t peak_allocated = atomic_load(&stats.peak_allocated); - const size_t allocation_count = atomic_load(&stats.allocation_count); - const size_t free_count = atomic_load(&stats.free_count); - - assert(allocation_count <= INT_MAX); - assert(free_count <= INT_MAX); - - bongocat::details::log_info("Memory Statistics:"); - bongocat::details::log_info(" Total allocated: %zu bytes (%.2f MB)", total_allocated, static_cast(total_allocated) / (1024.0 * 1024.0)); - bongocat::details::log_info(" Current allocated: %zu bytes (%.2f MB)", current_allocated, static_cast(current_allocated) / (1024.0 * 1024.0)); - bongocat::details::log_info(" Peak allocated: %zu bytes (%.2f MB)", peak_allocated, static_cast(peak_allocated) / (1024.0 * 1024.0)); - bongocat::details::log_info(" Allocations: %zu", allocation_count); - bongocat::details::log_info(" Frees: %zu", free_count); - bongocat::details::log_info(" Potential leaks: %d", static_cast(allocation_count) - static_cast(free_count)); - } - } +void memory_print_stats() { + memory_stats_t stats; + memory_get_stats(&stats); + + { + using namespace details; + platform::LockGuard guard(get_memory_mutex()); + const size_t total_allocated = atomic_load(&stats.total_allocated); + const size_t current_allocated = atomic_load(&stats.current_allocated); + const size_t peak_allocated = atomic_load(&stats.peak_allocated); + const size_t allocation_count = atomic_load(&stats.allocation_count); + const size_t free_count = atomic_load(&stats.free_count); + + assert(allocation_count <= INT_MAX); + assert(free_count <= INT_MAX); + + bongocat::details::log_info("Memory Statistics:"); + bongocat::details::log_info(" Total allocated: %zu bytes (%.2f MB)", total_allocated, + static_cast(total_allocated) / (1024.0 * 1024.0)); + bongocat::details::log_info(" Current allocated: %zu bytes (%.2f MB)", current_allocated, + static_cast(current_allocated) / (1024.0 * 1024.0)); + bongocat::details::log_info(" Peak allocated: %zu bytes (%.2f MB)", peak_allocated, + static_cast(peak_allocated) / (1024.0 * 1024.0)); + bongocat::details::log_info(" Allocations: %zu", allocation_count); + bongocat::details::log_info(" Frees: %zu", free_count); + bongocat::details::log_info(" Potential leaks: %d", + static_cast(allocation_count) - static_cast(free_count)); + } +} #else - void memory_print_stats() {} +void memory_print_stats() {} #endif #ifndef NDEBUG - void* malloc_debug(size_t size, const char *file, int line) { - void *ptr = bongocat::malloc(size); - if (!ptr) return nullptr; - - { - using namespace details; - platform::LockGuard guard(details::get_memory_mutex()); - auto *record = static_cast(::malloc(sizeof(allocation_record_t))); - if (record) { - record->ptr = ptr; - record->size = size; - record->file = file; - record->line = line; - record->next = get_allocations(); - get_allocations() = record; - } - } - - return ptr; +void *malloc_debug(size_t size, const char *file, int line) { + void *ptr = bongocat::malloc(size); + if (ptr == BONGOCAT_NULLPTR) { + return BONGOCAT_NULLPTR; + } + + { + using namespace details; + platform::LockGuard guard(details::get_memory_mutex()); + auto *record = static_cast(::malloc(sizeof(allocation_record_t))); + if (record) { + record->ptr = ptr; + record->size = size; + record->file = file; + record->line = line; + record->next = get_allocations(); + get_allocations() = record; } + } - void free_debug(void *ptr, const char *file, int line) { - if (!ptr) return; - - { - using namespace details; - platform::LockGuard guard(details::get_memory_mutex()); - allocation_record_t **current = &get_allocations(); - while (*current) { - if ((*current)->ptr == ptr) { - allocation_record_t *to_remove = *current; - *current = (*current)->next; - assert(to_remove); - ::free(to_remove); - break; - } - current = &(*current)->next; - } - } - - bongocat::free(ptr); + return ptr; +} -#if !defined(BONGOCAT_DISABLE_MEMORY_STATISTICS) || defined(BONGOCAT_ENABLE_MEMORY_STATISTICS) - { - using namespace details; - platform::LockGuard guard(details::get_memory_mutex()); - const size_t free_count = atomic_load(&get_memory_stats().free_count); - const size_t current_allocated = atomic_load(&get_memory_stats().current_allocated); - if (free_count > current_allocated) { - BONGOCAT_LOG_WARNING("possible double free, one free is to much: Frees: %zu; Allocations: %zu %s:%d", free_count > current_allocated, file, line); - } - } -#endif +void free_debug(void *ptr, const char *file, int line) { + if (!ptr) + return; + + { + using namespace details; + platform::LockGuard guard(details::get_memory_mutex()); + allocation_record_t **current = &get_allocations(); + while (*current) { + if ((*current)->ptr == ptr) { + allocation_record_t *to_remove = *current; + *current = (*current)->next; + assert(to_remove); + ::free(to_remove); + break; + } + current = &(*current)->next; } - - void memory_leak_check() { - using namespace details; - platform::LockGuard guard(get_memory_mutex()); - if (!get_allocations()) { - BONGOCAT_LOG_INFO("No memory leaks detected"); - return; - } - - BONGOCAT_LOG_ERROR("Memory leaks detected:"); - allocation_record_t *current = get_allocations(); - while (current) { - BONGOCAT_LOG_ERROR(" %zu bytes at %s:%d", current->size, current->file, current->line); - current = current->next; - } + } + + bongocat::free(ptr); + +# if !defined(BONGOCAT_DISABLE_MEMORY_STATISTICS) || defined(BONGOCAT_ENABLE_MEMORY_STATISTICS) + { + using namespace details; + platform::LockGuard guard(details::get_memory_mutex()); + const size_t free_count = atomic_load(&get_memory_stats().free_count); + const size_t current_allocated = atomic_load(&get_memory_stats().current_allocated); + if (free_count > current_allocated) { + BONGOCAT_LOG_WARNING("possible double free, one free is to much: Frees: %zu; Allocations: %zu %s:%d", + free_count > current_allocated, file, line); } -#endif + } +# endif } + +void memory_leak_check() { + using namespace details; + platform::LockGuard guard(get_memory_mutex()); + if (!get_allocations()) { + BONGOCAT_LOG_INFO("No memory leaks detected"); + return; + } + + BONGOCAT_LOG_ERROR("Memory leaks detected:"); + allocation_record_t *current = get_allocations(); + while (current) { + BONGOCAT_LOG_ERROR(" %zu bytes at %s:%d", current->size, current->file, current->line); + current = current->next; + } +} +#endif +} // namespace bongocat diff --git a/src/utils/random.cpp b/src/utils/random.cpp index f5e6ab85..48d264cf 100644 --- a/src/utils/random.cpp +++ b/src/utils/random.cpp @@ -1,55 +1,56 @@ #include "utils/random.h" + #include "utils/system_memory.h" + #include -#include -#include #include - +#include +#include namespace bongocat::platform { - static inline constexpr int MAX_ATTEMPTS = 2048; - - uint32_t slow_rand() { - constexpr const char* random_device_filename = "/dev/urandom"; - FileDescriptor fd (open(random_device_filename, O_RDONLY | O_CLOEXEC | O_NONBLOCK)); - if (fd._fd < 0) { - BONGOCAT_LOG_ERROR("Can not open random device: %s", random_device_filename); - } - uint32_t val = 0; +static inline constexpr int MAX_ATTEMPTS = 2048; - ssize_t r = read(fd._fd, &val, sizeof(val)); - if (r == static_cast(sizeof(val))) { - return val; - } - if (r == -1) { - // Non-blocking mode: return on EAGAIN (no entropy available yet) - // or on other read errors (errno preserved). - return 0; - } +uint32_t slow_rand() { + constexpr const char *random_device_filename = "/dev/urandom"; + FileDescriptor fd(open(random_device_filename, O_RDONLY | O_CLOEXEC | O_NONBLOCK)); + if (fd._fd < 0) { + BONGOCAT_LOG_ERROR("Can not open random device: %s", random_device_filename); + } + uint32_t val = 0; - // Partial read: try to finish the request but still respect non-blocking. - // If we encounter EAGAIN or other error, return 0. - size_t got = (r > 0) ? static_cast(r) : 0; - unsigned char *p = reinterpret_cast(&val); - int attempts = 0; - while (got < sizeof(val) && attempts < MAX_ATTEMPTS) { - r = read(fd._fd, p + got, sizeof(val) - got); - if (r > 0) { - got += static_cast(r); - continue; - } - if (r == 0) { - errno = EIO; - return 0; - } - if (errno == EINTR) { - attempts++; - continue; /* retry on interrupt */ - } - // If EAGAIN (non-blocking & no data), or any other error -> fail - return 0; - } + ssize_t r = read(fd._fd, &val, sizeof(val)); + if (r == static_cast(sizeof(val))) { + return val; + } + if (r == -1) { + // Non-blocking mode: return on EAGAIN (no entropy available yet) + // or on other read errors (errno preserved). + return 0; + } - return val; + // Partial read: try to finish the request but still respect non-blocking. + // If we encounter EAGAIN or other error, return 0. + size_t got = (r > 0) ? static_cast(r) : 0; + unsigned char *p = reinterpret_cast(&val); + int attempts = 0; + while (got < sizeof(val) && attempts < MAX_ATTEMPTS) { + r = read(fd._fd, p + got, sizeof(val) - got); + if (r > 0) { + got += static_cast(r); + continue; + } + if (r == 0) { + errno = EIO; + return 0; } + if (errno == EINTR) { + attempts++; + continue; /* retry on interrupt */ + } + // If EAGAIN (non-blocking & no data), or any other error -> fail + return 0; + } + + return val; } +} // namespace bongocat::platform diff --git a/src/utils/system_memory.cpp b/src/utils/system_memory.cpp index 8aa3d8ea..c3270720 100644 --- a/src/utils/system_memory.cpp +++ b/src/utils/system_memory.cpp @@ -1,57 +1,66 @@ #include "utils/system_memory.h" + #include "utils/error.h" #include "utils/time.h" + #include namespace bongocat::platform { - static inline constexpr time_ms_t THREAD_JOIN_TIMEOUT_MS = 5000; // maximum wait for graceful exit - static inline constexpr time_ms_t THREAD_SlEEP_WHEN_WAITING_FOR_THREAD_MS = 100; - static inline constexpr int THREAD_SLEEP_MAX_ATTEMPTS = 2048; - - - int join_thread_with_timeout(pthread_t& thread, time_ms_t timeout_ms) { - if (thread == 0) return 0; - - timespec start{}; - timespec now{}; - clock_gettime(CLOCK_MONOTONIC, &start); - - int attempts = 0; - while (attempts < THREAD_SLEEP_MAX_ATTEMPTS) { - const int ret = pthread_tryjoin_np(thread, nullptr); - if (ret == 0) { - thread = 0; - return 0; - } - if (ret != EBUSY) return ret; // error other than "still running" - - // Check elapsed time - clock_gettime(CLOCK_MONOTONIC, &now); - const time_ms_t elapsed_ms = (now.tv_sec - start.tv_sec) * 1000 + (now.tv_nsec - start.tv_nsec) / 1000000L; - if (elapsed_ms >= timeout_ms) return ETIMEDOUT; - - // small sleep to avoid busy waiting - timespec ts = {.tv_sec = 0, .tv_nsec = 1000000L * THREAD_SlEEP_WHEN_WAITING_FOR_THREAD_MS}; - nanosleep(&ts, nullptr); - attempts++; - } - - return EBUSY; - } +static inline constexpr time_ms_t THREAD_JOIN_TIMEOUT_MS = 5000; // maximum wait for graceful exit +static inline constexpr time_ms_t THREAD_SlEEP_WHEN_WAITING_FOR_THREAD_MS = 100; +static inline constexpr int THREAD_SLEEP_MAX_ATTEMPTS = 2048; - int stop_thread_graceful_or_cancel(pthread_t& thread, atomic_bool &running_flag) { - if (thread == 0) return 0; +int join_thread_with_timeout(pthread_t& thread, time_ms_t timeout_ms) { + if (thread == 0) { + return 0; + } - atomic_store(&running_flag, false); - const int ret = join_thread_with_timeout(thread, THREAD_JOIN_TIMEOUT_MS); - if (thread != 0 && ret == ETIMEDOUT) { - BONGOCAT_LOG_WARNING("Thread did not exit in time, cancelling: %dms", THREAD_JOIN_TIMEOUT_MS); - pthread_cancel(thread); - pthread_join(thread, nullptr); - } + timespec start{}; + timespec now{}; + clock_gettime(CLOCK_MONOTONIC, &start); - thread = 0; + int attempts = 0; + while (attempts < THREAD_SLEEP_MAX_ATTEMPTS) { + const int ret = pthread_tryjoin_np(thread, BONGOCAT_NULLPTR); + if (ret == 0) { + thread = 0; + return 0; + } + if (ret != EBUSY) { + return ret; // error other than "still running" + } - return ret; + // Check elapsed time + clock_gettime(CLOCK_MONOTONIC, &now); + const time_ms_t elapsed_ms = ((now.tv_sec - start.tv_sec) * 1000L) + ((now.tv_nsec - start.tv_nsec) / 1000000L); + if (elapsed_ms >= timeout_ms) { + return ETIMEDOUT; } -} \ No newline at end of file + + // small sleep to avoid busy waiting + timespec ts = {.tv_sec = 0, .tv_nsec = 1000000L * THREAD_SlEEP_WHEN_WAITING_FOR_THREAD_MS}; + nanosleep(&ts, BONGOCAT_NULLPTR); + attempts++; + } + + return EBUSY; +} + +int stop_thread_graceful_or_cancel(pthread_t& thread, atomic_bool& running_flag) { + if (thread == 0) { + return 0; + } + + atomic_store(&running_flag, false); + const int ret = join_thread_with_timeout(thread, THREAD_JOIN_TIMEOUT_MS); + if (thread != 0 && ret == ETIMEDOUT) { + BONGOCAT_LOG_WARNING("Thread did not exit in time, cancelling: %dms", THREAD_JOIN_TIMEOUT_MS); + pthread_cancel(thread); + pthread_join(thread, BONGOCAT_NULLPTR); + } + + thread = 0; + + return ret; +} +} // namespace bongocat::platform \ No newline at end of file diff --git a/src/utils/time.cpp b/src/utils/time.cpp index fdc8bf0f..49d4ef11 100644 --- a/src/utils/time.cpp +++ b/src/utils/time.cpp @@ -1,25 +1,26 @@ #include "utils/time.h" + #include #include namespace bongocat::platform { - timestamp_us_t get_current_time_us() { - timeval now{}; - gettimeofday(&now, nullptr); - return now.tv_sec * 1000000LL + now.tv_usec; - } - timestamp_ms_t get_current_time_ms() { - return get_current_time_us() / 1000; - } +timestamp_us_t get_current_time_us() { + timeval now{}; + gettimeofday(&now, nullptr); + return (now.tv_sec * 1000000LL) + now.tv_usec; +} +timestamp_ms_t get_current_time_ms() { + return get_current_time_us() / 1000L; +} - time_us_t get_uptime_us() { - timespec ts{}; - if (clock_gettime(CLOCK_BOOTTIME, &ts) != 0) { - return 0; - } - return ts.tv_sec * 1000000000LL + ts.tv_nsec; - } - time_ms_t get_uptime_ms() { - return get_uptime_us() / 1000; - } -} \ No newline at end of file +time_us_t get_uptime_us() { + timespec ts{}; + if (clock_gettime(CLOCK_BOOTTIME, &ts) != 0) { + return 0; + } + return (ts.tv_sec * 1000000000LL) + ts.tv_nsec; +} +time_ms_t get_uptime_ms() { + return get_uptime_us() / 1000L; +} +} // namespace bongocat::platform \ No newline at end of file