fix enet; add enet testing
Some checks failed
Build and Deploy / build-windows (CLANG64) (push) Has been cancelled
Build and Deploy / package-dist (push) Has been cancelled
Build and Deploy / deploy-itch (push) Has been cancelled
Build and Deploy / deploy-gitea (push) Has been cancelled
Build and Deploy / build-linux (push) Has been cancelled
Some checks failed
Build and Deploy / build-windows (CLANG64) (push) Has been cancelled
Build and Deploy / package-dist (push) Has been cancelled
Build and Deploy / deploy-itch (push) Has been cancelled
Build and Deploy / deploy-gitea (push) Has been cancelled
Build and Deploy / build-linux (push) Has been cancelled
This commit is contained in:
@@ -230,7 +230,8 @@ copy_tests = custom_target(
|
||||
tests = [
|
||||
'spawn_actor',
|
||||
'empty',
|
||||
'nota'
|
||||
'nota',
|
||||
'enet'
|
||||
]
|
||||
|
||||
foreach file : tests
|
||||
|
||||
151
scripts/modules/enet.js
Normal file
151
scripts/modules/enet.js
Normal file
@@ -0,0 +1,151 @@
|
||||
// enet.js doc (updated)
|
||||
var enet = this;
|
||||
|
||||
//------------------------------------------------
|
||||
// Top-level ENet functions
|
||||
//------------------------------------------------
|
||||
|
||||
enet.initialize[prosperon.DOC] = `
|
||||
Initialize the ENet library. Must be called before using any ENet functionality.
|
||||
Throws an error if initialization fails.
|
||||
|
||||
:return: None
|
||||
`;
|
||||
|
||||
enet.deinitialize[prosperon.DOC] = `
|
||||
Deinitialize the ENet library, cleaning up all resources. Call this when you no longer
|
||||
need any ENet functionality.
|
||||
|
||||
:return: None
|
||||
`;
|
||||
|
||||
enet.create_host[prosperon.DOC] = `
|
||||
Create an ENet host for either a client-like unbound host or a server bound to a specific
|
||||
address and port:
|
||||
|
||||
- If no argument is provided, creates an unbound "client-like" host with default settings
|
||||
(maximum 32 peers, 2 channels, unlimited bandwidth).
|
||||
- If you pass an "ip:port" string (e.g. "127.0.0.1:7777"), it creates a server bound to
|
||||
that address. The server supports up to 32 peers, 2 channels, and unlimited bandwidth.
|
||||
|
||||
Throws an error if host creation fails for any reason.
|
||||
|
||||
:param address: (optional) A string in 'ip:port' format to bind the host (server), or
|
||||
omit to create an unbound client-like host.
|
||||
:return: An ENetHost object.
|
||||
`;
|
||||
|
||||
//------------------------------------------------
|
||||
// ENetHost methods
|
||||
//------------------------------------------------
|
||||
|
||||
var enet_host = prosperon.c_types.enet_host;
|
||||
|
||||
enet_host.service[prosperon.DOC] = `
|
||||
Poll for and process any available network events (connect, receive, disconnect, or none)
|
||||
from this host, calling the provided callback for each event. This function loops until
|
||||
no more events are available in the current timeframe.
|
||||
|
||||
Event object properties:
|
||||
- type: String, one of "connect", "receive", "disconnect", or "none".
|
||||
- peer: (present if type = "connect") The ENetPeer object for the new connection.
|
||||
- channelID: (present if type = "receive") The channel on which the data was received.
|
||||
- data: (present if type = "receive") The received data as a *plain JavaScript object*.
|
||||
If the JSON parse fails or the data isn't an object, a JavaScript error is thrown.
|
||||
|
||||
:param callback: A function called once for each available event, receiving an event
|
||||
object as its single argument.
|
||||
:param timeout: (optional) Timeout in milliseconds. Defaults to 0 (non-blocking).
|
||||
:return: None
|
||||
`;
|
||||
|
||||
enet_host.connect[prosperon.DOC] = `
|
||||
Initiate a connection from this host to a remote server. Throws an error if the
|
||||
connection cannot be started.
|
||||
|
||||
:param host: The hostname or IP address of the remote server (e.g. "example.com" or "127.0.0.1").
|
||||
:param port: The port number to connect to.
|
||||
:return: An ENetPeer object representing the connection.
|
||||
`;
|
||||
|
||||
enet_host.flush[prosperon.DOC] = `
|
||||
Flush all pending outgoing packets for this host immediately.
|
||||
|
||||
:return: None
|
||||
`;
|
||||
|
||||
enet_host.broadcast[prosperon.DOC] = `
|
||||
Broadcast a JavaScript object to all connected peers on channel 0. The object is
|
||||
serialized to JSON, and the packet is sent reliably. Throws an error if serialization fails.
|
||||
|
||||
:param data: A JavaScript object to broadcast to all peers.
|
||||
:return: None
|
||||
`;
|
||||
|
||||
//------------------------------------------------
|
||||
// ENetPeer methods
|
||||
//------------------------------------------------
|
||||
|
||||
var enet_peer = prosperon.c_types.enet_peer;
|
||||
|
||||
enet_peer.send[prosperon.DOC] = `
|
||||
Send a JavaScript object to this peer on channel 0. The object is serialized to JSON and
|
||||
sent reliably. Throws an error if serialization fails.
|
||||
|
||||
:param data: A JavaScript object to send.
|
||||
:return: None
|
||||
`;
|
||||
|
||||
enet_peer.disconnect[prosperon.DOC] = `
|
||||
Request a graceful disconnection from this peer. The connection will close after
|
||||
pending data is sent.
|
||||
|
||||
:return: None
|
||||
`;
|
||||
|
||||
enet_peer.disconnect_now[prosperon.DOC] = `
|
||||
Immediately terminate the connection to this peer, discarding any pending data.
|
||||
|
||||
:return: None
|
||||
`;
|
||||
|
||||
enet_peer.disconnect_later[prosperon.DOC] = `
|
||||
Request a disconnection from this peer after all queued packets are sent.
|
||||
|
||||
:return: None
|
||||
`;
|
||||
|
||||
enet_peer.reset[prosperon.DOC] = `
|
||||
Reset this peer's connection, immediately dropping it and clearing its internal state.
|
||||
|
||||
:return: None
|
||||
`;
|
||||
|
||||
enet_peer.ping[prosperon.DOC] = `
|
||||
Send a ping request to this peer to measure latency.
|
||||
|
||||
:return: None
|
||||
`;
|
||||
|
||||
enet_peer.throttle_configure[prosperon.DOC] = `
|
||||
Configure the throttling behavior for this peer, controlling how ENet adjusts its sending
|
||||
rate based on packet loss or congestion.
|
||||
|
||||
:param interval: The interval (ms) between throttle adjustments.
|
||||
:param acceleration: The factor to increase sending speed when conditions improve.
|
||||
:param deceleration: The factor to decrease sending speed when conditions worsen.
|
||||
:return: None
|
||||
`;
|
||||
|
||||
enet_peer.timeout[prosperon.DOC] = `
|
||||
Set timeout parameters for this peer, determining how long ENet waits before considering
|
||||
the connection lost.
|
||||
|
||||
:param timeout_limit: The total time (ms) before the peer is disconnected.
|
||||
:param timeout_min: The minimum timeout (ms) used for each timeout attempt.
|
||||
:param timeout_max: The maximum timeout (ms) used for each timeout attempt.
|
||||
:return: None
|
||||
`;
|
||||
|
||||
// Return the enet object.
|
||||
return enet;
|
||||
@@ -1,4 +1,4 @@
|
||||
// enet_qjs.c
|
||||
// qjs_enet.c
|
||||
#include "quickjs.h"
|
||||
#include <enet/enet.h>
|
||||
#include <stdio.h>
|
||||
@@ -9,6 +9,7 @@
|
||||
static JSClassID enet_host_id;
|
||||
static JSClassID enet_peer_class_id;
|
||||
|
||||
/* Finalizers */
|
||||
static void js_enet_host_finalizer(JSRuntime *rt, JSValue val) {
|
||||
ENetHost *host = JS_GetOpaque(val, enet_host_id);
|
||||
if (host) {
|
||||
@@ -18,95 +19,111 @@ static void js_enet_host_finalizer(JSRuntime *rt, JSValue val) {
|
||||
|
||||
static void js_enet_peer_finalizer(JSRuntime *rt, JSValue val) {
|
||||
ENetPeer *peer = JS_GetOpaque(val, enet_peer_class_id);
|
||||
if (peer) {
|
||||
// No explicit cleanup needed for ENetPeer
|
||||
}
|
||||
// No explicit cleanup needed for ENetPeer itself
|
||||
(void)peer;
|
||||
}
|
||||
|
||||
static JSValue js_enet_initialize(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) {
|
||||
/* ENet init/deinit */
|
||||
static JSValue js_enet_initialize(JSContext *ctx, JSValueConst this_val,
|
||||
int argc, JSValueConst *argv) {
|
||||
if (enet_initialize() != 0) {
|
||||
return JS_ThrowInternalError(ctx, "An error occurred while initializing ENet.");
|
||||
return JS_ThrowInternalError(ctx, "Error initializing ENet.");
|
||||
}
|
||||
return JS_UNDEFINED;
|
||||
}
|
||||
|
||||
static JSValue js_enet_deinitialize(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) {
|
||||
static JSValue js_enet_deinitialize(JSContext *ctx, JSValueConst this_val,
|
||||
int argc, JSValueConst *argv) {
|
||||
enet_deinitialize();
|
||||
return JS_UNDEFINED;
|
||||
}
|
||||
|
||||
static JSValue js_enet_host_create(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) {
|
||||
/* Host creation */
|
||||
static JSValue js_enet_host_create(JSContext *ctx, JSValueConst this_val,
|
||||
int argc, JSValueConst *argv) {
|
||||
ENetHost *host;
|
||||
ENetAddress address;
|
||||
JSValue obj;
|
||||
|
||||
if (argc < 1) {
|
||||
// Create client-like host, unbound
|
||||
host = enet_host_create(NULL, 32, 2, 0, 0);
|
||||
if (!host) {
|
||||
return JS_ThrowInternalError(ctx, "Failed to create ENet host (null address).");
|
||||
}
|
||||
goto RET;
|
||||
}
|
||||
|
||||
// If arg is provided, interpret as "ip:port" for server
|
||||
const char *address_str = JS_ToCString(ctx, argv[0]);
|
||||
if (!address_str)
|
||||
return JS_EXCEPTION;
|
||||
|
||||
if (!address_str) {
|
||||
return JS_EXCEPTION; // memory or conversion error
|
||||
}
|
||||
char ip[64];
|
||||
int port;
|
||||
|
||||
if (sscanf(address_str, "%63[^:]:%d", ip, &port) != 2) {
|
||||
JS_FreeCString(ctx, address_str);
|
||||
return JS_ThrowTypeError(ctx, "Invalid address format. Expected format: 'ip:port'");
|
||||
return JS_ThrowTypeError(ctx, "Invalid address format. Expected 'ip:port'.");
|
||||
}
|
||||
|
||||
JS_FreeCString(ctx, address_str);
|
||||
|
||||
int err;
|
||||
if ((err = enet_address_set_host_ip(&address, ip)) != 0) {
|
||||
int err = enet_address_set_host_ip(&address, ip);
|
||||
if (err != 0) {
|
||||
return JS_ThrowInternalError(ctx, "Failed to set host IP from %s. Error %d.", ip, err);
|
||||
}
|
||||
address.port = port;
|
||||
|
||||
host = enet_host_create(&address, 32, 2, 0, 0); // server host with max 32 clients
|
||||
// Create server host with max 32 clients, 2 channels
|
||||
host = enet_host_create(&address, 32, 2, 0, 0);
|
||||
if (!host) {
|
||||
return JS_ThrowInternalError(ctx, "Failed to create ENet host.");
|
||||
}
|
||||
|
||||
RET:
|
||||
RET:
|
||||
obj = JS_NewObjectClass(ctx, enet_host_id);
|
||||
if (JS_IsException(obj)) {
|
||||
enet_host_destroy(host);
|
||||
return obj;
|
||||
}
|
||||
|
||||
JS_SetOpaque(obj, host);
|
||||
return obj;
|
||||
}
|
||||
|
||||
static JSValue js_enet_host_service(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) {
|
||||
/* Host service: poll for events */
|
||||
static JSValue js_enet_host_service(JSContext *ctx, JSValueConst this_val,
|
||||
int argc, JSValueConst *argv) {
|
||||
ENetHost *host = JS_GetOpaque(this_val, enet_host_id);
|
||||
if (!host)
|
||||
if (!host) {
|
||||
return JS_EXCEPTION;
|
||||
|
||||
if (argc < 1 || !JS_IsFunction(ctx, argv[0])) {
|
||||
return JS_ThrowTypeError(ctx, "Expected a callback function as the first argument");
|
||||
}
|
||||
|
||||
// Expect a callback function
|
||||
if (argc < 1 || !JS_IsFunction(ctx, argv[0])) {
|
||||
return JS_ThrowTypeError(ctx, "Expected a callback function as first argument.");
|
||||
}
|
||||
JSValue callback = argv[0];
|
||||
JS_DupValue(ctx, callback);
|
||||
|
||||
ENetEvent event;
|
||||
int timeout = 1000; // 1 second timeout by default
|
||||
|
||||
// Optional timeout
|
||||
int timeout = 0;
|
||||
if (argc > 1) {
|
||||
JS_ToInt32(ctx, &timeout, argv[1]);
|
||||
}
|
||||
|
||||
ENetEvent event;
|
||||
while (enet_host_service(host, &event, timeout) > 0) {
|
||||
JSValue event_obj = JS_NewObject(ctx);
|
||||
|
||||
switch (event.type) {
|
||||
case ENET_EVENT_TYPE_CONNECT: {
|
||||
JS_SetPropertyStr(ctx, event_obj, "type", JS_NewString(ctx, "connect"));
|
||||
JSValue peer_obj = JS_NewObjectClass(ctx, enet_peer_class_id);
|
||||
if (JS_IsException(peer_obj))
|
||||
if (JS_IsException(peer_obj)) {
|
||||
JS_FreeValue(ctx, event_obj);
|
||||
JS_FreeValue(ctx, callback);
|
||||
return peer_obj;
|
||||
}
|
||||
JS_SetOpaque(peer_obj, event.peer);
|
||||
JS_SetPropertyStr(ctx, event_obj, "peer", peer_obj);
|
||||
break;
|
||||
@@ -114,19 +131,50 @@ static JSValue js_enet_host_service(JSContext *ctx, JSValueConst this_val, int a
|
||||
case ENET_EVENT_TYPE_RECEIVE: {
|
||||
JS_SetPropertyStr(ctx, event_obj, "type", JS_NewString(ctx, "receive"));
|
||||
JS_SetPropertyStr(ctx, event_obj, "channelID", JS_NewInt32(ctx, event.channelID));
|
||||
JSValue packet_data = JS_ParseJSON(ctx, (const char *)event.packet->data, event.packet->dataLength, "packet");
|
||||
char *tmp = js_mallocz(ctx, event.packet->dataLength+1);
|
||||
memcpy(tmp, event.packet->data, event.packet->dataLength);
|
||||
tmp[event.packet->dataLength] = '\0';
|
||||
|
||||
// We expect strictly a JSON object
|
||||
JSValue packet_data = JS_ParseJSON(ctx,
|
||||
tmp,
|
||||
event.packet->dataLength,
|
||||
"<enet-packet>");
|
||||
|
||||
js_free(ctx,tmp);
|
||||
|
||||
if (JS_IsException(packet_data)) {
|
||||
packet_data = JS_NULL;
|
||||
// Malformed JSON -> throw error, abort
|
||||
printf("INVALID JSON!\n");
|
||||
enet_packet_destroy(event.packet);
|
||||
JS_FreeValue(ctx, event_obj);
|
||||
JS_FreeValue(ctx, callback);
|
||||
return JS_ThrowTypeError(ctx, "Received invalid JSON (parse error).");
|
||||
}
|
||||
|
||||
if (!JS_IsObject(packet_data)) {
|
||||
// It might be a string/number/array/... -> we want only a plain object
|
||||
JS_FreeValue(ctx, event_obj);
|
||||
JS_FreeValue(ctx, callback);
|
||||
JS_FreeValue(ctx, packet_data);
|
||||
enet_packet_destroy(event.packet);
|
||||
return JS_ThrowTypeError(ctx,
|
||||
"Received data is not an object (must send a plain object).");
|
||||
}
|
||||
|
||||
JS_SetPropertyStr(ctx, event_obj, "data", packet_data);
|
||||
enet_packet_destroy(event.packet);
|
||||
break;
|
||||
}
|
||||
case ENET_EVENT_TYPE_DISCONNECT: {
|
||||
case ENET_EVENT_TYPE_DISCONNECT:
|
||||
JS_SetPropertyStr(ctx, event_obj, "type", JS_NewString(ctx, "disconnect"));
|
||||
break;
|
||||
case ENET_EVENT_TYPE_NONE:
|
||||
JS_SetPropertyStr(ctx, event_obj, "type", JS_NewString(ctx, "none"));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Invoke callback
|
||||
JS_Call(ctx, callback, JS_UNDEFINED, 1, &event_obj);
|
||||
JS_FreeValue(ctx, event_obj);
|
||||
}
|
||||
@@ -135,161 +183,230 @@ static JSValue js_enet_host_service(JSContext *ctx, JSValueConst this_val, int a
|
||||
return JS_UNDEFINED;
|
||||
}
|
||||
|
||||
static JSValue js_enet_host_connect(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) {
|
||||
/* Host connect: client -> connect to server */
|
||||
static JSValue js_enet_host_connect(JSContext *ctx, JSValueConst this_val,
|
||||
int argc, JSValueConst *argv) {
|
||||
ENetHost *host = JS_GetOpaque(this_val, enet_host_id);
|
||||
if (!host)
|
||||
if (!host) {
|
||||
return JS_EXCEPTION;
|
||||
}
|
||||
|
||||
if (argc < 2)
|
||||
return JS_ThrowTypeError(ctx, "Expected at least 2 arguments (host, port)");
|
||||
if (argc < 2) {
|
||||
return JS_ThrowTypeError(ctx, "Expected 2 arguments: hostname, port.");
|
||||
}
|
||||
|
||||
const char *host_name = JS_ToCString(ctx, argv[0]);
|
||||
const char *hostname = JS_ToCString(ctx, argv[0]);
|
||||
if (!hostname) {
|
||||
return JS_EXCEPTION; // out of memory or conversion error
|
||||
}
|
||||
int port;
|
||||
JS_ToInt32(ctx, &port, argv[1]);
|
||||
|
||||
ENetAddress address;
|
||||
enet_address_set_host(&address, host_name);
|
||||
enet_address_set_host(&address, hostname);
|
||||
JS_FreeCString(ctx, hostname);
|
||||
address.port = port;
|
||||
JS_FreeCString(ctx, host_name);
|
||||
|
||||
ENetPeer *peer = enet_host_connect(host, &address, 2, 0);
|
||||
if (!peer)
|
||||
if (!peer) {
|
||||
return JS_ThrowInternalError(ctx, "Failed to initiate connection.");
|
||||
}
|
||||
|
||||
JSValue peer_obj = JS_NewObjectClass(ctx, enet_peer_class_id);
|
||||
if (JS_IsException(peer_obj))
|
||||
if (JS_IsException(peer_obj)) {
|
||||
return peer_obj;
|
||||
}
|
||||
JS_SetOpaque(peer_obj, peer);
|
||||
return peer_obj;
|
||||
}
|
||||
|
||||
static JSValue js_enet_host_flush(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) {
|
||||
/* Flush queued packets */
|
||||
static JSValue js_enet_host_flush(JSContext *ctx, JSValueConst this_val,
|
||||
int argc, JSValueConst *argv) {
|
||||
ENetHost *host = JS_GetOpaque(this_val, enet_host_id);
|
||||
if (!host)
|
||||
if (!host) {
|
||||
return JS_EXCEPTION;
|
||||
|
||||
}
|
||||
enet_host_flush(host);
|
||||
return JS_UNDEFINED;
|
||||
}
|
||||
|
||||
static JSValue js_enet_host_broadcast(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) {
|
||||
/* Broadcast a plain object */
|
||||
static JSValue js_enet_host_broadcast(JSContext *ctx, JSValueConst this_val,
|
||||
int argc, JSValueConst *argv) {
|
||||
ENetHost *host = JS_GetOpaque(this_val, enet_host_id);
|
||||
if (!host)
|
||||
if (!host) {
|
||||
return JS_EXCEPTION;
|
||||
}
|
||||
|
||||
if (argc < 1)
|
||||
return JS_ThrowTypeError(ctx, "Expected at least 1 argument (data)");
|
||||
if (argc < 1) {
|
||||
return JS_ThrowTypeError(ctx, "Expected an object to broadcast.");
|
||||
}
|
||||
// Must be a JavaScript object
|
||||
if (!JS_IsObject(argv[0])) {
|
||||
return JS_ThrowTypeError(ctx,
|
||||
"broadcast() only accepts a plain JS object, not strings/numbers.");
|
||||
}
|
||||
|
||||
// JSON.stringify the object
|
||||
JSValue json_data = JS_JSONStringify(ctx, argv[0], JS_NULL, JS_NULL);
|
||||
if (JS_IsException(json_data)) {
|
||||
return JS_ThrowTypeError(ctx,
|
||||
"Failed to stringify object (circular ref or non-serializable).");
|
||||
}
|
||||
|
||||
size_t data_len;
|
||||
const char *data = JS_ToCStringLen(ctx, &data_len, argv[0]);
|
||||
if (!data)
|
||||
return JS_EXCEPTION;
|
||||
const char *data_str = JS_ToCStringLen(ctx, &data_len, json_data);
|
||||
JS_FreeValue(ctx, json_data);
|
||||
|
||||
ENetPacket *packet = enet_packet_create(data, data_len, ENET_PACKET_FLAG_RELIABLE);
|
||||
JS_FreeCString(ctx, data);
|
||||
if (!data_str) {
|
||||
return JS_EXCEPTION; // out of memory
|
||||
}
|
||||
|
||||
ENetPacket *packet = enet_packet_create(data_str, data_len, ENET_PACKET_FLAG_RELIABLE);
|
||||
JS_FreeCString(ctx, data_str);
|
||||
|
||||
if (!packet) {
|
||||
return JS_ThrowInternalError(ctx, "Failed to create ENet packet.");
|
||||
}
|
||||
enet_host_broadcast(host, 0, packet);
|
||||
return JS_UNDEFINED;
|
||||
}
|
||||
|
||||
static JSValue js_enet_peer_disconnect(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) {
|
||||
/* Peer-level operations */
|
||||
static JSValue js_enet_peer_disconnect(JSContext *ctx, JSValueConst this_val,
|
||||
int argc, JSValueConst *argv) {
|
||||
ENetPeer *peer = JS_GetOpaque(this_val, enet_peer_class_id);
|
||||
if (!peer)
|
||||
if (!peer) {
|
||||
return JS_EXCEPTION;
|
||||
|
||||
}
|
||||
enet_peer_disconnect(peer, 0);
|
||||
return JS_UNDEFINED;
|
||||
}
|
||||
|
||||
static JSValue js_enet_peer_send(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) {
|
||||
/* Peer send must only accept an object */
|
||||
static JSValue js_enet_peer_send(JSContext *ctx, JSValueConst this_val,
|
||||
int argc, JSValueConst *argv) {
|
||||
ENetPeer *peer = JS_GetOpaque(this_val, enet_peer_class_id);
|
||||
if (!peer)
|
||||
if (!peer) {
|
||||
return JS_EXCEPTION;
|
||||
|
||||
if (argc < 1 || !JS_IsObject(argv[0]))
|
||||
return JS_ThrowTypeError(ctx, "Expected at least 1 argument (object)");
|
||||
|
||||
JSValue json_data = JS_JSONStringify(ctx, argv[0], JS_NULL, JS_NULL);
|
||||
if (JS_IsException(json_data))
|
||||
return JS_EXCEPTION;
|
||||
|
||||
const char *data = JS_ToCString(ctx, json_data);
|
||||
if (!data)
|
||||
return JS_EXCEPTION;
|
||||
|
||||
ENetPacket *packet = enet_packet_create(data, strlen(data) + 1, ENET_PACKET_FLAG_RELIABLE);
|
||||
JS_FreeCString(ctx, data);
|
||||
JS_FreeValue(ctx, json_data);
|
||||
|
||||
if (enet_peer_send(peer, 0, packet) < 0) {
|
||||
return JS_ThrowInternalError(ctx, "Failed to send packet.");
|
||||
}
|
||||
|
||||
if (argc < 1) {
|
||||
return JS_ThrowTypeError(ctx, "Expected an object to send.");
|
||||
}
|
||||
if (!JS_IsObject(argv[0])) {
|
||||
return JS_ThrowTypeError(ctx,
|
||||
"peer.send() only accepts a plain JS object, not strings/numbers.");
|
||||
}
|
||||
|
||||
JSValue json_data = JS_JSONStringify(ctx, argv[0], JS_NULL, JS_NULL);
|
||||
if (JS_IsException(json_data)) {
|
||||
return JS_ThrowTypeError(ctx,
|
||||
"Failed to stringify object (circular ref or non-serializable).");
|
||||
}
|
||||
|
||||
size_t data_len;
|
||||
const char *data_str = JS_ToCStringLen(ctx, &data_len, json_data);
|
||||
JS_FreeValue(ctx, json_data);
|
||||
if (!data_str) {
|
||||
return JS_EXCEPTION;
|
||||
}
|
||||
|
||||
// Create packet
|
||||
ENetPacket *packet = enet_packet_create(data_str, data_len, ENET_PACKET_FLAG_RELIABLE);
|
||||
JS_FreeCString(ctx, data_str);
|
||||
if (!packet) {
|
||||
return JS_ThrowInternalError(ctx, "Failed to create ENet packet.");
|
||||
}
|
||||
|
||||
if (enet_peer_send(peer, 0, packet) < 0) {
|
||||
return JS_ThrowInternalError(ctx, "enet_peer_send returned error.");
|
||||
}
|
||||
return JS_UNDEFINED;
|
||||
}
|
||||
|
||||
static JSValue js_enet_peer_disconnect_now(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) {
|
||||
static JSValue js_enet_peer_disconnect_now(JSContext *ctx, JSValueConst this_val,
|
||||
int argc, JSValueConst *argv) {
|
||||
ENetPeer *peer = JS_GetOpaque(this_val, enet_peer_class_id);
|
||||
if (!peer)
|
||||
if (!peer) {
|
||||
return JS_EXCEPTION;
|
||||
|
||||
}
|
||||
enet_peer_disconnect_now(peer, 0);
|
||||
return JS_UNDEFINED;
|
||||
}
|
||||
|
||||
static JSValue js_enet_peer_disconnect_later(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) {
|
||||
static JSValue js_enet_peer_disconnect_later(JSContext *ctx, JSValueConst this_val,
|
||||
int argc, JSValueConst *argv) {
|
||||
ENetPeer *peer = JS_GetOpaque(this_val, enet_peer_class_id);
|
||||
if (!peer)
|
||||
if (!peer) {
|
||||
return JS_EXCEPTION;
|
||||
|
||||
}
|
||||
enet_peer_disconnect_later(peer, 0);
|
||||
return JS_UNDEFINED;
|
||||
}
|
||||
|
||||
static JSValue js_enet_peer_reset(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) {
|
||||
static JSValue js_enet_peer_reset(JSContext *ctx, JSValueConst this_val,
|
||||
int argc, JSValueConst *argv) {
|
||||
ENetPeer *peer = JS_GetOpaque(this_val, enet_peer_class_id);
|
||||
if (!peer)
|
||||
if (!peer) {
|
||||
return JS_EXCEPTION;
|
||||
|
||||
}
|
||||
enet_peer_reset(peer);
|
||||
return JS_UNDEFINED;
|
||||
}
|
||||
|
||||
static JSValue js_enet_peer_ping(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) {
|
||||
static JSValue js_enet_peer_ping(JSContext *ctx, JSValueConst this_val,
|
||||
int argc, JSValueConst *argv) {
|
||||
ENetPeer *peer = JS_GetOpaque(this_val, enet_peer_class_id);
|
||||
if (!peer)
|
||||
if (!peer) {
|
||||
return JS_EXCEPTION;
|
||||
|
||||
}
|
||||
enet_peer_ping(peer);
|
||||
return JS_UNDEFINED;
|
||||
}
|
||||
|
||||
static JSValue js_enet_peer_throttle_configure(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) {
|
||||
static JSValue js_enet_peer_throttle_configure(JSContext *ctx, JSValueConst this_val,
|
||||
int argc, JSValueConst *argv) {
|
||||
ENetPeer *peer = JS_GetOpaque(this_val, enet_peer_class_id);
|
||||
if (!peer)
|
||||
if (!peer) {
|
||||
return JS_EXCEPTION;
|
||||
}
|
||||
|
||||
int interval, acceleration, deceleration;
|
||||
if (argc < 3 || JS_ToInt32(ctx, &interval, argv[0]) || JS_ToInt32(ctx, &acceleration, argv[1]) || JS_ToInt32(ctx, &deceleration, argv[2]))
|
||||
return JS_ThrowTypeError(ctx, "Expected 3 integer arguments (interval, acceleration, deceleration)");
|
||||
if (argc < 3 ||
|
||||
JS_ToInt32(ctx, &interval, argv[0]) ||
|
||||
JS_ToInt32(ctx, &acceleration, argv[1]) ||
|
||||
JS_ToInt32(ctx, &deceleration, argv[2])) {
|
||||
return JS_ThrowTypeError(ctx,
|
||||
"Expected 3 int arguments: interval, acceleration, deceleration");
|
||||
}
|
||||
|
||||
enet_peer_throttle_configure(peer, interval, acceleration, deceleration);
|
||||
return JS_UNDEFINED;
|
||||
}
|
||||
|
||||
static JSValue js_enet_peer_timeout(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) {
|
||||
static JSValue js_enet_peer_timeout(JSContext *ctx, JSValueConst this_val,
|
||||
int argc, JSValueConst *argv) {
|
||||
ENetPeer *peer = JS_GetOpaque(this_val, enet_peer_class_id);
|
||||
if (!peer)
|
||||
if (!peer) {
|
||||
return JS_EXCEPTION;
|
||||
}
|
||||
|
||||
int timeout_limit, timeout_min, timeout_max;
|
||||
if (argc < 3 || JS_ToInt32(ctx, &timeout_limit, argv[0]) || JS_ToInt32(ctx, &timeout_min, argv[1]) || JS_ToInt32(ctx, &timeout_max, argv[2]))
|
||||
return JS_ThrowTypeError(ctx, "Expected 3 integer arguments (timeout_limit, timeout_min, timeout_max)");
|
||||
if (argc < 3 ||
|
||||
JS_ToInt32(ctx, &timeout_limit, argv[0]) ||
|
||||
JS_ToInt32(ctx, &timeout_min, argv[1]) ||
|
||||
JS_ToInt32(ctx, &timeout_max, argv[2])) {
|
||||
return JS_ThrowTypeError(ctx,
|
||||
"Expected 3 integer arguments: timeout_limit, timeout_min, timeout_max");
|
||||
}
|
||||
|
||||
enet_peer_timeout(peer, timeout_limit, timeout_min, timeout_max);
|
||||
return JS_UNDEFINED;
|
||||
}
|
||||
|
||||
/* Class definitions */
|
||||
static JSClassDef enet_host = {
|
||||
"ENetHost",
|
||||
.finalizer = js_enet_host_finalizer,
|
||||
@@ -300,6 +417,7 @@ static JSClassDef enet_peer_class = {
|
||||
.finalizer = js_enet_peer_finalizer,
|
||||
};
|
||||
|
||||
/* Function lists */
|
||||
static const JSCFunctionListEntry js_enet_funcs[] = {
|
||||
JS_CFUNC_DEF("initialize", 0, js_enet_initialize),
|
||||
JS_CFUNC_DEF("deinitialize", 0, js_enet_deinitialize),
|
||||
@@ -307,7 +425,7 @@ static const JSCFunctionListEntry js_enet_funcs[] = {
|
||||
};
|
||||
|
||||
static const JSCFunctionListEntry js_enet_host_funcs[] = {
|
||||
JS_CFUNC_DEF("service", 1, js_enet_host_service),
|
||||
JS_CFUNC_DEF("service", 2, js_enet_host_service),
|
||||
JS_CFUNC_DEF("connect", 2, js_enet_host_connect),
|
||||
JS_CFUNC_DEF("flush", 0, js_enet_host_flush),
|
||||
JS_CFUNC_DEF("broadcast", 1, js_enet_host_broadcast),
|
||||
@@ -320,31 +438,49 @@ static const JSCFunctionListEntry js_enet_peer_funcs[] = {
|
||||
JS_CFUNC_DEF("disconnect_later", 0, js_enet_peer_disconnect_later),
|
||||
JS_CFUNC_DEF("reset", 0, js_enet_peer_reset),
|
||||
JS_CFUNC_DEF("ping", 0, js_enet_peer_ping),
|
||||
JS_CFUNC_DEF("throttle_configure", 3, js_enet_peer_throttle_configure),
|
||||
JS_CFUNC_DEF("throttle_configure",3, js_enet_peer_throttle_configure),
|
||||
JS_CFUNC_DEF("timeout", 3, js_enet_peer_timeout),
|
||||
};
|
||||
|
||||
JSValue js_enet_use(JSContext *js)
|
||||
{
|
||||
/* Module entry point */
|
||||
static int js_enet_init(JSContext *ctx, JSModuleDef *m);
|
||||
|
||||
/* This function returns the default export object */
|
||||
JSValue js_enet_use(JSContext *ctx) {
|
||||
// Register ENetHost class
|
||||
JS_NewClassID(&enet_host_id);
|
||||
JS_NewClass(JS_GetRuntime(js), enet_host_id, &enet_host);
|
||||
JSValue host_proto = JS_NewObject(js);
|
||||
JS_SetPropertyFunctionList(js, host_proto, js_enet_host_funcs, countof(js_enet_host_funcs));
|
||||
JS_SetClassProto(js, enet_host_id, host_proto);
|
||||
JS_NewClass(JS_GetRuntime(ctx), enet_host_id, &enet_host);
|
||||
JSValue host_proto = JS_NewObject(ctx);
|
||||
JS_SetPropertyFunctionList(ctx, host_proto, js_enet_host_funcs, countof(js_enet_host_funcs));
|
||||
JS_SetClassProto(ctx, enet_host_id, host_proto);
|
||||
|
||||
// Register ENetPeer class
|
||||
JS_NewClassID(&enet_peer_class_id);
|
||||
JS_NewClass(JS_GetRuntime(js), enet_peer_class_id, &enet_peer_class);
|
||||
JSValue peer_proto = JS_NewObject(js);
|
||||
JS_SetPropertyFunctionList(js, peer_proto, js_enet_peer_funcs, countof(js_enet_peer_funcs));
|
||||
JS_SetClassProto(js, enet_peer_class_id, peer_proto);
|
||||
JS_NewClass(JS_GetRuntime(ctx), enet_peer_class_id, &enet_peer_class);
|
||||
JSValue peer_proto = JS_NewObject(ctx);
|
||||
JS_SetPropertyFunctionList(ctx, peer_proto, js_enet_peer_funcs, countof(js_enet_peer_funcs));
|
||||
JS_SetClassProto(ctx, enet_peer_class_id, peer_proto);
|
||||
|
||||
JSValue export = JS_NewObject(js);
|
||||
JS_SetPropertyFunctionList(js, export, js_enet_funcs, sizeof(js_enet_funcs)/sizeof(JSCFunctionListEntry));
|
||||
return export;
|
||||
// Optional: store references in a "prosperon.c_types" for your environment
|
||||
JSValue global = JS_GetGlobalObject(ctx);
|
||||
JSValue prosp = JS_GetPropertyStr(ctx, global, "prosperon");
|
||||
JSValue c_types = JS_GetPropertyStr(ctx, prosp, "c_types");
|
||||
|
||||
JS_SetPropertyStr(ctx, c_types, "enet_host", JS_DupValue(ctx, host_proto));
|
||||
JS_SetPropertyStr(ctx, c_types, "enet_peer", JS_DupValue(ctx, peer_proto));
|
||||
|
||||
JS_FreeValue(ctx, c_types);
|
||||
JS_FreeValue(ctx, prosp);
|
||||
JS_FreeValue(ctx, global);
|
||||
|
||||
// Create the default export object with top-level ENet functions
|
||||
JSValue export_obj = JS_NewObject(ctx);
|
||||
JS_SetPropertyFunctionList(ctx, export_obj, js_enet_funcs, countof(js_enet_funcs));
|
||||
return export_obj;
|
||||
}
|
||||
|
||||
static int js_enet_init(JSContext *js, JSModuleDef *m) {
|
||||
return JS_SetModuleExport(js, m, "default", js_enet_use(js));
|
||||
static int js_enet_init(JSContext *ctx, JSModuleDef *m) {
|
||||
return JS_SetModuleExport(ctx, m, "default", js_enet_use(ctx));
|
||||
}
|
||||
|
||||
#ifdef JS_SHARED_LIBRARY
|
||||
@@ -353,10 +489,12 @@ static int js_enet_init(JSContext *js, JSModuleDef *m) {
|
||||
#define JS_INIT_MODULE js_init_module_enet
|
||||
#endif
|
||||
|
||||
JSModuleDef *JS_INIT_MODULE(JSContext *js, const char *module_name) {
|
||||
JSModuleDef *m = JS_NewCModule(js, module_name, js_enet_init);
|
||||
if (!m)
|
||||
/* Module definition */
|
||||
JSModuleDef *JS_INIT_MODULE(JSContext *ctx, const char *module_name) {
|
||||
JSModuleDef *m = JS_NewCModule(ctx, module_name, js_enet_init);
|
||||
if (!m) {
|
||||
return NULL;
|
||||
JS_AddModuleExport(js, m, "default");
|
||||
}
|
||||
JS_AddModuleExport(ctx, m, "default");
|
||||
return m;
|
||||
}
|
||||
|
||||
191
tests/enet.js
Normal file
191
tests/enet.js
Normal file
@@ -0,0 +1,191 @@
|
||||
var os = use('os');
|
||||
var enet = use('enet');
|
||||
var json = use('json'); // Some QuickJS environments need this import for JSON
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// 1. Initialization
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// Make sure ENet is initialized before we do anything else
|
||||
enet.initialize();
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// 2. "Framework": test runner & polling helper
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
let results = [];
|
||||
|
||||
/**
|
||||
* Simple test runner. Each test is given a name and a function.
|
||||
* If the function throws an Error, the test is marked failed.
|
||||
*/
|
||||
function runTest(testName, testFunc) {
|
||||
console.log(`=== Running Test: ${testName} ===`);
|
||||
try {
|
||||
testFunc();
|
||||
results.push({ testName, passed: true });
|
||||
} catch (err) {
|
||||
console.log(`Test "${testName}" failed: ${err}`);
|
||||
results.push({ testName, passed: false, error: err });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* runSteps polls both client and server for `steps` iterations,
|
||||
* each iteration calling service() with a small timeout. Any events
|
||||
* are captured and returned in arrays for further inspection.
|
||||
*
|
||||
* @param {Object} client - the client ENet host
|
||||
* @param {Object} server - the server ENet host
|
||||
* @param {number} steps - how many iterations to process
|
||||
* @param {boolean} printEvents - whether to log events to console
|
||||
* @param {number} timeout - milliseconds to pass to service()
|
||||
* @returns { clientEvents, serverEvents } arrays of all events captured
|
||||
*/
|
||||
function runSteps(client, server, steps = 5, printEvents = false, timeout = 10) {
|
||||
let clientEvents = [];
|
||||
let serverEvents = [];
|
||||
|
||||
for (let i = 0; i < steps; i++) {
|
||||
// Poll the client
|
||||
client.service((evt) => {
|
||||
if (evt) {
|
||||
clientEvents.push(evt);
|
||||
if (printEvents) {
|
||||
console.log("client:" + json.encode(evt));
|
||||
}
|
||||
}
|
||||
}, timeout);
|
||||
|
||||
// Poll the server
|
||||
server.service((evt) => {
|
||||
if (evt) {
|
||||
serverEvents.push(evt);
|
||||
if (printEvents) {
|
||||
console.log("server:" + json.encode(evt));
|
||||
}
|
||||
}
|
||||
}, timeout);
|
||||
}
|
||||
|
||||
return { clientEvents, serverEvents };
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// 3. Actual Tests
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
let serverHost = null;
|
||||
let clientHost = null;
|
||||
let clientPeer = null;
|
||||
|
||||
runTest("Create Server", () => {
|
||||
// Bind on 127.0.0.1:12345
|
||||
serverHost = enet.create_host("127.0.0.1:12345");
|
||||
if (!serverHost) {
|
||||
throw new Error("Failed to create server host");
|
||||
}
|
||||
});
|
||||
|
||||
runTest("Create Client", () => {
|
||||
clientHost = enet.create_host();
|
||||
if (!clientHost) {
|
||||
throw new Error("Failed to create client host");
|
||||
}
|
||||
});
|
||||
|
||||
runTest("Connect Client to Server", () => {
|
||||
clientPeer = clientHost.connect("127.0.0.1", 12345);
|
||||
if (!clientPeer) {
|
||||
throw new Error("Failed to create client->server peer");
|
||||
}
|
||||
|
||||
// Poll both sides for a few steps so the connection can succeed
|
||||
const { clientEvents, serverEvents } = runSteps(clientHost, serverHost, 5, true);
|
||||
|
||||
// Verify the server got a 'connect' event
|
||||
let connectEvent = serverEvents.find(evt => evt.type === "connect");
|
||||
if (!connectEvent) {
|
||||
throw new Error("Server did not receive a connect event");
|
||||
}
|
||||
});
|
||||
|
||||
runTest("Send Data from Client to Server", () => {
|
||||
// Send some JSON object from client -> server
|
||||
clientPeer.send({ hello: "HELLO" });
|
||||
|
||||
// Process for a few steps so the data actually arrives
|
||||
const { clientEvents, serverEvents } = runSteps(clientHost, serverHost, 5, true);
|
||||
|
||||
// The server should get a 'receive' event
|
||||
let receiveEvent = serverEvents.find(evt => evt.type === "receive");
|
||||
if (!receiveEvent) {
|
||||
throw new Error("Server did not receive data from the client");
|
||||
}
|
||||
|
||||
// Check the payload
|
||||
if (!receiveEvent.data || receiveEvent.data.hello !== "HELLO") {
|
||||
throw new Error(`Server got unexpected data: ${JSON.stringify(receiveEvent.data)}`);
|
||||
}
|
||||
});
|
||||
|
||||
runTest("Broadcast from Server to Client", () => {
|
||||
// The server broadcasts a JSON string
|
||||
serverHost.broadcast({ broadcast: "HelloAll" });
|
||||
|
||||
// Let data flow
|
||||
const { clientEvents, serverEvents } = runSteps(clientHost, serverHost, 5, true);
|
||||
|
||||
// Client should get a 'receive' event with broadcast data
|
||||
let broadcastEvent = clientEvents.find(evt => evt.type === "receive");
|
||||
if (!broadcastEvent) {
|
||||
throw new Error("Client did not receive broadcast data");
|
||||
}
|
||||
|
||||
// The broadcastEvent.data should be an object with broadcast="HelloAll"
|
||||
if (!broadcastEvent.data || broadcastEvent.data.broadcast !== "HelloAll") {
|
||||
throw new Error(`Client received unexpected broadcast: ${JSON.stringify(broadcastEvent.data)}`);
|
||||
}
|
||||
});
|
||||
|
||||
runTest("Disconnect Client", () => {
|
||||
// Disconnect from the client side
|
||||
clientPeer.disconnect();
|
||||
|
||||
// Let both sides see the disconnect
|
||||
const { clientEvents, serverEvents } = runSteps(clientHost, serverHost, 5, true);
|
||||
|
||||
// The server should eventually get a "disconnect"
|
||||
let disconnectEvent = serverEvents.find(evt => evt.type === "disconnect");
|
||||
if (!disconnectEvent) {
|
||||
throw new Error("Server never received a disconnect event");
|
||||
}
|
||||
});
|
||||
|
||||
runTest("Deinitialize ENet", () => {
|
||||
enet.deinitialize();
|
||||
});
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// 4. Print Summary & Exit
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
console.log("\n=== Test Summary ===");
|
||||
let passedCount = 0;
|
||||
results.forEach(({ testName, passed, error }, idx) => {
|
||||
console.log(`Test ${idx + 1}: ${testName} - ${passed ? "PASSED" : "FAILED"}`);
|
||||
if (!passed && error) {
|
||||
console.log(" Reason:", error);
|
||||
}
|
||||
if (passed) passedCount++;
|
||||
});
|
||||
|
||||
console.log(`\nResult: ${passedCount}/${results.length} tests passed`);
|
||||
|
||||
if (passedCount < results.length) {
|
||||
console.log("Overall: FAILED");
|
||||
os.exit(1);
|
||||
} else {
|
||||
console.log("Overall: PASSED");
|
||||
os.exit(0);
|
||||
}
|
||||
Reference in New Issue
Block a user