Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
How to remove Cyber Engine Tweaks
Remove the following folders/files:
/bin/x64/plugins/
/bin/x64/global.ini
/bin/x64/LICENSE
/bin/x64/version.dll
Removing Cyber Engine Tweaks will not undo any edits made to your character and your save files.
If you have added items to your inventory, you will not be able to remove them once you have removed this mod. Please ensure you have done so using the appropriate commands first.
Saves made with the patch should be completely fine without it, thought that might change once proper mods start coming out
How to use the Cyber Engine Tweaks Console
The console tab is where you type commands to use cheats and other useful functions.
Clear Output
Clear the output log displayed in the console.
Auto-Scroll
Will automatically scroll to the bottom when there is a new output log.
Reload All Mods
Reload all installed mods.
The console uses Lua script language, you may want to look up how to use it before proceeding. There are some guides here to get you started:
An overview of how to use CET to create custom mods
In the early days of Cyberpunk 2077 modding, we noticed a lack of discipline and proper use of the SDK provided by CET. So we strongly recommend you read the documentation and understand what is expected of you and your mods.
Before you start, it is highly recommended to check out the Modding Resources page
If you are using VSCode, make sure to also pick up the CET VSCode extension
Next, it is recommended to read through the Scripts and Mod Structure page, which gives an overview on how to create a mod / run a single script. It also covers the basic elements of a mod
After that, you might want to check out the Scripting API page, which explains how you interact with the games systems and code.
Once you've made yourself familiar with how a general mod is structured and how to interact with the game, you might want to read up on the different functions offered by CET itself, such as Events, Keybinds, Overrides and more.
Check out the Examples section for a collection of small example mods and useful code snippets
Explaining how to create and run a custom mod
An overview of CET-Exclusive functions
How to use Cyber Engine Tweaks Bindings
The Hotkeys tab is where you manage all the hotkeys of the mods you installed.
Binding hotkeys for a mod is the same as how you bind the hotkey for the CET Overlay.
You can unbind a hotkey by pressing the UNBIND
button behind it.
To save the changes, press the Save
button.
Undefined hotkeys appear in red text. Unsaved changes appear in yellow text.
Frequently asked questions about Cyber Engine Tweaks based mods.
"mod" or "mods" on this page refers to mods that are dependent on Cyber Engine Tweaks.
Cyber Engine Tweaks will be referred to as CET from now on.
Make sure that CET works fine without any mods installed.
Make sure the mod that you are installing is compatible with the latest CET.
You need to bind a hotkey in the Hotkeys tab of the CET overlay. Check how-to .
The hotkeys only work when the CET overlay is closed.
You need to press the hotkey combination in the right order.
For example Ctrl + Shift + S
is different from Shift + Ctrl + S
You need to open the CET overlay to interact with anything using your mouse.
?
(question mark).You need to change the default font of CET in order for the characters to show properly. You can follow this guide .
Click for the ultimate guide.
How to use the Cyber Engine Tweaks Overlay
The Cyber Engine Tweaks Overlay is the interface that is used to display and set various settings, and configure mods hotkeys.
Additionally, the most advanced mods will also display their own settings window in this overlay. It contains different tabs:
Upon the installation of CET, the overlay will appear automatically when you launch the game. You need to assign a hotkey to it before you can move on:
Press the Hotkey
button next to the Overlay Key
.
Press a key or a key combination on your keyboard after the button becomes BINDING...
to bind it.
Now the hotkey has been bound, you can press that key anytime to open/close the Overlay.
On onInit gets triggered once, when CET loads in all the mods. This event ensures that CET has fully loaded, and you have access to the Scripting API. There can be only one onInit event per mod.
Use it to register any , as well as do any one-off actions needed for your mod.
This event is a safe starting point for your code interaction with the game, as it guarantees the access to most of the Scripting API.
Welcome to Cyber Engine Tweaks official Wiki!
Cyber Engine Tweaks is a framework that allows modders to interact with the game's internal functions using . It also provides a user-interface with a console and additional tools to assist developers.
For information on how to install Cyber Engine Tweaks, check out the link below.
Learn how to use each Cyber Engine Tweaks tabs.
Everything you need to know to create your own mod for Cyber Engine Tweaks.
If you wish to contribute to the main repo, try to follow the coding style in the code, otherwise not much to say, don't use code that is not yours unless the license is compatible with MIT.
This event gets triggered on every frame that the game runs, thus is framerate dependent. The callback function gets the deltaTime
passed along with it, which measures the frametime in ms, useful for making e.g. timers.
In here you can put any code that you want to run every frame, although it is best practice to only run your code when needed, e.g. using .
Use this event with caution, as it is triggered continuously once the game is launched.
How to use Cyber Engine Tweaks Settings
registerForEvent('onInit', function()
print('Game is loaded')
end)
registerForEvent('onUpdate', function(deltaTime)
print('It has been ' .. deltaTime .. ' ms since the last call')
end)
// my_script.reds
LogChannel(n"DEBUG", "My Custom Log");
A collection of helpful links and resources
Decompiled game scripts: These are the game's scripts decompiled to redscript. Extremely useful for understanding how functions work under the hood, game systems and classes interact with each other and what classes and functions are available. For more information on how to transpile redscript code to .lua code, read up on the Scripting API. Works best if downloaded and opened in some IDE like VSCode that lets you search across all files at once.
cp2077-cet-kit: psiberx's github repository with helper functions and scripts
NativeDB: This is a database containing all the classes and their functions, their relation to each other, their functions, attributes and all the Enum's. Very useful for quickly looking up what functions a class offers and what parameters it takes. Also helpful for understanding which classes inherit from which. Nativedb has more types than decompiled scripts.
CET translation helper: A utility that helps you translating your CET mods, by lgomes_gvizstudio on github
CET Kit: A collection of utility scripts, for tracking game states (In Game etc.), setting up timers and timed actions, storing data per game save and more.
Official CET Examples: A collection of small example mods, such as controlling NPC AI, working with map pins, reading player inputs and more.
Cyberpunk 2077 Explorer: Visual overview of the game's classes and structure. Very helpful for understanding the higher level structure.
Cyberpunk 2077 Modding Community: Discord server with multiple CET related channels, lots of more resources and snippets (Discord search is your best friend)
Simple Gameplay Mod Tutorial: An extensive tutorial covering how to work with the TweakDB, create a CET mod, override and observe functions and use redscript.
LUA code playground: An online sandbox to familiarize yourself with LUA and test some code without having to launch Cyberpunk with CET. Note that any CET/Game functions won't work in it.
This event get triggered when the CET Overlay gets shown.
Use this to keep track of the overlays state, and e.g. to only draw your own UI when CETs overlay is visible. Use it in conjunction with onOverlayClose to get a proper on/off switch case.
-- onOverlayOpen
registerForEvent('onOverlayOpen', function()
-- get player
local player = Game.GetPlayer()
-- bail early if player doesn't exists
if not player then
return
end
-- display warning message
player:SetWarningMessage('Overlay is open')
end)
-- set initial var
local isOverlayVisible = false
-- onOverlayOpen
registerForEvent('onOverlayOpen', function()
isOverlayVisible = true
end)
-- onOverlayClose
registerForEvent('onOverlayClose', function()
isOverlayVisible = false
end)
-- onDraw
-- this event is triggered continuously
registerForEvent('onDraw', function()
-- bail early if overlay is not open
if not isOverlayVisible then
return
end
-- draw ImGui window
if ImGui.Begin('Window Title', ImGuiWindowFlags.AlwaysAutoResize) then
ImGui.Text('Hello World!')
end
ImGui.End()
end)
Explaining where to find logs file and write them
On game launch, Cyber Engine Tweaks generates individual logs files in each mod folder, using their name. eg, <
>/mods/my_mod/my_mod.log
.
../
├─ cyber_engine_tweaks/
│ ├─ mods/
│ │ ├─ my_mod/
│ │ │ ├─ init.lua
│ │ │ ├─ my_mod.log <- automatically generated
This file contains all notices, errors and logs generated by the mod.
You can write the log file using the spdlog functions.
You can use Tailblazer App to watch files in live with a GUI, or use the following command line in the Windows Command shell:
tail -f "C:/path/to/Cyberpunk 2077/bin/x64/plugins/cyber_engine_tweaks/mods/my_mod/my_mod.log"
Cyber Engine Tweaks has a global log file located in <
>/scripting.log
.
../
├─ cyber_engine_tweaks/
│ ├─ scripting.log
This file contains all logs from the CET Console.
You can write the log file by using the print() function.
You can use Tailblazer App to watch files in live with a GUI, or use the following command line in the Windows Command shell:
tail -f "C:/path/to/Cyberpunk 2077/bin/x64/plugins/cyber_engine_tweaks/scripting.log"
Guide to installing the Cyber Engine Tweaks into the game
This page tells you how to install Cyber Engine Tweaks - or you can check the 📺 Video Guide.
Make sure Cyberpunk 2077 is updated to the latest version
Make sure you installed the latest Microsoft Visual C++ Redistributable
Make sure you run the game in Windowed Borderless
Ensure all apps overlays are disabled as the console may not open: Steam, Discord, GOG, Geforce Experience, Windows 10 Game Bar, Rivatuner, Fraps, Afterburner etc...
Download the latest cet_x_xx_x.zip file. (Do not download the source code)
If using Windows, you should right click on the zip file, and check under Properties for a security notice saying "This file came from another computer and might be blocked to help protect this computer" - if that security notice is present, click the checkbox to unblock the contents of the zip file.
You can now either
Unzip the download directly into your or
Unzip to anywhere on your desktop. After unzipping the file, you will see a bin folder.
Drag the bin folder to your
If installed properly, the Cyber Engine Tweaks files will be in the /bin/x64
folder. As follow:
Cyberpunk 2077/
├─ bin/
│ ├─ x64/
│ │ ├─ global.ini
│ │ ├─ LICENSE
│ │ ├─ version.dll
│ │ ├─ plugins/
│ │ │ ├─ cyber_engine_tweaks.asi
│ │ │ ├─ cyber_engine_tweaks/
│ │ │ │ ├─ fonts/
│ │ │ │ ├─ scripts/
│ │ │ │ ├─ tweakdb/
│ │ │ │ ├─ ThirdParty_LICENSES
When you first start the game, a window will appear prompting you to choose a keybind to bring up the Overlay. Pick any key and save it. It will open with that key from now on.
(Optional) To check that everything works you can look at the logs in <cyberpunk install path>/bin/x64/plugins/cyber_engine_tweaks/cyber_engine_tweaks.log
(Optional) When installing through Nexus Vortex, make sure to use hardlink deployment.
Cyber Engine Tweaks has builtin events which can be listened using the registerForEvent()
function. These listeners must be registered inside the init.lua
file.
Keep in mind that CET events are not game events. For example, onInit doesn't mean the game the initializing, but rather that CET loaded mods and the game's Scripting API.
If you need to listen for game events, take a look at the Observers functions.
registerForEvent(event, callback)
--
-- registerForEvent()
--
-- @param string event The event name
-- @param function callback The callback function
--
registerForEvent('event', function()
-- event is triggered
end)
Here is a video showing when events are triggered in real-time:
How to modify the TweakDB via CET
This page documents CET's in-game Tweak Editor.
To learn what the TweakDB is, read the page on
For an introduction on TweakDB modding with CET, check the Simple Gameplay Mod guide
For further information, refer to the TweakDB section on this wiki
After opening CET with the key that you bound during the installation, you can find it in the "TweakDB Editor" tab. Here you can browse and update the TweakDatabase in real time.
Note that changes you made in the editor do not persist across game restarts.
This event works similarly to , except that it is used for drawing custom ImGui UI. It gets triggered on every frame that the game runs, thus is framerate dependent.
Use this event with caution, as it is triggered continuously once the game is launched.
Trying to draw ImGui UI outside this callback is prohibited
This event get triggered when the gets hidden.
Use this to keep track of the overlays state, and e.g. to only draw your own UI when CETs overlay is visible. Use it in conjunction with to get a proper on/off switch case.
registerForEvent('onDraw', function()
if ImGui.Begin('Window Title', ImGuiWindowFlags.AlwaysAutoResize) then
ImGui.Text('Hello World!')
end
ImGui.End()
end)
-- set initial switch
isSprinting = false
-- onInit
registerForEvent('onInit', function()
-- observe Sprint OnEnter state
Observe('SprintEvents', 'OnEnter', function(stateContext, scriptInterface)
isSprinting = true
end)
-- observe Sprint OnExit state
Observe('SprintEvents', 'OnExit', function(stateContext, scriptInterface)
isSprinting = false -- reset switch
end)
end)
-- onDraw
registerForEvent('onDraw', function()
-- bail early if not sprinting
if not isSprinting then
return
end
-- draw window
if ImGui.Begin('Notification', ImGuiWindowFlags.AlwaysAutoResize) then
ImGui.Text('Nice sprint!')
end
ImGui.End()
end)
-- onOverlayOpen
registerForEvent('onOverlayOpen', function()
-- get player
local player = Game.GetPlayer()
-- bail early if player doesn't exists
if not player then
return
end
-- display warning message
player:SetWarningMessage('Overlay is closed')
end)
-- set initial var
local isOverlayVisible = false
-- onOverlayOpen
registerForEvent('onOverlayOpen', function()
isOverlayVisible = true
end)
-- onOverlayClose
registerForEvent('onOverlayClose', function()
isOverlayVisible = false
end)
-- onDraw
-- this event is triggered continuously
registerForEvent('onDraw', function()
-- bail early if overlay is not open
if not isOverlayVisible then
return
end
-- draw ImGui window
if ImGui.Begin('Window Title', ImGuiWindowFlags.AlwaysAutoResize) then
ImGui.Text('Hello World!')
end
ImGui.End()
end)
Movement and camera-related functions
A guide on how to change the font and font size for Cyber Engine Tweaks and mods based on Cyber Engine Tweaks. And how to display non-English characters.
Use a text editor to open the config file at /config.json
to adjust the settings.
In this page, we use this example JSON snippet to guide you through adjusting the settings:
{
// other lines
"font": {
"base_size": 16.0,
"language": "Default",
"oversample_horizontal": 3,
"oversample_vertical": 1,
"path": ""
},
// other lines
}
Locate this line under the "font"
curly brace:
"path": ""
Add the path of the font you want to change to between the double-quotes. For example, if you want to change the font to Comic Sans, just change this line into:
"path": "C:/Windows/Fonts/comic.ttf"
Note: You should use slash (/
) instead of backslash (\
) for the path.
Some mods may have implemented multilingual support. But the non-English characters will display as a question mark "?". This is because the default font only contains ASCII printable characters.
To display a language that only uses ASCII characters (such as French, German), all you need to do is to change the font to one with a wider range (Literally any font).
To display a language that uses non-ASCII characters (such as Chinese, Russian), besides changing the font, you also need to change this line in the config file under the "font"
curly brace:
"language": "",
Here is a table of the "language" options and their descriptions.
Cyrillic
Supports languages that use (e.g. Russian, Bulgarian..)
ChineseFull
Supports traditional Chinese, simplified Chinese, and Japanese display
ChineseSimplifiedCommon
Only supports simplified Chinese display
Japanese
Only supports Japanese display
Korean
For Korean
Thai
For Thai
Vietnamese
For Vietnamese
For example, to display Chinese all you need to do is:
{
// other lines
"font": {
"base_size": 16.0,
"language": "ChineseFull",
"oversample_horizontal": 3,
"oversample_vertical": 1,
"path": "C:/Windows/Fonts/simhei.ttf"
},
// other lines
}
The result (using the Sim Hei / 黑体 font):
Code completion for built-in Cyber Engine Tweaks, Dear ImGui and Cyberpunk 2077 types and functions (except sqlite)
Type resolving for Game.GetScriptableSystemsContainer():Get("Type")
, NewObject("Type")
, and GetSingleton("Type")
Contextual suggestions for default values and predefined sets
Download and extract the Cyber Engine Tweaks Lua Lib.
The lib files linked above is not the newest version.
You can join the Cyberpunk 2077 Modding Community discord server and then visit this message to download the newest version (CET 1.27 / Patch 2.01 Typedefs).
You can alos check the pinned messages in the #cet-lua-scripting channel for any future update.
Install the Lua by sumneko extension.
Locate settings.json in VS code
Ctrl + Shift + P
C: > Users > (yourname) > AppData > Roaming > Code > User > {} settings.json
Add next settings to the .vscode\settings.json
or in the Settings GUI:
NOTE: ensure commas occur after every line
"Lua.runtime.version": "LuaJIT",
"Lua.workspace.preloadFileSize": 15360,
"Lua.workspace.library": [
"c:\\path\\to\\cet-lua-lib",
],
On first use it takes a couple of minutes to parse and cache all definitions. Unfortunately there is no visual indication of this process. When it's done the code assistance will be more responsive.
To install the typedefs for Dear ImGui, you can manually download the files from GitHub repository Nats-ji/CET_ImGui_lua_type_defines and then follow the same installation process in CET typedef to install them in VSCode.
You can specify the type of the parameters with @param
annotation. It's very handy for Observe
and Override
:
---@param request PerformFastTravelRequest
Observe("FastTravelSystem", "OnPerformFastTravelRequest", function(request)
-- Now request object has type and suitable for code completion
end)
If type of some variable cannot be resolved or ambiguous you can force the type with @type
annotation:
---@type ScriptedPuppet
local puppet
The type of the result of some functions depends on the parameters passed to the function. If a valid type name is passed as a parameter, then the resulting type must be resolved without custom annotations.
Overview of the different functions available to use on the console.
Ready-to-paste CET code
Cyber Engine Tweaks allow mods to listen or overwrite game's script functions/methods. There are three kinds of functions:
Observe allows to listen for function/method execution
Override allows to overwrite a function/method
NewProxy allows to trigger a function/method callback using game's script system
A collection of commonly requested commands. How do I...
After installing CET, you can open the console overlay with the .
The commands are case-sensitive!
Many console commands have changed with 2.0. If something you try doesn't work, check the most up-to-date list under .
Open the Console
tab to run Lua scripts or engine functions. To find out what you can do here, keep reading.
To find all the codes you can enter, check
(source: #cet-support on )
For the most common commands, see below.
If you want to find a specific item, look for how to find the hash code for the .
Use Game.AddToInventory
Use Game.SetLevel
Formerly Game.SetAttribute
Formerly Game.GiveDevPoints
All vehicle commands are documented on the VehicleSystem page linked below.
You can print V's current coordinates to the CET console with the following command:
You will need to know the coordinates of where you want to go in the game world, some popular . Replace X, Y, and Z with your coordinates. Coordinates can also be found in the nodeData
of .streamingsector
files.
At this time there is no way of triggering or "fixing" stuck quests with the console.
For a more cohesive list of quest facts, check the .
Using Cyber Engine Tweaks on Linux
This is a basic guide to using the mod within Proton (aka Linux).
Please make sure your game works in proton first, before installing this mod.
While this isn't a required step, it's useful for performance.
Right click the game in Steam and select 'Properties'. Go to the 'Compatibility' tab and tick 'Force the use of a specific Steam Play compatibility tool'.
In the dropdown, select 'Proton Experimental'.
Please note - this method requires the use of Steam. For other options, see below.
First, follow the steps to install the mod. These are in the .
Run protontricks 1091500 --gui
, you may get an error saying "You are using a 64 bit WINEPREFIX", you can ignore this. Press 'Ok' when this error appears.
Choose the option 'Select the default wineprefix', then 'Run winecfg'. In the window that opens, select the 'Libraries' tab. In the dropdown on 'New override for library', select version
. Click Add
, then Apply
and Ok
.
You can now exit out of Protontricks and start your game. Check to see if the log exists after you run the game. If it's there, you've successfully installed the mod.
If you have issues with the mod still not working or the game just crashing, you can try this: re-open Protontricks like you did before, choose the option 'Select the default wineprefix', then 'Install a Windows DLL or component'. Tick vcrun2022
and press Ok. If a window pops up, then go through the steps to install that runtime (just click next really), otherwise just wait until the Protontricks screen comes back. Now exit out of Protontricks and try launching the game. DON'T do this without first checking if the mod works without this step, since installing vcrun2022
has been observed breaking game installs before, preventing the game from ever launching again and requiring a re-install.
NOTES: You will also need to install d3dcompiler_47
with winetricks to get it to work now, contribution on how to do this would be much appreciated!
If vcrun2019
breaks your install, it's been reported it can be fixed without reinstalling by deleting the contents of/steamapps/compatdata/1091500
.
Please note, this method still requires the use of Steam. You must install Cyberpunk 2077 and then add it to your Steam library by following the .
Search for the game APPID with: $ protontricks -s name_of_the_game_on_your_steam_lib
like:
$ protontricks -s Cyberpunk 2077
copy the APPID from the terminal then run:
$ protontricks "APPID" --gui
From now on, you can follow the Setting up the mod with ProtonTricks instructions.
For users running on the GOG version of the game, via launcher like , or , those running from the direct GOG Galaxy Windows application through Wine, or those directly using the Game libraries, Protontricks won´t be helpful in your case, as Protontricks will notice that you don´t have the game on Steam.
Instead, use Winetricks in which Protontricks is derivated from. Please note that you may need to install first, either through the official website or through your distribution's app store.
Lutris users may perform the following steps to activate the mod:
Authenticate with GOG Galaxy by clicking on the small profile icon to the right of the GOG entry. You will then be presented with a list of your owned games. Select Cyberpunk and install it - this will install the base version of the game with no other options selected. You may right click on the game icon after installation and choose the option to install the Phantom Liberty DLC if desired. Once installation is complete, select Cyberpunk 2077 and click on the Winetricks 'wine flute' icon along the bottom of the Lutris window. In the expanded menu, select 'Winetricks'.
Choose the option 'Select the default wineprefix', then 'Run winecfg'. In the window that opens, select the 'Libraries' tab. In the dropdown on 'New override for library', select version
. Click Add
, then Apply
and Ok
.
Alternatively, you may run Winetricks in itself if you use your default prefix, or from your custom prefix via: WINEPREFIX=<Location of your WinePrefix directory> winetricks --gui
via the terminal.
From now on, you can follow the Setting up the mod with ProtonTricks instructions. If you want to verify in Winetricks has correctly taken in account your custom Wine directory, click on Browse files
to see if it brings you to the designated folder.
Explaining how to create and run a custom mod
This page will tell you how the CET mod folder structure works. To download a ready-to-go example project, check the wiki's .
Create a new folder in the <
>/mods/
folder to get started, e.g. my_mod
(snake case recommended)
init.lua
fileCET will be looking for an init.lua
file inside your mod folder. This is the entry point of your mod, and gets executed when the game is launched.
The init.lua
file is the only file that gets automatically loaded, thus should contain all your initialization code.
Start by printing simple info into the using the function. Then register a listener on the event, and print something else.
Launch the game, and open the . You should see your printed text.
To now actually create a complex mod, you'll want to interact with the game's system, modify and create instances of game classes and modify the game's code. For that, head to the section
Additionally read up on the CET Functions, for information on how to create custom keybinds, Observer and Override game functions, spawn objects and more
A collection of LUA scripting utilities to make your life easier
A list of mods that you may find helpful for making an ImGui window
CET Overlay Window Builder Tool:
FellowImGui (online tool):
Cyberpunk 2077/
├─ bin/
│ ├─ x64/
│ │ ├─ plugins/
│ │ │ ├─ cyber_engine_tweaks/
│ │ │ │ ├─ mods/
│ │ │ │ │ ├─ mod_1/
│ │ │ │ │ ├─ mod_2/
│ │ │ │ │ ├─ mod_3/
│ │ │ │ │ ├─ my_mod/ <- here
../
├─ cyber_engine_tweaks/
│ ├─ mods/
│ │ ├─ my_mod/
│ │ │ ├─ init.lua
-- mod info
mod = {
ready = false
}
-- print on load
print('My Mod is loaded!')
-- onInit event
registerForEvent('onInit', function()
-- set as ready
mod.ready = true
-- print on initialize
print('My Mod is initialized!')
end)
-- return mod info
-- for communication between mods
return mod
registerForEvent('onShutdown', function()
-- cleanup code
end)
Codes good practices, pitfalls and tips
You're probably used to coding like this:
local function myMethod()
if condition then
-- do a little dance, make a little love…
end
end
In LUA, you can skim a few processing cycles by using the early return style:
local function myMethod()
if not condition then return end
-- do a little dance, make a little love…
end
You can gain a significant amount of performance this way, especially when doing this in a loop.
LUA throws an exception if it encounters nil
in unexpected places. The corresponding error will look like this:
attempt to access local '<variable name>' (a nil value)
stack traceback:
my_example.lua:1234 in function 'MyFunctionName
Open the corresponding file, find the correct line, and check what is being accessed there. It will look like variable.property
, or perhaps something will be concatenated to a string ("something something text" .. variable
).
You can assign a default value to the property in question:
myString = <original string assignment> or ""
myNumber = <original number assignment> or 0
myEverythingElse = <original object assignment> or {}
Who doesn't know the problem? You want to know if your string is A, B, or C, but not D — and LUA doesn't have a switch statement.
Fortunately, there is a built-in and performant way to
String concatenation and comparison can be the difference between a brief stutter and a complete freeze or even crash to desktop. This is not a joke — see here for more detail.
Lua internalizes strings. That means these two strings will share a single representation in memory:
local string1 = "This is the same object!"
local string2 = "This is the same object!"
The comparison between those two strings will be almost-instant.
This becomes a problem when comparing strings in a loop (see Scopes):
for (_, mystring) in ipairs(mytable) do
if mystring == "This is the same object!" then
-- do something
end
end
Every single pass of the loop will create a memory representation of "This is the same object!" and then discard it again.
local myCompareString = "This is the same object!"
for (_, mystring) in ipairs(mytable) do
if mystring == myCompareString then
-- do something
end
end
Takeaway:
If at all possible, define things outside the scope of loops!
Lua's regex implementation is very limited. There is a limitation for pipes. For example, the following example will actually iterate twice after creating internal string representations:
if string.find("catastrophe", "dog|cat") then
-- do something
end
It is faster to just do this:
if string.find("catastrophe", "dog") or string.find("catastrophe", "cat") then
-- do something
end
On top of that, string.match
will return the entire string if no match is found:
local match = string.match("catastrophe", "dog")
if match ~= "catastrophe" then
-- do something
end
The alternative:
if string.find("catastrophe", "dog")
-- do something
end
Takeaway:
Avoid regex
prefer String.find()
over String.match()
Don't do this (30% less performant):
function foo(x)
for i = 1, 1000000 do
x = x + math.sin(i)
end
return x
end
Do this instead:
local sin = math.sin
function foo(x)
for i = 1, 1000000 do
x = x + sin(i)
end
return x
end
Explaining how to create and run a custom mod
This page is still heavily WIP! Perhaps you'd like to work on it?
To browse the API, do yourself a favour and use NativeDB
What is this, what can you do with it, how does it work, redscript note
Helpers make it easy, some listed below
Game
Where can we use it, difference to redscript thing
GetPlayer
What is it in reds, dont store it etc.
IsDefined
Description
From/ToVariant
What names to use
.new, as constructor
When to use, how to use
When to use, how to use, use on member functions
How to use, nativeDB link, enumInt
Function to add CNames, string to cname
Note on how powerful it is etc, link to seperate doc
Blackboard is a kind of shared data storage and a framework to access/notify/listen to the data in the storage. Similar to a real blackboard, game objects put their data on the board that then other objects can observe, react to or update the data. E.g. various game state values.
They require a handle (more on that later). Assuming you have a player
handle:
print(player:IsNaked())
print(player:IsMoving())
player:OnDied()
Handles are a way to pass an object to the function. For example IsNaked
makes no sense without telling the engine for which object we want to know this information.
These handles are static, there is only one in the game, for example the gameTimeSystem
is a singleton so there is no need to tell the script engine which one you want. That being said you need a singleton handle so it knows you want to call a function on that system.
Sample:
gameTime = GetSingleton("gameTimeSystem")
print(gameTime:GetGameTime())
gameTime:SetGameTimeByHMS(12,0,0) -- 12h0m0s
These handles are not unique, for example, the game contains multiple NPCs so there are as many handles as NPCs. Currently as far as I know we can only get the handle of the player
by calling the global function Game.GetPlayer()
.
Sample:
player = Game.GetPlayer()
if player:IsNaked() then
player:OnDied() -- kill the player if it's naked
end
player = Game.GetPlayer()
ts = Game.GetTransactionSystem()
tid = TweakDBID.new("Items.money")
itemid = ItemID.new(tid)
result = ts:GiveItem(player, itemid, 100000)
if result then
print("We added " .. tostring(itemid) .. " to the inventory!")
else
print("Failed to add " .. tostring(itemid))
end
They do not need a handle and are all located in the Game
object. Sample:
Game.PrintHealth()
-- or
Game.AddToInventory("Items.money", 500)
How to make the most of psiberx's script expansion framework
You can find Codeware's existing documentation at the wiki on psiberx's github
As of Mar 2024, you can't access rRef or raRef natively via CET:
-- this does not work
ComponentParser.GetMeshComponent(Game.GetPlayer(), 't0_000_pwa_fpp__torso')
This will most likely return nil
or crash the game.
Even if it does not, no reference is kept in memory: you're effectively working on a copy and there is currently no way to pass your changes back to the game.
Currently, the only way to access resources is via Codeware, which will do its best to sync your changes with the actually-existing object.
Game.AddToInventory("Items.money", 10000) -- Gives 10,000 eurodollars
Game.AddToInventory("Items.SQ031_Samurai_Jacket", 1) -- Gives you the Replica of Johnny's Samurai Jacket
Game.AddToInventory("Ammo.HandgunAmmo", 500)
Game.AddToInventory("Ammo.RifleAmmo", 700)
Game.AddToInventory("Ammo.ShotgunAmmo", 100)
Game.AddToInventory("Ammo.SniperRifleAmmo", 100)
Game.AddToInventory("Items.CommonMaterial1", 1000)
Game.AddToInventory("Items.UncommonMaterial1", 1000)
Game.AddToInventory("Items.RareMaterial1", 1000)
Game.AddToInventory("Items.EpicMaterial1", 1000)
Game.AddToInventory("Items.LegendaryMaterial1", 1000)
Game.AddToInventory("Items.QuickHackUncommonMaterial1", 1000)
Game.AddToInventory("Items.QuickHackRareMaterial1", 1000)
Game.AddToInventory("Items.QuickHackEpicMaterial1", 1000)
Game.AddToInventory("Items.QuickHackLegendaryMaterial1", 1000)
Game.SetLevel("Level", 60, 1) -- Sets character level to 60
PlayerDevelopmentSystem.GetInstance(Game.GetPlayer()):GetDevelopmentData(Game.GetPlayer()):SetAttribute("Strength", 15) -- Sets Body to 15
PlayerDevelopmentSystem.GetInstance(Game.GetPlayer()):GetDevelopmentData(Game.GetPlayer()):AddDevelopmentPoints(5, gamedataDevelopmentPointType.Attribute) -- Attribute (skill) points
PlayerDevelopmentSystem.GetInstance(Game.GetPlayer()):GetDevelopmentData(Game.GetPlayer()):AddDevelopmentPoints(3, gamedataDevelopmentPointType.Primary) -- Perk points
print(Game.GetPlayer():GetWorldPosition())
Game.GetTeleportationFacility():Teleport(GetPlayer(), ToVector4{x=X, y=Y, z=Z, w=1}, ToEulerAngles{roll=0, pitch=0, yaw=0})
Game.GetQuestsSystem():SetFactStr("sq032_johnny_friend", 1)
Game.GetQuestsSystem():SetFactStr("mq007_skippy_aim_at_head", 1)
Game.GetQuestsSystem():SetFactStr("mq007_skippy_goes_emo", 0)
Game.GetQuestsSystem():SetFactStr("q005_jackie_to_hospital", 0)
Game.GetQuestsSystem():SetFactStr("q005_jackie_to_mama", 0)
Game.GetQuestsSystem():SetFactStr("q005_jackie_stay_notell", 1)
Game.GetQuestsSystem():SetFactStr("q112_takemura_dead", 1)
Miscellaneous functions useful for debugging and getting special objects and info.
Hotkeys are buttons events triggered on key release. They must be registered using registerHotkey()
at root level, outside of any event, in the init.lua
file.
are not triggered while the player stay pressed on a game's keybind.
For example, if the player move forward with "W" and press a Hotkey at the same time, it won't be triggered.
For this reason, it is recommended to use instead, as they are always triggered.
registerHotkey(slug, label, callback)
--
-- registerHotkey()
--
-- @param string slug The internal slug (must be unique in your mod scope)
-- @param string label The label displayed in CET Bindings
-- @param function callback The callback function
--
registerHotkey('slug', 'label', function()
-- hotkey is released
end)
registerHotkey('give_money', 'Give Money', function()
Game.AddToInventory('Items.money', 1000)
end)
NewProxy is a built-in CET feature you can use to declare callback functions. You can use it to bind a Lua function as a callback with game's script system.
listener = NewProxy({
CallbackName = {
args = ["ArgType1", "ArgType2", ...],
callback = function([arg1, arg2, ...]) end
},
...
})
CallbackName is an arbitrary name you define. A callback name can be formatted as follow On[Action]
(e.g. OnDamaged
or also OnPlayerDamaged
).
CallbackDefinition is the signature of the script function (args) and the Lua function (callback) you want to be executed when the callback is triggered.
The list of arguments must indicate the name of the types to expect. For example with a callback function which receives a String
, an Int32
and a reference to a GameObject
, it should be defined like this:
args = {"String", "Int32", "handle:GameObject"}
In this case, you can define your function callback like this:
callback = function(arg1, arg2, arg3)
-- print("arg1: " .. arg1)
-- print("arg2: " .. tostring(arg2))
-- print("arg3: " .. NameToString(arg3:GetClassName()))
end
The signature of the function depends on the game's script function you want to register a callback for.
You can use NativeDB to know the types of arguments to declare. By default, the syntax will be written in Redscript. You can change the option Code syntax
in the settings and select Pseudocode · Legacy
instead. Basically, it will show you handle:GameObject
instead of ref<GameObject>
(among other things).
Lets create a proxy:
listener = NewProxy({
OnHit = {
args = {"handle:GameObject", "Uint32"},
callback = function(shooter, damage)
print("Hit by " .. NameToString(shooter:GetClassName()) .. "!")
print("You lost " .. tostring(damage) .. " HP.")
end
}
})
After creating the proxy, you can use it to pass the target and function you want to callback. Lets say a game's script is defined as:
// redscript syntax
public class AwesomePlayer extends PlayerPuppet {
public func RegisterHit(target: ref<IScriptable>, fn: CName) -> Void;
public func RegisterShoot(target: ref<IScriptable>, fn: CName) -> Void;
}
We can call the function RegisterHit
to register our callback with our proxy like this:
local awesome = AwesomePlayer.new() -- for example only
awesome:RegisterHit(listener:Target(), listener:Function("OnHit"))
Note that the value, when calling listener:Function("OnHit")
, is the same we declared in the proxy.
This way, you can create multiple callback in a proxy and you just need to call listener:Function
with the name of the callback you want to use. For example:
listener = NewProxy({
OnHit = {...},
OnShoot = {
args = {"handle:GameObject", "Uint32"},
callback = function(enemy, damage)
print("You hit " .. NameToString(enemy:GetClassName()) .. "!")
print("He/She lost " .. tostring(damage) .. " HP.")
end
}
})
-- ...
awesome:RegisterShoot(listener:Target(), listener:Function("OnShoot"))
This example will be using Codeware and its system to listen for game events. It will listen for the event Session/Ready
and print a message in CET logs.
local mod = {
listener = nil
}
-- Define our function to callback
function OnReady(event)
local isMenu = event:IsPreGame()
print("Event \"Session/Ready\" triggered!")
if isMenu then
print("Player is in the pre-game menu")
else
print("Player is in the game")
end
end
registerForEvent('onInit', function()
-- Create our proxy
mod.listener = NewProxy({
OnSessionReady = {
-- Type is defined in the wiki of Codeware
args = {"handle:GameSessionEvent"},
callback = function(event) OnReady(event) end
}
})
-- Register our callback to listen for event "Session/Ready".
local callbackSystem = Game.GetCallbackSystem()
local target = mod.listener:Target()
local fn = mod.listener:Function("OnSessionReady")
callbackSystem:RegisterCallback("Session/Ready", target, fn)
end)
registerForEvent('onShutdown', function()
-- Unregister our callback before our mod is "removed".
local target = mod.listener:Target()
local fn = mod.listener:Function("OnSessionReady")
callbackSystem:UnregisterCallback("Session/Ready", target, fn)
end)
Do stuff later without blocking
Published: Feb 18 2024 by Last documented edit: Feb 18 2024 by
You can get Cron.lua from psiberx's cp2077-cet-kit on github. Add it to your sources, and then define it as a global:
-- don't overwrite it if it's already defined in the global scope. Or do, up to you.
Cron = Cron or require('External/Cron.lua')
Cron offers a bunch of functions for executing code later. The code you want to execute has to be inside a function (=> callback) and is passed among the arguments.
For periodically running functions, you need to include logic when to stop the execution.
To save processor load, it is best practice to define your callback outside of the function call. This can have a noticable performance impact, so you're gonna want to do this.
local callback = function()
-- do a little dance, make a little love
end
Cron.Every(0.1, callback)
The callback is defined once.
Cron.Every(0.1, function()
-- do a little dance, make a little love
end)
The callback is defined every time cron runs.
To pass arguments to your callback function, you pass them as arguments to cron:
local callback = function(timer)
timer.tick = timer.tick +1
-- do a little dance, make a little love
end
Cron.Every(0.1, { tick = 1 }, callback)
Execute the callback after X seconds
local callback = function() --[[ do a little dance, make a little love ]] end
Cron.After(0.2, callback)
Executes a function every X seconds.
The function will run forever unless you stop it!
local callback = function(timer)
timer.tick = timer.tick +1
spdlog.info("Timer is running: " .. tostring(timer.tick))
-- do a little dance, make a little love
if timer.tick > 150 then
Cron.Halt(timer)
end
end
-- define outside of function scope to re-use this in Cron.Pause example below
local timerArg = { tick = 1 }
-- start timer
Cron.Every(0.2, timerArg, callback)
You can pause a timer by passing its reference to cron:
-- callback and timer have been defined in the example above
-- the timer is running
Cron.Pause(timerArg)
spdlog.info("Timer is paused after " .. tostring(timerArg.tick) .. " ticks")
-- wait 20 seconds, then resume our function
Cron.After(20, function () Cron.Resume(timerArg) end)
⚠️ All examples in this Observe/Override method
ShootEvents.OnEnter
to gain access tostateContext
.
AdjustTransform
Base class for AdjustTransformWithDurations
. Seems to do nothing if passed to adjustTransform
scriptable parameter. Do not use: your movement will be blocked indefinitely until you reset adjustTransform
.
AdjustTransformWithDurations
Discovered by void*
Request to change player trajectory (view angles) and/or player position.
This code has to be executed in the context where you have access to StateContext for example:
Function has 3 types of behavior: set trajectory, set position or set both.
This code will smoothly point your camera towards {0, 0, 0}
.
GameObject
for trajectory
Does not change player's pitch.
Seems to target "nearest" targeting component (aka hitbox).
SetRotation
is ignored if target is set.Will freeze player in place/in air while it's aiming (same behavior as )
This function seem to use TargetingSystem to find the "best component" and then calls a LookAt
.
(Look at function and one)
Returns the width and height (respectively) of the game window in pixels.
GetDisplayResolution() -> float, float
width, height = GetDisplayResolution()
print(width, height)
> 1920 1080
Returns the return value of the mod with the given name.
If you don't return anything in your init.lua
file, this function will return nil
. This will often look like return MyMod:new()
at the end of your file. See the Lua documentation for info about how classes can be implemented in Lua.
GetMod(modName: string) -> object
mod = GetMod('MyMod')
-- any data returned by the mod
Returns the singleton for the given type name if it exists, otherwisenil
.
GetSingleton(singletonTypeName: string) -> SingletonReference | nil
gameTime = GetSingleton("gameTimeSystem")
-- get time
gameTime:GetGameTime()
-- set time
gameTime:SetGameTimeByHMS(12, 0, 0) -- 12h0m0s
GameTime[ seconds:734466 ]
Returns the current CET build's commit hash.
GetVersion() -> string
version = GetVersion()
print(version)
> v1.27.1
Returns whether the specified .archive
file is present in the archive/pc/mod
folder
ModArchiveExists(archiveName: string) -> bool
print(ModArchiveExists('myModFiles.archive'))
> true
This technique can be used to detect whether your Redscript mod is present, from CET side. It can be useful if you are writing a mod with both languages for some reason.
You can declare a static method without a body. It will do nothing:
// Redscript side
public static func IsDetected_NameOfYourMod() -> Void {}
You can now test whether this method is defined from CET side using this:
-- CET side
function IsRedscriptDetected()
return Game["IsDetected_NameOfYourMod"] ~= nil
end
You can call IsRedscriptDetected()
and it will return true
when your Redscript method is detected (meaning your Redscript mod is loaded) or false otherwise.
If you're using redscript modules, and your IsDetected
function is inside a module, the name will be mangled to include the full module name and a dot separating it:
// Redscript side
module Module.Name
public static func IsDetected() -> Void {}
-- CET side
function IsRedscriptDetected()
return Module_Name_IsDetected ~= nil
-- equivalent syntax without conversion:
-- return Game["Module.Name.IsDetected"] ~= nil
end
Methods for interacting with TweakDB, the game's internal data store.
TweakDB is like the game’s database for gameplay features. It’s not actually code itself, but very large amounts of data structures, many of which link together, to make coherent gameplay elements.
TweakDB has two main elements: Flats, and Records Flats are just like a single piece of data, like a number, a string, a pointer to a record, etc. Records are like a collection of flats. Records have possible types, like a character record or a clothing record. The type of record determines what set of flats the record has.
An example of these gameplay features nearly entirely contained in TweakDB, would be the damaging quickhacks like Overheat, Contagion, and Short Circuit. The quickhack item to unlock them is in tweakdb, as well as the process of unlocking, making them show up on enemies, the uploading process, the status effect applied, all the special effects possible, as well as the stats to customize all of these things. Almost anything about these quickhacks can be changed in TweakDB.
Print info about the TweakDB. Displays the number of flats, records, and queries, the size in bytes of flatDataBuffer, and the number of created records.
TweakDB:DebugStats() -> nil
TweakDB:DebugStats()
Get a TweakDB record by name or ID.
TweakDB:GetRecord(recordName: string) -> TweakDBRecord
TweakDB:GetRecord(recordID: TweakDBID) -> TweakDBRecord
sticky_frag = TweakDB:GetRecord("Items.GrenadeFragSticky")
Get a table of all TweakDB records under a given type.
TweakDB:GetRecords(recordTypeName: string) -> table<TweakDBRecord>
grenades = TweakDB:GetRecords("gamedataGrenade_Record")
Get a TweakDB query by name or ID. Returns a table of TweakDBIDs.
TweakDB:Query(queryName: string) -> table<TweakDBID>
TweakDB:Query(queryID: TweakDBID) -> table<TweakDBID>
grenade_ids = TweakDB:Query("Query.GrenadeNoFaction")
Get a TweakDB flat by name or ID.
TweakDB:GetFlat(flatName: string) -> object
TweakDB:GetFlat(flatID: TweakDBID) -> object
jump_stamina_cost = TweakDB:GetFlat("player.staminaCosts.jump")
Set a TweakDB flat by name or ID and update it. Returns whether the flat was successfully set.
TweakDB:SetFlat(flatName: string, newValue: object) -> boolean
TweakDB:SetFlat(flatID: TweakDBID, newValue: object) -> boolean
success = TweakDB:SetFlat("player.staminaCosts.jump", 7.5)
Set a TweakDB flat by name or ID without updating it. Returns whether the flat was successfully set.
TweakDB:SetFlatNoUpdate(flatName: string, newValue: object) -> boolean
TweakDB:SetFlatNoUpdate(flatName: TweakDBID, newValue: object) -> boolean
success = TweakDB:SetFlatNoUpdate("player.staminaCosts.jump", 7.5)
Update (flush data of) a TweakDB record by name, ID, or handle. Returns whether the update was successful.
TweakDB:Update(recordName: string) -> boolean
TweakDB:Update(recordID: TweakDBID) -> boolean
TweakDB:Update(recordHandle: TweakDBRecord) -> boolean
success = TweakDB:Update("player.staminaCosts.jump")
Create a new TweakDB record. Returns whether the record was created successfully.
TweakDB:CreateRecord(recordName: string, recordTypeName: string) -> boolean
TweakDB:CreateRecord(recordID: TweakDBID, recordTypeName: string) -> boolean
Clone an existing record identified by clonedRecordName
or clonedRecordID
to a new record named recordName
or with a TweakDBID
of recordID
. Returns whether the record was cloned successfully. If a record named recordName
or with ID recordID
already exists, this method will fail.
TweakDB:CloneRecord(recordName: string, clonedRecordName: string) -> boolean
TweakDB:CloneRecord(recordName: string, clonedRecordID: TweakDBID) -> boolean
TweakDB:CloneRecord(recordID: TweakDBID, clonedRecordName: string) -> boolean
TweakDB:CloneRecord(recordID: TweakDBID, clonedRecordID: TweakDBID) -> boolean
Delete an existing TweakDB record. Returns whether the record was deleted successfully.
TweakDB:DeleteRecord(recordName: string) -> boolean
TweakDB:DeleteRecord(recordID: TweakDBID) -> boolean
local adjustRequest = AdjustTransformWithDurations.new();
-- Setting any duration to 0 or lower means "do not change"
-- Slide = change player position
-- Rotation = change player rotation / trajectory / viewing angles
adjustRequest:SetSlideDuration(-1.0); -- Disabling position adjustion
adjustRequest:SetRotationDuration(1); -- Rotation show be smooth and should happen in 1 second (presumably, second!)
adjustRequest:SetUseParabolicMotion(true); -- Do not just interpolate movement/rotation, use parabolic formula
adjustRequest:SetCurve(CName.new("None")); -- Setting curve to "None", no other curves were found in the sources
adjustRequest:SetRotation(ToEulerAngles {0, 0, 0}:ToQuat()); -- Setting target player's rotation
stateContext:SetTemporaryScriptableParameter("adjustTransform", adjustRequest, true);
local adjustRequest = AdjustTransformWithDurations.new();
adjustRequest:SetSlideDuration(-1.0); -- Disabling position adjustion
adjustRequest:SetRotationDuration(1.0); -- Rotation show be smooth and should happen in ~1 second (presumably, seconds!)
adjustRequest:SetUseParabolicMotion(true); -- Do not just interpolate movement/rotation, use parabolic formula
adjustRequest:SetCurve(CName.new("None")); -- Setting curve to "None", no other curves were found in the sources
-- In this case aimAt is a GameObject
adjustRequest:SetTarget(aimAt); -- Setting target player's rotation
stateContext:SetTemporaryScriptableParameter("adjustTransform", adjustRequest, true);
local adjustRequest = AdjustTransformWithDurations.new();
adjustRequest:SetSlideDuration(1.0); -- Setting duration of position change.
adjustRequest:SetRotationDuration(-1.0); -- Disabling rotation adjustion
adjustRequest:SetUseParabolicMotion(true); -- Do not just interpolate movement/rotation, use parabolic formula
adjustRequest:SetCurve(CName.new("None")); -- Setting curve to "None", no other curves were found in the sources
-- Getting player's position
local playerPosition = Game.GetPlayer():GetWorldPosition();
-- Adding 10 on the vertical axis
playerPosition.z = playerPosition.z + 10;
adjustRequest:SetPosition(playerPosition);
stateContext:SetTemporaryScriptableParameter("adjustTransform", adjustRequest, true);
This page contains examples for creating UI for your mod using the built-in ImGui library.
Everything you need to know about Dear ImGui can be found and .
You can also take a look at . It is a free online tool to create ImGui layouts with an easy to use interface. It also generates Lua code for you. Feel free to come on this Discord if you have issues or feedback to share.
Any ImGui code needs to be run from inside the callback.
Default tooltip on a button.
To customize the tooltip window use BeginTooltip
and EndTooltip
.
Creating a multi-column layout is a pain in the ass, but you can go about it like this:
You can probably use dynamic dimensions somehow. If you find out, please update this guide.
Inputs are buttons events that handle both key press and release states. They must be registered using registerInput()
at root level, outside of any event, in the init.lua
file.
The button state (press/release) is defined in the first argument passed to the callback.
You can register an Input and make it behave like a Hotkey. This method is more reactive as it triggers on key press, when a Hotkey is triggered on release.
It is important to check the keypress
argument inside the callback. Otherwise the code will be executed twice:
One time when the key is pressed
A second time when released
This is a preliminary list of item hashes for clothes recipes, provided by Bettik#0331. I have preserved it here so the data doesn't get lost until we can find a proper home for it.
Game.AddToInventory("Items.LegendaryBootsRecipe", 1)
Game.AddToInventory("Items.LegendaryCapRecipe", 1)
Game.AddToInventory("Items.LegendaryCasualShoesRecipe", 1)
Game.AddToInventory("Items.LegendaryFormalPantsRecipe", 1)
Game.AddToInventory("Items.LegendaryFormalShoesRecipe", 1)
Game.AddToInventory("Items.LegendaryFormalSkirtRecipe", 1)
Game.AddToInventory("Items.LegendaryGlassesRecipe", 1)
Game.AddToInventory("Items.LegendaryGogglesRecipe", 1)
Game.AddToInventory("Items.LegendaryHatRecipe", 1)
Game.AddToInventory("Items.LegendaryJacketRecipe", 1)
Game.AddToInventory("Items.LegendaryJumpsuitRecipe", 1)
Game.AddToInventory("Items.LegendaryPantsRecipe", 1)
Game.AddToInventory("Items.LegendaryScarfRecipe", 1)
Game.AddToInventory("Items.LegendaryShirtRecipe", 1)
Game.AddToInventory("Items.LegendaryShortsRecipe", 1)
Game.AddToInventory("Items.LegendaryTechRecipe", 1)
Game.AddToInventory("Items.LegendaryTightJumpsuitRecipe", 1)
Game.AddToInventory("Items.LegendaryTShirtRecipe", 1)
Game.AddToInventory("Items.LegendaryVestRecipe", 1)
Game.AddToInventory("Items.LegendaryVisorRecipe", 1)
Game.AddToInventory("Items.EpicBootsRecipe", 1)
Game.AddToInventory("Items.EpicCapRecipe", 1)
Game.AddToInventory("Items.EpicCasualShoesRecipe", 1)
Game.AddToInventory("Items.EpicCoatRecipe", 1)
Game.AddToInventory("Items.EpicFormalJacketRecipe", 1)
Game.AddToInventory("Items.EpicFormalShoesRecipe", 1)
Game.AddToInventory("Items.EpicFormalSkirtRecipe", 1)
Game.AddToInventory("Items.EpicGlassesRecipe", 1)
Game.AddToInventory("Items.EpicHatRecipe", 1)
Game.AddToInventory("Items.EpicHelmetRecipe", 1)
Game.AddToInventory("Items.EpicLooseShirtRecipe", 1)
Game.AddToInventory("Items.EpicMaskRecipe", 1)
Game.AddToInventory("Items.EpicPantsRecipe", 1)
Game.AddToInventory("Items.EpicScarfRecipe", 1)
Game.AddToInventory("Items.EpicShortsRecipe", 1)
Game.AddToInventory("Items.EpicTShirtRecipe", 1)
Game.AddToInventory("Items.EpicUndershirtRecipe", 1)
Game.AddToInventory("Items.EpicVisorRecipe", 1)
Game.AddToInventory("Items.RareBootsRecipe", 1)
Game.AddToInventory("Items.RareCasualShoesRecipe", 1)
Game.AddToInventory("Items.RareCoatRecipe", 1)
Game.AddToInventory("Items.RareFormalJacketRecipe", 1)
Game.AddToInventory("Items.RareFormalPantsRecipe", 1)
Game.AddToInventory("Items.RareFormalShoesRecipe", 1)
Game.AddToInventory("Items.RareGlassesRecipe", 1)
Game.AddToInventory("Items.RareHatRecipe", 1)
Game.AddToInventory("Items.RareHelmetRecipe", 1)
Game.AddToInventory("Items.RareJacketRecipe", 1)
Game.AddToInventory("Items.RareMaskRecipe", 1)
Game.AddToInventory("Items.RarePantsRecipe", 1)
Game.AddToInventory("Items.RareScarfRecipe", 1)
Game.AddToInventory("Items.RareShirtRecipe", 1)
Game.AddToInventory("Items.RareShortsRecipe", 1)
Game.AddToInventory("Items.RareTechRecipe", 1)
Game.AddToInventory("Items.RareTShirtRecipe", 1)
Game.AddToInventory("Items.RareUndershirtRecipe", 1)
Game.AddToInventory("Items.RareVestRecipe", 1)
Game.AddToInventory("Items.UncommonBalaclavaRecipe", 1)
Game.AddToInventory("Items.UncommonBootsRecipe", 1)
Game.AddToInventory("Items.UncommonCapRecipe", 1)
Game.AddToInventory("Items.UncommonCasualShoesRecipe", 1)
Game.AddToInventory("Items.UncommonFormalPantsRecipe", 1)
Game.AddToInventory("Items.UncommonFormalShoesRecipe", 1)
Game.AddToInventory("Items.UncommonFormalSkirtRecipe", 1)
Game.AddToInventory("Items.UncommonHatRecipe", 1)
Game.AddToInventory("Items.UncommonHelmetRecipe", 1)
Game.AddToInventory("Items.UncommonJacketRecipe", 1)
Game.AddToInventory("Items.UncommonMaskRecipe", 1)
Game.AddToInventory("Items.UncommonPantsRecipe", 1)
Game.AddToInventory("Items.UncommonShirtRecipe", 1)
Game.AddToInventory("Items.UncommonTechRecipe", 1)
Game.AddToInventory("Items.UncommonUndershirtRecipe", 1)
Game.AddToInventory("Items.UncommonVestRecipe", 1)
Game.AddToInventory("Items.UncommonVisorRecipe", 1)
The txt file below contains a dump directly from the game files of all currently known items in the game's TweakDB.
ImGui.SetNextWindowPos(100, 500, ImGuiCond.FirstUseEver) -- set window position x, y
ImGui.SetNextWindowSize(300, 600, ImGuiCond.Appearing) -- set window size w, h
if ImGui.Begin("Unique Window Name") then
ImGui.Text("Hello World")
-- more window contents here
end
ImGui.End()
if ImGui.Button("Pop Button", 120, 0) then
ImGui.OpenPopup("Delete?")
end
if ImGui.BeginPopupModal("Delete?", true, ImGuiWindowFlags.AlwaysAutoResize) then
ImGui.Text("This is a popup")
if ImGui.Button("Close") then ImGui.CloseCurrentPopup() end
ImGui.EndPopup()
end
local DropdownOptions = {"1", "2", "3", "4", "5"}
local DropdownSelected = "1"
if ImGui.BeginCombo("##My Combo Box", DropdownSelected) then -- Remove the ## if you'd like for the title to display above combo box
for _, option in ipairs(DropdownOptions) do
if ImGui.Selectable(option, (option == DropdownSelected)) then
DropdownSelected = option
ImGui.SetItemDefaultFocus()
end
end
ImGui.EndCombo()
end
MyMod.myValue = ImGui.Checkbox("Check me", MyMod.myValue)
if ImGui.Button("Click Me!", 100, 20) then -- Label, width, height - Use -1 as width for button to span available horizontal space
-- do stuff here when button is clicked
end
local pressed
registerForEvent("onUpdate", function()
if pressed then
print("You pressed me!")
end
end)
registerForEvent("onDraw", function()
pressed = ImGui.Button("Click me I'm a Sexy Button", 250, 25)
end)
local pressed = ImGui.Button("button text")
if ImGui.IsItemHovered() then
ImGui.SetTooltip("tooltip text")
else
ImGui.SetTooltip(nil)
end
local pressed = ImGui.Button("button text")
if ImGui.IsItemHovered() then
ImGui.BeginTooltip()
ImGui.PushTextWrapPos(50)
ImGui.TextWrapped("tooltip text")
ImGui.EndTooltip()
else
ImGui.SetTooltip(nil)
end
ImGui.BeginChild("Column 1", 300, 400) -- this will not display a label
ImGui.Text('Column 1')
ImGui.EndChild()
ImGui.BeginChild("Column 2", 300, 400) -- this will not display a label
ImGui.Text('Column 2')
ImGui.EndChild()
registerInput(slug, label, callback)
--
-- registerInput()
--
-- @param string slug The internal slug (must be unique in your mod scope)
-- @param string label The label displayed in CET Bindings
-- @param function callback The callback function
--
registerInput('slug', 'label', function(keypress)
if keypress then
-- key is pressed
else
-- key is released
end
end)
registerInput('slug', 'label', function(keypress)
-- bail early on key release
if not keypress then
return
end
-- key is pressed
end)
registerInput('slug', 'label', function(keypress)
-- this will be called 2 times!
end)
-- register input
registerInput('slow_motion', 'Slow Motion', function(keypress)
-- get time system
local timeSystem = Game.GetTimeSystem()
-- bail early if time system doesn't exists
if not timeSystem then
return
end
-- key is pressed
if keypress then
timeSystem:SetTimeDilation('MySlowMo', 0.3)
-- key is released
else
timeSystem:UnsetTimeDilation('MySlowMo')
end
end)
-- set initial switch state
keep_giving = false
-- register input
registerInput('give_continuous_money', 'Give Continuous Money', function(keypress)
-- input pressed
if keypress then
keep_giving = true -- switch on
-- input released
else
keep_giving = false -- switch off
end
end)
-- onUpdate
-- this event is triggered continuously
registerForEvent('onUpdate', function()
-- check switch state
if keep_giving then
Game.AddToInventory('Items.money', 20)
end
end)
fileExists(path: string) -> boolean
print(fileExists('my_file.lua'))
-- true
function fileExists(path)
local f = io.open(path, 'r')
return f ~= nil and io.close(f)
end
getFilesRecursive(path: string, maxDepth: integer) -> table
--
-- getFilesRecursive()
--
-- @param string path The starting path: Default: '.' (mod's root folder)
-- @param integer maxDepth The maximum depth allowed. Default: -1 (unlimited)
--
getFilesRecursive('.', -1)
local files = getFilesRecursive('.', 2)
files = {
"./includes/module-1.lua",
"./includes/module-2.lua",
"./includes/sub-folder/sub-module-1.lua",
"./init.lua",
}
function getFilesRecursive(path, maxDepth, depth, storage)
-- vars
path = path or '.'
maxDepth = maxDepth or -1
depth = depth or 0
storage = storage or {}
-- validate maxDepth
if maxDepth ~= -1 and depth > maxDepth then
return storage
end
-- sanitize path leading "."
if not string.find(path, '^%.') then
path = '.' .. path
end
-- sanitize path leading double "//"
if string.find(path, '^%.//') then
path = string.gsub(path, '^%.//', './')
end
-- get dir/files
local files = dir(path)
-- validate files
if not files or (type(files) == 'table' and #files == 0) then
return storage
end
-- loop files
for _, file in ipairs(files) do
-- directory
if file.type == 'directory' then
getFilesRecursive(path .. '/' .. file.name, maxDepth, depth + 1, storage)
-- lua file
elseif string.find(file.name, '%.lua$') then
table.insert(storage, path .. '/' .. file.name)
end
end
return storage
end
getFoldersRecursive(path: string, maxDepth: int) -> table
--
-- getFoldersRecursive()
--
-- @param string path The starting path: Default: '.' (mod's root folder)
-- @param integer maxDepth The maximum depth allowed. Default: -1 (unlimited)
--
getFoldersRecursive('.', -1)
local folders = getFoldersRecursive('.', 2)
folders = {
"./includes",
"./includes/sub-folder",
}
function getFoldersRecursive(path, maxDepth, depth, storage)
-- vars
path = path or '.'
maxDepth = maxDepth or -1
depth = depth or 0
storage = storage or {}
-- validate maxDepth
if maxDepth ~= -1 and depth > maxDepth then
return storage
end
-- sanitize path leading "."
if not string.find(path, '^%.') then
path = '.' .. path
end
-- sanitize path leading double "//"
if string.find(path, '^%.//') then
path = string.gsub(path, '^%.//', './')
end
-- get dir/files
local files = dir(path)
-- validate files
if not files or (type(files) == 'table' and #files == 0) then
return storage
end
-- loop files
for _, file in ipairs(files) do
-- directory
if file.type == 'directory' then
table.insert(storage, path .. '/' .. file.name)
getFoldersRecursive(path .. '/' .. file.name, maxDepth, depth + 1, storage)
end
end
return storage
end
Prints one or more objects to the and file. Multiple objects will be printed separated by a space.
Write a string in the file, eg, <
>/mods/my_mod/my_mod.log
.
There are several spdlog
methods, all are identical and have the same output:
spdlog.debug()
spdlog.trace()
spdlog.info()
spdlog.warning()
spdlog.error()
spdlog.critical()
It is recommended to use spdlog.info(tostring(my_var))
to make sure the variable is compatible, as boolean
, interger
and other types will throw an error.
Returns a JSON-ish string with details about the members of obj
:
Type name
Instance functions
Static functions
Properties
The listed functions include their signatures.
If detailed
is true, function descriptions will include the function's full name hash, short name, and short name hash.
Returns the game's internal debug string for the given object. This string displays the object's properties and their values.
Beware that this string probably won't have any line breaks, so it's more advisable to open <
>/scripting.log
in a text editor with line wrapping to view the output of this function.
Like , but takes a type name rather than an object instance. Use to get a list of all types.
Dump all type names to <
>/cyber_engine_tweaks.log
and prints to the the number of types dumped.
This event gets triggered even before , and can be useful for making changes to the TweakDB, as some values can only be changes very early during the loading process.
print(obj: object, [obj2: object, [...]]) -> nil
print('Hey', 'there', 1, 2, true)
> Hey there 1 2 true
spdlog.info(string: string) -> nil
spdlog.info('Custom log')
> [02:31:08] Cutom log
Dump(obj: GameDataType, detailed: bool) -> string
print(Dump(Game.GetPlayer(), false))
{
name: PlayerPuppet,
functions: {
IsPlayer() => (Bool),
IsReplacer() => (Bool),
IsVRReplacer() => (Bool),
IsJohnnyReplacer() => (Bool),
IsReplicable() => (Bool),
GetReplicatedStateClass() => (CName),
IsCoverModifierAdded() => (Bool),
IsWorkspotDamageReductionAdded() => (Bool),
IsWorkspotVisibilityReductionActive() => (Bool),
GetOverlappedSecurityZones() => (array:gamePersistentID),
...
}
GameDump(obj: GameDataType) -> string
print(GameDump(Game.GetPlayer()))
> PlayerPuppet[ customCameraTarget:ECCTV_OnlyOnscreen, renderSceneLayerMask:<Default;Cyberspace>, persistentState:, playerSocket:gamePlayerSocket[ ], ...]
DumpType(typeName: string, detailed: boolean) -> string
print(DumpType('PlayerPuppet', false))
{
name: PlayerPuppet,
functions: {
IsPlayer() => (Bool),
IsReplacer() => (Bool),
IsVRReplacer() => (Bool),
IsJohnnyReplacer() => (Bool),
IsReplicable() => (Bool),
GetReplicatedStateClass() => (CName),
IsCoverModifierAdded() => (Bool),
IsWorkspotDamageReductionAdded() => (Bool),
IsWorkspotVisibilityReductionActive() => (Bool),
GetOverlappedSecurityZones() => (array:gamePersistentID),
...
}
DumpAllTypeNames() -> nil
DumpAllTypeNames()
> Dumped 26811 types
[operator ()] [4416] handle:gamedataUICharacterCreationAttribute_Record
[operator ()] [4416] DamageTypeIndicator
[operator ()] [4416] worldAIDirectorSpawnNode
[operator ()] [4416] UI_DEV_ScriptableSystemUseNewTooltips
[operator ()] [4416] array:SDocumentThumbnailWidgetPackage
[operator ()] [4416] gameIPersistencySystem
[operator ()] [4416] DelayedComDeviceClose
[operator ()] [4416] inkSettingsSelectorController
[operator ()] [4416] array:handle:AICombatSquadScriptInterface
[operator ()] [4416] animAnimNode_LocomotionAdjusterOnEvent
[operator ()] [4416] whandle:gamedataLightPreset_Record
[operator ()] [4416] handle:C4ControllerPS
...
It is generally advisable to useObserve
whenever possible, and only use Override
when absolutely necessary
Override is a built-in CET function allowing developers to rewrite a game's class method. It must be registered using the Override()
function inside the onInit event, in the init.lua
file.
The provided callback
is always filled as follows:
The first argument is the current object that execute the method. This argument is not present when function/method is static.
Others arguments passed to the targeted method, if any.
The last argument is the original callable method.
Override(className, method, callback)
--
-- Override()
--
-- @param string className The parent class name
-- @param string method The method name to target
-- @param function callback The callback function
--
Override('className', 'method', function(self [, arg1, arg2, ...], wrappedMethod)
-- rewrite method()
end)
Considering the following class and method:
public class AimingStateEvents extends UpperBodyEventsTransition {
protected func OnEnter(stateContext: ref<StateContext>, scriptInterface: ref<StateGameScriptInterface>) -> Void {
let aimingCost: Float;
let focusEventUI: ref<FocusPerkTriggerd>;
let timeDilationFocusedPerk: Float;
let weaponType: gamedataItemType;
let player: ref<PlayerPuppet> = scriptInterface.executionOwner as PlayerPuppet;
// ...
}
}
When applying Override()
, the callback would be filled with the following arguments:
Override('AimingStateEvents', 'OnEnter', function(self, stateContext, scriptInterface, wrappedMethod)
-- self the 'AimingStateEvents' object
-- stateContext the original method first argument
-- scriptInterface the original method second argument
-- wrappedMethod the original 'OnEnter' callable method
end)
The last argument of the Override()
function, wrappedMethod
, contains the original method as a callable function. We can use it to execute the original code and/or retrieve its result (if it returns something).
It is highly recommended to know the overriden method return statement to avoid breaking the script, and use wrappedMethod
accordingly.
Any method declared void doesn't return a value. Overriding it follow the same logic:
public class CrouchDecisions extends LocomotionGroundDecisions {
public func OnStatusEffectApplied(statusEffect: wref<StatusEffect_Record>) -> Void {
// ...
}
}
Override('CrouchDecisions', 'OnStatusEffectApplied', function(self, statusEffect, wrappedMethod)
-- execute original code
wrappedMethod(statusEffect)
-- do something
end)
The same logic is applied to methods with a specific return statement. Overriding it must return a compatible type:
public class CrouchDecisions extends LocomotionGroundDecisions {
protected cb func OnControllingDeviceChange(value: Bool) -> Bool {
// ...
}
}
Override('CrouchDecisions', 'OnControllingDeviceChange', function(self, value, wrappedMethod)
-- execute & retrieve original code
local result = wrappedMethod(value)
-- check our condition
if condition then
return result -- return original code
else
return false -- return false
end
end)
Do not allow the player to crouch:
-- onInit
registerForEvent('onInit', function()
-- observe CrouchDecisions EnterCondition state
Override('CrouchDecisions', 'EnterCondition', function(self, stateContext, scriptInterface, wrappedMethod)
return false
end)
end)
Do not allow the player to crouch if in ADS:
-- set initial vars
isADS = false
-- onInit
registerForEvent('onInit', function()
-- observe ADS OnEnter state
Observe('AimingStateEvents', 'OnEnter', function(self, stateContext, scriptInterface)
isADS = true
end)
-- observe ADS OnExit state
Observe('AimingStateEvents', 'OnExit', function(self, stateContext, scriptInterface)
isADS = false -- reset condition
end)
-- observe CrouchDecisions EnterCondition state
Override('CrouchDecisions', 'EnterCondition', function(self, stateContext, scriptInterface, wrappedMethod)
-- retrieve original code result
local result = wrappedMethod(stateContext, scriptInterface)
-- check ADS state
if isADS then
return false -- disallow crouch
end
-- otherwise return original logic
return result
end)
end)
Description of all the configuration variables available
The config file is generated automatically and manual edits are not recommended. To modify configurations, use the in the CET overlay.
For adjustments to fonts and other stuffs not available in the CET overlay, manual editing may be considered.
The config file can be found in: /config.json
The config file is structured with two JSON layers, we use the format <first level>.<second level>
to represent the key.
Consider the example JSON snippet below, the key for the base size of the font is noted as font.base_size
, which carries a value of 16.0
.
These are the 0old Cyber Engine Tweaks config file patches for reference.
A guide on how to change the font and font size for Cyber Engine Tweaks and mods based on Cyber Engine Tweaks. And how to display non-English characters.
How to teleport V
When teleporting to otherwise inaccessible areas, you may not be able to exit these locations naturally. It is recommended that you log your location before teleporting so that you can teleport back after exploring said areas.
Cyber Engine Tweaks allow mods to register hotkeys that can be assigned by the player in the , and execute custom code once pressed. There are two kinds of hotkeys:
are triggered on key release
are triggered on key press and release
Hotkeys and Inputs, while having similar functionalities, are displayed in two separated groups.
are not triggered while the player stay pressed on a game's keybind.
For example, if the player move forward with "W" and press a Hotkey at the same time, it won't be triggered.
For this reason, it is recommended to use instead, as they are always triggered.
Prints one or multiple objects into CET Console
Prints one or multiple objects into Individual Mod Logs file
Handle table
with indentation
Handle boolean
, nil
, string
and other classic types
Parse userdata
via GameDump()
or Dump()
if possible
Parse CName
via NameToString()
if possible
Prints to CET Console, similar to print() function.
debug.print(obj: object [, obj2, ...]) -> nil
Write in Individual Mod Logs file. eg, <
>/mods/my_mod/my_mod.log
. Similar to spdlog.
debug.log(obj: object [, obj2, ...]) -> nil
local myTable = {
var = true,
enabled = false,
subTable = {
player = Game.GetPlayer(),
stats = Game.GetStatsSystem()
}
}
debug.log('myTable:', myTable)
[13:49:03] myTable: {
["var"] = true,
["enabled"] = false,
["subTable"] = {
["player"] = PlayerPuppet[ customCameraTarget:ECCTV_OnlyOnscreen, ...]
["stats"] = gameStatsSystem[ ],
},
}
debug = {
-- log
log = function(...)
spdlog.info(debug.parse(...))
end,
-- print
print = function(...)
print(debug.parse(...))
end,
-- parse arguments
parse = function(...)
local args = {}
for _, v in pairs{debug.null(...)} do
table.insert(args, debug.parseDeep(v))
end
local output = ""
for i, value in pairs(args) do
output = output .. tostring(value)
if i ~= #args then
output = output .. " "
end
end
return output
end,
-- parse deep table
parseDeep = function(t, max, depth)
-- convert nil to 'nil'
t = debug.null(t)
if type(t) ~= 'table' then
if type(t) == 'userdata' then
t = debug.parseUserData(t)
end
return t
end
max = max or 63
depth = depth or 4
local dumpStr = '{\n'
local indent = string.rep(' ', depth)
for k, v in pairs(t) do
-- vars
local ktype = type(k)
local vtype = type(v)
local vstr = ''
-- key
local kstr = ''
if ktype == 'string' then
kstr = string.format('[%q] = ', k)
end
-- string
if vtype == 'string' then
vstr = string.format('%q', v)
-- table
elseif vtype == 'table' then
if depth < max then
vstr = debug.parseDeep(v, max, depth + 4)
end
-- userdata
elseif vtype == 'userdata' then
vstr = debug.parseUserData(v)
-- thread (do nothing)
elseif vtype == 'thread' then
-- else
else
vstr = tostring(v)
end
-- format dump
if vstr ~= '' then
dumpStr = string.format('%s%s%s%s,\n', dumpStr, indent, kstr, vstr)
end
end
-- unindent
local unIndent = indent:sub(1, -5)
-- return
return string.format('%s%s}', dumpStr, unIndent)
end,
-- parse userdata
parseUserData = function(t)
local tstr = tostring(t)
if tstr:find('^ToCName{') then
tstr = NameToString(t)
elseif tstr:find('^userdata:') or tstr:find('^sol%.') then
local gdump = false
local ddump = false
pcall(function() gdump = GameDump(t) end)
pcall(function() ddump = Dump(t, true) end)
if gdump then
tstr = GameDump(t)
elseif ddump then
tstr = ddump
end
end
return tstr
end,
-- convert nil into 'nil'
null = function(...)
local t, n = {...}, select('#', ...)
for k = 1, n do
local v = t[k]
if v == debug.null then t[k] = 'nil'
elseif v == nil then t[k] = debug.null
end
end
return (table.unpack or unpack)(t, 1, n)
end,
}
{
// other lines
"font": {
"base_size": 16.0,
"language": "Default",
"oversample_horizontal": 3,
"oversample_vertical": 1,
"path": ""
},
// other lines
}
developer.dump_game_options
false
If true
, CET will dump all options and their default values in the log file.
developer.enable_imgui_assertions
false
WIP
developer.max_lines_console_history
number
1000
CET stores a history of console commands, allowing users to easily access previous commands by pressing the up arrow key. This setting determines the number of commands that CET retains in its history.
developer.persistent_console
true
When true
, CET saves your console command history on your disk, so you can access it in future. When false
, the history is only kept in memory while the game is running and disappears once you exit the game.
developer.remove_dead_bindings
true
WIP
font.base_size
number
18.0
Set the font size of the overlay and mods.
font.language
string
"Default"
Check here for details.
font.oversample_horizontal
number
3
WIP
font.oversample_vertical
number
1
WIP
font.path
string
""
(empty)
Check here for details.
patches.disable_antialiasing
false
Setting this to true will disable antialiasing (TAA), which can increase performance on older hardware but looks horrible.
patches.disable_async_compute
false
Setting this to true will disable async compute, which can increase performance on older hardware.
patches.disable_boundary_teleport
false
Remove the teleport caused by going beyond the boundary in the game.
patches.disable_vignette
false
Disable the vignette effect while crouching.
patches.disable_win7_vsync
false
Disable V-sync for Windows7
disable_antialiasing
bool
false
Setting this to true will disable antialiasing (TAA), which can increase performance on older hardware but looks horrible.
disable_async_compute
bool
false
Setting this to true will disable async compute, which can increase performance on older hardware.
disable_boundary_teleport
bool
false
Remove the teleport caused by going beyond the boundary in the game.
disable_intro_movies
bool
true
Disable the first movies that play at each launch.
disable_vignette
bool
false
Disable the vignette effect while crouching.
disable_win7_vsync
bool
false
Disable V-sync for Windows7
dump_game_options
bool
false
Will dump all options and their default values in the log file.
enable_debug
bool
false
Unlocks the debug menu, use at your own risk!
font_glyph_ranges
string
Check here for details.
font_path
string
Check here for details.
font_size
integer
13
Set the font size of the overlay and mods.
overlay_key
integer
Key to open the console.
remove_pedestrians
bool
false
Removes most of the pedestrians and traffic from the game. Be careful using this, a save made with this activated will NEVER have pedestrians/traffic anymore!
skip_start_menu
bool
true
Skips the menu asking you to press the space bar to continue (Breaching...)
patches.disable_intro_movies
false
Disable the first movies that play at each launch.
patches.skip_start_menu
false
Skips the menu asking you to press the space bar to continue (Breaching...)
patches.minimap_flicke
false
f you're experiencing flicker with the minimap in the top-right corner of the game, set true
. Only change this setting if you are indeed noticing the flicker issue.
print(Game.GetPlayer():GetWorldPosition())
// example showing necessary parameters as <?>
Game.GetTeleportationFacility():Teleport(GetPlayer(), ToVector4{x=<?>, y=<?>, z=<?>, w=1}, ToEulerAngles{roll=0, pitch=0, yaw=45})
// example usage with real coords
Game.GetTeleportationFacility():Teleport(GetPlayer(), ToVector4{x=-1442.981689, y=139.817459, z=141.996506, w=1}, ToEulerAngles{roll=0, pitch=0, yaw=45})
Location
Coordinates
Akulov penthouse
-1218.135986, 1409.635010, 113.524445
Peralezes Apt
-75.815399, -113.607819, 111.161728
Gutierrez Apt
20.760391, 5.750076, 138.900955
Time Machine Guitar room
-1843.676392, -575.336243, 7.754036
Denny's Estate Inside
486.977325, 1291.791016, 234.458664
Hanako Estate bedroom
290.197662, 1022.468079, 229.920425
Voodoo Boys Subway
-1662.758545, -1867.002563, 54.982040
Slayton Apt
-1450.692139, -1038.510742, 77.555298
NCPD Conference room
-1761.547729, -1010.821655, 94.300003
JigJig St. hotel room
-664.977112, 847.753113, 28.499626
Dark Matter hotel room
-372.268982, 271.240143, 215.515579
Megatower H8 penthouse
-701.484680, 849.270264, 322.252228
Mr. Hands (Heavy Hearts)
-1577.5237, -2336.9883, 57.89293
Outside the city (Nomad starting)
-3235.881592, -6146.751465, 96.834175
V's Mansion
-1341.383545, 1242.970337, 111.100006
Grand Imperial Plaza Mall
-2278.209473, -1992.328613, 20.570023
Unfinished Casino
934.451233, 1458.630981, 242.120010
VB Data Fortress
-1661.448242, -1869.755859, 54.992889
River BD School
-6491.909180, -3167.271240, 57.558006
Monorail Tunnel
-1663.618774, -1867.443726, 54.990150
Konpeki Tower
-2229.413818, 1769.449707, 21.000000
Konpeki Tower Penthouse
-2220.772705, 1765.388916, 308.000000
Konpeki Tower - V Suite
-2202.186035, 1783.184204, 163.000000
Clouds
-625.404236, 794.564392, 132.252228
Embers
-1795.816162, -526.988647, 74.241196
Atlantis
-753.868530, 1107.427612, 61.000000
Badlands Tunnel
-1255.622437, 126.991272, -43.753677
Badlands Tunnel Entrance
185.345749, 2365.449707, 67.081177
Petrochem Off-limits area
-118.046112, -486.535583, 7.296860
Arasaka Tower Abandoned area
-1475.830200, 161.548401, 208.637604
Arasaka Tower Jungle
-1449.256470, 118.300171, 321.639038
Arasaka Tower Upper Atrium
-1390.497559, 162.406921, 388.347961
Arasaka Tower CEO level
-1437.788208, 157.247620, 565.346008
Arasaka Tower CEO office
-1447.286621, 73.579651, 568.946045
Arasaka Tower Unlisted levels
-1428.207520, 95.437912, 543.348022
Arasaka Tower Unlisted temple
-1383.655518, 118.832474, 542.696289
Arasaka Tower Counter-intel
-1442.981689, 139.817459, 141.996506
Arasaka Tower Underground
-1376.191528, 143.706009, -26.648010
Arasaka Tower Boring Machine
-1447.010010, 40.182648, -36.814171
Arasaka Mikoshi Mainframe
-1448.108398, 149.156219, -27.652016
Arasaka Orbital Station
4743.650879, -1091.755127, 1310.439575
Johnny interrogation room
-1389.446533, 141.266556, -139.361572
Helper functions which let you construct internal data types.
string
) -> GameType
Create a new object of the type given by typeName
. This type should be an internal game type.
table<x: float, y: float, z: float>
) -> Vector3
Create a new Vector3
from a table of x
, y
, and z
floats.
table<x: float, y: float, z: float, w: float>
) -> Vector4
Create a new Vector4
from a table of x
, y
, z
, and w
floats.
You may notice that functions like PlayerPuppet:GetWorldPosition
will return a Vector4
, even though, intuitively, 3D coordinates only include x, y, and z values. These Vector4
values are known as in graphical programming, useful in matrix transformations.
All you really need to know is that the 4th value, w
, should always be 1 when you are dealing with 3D coordinates.
table<roll: float, pitch: float, yaw: float>
) -> EulerAngles
Create a new EulerAngles
from a table of roll
, pitch
, and yaw
. Angle values are in degrees.
table<i: float, j: float, k: float, r: float>
) -> Quaternion
Create a new Quaternion
from a table of a vector part i
, j
, k
, and a scalar (real) part r
.
table<hash_hi: uint32, hash_lo: uint32>
) -> CName
Create a new CName
from a table of hash_hi
and hash_lo
.
table<hash: uint32, length: uint8>
) -> TweakDBID
Create a new TweakDBID
from a table of hash
and length
.
table<id: TweakDBID, rng_seed: uint32, unknown: uint16, maybe_type: uint8>
) -> ItemID
Create an ItemID
from a table of id
, rng_seed
, an unknown field unknown
, and an unknown field maybe_type
.
obj = NewObject("gameObject")
puppet = NewObject("PlayerPuppet")
vec2 = NewObject("Vector2")
vec3 = NewObject("Vector3")
matrix = NewObject("Matrix")
world_transform = NewObject("WorldTransform")
position = ToVector3{x=-1788, y=-450, z=7.75}
Property
Description
x: float
The x-axis component of the vector
y: float
The y-axis component of the vector
z: float
The z-axis component of the vector
position = ToVector4{x=-1788, y=-450, z=7.75, w=1}
Property
Description
x: float
The x-axis component of the vector
y: float
The y-axis component of the vector
z: float
The z-axis component of the vector
w: float
The w-axis component of the vector
rotation = ToEulerAngles{roll=0, pitch=0, yaw=45}
player = Game.GetPlayer()
Game.GetTeleportationFacility():Teleport(player, player:GetWorldPosition(), rotation)
Property
Description
yaw: float
The horizontal angle in degrees along the axis pointing up.
0 is north, 90 is west, 180 and -180 are south, and -90 is east.
pitch: float
The vertical angle in degrees along the axis pointing to the right.
-90 is down, 0 is straight ahead, and 90 is up.
roll: float
The roll angle in degrees along the axis pointing forward.
Positive values rotate anti-clockwise and negative values rotate clockwise.
-- 45 degree rotation about the Z axis (yaw)
rotation = ToQuaternion{i=0, j=0, k=0.383, r=0.924}
Property
Description
i: float
The x-axis component of the quaternion
j: float
The y-axis component of the quaternion
k: float
The z-axis component of the quaternion
r: float
The scalar (real) part of the quaternion
cname = ToCName{hash_hi=0x01234567, hash_lo=0x89abcdef}
Property
Description
hash_hi: uint32
The higher 32 bits of the CName
's 64-bit hash
hash_lo: uint32
The lower 32 bits of the CName
's 64-bit hash
value: string
The text value of the CName
id = ToTweakDB{hash=0x01234567, length=12}
Property
Description
hash: uint32
The CRC32 hash of the item name
length: uint8
The length of the item name
dbid = ToTweakDB{hash=0x01234567, length=12}
itemid = ToItemID{id=dbid, rng_seed=0x01234567}
Property
Description
id: TweakDBID
The TweakDBID referenced by this ItemID
tdbid: TweakDBID
Alias for id
rng_seed: uint32
The RNG (random number generation) seed used for loot generation
unknown: uint16
Unknown field
maybe_type: uint8
Unknown field, possibly a type
A custom hooks system for your LUA script
doAction(name: string, context: any [, context2 ...]) -> nil
addAction(name: string, callback: function, opt priority: integer) -> nil
registerForEvent('oninit', function()
-- execute all addAction() using this name
doAction('my_action', 'init', 'something')
end)
addAction('my_action', function(context, context2)
-- context = 'init'
-- context2 = 'something'
-- do something
end, 10)
addAction('my_action', function(context, context2)
-- context = 'init'
-- context2 = 'something'
-- do something before (note the priority: 5)
end, 5)
applyFilters(name: string, value: any, context: any [, context2 ...]) -> nil
addFilter(name: string, callback: function, opt priority: integer) -> nil
registerForEvent('oninit', function()
local myVar = 'yes'
myVar = applyFilters('my_filter', myVar, 'init', 'something')
print(myVar)
-- no
end)
addFilter('my_filter', function(myVar, context, context2)
-- myVar = 'maybe' (set by the other filter with priortiy: 5)
-- context = 'init'
-- context2 = 'something'
-- update myVar to 'no'
return 'no'
end, 10)
addFilter('my_filter', function(myVar, context, context2)
-- myVar = 'yes' (set by initial applyFilters())
-- context = 'init'
-- context2 = 'something'
-- update myVar to 'maybe' (note the priority: 5)
return 'maybe'
end, 5)
local this
this = {
storage = {
actions = {},
filters = {}
},
addAction = function(action, callback, priority, context)
if type(action) == 'string' and type(callback) == 'function' then
priority = tonumber(priority or 10)
this._addHook('actions', action, callback, priority, context)
end
return this
end,
doAction = function(...)
local args = { ... }
local action = table.remove(args, 1)
if type(action) == 'string' then
this._runHook('actions', action, args)
end
return this
end,
removeAction = function(action, callback)
if type(action) == 'string' then
this._removeHook('actions', action, callback)
end
return this
end,
addFilter = function(filter, callback, priority, context)
if type(filter) == 'string' and type(callback) == 'function' then
priority = tonumber(priority or 10)
this._addHook('filters', filter, callback, priority, context)
end
return this
end,
applyFilters = function(...)
local args = { ... }
local filter = table.remove(args, 1)
if type(filter) == 'string' then
return this._runHook('filters', filter, args)
end
return this
end,
removeFilter = function(filter, callback)
if type(filter) == 'string' then
this._removeHook('filters', filter, callback)
end
return this
end,
_removeHook = function(type, hook, callback, context)
if not this.storage[type][hook] then
return
end
if not callback then
this.storage[type][hook] = {}
else
local handlers = this.storage[type][hook]
--local i
if not context then
for i = #handlers, 1, -1 do
if handlers[i].callback == callback then
table.remove(handlers, i)
end
end
else
for i = #handlers, 1, -1 do
local handler = handlers[i]
if handler.callback == callback and handler.context == context then
table.remove(handlers, i)
end
end
end
end
end,
_addHook = function(type, hook, callback, priority, context)
local hookObject = {
callback = callback,
priority = priority,
context = context
}
local hooks = this.storage[type][hook]
if hooks then
table.insert(hooks, hookObject)
hooks = this._hookInsertSort(hooks)
else
hooks = { hookObject }
end
this.storage[type][hook] = hooks
end,
_hookInsertSort = function(hooks)
local tmpHook, j, prevHook
for i = 2, #hooks do
tmpHook = hooks[i]
j = i
prevHook = hooks[j - 1]
while prevHook and prevHook.priority > tmpHook.priority do
hooks[j] = hooks[j - 1]
j = j - 1
prevHook = hooks[j - 1]
end
hooks[j] = tmpHook
end
return hooks
end,
_runHook = function(type, hook, args)
local handlers = this.storage[type][hook]
if not handlers then
return type == 'filters' and args[1] or false
end
local i = 1
local len = #handlers
if type == 'filters' then
for i = 1, len do
args[1] = handlers[i].callback(unpack(args))
end
else
for i = 1, len do
handlers[i].callback(unpack(args))
end
end
return type == 'filters' and args[1] or true
end
}
-- history storage
local actionHistory = {}
addAction = function(...)
this.addAction(...)
return this
end
removeAction = function(...)
this.removeAction(...)
return this
end
doAction = function(...)
local args = {...}
local action = args[1]
actionHistory[action] = 1
this.doAction(...)
actionHistory[action] = 0
return this
end
doingAction = function(action)
return actionHistory[action] == 1
end
didAction = function(action)
return actionHistory[action] ~= nil
end
currentAction = function()
for k in pairs(actionHistory) do
if actionHistory[k] then
return k
end
end
return false
end
addFilter = function(...)
this.addFilter(...)
return this
end
removeFilter = function(...)
this.removeFilter(...)
return this
end
applyFilters = function(...)
return this.applyFilters(...)
end
If Cyber Engine Tweaks isn't working, here's what you can do
Before trying anything else, please make sure that your Windows and your graphics driver are up-to-date – this actually helps in around 50% of all cases. If you need instructions, you can find them in the red box under the general troubleshooting guide's section.
Go to your, since all paths given in this guide will be relative to that folder.
TL;DR:
CET is installed to bin\x64\plugins\cyber_engine_tweaks
The log file is bin\x64\plugins\cyber_engine_tweaks\cyber_engine_tweaks.log
Your keybind is stored in bin\x64\plugins\cyber_engine_tweaks\bindings.json
For that reason, the Redmodding Discord can only support you the most recent version.
If there's a mismatch, then you can find a warning in the following file:
If you don't have that file, proceed to Step 2.
Go to your and check the following subfolder:
It should have at least the following files:
If you do not have these folders, then CET isn't installed correctly. Head to and start with step 3.
If everything looks okay in step 1, check if you have a file cyber_engine_tweaks.log
inside the cyber_engine_tweaks
folder from the previous screenshot.
No log file means that CET isn't starting up.
The easiest fix is to .
If you did that and it didn't help, check your antivirus. Most likely, it is protecting you from scary things like Cyber Engine Tweaks. Disable it.
If that didn't help either, update everything - Windows, your drivers, etc. You can find links and instructions in the red box on the .
If you do have it, you can open it and try to make sense of the error message. You can find more information on that , or find help in the #cet-suppport channel on our .
If you are using Vortex, turn it off (it will get angry otherwise)
Complete either of the following steps inside your :
rename bin/x6/plugins
to plugins_
or
delete the file bin/x6/plugins/cyber_engine_tweaks.asi
move the folder bin/x6/plugins/cyber_engine_tweaks
out of plugins
(
e.g. your Desktop). Do not delete it yet.
Download the most recent CET from
Extract the downloaded archive to your (the bin folder will merge with the one that is already there)
Start the game: CET should now ask you to bind a key. If it doesn't:
(double-check if you're sure)
Head to the and follow the steps in the red box at the top
Optional: Restore your mods and/or settings Now that CET works, find the old directory from step 2. Move the following files/folders:
Mods: the directory mods
from the old cyber_engine_tweaks
folder
Settings: The .json files directly inside cyber_engine_tweaks
Optional: delete the old folder (or leave it, it's your diskspace)
That's it! You're good to go!
Find the file \bin\x64\plugins\cyber_engine_tweaks\bindings.json
and delete it.
Start the game.
You should now see a popup asking you to bind a key. Otherwise, read on.
Click into the "Binding..." field.
If that doesn't help, make sure to turn off all your overlays (Discord, Steam, Microsoft Game Bar, Random Useless Bloatware) or start Cyberpunk in the Windowed Borderless mode.
If you're experiencing performance issues with CET, first make sure to as per the instructions to rule out side effects from old files.
If that doesn't make the problems go away, make sure to update Windows and your graphics driver as per the red box under Troubleshooting's section.
If that doesn't help, you can find us in #cet-support
on the . Please note that people there will first suggest everything else from this guide, so you might as well get it out of the way first.
Make sure to update Windows and your graphics driver as per the red box under Troubleshooting's section, as this is the only chance you have of getting rid of your crashes.
This is a known issue with AMD cards, and it's not something the CET developers can fix. Everyone who could fix it is informed, but such things take time. If you're on an AMD card, you can try running your game in Windowed Borderless mode. Otherwise, you just have to live with the crashes.
Please ensure the following:
If you have modified your game's .exe file yourself, the mod will not work - to undo this, delete the file and verify your game through GOG, Steam or Epic to download a fresh copy of the newest exe.
Update your game. You are using a patch that is not supported. Only current versions are supported. (current version is 1.6)
You used the old version of the mod and still have the old files installed. Delete bin/x64/plugins/ and reinstall the mod.
Do a repair on your installation with no other mod files installed (redscript, cet, et cetera) then install CET as you normally would and check the log file. If it still isn't working, uninstall cyberpunk 2077 and do a complete wipe of the install folder.
bin/x64/plugins/cyber_engine_tweaks/
Install - if nothing happens, please make sure all of your overlays and monitoring programs are disabled. There is a list of known programs that cause issues below. If you find one not on the list, contact Arsenic_Touch via the discord.
bin/x64/plugins/cyber_engine_tweaks/
Cyber Engine Tweaks no longer comes with a config file, one is generated when you first launch the mod.
When first launching cyber engine tweaks, it will prompt you to pick a keybind. Tilde is no longer the default. Keyboard input is blocked until a keybind is chosen. If you do not see the pop up, look below for further troubleshooting steps.
Certain programs conflict with the hook that Cyber Engine Tweaks use to display the console - to avoid this, disable any software with overlays that may impact the mod (some examples below). This includes crashing the game or not launching at all. OBS graphics-hook32.dll and graphics-hook64.dll can also interfere. Do a search of your windows files to see if you have a program running them. For rivatuner, you can also open the Global file with notepad++, which is located in RivaTuner Statistics Server\ProfileTemplates and add cyber_engine_tweaks.asi to the end of the decimal in the line: InjectionDelayTriggers
Open Windows Security
Go to Apps & Browser control on the left tabs
Click Exploit protection settings Under the Exploit Protection section
In the Systems Settings tab locate Randomize memory allocations (Button-up ASLR) and High-entropy ASLR and change their settings to Use default (On).
Restart your computer.
Credit to discord user @rachmanov1c @OrBy
Please verify that:
Your version of Cyberpunk is compatible with the newest release on GitHub or Nexus (this can be seen in the menu, underneath "Quit Game")
You have the most recent version of the mod from or
The has been followed correctly
You have verified that the log found in bin/x64/plugins/cyber_engine_tweaks/cyber_engine_tweaks.log
is 'last modified' at the same time and date as your most recent attempt to launch the game
Remove all previous mods in the mods folder and try to launch the game. If it works, add your mods back one by one until you find the one that breaks CET. Some mods can break CET completely or change the overlay colour if they are not up to date.
Make sure cyber engine tweaks and your mods are installed with hardlink If hardlink doesn't show up under vortex, make sure the Vortex Mods folder is on the same partition as the game mods folder. This explains it .
Make sure you're using the most up to date vortex plugin for cyberpunk 2077. Check the channel on the discord for the latest information.
There are six possible solutions to this:
Ensure your game is in - Cyberpunk will usually make itself the topmost application on your screen if in fullscreen.
Ensure you don't have any or monitoring software running such as Rivatuner, Fraps, etc.
Make sure your game is up to date and you are using the latest version of Cyber Engine Tweaks and that you installed it properly. (do a clean install by deleting the plugins folder, LICENSE, global.ini, version.dll files if you are running into a problem)
Run the game as administrator. (this works for some people where other methods have failed)
Check to make sure your antivirus software is not removing or blocking cyber engine tweaks.
Do a complete reinstall of cyberpunk 2077.
Delete the bindings.json file located inbin/x64/plugins/cyber_engine_tweaks/
and start your game, the overlay will to choose a new keybind should come up. If it doesn't, check the known list of programs that can interfere.
If you see in your log that your game is freezing on CRender and you have problem like this you should try steps provided below.
1. If you are using mod manager disable CET temporarily, if you are not using mod manager then follow guide. 2. Launch the game without CET and disable FSR/DLSS 3. Install/Enable Cyber Engine Tweaks and launch the game, the problem should be gone.
Please ensure you attempt deleting and reinstalling the mod before submitting an issue! Alternatively, . Additionally, please check to ensure your issue has not already been reported!
If you cannot find your problem listed, with the following:
Your configuration file
Your Cyber Engine Tweaks version and your Cyberpunk 2077 version
Your log file
Your OS version
A description of your issue
Your
C:\GOG Games\Cyberpunk 2077\
Path from the guide
bin\x64\plugins\cyber_engine_tweaks
Folder you go to
C:\GOG Games\Cyberpunk 2077\bin\x64\plugins\cyber_engine_tweaks
bin\x64\plugins\cyber_engine_tweaks\cyber_engine_tweaks.log
bin\x64\plugins
Key notes on upgrading existing mod for the CET 1.14+:
Don't use Game
object and any game types before onInit
.
Don't use GetMod()
before onInit
.
Don't pass excessive params to game functions. If the max number of params is 3, then passing 4 params will result in an error.
Update Observe()
/ Override()
handlers so they accept self
as the first param. Previously, some observers lacked the self
param:
Observe("PlayerPuppet", "OnAction", function(self, action)
-- First param self is required now
end)
If your mod stores player reference then unset it when game session ends.
An unreleased reference can lead to bugs such as disappearing footsteps.
Accessing Game.GetPlayer()
in onUpdate
will not cause the issue.
This can be achieved using next snippet or with GameSession
/ GameUI
libs:
Observe("QuestTrackerGameController", "OnUninitialize", function()
if Game.GetPlayer() == nil then
Mod.Player = nil -- var containing a reference to the player
end
end)
Use corresponding getters for game systems instead of GetSingleton()
. For example, Game.GetTeleportationFacility()
instead of GetSingleton('gameTeleportationFacility')
.
New scripting capabilities were introduced in CET 1.14 to make it more convenient and simple:
All game classes are directly accessible by name. For example, entEntityId
, PlayerPuppet
.
All game enums are directly accessible by name. For example, gamedataStatType.BaseDamage
, gameGameVersion.Current
.
Classes can also be accessed by their aliases from redscript. For example, WeaponObject
instead of gameweaponObject
.
Classes have the conventional .new()
constructor. For example, MappinData.new()
.
A constructor can take an array of properties to create and initialize an object in a single statement. For example, EntityID.new({ hash = 12345 })
.
Static methods are accessible from classes using the dot. For example, ScriptedPuppet.IsDefeated(npc)
.
A static method can be called from an instance if the first parameter is of the same type. For example, vec4:Length()
instead of Vector4.Length(vec4)
.
The overloaded function is resolved based on passed parameters when called by its short name. For example, StatusEffectHelper.HasStatusEffect(target, gamedataStatusEffectType.Overheat)
.
Partial Variant
type support. ToVariant()
and FromVariant()
are only applicable to classes.
All new scripting features are optional. You're not required to rewrite an existing mod using these features.
The new API allows code that is also valid redscript or very close to its redscript counterpart. Thus, it simplifies the research and the transition to / from the redscript.
For example, this line looks exactly the same in Lua and in the redscript:
RPGManager.CreateStatModifier(gamedataStatType.BaseDamage, gameStatModifierType.Additive, 50)
Constructing objects
Old API:
local mappinData = NewObject("gamemappinsMappinData")
mappinData.mappinType = TweakDBID.new("Mappins.DefaultStaticMappin")
mappinData.variant = Enum.new("gamedataMappinVariant", "FastTravelVariant")
mappinData.visibleThroughWalls = true
New API:
local mappinData = MappinData.new()
mappinData.mappinType = "Mappins.DefaultStaticMappin"
mappinData.variant = gamedataMappinVariant.FastTravelVariant
mappinData.visibleThroughWalls = true
Constructor with initialization
Old API:
function getStash()
local stashId = NewObject("entEntityID")
stashId.hash = 16570246047455160070ULL
return Game.FindEntityByID(stashId)
end
New API:
function getStash()
return Game.FindEntityByID(EntityID.new({ hash = 16570246047455160070ULL }))
end
Scripted static call
Old API:
Game["PreventionSystem::ShowMessage;GameInstanceStringFloat"]("Message", 5.0)
New API:
PreventionSystem.ShowMessage("Message", 5.0)
Shorthand static call
Old API:
Observe("PlayerPuppet", "OnAction", function(action)
-- Option 1 --
print(GetSingleton("gameinputScriptListenerAction"):GetName(action))
-- Option 2 --
print(action:GetName(action))
end)
New API:
Observe("PlayerPuppet", "OnAction", function(action)
-- Option 1 --
print(ListenerAction.GetName(action))
-- Option 2 --
print(action:GetName())
end)
Working with enums
Old API:
print(Enum.new('gameGameVersion', 'Current') == Enum.new('gameGameVersion', 'CP77_GoldMaster'))
print(Enum.new('gameGameVersion', 'Current') == Enum.new('gameGameVersion', 'CP77_Patch_1_2_Hotfix2'))
New API:
print(gameGameVersion.Current == gameGameVersion.CP77_GoldMaster)
print(gameGameVersion.Current == gameGameVersion.CP77_Patch_1_2_Hotfix2)
Variants
local message = SimpleScreenMessage.new()
message.message = "Test"
message.isShown = true
local blackboardDefs = Game.GetAllBlackboardDefs()
local blackboardUI = Game.GetBlackboardSystem():Get(blackboardDefs.UI_Notifications)
blackboardUI:SetVariant(
blackboardDefs.UI_Notifications.OnscreenMessage,
ToVariant(message),
true
)
Game.TeleportPlayerToPosition(-2382.430176, -610.183594, 12.673874)
Observers are builtin CET functions that allow developers to detect when a function/method is executed by the game. They must be registered inside the onInit event, in the init.lua
file.
There are two kinds observers:
ObserveBefore()
which is triggered right at the moment the function/method is called
ObserveAfter()
which is triggered once the game finished to execute the function/method
The provided callback
is always filled as follow:
The first argument is the current object that execute the function/method. This argument is not present when function/method is static.
Others arguments passed to the targeted function/method, if any.
Observe(className, method, callback) -- alias of ObserveBefore()
ObserveBefore(className, method, callback)
ObserveAfter(className, method, callback)
--
-- ObserveBefore()
--
-- @param string className The parent class name
-- @param string method The method name to target
-- @param function callback The callback function
--
ObserveBefore('className', 'method', function(self [, arg1, arg2, ...])
-- method() has just been called
end)
--
-- ObserveAfter()
--
-- @param string className The parent class name
-- @param string method The method name to target
-- @param function callback The callback function
--
ObserveAfter('className', 'method', function(self [, arg1, arg2, ...])
-- method() has been called and fully executed
end)
Here is a visual representation showing where observers are executed in the game's script:
public class AimingStateEvents extends UpperBodyEventsTransition {
protected func OnEnter(stateContext: ref<StateContext>, scriptInterface: ref<StateGameScriptInterface>) -> Void {
// ObserveBefore('AimingStateEvents', 'OnEnter')
let aimingCost: Float;
let focusEventUI: ref<FocusPerkTriggerd>;
let timeDilationFocusedPerk: Float;
let weaponType: gamedataItemType;
let player: ref<PlayerPuppet> = scriptInterface.executionOwner as PlayerPuppet;
// ...
// ObserveAfter('AimingStateEvents', 'OnEnter')
}
}
In this representation, the observer callback would be filled with the following arguments:
Observe('AimingStateEvents', 'OnEnter', function(self, stateContext, scriptInterface)
-- self the 'AimingStateEvents' class
-- stateContext the original method argument
-- scriptInterface the original method argument
end)
In the code above, the observer listens to AimingStateEvents:OnEnter()
, which is triggered everytime the player enters in ADS state.
Additionally, the OnEnter()
method is responsible to apply different effects, like the camera zoom, the initial stamina drain etc... Following this logic, this means:
Using ObserveBefore()
allows to hook in before these effects are applied
Using ObserveAfter()
guarantees the method finished to apply all the effects
-- onInit
registerForEvent('onInit', function()
-- observe crouch OnEnter state
Observe('CrouchEvents', 'OnEnter', function(self, stateContext, scriptInterface)
Game.AddToInventory('Items.money', 1000)
end)
end)
-- onInit
registerForEvent('onInit', function()
-- observe crouch OnUpdate state
-- this is triggered continuously, as long as the player is crouched
Observe('CrouchEvents', 'OnUpdate', function(self, timeDelta, stateContext, scriptInterface)
Game.AddToInventory('Items.money', 20)
end)
end)
-- init.lua
-- onInit
registerForEvent('onInit', function()
-- NativeDB will copy 'entAnimationControllerComponent::PushEvent;GameObjectCName'
-- We can ignore the left part to only keep:
Observe('AnimationControllerComponent', 'PushEvent;GameObjectCName', function(gameObject, eventName)
print('GameObject:', gameObject:GetClassName())
print('Event:', eventName)
end)
end)
-- set initial vars
isADS = false
isCrouch = false
-- decide to give money
-- this function can be defined outside of onInit
-- as it is only called within observers
shouldGiveMoney = function()
-- is in ADS and is crouched
if isADS and isCrouch then
Game.AddToInventory('Items.money', 1000)
end
end
-- onInit
registerForEvent('onInit', function()
-- observe ADS OnEnter state
Observe('AimingStateEvents', 'OnEnter', function(self, stateContext, scriptInterface)
isADS = true
shouldGiveMoney()
end)
-- observe ADS OnExit state
Observe('AimingStateEvents', 'OnExit', function(self, stateContext, scriptInterface)
isADS = false -- reset condition
end)
-- observe Crouch OnEnter state
Observe('CrouchEvents', 'OnEnter', function(self, stateContext, scriptInterface)
isCrouch = true
shouldGiveMoney()
end)
-- observe Crouch OnExit state
Observe('CrouchEvents', 'OnExit', function(self, stateContext, scriptInterface)
isCrouch = false -- reset condition
end)
end)
Descriptions of effects that can be passed into certain functions such as Game.ApplyEffectOnNPC(), any function that takes effect as a parameter.
Makes NPC T-Pose
Puts them in combat search state, but with a sleep status effect symbol over their head. Kinda weird
Works the same for both applying to NPC & Player
Unsure which is stronger this is BeserkPlayerBuff
Works the same for both applying to NPC & Player
Unsure which is stronger this is BeserkNPCBuff
Visual fx from healing
Only sound effect plays for both Player & NPC
Only sound effect plays for both Player & NPC
CyberwareMalfunctionLvl2
CyberwareMalfunctionLvl3
Freezes an NPC in place, Player gets a tiny visual effect
Gives visual effect only to Player
Effects NPCs weirdly, had me blow up in flames after hitting me...
Briefly puts weapon away then pulls it back out. Same for both NPCs and Player.
Works similar as encumbered, but puts target in combat state
Works as expected on NPCs
Slight visual/sound fx and combat state enabled for Player
Glitch/Hacked visual effect
Removes player drowning/out of oxygen
Works as expected when applied to NPC
Visual fx for Player no damage is done
Same effect as getting hit by a car but no damage
Sets NPCs into combat mode only, no actual visual knockdown occurs
Visual effect and some stat boost that I couldn't figure out maybe a health boost. After that a stun effect ocurs briefly.
Puts NPC into cyberpsychosis state
Visual/sound fx for Player
Instakills NPC, looks like both overload effect
Sound fx when applied to Player but no damage is done
Puts Player in combat mode.
Makes NPC commit suicide
PingLevel2
PingLevel3
PingLevel4
AntiVirusCooldown
BaseQuickHackDuration
BerserkCooldown
BikeKnockdown
BlockBodyDisposal
BlockBroken
BlockCoverVisibilityReduction
BlockGrapple
BossNoInterrupt
BossNoTakeDown
BossTakedownCooldown
BrainMeltCooldown
BreathingHeavy
BreathingLow
BreathingMedium
BreathingSick
CallInCooldown
CommsNoiseCooldown
CommsNoisePassiveEffect
ContagionCooldown
CrippledArmLeft
CrippledArmRight
CrippledHandLeft
CrippledHandRight
CrippledLegLeft
CrippledLegRight
Deaf
DistractionDuration
DodgeAirBuff
DodgeAirCooldown
DodgeBuff
DodgeCooldown
DoNotBlockShootingOnFriendlyFire
FollowerDefeated
GrenadeCooldown
InvulnerableAfterDefeated
JohnnySicknessHeavy
JohnnySicknessLow
JohnnySicknessMedium
JohnnySicknessMediumQuest
MadnessCooldown
MemoryWipeCooldown
MonowireGrapple
MuteAudioStims
NonInteractable
OverheatCooldown
OverloadCooldown
PainInhibitors
Parry
PlayerBleeding
PlayerBurning
PlayerElectrocuted
PlayerExhausted
PlayerMovementLocked
PlayerPoisoned
Poisoned
QuickHackCooldownDuration
QuickHackUploaded
RebootOpticsBossCooldown
RebootOpticsCooldown
ReduceNextHackCostBy1
ReduceUltimateHackCostBy2
RemoteBreachCooldown
SandevistanBuff
SandevistanCooldown
SandevistanPlayerBuff
StrongArmsChemicalActive
StrongArmsElecricActive
StrongArmsPhysicalActive
StrongArmsThermalActive
SuicideCooldown
SystemCollapseCooldown
TemporarilyBlockMovement
ThreeOrMoreProgramsCooldownRedPerk
ThreeOrMoreProgramsMemoryRegPerk1
ThreeOrMoreProgramsMemoryRegPerk2
Unconscious
UncontrolledMovement_Default
UncontrolledMovement_RagdollOffLedge
VehicleTrunkBodyPickup
WasQuickHacked
WaterCollision
WhistleCooldown
Wounded
Support for patch 1.6
TweakDB.CreateRecord
, TweakDB.CloneRecord
, and TweakDB.DeleteRecord
now support TweakDBID
as a parameter type
-- Get the sticky frag grenade record
sticky_grenade = TweakDB:GetRecord("Items.GrenadeFragSticky")
-- Create a clone of "Items.GrenadeFragSticky" with the name "Items.GrenadeFragStickyNew"
TweakDB:CloneRecord(sticky_grenade:GetID() + "New", sticky_grenade:GetID())
-- Get the new record
sticky_grenade_new = TweakDB:GetRecord("Items.GrenadeFragStickyNew")
-- or
sticky_grenade_new = TweakDB:GetRecord(sticky_grenade:GetID() + "New")
-- manipulate the new record as needed
-- delete record many different ways
TweakDB:DeleteRecord(sticky_grenade_new:GetID())
TweakDB:DeleteRecord(sticky_grenade:GetID() + "New")
TweakDB:DeleteRecord("Items.GrenadeFragStickyNew")
Updated usedhashes.kark
to the new URL
Moved to OpenResty Lua JIT
Support for patch 1.52.1 (made up name, they didn't bother to name it AGAIN)
Support for patch 1.52
Fix crash on Windows 7.
Fix LoadTexture overwriting the previously loaded texture.
Support for patch 1.5 hotfix 2
ImGui.LoadTexture(string path)
returns a texture loaded from an image on disk in the mods' directory.
ImGui.Image(texture)
displays a texture loaded with the LoadTexture
function.
Full Api is as follows, note that you may omit parameters after texture and default values will be used (Texture texture, ImVec2 size, ImVec2 uv0, ImVec2 uv1, ImVec4 tintColor, borderColor)
Added stb to load images.
Fixed bad screen skip fix.
Support for patch 1.5 v2
Changed the archive hashes to use the wolvenkit KARK files for TweakDB editing
red4ext updated to 1.3.1
Support for patch 1.5
red4ext updated to 1.3.0
mimalloc updated to 2.0.3
sqlite3 updated to 3.37.0+200
Special thanks to WopsS, psiberx and Sombra for their help with this patch!
exEntitySpawner does not work.
No pedestrian patch is not tested.
Fix cursor not getting locked to the game.
Fix skip start screen patch not working.
Support for patch 1.5
exEntitySpawner does not work.
No pedestrian patch is not tested.
Feel free to tell us any other issue that may arise on github/discord.
mimalloc updated to 2.0.3
sqlite3 updated to 3.37.0+200
Improve the d3d12 hook to fix a crash when using ReShade < 5.
Improve the d3d12 hook to avoid conflicts with other overlays.
Fixed random crashes when overridden method has a script reference parameter.
Fixed random crashes when working with game structs.
Fixed random crashes for very frequently called overrides (such as GameObject.OnGameAttached
and GameObject.OnDetach
on game reload).
Fixed random crashes for hotkeys and inputs (for example, binding the mouse wheel had a very high chance of causing crashes).
Fixed potential issues when CET mods that modify TweakDB are used with TweakDBext mods at the same time.
Added the ability to add new free flats to TweakDB.
Contains: 623, 625, 628 and 629
Fix bad allocation causing crashes when using some RTTI types
When using Override()
the handler receives the original function (or next in the override chain) as the last parameter
Override('PlayerPuppet', 'SetWarningMessage', function(_, message, wrappedMethod)
wrappedMethod('[Overridden] ' .. message)
end)
The wrapped function can be called multiple times
Override('PlayerPuppet', 'GetGunshotRange', function(_, originalRangeFunc)
return originalRangeFunc() + originalRangeFunc()
end)
If any of the overrides fail then the entire override chain also fails
Observers can be called before and after the target function using ObserveBefore()
and ObserveAfter()
ObserveBefore('PlayerPuppet', 'GetGunshotRange', function()
print('PlayerPuppet::GetGunshotRange::Before')
end)
ObserveAfter('PlayerPuppet', 'GetGunshotRange', function()
print('PlayerPuppet::GetGunshotRange::After')
end)
Observe()
is an alias for ObserveBefore()
All observers are guaranteed to be called even if overrides and other observers fail
It's possible to observe and override static class methods
Override('RPGManager', 'GetFloatItemQuality', function(value)
print('RPGManager.GetFloatItemQuality(' .. value .. ')')
return gamedataQuality.Legendary
end)
Full name should be used when overriding a scripted static method
Observe('PlayerPuppet', 'IsSwimming;PlayerPuppet', function(player)
print('PlayerPuppet.IsSwimming(player)')
end)
Use weak handle for self
in overrides. This should decrease the number of unreleased references and make CET less dependent on garbage collection.
Reuse overridden functions on mods reload. This fixes a potential issue with the native function registry, which has a hard limit on the number of registered functions.
Support out params in overrides.To properly handle out params in overrides:
Accept all params in the handler
Don't pass out params to the wrapped function
Return all results from the handler
registerForEvent('onInit', function()
Override('SpatialQueriesSystem', 'SyncRaycastByCollisionGroup', function(_, a1, a2, a3, a4, a5, a6, wrapped)
-- Skip a4 here because it's an out param
local success, result = wrapped(a1, a2, a3, a5, a6)
print(success, GameDump(result))
-- Return all results
return success, result
end)
end)
registerHotkey('SyncRaycastByCollisionGroup', 'SyncRaycastByCollisionGroup', function()
local from, forward = Game.GetTargetingSystem():GetCrosshairData(GetPlayer())
local to = Vector4.new(from.x + forward.x * 100, from.y + forward.y * 100, from.z + forward.z * 100, from.w)
local success, result = Game.GetSpatialQueriesSystem():SyncRaycastByCollisionGroup(from, to, 'Static', false, false)
print(success, GameDump(result))
end)
Added support for mods deployed as a symlink for the mod directory
Added Lua function to check if the mod archive is installed:
if not ModArchiveExists("my_mod.archive") then
print("This mod requires my_mod.archive to be installed")
end
Use FromVariant(variant)
to extract the value
Use ToVariant(value)
to pack the value when the type is unambiguous
ToVariant(Vector4.new(1, 2, 3))
ToVariant(gamedataQuality.Epic)
ToVariant("Nam libero tempore")
ToVariant(16570246047455160070ULL)
ToVariant(true)
ToVariant(TweakDBID("Items.money"))
ToVariant(CName("deflect"))
ToVariant(LocKey(12345))
ToVariant(CRUID(1337))
Use ToVariant(value, type)
when the type is ambiguous or you wanna force the type
ToVariant(1, "Int32") -- Same numeric value can be a signed integer
ToVariant(1, "Uint32") -- or unsigned integer
ToVariant(1, "Float") -- or float
ToVariant(nil, "handle:PlayerPuppet") -- Null requires a concrete type
The type of the array is resolved from the first element when possible
ToVariant({ "a", "b", "c" }) -- Auto resolved as "array:String"
ToVariant({ 1, 2, 3 }, "array:Uint32") -- Ambiguous type
Added event for early TweakDB changes:
registerForEvent('onTweak', function()
TweakDB:SetFlat('PreventionSystem.setup.totalEntitiesLimit', 20)
end)
Contains: 617, 618, 619 and 620
Fixed directx12 hooking not working when overlays were present.
Fixed symlink deployment with Vortex.
Drop second implementation of Scripting::ToRED()
which was very outdated. This was limiting what overridden functions can return.
Fix gamedataTDBIDHelper
helper that behaves the same as gamedataTweakDBInterface
.
Sanitize type names to be a valid Lua identifier. This makes namespaced types added by redscript accessible in Lua.
Force garbage collection for overridden functions of inkGameController
and it's descendants.
In some cases, there seem to be issues with unreleased instances of inkGameController
leading to crashes. This was introduced when we started to properly wrap the context in Handle<>
.
Changed the behavior of observers so that all observers will be called even if an override is defined.
Observe('PlayerPuppet', 'IsPlayer', function() print('Observer 1') end)
Override('PlayerPuppet', 'IsPlayer', function() print('Override 1') end)
Override('PlayerPuppet', 'IsPlayer', function() print('Override 2') end)
Observe('PlayerPuppet', 'IsPlayer', function() print('Observer 2') end)
Will output
Observer 1
Observer 2
Override 1
Stricter check of the passed context when calling the RTTI function. Prevents unwanted crashes when invalid value passed as self
/ this
. In particular, it prevents crashes when a dot is accidentally used instead of a colon.
Support for ISerializable
strong and weak references. This opens access to some new classes, but not all of them.
Function to add new CName
s to the pool
print(CName("CET").value) -- Empty string
CName.add("CET")
print(CName("CET").value) -- CET
Workaround for parameterized struct constructor. Previously, fields of a reference type could be corrupted right after construction.
Support of nulls for strong and weak references.
Assignment example:
-- Set reference to null
self.resetConfirmationToken = nil
Parameter example:
-- Omit optional task data
Game.GetDelaySystem():QueueTask(this, nil, "ResolveGameplayStateTask", gameScriptTaskExecutionStage.PostPhysics)
IsDefined
helper function as in redscript. Checks if reference is not null and valid. Can be used on game object's field and lua variable.
EnumInt
helper function as in redscript.
print(EnumInt(GameplayTier.Tier2_StagedGameplay)) -- 2ULL
Support for CRUID
type.
local dialogLine = scnDialogLineData.new()
dialogLine.id = CRUID(12345)
Support for gamedataLocKeyWrapper
type.
TweakDB:SetFlat("Items.Preset_Overture_Kerry.displayName", LocKey(40475))
Support for patch 1.31
Contains: 611
Updated to Imgui 1.84.2
Support for patch 1.30 second edition.
Contains: 603
Fix disappearing footsteps (1.16.1 regression).
Alternative syntax for CName
and TweakDBID
conversions without .new()
print(TweakDBID("Items.money"))
print(CName(0))
print(CName("DEBUG"))
Direct access to global functions.
print(GetPlayer()) -- Equivalent to print(Game.GetPlayer())
Throw Lua errors from RTTI calls as the Lua script will do.
registerForEvent('onInit', function()
Observe('TimeSystem', 'SetTimeDilation', function(self, reason)
if reason.value == 'consoleCommand' then
Game.FloorF('XXX')
end
end)
end)
registerHotkey('TestError', 'TestError', function()
Game.GetTimeSystem():SetTimeDilation('consoleCommand', 0.25, 1.0, 'Linear')
end)
Will print the full callstack in the log:
[error] ...x64\plugins\cyber_engine_tweaks\mods\test-error\init.lua:4: Function 'FloorF' parameter 1 must be Float.
stack traceback:
[C]: in function 'FloorF'
...x64\plugins\cyber_engine_tweaks\mods\test-error\init.lua:4: in function <...x64\plugins\cyber_engine_tweaks\mods\test-error\init.lua:2>
[C]: in function 'SetTimeDilation'
...x64\plugins\cyber_engine_tweaks\mods\test-error\init.lua:10: in function <...x64\plugins\cyber_engine_tweaks\mods\test-error\init.lua:9>
Fix channel logs not printing the message.
Added a new channel "DEBUG" that always prints even when game logs are disabled.
Re-added TweakDB printing.
Register global PlayerPuppet.OnAction
handler as for pre patch 1.3.
Override function by full name. Allows to observe/override all variants of overloaded function.
Support for patch 1.30 - huge thanks to everyone who helped with this and all testers!
Contains: 581, 582, 583, 585 and 589
Fix crash when calling functions that have optional parameters not defaulted.
Fix crash when using RegisterInputListener from an observer.
Contains: 568, 572, 573, 574, 575, 577, 578 and 579
Whitelisted collectgarbage
to let scripts force release game references.
Added exEntitySpawner
to spawn entities easily
spawnPosition = Game.GetPlayer():GetWorldTransform()
-- [Simple]
-- Spawn 'Character.Judy' just like prevention system
-- Optionally set appearance to 'judy_diving_suit'
judyEntityID = exEntitySpawner.SpawnRecord('Character.Judy', spawnPosition)
judyEntityID = exEntitySpawner.SpawnRecord('Character.Judy', spawnPosition, 'judy_diving_suit')
-- [Advanced]
-- Spawn base\quest\secondary_characters\judy.ent
-- Optionally set appearance to 'default'
-- Optionally set TweakDB record to 'Character.Judy' (VO/Name/Equipment/etc)
judyEntityID = exEntitySpawner.Spawn([[base\quest\secondary_characters\judy.ent]], spawnPosition)
judyEntityID = exEntitySpawner.Spawn([[base\quest\secondary_characters\judy.ent]], spawnPosition, 'default')
judyEntityID = exEntitySpawner.Spawn([[base\quest\secondary_characters\judy.ent]], spawnPosition, 'default', 'Character.Judy')
-- Some time later...
-- entities are not spawned instantly, FindEntityByID may return nil
judy = Game.FindEntityByID(judyEntityID)
exEntitySpawner.Despawn(judy)
Fixed a crash that would occur on machines without ASLR enabled by enabling LuaJIT's GC64 option.
Fixed nested RTTI calls that would crash the game due to memory release of the previous call's memory.
Fix the disable anti AA and async compute patches.
Fix for TweakDB default values.
Fix tooltips that contained C format characters and would crash the game.
Improved update hook.
xmake install
will now install in the game's directory to make it easier for those installing from source.
Contains: 551, 552, 553, 557, 558, 559
All game classes are directly accessible by name. For example, entEntityId
, PlayerPuppet
.
All game enums are directly accessible by name. For example, gamedataStatType.BaseDamage
, gameGameVersion.Current
.
Classes can also be accessed by their aliases from redscript. For example, WeaponObject
instead of gameweaponObject
.
Classes have the conventional .new()
constructor. For example, MappinData.new()
.
A constructor can take an array of properties to create and initialize an object in a single statement. For example, EntityID.new({ hash = 12345 })
.
Static methods are accessible from classes using the dot. For example, ScriptedPuppet.IsDefeated(npc)
.
A static method can be called from an instance if the first parameter is of the same type. For example, vec4:Length()
instead of Vector4.Length(vec4)
.
The overloaded function is resolved based on passed parameters when called by its short name. For example, StatusEffectHelper.HasStatusEffect(target, gamedataStatusEffectType.Overheat)
.
Partial Variant
type support. ToVariant()
and FromVariant()
are only applicable to classes.
Observe()
and Override()
now accept both native and scripted type names.
Added Game.GetSystemRequestsHandler()
as an alternative to GetSingleton("inkMenuScenario"):GetSystemRequestsHandler()
.
Added TweakDB:SetFlats
TweakDB:SetFlats('Character.Judy', {
level = 99,
tags = { 'Immortal' }
});
Added WorldFunctionalTests
transform = Game.GetPlayer():GetWorldTransform()
entityID = WorldFunctionalTests.SpawnEntity('base\\quest\\secondary_characters\\judy.ent', transform, '')
entity = Game.FindEntityByID(entityID)
WorldFunctionalTests.DespawnEntity(entity)
Added Imgui.GetStyle()
Added support for ResourceAsyncReference
. Allows editing of TweakDB props of that type by the mods.
Fixed disappearing footsteps issue.
The issue still can occur if the mods do not properly release the player reference.
The GetMod()
is only available after onInit
as there is no guarantee that the required mod will be loaded before this event.
Types and functions that rely on RTTI are only available after onInit
event to prevent some unwanted crashes and unexpected behavior.
Improved detection of whether the game is running. Prevents more crashes when exiting the game.
Implemented optional parameters support. Prevents some unexpected crashes.
Fixed DumpType("Type")
returning empty result.
Fixed crash when accessing properties of invalid Enum
, eg. Enum.new('', '').value
.
Fixed crash when setting an incompatible value for an object property.
Fixed crash when calling function with out parameters of Enum
, CName
or TweakDBID
type.
Fixed memory leaks when passing strong or weak references to the function.
Fixed memory leaks when invalid parameters passed to the function.
Fixed memory leaks for function results and out parameters of certain types.
Fixed memory leaks when creating new objects.
Fixed memory leaks when setting object properties.
Fixed memory leak for arrays with elements of certain types passed as an argument.
Fixed a crash when passing an incompatible array as an argument. For example, an array of numbers instead of an array of handles.
Fixed memory leak when passing an object instead of an array as an argument. This resulted in a silent crash without calling ResetAllocator
.
Fixed inconsistent self
and random crashes in Observe
and Override
.
Reverted Override()
to the previous behavior so when the handler fails, the original game function is not called.
Added implicit conversion from Int64/UInt64 to other arithmetic types.
Added type safety checks for Int64/UInt64.
Added recursive freeing of arrays.
Added logging for errors occurred in the module loaded with require()
. Should make the transition to the new version less painful.
Global fallback table is used now for all mod environments. No need to whitelist what's defined there.
Aliases like Game['GetPlayer'] = Game['GetPlayer;GameInstance']
aren't needed in autoexec.lua
. All global and class functions are automatically resolved by short name now.
Added implicit class to strong reference conversion.
Updated RED4ext to latest version.
This fixes the error when trying to use a CName
that is None
Patch for the Minimap flicker
Support for patch 1.22
Fixed
Crash when using TweakDB:Update in an Observe/Override callback
Direct3d12 command queue could be null sometimes
Error logging from Observe/Override callbacks
Redundant Lua environment passing
64bits numbers are now correctly interpreted as numbers by Lua
Added
Strings can now be implicitly cast to a TweakDBID when the scripting framework expects a TweakDBID
Support for patch 1.21
Fixed
Lua scripting errors with functions returning some values with out params
GameOptions.Toggle() now works
Fix falsy "Unknown error" when calling a global that returns nil
Return out params from global function the same way as for instance methods
EulerAngles::ToString returning swapped roll and yaw values
SQLite3 database not closing on mod reload
Added
Equality comparison to some types (Vector3, Enum, TweakDBID,...)
Concatenation for TweakDBID
Fixed
Regressions with GameOptions
Wrong version info returned back by GetVersion()
Problems with Console widget history
3rd party licences missing
ImGui.TreePop unavailable in Lua
Multiple registerInput handlers unable to be invoked at same time
Conversion of 64-bit integral values from object properties
Scroll wheel failling to register properly
Added
Modal popups for unsaved changes into Bindings and Settings widget
Modal popup on first launch asking user explicitly to bind some hotkey for toggling Overlay
Option to enable removing of dead binds (default is on)
Option to enable ImGui assertions to make sure mods are not breaking something (default is off)
Option to toggle ImGui Diagnostic window (default is off, this option is not preserved after restart!)
Changed
Nicer formatting of headings inside Bindings widget (replace characters that are not alphanumeric by space and autocapitalize each word)
Reworked Settings menu (options are now split into two categories - Patches and Dev)
Reworked Bindings menu (separated hotkeys and inputs into two categories)
onDraw is not called for mods while CET modal dialog is active (including first launch)
First time launch (it should now be more streamlined)
Decoupled config.json from bindings.json (overlay key is now located only inside bindings.json and is left out of config.json)
Overlay key was moved to Bindings menu
Updated TiltedCore to 0.2.2
Added support for patch 1.2.
Fixed
os.remove and os.rename were not working properly.
The debug menu patch would not show the menus.
Fixed dofile, loadfile and loadstring.
Lua environment would not be applied correctly to some callbacks.
Lua to RED engine converter now works correctly with nil values and double.
Security issue where scripts could override another script's functions, calling another script is now read only.
Changed
Key bindings were improved.
Support for scroll wheel.
registerInput for single inputs. This handler is similar to registerHotkey, but function now takes one boolean argument denoting if input was pressed/released.
IsBound and GetBind added to check if action is bound and retrieve string representation of it.
TweakDB experienced many changes:
All TweakDB functions can now be called with strings instead of TweakDBID. TweakDBID is still accepted.
TweakDB:SetFlat now updates records if it already exists (see SetFlatNoUpdate)
Performance improved.
Lua version changed to 5.1 this can break your mod if you were using post 5.1 language features such as bit operators. This change was done to fix environment issues, we also got a performance boost by switching to LuaJIT instead of the default Lua implementation.
Updated Imgui to version 1.82.
Updated spdlog to version 1.8.5.
Updated sqlite3 to version 3.35.0.
Added
Re-added the SMT patch for AMD CPUs that did not get a performance boost after CDPR's patch.
Added Lua's bit32 library.
TweakDB additions:
CloneRecord, CreateRecord and DeleteRecord.
GetRecords, obtain a list of records associated with a type.
SetFlatNoUpdate, faster way to set without checking if there is an old record to update. Requires call to TweakDB:Update afterwards to apply changes!
TweakDB editor in the UI.
Supports loading archivehashes.txt
.
Can now search through record flats.
Fixed
Fix memory allocation errors due to missing argument.
Fix Observe/Override sometimes affecting scripts when they shouldn't.
Fix crash when using multiple mods.
Fixed
Crash on game exit when a reference had not been garbage collected by the lua vm.
Missing enum value CellPadding in Imgui Lua (Pull Request: 496).
Crash when using Override/Observe on pure script functions.
Crash when reloading mods using Override/Observe.
Crash when a malformed lua file was loaded with "require".
Lua execution error when a callback used a global variable because environments were not applied.
Fix memory leaks when reloading mods using Observe/Override.
Changed
Memory optimization for calls.
Replaced the hacky thread safety used for the Lua VM with TiltedCore's Lockable<T>.
Added
API changes to support Imgui 1.81 and added missing ColorButton overloads.
Override has now 2 overloads to simplify usage, this will completely remove the original function or add one if it doesn't exist:
Override(typeName, fullFunctionName, shortFunctionName, function)
Override(typeName, functionName, function) - both full and short names are set to functionName
Observe has now 2 overloads to simplify usage, this will not remove the original function but will add one if it doesn't exist:
Observe(typeName, fullFunctionName, shortFunctionName, function)
Observe(typeName, functionName, function) - both full and short names are set to functionName
Changed
Upgrade Imgui 1.80 to 1.81.
Fixed Override so that it can override at runtime after scripts have been loaded.
Revert Lua caching of functions as the new override method will work regardless.
Fixed
Fix game crash when a script used "require" on a malformed file.
Added
Override function to either listen to a function or to replace a game function. Despite its name it can also be used to extend a type by adding a new function. Usage is as such:
Override("PlayerPuppet", "IsA", "IsA", true, function(self, className) print(className); return true end)
The first parameter is the class type, second the full name of the function, the third is the short name (usually they are the same), the fourth boolean is used to specify if this is a replacement of the function or not, if set to true it will replace entirely the original function, if set to false, your function will be called before the original function is called. The last parameter is your handler function, note that the parameters are passed according to the RED script definition.
Removed
Telemetry, the experiment gave us the data we wanted, obviously a lot of people use the mod and we are very happy about that! We are sorry this caused so much drama, it wasn't the intention and quite frankly we still don't really understand why.
Changed
Calling RED functions used to be cached, given that functions can be overridden at any time the cache has been removed, it's a bit slower but shouldn't be visible in the real world.
Using the up and down arrows in the console now retrieves the command history
Added Telemetry so we know how many people use CET (can be disabled in settings), note that we do not store any information we just count the number of players
Fix TweakDBID issues with large arrays
Fix TweakDBID crash when using specific functions
Changed
Updated to game version 1.12
Updated RED4ext.sdk to the latest
Proper sandboxing (#454):
Each mod is (right now) assigned sqlite3 database implicitly, accessible through db
lua object (will be changed later to on-demand through info in metadata file)
Introduced proper sandbox with LuaSandbox helper class
Bytecode cannot be loaded now (native modules included)
require
cache is reset on Reload all mods
now
require
does not support '.' instead of '/' now (only breaking change, functions the same otherwise)
All pathing is now relative to mod (breaking change, includes all io functions, dofile()
and such included)
stripped ImGui, json, io and dofile and such from console sandbox (only needed things left for it in)
Added dir()
command for mods to list directory contents
Errors that happen in Game, game class methods,... should now log into proper environment (to console when executed from console, to mod log when executed from mod)
More ImGui stuff(#452, #453, #457, #460):
Enabled ImGui keyboard navigation. (ctrl + tab
to switch window, arrow keys to navigate)
Removed the deprecated power
argument in Drag and Slider widgets, replace it with the newer ImGuiSliderFlag (breaking change)
Made the argument open
optional in ImGui.Begin()
, ImGui.BeginModal()
, ImGui.BeginTabItem()
, so the close button can be hidden when setting flags.
Added bindings for the new tables widget in ImGui 1.8.0
Added selected mouse utilities back
Added bindings for ImDrawlist
TweakDB binding (#461)
TweakDB:GetFlat(...)
TweakDB:SetFlat(...)
TweakDB:GetRecord(...)
TweakDB:Query(...)
TweakDB:Update(...)
Changed
Major vulnerability in the game's save loader fixed
Changed
Updated to game version 1.11
Updated RED4ext.sdk to the latest
Updated to spdlog 1.8.2
Replaced pattern search with much faster library
Fix issue when resizing game window (eg. resolution change in Options)
Fix issue causing game window to stop rendering and freeze on last frame
Fix ImGui.CalcTextSize overloads (now, second parameter is removed from all, as it did not make sense for Lua anyway)
Fix Windows 7 flatlining
Added json
support to Lua
Added support for the latest patch 1.10
UI with settings will now ask you for a console key on first launch, you can edit it later
Mods can now request hotkeys via registerHotkey
, the user can pick what key they want to use in the menu
You can now instantiate game objects with NewObject('class name')
, do not expect this to work with every class without any issues
SQLite3 support was added, you can use the entire API at the moment, we are going to remove all open functions in the near future and replace it with a open that can only open a single database linked to your mod
Mods will now log to their own log files, you can also use spdlog
to print in your log
Scripting is now handling memory correctly, calling Game.GetPlayer()
every frame is fine now
Game doesn't crash when exiting anymore
dofile
is now deprecated, use only mods please, it will be removed entirely in the next update
Access and write tables using dot "." notation, also known as "flat path". Similar to TweakDB Flat logic but for any kind of LUA table.
tFlat.get(obj: table, path: string, opt default: any) -> value/default
tFlat.has(obj: table, path: string) -> boolean
tFlat.set(obj: table, path: string, value: any) -> table
tFlat.insert(obj: table, path: string, value: any) -> table
tFlat.delete(obj: table, path: string) -> table
All code examples below use this predefined table data. Note that items
is an associative object and elements
is a sequential table.
local myTable = {
ready = true,
config = {
items = {
item1 = true,
item2 = false
},
elements = {
'element1',
'element2'
}
}
}
Get value
local item2 = tFlat.get(myTable, 'config.items.item2')
-- false
local element2 = tFlat.get(myTable, 'config.elements.2')
-- element2
Has value
local hasItem2 = tFlat.has(myTable, 'config.items.item2')
-- true
local hasElement2 = tFlat.has(myTable, 'config.elements.2')
-- true
Set value
tFlat.set(myTable, 'config.items.item2', true)
tFlat.set(myTable, 'config.elements.2', 'newElement2')
myTable = {
["ready"] = true,
["config"] = {
["items"] = {
["item1"] = true,
["item2"] = true,
},
["elements"] = {
"element1",
"newElement2",
},
},
}
Insert sequential table value
tFlat.insert(myTable, 'config.elements', 'element3')
myTable = {
["ready"] = true,
["config"] = {
["items"] = {
["item1"] = true,
["item2"] = false,
},
["elements"] = {
"element1",
"element2",
"element3",
},
},
}
Delete value
tFlat.delete(myTable, 'config.items.item1')
tFlat.delete(myTable, 'config.elements.1')
myTable = {
["ready"] = true,
["config"] = {
["items"] = {
["item2"] = false,
},
["elements"] = {
"element2",
},
},
}
tFlat = {
get = function(obj, path, default)
-- get path array
path = tFlat.split(path)
-- vars
local length = #path
local current = obj
local key
-- loop through path
for index = 1, length, 1 do
-- current key
key = path[ index ]
-- convert key to number (sequential table)
if tonumber(key) then
key = tonumber(key)
end
-- stop searching if a child object is missing
if current[ key ] == nil then
return default
end
current = current[ key ]
end
return current
end,
has = function(obj, path)
return tFlat.get(obj, path) ~= nil
end,
set = function(obj, path, val)
-- get path array
path = tFlat.split(path)
-- vars
local length = #path
local current = obj
local key
-- loop through path
for index = 1, length, 1 do
-- current key
key = path[ index ]
-- convert key to number (sequential table)
if tonumber(key) then
key = tonumber(key)
end
-- set value on last key
if index == length then
current[ key ] = val
-- current key exists
elseif current[ key ] then
if type(current[ key ]) ~= 'table' then
current[ key ] = {}
end
current = current[ key ]
-- current key doesn't exist
else
current[ key ] = {}
current = current[ key ]
end
end
-- return
return obj
end,
insert = function(obj, path, val)
-- get target
local target = tFlat.get(obj, path)
-- check if table and sequential
if type(target) == 'table' and tFlat.isSequential(target) then
table.insert(target, val)
end
-- return
return obj
end,
delete = function(obj, path)
-- get path array
path = tFlat.split(path)
-- vars
local length = #path
local current = obj
local key
-- loop through path
for index = 1, length, 1 do
-- current key
key = path[ index ]
-- convert key to number (sequential table)
if tonumber(key) then
key = tonumber(key)
end
-- set value on last key
if index == length then
current[ key ] = nil
else
current = current[ key ] or {}
end
end
-- return
return obj
end,
split = function(s)
s = tostring(s)
local fields = {}
local pattern = string.format("([^%s]+)", '.')
string.gsub(s, pattern, function(c)
fields[ #fields + 1 ] = c
end)
return fields
end,
isSequential = function(array)
for k, _ in pairs(array) do
if type(k) ~= "number" then
return false
end
end
return true
end,
}