Update Files

This commit is contained in:
2025-01-22 16:18:30 +01:00
parent ed4603cf95
commit a36294b518
16718 changed files with 2960346 additions and 0 deletions

View File

@ -0,0 +1,131 @@
package leenkx.system;
import haxe.Exception;
import haxe.PosInfos;
import haxe.exceptions.PosException;
import haxe.macro.Context;
import haxe.macro.Expr;
using haxe.macro.ExprTools;
class Assert {
/**
Checks whether the given expression evaluates to true. If this is not
the case, an `LnxAssertionException` is thrown or a warning is printed
(depending on the assertion level).
The assert level describes the severity of the assertion. If the
severity is lower than the level stored in the `lnx_assert_level` flag,
the assertion is omitted from the code so that it doesn't decrease the
runtime performance.
@param level The severity of this assertion.
@param condition The conditional expression to test.
@param message Optional message to display when the assertion fails.
@see `AssertLevel`
**/
macro public static function assert(level: ExprOf<AssertLevel>, condition: ExprOf<Bool>, ?message: ExprOf<String>): Expr {
final levelVal: AssertLevel = AssertLevel.fromExpr(level);
final assertThreshold = AssertLevel.fromString(Context.definedValue("lnx_assert_level"));
if (levelVal < assertThreshold) {
return macro {};
}
switch (levelVal) {
case Warning:
return macro {
if (!$condition) {
@:pos(condition.pos)
trace(@:privateAccess leenkx.system.Assert.LnxAssertionException.formatMessage($v{condition.toString()}, ${message}));
}
}
case Error:
return macro {
if (!$condition) {
#if lnx_assert_quit kha.System.stop(); #end
@:pos(condition.pos)
@:privateAccess leenkx.system.Assert.throwAssertionError($v{condition.toString()}, ${message});
}
}
default:
throw new Exception('Unsupported assert level: $levelVal');
}
}
/**
Helper function to prevent Haxe "bug" that actually throws an error
even when using `macro throw` (inlining this method also does not work).
**/
static function throwAssertionError(exprString: String, message: String, ?pos: PosInfos) {
throw new LnxAssertionException(exprString, message, pos);
}
}
/**
Exception that is thrown when an assertion fails.
@see `Assert`
**/
class LnxAssertionException extends PosException {
/**
@param exprString The string representation of the failed assert condition.
@param message Custom error message, use `null` to omit this.
**/
public inline function new(exprString: String, message: Null<String>, ?previous: Exception, ?pos: Null<PosInfos>) {
super('\n${formatMessage(exprString, message)}', previous, pos);
}
static inline function formatMessage(exprString: String, message: Null<String>): String {
final optMsg = message != null ? '\n\tMessage: $message' : "";
return 'Failed assertion:$optMsg\n\tExpression: ($exprString)';
}
}
enum abstract AssertLevel(Int) from Int to Int {
/**
Assertions with this severity don't throw exceptions and only print to
the console.
**/
var Warning: AssertLevel = 0;
/**
Assertions with this severity throw an `LnxAssertionException` if they
fail, and optionally quit the game if the `lnx_assert_quit` flag is set.
**/
var Error: AssertLevel = 1;
/**
Completely disable assertions. Don't use this level in `assert()` calls!
**/
var NoAssertions: AssertLevel = 2;
public static function fromExpr(e: ExprOf<AssertLevel>): AssertLevel {
switch (e.expr) {
case EConst(CIdent(v)): return fromString(v);
default: throw new Exception('Unsupported expression: $e');
};
}
/**
Converts a string into an `AssertLevel`, the string must be spelled
exactly as the assert level. `null` defaults to
`AssertLevel.NoAssertions`.
**/
public static function fromString(s: Null<String>): AssertLevel {
return switch (s) {
case "Warning": Warning;
case "Error": Error;
case "NoAssertions" | null: NoAssertions;
default: throw new Exception('Could not convert "$s" to AssertLevel');
}
}
@:op(A < B) static function lt(a: AssertLevel, b: AssertLevel): Bool;
@:op(A > B) static function gt(a: AssertLevel, b: AssertLevel): Bool;
}

View File

@ -0,0 +1,83 @@
package leenkx.system;
/**
Detailed documentation of the event system:
[Leenkx Wiki: Events](https://github.com/leenkx3d/leenkx/wiki/events).
**/
class Event {
static var events = new Map<String, Array<TEvent>>();
/**
Send an event with the given name to all corresponding listeners. This
function directly executes the `onEvent` callbacks of those listeners.
For an explanation of the `mask` value, please refer to the
[wiki](https://github.com/leenkx3d/leenkx/wiki/events#event-masks).
**/
public static function send(name: String, mask = -1) {
var entries = get(name);
if (entries != null) for (e in entries) if (mask == -1 || mask == e.mask ) e.onEvent();
}
/**
Return the array of event listeners registered for events with the
given name, or `null` if no listener is currently registered for the event.
**/
public static function get(name: String): Array<TEvent> {
return events.get(name);
}
/**
Add a listener to the event with the given name and return the
corresponding listener object. The `onEvent` callback will be called
when a matching event is sent.
For an explanation of the `mask` value, please refer to the
[wiki](https://github.com/leenkx3d/leenkx/wiki/events#event-masks).
**/
public static function add(name: String, onEvent: Void->Void, mask = -1): TEvent {
var e: TEvent = { name: name, onEvent: onEvent, mask: mask };
var entries = events.get(name);
if (entries != null) entries.push(e);
else events.set(name, [e]);
return e;
}
/**
Remove _all_ listeners that listen to events with the given `name`.
**/
public static function remove(name: String) {
events.remove(name);
}
/**
Remove a specific listener. If the listener is not registered/added,
this function does nothing.
**/
public static function removeListener(event: TEvent) {
var entries = events.get(event.name);
if (entries != null) {
entries.remove(event);
if (entries.length == 0) {
events.remove(event.name);
}
}
}
}
/**
Represents an event listener.
@see `leenkx.system.Event`
**/
typedef TEvent = {
/** The name of the events this listener is listening to. **/
var name: String;
/** The callback function that is called when a matching event is sent. **/
var onEvent: Void->Void;
/** The mask of the events this listener is listening to. **/
var mask: Int;
}

View File

@ -0,0 +1,74 @@
package leenkx.system;
class FSM<T> {
final transitions = new Array<Transition<T>>();
final tempTransitions = new Array<Transition<T>>();
var state: Null<State<T>>;
var entered = false;
public function new() {}
public function bindTransition(canEnter: Void -> Bool, fromState: State<T>, toState: State<T>) {
final transition = new Transition<T>(canEnter, fromState, toState);
transitions.push(transition);
syncTransitions();
}
public function setInitState(state: State<T>) {
this.state = state;
syncTransitions();
}
public function update() {
if (!entered) {
state.onEnter();
entered = true;
}
state.onUpdate();
for (transition in tempTransitions) {
if (transition.canEnter()) {
state.onExit();
state = transition.toState;
entered = false;
syncTransitions();
break;
}
}
}
public function syncTransitions() {
tempTransitions.resize(0);
for (transition in transitions) {
if (transition.fromState == state) tempTransitions.push(transition);
}
}
}
class Transition<T> {
public final canEnter: Void -> Bool;
public final fromState: State<T>;
public final toState: State<T>;
public function new(canEnter: Void -> Bool, fromState: State<T>, toState: State<T>) {
this.canEnter = canEnter;
this.fromState = fromState;
this.toState = toState;
}
}
class State<T> {
final owner: T;
public function new(owner: T) {
this.owner = owner;
}
public function onEnter() {}
public function onUpdate() {}
public function onExit() {}
}

View File

@ -0,0 +1,226 @@
package leenkx.system;
import kha.FastFloat;
import iron.system.Input;
class InputMap {
static var inputMaps = new Map<String, InputMap>();
public var keys(default, null) = new Array<InputMapKey>();
public var lastKeyPressed(default, null) = "";
public function new() {}
public static function getInputMap(inputMap: String): Null<InputMap> {
if (inputMaps.exists(inputMap)) {
return inputMaps[inputMap];
}
return null;
}
public static function addInputMap(inputMap: String): InputMap {
return inputMaps[inputMap] = new InputMap();
}
public static function getInputMapKey(inputMap: String, key: String): Null<InputMapKey> {
if (inputMaps.exists(inputMap)) {
for (k in inputMaps[inputMap].keys) {
if (k.key == key) {
return k;
}
}
}
return null;
}
public static function removeInputMapKey(inputMap: String, key: String): Bool {
if (inputMaps.exists(inputMap)) {
var i = inputMaps[inputMap];
for (k in i.keys) {
if (k.key == key) {
return i.removeKey(k);
}
}
}
return false;
}
public function addKeyboard(key: String, scale: FastFloat = 1.0): InputMapKey {
return addKey(new KeyboardKey(key, scale));
}
public function addMouse(key: String, scale: FastFloat = 1.0, deadzone: FastFloat = 0.0): InputMapKey {
return addKey(new MouseKey(key, scale, deadzone));
}
public function addGamepad(key: String, scale: FastFloat = 1.0, deadzone: FastFloat = 0.0): InputMapKey {
return addKey(new GamepadKey(key, scale, deadzone));
}
public function addKey(key: InputMapKey): InputMapKey {
keys.push(key);
return key;
}
public function removeKey(key: InputMapKey): Bool {
return keys.remove(key);
}
public function value(): FastFloat {
var v = 0.0;
for (k in keys) {
v += k.value();
}
return v;
}
public function started() {
for (k in keys) {
if (k.started()) {
lastKeyPressed = k.key;
return true;
}
}
return false;
}
public function released() {
for (k in keys) {
if (k.released()) {
lastKeyPressed = k.key;
return true;
}
}
return false;
}
}
class InputMapKey {
public var key: String;
public var scale: FastFloat;
public var deadzone: FastFloat;
public function new(key: String, scale = 1.0, deadzone = 0.0) {
this.key = key.toLowerCase();
this.scale = scale;
this.deadzone = deadzone;
}
public function started(): Bool {
return false;
}
public function released(): Bool {
return false;
}
public function value(): FastFloat {
return 0.0;
}
public function setIndex(index: Int) {}
function evalDeadzone(value: FastFloat): FastFloat {
var v = 0.0;
if (value > deadzone) {
v = value - deadzone;
} else if (value < -deadzone) {
v = value + deadzone;
}
return v * scale;
}
function evalPressure(value: FastFloat): FastFloat {
var v = value - deadzone;
if (v > 0.0) {
v /= (1.0 - deadzone);
} else {
v = 0.0;
}
return v;
}
}
class KeyboardKey extends InputMapKey {
var kb = Input.getKeyboard();
public inline override function started() {
return kb.started(key);
}
public inline override function released() {
return kb.released(key);
}
public inline override function value(): FastFloat {
return kb.down(key) ? scale : 0.0;
}
}
class MouseKey extends InputMapKey {
var m = Input.getMouse();
public inline override function started() {
return m.started(key);
}
public inline override function released() {
return m.released(key);
}
public override function value(): FastFloat {
return switch (key) {
case "movement x": evalDeadzone(m.movementX);
case "movement y": evalDeadzone(m.movementY);
case "wheel": evalDeadzone(m.wheelDelta);
default: m.down(key) ? scale : 0.0;
}
}
}
class GamepadKey extends InputMapKey {
var g = Input.getGamepad();
public inline override function started() {
return g.started(key);
}
public inline override function released() {
return g.released(key);
}
public override function value(): FastFloat {
return switch(key) {
case "ls movement x": evalDeadzone(g.leftStick.movementX);
case "ls movement y": evalDeadzone(g.leftStick.movementY);
case "rs movement x": evalDeadzone(g.rightStick.movementX);
case "rs movement y": evalDeadzone(g.rightStick.movementY);
case "lt pressure": evalDeadzone(evalPressure(g.down("l2")));
case "rt pressure": evalDeadzone(evalPressure(g.down("r2")));
default: evalDeadzone(g.down(key));
}
}
public override function setIndex(index: Int) {
g = Input.getGamepad(index);
}
}

View File

@ -0,0 +1,298 @@
package leenkx.system;
import leenkx.logicnode.*;
class Logic {
static var nodes: Array<TNode>;
static var links: Array<TNodeLink>;
static var parsed_nodes: Array<String> = null;
static var parsed_labels: Map<String, String> = null;
static var nodeMap: Map<String, leenkx.logicnode.LogicNode>;
public static var packageName = "leenkx.logicnode";
public static function getNode(id: Int): TNode {
for (n in nodes) if (n.id == id) return n;
return null;
}
public static function getLink(id: Int): TNodeLink {
for (l in links) if (l.id == id) return l;
return null;
}
public static function getInputLink(inp: TNodeSocket): TNodeLink {
for (l in links) {
if (l.to_id == inp.node_id) {
var node = getNode(inp.node_id);
if (node.inputs.length <= l.to_socket) return null;
if (node.inputs[l.to_socket] == inp) return l;
}
}
return null;
}
public static function getOutputLinks(out: TNodeSocket): Array<TNodeLink> {
var res: Array<TNodeLink> = [];
for (l in links) {
if (l.from_id == out.node_id) {
var node = getNode(out.node_id);
if (node.outputs.length <= l.from_socket) continue;
if (node.outputs[l.from_socket] == out) res.push(l);
}
}
return res;
}
static function safesrc(s: String): String {
return StringTools.replace(s, " ", "");
}
static function node_name(node: TNode): String {
var s = safesrc(node.name) + node.id;
return s;
}
static var tree: leenkx.logicnode.LogicTree;
public static function parse(canvas: TNodeCanvas, onAdd = true): leenkx.logicnode.LogicTree {
nodes = canvas.nodes;
links = canvas.links;
parsed_nodes = [];
parsed_labels = new Map();
nodeMap = new Map();
var root_nodes = get_root_nodes(canvas);
tree = new leenkx.logicnode.LogicTree();
if (onAdd) {
tree.notifyOnAdd(function() {
for (node in root_nodes) build_node(node);
});
}
else {
for (node in root_nodes) build_node(node);
}
return tree;
}
static function build_node(node: TNode): String {
// Get node name
var name = node_name(node);
// Check if node already exists
if (parsed_nodes.indexOf(name) != -1) {
return name;
}
parsed_nodes.push(name);
// Create node
var v = createClassInstance(node.type, [tree]);
nodeMap.set(name, v);
#if lnx_patch
tree.nodes.set(name, v);
#end
// Properties
for (i in 0...5) {
for (b in node.buttons) {
if (b.name == "property" + i) {
Reflect.setProperty(v, b.name, b.data[b.default_value]);
}
}
}
@:privateAccess v.preallocInputs(node.inputs.length);
@:privateAccess v.preallocOutputs(node.outputs.length);
// Create inputs
var inp_node: leenkx.logicnode.LogicNode = null;
var inp_from = 0;
var from_type: String;
for (i in 0...node.inputs.length) {
var inp = node.inputs[i];
// Is linked - find node
var l = getInputLink(inp);
if (l != null) {
var n = getNode(l.from_id);
var socket = n.outputs[l.from_socket];
inp_node = nodeMap.get(build_node(n));
for (i in 0...n.outputs.length) {
if (n.outputs[i] == socket) {
inp_from = i;
from_type = socket.type;
break;
}
}
}
else { // Not linked - create node with default values
inp_node = build_default_node(inp);
inp_from = 0;
from_type = inp.type;
}
// Add input
var link = LogicNode.addLink(inp_node, v, inp_from, i);
#if lnx_patch
link.fromType = from_type;
link.toType = inp.type;
link.toValue = getSocketDefaultValue(inp);
#end
}
// Create outputs
for (i in 0...node.outputs.length) {
var out = node.outputs[i];
var ls = getOutputLinks(out);
// Linked outputs are already handled after iterating over inputs
// above, so only unconnected outputs are handled here
if (ls == null || ls.length == 0) {
var link = LogicNode.addLink(v, build_default_node(out), i, 0);
#if lnx_patch
link.fromType = out.type;
link.toType = out.type;
link.toValue = getSocketDefaultValue(out);
#end
}
}
return name;
}
static function get_root_nodes(node_group: TNodeCanvas): Array<TNode> {
var roots: Array<TNode> = [];
for (node in node_group.nodes) {
// if (node.bl_idname == 'NodeUndefined') {
// lnx.log.warn('Undefined logic nodes in ' + node_group.name)
// return []
// }
var linked = false;
for (out in node.outputs) {
var ls = getOutputLinks(out);
if (ls != null && ls.length > 0) {
linked = true;
break;
}
}
if (!linked) { // Assume node with no connected outputs as roots
roots.push(node);
}
}
return roots;
}
static function build_default_node(inp: TNodeSocket): leenkx.logicnode.LogicNode {
var v: leenkx.logicnode.LogicNode = null;
if (inp.type == "OBJECT") {
v = createClassInstance("ObjectNode", [tree, inp.default_value]);
}
else if (inp.type == "ANIMACTION") {
v = createClassInstance("StringNode", [tree, inp.default_value]);
}
else if (inp.type == "VECTOR") {
if (inp.default_value == null) inp.default_value = [0, 0, 0]; // TODO
v = createClassInstance("VectorNode", [tree, inp.default_value[0], inp.default_value[1], inp.default_value[2]]);
}
else if (inp.type == "RGBA") {
if (inp.default_value == null) inp.default_value = [0, 0, 0]; // TODO
v = createClassInstance("ColorNode", [tree, inp.default_value[0], inp.default_value[1], inp.default_value[2], inp.default_value[3]]);
}
else if (inp.type == "RGB") {
if (inp.default_value == null) inp.default_value = [0, 0, 0]; // TODO
v = createClassInstance("ColorNode", [tree, inp.default_value[0], inp.default_value[1], inp.default_value[2]]);
}
else if (inp.type == "VALUE") {
v = createClassInstance("FloatNode", [tree, inp.default_value]);
}
else if (inp.type == "INT") {
v = createClassInstance("IntegerNode", [tree, inp.default_value]);
}
else if (inp.type == "BOOLEAN") {
v = createClassInstance("BooleanNode", [tree, inp.default_value]);
}
else if (inp.type == "STRING") {
v = createClassInstance("StringNode", [tree, inp.default_value]);
}
else { // ACTION, ARRAY
v = createClassInstance("NullNode", [tree]);
}
return v;
}
static function getSocketDefaultValue(socket: TNodeSocket): Any {
var v: leenkx.logicnode.LogicNode = null;
return switch (socket.type) {
case "OBJECT" | "VALUE" | "INT" | "BOOLEAN" | "STRING":
socket.default_value;
case "VECTOR" | "RGB":
socket.default_value == null ? [0, 0, 0] : [socket.default_value[0], socket.default_value[1], socket.default_value[2]];
case "RGBA":
socket.default_value == null ? [0, 0, 0, 1] : [socket.default_value[0], socket.default_value[1], socket.default_value[2], socket.default_value[3]];
default:
null;
}
}
static function createClassInstance(className: String, args: Array<Dynamic>): Dynamic {
var cname = Type.resolveClass(packageName + "." + className);
if (cname == null) return null;
return Type.createInstance(cname, args);
}
}
typedef TNodeCanvas = {
var name: String;
var nodes: Array<TNode>;
var links: Array<TNodeLink>;
}
typedef TNode = {
var id: Int;
var name: String;
var type: String;
var x: Float;
var y: Float;
var inputs: Array<TNodeSocket>;
var outputs: Array<TNodeSocket>;
var buttons: Array<TNodeButton>;
var color: Int;
}
typedef TNodeSocket = {
var id: Int;
var node_id: Int;
var name: String;
var type: String;
var color: Int;
var default_value: Dynamic;
@:optional var min: Null<Float>;
@:optional var max: Null<Float>;
}
typedef TNodeLink = {
var id: Int;
var from_id: Int;
var from_socket: Int;
var to_id: Int;
var to_socket: Int;
}
typedef TNodeButton = {
var name: String;
var type: String;
@:optional var output: Null<Int>;
@:optional var default_value: Dynamic;
@:optional var data: Dynamic;
@:optional var min: Null<Float>;
@:optional var max: Null<Float>;
}

View File

@ -0,0 +1,35 @@
package leenkx.system;
import haxe.Constraints.Function;
class Signal {
var callbacks:Array<Function> = [];
public function new() {
}
public function connect(callback:Function) {
if (!callbacks.contains(callback)) callbacks.push(callback);
}
public function disconnect(callback:Function) {
if (callbacks.contains(callback)) callbacks.remove(callback);
}
public function emit(...args:Any) {
for (callback in callbacks) Reflect.callMethod(this, callback, args);
}
public function getConnections():Array<Function> {
return callbacks;
}
public function isConnected(callBack:Function):Bool {
return callbacks.contains(callBack);
}
public function isNull():Bool {
return callbacks.length == 0;
}
}

View File

@ -0,0 +1,141 @@
package leenkx.system;
import kha.WindowOptions;
class Starter {
#if lnx_loadscreen
public static var drawLoading: kha.graphics2.Graphics->Int->Int->Void = null;
public static var numAssets: Int;
#end
public static function main(scene: String, mode: Int, resize: Bool, min: Bool, max: Bool, w: Int, h: Int, msaa: Int, vsync: Bool, getRenderPath: Void->iron.RenderPath) {
var tasks = 0;
function start() {
if (tasks > 0) return;
if (leenkx.data.Config.raw == null) leenkx.data.Config.raw = {};
var c = leenkx.data.Config.raw;
if (c.window_mode == null) c.window_mode = mode;
if (c.window_resizable == null) c.window_resizable = resize;
if (c.window_minimizable == null) c.window_minimizable = min;
if (c.window_maximizable == null) c.window_maximizable = max;
if (c.window_w == null) c.window_w = w;
if (c.window_h == null) c.window_h = h;
if (c.window_scale == null) c.window_scale = 1.0;
if (c.window_msaa == null) c.window_msaa = msaa;
if (c.window_vsync == null) c.window_vsync = vsync;
leenkx.object.Uniforms.register();
var windowMode = c.window_mode == 0 ? kha.WindowMode.Windowed : kha.WindowMode.Fullscreen;
var windowFeatures = None;
if (c.window_resizable) windowFeatures |= FeatureResizable;
if (c.window_maximizable) windowFeatures |= FeatureMaximizable;
if (c.window_minimizable) windowFeatures |= FeatureMinimizable;
#if (kha_webgl && (!lnx_legacy) && (!kha_node))
try {
#end
kha.System.start({title: Main.projectName, width: c.window_w, height: c.window_h, window: {mode: windowMode, windowFeatures: windowFeatures}, framebuffer: {samplesPerPixel: c.window_msaa, verticalSync: c.window_vsync}}, function(window: kha.Window) {
iron.App.init(function() {
#if lnx_loadscreen
function load(g: kha.graphics2.Graphics) {
if (iron.Scene.active != null && iron.Scene.active.ready) iron.App.removeRender2D(load);
else drawLoading(g, iron.data.Data.assetsLoaded, numAssets);
}
iron.App.notifyOnRender2D(load);
#end
iron.Scene.setActive(scene, function(object: iron.object.Object) {
iron.RenderPath.setActive(getRenderPath());
#if lnx_patch
iron.Scene.getRenderPath = getRenderPath;
#end
#if lnx_draworder_shader
iron.RenderPath.active.drawOrder = iron.RenderPath.DrawOrder.Shader;
#end // else Distance
});
});
});
#if (kha_webgl && (!lnx_legacy) && (!kha_node))
}
catch (e: Dynamic) {
if (!kha.SystemImpl.gl2) {
trace("This project was not compiled with legacy shaders flag - please use WebGL 2 capable browser.");
}
}
#end
}
#if (js && lnx_bullet)
function loadLibAmmo(name: String) {
kha.Assets.loadBlobFromPath(name, function(b: kha.Blob) {
js.Syntax.code("(1,eval)({0})", b.toString());
#if kha_krom
js.Syntax.code("Ammo({print:function(s){iron.log(s);},instantiateWasm:function(imports,successCallback) {
var wasmbin = Krom.loadBlob('ammo.wasm.wasm');
var module = new WebAssembly.Module(wasmbin);
var inst = new WebAssembly.Instance(module,imports);
successCallback(inst);
return inst.exports;
}}).then(function(){ tasks--; start();})");
#else
js.Syntax.code("Ammo({print:function(s){iron.log(s);}}).then(function(){ tasks--; start();})");
#end
});
}
#end
#if (js && lnx_navigation)
function loadLib(name: String) {
kha.Assets.loadBlobFromPath(name, function(b: kha.Blob) {
js.Syntax.code("(1, eval)({0})", b.toString());
#if kha_krom
js.Syntax.code("Recast({print:function(s){iron.log(s);},instantiateWasm:function(imports,successCallback) {
var wasmbin = Krom.loadBlob('recast.wasm.wasm');
var module = new WebAssembly.Module(wasmbin);
var inst = new WebAssembly.Instance(module,imports);
successCallback(inst);
return inst.exports;
}}).then(function(){ tasks--; start();})");
#else
js.Syntax.code("Recast({print:function(s){iron.log(s);}}).then(function(){ tasks--; start();})");
#end
});
}
#end
tasks = 1;
#if (js && lnx_bullet)
tasks++;
#if kha_krom
loadLibAmmo("ammo.wasm.js");
#else
loadLibAmmo("ammo.js");
#end
#end
#if (js && lnx_navigation)
tasks++;
#if kha_krom
loadLib("recast.wasm.js");
#else
loadLib("recast.js");
#end
#end
#if (lnx_config)
tasks++;
leenkx.data.Config.load(function() { tasks--; start(); });
#end
tasks--; start();
}
}