Kjetil's Information Center: A Blog About My Projects

Outlaws in Wine

I have made an effort in getting the classic LucasArts game Outlaws working in Wine with music, which is essential due to its excellent soundtrack. This is a similar to my other effort with M.I.A., except this time the music playback mechanism is more advanced. I had to make additional hacks to the Wine "mcicda" library to make it pause and resume in the middle of tracks.

Here is a rough guide for the commands required for installation. Also, when the installation asks about DirectX 3.0A, just skip this.

mkdir -p ~/opt/outlaws
mkdir ~/opt/outlaws/cd1 # Then copy files from CD1 into here.
mkdir ~/opt/outlaws/cd2 # Then copy files from CD2 into here.
echo "OUTLAWS_1" > ~/opt/outlaws/cd1/.windows-label
echo "OUTLAWS_2" > ~/opt/outlaws/cd2/.windows-label
ln -s "cd1" ~/opt/outlaws/drive_d
WINEARCH=win32 WINEPREFIX=~/opt/outlaws winecfg # D: = "drive_d" = CD-ROM
WINEARCH=win32 WINEPREFIX=~/opt/outlaws wine ~/opt/outlaws/drive_d/SETUP.EXE
          


I had to enable the "Virtual Desktop" setting with "winecfg" for certain stuff like menus to work. I also just configured my X windows resolution to 800x600 before playing the game.

To make the music playback work, a lot of additional steps are required. First the patched "mcicda.dll", which can be downloaded here. This should placed at "~/opt/outlaws/drive_c/windows/system32/mcicda.dll"

Here is the patched code in case you want to compile it yourself, using the Wine 4.0.2 source code as a basis:

--- mcicda.c.orig	2020-08-22 14:22:13.861217377 +0200
+++ mcicda.c	2020-08-22 14:22:18.323217425 +0200
@@ -20,11 +20,21 @@
  * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
  */
 
+#define MPLAYER_FIFO_LOCATION "/tmp/mplayer.fifo"
+#define TOC_FILE_LOCATION "/tmp/toc.txt"
+
 #include "config.h"
 #include <stdarg.h>
 #include <stdio.h>
 #include <string.h>
 
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <fcntl.h>
+#include <unistd.h>
+#include <limits.h>
+#include <time.h>
+
 #define WIN32_NO_STATUS
 #include "windef.h"
 #include "winbase.h"
@@ -79,10 +89,211 @@
 typedef HRESULT(WINAPI*LPDIRECTSOUNDCREATE)(LPCGUID,LPDIRECTSOUND*,LPUNKNOWN);
 static LPDIRECTSOUNDCREATE pDirectSoundCreate;
 
+static int mplayer_current_track = 0;
+static int mplayer_current_min   = 0;
+static int mplayer_current_sec   = 0;
+static int mplayer_current_frame = 0;
+static struct timespec mplayer_playback_started = {0,0};
+
+static void mplayer_command(const char *command)
+{
+    int fd, written;
+
+    fd = open(MPLAYER_FIFO_LOCATION, 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);
+
+    int track_no, min, sec, frame;
+    BOOL ret = TRUE;
+    CDROM_TOC *toc;
+    CDROM_SUB_Q_DATA_FORMAT *qfmt;
+    SUB_Q_CHANNEL_DATA *qdata;
+    FILE *fh;
+    char buf[16];
+    struct timespec now;
+
+    *retsize = 0;
+
+    switch (code) {
+    case IOCTL_CDROM_READ_TOC:
+        toc = (CDROM_TOC *)outbuffer;
+        *retsize = CDROM_TOC_SIZE;
+
+        toc->Length[0] = 0;
+        toc->Length[1] = 0;
+        toc->FirstTrack = 1;
+        toc->LastTrack = 1;
+
+        // 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;
+
+        // Get other audio tracks from toc file.
+        fh = fopen(TOC_FILE_LOCATION, "r");
+        if (fh == NULL) {
+          TRACE("IOCTL_CDROM_READ_TOC, Failed to open: %s\n", TOC_FILE_LOCATION);
+          break;
+        }
+
+        while (fgets(buf, sizeof(buf), fh) != NULL)
+        {
+          sscanf(buf, "%02d:%02d:%02d", &min, &sec, &frame);
+
+          toc->TrackData[toc->LastTrack].TrackNumber = toc->LastTrack + 1;
+          toc->TrackData[toc->LastTrack].Control = 0;
+          toc->TrackData[toc->LastTrack].Address[1] = min;
+          toc->TrackData[toc->LastTrack].Address[2] = sec;
+          toc->TrackData[toc->LastTrack].Address[3] = frame;
+
+          toc->LastTrack++;
+
+          TRACE("IOCTL_CDROM_READ_TOC, Track %d = %02d:%02d:%02d\n", toc->LastTrack, min, sec, frame);
+        }
+
+        toc->LastTrack--; // Remove the last dummy track:
+
+        fclose(fh);
+        break;
+
+    case IOCTL_CDROM_STOP_AUDIO:
+        mplayer_command("stop\n");
+        break;
+
+    case IOCTL_CDROM_PAUSE_AUDIO:
+        mplayer_command("stop\n");
+        break;
+
+    case IOCTL_CDROM_READ_Q_CHANNEL:
+        qfmt  = (CDROM_SUB_Q_DATA_FORMAT *)inbuffer;
+        qdata = (SUB_Q_CHANNEL_DATA *)outbuffer;
+        *retsize = sizeof(SUB_Q_CHANNEL_DATA);
+
+        if (qfmt->Format == IOCTL_CDROM_CURRENT_POSITION)
+        {
+            qdata->CurrentPosition.FormatCode = IOCTL_CDROM_CURRENT_POSITION;
+            qdata->CurrentPosition.Control = 0;
+            qdata->CurrentPosition.ADR = 0;
+            qdata->CurrentPosition.TrackNumber = mplayer_current_track;
+            qdata->CurrentPosition.IndexNumber = 0;
+
+            clock_gettime(CLOCK_MONOTONIC, &now);
+            TRACE("IOCTL_CDROM_READ_Q_CHANNEL, Started = %lu.%lu\n",
+                mplayer_playback_started.tv_sec, mplayer_playback_started.tv_nsec);
+            TRACE("IOCTL_CDROM_READ_Q_CHANNEL, Now = %lu.%lu\n", now.tv_sec, now.tv_nsec);
+
+            min = (now.tv_sec - mplayer_playback_started.tv_sec) / 60;
+            sec = (now.tv_sec - mplayer_playback_started.tv_sec) % 60;
+            frame = (now.tv_nsec - mplayer_playback_started.tv_nsec);
+            if (frame < 0) {
+                frame = 0 - frame;
+                sec--;
+                if (sec < 0) {
+                  sec = 59;
+                  min--;
+                }
+            }
+            frame = (frame / 10000000) * 0.75;
+
+            if (mplayer_current_min > 0 ||
+                mplayer_current_sec > 0 ||
+                mplayer_current_frame > 0)
+            {
+                min += mplayer_current_min;
+                sec += mplayer_current_sec;
+                if (sec >= 60) {
+                    sec -= 60;
+                    min++;
+                }
+                frame += mplayer_current_frame;
+                if (frame >= 76) {
+                    frame -= 76;
+                    sec++;
+                    if (sec >= 60) {
+                        sec -= 60;
+                        min++;
+                    }
+                }
+            }
+
+            qdata->CurrentPosition.TrackRelativeAddress[0] = 0;
+            qdata->CurrentPosition.TrackRelativeAddress[1] = min;
+            qdata->CurrentPosition.TrackRelativeAddress[2] = sec;
+            qdata->CurrentPosition.TrackRelativeAddress[3] = frame;
+
+            qdata->CurrentPosition.AbsoluteAddress[0] = 0;
+            qdata->CurrentPosition.AbsoluteAddress[1] = min;
+            qdata->CurrentPosition.AbsoluteAddress[2] = sec;
+            qdata->CurrentPosition.AbsoluteAddress[3] = frame;
+
+            fh = fopen(TOC_FILE_LOCATION, "r");
+            if (fh == NULL) {
+              TRACE("IOCTL_CDROM_READ_Q_CHANNEL, Failed to open: %s\n", TOC_FILE_LOCATION);
+              break;
+            }
+
+            track_no = 2;
+            while (fgets(buf, sizeof(buf), fh) != NULL)
+            {
+                if (track_no == mplayer_current_track) {
+                    sscanf(buf, "%02d:%02d:%02d", &min, &sec, &frame);
+                    qdata->CurrentPosition.AbsoluteAddress[1] += min;
+                    qdata->CurrentPosition.AbsoluteAddress[2] += sec;
+                    if (qdata->CurrentPosition.AbsoluteAddress[2] >= 60) {
+                        qdata->CurrentPosition.AbsoluteAddress[2] -= 60;
+                        qdata->CurrentPosition.AbsoluteAddress[1]++;
+                    }
+                    qdata->CurrentPosition.AbsoluteAddress[3] += frame;
+                    if (qdata->CurrentPosition.AbsoluteAddress[3] >= 76) {
+                        qdata->CurrentPosition.AbsoluteAddress[3] -= 76;
+                        qdata->CurrentPosition.AbsoluteAddress[2]++;
+                        if (qdata->CurrentPosition.AbsoluteAddress[2] >= 60) {
+                            qdata->CurrentPosition.AbsoluteAddress[2] -= 60;
+                            qdata->CurrentPosition.AbsoluteAddress[1]++;
+                        }
+                    }
+                    break;
+                }
+                track_no++;
+            }
+
+            fclose(fh);
+
+            TRACE("IOCTL_CDROM_READ_Q_CHANNEL, Current Track = %d\n", mplayer_current_track);
+            TRACE("IOCTL_CDROM_READ_Q_CHANNEL, Rel Pos = %02d:%02d:%02d\n",
+                qdata->CurrentPosition.TrackRelativeAddress[1],
+                qdata->CurrentPosition.TrackRelativeAddress[2],
+                qdata->CurrentPosition.TrackRelativeAddress[3]);
+            TRACE("IOCTL_CDROM_READ_Q_CHANNEL, Abs Pos = %02d:%02d:%02d\n",
+                qdata->CurrentPosition.AbsoluteAddress[1],
+                qdata->CurrentPosition.AbsoluteAddress[2],
+                qdata->CurrentPosition.AbsoluteAddress[3]);
+        }
+        else
+        {
+           TRACE("IOCTL_CDROM_READ_Q_CHANNEL, Unknown format: %d\n", qfmt->Format);
+        }
+        break;
+
+    default:
+        break;
+    }
 
 #define XX(x) case (x): str = #x; break
     switch (code)
@@ -906,6 +1117,9 @@
     SUB_Q_CHANNEL_DATA          data;
     CDROM_TOC			toc;
 
+    int track_no, min, sec, frame;
+    char command[PATH_MAX];
+
     TRACE("(%04X, %08X, %p);\n", wDevID, dwFlags, lpParms);
 
     if (lpParms == NULL)
@@ -914,6 +1128,35 @@
     if (wmcda == NULL)
 	return MCIERR_INVALID_DEVICE_ID;
 
+    mplayer_command("stop\n");
+
+    track_no = MCI_TMSF_TRACK(lpParms->dwFrom);
+    min      = MCI_TMSF_MINUTE(lpParms->dwFrom);
+    sec      = MCI_TMSF_SECOND(lpParms->dwFrom);
+    frame    = MCI_TMSF_FRAME(lpParms->dwFrom);
+    TRACE("Track no: %d (%02d:%02d:%02d)\n", track_no, min, sec, frame);
+
+    snprintf(command, PATH_MAX, "loadfile track%02d.flac\n", track_no);
+    mplayer_command(command);
+
+    mplayer_current_track = track_no;
+    clock_gettime(CLOCK_MONOTONIC, &mplayer_playback_started);
+
+    if (min > 0 || sec > 0 || frame > 0) {
+      mplayer_current_min = min;
+      mplayer_current_sec = sec;
+      mplayer_current_frame = frame;
+
+      sec += (min * 60);
+      frame *= 1.333333;
+      TRACE("Seek to: %d.%02d\n", sec, frame);
+
+      snprintf(command, PATH_MAX, "seek %d.%02d 2\n", sec, frame);
+      mplayer_command(command);
+    }
+
+    return 0; // Because of hijacking, this ends here.
+
     if (!MCICDA_ReadTOC(wmcda, &toc, &br))
         return MCICDA_GetError(wmcda);
          


To make music playback work as painlessly as possible, I have once again made a script to start the game:

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

MPLAYER_FIFO=/tmp/mplayer.fifo
TOC_FILE=/tmp/toc.txt
MPLAYER_PID_FILE=/tmp/mplayer.pid

function mplayer_stop {
  if [ -f "$MPLAYER_PID_FILE" ]; then
    kill `cat "$MPLAYER_PID_FILE"`
    rm -f "$MPLAYER_PID_FILE"
  fi
  rm -f "$MPLAYER_FIFO"
}

function mplayer_start {
  if [ ! -p "$MPLAYER_FIFO" ]; then
    mkfifo "$MPLAYER_FIFO"
  fi
  cd "${WINEPREFIX}/drive_d"
  mplayer -vo null -idle -slave -input file=$MPLAYER_FIFO 1&>/dev/null &
  echo "$!" > "$MPLAYER_PID_FILE"
  cd -
}

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

MODE="0"
while [ "$MODE" != "g" ] && [ "$MODE" != "c" ]; do
  read -p "Start [g]ame or [c]hange CD only? " MODE
done

mplayer_stop

rm -f "${WINEPREFIX}/drive_d"
ln -s "cd$CD_NO" "${WINEPREFIX}/drive_d"
cp "${WINEPREFIX}/drive_d/toc.txt" "$TOC_FILE"

mplayer_start

if [ "$MODE" == "c" ]; then
  # If only changing CD, exit now.
  exit 0
fi

(cd "${WINEPREFIX}/drive_c/Program Files/LucasArts/Outlaws" && WINEARCH=win32 wine OLWIN.EXE)

mplayer_stop
rm -f "$TOC_FILE"
          

The same script is also used to change the CD while the game is running, as this is required in some instances.

The CD audio tracks should be ripped to FLAC format and be placed as track02.flac to track08.flac in "~/opt/outlaws/cd1/" for CD1, followed by track02.flac to track09.flac in "~/opt/outlaws/cd2/" for CD2. In addition, you will need a couple of "Table of Contents" files for each CD. These will inform the patched "mcicda.dll" file about the length of each track.

Put the following in "~/opt/outlaws/cd1/toc.txt"

00:00:00
03:16:64
07:34:64
11:23:01
15:13:33
19:58:24
23:39:66
27:15:61
          


Put the following in "~/opt/outlaws/cd2/toc.txt"

00:00:00
04:42:15
10:16:03
13:41:47
20:55:08
24:58:10
30:09:45
33:50:23
36:16:17
          


Enjoy!

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