Fix towers not firing on resume and expand game persistence - #191
Conversation
- Fixed synchronization issue where towers stopped firing after resume because the virtual clock (simulationTimeMs) was reset to 0 while towers held last-fire timestamps from the previous session. - Implemented comprehensive game state persistence, saving enemies, projectiles, puddles, and wave metadata to ensure a seamless "resume where you left off" experience. - Added auto-save triggers to all significant board actions and navigation events. - Ensured backward compatibility with legacy save files using null-safe deserialization. - Excluded Bak Kut Teh buff type from persistence per user preference for randomized selection on wave start. Co-authored-by: candour <4670475+candour@users.noreply.github.com>
|
👋 Jules, reporting for duty! I'm here to lend a hand with this pull request. When you start a review, I'll add a 👀 emoji to each comment to let you know I've read it. I'll focus on feedback directed at me and will do my best to stay out of conversations between you and other bots or reviewers to keep the noise down. I'll push a commit with your requested changes shortly after. Please note there might be a delay between these steps, but rest assured I'm on the job! For more direct control, you can switch me to Reactive Mode. When this mode is on, I will only act on comments where you specifically mention me with New to Jules? Learn more at jules.google/docs. For security, I will only act on instructions from the user who triggered this task. |
📝 WalkthroughWalkthrough
ChangesGame State Persistence Expansion
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Warning Review ran into problems🔥 ProblemsStopped waiting for pipeline failures after 30000ms. One of your pipelines takes longer than our 30000ms fetch window to run, so review may not consider pipeline-failure results for inline comments if any failures occurred after the fetch window. Increase the timeout if you want to wait longer or run a 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. Comment |
Build Successful! 🚀Note: This link will be removed when the PR is closed. |
There was a problem hiding this comment.
Actionable comments posted: 3
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 64664251-7e38-4342-856a-8ac4c0fe997b
📒 Files selected for processing (2)
app/src/main/java/com/messark/hawker/MainViewModel.ktapp/src/main/java/com/messark/hawker/utils/GameStateRepository.kt
- Resolved stall firing issue by persisting simulationTimeMs, ensuring combat timers stay synchronized on resume. - Expanded PersistentGameState to include enemies, projectiles, puddles, and wave metadata for full session resumption. - Offloaded all game saving operations to background threads (Dispatchers.IO) to prevent UI jank. - Added auto-save triggers for all board actions, upgrades, and menu navigation. - Maintained backward compatibility with null-safe deserialization for legacy save files. Co-authored-by: candour <4670475+candour@users.noreply.github.com>
Build Successful! 🚀Note: This link will be removed when the PR is closed. |
|
@CodeRabbit review |
✅ Action performedReview finished.
|
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
app/src/main/java/com/messark/hawker/MainViewModel.kt (1)
103-107:⚠️ Potential issue | 🟠 Major | ⚡ Quick winPause simulation when leaving the game screen.
navigateTo(MAIN_MENU)saves the state, butupdateGame()continues running because it only checksactiveTutorial. During an active wave, enemies can keep progressing in the main menu; if health reaches zero,handleGameOver()deletes the saved game the player expects to resume.🐛 Proposed fix
_gameState.update { state -> - if (state.activeTutorial != null) return@update state + if (state.currentScreen != AppScreen.GAME || state.activeTutorial != null) return@update stateAlso applies to: 420-422
🧹 Nitpick comments (1)
app/src/main/java/com/messark/hawker/MainViewModel.kt (1)
1265-1266: ⚡ Quick winRemove the duplicate save call.
sellStall()schedules two identical saves for the samenewState, doubling IO work and amplifying save-order races.🧹 Proposed cleanup
updateBoostCache(newState) saveGame(newState) - saveGame(newState) newState
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 9e46690e-3f72-4e14-b681-884f40e28f44
📒 Files selected for processing (1)
app/src/main/java/com/messark/hawker/MainViewModel.kt
- Replaced synchronous saveGameState call in applyCheat with the asynchronous saveGame wrapper. - This ensures that applying cheats doesn't cause UI hitches and maintains consistency with the rest of the saving logic. Co-authored-by: candour <4670475+candour@users.noreply.github.com>
Build Successful! 🚀Note: This link will be removed when the PR is closed. |
- Resolved the stall firing bug by persisting 'simulationTimeMs' in the game state. - Expanded 'PersistentGameState' and 'loadGameState' to fully preserve active waves, including enemies, projectiles, puddles, and visual effects. - Refactored 'MainViewModel' to perform all game saving operations asynchronously on 'Dispatchers.IO' to eliminate UI jank during auto-saves. - Added a guard in 'updateGame' to effectively pause the simulation loop whenever the player navigates away from the Game screen, preventing background Game Over events. - Implemented null-safe loading in 'GameStateRepository' to maintain backward compatibility with legacy save files. Co-authored-by: candour <4670475+candour@users.noreply.github.com>
Build Successful! 🚀Note: This link will be removed when the PR is closed. |
There was a problem hiding this comment.
♻️ Duplicate comments (2)
app/src/main/java/com/messark/hawker/MainViewModel.kt (2)
209-213:⚠️ Potential issue | 🟠 Major | ⚡ Quick winSerialize save writes through one path.
saveGame()launches an independent IO write for every mutation, whileGameStateRepository.saveGameState()writes the same file directly. Rapid actions can let an older snapshot finish after a newer one and overwrite progress. Route saves through aMutex/single-writer queue, and have every caller use that path. This is the same persistence race previously discussed, now centralized insaveGame().🛠️ Suggested direction
+ private val saveMutex = Mutex() + fun saveGame(state: GameState = _gameState.value) { viewModelScope.launch(Dispatchers.IO) { - gameStateRepository.saveGameState(state) + saveMutex.withLock { + gameStateRepository.saveGameState(state) + } } }import kotlinx.coroutines.* import kotlinx.coroutines.flow.* +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock
182-201:⚠️ Potential issue | 🟠 Major | ⚡ Quick winKeep
_gameState.updatetransforms side-effect free.These transforms still call
saveGame(newState).MutableStateFlow.updatecan re-run its lambda under contention, so this can enqueue duplicate saves or save a candidate state that was not committed. UseupdateAndGet/post-update saving for mutations that actually changed state.#!/bin/bash # Description: Find save calls still executed from inside MutableStateFlow.update transforms. # Expectation after the fix: no saveGame(...) call appears before the matching end of an _gameState.update lambda. rg -n -C 4 '_gameState\.update\s*\{|saveGame\(' app/src/main/java/com/messark/hawker/MainViewModel.kt🛠️ Suggested pattern
- _gameState.update { + val committedState = _gameState.updateAndGet { val newState = it.copy( // mutation fields... ) - saveGame(newState) newState } + saveGame(committedState)Also applies to: 291-304, 420-545, 1065-1099, 1192-1215, 1236-1266, 1271-1295, 1338-1346, 1352-1360, 1366-1374, 1429-1444, 1454-1473, 1482-1512, 1529-1532
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 248e3739-ddaf-4ea3-a076-ac7a05e5671c
📒 Files selected for processing (2)
app/src/main/java/com/messark/hawker/MainViewModel.ktapp/src/main/java/com/messark/hawker/utils/GameStateRepository.kt
🚧 Files skipped from review as they are similar to previous changes (1)
- app/src/main/java/com/messark/hawker/utils/GameStateRepository.kt
… pause - Fixed stall firing bug by persisting 'simulationTimeMs' in the game state. - Implemented a serialized save queue using a 'ConflatedChannel' to ensure background I/O operations are processed in order and don't block the UI thread. - Added a guard in 'updateGame' to effectively pause the simulation loop whenever the player is not on the 'GAME' screen, preventing background progress or Game Over events. - Expanded 'PersistentGameState' and 'loadGameState' to fully preserve active waves, including enemies, projectiles, puddles, visual effects, and Kitchelin Star bonuses. - Ensured backward compatibility with null-safe deserialization for older save files. Co-authored-by: candour <4670475+candour@users.noreply.github.com>
Build Successful! 🚀Note: This link will be removed when the PR is closed. |
… pause - Fixed stall firing bug by persisting 'simulationTimeMs' in the game state, ensuring combat timers remain synchronized on resume. - Implemented a serialized save queue using a 'ConflatedChannel' to ensure background I/O operations are processed in order and don't block the UI thread. - Refactored state updates to be side-effect free by moving 'saveGame' and cache updates outside of '_gameState.update' blocks. - Added a guard in 'updateGame' to effectively pause the simulation loop whenever the player is not on the 'GAME' screen, preventing background progress or unintended Game Over events. - Expanded 'PersistentGameState' and 'loadGameState' to fully preserve active waves, including enemies, projectiles, puddles, visual effects, and Kitchelin Star bonuses. - Ensured backward compatibility with null-safe deserialization for older save files. Co-authored-by: candour <4670475+candour@users.noreply.github.com>
Build Successful! 🚀Note: This link will be removed when the PR is closed. |
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
app/src/main/java/com/messark/hawker/MainViewModel.kt (1)
557-571:⚠️ Potential issue | 🟠 Major | 🏗️ Heavy liftSide effects inside
updateAndGetmay execute with uncommitted state.
updateBoostCache(newState)and theshouldSave = truemutation are inside theupdateAndGettransform. Per Kotlin coroutines documentation, this transform may be evaluated multiple times if CAS fails. If attempt 1 triggers wave completion and callsupdateBoostCache(state_v1), but CAS fails and attempt 2 doesn't trigger completion,stallBoostsreflectsstate_v1while the committed state isstate_v2.🛠️ Suggested pattern
Track wave completion detection externally and apply side effects after the atomic update:
+var waveWasActive = false var shouldSave = false val finalUpdatedState = _gameState.updateAndGet { state -> if (state.currentScreen != AppScreen.GAME || state.activeTutorial != null) return@updateAndGet state + waveWasActive = state.waveActive && state.enemiesToSpawn == 0 && state.enemies.isEmpty() // ... existing transform logic ... if (newState.waveActive && newState.enemiesToSpawn == 0 && newState.enemies.isEmpty()) { // ... wave completion logic ... - updateBoostCache(newState) - shouldSave = true } newState } +// Check if wave just completed by comparing before/after +if (!finalUpdatedState.waveActive && waveWasActive) { + updateBoostCache(finalUpdatedState) + saveGame(finalUpdatedState) +}Alternatively, detect wave completion by comparing committed state against known invariants outside the transform.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: d9f7b5d1-2732-4de3-bbb4-37c239152645
📒 Files selected for processing (3)
app/src/main/java/com/messark/hawker/MainViewModel.ktapp/src/main/java/com/messark/hawker/utils/GameStateRepository.ktapp/src/test/java/com/messark/hawker/SpatialIndexIntegrationTest.kt
🚧 Files skipped from review as they are similar to previous changes (1)
- app/src/main/java/com/messark/hawker/utils/GameStateRepository.kt
This PR fixes a bug where towers would stop firing after a player exited to the main menu and resumed the game. The root cause was that the
simulationTimeMsclock (which regulates combat timing) was not being saved, causing it to reset to zero on resume while towers still held fire-cooldown timestamps from the previous session.In addition to fixing the clock, I've expanded the save system to persist the full active wave state, including:
I also ensured that the game auto-saves whenever the player makes a change to the board (building, selling, or upgrading stalls) or navigates away from the game screen. Backward compatibility is maintained for older save files.
PR created automatically by Jules for task 977308410548281756 started by @candour
Summary by CodeRabbit
Release Notes
Bug Fixes
Tests