For Rivals 2's Steam Workshop launch, I developed a tool that would allow modders to automatically "one-click" port their existing Rivals 1 workshop characters. Bringing over Rivals 1's catalog of ~4000 workshop characters would be a big boost in launch content and make the feature feel fleshed out while modders worked on finishing their original Rivals 2 characters.
I am generally not one to brag, but this was a genuinely herculean feat, especially given the time constraints I was operating in. Nobody on the team believed it would even be possible until they saw it with their own two eyes.
Rivals 2 is not simply an updated version of Rivals 1. It is an entirely different game, built on an entirely different engine, with entirely different underlying architecture and systems. It's not even the same number of dimensions (2D vs 3D).
In order of importance, the main issues to solve were:
The games were written in entirely different languages: Rivals 1 is GML, Rivals 2 is C++ (with lua scripting).
The engine primitives are entirely different: This is expressed in MANY different ways, but one particularly troublesome feature is that Rivals 1 didn't possess the concept of an "article" — all auxiliary effects are implemented and rendered as part of their owning character.
Declarative vs Imperative data setup: Rivals 1 set up data in a largely imperative way, involving an init function that makes calls to functions like set_hitbox_property(). This also meant R1 had a lot of runtime control over data — modders could mutate pretty much anything mid-game. On the other hand, R2 stored the bulk of a character's core features in static, runtime-immutable data assets and only imperatively defined decidedly custom behavior.
2D vs 3D: Rivals 1 is a 2D pixel-art game. Rivals 2 is a full 3D game driven by skeletal meshes and animations.
In order to resolve the first issue, I had to build a custom GML → lua transpiler. Like most transpilers, it functioned by first building an AST from the source language, and then rendering that AST into the target language. Fortunately, GML and lua are semantically similar enough that this task wasn't too absurd, but there were a lot of functional differences that had to be resolved.
One stumbling block I found particularly funny involved the truthiness of numbers. A number is considered "truthy" in a language if that number evaluates as true in situations when interpreted as a boolean value. In most languages, all nonzero numbers are truthy, and zero is falsy. In lua, however, all numbers are truthy, including zero. To make matters even more confusing, GML has an even stranger truthiness convention: in GML all numbers less than 0.5 are falsy. This tripped me up for quite a bit, but the ultimate solution involved wrapping any context where a number might be evaluated as a bool with a custom truthy() call.
Another less interesting hiccup: lua does not support switch statements. While the basic function of a switch statement can be transpiled to an if-else chain, lua switches also support case fallthrough and switch breaks. Supporting these required a somewhat unpleasant set of manipulations to the AST→lua intermediary state to virtualize the scopes of each case block.
The solution to the difference in core engine primitives ended up being very simple conceptually, and VERY involved as far as the details.
Primarily, this consisted of a large shim on the lua side that exposed an API surface nearly identical to the Rivals 1 engine. Behind the scenes, that API would route most calls and property reads/writes to the corresponding Rivals 2 primitives, with whatever translation logic was necessary to achieve the best parity between the two. It also included a deferred resolve-and-sync layer that would run custom logic to synchronize any features that didn't easily map between the two engines at the boundary between gameplay frames.
The bulk of the work was in this part — finding disparities between the two fundamentally different gameplay systems and coming up with correct ways to resolve them that are robust against the numerous use-cases and outright hacks that modders would expose.
Finally, in order to minimize the amount of lifting that the engine shim layer had to do (which is a relatively expensive indirection), I built a system for statically analyzing mods and stamping anything I could onto Rivals 2's static data assets. This is a very annoying problem (technically impossible according to computability theory), and relies on a lot of unpleasant heuristics, assumptions about "what most mods do", and runtime fallbacks for false positives.
Achieving something like 80-90% coverage with this would have been very easy (the average mod doesn't do runtime hitbox mutation), but covering all the potential problematic cases made it a much more painful endeavor.
The 2D vs 3D problem was actually a relatively easy problem to fix, and the first one I tackled. Imported Rivals 1 characters simply render on a 2D plane with a one-bone skeleton. I built an extension of our base character type that used this skeleton and supported slotting in spritesheet animations where skeletal mesh animations would normally slot in, as well as handling a few bone-driven functions (computing collision bounds, for example) in terms of the sprite sheet instead of bone positions. And that was that.
The only real hiccup was that Rivals 1 used 2D sprites as collision primitives in some situations, while Rivals 2 uses strictly mathematical collision (capsule-vs-capsule typically). I solved this by writing a lossy mass-decomposition as part of the pipeline that split collision sprites into an approximate collection of primitives.
As an added bonus, I was able to ship the Character2D template as a standalone feature, allowing modders to build entirely original 2D characters with the same underlying Rivals 2 primitives that any new, non-imported mod character would use.