PSU lives on the Clementine server. Support the people who run it. Support Clementine on Patreon (opens in a new tab)

← Dev blog

The first time the Mac client got into the lobby

This is a private educational proof of concept for the Mac client of a private PSU server. I asked the Clementine staff first, and they said it was fine to go ahead as long as it does not disrupt other players. They also said the client belongs to Sega, and I agree. Nothing public ships, nothing from the game is in anything I build, and I am not affiliated with SEGA or the Clementine team.

With that said, the client now logs in and reaches the in-game lobby with other players, on my Mac, with no Wine anywhere. This post is about how it got there and how much of that I can honestly claim.

The run

The longest session so far was 254 seconds in the lobby. It ended when I pressed Cmd-Q, and the game closed normally. No crash.

The client made 3 connections to the server and all 3 completed. It received 1,737,586 bytes and sent 86,856. My network layer refused 0 connections, and the only error the game ever saw was the ordinary "this would block" answer that a non-blocking socket gives all day long.

It presented 7,162 frames at the game's own pacing, which works out to about 28 frames a second. I do not uncap the frame rate, since the Clementine rules say not to.

The graphics bridge handled 68 distinct Direct3D 9 methods and about 13.0 million calls, 1.8 million of them draw calls with indexed geometry, plus vertex buffers, index buffers and vertex declarations. The number of methods it could not translate was 0. The game called 213 distinct Windows functions and none of them were missing from my runtime. For sound it created 126 buffers and the loudest sample peaked at 0.972 of full scale. It received 678 key events and 561 mouse events.

All of these come from the summary the runtime prints when the game exits.

What had to be fixed, in the order it happened

The game wants to talk to a server, in a chain. The first step was just letting it connect. It reaches one server, and that server tells it which address and port to use next, and then that one does the same. The network layer has a short allow list, and its log records only how many bytes moved, never what was in them. It also has a kill switch and a time cap, which I will come back to.

The update step asks for a launcher I do not run. On the first run the game downloaded an update, somewhere between 40 and 57 MB. It writes the new files with a temporary ending, and then it asks for the separate launcher program to rename them into place and start the game again. I refuse that request on purpose, because I never run the real launcher. My own launcher does the renames inside the copy of the install, then restarts the game with the same settings. The player's original folder is not touched.

The game reads the network adapter address through a Windows DLL I did not have. After choosing a server the game jumped to address zero and died. I traced it by reading the code. The game loads a small Windows library to ask the system for the network adapter address, looks up four functions in it, and then calls one of them without checking that it exists. Windows always has that library. My runtime did not, so the lookup failed quietly and the call went into nothing. I wrote a stand-in that answers the same questions. It reports the real address of the Mac's main network adapter, which is what a normal Windows client does with its own adapter. The address never goes into a log or a crash report.

Windows reuses socket numbers, and the game counted on it. The game closes a connection, leaves the old socket number in a list it checks every frame, and then opens the next connection. On Windows the new socket usually gets the same number back, so the stale entry is harmless. My runtime handed out ever increasing numbers. The next check named a socket that no longer existed, the whole check failed, and the game decided it could not connect. I changed the numbering to reuse the most recently freed number first. It only showed up after I logged every connection outcome and read the game's polling code.

A sound buffer returned the wrong object. The first time I got into the lobby, it crashed after about 65 seconds. The game asks a sound buffer for its 3D interface. My code answered every such question by handing back the buffer itself. The game then made 3D calls on it, and those calls landed on the wrong functions, which take a different number of arguments. Each one left the stack off by a few bytes, and a few calls later the game returned to garbage. The fix was to answer only the questions that should be answered, and to return a small separate 3D object that stores its settings and does nothing else. The sound is not positioned in 3D space. It just plays.

My own time cap cut the network. I start every networked run with a limit on how many seconds the network may stay up, so a run I forget about cannot sit on the server. The cap expired overnight and shut networking off for good, as designed, and the game then looked broken in a way that had nothing to do with the code. That one was my mistake and a reminder to read the log line before I suspect the runtime.

What I have not verified

I saw the lobby with other players in a live session. The numbers above come from a terminal summary, though, and I have not checked them against a recording of the screen.

A second server shows an account error. As far as I know, my account is not set up on that server. I have not confirmed that, and I have not tried to work around it.

I have played for a few minutes in total. Controllers and input method editors are untested. Fullscreen online is untested. 254 seconds is one data point.

Public release is not approved. The staff said private use is fine. Sharing builds publicly still needs an explicit yes, and I have not asked for one yet.