Compare commits
2 Commits
5ebe6ee786
...
8d2c97269a
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8d2c97269a | ||
|
|
45ab9d7861 |
@@ -1,5 +1,9 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## Unreleased
|
||||||
|
|
||||||
|
- Fixed Runtime node color alpha composition so `#AARRGGBB` alpha now multiplies with node/runtime alpha instead of being overwritten.
|
||||||
|
|
||||||
## 0.1.0
|
## 0.1.0
|
||||||
|
|
||||||
- Initial extracted package skeleton for `flame_lua_runtime`.
|
- Initial extracted package skeleton for `flame_lua_runtime`.
|
||||||
|
|||||||
@@ -34,6 +34,16 @@ Supported node concepts include:
|
|||||||
|
|
||||||
Lua may compose higher-level widgets, but those widgets must normalize into supported runtime nodes.
|
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
|
## RuntimeCommand
|
||||||
|
|
||||||
Runtime commands request generic side effects owned by Dart/Flame.
|
Runtime commands request generic side effects owned by Dart/Flame.
|
||||||
|
|||||||
@@ -552,6 +552,7 @@
|
|||||||
|
|
||||||
---@class RuntimeImportApi
|
---@class RuntimeImportApi
|
||||||
---@field import fun(moduleName: string): table
|
---@field import fun(moduleName: string): table
|
||||||
|
---@field log fun(...: any)
|
||||||
|
|
||||||
---@type RuntimeImportApi
|
---@type RuntimeImportApi
|
||||||
runtime = runtime
|
runtime = runtime
|
||||||
|
|||||||
@@ -552,6 +552,7 @@
|
|||||||
|
|
||||||
---@class RuntimeImportApi
|
---@class RuntimeImportApi
|
||||||
---@field import fun(moduleName: string): table
|
---@field import fun(moduleName: string): table
|
||||||
|
---@field log fun(...: any)
|
||||||
|
|
||||||
---@type RuntimeImportApi
|
---@type RuntimeImportApi
|
||||||
runtime = runtime
|
runtime = runtime
|
||||||
|
|||||||
@@ -552,6 +552,7 @@
|
|||||||
|
|
||||||
---@class RuntimeImportApi
|
---@class RuntimeImportApi
|
||||||
---@field import fun(moduleName: string): table
|
---@field import fun(moduleName: string): table
|
||||||
|
---@field log fun(...: any)
|
||||||
|
|
||||||
---@type RuntimeImportApi
|
---@type RuntimeImportApi
|
||||||
runtime = runtime
|
runtime = runtime
|
||||||
|
|||||||
@@ -552,6 +552,7 @@
|
|||||||
|
|
||||||
---@class RuntimeImportApi
|
---@class RuntimeImportApi
|
||||||
---@field import fun(moduleName: string): table
|
---@field import fun(moduleName: string): table
|
||||||
|
---@field log fun(...: any)
|
||||||
|
|
||||||
---@type RuntimeImportApi
|
---@type RuntimeImportApi
|
||||||
runtime = runtime
|
runtime = runtime
|
||||||
|
|||||||
@@ -130,6 +130,7 @@ String _formatDebugValue(Object? value) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
enum RuntimeDiagnosticType {
|
enum RuntimeDiagnosticType {
|
||||||
|
luaLog,
|
||||||
luaEventError,
|
luaEventError,
|
||||||
diffApplyError,
|
diffApplyError,
|
||||||
packageActivationError,
|
packageActivationError,
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import 'package:flame/game.dart';
|
import 'package:flame/game.dart';
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
|
|
||||||
|
import '../diagnostics/runtime_diagnostics.dart';
|
||||||
import '../packages/game_package_repository.dart';
|
import '../packages/game_package_repository.dart';
|
||||||
import '../scripting/lua_dardo_script_engine.dart';
|
import '../scripting/lua_dardo_script_engine.dart';
|
||||||
import 'flame_lua_game.dart';
|
import 'flame_lua_game.dart';
|
||||||
@@ -24,10 +25,13 @@ class LuaGameWidget extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final diagnostics = RuntimeDiagnostics();
|
||||||
return GameWidget(
|
return GameWidget(
|
||||||
game: FlameLuaGame(
|
game: FlameLuaGame(
|
||||||
scriptEngine: LuaDardoScriptEngine(),
|
scriptEngine: LuaDardoScriptEngine(diagnostics: diagnostics),
|
||||||
scriptEngineFactory: LuaDardoScriptEngine.new,
|
scriptEngineFactory: () =>
|
||||||
|
LuaDardoScriptEngine(diagnostics: diagnostics),
|
||||||
|
diagnostics: diagnostics,
|
||||||
packageRepository:
|
packageRepository:
|
||||||
packageRepository ??
|
packageRepository ??
|
||||||
(serverUrl == null
|
(serverUrl == null
|
||||||
|
|||||||
@@ -12,6 +12,11 @@ import '../models/runtime_node.dart';
|
|||||||
import '../protocol/runtime_protocol.dart';
|
import '../protocol/runtime_protocol.dart';
|
||||||
import '../resources/game_resource_manager.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
|
class RuntimeComponent extends PositionComponent
|
||||||
with HasVisibility, TapCallbacks {
|
with HasVisibility, TapCallbacks {
|
||||||
RuntimeComponent({
|
RuntimeComponent({
|
||||||
@@ -156,7 +161,10 @@ class RuntimeComponent extends PositionComponent
|
|||||||
}
|
}
|
||||||
|
|
||||||
final paint = Paint()
|
final paint = Paint()
|
||||||
..color = (_node.color ?? _defaultColor()).withValues(alpha: renderAlpha);
|
..color = composeRuntimeColorAlpha(
|
||||||
|
_node.color ?? _defaultColor(),
|
||||||
|
renderAlpha,
|
||||||
|
);
|
||||||
|
|
||||||
canvas.save();
|
canvas.save();
|
||||||
canvas.transform(Float64List.fromList(transform.transformMatrix.storage));
|
canvas.transform(Float64List.fromList(transform.transformMatrix.storage));
|
||||||
@@ -177,7 +185,10 @@ class RuntimeComponent extends PositionComponent
|
|||||||
@override
|
@override
|
||||||
void render(Canvas canvas) {
|
void render(Canvas canvas) {
|
||||||
final paint = Paint()
|
final paint = Paint()
|
||||||
..color = (_node.color ?? _defaultColor()).withValues(alpha: renderAlpha);
|
..color = composeRuntimeColorAlpha(
|
||||||
|
_node.color ?? _defaultColor(),
|
||||||
|
renderAlpha,
|
||||||
|
);
|
||||||
|
|
||||||
switch (_node.type) {
|
switch (_node.type) {
|
||||||
case RuntimeNodeType.circle:
|
case RuntimeNodeType.circle:
|
||||||
@@ -224,7 +235,7 @@ class RuntimeComponent extends PositionComponent
|
|||||||
final rect = Rect.fromLTWH(0, 0, size.x, size.y);
|
final rect = Rect.fromLTWH(0, 0, size.x, size.y);
|
||||||
final radius = Radius.circular(_node.radius ?? 4);
|
final radius = Radius.circular(_node.radius ?? 4);
|
||||||
final backgroundPaint = Paint()
|
final backgroundPaint = Paint()
|
||||||
..color = const Color(0x33475569).withValues(alpha: renderAlpha);
|
..color = composeRuntimeColorAlpha(const Color(0x33475569), renderAlpha);
|
||||||
canvas.drawRRect(RRect.fromRectAndRadius(rect, radius), backgroundPaint);
|
canvas.drawRRect(RRect.fromRectAndRadius(rect, radius), backgroundPaint);
|
||||||
|
|
||||||
final value = _node.value ?? 0;
|
final value = _node.value ?? 0;
|
||||||
@@ -327,11 +338,15 @@ class RuntimeComponent extends PositionComponent
|
|||||||
final contentRect = _listViewContentRect();
|
final contentRect = _listViewContentRect();
|
||||||
final scrollbars = _listViewScrollbarVisibility();
|
final scrollbars = _listViewScrollbarVisibility();
|
||||||
final trackPaint = Paint()
|
final trackPaint = Paint()
|
||||||
..color = (_node.scrollbarTrackColor ?? const Color(0x33475569))
|
..color = composeRuntimeColorAlpha(
|
||||||
.withValues(alpha: renderAlpha);
|
_node.scrollbarTrackColor ?? const Color(0x33475569),
|
||||||
|
renderAlpha,
|
||||||
|
);
|
||||||
final thumbPaint = Paint()
|
final thumbPaint = Paint()
|
||||||
..color = (_node.scrollbarThumbColor ?? const Color(0xaa94a3b8))
|
..color = composeRuntimeColorAlpha(
|
||||||
.withValues(alpha: renderAlpha);
|
_node.scrollbarThumbColor ?? const Color(0xaa94a3b8),
|
||||||
|
renderAlpha,
|
||||||
|
);
|
||||||
|
|
||||||
final contentHeight = _node.contentHeight;
|
final contentHeight = _node.contentHeight;
|
||||||
if (scrollbars.vertical &&
|
if (scrollbars.vertical &&
|
||||||
@@ -415,7 +430,7 @@ class RuntimeComponent extends PositionComponent
|
|||||||
image,
|
image,
|
||||||
Rect.fromLTWH(0, 0, image.width.toDouble(), image.height.toDouble()),
|
Rect.fromLTWH(0, 0, image.width.toDouble(), image.height.toDouble()),
|
||||||
rect,
|
rect,
|
||||||
Paint()..color = Colors.white.withValues(alpha: renderAlpha),
|
Paint()..color = composeRuntimeColorAlpha(Colors.white, renderAlpha),
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -948,7 +963,7 @@ class RuntimeComponent extends PositionComponent
|
|||||||
final text = label ?? '';
|
final text = label ?? '';
|
||||||
final color = _textColor(node);
|
final color = _textColor(node);
|
||||||
final style = TextStyle(
|
final style = TextStyle(
|
||||||
color: color.withValues(alpha: renderAlpha),
|
color: composeRuntimeColorAlpha(color, renderAlpha),
|
||||||
fontSize: node.fontSize ?? 18,
|
fontSize: node.fontSize ?? 18,
|
||||||
fontWeight: node.type == RuntimeNodeType.button
|
fontWeight: node.type == RuntimeNodeType.button
|
||||||
? FontWeight.w600
|
? FontWeight.w600
|
||||||
|
|||||||
@@ -1,11 +1,16 @@
|
|||||||
import 'package:lua_dardo_plus/lua.dart';
|
import 'package:lua_dardo_plus/lua.dart';
|
||||||
|
|
||||||
|
import '../diagnostics/runtime_diagnostics.dart';
|
||||||
import '../models/game_diff.dart';
|
import '../models/game_diff.dart';
|
||||||
import '../models/runtime_event.dart';
|
import '../models/runtime_event.dart';
|
||||||
import '../packages/game_package.dart';
|
import '../packages/game_package.dart';
|
||||||
import 'script_engine.dart';
|
import 'script_engine.dart';
|
||||||
|
|
||||||
class LuaDardoScriptEngine implements ScriptEngine {
|
class LuaDardoScriptEngine implements ScriptEngine {
|
||||||
|
LuaDardoScriptEngine({RuntimeDiagnostics? diagnostics})
|
||||||
|
: _diagnostics = diagnostics;
|
||||||
|
|
||||||
|
final RuntimeDiagnostics? _diagnostics;
|
||||||
late final LuaState _lua;
|
late final LuaState _lua;
|
||||||
late final Map<String, String> _moduleScripts;
|
late final Map<String, String> _moduleScripts;
|
||||||
final Set<String> _loadingModules = {};
|
final Set<String> _loadingModules = {};
|
||||||
@@ -104,9 +109,28 @@ class LuaDardoScriptEngine implements ScriptEngine {
|
|||||||
_lua.pushDartFunction(_importModule);
|
_lua.pushDartFunction(_importModule);
|
||||||
_lua.setField(-2, 'import');
|
_lua.setField(-2, 'import');
|
||||||
|
|
||||||
|
_lua.pushDartFunction(_log);
|
||||||
|
_lua.setField(-2, 'log');
|
||||||
|
|
||||||
_lua.setGlobal('runtime');
|
_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) {
|
int _importModule(LuaState lua) {
|
||||||
final moduleName = lua.toStr(1);
|
final moduleName = lua.toStr(1);
|
||||||
if (moduleName == null || moduleName.isEmpty) {
|
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) {
|
bool _isSafeModuleName(String value) {
|
||||||
return RegExp(r'^[A-Za-z0-9_.-]+$').hasMatch(value) &&
|
return RegExp(r'^[A-Za-z0-9_.-]+$').hasMatch(value) &&
|
||||||
!value.contains('..') &&
|
!value.contains('..') &&
|
||||||
|
|||||||
@@ -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/protocol/runtime_protocol.dart';
|
||||||
import 'package:flame_lua_runtime/runtime/rendering/runtime_component.dart';
|
import 'package:flame_lua_runtime/runtime/rendering/runtime_component.dart';
|
||||||
import 'package:flame_lua_runtime/runtime/resources/game_resource_manager.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';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
@@ -114,6 +115,29 @@ void main() {
|
|||||||
expect(component.renderAlpha, 1);
|
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', () {
|
test('multi-line non-button text is top aligned', () {
|
||||||
final component = RuntimeComponent(
|
final component = RuntimeComponent(
|
||||||
node: const RuntimeNode(
|
node: const RuntimeNode(
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import 'dart:io';
|
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.dart';
|
||||||
import 'package:flame_lua_runtime/runtime/packages/game_package_manifest.dart';
|
import 'package:flame_lua_runtime/runtime/packages/game_package_manifest.dart';
|
||||||
import 'package:flame_lua_runtime/runtime/models/runtime_event.dart';
|
import 'package:flame_lua_runtime/runtime/models/runtime_event.dart';
|
||||||
@@ -895,6 +896,51 @@ end
|
|||||||
expect(c.y, 53);
|
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 {
|
test('rejects undeclared module imports', () async {
|
||||||
final package = await _createPackage(
|
final package = await _createPackage(
|
||||||
mainScript: '''
|
mainScript: '''
|
||||||
|
|||||||
@@ -552,6 +552,7 @@
|
|||||||
|
|
||||||
---@class RuntimeImportApi
|
---@class RuntimeImportApi
|
||||||
---@field import fun(moduleName: string): table
|
---@field import fun(moduleName: string): table
|
||||||
|
---@field log fun(...: any)
|
||||||
|
|
||||||
---@type RuntimeImportApi
|
---@type RuntimeImportApi
|
||||||
runtime = runtime
|
runtime = runtime
|
||||||
|
|||||||
Reference in New Issue
Block a user