Pangram verdict · v3.3
We believe that this text is a mix of AI and human-written content.
AI likelihood · overall
MixedArticle text · 1,717 words · 4 segments analyzed
An 11 year journey to play DOS Wing Commander together By danielrh Watch on YouTube Play it in your browser When we were kids, my brother Patrick and I would steer our space fighter with one of us on controls and the other on the lasers and afterburner boost. We always dreamed of playing the Wing Commander series cooperatively as wingmates rather than as a two headed hydra in a single craft. In the beginning we were searching for where we were On October 4, 2015 as I was playing the classic Wing Commander in Dosbox I realized the whole memory probably fit in that 640K Bill Gates apocryphally said “ought to be enough for anybody”. If I could dump the memory and identify the locations that held the X,Y,Z coords for the player & wingman we could let the network packets control the wingman. The actual quote “I have to say that in 1981, making those decisions, I felt like I was providing enough freedom for 10 years. That is, a move from 64 K to 640 K felt like something that would last a great deal of time. Well, it didn't.” Source 1 MB 0000 01A2 WC.EXE root image DS 13D3 data segment, 64K 640K A000 VGA E000 EMS page frame FFFF:F the data segment, dseg:0000 to FFFF 0187 dead string 7728 random seed A9C2 position D248 mission tree 8DF8 comm text CAE2 velocity FFxx stack AEB6 · B1B6 · B4B6 right, up, forward C280 victory points Here is a breakdown of the megabyte of addressable DOS memory and the 64K data segment where Wing Commander puts its game state. This presented a clear problem to attack: out of fewer than a million bytes how would we identify that location? I decided to simplify the problem by trying to empty the game of other entities. A stopped player shouldn’t change coordinates. We could snapshot the memory twice when stopped to identify “background” memory churn. Then burst forward and measure again. If we didn’t turn (mouse was disabled) we might only change one axis. The next problem was to find an empty region of space. We had to start without our wingman Paladin, since he would move with us. After a miserable mission, letting the nefarious space cats overwhelm Paladin and running back to the spacecraft carrier, the Claw, the next mission was primed for a flight through empty space. I launched and flew directly ahead until everything despawned and took the memory snapshots using the DOSBox MEMDUMPBIN debug-command. The Hornet cockpit on launch, where the VDU confirms an empty patch of space with nothing to target. Now I had to discover anything that had not changed between the memory snapshots where my star fighter was in the original position but things that had changed when my star fighter moved. (snapshotorigin ⊙ snapshotlater) ∧ (snapshotorigin ⊕ snapshotmoved) A stoppedstopped5 B stopped againstopped6 C after movingstraight4 origin ⊙ later same in origin and later origin ⊕ moved different in origin and moved ∧ candidates 413 bytes 132 with chase view 1,048,323 bytes still 650 bytes moved 253 bytes churn even while stopped 253 bytes churn when stopped, and 650 change during a move. 413 do both. By cross referencing with the chase cam, the total is only 132 bytes. There were 35 distinct clusters of bytes that changed in the data segment. By inspecting these visually and looking for floats or small integer changes when I moved, I narrowed it down to a dozen locations it could be. I went to the chase camera view as well, and this helped to narrow it down to 13D3:A9C2. I had the DOSBox debugger add integers at that address with its SM command and that caused the ship to jolt forward to a new location. This technique identified velocity as well at CAE2, since thre were three 32 bit ints that zeroed out when the craft was stopped. Craft position, camera position, set speed and velocity changes lit up in yellow I had fun driving my ship around with the memory editor and reproduced it around the Claw and other NPC ships. Then we searched for what we faced I set it down for nearly a year but in September 2016 I decided to write a small tool to automatically compare DOSBox snapshots instead of using the vbindiff GUI only. Directionality turned out to be more difficult so I set down the problem for a few more months and picked it back up in November. Diff of two DOSBox memory dumps from turning. The bright band is the data segment. A9C2position AEB6right B1B6up B4B6forward CAE2velocity 012 81018202830383D3F slot 0 player · 1 wingman · 2 Tiger's Claw · 3D camera · 3F temporary camera 0x300 apart64 × 12 bytescontiguous slot 0 → slot 0x3D: 0x2DC bytes (A9C2 → AC9E, CAE2 → CDBE) int32 xint32 yint32 z one slot = 12 bytes In the 64 slot arrays, each slot is 12 bytes. Slot 0 is the player, 0x3D is the camera and 0x3F is a temporary slot. The camera copy caused each value to appear twice 0x2DC bytes apart. To find the memory location of the rotation, I needed to look harder. As it happened the vectors were made of 4 byte scalars with 256 as 1.0 and valid positive vector positions between 0 and 255. This meant that most small rotations only changed a single byte. However, fortune was smiling on me that day because of what appeared to be a programmer joke at the time. Wing Commander reserved 64 slots for entity properties. In hex this would span numbers from 0x00 to 0x3F. However the programmers decided to use 0x3F for temporary usages and place the camera in 0x3D. Since 3D graphics were still nascent, the programmers may have thought it a good laugh to write 0x3D every time they needed to manipulate the 3D camera. That put a temporary variable two slots from the camera variable. And up and forward were also next to one another. This gave a nice wide region where I could locate those single byte changes. The first byte I understood was the y of the right vector; the print window I put around it happened to cover the camera's copy of up, which is where the overwrite experiment started. Furthermore, I rolled the ship over, and tripped sign extension, which caused all 3 adjacent bytes to switch from 0x00 to 0xff. Between these two facts, I noticed the temporary and camera bytes in the hex dumps. Writing to them did not help, since they were overwritten every frame, but luckily I wrote some extra bytes to the next vector as well, which was forward[0]. This caused a noticeable shift in the starfield and meant that I could now start investigating the code. end of the up table start of the forward table up[0]B1B6the ship copiedevery frame up[0x3D]B492camera copy up[0x3E]B49Eempty up[0x3F]B4AAtemporary forward[0]B4B6the ship my write overwritten every frame starfield shifts 000000000001000000000000 level: (0, 256, 0), 256 = 1.0 82FFFFFF34FFFFFFA5FFFFFF rolled: (-126, -204, -91), 00 bytes became FF xyzthree 4-byte scalars, little-endian The up vector array has the camera copy at offset 0x3D, an empty slot at 0x3E, and the temporary intermediate value at 0x3F. Int32 where 256 means 1.0, thus the nonzero unit vector axis would be 00 01 00 00 in little endian. Yaw from level flight where forward[0] has a 9 byte change and up[0] changed from 00 01 to 7E 00. I put memory watchpoints on the addresses and traced the vector-add function called by the per-ship physics function called by a per-frame function and eventually the main loop. BPM 13D3:A9C2 write watch onposition[0] vector_add 329A:04DA (stub140) pos[si] += vel[si] physics per ship 2B0F:19E0 (stub141) for si in 0..3C per frame 0560:074C seg001 main loop 0560:20E3 once per frame fires in called by called by called by DOSBox hook: CS:IP == 0560:20E3 run queued calls, exchange a frame neighbours in overlay 140: sub 0568 · zero 047A · cross 09BE normalize 0A7F · orthogonalize 1163 yaw 12B1 · pitch 1280 also seen at 34A5:19E0: same overlay, another load Call stack after memory watchpoint. It was now time to locate the byte sequences in IDA and start annotating the game. Wing Commander Multiplayer was now just a matter of repeating these techniques and figuring out how to puppeteer one ship from another's input. Then we searched for how to act December 2016 had shattered my illusion that we would be able to drive Wing Commander with writes to global memory alone. The details of manifesting laser fire or heat seekers were far too complicated to puppeteer solely from the addresses. We would need to co-opt the very functions in the original game to get the most accurate experience. One initial thought was to “will” the program counter to the right place, but we would need a way to insert arguments and return back from whence we came. Moreover, many of the functions were in various overlays that required indirection to invoke. It appears that Borland had a different overlay per compilation unit (C++ file) and they were dynamically loaded in different locations depending on the path the player took beforehand.
root image (fixed addresses) stub143 = segment 12D7 12D7:0084 EA 99 0A 31 2F jmp far 2F31:0A99 12D7:012E EA 78 29 31 2F jmp far 2F31:2978 12D7:0156 EA 76 2E 31 2F jmp far 2F31:2E76 … one entry per exported function call far 12D7:0084 overlay 143, loaded on demand this run: 2F31 · other runs: 2BBE, 34A5 … 2F31:0A99 do_damage(src, dst, qty, vec) 2F31:2978 fireGunFromShip(ship, gun) 2F31:2E76 fireAllGuns(ship) ships, damage, weapons: ovr143 in IDA via the EMS page frame DOSBox hook, isExecutingFunction(0x12D7, 0x0084) stub bytes 1..4 → live segment:offset reads matches Borland overlays: table of jmp far thunks.
Patrick and I had to figure out a good trampoline system to call functions with the correct arguments. We chose the “Loading WING COMMANDER” string at 13D3:0187 and eventually 13D3:0395 “Sorry, an error has occured while %s”[sic] to hold the trampoline. We would have DOSBox push CS:IP to the stack, then relocate the instruction pointer to the string above. 00 terminates any string pointing here.
pushf; pusha the game is mid-frame; leave registers as we found them xor si,si; push si argument: ship 0.