Patching MechCommander's "left arm bug" for fun and profit
Background
MechCommander 1 has an annoying quirk where it shoves all of a mech's biggest weapons into its left arm. If your mech is kitted out with mostly/only big weapons, then losing that arm means you basically lose the mech.
Normally this issue doesn't have much impact on the outcome of a mission. Specifically targeting individual locations on a mech has pretty low accuracy (even with high-gunnery pilots) so it tends to not matter much for enemy mechs, and the chance that one of your own mechs loses specifically that arm by pure chance is pretty low - though it can happen.
It's still a weird issue though, and the times where it does happen to you can be a little rough. Well no more! After being infected by Vana with a brain worm about why the code for distributing components didn't seem to work for big weapons, I spent a day figuring it out and now have a working fix.
(Note that I'm going to be referring to the location in question as the "left arm" even though it's not clear whether it's actually the "left arm" or the "right arm" and the visuals of the mech are inconsistent after losing an arm. This is the location that gets blown off when you use the "fire at left arm" command as labeled in the manual, so I'm going with that.)
Investigating the Code
About a month ago I noted that MechCommander 1 (or at least the MechCommander Gold copy that I have) seems to come with embedded debugging symbols of some kind, giving some useful limited insight into what the game's code is actually doing. This means that we can take a gander to find out:
- Which weapons count as "big" (is it just the >=9 tons rule I've seen tossed around?)
- How weapons and equipment are distributed on a mech
- Why the hell "big" weapons specifically all get lumped into the left arm
The game exe itself is in machine code (or if first disassembled, assembly). But thanks to tools like Ghidra (yes, by the wonderful folks at the NSA) we can also translate individual functions into C-like code. This translation won't necessarily be accurate to the structure of the original source code, but it generally has much better at-a-glance readability than the assembly does.
To start off with, here's the function that seems to determine whether a weapon is "big" / "large", which is the classification that's relevant to the left arm bug. When combined with the weapon IDs in the compbas.csv file (which is packed inside the MISC.FST file) and other sources (including some testing of my own), we can translate this into a list of "large" and "small" weapons, where "small" is just everything that isn't explicitly defined as "large".
| ID | Weapon | Availability | Tonnage | Size |
|---|---|---|---|---|
| 98 | Rail Gun | Expansion only | 30.0 | Small |
| 99 | Light Gauss Rifle | Expansion only | 13.5 | Small |
| 100 | Light Autocannon | Base game | 9.5 | Large |
| 101 | Autocannon | Base game | 15.5 | Large |
| 102 | Heavy Autocannon | Base game | 19.5 | Large |
| 103 | Light Ultra Autocannon | Base game | 11.0 | Large |
| 104 | Gauss Rifle | Base game | 16.5 | Large |
| 107 | Light LB-X Autocannon | Expansion only | 9.5 | Small |
| 108 | LB-X Autocannon | Expansion only | 14.5 | Small |
| 109 | Heavy LB-X Autocannon | Expansion only | 19.5 | Small |
| 110 | C. Light Ultra Autocannon | Base game | 8.5 | Large |
| 111 | C. Ultra Autocannon | Base game | 13.5 | Large |
| 112 | C. Heavy Ultra Autocannon | Base game | 17.5 | Large |
| 113 | C. Gauss Rifle | Base game | 13.5 | Large |
| 116 | C. Light LB-X Autocannon | Expansion only | 8.5 | Small |
| 117 | C. LB-X Autocannon | Expansion only | 13.5 | Small |
| 118 | C. Heavy LB-X Autocannon | Expansion only | 17.5 | Small |
| 120 | LRM Rack | Base game | 4.0 | Small |
| 123 | SRM Pack | Base game | 3.0 | Small |
| 125 | Streak SRM Pack | Base game | 5.0 | Small |
| 126 | Heavy Thunderbolt | Expansion only | 21.0 | Small |
| 130 | C. LRM Rack | Base game | 3.0 | Small |
| 133 | C. SRM Pack | Base game | 3.0 | Small |
| 135 | C. Streak SRM Pack | Base game | 4.75 | Small |
| 139 | Large X-Pulse Laser | Expansion only | 13 | Small |
| 140 | Laser Laser | Base game | 9.5 | Small |
| 141 | Large ER Laser | Base game | 11.0 | Large |
| 142 | Large Pulse Laser | Base game | 12.0 | Large |
| 143 | Laser | Base game | 4.0 | Small |
| 144 | Pulse Laser | Base game | 6.0 | Small |
| 145 | PPC | Base game | 12.0 | Large |
| 146 | ER PPC | Base game | 15.5 | Large |
| 147 | Heavy Flamer | Base game | 8.0 | Small |
| 150 | C. Large ER Laser | Base game | 10.0 | Large |
| 151 | C. Large Pulse Laser | Base game | 11.0 | Large |
| 152 | C. ER Laser | Base game | 3.5 | Small |
| 153 | C. Pulse Laser | Base game | 4.0 | Small |
| 154 | C. ER PPC | Base game | 13.5 | Large |
| 155 | C. Heavy Flamer | Base game | 7.0 | Small |
| 160 | Long Tom Cannon | Expansion only | 34.0 | Small |
(I've excluded several invalid weapons from the list, including a few weapons explicitly checked in the getWeaponLarge() function like the LRM 15 that I assume are just cut content and that - as far as I know - you never actually encounter in-game. The Sniper Cannon which you only get from cheating is also treated as inaccessible content for the purpose of this article.)
Observations
So the first and most obvious takeaway is that LRMs come in racks but SRMs come in packs, which is vital information for defeating the Clan invaders. Can you imagine if somebody called them LRM packs or SRM racks? You can't expect the Inner Sphere to fight a war with incorrectly named equipment.
The second most obvious takeaway is that every single weapon added in Desperate Measures has been accidentally classified as a "small" weapon because the getWeaponLarge() function evidently wasn't updated to consider them, and the default output of the classification function for weapons without explicit exceptions is "small".
There's a few classifications that are subjectively a bit borderline. The Light Autocannon is 9.5 tons and "large", while the Large Laser is the same weight and isn't considered "large". I would personally say that if the Large Laser doesn't count as "large" then the light ACs shouldn't either, or alternatively the Large Laser should count as Large if the light ACs do, but I don't think it's worth losing sleep over and there are some reasonable arguments to keep it the way it is.
This also shows that although the "being >=9 tons makes it large" rule is close, it's not quite accurate - even if you give a blanket exclusion of the rule for all of the incorrectly classified Desperate Measures weapons. The aforementioned Large Laser (9.5 tons) and Clan Light Ultra Autocannon (8.5 tons) break the rule.
Testing the Theory
To spot-check that our baseline understanding is correct, we can subject our pilots to several friendly-fire accidents. These results support that:
- "Large" weapons do just go into the left arm (and that the paperdoll is mirrored.. or they're all in the right arm and it's not mirrored and the manual uses the term "left arm" wrongly).
- "Small" weapons don't just go into the left arm and are more distributed across the mech.
- Desperate Measures weapons are being incorrectly classified as "small" weapons even though they should logically be "large".
- Mech pilots should unionize.
If you look too closely you may notice that sometimes the mech being tested on appears to have its right arm destroyed, not its left arm. That seems to just be either a visual bug or an art-resources constraint (where maybe they didn't make a separate version for each missing arm?) โ the arm that's visually missing on the mech sprite seems to vary depending on either the angle the mech is facing or whether mercury is in retrograde. And fuck, man, I don't want to go and fix up every single bug this game must have, so we're just going to ignore it.
One other oddity is that in the Heavy LB-X test, Beast's health (power?) bar is acting like most of his weapons are gone even though none of them are. I thought there might be a risk that the actual in-game functionality and the weapon listings in the UI aren't synchronized, but all four weapons seemed to fire just fine โ I'm assuming the issue is just pilot health + some damage on presumably where all four weapons are located. Maybe the side torsos have taken some structure damage from the destroyed arms, and it's not showing on the large paperdoll (because they still have armor), only on the simplified one at the bottom of the screen? Interesting as a detour it would be, figuring out exactly what's going on with the paperdoll discrepancy is out of scope, so let's move on.
The Distribution of Weapons
Alrighty, so how - in general - is equipment distributed to a mech's various locations? Well, first thing is to find out where in the game code this is handled.
One way to know if you've found the right code is to change the suspected function and see what happens in game. In this case, I changed the suspected function to distribute unknown-thing into places where unknown-thing wasn't supposed to go. After making this change, blowing off a mech's left arm had the effect of immediately killing the pilot.. and not destroying any of the mech's weapons. With the left arms popped and the pilots dead, I shot the mechs on the ground to blow off their right arms as well and they still didn't lose any weapons โ a fun side effect of the change I guess.
I'm still kind of curious what locations the weapons actually did go to, but couldn't reasonably explore every single question I had along this journey so I skipped answering this (focus-firing specific locations is very slow - even against stationary targets - so it would not be entirely trivial to answer empirically).
Tracing through the assignment of equipment onto the mech in a debugger suggests that I may have shifted at least the life support and engine into the left arm with my change, which would certainly explain the loss of both meat and metal once those were out of the picture. This itself is quite interesting, because it suggests that MechCommander actually does - at least in some capacity - make use of "internal equipment" like this instead of purely relying on checking the structural condition of a given location (head / CT, etc) โ which is honestly what I thought it was doing in this situation.
As trivia, these are the components that seemed to be assigned to locations on the mech during my test (I didn't specifically track which locations received which equipment):
- life support
- 2x hip
- 2x shoulder
- gyro
- sensors (presumably the specific sensor will match whatever the mech is equipped with)
- cockpit
- 2x leg actuator
- 2x arm actuator
- fusion engine
I can't guarantee that all of this equipment actually does anything, but some non-zero amount of it does seem to have some non-zero effect on the game given the death-by-arm observed above. Anyway, diving into that more is another out of scope concern. If you want to actually solve the problem you're trying to figure out, sometimes you have to aggressively fend off these offers of dubious quests along winding paths amidst the forest of knowledge.
The Equipment Distribution Function
Here's the top of the function that seems to be handling equipment distribution. I'm not going to run through the whole thing because it's a couple hundred lines long, I don't understand everything that it does, and doing so would be yet another out-of-scope detour.
Relevant to us is the check at the top where param_1 is compared against 100. In the base game, weapons start at ID 100, which gives some insight to speculate that the top of the function handles the placement of non-weapon equipment (like the stuff listed above). That theory is supported by the available evidence, so it's probably at least mostly correct, but it's out of scope to exhaustively confirm it.
The part we really care about is how this function handles placement of weapons: equipment with an ID of >=100. Thanks to the debugging symbols giving us some function names, most of the high-level details are pretty self-evident if you make some assumptions about what the called functions do:
- Check if a weapon is large or small.
- If small, assign to locations 2-5 based on an algorithm that checks for small weapons at those locations.
- If large, assign to locations 2-5 based on an algorithm that checks for large weapons at those locations.
If We Play Our Arms Left, We Get to Keep All the Weapons
By time spent, going from the situation retold up to this point to then ultimately figuring out what was wrong was the longest part of this whole thing, but also wasn't particularly interesting, so I'm going to give the highlights and skip over some of the boring stuff.
- The
getWeaponLarge()function itself seemed to work as expected (with the caveat that the way it's designed does itself have some issues e.g., with Desperate Measures weapons), so that didn't seem to be the problem. - The
getSmallWeaponCount()andgetLargeWeaponCount()functions themselves both seemed to work as expected, so neither of those seemed to be the problem either. - The left arm seems to be location 4, and I assume the other three locations are right arm, left torso, and right torso, but I don't know which location aligns with which number and it was out of scope to find out.
Tracing through component assignments, we see the in-game circumstances mirrored in memory: large weapons all end up in the same place, but small weapons are distributed. Note how Clan ER PPCs (blue outline in middle-right image) and Clan ER Large Lasers (red outline in middle-right image) are all placed contiguously in memory, which means they're in the same location. Contrast this with the mix of Clan Pulse Lasers and Clan Heavy Flamers (pink outlines in bottom image) which are distributed in two places in this particular screenshot. Blue outlines in the bottom image are probably shoulders + arm actuators since that align
Comments
No comments yet. Start the discussion.