Introduction

CLEO Redux is an embeddable and extensible JavaScript runtime capable of running user-made scripts in games and desktop applications. It is inspired by CLEO Library and is partially compatible with its scripts and plugins. CLEO Redux, however, aims to provide a better experience for developers and users and support games beyond just GTA 3D series.

Follow the first steps tutorial to learn how to write and run your first script. Also check our YouTube playlist for video guides.

Supported Releases

Grand Theft Auto series:

  • GTA III 1.0
  • GTA Vice City 1.0
  • GTA San Andreas 1.0 (only with CLEO 4.4)
  • GTA IV 1.2.0.43 (The Complete Edition, Steam)

Grand Theft Auto: The Trilogy - The Definitive Edition:

  • Title Update 1.03 and Title Update 1.04 (see details)

Other games and applications:

For the complete reference on supported features refer to this page. Also there are known limitations listed here.

License

CLEO Redux is available under the end-user license agreement

Relation to CLEO Library

CLEO is a common name for the custom libraries designed and created for GTA III, Vice City or San Andreas. Each version can be found and downloaded here. CLEO Redux is another CLEO implementation made from scratch with a few distinctive features, such as shared code base between all games and JavaScript support.

At the moment CLEO Redux can not be considered as a complete replacement for CLEO Library due to the lack of support for many widely used CLEO commands. To solve this issue and get the best out of the two libraries, CLEO Redux supports two different usage strategies.

CLEO Redux can run as a standalone software, or as an addon to CLEO Library.

Running CLEO Redux as a standalone software

As a standalone software CLEO Redux runs compiled scripts and JavaScript and provides access to all native script commands. It also provides a limited set of custom commands backward-compatible to CLEO Library. Existing CLEO scripts may not work if they use custom commands (for example from a third-party plugin) not supported by CLEO Redux.

In the standalone mode your game directory contains cleo_redux.asi (or cleo_redux64.asi) file and there is no cleo.asi (or III.CLEO.asi or VC.CLEO.asi) file from CLEO Library. This mode does not work in the classic GTA San Andreas.

Running CLEO Redux as an addon to CLEO library

As an addon CLEO Redux runs alongside CLEO Library delegating it all the care for custom scripts. It means all custom scripts and plugins made for CLEO Library will continue working exactly the same. CLEO Redux only manages JS scripts. All custom commands become available to JavaScript runtime, which means you can use commands currently not implemented natively in CLEO Redux, for example for files.

In the delegate mode your game directory contains both cleo.asi (or III.CLEO.asi or VC.CLEO.asi) from CLEO Library and cleo_redux.asi (or cleo_redux64.asi). This mode works in classic GTA III, GTA Vice City and GTA San Andreas where CLEO Library exists.

Installation

CLEO Redux comes with a hassle-free installer that identifies the selected game and downloads all the dependencies. Just run cleo_redux_setup.exe and follow its steps.

Both CLEO Redux and its installer recognize the target game purely by the executable name in the selected/working directory.

  • GTA III - gta3.exe
  • GTA VC - gta-vc.exe
  • GTA SA - gta_sa.exe, gta-sa.exe, or gta_sa_compact.exe
  • GTA IV - GTAIV.exe
  • re3 - re3.exe
  • reVC - reVC.exe
  • GTA III: DE - libertycity.exe
  • GTA VC: DE - vicecity.exe
  • GTA SA: DE - sanandreas.exe
  • Bully: SE - bully.exe

Names matching is case-insensitive. If the exe file does not contain a version information CLEO Redux always assumes the version is correct (see supported versions).

Once the installation is complete, CLEO Redux is ready to use. You can now install scripts and extra plugins not included in the installer, and change the default configuration if needed.

CLEO Directory

CLEO directory is the primary location where you install CLEO scripts, CLEO plugins and custom texts. CLEO Redux automatically creates this folder during installation and any time the game starts.

In most cases this directory can be found in the game folder. If, however, CLEO lacks write permissions there and fails to create new files, it uses an alternate path at C:\Users\<your_username>\AppData\Roaming\CLEO Redux. cleo_redux.log and the CLEO directory can be found there.

Dependency on ASI Loader

CLEO Redux is distributed as a dynamic-load library with an .asi extension. Historically ASI files have been used in GTA III and Vice City as addons to the Miles Sound System library (Mss32) which loads them into the game process. More recent titles didn't use MSS, so the modding community developed custom loaders commonly named "ASI Loader" to continue using ASI for any custom code to be injected into the game.

Ultimate ASI Loader by ThirteenAG can be used to load CLEO Redux in any game. It gets downloaded during CLEO Redux setup. You may opt out of installing Ultimate ASI Loader if you have other means of injecting ASI files into the game (i.e. an alternative loader) or the game already supports ASI, e.g. by using MSS lib.

Note that the Ultimate ASI Loader dll file can be renamed, e.g. to dinput8.dll or d3d9.dll depending on the game. By default CLEO Redux installs it as vorbisFile.dll in 32-bit games and version.dll in 64-bit games. There are some exceptions:

  • it is d3d9.dll in re3 and reVC (x64)
  • it is dinput8.dll in GTA IV

If the default name does not work with the given game, try renaming the dll file into another compatible name. Visit Ultimate ASI loader repo for more information.

Note on re3 or reVC

CLEO Redux supports "Windows D3D9" version of re3 or reVC (both 32-bit and 64-bit).

During installation you must select the correct version of re3 or reVC: either 32-bit or 64-bit.

When running on re3 and reVC make sure the game directory contains the file re3.pdb (for re3) or reVC.pdb (for reVC). Due to the dynamic nature of memory addresses in those implementations CLEO Redux relies on debug information stored in the PDB file to correctly locate itself.

Uninstallation

To uninstall CLEO Redux perform the following steps:

  • Delete cleo_redux.asi (or cleo_redux64.asi).
  • Delete the CLEO folder (optional).
  • Delete the cleo_redux.log (optional)

Plugins

Plugins are optional programs adding extra scripting commands with the help of CLEO Redux SDK. A plugin file has a .cleo extension and should be copied to the CLEO\CLEO_PLUGINS directory.

List of plugins

NameDescriptionLink
IniFilesReading from and writing to INI filessrc
DylibLoading DLL files and importing functionssrc
InputChecking for keyboard and mouse input, emulating key pressessrc
ImGuiReduxDear ImGui bindingsGitHub repo
MemoryOperationsLow-level memory operationsGitHub repo
FrontendChecks the latest version on GitHub and display information in the main menu

Plugins are included in the CLEO Redux installer. You can opt out of some of them by unchecking the corresponding checkbox in the installer.

Scripts

Adding a new script

Generally a script file (.cs, .js) should just be copied to the CLEO directory. Some scripts may require extra steps for installation. In case of any issues check the script documentation or ask its author.

Complex JS scripts with dependencies may be distributed as folders with index.js and other files in it. Copy such folders to the CLEO directory.

Removing the script

Delete the script file from CLEO directory. Some scripts may require extra steps for undoing the installation. In case of any issues check the script documentation or ask its author.

First steps

Since available commands vary from game to game for the purpose of this tutorial we will be using CLEO's built-in commands such as log command that is available everywhere.

Once you have CLEO Redux installed run the game once to make sure CLEO is loading. You can verify it by having cleo_redux.log created in the game root folder. If there are no errors in the log you can start adding new scripts.

Go to the CLEO directory and create a new file named intro.js. This will be a text file and you can edit it in any text editor. We recommend using VS Code as the most advanced choice available today, but other editors, even Notepad, would work too. Open intro.js and add two lines:

// intro script, revision 1
log("Hello world");

Lines starting with // are comments and ignored by the game. The only meaningful line is the second that instructs JavaScript runtime to print a message in the log file.

Now save the file and run the game. Start a new game or load a save file, play a few seconds and then go to cleo_redux.log (do not exit the game yet). You should now see a new line like:

10:12:19 [INFO] ["intro"] "Hello world"

If you can't find this line, try to read through other messages, they might point you out the issue.

If you see the message, open intro.js again and change "Hello world" to another text. Save the file and go back to game. Play, then switch back to cleo_redux.log. Notice how the log now contains the new message. The script was reloaded on the fly as you saved it and the game now runs an updated version of it. This is called hot reloading.

Concurrency

There is so little fun in a single script, so let's explore another important aspect. CLEO Redux can load and run many scripts concurrently. But they do not run in parallel, or at the same time. Instead there is a queue of scripts to execute them sequentually on each game iteration. This happens to minimize number of issues caused by accessing shared resources from multiple parallel scripts. The game's main loop is also locked while scripts get processed.

When the script is ready to return control to the main script or another script in the queue, it must call the wait(n) command where n is a number greater than or equal to zero. This command pauses the current script for at least n milliseconds:

wait(250); // pause current script and wake up after 250 milliseconds

Low values (such as 0 or generally anything lower than 16) essentially make the script run on each tick of the game's main loop.

Let's create another JS file in the CLEO directory and call it second.js. Add the following lines:

wait(1000);
log("This is the second script");

Save the file and re-run the game. You should now see two lines in the cleo_redux.log:

10:12:19 [INFO] ["intro"] "Hello world"
10:12:20 [INFO] ["second"] "This is the second script"

Notice that there is a difference in one second between two messages. Try different combinations of log and wait commands in both scripts and see the results. Do they match your expectations?

Variables

Variables keep your script's state and intermediate results. Each variable has a unique name and a type and can be either mutable or immutable. Mutable variables are created with var or let keywords. Immutable variables (or constants) are created with a const keyword.

let x = 5;
let name = "CJ";
const z = -100;

Constants can not be reassigned to another value. The following code will throw an error:

const z = -100;
z = -200;

On the other side var or let variables can get new values. Those values can be of a different type, e.g. you can reuse same variable for different types of data:

let temp;

temp = 1; // number
temp = "str"; // string
temp = {}; // object
temp = []; // array

Use operator typeof to find the type of the variable at any given moment:

let temp;

temp = 1;
log(typeof temp); // prints "number" in cleo_redux.log
temp = "str";
log(typeof temp); // prints "string" in cleo_redux.log
temp = {};
log(typeof temp); // prints "object" in cleo_redux.log
temp = [];
log(typeof temp); // prints "object" in cleo_redux.log*

*typeof returns "object" for an array ([]). This is a very well known quirk in the language. You can read more about it here and find other means of differenting between arrays and plain objects.

Control Flow

Conditions

So far we have been running commands in the script in a sequence from top to bottom. The runtime executed the first line, then proceed to the second line if there was one, then to the third line, etc. But what if we want to execute a command only when some condition is met, lets say a button is pressed? We can use conditions.

In JavaScript an if statement allows to conditionally execute a branch of code if the condition is met. For example, when you write:

if (true) {
  log("this is always printed");
}

You always get the log line printed since the condition in if always evaluates to true. Likewise

if (false) {
  log("this will never be printed");
}

when the condition evaluates to false inside the if block does not run so you won't see the log messages.

You can have any expression in the if statement. JavaScript runtime will try its best to convert it to a boolean (simply saying, convert to true or false). Here are some examples of expressions that get evaluated to true (they are called truthy):

if (1) {...}
if ("string") {...}
if (5 > 4) {...}
if ([]) {...}
if ({}) {...}

Note that 0 and an empty string always get evaluated to false (they are "falsy" values):

if (0) {...}
if ("") {...}

Of course you can have complex experession like functions or methods calls inside the if condition:

if ("string".length > 0) {...}
if ([1,2,3].includes(3)) {...}
if ( ({ status: 'open'}).status === 'open' ) {...}

CLEO API has many conditional commands for different types of checks. For example, the Input plugin provides commands to check the user input (i.g. a check that the button is pressed). Go to intro.js and change its content to:

// intro script, revision 2
wait(2000);
if (native("IS_KEY_PRESSED", 115)) {
  log("F4 button is pressed");
}

Run the game. In cleo_redux.log you should not see the line "F4 button is pressed". Now try running it while holding the F4 button. A new log entry should appear.

You can combine multiple conditions in the same if statement using && (AND) or || (or) operators :

if (native("IS_KEY_PRESSED", 115) || native("IS_KEY_PRESSED", 116)) {
  log("Either F4 or F5 button is pressed");
}
if (native("IS_KEY_PRESSED", 115) && native("IS_KEY_PRESSED", 116)) {
  log("Both F4 and F5 button are pressed");
}

You can execute commands in else block if the condition is not met:

if (native("IS_KEY_PRESSED", 115)) {
  log("F4 button is pressed");
} else {
  log("F4 button is not pressed");
}

Loops

TBD

Functions

TBD

Configuration

CLEO Redux exposes some of the configurable settings in the file CLEO\.config\cleo.ini.

General

  • AllowCs - when set to 1 CLEO loads and executes *.cs files located in the CLEO directory. Enabled by default.
  • AllowJs - when set to 1 CLEO loads and executes *.js files located in the CLEO directory. Enabled by default.
  • AllowFxt - when set to 1 CLEO loads and uses *.fxt files located in the CLEO\CLEO_TEXT directory. Enabled by default.
  • CheckUpdates - (deprecated in favor of Frontend plugin)
  • LogOpcodes - when set to 1 CLEO logs all executed opcodes in custom scripts.
  • DisplayMenuInfo - when set to 1 CLEO displays some information in the main menu. Enabled by default.
  • PermissionLevel - sets the permission level for unsafe operations (see below). Default is Lax.

Host

  • EnableSelfHost - when set to 1 CLEO runs in the self-host mode. Only applicable on an Unknown host. See the Emdedding guide for more information.
  • SelfHostFps - the amount of iterations per second the CLEO's main loop will do. Only applicable when EnableSelfHost is 1. Default is 30.

Permissions

This section lists permission tokens and sets whether they are allowed or not in the Strict mode.

Permissions

CLEO Redux acknowledges some custom commands (opcodes) as unsafe and requires the user to decide whether to run them or not. Raw access to the process memory, loading external libraries or making network requests can be harmful and produce undesired side-effects. Hence CLEO introduces permission levels for running the unsafe code.

There are four available levels:

All

Any unsafe operation is allowed. Use this only when you trust the scripts you run.

Lax

This is the default permission level.

No unsafe operation is allowed unless the script explicitly requests it. Currently to request a permission, the name of the script file must include the permissions tokens wrapped in square brackets.

For example, if the script wants to access the memory via 0A8D READ_MEMORY the file name must contain [mem], e.g. cool-spawner[mem].cs. If the file is named differently CLEO rejects 0A8D and the script crashes.

Strict

No unsafe operation is allowed unless the script explicitly requests it (see "Lax") and the CLEO config file permits this type of operation under the Permissions section.

Permissions section in cleo.ini allows to enable or disable groups of unsafe operations by using the permission tokens. For example,

mem=0

disables all memory-related opcodes even if the script has the [mem] token in the file name.

Permissions section in the cleo.ini only takes effect when PermissionLevel is Strict.

None

No unsafe operation is allowed.

Known Permission Tokens

  • mem - reading from and writing to the process memory, calling foreign functions
  • dll - loading dynamic libraries and finding exported functions
  • fs - creating, reading and deleting files from the local file system

Scripts

CLEO Redux provides a runtime capable of executing compiled binary scripts (*.cs) in the native SCM format and plain text scripts (*.js) written in JavaScript.

To find the information on how to install scripts see this page.

Compiled Scripts

Older games in Grand Theft Auto 3D series (GTA III, Vice City, San Andreas) use a custom binary format to store compiled scripts. This format is known as SCM. CLEO Library and CLEO Redux uses the same format for custom scripts. They usually have a .cs extension.

CS scripts run alongside the original SCM scripts and are subject to the same limitations.

Check the FAQ for the information on CS support in the remastered games.

Writing a Compiled Script

For a detailed step-by-step tutorial visit this page. It's applicable to CLEO Redux.

To create a new script use Sanny Builder 3 in GTA III, GTA VC or GTA SA edit modes respectively. Add a directive {$CLEO .cs} at the top of your script, write the code and run "Compile and Copy". Sanny Builder will create a new CS file in the CLEO directory.

At the moment CLEO Redux only provides a few custom commands. Most of the commands implemented in CLEO Library or its plugins are not available yet.

There are a few basic rules to follow when writing compiled scripts:

  1. One file - one script. CLEO only supports one script per file.

  2. Never use opcode 004E to terminate a script. This opcode is only supported in the main.scm. Use 0A93 instead.

  3. Minimize the usage of global variables as they might conflict with other scripts. Some well-known variables such $PLAYER_CHAR, $PLAYER_ACTOR and $ONMISSION can be used.

CLEO Redux does not support saving the script status (a feature of CLEO Library) and there are no plans on adding this feature.

Custom Commands

The following commands are for classic GTA games only. For GTA The Trilogy check this information.

CLEO Redux supports all original opcodes available in the given game. On top of those it adds a few new commands listed below. Note that they strictly match the opcodes of CLEO Library.

This list might not be complete as there are custom plugins with extra commands (see Using SDK). Refer to Sanny Builder Library for the complete list of available commands for each game.

JavaScript

CLEO Redux targets JavaScript as the primary language for custom scripts. JavaScript is a popular programming language with rich ecosystem and plenty of available information. It's free from SCM language limits and pitfalls such as lack of support for functions, arrays, or the low number of variables.

To make a JS script use VS Code (recommended) or any editor of your choice. Create a new file with .js extension and put it in the CLEO folder. See Script lifecycle for extra information.

The runtime supports ECMAScript 2020 standard. It means you are able to use the most recent JavaScript features out of the box, such as imports, classes, arrow functions, etc.

CLEO Redux is not Node.js. Don't expect sockets, file system operations or other Node.js features to be available here.

Definitions

Due to the dynamic nature of JavaScript CLEO Redux needs to know an interface of each native command, i.e. a number of input arguments and returned values and their types. Sanny Builder Library serves as the source of command definitions for CLEO Redux.

At start CLEO validates that a definition file is present and correct and if not tries to download it from GitHub (see the table below) into your local CLEO/.config directory. If that did not happen, or you don't want to let CLEO make network calls, manually download the required file and place it in the CLEO/.config directory.

GameFileMinimum Required Version
GTA III, re3gta3.json0.243
GTA VC, reVCvc.json0.249
GTA San Andreas (Classic) 1.0sa.json0.287
GTA IVgta_iv.json0.45
GTA III: The Definitive Editiongta3_unreal.json0.222
Vice City: The Definitive Editionvc_unreal.json0.228
San Andreas: The Definitive Editionsa_unreal.json0.249
Bully: Scholarship Editionbully.json0.29
Unknown (32-bit)unknown_x86.json0.216
Unknown (64-bit)unknown_x64.json0.220

Starting from v1.0.0 CLEO Redux uses compound definitions (a combination of the primary JSON file for the current game and a JSON file for the Unknown host). It lets SDK commands to work in JS scripts regardless of them being defined or not in the primary JSON file. You should notice that during updates CLEO downloads both <game>.json and unknown.json as well as the accompanying enums.js files. It should not affect any existing scripts.

Script Lifecycle

A file with the JavaScript code should have a .js extension and contain valid instructions.

Valid instructions include a code conforming to the ECMAScript 2020 standard and custom bindings available in the CLEO Redux runtime. If the runtime encounters an unknown or illegal instruction the execution halts and a new error is logged in cleo_redux.log. The script may have no instructions (the empty script).

CLEO script runs as soon as the new game starts or a save file is loaded. If you change the file while the game is running, CLEO reloads the script and it restarts.

The script terminates automatically when the last instruction has been executed. The runtime also terminates stuck scripts to prevent the game from freezing. The stuck script is the one that took more than 2 seconds to run since the last wait command. If that happened, check out your loops, some of the are missing the wait command.

while (true) {
  // meaningless infinite loop normally freezing the game
  // will be terminated after two seconds
}

The runtime will terminate this script. To avoid that, add the wait command

while (true) {
  wait(250);
  // still meaningless, but does not freeze the game
}

Organizing Scripts

A single self-contained script file can be placed directly in the CLEO directory.

Complex JS scripts having a lot of dependencies (imported files, FXT dictionaries) can be organized in folders. CLEO Redux scans the subdirectories of the CLEO directory and checks if there is a file named index.js. If index.js is found, CLEO Redux runs it. It also loads all FXT files from the same directory.

Let's have a look at the example structure:

CLEO/
├─ CLEO_TEXT/
│  ├─ main.fxt
├─ folderA/
│  ├─ text.fxt
│  ├─ index.js
├─ folderB/
│  ├─ test.fxt
│  ├─ test.js
├─ script1.js
├─ script2.cs

By default CLEO Redux loads all CS and JS files from the root of the CLEO directory and FXT files from the CLEO_TEXT folder. So it loads script1.js, script2.cs, and main.fxt.

After scanning subdirectories, CLEO loads index.js and text.fxt located in the folderA and skips folderB as there is no index.js file.

CLEO_TEXT, CLEO_PLUGINS, and .config are not considered as script directories.

Imports

You can import other scripts and some custom file formats in your code to make the code modular and share the common logic.

Importing scripts

  • Extensions: .js, .mjs

The runtime supports the import statement as described in this article. To avoid running imported .js files as standalone scripts, either put them into a separate folder outside of the main CLEO directory (e.g. CLEO/includes/) or use the extension .mjs.

// imports default export from other.js or other.mjs located in the same directory
import func from "./other";

// imports named export PedType from types.js or types.mjs located in the CLEO/includes directory
import { PedType } from "./includes/types";

See also Dynamic Imports.

Importing JSON files

  • Extensions: .json
// imports vehicles.json as a JavaScript value (an array, object).
import data from "./vehicles.json";

Importing other formats

CLEO Redux supports custom file loaders. The purpose of a loader is to load a provided file, parse it and serialize into a JSON string suitable for the use in JavaScript. Those loaders are implemented as CLEO plugins and use CLEO SDK to associate itself with particular file extensions.

Default CLEO installation includes loaders for .txt and .ide files.

TXT

  • Extensions: .txt, .text
  • Plugin name: TextLoader.cleo

The TXT loader transforms a text file into an array of strings where each element is a line in the text file. For an empty file an empty array is constructed. If the file can not be open for reading the import statement raises an exception.

import lines from "./some-file.txt";

for (const line of lines) {
  log(line); // prints a line
}

This script prints each line from the some-file.txt file into the log. If you need the content of the file as a single string use .join('') method to concatenate all array elements:

import lines from "./some-file.txt";

log(lines.join("")); // prints the entire file

IDE

  • Extensions: .ide
  • Plugin name: IdeLoader.cleo

The IDE loader transforms an item definition file (*.ide) that is widely used in GTA series games. The file is imported as an object where each key corresponds to a section in the file and the value is an array of array of strings:

interface Ide {
  [key: string]: Array<Array<string>>;
}

A first-level array represents a list of lines within this section in the file, and a second-level array represents a list of columns within that line. E.g. the following IDE file

peds
0, null, generic, PLAYER1, STAT_PLAYER, player, 7f
1, cop, cop, COP, STAT_COP, man, 7f
end

objs
170, grenade, generic, 1, 100, 0
end

would be transformed into:

{
    "peds": [
        ["0", "null", "generic", "PLAYER1", "STAT_PLAYER", "player", "7f"],
        ["1", "cop", "cop", "COP", "STAT_COP", "man", "7f"]
    ],
    "objs": [
        "170", "grenade", "generic", "1", "100", "0"
    ]
}

Note that each data element becomes a string, even if it was a number in the original file. The loader does not make any assumption about specific sections, nor validate them. It follows a few simple rules where each section should start with an identifier ("peds", "objs") and end with the "end" string. It considers each line to be a comma- or space-separated set of columns that could be anything.

// read data/default.ide file in the game directory
import data from "data/default.ide";

// read "peds" section from the file
const peds = data.peds;

// find a line in the peds section where the second element (column) is "cop"
const cop = peds.find((line) => line[1] === "cop");

// if the line is found print the model id from the first column
if (cop) {
  log("Cop model ID is " + parseInt(cop[0]));
} else {
  log("Model with the name 'cop' not found");
}

This script shows how you can use .find and other common array methods to find a line in the imported IDE file and use it. It prints 1 when given a file with the content shown above.

Asynchronous Programming

This feature is highly experimental and might be unstable.

Since 1.0.4 CLEO Redux receives support for Promises and async/await syntax. This enables lots of advanced code patterns.

Note that asynchoronous programming is a fairly complex topic and if you want to learn more about visit the following resources:

See an example of an async script for GTA III here.

Async API

CLEO Redux 1.0.4 has one native async command: asyncWait. This command is used to pause a script for a specified amount of time. It returns a Promise that resolves after the specified amount of time. asyncWait can only be used inside a async function.

async function delay(ms) {
  log("Waiting for " + ms + "ms");
  await asyncWait(ms);
  log("Done waiting");
}

delay(1000);

Difference between asyncWait and wait is that the former is not a blocking command. If you don't put the await keyword in front of it, the script execution will continue.

async function delay(ms) {
  log("Waiting for " + ms + "ms");
  asyncWait(ms); // no await here, the script continues
  log("Executed immediately");
}

delay(1000);

Async Functions

CLEO Redux does not support a top-level await. This means that you cannot use the await keyword in the main body of the script.

// script begins
await asyncWait(1000); // won't work
showTextBox("Hello");

To bypass this limitation wrap your code in an anonymous async function.

(async function () {
  // script begins
  await asyncWait(1000); // works, because it's inside an async function
  showTextBox("Hello");
})();

Note that by design any exception thrown inside an async function is not caught automatically. This means that if you want to catch an exception you need to wrap your code in a try/catch block or add .catch handler to the promise.

(async function () {
  // script code goes here
  non_existing_function();
})().catch((e) => {
  log(e);
});

The log will contain the following message:

"ReferenceError: 'non_existing_function' is not defined"

Concurrent Async Functions

One of the advantages of async functions is that they could run concurrently. This means that you can run multiple async functions at the same time. Some languages call these coroutines.

import { KeyCode } from "./.config/enums";
let gVar = 0;

(async () => {
  await asyncWait(0);

  // running task1, task2, and task3 concurrently
  task1();
  task2();
  task3();
})();

// shakes the camera
async function task1() {
  while (true) {
    await asyncWait(0);
    Camera.Shake(30);
  }
}

// awards the player with $1000 every second and increments the global variable gVar
async function task2() {
  let p = new Player(0);
  while (true) {
    await asyncWait(1000);

    p.addScore(1);
    gVar++;
  }
}

// waits for the button J to be pressed and displays the value of gVar
async function task3() {
  while (true) {
    await asyncWait(0);
    if (Pad.IsKeyDown(KeyCode.J)) {
      showTextBox("gVar is " + gVar);
    }
  }
}

This is very similar to writing traditional while(true){} loops with a wait command in it, with the difference that the functions must have async keyword and use asyncWait instead of wait Each of the three tasks are executed independently from each other. Note that runtime guarantees that all async functions are executed in the same thread. They could share global variables and mutate them. Look at how gVar is incremented in task2 and read in task3.

Dynamic Imports

CLEO Redux supports dynamic imports. It allows to load script files on-demand when they are needed. This is useful for large scripts that are not needed all the time.

(async () => {
  if (somethingIsTrue) {
    // import module for side effects
    await import("./my-module.mjs");
  }
})();

Imported modules can export functions and variables. They can be used in the same way as in regular scripts.

my-module.mjs:

export const myVar = 42;

main.js:

(async () => {
  const { myVar } = await import("./my-module.mjs");
  log(myVar); // prints 42
})();

JavaScript API

Native functions

CLEO Redux supports all commands native to the current game. In the classic GTA 3D series they are also known as opcodes. In GTA IV they are known as native functions. You can find them in Sanny Builder Library.

Each available command has a predefined name that the game associates with a particular set of instructions running as you execute that command with arguments. To call a command by name use a built-in function native. For example, to change the player's health run native("SET_PLAYER_HEALTH", 0, 100), where 0 is the player's id and 100 is the desired health.

For convenience, CLEO Redux also defines a wide set of abstractions on top of the native functions called classes. Each class represents a group of commands around some domain, e.g. commands related to the player, vehicles, or text display can be found in classes Player, Car, or Text respectively. You can browse available classes and methods they provide in Sanny Builder Library.

For example, to change the player's health using classes run p.setHealth(100), where p is an instance of the Player class created with new Player() or Player.Create functions.

CLEO Redux Bindings

In addition to native commands CLEO Redux adds extra variables and functions.

Variables

HOST

the host name (previously available as GAME variable). Possible values are gta3, vc, re3, reVC, sa, gta3_unreal, vc_unreal, sa_unreal, gta_iv, bully, unknown.

CLEO plugins can use SDK to customize the name for their needs.

if (HOST === "gta3") {
  showTextBox("This is GTA III");
}
if (HOST === "sa") {
  showTextBox("This is San Andreas");
}
if (HOST === "unknown") {
  showTextBox("This host is not natively supported");
}

ONMISSION

the global flag controlling whether the player is on a mission now. Not available on an unknown host.

if (!ONMISSION) {
  showTextBox("Not on a mission. Setting ONMISSION to true");
  ONMISSION = true;
}

Setting ONMISSION to true has a side effect of turning the current script into a mission script, e.g. you can use mission-only commands, such as STORE_CAR_CHAR_IS_IN or MISSION_HAS_FINISHED. Setting ONMISSION to false turns the current script into a normal script.

TIMERA, TIMERB

two auto-incrementing timers useful for measuring time intervals.

while (true) {
  TIMERA = 0;
  // wait 1000 ms
  while (TIMERA < 1000) {
    wait(0);
  }
  showTextBox("1 second passed");
}

__dirname

an absolute path to the directory with the current file

__filename

an absolute path to the current file

Functions

log

log(...values) prints comma-separated {values} to the cleo_redux.log

var x = 1;
log("value of x is ", x);

wait

wait(timeInMs) pauses the script execution for at least {timeInMs} milliseconds

while (true) {
  wait(1000);
  log("1 second passed");
}

showTextBox

showTextBox(text) displays {text} in the black rectangular box. Not available on an unknown host.

showTextBox("Hello, world!");

exit

exit(reason?) terminates the script immediately. exit function accepts an optional string argument that will be added to the cleo_redux.log.

exit("Script ended");

native

native(command_name, ...input_args) is a low-level function to execute a command using its name {command_name}. The command name matches name property in a JSON file provided by Sanny Builder Library.

native("SET_TIME_OF_DAY", 12, 30); // sets the time of day to 12:30

For the commands that return a single value, the result is this value.

const progress = native("GET_PROGRESS_PERCENTAGE");
showTextBox(`Progress is ${progress}`);

For the commands that return multiple values, the result is an object where each key corresponds to a returned value. The key names match the output names given in the command definition

var pos = native("GET_CHAR_COORDINATES", char); // returns char's coordinates vector {x, y, z}
showTextBox(`Character pos: x ${pos.x} y ${pos.y} z ${pos.z}`);

For the conditional commands the result is the boolean value true or false

if (native("HAS_MODEL_LOADED", 101)) {
  // checks the condition
  showTextBox("Model with id 101 has been loaded");
}

Static Objects

Memory

  • Memory object allows to manipulate the process memory. See the Memory guide for more information.

Math

  • Math object is a standard object available in the JS runtime that provides common mathematical operations. CLEO Redux extends it with some extra commands. See the Math object for more information.

FxtStore

  • FxtStore allows to update the content of in-game texts. See the Custom Text guide for details.

CLEO

  • CLEO object provides access to the runtime information and utilities:

    CLEO.debug
    • CLEO.debug.trace(flag) toggles on and off command tracing in the current script. When {flag} is true all executed commands get added to cleo_redux.log:
    CLEO.debug.trace(true);
    wait(50);
    const p = new Player(0);
    CLEO.debug.trace(false);
    
    CLEO.version
    • CLEO.version - a complex property providing information about current CLEO version
    log(CLEO.version); // "0.9.4-dev.20220427"
    log(CLEO.version.major); // "0"
    log(CLEO.version.minor); // "9"
    log(CLEO.version.patch); // "4"
    log(CLEO.version.pre); // "dev"
    log(CLEO.version.build); // "20220427"
    
    CLEO.apiVersion
    • CLEO.apiVersion - a complex property providing information about current API (using meta.version field in the primary definition file). Scripts can use it to check if the user has a particular API version installed.
    log(CLEO.apiVersion); // "0.219"
    log(CLEO.apiVersion.major); // "0"
    log(CLEO.apiVersion.minor); // "219"
    log(CLEO.apiVersion.patch); // undefined
    log(CLEO.apiVersion.pre); // undefined
    log(CLEO.apiVersion.build); // undefined
    
CLEO.runScript
  • (since 1.0.4) CLEO.runScript(fileName, args?) - method that spawns a new instance of the script. fileName is the path to the script to launch. args is an optional parameter to pass arguments to the script.

    Don't overuse this feature as spawning a new script is a costly operation. Avoid spawning too many scripts in a loop.

    runScript has the following limitations:

    • JS scripts must have an extension .mjs, CS scripts must have an extension .s. This is necessary to avoid automatic script loading.
    • spawning CS scripts is not supported in the delegate mode (i.e. won't work in GTA San Andreas with CLEO 4 installed.)

    When running a new script you can also provide arguments to it. args is a JavaScript object which keys correspond to variable names in the script. Key names for a CS script are numeric and correspond to local variables (0@, 1@, 2@, etc). JS scripts can receive both numbers and strings as arguments, whereas CS scripts can only receive numbers.

    You can spawn multiple instances of the same script with different arguments.

    Launching a new JS script

    Imagine that you have two files main.js and child.mjs in the CLEO directory:

    main.js:

    CLEO.runScript("./child.mjs", { a: 1, b: "str", c: 10.5 });
    

    child.mjs:

    showTextBox("child.mjs was launched with: " + a + " " + b + " " + c);
    

    Now if you run the game you should see the following message: child.mjs was launched with: 1 str 10.5.

    Launching a new CS script

    main.js:

    CLEO.runScript("./child.cs", { 1: 500 });
    

    child.cs:

    0109: player $PLAYER_CHAR money += 1@
    0A93: terminate_this_custom_script
    

    The player will get 500 dollars.

    To pass floating-point numbers to a CS script use Memory.FromFloat function:

    main.js:

    CLEO.runScript("./child.cs", {
      0: Memory.FromFloat(-921.25),
      1: Memory.FromFloat(662.125),
      2: Memory.FromFloat(-100.0),
    });
    

    child.cs:

    00A1: set_char_coordinates $PLAYER_ACTOR x 0@ y 1@ z 2@
    0A93: terminate_this_custom_script
    

Fluent Interface

Methods on constructible entities (such as Player, Car, Char -- any entities created via a constructor method) support chaining (also known as Fluent Interface). It allows to write code like this:

var p = new Player(0);

p.giveWeapon(2, 100)
  .setHealth(5)
  .setCurrentWeapon(2)
  .getChar()
  .setCoordinates(1144, -600, 14)
  .setBleeding(true);

Destructor methods interrupt the chain. E.g. given the code:

Char.Create(0, 0, 0, 0, 0).setCoordinates(0, 0, 0).delete()

the chain can not continue after delete method as the character gets removed and its handle is not longer valid.

Enums

Many commands make use of enumerated types or enums for parameters with a finite range of allowed values. Each value in the range is associated with a text identifier so you don't have to memorize numbers for parameters such as weapon ids, ped types, key codes, etc.

Enum values can be either numbers or strings.

Enum names start with a capital letter similar to class names, e.g. PedType, KeyCode, Button.

Sanny Builder Library lists all known enums for each game. It also provides a enums.js file with all enums exported as JavaScript objects for use in scripts. CLEO Redux automatically downloads this file along with the primary definitions file and places it in the .config directory.

To use an enum in a script import it like this:

import { EnumName } from "./.config/enums"; // replace EnumName with the actual name

You can also import all enums at once using the following syntax:

import * as enums from "./.config/enums";

Then you can access values in the enum as regular properties in a JavaScript object:

import { KeyCode } from "./.config/enums";

if (Pad.IsKeyPressed(KeyCode.A)) {
  log("A is pressed");
}

or

import * as enums from "./.config/enums";

if (Pad.IsKeyPressed(enums.KeyCode.A)) {
  log("A is pressed");
}

You can iterate over enum fields by using Object.values, Object.keys, or Object.entries functions:

Object.keys(KeyCode).forEach((key) => {
  log(key);
});

Object.values(KeyCode).forEach((value) => {
  log(value);
});

Object.keys(KeyCode).forEach(([key, value]) => {
  log(key, value);
});

If you find an issue with an enum, a missing or incorrect value, report it in Sanny Builder Library repo on GitHub.

ScriptObject vs Object

Sanny Builder Library defines a static class Object to group commands allowing to create and manipulate 3D objects in-game. At the same time JavaScript has the native Object class with its own methods.

To avoid mixing the two, CLEO Redux uses ScriptObject class instead of the Library's Object with the same interface.

// opcode 0107, creates a new object in the game
var x = ScriptObject.Create(modelId, x, y, z); 

// native JavaScript code, creates a new object in JS memory
var x = Object.create(null); 

Using Math Object

JavaScript has a built-in Math object that provides common mathematical operations, such as abs, sin, cos, random, pow, sqr, etc. CLEO Redux extends this object to include extra operations supported by the game. The interface of Math looks as follows:

interface Math {
    // native code
    readonly E: number;
    readonly LN10: number;
    readonly LN2: number;
    readonly LOG2E: number;
    readonly LOG10E: number;
    readonly PI: number;
    readonly SQRT1_2: number;
    readonly SQRT2: number;
    abs(x: number): number;
    acos(x: number): number;
    asin(x: number): number;
    atan(x: number): number;
    atan2(y: number, x: number): number;
    ceil(x: number): number;
    cos(x: number): number;
    exp(x: number): number;
    floor(x: number): number;
    log(x: number): number;
    max(...values: number[]): number;
    min(...values: number[]): number;
    pow(x: number, y: number): number;
    random(): number;
    round(x: number): number;
    sin(x: number): number;
    sqrt(x: number): number;
    tan(x: number): number;
    clz32(x: number): number;
    imul(x: number, y: number): number;
    sign(x: number): number;
    log10(x: number): number;
    log2(x: number): number;
    log1p(x: number): number;
    expm1(x: number): number;
    cosh(x: number): number;
    sinh(x: number): number;
    tanh(x: number): number;
    acosh(x: number): number;
    asinh(x: number): number;
    atanh(x: number): number;
    hypot(...values: number[]): number;
    trunc(x: number): number;
    fround(x: number): number;
    cbrt(x: number): number;

    // GTA III, GTA Vice City, GTA SA commands
    ConvertMetersToFeet(meters: int): int;
    RandomFloatInRange(min: float, max: float): float;
    RandomIntInRange(min: int, max: int): int;

    // GTA Vice City, GTA SA commands
    GetDistanceBetweenCoords2D(fromX: float, fromY: float, toX: float, toZ: float): float;
    GetDistanceBetweenCoords3D(fromX: float, fromY: float, fromZ: float, toX: float, toY: float, toZ: float): float;

    // GTA SA commands
    GetAngleBetween2DVectors(xVector1: float, yVector1: float, xVector2: float, yVector2: float): float;
    GetHeadingFromVector2D(_p1: float, _p2: float): float;
    LimitAngle(value: float): float;
}

The first group includes the native constants and methods provided by the JavaScript runtime. They start with a lowercase letter, e.g. Math.abs. You can find the detailed documentation for these methods here.

Then the game-specific commands follow. According to the naming convention, each method that is bound to a script opcode starts with a capital letter, e.g. Math.RandomIntInRange (opcode 0209). You can find the documentation in Sanny Builder Library.

    var x = Math.abs(-1); // x = 1
    var f = Math.ConvertMetersToFeet(10) // f = 32
    var pi = Math.floor(Math.PI) // pi = 3

Native Math methods were given a higher priority over the game commands with the same functionality. For example, to calculate an absolute value of the number, there is Math.abs, not Math.Abs.

This guide is for x86 hosts (such as classic era games). For the information on using the Memory class on x64 hosts (such as the Definitive edition) click here.

Memory Object

An intrinsic object Memory provides methods for accessing and manipulating the data or code in the current process. It has the following interface:

interface Memory {
    ReadFloat(address: int, vp: boolean): float;
    WriteFloat(address: int, value: float, vp: boolean): void;
    ReadI8(address: int, vp: boolean): int;
    ReadI16(address: int, vp: boolean): int;
    ReadI32(address: int, vp: boolean): int;
    ReadU8(address: int, vp: boolean): int;
    ReadU16(address: int, vp: boolean): int;
    ReadU32(address: int, vp: boolean): int;
    ReadUtf8(address: int): string;
    ReadUtf16(address: int): string;
    WriteI8(address: int, value: int, vp: boolean): void;
    WriteI16(address: int, value: int, vp: boolean): void;
    WriteI32(address: int, value: int, vp: boolean): void;
    WriteU8(address: int, value: int, vp: boolean): void;
    WriteU16(address: int, value: int, vp: boolean): void;
    WriteU32(address: int, value: int, vp: boolean): void;
    WriteUtf8(address: int, value: string, vp: boolean): void;
    WriteUtf16(address: int, value: string, vp: boolean): void;
    Read(address: int, size: int, vp: boolean): int;
    Write(address: int, size: int, value: int, vp: boolean): void;

    ToFloat(value: int): float;
    FromFloat(value: float): int;
    ToU8(value: int): int;
    ToU16(value: int): int;
    ToU32(value: int): int;
    ToI8(value: int): int;
    ToI16(value: int): int;
    ToI32(value: int): int;

    Translate(symbol: string): int;

    CallFunction(address: int, numParams: int, pop: int, ...funcParams: int[]): void;
    CallFunctionReturn(address: int, numParams: int, pop: int, ...funcParams: int[]): int;
    CallFunctionReturnFloat(address: int, numParams: int, pop: int, ...funcParams: int[]): float;
    CallMethod(address: int, struct: int, numParams: int, pop: int, ...funcParams: int[]): void;
    CallMethodReturn(address: int, struct: int, numParams: int, pop: int, ...funcParams: int[]): int;
    CallMethodReturnFloat(address: int, struct: int, numParams: int, pop: int, ...funcParams: int[]): float;

    Fn: {
        Cdecl(address: int): (...funcParams: int[]) => int;
        CdeclFloat(address: int): (...funcParams: int[]) => float;
        CdeclI8(address: int): (...funcParams: int[]) => int;
        CdeclI16(address: int): (...funcParams: int[]) => int;
        CdeclI32(address: int): (...funcParams: int[]) => int;
        CdeclU8(address: int): (...funcParams: int[]) => int;
        CdeclU16(address: int): (...funcParams: int[]) => int;
        CdeclU32(address: int): (...funcParams: int[]) => int;

        Stdcall(address: int): (...funcParams: int[]) => int;
        StdcallFloat(address: int): (...funcParams: int[]) => float;
        StdcallI8(address: int): (...funcParams: int[]) => int;
        StdcallI16(address: int): (...funcParams: int[]) => int;
        StdcallI32(address: int): (...funcParams: int[]) => int;
        StdcallU8(address: int): (...funcParams: int[]) => int;
        StdcallU16(address: int): (...funcParams: int[]) => int;
        StdcallU32(address: int): (...funcParams: int[]) => int;

        Thiscall(address: int, struct: int): (...funcParams: int[]) => int;
        ThiscallFloat(address: int, struct: int): (...funcParams: int[]) => float;
        ThiscallI8(address: int, struct: int): (...funcParams: int[]) => int;
        ThiscallI16(address: int, struct: int): (...funcParams: int[]) => int;
        ThiscallI32(address: int, struct: int): (...funcParams: int[]) => int;
        ThiscallU8(address: int, struct: int): (...funcParams: int[]) => int;
        ThiscallU16(address: int, struct: int): (...funcParams: int[]) => int;
        ThiscallU32(address: int, struct: int): (...funcParams: int[]) => int;
    }
}

Reading and Writing Values

Group of memory access methods (ReadXXX/WriteXXX) can be used for reading or modifying values stored in the memory. Each method is designed for a particular data type. To change a floating-point value (which occupies 4 bytes in the original game) use Memory.WriteFloat, e.g.:

    Memory.WriteFloat(address, 1.0, false)

where address is a variable storing the memory location, 1.0 is the value to write and false means it's not necessary to change the memory protection with VirtualProtect (the address is already writable).

Similarly, to read a value from the memory, use one of the ReadXXX methods, depending on what data type the memory address contains. For example, to read a 8-bit signed integer (also known as a char or uint8) use Memory.ReadI8, e.g.:

    var x = Memory.ReadI8(address, true)

variable x now holds a 8-bit integer value in the range (0..255). For the sake of showing possible options, this example uses true as the last argument, which means the default protection attribute for this address will be changed to PAGE_EXECUTE_READWRITE before the read.

    var gravity = Memory.ReadFloat(gravityAddress, false);
    gravity += 0.05;
    Memory.WriteFloat(gravityAddress, gravity, false);

Finally, last two methods Read and Write is what other methods use under the hood. They have direct binding to opcodes 0A8D READ_MEMORY and 0A8C WRITE_MEMORY and produce the same result.

The size parameter in the Read method can only be 1, 2 or 4. CLEO treats the value as a signed integer stored in the little-endian format.

In the Write method any size larger than 0 is allowed. Sizes 3 and 5 onwards can only be used together with a single byte value. CLEO uses them to fill a continious block of memory starting at the address with the given value (think of it as memset in C++).

    Memory.Write(addr, 0x90, 10, true) // "noping" 10 bytes of code starting from addr

The usage of any of the read/write methods requires the mem permission.

Reading and Writing Strings

The ReadUtf8 and ReadUtf16 methods are used to read strings from the memory and return it as a JavaScript string. They read the character sequence until the first null terminator is found. ReadUtf8 expects the string to be encoded in UTF-8, while ReadUtf16 expects UTF-16. Null terminator is not included in the result.

    var str = Memory.ReadUtf8(address);
    var str = Memory.ReadUtf16(address);

The WriteUtf8 and WriteUtf16 methods are used to write a JavaScript string to the memory. They write any character sequence including null terminator to the memory. WriteUtf8 encodes the string in UTF-8, while WriteUtf16 encodes it in UTF-16. Last argument is a boolean value indicating whether the command is allowed to overwrite the memory protection.

    Memory.WriteUtf8(address, "Hello, world!\0", true);
    Memory.WriteUtf16(address, "Hello, world!\0", true);

Casting methods

By default Read and Write methods treat data as signed integer values. It can be inconvinient if the memory holds a floating-point value in IEEE 754 format or a large 32-bit signed integer (e.g. a pointer). In this case use casting methods ToXXX/FromXXX. They act similarly to reinterpret_cast operator in C++.

To get a quick idea what to expect from those methods see the following examples:

    Memory.FromFloat(1.0) => 1065353216
    Memory.ToFloat(1065353216) => 1.0
    Memory.ToU8(-1) => 255
    Memory.ToU16(-1) => 65535
    Memory.ToU32(-1) => 4294967295
    Memory.ToI8(255) => -1
    Memory.ToI16(65535) => -1
    Memory.ToI32(4294967295) => -1

Alternatively, use appropriate methods to read/write the value as a float (ReadFloat/WriteFloat) or as an unsigned integer (ReadUXXX/WriteUXXX).

Calling Foreign Functions

Memory object allows to invoke a foreign (native) function by its address using one of the following methods:

  • Memory.CallFunction - calls a function at the address and discards the returned value

  • Memory.CallFunctionReturn - calls a function and at the address and returns an integer value

  • Memory.CallFunctionReturnFloat - calls a function and at the address and returns a floating-point value

  • Memory.CallMethod - calls a class instance method and discards the returned value

  • Memory.CallMethodReturn - calls a class instance method and returns an integer value

  • Memory.CallMethodReturnFloat - calls a class instance method and returns a floating-point value

    Memory.CallFunction(0x1234567, 2, 0, 1000, 2000)

where 0x1234567 is the address of the function, 2 is the number of arguments, 0 is the pop parameter (see below), 1000 and 2000 are the two arguments passed into the function.

Legacy SCM implementation of the call commands require the arguments of the invoked function to be listed in reverse order. That's it, you would see the same call in SCM as:

0AA5: call 0x1234567 num_params 2 pop 0 2000 1000

where 2000 is the second argument passed to the function located at 0x1234567 and 1000 is the first one. In JS code all input arguments go in the direct order.

The third parameter (pop) in Memory.CallFunction defines the calling convention. When it is set to 0, the function is called using the stdcall convention. When it is set to the same value as numParam, the function is called using the cdecl convention. Any other value breaks the code.

Memory.CallFunctionReturn has the same interface but additionally it writes the result of the function to a variable.

Memory.CallMethod invokes a method of an object:

    Memory.CallMethod(0x2345678, 0x7001234, 2, 0, 1000, 2000)

The second parameter (0x7001234) is the object address. The pop parameter is always 0 (the method uses the thiscall convention).

To call the method and get the result out of it, use Memory.CallMethodReturn.

Note that all arguments are read as 32-bit signed integers. If you need to provide an argument of the float type, use Memory.FromFloat, e.g.

    Memory.CallFunction(0x1234567, 1, 1, Memory.FromFloat(123.456))

CLEO Redux supports calling foreign functions with up to 16 parameters.

The usage of any of the call methods requires the mem permission.

Convenience methods with Fn object

Memory.Fn provides a lot of convenient methods for calling different types of foreign functions.

Fn: {
        Cdecl(address: int): (...funcParams: int[]) => int;
        CdeclFloat(address: int): (...funcParams: int[]) => float;
        CdeclI8(address: int): (...funcParams: int[]) => int;
        CdeclI16(address: int): (...funcParams: int[]) => int;
        CdeclI32(address: int): (...funcParams: int[]) => int;
        CdeclU8(address: int): (...funcParams: int[]) => int;
        CdeclU16(address: int): (...funcParams: int[]) => int;
        CdeclU32(address: int): (...funcParams: int[]) => int;

        Stdcall(address: int): (...funcParams: int[]) => int;
        StdcallFloat(address: int): (...funcParams: int[]) => float;
        StdcallI8(address: int): (...funcParams: int[]) => int;
        StdcallI16(address: int): (...funcParams: int[]) => int;
        StdcallI32(address: int): (...funcParams: int[]) => int;
        StdcallU8(address: int): (...funcParams: int[]) => int;
        StdcallU16(address: int): (...funcParams: int[]) => int;
        StdcallU32(address: int): (...funcParams: int[]) => int;

        Thiscall(address: int, struct: int): (...funcParams: int[]) => int;
        ThiscallFloat(address: int, struct: int): (...funcParams: int[]) => float;
        ThiscallI8(address: int, struct: int): (...funcParams: int[]) => int;
        ThiscallI16(address: int, struct: int): (...funcParams: int[]) => int;
        ThiscallI32(address: int, struct: int): (...funcParams: int[]) => int;
        ThiscallU8(address: int, struct: int): (...funcParams: int[]) => int;
        ThiscallU16(address: int, struct: int): (...funcParams: int[]) => int;
        ThiscallU32(address: int, struct: int): (...funcParams: int[]) => int;
    }

These methods is designed to cover all possible function signatures. For example, this code

    Memory.CallMethod(0x2345678, 0x7001234, 2, 0, 1000, 2000)

can also be written as

    Memory.Fn.Thiscall(0x2345678, 0x7001234)(1000, 2000)

Note a few key differences here. First of all, Memory.Fn methods don't invoke a foreign function directly. Instead, they return a new JavaScript function that can be stored in a variable and reused to call the associated foreign function many times with different arguments:

    var myMethod = Memory.Fn.Thiscall(0x2345678, 0x7001234);
    myMethod(1000, 2000); // calls method 0x2345678 with arguments 1000 and 2000
    myMethod(3000, 5000); // calls method 0x2345678 with arguments 3000 and 5000

The second difference is that there are no numParams and pop parameters. Each Fn method figures them out automatically.

By default a returned result is considered a 32-bit signed integer value. If the function returns another type (a floating-point value, or a signed integer), use one of the methods matching the function signature, e.g.:

    var flag = Memory.Fn.CdeclU8(0x1234567)()

This code invokes a cdecl function at 0x1234567 with no arguments and stores the result as a 8-bit unsigned integer value.

Finding Memory Addresses in re3 and reVC

Since re3 and reVC use address space layout randomization (ASLR) feature, it can be difficult to locate needed addresses. CLEO Redux provides a helper function Memory.Translate that accepts a name of the function or variable and returns its current address. If the requested symbol is not found, the result is 0.

    var addr = Memory.Translate("CTheScripts::MainScriptSize");

    // check if address is not zero
    if (addr) {
        showTextBox("MainScriptSize = " + Memory.ReadI32(addr, 0))
    }

At the moment Memory.Translate should only be used in re3 and reVC. In other games you will be getting 0 as a result most of the time.

This guide is for x64 hosts (e.g. the remastered trilogy). For the information on using the Memory class on x86 (classic era games) click here.

Memory Object (x64)

An intrinsic object Memory provides methods for accessing and manipulating the data or code in the current process. It has the following interface:

interface Memory {
    ReadFloat(address: int, vp: boolean, ib: boolean): float;
    WriteFloat(address: int, value: float, vp: boolean, ib: boolean): void;
    ReadI8(address: int, vp: boolean, ib: boolean): int;
    ReadI16(address: int, vp: boolean, ib: boolean): int;
    ReadI32(address: int, vp: boolean, ib: boolean): int;
    ReadU8(address: int, vp: boolean, ib: boolean): int;
    ReadU16(address: int, vp: boolean, ib: boolean): int;
    ReadU32(address: int, vp: boolean, ib: boolean): int;
    ReadUtf8(address: int, ib: boolean): string;
    ReadUtf16(address: int, ib: boolean): string;
    WriteI8(address: int, value: int, vp: boolean, ib: boolean): void;
    WriteI16(address: int, value: int, vp: boolean, ib: boolean): void;
    WriteI32(address: int, value: int, vp: boolean, ib: boolean): void;
    WriteU8(address: int, value: int, vp: boolean, ib: boolean): void;
    WriteU16(address: int, value: int, vp: boolean, ib: boolean): void;
    WriteU32(address: int, value: int, vp: boolean, ib: boolean): void;
    WriteUtf8(address: int, value: string, vp: boolean, ib: boolean): void;
    WriteUtf16(address: int, value: string, vp: boolean, ib: boolean): void;
    Read(address: int, size: int, vp: boolean, ib: boolean): int;
    Write(address: int, size: int, value: int, vp: boolean, ib: boolean): void;

    ToFloat(value: int): float;
    FromFloat(value: float): int;
    ToU8(value: int): int;
    ToU16(value: int): int;
    ToU32(value: int): int;
    ToI8(value: int): int;
    ToI16(value: int): int;
    ToI32(value: int): int;

    CallFunction(address: int, ib: boolean, numParams: int, ...funcParams: int[]): void;
    CallFunctionReturn(address: int, ib: boolean, numParams: int, ...funcParams: int[]): int;
    CallFunctionReturnFloat(address: int, ib: boolean, numParams: int, ...funcParams: int[]): float;

    Fn: {
        X64(address: int, ib: boolean): (...funcParams: int[]) => int;
        X64Float(address: int, ib: boolean): (...funcParams: int[]) => float;
        X64I8(address: int, ib: boolean): (...funcParams: int[]) => int;
        X64I16(address: int, ib: boolean): (...funcParams: int[]) => int;
        X64I32(address: int, ib: boolean): (...funcParams: int[]) => int;
        X64U8(address: int, ib: boolean): (...funcParams: int[]) => int;
        X64U16(address: int, ib: boolean): (...funcParams: int[]) => int;
        X64U32(address: int, ib: boolean): (...funcParams: int[]) => int;
    }
}

Reading and Writing Values

Group of memory access methods (ReadXXX/WriteXXX) can be used for reading or modifying values stored in the memory. Each method is designed for a particular data type. To change a floating-point value (which occupies 4 bytes in the original game) use Memory.WriteFloat, e.g.:

    Memory.WriteFloat(address, 1.0, false, false)

where address is a variable storing the memory location, 1.0 is the value to write, the first false means it's not necessary to change the memory protection with VirtualProtect (the address is already writable). The second false is the value of the ib flag that instructs CLEO to treat the address either as an absolute address (ib = false) or a relative offset to the current image base address (ib = true). Due to an ASLR feature memory addresses could change when the game runs because the base address changes. Consider the following example:

0x1400000000 ImageBase
...
...
0x1400000020 SomeValue

You want to change SomeValue that is currently located at 0x1400000020. You can do it with Memory.Write(0x1400000020, 1, 1, false, false). However on the next game run the memory layout might look like this:

0x1500000000 ImageBase
...
...
0x1500000020 SomeValue

effectively breaking the script. In this case, calculate a relative offset from the image base ( 0x1500000020 - 0x1500000000 = 0x20 ), that will be permanent for the particular game version. Use Memory.Write as follows: Memory.Write(0x20, 1, 1, false, true). CLEO will sum up the offset (0x20) with the current value of the image base (0x1400000000, 0x1500000000, etc) and write to the correct absolute address.

For your convenience you can find the current value of the image base in the cleo_redux.log, e.g.:

09:27:35 [INFO] Image base address 0x7ff7d1f50000

Similarly, to read a value from the memory, use one of the ReadXXX methods, depending on what data type the memory address contains. For example, to read a 8-bit signed integer (also known as a char or uint8) use Memory.ReadI8, e.g.:

    var x = Memory.ReadI8(offset, true, true)

variable x now holds a 8-bit integer value in the range (0..255). For the sake of showing possible options, this example uses true as the last argument, which means the default protection attribute for this address will be changed to PAGE_EXECUTE_READWRITE before the read.

    var gravity = Memory.ReadFloat(gravityOffset, false, true);
    gravity += 0.05;
    Memory.WriteFloat(gravityOffset, gravity, false, true);

Finally, last two methods Read and Write is what other methods use under the hood. They have direct binding to the Rust code that reads and write the memory. In JavaScript code you can use input arguments as large as 53-bit numbers.

The size parameter in the Read method can only be 1, 2, 4 or 8. CLEO treats the value as a signed integer stored in the little-endian format.

In the Write method any size larger than 0 is allowed. Sizes 3, 5, 6, 7 and 9 onwards can only be used together with a single byte value. CLEO uses them to fill a continious block of memory starting at the address with the given value (think of it as memset in C++).

    Memory.Write(offset, 0x90, 10, true, true) // "noping" 10 bytes of code starting from offset+image base

The usage of any of the read/write methods requires the mem permission.

Reading and Writing Strings

The ReadUtf8 and ReadUtf16 methods are used to read strings from the memory and return it as a JavaScript string. They read a character sequence until the first null terminator is found. ReadUtf8 expects the string to be encoded in UTF-8, while ReadUtf16 expects UTF-16. Null terminator is not included in the result. Last argument ib indicates whethers the address is an absolute address or a relative offset to the image base.

    var str = Memory.ReadUtf8(0x100000, false); // read string from address 0x100000
    var str = Memory.ReadUtf8(0x100000, true); // read string from address 0x100000+IMAGE_BASE

The WriteUtf8 and WriteUtf16 methods are used to write a JavaScript string to the memory. They write any character sequence including null terminator to the memory. WriteUtf8 encodes the string in UTF-8, while WriteUtf16 encodes it in UTF-16. Third argument is a boolean value indicating whether the command is allowed to overwrite the memory protection. Last argument ib indicates whethers the address is an absolute address or a relative offset to the image base.

    Memory.WriteUtf8(0x100000, "Hello, world!\0\0", true, false); // write string to address 0x100000
    Memory.WriteUtf8(0x100000, "Hello, world!\0\0", true, true); // write string to address 0x100000+IMAGE_BASE

Casting methods

By default Read and Write methods treat data as signed integer values. It can be inconvinient if the memory holds a floating-point value in IEEE 754 format or a large 32-bit signed integer (e.g. a pointer). In this case use casting methods ToXXX/FromXXX. They act similarly to reinterpret_cast operator in C++.

To get a quick idea what to expect from those methods see the following examples:

    Memory.FromFloat(1.0) => 1065353216
    Memory.ToFloat(1065353216) => 1.0
    Memory.ToU8(-1) => 255
    Memory.ToU16(-1) => 65535
    Memory.ToU32(-1) => 4294967295
    Memory.ToI8(255) => -1
    Memory.ToI16(65535) => -1
    Memory.ToI32(4294967295) => -1

Alternatively, use appropriate methods to read/write the value as a float (ReadFloat/WriteFloat) or as an unsigned integer (ReadUXXX/WriteUXXX).

Calling Foreign Functions

Memory object allows to invoke a foreign (native) function by its address using one of the following methods:

  • Memory.CallFunction - calls a function at the address and discards the returned value
  • Memory.CallFunctionReturn - calls a function and at the address and returns an integer value
  • Memory.CallFunctionReturnFloat - calls a function and at the address and returns a floating-point value
    Memory.CallFunction(0xEFFB30, true, 1, 13)

where 0xEFFB30 is the function offset relative to IMAGE BASE (think of it a randomized start address of the game memory), true is the ib flag (see below), 1 is the number of input arguments, and 13 are the only argument passed into the function.

The ib parameter in Memory.CallFunction has the same meaning as in memory read/write commands. When set to true CLEO adds the current known address of the image base to the value provided as the first argument to calculate the absolute memory address of the function. When set to false no changes to the first argument are made.

To pass floating-point values to the function, convert the value to integer using Memory.FromFloat:

    Memory.CallFunction(0x1234567, true, 1, Memory.FromFloat(123.456));

The returned value of the function called with Memory.CallFunction is ignored. To read the result use Memory.CallFunctionReturn that has the same parameters. Use Memory.CallFunctionReturnFloat to call a function that returns a floating-point value.

CLEO Redux supports calling foreign functions with up to 16 parameters.

The usage of any of the call methods requires the mem permission.

Convenience methods with Fn object

Memory.Fn provides convenient methods for calling different types of foreign functions.

    Fn: {
        X64(address: int, ib: boolean): (...funcParams: int[]) => int;
        X64Float(address: int, ib: boolean): (...funcParams: int[]) => float;
        X64I8(address: int, ib: boolean): (...funcParams: int[]) => int;
        X64I16(address: int, ib: boolean): (...funcParams: int[]) => int;
        X64I32(address: int, ib: boolean): (...funcParams: int[]) => int;
        X64U8(address: int, ib: boolean): (...funcParams: int[]) => int;
        X64U16(address: int, ib: boolean): (...funcParams: int[]) => int;
        X64U32(address: int, ib: boolean): (...funcParams: int[]) => int;
    }

These methods is designed to cover all supported return types. For example, this code

    Memory.CallFunction(0xEFFB30, true, 1, 13)

can also be written as

    Memory.Fn.X64(0xEFFB30, true)(13)

Note a few key differences here. First of all, Memory.Fn methods don't invoke a foreign function directly. Instead, they return a new JavaScript function that can be stored in a variable and reused to call the associated foreign function many times with different arguments:

    var f = Memory.Fn.X64(0xEFFB30, true);
    f(13) // calls function 0xEFFB30 with the argument of 13
    f(11) // calls method 0xEFFB30 with the argument of 11

The second difference is that there is no numParams parameter. Each Fn method figures it out automatically.

By default a returned result is considered a 64-bit signed integer value. If the function returns another type (e.g. a boolean), use one of the methods matching the function signature:

    var flag = Memory.Fn.X64U8(0x1234567, true)()

This code invokes a function at 0x1234567 + IMAGE_BASE with no arguments and stores the result as a 8-bit unsigned integer value.

    var float = Memory.Fn.X64Float(0x456789, true)()

This code invokes a function at 0x456789 + IMAGE_BASE with no arguments and stores the result as a floating-point value.

Deprecated

Usage of the following commands is not recommended.

GAME

GAME variable has been renamed to HOST.

Troubleshooting

CLEO does not work with re3 or reVC

CLEO Redux supports "Windows D3D9" version of re3 or reVC (both 32-bit and 64-bit).

During installation you must select the correct version of re3 or reVC: either 32-bit or 64-bit.

When running on re3 and reVC make sure the game directory contains the file re3.pdb (for re3) or reVC.pdb (for reVC). Due to the dynamic nature of memory addresses in those implementations CLEO Redux relies on debug information stored in the PDB file to correctly locate itself.

Game crashes with CLEO on San Andreas: The Definitive Edition

  • make sure you installed the 64-bit version of Ultimate ASI Loader (direct link to the latest release).
    • Put version.dll to Gameface\Binaries\Win64
  • make sure you run the latest CLEO Redux version (0.8.2 and above)
  • delete config files from Documents\Rockstar Games\GTA San Andreas Definitive Edition\Config\WindowsNoEditor
  • run the game (or Rockstar Games Launcher) as administrator

If CLEO can't create files in Gameface\Binaries\Win64 it uses another path at C:\Users\<your_usename>\AppData\Roaming\CLEO Redux. There should be cleo_redux.log and the CLEO folder where all your scripts go.

Scripts stopped working after CLEO Redux update

In rare cases there might be an error in command definitions in Sanny Builder Library. Delete the definition file from the .config folder and restart the game. CLEO will redownload the latest version of the file. If the problem persists, report the issue using links below.

Game is stuck loading

If the last entry in cleo_redux.log is "Checking for updates..." CLEO can't access GitHub API. It might a temporary issue or your IP address is blocked from GitHub. Open cleo.ini, change CheckUpdates to 0 and restart the game.

Nothing happens when I run the game, not even a log file is there

Check that you have installed Ultimate ASI Loader correctly. Sometimes you might need to give its dll file another name for the game to load it. Refer to this documentation.

My problem is not listed there

Log

CLEO logs important events and errors immediately as they occur in the cleo_redux.log file located in the game directory.

If CLEO fails to create new files in the game directory due to the lack of write permissions, it fallbacks to using alternate path at C:\Users\<your_username>\AppData\Roaming\CLEO Redux. cleo_redux.log and the CLEO directory can be found there.

The log file gets overwritten on each game run. If you experience any issue when using CLEO Redux, start investigating the root cause from this file.

To stream events in your terminal while testing a script, run:

tail -f cleo_redux.log

tail is a unix command so a compatible environment is needed (for example Git Bash).

The log file also lists all executed opcodes with LogOpcodes=1 and JavaScript commands with CLEO.debug.trace(true).

Here you can find answers to the frequently asked questions about support for The Trilogy remaster.

What versions are supported?

  • GTA III: The Definitive Edition 1.0.0.14718 (Title Update 1.03), 1.0.0.15284 (Title Update 1.04), 1.0.8.11827 (Title Update 1.04.5)
  • GTA Vice City: The Definitive Edition 1.0.0.14718 (Title Update 1.03), 1.0.0.15399 (Title Update 1.04), 1.0.8.11827 (Title Update 1.04.5)
  • San Andreas: The Definitive Edition 1.0.0.14296, 1.0.0.14388, 1.0.0.14718 (Title Update 1.03), 1.0.0.15483 (Title Update 1.04), 1.0.8.11827 (Title Update 1.04.5)

Is there any difference from support of the classic games?

In short, yes. See this page for detail on what's supported and what's not.

Can I use original opcodes?

Yes, you can. Refer to the Sanny Builder library https://library.sannybuilder.com/#/sa_unreal. Take a note that some opcodes have been changed from the classic games, so don't expect everything to work like it was in classic. If you run into an issue, find help in our Discord.

How do I know what commands can I use in JavaScript?

After each game run, CLEO generates a d.ts file in the CLEO.config directory. It's called gta3.d.ts, vc.d.ts or sa.d.ts depending on the game. This file lists all supported functions and methods that you can use in JavaScript code.

To enable autocomplete in VS Code include the following line in your JS script:

/// <reference path="./.config/sa.d.ts" />

Update the path accordingly depending on which game your script is for and the file location.

Can I use CLEO opcodes?

Opcodes from CLEO Library (CLEO 4 or CLEO for GTA III and Vice City) are not supported. But CLEO Redux adds its own new opcodes for some operations.

Note that Sanny Builder does not support these new opcodes out-of-the-box yet. To enable new opcodes in your CS scripts add the following lines on top of your script:

{$O 0C00=1,  is_key_pressed %1d% }
{$O 0C01=3,%3d% = %1d% + %2d% }
{$O 0C02=3,%3d% = %1d% - %2d% }
{$O 0C03=3,%3d% = %1d% * %2d% }
{$O 0C04=3,%3d% = %1d% / %2d% }
{$O 0C05=0,terminate_this_custom_script }
{$O 0C06=5,write_memory %1d% size %2d% value %3d% virtual_protect %4d% ib %5d% }
{$O 0C07=5,%5d% = read_memory %1d% size %2d% virtual_protect %3d% ib %4d% }
{$O 0C08=-1,call_function %1d% ib %2d% num_params %3d%}
{$O 0C09=-1,call_function_return %1d% ib %2d% num_params %3d%}

This list might not be complete as there are custom plugins with extra commands (see Using SDK). Refer to Sanny Builder Library for the complete list of available commands for each game.

Can I work with the game memory or call the game functions?

Yes, check the Memory guide.

How do I compile CLEO scripts with Sanny Builder?

Use SA Mobile mode to compile CLEO scripts for San Andreas: The Definitive Edition. Note that CLEO Redux does not support CS scripts in GTA III: DE and VC: DE. JS scripts are supported in all games.

I can't find an answer to my question here, where do I go?

Other Features

CLEO Redux puts focus on improving dev experience and make scripting process easier.

Integration with Visual Studio Code

VS Code has excellent JavaScript support out of the box and is highly customizable. CLEO Redux generates typings for all supported commands that you can use when writing JavaScript in VS Code. Add the following line in your *.js script to get the full autocomplete support:

For GTA III or re3:

/// <reference path="./.config/gta3.d.ts" />

For Vice City or reVC

/// <reference path="./.config/vc.d.ts" />

For San Andreas

/// <reference path="./.config/sa.d.ts" />

For Unknown host

/// <reference path="./.config/unknown.d.ts" />

This line instructs VS Code where to look for the commands definitions for the autocomplete feature. The path can be relative to the script file or be absolute. Examples above assume JS files are located in the CLEO directory. Find out more information on the official TypeScript portal.

SCM Log

CLEO Redux has built-in support for basic tracing of SCM instructions. To trace opcodes in all running CS scripts open up cleo.ini and change LogOpcodes to 1. Note that it can greatly impact game performance due to frequent microdelays during writes to the log file. Use this option only for debugging purposes.

In JavaScript code use CLEO.debug.trace(true) to trace all commands. Use CLEO.debug.trace(false) to turn it off.

Hot Reload

CLEO monitors active scripts and reloads them in game as they change

Adding a new script file in the CLEO directory or deleting one while the game is running starts or stops the script automatically

Hot reload for CS scripts does not work when CLEO Redux runs alongside CLEO Library (e.g. in classic San Andreas).

CLEO Redux displays the information such as the version and the amount of active scripts in the main menu of GTA III / Vice City and San Andreas. To disable this information set DisplayMenuInfo to 0.

Custom Text (FXT)

CLEO Redux supports custom text content without the need to edit game files.

Static FXT files

CLEO Redux can load and serve static text content. Create a new file with the .fxt extension and put it in the CLEO\CLEO_TEXT folder. The file name can be any valid name.

Each FXT file contains a list of the key-value entries in the following format:

<KEY1> <TEXT1>
<KEY2> <TEXT2>
...
<KEYN> <TEXTN>

There should be a single space character between a key and a value. The key's maximum length is 7 characters. Try using unique keys that are unlikely to clash with other entries. The text length is unlimited, however each game may impose its own limitations.

CLEO loads FXT files on startup and merges their content in a single dictionary. It also monitors the files and reloads them if any change is made.

You can also download an editor for FXT files from the CLEO library website.

To display the custom content in game, use the Text class. The key defined in the FXT file is usually the first argument to the Text class methods, e.g.

Text.PrintHelp('KEY1') // displays <TEXT1>

You can find the commands available in each game in the Sanny Builder Library, e.g. for San Andreas: DE.

CLEO Redux only supports texts encoded in UTF-8. It means that any non-standard encoding (e.g. for Russian localization by 1C) most likely will not work.

FxtStore

CLEO Redux provides an interface for manipulating custom text directly in JavaScript code. There is a static variable, named FxtStore with the following interface:

declare interface FxtStore {
  /**
   * Inserts new text content in the script's fxt storage overwriting the previous content and shadowing static fxt with the same key
   * @param key GXT key that can be used in text commands (7 characters max)
   * @param value text content
   */
  insert(key: string, value: string): void;
  /**
   * Removes the text content associated with the key in the local fxt storage
   * @param key GXT key
   */
  delete(key: string): void;
}

Using FxtStore you can create unique keys and values in the script and put it in local FXT storage. Each script owns a private storage and keys from one script will not conflict with other scripts. Also keys defined in the FxtStore shadow the same keys defined in static FXT files. Consider the example:

custom.fxt:

MY_KEY Text from FXT file

custom.js:

Text.PrintHelp("MY_KEY"); // this displays "Text from FXT file"
FxtStore.insert("MY_KEY", "Text from script");
Text.PrintHelp("MY_KEY"); // this displays "Text from script"
FxtStore.delete("MY_KEY");
Text.PrintHelp("MY_KEY"); // this displays "Text from FXT file" again

A private FXT storage is not supported in San Andreas: The Definitive Edition. Each script there modifies the global FXT storage. This behavior may change in the future.

Custom text can be constructed dynamically, e.g.:

while (true) {
  wait(0);
  FxtStore.insert("TIME", "Timestamp: " + Date.now());
  Text.PrintHelp("TIME"); // this displays "Timestamp: " and the updated timestamp value
}

Unsupported or limited support scenarios

Despite our best effort some scenarios while available in game are not supported or supported with limitations by CLEO Redux. Some of them are imposed by the nature of SCM format or JavaScript language or the difficulties in bridging JavaScript and the native code.

Check the Features support page to find out high-level features and the status of their support in different games.

The following items are known to be not working and there is no specific timeline on getting them fixed.

Unsupported features in CS

  • in x64 games (SA: DE) you can't read and write 64-bit values as the script engine only supports 32-bit values. You may need to use other means to access the game memory (e.g. from JavaScript)

Unsupported features in JS

  • commands requiring an scm variable (e.g. countdown timers). Tracking issue.

  • commands implicitly loading models or textures (such as widgets) Tracking issue. You can circumvent the issue by preloading needed resources, e.g. by calling them in a .CS script first.

  • you can't call the game functions that need references to variables to store the result. There is no "take an address of the variable" syntax. As a workaround, if the platform permits (e.g. in San Andreas with CLEO 4.4) you can allocate a memory block and pass its address to the function, then read the result from the memory using Memory.Read.

Embedding to custom host

CLEO Redux can be embedded and run JS scripts on an unknown (i.e. not supported officially) host. A host is an application in which process cleo_redux.asi or cleo_redux64.asi gets loaded or injected and where the CLEO runtime runs. This feature is highly experimental and subject to change at any moment.

Loading into custom process

There are multiple ways of loading ASI file into the target process. They include but not limited to Ultimate ASI Loader or any DLL injector available on GitHub. The host can load CLEO ASI file as a dynamic library when needed using WinAPI's LoadLibrary function.

Launching the CLEO runtime

After injecting CLEO into the target process you should launch the runtime to execute scripts. There are two ways of doing it: automatic and manual.

Automatic launch

To launch the runtime on an unknown host immediately after loading, open the config file and set EnableSelfHost to 1. When loaded as a self host CLEO Redux scans the CLEO directory for plugins and scripts and runs them. This option is suitable if you don't have control over the host's source code and inject the library using an injector.

Manually Controlling the Runtime

The host can start the runtime and advance its main loop using SDK methods RuntimeInit and RuntimeNextTick. This option is suitable if you have control over the host's source code and can execute arbitrary instructions.

This is how it can be implemented in Rust:

[dependencies]
ctor = "0.1.21"
cleo_redux_sdk = "^0.0.6"

#![allow(unused)]
fn main() {
use ctor::*;

#[cfg_attr(target_arch = "x86", link(name = "cleo_redux"))]
#[cfg_attr(target_arch = "x86_64", link(name = "cleo_redux64"))]

#[ctor]
fn init() {
    use cleo_redux_sdk;
    use std::{thread, time};

    // load CLEO scripts, FXT, enable file watcher
    cleo_redux_sdk::runtime_init();

    // init time variables
    const FPS: i32 = 30;
    let time_step = 1000 / FPS;
    let started = time::Instant::now();

    thread::spawn(move || loop {
        let current_time = started.elapsed().as_millis() as u32;

        // advance main loop providing current time and time step
        // current time is used to determine whether a script should "wake up" after wait command
        // time step is used to increment TIMERA and TIMERB variables
        cleo_redux_sdk::runtime_next_tick(current_time, time_step);

        // pause for at least time step ms
        thread::sleep(time::Duration::from_millis(time_step as u64));
    });
}
}

Available Commands

In the self-hosted mode CLEO Redux supports own bindings and commands made with SDK. It uses command definitions for the Unknown host from Sanny Builder Library (available for 32-bit and 64-bit).

You can use all standard JavaScript features. The list of available commands can be seen in the auto-generated file .config/unknown.d.ts.

Manifest

Manifest is a file with static configuration for the given host. Only unknown hosts make use of it. The configuration should be stored in .config\manifest.json with the following structure:

{
    "host": string,
    "host_name": string,
    "compound": boolean
}
  • host should match the host's executable name. E.g. if the host runs via application.exe, the value is application. Available in scripts as the HOST variable.

  • host_name defines the host's custom name used in the log

  • compound defines whether the host uses compound definitions. By default the host uses definitions from the file matching <host>.json, e.g. application.json. This file should be provided by the person managing integration of CLEO Redux with the given host and placed in the .config folder.

    When compound is set to true the host also uses command definitions for the Unknown host (e.g. unknown_x86.json). If this file is missing CLEO downloads it from Sanny Builder Library.

Example

Host: Sanny Builder 3 (sanny.exe)

{
    "host": "sanny",
    "host_name": "Sanny Builder 3",
    "compound": true
}

Sanny Builder 3\CLEO\.config folder contains sanny.json and manifest.json before the first run. The other files are downloaded or generated automatically.

CLEO Redux SDK

SDK provides a way to create new script commands for any game that CLEO Redux supports. It is agnostic to the game title and the underlying runtime (CS or JS). At this moment CLEO provides SDK for the C++ and Rust languages.

SDK Version

The current version is 6. Changes to SDK advance this number by one.

Platforms Support

CLEO Redux provides SDK for both 32-bit and 64-bit games. There is one notable change between the two: on the 32-bit platform the SDK functions GetIntParam and SetIntParam operate on signed 32-bit numbers, whereas on the 64-bit platform they operate on signed 64-bit numbers (declared as the type isize).

It's recommended for 64-bit plugins to have 64 in their names (e.g. myplugin64.cleo).

Plugin Lifetime

Each plugin is a dynamic-link library (DLL) with the .cleo extension that must be placed in CLEO\CLEO_PLUGINS. CLEO Redux scans this directory on startup and loads all .cleo files using WinAPI's function LoadLibrary.

Path Resolution Convention

String arguments representing a path to the directory or file must be normalized using SDK's function ResolvePath. This function takes a path and returns the absolute path resolved by the following rules:

  • an absolute path gets resolved as is
  • path starting with "CLEO/" or "CLEO\" gets resolved relative to the CLEO directory which is either
    • {game}\CLEO or
    • {user}\AppData\Roaming\CLEO Redux\CLEO
  • path starting with "./" gets resolved relative to the script's directory. For .cs scripts the directory is always {game}\CLEO.
  • all other paths get resolved relative to the current working directory (the game directory)

String Arguments

Strings passed in and out of the SDK methods are UTF-8 encoded.

If the script uses an integer value where a string is expected SDK treats this number as a pointer to a null-terminated UTF-8 character sequence to read, or to a large enough buffer to store the result to:

IniFile.WriteString(0xDEADBEEF, "my.ini", "section", "key")

SDK will read a string from the address 0xDEADBEEF and write it to the ini file.

0AF4: read_string_from_ini_file 'my.ini' section 'section' key 'key' store_to 0xDEADBEEF

SDK will read a string from the ini file and write it at the address 0xDEADBEEF.

Developing New Commands

To register custom command handlers the plugin must call RegisterCommand in the DllMain function. Once a user script encounters this command CLEO Redux invokes the handler with the one argument which is a pointer to the current context. This pointer must be used for calling other SDK methods. See C++ SDK guide or Rust SDK guide for examples.

Unsafe commands

New commands that use low-level WinAPI and can potentially damage user environment must be explicitly registered with a permission token (third argument to the RegisterCommand). User can disallow usage of unsafe commands in the scripts using permission config. At the moment three permission tokens are used: mem, fs, and dll. They mark commands operating with the host process, user files and external libraries.

Command interface

CLEO Redux uses Sanny Builder Library to know an interface of any command. For a new command to become available in the scripts, the JSON file (gta3.json, vc.json, sa.json) must have the command definition, including the name that matches with the value that the plugin uses RegisterCommand with. E.g. if the plugin registers SHOW_MESSAGE command, the JSON file must have a command with the name property set to SHOW_MESSAGE. The number and order of the input and output parameters in the definition must match the order of methods used by the plugin (i.e. GetXXXParam for each input argument and SetXXXParam for each output argument).

Claiming Opcodes

Opcodes get assigned to new commands in Sanny Builder Library based on the availability, similarity with existing commands in other games, and other factors. To claim an opcode reach out to Sanny Builder Library maintainers on GitHub.

Why use command names and not an id for the command lookup?

One of the common issues with CLEO Library plugins was that commands authored by different people often had id collisions. If two plugins add commands with the same id, it is impossible to use them both. Using string names minimizes the collisions with custom plugins as well as with native opcodes. The library's definitions ensure each command claims only an available id. Also it helps to track and document plugins in a single place.

Developing File Loaders

A plugin can associate itself with a particular glob pattern (or in other word a file name with wildcard characters in it, e.g. *.txt) and provide a handler to be called when a script imports a file matching the pattern.

The glob pattern can be used to match an arbitrary file name using wildcards to represent any characters (*.pak* would match 1.pak1, 2.pak1, 3.pak3, etc) or a specific file name (gta.dat would match only a gta.dat file). Be mindful about using wildcards to avoid matching unnecessary files. The plugin can associate itself with many globs (for example TextLoader uses *.txt and *.text), but it's recommended to support only related formats in one plugin.

The handler gets a full path to the file as an input and returns a pointer to the buffer with the content of the file that has been serialized into JSON. The serialized JSON content should be a valid null-terminated UTF-8 string.

To allocate a memory buffer for the serialized string the plugin MUST call SDK's method AllocMem. This memory will be released by CLEO Redux once it reads the data from it. Allocating the memory inside the plugin will lead to an error.

If the serialization has failed or the file does not have the expected format (for example due to a glob matching too many files), the handler should return a null pointer. In this case CLEO proceeds to another loader that could handle this file.

See examples of the file loaders implemented for Text files (in C++) and IDE files (in Rust).

C++ SDK

This page describes basic steps needed to create a new custom command with C++ SDK that reads two integer arguments and returns their sum. This guide has been tested on Visual Studio 2019 and Visual Studio 2022 (Community Edition) and may differ for other IDEs and compilers.

Developing a Plugin

  • Create a new Dynamic-link Library (DLL) project. In Project Settings->Advanced set Target File Extension to .cleo.

  • Download cleo_redux_sdk.h and add it in your project

#include "cleo_redux_sdk.h"

If this header file is located outside of your project directory you need to add a folder with this file in Project Settings->VC++ Directories->Include Directories to let Visual Studio discover this file.

  • In Project Settings->Linker->Input add the full path to cleo_redux.lib (if you develop a 32-bit plugin) or to cleo_redux64.lib for 64-bit plugins.

  • in dllmain.cpp create a new static class with the constructor function. This constructor will be called as soon as CLEO loads your plugin

class TestPlugin {
public:
	TestPlugin() {
		Log("My Test Plugin");
	}
} TestPlugin;
  • in the plugin class constructor call RegisterCommand function for each new command
class TestPlugin {
public:
	TestPlugin() {
		Log("My Test Plugin");
        RegisterCommand("INT_ADD", IntAdd);
	}

    static HandlerResult IntAdd(Context ctx) {
        return HandlerResult::CONTINUE;
    }
} TestPlugin;
  • implement handlers for new commands. Each command handler gets one input argument which is Context ctx. This argument is used to call other SDK methods
class TestPlugin {
public:
	TestPlugin() {
		Log("My Test Plugin");
        RegisterCommand("INT_ADD", IntAdd);
	}

    static HandlerResult IntAdd(Context ctx) {
        auto arg1 = GetIntParam(ctx);
        auto arg2 = GetIntParam(ctx);
        SetIntParam(ctx, arg1 + arg2);

        return HandlerResult::CONTINUE;
    }
} TestPlugin;
  • compile the project and place .cleo file to the CLEO_PLUGINS folder.

  • add command definition in the JSON file for the target game (e.g. gta3.json for GTA III or unknown_x86.json for an Unknown (x86) host). Each GetXXXParam/SetXXXParam should be paired with an input or output in the command definition

{
    "input": [
        { "name": "arg1", "type": "int" },
        { "name": "arg2", "type": "int" }
    ],
    "output": [
        { "name": "result", "type": "int", "source": "var_any" }
    ],
    "id": "0DDD",
    "name": "INT_ADD",
    "num_params": 3,
    "short_desc": "Adds together two integer values and writes the result into the variable",
},

id is optional for unknown hosts. For known and supported games an id must be a unique opcode not used elsewhere.

The best way to generate correct command definition is to use Sanny Builder Library. If you plan to share the plugin and make it available over the Internet consider contacting the library maintainers to get your command published there.

  • you can now use the new command in the code using the native command
var result = native("INT_ADD", 10, 20); // 30

Example

See the IniFiles plugin that includes a complete project for Visual Studio 2019.

Rust SDK

Rust SDK uses similar to C++ interface with some extra wrapping methods to allow easily convert between C and Rust types. The header file is available as a crate on crates.io. See the documentation here.

Example

See the Dylib plugin. It adds a class DynamicLibrary with the following methods:

declare class DynamicLibrary {
    constructor(handle: number);
    static Load(libraryFileName: string): DynamicLibrary | undefined;
    free(): void;
    getProcedure(procName: string): int | undefined;
}

See more information in Sanny Builder Library. The usage of the DynamicLibrary class requires a dll permission.