Patching Pinball to make its music work under Wine

Lately I've been going through a sort of "old software phase", in which I did everything from writing my first TSR in 2021 (!) to getting Windows 3.1 running natively on my Core i7 machine from 2014 (albeit in Standard Mode)! After all of that, I decided it was time to have a bit more fun, so I dug up 3D Pinball Space Cadet, that nice little game that came with versions of Windows NT from 4.0 to XP.

Once I figured out which files I needed to copy from the Windows XP CD, it was a simple enough matter of getting it up and running under Wine, except that the music didn't work (sound effects were fine). That seemed fair enough, since I noticed the music was in MIDI format, and I hadn't done any work to get MIDI working under Wine. If I wanted music, I could run it in my Windows 98 VM under qemu (although I had to reduce the background load on my system to make said VM run smoothly enough for that).

In the end though, I decided that booting Windows 98, and reducing my background load, was too tall an order just to play Pinball with music, so I did take it upon myself to get MIDI working with Wine. I soon figured out how to get fluidsynth running as a user systemd service, and at that stage it was pretty plug-and-play. Despite all the references to ALSA in the linked article, the whole thing now works fine in PulseAudio mode, which is great. I was able to use the mcishell tool mentioned there to open and play PINBALL.MID with no issue. Sure, it sounds a bit different due to the different soundfont, but at least it plays. So now, I could enjoy the full Pinball experience with just Wine – or so I thought!

In fact, when I launched the game and turned on music on the options menu, I still didn't hear the catchy tune. Instead, I got this mystifying line printed in the terminal:

0024:fixme:mci:MCI_LoadMciDriver Couldn't load driver for type L"PINBALL.MID".

Strangely enough, I couldn't find any other references to this exact line by searching on the web. There were similar ones suggesting to adjust system.ini to include more media types, but that didn't make any sense. Why on earth would it be trying to open media of 'type L"PINBALL.MID"'? As opposed to, say 'type L"MPEGVIDEO"', as mentioned here.

For the heck of it, I added the following lines to the [mci] section of ~/.wine/drive_c/windows/system.ini:

; AAAAAAAAAAARRRGHHH... :(
PINBALL.MID=mciseq.dll

Of course, that didn't work – setting WINEDEBUG=+mci,mcimidi and running Pinball revealed that now it was successfully opening the MIDI sequencer device, but it didn't know what file to play!

One other thing I tried was swapping the pinball.exe file I had from the Windows XP CD with the version from the Windows NT 4.0 CD. Interestingly, they are not identical – the latter contains relocation information (and hence can run on Windows 3.1 with Win32s), while the former does not! But anyway, this made no difference to the issue at hand…

With that, I dug into the Wine source code (specifically dlls/winmm/mci.c and include/mmsystem.h) and it became clear that the MCI "open" call from Pinball was ending up on a codepath where it was supposed to have specified the media type (lpstrDeviceType) rather than the filename (lpstrElementName). The only plausible reason was that Pinball was simply sending a malformed MCI request!

I fired up IDA and disassembled Pinball. Given that it's a standard PE-format executable, it was no trouble at all to find calls to the imported symbol mciSendCommandA. After identifying and assigning the appropriate type to the MCI_OPEN_PARMSA struct (again easy enough due to IDA's built-in knowledge of Windows API types), the code surrounding the first such call looked something like this:

push    offset mciParams ; dwParam2
mov     mciParams.lpstrDeviceType, edi
push    2002h           ; dwParam1
mov     mciParams.lpstrElementName, esi
push    MCI_OPEN        ; uMsg
push    esi             ; mciId
call    ds:mciSendCommandA
test    eax, eax
jnz     short loc_1E27BBA

Where edi contains a pointer to the PINBALL.MID string, and esi is set to zero. Furthermore, the parameter 2002h is equivalent to MCI_OPEN_TYPE | MCI_WAIT. So, yes, this API call is definitely trying to open an MCI device of type "PINBALL.MID", for some unfathomable reason.

My next step was to fire up winedbg --gdb to see if I could modify this behaviour. I set a breakpoint on the first offending line (the one setting mciParams.lpstrDeviceType) and proceeded to perform surgery on it and the two following lines, as I single-stepped through them. I changed the 2002h to a 202h (i.e. changing MCI_OPEN_TYPE to MCI_OPEN_ELEMENT) and swapped the assignments of lpstrDeviceType and lpstrElementName. I then allowed execution to continue, and lo and behold, the music worked!

Finally, I used IDA's "patch program" feature to make the fix permanent. My steps were roughly as follows:

  • I highlighted the instructions assigning lpstrDeviceType and lpstrElementName in the IDA View, switched to Hex View, and noted the positions of the 89 3D and 89 35 opcodes, corresponding to edi and esi source operands respectively. (If you're following along, keep in mind that the destination operands will be of the form dword_XXX in a fresh disassembly!)
  • I went "Edit > Patch program > Change byte…" and swapped the positions of 3D and 35 in the sequence of bytes.
  • I went back to the IDA View and highlighted the push 2002h instruction.
  • I used "Edit > Patch program > Assemble…" to change it to push 202h.
  • Finally, I used "Edit > Patch program > Apply patches to input file…" to create a new pinball.exe sending correctly-formed MCI requests!

So now I can enjoy Pinball with music under Wine, but I'm still scratching my head as to why it even works on Windows – as mentioned, it even works under Windows 3.1 with Win32s! Even stranger, someone did report it working on an old version of Wine (albeit Staging), even though the relevant source files don't seem to have been touched in over a decade…

I can only guess that the Windows API has some idiot-proofing, where if the DeviceType isn't found, it assumes the programmer meant to specify an ElementName, with the corresponding swap in call from MCI_OPEN_TYPE to MCI_OPEN_ELEMENT. In fairness, Microsoft have done a lot of work in APIs down through the years to try to prevent "Garbage In" from invariably resulting in "Garbage Out", which can be a good or a bad thing depending on your perspective! It should be possible to patch Wine to do the same thing, but I haven't yet been able to determine what exactly Windows is doing…

Anyway, hopefully now that the error message involving type L"PINBALL.MID" is up here on the web, this article might help someone else who wants to get music working!

links

social