import 'dart:async' as async; import 'package:flame/components.dart'; import 'package:flame/effects.dart'; import 'package:flutter/services.dart'; import '../audio/runtime_audio_manager.dart'; import '../lifecycle/runtime_task_registry.dart'; import '../models/game_diff.dart'; import '../models/runtime_command.dart'; import '../models/runtime_event.dart'; import '../models/runtime_node.dart'; import '../protocol/runtime_protocol.dart'; import '../rendering/render_tree_controller.dart'; import '../resources/game_resource_manager.dart'; import '../rendering/runtime_component.dart'; import 'runtime_command_registry.dart'; // These part files keep CommandExecutor as a single private implementation // unit while grouping command handlers by responsibility. They are not a // plugin system and should not expose additional public API. part 'command_target_effects.dart'; part 'command_composite.dart'; part 'command_audio.dart'; part 'command_resources.dart'; part 'command_lifecycle_context.dart'; part 'command_toast.dart'; part 'command_validation.dart'; part 'command_support.dart'; class CommandExecutor { CommandExecutor({ required RenderTreeController renderTree, required void Function(RuntimeEvent event) eventSink, RuntimeAudioManager? audio, GameResourceManager? resources, Vector2? overlaySize, }) : _renderTree = renderTree, _eventSink = eventSink, _audio = audio, _resources = resources, _overlaySize = overlaySize ?? Vector2(720, 720); final RenderTreeController _renderTree; final void Function(RuntimeEvent event) _eventSink; final RuntimeAudioManager? _audio; final GameResourceManager? _resources; final Vector2 _overlaySize; late final RuntimeTaskRegistry<_CommandResult> _tasks = RuntimeTaskRegistry<_CommandResult>( cancelledValue: _CommandResult.cancelled, ); final RuntimeCommandRegistry _commandRegistry = RuntimeCommandRegistry(); final Set _ownedBgmChannels = {}; final Map> _bgmChannelsByScope = {}; final Map _bgmScopeByChannel = {}; int _toastSerial = 0; bool _disposed = false; void dispose() { _disposed = true; _commandRegistry.dispose(); _tasks.dispose(); final channels = _ownedBgmChannels.toList(growable: false); _ownedBgmChannels.clear(); _bgmChannelsByScope.clear(); _bgmScopeByChannel.clear(); for (final channel in channels) { async.unawaited(_audio?.stopBgm(channel: channel)); } } void cancelScope(String scope) { _commandRegistry.cancelScope(scope); _tasks.cancelScope(scope); final channels = _bgmChannelsByScope.remove(scope) ?? const {}; for (final channel in channels) { _bgmScopeByChannel.remove(channel); _ownedBgmChannels.remove(channel); async.unawaited(_audio?.stopBgm(channel: channel)); } } void executeAll(List commands) { for (final command in commands) { execute(command); } } void execute(RuntimeCommand command) { if (_disposed) { return; } _validate(command); async.unawaited(_execute(command, const _CommandContext())); } Future<_CommandResult> _execute( RuntimeCommand command, _CommandContext context, ) async { if (_disposed) { return _CommandResult.cancelled; } final commandContext = _commandContextFor(command, context); final handle = _createCommandHandle(command, commandContext); try { if (handle?.isCancelled ?? false) { return _CommandResult.cancelled; } final result = await _executeCore(command, commandContext, handle); if (handle?.isCancelled ?? false) { return _CommandResult.cancelled; } return result; } finally { handle?.complete(); } } Future<_CommandResult> _executeCore( RuntimeCommand command, _CommandContext context, RuntimeCommandHandle? handle, ) async { if (_disposed || (handle?.isCancelled ?? false)) { return _CommandResult.cancelled; } switch (command.type) { case RuntimeCommandType.movePath: return _movePath(command, context, handle); case RuntimeCommandType.moveTo: return _targetEffect(command, context, handle, (component, duration) { return MoveToEffect( _requiredVector(command), EffectController(duration: duration), ); }); case RuntimeCommandType.fadeTo: return _targetEffect(command, context, handle, (component, duration) { final alpha = _requiredNormalizedDouble( command.payload['alpha'], 'fade_to.alpha', ); final start = component.renderAlpha; return FunctionEffect((progress, _) { final t = _readDouble(progress) ?? 1; component.setRuntimeAlpha(start + (alpha - start) * t); }, EffectController(duration: duration)); }); case RuntimeCommandType.scaleTo: return _targetEffect(command, context, handle, (component, duration) { final scale = _requiredDouble( command.payload['scale'], 'scale_to.scale', ); return ScaleEffect.to( Vector2.all(scale), EffectController(duration: duration), ); }); case RuntimeCommandType.rotateTo: return _targetEffect(command, context, handle, (component, duration) { final angle = _requiredDouble( command.payload['angle'], 'rotate_to.angle', ); return RotateEffect.to(angle, EffectController(duration: duration)); }); case RuntimeCommandType.removeNode: return _removeNode(command, context); case RuntimeCommandType.sequence: return _sequence(command, context, handle); case RuntimeCommandType.parallel: return _parallel(command, context, handle); case RuntimeCommandType.delay: return _delay(command, context, handle); case RuntimeCommandType.toast: return _toast(command, context, handle); case RuntimeCommandType.playSound: return _playSound(command, context, handle); case RuntimeCommandType.playBgm: return _playBgm(command, context, handle); case RuntimeCommandType.pauseBgm: return _controlBgm(command, context, _BgmControl.pause); case RuntimeCommandType.resumeBgm: return _controlBgm(command, context, _BgmControl.resume); case RuntimeCommandType.stopBgm: return _controlBgm(command, context, _BgmControl.stop); case RuntimeCommandType.preloadResources: return _preloadResources(command, context, handle); case RuntimeCommandType.evictResources: return _evictResources(command, context, handle); case RuntimeCommandType.cancelCommands: return _cancelCommands(command, context); case RuntimeCommandType.playSpineAnimation: return _playSpineAnimation(command, context); case RuntimeCommandType.copyText: await Clipboard.setData( ClipboardData(text: _requiredText(command, 'copy_text.text')), ); _emitCommandCompletion(command, context); return _CommandResult.completed; default: throw UnsupportedError('Unsupported runtime command: ${command.type}'); } } } class _CommandContext { const _CommandContext({this.scope, this.scopeEpoch, this.group}); final String? scope; final int? scopeEpoch; final String? group; _CommandContext copyWith({String? scope, int? scopeEpoch, String? group}) { return _CommandContext( scope: scope ?? this.scope, scopeEpoch: scopeEpoch ?? this.scopeEpoch, group: group ?? this.group, ); } } enum _CommandResult { completed, cancelled } enum _BgmControl { pause, resume, stop }