Skip to content

Rebind TimeService to the loaded player and stats after save load - #123

Merged
dmccoystephenson merged 1 commit into
mainfrom
fix/timeservice-stale-references
Jul 26, 2026
Merged

Rebind TimeService to the loaded player and stats after save load#123
dmccoystephenson merged 1 commit into
mainfrom
fix/timeservice-stale-references

Conversation

@dmccoystephenson

@dmccoystephenson dmccoystephenson commented Jul 26, 2026

Copy link
Copy Markdown
Member

Summary

  • FishE.__init__ builds TimeService(self.player, self.stats) from the new-game defaults, then loads each save file only if it exists and is non-empty. loadPlayer()/loadStats() rebind self.player/self.stats to brand-new objects, but only loadTimeService() rebuilds the TimeService around them — so a slot holding player.json but no timeService.json left self.timeService.player/.stats pointing at the discarded defaults.
  • Everything on the daily tick runs off those references: TimeService.increaseDay credits bank interest to self.player, then hands self.player, self.stats to business.runDailyProduction, investments.runDailyIncome and housing.runDailyRent. With a stale reference, a day rollover silently credited interest, crew catch and rental income to an object nobody reads, and charged rent against it too.
  • Fixed by rebinding self.timeService.player/.stats once after the load block, alongside the existing self.userInterface.player/.timeService repointing.

That slot shape is reachable in practice: SaveFileManager.migrate_old_save_files gates on player.json only and moves whichever of the three files exist; the same shape also results from a save interrupted partway through FishE.save() (which writes player.json first), or from a zero-byte timeService.json that the getsize(...) > 0 guard skips.

Test plan

  • python3 -m compileall -q src tests
  • python3 -m pytest --cov=src --cov-report=term-missing --cov-report=xml:cov.xml — 371 passed (368 before), coverage 94%
  • All three new tests were confirmed to fail against main (stashing only the src/fishE.py change) and pass with it
  • No front-end change — this is game-state wiring in FishE.__init__, below the BaseUserInterface contract, so console/pygame/web are all unaffected
  • No Player/Stats/TimeService field added, renamed or retyped, so schemas/*.json and the *JsonReaderWriter classes are unchanged (re-verified all three still match their reader-writer field-for-field)

New tests in tests/test_fishE.py, driving the real FishE.__init__ against a temp save slot with only the save-slot menu and front-end stubbed:

  • test_init_rebinds_timeService_to_loaded_player_without_timeService_file — slot with player.json + stats.json, no timeService.json
  • test_init_rebinds_timeService_when_only_player_file_present — interrupted-save shape
  • test_init_daily_tick_credits_the_loaded_player — behavioural: increaseDay() reaches the player the game actually reads

Closes #120


This PR description was drafted during a Gardener session (Stephenson-Software/gardener).

loadPlayer()/loadStats() replace self.player/self.stats with new objects,
but only loadTimeService() rebuilds the TimeService around them. A slot
holding player.json without timeService.json therefore left the
TimeService pointing at the discarded new-game defaults, so every daily
tick - bank interest, crew catch, investment income, rent - applied to a
player nobody reads.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

@dmccoystephenson dmccoystephenson left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Self-review: the fix is correct and minimally scoped. Verified loadPlayer/loadStats/loadTimeService are called only from __init__ (src/fishE.py:57, :61, :65 — no other call sites), so a single rebind after the load block covers every path that can replace self.player/self.stats, including the except fallbacks inside the loaders. It runs unconditionally, so the already-correct case (timeService.json present) re-assigns the same objects and is a no-op. Placement is before the self.locations dict is built, so every location gets the same TimeService instance it always did. Schemas and reader-writers re-checked field-for-field and are untouched by this change; no front-end path is involved. Two notes below, neither blocking.

Comment thread src/fishE.py
# the TimeService built from the defaults above would keep pointing at
# the discarded objects, so every daily tick (interest, crew catch,
# investment income, rent) would apply to a player nobody reads.
self.timeService.player = self.player

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Rebinding attributes here keeps the fix small, but it does leave two places that must agree about what the current player is (this block and the TimeService.__init__ that captured them). A more structural fix would be to defer constructing TimeService until after the load block entirely — it is only needed earlier because UserInterfaceFactory.create_user_interface takes it at line 44 so the save-file menu can render. Not worth the churn in this PR, and the comment above documents the coupling, but worth knowing if the load sequence is reworked later.

Comment thread tests/test_fishE.py
# saveFiles ({filename: json-serializable}), with only the save-slot menu
# and the front-end stubbed out - everything else is the real wiring, so
# the load block and the state it hands to TimeService are exercised.
fishE.Player = Player

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

This helper reassigns fishE's module globals without restoring them, which is the same thing createFishE (line 22) and the existing persistence tests already do in this file, so it is consistent rather than new. It is also order-independent in both directions: createFishE re-mocks every global it cares about, and this helper re-binds every real class it needs, so neither can be poisoned by the other regardless of which runs first. Confirmed by running the full suite (371 passed).

@dmccoystephenson
dmccoystephenson merged commit 65aa09f into main Jul 26, 2026
1 check passed
@dmccoystephenson
dmccoystephenson deleted the fix/timeservice-stale-references branch July 26, 2026 07:00
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.

TimeService keeps stale player/stats references when a save slot has no timeService.json

1 participant