simvx.editor.autosave

Editor autosave + crash recovery.

The editor’s autosave is conceptually distinct from :class:~simvx.core.SaveManager:

  • SaveManager snapshots only Property descriptors flagged persist=True – a deliberately narrow contract for in-game state.

  • The editor’s autosave needs the full scene structure so that, after a crash, no newly-added nodes are lost. We therefore reuse the engine’s in-memory scene snapshot helper (:func:simvx.core._scene_internal._serialize_node) and pickle that dict. Restoring goes through :func:_deserialize_node.

Both paths share :func:simvx.core.save_manager.pickle_atomic for the write-tmp + fsync + rename + two-deep .bak/.bak2 rotation, so the crash-safety guarantees are identical.

That split is deliberate, not an oversight waiting to be tidied away. The two paths answer different questions (what a game chose to persist, versus what the editor must not lose in a crash), so a future “unify everything” refactor has to start by deciding which of the two guarantees it is prepared to give up. Until then, do not route the editor’s autosave through persist=True: a newly-added node carries no persisted Properties, and losing it is the exact failure this module exists to prevent.

The autosave file lives at $XDG_DATA_HOME/simvx/editor/autosave.sav (falling back to ~/.local/share/simvx/editor/autosave.sav). One file – the most-recent project’s working tree – because the editor only ever has one foreground workspace.

Module Contents

Classes

Autosave

Periodic full-tree autosave with crash-recovery on launch.

AutosaveRecoveryDialog

Modal prompt shown on startup when a fresh autosave is available.

Functions

format_recovery_message

Build the human-readable message body for :class:AutosaveRecoveryDialog.

Data

log

API

simvx.editor.autosave.log

‘getLogger(…)’

class simvx.editor.autosave.Autosave(state, autosave_dir: pathlib.Path | None = None)[source]

Periodic full-tree autosave with crash-recovery on launch.

Polled each frame via :meth:poll. When the autosave interval has elapsed and the editor’s scene is marked modified, a pickle of

Func:

_serialize_node(scene_root) is written atomically to <autosave_dir>/autosave.sav. On startup, :meth:check_recovery returns the autosave envelope when it is newer than the most-recently opened project file (if any).

Initialization

INTERVAL

60.0

property autosave_path: pathlib.Path[source]

Single canonical autosave file path.

poll(now: float = 0.0) → None[source]

Save the working tree if the interval has elapsed and it’s dirty.

check_recovery(project_path: pathlib.Path | None = None) → dict | None[source]

Return the autosave envelope when recovery should be offered.

Returns None when:

  • no autosave file exists, or

  • a project_path was supplied and the autosave is older than the project file (i.e. the project’s on-disk state is already fresher).

Otherwise returns the decoded envelope dict (caller decides whether to show a dialog and/or restore).

restore(envelope: dict)[source]

Reconstruct a Node tree from a recovery envelope.

The scene’s own module is imported first. A .py scene’s root class lives in a module named after the file’s path, which is on no search path, so nothing brings it back on its own: recovery runs in a process started after the crash, where neither sys.modules nor Node._registry has ever heard of the class. Without the import the rebuild raises Unknown node type on the scene’s own root.

The caller is responsible for swapping the returned root into the editor’s active scene tab. Raises RuntimeError if the envelope is malformed (missing scene key); strict per the engine’s error philosophy.

discard() → None[source]

Delete the live autosave file (keeps .bak / .bak2).

class simvx.editor.autosave.AutosaveRecoveryDialog(**kwargs)[source]

Bases: simvx.core.Panel

Modal prompt shown on startup when a fresh autosave is available.

Three outcomes, exposed as signals:

  • attr:

    restore_requested – user wants the autosaved tree swapped in.

  • attr:

    discard_requested – delete the autosave and continue with the project file as-is.

  • attr:

    cancelled – close the dialog without acting (e.g. user wants to inspect things first; autosave is left intact for later).

Initialization

pointer_filter

‘Property(…)’

DIALOG_WIDTH

380.0

DIALOG_HEIGHT

180.0

BUTTON_HEIGHT

32.0

BUTTON_GAP

8.0

visible

‘Property(…)’

show_dialog(message: str, parent_size: simvx.core.Vec2 | None = None) → None[source]

Reveal the dialog with the given message body.

gui_input(event)[source]
style

‘ThemeStyleBox(…)’

property bg_colour
property border_colour
property border_width
get_minimum_size() → simvx.core.math.types.Vec2
on_draw(renderer)
size_x

‘Property(…)’

size_y

‘Property(…)’

min_size_x

‘Property(…)’

min_size_y

‘Property(…)’

anchor_left

‘Property(…)’

anchor_top

‘Property(…)’

anchor_right

‘Property(…)’

anchor_bottom

‘Property(…)’

margin_left

‘Property(…)’

margin_top

‘Property(…)’

margin_right

‘Property(…)’

margin_bottom

‘Property(…)’

anchor

‘Property(…)’

inset

‘Property(…)’

dismiss_on_outside_click

‘Property(…)’

clip_contents

‘Property(…)’

focus_mode

‘Property(…)’

tooltip

‘Property(…)’

cursor_shape

‘Property(…)’

pointer_types: frozenset[simvx.core.input.enums.PointerType] | None

None

touch_target: bool

False

property wants_text_input: bool
size_flags_horizontal

‘Property(…)’

size_flags_vertical

‘Property(…)’

stretch_ratio

‘Property(…)’

max_size

‘Property(…)’

property size: simvx.core.math.types.Vec2
property min_size: simvx.core.math.types.Vec2
content_signature() → object
content_uses(chars: str) → bool
autosize_to_content() → None
accepts_pointers

‘Property(…)’

property theme: simvx.core.ui.types.Theme | None
property mouse_over: bool
property disabled: bool
get_theme() → simvx.core.ui.types.Theme
property viewport_size: simvx.core.math.types.Vec2
property viewport_insets: tuple[float, float, float, float]
queue_redraw()
size_floor() → tuple[float, float]
add_child(node: simvx.core.node.T) → simvx.core.node.T
get_rect() → tuple[float, float, float, float]
get_global_rect() → tuple[float, float, float, float]
property focus_rect: tuple[float, float, float, float] | None
is_point_inside(point) → bool
is_hit(event: simvx.core.input.events.PointerEvent) → bool
classmethod __init_subclass__(**kwargs)
activate() → bool
focus_next_control()
focus_previous_control()
set_drag_preview(control: simvx.core.ui.core.Control)
property is_overlay_open: bool
show_overlay(modality: str = 'light', *, dim: bool | None = None, dismiss: bool | None = None, inert: bool | None = None, owner: simvx.core.ui.core.Control | None = None, initial_focus: simvx.core.ui.core.Control | None = None) → None
close_overlay() → None
position

‘_SpatialVecProperty(…)’

rotation

‘Property(…)’

scale

‘_SpatialVecProperty(…)’

top_level

‘Property(…)’

z_index

‘Property(…)’

z_as_relative

‘Property(…)’

render_layer

‘Property(…)’

set_render_layer(index: int, enabled: bool = True) → None
is_on_render_layer(index: int) → bool
property absolute_z_index: int
property rotation_degrees: float
property world_position: simvx.core.math.types.Vec2
property world_rotation: float
property world_scale: simvx.core.math.types.Vec2
property world_transform: tuple[simvx.core.math.types.Vec2, simvx.core.math.types.Vec2, float]
property forward: simvx.core.math.types.Vec2
property right: simvx.core.math.types.Vec2
translate(offset: tuple[float, float] | numpy.ndarray)
rotate(radians: float)
rotate_deg(degrees: float)
look_at(target: tuple[float, float] | numpy.ndarray)
to_global(point) → simvx.core.math.types.Vec2
to_local(point) → simvx.core.math.types.Vec2
transform_points(points: list[simvx.core.math.types.Vec2]) → list[simvx.core.math.types.Vec2]
wrap_screen(margin: float = 20)
post_processed

‘Property(…)’

property transform_render_dirty: bool
strict_errors: ClassVar[bool]

True

dev_checks: ClassVar[bool]

None

dev_warnings: ClassVar[bool]

None

freeze_check_interval: ClassVar[float]

1.0

dynamic: bool

False

update_mode

‘Property(…)’

drag_threshold

‘Property(…)’

focus_entered

‘Signal(…)’

focus_exited

‘Signal(…)’

__properties__: ClassVar[dict[str, simvx.core.descriptors.Property]]

None

property name: str
property visible_in_tree: bool
property input_player: simvx.core.input.players.InputPlayer
property input_context: simvx.core.input.contexts.InputContext | None
reset_error() → None
remove_child(node: simvx.core.node.Node) → None
reparent(new_parent: simvx.core.node.Node)
node_at(path, default=_NO_DEFAULT)
find(target, *, direct: bool = False)
find_all(target, *, direct: bool = False)
expect(target, *, direct: bool = False)
ancestor(target)
walk(*, include_self: bool = True) → collections.abc.Iterator[simvx.core.node.Node]
property path: str
property is_scene_root: bool
add_to_group(group: str)
remove_from_group(group: str)
is_in_group(group: str) → bool
on_ready() → None
on_enter_tree() → None
on_exit_tree() → None
on_update(dt: float) → None
on_fixed_update(dt: float) → None
on_draw_3d(gizmos) → None
on_picked(event: simvx.core.input.events.PickEvent) → bool | None
on_gui_input(event: simvx.core.input.events.GuiInputEvent) → bool | None
start_coroutine(gen: simvx.core.descriptors.Coroutine) → simvx.core.descriptors.CoroutineHandle
stop_coroutine(gen_or_handle)
property focused: bool
drag_threshold_for(pointer_type: simvx.core.input.enums.PointerType | str) → float
grab_focus() → None
release_focus() → None
capture_pointer(pointer_id: int | None = None) → None
release_pointer(pointer_id: int | None = None) → None
property render_dirty: bool
clear_children()
destroy()
property destroying: bool
property destroyed: bool
call_deferred(method: collections.abc.Callable[..., Any], *args: Any) → None
property app
property tree: simvx.core.scene_tree.SceneTree
property physics
property physics_2d
__getitem__(key: str)
classmethod get_properties() → dict[str, simvx.core.descriptors.Property]
__repr__()
simvx.editor.autosave.format_recovery_message(envelope: dict) → str[source]

Build the human-readable message body for :class:AutosaveRecoveryDialog.