Kjetil's Information Center: A Blog About My Projects

M.I.A. in Wine

M.I.A.: Missing In Action is a relatively unknown game that came out for Windows back in 1998. I got the game back then in my childhood as part of some bundle with a new computer. I do remember the game being fun, but I had issues getting it to run later on because it will only install on Windows 98. Fast forward some 20 years, Wine has now become a better Windows than Windows on Linux in many aspects, especially running older games.
I have been using Wine version 4.0.2 for these experiments.

Using some tricks I finally got to run and play this on Linux! In order to get there I had do spend some time with both Winedbg and OllyDbg debuggers to figure out what the game tried to do and failed on.

The first problem was the detection of the CDs, which I figured out it does by calling GetVolumeInformationA() and looking at the volume label. This is fixed by creating a ".windows-label" file in the emulated CD drive with the correct label.

The second problem was getting the CD audio to work correctly. Apart from having to fake this somehow, the game uses the ancient Media Control Interface which still have some missing features (bugs?) in Wine at the time of writing. Maybe this will be fixed in an upcoming version, but I had no time to wait for that. The root of the problem is that Wine returns the code (as string) "1088" instead of the string "audio" when the game asks what type of track is on the CD. The quickest way to fix this is to simply patch the game binary to look for that other string.

To actually play the CD audio without the CDs I figured out it was easiest to hack the Wine "mcicda.dll" library and make it call MPlayer to play the tracks as .FLAC files. This is done in MPlayer's FIFO mode to avoid blocking anything.

A third problem is that the in-game video cutscenes, using Smacker Video Technology still does not play correctly in Wine. The symptom is that the video may play for some seconds, but then just hangs. Since it's possible to bypass this by hitting Escape, I have simply ignored this for now.

Anyway, the common steps to install and run are as follows:
1) Create a new directory to store a Wine prefix for M.I.A.:
mkdir -p ~/opt/mia
2) Run winecfg on the prefix, in 32-bit mode, and set it has "Windows 98":
WINEARCH=win32 WINEPREFIX=~/opt/mia winecfg
3) Create two directories for each M.I.A. CD in the prefix:
mkdir ~/opt/mia/cd1
mkdir ~/opt/mia/cd2
4) Copy the CD contents into the respective directories.
Either directly from the CDs or ISO images mounted as loopback devices.
5) Create fake volume labels for each CD, as needed by the game:
echo "MIA_VOL1" > ~/opt/mia/cd1/.windows-label
echo "MIA_VOL2" > ~/opt/mia/cd2/.windows-label
6) Create a symbolic link kalled "drive_d" pointing to "cd1"
ln -s "cd1" ~/opt/mia/drive_d
7) Run winecfg again to map D: to the newly created "drive_d" directory.
Also set the type as "CD-ROM".
WINEARCH=win32 WINEPREFIX=~/opt/mia winecfg
8) Allow low memory to be mapped, since it is needed by M.I.A. installer.
sudo sysctl vm.mmap_min_addr=0
9) Start the M.I.A. installation through Wine,
WINEARCH=win32 WINEPREFIX=~/opt/mia wine ~/opt/mia/drive_d/mia.exe
10) When prompted for installation of installation type...
Select "Leave ground textures on CD".
This is needed because the installer has issues finding CD #2.
11) Run the following script to easily start the game:

#!/bin/sh
export WINEPREFIX=~/opt/mia

sudo sysctl vm.mmap_min_addr=0 # Wine needs to be allowed to map low memory.

CD_NO=0
while [ "$CD_NO" != "1" ] && [ "$CD_NO" != "2" ]; do
  read -p "CD Number? (1 or 2) " CD_NO
done

rm -f "${WINEPREFIX}/drive_d"
ln -s "cd$CD_NO" "${WINEPREFIX}/drive_d"

(cd "${WINEPREFIX}/drive_c/MIA" && WINEARCH=win32 wine miarel.exe -avhpd)
          


If you also want the in-game music, some additional steps are required:
12) Assuming all tracks are ripped from the CD's in FLAC format.
4 audio tracks from CD #1 named from "track02.flac" to "track05.flac".
5 audio tracks from CD #2 mamed from "track02.flac" to "track06.flac".
Copy .flac files into the root of each respective directory "cd1" and "cd2".
13) Patch the "mcicda.dll" file in ~/opt/mia/drive_c/windows/system32/
14) Patch the "miarel.exe" file in ~/opt/mia/drive_c/MIA/
15) Run the following modified script to start the game:

#!/bin/sh
export WINEPREFIX=~/opt/mia

MPLAYER_FIFO=/tmp/mplayer.fifo # Do not change, hardcoded in patched mcicda.dll

sudo sysctl vm.mmap_min_addr=0 # Wine needs to be allowed to map low memory.

CD_NO=0
while [ "$CD_NO" != "1" ] && [ "$CD_NO" != "2" ]; do
  read -p "CD Number? (1 or 2) " CD_NO
done

rm -f "${WINEPREFIX}/drive_d"
ln -s "cd$CD_NO" "${WINEPREFIX}/drive_d"

if [ ! -p "$MPLAYER_FIFO" ]; then
  mkfifo "$MPLAYER_FIFO"
fi

cd "${WINEPREFIX}/drive_d"
mplayer -idle -slave -input file=$MPLAYER_FIFO 1&>/dev/null &
MPLAYER_PID=$!
cd -

(cd "${WINEPREFIX}/drive_c/MIA" && WINEARCH=win32 wine miarel.exe -avhpd)

kill $MPLAYER_PID
rm -f "$MPLAYER_FIFO"
          


To patch "miarel.exe", open it in a hex-editor and go to offset 0x1427c4. At this location the string "audio" should be present. Replace this with 0x31 0x30 0x38 0x38 0x00 which represents the string "1088" with an additional NULL terminator.

Patching "mcicda.dll" is more complicated as it requires a rebuild of Wine. Get the Wine source code and apply the following source code patch to "./dlls/mcicda/mcicda.c":

--- mcicda.c.orig	2020-05-13 17:55:02.433346437 +0200
+++ mcicda.c	2020-05-13 17:54:53.230346338 +0200
@@ -25,6 +25,12 @@
 #include <stdio.h>
 #include <string.h>
 
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <fcntl.h>
+#include <unistd.h>
+#include <limits.h>
+
 #define WIN32_NO_STATUS
 #include "windef.h"
 #include "winbase.h"
@@ -79,10 +85,72 @@
 typedef HRESULT(WINAPI*LPDIRECTSOUNDCREATE)(LPCGUID,LPDIRECTSOUND*,LPUNKNOWN);
 static LPDIRECTSOUNDCREATE pDirectSoundCreate;
 
+static void mplayer_command(const char *command)
+{
+    int fd, written;
+
+    fd = open("/tmp/mplayer.fifo", O_NONBLOCK | O_WRONLY);
+    if (fd == -1) {
+        TRACE("No pipe\n");
+        return;
+    }
+
+    written = write(fd, command, strlen(command));
+    if (written <= 0) {
+        TRACE("Write failed\n");
+    }
+
+    close(fd);
+}
+
 static BOOL device_io(HANDLE dev, DWORD code, void *inbuffer, DWORD insize, void *outbuffer, DWORD outsize, DWORD *retsize, OVERLAPPED *overlapped)
 {
     const char *str;
-    BOOL ret = DeviceIoControl(dev, code, inbuffer, insize, outbuffer, outsize, retsize, overlapped);
+//    BOOL ret = DeviceIoControl(dev, code, inbuffer, insize, outbuffer, outsize, retsize, overlapped);
+
+    int i;
+    BOOL ret = TRUE;
+    CDROM_TOC *toc;
+
+    *retsize = 0;
+
+    switch (code) {
+    case IOCTL_CDROM_READ_TOC:
+        toc = (CDROM_TOC *)outbuffer;
+
+        toc->FirstTrack = 1;
+        toc->LastTrack = 6;
+
+        // Set up first track as data track.
+        toc->TrackData[0].TrackNumber = 1;
+        toc->TrackData[0].Control = 0x4;
+        toc->TrackData[0].Address[1] = 0;
+        toc->TrackData[0].Address[2] = 0;
+        toc->TrackData[0].Address[3] = 0;
+
+        // Set up remaining tracks as dummy audio tracks.
+        for (i = 1; i < toc->LastTrack; i++) {
+            toc->TrackData[i].TrackNumber = i + 1;
+            toc->TrackData[i].Control = 0;
+            toc->TrackData[i].Address[1] = i;
+            toc->TrackData[i].Address[2] = 0;
+            toc->TrackData[i].Address[3] = 0;
+        }
+
+        *retsize = CDROM_TOC_SIZE;
+        break;
+
+    case IOCTL_CDROM_STOP_AUDIO:
+        mplayer_command("stop\n");
+        break;
+
+    case IOCTL_CDROM_PAUSE_AUDIO:
+        mplayer_command("stop\n");
+        break;
+
+    default:
+        break;
+    }
 
 #define XX(x) case (x): str = #x; break
     switch (code)
@@ -906,6 +974,9 @@
     SUB_Q_CHANNEL_DATA          data;
     CDROM_TOC			toc;
 
+    int track_no;
+    char command[PATH_MAX];
+
     TRACE("(%04X, %08X, %p);\n", wDevID, dwFlags, lpParms);
 
     if (lpParms == NULL)
@@ -914,6 +985,20 @@
     if (wmcda == NULL)
 	return MCIERR_INVALID_DEVICE_ID;
 
+    // HIJACK START
+
+    mplayer_command("stop\n");
+
+    track_no = MCI_TMSF_TRACK(lpParms->dwFrom);
+    TRACE("Track no: %d\n", track_no);
+
+    snprintf(command, PATH_MAX, "loadfile track%02d.flac\n", track_no);
+    mplayer_command(command);
+
+    return 0;
+
+    // HIJACK END
+
     if (!MCICDA_ReadTOC(wmcda, &toc, &br))
         return MCICDA_GetError(wmcda);
          

To build the DLL file, it should be enough to run ./configure and make on the Wine source code. The resulting file will be named "mcicda.dll.so" but can be renamed to "mcicda.dll".

Topic: Configuration, by Kjetil @ 24/05-2020, Article Link