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
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)
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)
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)