class_name StudioSimulationManager extends Node ## Manages Play / Run / Stop simulation harness for ModBison Studio. ## Takes full state snapshot before play and restores on stop. signal simulation_started(mode: String) signal simulation_stopped enum SimulationState { EDIT, PLAYING, PAUSED } var state: SimulationState = SimulationState.EDIT var _snapshot: Array[Dictionary] = [] var _spawned_avatar: Node = null var _core: EditorCore = null var _world_container: Node3D = null func setup(core: EditorCore, world_container: Node3D = null) -> void: _core = core _world_container = world_container func start_play(spawn_player: bool = true) -> void: if state == SimulationState.PLAYING: return # 1. Capture snapshot of all objects _capture_snapshot() state = SimulationState.PLAYING # 2. Spawn test avatar if requested if spawn_player and _world_container != null: _spawn_test_avatar() # 3. Activate moving objects for m in _get_moving_nodes(): if m.has_method("start_motion"): m.start_motion() simulation_started.emit("play" if spawn_player else "run") func pause_simulation() -> void: if state != SimulationState.PLAYING: return state = SimulationState.PAUSED for m in _get_moving_nodes(): if m.has_method("stop_motion"): m.stop_motion() func resume_simulation() -> void: if state != SimulationState.PAUSED: return state = SimulationState.PLAYING for m in _get_moving_nodes(): if m.has_method("start_motion"): m.start_motion() func stop_simulation() -> void: if state == SimulationState.EDIT: return # 1. Stop all moving objects for m in _get_moving_nodes(): if m.has_method("stop_motion"): m.stop_motion() # 2. Despawn test avatar if _spawned_avatar != null and is_instance_valid(_spawned_avatar): _spawned_avatar.queue_free() _spawned_avatar = null # 3. Restore state snapshot _restore_snapshot() state = SimulationState.EDIT simulation_stopped.emit() func _get_moving_nodes() -> Array: if is_inside_tree() and get_tree() != null: return get_tree().get_nodes_in_group("moving_objects") var main_loop = Engine.get_main_loop() if main_loop is SceneTree: return (main_loop as SceneTree).get_nodes_in_group("moving_objects") return [] func _capture_snapshot() -> void: _snapshot.clear() if _core == null or _core.model == null: return for obj in _core.model.payload.objects: _snapshot.append({ "id": obj.id, "position": obj.position, "rotation": obj.rotation, "scale": obj.scale, "properties": obj.properties.duplicate(true) }) func _restore_snapshot() -> void: if _core == null or _core.model == null: return for snap in _snapshot: var obj = _core.model.find_object(snap["id"]) if obj != null: obj.position = snap["position"] obj.rotation = snap["rotation"] obj.scale = snap["scale"] obj.properties = snap["properties"].duplicate(true) _snapshot.clear() func _spawn_test_avatar() -> void: var player_scene = load("res://scenes/player.tscn") if player_scene != null: var p = player_scene.instantiate() p.name = "TestStudioPlayer" p.position = Vector3(0, 2, 0) _world_container.add_child(p) _spawned_avatar = p else: var p: CharacterBody3D = CharacterBody3D.new() p.name = "TestStudioPlayer" p.position = Vector3(0, 2, 0) _world_container.add_child(p) _spawned_avatar = p func is_playing() -> bool: return state == SimulationState.PLAYING func is_editing() -> bool: return state == SimulationState.EDIT