Skip to content

Show server ping in the public server list - #948

Open
jqntn wants to merge 5 commits into
tiltedphoques:devfrom
jqntn:feat/server-list-ping
Open

jqntn wants to merge 5 commits into
tiltedphoques:devfrom
jqntn:feat/server-list-ping

Conversation

@jqntn

@jqntn jqntn commented Nov 5, 2026 •

Copy link
Copy Markdown

Adds a Ping column to the public server list, so players can find a server with a low ping before they join. Players can sort the list by ping. A server that does not answer shows no ping.

Before:
list-1080p

After:
list-1080p

2560×1440, 3840×2160, and sorted by ping list-1440p list-2160p list-1080p-sorted

How it works:

  • When the list opens or the player presses Refresh, the UI sends all listed servers in one pingServers call, with compatible servers first. Each server gets one probe.
  • The client sends each server a GameNetworkingSockets ChallengeRequest over UDP. This is the first packet of a connection. It holds an id from the UI in connection_id and the send time in my_timestamp.
  • The server's listen socket answers with a ChallengeReply, which echoes these two values. The server does not open a connection and does not log the packet. So this works with all running servers, and the server needs no change.
  • A receive thread timestamps each reply when it arrives, and OverlayService sends serverPing(id, ms) to the UI on the next frame. Reading the replies once per frame instead made values 8 ms too high on average at 60 fps.
  • The client sends one probe per frame, at most one every 10 ms, so a slow upload does not make later probes read high. A list of 100 servers fills in over about 1.7 s at 60 fps, and the servers shown by default get a ping within about 0.3 s.
  • One dual-stack UDP socket sends to IPv4 and IPv6 servers. Each list load or Refresh opens a new socket and thread, and closing the socket stops the thread.
  • TiltedOnlinePCH.h now includes winsock2.h before windows.h. Without this, windows.h includes the old winsock.h, and the IPv6 types from ws2tcpip.h do not compile. No other client code used Winsock, and the client builds with the same warnings as before.

Packet layout, from GameNetworkingSockets v1.4.1 (the version in xmake.lua): message definitions, server reply. The request must be padded to 512 bytes.

This also fixes three alignment bugs in the list:

  • The header kept 1rem free for the scrollbar, but the scrollbar is 0.7rem (--scrollbar-thickness). The header columns were about 5px to the left of the rows.
  • A long server name made the fixed-width columns of its row narrower. These columns moved out of line with the header, by up to 255px at 1920×1080 with the XL font. The name now uses the remaining space and ends with an ellipsis after two lines.
  • A long version string went over the Favorite column.
Before After
Header and scrollbar (1920×1080, 2× zoom) bug1-header-offset bug1-header-offset
Long name (1920×1080, L font) bug2-long-name bug2-long-name
Long version string (1920×1080, 2× zoom) bug3-version bug3-version

The blue lines mark the left edge of each header cell. They are not part of the UI.

New key: COMPONENT.SERVER_LIST.TABLE_HEADERS.PING. Most languages use "Ping". Korean uses 핑, Russian Пинг, Ukrainian Пінг and Chinese 延迟.

This also fixes a typo in the French Connect box. "Addresse" is now "Adresse" in the address field and in its format hint.

Tested

  • Probe: I compiled the probe code alone, with the same headers, and sent it to the live server list (100 servers). 79 to 82 of 93 IPv4 servers and 1 of 7 IPv6 servers answered, depending on the run.
  • Errors, Refresh and shutdown: a closed port gives WSAECONNRESET, and the thread keeps reading. A Refresh while probes are still going out reopens the socket and thread in under 0.5 ms, and no late reply from the earlier list gets through. On shutdown, the thread stops (WSAEINTR) and is joined in less than 1 ms.
  • Build: SkyrimTogetherClient, SkyrimImmersiveLauncher and TPProcess build in releasedbg with MSVC 14.51 (VS 2026). On dev, SkyrimImmersiveLauncher does not compile with this MSVC (constinit in MemoryLayout.cpp). I removed constinit locally only, to check the link.
  • UI: browser mode (ng serve --configuration=browser) on Chrome for Testing 141.0.7390.122. The game's CEF 141.0.11 uses Chromium 141.0.7390.123. I checked the live server list and the mock list at 6 resolutions, from 1280×720 to 3840×2160, and at 1920×1080 with all 5 font sizes. All headers line up with their columns (less than 1px), no cell overflows its width, and no name or ping is taller than its row. The Ping header fits in all 15 languages with the M and XL fonts. ng build --configuration=production passes.

Not tested in game.

The screenshots use a mock server list with fixed ping values.

Replaces #943, after the discussion there. Part of #374.

jqntn added 2 commits November 6, 2026 00:29
Players could not compare the latency of public servers without joining
each one (tiltedphoques#374). Add a Ping column that players can sort.

The client sends each listed server the first packet of a
GameNetworkingSockets connection. The server answers it without opening
a connection, so this works with all running servers and needs no
server change.

Also align the header with its columns. It kept 1rem free for a 0.7rem
scrollbar, and long names or version strings pushed the fixed-width
columns out of line.

Fix the "Addresse" typo in the French Connect box.
The request struct and the reply check repeated the same protocol values.
Name each value once, and check `MsgLength` against the real body size in
the `static_assert`, so a wrong length fails the build.
@jqntn
jqntn force-pushed the feat/server-list-ping branch from e82b776 to a02963b Compare November 5, 2026 23:58
@jqntn
jqntn marked this pull request as ready for review November 5, 2026 23:58
jqntn added 3 commits November 6, 2026 01:36
The client read ping replies once per frame, so each value was up to one
frame too high: 8 ms on average at 60 fps, more at lower frame rates.
Read them on a thread that timestamps each reply when it arrives, and
send the results to the UI on the next frame.

Each list load or Refresh opens a new socket and thread. A setup failure
or a stopped thread then lasts only until the next Refresh, and late
replies to an earlier list are dropped. Log these failures, and keep
reading after `WSAENETRESET`, which an expired earlier ping can cause.
The client sent a probe to every listed server at once, about 50 KB for
100 servers. On a slow upload the last probes wait behind the first ones,
about 0.4 s at 1 Mbps, and their ping reads that much too high. Send one
probe per frame, at most one every 10 ms. A list of 100 servers then
fills in over about 1.7 s at 60 fps.
Probes go out one per frame in list order, and the list hides servers
with another version by default, often most of them. Send the servers
with the client's version first, so the servers the player sees get a
ping within about 0.3 s instead of up to 1.4 s.
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.

1 participant