231 lines
7.7 KiB
Dart
231 lines
7.7 KiB
Dart
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<String> _ownedBgmChannels = {};
|
|
final Map<String, Set<String>> _bgmChannelsByScope = {};
|
|
final Map<String, String> _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 <String>{};
|
|
for (final channel in channels) {
|
|
_bgmScopeByChannel.remove(channel);
|
|
_ownedBgmChannels.remove(channel);
|
|
async.unawaited(_audio?.stopBgm(channel: channel));
|
|
}
|
|
}
|
|
|
|
void executeAll(List<RuntimeCommand> 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 }
|