Skip to content

Enet (2026's edition) - #3437

Draft
ohlidalp wants to merge 18 commits into
RigsOfRods:masterfrom
ohlidalp:enet
Draft

Enet (2026's edition)#3437
ohlidalp wants to merge 18 commits into
RigsOfRods:masterfrom
ohlidalp:enet

Conversation

@ohlidalp

@ohlidalp ohlidalp commented Jul 20, 2026

Copy link
Copy Markdown
Member

An experimental RoRnet upgrade to use UDP packets (instead of TCP packets) via popular ENet library.
RoRserver counterpart: RigsOfRods/ror-server#143

Features:

  • backward compatibility with older rorservers - first connection is made via TCP (socketw) as usual to check version.
  • actor-actor collisions via additional RoRnet stream containing collision forces (not lag-compensated yet).
  • character-actor sticky collision: character can walk on cabs of moving networked actor
  • extended MP ClientList UI showing net traffic graphs.

@Zentro

Zentro commented Jul 26, 2026

Copy link
Copy Markdown
Member

Trying to get this to start.. Fedora Linux 44

After compiling, I tried to run rorserver and saw:

26-07-2026 01:08:52|t140167781660544|ERROR|Failed to create ENet server host.

But this didn't cause the server to sigkill, it kept going. It seems the listener thread started.

I connected anyways with a TCP client, and saw "error getting server version" with this in the logs:

26-07-2026 01:15:24|t140695771834048|VERBO|Listener got a new connection
26-07-2026 01:15:24|t140695771834048|ERROR|SWReceiveMessage(): payload too long: 1850896210 b (max. is 8192 b)
26-07-2026 01:15:24|t140695771834048|ERROR|ERROR Listener: receiving first message
26-07-2026 01:15:24|t140695771834048|VERBO|Listener awaiting connections

Then, I connected with a valid UDP client and saw "Connecting via UDP" with:

26-07-2026 01:31:06|t140574239315648|VERBO|Listener got a new connection
26-07-2026 01:31:06|t140574239315648|VERBO|Listener awaiting connections

But enet isn't initialized, so we're just stuck trying to connect until it eventually fails.

Unrelated for RoR server, but the annoying "foreground" feature causes a coredump. I can remove this in a separate PR.

--- edit ---

Inspected errno with:

diff --git a/source/server/dispatcher_enet.cpp b/source/server/dispatcher_enet.cpp
index edc037c..03f97eb 100644
--- a/source/server/dispatcher_enet.cpp
+++ b/source/server/dispatcher_enet.cpp
@@ -7,6 +7,9 @@
 
 #include <cassert>
 
+#include <cerrno>
+#include <cstring>
+
 void DispatcherENet::Initialize()
 {
     // Make sure it's not started twice
@@ -41,7 +44,7 @@ void DispatcherENet::Initialize()
                               0      /* assume any amount of outgoing bandwidth */);
     if (m_host == nullptr)
     {
-        Logger::Log(LOG_ERROR, "Failed to create ENet server host.");
+        Logger::Log(LOG_ERROR, "Failed to create ENet server host: %s", std::strerror(errno));
         return;
     }
     else
@@ -201,4 +204,4 @@ void DispatcherENet::QueueMessage(ENetPeer *peer, int type, int source, unsigned
     ENetPacket* packet = enet_packet_create(buffer, msgsize, packet_flags);
     enet_peer_send(peer, 0, packet);
     Messaging::StatsAddOutgoing(msgsize);
-}
\ No newline at end of file
+}

which noted:

26-07-2026 01:39:06|t140626541614976|ERROR|Failed to create ENet server host: Cannot assign requested address

So I set forced my IP to be local (seems trying to fetch the IP from an HTTP call is bad, should probably stop that) and now it works!

image

Users disconnect cleanly.

ohlidalp and others added 18 commits July 31, 2026 13:42
THIS IS A DIRTY PROTOTYPE!

Dumped full .tar.gz download of enet to /dependencies/enet/.
Tested to build and link (windows 10, VS2019)
For maximum backwards compatibility, RoRnet version was bumped, SocketW is kept and server still listens for TCP connections on ip/port advertised on master server. Only if client version matches, the server instructs the client to reconnect using ENet (using pre-existing unused RoRnet message MSG2_VERSION).

ENet communication is exactly the same as legacy TCP connection, with only one cosmetic detail - server info is sent to client using pre-existing unused RoRnet message MSG2_SERVER_SETTINGS rather than MSG2_HELLO.

This is a prototype done with minimum code changes, for easy understanding.
Code changes:
* `class Network`: removed send/recv threads. TCP functions renamed.
* `ConnectThread()`removed all TCP processing except version check, placed ENet dispatch loop here - the thread now runs until disconnect.
* `RecvThread()` changed to `OnPacketReceived()` - it does all it's previous work plus the user auth previously done by ConnectThread(). Function OnPacketReceived() is now called with enet mutex unlocked and must only call safe functions like AddPacket() and DisconnectEnet().
* Added enet_initialize() to main.cpp
* Added handling of ENet event CONNECT - packet cannot be queued earlier.
* ENet port number must be TCP port + 1.
* class Network: renamed func `StartConnecting()` to `Connect()`
* class Network: Added mutex protecting enet library; `AddPacket()` and `Disconnect()` lock the mutex.
* Network.h: added DisconnectENet(); which locks the enet mutex.
* class Network.h - Function `StopConnecting()` fused into `Disconnect()` which now carefully considers current state.
* DO NOT reset 'mp_state' cvar and `m_progress` variable on dispatch thread - should be done by main thread after cleanup.
* main(): MSG_SIM_UNLOAD_TERRN_REQUESTED - do not crash if no terrain is loaded.
* main(): added missing handler for MSG_NET_USER_DISCONNECT.

Note: Apparently using non-zero wait timeout for enet_host_service() causes nearby locks to become laggy, eventually never being able to lock. In this case, using timeout 100 causes RoR to hang on disconnecting from server because `Network::Disconnect()` is never able to grab the lock. Observed under Win10/VS2019/Debug.
Take advantage of the fact recv. thread checks for chat messages anyway and console is threadsafe.

Also removed dummy `ChatSystem::SendStreamSetup()` because chat doesn't flow through MSG2_STREAM_DATA
Processing of MSG2_STREAM_DATA packets was separated from the rest.
This allows the game to dispatch received packets more effectively (which will become important when networked collisions are implemented).
The shared recv. packet queue in 'Network.h' was removed, instead 2 separate were added in 'CharacterFactory.h' and 'ActorManager.h'.
BEFORE:
Packets were sent and received on main thread while physics were not running. This means they got delayed by 1/FPS on both ends. With 60FPS (16.6ms per frame) that's 16.6 ms delay.
Additionally, a timer was checked to ensure at least 100ms intervals between sending packets. Assuming 60FPS (16.6ms per frame) this would, on average, add another 8.3ms delay.
Finally the actor node positions were updated just once per frame, making it unsuitable for collision detection.

AFTER:
Packets are sent and received directly from physics stepping thread at precise intervals, configurable by cvars: 'mp_actor_send_interval' and 'mp_actor_recv_interval' (values are in milliseconds, both are 100ms by default).
Updating actor node positions is also done this way, configurable by mp_actor_calc_interval (default 10ms).
Risk was minimal, but this eliminates any concern.
Cherry picked from RigsOfRods#3049 - original code by @tritonas00 dated May 2023
Cherry picked from RigsOfRods#3050

Since the collision works by "sticking" the character to the actor while in contact, I realized I could extend our existing driver-attachment logic to also handle this attachment.

It's glitchy right now, partly because the networked cab offset is always in world coordinates, so it causes sliding when the vehicle turns.
BEFORE:
There were separate packet types CHARACTER_CMD_ATTACH*, CHARACTER_CMD_DETACH to persistently seat the character in vehicle or "glue" it to cab to allow cab-walking.
Note the ATTACH packet must perform a round trip to take effect.
This was bearable for seating but unsuitable for dynamic contact.

AFTER:
The coupling state is sent as part of regular position update.
The client simply detects differences from existing state and updates accordingly.
Cherry picked from RigsOfRods#3056

WIP; Only tested on local machine without any fake lag (I don't know how to set that up under ENet yet).
Principle:
1. I build on top of existing logic: When client A spawns "LOCAL_*" actor, it sends STREAM_REGISTER (type 0) and client B spawns "REMOTE" actor. This stream transmits compressed node positions and vehicle state data.
2. If net. collisions are enabled, the REMOTE actor on client B sends STREAM_REGISTER (type 4) and client A links it to the original "LOCAL_*" actor. This stream sends uncompressed node forces.

Status: There is no lag compensation so remote actor reacts with delay... but IT MOVES! :D
PROBLEM: The contact-detection ray points _upwards_ from character's position ~ it's primary function is to make character 'step up' to the elevated cab when coming from ground.
FIX: Let's add negative bias to the ray, to avoid losing contact when already on the cab.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants