@@ -42,6 +42,210 @@ typedef int socklen_t;
4242
4343// -------------------------------------------------------------------------
4444
45+ #if defined(__EMSCRIPTEN__)
46+ // TheSuperHackers @feature githubawn 27/06/2026 WebAssembly LAN networking.
47+ // Browsers cannot open raw UDP/TCP sockets, so on Emscripten the UDP class is
48+ // reimplemented on top of a single WebSocket to a relay (relay.py). Every datagram is
49+ // framed [srcIP|srcPort|dstIP|dstPort|payload] (big-endian header) and the relay routes
50+ // unicast by destination virtual IP and 255.255.255.255 broadcast (LANAPI discovery) to
51+ // all peers. One WebSocket is shared by every UDP in the process; inbound datagrams are
52+ // demuxed into per-bound-port queues. Everything runs on the main browser thread (LAN is
53+ // polled each frame), so the WebSocket callbacks and Read()/Write() never race.
54+ #include < emscripten/emscripten.h>
55+ #include < emscripten/websocket.h>
56+ #include < map>
57+ #include < deque>
58+ #include < vector>
59+ #include < cstring>
60+ #include < cstdio>
61+
62+ namespace
63+ {
64+ inline void putBE32 (unsigned char *p, UnsignedInt v) { p[0 ]=(unsigned char )(v>>24 ); p[1 ]=(unsigned char )(v>>16 ); p[2 ]=(unsigned char )(v>>8 ); p[3 ]=(unsigned char )v; }
65+ inline void putBE16 (unsigned char *p, UnsignedShort v) { p[0 ]=(unsigned char )(v>>8 ); p[1 ]=(unsigned char )v; }
66+ inline UnsignedInt getBE32 (const unsigned char *p) { return ((UnsignedInt)p[0 ]<<24 )|((UnsignedInt)p[1 ]<<16 )|((UnsignedInt)p[2 ]<<8 )|(UnsignedInt)p[3 ]; }
67+ inline UnsignedShort getBE16 (const unsigned char *p) { return (UnsignedShort)(((UnsignedInt)p[0 ]<<8 )|(UnsignedInt)p[1 ]); }
68+
69+ struct WsDatagram
70+ {
71+ UnsignedInt srcIP;
72+ UnsignedShort srcPort;
73+ std::vector<unsigned char > data;
74+ };
75+
76+ class WsRelay
77+ {
78+ public:
79+ static WsRelay &get () { static WsRelay s; return s; }
80+
81+ void connect ()
82+ {
83+ if (m_sock || !emscripten_websocket_is_supported ())
84+ return ;
85+ char url[256 ];
86+ EM_ASM ({
87+ var h = (typeof location !== ' undefined' && location.hostname ) ? location.hostname : ' localhost' ;
88+ stringToUTF8 (' ws://' + h + ' :8090' , $0 , 256 );
89+ }, url);
90+ EmscriptenWebSocketCreateAttributes attr;
91+ emscripten_websocket_init_create_attributes (&attr);
92+ attr.url = url;
93+ attr.createOnMainThread = EM_TRUE ;
94+ m_sock = emscripten_websocket_new (&attr);
95+ if (m_sock <= 0 ) { m_sock = 0 ; return ; }
96+ emscripten_websocket_set_onopen_callback (m_sock, this , &WsRelay::onOpenCb);
97+ emscripten_websocket_set_onmessage_callback (m_sock, this , &WsRelay::onMessageCb);
98+ emscripten_websocket_set_onclose_callback (m_sock, this , &WsRelay::onCloseCb);
99+ emscripten_websocket_set_onerror_callback (m_sock, this , &WsRelay::onErrorCb);
100+ }
101+
102+ UnsignedInt localIP () const { return m_assignedIP; }
103+
104+ void registerPort (UnsignedShort port) { m_queues[port]; }
105+ void unregisterPort (UnsignedShort port) { m_queues.erase (port); }
106+
107+ void send (UnsignedInt srcIP, UnsignedShort srcPort, UnsignedInt dstIP, UnsignedShort dstPort,
108+ const unsigned char *p, UnsignedInt len)
109+ {
110+ if (!m_open)
111+ return ;
112+ std::vector<unsigned char > buf (12 + len);
113+ putBE32 (&buf[0 ], srcIP); putBE16 (&buf[4 ], srcPort);
114+ putBE32 (&buf[6 ], dstIP); putBE16 (&buf[10 ], dstPort);
115+ if (len) memcpy (&buf[12 ], p, len);
116+ emscripten_websocket_send_binary (m_sock, buf.data (), (uint32_t )buf.size ());
117+ }
118+
119+ bool recv (UnsignedShort port, WsDatagram &out)
120+ {
121+ std::map<UnsignedShort, std::deque<WsDatagram> >::iterator it = m_queues.find (port);
122+ if (it == m_queues.end () || it->second .empty ())
123+ return false ;
124+ out = it->second .front ();
125+ it->second .pop_front ();
126+ return true ;
127+ }
128+
129+ private:
130+ WsRelay () : m_sock(0 ), m_open(false ), m_assignedIP(0 ) {}
131+
132+ void handleText (const char *s)
133+ {
134+ unsigned a, b, c, d;
135+ if (s && strncmp (s, " IP " , 3 ) == 0 && sscanf (s + 3 , " %u.%u.%u.%u" , &a, &b, &c, &d) == 4 )
136+ m_assignedIP = (a << 24 ) | (b << 16 ) | (c << 8 ) | d;
137+ }
138+
139+ void handleBinary (const unsigned char *p, int len)
140+ {
141+ if (len < 12 )
142+ return ;
143+ WsDatagram dg;
144+ dg.srcIP = getBE32 (p);
145+ dg.srcPort = getBE16 (p + 4 );
146+ UnsignedShort dstPort = getBE16 (p + 10 );
147+ dg.data .assign (p + 12 , p + len);
148+ m_queues[dstPort].push_back (dg);
149+ }
150+
151+ static EM_BOOL onOpenCb (int , const EmscriptenWebSocketOpenEvent *, void *ud) { ((WsRelay *)ud)->m_open = true ; return EM_TRUE ; }
152+ static EM_BOOL onCloseCb (int , const EmscriptenWebSocketCloseEvent *, void *ud) { ((WsRelay *)ud)->m_open = false ; return EM_TRUE ; }
153+ static EM_BOOL onErrorCb (int , const EmscriptenWebSocketErrorEvent *, void *) { return EM_TRUE ; }
154+ static EM_BOOL onMessageCb (int , const EmscriptenWebSocketMessageEvent *e, void *ud)
155+ {
156+ WsRelay *self = (WsRelay *)ud;
157+ if (e->isText ) self->handleText ((const char *)e->data );
158+ else self->handleBinary (e->data , (int )e->numBytes );
159+ return EM_TRUE ;
160+ }
161+
162+ EMSCRIPTEN_WEBSOCKET_T m_sock;
163+ bool m_open;
164+ UnsignedInt m_assignedIP;
165+ std::map<UnsignedShort, std::deque<WsDatagram> > m_queues;
166+ };
167+ } // namespace
168+
169+ // Exposed to IPEnumeration / SDL3Main so the engine can learn its relay-assigned LAN IP
170+ // and start connecting early (well before the user reaches the LAN lobby).
171+ extern " C" void ggc_ws_connect (void ) { WsRelay::get ().connect (); }
172+ extern " C" unsigned int ggc_ws_local_ip (void ) { WsRelay &r = WsRelay::get (); r.connect (); return r.localIP (); }
173+
174+ UDP ::UDP () { fd = 0 ; myIP = 0 ; myPort = 0 ; m_lastError = 0 ; WsRelay::get ().connect (); }
175+ UDP ::~UDP () { if (fd) WsRelay::get ().unregisterPort (myPort); }
176+
177+ Int UDP::Bind (const char * /* Host*/ , UnsignedShort port) { return Bind ((UnsignedInt)0 , port); }
178+
179+ Int UDP::Bind (UnsignedInt IP , UnsignedShort Port)
180+ {
181+ WsRelay &r = WsRelay::get ();
182+ r.connect ();
183+ // The caller passes the local IP it chose (the 127.0.0.N selected in Options, surfaced
184+ // via IPEnumeration / m_defaultIP). That value is this client's identity on the relay:
185+ // the relay learns it from the src field of our datagrams and routes peers' unicasts to
186+ // it. Fall back to the relay-assigned IP only if none was supplied.
187+ myIP = IP ? IP : r.localIP ();
188+ if (Port == 0 )
189+ {
190+ static UnsignedShort s_ephemeral = 50000 ;
191+ Port = ++s_ephemeral; // implicit bind -> pick a high ephemeral port
192+ }
193+ myPort = Port;
194+ r.registerPort (myPort);
195+ fd = 1 ; // non-zero = bound
196+ return OK ;
197+ }
198+
199+ Int UDP::getLocalAddr (UnsignedInt &ip, UnsignedShort &port)
200+ {
201+ if (myIP == 0 ) myIP = WsRelay::get ().localIP ();
202+ ip = myIP;
203+ port = myPort;
204+ return OK ;
205+ }
206+
207+ Int UDP::SetBlocking (Int /* block*/ ) { return OK ; } // always non-blocking on web
208+
209+ Int UDP::Write (const unsigned char *msg, UnsignedInt len, UnsignedInt IP , UnsignedShort port)
210+ {
211+ if ((IP == 0 ) || (port == 0 )) return ADDRNOTAVAIL ;
212+ UnsignedInt srcIP = myIP ? myIP : WsRelay::get ().localIP ();
213+ WsRelay::get ().send (srcIP, myPort, IP , port, msg, len);
214+ return (Int)len;
215+ }
216+
217+ Int UDP::Read (unsigned char *msg, UnsignedInt len, sockaddr_in *from)
218+ {
219+ WsDatagram dg;
220+ if (!WsRelay::get ().recv (myPort, dg))
221+ return 0 ; // no data pending (caller treats 0 as would-block)
222+ UnsignedInt n = (UnsignedInt)dg.data .size ();
223+ if (n > len) n = len;
224+ if (n) memcpy (msg, &dg.data [0 ], n);
225+ if (from)
226+ {
227+ memset (from, 0 , sizeof (*from));
228+ from->sin_family = AF_INET ;
229+ from->sin_addr .s_addr = htonl (dg.srcIP );
230+ from->sin_port = htons (dg.srcPort );
231+ }
232+ return (Int)n;
233+ }
234+
235+ void UDP::ClearStatus () { m_lastError = 0 ; }
236+ UDP ::sockStat UDP::GetStatus () { return OK ; }
237+ Int UDP::SetInputBuffer (UnsignedInt /* bytes*/ ) { return TRUE ; }
238+ Int UDP::SetOutputBuffer (UnsignedInt /* bytes*/ ) { return TRUE ; }
239+ int UDP::GetInputBuffer () { return 0 ; }
240+ int UDP::GetOutputBuffer () { return 0 ; }
241+ Int UDP::AllowBroadcasts (Bool /* status*/ ) { return TRUE ; }
242+
243+ #ifdef DEBUG_LOGGING
244+ AsciiString GetWSAErrorString ( Int error ) { AsciiString s; s.format (" err %d" , error); return s; }
245+ #endif
246+
247+ #else // !__EMSCRIPTEN__
248+
45249#ifdef DEBUG_LOGGING
46250
47251#define CASE (x ) case (x): return #x;
@@ -546,3 +750,5 @@ Int UDP::AllowBroadcasts(Bool status)
546750 else
547751 return FALSE ;
548752}
753+
754+ #endif // __EMSCRIPTEN__
0 commit comments