Files
flutter_lua_runtime/lib/runtime/scripting/lua_dardo_script_engine.dart
2026-06-09 10:55:08 +08:00

340 lines
8.4 KiB
Dart

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<String, String> _moduleScripts;
final Set<String> _loadingModules = {};
@override
Future<void> 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<String, Object?> 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<String, Object?> context) {
return _callDiffFunction('init', context);
}
@override
GameDiff dispatchEvent(RuntimeEvent event) {
return _callDiffFunction('on_event', event.toMap());
}
GameDiff _callDiffFunction(String name, Map<String, Object?> 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<String, Object?>.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 = <String>[];
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<Object?> v:
_pushList(v);
case Map<String, Object?> v:
_pushMap(v);
default:
throw UnsupportedError('Unsupported value for Lua: $value');
}
}
void _pushList(List<Object?> values) {
_lua.newTable();
for (var i = 0; i < values.length; i++) {
_pushValue(values[i]);
_lua.setI(-2, i + 1);
}
}
void _pushMap(Map<String, Object?> 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 = <Object?>[];
var hasOnlyArrayKeys = length > 0;
final map = <String, Object?>{};
_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;
}
}