During the COVID lockdowns my son and I started playing The Settlers II in DOSBox.
One of the coolest features of the game is that two players can play on the same computer in split screen, each controlling their own cursor — a surprisingly social multiplayer mode for a 1996 strategy game.
The trick is that the second player uses a serial mouse.
Unfortunately modern operating systems don't really expose the concept of multiple independent mice anymore — they all get merged into a single pointer.
So if I wanted that old-school multiplayer experience back, I needed an adapter.
My first thought was simple: maybe I can fake a serial mouse?
In Unix systems everything is a file. If DOSBox expects a serial device, perhaps I can generate the right byte stream and feed it to it?
Reading through the Linux docs and trying to hexdump the mouse device I realized I need to convert from PS/2 mouse protocol into Microsoft serial mouse protocol. The descriptions of both I quickly found on the Internet: https://roborooter.com/post/serial-mice/.
Quickly noticing some key differences:
- Both communicate via groups of three bytes;
- The most significant bit of each byte is used for framing in the Microsoft protocol, so the high bits of the coordinates are packed into the first byte;
- Both encode movement deltas, but PS/2 uses sign bits in the status byte while the Microsoft serial protocol splits the high bits of the coordinates into the first byte;
- As a result PS/2 effectively has one extra bit of precision for each axis.
The first idea was to just write the packets into a pipe and connect DOSBox to it.
Unfortunately, DOSBox expects something that behaves like a real serial device, not just a pipe.
Eventually I landed on socat, which can create a pair of pseudo-terminals:
socat -d -d pty,raw,echo=0 pty,raw,echo=0
This creates two linked devices:
/dev/pts/24
/dev/pts/25
Whatever you write to one appears on the other.
Now DOSBox can connect to one side:
serial1=directserial realport:pts/25 rxdelay:0
And the script writes to the other.
The first version worked — but the mouse felt strange: movements were extremely smooth and continued even after I stopped moving the mouse.
Modern mice have extremely high DPI, which means the adapter was sending a huge number of tiny movements.
DOSBox replayed them with a delay.
The solution was simple: accumulate movement and send it in larger steps.
I deliberately didn’t try to make it neat or reusable — it only needed to run on my laptop:
import struct
def send(conn, x, y):
(x,) = struct.pack('b', x)
(y,) = struct.pack('b', y)
#
byte1 = 0b01000000
byte1 += (x & 0b11000000) >> 6
byte1 += (y & 0b11000000) >> 4
#
buff = [byte1]
buff.append(x & 0b00111111)
buff.append(y & 0b00111111)
conn.write(bytes(buff))
conn.flush()
BUNDLING = 1
SENSITIVITY = 1
with open('/dev/pts/24', 'wb', 0) as conn:
acc_dx = acc_dy = 0
with open('/dev/input/mouse1', 'rb', 0) as mouse:
print("let's get it started!")
while True:
b1, dx, dy = mouse.read(3)
if b1 & 0b00010000:
dx += -256
if b1 & 0b00100000:
dy += -256
acc_dx += dx
acc_dy += dy
dx = dy = 0
if abs(acc_dx) >= BUNDLING:
dx = int(acc_dx // SENSITIVITY)
acc_dx -= dx * SENSITIVITY
if abs(acc_dy) >= BUNDLING:
dy = int(acc_dy // SENSITIVITY)
acc_dy -= dy * SENSITIVITY
if dx or dy:
send(conn, dx, -dy)
As you can see the script is very simple:
- Main loop reads PS/2 packets from
/dev/input/mouse1; - Decodes movement deltas;
- Accumulates them over time;
- Applies
BUNDLINGto avoid overwhelming DOSBox with packets; - Applies
SENSITIVITYso that our high-DPI mouse feels good in small resolution of the old game; - Every now and then the
sendroutine builds MS Serial Mouse packets back from the accumulated movements and sends it to theptsdevices so thatsocatmirrors it into the otherptswhich DOSBox will read.
Note that The Microsoft protocol has slightly lower resolution for movement deltas than PS/2, so very large movements would technically need to be split across multiple packets. In practice this isn't an issue because mice report movement in small increments.
The UNIX way
In the end the entire adapter was about 40 lines of Python, no third-party libraries — just the standard library and a bit of Unix plumbing.
Interestingly, modern DOSBox builds now support this feature natively: https://www.dosbox-staging.org/releases/release-notes/0.80.0/?utm_source=chatgpt.com#dual-mouse-gaming, so the little adapter is no longer necessary.
But solving the problem was half the fun — and it’s a nice illustration of how Unix-style abstractions let you insert a tiny translator between two pieces of software from completely different eras.
Even if one of them thinks it's talking to a serial mouse from 1995.
Top comments (0)