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

174 lines
4.5 KiB
Dart

import 'runtime_command.dart';
import 'runtime_node.dart';
import '../protocol/runtime_protocol.dart';
class NodeUpdate {
const NodeUpdate({required this.id, required this.props});
final String id;
final Map<String, Object?> props;
static NodeUpdate fromMap(Map<String, Object?> map) {
RuntimeProtocolSchema.ensureKnownKeys(
map,
allowed: RuntimeProtocolSchema.nodeUpdateFields,
context: 'NodeUpdate',
);
final id = map[RuntimeProtocolField.id];
if (id is! String || id.isEmpty) {
throw const FormatException('NodeUpdate.id must be a string');
}
final props = map[RuntimeProtocolField.props];
if (props is! Map) {
throw const FormatException('NodeUpdate.props must be a map');
}
final typedProps = Map<String, Object?>.from(props);
RuntimeProtocolSchema.ensureKnownKeys(
typedProps,
allowed: RuntimeProtocolSchema.nodePropsFields,
context: 'RuntimeNode.props',
);
return NodeUpdate(id: id, props: typedProps);
}
}
class NodeRemove {
const NodeRemove({required this.id});
final String id;
static NodeRemove fromValue(Object? value) {
if (value is String && value.isNotEmpty) {
return NodeRemove(id: value);
}
if (value is Map) {
RuntimeProtocolSchema.ensureKnownKeys(
value,
allowed: RuntimeProtocolSchema.nodeRemoveFields,
context: 'NodeRemove',
);
final id = value[RuntimeProtocolField.id];
if (id is String && id.isNotEmpty) {
return NodeRemove(id: id);
}
}
throw const FormatException('NodeRemove must be an id string or {id}');
}
}
class NodeDiff {
const NodeDiff({
this.creates = const [],
this.updates = const [],
this.removes = const [],
});
final List<RuntimeNode> creates;
final List<NodeUpdate> updates;
final List<NodeRemove> removes;
static NodeDiff empty = const NodeDiff();
static NodeDiff fromMap(Object? value) {
if (value == null) {
return NodeDiff.empty;
}
if (value is! Map) {
throw const FormatException('NodeDiff must be a map');
}
RuntimeProtocolSchema.ensureKnownKeys(
value,
allowed: RuntimeProtocolSchema.nodeDiffFields,
context: 'NodeDiff',
);
return NodeDiff(
creates: _readList(
value[RuntimeProtocolField.creates],
(item) => RuntimeNode.fromMap(Map<String, Object?>.from(item as Map)),
),
updates: _readList(
value[RuntimeProtocolField.updates],
(item) => NodeUpdate.fromMap(Map<String, Object?>.from(item as Map)),
),
removes: _readList(
value[RuntimeProtocolField.removes],
NodeRemove.fromValue,
),
);
}
static List<T> _readList<T>(Object? value, T Function(Object? value) mapper) {
if (value == null) {
return const [];
}
if (value is List) {
return value.map(mapper).toList(growable: false);
}
if (value is Map && value.isEmpty) {
return const [];
}
if (value is Map && value.keys.every(_isPositiveIntegerKey)) {
final entries = value.entries.toList()
..sort(
(a, b) => int.parse(
a.key.toString(),
).compareTo(int.parse(b.key.toString())),
);
return entries
.map((entry) => mapper(entry.value))
.toList(growable: false);
}
throw const FormatException('Diff field must be a list');
}
static bool _isPositiveIntegerKey(Object? key) {
final value = int.tryParse(key.toString());
return value != null && value > 0;
}
}
class GameDiff {
const GameDiff({
required this.render,
required this.ui,
required this.commands,
});
final NodeDiff render;
final NodeDiff ui;
final List<RuntimeCommand> commands;
static const empty = GameDiff(
render: NodeDiff(),
ui: NodeDiff(),
commands: [],
);
static GameDiff fromMap(Map<String, Object?> map) {
RuntimeProtocolSchema.ensureKnownKeys(
map,
allowed: RuntimeProtocolSchema.gameDiffFields,
context: 'GameDiff',
);
final commandsValue = map[RuntimeProtocolField.commands];
final commands = commandsValue == null
? const <RuntimeCommand>[]
: NodeDiff._readList(
commandsValue,
(item) =>
RuntimeCommand.fromMap(Map<String, Object?>.from(item as Map)),
);
return GameDiff(
render: NodeDiff.fromMap(map[RuntimeProtocolField.render]),
ui: NodeDiff.fromMap(map[RuntimeProtocolField.ui]),
commands: commands,
);
}
}