import 'package:lua_dardo_plus/lua.dart'; import '../diagnostics/runtime_diagnostics.dart'; import '../models/game_diff.dart'; import '../models/runtime_event.dart'; import '../packages/game_package.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; final Set _loadingModules = {}; @override Future loadPackage(GamePackage package) async { final script = await package.readText(package.manifest.entry); _moduleScripts = {}; for (final entry in package.manifest.modules.entries) { _moduleScripts[entry.key] = await package.readText(entry.value); } _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.setGlobal('runtime'); } 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; } }