Ren'Py Schema Changes: fix_rollback() vs Disabling Rollback for Save Compatibility
0 reputation · 01 Apr 2023, 11:36 UTC
0 reputation · 01 Apr 2023, 11:36 UTC
After a schema change in a Ren'Py project—such as adding a default field to an object stored in the game state—rollback can restore objects that lack the new field. The goal is to prevent rollback from producing an AttributeError while keeping existing saves loadable. Two documented approaches are available: call renpy.fix_rollback() at the schema boundary, or disable rollback for the affected context.
renpy.fix_rollback() prevents rollback from crossing a specific point, which can protect against stale object states. Disabling rollback removes the risk of restoring pre-change objects but changes player-facing behavior. The trade-off is between save compatibility and rollback stability.
The unresolved decision is whether to prioritize saves created before the schema change or the stability of rollback after it. In current Ren'Py releases, rollback behavior and the correct placement of fix_rollback() depend on when the schema change is applied.
Which approach avoids AttributeError without silently breaking rollback for players who rely on it? Does fix_rollback() need to be called before or after the schema change is finalized? How does disabling rollback interact with saves created before the change?
32775 reputation · 01 Apr 2023, 13:08 UTC
Call renpy.fix_rollback() after the schema change has been applied to the live game state (i.e., after migration logic runs and new defaults are in place). This prevents rollback from rewinding into pre-change snapshots while keeping old saves loadable. Disabling rollback entirely is a heavier-handed fix that removes a player-facing feature and is rarely necessary.
Rollback restores pickled object snapshots. When you add a field to a class, old snapshots lack that attribute. Post-change code reading the new field on a rolled-back snapshot raises AttributeError. renpy.fix_rollback() truncates the rollback log at the call point, so the player cannot roll back past the migration boundary. The save file itself is untouched—old saves still load.
To make the schema change rollback-safe by construction, give the field a class-level default or use __getattr__/defaultdict-style fallbacks. This ensures old own resolve the attribute without needing to truncate the history.
The goal is to apply the field default on load (e.g., via an after_load or init-time migration), then call renpy.fix_rollback() so players cannot roll back into the unmigrated state.
init python:
def migrate_schema():
for obj in store.affected_objects:
if not hasattr(obj, 'new_field'):
setattr(obj, 'new_field', default_value)
renpy.fix_rollback()
config.after_load(migrate_schema)
Disabling rollback globally (via config.rollback_enabled = False) eliminates stale-snapshot restores but does not fix old saves—it only stops the crash path. This is a visible regression for players who rely on the mechanic.
Use comments to ask for clarification. Post a solution as an answer.
No question comments on this page.