Add runtime networking APIs
This commit is contained in:
@@ -1,9 +1,13 @@
|
||||
import 'dart:async' as async;
|
||||
|
||||
import 'package:lua_dardo_plus/lua.dart';
|
||||
|
||||
import '../diagnostics/runtime_diagnostics.dart';
|
||||
import '../models/game_diff.dart';
|
||||
import '../models/runtime_event.dart';
|
||||
import '../network/runtime_network_manager.dart';
|
||||
import '../packages/game_package.dart';
|
||||
import 'runtime_script_services.dart';
|
||||
import 'script_engine.dart';
|
||||
|
||||
class LuaDardoScriptEngine implements ScriptEngine {
|
||||
@@ -13,10 +17,17 @@ class LuaDardoScriptEngine implements ScriptEngine {
|
||||
final RuntimeDiagnostics? _diagnostics;
|
||||
late final LuaState _lua;
|
||||
late final Map<String, String> _moduleScripts;
|
||||
RuntimeScriptServices _services = const RuntimeScriptServices();
|
||||
int _networkRequestCounter = 0;
|
||||
final Set<String> _loadingModules = {};
|
||||
|
||||
@override
|
||||
Future<void> loadPackage(GamePackage package) async {
|
||||
Future<void> loadPackage(
|
||||
GamePackage package, {
|
||||
RuntimeScriptServices services = const RuntimeScriptServices(),
|
||||
}) async {
|
||||
_services = services;
|
||||
_networkRequestCounter = 0;
|
||||
final script = await package.readText(package.manifest.entry);
|
||||
_moduleScripts = {};
|
||||
for (final entry in package.manifest.modules.entries) {
|
||||
@@ -112,9 +123,168 @@ class LuaDardoScriptEngine implements ScriptEngine {
|
||||
_lua.pushDartFunction(_log);
|
||||
_lua.setField(-2, 'log');
|
||||
|
||||
_lua.pushDartFunction(_httpRequest);
|
||||
_lua.setField(-2, 'http_request');
|
||||
|
||||
_lua.pushDartFunction(_wsConnect);
|
||||
_lua.setField(-2, 'ws_connect');
|
||||
|
||||
_lua.pushDartFunction(_wsSend);
|
||||
_lua.setField(-2, 'ws_send');
|
||||
|
||||
_lua.pushDartFunction(_wsClose);
|
||||
_lua.setField(-2, 'ws_close');
|
||||
|
||||
_lua.setGlobal('runtime');
|
||||
}
|
||||
|
||||
int _httpRequest(LuaState lua) {
|
||||
final network = _requireNetwork();
|
||||
final options = _requiredMapArgument(1, 'runtime.http_request(options)');
|
||||
final url = _requiredString(options, 'url');
|
||||
final uri = Uri.parse(url);
|
||||
final id = _optionalString(options, 'id') ?? _nextNetworkRequestId('http');
|
||||
final method = (_optionalString(options, 'method') ?? 'GET').toUpperCase();
|
||||
final headers = _optionalStringMap(options['headers'], 'headers');
|
||||
final body = _optionalString(options, 'body');
|
||||
final timeout = Duration(
|
||||
milliseconds: ((_optionalNumber(options, 'timeout') ?? 15) * 1000)
|
||||
.round(),
|
||||
);
|
||||
async.unawaited(
|
||||
network.httpRequest(
|
||||
RuntimeHttpRequest(
|
||||
id: id,
|
||||
method: method,
|
||||
uri: uri,
|
||||
headers: headers,
|
||||
body: body,
|
||||
timeout: timeout,
|
||||
),
|
||||
),
|
||||
);
|
||||
lua.pushString(id);
|
||||
return 1;
|
||||
}
|
||||
|
||||
int _wsConnect(LuaState lua) {
|
||||
final network = _requireNetwork();
|
||||
final options = _requiredMapArgument(1, 'runtime.ws_connect(options)');
|
||||
final url = _requiredString(options, 'url');
|
||||
final id = _optionalString(options, 'id') ?? _nextNetworkRequestId('ws');
|
||||
network.wsConnect(
|
||||
RuntimeWebSocketConnectRequest(
|
||||
id: id,
|
||||
uri: Uri.parse(url),
|
||||
protocols: _optionalStringList(options['protocols'], 'protocols'),
|
||||
),
|
||||
);
|
||||
lua.pushString(id);
|
||||
return 1;
|
||||
}
|
||||
|
||||
int _wsSend(LuaState lua) {
|
||||
final network = _requireNetwork();
|
||||
final id = lua.toStr(1);
|
||||
if (id == null || id.isEmpty) {
|
||||
throw const FormatException('runtime.ws_send(id, message) requires id');
|
||||
}
|
||||
final message = _formatLuaLogValue(lua, 2);
|
||||
lua.pushBoolean(network.wsSend(id, message));
|
||||
return 1;
|
||||
}
|
||||
|
||||
int _wsClose(LuaState lua) {
|
||||
final network = _requireNetwork();
|
||||
final id = lua.toStr(1);
|
||||
if (id == null || id.isEmpty) {
|
||||
throw const FormatException('runtime.ws_close(id) requires id');
|
||||
}
|
||||
lua.pushBoolean(network.closeWebSocket(id));
|
||||
return 1;
|
||||
}
|
||||
|
||||
RuntimeNetworkManager _requireNetwork() {
|
||||
final network = _services.network;
|
||||
if (network == null) {
|
||||
throw StateError('Runtime network service is not installed');
|
||||
}
|
||||
return network;
|
||||
}
|
||||
|
||||
String _nextNetworkRequestId(String prefix) {
|
||||
_networkRequestCounter += 1;
|
||||
return '$prefix:$_networkRequestCounter';
|
||||
}
|
||||
|
||||
Map<String, Object?> _requiredMapArgument(int index, String label) {
|
||||
final value = _readValue(index);
|
||||
if (value is Map) {
|
||||
return Map<String, Object?>.from(value);
|
||||
}
|
||||
throw FormatException('$label requires a table');
|
||||
}
|
||||
|
||||
String _requiredString(Map<String, Object?> map, String key) {
|
||||
final value = _optionalString(map, key);
|
||||
if (value == null) {
|
||||
throw FormatException('$key must be a non-empty string');
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
String? _optionalString(Map<String, Object?> map, String key) {
|
||||
final value = map[key];
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
if (value is String && value.isNotEmpty) {
|
||||
return value;
|
||||
}
|
||||
throw FormatException('$key must be a non-empty string');
|
||||
}
|
||||
|
||||
double? _optionalNumber(Map<String, Object?> map, String key) {
|
||||
final value = map[key];
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
if (value is num) {
|
||||
return value.toDouble();
|
||||
}
|
||||
throw FormatException('$key must be a number');
|
||||
}
|
||||
|
||||
Map<String, String> _optionalStringMap(Object? value, String key) {
|
||||
if (value == null) {
|
||||
return const {};
|
||||
}
|
||||
if (value is! Map) {
|
||||
throw FormatException('$key must be a string map');
|
||||
}
|
||||
return {
|
||||
for (final entry in value.entries)
|
||||
entry.key.toString(): entry.value?.toString() ?? '',
|
||||
};
|
||||
}
|
||||
|
||||
List<String> _optionalStringList(Object? value, String key) {
|
||||
if (value == null) {
|
||||
return const [];
|
||||
}
|
||||
if (value is List) {
|
||||
return value.map((item) => item.toString()).toList(growable: false);
|
||||
}
|
||||
if (value is Map) {
|
||||
final entries = value.entries.toList()
|
||||
..sort((a, b) => a.key.toString().compareTo(b.key.toString()));
|
||||
return entries
|
||||
.map((entry) => entry.value.toString())
|
||||
.toList(growable: false);
|
||||
}
|
||||
throw FormatException('$key must be a string list');
|
||||
}
|
||||
|
||||
int _log(LuaState lua) {
|
||||
final argumentCount = lua.getTop();
|
||||
final messageParts = <String>[];
|
||||
|
||||
Reference in New Issue
Block a user