P2P Export - Nodes - Patch

This commit is contained in:
2026-10-06 02:10:08 -07:00
parent 9fee67d231
commit 4ab646015d
49 changed files with 2478 additions and 1164 deletions

View File

@ -130,6 +130,11 @@ extern class Krom {
static function readStorage(name: String): haxe.io.BytesData;
static function fileSaveBytes(path: String, bytes: haxe.io.BytesData): Void;
static function fileWriteBytes(path: String, bytes: haxe.io.BytesData, ?offset: Float, ?byteLength: Int): Void;
static function fileReadBytes(path: String, ?offset: Float, ?length: Int): haxe.io.BytesData;
static function fileSize(path: String): Float;
static function fileExists(path: String): Bool;
static function deleteFile(path: String): Void;
static function sysCommand(cmd: String, ?args: Array<String>): Int;
static function savePath(): String;
static function getArgCount(): Int;

View File

@ -1,79 +0,0 @@
package leenkx.logicnode;
import leenkx.system.Event;
#if js
import leenkx.network.Leenkx;
#end
import iron.object.Object;
class AddTorrentNode extends LogicNode {
public var title: String;
public var values: Array<Dynamic>;
public var net_Url: String;
public var data: Dynamic;
public var runner: Bool = false;
public function new(tree:LogicTree) {
super(tree);
}
public function promiseResult(connection,torrentid,net_Url){
#if js
if (runner == false){
values = [];
var script = 'var torrentId = "' + torrentid + '";
lx_' + net_Url +'.wt.add(torrentId, function (torrent) {
leenkx.network.Leenkx.torrent.set("' + net_Url +'", torrent.files);
torrent.on("done", function () {
leenkx.network.Leenkx.connections.h["' + net_Url +'"].ontorrentdone();
})
});
';
js.Syntax.code('(1, eval)({0})', script.toString());
runner = true;
promiseResult(connection,torrentid,net_Url);
return;
}else{
try{
values = leenkx.network.Leenkx.torrent.get(net_Url);
if(values.length > 0){
runOutput(0);
runner = false;
return;
}
}
catch(error){
haxe.Timer.delay(function () {
promiseResult(connection,torrentid,net_Url);
return;
}, 100);
}
}
#end
}
override function run(from:Int) {
var connection = inputs[1].get();
if (connection == null) return;
var torrentid = inputs[2].get();
if (torrentid == null) return;
#if js
net_Url = connection._url;
promiseResult(connection,torrentid,net_Url);
#end
}
override function get(from: Int): Dynamic {
#if js
return switch (from) {
case 1: Leenkx.id.get(net_Url);
case 2: leenkx.network.Leenkx.torrent.get(net_Url);
default: throw "Unreachable";
}
#else
return null;
#end
}
}

View File

@ -1,131 +1,168 @@
package leenkx.logicnode;
import iron.object.Object;
#if js
import leenkx.network.Types;
import leenkx.network.Util;
#if lnx_torrent
import leenkx.network.Leenkx;
import leenkx.network.torrent.LeenkxClient;
import leenkx.network.torrent.TorrentClient;
import leenkx.network.torrent.peer.RtcPeerPool;
#end
import leenkx.system.Event;
class CreateLeenkxNode extends LogicNode {
public var property0: String;
public var property1: Bool;
public var property2: String;
public var property3: String;
public var net_Url: String;
public var data: Dynamic;
public var client: Dynamic;
public var value: String;
public var listener: TEvent = null;
public function new(tree:LogicTree) {
super(tree);
super(tree);
}
override function run(from:Int) {
#if js
net_Url = inputs[1].get();
if (net_Url == null) return;
if(Leenkx.connections[net_Url] == null){
var object = tree.object;
value = Leenkx.onLoadEvent;
listener = Event.add(value, onEvent, tree.object.uid);
client = new leenkx.network.Leenkx(net_Url, object);
} else {
return;
}
#end
}
function onEvent() {
#if js
var lnxjs:Dynamic = js.Lib.global;
var lnxCxNew = lnxjs.lnxNew;
if (lnxCxNew == null) {
trace("ERROR: lnxNew not available");
return;
#if lnx_torrent
net_Url = inputs[1].get();
if (net_Url == null) return;
if (Leenkx.connections[net_Url] == null) {
value = Leenkx.onLoadEvent;
listener = Event.add(value, onEvent, tree.object.uid);
new leenkx.network.Leenkx(net_Url, tree.object);
}
var lnxCn:Dynamic = lnxCxNew(net_Url);
lnxCn.on("connections", function(c) {
leenkx.network.Leenkx.data.set(net_Url, c + 1);
leenkx.network.Leenkx.connections[net_Url].onconnections();
});
lnxCn.on("message", function(address, message) {
leenkx.network.Leenkx.data.set(net_Url, message);
leenkx.network.Leenkx.id.set(net_Url, address);
leenkx.network.Leenkx.connections[net_Url].onmessage();
});
lnxCn.on("seen", function(address) {
leenkx.network.Leenkx.id.set(net_Url, address);
leenkx.network.Leenkx.connections[net_Url].onseen();
});
lnxCn.on("left", function(address) {
leenkx.network.Leenkx.id.set(net_Url, address);
leenkx.network.Leenkx.connections[net_Url].onleft();
});
lnxCn.on("server", function(address) {
leenkx.network.Leenkx.id.set(net_Url, address);
leenkx.network.Leenkx.connections[net_Url].onserver();
});
lnxCn.on("ping", function(address) {
leenkx.network.Leenkx.id.set(net_Url, address);
leenkx.network.Leenkx.connections[net_Url].onping();
});
lnxCn.on("timeout", function(address) {
leenkx.network.Leenkx.id.set(net_Url, address);
leenkx.network.Leenkx.connections[net_Url].ontimeout();
});
lnxCn.on("rpc", function(address, call, args, nonce) {
leenkx.network.Leenkx.data.set(net_Url, call);
leenkx.network.Leenkx.id.set(net_Url, address);
call(args);
leenkx.network.Leenkx.connections[net_Url].onrpc();
});
lnxCn.on("rpc-response", function(address, nonce, response) {
leenkx.network.Leenkx.id.set(net_Url, address);
leenkx.network.Leenkx.connections[net_Url].onrpcresponse();
});
lnxCn.on("wireleft", function(wirecount, wire) {
leenkx.network.Leenkx.data.set(net_Url, wirecount);
leenkx.network.Leenkx.id.set(net_Url, wire.peerId);
leenkx.network.Leenkx.connections[net_Url].onwireleft();
});
lnxCn.on("wireseen", function(wirecount, wire) {
leenkx.network.Leenkx.data.set(net_Url, wirecount);
leenkx.network.Leenkx.id.set(net_Url, wire.peerId);
leenkx.network.Leenkx.connections[net_Url].onwireseen();
});
lnxCn.on("torrent", function(identifier, torrent) {
leenkx.network.Leenkx.data.set(net_Url, torrent);
leenkx.network.Leenkx.id.set(net_Url, identifier);
leenkx.network.Leenkx.connections[net_Url].ontorrent();
});
lnxCn.on("tracker", function(identifier) {
leenkx.network.Leenkx.id.set(net_Url, identifier);
leenkx.network.Leenkx.connections[net_Url].ontracker();
});
lnxCn.on("announce", function(identifier) {
leenkx.network.Leenkx.id.set(net_Url, identifier);
leenkx.network.Leenkx.connections[net_Url].onannounce();
});
Reflect.setField(lnxjs, "lnx_" + net_Url, lnxCn);
Leenkx.connections[net_Url].client = lnxCn;
var script = 'globalThis.addEventListener("beforeunload", function (e) {
leenkx.network.Leenkx.connections.h["' + net_Url + '"].client.destroy();
delete e["returnValue"];
});';
js.Syntax.code('(1, eval)({0})', script);
runOutput(0);
#end
}
function onEvent() {
if (listener != null) {
Event.removeListener(listener);
listener = null;
}
#if lnx_torrent
var sock = Leenkx.connections[net_Url];
if (sock == null) return;
if (sock.client != null) {
runOutput(0);
return;
}
var opts:leenkx.network.torrent.Types.LeenkxOptions = {deferJoin: true};
var nextInput = 2;
if (property0 == "datachannel") {
opts.transport = "datachannel";
var imode = property2 != null ? property2 : "inherit";
if (imode != "inherit") {
if (inputs.length > nextInput) {
var ice = TorrentClient.trackerUrls(inputs[nextInput].get());
if (ice != null) {
opts.iceServers = imode == "override" ? ice : RtcPeerPool.defaultIceServers.concat(ice);
}
}
nextInput++;
}
}
var tmode = property3 != null ? property3 : "inherit";
var trackers:Array<String> = null;
if (tmode != "inherit" && inputs.length > nextInput) {
trackers = TorrentClient.trackerUrls(inputs[nextInput].get());
}
opts.announce = tmode == "override" && trackers != null ? trackers
: trackers != null ? TorrentClient.defaultTrackers.concat(trackers)
: TorrentClient.defaultTrackers;
var lnxCn = new LeenkxClient(net_Url, opts);
lnxCn.isRelay = property1 == true;
lnxCn.onOpen = function() {
Leenkx.data.set(net_Url, lnxCn.lastPeerCount);
Leenkx.id.set(net_Url, lnxCn.lastPeerId);
sock.onopen();
};
lnxCn.onConnections = function(c) {
Leenkx.data.set(net_Url, c + 1);
sock.onconnections();
};
lnxCn.onMessage = function(address, message, nonce) {
Leenkx.data.set(net_Url, message);
Leenkx.id.set(net_Url, address);
sock.onmessage();
};
lnxCn.onSeen = function(address) {
Leenkx.id.set(net_Url, address);
sock.onseen();
};
lnxCn.onLeft = function(address) {
Leenkx.id.set(net_Url, address);
sock.onleft();
};
lnxCn.onServer = function(address) {
Leenkx.id.set(net_Url, address);
sock.onserver();
};
lnxCn.onPing = function(address) {
Leenkx.id.set(net_Url, address);
sock.onping();
};
lnxCn.onTimeout = function(address) {
Leenkx.id.set(net_Url, address);
sock.ontimeout();
};
lnxCn.onWireLeft = function(wirecount, wire) {
Leenkx.data.set(net_Url, wirecount);
if (wire != null && wire.peerId != null) {
Leenkx.id.set(net_Url, wire.peerId);
}
sock.onwireleft();
};
lnxCn.onWireSeen = function(wirecount, wire) {
Leenkx.data.set(net_Url, wirecount);
if (wire != null && wire.peerId != null) {
Leenkx.id.set(net_Url, wire.peerId);
}
sock.onwireseen();
};
lnxCn.onTorrent = function(identifier, torrent) {
Leenkx.data.set(net_Url, torrent);
Leenkx.id.set(net_Url, identifier);
sock.ontorrent();
};
lnxCn.onTracker = function(identifier, tracker) {
Leenkx.id.set(net_Url, identifier);
sock.ontracker();
};
lnxCn.onAnnounce = function(identifier) {
Leenkx.id.set(net_Url, identifier);
sock.onannounce();
};
lnxCn.onError = function(msg) {
Leenkx.data.set(net_Url, msg);
sock.onerror();
};
lnxCn.onTorrentAdded = function(t) {
Leenkx.torrent.set(net_Url, t);
Leenkx.file.set(net_Url, t.files);
Leenkx.data.set(net_Url, t);
sock.ontorrentadded();
};
lnxCn.onTorrentDone = function(t) {
Leenkx.data.set(net_Url, t);
sock.ontorrentdone();
};
sock.client = lnxCn;
lnxCn.connect();
#if (js && !kha_krom)
js.Browser.window.addEventListener("beforeunload", function(e) {
lnxCn.destroy();
});
#end
#end
runOutput(0);
}
override function get(from: Int): Dynamic {
#if js
#if lnx_torrent
return switch (from) {
case 1: Leenkx.connections[net_Url];
case 2: Leenkx.id.get(net_Url);

View File

@ -1,43 +1,100 @@
package leenkx.logicnode;
import leenkx.system.Event;
#if lnx_torrent
import leenkx.network.Leenkx;
import iron.object.Object;
import leenkx.network.torrent.Magnet;
import leenkx.network.torrent.TorrentClient;
#end
class CreateTorrentNode extends LogicNode {
public var torrent: Dynamic;
public var net_Url: String;
public var meta:Dynamic;
public var metaBytes:Dynamic;
public var magnetUri:Dynamic;
public var infoHashHex:String;
public var property0:String;
public var property1:String;
public function new(tree:LogicTree) {
super(tree);
}
override function run(from:Int) {
#if lnx_torrent
meta = null;
metaBytes = null;
magnetUri = null;
infoHashHex = null;
var connection = inputs[1].get();
if (connection == null) return;
var data = inputs[2].get();
var data = inputs[1].get();
if (data == null) return;
net_Url = connection._url;
var mode = property1 != null ? property1 : "file";
var opts:Dynamic = {};
var mv = property0 != null ? Std.parseInt(property0) : null;
if (mv != null) Reflect.setField(opts, "metaVersion", mv);
field(2, opts, "name");
field(3, opts, "comment");
field(4, opts, "createdBy");
field(5, opts, "source");
field(6, opts, "announce");
field(7, opts, "private");
field(8, opts, "pieceLength");
field(9, opts, "urlList");
var script = 'var file = ' + data + ';
lx_' + net_Url +'.wt.add(file, function (torrent) {
leenkx.network.Leenkx.file.set("' + net_Url +'", torrent.files);
torrent.on("done", function () {
leenkx.network.Leenkx.connections.h["' + net_Url +'"].ontorrentdone();
});
});
';
if (mode == "folder") {
#if sys
data = TorrentClient.readFolder(Std.string(data));
var first:Dynamic = data[0];
if (first != null && Reflect.field(opts, "name") == null) {
var p = Std.string(first.path);
var i = p.indexOf("/");
Reflect.setField(opts, "name", p.substring(0, i >= 0 ? i : p.length));
}
#else
trace("Folder mode requires a sys target";
#end
}
js.Syntax.code('(1, eval)({0})', script.toString());
var m;
try {
m = TorrentClient.buildMeta(data, opts);
} catch(e:Dynamic) {
trace(e);
return;
}
meta = m;
metaBytes = m.toTorrentBytes();
infoHashHex = m.infoHashHex != null ? m.infoHashHex : m.infoHashV2Hex;
magnetUri = Magnet.build(m.infoHash, m.name, m.announce, m.urlList, m.infoHashV2);
#end
runOutput(0);
}
#if lnx_torrent
function field(i:Int, opts:Dynamic, name:String):Void {
if (i >= inputs.length) return;
var v:Dynamic = inputs[i].get();
if (v == null || Std.string(v) == "") {
return;
}
if (name == "announce") {
v = TorrentClient.trackerUrls(v);
if (v == null) return;
}
Reflect.setField(opts, name, v);
}
#end
override function get(from: Int): Dynamic {
return switch (from) {
case 1: Leenkx.file.get(net_Url);
default: throw "Unreachable";
}
}
override function get(from:Int):Dynamic {
#if lnx_torrent
return switch (from) {
case 1: meta;
case 2: metaBytes;
case 3: infoHashHex;
case 4: magnetUri;
default: throw "Unreachable";
}
#else
return null;
#end
}
}

View File

@ -0,0 +1,88 @@
package leenkx.logicnode;
#if (lnx_torrent && (sys || kha_krom))
import leenkx.network.torrent.tracker.HttpTracker.HttpTrackerServer;
import leenkx.network.torrent.tracker.UdpTracker.UdpTrackerServer;
import leenkx.network.torrent.transport.WsTrackerClient.WsTrackerServer;
#end
#if (lnx_torrent && sys)
import leenkx.network.torrent.transport.WsTrackerClient.WssTrackerServer;
#end
class CreateTrackerNode extends LogicNode {
public var property0:String;
public var tracker:Dynamic;
public var url:String;
public function new(tree:LogicTree) {
super(tree);
}
override function run(from:Int) {
tracker = null;
url = null;
startServer();
runOutput(0);
}
function startServer():Void {
#if (lnx_torrent && (sys || kha_krom))
var hv = inputs[1].get();
var host = hv != null ? Std.string(hv) : "127.0.0.1";
if (host == "") host = "127.0.0.1";
var pv = inputs[2].get();
var port = pv != null ? Std.int(Std.parseFloat(Std.string(pv))) : 0;
if (port <= 0) port = 6969;
var scheme = property0 != null ? property0 : "udp";
switch (scheme) {
case "http":
var s = new HttpTrackerServer();
s.onError = function(e) trace(e);
s.start(port);
tracker = s;
url = "http://" + host + ":" + port + "/announce";
case "ws":
var s = new WsTrackerServer(host, port);
s.onError = function(e) trace(e);
s.start();
tracker = s;
url = "ws://" + host + ":" + port;
case "wss":
#if sys
var cert = inputs.length > 3 ? Std.string(inputs[3].get()) : null;
var key = inputs.length > 4 ? Std.string(inputs[4].get()) : null;
if (cert == null || cert == "" || key == null || key == "") {
trace("wss server needs Cert File and Key File");
return;
}
var s = new WssTrackerServer(host, port, cert, key);
s.onError = function(e) trace(e);
s.start();
tracker = s;
url = "wss://" + host + ":" + port;
#else
trace("wss server requires a sys target");
#end
default:
var s = new UdpTrackerServer();
s.onError = function(e) trace(e);
s.start(port);
tracker = s;
url = "udp://" + host + ":" + port;
}
#else
trace("server mode needs a sys or krom target");
#end
}
override function get(from:Int):Dynamic {
#if lnx_torrent
return switch (from) {
case 1: tracker;
case 2: url;
default: throw "Unreachable";
}
#else
return null;
#end
}
}

View File

@ -0,0 +1,101 @@
package leenkx.logicnode;
import leenkx.system.Event;
#if lnx_torrent
import leenkx.network.Leenkx;
import leenkx.network.torrent.TorrentClient;
#end
class DownloadTorrentNode extends LogicNode {
public var net_Url:String;
public var torrent:Dynamic;
public var property0:String;
public var property1:String;
var listener:TEvent = null;
public function new(tree:LogicTree) {
super(tree);
}
public function promiseResult(connection, torrentid, net_Url, ?opts:Dynamic) {
#if lnx_torrent
var socket = Leenkx.connections[net_Url];
if (socket == null) {
if (listener == null) {
var self = this;
listener = Event.add(Leenkx.onLoadEvent, function() {
if (Leenkx.connections[net_Url] == null) return;
if (self.listener != null) {
Event.removeListener(self.listener);
self.listener = null;
}
self.promiseResult(connection, torrentid, net_Url, opts);
});
}
return;
}
var client = socket.client;
if (client == null) return;
var self = this;
client.add(torrentid, opts, function(torrent) {
self.torrent = torrent;
var onReady = function() {
self.runOutput(0);
};
var onDone = function() {
self.runOutput(4);
};
if (torrent.ready) onReady();
else torrent.on("ready", onReady);
if (torrent.done) onDone();
else torrent.on("done", onDone);
});
#end
}
function onRemove() {
if (listener != null) {
Event.removeListener(listener);
listener = null;
}
}
override function run(from:Int) {
var connection = inputs[1].get();
if (connection == null) return;
var v = inputs[2].get();
if (v == null) return;
#if lnx_torrent
net_Url = connection._url;
var torrentId:Dynamic = switch (property0 != null ? property0 : "magnet") {
case "file": v;
case "id": TorrentClient.torrentIdFor(v);
default: Std.string(v);
};
var opts:Dynamic = null;
var tmode = property1 != null ? property1 : "inherit";
if (tmode != "inherit" && inputs.length > 3) {
var announce = TorrentClient.trackerUrls(inputs[3].get());
opts = {};
if (announce != null) opts.announce = announce;
if (tmode == "override") opts.announceOverride = true;
}
promiseResult(connection, torrentId, net_Url, opts);
#end
}
override function get(from:Int):Dynamic {
#if lnx_torrent
var t:leenkx.network.torrent.Torrent = torrent;
return switch (from) {
case 1: torrent;
case 2: t != null && t.meta != null ? t.meta.name : null;
case 3: t != null ? t.files : null;
case 5: t != null ? t.infoHashHex : null;
case 6: t != null ? t.magnetURI : null;
default: throw "Unreachable";
}
#else
return null;
#end
}
}

View File

@ -0,0 +1,83 @@
package leenkx.logicnode;
#if lnx_torrent
import leenkx.network.Leenkx;
import leenkx.network.torrent.Torrent;
import leenkx.network.torrent.TorrentFile;
#end
class GetTorrentFileNode extends LogicNode {
public var property0:String;
public var file:Dynamic;
var net_Url:String;
public function new(tree:LogicTree) {
super(tree);
}
override function run(from:Int) {
#if lnx_torrent
file = null;
var id = inputs[2].get();
var match = inputs[3].get();
if (id != null && match != null) {
var connection = inputs[1].get();
if (connection != null) net_Url = connection._url;
var socket = net_Url != null ? Leenkx.connections[net_Url] : null;
var client = socket != null ? socket.client : null;
var t = client != null ? client.get(id) : (Std.isOfType(id, Torrent) ? id : null);
if (t != null) file = findFile(t, match);
}
#end
runOutput(0);
}
#if lnx_torrent
function findFile(t:Torrent, match:Dynamic):TorrentFile {
var files = t.files;
switch (property0) {
case "index":
var i = Std.parseInt(Std.string(match));
if (i != null && i >= 0 && i < files.length) {
return files[i];
}
case "name":
var s = Std.string(match);
for (f in files) {
if (f.name == s) return f;
}
case "path":
var s = Std.string(match);
for (f in files) {
if (f.path == s) return f;
}
default:
var s = Std.string(match);
if (!StringTools.startsWith(s, ".")) s = "." + s;
for (f in files) {
if (StringTools.endsWith(f.name, s)) return f;
}
}
return null;
}
#end
override function get(from:Int):Dynamic {
#if lnx_torrent
var f:TorrentFile = file;
return switch (from) {
case 1: file;
case 2: f != null ? f.name : null;
case 3: f != null ? f.path : null;
case 4: f != null ? f.length : 0;
case 5: f != null ? f.type : null;
case 6: f != null ? f.getBytes() : null;
case 7: f != null;
case 8: f != null ? f.downloaded : 0.0;
case 9: f != null ? f.progress : 0.0;
default: throw "Unreachable";
}
#else
return null;
#end
}
}

View File

@ -1,8 +1,8 @@
package leenkx.logicnode;
import leenkx.system.Event;
import leenkx.network.Connect;
import iron.object.Object;
#if lnx_torrent
import leenkx.network.Leenkx;
import leenkx.network.torrent.LeenkxClient;
#end
class LeenkxCloseConnectionNode extends LogicNode {
public var property0: String;
@ -12,41 +12,29 @@ class LeenkxCloseConnectionNode extends LogicNode {
}
override function run(from:Int) {
#if lnx_torrent
var connection = inputs[1].get();
if (connection == null) return;
if (connection == null) return;
var socket = Leenkx.connections[connection._url];
if (socket == null) return;
if(property0 == "client") {
var script = '
for (p in leenkx.network.Leenkx.connections.h["' + connection._url + '"].client.torrent._peers){
try{
leenkx.network.Leenkx.id.set("' + connection._url + '",p);
leenkx.network.Leenkx.connections.h["' + connection._url + '"].client.torrent._peers[p].conn._pc.close();
leenkx.network.Leenkx.connections.h["' + connection._url + '"].client.torrent._peers[p].conn.destroy();
leenkx.network.Leenkx.connections.h["' + connection._url + '"].onclose();
}catch(error){
console.log("Error: " + error);
}
}
';
js.Syntax.code('(1, eval)({0})', script.toString());
} else {
var script = '
try{
var lnxConn = leenkx.network.Leenkx.connections.h["' + connection._url + '"];
if (lnxConn && lnxConn.client && lnxConn.client.torrent && lnxConn.client.torrent._peers) {
for (var p in lnxConn.client.torrent._peers) {
lnxConn.client.torrent._peers[p].conn._pc.close();
lnxConn.client.torrent._peers[p].conn.destroy();
leenkx.network.Leenkx.id.set("' + connection._url + '", p);
lnxConn.onclose();
}
}
}catch(error){
console.log("Error: " + error);
}
';
js.Syntax.code('(1, eval)({0})', script.toString());
var lnxCn:LeenkxClient = socket.client;
if (property0 == "peer") {
var id:String = inputs.length > 2 ? inputs[2].get() : null;
if (id != null && lnxCn != null) {
lnxCn.peers.remove(id);
lnxCn.seenFired.remove(id);
}
runOutput(0);
return;
}
if (lnxCn != null) {
lnxCn.destroy();
socket.client = null;
}
Leenkx.connections.remove(connection._url);
socket.onclose();
#end
runOutput(0);
}
}

View File

@ -1,10 +1,10 @@
package leenkx.logicnode;
import leenkx.system.Event;
#if lnx_torrent
import leenkx.network.Leenkx;
import iron.object.Object;
#end
class LeenkxEventNode extends LogicNode {
public var property0: String;
public var property1: String;
public var value: String;
@ -14,90 +14,63 @@ class LeenkxEventNode extends LogicNode {
public function new(tree:LogicTree) {
super(tree);
tree.notifyOnInit(init);
tree.notifyOnRemove(onRemove);
}
function init() {
if (property0 == "client") {
net_Url = inputs[0].get();
switch (property1) {
case "onopen": value = Leenkx.onOpenEvent;listener = Event.add(value, onEvent, tree.object.uid);
case "onmessage": value = Leenkx.onMessageEvent;listener = Event.add(value, onEvent, tree.object.uid);
case "onerror": value = Leenkx.onErrorEvent;listener = Event.add(value, onEvent, tree.object.uid);
case "onclose": value = Leenkx.onCloseEvent;listener = Event.add(value, onEvent, tree.object.uid);
case "onseen": value = Leenkx.onSeenEvent;listener = Event.add(value, onEvent, tree.object.uid);
case "onserver": value = Leenkx.onServerEvent;listener = Event.add(value, onEvent, tree.object.uid);
case "onconnections": value = Leenkx.onConnectionsEvent;listener = Event.add(value, onEvent, tree.object.uid);
case "onping": value = Leenkx.onPingEvent;listener = Event.add(value, onEvent, tree.object.uid);
case "onleft": value = Leenkx.onLeftEvent;listener = Event.add(value, onEvent, tree.object.uid);
case "ontimeout": value = Leenkx.onTimeOutEvent;listener = Event.add(value, onEvent, tree.object.uid);
case "onrpc": value = Leenkx.onRpcEvent;listener = Event.add(value, onEvent, tree.object.uid);
case "onrpcresponse": value = Leenkx.onRpcResponseEvent;listener = Event.add(value, onEvent, tree.object.uid);
case "onwireleft": value = Leenkx.onWireLeftEvent;listener = Event.add(value, onEvent, tree.object.uid);
case "onwireseen": value = Leenkx.onWireSeenEvent;listener = Event.add(value, onEvent, tree.object.uid);
case "ontorrent": value = Leenkx.onTorrentEvent;listener = Event.add(value, onEvent, tree.object.uid);
case "ontracker": value = Leenkx.onTrackerEvent;listener = Event.add(value, onEvent, tree.object.uid);
case "onannounce": value = Leenkx.onAnnounceEvent;listener = Event.add(value, onEvent, tree.object.uid);
default: throw "Failed to set client event type.";
}
} else if (property0 == "host") {
#if (sys || kha_krom)
var net_Domain = inputs[0].get();
var net_Port = inputs[1].get();
net_Url = "ws://" + net_Domain + ":" + Std.string(net_Port);
switch (property1) {
case "onopen": value = Leenkx.onOpenEvent;listener = Event.add(value, onEvent, tree.object.uid);
case "onmessage": value = Leenkx.onMessageEvent;listener = Event.add(value, onEvent, tree.object.uid);
case "onerror": value = Leenkx.onErrorEvent;listener = Event.add(value, onEvent, tree.object.uid);
case "onclose": value = Leenkx.onCloseEvent;listener = Event.add(value, onEvent, tree.object.uid);
default: throw "Failed to set host event type.";
}
#end
} else if (property0 == "securehost"){
#if (sys || kha_krom)
var net_Domain = inputs[0].get();
var net_Port = inputs[1].get();
net_Url = "wss://" + net_Domain + ":" + Std.string(net_Port);
switch (property1) {
case "onopen": value = Leenkx.onOpenEvent;listener = Event.add(value, onEvent, tree.object.uid);
case "onmessage": value = Leenkx.onMessageEvent;listener = Event.add(value, onEvent, tree.object.uid);
case "onerror": value = Leenkx.onErrorEvent;listener = Event.add(value, onEvent, tree.object.uid);
case "onclose": value = Leenkx.onCloseEvent;listener = Event.add(value, onEvent, tree.object.uid);
default: throw "Failed to set host event type.";
}
#end
#if lnx_torrent
var u:Dynamic = inputs[0].get();
net_Url = Std.isOfType(u, String) ? u : (u != null ? u._url : null);
switch (property1) {
case "onopen": value = Leenkx.onOpenEvent;
case "onmessage": value = Leenkx.onMessageEvent;
case "onerror": value = Leenkx.onErrorEvent;
case "onclose": value = Leenkx.onCloseEvent;
case "onseen": value = Leenkx.onSeenEvent;
case "onserver": value = Leenkx.onServerEvent;
case "onconnections": value = Leenkx.onConnectionsEvent;
case "onping": value = Leenkx.onPingEvent;
case "onleft": value = Leenkx.onLeftEvent;
case "ontimeout": value = Leenkx.onTimeOutEvent;
case "onwireleft": value = Leenkx.onWireLeftEvent;
case "onwireseen": value = Leenkx.onWireSeenEvent;
case "ontorrent": value = Leenkx.onTorrentEvent;
case "ontorrentadded": value = Leenkx.onTorrentAddedEvent;
case "ontracker": value = Leenkx.onTrackerEvent;
case "onannounce": value = Leenkx.onAnnounceEvent;
case "ontorrentdone": value = Leenkx.onTorrentDoneEvent;
case "onload": value = Leenkx.onLoadEvent;
default:
trace("Failed to set client event type");
}
listener = Event.add(value, onEvent, tree.object.uid);
#end
}
function onEvent() {
runOutput(0);
}
function onRemove() {
if (listener != null) {
Event.removeListener(listener);
listener = null;
}
}
override function get(from: Int): Dynamic {
if (property0 == "host") {
return switch (from) {
case 1: Leenkx.id.get(net_Url);
case 2: Leenkx.data.get(net_Url);
default: throw "Unreachable";
}
}
else if (property0 == "securehost") {
return switch (from) {
case 1: Leenkx.id.get(net_Url);
case 2: Leenkx.data.get(net_Url);
default: throw "Unreachable";
}
}
else {
return switch (from) {
case 1: Leenkx.id.get(net_Url);
case 2: Leenkx.data.get(net_Url);
default: throw "Unreachable";
}
#if lnx_torrent
return switch (from) {
case 1: Leenkx.id.get(net_Url);
case 2: Leenkx.data.get(net_Url);
default: throw "Unreachable";
}
#else
return null;
#end
}
}

View File

@ -1,8 +1,6 @@
package leenkx.logicnode;
import leenkx.network.Connect;
import leenkx.network.Buffer;
import haxe.io.Bytes;
import iron.object.Object;
import iron.math.Vec4;
import iron.math.Quat;
import iron.math.Mat4;
@ -22,139 +20,106 @@ class LeenkxMessageParserNode extends LogicNode {
if (data == null) return;
api = inputs[1].get();
if (api == null) api = "";
var buffer:Buffer;
if (Std.isOfType(data, Buffer)) {
buffer = cast data;
} else {
var rawBytes:Bytes;
try {
rawBytes = haxe.crypto.Base64.decode(Std.string(data));
} catch(e) {
trace("Base64 decode failed: " + e);
return;
}
buffer = new Buffer();
buffer.writeBytes(rawBytes);
}
/**
var buffer = cast(data, Buffer);
api = inputs[1].get();
if(buffer.endsWith(api) == false){
return;
}
**/
var buffer = new Buffer();
if(!StringTools.startsWith(data,api)){
return;
}
switch (property0) {
case "string":
parsed = data.substr(api.length, data.length);
runOutput(0);
case "vector":
var vec = data.substr(api.length, data.length);
vec = StringTools.replace(vec, "(", "");
vec = StringTools.replace(vec, ")", "");
vec = StringTools.replace(vec, " ", "");
vec = StringTools.trim(vec);
var array = vec.split(",").map(Std.parseFloat);
parsed = new Vec4( array[0], array[1], array[2], array[3] );
runOutput(0);
case "float":
var flt:String = data.substr(api.length, data.length);
var float = StringTools.trim(flt);
parsed = Std.parseFloat(float);
runOutput(0);
case "integer":
var int:String = data.substr(api.length, data.length);
var integer = StringTools.trim(int);
parsed = Std.parseInt(integer);
runOutput(0);
case "boolean":
var boolean:String = data.substr(api.length, data.length);
if(boolean == "true"){
parsed = true;
} else {
parsed = false;
}
runOutput(0);
case "transform":
var tsfm = data.substr(api.length, data.length);
tsfm = StringTools.replace(tsfm, "[", "");
tsfm = StringTools.replace(tsfm, "]", "");
tsfm = StringTools.replace(tsfm, " ", "");
tsfm = StringTools.trim(tsfm);
var array = tsfm.split(",").map(Std.parseFloat);
parsed = new Mat4(
array[0], array[1], array[2], array[3],
array[4], array[5], array[6], array[7],
array[8], array[9], array[10], array[11],
array[12], array[13], array[14], array[15]
);
runOutput(0);
case "rotation":
var bytes = new haxe.io.BytesInput(buffer.readUntil(api));
var rot = new Quat();
for (f in Reflect.fields(rot)) {
Reflect.setField(rot, f, bytes.readFloat());
}
parsed = rot;
runOutput(0);
default: throw "Failed to parse data.";
}
return;
switch (property0) {
case "string":
var bytes = buffer.readAllAvailableBytes();
parsed = bytes.toString().substr(0, bytes.length - api.length);
trace(parsed);
runOutput(0);
case "vector":
var bytes = new haxe.io.BytesInput(buffer.readUntil(api));
var readBytes = buffer.readUntil(api);
if (readBytes == null) return;
var bytes = new haxe.io.BytesInput(readBytes);
var vec = new Vec4();
for (f in Reflect.fields(vec)) {
Reflect.setField(vec, f, bytes.readFloat());
}
vec.x = bytes.readFloat();
vec.y = bytes.readFloat();
vec.z = bytes.readFloat();
vec.w = bytes.readFloat();
parsed = vec;
runOutput(0);
case "float":
var bytes = new haxe.io.BytesInput(buffer.readUntil(api));
var readBytes = buffer.readUntil(api);
if (readBytes == null) return;
var bytes = new haxe.io.BytesInput(readBytes);
var float: Float = bytes.readFloat();
parsed = float;
runOutput(0);
case "integer":
var bytes = new haxe.io.BytesInput(buffer.readUntil(api));
var readBytes = buffer.readUntil(api);
if (readBytes == null) return;
var bytes = new haxe.io.BytesInput(readBytes);
var integer: Int = bytes.readInt32();
parsed = integer;
runOutput(0);
case "boolean":
var bytes = buffer.readAllAvailableBytes();
var boolean = bytes.toString().substr(0, bytes.length - api.length);
if(boolean == "true"){
if (boolean == "true") {
parsed = true;
} else {
parsed = false;
}
runOutput(0);
case "transform":
var bytes = new haxe.io.BytesInput(buffer.readUntil(api));
var readBytes = buffer.readUntil(api);
if (readBytes == null) return;
var bytes = new haxe.io.BytesInput(readBytes);
var loc: Vec4 = new Vec4();
var rot: Quat = new Quat();
var scl: Vec4 = new Vec4();
for (f in Reflect.fields(loc)) {
Reflect.setField(loc, f, bytes.readFloat());
}
for (f in Reflect.fields(rot)) {
Reflect.setField(rot, f, bytes.readFloat());
}
for (f in Reflect.fields(scl)) {
Reflect.setField(scl, f, bytes.readFloat());
try {
loc.x = bytes.readFloat();
loc.y = bytes.readFloat();
loc.z = bytes.readFloat();
loc.w = bytes.readFloat();
rot.x = bytes.readFloat();
rot.y = bytes.readFloat();
rot.z = bytes.readFloat();
rot.w = bytes.readFloat();
scl.x = bytes.readFloat();
scl.y = bytes.readFloat();
scl.z = bytes.readFloat();
scl.w = bytes.readFloat();
} catch(e: Dynamic) {
trace("readFloat error: " + e);
return;
}
var transform = Mat4.identity();
parsed = transform.compose(loc, rot, scl);
runOutput(0);
case "rotation":
var bytes = new haxe.io.BytesInput(buffer.readUntil(api));
var readBytes = buffer.readUntil(api);
if (readBytes == null) return;
var bytes = new haxe.io.BytesInput(readBytes);
var rot = new Quat();
for (f in Reflect.fields(rot)) {
Reflect.setField(rot, f, bytes.readFloat());
}
rot.x = bytes.readFloat();
rot.y = bytes.readFloat();
rot.z = bytes.readFloat();
rot.w = bytes.readFloat();
parsed = rot;
runOutput(0);
default: throw "Failed to parse data.";
}
}
override function get(from: Int): Dynamic {

View File

@ -1,401 +1,124 @@
package leenkx.logicnode;
import leenkx.network.Connect;
import leenkx.network.Leenkx;
import leenkx.network.Buffer;
import haxe.io.Bytes;
import iron.object.Object;
#if lnx_torrent
import leenkx.network.Leenkx;
import leenkx.network.torrent.LeenkxClient;
#end
import iron.math.Vec4;
import iron.math.Quat;
import iron.math.Mat4;
class LeenkxSendMessageNode extends LogicNode {
public var property0: String;
public var property1: String;
public var id: String;
public var all: Bool;
public var property2: Bool;
public function new(tree:LogicTree) {
super(tree);
}
override function run(from:Int) {
#if js
var connection = inputs[1].get();
if (connection == null) return;
var api: String = inputs[2].get();
var api: String = inputs[2].get();
var message: Dynamic = inputs[3].get();
if (message == null) return;
var broadcast: Bool = inputs.length > 4 && inputs[4].get() == true;
var targetAddress: String = inputs.length > 5 ? inputs[5].get() : null;
if (!broadcast && (targetAddress == null || targetAddress == "")) {
return;
}
#if lnx_torrent
var socket = Leenkx.connections[connection._url];
if (socket == null) return;
var lnxCn:LeenkxClient = socket.client;
if (lnxCn == null) return;
var buffer = new Buffer();
switch (property1) {
case "string":
buffer.writeBytes(Bytes.ofString(message));
case "vector":
var bytesOut = new haxe.io.BytesOutput();
var f:Null<Float>;
if ((f = message.x) != null) bytesOut.writeFloat(f);
if ((f = message.y) != null) bytesOut.writeFloat(f);
if ((f = message.z) != null) bytesOut.writeFloat(f);
if ((f = message.w) != null) bytesOut.writeFloat(f);
buffer.writeBytes(bytesOut.getBytes());
case "float":
var bytesOut = new haxe.io.BytesOutput();
bytesOut.writeFloat(message);
buffer.writeBytes(bytesOut.getBytes());
case "integer":
var bytesOut = new haxe.io.BytesOutput();
bytesOut.writeInt32(message);
buffer.writeBytes(bytesOut.getBytes());
case "boolean":
if (message == true) {
buffer.writeBytes(Bytes.ofString("true"));
} else {
buffer.writeBytes(Bytes.ofString("false"));
}
case "transform":
var bytesOut = new haxe.io.BytesOutput();
var transform:Mat4 = message;
var loc = new Vec4();
var rot = new Quat();
var scale = new Vec4();
transform.decompose(loc, rot, scale);
bytesOut.writeFloat(loc.x);
bytesOut.writeFloat(loc.y);
bytesOut.writeFloat(loc.z);
bytesOut.writeFloat(loc.w);
bytesOut.writeFloat(rot.x);
bytesOut.writeFloat(rot.y);
bytesOut.writeFloat(rot.z);
bytesOut.writeFloat(rot.w);
bytesOut.writeFloat(scale.x);
bytesOut.writeFloat(scale.y);
bytesOut.writeFloat(scale.z);
bytesOut.writeFloat(scale.w);
buffer.writeBytes(bytesOut.getBytes());
case "rotation":
var bytesOut = new haxe.io.BytesOutput();
var f:Null<Float>;
if ((f = message.x) != null) bytesOut.writeFloat(f);
if ((f = message.y) != null) bytesOut.writeFloat(f);
if ((f = message.z) != null) bytesOut.writeFloat(f);
if ((f = message.w) != null) bytesOut.writeFloat(f);
buffer.writeBytes(bytesOut.getBytes());
default: trace("Failed to send data.");
}
if (api != null && api != "") {
buffer.writeBytes(Bytes.ofString(api));
}
var rawBytes = buffer.readAllAvailableBytes();
try {
if(inputs[5].get() == true){
var script = 'for (p in lnx_' + connection._url +'.torrent._peers){
try{
lnx_' + connection._url +'.torrent._peers[p].conn.send(`'+ api + message + '`);
}catch(error){
console.log("Error: " + error);
}
}';
js.Syntax.code('(1, eval)({0})', script.toString());
runOutput(0);
if (property2) {
var payload = haxe.crypto.Base64.encode(rawBytes);
if (broadcast) {
lnxCn.send(null, payload);
} else {
lnxCn.send(targetAddress, payload);
}
} else {
var script = 'lnx_' + connection._url +'.send(`' + inputs[4].get() + '`, `'+ api + message + '` );';
js.Syntax.code('(1, eval)({0})', script.toString());
runOutput(0);
lnxCn.sendRawMessage(rawBytes, broadcast ? null : targetAddress);
}
} catch(error) {
trace("Error: " + error);
}
return;
runOutput(0);
switch (property1) {
case "string":
if(property0 == "client"){
var buffer = new Buffer();
buffer.writeBytes(Bytes.ofString(message));
if(api != "" || api != null){
buffer.writeBytes(Bytes.ofString(api));
#else
runOutput(0);
try {
connection.buffer = buffer;
if(inputs[5].get() == true){
var script = 'for (p in lnx_' + connection._url +'.torrent._peers){
try{
lnx_' + connection._url +'.torrent._peers[p].conn.send(leenkx.network.Leenkx.connections.h["'+ connection._url +'"].buffer );
}catch(error){
console.log("Error: " + error);
}
}';
js.Syntax.code('(1, eval)({0})', script.toString());
runOutput(0);
} else {
var script = 'lnx_' + connection._url +'.send("' + inputs[4].get() + '",leenkx.network.Leenkx.connections.h["'+ connection._url +'"].buffer );';
js.Syntax.code('(1, eval)({0})', script.toString());
runOutput(0);
}
} catch(error) {
trace("Error: " + error);
}
}
} else {
if(inputs[5].get() == true){
var script = 'lnx_' + connection._url + '.send(`' + api + message + '`);';
js.Syntax.code('(1, eval)({0})', script);
}else{
var script = 'lnx_' + connection._url + '.send(`' + inputs[4].get() + '`, `' + api + message + '`);';
js.Syntax.code('(1, eval)({0})', script);
}
runOutput(0);
return;
}
case "vector":
if(property0 == "client"){
var bytesOut = new haxe.io.BytesOutput();
var i = Reflect.fields(message);
for (b in i) {
bytesOut.writeFloat(Reflect.field(message, b));
}
var buffer = new Buffer();
buffer.writeBytes(bytesOut.getBytes());
if(api != "" || api != null){
buffer.writeBytes(Bytes.ofString(api));
}
try {
connection.buffer = buffer;
if(inputs[5].get() == true){
var script = 'for (p in lnx_' + connection._url +'.torrent._peers){
try{
lnx_' + connection._url +'.torrent._peers[p].conn.send("' + api + message + '");
}catch(error){
console.log("Error: " + error);
}
}';
js.Syntax.code('(1, eval)({0})', script.toString());
runOutput(0);
} else {
var script = 'lnx_' + connection._url +'.send("' + inputs[4].get() + '",leenkx.network.Leenkx.connections.h["'+ connection._url +'"].buffer );';
js.Syntax.code('(1, eval)({0})', script.toString());
runOutput(0);
}
} catch(error) {
trace("Error: " + error);
}
} else {
if(inputs[5].get() == true){
var script = 'lnx_' + connection._url + '.send(`' + api + message + '`);';
js.Syntax.code('(1, eval)({0})', script);
}else{
var script = 'lnx_' + connection._url + '.send(`' + inputs[4].get() + '`, `' + api + message + '`);';
js.Syntax.code('(1, eval)({0})', script);
}
runOutput(0);
return;
}
case "float":
if(property0 == "client"){
var bytesOut = new haxe.io.BytesOutput();
bytesOut.writeFloat(message);
var buffer = new Buffer();
buffer.writeBytes(bytesOut.getBytes());
if(api != "" || api != null){
buffer.writeBytes(Bytes.ofString(api));
}
try {
connection.buffer = buffer;
if(inputs[5].get() == true){
var script = 'for (p in lnx_' + connection._url +'.torrent._peers){
try{
lnx_' + connection._url +'.torrent._peers[p].conn.send(leenkx.network.Leenkx.connections.h["'+ connection._url +'"].buffer );
}catch(error){
console.log("Error: " + error);
}
}';
js.Syntax.code('(1, eval)({0})', script.toString());
runOutput(0);
} else {
var script = 'lnx_' + connection._url +'.send("' + inputs[4].get() + '",leenkx.network.Leenkx.connections.h["'+ connection._url +'"].buffer );';
js.Syntax.code('(1, eval)({0})', script.toString());
runOutput(0);
}
} catch(error) {
trace("Error: " + error);
}
} else {
if(inputs[5].get() == true){
var script = 'lnx_' + connection._url + '.send(`' + api + message + '`);';
js.Syntax.code('(1, eval)({0})', script);
}else{
var script = 'lnx_' + connection._url + '.send(`' + inputs[4].get() + '`, `' + api + message + '`);';
js.Syntax.code('(1, eval)({0})', script);
}
runOutput(0);
return;
}
case "integer":
if(property0 == "client"){
var bytesOut = new haxe.io.BytesOutput();
bytesOut.writeInt32(message);
var buffer = new Buffer();
buffer.writeBytes(bytesOut.getBytes());
if(api != "" || api != null){
buffer.writeBytes(Bytes.ofString(api));
}
try {
connection.buffer = buffer;
if(inputs[5].get() == true){
var script = 'for (p in lnx_' + connection._url +'.torrent._peers){
try{
lnx_' + connection._url +'.torrent._peers[p].conn.send(leenkx.network.Leenkx.connections.h["'+ connection._url +'"].buffer );
}catch(error){
console.log("Error: " + error);
}
}';
js.Syntax.code('(1, eval)({0})', script.toString());
runOutput(0);
} else {
var script = 'lnx_' + connection._url +'.send("' + inputs[4].get() + '",leenkx.network.Leenkx.connections.h["'+ connection._url +'"].buffer );';
js.Syntax.code('(1, eval)({0})', script.toString());
runOutput(0);
}
} catch(error) {
trace("Error: " + error);
}
} else {
if(inputs[5].get() == true){
var script = 'lnx_' + connection._url + '.send(`' + api + message + '`);';
js.Syntax.code('(1, eval)({0})', script);
}else{
var script = 'lnx_' + connection._url + '.send(`' + inputs[4].get() + '`, `' + api + message + '`);';
js.Syntax.code('(1, eval)({0})', script);
}
runOutput(0);
return;
}
case "boolean":
if(property0 == "client"){
var buffer = new Buffer();
if(message == true){
buffer.writeBytes(Bytes.ofString("true"));
} else {
buffer.writeBytes(Bytes.ofString("false"));
}
if(api != "" || api != null){
buffer.writeBytes(Bytes.ofString(api));
}
try {
connection.buffer = buffer;
if(inputs[5].get() == true){
var script = 'for (p in lnx_' + connection._url +'.torrent._peers){
try{
lnx_' + connection._url +'.torrent._peers[p].conn.send(leenkx.network.Leenkx.connections.h["'+ connection._url +'"].buffer );
}catch(error){
console.log("Error: " + error);
}
}';
js.Syntax.code('(1, eval)({0})', script.toString());
runOutput(0);
} else {
var script = 'lnx_' + connection._url +'.send("' + inputs[4].get() + '",leenkx.network.Leenkx.connections.h["'+ connection._url +'"].buffer );';
js.Syntax.code('(1, eval)({0})', script.toString());
runOutput(0);
}
} catch(error) {
trace("Error: " + error);
}
} else {
if(inputs[5].get() == true){
var script = 'lnx_' + connection._url + '.send(`' + api + message + '`);';
js.Syntax.code('(1, eval)({0})', script);
}else{
var script = 'lnx_' + connection._url + '.send(`' + inputs[4].get() + '`, `' + api + message + '`);';
js.Syntax.code('(1, eval)({0})', script);
}
runOutput(0);
return;
}
case "transform":
if(property0 == "client"){
var bytesOut = new haxe.io.BytesOutput();
var transform:Mat4 = message;
var loc = new Vec4();
var rot = new Quat();
var scale = new Vec4();
transform.decompose(loc, rot, scale);
var l = Reflect.fields(loc);
for (b in l) {
bytesOut.writeFloat(Reflect.field(loc, b));
}
var r = Reflect.fields(rot);
for (b in r) {
bytesOut.writeFloat(Reflect.field(rot, b));
}
var s = Reflect.fields(scale);
for (b in s) {
bytesOut.writeFloat(Reflect.field(scale, b));
}
var buffer = new Buffer();
buffer.writeBytes(bytesOut.getBytes());
if(api != "" || api != null){
buffer.writeBytes(Bytes.ofString(api));
}
try {
connection.buffer = buffer;
if(inputs[5].get() == true){
var script = 'for (p in lnx_' + connection._url +'.torrent._peers){
try{
//console.log("Mine: " + lx_8001.torrent.discovery.peerId + " || Incomming: " + p);
lnx_' + connection._url +'.torrent._peers[p].conn.send(leenkx.network.Leenkx.connections.h["'+ connection._url +'"].buffer );
}catch(error){
console.log("Error: " + error);
}
}';
js.Syntax.code('(1, eval)({0})', script.toString());
runOutput(0);
} else {
//var script = 'lnx_' + connection._url +'.send("' + inputs[4].get() + '",leenkx.network.Leenkx.connections.h["'+ connection._url +'"].buffer );';
var script = 'lnx_' + connection._url +'.send(leenkx.network.Leenkx.connections.h["'+ connection._url +'"].buffer );';
js.Syntax.code('(1, eval)({0})', script.toString());
runOutput(0);
}
} catch(error) {
trace("Error: " + error);
}
}
case "rotation":
if(property0 == "client"){
var bytesOut = new haxe.io.BytesOutput();
var i = Reflect.fields(message);
for (b in i) {
bytesOut.writeFloat(Reflect.field(message, b));
}
var buffer = new Buffer();
buffer.writeBytes(bytesOut.getBytes());
if(api != "" || api != null){
buffer.writeBytes(Bytes.ofString(api));
}
try {
connection.buffer = buffer;
if(inputs[5].get() == true){
var script = 'for (p in lnx_' + connection._url +'.torrent._peers){
try{
lnx_' + connection._url +'.torrent._peers[p].conn.send(leenkx.network.Leenkx.connections.h["'+ connection._url +'"].buffer );
}catch(error){
console.log("Error: " + error);
}
}';
js.Syntax.code('(1, eval)({0})', script.toString());
runOutput(0);
} else {
var script = 'lnx_' + connection._url +'.send("' + inputs[4].get() + '",leenkx.network.Leenkx.connections.h["'+ connection._url +'"].buffer );';
js.Syntax.code('(1, eval)({0})', script.toString());
runOutput(0);
}
} catch(error) {
trace("Error: " + error);
}
} else {
if(inputs[5].get() == true){
var script = 'lnx_' + connection._url + '.send(`' + api + message + '`);';
js.Syntax.code('(1, eval)({0})', script);
}else{
var script = 'lnx_' + connection._url + '.send(`' + inputs[4].get() + '`, `' + api + message + '`);';
js.Syntax.code('(1, eval)({0})', script);
}
runOutput(0);
return;
}
}
#end
}
}
/*******_________________________________________
try {
if(inputs[5].get() == true){
var script = 'for (p in lnx_' + connection._url +'.torrent._peers){
try{
lnx_' + connection._url +'.torrent._peers[p].conn.send("' + api + message + '");
}catch(error){
console.log("Error: " + error);
}
}';
js.Syntax.code('(1, eval)({0})', script.toString());
runOutput(0);
} else {
var script = 'for (p in lnx_' + connection._url +'.torrent._peers){
try{
lx_' + connection._url +'.torrent._peers[' + inputs[4].get() + '].conn.send("' + api + message + '");
}catch(error){
console.log("Error: " + error);
}
}';
js.Syntax.code('(1, eval)({0})', script.toString());
runOutput(0);
}
} catch(error) {
trace("Error: " + error);
}
return;
_________________________________________*****/

View File

@ -23,6 +23,7 @@ class NetworkMessageParserNode extends LogicNode {
var buffer = cast(data, Buffer);
api = inputs[1].get();
if (api == null) api = "";
if(buffer.endsWith(api) == false){
return;
}

View File

@ -0,0 +1,65 @@
package leenkx.logicnode;
#if lnx_torrent
import leenkx.network.Leenkx;
import leenkx.network.torrent.Torrent;
#end
class OnTorrentEventNode extends LogicNode {
public var property0:String;
public var eventValue:Dynamic;
var net_Url:String;
var torrent:Dynamic;
var registeredTorrents:Map<String, Bool> = new Map();
public function new(tree:LogicTree) {
super(tree);
}
override function run(from:Int) {
#if lnx_torrent
var connection = inputs[1].get();
if (connection == null) return;
var id = inputs[2].get();
if (id == null) return;
net_Url = connection._url;
var socket = Leenkx.connections[connection._url];
var client = socket != null ? socket.client : null;
var t = client != null ? client.get(id) : (Std.isOfType(id, Torrent) ? id : null);
if (t == null) return;
var event = property0 != null ? property0 : "done";
var self = this;
var fire = function(v:Dynamic) {
self.eventValue = v;
self.runOutput(0);
};
var key = t.infoHashHex != null ? t.infoHashHex : "v2:" + t.infoHashV2Hex;
var alreadyRegistered = registeredTorrents.exists(key);
if (!alreadyRegistered) registeredTorrents.set(key, true);
switch (event) {
case "ready":
if (t.ready) fire(null);
else if (!alreadyRegistered) t.on("ready", function() fire(null));
case "piece" | "wire" | "metadata" | "error":
if (!alreadyRegistered) t.on(event, function(v) fire(v));
default:
if (t.done) fire(null);
else if (!alreadyRegistered) t.on("done", function() fire(null));
}
torrent = t;
#end
}
override function get(from:Int):Dynamic {
#if lnx_torrent
return switch (from) {
case 1: torrent;
case 2: eventValue;
default: throw "Unreachable";
}
#else
return null;
#end
}
}

View File

@ -0,0 +1,73 @@
package leenkx.logicnode;
#if lnx_torrent
import haxe.io.Bytes;
import leenkx.network.torrent.Magnet;
import leenkx.network.torrent.MetaInfo;
#end
class ParseTorrentNode extends LogicNode {
var meta:Dynamic;
var magnet:Dynamic;
var idHex:String;
public var property0:String;
public function new(tree:LogicTree) {
super(tree);
}
override function run(from:Int) {
#if lnx_torrent
meta = null;
magnet = null;
idHex = null;
var src = inputs[1].get();
if (src == null) return;
switch (property0 != null ? property0 : "magnet") {
case "file":
if (Std.isOfType(src, MetaInfo)) {
meta = src;
} else {
var b = Std.isOfType(src, Bytes) ? src : Bytes.ofString(Std.string(src));
try {
meta = MetaInfo.fromTorrentBytes(b);
} catch(e:Dynamic) {
trace(e);
return;
}
}
case "id":
var s = Std.string(src);
idHex = s.length == 64 ? "v2:" + s : s;
default:
var m = Magnet.parse(Std.string(src));
if (m == null) {
return;
}
magnet = m;
idHex = m.infoHashHex != null ? m.infoHashHex : m.infoHashV2Hex;
}
#end
runOutput(0);
}
override function get(from:Int):Dynamic {
#if lnx_torrent
var m:MetaInfo = meta;
var mg:Magnet = magnet;
return switch (from) {
case 1: m;
case 2: m != null ? (m.infoHashHex != null ? m.infoHashHex : m.infoHashV2Hex) : idHex;
case 3: m != null ? m.name : (mg != null ? mg.name : null);
case 4: m != null ? m.announce : (mg != null ? mg.trackers : null);
case 5: m != null ? m.urlList : (mg != null ? mg.webSeeds : null);
case 6: m != null ? m.files : null;
case 7: m != null ? m.totalLength : 0;
case 8: m != null ? m.toTorrentBytes() : null;
default: throw "Unreachable";
}
#else
return null;
#end
}
}

View File

@ -1,52 +1,124 @@
package leenkx.logicnode;
import leenkx.system.Event;
import iron.object.Object;
#if lnx_torrent
import haxe.io.Bytes;
import leenkx.network.Leenkx;
import leenkx.network.torrent.MetaInfo;
import leenkx.network.torrent.Torrent;
#end
class SeedTorrentNode extends LogicNode {
public var torrent: Dynamic;
public var net_Url: Dynamic;
public var torrent:Dynamic;
public var net_Url:String;
public function new(tree:LogicTree) {
super(tree);
}
override function run(from:Int) {
#if js
#if lnx_torrent
var connection = inputs[1].get();
if (connection == null) return;
trace("got connection");
var data = inputs[3].get();
if (data == null) return;
trace("got data");
var opts = inputs[2].get();
var src = inputs[2].get();
net_Url = connection._url;
var cx = "lx_" + net_Url;
var lx:Dynamic = untyped js.Browser.window[cx];
var args = opts.toString();
trace(args);
var func = Reflect.field(lx.wt, "seed");
if (func != null) {
torrent = func(data, function (torrent) {
trace('Client is seeding ' + torrent.magnetURI);
});
//trace(torrent);
var socket = Leenkx.connections[connection._url];
if (socket == null || socket.client == null) return;
var client = socket.client;
var self = this;
var onSeeded = function(t:Torrent) {
if (t == null) return;
self.torrent = t;
var onDone = function() {
self.runOutput(2);
};
if (t.done) onDone();
else t.on("done", onDone);
};
if (Std.isOfType(src, Torrent)) {
var t:Torrent = src;
t.start();
torrent = t;
runOutput(0);
return;
}
var meta:MetaInfo = null;
if (Std.isOfType(src, MetaInfo)) {
meta = cast src;
} else if (Std.isOfType(src, Bytes)) {
try {
meta = MetaInfo.fromTorrentBytes(cast src);
} catch(e:Dynamic) {
return;
}
}
if (meta == null) {
return;
}
var files = filesFor(meta, inputs.length > 3 ? inputs[3].get() : null);
if (files == null) {
return;
}
client.seedMeta(meta, files, null, onSeeded);
runOutput(0);
#end
}
override function get(from: Int): Dynamic {
return switch (from) {
case 1: torrent;
default: throw "Unreachable";
}
}
#if lnx_torrent
function filesFor(meta:MetaInfo, data:Dynamic):Array<{path:String, data:Bytes}> {
if (data == null) return null;
var entries = meta.files;
if (Std.isOfType(data, Bytes)) {
var blob:Bytes = data;
if (blob.length != meta.totalLength) return null;
var files:Array<{path:String, data:Bytes}> = [];
var cursor = 0;
for (f in entries) {
files.push({path: f.path, data: blob.sub(cursor, f.length)});
cursor += f.length;
}
return files;
}
if (Std.isOfType(data, Array)) {
var items:Array<Dynamic> = data;
var files:Array<{path:String, data:Bytes}> = [];
for (it in items) {
var p = Reflect.field(it, "path");
var d:Bytes = Reflect.field(it, "data");
if (d == null) d = Reflect.field(it, "content");
if (p == null || d == null) continue;
var sp = Std.string(p);
var matched = false;
for (f in entries) {
if (f.path == sp || StringTools.endsWith(f.path, "/" + sp)) {
files.push({path: f.path, data: d});
matched = true;
break;
}
}
if (!matched) files.push({path: sp, data: d});
}
if (files.length == entries.length) return files;
return null;
}
return null;
}
#end
override function get(from:Int):Dynamic {
#if lnx_torrent
var t:Torrent = torrent;
return switch (from) {
case 1: torrent;
case 3: t != null ? t.infoHashHex : null;
case 4: t != null ? t.magnetURI : null;
default: throw "Unreachable";
}
#else
return null;
#end
}
}

View File

@ -0,0 +1,90 @@
package leenkx.logicnode;
#if lnx_torrent
import leenkx.network.Leenkx;
import leenkx.network.torrent.Torrent;
import leenkx.network.torrent.TorrentClient;
#end
class SetTrackersNode extends LogicNode {
public var property0:String;
public var target:Dynamic;
public function new(tree:LogicTree) {
super(tree);
}
override function run(from:Int) {
#if lnx_torrent
var p = property0 != null ? property0 : "add";
switch (p) {
case "add" | "override":
setTorrentTrackers(p == "override");
default:
setTrackerProperty(p);
}
#end
runOutput(0);
}
#if lnx_torrent
function setTorrentTrackers(trackerOverride:Bool):Void {
var id = inputs[2].get();
var urls = TorrentClient.trackerUrls(inputs[3].get());
if (urls == null) return;
var t:Torrent = Std.isOfType(id, Torrent) ? id : null;
if (t == null && id != null) {
var connection = inputs[1].get();
var socket = connection != null ? Leenkx.connections[connection._url] : null;
var client = socket != null ? socket.client : null;
if (client != null) t = client.get(id);
}
if (t != null) {
target = t;
if (trackerOverride) {
t.setTrackers(urls);
} else {
for (u in urls) t.addTracker(u);
}
} else if (id == null) {
target = null;
if (trackerOverride) {
TorrentClient.defaultTrackers = urls;
} else {
TorrentClient.mergeTrackers(TorrentClient.defaultTrackers, urls);
}
}
}
function setTrackerProperty(prop:String):Void {
var tracker:Dynamic = inputs[1].get();
if (tracker == null) return;
var v = inputs[2].get();
if (v == null) return;
var field = prop == "interval" ? "intervalSec" : prop;
var val:Dynamic = field == "trackerId" ? Std.string(v)
: Std.int(Std.parseFloat(Std.string(v)));
var setter:Dynamic = field == "intervalSec"
&& Reflect.field(tracker, "intervalSec") == null
? Reflect.field(tracker, "setAnnounceInterval") : null;
if (setter != null) {
try {
Reflect.callMethod(tracker, setter, [(val:Int) * 1000]);
} catch(e:Dynamic) {}
} else {
Reflect.setField(tracker, field, val);
}
target = tracker;
}
#end
override function get(from:Int):Dynamic {
#if lnx_torrent
return switch (from) {
case 1: target;
default: throw "Unreachable";
}
#else
return null;
#end
}
}

View File

@ -0,0 +1,74 @@
package leenkx.logicnode;
#if lnx_torrent
import leenkx.network.Leenkx;
import leenkx.network.torrent.Torrent;
#end
class TorrentClientInfoNode extends LogicNode {
var stats:Dynamic;
var list:Array<Dynamic>;
public function new(tree:LogicTree) {
super(tree);
}
override function run(from:Int) {
#if lnx_torrent
list = [];
stats = {downRate: 0.0, upRate: 0.0, progress: 0.0, ratio: 0.0};
var connection = inputs[1].get();
if (connection != null) {
var socket = Leenkx.connections[connection._url];
var client = socket != null ? socket.client : null;
if (client != null) collect(client);
}
#end
runOutput(0);
}
#if lnx_torrent
function collect(client:Dynamic) {
var down = 0.0;
var up = 0.0;
var received = 0.0;
var sent = 0.0;
var total = 0.0;
var left = 0.0;
var torrents:Map<String, Torrent> = client.torrents;
for (t in torrents) {
list.push(t);
var s = t.getStats();
down += s.downRate;
up += s.upRate;
received += s.downloaded;
sent += s.uploaded;
if (t.meta != null) {
total += t.meta.totalLength;
left += s.left;
}
}
stats = {
downRate: down,
upRate: up,
progress: total > 0 ? 1.0 - left / total : 0.0,
ratio: received > 0 ? sent / received : 0.0
};
}
#end
override function get(from:Int):Dynamic {
#if lnx_torrent
return switch (from) {
case 1: list;
case 2: list.length;
case 3: stats.downRate;
case 4: stats.upRate;
case 5: stats.progress;
case 6: stats.ratio;
default: throw "Unreachable";
}
#else
return null;
#end
}
}

View File

@ -0,0 +1,93 @@
package leenkx.logicnode;
#if lnx_torrent
import leenkx.network.Leenkx;
import leenkx.network.torrent.Torrent;
#end
class TorrentControlNode extends LogicNode {
public var property0:String;
var net_Url:String;
public function new(tree:LogicTree) {
super(tree);
}
override function run(from:Int) {
#if lnx_torrent
var connection = inputs[1].get();
if (connection == null) return;
var id = inputs[2].get();
if (id == null) return;
net_Url = connection._url;
var socket = Leenkx.connections[connection._url];
if (socket == null) return;
var client = socket.client;
var t = client != null ? client.get(id) : (Std.isOfType(id, Torrent) ? id : null);
var action = property0 != null ? property0 : "remove";
var file:Dynamic = inputs.length > 3 ? inputs[3].get() : null;
var t2 = t;
var startB = 0;
var endB = -1;
if (file != null) {
startB = file.offset;
endB = file.offset + file.length - 1;
if (t2 == null) t2 = file.torrent;
} else {
var sv = inputs.length > 4 ? inputs[4].get() : null;
var ev = inputs.length > 5 ? inputs[5].get() : null;
if (sv != null && Std.string(sv) != "") {
startB = Std.int(Std.parseFloat(Std.string(sv)));
}
if (ev != null && Std.string(ev) != "") {
endB = Std.int(Std.parseFloat(Std.string(ev)));
}
}
var pv = inputs.length > 6 ? inputs[6].get() : null;
var priority = pv != null && Std.string(pv) != "" ? Std.int(Std.parseFloat(Std.string(pv))) : null;
switch (action) {
case "start":
if (t != null) t.start();
case "stop":
if (t != null) t.stop();
case "select", "critical", "deselect":
if (t2 != null) {
if (endB < 0) {
endB = t2.meta != null ? t2.meta.totalLength - 1 : 0;
}
if (action == "critical") {
t2.critical(startB, endB);
} else if (action == "select") {
t2.select(startB, endB, priority);
} else {
t2.deselect(startB, endB, priority);
}
}
case "add-peer":
var peer = inputs.length > 3 ? inputs[3].get() : null;
if (t != null && peer != null) {
t.addPeer(Std.string(peer));
}
case "add-web-seed":
var url = inputs.length > 3 ? inputs[3].get() : null;
if (t != null && url != null) {
t.addWebSeed(Std.string(url));
}
case "remove", "remove-data":
var hex = t != null ? t.infoHashHex : Std.string(id);
if (hex == null) hex = Std.string(id);
if (client != null) {
client.remove(hex, action == "remove-data");
}
if (t != null && Leenkx.torrent.get(net_Url) == t) {
Leenkx.torrent.remove(net_Url);
Leenkx.file.remove(net_Url);
}
default:
}
#end
runOutput(0);
}
}

View File

@ -0,0 +1,74 @@
package leenkx.logicnode;
#if lnx_torrent
import leenkx.network.Leenkx;
import leenkx.network.torrent.Torrent;
import leenkx.network.torrent.TorrentClient;
#end
class TorrentInfoNode extends LogicNode {
public var torrent:Dynamic;
public var net_Url:String;
public function new(tree:LogicTree) {
super(tree);
}
override function run(from:Int) {
#if lnx_torrent
var id = inputs[2].get();
if (id == null) return;
torrent = null;
if (Std.isOfType(id, Torrent)) {
torrent = id;
} else {
var connection = inputs[1].get();
if (connection == null) return;
net_Url = connection._url;
var socket = Leenkx.connections[net_Url];
if (socket == null || socket.client == null) return;
torrent = socket.client.get(id);
}
#end
runOutput(0);
}
override function get(from:Int):Dynamic {
#if lnx_torrent
var t:Torrent = torrent;
return switch (from) {
case 1: t != null ? t.infoHashHex : null;
case 2: t != null && t.meta != null ? t.meta.name : null;
case 3: t != null ? t.numPeers : 0;
case 4: progress(t);
case 5: t != null ? t.getStats().downloaded : 0.0;
case 6: t != null ? t.getStats().uploaded : 0.0;
case 7: t != null ? t.done : false;
case 8: t != null ? t.magnetURI : null;
case 9: t != null ? t.files : null;
case 10: t != null && t.meta != null ? t.meta.numPieces : 0;
case 11: t != null && t.meta != null ? t.meta.pieceLength : 0;
case 12: t != null && t.meta != null ? t.meta.totalLength : 0;
case 13: t != null && t.meta != null ? t.meta.comment : null;
case 14: t != null && t.meta != null ? t.meta.createdBy : null;
case 15: t != null && t.meta != null ? t.meta.protocolVersion : 1;
case 16: t != null ? t.infoHashV2Hex : null;
case 17: t != null ? t.getStats().downRate : 0.0;
case 18: t != null ? t.getStats().upRate : 0.0;
case 19: t != null ? t.stopped : false;
case 20: t;
default: throw "Unreachable";
}
#else
return null;
#end
}
#if lnx_torrent
function progress(t:Torrent):Float {
if (t == null || t.meta == null || t.meta.totalLength == 0) return 0;
var stats = t.getStats();
return 1.0 - stats.left / t.meta.totalLength;
}
#end
}

View File

@ -121,7 +121,6 @@ class Leenkx {
class LeenkxSocket {
public var _url:String;
public var client:LeenkxClient;
public var torrentClient:leenkx.network.torrent.TorrentClient;
public var buffer:Dynamic;
public var onopen:Void->Void;
public var onclose:Void->Void;

View File

@ -9,7 +9,8 @@ import leenkx.network.torrent.TorrentInfo;
class LeenkxClient {
static var PEERTIMEOUT:Float = 5 * 60 * 1000;
public static var PEERTIMEOUT:Float = 5 * 60 * 1000;
public static var defaultHeartbeat:Int = 30000;
public var identifier:String;
public var seed:String;
@ -53,6 +54,54 @@ class LeenkxClient {
private var deferredOpts:TransportOpts = null;
private var heartbeatInterval:Int = 30000;
public var torrents(get, never):Map<String, Torrent>;
public var onTorrentAdded:Torrent -> Void;
public var onTorrentDone:Torrent -> Void;
var swarm:TorrentClient = null;
function get_torrents():Map<String, Torrent> {
return swarm != null ? swarm.torrents : [];
}
function getSwarm():TorrentClient {
if (swarm == null) {
swarm = new TorrentClient();
swarm.onError = function(e) {
if (onError != null) onError(e);
};
swarm.onTorrentAdded = function(t) {
if (onTorrentAdded != null) onTorrentAdded(t);
};
swarm.onTorrentDone = function(t) {
if (onTorrentDone != null) onTorrentDone(t);
};
}
return swarm;
}
public function add(torrentId:Dynamic, ?opts:Dynamic, ?cb:Torrent -> Void):Torrent {
return getSwarm().add(torrentId, opts, cb);
}
public function seedMeta(meta:MetaInfo,
files:Array<{path:String, data:Bytes}>,
?opts:Dynamic, ?cb:Torrent -> Void):Torrent {
return getSwarm().seedMeta(meta, files, opts, cb);
}
public function get(id:Dynamic):Torrent {
if (Std.isOfType(id, Torrent)) return id;
return swarm != null ? swarm.get(id) : null;
}
public function remove(infoHashHex:String, ?deleteData:Bool):Void {
if (swarm != null) swarm.remove(infoHashHex, deleteData);
}
public function listen(port:Int):Void {
getSwarm().listen(port);
}
public function new(?identifier:String, ?opts:LeenkxOptions) {
if (identifier != null && !Std.isOfType(identifier, String)) {
opts = cast identifier;
@ -60,7 +109,7 @@ class LeenkxClient {
}
opts = opts != null ? opts : {};
this.announce = opts.announce != null ? opts.announce : ["wss://ws1.leenkx.com"];
this.announce = opts.announce != null ? opts.announce : TorrentClient.defaultTrackers;
this.timeout = opts.timeout != null ? opts.timeout : PEERTIMEOUT;
if (opts.seed != null) {
@ -90,7 +139,7 @@ class LeenkxClient {
createTransport(opts, opts.deferJoin == true);
this.heartbeatInterval = opts.heartbeat != null ? opts.heartbeat : 30000;
this.heartbeatInterval = opts.heartbeat != null ? opts.heartbeat : defaultHeartbeat;
}
function createTransport(opts:LeenkxOptions, defer:Bool):Void {
@ -281,6 +330,10 @@ class LeenkxClient {
}
var packet = makePacket({y: PacketType.Leave});
sendRaw(packet);
if (swarm != null) {
swarm.destroy();
swarm = null;
}
if (transport != null) {
transport.destroy(cb);
} else if (cb != null) {

View File

@ -39,12 +39,9 @@ class Torrent {
public var announcePort:Int = 0;
public var downloadDir:String = null;
public function getStats():{downloaded:Float, uploaded:Float,
left:Float, downRate:Float, upRate:Float} {
public function getStats():{downloaded:Float, uploaded:Float, left:Float, downRate:Float, upRate:Float} {
var total = meta != null ? meta.totalLength : 0;
var downloaded = pieces != null
? Math.min(pieces.numHave * (meta.pieceLength:Float), total)
: 0;
var downloaded = pieces != null ? Math.min(pieces.numHave * (meta.pieceLength:Float), total) : 0;
var now = haxe.Timer.stamp();
var dt = now - lastStatT;
var downRate:Float = 0;
@ -93,8 +90,7 @@ class Torrent {
public function new(infoHash:Bytes, peerId:Bytes, trackers:Array<String>, ?meta:MetaInfo, ?storage:IStorage, ?downloadDir:String) {
TorrentClient.initMain();
this.infoHash = infoHash;
this.infoHashHex = infoHash != null
? Crypto.toHex(infoHash) : null;
this.infoHashHex = infoHash != null ? Crypto.toHex(infoHash) : null;
this.peerId = peerId;
this.trackers = trackers;
this.storage = storage;
@ -121,12 +117,9 @@ class Torrent {
function setMeta(m:MetaInfo):Void {
meta = m;
if (storage == null) {
var sz = m.storageLength > 0 ? m.storageLength
: m.totalLength;
var sz = m.storageLength > 0 ? m.storageLength : m.totalLength;
#if (sys || kha_krom)
storage = downloadDir != null
? new FileTreeStorage(downloadDir, m.files)
: new MemoryStorage(sz);
storage = downloadDir != null ? new FileTreeStorage(downloadDir, m.files) : new MemoryStorage(sz);
#else
storage = new MemoryStorage(sz);
#end
@ -139,8 +132,7 @@ class Torrent {
if (m.fileHashes == null && m.files != null) {
m.fileHashes = [for (f in m.files) null];
}
files = [for (f in m.files)
new TorrentFile(f, storage, this)];
files = [for (f in m.files) new TorrentFile(f, storage, this)];
infoHashV2 = m.infoHashV2;
infoHashV2Hex = m.infoHashV2Hex;
if (infoHash == null && m.infoHash != null) {
@ -151,8 +143,7 @@ class Torrent {
if (m.urlList == null && pendingWebSeeds != null) {
m.urlList = pendingWebSeeds;
}
magnetURI = Magnet.build(m.infoHash, m.name, trackers,
m.urlList, m.infoHashV2);
magnetURI = Magnet.build(m.infoHash, m.name, trackers, m.urlList, m.infoHashV2);
ready = true;
initWebSeeds();
emit1("metadata", m);
@ -214,6 +205,24 @@ class Torrent {
return startTracker(url);
}
public function setTrackers(urls:Array<String>):Void {
if (stopped) {
trackers = urls != null ? urls.copy() : [];
return;
}
if (rtcPool != null) {
rtcPool.close();
rtcPool = null;
}
for (t in httpTrackers) t.stop();
for (t in udpTrackers) t.stop();
httpTrackers = [];
udpTrackers = [];
trackerObjects = [];
trackers = urls != null ? urls.copy() : [];
startTrackers();
}
function startTracker(url:String):Dynamic {
var infoHashBin = TorrentInfo.toBinaryString(infoHash);
var peerIdBin = TorrentInfo.toBinaryString(peerId);
@ -231,9 +240,7 @@ class Torrent {
}
#if (sys || kha_krom)
else if (url.indexOf("http") == 0) {
var t = new leenkx.network.torrent.tracker.HttpTracker(
url, infoHash, peerId,
announcePort > 0 ? announcePort : 6881);
var t = new leenkx.network.torrent.tracker.HttpTracker(url, infoHash, peerId, announcePort > 0 ? announcePort : 6881);
t.statsProvider = getStats;
t.onPeers = onClassicPeers;
t.onError = function(e) {
@ -246,9 +253,7 @@ class Torrent {
#end
#if (sys || kha_krom)
else if (url.indexOf("udp") == 0) {
var t = new leenkx.network.torrent.tracker.UdpTracker(
url, infoHash, peerId,
announcePort > 0 ? announcePort : 6881);
var t = new leenkx.network.torrent.tracker.UdpTracker(url, infoHash, peerId, announcePort > 0 ? announcePort : 6881);
t.statsProvider = getStats;
t.onPeers = onClassicPeers;
t.onError = function(e) {
@ -272,8 +277,7 @@ class Torrent {
for (p in list) {
var key = "tcp:" + p.host + ":" + p.port;
if (wires.exists(key)) continue;
var ch = new leenkx.network.torrent.peer.TcpPeer(
p.host, p.port);
var ch = new leenkx.network.torrent.peer.TcpPeer(p.host, p.port);
addWire(key, ch);
}
}
@ -308,8 +312,7 @@ class Torrent {
requestBlocks(key);
};
wire.onHave = function(index) {
if (meta == null || index < 0
|| index >= meta.numPieces) return;
if (meta == null || index < 0 || index >= meta.numPieces) return;
var bf = peerBitfields.get(key);
if (bf != null && index < bf.length) bf[index] = true;
requestBlocks(key);
@ -317,17 +320,13 @@ class Torrent {
wire.onPiece = function(index, begin, data) {
var n = outstanding.get(key);
if (n != null && n > 0) outstanding.set(key, n - 1);
if (pieces != null && index >= 0
&& index < meta.numPieces
&& pieces.addBlock(index, begin, data)) {
if (pieces != null && index >= 0 && index < meta.numPieces && pieces.addBlock(index, begin, data)) {
broadcastHave(index);
}
requestBlocks(key);
};
wire.onRequest = function(index, begin, length) {
if (wire.amChoking || pieces == null || index < 0
|| index >= meta.numPieces || begin < 0 || length <= 0
|| begin + length > meta.pieceSize(index)) return;
if (wire.amChoking || pieces == null || index < 0 || index >= meta.numPieces || begin < 0 || length <= 0 || begin + length > meta.pieceSize(index)) return;
if (!pieces.have[index]) return;
var data = pieces.readBlock(index, begin, length);
uploadedBytes += data.length;
@ -384,8 +383,7 @@ class Torrent {
var out:Array<Bool> = [];
for (i in 0...n) {
var byte = i >> 3;
out.push(byte < bf.length
&& (bf.get(byte) & (0x80 >> (i & 7))) != 0);
out.push(byte < bf.length && (bf.get(byte) & (0x80 >> (i & 7))) != 0);
}
return out;
}
@ -479,8 +477,7 @@ class Torrent {
if (host == "" || port == null) return false;
var key = "tcp:" + host + ":" + port;
if (wires.exists(key)) return false;
addWire(key,
new leenkx.network.torrent.peer.TcpPeer(host, port));
addWire(key, new leenkx.network.torrent.peer.TcpPeer(host, port));
return true;
#else
return false;
@ -509,8 +506,7 @@ class Torrent {
var pendingLayers:Map<Int, {arr:Array<Bytes>, got:Int}> = [];
function pieceLayerBase():Int {
return Std.int(Math.log(meta.pieceLength / 16384)
/ Math.log(2));
return Std.int(Math.log(meta.pieceLength / 16384) / Math.log(2));
}
function requestPieceLayer(wire:WireProtocol, fi:Int):Void {
@ -528,8 +524,7 @@ class Torrent {
if (chunk > 512) chunk = 512;
var len = 2;
while (len < chunk) len <<= 1;
wire.sendHashRequest(root, pieceLayerBase(), sent,
len, 0);
wire.sendHashRequest(root, pieceLayerBase(), sent, len, 0);
hashReqNext.set(fi, sent + len);
hashReqTime.set(fi, haxe.Timer.stamp());
}
@ -547,11 +542,9 @@ class Torrent {
if (count <= 0) return;
for (fi in 0...meta.files.length) {
var f = meta.files[fi];
if (f.piecesRoot == null
|| !bytesEq(root, f.piecesRoot)) continue;
if (f.piecesRoot == null || !bytesEq(root, f.piecesRoot)) continue;
if (meta.fileHashes[fi] != null) return;
var np = Std.int(Math.ceil(f.length
/ meta.pieceLength));
var np = Std.int(Math.ceil(f.length / meta.pieceLength));
var pl = pendingLayers.get(fi);
if (pl == null) {
pl = {arr: [for (i in 0...np) null], got: 0};
@ -585,8 +578,7 @@ class Torrent {
var root = payload.sub(0, 32);
for (fi in 0...meta.files.length) {
var f = meta.files[fi];
if (f.piecesRoot != null
&& bytesEq(root, f.piecesRoot)) {
if (f.piecesRoot != null && bytesEq(root, f.piecesRoot)) {
hashReqNext.remove(fi);
pendingLayers.remove(fi);
return;
@ -604,10 +596,8 @@ class Torrent {
var np = Std.int(Math.ceil(f.length / meta.pieceLength));
l = [];
for (p in 0...np) {
var size = Std.int(Math.min(meta.pieceLength,
f.length - p * meta.pieceLength));
l.push(meta.pieceRootV2(storage.read(
f.offset + p * meta.pieceLength, size)));
var size = Std.int(Math.min(meta.pieceLength, f.length - p * meta.pieceLength));
l.push(meta.pieceRootV2(storage.read(f.offset + p * meta.pieceLength, size)));
}
layerCache.set(fi, l);
return l;
@ -620,13 +610,11 @@ class Torrent {
t = [level];
var depth = 0;
while (level.length > 1) {
var zero = MetaInfo.layerZero(pieceLayerBase()
+ depth + 1);
var zero = MetaInfo.layerZero(pieceLayerBase() + depth + 1);
var next:Array<Bytes> = [];
var i = 0;
while (i < level.length) {
var b = i + 1 < level.length
? level[i + 1] : zero;
var b = i + 1 < level.length ? level[i + 1] : zero;
next.push(MetaInfo.hashPair(level[i], b));
i += 2;
}

View File

@ -5,9 +5,7 @@ import leenkx.network.torrent.storage.Storage;
import leenkx.network.torrent.storage.Storage.IStorage;
class TorrentClient {
public static var defaultTrackers:Array<String> = [
"wss://ws1.leenkx.com"
];
public static var defaultTrackers:Array<String> = [];
public static var defaultDownloadDir:String = null;
public var downloadDir:String = null;
@ -247,8 +245,7 @@ class TorrentClient {
files.push({path: name, data: Bytes.ofString(Std.string(data))});
}
return {meta: MetaInfo.create(name, files, trackers, opts),
files: files};
return {meta: MetaInfo.create(name, files, trackers, opts), files: files};
}
public static function readFolder(path:String, ?base:String):Array<{path:String, data:Bytes}> {
@ -259,8 +256,7 @@ class TorrentClient {
root = root.substr(0, root.length - 1);
}
if (base == null) {
var i = Std.int(Math.max(root.lastIndexOf("/"),
root.lastIndexOf("\\")));
var i = Std.int(Math.max(root.lastIndexOf("/"), root.lastIndexOf("\\")));
base = i >= 0 ? root.substr(i + 1) : root;
}
var files:Array<{path:String, data:Bytes}> = [];
@ -317,20 +313,79 @@ class TorrentClient {
return t;
}
public function remove(infoHashHex:String, ?deleteData:Bool):Void {
var t = torrents.get(infoHashHex);
if (t == null) {
t = torrents.get("v2:" + infoHashHex);
if (t != null) infoHashHex = "v2:" + infoHashHex;
public function get(id:Dynamic):Torrent {
if (Std.isOfType(id, Torrent)) return id;
if (id == null) return null;
var hex = Std.string(id);
if (StringTools.startsWith(hex, "v2:")) {
hex = hex.substr(3);
}
if (t != null) {
t.destroy();
if (deleteData) t.deleteData();
torrents.remove(infoHashHex);
var t = torrents.get(hex);
if (t == null) t = torrents.get("v2:" + hex);
if (t == null) {
for (f in torrents) {
if (f.infoHashHex == hex || f.infoHashV2Hex == hex || (f.infoHash != null && Crypto.toHex(f.infoHash) == hex)) {
t = f;
break;
}
}
}
return t;
}
public static function torrentIdFor(v:Dynamic):Dynamic {
if (v == null) return null;
if (Std.isOfType(v, haxe.io.Bytes)) return v;
var s = Std.string(v);
if (s.indexOf("magnet:") == 0) return s;
if (s.indexOf("v2:") == 0) {
var h = s.substr(3);
return h.length == 64 ? "magnet:?xt=urn:btmh:1220" + h : s;
}
switch (s.length) {
case 40 | 32:
return "magnet:?xt=urn:btih:" + s;
case 64:
return "magnet:?xt=urn:btmh:1220" + s;
case 68:
return "magnet:?xt=urn:btmh:" + s;
default:
return s;
}
}
static function mergeTrackers(trackers:Array<String>, urls:Array<String>):Void {
public static function trackerUrls(v:Dynamic):Array<String> {
if (v == null) return null;
var items:Array<Dynamic> = Std.isOfType(v, Array)
? (v:Array<Dynamic>) : [v];
var out:Array<String> = [];
for (item in items) {
var u:Dynamic = null;
if (!Std.isOfType(item, String)) {
try {
u = Reflect.field(item, "url");
} catch(e:Dynamic) {}
}
var s = u != null ? Std.string(u) : Std.string(item);
if (s != "" && s != "null") out.push(s);
}
return out.length > 0 ? out : null;
}
public function remove(infoHashHex:String, ?deleteData:Bool):Void {
var t = get(infoHashHex);
if (t != null) {
t.destroy();
if (deleteData) t.deleteData();
var key:String = null;
for (k in torrents.keys()) {
if (torrents.get(k) == t) { key = k; break; }
}
if (key != null) torrents.remove(key);
}
}
public static function mergeTrackers(trackers:Array<String>, urls:Array<String>):Void {
if (urls == null) return;
for (u in urls) {
if (u != null && Lambda.indexOf(trackers, u) < 0) {

View File

@ -51,10 +51,12 @@ class RtcPeerPool {
var rtcInitialized = false;
#end
public static var defaultIceServers:Array<String> =
["stun:stun.l.google.com:19302", "stun:global.stun.twilio.com:3478"];
public function new(?iceServers:Array<String>, ?channelLabel:String, unordered:Bool = false, maxRetransmits:Int = -1) {
this.iceServers = iceServers != null ? iceServers
: ["stun:stun.l.google.com:19302",
"stun:global.stun.twilio.com:3478"];
: defaultIceServers.copy();
if (channelLabel != null) this.channelLabel = channelLabel;
this.unordered = unordered;
this.maxRetransmits = maxRetransmits;
@ -72,8 +74,7 @@ class RtcPeerPool {
}
#end
var signaling = new RtcSignaling(trackerUrl, infoHashBinary,
peerIdBinary);
var signaling = new RtcSignaling(trackerUrl, infoHashBinary, peerIdBinary);
signalings.push(signaling);
signaling.statsProvider = statsProvider;
signaling.onConnected = function() {
@ -223,8 +224,7 @@ class RtcPeerPool {
}
};
var dc = pc.createDataChannel(channelLabel,
{ordered: !unordered,
maxRetransmits: unordered ? maxRetransmits : null});
{ordered: !unordered, maxRetransmits: unordered ? maxRetransmits : null});
attachOfferChannel(offerIdHex, new DataChannelPeer(dc));
var iceComplete = false;
var iceTimer:haxe.Timer = null;
@ -271,8 +271,7 @@ class RtcPeerPool {
iceComplete = true;
if (iceTimer != null) iceTimer.stop();
if (!offerPeers.exists(offerIdHex)) return;
sig.sendAnswer(peerIdHex, offerIdHex,
pc.getLocalDescription());
sig.sendAnswer(peerIdHex, offerIdHex, pc.getLocalDescription());
};
pc.onGatheringStateChange = function(state) {
if (state == RTC_GATHERING_COMPLETE) sendAnswerSdp();
@ -312,8 +311,7 @@ class RtcPeerPool {
iceComplete = true;
if (iceTimer != null) iceTimer.stop();
if (!offerPeers.exists(offerIdHex)) return;
sig.sendAnswer(peerIdHex, offerIdHex,
RtcNative.getLocalDescription(pcId));
sig.sendAnswer(peerIdHex, offerIdHex, RtcNative.getLocalDescription(pcId));
};
RtcNative.setGatheringStateCallback(pcId, function(state:Int) {
if (state == RtcNative.RTC_GATHERING_COMPLETE) sendAnswerSdp();

View File

@ -36,6 +36,10 @@ class DatachannelTransport implements ITransport {
if (opts.announce != null && opts.announce.length > 0) {
url = opts.announce[0];
}
if (url == null || url == "") {
if (onError != null) onError("no announce tracker configured");
return;
}
if (opts.iceServers != null) iceServers = opts.iceServers;
pk = opts.pk != null ? opts.pk : "";
ek = opts.ek != null ? opts.ek : "";

View File

@ -48,7 +48,7 @@ class WebTorrentTransport implements ITransport {
var blob:Dynamic = createBlob(identifier);
var torrentOpts:Dynamic = {
name: identifier,
announce: opts.announce != null ? opts.announce : ["wss://ws1.leenkx.com"]
announce: opts.announce != null ? opts.announce : leenkx.network.torrent.TorrentClient.defaultTrackers
};
if (opts.torrentOpts != null) {
torrentOpts = js.lib.Object.assign(

View File

@ -43,6 +43,10 @@ class WireTransport implements ITransport {
if (opts.announce != null && opts.announce.length > 0) {
url = opts.announce[0];
}
if (url == null || url == "") {
if (onError != null) onError("no announce tracker configured");
return;
}
if (opts.iceServers != null) iceServers = opts.iceServers;
pk = opts.pk != null ? opts.pk : "";
ek = opts.ek != null ? opts.ek : "";

View File

@ -223,6 +223,9 @@ class LeenkxExporter:
cls.export_network = False
if wrd.lnx_network == 'Enabled':
cls.export_network = True
cls.export_torrent = False
if wrd.lnx_torrent == 'Enabled':
cls.export_torrent = True
@staticmethod
def write_matrix(matrix):

View File

@ -1,30 +0,0 @@
from lnx.logicnode.lnx_nodes import *
class AddTorrentNode(LnxLogicTreeNode):
"""Add Torrent"""
bl_idname = 'LNAddTorrentNode'
bl_label = 'Add Torrent'
bl_description = 'Add a torrent file from the network'
lnx_category = 'Leenkx'
lnx_version = 1
def __init__(self, *args, **kwargs):
super(AddTorrentNode, self).__init__(*args, **kwargs)
array_nodes[str(id(self))] = self
def lnx_init(self, context):
self.add_input('LnxNodeSocketAction', 'In')
self.add_input('LnxDynamicSocket', 'Connection')
self.add_input('LnxStringSocket', 'Torrent')
self.add_output('LnxNodeSocketAction', 'Out')
self.add_output('LnxDynamicSocket', 'Title')
self.add_output('LnxDynamicSocket', 'Files')
self.add_output('LnxNodeSocketAction', 'Done')
def register():
add_category('Leenkx', icon='ORIENTATION_CURSOR')
AddTorrentNode.on_register()

View File

@ -4,19 +4,71 @@ class CreateLeenkxNode(LnxLogicTreeNode):
"""Create Leenkx"""
bl_idname = 'LNCreateLeenkxNode'
bl_label = 'Create Leenkx'
bl_description = 'Create Leenkx'
bl_description = ('Open a Leenkx peer connection on the given swarm identifier')
lnx_category = 'Leenkx'
lnx_version = 1
def update_sockets(self, context):
if not self.inputs:
return
links = {}
for socket in self.inputs:
for link in socket.links:
links.setdefault(socket.name, []).append(
(link.from_node, link.from_socket))
for socket in self.inputs[2:]:
self.inputs.remove(socket)
if (self.property0 == 'datachannel'
and self.property2 != 'inherit'):
self.add_input('LnxNodeSocketArray', 'ICE Servers')
if self.property3 != 'inherit':
self.add_input('LnxNodeSocketArray', 'Trackers')
tree = self.id_data
for socket in self.inputs:
if socket.name in links:
for from_node, from_socket in links[socket.name]:
tree.links.new(from_socket, socket)
property0: HaxeEnumProperty('property0',
items=[('wire', 'Wire', 'BitTorrent wire protocol transport (default)'),
('datachannel', 'DataChannel', 'WebRTC data channels for browser/krom peers - shows the ICE Servers input')],
name='Transport', default='wire', update=update_sockets)
property1: HaxeBoolProperty('property1',
name='Gossip', description='Relay mode - forward messages to all connected peers', default=False)
property2: HaxeEnumProperty('property2',
items=[('inherit', 'Inherit', 'Use the built-in STUN server list; no ICE Servers input'),
('add', 'Add', 'Merge the ICE Servers input on top of the built-in STUN servers'),
('override', 'Override', 'Replace the ICE server list with the ICE Servers input')],
name='ICE Servers', default='inherit', update=update_sockets)
property3: HaxeEnumProperty('property3',
items=[('inherit', 'Inherit', 'Use the project torrent tracker list; no Trackers input'),
('add', 'Add', 'Merge the Trackers input on top of the project tracker list'),
('override', 'Override', 'Replace the announce tracker list with the Trackers input')],
name='Trackers', default='inherit', update=update_sockets)
def lnx_init(self, context):
self.add_input('LnxNodeSocketAction', 'In')
self.add_input('LnxStringSocket', 'Url', default_value="8001")
self.add_input('LnxStringSocket', 'Channel', default_value='8001')
if (self.property0 == 'datachannel'
and self.property2 != 'inherit'):
self.add_input('LnxNodeSocketArray', 'ICE Servers')
if self.property3 != 'inherit':
self.add_input('LnxNodeSocketArray', 'Trackers')
self.add_output('LnxNodeSocketAction', 'Out')
self.add_output('LnxDynamicSocket', 'Connection')
self.add_output('LnxDynamicSocket', 'ID')
def draw_buttons(self, context, layout):
layout.prop(self, 'property0')
layout.prop(self, 'property1')
if self.property0 == 'datachannel':
layout.prop(self, 'property2')
layout.prop(self, 'property3')
def register():
add_category('Leenkx', icon='ORIENTATION_CURSOR')

View File

@ -5,7 +5,7 @@ class CreateTorrentNode(LnxLogicTreeNode):
"""Create Torrent"""
bl_idname = 'LNCreateTorrentNode'
bl_label = 'Create Torrent'
bl_description = 'Create a torrent file from the network'
bl_description = ('Build a .torrent from content (file, files or folder)')
lnx_category = 'Leenkx'
lnx_version = 1
@ -13,15 +13,65 @@ class CreateTorrentNode(LnxLogicTreeNode):
super(CreateTorrentNode, self).__init__(*args, **kwargs)
array_nodes[str(id(self))] = self
def update_sockets(self, context):
if not self.inputs:
return
links = {}
for socket in self.inputs:
for link in socket.links:
links.setdefault(socket.name, []).append(
(link.from_node, link.from_socket))
for socket in self.inputs[1:]:
self.inputs.remove(socket)
self._add_data_inputs()
tree = self.id_data
for socket in self.inputs:
if socket.name in links:
for from_node, from_socket in links[socket.name]:
tree.links.new(from_socket, socket)
def _add_data_inputs(self):
mode_sockets = {
'file': ('LnxDynamicSocket', 'File (Bytes)', 'Content of the single file as Bytes'),
'files': ('LnxNodeSocketArray', 'Files ({path, data})', 'Array of {path, data} objects'),
'folder': ('LnxStringSocket', 'Folder Path', 'Folder read recursively for content (native targets only)'),
}
socket_type, label, desc = mode_sockets.get(self.property1, mode_sockets['file'])
self.add_input(socket_type, label)
self.add_input('LnxStringSocket', 'Name')
self.add_input('LnxStringSocket', 'Comment')
self.add_input('LnxStringSocket', 'Created By')
self.add_input('LnxStringSocket', 'Source')
self.add_input('LnxNodeSocketArray', 'Trackers')
self.add_input('LnxBoolSocket', 'Private')
self.add_input('LnxIntSocket', 'Piece Length')
self.add_input('LnxNodeSocketArray', 'Web Seeds')
property0: HaxeEnumProperty('property0',
items=[('1', 'V1', 'BitTorrent v1 - SHA-1 piece list over one packed stream; works with every client'),
('2', 'V2', 'BitTorrent v2 (BEP 52) - SHA-256 merkle tree per file, pieces aligned to file boundaries; needs modern peers'),
('3', 'Hybrid', 'One .torrent with both v1 and v2 metadata - two infohashes (btih + btmh) over the same info dict')],
name='Version', default='1')
property1: HaxeEnumProperty('property1',
items=[('file', 'File', 'Single file: Data is a Bytes payload'),
('files', 'Files', 'Multiple files: Data is an array of {path, data}'),
('folder', 'Folder', 'Read a folder recursively (native targets only): Data is a folder path string')],
name='Data', default='file', update=update_sockets)
def lnx_init(self, context):
self.add_input('LnxNodeSocketAction', 'In')
self.add_input('LnxDynamicSocket', 'Connection')
self.add_input('LnxDynamicSocket', 'Data')
self._add_data_inputs()
self.add_output('LnxNodeSocketAction', 'Out')
self.add_output('LnxDynamicSocket', 'Torrent')
self.add_output('LnxNodeSocketAction', 'Done')
self.add_output('LnxDynamicSocket', 'Meta')
self.add_output('LnxDynamicSocket', 'Torrent Bytes')
self.add_output('LnxStringSocket', 'InfoHash')
self.add_output('LnxStringSocket', 'Magnet')
def draw_buttons(self, context, layout):
layout.prop(self, 'property0')
layout.prop(self, 'property1')
def register():

View File

@ -0,0 +1,61 @@
from lnx.logicnode.lnx_nodes import *
class CreateTrackerNode(LnxLogicTreeNode):
"""Create Tracker"""
bl_idname = 'LNCreateTrackerNode'
bl_label = 'Create Tracker Server'
bl_description = ('Hosts a local tracker')
lnx_category = 'Leenkx'
lnx_version = 1
def __init__(self, *args, **kwargs):
super(CreateTrackerNode, self).__init__(*args, **kwargs)
array_nodes[str(id(self))] = self
def update_sockets(self, context):
if not self.inputs:
return
links = {}
for socket in self.inputs:
for link in socket.links:
links.setdefault(socket.name, []).append(
(link.from_node, link.from_socket))
for socket in self.inputs[1:]:
self.inputs.remove(socket)
self._add_mode_inputs()
tree = self.id_data
for socket in self.inputs:
if socket.name in links:
for from_node, from_socket in links[socket.name]:
tree.links.new(from_socket, socket)
def _add_mode_inputs(self):
self.add_input('LnxStringSocket', 'Host', default_value='127.0.0.1')
self.add_input('LnxIntSocket', 'Port', default_value=6969)
if self.property0 == 'wss':
self.add_input('LnxStringSocket', 'Cert File')
self.add_input('LnxStringSocket', 'Key File')
property0: HaxeEnumProperty('property0',
items=[('udp', 'UDP', 'UDP tracker (BEP 15, sys targets only)'),
('http', 'HTTP', 'HTTP tracker (BEP 3)'),
('ws', 'WS', 'Plain WebSocket tracker (local relay, WebTorrent/web peers)'),
('wss', 'WSS', 'Secure WebSocket tracker (WebTorrent/web peers)')],
name='Type', default='udp', update=update_sockets)
def lnx_init(self, context):
self.add_input('LnxNodeSocketAction', 'In')
self._add_mode_inputs()
self.add_output('LnxNodeSocketAction', 'Out')
self.add_output('LnxDynamicSocket', 'Tracker')
self.add_output('LnxStringSocket', 'URL')
def draw_buttons(self, context, layout):
layout.prop(self, 'property0')
def register():
add_category('Leenkx', icon='ORIENTATION_CURSOR')
CreateTrackerNode.on_register()

View File

@ -0,0 +1,76 @@
from lnx.logicnode.lnx_nodes import *
class DownloadTorrentNode(LnxLogicTreeNode):
"""Download Torrent"""
bl_idname = 'LNDownloadTorrentNode'
bl_label = 'Download Torrent'
bl_description = ('Add a torrent to a Leenkx connection and start downloading')
lnx_category = 'Leenkx'
lnx_version = 1
def __init__(self, *args, **kwargs):
super(DownloadTorrentNode, self).__init__(*args, **kwargs)
array_nodes[str(id(self))] = self
def update_sockets(self, context):
if not self.inputs:
return
links = {}
for socket in self.inputs:
for link in socket.links:
links.setdefault(socket.name, []).append(
(link.from_node, link.from_socket))
for socket in self.inputs[2:]:
self.inputs.remove(socket)
self._add_id_inputs()
tree = self.id_data
for socket in self.inputs:
if socket.name in links:
for from_node, from_socket in links[socket.name]:
tree.links.new(from_socket, socket)
def _add_id_inputs(self):
mode_sockets = {
'magnet': ('LnxStringSocket', 'Magnet URI', 'Magnet URI (magnet:?xt=...)'),
'id': ('LnxStringSocket', 'Infohash', '40-char v1 or 64-char v2 infohash hex'),
'file': ('LnxDynamicSocket', 'Torrent File (Bytes)', '.torrent file bytes'),
}
socket_type, label, desc = mode_sockets.get(self.property0, mode_sockets['magnet'])
self.add_input(socket_type, label)
if self.property1 != 'inherit':
self.add_input('LnxNodeSocketArray', 'Trackers')
property0: HaxeEnumProperty('property0',
items=[('magnet', 'Magnet', 'Magnet URI (magnet:?xt=...)'),
('id', 'Infohash', '40-char v1 or 64-char v2 infohash hex'),
('file', 'Torrent File', '.torrent file bytes')],
name='Input', default='magnet', update=update_sockets)
property1: HaxeEnumProperty('property1',
items=[('inherit', 'Inherit', 'Use the torrent\'s own trackers plus the connection\'s; no Trackers input'),
('add', 'Add', 'Merge the Trackers input on top of the torrent\'s own trackers'),
('override', 'Override', 'Replace all trackers with the Trackers input (torrent\'s own and connection defaults are dropped)')],
name='Trackers', default='inherit', update=update_sockets)
def lnx_init(self, context):
self.add_input('LnxNodeSocketAction', 'In')
self.add_input('LnxDynamicSocket', 'Connection')
self._add_id_inputs()
self.add_output('LnxNodeSocketAction', 'Out')
self.add_output('LnxDynamicSocket', 'Torrent')
self.add_output('LnxStringSocket', 'Name')
self.add_output('LnxNodeSocketArray', 'Files')
self.add_output('LnxNodeSocketAction', 'Done')
self.add_output('LnxStringSocket', 'InfoHash')
self.add_output('LnxStringSocket', 'Magnet')
def draw_buttons(self, context, layout):
layout.prop(self, 'property0')
layout.prop(self, 'property1')
def register():
add_category('Leenkx', icon='ORIENTATION_CURSOR')
DownloadTorrentNode.on_register()

View File

@ -0,0 +1,77 @@
from lnx.logicnode.lnx_nodes import *
class GetTorrentFileNode(LnxLogicTreeNode):
"""Get Torrent File"""
bl_idname = 'LNGetTorrentFileNode'
bl_label = 'Get Torrent File'
bl_description = ('Find one file inside a torrent and read its '
'stored bytes (webtorrent torrent.files.find + '
'file.arrayBuffer). Match By selects how the '
'Match input is interpreted: index, exact file '
'name, exact path, or extension')
lnx_category = 'Leenkx'
lnx_version = 1
def __init__(self, *args, **kwargs):
super(GetTorrentFileNode, self).__init__(*args, **kwargs)
array_nodes[str(id(self))] = self
def update_sockets(self, context):
if not self.inputs:
return
links = {}
for socket in self.inputs:
for link in socket.links:
links.setdefault(socket.name, []).append(
(link.from_node, link.from_socket))
for socket in self.inputs[3:]:
self.inputs.remove(socket)
self._add_match_input()
tree = self.id_data
for socket in self.inputs:
if socket.name in links:
for from_node, from_socket in links[socket.name]:
tree.links.new(from_socket, socket)
def _add_match_input(self):
match_sockets = {
'index': ('LnxIntSocket', 'Index', 'Position in the torrent file list'),
'name': ('LnxStringSocket', 'Name', 'Exact file name, e.g. movie.mp4'),
'path': ('LnxStringSocket', 'Path', 'Exact path inside the torrent, e.g. docs/readme.txt'),
'ext': ('LnxStringSocket', 'Extension', 'File extension like mp4 or .mp4; first matching file wins'),
}
socket_type, label, desc = match_sockets.get(self.property0, match_sockets['name'])
self.add_input(socket_type, label)
property0: HaxeEnumProperty('property0',
items=[('index', 'Index', 'Position in the torrent file list; Match is an int'),
('name', 'Name', 'Exact file name; Match is a string like movie.mp4'),
('path', 'Path', 'Exact path inside the torrent; Match is a string like docs/readme.txt'),
('ext', 'Extension', 'First file whose name ends with the extension; Match is mp4 or .mp4')],
name='Match By', default='name', update=update_sockets)
def lnx_init(self, context):
self.add_input('LnxNodeSocketAction', 'In')
self.add_input('LnxDynamicSocket', 'Connection')
self.add_input('LnxDynamicSocket', 'Torrent')
self._add_match_input()
self.add_output('LnxNodeSocketAction', 'Out')
self.add_output('LnxDynamicSocket', 'File')
self.add_output('LnxStringSocket', 'Name')
self.add_output('LnxStringSocket', 'Path')
self.add_output('LnxFloatSocket', 'Size')
self.add_output('LnxStringSocket', 'Type')
self.add_output('LnxDynamicSocket', 'Data')
self.add_output('LnxBoolSocket', 'Found')
self.add_output('LnxFloatSocket', 'Downloaded')
self.add_output('LnxFloatSocket', 'Progress')
def draw_buttons(self, context, layout):
layout.prop(self, 'property0')
def register():
add_category('Leenkx', icon='ORIENTATION_CURSOR')
GetTorrentFileNode.on_register()

View File

@ -5,63 +5,40 @@ class LeenkxCloseConnectionNode(LnxLogicTreeNode):
"""Close connection on the network"""
bl_idname = 'LNLeenkxCloseConnectionNode'
bl_label = 'Close Leenkx Connection'
bl_description = ('Tear down a Leenkx connection, or drop a single peer from it')
lnx_version = 1
lnx_category = 'Leenkx'
ind = 2
@staticmethod
def get_enum_id_value(obj, prop_name, value):
return obj.bl_rna.properties[prop_name].enum_items[value].identifier
def update_sockets(self, context):
if not self.inputs:
return
links = {}
for socket in self.inputs:
for link in socket.links:
links.setdefault(socket.name, []).append(
(link.from_node, link.from_socket))
for socket in self.inputs[2:]:
self.inputs.remove(socket)
if self.property0 == 'peer':
self.add_input('LnxStringSocket', 'ID')
tree = self.id_data
for socket in self.inputs:
if socket.name in links:
for from_node, from_socket in links[socket.name]:
tree.links.new(from_socket, socket)
@staticmethod
def get_count_in(type_name):
return {
'client': 0,
'peer': 1
}.get(type_name, 0)
def get_enum(self):
return self.get('property0', 0)
def set_enum(self, value):
# Checking the selection of each type
select_current = self.get_enum_id_value(self, 'property0', value)
select_prev = self.property0
#Check if type changed
if select_prev != select_current:
for i in self.inputs:
self.inputs.remove(i)
# Arguements for type Host
if (self.get_count_in(select_current) == 0):
self.add_input('LnxNodeSocketAction', 'In')
self.add_input('LnxDynamicSocket', 'Connection')
self.add_input('LnxStringSocket', 'Url')
# Arguements for type Client
if (self.get_count_in(select_current) == 1):
self.add_input('LnxNodeSocketAction', 'In')
self.add_input('LnxDynamicSocket', 'Connection')
self.add_input('LnxStringSocket', 'ID')
self['property0'] = value
property0: HaxeEnumProperty(
'property1',
property0: HaxeEnumProperty('property0',
items = [('client', 'Leenkx Client', 'Close the Leenkx Client connection on the network'),
('peer', 'Single Peer', 'Close a Peer connection on network')],
('peer', 'Single Peer', 'Drop a peer from the connection (forgets its address; it can rejoin on a new handshake)')],
name='', default='client',
set=set_enum,
get=get_enum)
update=update_sockets)
def lnx_init(self, context):
self.add_input('LnxNodeSocketAction', 'In')
self.add_input('LnxDynamicSocket', 'Connection')
self.add_input('LnxStringSocket', 'Url')
if self.property0 == 'peer':
self.add_input('LnxStringSocket', 'ID')
self.add_output('LnxNodeSocketAction', 'Out')

View File

@ -9,84 +9,26 @@ class LeenkxEventNode(LnxLogicTreeNode):
lnx_category = 'Leenkx'
lnx_version = 1
@staticmethod
def get_enum_id_value(obj, prop_name, value):
return obj.bl_rna.properties[prop_name].enum_items[value].identifier
@staticmethod
def get_count_in(type_name):
return {
'client': 0,
'host': 1,
'securehost': 2
}.get(type_name, 0)
def get_enum(self):
return self.get('property0', 0)
def set_enum(self, value):
# Checking the selection of each type
select_current = self.get_enum_id_value(self, 'property0', value)
select_prev = self.property0
#Check if type changed
if select_prev != select_current:
for i in self.inputs:
self.inputs.remove(i)
# Arguements for type Client
if (self.get_count_in(select_current) == 0):
self.add_input('LnxStringSocket', 'Url', default_value="8001")
# Arguements for type Host
if (self.get_count_in(select_current) == 1):
self.add_input('LnxStringSocket', 'Domain', default_value="127.0.0.1")
self.add_input('LnxIntSocket', 'Port', default_value=8001)
# Arguements for type Secure Host
if (self.get_count_in(select_current) == 2):
self.add_input('LnxStringSocket', 'Domain', default_value="127.0.0.1")
self.add_input('LnxIntSocket', 'Port', default_value=8001)
self['property0'] = value
property0: HaxeEnumProperty(
'property0',
items = [('client', 'Client', 'Network client Event listener'),
('host', 'Host', 'Network host Event listener'),
('securehost', 'Secure Host', 'Network secure host Event listener')],
name='',
default='client',
set=set_enum,
get=get_enum)
property1: HaxeEnumProperty(
'property1',
items = [('onopen', 'OnOpen', 'Listens to onOpen event'),
('onmessage', 'OnMessage', 'Listens to onMessage event'),
('onerror', 'OnError', 'Listens to onError event'),
('onclose', 'OnClose', 'Listens to onClose event'),
('onseen', 'OnSeen', 'Listens to onClose event'),
('onserver', 'OnServer', 'Listens to onClose event'),
('onconnections', 'OnConnections', 'Listens to onClose event'),
('onping', 'OnPing', 'Listens to onClose event'),
('onleft', 'OnLeft', 'Listens to onClose event'),
('ontimeout', 'OnTimeout', 'Listens to onClose event'),
('onrpc', 'OnRpc', 'Listens to onClose event'),
('onrpcresponse', 'OnRpcResponse', 'Listens to onClose event'),
('onwireleft', 'OnWireLeft', 'Listens to onClose event'),
('onwireseen', 'OnWireSeen', 'Listens to onClose event'),
('ontorrent', 'OnTorrent', 'Listens to onClose event'),
('ontracker', 'OnTracker', 'Listens to onClose event'),
('onannounce', 'OnAnnounce', 'Listens to onClose event')],
name='',
default='onopen')
property1: HaxeEnumProperty('property1',
items = [('onopen', 'OnOpen', 'The connection to the server is open'),
('onmessage', 'OnMessage', 'A message arrived; ID is the sender peer id, Data the payload'),
('onerror', 'OnError', 'A connection error occurred'),
('onclose', 'OnClose', 'The connection was closed'),
('onseen', 'OnSeen', 'A peer was discovered; ID is the peer id'),
('onserver', 'OnServer', 'The signaling server responded'),
('onconnections', 'OnConnections', 'The server sent the peer list; Data is the connection list'),
('onping', 'OnPing', 'Heartbeat ping roundtrip completed'),
('onleft', 'OnLeft', 'A peer left; ID is the peer id'),
('ontimeout', 'OnTimeout', 'A handshake timed out'),
('onwireleft', 'OnWireLeft', 'A BitTorrent wire peer disconnected; ID is the peer id'),
('onwireseen', 'OnWireSeen', 'A BitTorrent wire peer connected; ID is the peer id'),
('ontorrent', 'OnTorrent', 'Torrent activity on the connection; Data is the torrent'),
('ontorrentadded', 'OnTorrentAdded', 'A torrent registered on the connection; Data is the Torrent handle'),
('ontracker', 'OnTracker', 'A tracker event on the connection'),
('onannounce', 'OnAnnounce', 'A tracker announce completed'),
('ontorrentdone', 'OnTorrentDone', 'A torrent finished downloading; Data is the torrent'),
('onload', 'OnLoad', 'The connection was registered; fires once at startup')],
name='', default='onopen')
def __init__(self, *args, **kwargs):
super(LeenkxEventNode, self).__init__(*args, **kwargs)
@ -95,7 +37,7 @@ class LeenkxEventNode(LnxLogicTreeNode):
def lnx_init(self, context):
#self.add_input('LnxNodeSocketAction', 'In')
self.add_input('LnxStringSocket', 'Url', default_value="ws://127.0.0.1:8001")
self.add_input('LnxStringSocket', 'Channel', default_value='8001')
self.add_output('LnxNodeSocketAction', 'Out')
self.add_output('LnxDynamicSocket', 'ID')
@ -103,7 +45,6 @@ class LeenkxEventNode(LnxLogicTreeNode):
def draw_buttons(self, context, layout):
layout.prop(self, 'property0')
layout.prop(self, 'property1')
def register():

View File

@ -9,16 +9,15 @@ class LeenkxMessageParserNode(LnxLogicTreeNode):
lnx_category = 'Leenkx'
lnx_version = 1
property0: HaxeEnumProperty(
'property0',
items = [('string', 'String', 'Event for a string over the network'),
('vector', 'Vector', 'Event for a vector over the network'),
('float', 'Float', 'Event for a float over the network'),
('integer', 'Integer', 'Event for an integer over the network'),
('boolean', 'Boolean', 'Event for a boolean over the network'),
('transform', 'Transform', 'Event for a transform over the network'),
('rotation', 'Rotation', 'Event for a rotation over the network')],
name='', default='string')
property0: HaxeEnumProperty('property0', items=[
('string', 'String', 'Parse the message as a string'),
('vector', 'Vector', 'Parse the message as a vector (Vec4)'),
('float', 'Float', 'Parse the message as a float'),
('integer', 'Integer', 'Parse the message as an integer'),
('boolean', 'Boolean', 'Parse the message as a boolean'),
('transform', 'Transform', 'Parse the message as a transform (Mat4)'),
('rotation', 'Rotation', 'Parse the message as a rotation (Quat)')
], name='', default='string')
def lnx_init(self, context):

View File

@ -9,77 +9,55 @@ class LeenkxSendMessageNode(LnxLogicTreeNode):
lnx_category = 'Leenkx'
lnx_version = 1
@staticmethod
def get_enum_id_value(obj, prop_name, value):
return obj.bl_rna.properties[prop_name].enum_items[value].identifier
def update_sockets(self, context):
if not self.inputs:
return
links = {}
send_all = True
for socket in self.inputs:
for link in socket.links:
links.setdefault(socket.name, []).append(
(link.from_node, link.from_socket))
if socket.name == 'Send All':
send_all = socket.default_value_raw
for socket in self.inputs[3:]:
self.inputs.remove(socket)
self._add_data_inputs(send_all)
tree = self.id_data
for socket in self.inputs:
if socket.name in links:
for from_node, from_socket in links[socket.name]:
tree.links.new(from_socket, socket)
@staticmethod
def get_count_in(type_name):
return {
'client': 0,
'host': 1,
'securehost': 2
}.get(type_name, 0)
def _add_data_inputs(self, send_all=True):
data_sockets = {
'string': ('LnxStringSocket', 'String payload to send'),
'vector': ('LnxVectorSocket', 'Vector payload to send'),
'float': ('LnxFloatSocket', 'Float payload to send'),
'integer': ('LnxIntSocket', 'Integer payload to send'),
'boolean': ('LnxBoolSocket', 'Boolean payload to send'),
'transform': ('LnxDynamicSocket', 'Transform (Mat4) payload to send'),
'rotation': ('LnxRotationSocket', 'Rotation (Quat) payload to send'),
}
socket_type, desc = data_sockets.get(self.property1, data_sockets['string'])
self.add_input(socket_type, 'Data')
self.add_input('LnxBoolSocket', 'Send All', default_value=send_all)
if not send_all:
self._add_id_input()
def get_enum(self):
return self.get('property0', 0)
def _add_id_input(self):
self.add_input('LnxStringSocket', 'ID')
def set_enum(self, value):
# Checking the selection of each type
select_current = self.get_enum_id_value(self, 'property0', value)
select_prev = self.property0
def on_socket_val_update(self, context, socket):
if socket.name == 'Send All':
if (len(self.inputs) > 5
and self.inputs[5].name == 'ID'):
self.inputs.remove(self.inputs[5])
if not socket.default_value_raw:
self._add_id_input()
super().on_socket_val_update(context, socket)
#Check if type changed
if select_prev != select_current:
for i in self.inputs:
self.inputs.remove(i)
# Arguements for type Client
if (self.get_count_in(select_current) == 0):
self.add_input('LnxNodeSocketAction', 'In')
self.add_input('LnxDynamicSocket', 'Connection')
self.add_input('LnxStringSocket', 'API')
self.add_input('LnxDynamicSocket', 'Data')
self.add_input('LnxStringSocket', 'ID')
self.add_input('LnxBoolSocket', 'Send All')
# Arguements for type Host
if (self.get_count_in(select_current) == 1):
self.add_input('LnxNodeSocketAction', 'In')
self.add_input('LnxDynamicSocket', 'Connection')
self.add_input('LnxStringSocket', 'API')
self.add_input('LnxDynamicSocket', 'Data')
self.add_input('LnxStringSocket', 'ID')
self.add_input('LnxBoolSocket', 'Send All')
# Arguements for type Secure Host
if (self.get_count_in(select_current) == 2):
self.add_input('LnxNodeSocketAction', 'In')
self.add_input('LnxDynamicSocket', 'Connection')
self.add_input('LnxStringSocket', 'API')
self.add_input('LnxDynamicSocket', 'Data')
self.add_input('LnxStringSocket', 'ID')
self.add_input('LnxBoolSocket', 'Send All')
self['property0'] = value
property0: HaxeEnumProperty(
'property0',
items = [('client', 'Client', 'Network client Event listener'),
('host', 'Host', 'Network host Event listener'),
('securehost', 'Secure Host', 'Network secure host Event listener')],
name='',
default='client',
set=set_enum,
get=get_enum)
property1: HaxeEnumProperty(
'property1',
property1: HaxeEnumProperty('property1',
items = [('string', 'String', 'Send a string over the network to one or all of the clients'),
('vector', 'Vector', 'Send a vector over the network to one or all of the clients'),
('float', 'Float', 'Send a float over the network to one or all of the clients'),
@ -87,8 +65,12 @@ class LeenkxSendMessageNode(LnxLogicTreeNode):
('boolean', 'Boolean', 'Send a boolean over the network to one or all of the clients'),
('transform', 'Transform', 'Send a transform over the network to one or all of the clients'),
('rotation', 'Rotation', 'Send a rotation over the network to one or all of the clients')],
name='',
default='string')
name='', default='string', update=update_sockets)
property2: HaxeBoolProperty('property2',
name='Encrypted',
description='Use full encryption protocol (signing, bencode, box encryption). Disable for fast raw message mode',
default=False)
def __init__(self, *args, **kwargs):
@ -100,17 +82,15 @@ class LeenkxSendMessageNode(LnxLogicTreeNode):
self.add_input('LnxNodeSocketAction', 'In')
self.add_input('LnxDynamicSocket', 'Connection')
self.add_input('LnxStringSocket', 'API')
self.add_input('LnxDynamicSocket', 'Data')
self.add_input('LnxStringSocket', 'ID')
self.add_input('LnxBoolSocket', 'Send All')
self._add_data_inputs()
self.add_output('LnxNodeSocketAction', 'Out')
def draw_buttons(self, context, layout):
layout.prop(self, 'property0')
layout.prop(self, 'property1')
layout.prop(self, 'property2')
def register():
add_category('Leenkx', icon='ORIENTATION_CURSOR')

View File

@ -0,0 +1,68 @@
from lnx.logicnode.lnx_nodes import *
class ParseTorrentNode(LnxLogicTreeNode):
"""Parse Torrent"""
bl_idname = 'LNParseTorrentNode'
bl_label = 'Parse Torrent'
bl_description = ('Parse a torrent id without joining a swarm')
lnx_category = 'Leenkx'
lnx_version = 1
def __init__(self, *args, **kwargs):
super(ParseTorrentNode, self).__init__(*args, **kwargs)
array_nodes[str(id(self))] = self
def update_sockets(self, context):
if not self.inputs:
return
links = {}
for socket in self.inputs:
for link in socket.links:
links.setdefault(socket.name, []).append(
(link.from_node, link.from_socket))
for socket in self.inputs[1:]:
self.inputs.remove(socket)
self._add_id_input()
tree = self.id_data
for socket in self.inputs:
if socket.name in links:
for from_node, from_socket in links[socket.name]:
tree.links.new(from_socket, socket)
def _add_id_input(self):
mode_sockets = {
'magnet': ('LnxStringSocket', 'Magnet URI', 'Magnet URI (magnet:?xt=...)'),
'id': ('LnxStringSocket', 'Infohash', '40-char v1 or 64-char v2 infohash hex'),
'file': ('LnxDynamicSocket', 'Torrent File (Bytes)', '.torrent file bytes'),
}
socket_type, label, desc = mode_sockets.get(self.property0, mode_sockets['magnet'])
self.add_input(socket_type, label)
property0: HaxeEnumProperty('property0',
items=[('magnet', 'Magnet', 'Magnet URI (magnet:?xt=...)'),
('id', 'Infohash', '40-char v1 or 64-char v2 infohash hex'),
('file', 'Torrent File', '.torrent file bytes')],
name='Input', default='magnet', update=update_sockets)
def lnx_init(self, context):
self.add_input('LnxNodeSocketAction', 'In')
self._add_id_input()
self.add_output('LnxNodeSocketAction', 'Out')
self.add_output('LnxDynamicSocket', 'Meta')
self.add_output('LnxStringSocket', 'InfoHash')
self.add_output('LnxStringSocket', 'Name')
self.add_output('LnxNodeSocketArray', 'Trackers')
self.add_output('LnxNodeSocketArray', 'Web Seeds')
self.add_output('LnxNodeSocketArray', 'Files')
self.add_output('LnxFloatSocket', 'Total Size')
self.add_output('LnxDynamicSocket', 'Torrent Bytes')
def draw_buttons(self, context, layout):
layout.prop(self, 'property0')
def register():
add_category('Leenkx', icon='ORIENTATION_CURSOR')
ParseTorrentNode.on_register()

View File

@ -17,12 +17,14 @@ class SeedTorrentNode(LnxLogicTreeNode):
def lnx_init(self, context):
self.add_input('LnxNodeSocketAction', 'In')
self.add_input('LnxDynamicSocket', 'Connection')
self.add_input('LnxDynamicSocket', 'Opts [Map]')
self.add_input('LnxDynamicSocket', 'Torrent')
self.add_input('LnxDynamicSocket', 'Data')
self.add_output('LnxNodeSocketAction', 'Out')
self.add_output('LnxDynamicSocket', 'Torrent')
self.add_output('LnxNodeSocketAction', 'Done')
self.add_output('LnxStringSocket', 'InfoHash')
self.add_output('LnxStringSocket', 'Magnet')
def register():

View File

@ -0,0 +1,75 @@
from lnx.logicnode.lnx_nodes import *
class SetTrackersNode(LnxLogicTreeNode):
"""Set Trackers"""
bl_idname = 'LNSetTrackersNode'
bl_label = 'Set Trackers'
bl_description = ('Add/Override edit the tracker announce list of a live torrent')
lnx_category = 'Leenkx'
lnx_version = 1
tracker_props = {
'interval': ('LnxIntSocket', 'Interval'),
'port': ('LnxIntSocket', 'Port'),
'key': ('LnxIntSocket', 'Key'),
'numwant': ('LnxIntSocket', 'Num Want'),
'trackerId': ('LnxStringSocket', 'Tracker ID'),
}
def __init__(self, *args, **kwargs):
super(SetTrackersNode, self).__init__(*args, **kwargs)
array_nodes[str(id(self))] = self
def update_sockets(self, context):
if not self.inputs:
return
links = {}
for socket in self.inputs:
for link in socket.links:
links.setdefault(socket.name, []).append(
(link.from_node, link.from_socket))
for socket in self.inputs[1:]:
self.inputs.remove(socket)
self._add_target_inputs()
tree = self.id_data
for socket in self.inputs:
if socket.name in links:
for from_node, from_socket in links[socket.name]:
tree.links.new(from_socket, socket)
def _add_target_inputs(self):
if self.property0 in ('add', 'override'):
self.add_input('LnxDynamicSocket', 'Connection')
self.add_input('LnxDynamicSocket', 'Torrent')
self.add_input('LnxNodeSocketArray', 'Trackers')
else:
self.add_input('LnxDynamicSocket', 'Tracker')
socket_type, label = self.tracker_props.get(
self.property0, self.tracker_props['interval'])
self.add_input(socket_type, label)
property0: HaxeEnumProperty('property0',
items=[('add', 'Add', 'Merge the Trackers input on top of the existing announce list'),
('override', 'Override', 'Replace the announce list entirely; running tracker connections are restarted'),
('interval', 'Interval', 'Announce interval in seconds'),
('port', 'Port', 'Listening port reported to the tracker'),
('key', 'Key', 'Session key sent with announces'),
('numwant', 'Num Want', 'Number of peers requested per announce'),
('trackerId', 'Tracker ID', 'Tracker id echoed back by the tracker')],
name='Property', default='add', update=update_sockets)
def lnx_init(self, context):
self.add_input('LnxNodeSocketAction', 'In')
self._add_target_inputs()
self.add_output('LnxNodeSocketAction', 'Out')
self.add_output('LnxDynamicSocket', 'Target')
def draw_buttons(self, context, layout):
layout.prop(self, 'property0')
def register():
add_category('Leenkx', icon='ORIENTATION_CURSOR')
SetTrackersNode.on_register()

View File

@ -0,0 +1,31 @@
from lnx.logicnode.lnx_nodes import *
class TorrentClientInfoNode(LnxLogicTreeNode):
"""Torrent Client Info"""
bl_idname = 'LNTorrentClientInfoNode'
bl_label = 'Connection Torrents'
bl_description = ('Aggregate stats for the torrent client attached')
lnx_category = 'Leenkx'
lnx_version = 1
def __init__(self, *args, **kwargs):
super(TorrentClientInfoNode, self).__init__(*args, **kwargs)
array_nodes[str(id(self))] = self
def lnx_init(self, context):
self.add_input('LnxNodeSocketAction', 'In')
self.add_input('LnxDynamicSocket', 'Connection')
self.add_output('LnxNodeSocketAction', 'Out')
self.add_output('LnxNodeSocketArray', 'Torrents')
self.add_output('LnxIntSocket', 'Count')
self.add_output('LnxFloatSocket', 'Down Rate')
self.add_output('LnxFloatSocket', 'Up Rate')
self.add_output('LnxFloatSocket', 'Progress')
self.add_output('LnxFloatSocket', 'Ratio')
def register():
add_category('Leenkx', icon='ORIENTATION_CURSOR')
TorrentClientInfoNode.on_register()

View File

@ -0,0 +1,71 @@
from lnx.logicnode.lnx_nodes import *
class TorrentControlNode(LnxLogicTreeNode):
"""Torrent Control"""
bl_idname = 'LNTorrentControlNode'
bl_label = 'Torrent Control'
bl_description = ('Start, stop, remove or prioritize a torrent.')
lnx_category = 'Leenkx'
lnx_version = 1
def __init__(self, *args, **kwargs):
super(TorrentControlNode, self).__init__(*args, **kwargs)
array_nodes[str(id(self))] = self
property0: HaxeEnumProperty('property0',
items=[('start', 'Start', 'Start or resume the torrent session'),
('stop', 'Stop', 'Pause the torrent (announces stopped to trackers); it stays registered and can be started again'),
('remove', 'Remove', 'Destroy the torrent and remove it from the client'),
('remove-data', 'Remove + Data', 'Destroy the torrent and delete the stored content bytes'),
('select', 'Select', 'Prioritize a piece range: File handle or Start/End bytes, Priority optional (webtorrent torrent.select)'),
('deselect', 'Deselect', 'Drop a priority selection, back to normal order'),
('critical', 'Critical', 'Top-priority selection for streaming seeks'),
('add-peer', 'Add Peer', 'Connect to a peer directly; Address is host:port (sys/Krom targets only)'),
('add-web-seed', 'Add Web Seed', 'Register a BEP 19 webseed; Address is an http(s) URL, fills pieces when peers can\'t serve them')],
name='Action', default='start',
update=lambda self, context: self._rebuild_inputs())
def _rebuild_inputs(self):
if not self.inputs:
return
links = {}
for socket in self.inputs:
for link in socket.links:
links.setdefault(socket.name, []).append(
(link.from_node, link.from_socket))
for socket in self.inputs[3:]:
self.inputs.remove(socket)
self._add_action_inputs()
tree = self.id_data
for socket in self.inputs:
if socket.name in links:
for from_node, from_socket in links[socket.name]:
tree.links.new(from_socket, socket)
def _add_action_inputs(self):
if self.property0 in ('select', 'deselect', 'critical'):
self.add_input('LnxDynamicSocket', 'File')
self.add_input('LnxIntSocket', 'Start')
self.add_input('LnxIntSocket', 'End')
self.add_input('LnxIntSocket', 'Priority')
elif self.property0 == 'add-peer':
self.add_input('LnxStringSocket', 'Address')
elif self.property0 == 'add-web-seed':
self.add_input('LnxStringSocket', 'Address')
def lnx_init(self, context):
self.add_input('LnxNodeSocketAction', 'In')
self.add_input('LnxDynamicSocket', 'Connection')
self.add_input('LnxDynamicSocket', 'Torrent')
self._add_action_inputs()
self.add_output('LnxNodeSocketAction', 'Out')
def draw_buttons(self, context, layout):
layout.prop(self, 'property0')
def register():
add_category('Leenkx', icon='ORIENTATION_CURSOR')
TorrentControlNode.on_register()

View File

@ -0,0 +1,40 @@
from lnx.logicnode.lnx_nodes import *
class OnTorrentEventNode(LnxLogicTreeNode):
"""On Torrent Event"""
bl_idname = 'LNOnTorrentEventNode'
bl_label = 'On Torrent Event'
bl_description = ('Fires when the selected torrent event happens ')
lnx_category = 'Leenkx'
lnx_version = 1
def __init__(self, *args, **kwargs):
super(OnTorrentEventNode, self).__init__(*args, **kwargs)
array_nodes[str(id(self))] = self
property0: HaxeEnumProperty('property0',
items=[('ready', 'Ready', 'Metainfo is known; fires immediately if already ready'),
('piece', 'Piece', 'A piece finished downloading; Value is the piece index'),
('wire', 'Wire', 'A peer connected; Value is the WireProtocol'),
('metadata', 'Metadata', 'ut_metadata fetch completed; Value is the MetaInfo'),
('done', 'Done', 'All pieces are downloaded'),
('error', 'Error', 'Torrent failed; Value is the error string')],
name='Event', default='done')
def lnx_init(self, context):
self.add_input('LnxNodeSocketAction', 'In')
self.add_input('LnxDynamicSocket', 'Connection')
self.add_input('LnxDynamicSocket', 'Torrent')
self.add_output('LnxNodeSocketAction', 'Out')
self.add_output('LnxDynamicSocket', 'Torrent')
self.add_output('LnxDynamicSocket', 'Value')
def draw_buttons(self, context, layout):
layout.prop(self, 'property0')
def register():
add_category('Leenkx', icon='ORIENTATION_CURSOR')
OnTorrentEventNode.on_register()

View File

@ -0,0 +1,46 @@
from lnx.logicnode.lnx_nodes import *
class TorrentInfoNode(LnxLogicTreeNode):
"""Torrent Info"""
bl_idname = 'LNTorrentInfoNode'
bl_label = 'Torrent Info'
bl_description = ('Read the state of a torrent: infohash, name, peer count, progress, transfer totals, magnet URI')
lnx_category = 'Leenkx'
lnx_version = 1
def __init__(self, *args, **kwargs):
super(TorrentInfoNode, self).__init__(*args, **kwargs)
array_nodes[str(id(self))] = self
def lnx_init(self, context):
self.add_input('LnxNodeSocketAction', 'In')
self.add_input('LnxDynamicSocket', 'Connection')
self.add_input('LnxDynamicSocket', 'Torrent')
self.add_output('LnxNodeSocketAction', 'Out')
self.add_output('LnxStringSocket', 'InfoHash')
self.add_output('LnxStringSocket', 'Name')
self.add_output('LnxIntSocket', 'Peers')
self.add_output('LnxFloatSocket', 'Progress')
self.add_output('LnxFloatSocket', 'Downloaded')
self.add_output('LnxFloatSocket', 'Uploaded')
self.add_output('LnxBoolSocket', 'Done')
self.add_output('LnxStringSocket', 'Magnet')
self.add_output('LnxNodeSocketArray', 'Files')
self.add_output('LnxIntSocket', 'Num Pieces')
self.add_output('LnxIntSocket', 'Piece Length')
self.add_output('LnxFloatSocket', 'Total Size')
self.add_output('LnxStringSocket', 'Comment')
self.add_output('LnxStringSocket', 'Created By')
self.add_output('LnxIntSocket', 'Version')
self.add_output('LnxStringSocket', 'InfoHash2')
self.add_output('LnxFloatSocket', 'Down Rate')
self.add_output('LnxFloatSocket', 'Up Rate')
self.add_output('LnxBoolSocket', 'Stopped')
self.add_output('LnxDynamicSocket', 'Torrent')
def register():
add_category('Leenkx', icon='ORIENTATION_CURSOR')
TorrentInfoNode.on_register()

View File

@ -248,6 +248,7 @@ def export_data_impl(fp, sdk_path):
export_navigation = bpy.data.worlds['Lnx'].lnx_navigation != 'Disabled'
export_ui = bpy.data.worlds['Lnx'].lnx_ui != 'Disabled'
export_network = bpy.data.worlds['Lnx'].lnx_network != 'Disabled'
export_torrent = bpy.data.worlds['Lnx'].lnx_torrent != 'Disabled'
assets.reset()
mat_state.material_cache.clear()
@ -264,6 +265,7 @@ def export_data_impl(fp, sdk_path):
navigation_found = False
ui_found = False
network_found = False
torrent_found = False
LeenkxExporter.compress_enabled = wrd.lnx_asset_compression
LeenkxExporter.optimize_enabled = state.is_publish and wrd.lnx_optimize_data
if not os.path.exists(build_dir + '/compiled/Assets'):
@ -326,6 +328,8 @@ def export_data_impl(fp, sdk_path):
ui_found = True
if LeenkxExporter.export_network:
network_found = True
if LeenkxExporter.export_torrent:
torrent_found = True
assets.add(asset_path)
if physics_found is False: # Disable physics if no rigid body is exported
@ -340,6 +344,9 @@ def export_data_impl(fp, sdk_path):
if network_found == False:
export_network = False
if torrent_found == False:
export_torrent = False
# Ugly workaround: some logic nodes require Zui code even if no UI is used,
# for now enable UI export unless explicitly disabled.
export_ui = True
@ -349,6 +356,9 @@ def export_data_impl(fp, sdk_path):
if wrd.lnx_network == 'Enabled':
export_network = True
if wrd.lnx_torrent == 'Enabled':
export_torrent = True
modules = []
if wrd.lnx_audio == 'Enabled':
modules.append('audio')
@ -360,6 +370,8 @@ def export_data_impl(fp, sdk_path):
modules.append('ui')
if export_network:
modules.append('network')
if export_torrent:
modules.append('torrent')
defs = lnx.utils.def_strings_to_array(wrd.world_defs)
cdefs = lnx.utils.def_strings_to_array(wrd.compo_defs)
@ -451,7 +463,7 @@ def export_data_impl(fp, sdk_path):
wrd.lnx_project_version = lnx.utils.change_version_project(wrd.lnx_project_version)
# Write khafile.js
write_data.write_khafilejs(state.is_play, export_physics, export_navigation, export_ui, export_network, state.is_publish, LeenkxExporter.import_traits)
write_data.write_khafilejs(state.is_play, export_physics, export_navigation, export_ui, export_network, export_torrent, state.is_publish, LeenkxExporter.import_traits)
# Write Main.hx - depends on write_khafilejs for writing number of assets
scene_name = lnx.utils.get_project_scene_name()

View File

@ -34,6 +34,16 @@ else:
lnx_version = '2026.9'
lnx_commit = '$Id: 6b2644d47db169cedd95593497cc283207d23a74 $'
LNX_TORRENT_DEFAULT_TRACKERS = [
'wss://ws1.leenkx.com',
'wss://video.blender.org/tracker/socket',
]
LNX_TORRENT_DEFAULT_ICE_SERVERS = [
'stun:stun.l.google.com:19302',
'stun:global.stun.twilio.com:3478',
]
def get_project_html5_copy(self):
return self.get('lnx_project_html5_copy', False)
@ -282,6 +292,17 @@ def init_properties():
('Enabled', 'Enabled', 'Enabled'),
('Auto', 'Auto', 'Auto')],
name="Networking", default='Auto', description="Include Network library", update=assets.invalidate_compiler_cache)
bpy.types.World.lnx_torrent = EnumProperty(
items=[('Disabled', 'Disabled', 'Disabled'),
('Enabled', 'Enabled', 'Enabled')],
name="Torrent", default='Disabled', description="Include Torrent (Leenkx) library", update=assets.invalidate_compiler_cache)
bpy.types.World.lnx_torrent_download_dir = StringProperty(name="Download Path", description="Directory torrent downloads are written to. Relative paths resolve against the project directory at runtime; absolute paths are used as-is", default="Downloads", subtype='DIR_PATH', update=assets.invalidate_compiler_cache)
bpy.types.World.lnx_torrent_trackers = CollectionProperty(name="Torrent Trackers", description="Default torrent tracker announce URLs", type=bpy.types.PropertyGroup)
bpy.types.World.lnx_torrent_trackers_index = IntProperty(name="Torrent Tracker Index", default=0)
bpy.types.World.lnx_torrent_ice_servers = CollectionProperty(name="ICE Servers", description="Default ICE/STUN servers used for WebRTC peer connections", type=bpy.types.PropertyGroup)
bpy.types.World.lnx_torrent_ice_servers_index = IntProperty(name="ICE Server Index", default=0)
bpy.types.World.lnx_torrent_peer_timeout = IntProperty(name="Peer Timeout", description="Milliseconds after which an unresponsive peer is dropped", default=5 * 60 * 1000, min=0, update=assets.invalidate_compiler_cache)
bpy.types.World.lnx_torrent_heartbeat = IntProperty(name="Heartbeat", description="Milliseconds between heartbeat packets sent to peers", default=30000, min=0, update=assets.invalidate_compiler_cache)
bpy.types.World.lnx_audio = EnumProperty(
items=[('Disabled', 'Disabled', 'Disabled'),
('Enabled', 'Enabled', 'Enabled')],
@ -701,13 +722,23 @@ def create_wrd():
wrd.use_fake_user = True # Store data world object, add fake user to keep it alive
wrd.lnx_version = lnx_version
wrd.lnx_commit = lnx_commit
seed_torrent_trackers(wrd)
def seed_torrent_trackers(wrd):
if len(wrd.lnx_torrent_trackers) == 0:
for url in LNX_TORRENT_DEFAULT_TRACKERS:
wrd.lnx_torrent_trackers.add().name = url
if len(wrd.lnx_torrent_ice_servers) == 0:
for url in LNX_TORRENT_DEFAULT_ICE_SERVERS:
wrd.lnx_torrent_ice_servers.add().name = url
def init_properties_on_load():
if 'Lnx' not in bpy.data.worlds:
init_properties()
wrd = bpy.data.worlds['Lnx']
seed_torrent_trackers(wrd)
# New project?
if bpy.data.filepath == '':
wrd = bpy.data.worlds['Lnx']
wrd.lnx_debug_console = lnx.utils.get_debug_console_auto()
lnx.utils.fetch_script_names()

View File

@ -1358,11 +1358,150 @@ class LNX_PT_ProjectModulesPanel(bpy.types.Panel):
layout.prop(wrd, 'lnx_navigation_engine')
layout.prop(wrd, 'lnx_ui')
layout.prop(wrd, 'lnx_network')
layout.prop(wrd, 'lnx_torrent')
layout.prop_search(wrd, 'lnx_khafile', bpy.data, 'texts')
layout.prop(wrd, 'lnx_project_root')
layout.prop(wrd, 'lnx_external_blends_path')
class LNX_UL_TorrentTrackerList(bpy.types.UIList):
def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index):
layout.prop(item, "name", text="", emboss=False, icon='NETWORK_DRIVE')
class LnxTorrentTrackerListNewItem(bpy.types.Operator):
bl_idname = "lnx_torrent_tracker_list.new_item"
bl_label = "Add a new tracker"
def execute(self, context):
wrd = bpy.data.worlds['Lnx']
wrd.lnx_torrent_trackers.add()
wrd.lnx_torrent_trackers_index = len(wrd.lnx_torrent_trackers) - 1
assets.invalidate_compiler_cache(wrd, context)
return{'FINISHED'}
class LnxTorrentTrackerListDeleteItem(bpy.types.Operator):
bl_idname = "lnx_torrent_tracker_list.delete_item"
bl_label = "Deletes a tracker"
@classmethod
def poll(cls, context):
wrd = bpy.data.worlds['Lnx']
return len(wrd.lnx_torrent_trackers) > 0
def execute(self, context):
wrd = bpy.data.worlds['Lnx']
index = wrd.lnx_torrent_trackers_index
wrd.lnx_torrent_trackers.remove(index)
wrd.lnx_torrent_trackers_index = min(max(0, index - 1), len(wrd.lnx_torrent_trackers) - 1)
assets.invalidate_compiler_cache(wrd, context)
return{'FINISHED'}
class LnxTorrentTrackerListReset(bpy.types.Operator):
bl_idname = "lnx_torrent_tracker_list.reset_items"
bl_label = "Reset To Default Trackers"
bl_description = "Remove all trackers and add back the default ones"
def execute(self, context):
wrd = bpy.data.worlds['Lnx']
wrd.lnx_torrent_trackers.clear()
for url in props.LNX_TORRENT_DEFAULT_TRACKERS:
wrd.lnx_torrent_trackers.add().name = url
wrd.lnx_torrent_trackers_index = 0
assets.invalidate_compiler_cache(wrd, context)
return{'FINISHED'}
class LNX_UL_TorrentIceServerList(bpy.types.UIList):
def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index):
layout.prop(item, "name", text="", emboss=False, icon='URL')
class LnxTorrentIceServerListNewItem(bpy.types.Operator):
bl_idname = "lnx_torrent_ice_server_list.new_item"
bl_label = "Add a new ICE server"
def execute(self, context):
wrd = bpy.data.worlds['Lnx']
wrd.lnx_torrent_ice_servers.add()
wrd.lnx_torrent_ice_servers_index = len(wrd.lnx_torrent_ice_servers) - 1
assets.invalidate_compiler_cache(wrd, context)
return{'FINISHED'}
class LnxTorrentIceServerListDeleteItem(bpy.types.Operator):
bl_idname = "lnx_torrent_ice_server_list.delete_item"
bl_label = "Deletes an ICE server"
@classmethod
def poll(cls, context):
wrd = bpy.data.worlds['Lnx']
return len(wrd.lnx_torrent_ice_servers) > 0
def execute(self, context):
wrd = bpy.data.worlds['Lnx']
index = wrd.lnx_torrent_ice_servers_index
wrd.lnx_torrent_ice_servers.remove(index)
wrd.lnx_torrent_ice_servers_index = min(max(0, index - 1), len(wrd.lnx_torrent_ice_servers) - 1)
assets.invalidate_compiler_cache(wrd, context)
return{'FINISHED'}
class LnxTorrentIceServerListReset(bpy.types.Operator):
bl_idname = "lnx_torrent_ice_server_list.reset_items"
bl_label = "Reset To Default ICE Servers"
bl_description = "Remove all ICE servers and add back the default ones"
def execute(self, context):
wrd = bpy.data.worlds['Lnx']
wrd.lnx_torrent_ice_servers.clear()
for url in props.LNX_TORRENT_DEFAULT_ICE_SERVERS:
wrd.lnx_torrent_ice_servers.add().name = url
wrd.lnx_torrent_ice_servers_index = 0
assets.invalidate_compiler_cache(wrd, context)
return{'FINISHED'}
class LNX_PT_LeenkxNetworkPanel(bpy.types.Panel):
bl_label = "Leenkx Network"
bl_space_type = "PROPERTIES"
bl_region_type = "WINDOW"
bl_context = "render"
bl_options = {'DEFAULT_CLOSED'}
def draw(self, context):
layout = self.layout
layout.use_property_split = True
layout.use_property_decorate = False
wrd = bpy.data.worlds['Lnx']
col = layout.column()
col.enabled = wrd.lnx_torrent != 'Disabled'
col.prop(wrd, "lnx_torrent_download_dir")
col.prop(wrd, "lnx_torrent_peer_timeout")
col.prop(wrd, "lnx_torrent_heartbeat")
col.label(text="Trackers")
rows = 2
if len(wrd.lnx_torrent_trackers) > 1:
rows = 4
row = col.row()
row.template_list("LNX_UL_TorrentTrackerList", "The_List", wrd, "lnx_torrent_trackers", wrd, "lnx_torrent_trackers_index", rows=rows)
btns = row.column(align=True)
btns.operator("lnx_torrent_tracker_list.new_item", icon='ADD', text="")
btns.operator("lnx_torrent_tracker_list.delete_item", icon='REMOVE', text="")
col.operator("lnx_torrent_tracker_list.reset_items", icon='FILE_REFRESH')
col.label(text="ICE Servers")
rows = 2
if len(wrd.lnx_torrent_ice_servers) > 1:
rows = 4
row = col.row()
row.template_list("LNX_UL_TorrentIceServerList", "The_List", wrd, "lnx_torrent_ice_servers", wrd, "lnx_torrent_ice_servers_index", rows=rows)
btns = row.column(align=True)
btns.operator("lnx_torrent_ice_server_list.new_item", icon='ADD', text="")
btns.operator("lnx_torrent_ice_server_list.delete_item", icon='REMOVE', text="")
col.operator("lnx_torrent_ice_server_list.reset_items", icon='FILE_REFRESH')
class LnxVirtualInputPanel(bpy.types.Panel):
bl_label = "Leenkx Virtual Input"
bl_space_type = "PROPERTIES"
@ -3107,6 +3246,15 @@ __REG_CLASSES = (
LNX_PT_ProjectFlagsDebugConsolePanel,
LNX_PT_ProjectWindowPanel,
LNX_PT_ProjectModulesPanel,
LNX_UL_TorrentTrackerList,
LnxTorrentTrackerListNewItem,
LnxTorrentTrackerListDeleteItem,
LnxTorrentTrackerListReset,
LNX_UL_TorrentIceServerList,
LnxTorrentIceServerListNewItem,
LnxTorrentIceServerListDeleteItem,
LnxTorrentIceServerListReset,
LNX_PT_LeenkxNetworkPanel,
LNX_PT_RenderPathPanel,
LNX_PT_RenderPathRendererPanel,
LNX_PT_RenderPathShadowsPanel,

View File

@ -158,7 +158,7 @@ def remove_readonly(func, path, excinfo):
func(path)
def write_khafilejs(is_play, export_physics: bool, export_navigation: bool, export_ui: bool, export_network: bool, is_publish: bool,
def write_khafilejs(is_play, export_physics: bool, export_navigation: bool, export_ui: bool, export_network: bool, export_torrent: bool, is_publish: bool,
import_traits: List[str]) -> None:
wrd = bpy.data.worlds['Lnx']
rpdat = lnx.utils.get_rp()
@ -482,9 +482,16 @@ let project = new Project('""" + lnx.utils.safesrc(wrd.lnx_project_name + '-' +
if not os.path.exists('Libraries/network'):
khafile.write(add_leenkx_library(sdk_path, 'lib/network', rel_path=do_relpath_sdk))
assets.add_khafile_def('lnx_network')
lnxjs_path = sdk_path + '/lib/leenkx_tools/lnxjs/Leenkx.js'
lnxjs_path = lnxjs_path.replace('\\', '/').replace('//', '/')
khafile.write(add_assets(lnxjs_path, rel_path=do_relpath_sdk))
#lnxjs_path = sdk_path + '/lib/leenkx_tools/lnxjs/Leenkx.js'
#lnxjs_path = lnxjs_path.replace('\\', '/').replace('//', '/')
#khafile.write(add_assets(lnxjs_path, rel_path=do_relpath_sdk))
if export_torrent:
assets.add_khafile_def('lnx_torrent')
if state.target in ('windows-hl', 'linux-hl', 'macos-hl'):
if not os.path.exists('Libraries/datachannel'):
khafile.write(add_leenkx_library(sdk_path + '/lib/', 'datachannel', rel_path=do_relpath_sdk))
assets.add_khafile_def('lnx_datachannel')
if not wrd.lnx_minimize:
assets.add_khafile_def('lnx_json')
@ -736,6 +743,19 @@ class Main {
elif wrd.lnx_canvas_img_scaling_quality == 'high':
f.write("\n" + content_indent + "leenkx.ui.Canvas.imageScaleQuality = kha.graphics2.ImageScaleQuality.High;")
if wrd.lnx_torrent != 'Disabled':
dl_dir = wrd.lnx_torrent_download_dir
if dl_dir.startswith('//'):
dl_dir = bpy.path.abspath(dl_dir)
if dl_dir != '':
f.write("\n" + content_indent + "leenkx.network.torrent.TorrentClient.defaultDownloadDir = " + json.dumps(dl_dir) + ";")
trackers_list = [json.dumps(t.name) for t in wrd.lnx_torrent_trackers if t.name != '']
f.write("\n" + content_indent + "leenkx.network.torrent.TorrentClient.defaultTrackers = [" + ", ".join(trackers_list) + "];")
ice_list = [json.dumps(t.name) for t in wrd.lnx_torrent_ice_servers if t.name != '']
f.write("\n" + content_indent + "leenkx.network.torrent.peer.RtcPeerPool.defaultIceServers = [" + ", ".join(ice_list) + "];")
f.write("\n" + content_indent + "leenkx.network.torrent.LeenkxClient.PEERTIMEOUT = " + str(wrd.lnx_torrent_peer_timeout) + ";")
f.write("\n" + content_indent + "leenkx.network.torrent.LeenkxClient.defaultHeartbeat = " + str(wrd.lnx_torrent_heartbeat) + ";")
# Write Starter.main call
starter_indent = content_indent + " "
f.write("\n" + content_indent + "leenkx.system.Starter.main(")