part of 'command_executor.dart'; extension _CommandExecutorSupport on CommandExecutor { void _appendCompletionEffect( List effects, RuntimeCommand command, String target, int targetEpoch, RuntimeTask<_CommandResult> task, String? scope, int? scopeEpoch, ) { effects.add( FunctionEffect((_, __) { if (!_scopeIsAlive(scope) || !_renderTree.isNodeEpochAlive(target, targetEpoch)) { task.cancel(); return; } _emitCompletion(command, target, scope, targetEpoch, scopeEpoch); task.complete(_CommandResult.completed); }, EffectController(duration: 0.01)), ); } void _emitCompletion( RuntimeCommand command, String target, String? scope, [ int? targetEpoch, int? scopeEpoch, ]) { final onComplete = _optionalString( command.payload['onComplete'], 'onComplete', ); if (onComplete == null) { return; } _emitEventIfScopeAlive( RuntimeEvent( type: RuntimeEventType.animationDone, target: target, handler: onComplete, ), scope, targetEpoch: targetEpoch, scopeEpoch: scopeEpoch, ); } void _emitCommandCompletion(RuntimeCommand command, _CommandContext context) { final onComplete = _optionalString( command.payload['onComplete'], 'onComplete', ); if (onComplete == null) { return; } _emitEventIfScopeAlive( RuntimeEvent( type: RuntimeEventType.animationDone, target: command.target, handler: onComplete, ), _completionScopeFor(command, context), scopeEpoch: context.scopeEpoch, ); } void _emitEventIfScopeAlive( RuntimeEvent event, String? scope, { int? targetEpoch, int? scopeEpoch, }) { if (!_scopeIsAlive(scope) || _disposed) { return; } _eventSink( event.withLifecycle( scope: scope, targetEpoch: targetEpoch, scopeEpoch: scopeEpoch, ), ); } Vector2 _requiredVector(RuntimeCommand command) { final x = _readDouble(command.payload['x']); final y = _readDouble(command.payload['y']); if (x == null || y == null) { throw FormatException('${command.type}.x/y are required numbers'); } return Vector2(x, y); } String _requiredTarget(RuntimeCommand command) { final target = command.target; if (target == null || target.isEmpty) { throw FormatException('${command.type}.target is required'); } return target; } String _requiredText(RuntimeCommand command, String field) { return _optionalString(command.payload['text'], field)!; } double _duration(RuntimeCommand command, {required double defaultValue}) { final duration = _readDouble(command.payload['duration']) ?? defaultValue; if (duration < 0) { throw FormatException('${command.type}.duration must be >= 0'); } return duration; } double _requiredDouble(Object? value, String field) { final result = _readDouble(value); if (result == null) { throw FormatException('$field must be a number'); } return result; } void _registerBgmChannel({required String channel, required String? scope}) { _unregisterBgmChannel(channel); _ownedBgmChannels.add(channel); if (scope == null) { return; } _bgmScopeByChannel[channel] = scope; _bgmChannelsByScope.putIfAbsent(scope, () => {}).add(channel); } void _unregisterBgmChannel(String channel) { _ownedBgmChannels.remove(channel); final oldScope = _bgmScopeByChannel.remove(channel); if (oldScope == null) { return; } final channels = _bgmChannelsByScope[oldScope]; channels?.remove(channel); if (channels != null && channels.isEmpty) { _bgmChannelsByScope.remove(oldScope); } } String _audioChannel(RuntimeCommand command) { return _optionalString(command.payload['channel'], 'channel') ?? RuntimeAudioChannel.defaultBgm; } String _requiredResourceGroup(RuntimeCommand command) { return _optionalString(command.payload['group'], '${command.type}.group')!; } void _validateCancelCommands(RuntimeCommand command) { final id = _optionalString(command.payload['id'], 'cancel_commands.id'); final group = _optionalString( command.payload['group'], 'cancel_commands.group', ); final scope = _optionalString( command.payload['scope'], 'cancel_commands.scope', ); if (id == null && group == null && scope == null) { throw const FormatException( 'cancel_commands requires id, group or scope', ); } } String _requiredAudioResource(RuntimeCommand command) { final asset = command.payload['asset'] ?? command.payload['name']; return _optionalString(asset, 'play_sound.asset/name')!; } String _requiredSpineAnimation(RuntimeCommand command) { final value = _optionalString( command.payload['animation'], 'play_spine_animation.animation', ); if (value == null) { throw const FormatException( 'play_spine_animation.animation must be a non-empty string', ); } return value; } bool? _optionalBool(Object? value, String field) { if (value == null) { return null; } if (value is bool) { return value; } throw FormatException('$field must be a boolean'); } double _optionalVolume(RuntimeCommand command) { final value = command.payload['volume']; if (value == null) { return 1; } return _requiredNormalizedDouble(value, 'play_sound.volume'); } double _requiredNormalizedDouble(Object? value, String field) { final result = _requiredDouble(value, field); if (result < 0 || result > 1) { throw FormatException('$field must be between 0 and 1'); } return result; } String? _optionalString(Object? value, String field) { if (value == null) { return null; } if (value is String && value.isNotEmpty) { return value; } throw FormatException('$field must be a non-empty string'); } int? _optionalInt(Object? value, String field) { if (value == null) { return null; } if (value is num) { return value.toInt(); } throw FormatException('$field must be an integer'); } double? _readDouble(Object? value) { if (value == null) { return null; } if (value is num) { return value.toDouble(); } return null; } }