SakeTami
Seemann
Seemann

patreon


I converted a GTA III mission from SCM to JavaScript

It took a couple evenings to complete (it was Luigi's "Drive Misty for Me"), but it was a lot of fun, and I learned quite a few things during that process.

If you want to jump straight to the code, go here: https://github.com/x87/luigi3

1. Converting SCM commands to JavaScript was easy, given that we have Sanny Bulder Library, but some original commands use predefined constants and model names available only in the compiler context. Luckily it was easy to substitute them with the enums that we already have in the library. For model ids I used the IDE loader which allowed to get a model id by its name directly from default.ide:

Streaming.RequestModel(car`taxi`);

car function takes taxi model name and returns the id number.

2. Some models used in the mission are not defined in default.ide. The game has a built-in logic of resolving model names compiled in the main.scm's header, which is not available in JavaScript. I decided to just hardcode those model ids:

const LUIGIINEERCLUB = 243;

...

Streaming.RequestModel(LUIGIINEERCLUB);

Alternatively we could use a DEFINE OBJECT index for that model, which is -133, and let the game resolve the id by itself as it would normally do in SCM code:

const LUIGIINEERCLUB = -133;

...

Streaming.RequestModel(LUIGIINEERCLUB);

this would work, but isn't very appealing to me.

Lastly we could also import model data from data\maps\gta3.IDE using IDE loader, but I was lazy to do that.

3. Some commands require current script to be a mission. The game has a few specific flag for the script object in memory which are set when the script is run as a mission (normally via LOAD_AND_LAUNCH_MISSION_INTERNAL). CLEO Redux did not have any means of setting those flags, so I had to implement it in the core. The latest dev build sets those flags under the hood when you change the ONMISSION variable:

ONMISSION = true; // change the SCM variable and set mission flags

ONMISSION = false; // change the SCM variable and reset mission flags

It allows usage of commands like STORE_CAR_CHAR_IS_IN.

4. One of the notable features of SCM missions is the immediate failure when the player dies or gets caught by the police. It's a game's internal check for mission scripts which also requires a specific code structure for missions. When the failure condition is met, the game unwinds the SCM call stack and aborts the mission. That was quite easy to replicate in JavaScript using exception mechanism. Before running the mission itself the script decorates `wait` command with an extra check for the player's death or arrest and throws an exception:

const _wait = wait;

const p = new Player(0);

wait = (delay) => {

_wait(delay);

if (!p.isPlaying()) {

throw new Error("MISSION ABORTED");

}

};

wait = _wait;

Now you can use wait command as usual, but if the player dies or gets arrested, the mission will be aborted.

5. The mission extensively uses global SCM variables which was not a surprise. We have a script that allows to read and write the SCM variables, so there was no blocker here:

const flag_luigi_mission3_passed = 250;

...

let flag = readScmVariable(flag_luigi_mission3_passed)

You have to know the variable ID upfront to interop with the given main.scm and the id is only valid for the OG main.scm.

6. In one particular case the game used an object defined in the global scope and not anywhere in the mission. It was MISTYDOOR created in the beginning of main.scm. I had to read the object handle from the SCM variable and create a new ScriptObject to be able to call methods on it:

let door = new ScriptObject(readScmVariable(misty_door1));

let door1_position_lm3 = door.getHeading();

Normally you wouldn't do that if you created an object using ScriptObject constructor method, e.g. ScriptObject.Create. new ScriptObject does not create a new entity in-game, it just uses a provided handle to construct an instance of a JavaScript class. This is commonly used with new Player(0) to get an existing player instance and call methods on it.

The code was converted as is, with some minor optimizations such as method chaining:

p.setCoordinates(896.6, -426.2, 13.9).setHeading(270.0);

or extracting obviously duplicated code into a function:

function fading() {

while (Camera.GetFadingStatus()) {

wait(0);

}

}

....

while (!Streaming.HasSpecialCharacterLoaded(2)) {

wait(0);

}

fading();

Text.PrintNow("LM3_10", 5000, 1); //Get a vehicle!"

It allowed to reduce the code size and improve readability, while keeping the original structure.

The mission code is available here: https://github.com/x87/luigi3

If you want to play it, you are going to need CLEO Redux 1.03 (a dev build is available on Discord) and copy luigi3[mem] folder into the CLEO directory. Run the game and you should see a blue marker near you. Enter it to start the mission. If you fail the mission, you can run it again at the same location.

It should work on both GTA III and re3 with the OG main.scm.

Demo: https://youtu.be/Yp7YizGOe28

I hope you'll enjoy it!


More Creators