Introduction
CLEO Redux is an embeddable and extensible runtime capable of running user-made scripts in games and desktop applications. It supports JavaScript and TypeScript languages. Inspired by CLEO Library and partially compatible with its scripts and plugins, CLEO Redux, however, aims to provide 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. Or jump straight away to example scripts.
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 The Complete Edition (1.2.0.43, 1.2.0.59)
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
, orgta_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
(orcleo_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
Name | Description | Link |
---|---|---|
IniFiles | Reading from and writing to INI files | src |
Dylib | Loading DLL files and importing functions | src |
Input | Checking for keyboard and mouse input, emulating key presses | src |
ImGuiRedux | Dear ImGui bindings | GitHub repo |
MemoryOperations | Low-level memory operations | GitHub repo |
Frontend | Checking for updates on GitHub and displaying information in the main menu | |
Events | Emitting custom events | src |
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
Introduction
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 the 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 loop iteration. It reduces a number of possible issues caused due to the use of shared resources from multiple parallel scripts. The game's main loop is also locked while CLEO 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 to1
CLEO loads and executes*.cs
files located in the CLEO directory. Enabled by default. -
AllowJs
- when set to1
CLEO loads and executes*.js
files located in the CLEO directory. Enabled by default. -
AllowFxt
- when set to1
CLEO loads and uses*.fxt
files located in the CLEO\CLEO_TEXT directory. Enabled by default. -
LogOpcodes
- when set to1
CLEO logs all executed opcodes in custom scripts. -
PermissionLevel
- sets the permission level for unsafe operations (see below). Default isLax
. -
CheckUpdates
- (deprecated in favor of Frontend plugin) -
DisplayMenuInfo
- (deprecated in favor of Frontend plugin)
Host
EnableSelfHost
- when set to1
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 whenEnableSelfHost
is1
. Default is30
.
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 thecleo.ini
only takes effect whenPermissionLevel
isStrict
.
None
No unsafe operation is allowed.
Known Permission Tokens
mem
- reading from and writing to the process memory, calling foreign functionsdll
- loading dynamic libraries and finding exported functionsfs
- 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:
-
One file - one script. CLEO only supports one script per file.
-
Never use opcode
004E
to terminate a script. This opcode is only supported in themain.scm
. Use 0A93 instead. -
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.
- 0A8C WRITE_MEMORY (UNSAFE - requires
mem
permission) - 0A8D READ_MEMORY (UNSAFE - requires
mem
permission) - 0A8E INT_ADD
- 0A8F INT_SUB
- 0A90 INT_MUL
- 0A91 INT_DIV
- 0A93 TERMINATE_THIS_CUSTOM_SCRIPT
- 0AA5 CALL_FUNCTION (UNSAFE - requires
mem
permission) - 0AA6 CALL_FUNCTION_RETURN (UNSAFE - requires
mem
permission) - 0AA7 CALL_METHOD (UNSAFE - requires
mem
permission) - 0AA8 CALL_METHOD_RETURN (UNSAFE - requires
mem
permission) - 0AB0 IS_KEY_PRESSED
This list might not be complete as there are custom plugins with extra commands. 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.
Game | File | Minimum Required Version |
---|---|---|
GTA III, re3 | gta3.json | 0.267 |
GTA VC, reVC | vc.json | 0.273 |
GTA San Andreas (Classic) 1.0 | sa.json | 0.319 |
GTA III: The Definitive Edition | gta3_unreal.json | 0.228 |
Vice City: The Definitive Edition | vc_unreal.json | 0.234 |
San Andreas: The Definitive Edition | sa_unreal.json | 0.263 |
GTA IV | gta_iv.json | 0.78 |
Unknown (32-bit) | unknown_x86.json | 0.225 |
Unknown (64-bit) | unknown_x64.json | 0.228 |
Bully: Scholarship Edition | bully.json | 0.41 |
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. This page describes static import
statements (unconditional importing). Their format matches the ES6 import syntax. CLEO Redux also supports dynamic imports (importing on-demand).
Importing scripts
- Extensions:
.js
,.mjs
,.ts
./
in a path resolves to the current file's directory. If a script is located at C:\Game\CLEO\mod1\extra\addon.js
and it contains import { foo } from './bar.js'
, the runtime will try to load C:\Game\CLEO\mod1\extra\bar.js
.
../
resolves to the parent directory. If a script is located at C:\Game\CLEO\mod1\extra\addon.js
and it contains import { foo } from '../bar.js'
, the runtime will try to load C:\Game\CLEO\mod1\bar.js
. You can combine ../
to traverse multiple levels up.
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";
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
CLEO Redux supports asynchronous code via 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 pauses the 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 an 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 continues.
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 share global variables and can change them. Look at how gVar
is being 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
})();
When importing JSON files, its content is available via default
property.
(async function () {
const { default: data } = await import("./my.json");
log(data); // prints the content of my.json
})();
setTimeout and setInterval
It is easy to implement setTimeout
and setInterval
using asyncWait
and async
functions. You can find an implementation example here.
Since 1.0.6 these functions are part of the standard library and are available in all scripts.
Events
CLEO Redux 1.0.6 adds initial support for event-driven scripting. This feature allows you to write scripts that react to events that happen in the game or another scripts.
Listening to events
A globally available addEventListener
function creates a new event listener. It takes two arguments: an event name and a function that will be called when the event is triggered (a callback).
The event name argument corresponds to built-in or custom event names.
The callback receives a single argument. This argument is a JavaScript object with two fields: name
(the event name) and data
(custom data associated with this event).
Event listeners only work in async context. Scripts willing to react to game events must not use a blocking
wait
function. UseasyncWait
instead.
addEventListener("OnVehicleCreate", (event) => {
log("A vehicle is created!");
log(event.name); // logs "OnVehicleCreate"
});
addEventListener
returns a function that can be used to stop listening to the event. This is useful when you want to stop listening to the event after a certain condition is met.
const cancel = addEventListener("OnVehicleCreate", (event) => {
log("A vehicle is created!");
});
// ...
cancel(); // the event callback won't be called anymore
List of events
The following documentation describes the Events.cleo plugin shipped with CLEO Redux 1.0.6+. Other plugins may add their own events. Events are game- and version- specific. Some of them might not be available on certain games or versions.
OnVehicleCreate
Triggered after the game creates a new vehicle of any type in the world. An event's data
object contains the address of the vehicle structure.
Supported in: re3
, reVC
, GTA III
, GTA VC
, GTA SA
, GTA III: DE (1.0.17.39540)
, GTA VC: DE (1.0.17.39540)
, GTA SA: DE (1.0.17.39540)
interface OnVehicleCreateEvent {
name: string;
data: {
address: int;
}
}
addEventListener("OnVehicleCreate", (event: OnVehicleCreateEvent) => {
const name = event.name;
log(`Event ${name} is triggered!`}); // logs "Event OnVehicleCreate is triggered!"
const address = event.data.address;
log("A vehicle is created! Its address is " + address); // logs "A vehicle is created! Its address is 0x12345678"
});
OnPedCreate
Triggered after the game creates a new ped of any type in the world. An event's data
object contains the address of the ped structure.
Supported in: re3
, reVC
, GTA III
, GTA VC
, GTA SA
, GTA III: DE (1.0.17.39540)
, GTA VC: DE (1.0.17.39540)
, GTA SA: DE (1.0.17.39540)
interface OnPedCreateEvent {
name: string;
data: {
address: int;
};
}
addEventListener("OnPedCreate", (event: OnPedCreateEvent) => {
const name = event.name;
log(`Event ${name} is triggered!`); // logs "Event OnPedCreate is triggered!"
const address = event.data.address;
log("A ped is created! Its address is " + address); // logs "A ped is created! Its address is 0x12345678"
});
OnObjectCreate
Triggered after the game creates a new object of any type in the world. An event's data
object contains the address of the object structure.
Supported in: re3
, reVC
, GTA III
, GTA VC
, GTA SA
, GTA III: DE (1.0.17.39540)
, GTA VC: DE (1.0.17.39540)
, GTA SA: DE (1.0.17.39540)
interface OnObjectCreateEvent {
name: string;
data: {
address: int;
};
}
addEventListener("OnObjectCreate", (event: OnObjectCreateEvent) => {
const name = event.name;
log(`Event ${name} is triggered!`); // logs "Event OnObjectCreate is triggered!"
const address = event.data.address;
log("An object is created! Its address is " + address); // logs "An object is created! Its address is 0x12345678"
});
OnVehicleDelete
Triggered before the game deletes a vehicle from the world. An event's data
object contains the address of the vehicle structure.
Supported in: re3
, reVC
, GTA III
, GTA VC
, GTA SA
, GTA III: DE (1.0.17.39540)
, GTA VC: DE (1.0.17.39540)
, GTA SA: DE (1.0.17.39540)
interface OnVehicleDeleteEvent {
name: string;
data: {
address: int;
};
}
addEventListener("OnVehicleDelete", (event: OnVehicleDeleteEvent) => {
const name = event.name;
log(`Event ${name} is triggered!`); // logs "Event OnVehicleDelete is triggered!"
const address = event.data.address;
log("A vehicle is about to be deleted! Its address is " + address); // logs "A vehicle is about to be deleted! Its address is 0x12345678"
});
OnPedDelete
Triggered before the game deletes a ped from the world. An event's data
object contains the address of the ped structure.
Supported in: re3
, reVC
, GTA III
, GTA VC
, GTA SA
, GTA III: DE (1.0.17.39540)
, GTA VC: DE (1.0.17.39540)
, GTA SA: DE (1.0.17.39540)
interface OnPedDeleteEvent {
name: string;
data: {
address: int;
};
}
addEventListener("OnPedDelete", (event: OnPedDeleteEvent) => {
const name = event.name;
log(`Event ${name} is triggered!`); // logs "Event OnPedDelete is triggered!"
const address = event.data.address;
log("A ped is about to be deleted! Its address is " + address); // logs "A ped is about to be deleted! Its address is 0x12345678"
});
OnObjectDelete
Triggered before the game deletes an object from the world. An event's data
object contains the address of the object structure.
Supported in: re3
, reVC
, GTA III
, GTA VC
, GTA SA
, GTA III: DE (1.0.17.39540)
, GTA VC: DE (1.0.17.39540)
, GTA SA: DE (1.0.17.39540)
interface OnObjectDeleteEvent {
name: string;
data: {
address: int;
};
}
addEventListener("OnObjectDelete", (event: OnObjectDeleteEvent) => {
const name = event.name;
log(`Event ${name} is triggered!`); // logs "Event OnObjectDelete is triggered!"
const address = event.data.address;
log("An object is about to be deleted! Its address is " + address); // logs "An object is about to be deleted! Its address is 0x12345678"
});
Creating your own events
CLEO Redux SDK provides a method called TriggerEvent
that can be used to emit a new event along with some payload. The plugin must decide when the event should be triggered (usually by hooking into a game function). See the Events.cleo plugin source code for an example.
TriggerEvent
has two parameters: an event name and a serialized JSON. It will be passed to the event listeners as the data
property of the event object.
Dispatching events from scripts
Dispatching events is a safe way for scripts to communicate with each other. For example, one script can dispatch an event when the player enters a specific area, and another script can listen to that event and perform some action. dispatchEvent
function creates a new custom event. It has the following signature:
function dispatchEvent(name: string, data: any): void;
The name
parameter is the name of the event to dispatch. It corresponds to the name
argument in addEventListener
. The data
is an optional payload that will be passed to the event listeners as the data
property of the event object. The payload can be any JSON-serializable value (e.g. functions are not allowed).
// Dispatch an event from a script
dispatchEvent("OnMyCustomEvent", { foo: "bar" });
// Listen to the event in a script
addEventListener("OnMyCustomEvent", (event) => {
log(event.data.foo); // logs "bar"
});
Using events in TypeScript
Both addEventListener
and dispatchEvent
are generic functions that can be used to type-check event names and payloads. The following example shows how to use them in TypeScript:
// Define an event payload
export interface WantedLevelChangeEvent {
oldWantedLevel: number;
newWantedLevel: number;
change: number;
}
// Dispatch an event
dispatchEvent<WantedLevelChangeEvent>("OnWantedLevelChange", {
oldWantedLevel: 0,
newWantedLevel: 1,
change: 1,
});
// Listen to the event
addEventListener<WantedLevelChangeEvent>("OnWantedLevelChange", (event) => {
log(event.data.oldWantedLevel); // logs 0
log(event.data.newWantedLevel); // logs 1
log(event.data.change); // logs 1
});
WantedLevelChangeEvent
type allows VS Code to provide auto-completion and type-checking for the event payload.
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 their methods 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
addEventListener
addEventListener(name, callback)
calls the {callback}
function every time an event with the specified {name}
is triggered. The {callback}
function accepts a single argument that contains event-specific data. addEventListener
returns a function that can be used to remove the listener.
const cancel = addEventListener("OnVehicleCreate", (event) => {
log("A vehicle is created!");
});
// ...
cancel(); // the event callback won't be called anymore
asyncWait
await asyncWait(timeInMs)
pauses the script execution for at least {timeInMs}
milliseconds. asyncWait
can be used in async functions.
async function loop() {
while (true) {
await asyncWait(1000);
log("1 second passed");
}
}
clearTimeout
See setTimeout.
clearInterval
See setInterval.
dispatchEvent
dispatchEvent(name, data?)
triggers a custom event with the specified {name}
and {data}
. The {data}
argument is optional. The event can be caught by addEventListener
function from any active script.
addEventListener("greeting", (event) => {
log(event.data); // prints "hello"
});
dispatchEvent("greeting", "hello");
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");
log
log(...values)
prints comma-separated {values}
to the cleo_redux.log
var x = 1;
log("value of x is ", x);
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");
}
setInterval
setInterval(callback, timeInMs?)
calls the {callback}
function every {timeInMs}
milliseconds (or 0
if the argument is not present).
setInterval
returns an unique id that can be used to cancel the interval early using clearInterval.
let intervalId = setInterval(() => {
showTextBox("1 second passed");
}, 1000);
clearInterval(intervalId); // the callback won't be called anymore
setTimeout
setTimeout(callback, timeInMs?)
calls the {callback}
function after {timeInMs}
milliseconds (or 0
if the argument is not present).
setTimeout
returns an unique id that can be used to cancel the timeout early using clearTimeout.
let timeoutId = setTimeout(() => {
exit("Terminate script);
}, 1000);
clearTimeout(timeoutId); // the callback won't be called
showTextBox
showTextBox(text)
displays {text}
in the black rectangular box. Not available on an unknown
host.
showTextBox("Hello, world!");
wait
wait(timeInMs)
pauses the script execution for at least {timeInMs}
milliseconds
while (true) {
wait(1000);
log("1 second passed");
}
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 tocleo_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 (usingmeta.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.hostVersion
CLEO.hostVersion
- a complex property providing information about the host version. Currently only available if the current exe file has version info (e.g. GTA IV or GTA Trilogy)
log(CLEO.hostVersion); // "1.2.0.43" log(CLEO.hostVersion.major); // "1" log(CLEO.hostVersion.minor); // "2" log(CLEO.hostVersion.patch); // "0" log(CLEO.hostVersion.pre); // undefined log(CLEO.hostVersion.build); // "43"
CLEO.runScript
-
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:- script files must have one of the following extensions:
.mjs
,.js
(JS scripts),.ts
(TS scripts),.s
or.cs
(CS scripts). - 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
andchild.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@ // gives the player $500 0A93: terminate_this_custom_script
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@ // teleports the player at -921.25 662.125 -100.0 0A93: terminate_this_custom_script
- script files must have one of the following extensions:
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.
Memory Object
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.
- Reading and Writing Numbers
- Reading and Writing Strings
- Casting methods
- Calling Foreign Functions
- Finding Memory Addresses in re3 and reVC
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 Numbers
A group of memory access methods (ReadXXX
/WriteXXX
) can be used for reading or modifying numbers stored in the memory. Each method is designed for a particular data type. For example, to change a floating-point number (which occupies 4 bytes in the original game) use Memory.WriteFloat
:
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 number 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
:
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 and1000
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
.
Input arguments are treated as 32-bit signed integers. If you need to provide a floating-point number, 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.
Memory Object (x64)
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.
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 Numbers
A group of memory access methods (ReadXXX
/WriteXXX
) can be used for reading or modifying numbers stored in the memory. Each method is designed for a particular data type. For example, to change a floating-point number (which occupies 4 bytes in the original game) use Memory.WriteFloat
:
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
:
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 valueMemory.CallFunctionReturn
- calls a function and at the address and returns an integer valueMemory.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));
A 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.
TypeScript
CLEO Redux has first-class support for TypeScript. TypeScript is a superset of JavaScript with static types and other features. It is compiled to JavaScript and can be efficiently used in large-scale projects. TypeScript has excellent IDE support so you get autocompletion, type checking and other features in your editor for free.
TypeScript website: https://www.typescriptlang.org/.
TS scripts have .ts
extension and can be used anywhere where JS scripts are supported. They can be imported directly in both JS and TS scripts using import
statements.
TS Config
CLEO Redux creates tsconfig.json
file in the CLEO directory when you run it for the first time. It contains necessary information for VS Code and other IDEs on where to look for typing files, so you don't have to include /// <reference path="..." />
lines in your scripts. You can edit it to change the default settings.
Troubleshooting
- CLEO does not work with re3 or reVC
- Game crashes with CLEO on San Andreas: The Definitive Edition
- Scripts stopped working after CLEO Redux update
- Game is stuck loading
- Nothing happens when I run the game, not even a log file is there
- My problem is not listed there
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
toGameface\Binaries\Win64
- Put
- 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
- Check the GitHub tickets
- Check the Feature support page
- Ask a question in our Discord
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 theCLEO
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).
For windows users using powershell, run:
Get-Content cleo_redux.log -Wait -Tail 0
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?
- Is there any difference from support of the classic games?
- Can I use original opcodes?
- How do I know what commands can I use in JavaScript?
- Can I use CLEO opcodes?
- Can I work with the game memory or call the game functions?
- How do I compile CLEO scripts with Sanny Builder?
- I can't find an answer to my question here, where do I go?
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), 1.0.17.38838 (Steam release), 1.0.17.39540
- 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), 1.0.17.38838 (Steam release), 1.0.17.39540
- 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), 1.0.17.38838 (Steam release), 1.0.17.39540
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. Keep in mind 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 save your script file with .ts
extension.
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.
- 0C00 IS_KEY_PRESSED
- 0C01 INT_ADD
- 0C02 INT_SUB
- 0C03 INT_MUL
- 0C04 INT_DIV
- 0C05 TERMINATE_THIS_CUSTOM_SCRIPT
- 0C06 WRITE_MEMORY (UNSAFE - requires
mem
permission) - 0C07 READ_MEMORY (UNSAFE - requires
mem
permission) - 0C08 CALL_FUNCTION (UNSAFE - requires
mem
permission) - 0C09 CALL_FUNCTION_RETURN (UNSAFE - requires
mem
permission)
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?
- Check the troubleshooting guide.
- Check the GitHub tickets
- Check the Feature support page
- Ask a question in our Discord
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.
This line is not needed for TS scripts as VS Code automatically imports
*.d.ts
files with the help oftsconfig.json
file in the CLEO directory.
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).
Main Menu Information
Frontend
plugin displays some extra information such as the CLEO version and the amount of active scripts in the main menu of GTA III, Vice City, and San Andreas. It also checks for available updates. If this information is not needed, delete Frontend.cleo
file from the CLEO plugins directory.
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
* @param [isGlobal] if true, the text affects global FXT storage
*/
insert(key: string, value: string, isGlobal?: boolean): void;
/**
* Removes the text content associated with the key in the local fxt storage
* @param key GXT key
* @param [isGlobal] if true, the text affects global FXT storage
*/
delete(key: string, isGlobal?: boolean): 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
The private FXT storage is not supported in The Trilogy, Bully and GTA IV. Each script there modifies the global FXT storage. This behavior may change in the future.
insert
and delete
methods can be forced to change global FXT keys even when the current host supports a private storage. By setting the last argument isGlobal
to true
you can mutate (add or delete) keys in the global storage. It might be helpful to deal with those keys that the game uses after procesing all scripts in a given frame (e.g. in delayed messages or HUD elements text) when the script's private storage is not available.
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
-
for commands requiring a global SCM variable (e.g. countdown timers) use scm.ts library.
-
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.
-
only integer numbers can be used as arguments for commands with variable arguments, such as
Text.PrintFormattedNow
. In the Sanny Builder Library these arguments are referred to asarg: arguments
. If you need to pass a floating-point value, you can useMemory.FromFloat
:
Text.PrintFormattedNow('%d %d %d %.2f', 100, 1, 2, 3, Memory.FromFloat(1.5))
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 viaapplication.exe
, the value isapplication
. 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 totrue
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
- Platforms Support
- Plugin Lifetime
- Path Resolution Convention
- String Arguments
- Developing New Commands
- Developing File Loaders
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 according to the following rules:
-
an absolute path gets resolved as is
-
a path starting with
CLEO/
orCLEO\\
gets resolved relative to the CLEO directory which is either{game}\CLEO
or{user}\AppData\Roaming\CLEO Redux\CLEO
-
a path starting with
./
or../
gets resolved relative to the script directory.What is the script directory?
For CS scripts it is always the CLEO directory.
For JS scripts it could be the CLEO directory (if the JS file is located there). Or if the script is located in a subfolder inside the CLEO directory, then the script directory is the one that contains its
index.js
file.It is different from the resolution algorithm used by the
import
statement. Theimport
statement resolves paths relative to the current file's directory (that can be nested many levels deep), whereas theResolvePath
does not go deeper than theindex.js
directory. You can use the__dirname
variable to get the directory of the current file:DynamicLibrary.Load(__dirname + "\\mylib.dll");
-
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
setTarget 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 tocleo_redux.lib
(if you develop a 32-bit plugin) or tocleo_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 theCLEO_PLUGINS
folder. -
add command definition in the JSON file for the target game (e.g.
gta3.json
for GTA III orunknown_x86.json
for an Unknown (x86) host). EachGetXXXParam
/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 Input
plugin. It adds a bunch of new commands to check or emulate keyboard and mouse input.
https://github.com/cleolibrary/CLEO-Redux/tree/master/plugins/Input