Compare commits

...

2 Commits

Author SHA1 Message Date
gem
8d2c97269a Fix runtime color alpha composition 2026-06-09 11:26:51 +08:00
gem
45ab9d7861 Add Lua debug logging API 2026-06-09 10:55:08 +08:00
13 changed files with 163 additions and 11 deletions

View File

@@ -1,5 +1,9 @@
# Changelog
## Unreleased
- Fixed Runtime node color alpha composition so `#AARRGGBB` alpha now multiplies with node/runtime alpha instead of being overwritten.
## 0.1.0
- Initial extracted package skeleton for `flame_lua_runtime`.

View File

@@ -34,6 +34,16 @@ Supported node concepts include:
Lua may compose higher-level widgets, but those widgets must normalize into supported runtime nodes.
### Color and alpha
`RuntimeNode.color` supports `#RRGGBB` and `#AARRGGBB`. When the alpha channel is present in the color, it is multiplied with `RuntimeNode.alpha` and any runtime animation alpha.
```text
Final opacity = color alpha × node alpha × runtime animation alpha
```
`#RRGGBB` colors behave as fully opaque colors. `#00000000` is fully transparent.
## RuntimeCommand
Runtime commands request generic side effects owned by Dart/Flame.

View File

@@ -552,6 +552,7 @@
---@class RuntimeImportApi
---@field import fun(moduleName: string): table
---@field log fun(...: any)
---@type RuntimeImportApi
runtime = runtime

View File

@@ -552,6 +552,7 @@
---@class RuntimeImportApi
---@field import fun(moduleName: string): table
---@field log fun(...: any)
---@type RuntimeImportApi
runtime = runtime

View File

@@ -552,6 +552,7 @@
---@class RuntimeImportApi
---@field import fun(moduleName: string): table
---@field log fun(...: any)
---@type RuntimeImportApi
runtime = runtime

View File

@@ -552,6 +552,7 @@
---@class RuntimeImportApi
---@field import fun(moduleName: string): table
---@field log fun(...: any)
---@type RuntimeImportApi
runtime = runtime

View File

@@ -130,6 +130,7 @@ String _formatDebugValue(Object? value) {
}
enum RuntimeDiagnosticType {
luaLog,
luaEventError,
diffApplyError,
packageActivationError,

View File

@@ -1,6 +1,7 @@
import 'package:flame/game.dart';
import 'package:flutter/widgets.dart';
import '../diagnostics/runtime_diagnostics.dart';
import '../packages/game_package_repository.dart';
import '../scripting/lua_dardo_script_engine.dart';
import 'flame_lua_game.dart';
@@ -24,10 +25,13 @@ class LuaGameWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
final diagnostics = RuntimeDiagnostics();
return GameWidget(
game: FlameLuaGame(
scriptEngine: LuaDardoScriptEngine(),
scriptEngineFactory: LuaDardoScriptEngine.new,
scriptEngine: LuaDardoScriptEngine(diagnostics: diagnostics),
scriptEngineFactory: () =>
LuaDardoScriptEngine(diagnostics: diagnostics),
diagnostics: diagnostics,
packageRepository:
packageRepository ??
(serverUrl == null

View File

@@ -12,6 +12,11 @@ import '../models/runtime_node.dart';
import '../protocol/runtime_protocol.dart';
import '../resources/game_resource_manager.dart';
@visibleForTesting
Color composeRuntimeColorAlpha(Color color, double alpha) {
return color.withValues(alpha: color.a * alpha.clamp(0.0, 1.0));
}
class RuntimeComponent extends PositionComponent
with HasVisibility, TapCallbacks {
RuntimeComponent({
@@ -156,7 +161,10 @@ class RuntimeComponent extends PositionComponent
}
final paint = Paint()
..color = (_node.color ?? _defaultColor()).withValues(alpha: renderAlpha);
..color = composeRuntimeColorAlpha(
_node.color ?? _defaultColor(),
renderAlpha,
);
canvas.save();
canvas.transform(Float64List.fromList(transform.transformMatrix.storage));
@@ -177,7 +185,10 @@ class RuntimeComponent extends PositionComponent
@override
void render(Canvas canvas) {
final paint = Paint()
..color = (_node.color ?? _defaultColor()).withValues(alpha: renderAlpha);
..color = composeRuntimeColorAlpha(
_node.color ?? _defaultColor(),
renderAlpha,
);
switch (_node.type) {
case RuntimeNodeType.circle:
@@ -224,7 +235,7 @@ class RuntimeComponent extends PositionComponent
final rect = Rect.fromLTWH(0, 0, size.x, size.y);
final radius = Radius.circular(_node.radius ?? 4);
final backgroundPaint = Paint()
..color = const Color(0x33475569).withValues(alpha: renderAlpha);
..color = composeRuntimeColorAlpha(const Color(0x33475569), renderAlpha);
canvas.drawRRect(RRect.fromRectAndRadius(rect, radius), backgroundPaint);
final value = _node.value ?? 0;
@@ -327,11 +338,15 @@ class RuntimeComponent extends PositionComponent
final contentRect = _listViewContentRect();
final scrollbars = _listViewScrollbarVisibility();
final trackPaint = Paint()
..color = (_node.scrollbarTrackColor ?? const Color(0x33475569))
.withValues(alpha: renderAlpha);
..color = composeRuntimeColorAlpha(
_node.scrollbarTrackColor ?? const Color(0x33475569),
renderAlpha,
);
final thumbPaint = Paint()
..color = (_node.scrollbarThumbColor ?? const Color(0xaa94a3b8))
.withValues(alpha: renderAlpha);
..color = composeRuntimeColorAlpha(
_node.scrollbarThumbColor ?? const Color(0xaa94a3b8),
renderAlpha,
);
final contentHeight = _node.contentHeight;
if (scrollbars.vertical &&
@@ -415,7 +430,7 @@ class RuntimeComponent extends PositionComponent
image,
Rect.fromLTWH(0, 0, image.width.toDouble(), image.height.toDouble()),
rect,
Paint()..color = Colors.white.withValues(alpha: renderAlpha),
Paint()..color = composeRuntimeColorAlpha(Colors.white, renderAlpha),
);
return;
}
@@ -948,7 +963,7 @@ class RuntimeComponent extends PositionComponent
final text = label ?? '';
final color = _textColor(node);
final style = TextStyle(
color: color.withValues(alpha: renderAlpha),
color: composeRuntimeColorAlpha(color, renderAlpha),
fontSize: node.fontSize ?? 18,
fontWeight: node.type == RuntimeNodeType.button
? FontWeight.w600

View File

@@ -1,11 +1,16 @@
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 = {};
@@ -104,9 +109,28 @@ class LuaDardoScriptEngine implements ScriptEngine {
_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) {
@@ -179,6 +203,25 @@ class LuaDardoScriptEngine implements ScriptEngine {
}
}
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('..') &&

View File

@@ -3,6 +3,7 @@ import 'package:flame_lua_runtime/runtime/models/runtime_node.dart';
import 'package:flame_lua_runtime/runtime/protocol/runtime_protocol.dart';
import 'package:flame_lua_runtime/runtime/rendering/runtime_component.dart';
import 'package:flame_lua_runtime/runtime/resources/game_resource_manager.dart';
import 'package:flutter/material.dart' show Color;
import 'package:flutter_test/flutter_test.dart';
void main() {
@@ -114,6 +115,29 @@ void main() {
expect(component.renderAlpha, 1);
});
test('multiplies color alpha with node and runtime alpha', () {
expect(
composeRuntimeColorAlpha(const Color(0xffffffff), 1).a,
closeTo(1, 0.001),
);
expect(
composeRuntimeColorAlpha(const Color(0x80ffffff), 1).a,
closeTo(0.5, 0.003),
);
expect(
composeRuntimeColorAlpha(const Color(0x80ffffff), 0.5).a,
closeTo(0.25, 0.003),
);
expect(
composeRuntimeColorAlpha(const Color(0x00ffffff), 1).a,
closeTo(0, 0.001),
);
expect(
composeRuntimeColorAlpha(const Color(0x80ffffff), 0.25).a,
closeTo(0.125, 0.003),
);
});
test('multi-line non-button text is top aligned', () {
final component = RuntimeComponent(
node: const RuntimeNode(

View File

@@ -1,5 +1,6 @@
import 'dart:io';
import 'package:flame_lua_runtime/runtime/diagnostics/runtime_diagnostics.dart';
import 'package:flame_lua_runtime/runtime/packages/game_package.dart';
import 'package:flame_lua_runtime/runtime/packages/game_package_manifest.dart';
import 'package:flame_lua_runtime/runtime/models/runtime_event.dart';
@@ -895,6 +896,51 @@ end
expect(c.y, 53);
});
test('runtime.log records Lua debug messages in diagnostics', () async {
final package = await _createPackage(
mainScript: '''
function smoke_test(ctx)
runtime.log("smoke", ctx.runtimeApiVersion)
return true
end
function init(ctx)
runtime.log("init", true, nil)
return {}
end
function on_event(event)
runtime.log("event", event.type, event.target)
return {}
end
''',
);
final diagnostics = RuntimeDiagnostics();
final engine = LuaDardoScriptEngine(diagnostics: diagnostics);
await engine.loadPackage(package);
expect(engine.smokeTest({'runtimeApiVersion': 1}), isTrue);
engine.init({'runtimeApiVersion': 1});
engine.dispatchEvent(
const RuntimeEvent(
type: RuntimeEventType.tap,
target: 'debug_button',
handler: 'debug',
),
);
expect(
diagnostics.entries.map((entry) => entry.type),
everyElement(RuntimeDiagnosticType.luaLog),
);
expect(diagnostics.entries.map((entry) => entry.message), [
'smoke 1',
'init true nil',
'event tap debug_button',
]);
expect(diagnostics.entries.first.context, {'argumentCount': 2});
});
test('rejects undeclared module imports', () async {
final package = await _createPackage(
mainScript: '''

View File

@@ -552,6 +552,7 @@
---@class RuntimeImportApi
---@field import fun(moduleName: string): table
---@field log fun(...: any)
---@type RuntimeImportApi
runtime = runtime