297 lines
7.3 KiB
Dart
297 lines
7.3 KiB
Dart
import 'package:lua_dardo_plus/lua.dart';
|
|
|
|
import '../models/game_diff.dart';
|
|
import '../models/runtime_event.dart';
|
|
import '../packages/game_package.dart';
|
|
import 'script_engine.dart';
|
|
|
|
class LuaDardoScriptEngine implements ScriptEngine {
|
|
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.setGlobal('runtime');
|
|
}
|
|
|
|
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);
|
|
}
|
|
}
|
|
|
|
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;
|
|
}
|
|
}
|