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 readStorage(name: String): haxe.io.BytesData;
static function fileSaveBytes(path: String, bytes: haxe.io.BytesData): Void; 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 sysCommand(cmd: String, ?args: Array<String>): Int;
static function savePath(): String; static function savePath(): String;
static function getArgCount(): Int; 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,17 +1,21 @@
package leenkx.logicnode; package leenkx.logicnode;
import iron.object.Object; import iron.object.Object;
#if js
import leenkx.network.Types; import leenkx.network.Types;
import leenkx.network.Util; #if lnx_torrent
import leenkx.network.Leenkx; import leenkx.network.Leenkx;
import leenkx.network.torrent.LeenkxClient;
import leenkx.network.torrent.TorrentClient;
import leenkx.network.torrent.peer.RtcPeerPool;
#end #end
import leenkx.system.Event; import leenkx.system.Event;
class CreateLeenkxNode extends LogicNode { 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 net_Url: String;
public var data: Dynamic;
public var client: Dynamic;
public var value: String; public var value: String;
public var listener: TEvent = null; public var listener: TEvent = null;
@ -20,112 +24,145 @@ class CreateLeenkxNode extends LogicNode {
} }
override function run(from:Int) { override function run(from:Int) {
#if js #if lnx_torrent
net_Url = inputs[1].get(); net_Url = inputs[1].get();
if (net_Url == null) return; if (net_Url == null) return;
if (Leenkx.connections[net_Url] == null) { if (Leenkx.connections[net_Url] == null) {
var object = tree.object;
value = Leenkx.onLoadEvent; value = Leenkx.onLoadEvent;
listener = Event.add(value, onEvent, tree.object.uid); listener = Event.add(value, onEvent, tree.object.uid);
client = new leenkx.network.Leenkx(net_Url, object); new leenkx.network.Leenkx(net_Url, tree.object);
} else {
return;
} }
#end #end
} }
function onEvent() { function onEvent() {
#if js if (listener != null) {
var lnxjs:Dynamic = js.Lib.global; Event.removeListener(listener);
var lnxCxNew = lnxjs.lnxNew; listener = null;
}
if (lnxCxNew == null) { #if lnx_torrent
trace("ERROR: lnxNew not available"); var sock = Leenkx.connections[net_Url];
if (sock == null) return;
if (sock.client != null) {
runOutput(0);
return; 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;
var lnxCn:Dynamic = lnxCxNew(net_Url); 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();
};
lnxCn.on("connections", function(c) { sock.client = lnxCn;
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); lnxCn.connect();
Leenkx.connections[net_Url].client = lnxCn; #if (js && !kha_krom)
js.Browser.window.addEventListener("beforeunload", function(e) {
var script = 'globalThis.addEventListener("beforeunload", function (e) { lnxCn.destroy();
leenkx.network.Leenkx.connections.h["' + net_Url + '"].client.destroy(); });
delete e["returnValue"];
});';
js.Syntax.code('(1, eval)({0})', script);
runOutput(0);
#end #end
#end
runOutput(0);
} }
override function get(from: Int): Dynamic { override function get(from: Int): Dynamic {
#if js #if lnx_torrent
return switch (from) { return switch (from) {
case 1: Leenkx.connections[net_Url]; case 1: Leenkx.connections[net_Url];
case 2: Leenkx.id.get(net_Url); case 2: Leenkx.id.get(net_Url);

View File

@ -1,43 +1,100 @@
package leenkx.logicnode; package leenkx.logicnode;
import leenkx.system.Event; #if lnx_torrent
import leenkx.network.Leenkx; import leenkx.network.Leenkx;
import iron.object.Object; import leenkx.network.torrent.Magnet;
import leenkx.network.torrent.TorrentClient;
#end
class CreateTorrentNode extends LogicNode { class CreateTorrentNode extends LogicNode {
public var torrent: Dynamic; public var meta:Dynamic;
public var net_Url: String; 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) { public function new(tree:LogicTree) {
super(tree); super(tree);
} }
override function run(from:Int) { override function run(from:Int) {
#if lnx_torrent
meta = null;
metaBytes = null;
magnetUri = null;
infoHashHex = null;
var connection = inputs[1].get(); var data = inputs[1].get();
if (connection == null) return;
var data = inputs[2].get();
if (data == null) return; 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 + '; if (mode == "folder") {
lx_' + net_Url +'.wt.add(file, function (torrent) { #if sys
leenkx.network.Leenkx.file.set("' + net_Url +'", torrent.files); data = TorrentClient.readFolder(Std.string(data));
torrent.on("done", function () { var first:Dynamic = data[0];
leenkx.network.Leenkx.connections.h["' + net_Url +'"].ontorrentdone(); 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); 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 { override function get(from:Int):Dynamic {
#if lnx_torrent
return switch (from) { return switch (from) {
case 1: Leenkx.file.get(net_Url); case 1: meta;
case 2: metaBytes;
case 3: infoHashHex;
case 4: magnetUri;
default: throw "Unreachable"; 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; package leenkx.logicnode;
import leenkx.system.Event; #if lnx_torrent
import leenkx.network.Connect; import leenkx.network.Leenkx;
import iron.object.Object; import leenkx.network.torrent.LeenkxClient;
#end
class LeenkxCloseConnectionNode extends LogicNode { class LeenkxCloseConnectionNode extends LogicNode {
public var property0: String; public var property0: String;
@ -12,41 +12,29 @@ class LeenkxCloseConnectionNode extends LogicNode {
} }
override function run(from:Int) { override function run(from:Int) {
#if lnx_torrent
var connection = inputs[1].get(); 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 lnxCn:LeenkxClient = socket.client;
var script = ' if (property0 == "peer") {
for (p in leenkx.network.Leenkx.connections.h["' + connection._url + '"].client.torrent._peers){ var id:String = inputs.length > 2 ? inputs[2].get() : null;
try{ if (id != null && lnxCn != null) {
leenkx.network.Leenkx.id.set("' + connection._url + '",p); lnxCn.peers.remove(id);
leenkx.network.Leenkx.connections.h["' + connection._url + '"].client.torrent._peers[p].conn._pc.close(); lnxCn.seenFired.remove(id);
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);
} }
runOutput(0);
return;
} }
'; if (lnxCn != null) {
js.Syntax.code('(1, eval)({0})', script.toString()); lnxCn.destroy();
} else { socket.client = null;
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());
} }
Leenkx.connections.remove(connection._url);
socket.onclose();
#end
runOutput(0); runOutput(0);
} }
} }

View File

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

View File

@ -1,8 +1,6 @@
package leenkx.logicnode; package leenkx.logicnode;
import leenkx.network.Connect;
import leenkx.network.Buffer; import leenkx.network.Buffer;
import haxe.io.Bytes; import haxe.io.Bytes;
import iron.object.Object;
import iron.math.Vec4; import iron.math.Vec4;
import iron.math.Quat; import iron.math.Quat;
import iron.math.Mat4; import iron.math.Mat4;
@ -22,98 +20,54 @@ class LeenkxMessageParserNode extends LogicNode {
if (data == null) return; if (data == null) return;
api = inputs[1].get(); 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){ if(buffer.endsWith(api) == false){
return; 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) { switch (property0) {
case "string": case "string":
var bytes = buffer.readAllAvailableBytes(); var bytes = buffer.readAllAvailableBytes();
parsed = bytes.toString().substr(0, bytes.length - api.length); parsed = bytes.toString().substr(0, bytes.length - api.length);
trace(parsed);
runOutput(0); runOutput(0);
case "vector": 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(); var vec = new Vec4();
for (f in Reflect.fields(vec)) { vec.x = bytes.readFloat();
Reflect.setField(vec, f, bytes.readFloat()); vec.y = bytes.readFloat();
} vec.z = bytes.readFloat();
vec.w = bytes.readFloat();
parsed = vec; parsed = vec;
runOutput(0); runOutput(0);
case "float": 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(); var float: Float = bytes.readFloat();
parsed = float; parsed = float;
runOutput(0); runOutput(0);
case "integer": 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(); var integer: Int = bytes.readInt32();
parsed = integer; parsed = integer;
runOutput(0); runOutput(0);
@ -127,34 +81,45 @@ class LeenkxMessageParserNode extends LogicNode {
} }
runOutput(0); runOutput(0);
case "transform": 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 loc: Vec4 = new Vec4();
var rot: Quat = new Quat(); var rot: Quat = new Quat();
var scl: Vec4 = new Vec4(); var scl: Vec4 = new Vec4();
for (f in Reflect.fields(loc)) { try {
Reflect.setField(loc, f, bytes.readFloat()); loc.x = bytes.readFloat();
} loc.y = bytes.readFloat();
for (f in Reflect.fields(rot)) { loc.z = bytes.readFloat();
Reflect.setField(rot, f, bytes.readFloat()); loc.w = bytes.readFloat();
} rot.x = bytes.readFloat();
for (f in Reflect.fields(scl)) { rot.y = bytes.readFloat();
Reflect.setField(scl, f, 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(); var transform = Mat4.identity();
parsed = transform.compose(loc, rot, scl); parsed = transform.compose(loc, rot, scl);
runOutput(0); runOutput(0);
case "rotation": 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(); var rot = new Quat();
for (f in Reflect.fields(rot)) { rot.x = bytes.readFloat();
Reflect.setField(rot, f, bytes.readFloat()); rot.y = bytes.readFloat();
} rot.z = bytes.readFloat();
rot.w = bytes.readFloat();
parsed = rot; parsed = rot;
runOutput(0); runOutput(0);
default: throw "Failed to parse data."; default: throw "Failed to parse data.";
} }
} }
override function get(from: Int): Dynamic { override function get(from: Int): Dynamic {

View File

@ -1,401 +1,124 @@
package leenkx.logicnode; package leenkx.logicnode;
import leenkx.network.Connect;
import leenkx.network.Leenkx;
import leenkx.network.Buffer; import leenkx.network.Buffer;
import haxe.io.Bytes; 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.Vec4;
import iron.math.Quat; import iron.math.Quat;
import iron.math.Mat4; import iron.math.Mat4;
class LeenkxSendMessageNode extends LogicNode { class LeenkxSendMessageNode extends LogicNode {
public var property0: String;
public var property1: String; public var property1: String;
public var id: String; public var property2: Bool;
public var all: Bool;
public function new(tree:LogicTree) { public function new(tree:LogicTree) {
super(tree); super(tree);
} }
override function run(from:Int) { override function run(from:Int) {
#if js
var connection = inputs[1].get(); var connection = inputs[1].get();
if (connection == null) return; if (connection == null) return;
var api: String = inputs[2].get(); var api: String = inputs[2].get();
var message: Dynamic = inputs[3].get(); var message: Dynamic = inputs[3].get();
if (message == null) return;
try { var broadcast: Bool = inputs.length > 4 && inputs[4].get() == true;
if(inputs[5].get() == true){ var targetAddress: String = inputs.length > 5 ? inputs[5].get() : null;
var script = 'for (p in lnx_' + connection._url +'.torrent._peers){ if (!broadcast && (targetAddress == null || targetAddress == "")) {
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() + '`, `'+ api + message + '` );';
js.Syntax.code('(1, eval)({0})', script.toString());
runOutput(0);
}
} catch(error) {
trace("Error: " + error);
}
return; 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) { switch (property1) {
case "string": case "string":
if(property0 == "client"){
var buffer = new Buffer();
buffer.writeBytes(Bytes.ofString(message)); buffer.writeBytes(Bytes.ofString(message));
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 "vector": case "vector":
if(property0 == "client"){
var bytesOut = new haxe.io.BytesOutput(); var bytesOut = new haxe.io.BytesOutput();
var i = Reflect.fields(message); var f:Null<Float>;
for (b in i) { if ((f = message.x) != null) bytesOut.writeFloat(f);
bytesOut.writeFloat(Reflect.field(message, b)); if ((f = message.y) != null) bytesOut.writeFloat(f);
} if ((f = message.z) != null) bytesOut.writeFloat(f);
var buffer = new Buffer(); if ((f = message.w) != null) bytesOut.writeFloat(f);
buffer.writeBytes(bytesOut.getBytes()); 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": case "float":
if(property0 == "client"){
var bytesOut = new haxe.io.BytesOutput(); var bytesOut = new haxe.io.BytesOutput();
bytesOut.writeFloat(message); bytesOut.writeFloat(message);
var buffer = new Buffer();
buffer.writeBytes(bytesOut.getBytes()); 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": case "integer":
if(property0 == "client"){
var bytesOut = new haxe.io.BytesOutput(); var bytesOut = new haxe.io.BytesOutput();
bytesOut.writeInt32(message); bytesOut.writeInt32(message);
var buffer = new Buffer();
buffer.writeBytes(bytesOut.getBytes()); 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": case "boolean":
if(property0 == "client"){
var buffer = new Buffer();
if (message == true) { if (message == true) {
buffer.writeBytes(Bytes.ofString("true")); buffer.writeBytes(Bytes.ofString("true"));
} else { } else {
buffer.writeBytes(Bytes.ofString("false")); 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": case "transform":
if(property0 == "client"){
var bytesOut = new haxe.io.BytesOutput(); var bytesOut = new haxe.io.BytesOutput();
var transform:Mat4 = message; var transform:Mat4 = message;
var loc = new Vec4(); var loc = new Vec4();
var rot = new Quat(); var rot = new Quat();
var scale = new Vec4(); var scale = new Vec4();
transform.decompose(loc, rot, scale); transform.decompose(loc, rot, scale);
var l = Reflect.fields(loc); bytesOut.writeFloat(loc.x);
for (b in l) { bytesOut.writeFloat(loc.y);
bytesOut.writeFloat(Reflect.field(loc, b)); bytesOut.writeFloat(loc.z);
} bytesOut.writeFloat(loc.w);
var r = Reflect.fields(rot); bytesOut.writeFloat(rot.x);
for (b in r) { bytesOut.writeFloat(rot.y);
bytesOut.writeFloat(Reflect.field(rot, b)); bytesOut.writeFloat(rot.z);
} bytesOut.writeFloat(rot.w);
var s = Reflect.fields(scale); bytesOut.writeFloat(scale.x);
for (b in s) { bytesOut.writeFloat(scale.y);
bytesOut.writeFloat(Reflect.field(scale, b)); bytesOut.writeFloat(scale.z);
} bytesOut.writeFloat(scale.w);
var buffer = new Buffer();
buffer.writeBytes(bytesOut.getBytes()); 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": case "rotation":
if(property0 == "client"){
var bytesOut = new haxe.io.BytesOutput(); var bytesOut = new haxe.io.BytesOutput();
var i = Reflect.fields(message); var f:Null<Float>;
for (b in i) { if ((f = message.x) != null) bytesOut.writeFloat(f);
bytesOut.writeFloat(Reflect.field(message, b)); if ((f = message.y) != null) bytesOut.writeFloat(f);
} if ((f = message.z) != null) bytesOut.writeFloat(f);
var buffer = new Buffer(); if ((f = message.w) != null) bytesOut.writeFloat(f);
buffer.writeBytes(bytesOut.getBytes()); buffer.writeBytes(bytesOut.getBytes());
if(api != "" || api != null){ default: trace("Failed to send data.");
}
if (api != null && api != "") {
buffer.writeBytes(Bytes.ofString(api)); buffer.writeBytes(Bytes.ofString(api));
} }
var rawBytes = buffer.readAllAvailableBytes();
try { try {
connection.buffer = buffer; if (property2) {
if(inputs[5].get() == true){ var payload = haxe.crypto.Base64.encode(rawBytes);
var script = 'for (p in lnx_' + connection._url +'.torrent._peers){ if (broadcast) {
try{ lnxCn.send(null, payload);
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 { } else {
var script = 'lnx_' + connection._url +'.send("' + inputs[4].get() + '",leenkx.network.Leenkx.connections.h["'+ connection._url +'"].buffer );'; lnxCn.send(targetAddress, payload);
js.Syntax.code('(1, eval)({0})', script.toString()); }
runOutput(0); } else {
lnxCn.sendRawMessage(rawBytes, broadcast ? null : targetAddress);
} }
} catch(error) { } catch(error) {
trace("Error: " + 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); runOutput(0);
return;
} #else
} runOutput(0);
#end #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); var buffer = cast(data, Buffer);
api = inputs[1].get(); api = inputs[1].get();
if (api == null) api = "";
if(buffer.endsWith(api) == false){ if(buffer.endsWith(api) == false){
return; 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; package leenkx.logicnode;
import leenkx.system.Event; #if lnx_torrent
import iron.object.Object; import haxe.io.Bytes;
import leenkx.network.Leenkx;
import leenkx.network.torrent.MetaInfo;
import leenkx.network.torrent.Torrent;
#end
class SeedTorrentNode extends LogicNode { class SeedTorrentNode extends LogicNode {
public var torrent:Dynamic; public var torrent:Dynamic;
public var net_Url: Dynamic; public var net_Url:String;
public function new(tree:LogicTree) { public function new(tree:LogicTree) {
super(tree); super(tree);
} }
override function run(from:Int) { override function run(from:Int) {
#if js #if lnx_torrent
var connection = inputs[1].get(); var connection = inputs[1].get();
if (connection == null) return; if (connection == null) return;
trace("got connection"); var src = inputs[2].get();
var data = inputs[3].get();
if (data == null) return;
trace("got data");
var opts = inputs[2].get();
net_Url = connection._url; net_Url = connection._url;
var cx = "lx_" + net_Url; var socket = Leenkx.connections[connection._url];
var lx:Dynamic = untyped js.Browser.window[cx]; if (socket == null || socket.client == null) return;
var args = opts.toString(); var client = socket.client;
trace(args); var self = this;
var func = Reflect.field(lx.wt, "seed");
if (func != null) { var onSeeded = function(t:Torrent) {
torrent = func(data, function (torrent) { if (t == null) return;
trace('Client is seeding ' + torrent.magnetURI); self.torrent = t;
}); var onDone = function() {
//trace(torrent); 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); runOutput(0);
#end #end
} }
#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 { override function get(from:Int):Dynamic {
#if lnx_torrent
var t:Torrent = torrent;
return switch (from) { return switch (from) {
case 1: torrent; case 1: torrent;
case 3: t != null ? t.infoHashHex : null;
case 4: t != null ? t.magnetURI : null;
default: throw "Unreachable"; 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 { class LeenkxSocket {
public var _url:String; public var _url:String;
public var client:LeenkxClient; public var client:LeenkxClient;
public var torrentClient:leenkx.network.torrent.TorrentClient;
public var buffer:Dynamic; public var buffer:Dynamic;
public var onopen:Void->Void; public var onopen:Void->Void;
public var onclose:Void->Void; public var onclose:Void->Void;

View File

@ -9,7 +9,8 @@ import leenkx.network.torrent.TorrentInfo;
class LeenkxClient { 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 identifier:String;
public var seed:String; public var seed:String;
@ -53,6 +54,54 @@ class LeenkxClient {
private var deferredOpts:TransportOpts = null; private var deferredOpts:TransportOpts = null;
private var heartbeatInterval:Int = 30000; 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) { public function new(?identifier:String, ?opts:LeenkxOptions) {
if (identifier != null && !Std.isOfType(identifier, String)) { if (identifier != null && !Std.isOfType(identifier, String)) {
opts = cast identifier; opts = cast identifier;
@ -60,7 +109,7 @@ class LeenkxClient {
} }
opts = opts != null ? opts : {}; 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; this.timeout = opts.timeout != null ? opts.timeout : PEERTIMEOUT;
if (opts.seed != null) { if (opts.seed != null) {
@ -90,7 +139,7 @@ class LeenkxClient {
createTransport(opts, opts.deferJoin == true); 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 { function createTransport(opts:LeenkxOptions, defer:Bool):Void {
@ -281,6 +330,10 @@ class LeenkxClient {
} }
var packet = makePacket({y: PacketType.Leave}); var packet = makePacket({y: PacketType.Leave});
sendRaw(packet); sendRaw(packet);
if (swarm != null) {
swarm.destroy();
swarm = null;
}
if (transport != null) { if (transport != null) {
transport.destroy(cb); transport.destroy(cb);
} else if (cb != null) { } else if (cb != null) {

View File

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

View File

@ -5,9 +5,7 @@ import leenkx.network.torrent.storage.Storage;
import leenkx.network.torrent.storage.Storage.IStorage; import leenkx.network.torrent.storage.Storage.IStorage;
class TorrentClient { class TorrentClient {
public static var defaultTrackers:Array<String> = [ public static var defaultTrackers:Array<String> = [];
"wss://ws1.leenkx.com"
];
public static var defaultDownloadDir:String = null; public static var defaultDownloadDir:String = null;
public var downloadDir:String = null; public var downloadDir:String = null;
@ -247,8 +245,7 @@ class TorrentClient {
files.push({path: name, data: Bytes.ofString(Std.string(data))}); files.push({path: name, data: Bytes.ofString(Std.string(data))});
} }
return {meta: MetaInfo.create(name, files, trackers, opts), return {meta: MetaInfo.create(name, files, trackers, opts), files: files};
files: files};
} }
public static function readFolder(path:String, ?base:String):Array<{path:String, data:Bytes}> { 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); root = root.substr(0, root.length - 1);
} }
if (base == null) { if (base == null) {
var i = Std.int(Math.max(root.lastIndexOf("/"), var i = Std.int(Math.max(root.lastIndexOf("/"), root.lastIndexOf("\\")));
root.lastIndexOf("\\")));
base = i >= 0 ? root.substr(i + 1) : root; base = i >= 0 ? root.substr(i + 1) : root;
} }
var files:Array<{path:String, data:Bytes}> = []; var files:Array<{path:String, data:Bytes}> = [];
@ -317,20 +313,79 @@ class TorrentClient {
return t; return t;
} }
public function remove(infoHashHex:String, ?deleteData:Bool):Void { public function get(id:Dynamic):Torrent {
var t = torrents.get(infoHashHex); if (Std.isOfType(id, Torrent)) return id;
if (t == null) { if (id == null) return null;
t = torrents.get("v2:" + infoHashHex); var hex = Std.string(id);
if (t != null) infoHashHex = "v2:" + infoHashHex; if (StringTools.startsWith(hex, "v2:")) {
hex = hex.substr(3);
} }
if (t != null) { var t = torrents.get(hex);
t.destroy(); if (t == null) t = torrents.get("v2:" + hex);
if (deleteData) t.deleteData(); if (t == null) {
torrents.remove(infoHashHex); 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; if (urls == null) return;
for (u in urls) { for (u in urls) {
if (u != null && Lambda.indexOf(trackers, u) < 0) { if (u != null && Lambda.indexOf(trackers, u) < 0) {

View File

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

View File

@ -48,7 +48,7 @@ class WebTorrentTransport implements ITransport {
var blob:Dynamic = createBlob(identifier); var blob:Dynamic = createBlob(identifier);
var torrentOpts:Dynamic = { var torrentOpts:Dynamic = {
name: identifier, 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) { if (opts.torrentOpts != null) {
torrentOpts = js.lib.Object.assign( torrentOpts = js.lib.Object.assign(

View File

@ -43,6 +43,10 @@ class WireTransport implements ITransport {
if (opts.announce != null && opts.announce.length > 0) { if (opts.announce != null && opts.announce.length > 0) {
url = opts.announce[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; if (opts.iceServers != null) iceServers = opts.iceServers;
pk = opts.pk != null ? opts.pk : ""; pk = opts.pk != null ? opts.pk : "";
ek = opts.ek != null ? opts.ek : ""; ek = opts.ek != null ? opts.ek : "";

View File

@ -223,6 +223,9 @@ class LeenkxExporter:
cls.export_network = False cls.export_network = False
if wrd.lnx_network == 'Enabled': if wrd.lnx_network == 'Enabled':
cls.export_network = True cls.export_network = True
cls.export_torrent = False
if wrd.lnx_torrent == 'Enabled':
cls.export_torrent = True
@staticmethod @staticmethod
def write_matrix(matrix): 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""" """Create Leenkx"""
bl_idname = 'LNCreateLeenkxNode' bl_idname = 'LNCreateLeenkxNode'
bl_label = 'Create Leenkx' bl_label = 'Create Leenkx'
bl_description = 'Create Leenkx' bl_description = ('Open a Leenkx peer connection on the given swarm identifier')
lnx_category = 'Leenkx' lnx_category = 'Leenkx'
lnx_version = 1 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): def lnx_init(self, context):
self.add_input('LnxNodeSocketAction', 'In') 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('LnxNodeSocketAction', 'Out')
self.add_output('LnxDynamicSocket', 'Connection') self.add_output('LnxDynamicSocket', 'Connection')
self.add_output('LnxDynamicSocket', 'ID') 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(): def register():
add_category('Leenkx', icon='ORIENTATION_CURSOR') add_category('Leenkx', icon='ORIENTATION_CURSOR')

View File

@ -5,7 +5,7 @@ class CreateTorrentNode(LnxLogicTreeNode):
"""Create Torrent""" """Create Torrent"""
bl_idname = 'LNCreateTorrentNode' bl_idname = 'LNCreateTorrentNode'
bl_label = 'Create Torrent' 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_category = 'Leenkx'
lnx_version = 1 lnx_version = 1
@ -13,15 +13,65 @@ class CreateTorrentNode(LnxLogicTreeNode):
super(CreateTorrentNode, self).__init__(*args, **kwargs) super(CreateTorrentNode, self).__init__(*args, **kwargs)
array_nodes[str(id(self))] = self 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): def lnx_init(self, context):
self.add_input('LnxNodeSocketAction', 'In') self.add_input('LnxNodeSocketAction', 'In')
self.add_input('LnxDynamicSocket', 'Connection') self._add_data_inputs()
self.add_input('LnxDynamicSocket', 'Data')
self.add_output('LnxNodeSocketAction', 'Out') self.add_output('LnxNodeSocketAction', 'Out')
self.add_output('LnxDynamicSocket', 'Torrent') self.add_output('LnxDynamicSocket', 'Meta')
self.add_output('LnxNodeSocketAction', 'Done') 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(): 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""" """Close connection on the network"""
bl_idname = 'LNLeenkxCloseConnectionNode' bl_idname = 'LNLeenkxCloseConnectionNode'
bl_label = 'Close Leenkx Connection' bl_label = 'Close Leenkx Connection'
bl_description = ('Tear down a Leenkx connection, or drop a single peer from it')
lnx_version = 1 lnx_version = 1
lnx_category = 'Leenkx' lnx_category = 'Leenkx'
ind = 2
@staticmethod def update_sockets(self, context):
def get_enum_id_value(obj, prop_name, value): if not self.inputs:
return obj.bl_rna.properties[prop_name].enum_items[value].identifier return
links = {}
@staticmethod for socket in self.inputs:
def get_count_in(type_name): for link in socket.links:
return { links.setdefault(socket.name, []).append(
'client': 0, (link.from_node, link.from_socket))
'peer': 1 for socket in self.inputs[2:]:
}.get(type_name, 0) self.inputs.remove(socket)
if self.property0 == 'peer':
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.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)
self['property0'] = value property0: HaxeEnumProperty('property0',
property0: HaxeEnumProperty(
'property1',
items = [('client', 'Leenkx Client', 'Close the Leenkx Client connection on the network'), 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', name='', default='client',
set=set_enum, update=update_sockets)
get=get_enum)
def lnx_init(self, context): def lnx_init(self, context):
self.add_input('LnxNodeSocketAction', 'In') self.add_input('LnxNodeSocketAction', 'In')
self.add_input('LnxDynamicSocket', 'Connection') self.add_input('LnxDynamicSocket', 'Connection')
self.add_input('LnxStringSocket', 'Url') if self.property0 == 'peer':
self.add_input('LnxStringSocket', 'ID')
self.add_output('LnxNodeSocketAction', 'Out') self.add_output('LnxNodeSocketAction', 'Out')

View File

@ -9,84 +9,26 @@ class LeenkxEventNode(LnxLogicTreeNode):
lnx_category = 'Leenkx' lnx_category = 'Leenkx'
lnx_version = 1 lnx_version = 1
@staticmethod property1: HaxeEnumProperty('property1',
def get_enum_id_value(obj, prop_name, value): items = [('onopen', 'OnOpen', 'The connection to the server is open'),
return obj.bl_rna.properties[prop_name].enum_items[value].identifier ('onmessage', 'OnMessage', 'A message arrived; ID is the sender peer id, Data the payload'),
('onerror', 'OnError', 'A connection error occurred'),
@staticmethod ('onclose', 'OnClose', 'The connection was closed'),
def get_count_in(type_name): ('onseen', 'OnSeen', 'A peer was discovered; ID is the peer id'),
return { ('onserver', 'OnServer', 'The signaling server responded'),
'client': 0, ('onconnections', 'OnConnections', 'The server sent the peer list; Data is the connection list'),
'host': 1, ('onping', 'OnPing', 'Heartbeat ping roundtrip completed'),
'securehost': 2 ('onleft', 'OnLeft', 'A peer left; ID is the peer id'),
}.get(type_name, 0) ('ontimeout', 'OnTimeout', 'A handshake timed out'),
('onwireleft', 'OnWireLeft', 'A BitTorrent wire peer disconnected; ID is the peer id'),
def get_enum(self): ('onwireseen', 'OnWireSeen', 'A BitTorrent wire peer connected; ID is the peer id'),
return self.get('property0', 0) ('ontorrent', 'OnTorrent', 'Torrent activity on the connection; Data is the torrent'),
('ontorrentadded', 'OnTorrentAdded', 'A torrent registered on the connection; Data is the Torrent handle'),
def set_enum(self, value): ('ontracker', 'OnTracker', 'A tracker event on the connection'),
# Checking the selection of each type ('onannounce', 'OnAnnounce', 'A tracker announce completed'),
select_current = self.get_enum_id_value(self, 'property0', value) ('ontorrentdone', 'OnTorrentDone', 'A torrent finished downloading; Data is the torrent'),
select_prev = self.property0 ('onload', 'OnLoad', 'The connection was registered; fires once at startup')],
name='', default='onopen')
#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')
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(LeenkxEventNode, self).__init__(*args, **kwargs) super(LeenkxEventNode, self).__init__(*args, **kwargs)
@ -95,7 +37,7 @@ class LeenkxEventNode(LnxLogicTreeNode):
def lnx_init(self, context): def lnx_init(self, context):
#self.add_input('LnxNodeSocketAction', 'In') #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('LnxNodeSocketAction', 'Out')
self.add_output('LnxDynamicSocket', 'ID') self.add_output('LnxDynamicSocket', 'ID')
@ -103,7 +45,6 @@ class LeenkxEventNode(LnxLogicTreeNode):
def draw_buttons(self, context, layout): def draw_buttons(self, context, layout):
layout.prop(self, 'property0')
layout.prop(self, 'property1') layout.prop(self, 'property1')
def register(): def register():

View File

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

View File

@ -9,77 +9,55 @@ class LeenkxSendMessageNode(LnxLogicTreeNode):
lnx_category = 'Leenkx' lnx_category = 'Leenkx'
lnx_version = 1 lnx_version = 1
@staticmethod def update_sockets(self, context):
def get_enum_id_value(obj, prop_name, value): if not self.inputs:
return obj.bl_rna.properties[prop_name].enum_items[value].identifier 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 _add_data_inputs(self, send_all=True):
def get_count_in(type_name): data_sockets = {
return { 'string': ('LnxStringSocket', 'String payload to send'),
'client': 0, 'vector': ('LnxVectorSocket', 'Vector payload to send'),
'host': 1, 'float': ('LnxFloatSocket', 'Float payload to send'),
'securehost': 2 'integer': ('LnxIntSocket', 'Integer payload to send'),
}.get(type_name, 0) '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): def _add_id_input(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('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('LnxStringSocket', 'ID')
self.add_input('LnxBoolSocket', 'Send All')
# Arguements for type Host def on_socket_val_update(self, context, socket):
if (self.get_count_in(select_current) == 1): if socket.name == 'Send All':
self.add_input('LnxNodeSocketAction', 'In') if (len(self.inputs) > 5
self.add_input('LnxDynamicSocket', 'Connection') and self.inputs[5].name == 'ID'):
self.add_input('LnxStringSocket', 'API') self.inputs.remove(self.inputs[5])
self.add_input('LnxDynamicSocket', 'Data') if not socket.default_value_raw:
self.add_input('LnxStringSocket', 'ID') self._add_id_input()
self.add_input('LnxBoolSocket', 'Send All') super().on_socket_val_update(context, socket)
property1: HaxeEnumProperty('property1',
# 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',
items = [('string', 'String', 'Send a string over the network to one or all of the clients'), 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'), ('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'), ('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'), ('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'), ('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')], ('rotation', 'Rotation', 'Send a rotation over the network to one or all of the clients')],
name='', name='', default='string', update=update_sockets)
default='string')
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): def __init__(self, *args, **kwargs):
@ -100,17 +82,15 @@ class LeenkxSendMessageNode(LnxLogicTreeNode):
self.add_input('LnxNodeSocketAction', 'In') self.add_input('LnxNodeSocketAction', 'In')
self.add_input('LnxDynamicSocket', 'Connection') self.add_input('LnxDynamicSocket', 'Connection')
self.add_input('LnxStringSocket', 'API') self.add_input('LnxStringSocket', 'API')
self.add_input('LnxDynamicSocket', 'Data') self._add_data_inputs()
self.add_input('LnxStringSocket', 'ID')
self.add_input('LnxBoolSocket', 'Send All')
self.add_output('LnxNodeSocketAction', 'Out') self.add_output('LnxNodeSocketAction', 'Out')
def draw_buttons(self, context, layout): def draw_buttons(self, context, layout):
layout.prop(self, 'property0')
layout.prop(self, 'property1') layout.prop(self, 'property1')
layout.prop(self, 'property2')
def register(): def register():
add_category('Leenkx', icon='ORIENTATION_CURSOR') 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): def lnx_init(self, context):
self.add_input('LnxNodeSocketAction', 'In') self.add_input('LnxNodeSocketAction', 'In')
self.add_input('LnxDynamicSocket', 'Connection') self.add_input('LnxDynamicSocket', 'Connection')
self.add_input('LnxDynamicSocket', 'Opts [Map]') self.add_input('LnxDynamicSocket', 'Torrent')
self.add_input('LnxDynamicSocket', 'Data') self.add_input('LnxDynamicSocket', 'Data')
self.add_output('LnxNodeSocketAction', 'Out') self.add_output('LnxNodeSocketAction', 'Out')
self.add_output('LnxDynamicSocket', 'Torrent') self.add_output('LnxDynamicSocket', 'Torrent')
self.add_output('LnxNodeSocketAction', 'Done') self.add_output('LnxNodeSocketAction', 'Done')
self.add_output('LnxStringSocket', 'InfoHash')
self.add_output('LnxStringSocket', 'Magnet')
def register(): 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_navigation = bpy.data.worlds['Lnx'].lnx_navigation != 'Disabled'
export_ui = bpy.data.worlds['Lnx'].lnx_ui != 'Disabled' export_ui = bpy.data.worlds['Lnx'].lnx_ui != 'Disabled'
export_network = bpy.data.worlds['Lnx'].lnx_network != 'Disabled' export_network = bpy.data.worlds['Lnx'].lnx_network != 'Disabled'
export_torrent = bpy.data.worlds['Lnx'].lnx_torrent != 'Disabled'
assets.reset() assets.reset()
mat_state.material_cache.clear() mat_state.material_cache.clear()
@ -264,6 +265,7 @@ def export_data_impl(fp, sdk_path):
navigation_found = False navigation_found = False
ui_found = False ui_found = False
network_found = False network_found = False
torrent_found = False
LeenkxExporter.compress_enabled = wrd.lnx_asset_compression LeenkxExporter.compress_enabled = wrd.lnx_asset_compression
LeenkxExporter.optimize_enabled = state.is_publish and wrd.lnx_optimize_data LeenkxExporter.optimize_enabled = state.is_publish and wrd.lnx_optimize_data
if not os.path.exists(build_dir + '/compiled/Assets'): if not os.path.exists(build_dir + '/compiled/Assets'):
@ -326,6 +328,8 @@ def export_data_impl(fp, sdk_path):
ui_found = True ui_found = True
if LeenkxExporter.export_network: if LeenkxExporter.export_network:
network_found = True network_found = True
if LeenkxExporter.export_torrent:
torrent_found = True
assets.add(asset_path) assets.add(asset_path)
if physics_found is False: # Disable physics if no rigid body is exported 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: if network_found == False:
export_network = 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, # Ugly workaround: some logic nodes require Zui code even if no UI is used,
# for now enable UI export unless explicitly disabled. # for now enable UI export unless explicitly disabled.
export_ui = True export_ui = True
@ -349,6 +356,9 @@ def export_data_impl(fp, sdk_path):
if wrd.lnx_network == 'Enabled': if wrd.lnx_network == 'Enabled':
export_network = True export_network = True
if wrd.lnx_torrent == 'Enabled':
export_torrent = True
modules = [] modules = []
if wrd.lnx_audio == 'Enabled': if wrd.lnx_audio == 'Enabled':
modules.append('audio') modules.append('audio')
@ -360,6 +370,8 @@ def export_data_impl(fp, sdk_path):
modules.append('ui') modules.append('ui')
if export_network: if export_network:
modules.append('network') modules.append('network')
if export_torrent:
modules.append('torrent')
defs = lnx.utils.def_strings_to_array(wrd.world_defs) defs = lnx.utils.def_strings_to_array(wrd.world_defs)
cdefs = lnx.utils.def_strings_to_array(wrd.compo_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) wrd.lnx_project_version = lnx.utils.change_version_project(wrd.lnx_project_version)
# Write khafile.js # 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 # Write Main.hx - depends on write_khafilejs for writing number of assets
scene_name = lnx.utils.get_project_scene_name() scene_name = lnx.utils.get_project_scene_name()

View File

@ -34,6 +34,16 @@ else:
lnx_version = '2026.9' lnx_version = '2026.9'
lnx_commit = '$Id: 6b2644d47db169cedd95593497cc283207d23a74 $' 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): def get_project_html5_copy(self):
return self.get('lnx_project_html5_copy', False) return self.get('lnx_project_html5_copy', False)
@ -282,6 +292,17 @@ def init_properties():
('Enabled', 'Enabled', 'Enabled'), ('Enabled', 'Enabled', 'Enabled'),
('Auto', 'Auto', 'Auto')], ('Auto', 'Auto', 'Auto')],
name="Networking", default='Auto', description="Include Network library", update=assets.invalidate_compiler_cache) 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( bpy.types.World.lnx_audio = EnumProperty(
items=[('Disabled', 'Disabled', 'Disabled'), items=[('Disabled', 'Disabled', 'Disabled'),
('Enabled', 'Enabled', 'Enabled')], ('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.use_fake_user = True # Store data world object, add fake user to keep it alive
wrd.lnx_version = lnx_version wrd.lnx_version = lnx_version
wrd.lnx_commit = lnx_commit 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(): def init_properties_on_load():
if 'Lnx' not in bpy.data.worlds: if 'Lnx' not in bpy.data.worlds:
init_properties() init_properties()
wrd = bpy.data.worlds['Lnx']
seed_torrent_trackers(wrd)
# New project? # New project?
if bpy.data.filepath == '': if bpy.data.filepath == '':
wrd = bpy.data.worlds['Lnx']
wrd.lnx_debug_console = lnx.utils.get_debug_console_auto() wrd.lnx_debug_console = lnx.utils.get_debug_console_auto()
lnx.utils.fetch_script_names() 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_navigation_engine')
layout.prop(wrd, 'lnx_ui') layout.prop(wrd, 'lnx_ui')
layout.prop(wrd, 'lnx_network') layout.prop(wrd, 'lnx_network')
layout.prop(wrd, 'lnx_torrent')
layout.prop_search(wrd, 'lnx_khafile', bpy.data, 'texts') layout.prop_search(wrd, 'lnx_khafile', bpy.data, 'texts')
layout.prop(wrd, 'lnx_project_root') layout.prop(wrd, 'lnx_project_root')
layout.prop(wrd, 'lnx_external_blends_path') 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): class LnxVirtualInputPanel(bpy.types.Panel):
bl_label = "Leenkx Virtual Input" bl_label = "Leenkx Virtual Input"
bl_space_type = "PROPERTIES" bl_space_type = "PROPERTIES"
@ -3107,6 +3246,15 @@ __REG_CLASSES = (
LNX_PT_ProjectFlagsDebugConsolePanel, LNX_PT_ProjectFlagsDebugConsolePanel,
LNX_PT_ProjectWindowPanel, LNX_PT_ProjectWindowPanel,
LNX_PT_ProjectModulesPanel, LNX_PT_ProjectModulesPanel,
LNX_UL_TorrentTrackerList,
LnxTorrentTrackerListNewItem,
LnxTorrentTrackerListDeleteItem,
LnxTorrentTrackerListReset,
LNX_UL_TorrentIceServerList,
LnxTorrentIceServerListNewItem,
LnxTorrentIceServerListDeleteItem,
LnxTorrentIceServerListReset,
LNX_PT_LeenkxNetworkPanel,
LNX_PT_RenderPathPanel, LNX_PT_RenderPathPanel,
LNX_PT_RenderPathRendererPanel, LNX_PT_RenderPathRendererPanel,
LNX_PT_RenderPathShadowsPanel, LNX_PT_RenderPathShadowsPanel,

View File

@ -158,7 +158,7 @@ def remove_readonly(func, path, excinfo):
func(path) 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: import_traits: List[str]) -> None:
wrd = bpy.data.worlds['Lnx'] wrd = bpy.data.worlds['Lnx']
rpdat = lnx.utils.get_rp() 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'): if not os.path.exists('Libraries/network'):
khafile.write(add_leenkx_library(sdk_path, 'lib/network', rel_path=do_relpath_sdk)) khafile.write(add_leenkx_library(sdk_path, 'lib/network', rel_path=do_relpath_sdk))
assets.add_khafile_def('lnx_network') assets.add_khafile_def('lnx_network')
lnxjs_path = sdk_path + '/lib/leenkx_tools/lnxjs/Leenkx.js' #lnxjs_path = sdk_path + '/lib/leenkx_tools/lnxjs/Leenkx.js'
lnxjs_path = lnxjs_path.replace('\\', '/').replace('//', '/') #lnxjs_path = lnxjs_path.replace('\\', '/').replace('//', '/')
khafile.write(add_assets(lnxjs_path, rel_path=do_relpath_sdk)) #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: if not wrd.lnx_minimize:
assets.add_khafile_def('lnx_json') assets.add_khafile_def('lnx_json')
@ -736,6 +743,19 @@ class Main {
elif wrd.lnx_canvas_img_scaling_quality == 'high': elif wrd.lnx_canvas_img_scaling_quality == 'high':
f.write("\n" + content_indent + "leenkx.ui.Canvas.imageScaleQuality = kha.graphics2.ImageScaleQuality.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 # Write Starter.main call
starter_indent = content_indent + " " starter_indent = content_indent + " "
f.write("\n" + content_indent + "leenkx.system.Starter.main(") f.write("\n" + content_indent + "leenkx.system.Starter.main(")