import 'dart:async' as async; import 'package:lua_dardo_plus/lua.dart'; import '../diagnostics/runtime_diagnostics.dart'; import '../host/runtime_host_bridge.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 { LuaDardoScriptEngine({RuntimeDiagnostics? diagnostics}) : _diagnostics = diagnostics; final RuntimeDiagnostics? _diagnostics; late final LuaState _lua; late final Map _moduleScripts; RuntimeScriptServices _services = const RuntimeScriptServices(); int _networkRequestCounter = 0; int _hostCallCounter = 0; final Set _loadingModules = {}; @override Future loadPackage( GamePackage package, { RuntimeScriptServices services = const RuntimeScriptServices(), }) { return loadPackages([package], services: services); } @override Future loadPackages( List packages, { RuntimeScriptServices services = const RuntimeScriptServices(), }) async { if (packages.isEmpty) { throw const FormatException('loadPackages requires at least one package'); } _services = services; _networkRequestCounter = 0; _hostCallCounter = 0; _moduleScripts = {}; // 按顺序加载所有包的模块,后加载的同名模块覆盖先加载的。 for (final package in packages) { for (final entry in package.manifest.modules.entries) { _moduleScripts[entry.key] = await package.readText(entry.value); } } // 入口脚本使用最后一个包(游戏包)。 final entryPackage = packages.last; final script = await entryPackage.readText(entryPackage.manifest.entry); _loadingModules.clear(); _lua = LuaState.newState(); _lua.openLibs(); _disableUnsafeGlobals(); _installRuntimeApi(); final ok = _lua.doString(script); if (!ok) { final error = _lua.toStr(-1) ?? 'unknown Lua load error'; _lua.pop(1); throw StateError(error); } } @override bool smokeTest(Map context) { _lua.getGlobal('smoke_test'); if (!_lua.isFunction(-1)) { _lua.pop(1); return true; } _pushValue(context); final status = _lua.pCall(1, 1, 0); if (status != ThreadStatus.luaOk) { final error = _lua.toStr(-1) ?? 'unknown Lua smoke_test error'; _lua.pop(1); throw StateError(error); } final result = _lua.toBoolean(-1); _lua.pop(1); return result; } @override GameDiff init(Map context) { return _callDiffFunction('init', context); } @override GameDiff dispatchEvent(RuntimeEvent event) { return _callDiffFunction('on_event', event.toMap()); } GameDiff _callDiffFunction(String name, Map argument) { _lua.getGlobal(name); if (!_lua.isFunction(-1)) { _lua.pop(1); return GameDiff.empty; } _pushValue(argument); final status = _lua.pCall(1, 1, 0); if (status != ThreadStatus.luaOk) { final error = _lua.toStr(-1) ?? 'unknown Lua runtime error'; _lua.pop(1); throw StateError(error); } if (_lua.isNil(-1)) { _lua.pop(1); return GameDiff.empty; } if (!_lua.isTable(-1)) { _lua.pop(1); throw StateError('Lua function $name must return a table or nil'); } final value = _readValue(-1); _lua.pop(1); if (value is! Map) { throw StateError('Lua function $name returned a non-map value'); } return GameDiff.fromMap(Map.from(value)); } void _installRuntimeApi() { _lua.newTable(); _lua.newTable(); _lua.setField(-2, '__modules'); _lua.pushDartFunction(_importModule); _lua.setField(-2, 'import'); _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.pushDartFunction(_hostCall); _lua.setField(-2, 'host_call'); _lua.pushDartFunction(_hostNotify); _lua.setField(-2, 'host_notify'); _lua.pushDartFunction(_hostRespond); _lua.setField(-2, 'host_respond'); _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; } int _hostCall(LuaState lua) { final host = _requireHostBridge(); final options = _requiredMapArgument(1, 'runtime.host_call(options)'); final method = _requiredString(options, 'method'); final id = _optionalString(options, 'id') ?? _nextHostCallId(); async.unawaited( host.callHost( RuntimeHostCall(id: id, method: method, data: options['data']), ), ); lua.pushString(id); return 1; } int _hostNotify(LuaState lua) { final host = _requireHostBridge(); final options = _requiredMapArgument(1, 'runtime.host_notify(options)'); final method = _requiredString(options, 'method'); lua.pushBoolean( host.notifyHost( RuntimeHostNotification(method: method, data: options['data']), ), ); return 1; } int _hostRespond(LuaState lua) { final host = _requireHostBridge(); final options = _requiredMapArgument(1, 'runtime.host_respond(options)'); final id = _requiredString(options, 'id'); final error = _optionalString(options, 'error'); lua.pushBoolean( host.completeLuaCall(id, result: options['result'], error: error), ); return 1; } RuntimeHostBridgeManager _requireHostBridge() { final hostBridge = _services.hostBridge; if (hostBridge == null) { throw StateError('Runtime host bridge service is not installed'); } return hostBridge; } String _nextHostCallId() { _hostCallCounter += 1; return 'lua:$_hostCallCounter'; } 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 _requiredMapArgument(int index, String label) { final value = _readValue(index); if (value is Map) { return Map.from(value); } throw FormatException('$label requires a table'); } String _requiredString(Map 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 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 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 _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 _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 = []; for (var index = 1; index <= argumentCount; index++) { messageParts.add(_formatLuaLogValue(lua, index)); } final message = messageParts.join(' '); _diagnostics?.record( type: RuntimeDiagnosticType.luaLog, message: message, context: {'argumentCount': argumentCount}, ); return 0; } int _importModule(LuaState lua) { final moduleName = lua.toStr(1); if (moduleName == null || moduleName.isEmpty) { throw const FormatException( 'runtime.import(moduleName) requires a module name', ); } if (!_isSafeModuleName(moduleName)) { throw FormatException('Unsafe Lua module name: $moduleName'); } final source = _moduleScripts[moduleName]; if (source == null) { throw FormatException( 'Lua module is not declared in manifest.modules: $moduleName', ); } lua.getGlobal('runtime'); lua.getField(-1, '__modules'); final modulesIndex = lua.absIndex(-1); lua.getField(modulesIndex, moduleName); if (!lua.isNil(-1)) { lua.remove(-2); lua.remove(-2); return 1; } lua.pop(1); if (_loadingModules.contains(moduleName)) { lua.pop(2); throw FormatException('Circular Lua module import: $moduleName'); } _loadingModules.add(moduleName); try { final loadStatus = lua.loadString(source); if (loadStatus != ThreadStatus.luaOk) { final error = lua.toStr(-1) ?? 'unknown Lua module load error'; lua.pop(3); throw StateError('Failed to load Lua module $moduleName: $error'); } final callStatus = lua.pCall(0, 1, 0); if (callStatus != ThreadStatus.luaOk) { final error = lua.toStr(-1) ?? 'unknown Lua module runtime error'; lua.pop(3); throw StateError('Failed to run Lua module $moduleName: $error'); } if (lua.isNil(-1)) { lua.pop(1); lua.pushBoolean(true); } if (!lua.isTable(-1) && !lua.isBoolean(-1)) { lua.pop(3); throw StateError( 'Lua module $moduleName must return a table, true, or nil', ); } lua.pushValue(-1); lua.setField(modulesIndex, moduleName); lua.remove(-2); lua.remove(-2); return 1; } finally { _loadingModules.remove(moduleName); } } String _formatLuaLogValue(LuaState lua, int index) { if (lua.isNil(index) || lua.isNone(index)) { return 'nil'; } if (lua.isBoolean(index)) { return lua.toBoolean(index).toString(); } if (lua.isInteger(index)) { return lua.toInteger(index).toString(); } if (lua.isNumber(index)) { return lua.toNumber(index).toString(); } if (lua.isString(index)) { return lua.toStr(index) ?? ''; } return lua.typeName2(index); } bool _isSafeModuleName(String value) { return RegExp(r'^[A-Za-z0-9_.-]+$').hasMatch(value) && !value.contains('..') && !value.startsWith('.') && !value.endsWith('.'); } void _disableUnsafeGlobals() { for (final name in const [ 'os', 'package', 'dofile', 'loadfile', 'require', ]) { _lua.pushNil(); _lua.setGlobal(name); } } void _pushValue(Object? value) { switch (value) { case null: _lua.pushNil(); case bool v: _lua.pushBoolean(v); case int v: _lua.pushInteger(v); case double v: _lua.pushNumber(v); case String v: _lua.pushString(v); case List v: _pushList(v); case Map v: _pushMap(v); default: throw UnsupportedError('Unsupported value for Lua: $value'); } } void _pushList(List values) { _lua.newTable(); for (var i = 0; i < values.length; i++) { _pushValue(values[i]); _lua.setI(-2, i + 1); } } void _pushMap(Map values) { _lua.newTable(); for (final entry in values.entries) { _pushValue(entry.value); _lua.setField(-2, entry.key); } } Object? _readValue(int index) { if (_lua.isNil(index) || _lua.isNone(index)) { return null; } if (_lua.isBoolean(index)) { return _lua.toBoolean(index); } if (_lua.isInteger(index)) { return _lua.toInteger(index); } if (_lua.isNumber(index)) { return _lua.toNumber(index); } if (_lua.isString(index)) { return _lua.toStr(index); } if (_lua.isTable(index)) { return _readTable(index); } throw UnsupportedError('Unsupported Lua type: ${_lua.typeName2(index)}'); } Object _readTable(int index) { final tableIndex = _lua.absIndex(index); final length = _lua.rawLen(tableIndex); final list = []; var hasOnlyArrayKeys = length > 0; final map = {}; _lua.pushNil(); while (_lua.next(tableIndex)) { final value = _readValue(-1); final key = _readValue(-2); _lua.pop(1); if (key is int && key >= 1 && key <= length) { while (list.length < key) { list.add(null); } list[key - 1] = value; } else { hasOnlyArrayKeys = false; map[key.toString()] = value; } } if (hasOnlyArrayKeys && map.isEmpty) { return list; } for (var i = 0; i < list.length; i++) { if (list[i] != null) { map[(i + 1).toString()] = list[i]; } } return map; } }