Skip to content

TamaTac & SfxEngine + Sprite tools#24

Merged
KenVanHoeylandt merged 7 commits intoTactilityProject:mainfrom
Shadowtrance:tamatac-sfxengine
Feb 13, 2026
Merged

TamaTac & SfxEngine + Sprite tools#24
KenVanHoeylandt merged 7 commits intoTactilityProject:mainfrom
Shadowtrance:tamatac-sfxengine

Conversation

@Shadowtrance
Copy link
Contributor

@Shadowtrance Shadowtrance commented Feb 12, 2026

Fight me rabbit!

Summary by CodeRabbit

  • New Features
    • Full TamaTac virtual pet app: responsive animated pet UI, life stages, day/night, persistence/time-away, care actions, poop/clean mechanics, evolution, achievements, pet cemetery, stats and settings, toolbar/navigation.
    • Two mini-games (pattern & reaction) integrated with rewards/achievements.
    • Lightweight SFX engine with many curated sounds and volume/preset controls; sprite/icon support and generation tools.
  • Documentation
    • Comprehensive READMEs for TamaTac and SfxEngine (usage, build, assets, known issues).

@coderabbitai
Copy link

coderabbitai bot commented Feb 12, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds a new TamaTac virtual pet application with build/config files (CMake, manifest), app entrypoint, and TamaTac app class. Introduces game model and logic (PetStats, PetLogic), UI views (Main, Menu, Stats, Settings, Cemetery, Achievements), two mini-games (PatternGame, ReactionGame), sprites/icons and sprite tooling, persistence, an SfxEngine audio library (header, implementation, SfxDefinitions, GPLv3), achievements/cemetery systems, and a README for the TamaTac project.

🚥 Pre-merge checks | ✅ 3 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 29.82% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly identifies the two main components being added: TamaTac (a virtual pet app) and SfxEngine (audio system) plus sprite tools, which aligns with the substantial additions across multiple source files.
Merge Conflict Detection ✅ Passed ✅ No merge conflicts detected when merging into main

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 13

🧹 Nitpick comments (31)
Tools/sprite_tools/spritedata2png.py (1)

59-73: Minor: hex_pattern is recompiled on every iteration.

The regex on line 69 is compiled inside the loop body, creating a new compiled pattern for every sprite array found. Move it outside the loop.

♻️ Suggested fix
     arrays = OrderedDict()
+    hex_pattern = re.compile(r'0x([0-9A-Fa-f]+)')
     for match in pattern.finditer(header_text):
         name = match.group(1)
         data_str = match.group(2)
 
-        hex_pattern = re.compile(r'0x([0-9A-Fa-f]+)')
         values = [int(h.group(1), 16) for h in hex_pattern.finditer(data_str)]
         arrays[name] = values
Tools/sprite_tools/sprite2c.py (1)

275-280: Image.open without context manager — file handle is not closed.

generate_spritedata (line 231) correctly uses with Image.open(…) as img:, but process_file does not, leaving the file descriptor open until GC.

♻️ Suggested fix
 def process_file(filepath, name, frame_width, frame_height, transparent_color):
     """Process a single PNG file and return header content."""
-    img = Image.open(filepath)
-    frames = convert_sprite(img, frame_width, frame_height, transparent_color)
+    with Image.open(filepath) as img:
+        frames = convert_sprite(img, frame_width, frame_height, transparent_color)
     print(f"  {name}: {len(frames)} frame(s) from {os.path.basename(filepath)}")
     return generate_header(name, frames, frame_width, frame_height)
Tools/sprite_tools/generate_placeholders.py (2)

405-415: Unused loop variables delay and loop in frame-data and AnimFrame loops.

These variables are unpacked but unused in the first two loops (lines 406, 412), while they're needed only in the final animatedSprites loop (line 419). Use _ placeholders for clarity.

♻️ Suggested fix
     # Frame data arrays
-    for name, frames, delay, loop in sprites:
+    for name, frames, _, _ in sprites:
         for i, frame in enumerate(frames):
             lines.append(format_array(name, i, frame))
             lines.append("")
 
     # AnimFrame arrays
-    for name, frames, delay, loop in sprites:
+    for name, frames, _, _ in sprites:
         frame_refs = ", ".join(f"{{sprite_{name}_frame{i}}}" for i in range(len(frames)))
         lines.append(f"constexpr AnimFrame frames_{name}[] = {{ {frame_refs} }};")

12-17: rgb565 here and rgb_to_rgb565 in sprite2c.py are duplicated.

Both generate_placeholders.py (line 12) and sprite2c.py (line 86) implement the same RGB→RGB565 conversion with the same transparent-avoidance logic. Consider extracting to a shared module if these tools evolve together.

Also applies to: 86-88

Libraries/SfxEngine/README.md (1)

103-112: Add a language identifier to the fenced code block.

The file-structure code block is missing a language specifier. Using an empty language or a generic one like text satisfies the linter and improves rendering.

📝 Proposed fix
-```
+```text
 Libraries/SfxEngine/
Apps/TamaTac/main/Source/CemeteryView.h (1)

14-19: Consider adding default member initializers to PetRecord.

If a PetRecord array is declared without calling loadRecords (or used partially), the uninitialized valid field could lead to reading garbage. Defensive defaults cost nothing here.

📝 Proposed fix
 struct PetRecord {
-    Personality personality;
-    LifeStage stageReached;
-    uint16_t ageHours;
-    bool valid;
+    Personality personality{};
+    LifeStage stageReached{};
+    uint16_t ageHours = 0;
+    bool valid = false;
 };
Apps/TamaTac/README.md (1)

174-196: Add a language identifier to the fenced code block.

The project structure code block is missing a language specifier.

📝 Proposed fix
-```
+```text
 TamaTac/
Apps/TamaTac/main/Source/CemeteryView.cpp (2)

57-89: Record shifting leaves stale field data for invalid entries.

When srcValid is false, only the valid flag is propagated to the destination slot—old pers, stage, and age values linger in storage. This is safe today because loadRecords gates reads on valid, but it's a latent data-hygiene issue. Something to be aware of if the persistence format evolves.


13-34: Move enum-to-string mappings to PetStats.h to eliminate duplication.

The conversion logic for Personality and LifeStage enums to strings is duplicated across three files: CemeteryView.cpp (as static helper functions), StatsView.cpp (as local variable + array), and MainView.cpp (as local variable). Moving these to PetStats.h as inline functions alongside the enum definitions will prevent divergence if enum values are added or modified.

Libraries/SfxEngine/Include/SfxDefinitions.h (1)

556-585: Jingle sequences are the most complex — consider a brief inline comment on the musical intent.

The LevelUp and GameOver jingles are the densest sequences (8-9 notes with layered voices). A one-line comment noting the musical pattern (e.g., "ascending major arpeggio" or "chromatic descent") would help future maintainers tune these without trial-and-error.

Apps/TamaTac/main/Source/StatsView.cpp (2)

110-119: Duplicated stageName mapping — consider extracting a shared utility.

An identical LifeStage-to-string switch exists in CemeteryView.cpp (see stageName function). Extracting this into a shared helper (e.g., in PetTypes.h or a common utility header) would eliminate the duplication and reduce the risk of the two copies drifting apart.


138-143: Hardcoded personality names array with magic number 5 is fragile.

The Personality enum in PetStats.h currently has exactly 5 members (Energetic, Lazy, Glutton, Cheerful, Hardy), and the array matches this order. However, if the enum is extended or reordered, this array and bounds check will silently break. Consider using Personality::Hardy directly as the upper bound (a pattern already present in PetLogic.cpp:459), or add a Personality::COUNT sentinel to the enum and derive the bounds check from it.

♻️ Suggested approach
-    if (personalityValue) {
-        const char* personalityNames[] = {"Energetic", "Lazy", "Glutton", "Cheerful", "Hardy"};
-        int idx = static_cast<int>(stats.personality);
-        if (idx >= 0 && idx < 5) {
-            lv_label_set_text(personalityValue, personalityNames[idx]);
-        }
-    }
+    if (personalityValue) {
+        static constexpr const char* personalityNames[] = {
+            "Energetic", "Lazy", "Glutton", "Cheerful", "Hardy"
+        };
+        int idx = static_cast<int>(stats.personality);
+        if (idx >= 0 && idx <= static_cast<int>(Personality::Hardy)) {
+            lv_label_set_text(personalityValue, personalityNames[idx]);
+        }
+    }
Apps/TamaTac/main/Source/MenuView.cpp (1)

39-65: Consider extracting a helper for styled menu button creation.

Each of the four menu items repeats the same 5-line styling block (bg_color default/pressed, text_color). A small helper like addStyledMenuItem(menuList, icon, text, handler, this) would reduce ~20 lines of boilerplate and make adding/removing items trivial.

Apps/TamaTac/main/Source/ReactionGame.cpp (2)

96-105: No timeout when target is shown — player can wait indefinitely.

Once showTarget() is called and phase becomes TargetShown, there is no timer scheduled to expire the round. A player could leave the screen indefinitely. If this is intentional (lenient design), no action needed. Otherwise, scheduling a timeout (e.g., 3–5 seconds) after showTarget() that auto-fails the round would tighten the gameplay loop.


174-196: Dead enum value: Phase::FinalResult is listed but never entered.

showFinalResult() sets phase = Phase::Done (line 160), so the Phase::FinalResult case at line 193 is unreachable dead code. Either remove the enum value or use it as the phase during the final result display (setting Done only on the subsequent timer tick).

Libraries/SfxEngine/Source/SfxEngine.cpp (1)

25-38: Global mutable LFSR state is not thread-safe.

s_lfsr and s_retro_lfsr are file-scope globals mutated on every noise sample. While currently only one audio task calls these, if the engine is ever instantiated twice or noise is triggered from another context, these will race. Consider making them Voice-level or instance-level members.

Apps/TamaTac/main/Source/Achievements.h (1)

35-55: Missing deleted copy/move constructors — inconsistent with sibling views.

MenuView and SettingsView explicitly delete copy constructor and copy assignment. AchievementsView does not, creating an inconsistency. While not a bug (the class only holds raw pointers), copying an AchievementsView would produce a shallow copy with aliased LVGL pointers, which is almost certainly unintended.

Suggested fix
 class AchievementsView {
 private:
     TamaTac* app = nullptr;
     lv_obj_t* parent = nullptr;
     lv_obj_t* mainWrapper = nullptr;
 
 public:
+    AchievementsView() = default;
+    ~AchievementsView() = default;
+
+    AchievementsView(const AchievementsView&) = delete;
+    AchievementsView& operator=(const AchievementsView&) = delete;
+
     void onStart(lv_obj_t* parentWidget, TamaTac* appInstance);
Apps/TamaTac/main/Source/SettingsView.cpp (1)

113-125: Redundant load-before-save — read the other setting from the UI instead.

Both onSoundToggled and onDecayChanged load all settings from Preferences just to get the other setting's value before saving. Since both widgets are members of the same view, you can read their state directly, avoiding the extra Preferences I/O and a subtle TOCTOU window:

♻️ Simplified onSoundToggled
 void SettingsView::onSoundToggled(lv_event_t* e) {
     SettingsView* view = static_cast<SettingsView*>(lv_event_get_user_data(e));
     if (view == nullptr || view->soundSwitch == nullptr || view->app == nullptr) return;
 
     bool isChecked = lv_obj_has_state(view->soundSwitch, LV_STATE_CHECKED);
-
     view->app->setSoundEnabled(isChecked);
 
-    bool soundEnabled;
-    DecaySpeed decaySpeed;
-    view->loadSettings(&soundEnabled, &decaySpeed);
-    view->saveSettings(isChecked, decaySpeed);
+    uint16_t selected = view->decayDropdown ? lv_dropdown_get_selected(view->decayDropdown) : 1;
+    view->saveSettings(isChecked, static_cast<DecaySpeed>(selected > 2 ? 1 : selected));
 }

Also applies to: 127-144

Apps/TamaTac/main/Source/PetStats.h (1)

162-162: Minor type inconsistency: PET_HAPPINESS_GAIN is int while all other constants are uint8_t.

All other action effect constants are constexpr uint8_t, but this one is static constexpr int. Since clampStat takes int16_t, it works, but this is inconsistent and the static keyword is redundant inside a namespace.

♻️ Suggested fix
-    static constexpr int PET_HAPPINESS_GAIN = 5;
+    constexpr uint8_t PET_HAPPINESS_GAIN = 5;
Apps/TamaTac/main/Source/Achievements.cpp (1)

75-82: count == 10 is fragile; consider >= 10.

If the persisted cleanCnt is somehow corrupted or skips past 10 (e.g., due to a race or a prior bug), the CleanFreak achievement will never unlock. Since unlock() is idempotent (it checks the bit before writing), using >= 10 has no downside and is more robust.

♻️ Proposed fix
-    if (count == 10) {
+    if (count >= 10) {
         unlock(AchievementId::CleanFreak);
     }
Apps/TamaTac/main/Source/PetLogic.h (1)

31-34: checkHealth, checkEvolution, and applyStatDecay are public but appear to be internal implementation details.

These methods are called from update() and loadState() within PetLogic itself. Exposing them publicly breaks encapsulation and could lead to inconsistent state if called externally out of sequence. Consider moving them to the private section.

♻️ Proposed fix
     // Action handlers
     void performAction(PetAction action);
 
-    // State checks
-    void checkHealth();
-    void checkEvolution();
-    void applyStatDecay(uint32_t elapsedSeconds);
-
     // Getters
     ...
 
 private:
+    // State checks (internal)
+    void checkHealth();
+    void checkEvolution();
+    void applyStatDecay(uint32_t elapsedSeconds);
+
     // Action implementations
     void feed();
Apps/TamaTac/main/Source/TamaTac.cpp (2)

327-350: Inconsistent clamping: onReactionGameComplete doesn't clamp score while onPatternGameComplete does.

onPatternGameComplete (Line 341) clamps the score with std::max(0, std::min(score, PatternGame::MAX_ROUNDS)), but onReactionGameComplete (Line 329) passes score directly. Although applyPlayResult handles any value gracefully, the inconsistency suggests one was overlooked.

♻️ Proposed fix
 void TamaTac::onReactionGameComplete(int score, bool won) {
     if (petLogic) {
-        petLogic->applyPlayResult(score, ReactionGame::MAX_ROUNDS);
+        int clampedScore = std::max(0, std::min(score, ReactionGame::MAX_ROUNDS));
+        petLogic->applyPlayResult(clampedScore, ReactionGame::MAX_ROUNDS);
         petLogic->saveState();

471-479: onResetClicked ignores lv_event_t* parameter — consider a null-guard for consistency.

All other event handlers extract and null-check user_data from the event. This handler doesn't use it since resetDialogId is static, but adding a guard or a comment would maintain consistency and prevent surprises if the function signature changes.

Apps/TamaTac/main/Source/Sprites.h (1)

84-86: No bounds checking in getAnimSprite and getIcon.

Both accessors index directly into arrays using unscoped enums, which allow implicit conversion from arbitrary integers. An out-of-range value would cause an out-of-bounds read. While callers appear to use valid enum values, a defensive check would be prudent — especially since getInfo() in Achievements.cpp already does bounds checking for a similar pattern.

🛡️ Proposed fix
 inline const AnimatedSprite& getAnimSprite(SpriteId id) {
+    if (id < 0 || id >= PET_SPRITE_COUNT) return animatedSprites[0];
     return animatedSprites[id];
 }
 ...
 inline const Icon& getIcon(IconId id) {
+    if (id < 0 || id >= ICON_COUNT) return icons[0];
     return icons[id];
 }

Also applies to: 138-140

Apps/TamaTac/main/Source/MainView.cpp (2)

493-521: Tight coupling: drawOverlays directly accesses TamaTac::petLogic static member.

Rather than taking a PetLogic* or const PetStats& parameter (as other methods do), drawOverlays reaches into the TamaTac class to read petLogic. This creates an implicit dependency and makes the method harder to test or reuse. Consider passing the needed state as a parameter, consistent with getSpriteForCurrentState.


557-572: onAnimTimer accesses TamaTac::pendingResetUI — ensure this flag is safe across tasks.

pendingResetUI is set in TamaTac::onResult (which may run on a different task than LVGL) and read here on the LVGL task. Since it's a bool (likely atomic on most architectures) and only transitions true→false, this is safe in practice, but making it std::atomic<bool> would formalize the intent.

Apps/TamaTac/main/Source/TamaTac.h (3)

1-27: Includes and global buffers look reasonable.

The extern canvas buffers total ~16 KB of static RAM (72×72 + 12×16×16 in lv_color_t). For an ESP32 app this is fine, but worth keeping in mind if memory pressure grows.

Minor inconsistency: Line 10 uses <freertos/freertos.h> (lowercase), while SfxEngine.h uses "freertos/FreeRTOS.h" (proper casing). On case-sensitive file systems (e.g., Linux CI), the lowercase form may fail to resolve if the SDK only provides the canonical capitalized header.

Suggested fix
-#include <freertos/freertos.h>
+#include <freertos/FreeRTOS.h>

60-70: Static members make TamaTac implicitly a singleton — consider documenting the constraint.

All core state (petLogic, sfxEngine, updateTimer, timerMutex, etc.) is static, so a second TamaTac instance would silently share and corrupt this state. The onCreate/onDestroy code in TamaTac.cpp does null-check-and-allocate, but there's no guard against concurrent instances.

If the Tactility framework guarantees at most one App instance per registered app, this is fine — but a brief comment here (e.g., // Singleton — framework ensures single instance) would save future readers from puzzling over the static design choice.


122-129: Eight friend declarations create tight coupling across all view/game classes.

Every view and game class gets full private access. This works for a monolithic embedded app, but if any of these modules grow, consider introducing a narrow interface (e.g., an AppActions struct or abstract base) that views call instead of reaching into TamaTac internals. No action needed now — just flagging for future maintainability.

Libraries/SfxEngine/Include/SfxEngine.h (2)

277-292: volatile on shared settings fields — works in practice but not idiomatic C++.

masterVolume_, enabled_, polyphonicGateEnabled_, etc. are read by the audio task and written from the app thread. On ESP32 (Xtensa), single aligned 32-bit reads/writes are naturally atomic, so volatile float works here. However, std::atomic<float> (with memory_order_relaxed) would be more correct per the C++ memory model and would communicate intent to future maintainers.

No urgency — the current approach is a common embedded pattern and is safe on this target.


136-142: Move operations are implicitly suppressed but not explicitly deleted.

Copy is correctly deleted. Move constructor/assignment are implicitly suppressed because copy ops are user-declared. Since SfxEngine owns RTOS handles (task_, msgQueue_), an accidental move would be dangerous. Explicitly deleting them makes the intent clear:

Suggested addition
     SfxEngine(const SfxEngine&) = delete;
     SfxEngine& operator=(const SfxEngine&) = delete;
+    SfxEngine(SfxEngine&&) = delete;
+    SfxEngine& operator=(SfxEngine&&) = delete;

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

🧹 Nitpick comments (20)
Apps/TamaTac/main/Source/CemeteryView.h (1)

14-19: Consider default-initializing all PetRecord members.

personality and stageReached are left uninitialized when valid is false. While the code consistently checks valid before reading them, defensive initialization prevents surprises if a future caller forgets the guard.

📝 Proposed fix
 struct PetRecord {
-    Personality personality;
-    LifeStage stageReached;
-    uint16_t ageHours;
+    Personality personality = Personality::Energetic;
+    LifeStage stageReached = LifeStage::Egg;
+    uint16_t ageHours = 0;
     bool valid = false;
 };
Apps/TamaTac/main/Source/PetLogic.cpp (2)

195-260: Dual-loop decay applies base rates before personality modifiers — intentional?

The two separate loops (lines 207–216 for base decay, lines 220–255 for personality/day-night) mean base decay can zero out stats before personality bonuses offset them. If merged into a single per-tick loop, personalities like Energetic (+1 energy) would counteract decay each tick rather than being applied after stats are already floored. This makes the current approach slightly more punishing during long absences.

Not a bug — just worth confirming this is the intended gameplay behavior.


262-273: stats.health <= 0 is always == 0 for uint8_t.

Since health is uint8_t and clampStat prevents underflow, the <= 0 comparison can never be true for negative values. Using == 0 would be semantically clearer, though functionally identical.

Tools/sprite_tools/generate_placeholders.py (2)

406-416: Use _ for unused loop variables.

Ruff flags delay and loop as unused in the loops at lines 407 and 413 (B007). Use _ to indicate intentionally ignored values.

📝 Proposed fix
     # Frame data arrays
-    for name, frames, delay, loop in sprites:
+    for name, frames, _, _ in sprites:
         for i, frame in enumerate(frames):
             lines.append(format_array(name, i, frame))
             lines.append("")

     # AnimFrame arrays
-    for name, frames, delay, loop in sprites:
+    for name, frames, _, _ in sprites:
         frame_refs = ", ".join(f"{{sprite_{name}_frame{i}}}" for i in range(len(frames)))
         lines.append(f"constexpr AnimFrame frames_{name}[] = {{ {frame_refs} }};")

427-434: Move import os to the top of the file.

PEP 8 and common convention place all imports at module level. The late import on line 428 inside main() is functional but unexpected.

📝 Proposed fix
 import math
+import os
 
 T = 0xF81F  # Transparent (magenta)

Then remove import os from line 428 inside main().

Libraries/SfxEngine/Include/SfxEngine.h (1)

285-296: volatile for cross-thread shared state is functional but non-portable.

On ESP32 (32-bit aligned accesses are atomic), volatile works for these simple load/store patterns. However, std::atomic with memory_order_relaxed would provide the same zero-overhead guarantees while being formally correct per the C++ memory model. Consider this for future portability.

Libraries/SfxEngine/Include/SfxDefinitions.h (1)

23-33: Inline private method defined in a separate header — ensure inclusion is restricted.

loadSound is a private inline member function defined outside the class's main header. This works correctly as long as SfxDefinitions.h is only included in SfxEngine.cpp. If accidentally included elsewhere, it would cause ODR violations or link errors if the definition diverges.

A brief comment at the top (e.g., // Internal — include only from SfxEngine.cpp) would help prevent accidental misuse.

Libraries/SfxEngine/Source/SfxEngine.cpp (1)

476-478: Best-effort silence flush after I2S error exit.

If the audio task exits due to an I2S write error (line 471), the flush at line 478 writes to the same possibly-failing device. This is acceptable as a best-effort cleanup, but the error return is silently ignored. Consider guarding this flush:

Optional: skip flush if the loop exited on I2S error
+    // Flush silence (skip if we exited due to I2S error)
+    if (self->i2sDevice_ != nullptr) {
         memset(self->audioBuffer_, 0, sizeof(self->audioBuffer_));
         i2s_controller_write(self->i2sDevice_, self->audioBuffer_, sizeof(self->audioBuffer_), &written, pdMS_TO_TICKS(50));
+    }
Tools/sprite_tools/spritedata2png.py (1)

127-128: Update Image.NEAREST to Image.Resampling.NEAREST for Pillow 9.1.0+ compatibility.

Image.NEAREST was deprecated in Pillow 9.1.0 and scheduled for removal in 10.0.0. While it still works currently, replace it with Image.Resampling.NEAREST to eliminate deprecation warnings and ensure forward compatibility.

Proposed fix
-        img = img.resize((width * scale, height * scale), Image.NEAREST)
+        img = img.resize((width * scale, height * scale), Image.Resampling.NEAREST)

Apply the same change at line 144.

Apps/TamaTac/main/Source/CemeteryView.cpp (2)

110-117: Potential buffer overflow in snprintf for record text.

The text[80] buffer holds "#%d %s %s %dd %dh". If personalityToString or lifeStageToString return longer strings, this could truncate. With 5 entries and typical enum-to-string values this is likely safe, but consider increasing the buffer or verifying the max combined length of both string conversions.


34-66: Record shift leaves orphaned preference keys for invalid records.

When srcValid is false (line 46), only the valid flag is copied to the destination slot — the pers, stage, and age keys for that destination remain stale in persistent storage. This is functionally harmless since loadRecords gates on valid, but it means preferences accumulate stale data that's never cleaned up.

Tools/sprite_tools/sprite2c.py (1)

119-134: Performance: getpixel in a tight loop is very slow for large sprites.

img.getpixel() is called per-pixel inside nested loops. For large spritesheets this will be noticeably slow. Using img.load() to get a pixel-access object, or converting to a NumPy array, would be significantly faster.

Example using pixel access object
+    pixels = img.load()
     frames = []
     for row in range(rows):
         for col in range(cols):
             frame = []
             for y in range(frame_height):
                 for x in range(frame_width):
-                    px = img.getpixel((col * frame_width + x, row * frame_height + y))
+                    px = pixels[col * frame_width + x, row * frame_height + y]
                     r, g, b, a = px
Apps/TamaTac/main/Source/Achievements.h (1)

34-59: Achievement logic mixed into the view class.

The static bitfield operations (loadAchievements, unlock, hasAchievement, etc.) are pure data/persistence logic with no UI dependency, yet they live on AchievementsView. This couples callers (e.g., TamaTac.cpp calling AchievementsView::unlock) to a UI class. Extracting them into a standalone Achievements utility class/namespace would improve separation of concerns.

Not blocking, but worth considering if the achievement surface grows.

Apps/TamaTac/main/Source/Achievements.cpp (1)

13-26: Verify achievementInfos array size stays in sync with AchievementId::COUNT.

The array has 12 entries matching the current COUNT = 12, but there's no compile-time assertion to catch a mismatch if new achievements are added to the enum without updating the array.

Add a static_assert
 static const AchievementInfo achievementInfos[] = {
     {"First Feed",   "Feed your pet"},
     ...
     {"Night Owl",    "Play at night"},
 };
+static_assert(sizeof(achievementInfos) / sizeof(achievementInfos[0]) == static_cast<int>(AchievementId::COUNT),
+              "achievementInfos must match AchievementId::COUNT");
Apps/TamaTac/main/Source/TamaTac.cpp (2)

17-27: All app state is static — prevents multiple instances and complicates testing.

Every member (petLogic, updateTimer, timerMutex, sfxEngine, etc.) is a static field. This is acceptable for a single-instance embedded app, but be aware it means onCreate/onDestroy aren't truly re-entrant: a second onCreate without a prior onDestroy will leak or reuse stale state (the nullptr guards help, but they also mean a second create silently reuses old state).


432-477: Timer callback performs persistence I/O and achievement unlocks — may stall the FreeRTOS timer service.

onTimerUpdate runs on the shared FreeRTOS timer-daemon task. It calls petLogic->saveState() and multiple AchievementsView::unlock() calls (each instantiating Preferences and doing I/O). If preferences I/O blocks (e.g., flash write), all other software timers in the system are stalled until this callback completes.

Consider deferring the heavy work to a dedicated task or using lv_async_call to bounce it to the LVGL thread.

Apps/TamaTac/main/Source/Sprites.h (1)

92-138: ODR concern: constexpr icon arrays have internal linkage, but inline icons[] references them.

The constexpr uint8_t icon_poop[] (and siblings) have internal linkage in C++17, meaning each translation unit gets its own copy with a distinct address. The inline const Icon icons[] array has external linkage and is deduplicated by the linker, but its initializer references the TU-local icon arrays — technically an ODR violation since the initializer produces different pointer values in different TUs.

In practice the data is identical so this won't cause bugs, but for correctness, mark the icon arrays as inline constexpr to give them external linkage:

Fix linkage
-constexpr uint8_t icon_poop[] = {
+inline constexpr uint8_t icon_poop[] = {
     0x00, 0x18, 0x3C, 0x7E, 0x7E, 0xFF, 0xFF, 0x00,
 };

Apply the same inline addition to all other icon_* arrays.

Apps/TamaTac/main/Source/TamaTac.h (3)

24-26: Consider defining named constants for magic numbers in buffer dimensions.

72, 16, and 12 appear as raw literals. Extracting them into constexpr constants (e.g., kCanvasSize, kIconSize, kIconCount) would make the relationship between sprite assets and buffer sizes self-documenting and easier to keep in sync.


60-70: Static singleton state is fragile if lifecycle isn't strictly managed.

All 10 static members rely on the assumption that exactly one TamaTac instance exists. If onDestroy doesn't fully zero out every static (or if a second instance is created before the first is destroyed), stale pointers (petLogic, sfxEngine, currentApp) will silently corrupt state. Consider either:

  • Using a true singleton accessor with a guard, or
  • Making these instance members (possibly behind an inner SharedState struct allocated once and ref-counted).

This isn't blocking for a single-app embedded target, but it's worth hardening if the framework ever permits concurrent app instances.


122-129: Eight friend declarations couple every view/game to all private internals.

Every friend class can access every private member — not just the subset it needs. If any view is refactored, the blast radius is the entire private surface. A narrower alternative is to pass a small context/interface (e.g., a TamaTacContext struct or abstract interface) to each view, exposing only what it requires.

Low priority for a self-contained app, but worth considering if the view count keeps growing.


wrapperWidget = nullptr;
toolbar = nullptr;
menuButton = nullptr;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Timer start/stop should be moved to onShow() and onHide().

onCreate() and onDestroy() should only contain things that you need while the app is not visible (e.g. another app like a dialog is in front of it) such as remember the state of the game.

(This is why the screensaver suffers when the app is running)

@KenVanHoeylandt KenVanHoeylandt merged commit 0b22f67 into TactilityProject:main Feb 13, 2026
11 checks passed
@KenVanHoeylandt
Copy link
Contributor

Thanks again! Great job!

@Shadowtrance Shadowtrance deleted the tamatac-sfxengine branch February 13, 2026 20:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants