Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Networking

The networking subsystem owns the active transport, turns a byte stream into packet bodies, applies the protocol’s XOR transformations, and moves decoded server packets into the pane event hierarchy. Client packet builders use raw byte arrays and the same small big-endian field writers.

The directional action indexes are Client actions and Server actions. Each index links to one payload-relative page per action.

High-level operation

Connection and Windows notification

The normal game transport uses Winsock 1.1 and an IPv4 TCP stream. Connection setup creates an AF_INET, SOCK_STREAM socket, requests a 0x4000 byte Winsock receive buffer, resolves the configured host, and connects to the configured port. Once connected, WSAAsyncSelect associates the socket with the main window using message 0x0402 and event mask 0x21, which is FD_READ | FD_CLOSE.

The main window procedure handles the low word of lParam for message 0x0402:

Winsock eventValueClient action
FD_READ0x0001Queue socket work code 4, which runs the receive framer.
FD_CLOSE0x0020Close the socket and serial handle, clear connection state, and offer the reconnect UI.

The executable also retains serial and printable-text transports. They eventually feed the same logical packet event path, but the standard Stone server path is the binary TCP path described here.

Binary frame

Every binary TCP frame begins with 0xAA. The two-byte size is unsigned and big-endian. It counts the body beginning at the action byte, not the marker or the size itself.

OffsetWidthTypeMeaning
0x001uint8_tMarker, always 0xAA.
0x012uint16_t, big-endianWire body length.
0x03body_lengthbytesAction, transformation metadata where applicable, and payload.

The total frame length is therefore 3 + body_length. The receive code requires a positive body length and stores at most 0x10000 body bytes. Since the wire field is 16 bits, the largest representable wire value is 0xFFFF.

The application-facing logical packet is:

[uint8 action] [payload bytes...] [zero sentinel]

net_c_queue_send receives the action and payload length from a packet builder, copies those bytes, and appends the zero sentinel. The sentinel is an implementation guard, not a checksum. The decoder does not validate its transformed value. After decoding it writes another zero immediately after the returned logical bytes.

Direction defines the action

Action values are not globally unique. A client action and a server action with the same byte can have unrelated meanings. For example, both directions support 0x0D, but the send builders and receive handlers are separate code paths. Protocol implementations must key dispatch by (direction, action), never by action alone.

The executable contains 52 established client action values and 59 server action values. Fifty-one client values occur directly at net_c_queue_send call sites. Action 0x0F is built by the spell-slot path and reaches the queue through the spell-delay controller. Thirty-five values occur in both directions. A server 0x4B packet can also ask the client to forward an embedded logical client packet, so that path is not restricted to one statically embedded client action constant.

Receive and event routing

The TCP reader refills a 0x18000 byte cache with recv and then supplies one byte at a time to the framer. This preserves a partial header or body across Windows notifications and permits multiple complete frames to be drained from one receive cache.

After a frame is complete, the client decodes it when required and posts a copy as internal work code 0x0E. The EventMan worker converts that work item into an Event with type 9, packet pointer at offset +0x14, and packet size at offset +0x18. It queues a copied Event to the dispatcher worker, which recursively walks the pane hierarchy and calls virtual slot +0x40 on each eligible pane. That virtual method is the pane’s server-packet handler.

Dispatch is therefore distributed rather than centralized. MapPane owns the largest switch, while MainMenuPane, ServerSelectDialogPane, exchange panes, bulletin panes, and other UI objects handle actions relevant to their current state. The server action index combines those separate handlers.

Send path

Packet builders assemble a logical packet in a local byte array and call net_c_queue_send. The Socket object copies the bytes, appends the sentinel, and queues work code 5. The socket worker then applies the client transformation rule, allocates body_length + 3 bytes, writes the frame header, and calls Winsock send once.

The send path treats SOCKET_ERROR as failure. It does not retry when send succeeds with a byte count smaller than the requested frame length. A compatible implementation should use a full-write loop even though this client does not.

During a server transfer state, net_c_queue_send drops every client action except 0x10, the client transfer-server action.

Queue concurrency and Socket affinity

The Socket work queue has capacity 0x80. Its ring is protected by a monitor and paired not-full and not-empty conditions. net_c_queue_send can therefore run concurrently on a native client thread and an injected producer without racing the head, tail, count, or copied packet ownership.

The enqueue is not nonblocking. util_thread_queue_post_async calls util_ring_buffer_push_wait, which waits indefinitely when the ring is full, then signals the worker semaphore after admission. Calling it from an IPC read thread would make pipe liveness depend on Socket progress. Calling it from the Socket consumer itself can deadlock if other producers refill the ring before the self-enqueue.

Sequence, XOR state, framing buffers, and the transport call are mutated only by the Socket worker after it consumes code 5. A production Event Proxy should preserve that affinity. The documented design wakes the Socket worker with an empty native-queue signal and drains a bounded proxy queue from the worker’s net_poll_receive periodic callback. A simpler dedicated producer thread may call net_c_queue_send, but it must be drained before EventMan destroys its owned Socket during app_shutdown.

The active Socket is reachable through net_socket_instance at Darkages.exe:0x004F51BC. EventMan owns the Socket at object offset +0x68, publishes the static root during construction, and clears it during destruction. This is the established runtime root for logical packet injection, but callers must still validate the live net_socket_vtable before using a cached pointer. See Event Proxy Hooks and Injection for the injection contract and ownership boundaries.

Packet representation and C++ class evidence

The traced send and receive paths do not instantiate a class per packet or a common ClientPacket or ServerPacket object. Client builders write directly into byte arrays. The receiver copies decoded bytes into an Event, and pane handlers switch on packet[0] and call field readers on later offsets.

The code does establish these surrounding C++ types through constructors, virtual tables, source assertions, and diagnostics:

Established typeEvidence in the packet path
SocketSocket.cpp diagnostics, a constructor-installed virtual table, transport state fields, and virtual work-item processing.
EventSocket Event type 9 owns the packet pointer and size and frees the packet after dispatch.
MainMenuPaneNamed diagnostics and virtual table Darkages.exe:0x005177C0; handles initial version and key negotiation.
MapPaneMapPane.cpp, named method diagnostics, and virtual table Darkages.exe:0x00519280; owns the main in-game action switch.
ServerSelectDialogPaneNamed constructor and deletion diagnostics and virtual table Darkages.exe:0x00526140; handles server action 0x56.

Names such as kServerRequestPortrait and kClientTransferServer are enum-style action names embedded in diagnostics, not evidence of packet classes. The safest compatible-client model for the mapped path is a direction-tagged byte buffer plus per-action serializers and handlers.

Big-endian serialization

The packet builders and handlers share small integer helpers:

WidthWriterReaderByte order
1 bytenet_write_u8net_read_u8Not applicable
2 bytesnet_write_u16_benet_read_u16_beMost significant byte first
4 bytesnet_write_u32_benet_read_u32_beMost significant byte first

The writers place a zero byte immediately after the value. Packet builders subsequently overwrite that byte when adding the next field, or leave it as the packet sentinel at the final logical offset.

Sequence and XOR transformation

Ordinary bodies carry a sequence byte and three XOR passes over the payload and trailing zero. The message indexes label this branch Encrypted: Yes, but the transformation does not provide authentication or cryptographic security.

The default repeating key is the nine ASCII bytes NexonInc., the default table is generated by function 0, and the client send sequence starts at zero. Clear server action 0x00, subtype 0x00, can replace the nine-byte key and select any table function from 0 through 9. Only encrypted client packets increment the client send sequence.

The decoder does not verify that the transformed trailing byte becomes zero. It retains that byte and writes a second local zero guard after the returned logical packet.

See Sequence and XOR Transformation for the exact wire placement, pass order, negotiation payload, all ten table formulas, and implementation addresses.

Failure and bounds behavior

The standard receive path has these established edge behaviors:

  • Outside a frame, the next consumed byte is asserted to be 0xAA; the code does not scan ahead for a later marker.
  • A body length of zero or greater than 0x10000 fails an internal assertion. The two-byte wire field cannot encode a value greater than 0xFFFF.
  • recv == 0 closes the socket, sets it to INVALID_SOCKET, clears connection state, and stops reading.
  • recv == SOCKET_ERROR clears the receive cache and returns no byte. This path does not distinguish individual WSAGetLastError values.
  • Completed packet bytes are copied for the event worker. The Event owns the copy and frees it after pane dispatch.
  • No checksum or message authentication value is validated by the mapped frame or XOR code.

Code-level flow

Startup and connection

  1. Darkages.exe:0x004A32F0 net_socket_ctor installs the Socket virtual table, initializes the transport buffers and state, and installs NexonInc. as the default repeating key.
  2. Darkages.exe:0x004A59F4 net_connect_configured_server starts Winsock 1.1, checks that version 1.1 was returned, resolves the configured host, creates and connects the TCP socket, and registers WM_USER + 2 with FD_READ | FD_CLOSE.
  3. Darkages.exe:0x0045BDC0 win_main_window_proc receives message 0x0402. FD_READ calls net_queue_receive; FD_CLOSE calls net_handle_disconnect.

Receive, decode, and dispatch

win_main_window_proc
  -> net_queue_receive                         work code 4
  -> net_process_work_item
      -> net_s_receive_frames
          -> net_read_transport_byte
              -> recv                          refill up to 0x18000 bytes
          -> net_s_decode_packet_body          ordinary server actions
          -> event_post_socket_bytes           work code 0x0E
              -> event_process_work_item
                  -> event_queue_socket_packet Event type 9
                      -> event_dispatch
                          -> event_dispatch_hierarchy
                              -> pane vtable +0x40

net_s_receive_frames maintains three important pieces of persistent state: whether a frame is open, the number of header or body bytes collected, and the current body length. It combines the first two body-header bytes as (high << 8) | low. Completion occurs after exactly body_length bytes have been placed in the packet buffer.

event_dispatch_hierarchy tests Event type 9 with event_is_socket_packet before calling pane virtual slot +0x40. The return value indicates whether the event was consumed. After event_dispatch returns, event_dispatcher_process_work_item frees the packet pointer for a socket Event and deletes the copied Event.

Build, encode, and send

per-action packet builder
  -> net_write_u8 / net_write_u16_be / net_write_u32_be
  -> net_c_queue_send                          copy and append zero
      -> net_process_work_item                 work code 5
          -> net_c_send_packet_body
              -> net_c_encode_packet_body      ordinary client actions
              -> net_write_u8(0xAA)
              -> net_write_u16_be(body_length)
              -> send                          one call

net_c_encode_packet_body returns one byte more than its input size because it inserts the sequence after the action. net_s_decode_packet_body returns one byte less than its input size because it removes that sequence. Bypass actions keep their original body size.

Representative server dispatch

Darkages.exe:0x00468A90 ui_map_dispatch_server_packet is the largest action switch. It accepts 31 action values and rejects the other values in its 0x03 through 0x4B switch range. Important established branches include:

Server actionCallee or effect
0x03Transfer-server processing.
0x0ASecondary subtype dispatch within MapPane.
0x0EDelete an object from map state.
0x11ui_map_handle_object_direction, which reads a big-endian object ID and a direction from 0 through 3.
0x32Inline tile or object updates.
0x4Bnet_forward_embedded_client_packet, which reads a big-endian embedded length and sends the embedded logical client packet.

The complete accepted set and all other pane handlers are in the server action index. The separate exit-wait pane handles SReconnect, server action 0x4C. Its subtype-one branch completes an orderly-exit exchange and does not call the Socket reconnect path.

Function table

AddressCurrent IDA namePrototypePurposeCall relationships and notes
Darkages.exe:0x00431B84event_dispatchint __thiscall(void *this, void *event)Dispatch one internal Event.Calls event_dispatch_hierarchy; participates in capture and consumed-event handling.
Darkages.exe:0x00431D54event_dispatch_hierarchyint __thiscall(void *this, void *event, void *hierarchy)Recursively deliver an Event to panes.Calls pane virtual slot +0x40 for socket Event type 9.
Darkages.exe:0x00432610event_is_socket_packetint __thiscall(void *event)Test for socket Event type 9.Reads the Event type byte at +0x0C.
Darkages.exe:0x00432E50event_post_socket_bytesvoid __thiscall(void *this, const uint8_t *packet, int length)Copy decoded packet bytes to the worker queue.Posts work code 0x0E.
Darkages.exe:0x00433110event_process_work_itemvoid __thiscall(void *this, int code, void *data, int value)Convert worker records to internal Events.Code 0x0E calls event_queue_socket_packet.
Darkages.exe:0x00433DC4event_queue_socket_packetvoid __stdcall(uint8_t *packet, uint32_t size)Create and queue a socket Event.Sets Event type 9, pointer +0x14, and size +0x18.
Darkages.exe:0x0045BDC0win_main_window_procLRESULT __stdcall(HWND, UINT, WPARAM, LPARAM)Main window procedure and Winsock notification bridge.Message 0x0402 handles FD_READ and FD_CLOSE.
Darkages.exe:0x004A32F0net_socket_ctorvoid *__thiscall(void *this, void *event_sink)Construct and initialize the Socket object.Installs default key and transport buffers.
Darkages.exe:0x004A3294net_socket_dtorvoid __thiscall(void *socket_object)Close transport handles and tear down the Socket worker.Called by the deleting destructor owned by EventMan; worker teardown uses TerminateThread.
Darkages.exe:0x004A3560net_queue_receivevoid __thiscall(void *this)Queue receive processing.Posts Socket work code 4.
Darkages.exe:0x004A3570net_c_queue_sendvoid __thiscall(void *this, const uint8_t *packet, int16_t length)Copy and queue a logical client packet.Adds the zero sentinel and posts work code 5.
Darkages.exe:0x004A36C0net_queue_xor_keyvoid __thiscall(void *this, unsigned int key_length, const uint8_t *key)Queue a replacement XOR key.Copies the key and posts Socket work code 10; only called by the clear action 0x00 handler.
Darkages.exe:0x004A3700net_queue_xor_table_functionvoid __thiscall(void *this, unsigned int function_index)Queue an XOR-table function selector.Posts Socket work code 11; only called by the clear action 0x00 handler.
Darkages.exe:0x004A3770net_write_u32_bevoid __cdecl(uint32_t value, uint8_t *dest)Write a big-endian 32-bit field.Writes a zero byte at dest[4].
Darkages.exe:0x004A37D0net_read_u8uint32_t __cdecl(const uint8_t *src)Read an unsigned byte.Used by server action handlers.
Darkages.exe:0x004A3810net_read_u16_beuint32_t __cdecl(const uint8_t *src)Read a big-endian 16-bit field.Used by framing-related and packet handlers.
Darkages.exe:0x004A38B0net_read_u32_beuint32_t __cdecl(const uint8_t *src)Read a big-endian 32-bit field.Returns the combined 32-bit value.
Darkages.exe:0x004A3910net_handle_disconnectvoid __thiscall(void *this)Close transports and handle connection loss.Called for FD_CLOSE.
Darkages.exe:0x004A39C0net_poll_receivevoid __thiscall(void *this)Poll transport readiness outside the async window path.Uses select for TCP and calls net_s_receive_frames.
Darkages.exe:0x004A3A74net_s_receive_framesvoid __thiscall(void *this)Extract, decode, and post complete packets.Calls net_read_transport_byte, net_s_decode_packet_body, and event_post_socket_bytes.
Darkages.exe:0x004A44D4net_s_decode_packet_bodyint __stdcall(const uint8_t *src, int src_size, uint8_t *dest)Reverse the ordinary server-body XOR transformation.Returns src_size - 1.
Darkages.exe:0x004A4D34net_read_transport_byteint __thiscall(void *this, uint8_t *out_byte)Supply one cached transport byte.Refills with recv or ReadFile.
Darkages.exe:0x004A54D0net_process_work_itemvoid __thiscall(void *this, int code, void *data, int value)Execute queued Socket work.Codes 4, 5, 10, and 11 drive receive, send, key, and table operations.
Darkages.exe:0x004A59F4net_connect_configured_servervoid __thiscall(void *this)Establish the configured connection.Uses Winsock startup, resolution, connect, and WSAAsyncSelect.
Darkages.exe:0x004A72B4net_c_send_packet_bodyvoid __thiscall(void *this, const uint8_t *packet, int16_t length)Transform, frame, and send a client packet.Calls send once and frees the temporary frame.
Darkages.exe:0x004A7940net_write_u8void __cdecl(uint32_t value, uint8_t *dest)Write one byte.Writes a zero byte at dest[1].
Darkages.exe:0x004A7990net_write_u16_bevoid __cdecl(uint32_t value, uint8_t *dest)Write a big-endian 16-bit field.Used for the frame length and packet fields.
Darkages.exe:0x004A79E4net_c_encode_packet_bodyint __stdcall(const uint8_t *src, int src_size, uint8_t *dest)Apply the ordinary client-body sequence and XOR transformation.Returns src_size + 1.
Darkages.exe:0x00498080util_ring_buffer_push_waitvoid __thiscall(void *ring_buffer, const void *element)Append a synchronized bounded-ring record.Waits indefinitely on the not-full condition when the Socket queue reaches 128 records.
Darkages.exe:0x004A8414net_set_xor_keyvoid __stdcall(int length, const uint8_t *key)Replace the runtime XOR key.Requires length at most 9 and writes four repeated copies.
Darkages.exe:0x004A8590net_build_xor_tablevoid __stdcall(int function_index)Generate the 256-entry XOR table.Accepts function indexes 0 through 9.
Darkages.exe:0x00468A90ui_map_dispatch_server_packetint __thiscall(void *this, void *event)Dispatch in-game server actions in MapPane.Supports 31 fixed action values.
Darkages.exe:0x0046B574ui_map_handle_object_directionint __thiscall(void *this, const uint8_t *packet)Apply a server direction update to a map object.Called by the MapPane action 0x11 branch; reads a big-endian object ID at +0x01 and direction at +0x05.
Darkages.exe:0x0046CA54net_forward_embedded_client_packetestablished MapPane action handlerForward the client packet embedded in server action 0x4B.Reads a big-endian length and calls net_c_queue_send.
Darkages.exe:0x0045F780ui_main_menu_handle_server_packetint __thiscall(void *this, void *event)Handle main-menu server actions.Supports 0x00, 0x02, 0x03, and 0x0A.
Darkages.exe:0x004921F0ui_exit_wait_handle_server_packetint __thiscall(void *this, void *event)Handle server action 0x4C in the exit-wait pane.Subtype 0x01 sends CQuit subtype 0x00 and displays the safe-exit text; no reconnect call is made.
Darkages.exe:0x00492310ui_exit_wait_pane_ctorestablished __thiscall constructorCreate the exit-wait pane.Displays the wait warning and sends CQuit subtype 0x01.
Darkages.exe:0x004A2ED0ui_server_select_handle_server_packetint __thiscall(void *this, void *event)Deserialize the server table from action 0x56.Owns and replaces the per-server entry allocation.
Darkages.exe:0x0040B3A0ui_handle_server_request_portraitint __thiscall(void *this, void *event)Handle kServerRequestPortrait, action 0x49.Calls net_c_build_portrait_response.
Darkages.exe:0x0040A664net_c_build_portrait_responseestablished __thiscall builderBuild client action 0x4F from a local portrait.Called after server action 0x49.
Darkages.exe:0x004C33B2recvWinsock importRefill the TCP receive cache.Called with a maximum length of 0x18000.
Darkages.exe:0x004C33D6sendWinsock importSend a complete allocated frame.The caller does not handle a short successful write.
Darkages.exe:0x004C33D0WSAAsyncSelectWinsock importRegister socket notifications with the main window.Uses message 0x0402 and mask 0x21.