All pages
Powered by GitBook
1 of 4

Observe, Override, NewProxy

Introduction

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

Available Functions

ObserveOverride

Observe

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.

You can now generate Observable and ObservableAfter using NativeDB. You need to configure option Clipboard syntax to Lua. You can click on the "copy" button of a function, pick Copy Observable or Copy ObservableAfter and it will copy the code in your clipboard.

Definition

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)

Observe() is an alias of ObserveBefore(). They both work the same.

If you observe a static function, you must define the field 'method' with the full name of the function. Otherwise it won't work. You can find the full name using NativeDB. See below for an example.

Representation

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)

The self parameter can be renamed at your convenience:

  • _

  • this

  • class

  • whatever

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

Usage Examples

Give money each time the player crouches:

init.lua
-- onInit
registerForEvent('onInit', function()
    
    -- observe crouch OnEnter state
    Observe('CrouchEvents', 'OnEnter', function(self, stateContext, scriptInterface)
        
        Game.AddToInventory('Items.money', 1000)
        
    end)
    
end)

Give money as long as the player is crouched:

init.lua
-- 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)

Observe event send to an AnimationControllerComponent (click on function's name in NativeDB to copy the full name in your clipboard):

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

Advanced Example

Give money when the player is crouched and in ADS:

init.lua
-- 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)

Override

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.

You can now generate Override using NativeDB. You need to configure option Clipboard syntax to Lua. You can click on the "copy" button of a function, pick Copy Override and it will copy the code in your clipboard.

Definition

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)

If you override a static function, you must define the field 'method' with the full name of the function. Otherwise it won't work. You can find the full name using NativeDB. See a similar example with Observe.

Representation

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 self and wrappedMethod parameters can be renamed at your convenience:

  • _

  • this

  • class

  • method

  • whatever

Using Wrapped Method

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.

Method -> Void

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)

Method -> Bool / Int / Float etc...

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)

It is not required to execute the original code using wrappedMethod. This can be omitted, but it exposes the script to malfunctions if not handled properly.

Usage Example

Do not allow the player to crouch:

init.lua
-- onInit
registerForEvent('onInit', function()
    
    -- observe CrouchDecisions EnterCondition state
    Override('CrouchDecisions', 'EnterCondition', function(self, stateContext, scriptInterface, wrappedMethod)
        
        return false
        
    end)
    
end)

Advanced Example

Do not allow the player to crouch if in ADS:

init.lua
-- 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

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.

You can now generate NewProxy using NativeDB. You need to configure option Clipboard syntax to Lua. You can click on the "copy" button of a function, pick Copy NewProxy and it will copy the code in your clipboard. It only works for classes with their names ending with Listener.

Definition

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

Find the signature of a function

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

Use a proxy

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

Usage Examples

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)