Files
flutter_lua_runtime/lib/runtime/commands/command_support.dart
2026-06-07 22:53:58 +08:00

249 lines
6.4 KiB
Dart

part of 'command_executor.dart';
extension _CommandExecutorSupport on CommandExecutor {
void _appendCompletionEffect(
List<Effect> 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;
}
}