Changing returning hooks works + better documentation?

Discuss API design here.
User avatar
Doldol
Senior Member
Posts: 200
Joined: Sat Jul 07, 2012 7:09 pm
Location: Belgium

Changing returning hooks works + better documentation?

Postby Doldol » Sun Feb 04, 2018 3:52 pm

As far as I know this is how a PreHook works (I couln't find any documentation on this)

Modify argument C++ func gets called with

Syntax: Select all

import memory
from entities import TakeDamageInfo
from players.entity import Player
from entities.hooks import EntityPreHook, EntityCondition

@EntityPreHook(EntityCondition.is_player, 'on_take_damage')
def pre_on_take_damage(args):
victim = memory.make_object(Player, args[0])
info = memory.make_object(TakeDamageInfo, args[1])
info.damage = 500


block C++ func from getting called (and pass return val back to the C++ func trying ot call our hooked func)

Syntax: Select all

import memory
from entities import TakeDamageInfo
from players.entity import Player
from entities.hooks import EntityPreHook, EntityCondition

@EntityPreHook(EntityCondition.is_player, 'on_take_damage')
def pre_on_take_damage(args):
return False # or True


Should do absolutely nothing

Syntax: Select all

import memory
from entities import TakeDamageInfo
from players.entity import Player
from entities.hooks import EntityPreHook, EntityCondition

@EntityPreHook(EntityCondition.is_player, 'on_take_damage')
def pre_on_take_damage(args):
return None


(I don't exactly know how a post hook would work? I know you get the hooked func's return value, I have no clue what returning anything would do)

Wouldn't this be better? (Maybe too costly performance-wise?)

Modify argument C++ func gets called with

Syntax: Select all

import memory
from entities import TakeDamageInfo
from players.entity import Player
from entities.hooks import EntityPreHook, EntityCondition
from memory.hooks import PreHookReturn

@EntityPreHook(EntityCondition.is_player, 'on_take_damage')
def pre_on_take_damage(args):
victim = memory.make_object(Player, args[0])
info = memory.make_object(TakeDamageInfo, args[1])
info.damage = 500
return PreHookReturn.Modify(victim,info) # positional args


block C++ func from getting called (and pass return val back to the C++ func trying to call our hooked func)

Syntax: Select all

import memory
from entities import TakeDamageInfo
from players.entity import Player
from entities.hooks import EntityPreHook, EntityCondition,
from memory.hooks import PreHookReturn

@EntityPreHook(EntityCondition.is_player, 'on_take_damage')
def pre_on_take_damage(args):
return PreHookReturn.Block(False)# or True or any value not None and PreHookReturn.Block() would be void (or None == void. So void would be calling PreHookReturn.Block(None)) (could do a check with rtn specified (e.g. Argument.BOOL) here)


Should do absolutely nothing
(used as a listener)

Syntax: Select all

import memory
from entities import TakeDamageInfo
from players.entity import Player
from entities.hooks import EntityPreHook, EntityCondition
from memory.hooks import PreHookReturn

@EntityPreHook(EntityCondition.is_player, 'on_take_damage')
def pre_on_take_damage(args):
return PreHookReturn.PASS# PreHookReturn.PASS == CONST_PASS == None


Then an idea for how this could work behind the curtain

Syntax: Select all

call: PreHookReturn.Modify(victim,info)
return: (Enum.MODIFY, victim, info)

call: PreHookReturn.Block(False)
return: (Enum.BLOCK, False)


or

Syntax: Select all

CONST_PASS = None

CONST_MODIFY = True
CONST_BLOCK = False

call: PreHookReturn.Modify(victim,info)
return: (CONST_MODIFY, victim, info)

call: PreHookReturn.Block(False)
return: (CONST_BLOCK, False)



And maybe it could be beneficial to call a function at any point in the hook (ASAP) instead of returning a value, so the engine doesn't have to wait for the Py function(our hook) to execute, more of a nice-to-have though, possible downside is losing clarity because function call would be working as using return implicitly.
User avatar
Ayuto
Project Leader
Posts: 2193
Joined: Sat Jul 07, 2012 8:17 am
Location: Germany

Re: Changing returning hooks works + better documentation?

Postby Ayuto » Sun Feb 04, 2018 8:28 pm

Your modifying example is correct, but you can also replace arguments. This is neccessary if the argument is not a pointer.

Syntax: Select all

# Assuming we hook:    int multiply(int x, int y)
def pre_multiply(args):
# Replace passed value for x by 5
args[0] = 5


You blocking example is not quite correct. Any value that is not None blocks the original function from being executed. However, if the hooked function has a return type (not void), your return value must match the return type. So, if the hooked function returns a string, you also need to return a string.

A post hook allows you to modify the value that will be returned to the caller. That means when your post hook is being executed, the caller still has not received the return value (from the pre hook or the original function).

Doldol wrote:Wouldn't this be better? (Maybe too costly performance-wise?)

Modify argument C++ func gets called with

Syntax: Select all

import memory
from entities import TakeDamageInfo
from players.entity import Player
from entities.hooks import EntityPreHook, EntityCondition
from memory.hooks import PreHookReturn

@EntityPreHook(EntityCondition.is_player, 'on_take_damage')
def pre_on_take_damage(args):
victim = memory.make_object(Player, args[0])
info = memory.make_object(TakeDamageInfo, args[1])
info.damage = 500
return PreHookReturn.Modify(victim,info) # positional args
No, what exactly should make it faster? When "info.damage = 500" has been executed, the argument already has been modified. There is really nothing that costs performance.

Doldol wrote:block C++ func from getting called (and pass return val back to the C++ func trying to call our hooked func)

Syntax: Select all

import memory
from entities import TakeDamageInfo
from players.entity import Player
from entities.hooks import EntityPreHook, EntityCondition,
from memory.hooks import PreHookReturn

@EntityPreHook(EntityCondition.is_player, 'on_take_damage')
def pre_on_take_damage(args):
return PreHookReturn.Block(False)# or True or any value not None and PreHookReturn.Block() would be void (or None == void. So void would be calling PreHookReturn.Block(None)) (could do a check with rtn specified (e.g. Argument.BOOL) here)

This is actually a good idea. I also thought about this, but I would rather just call it "Override". E. g.

Syntax: Select all

# For void functions:
return Override()

# For non-void functions:
return Override(<my_value>)

# This won't override it, but call the original function instead
return None
We could even add this without breaking compatibility with existing hooks.

Doldol wrote:And maybe it could be beneficial to call a function at any point in the hook (ASAP) instead of returning a value, so the engine doesn't have to wait for the Py function(our hook) to execute, more of a nice-to-have though, possible downside is losing clarity because function call would be working as using return implicitly.

I'm not sure what you mean.
User avatar
Doldol
Senior Member
Posts: 200
Joined: Sat Jul 07, 2012 7:09 pm
Location: Belgium

Re: Changing returning hooks works + better documentation?

Postby Doldol » Sun Feb 04, 2018 10:25 pm

Ayuto wrote:Your modifying example is correct, but you can also replace arguments. This is neccessary if the argument is not a pointer.

Syntax: Select all

# Assuming we hook:    int multiply(int x, int y)
def pre_multiply(args):
# Replace passed value for x by 5
args[0] = 5


You blocking example is not quite correct. Any value that is not None blocks the original function from being executed. However, if the hooked function has a return type (not void), your return value must match the return type. So, if the hooked function returns a string, you also need to return a string.

Yeah I'm actually aware, I was trying to keep the examples simple and probably misrepresented doing so.

Ayuto wrote:A post hook allows you to modify the value that will be returned to the caller. That means when your post hook is being executed, the caller still has not received the return value (from the pre hook or the original function).

Ah that makes sense, what would you do atm if you don't want to modify the return value, only read it in a post hook? Or does setting the return value in the hook immediately update what will be returned to the caller, if so what do different values for the return statement do in a post hook atm?

Ayuto wrote:
Doldol wrote:Wouldn't this be better? (Maybe too costly performance-wise?)

Modify argument C++ func gets called with

Syntax: Select all

import memory
from entities import TakeDamageInfo
from players.entity import Player
from entities.hooks import EntityPreHook, EntityCondition
from memory.hooks import PreHookReturn

@EntityPreHook(EntityCondition.is_player, 'on_take_damage')
def pre_on_take_damage(args):
victim = memory.make_object(Player, args[0])
info = memory.make_object(TakeDamageInfo, args[1])
info.damage = 500
return PreHookReturn.Modify(victim,info) # positional args
No, what exactly should make it faster? When "info.damage = 500" has been executed, the argument already has been modified. There is really nothing that costs performance.


Oops I meant that in my suggestion setting info.damage = 500 actually only changes the value locally (only in the hook, in python), calling return PreHookReturn.Modify(victim,info) would actually update the arguments the C++ func gets called with, it's just a suggestion, but this seems more like what I would normally expect to happen in python, but I understand that this might not be worth implementing. And this is what I was referring to as possibly costing some performance, I didn't mean to say that this implementation could make it faster.

Ayuto wrote:
Doldol wrote:block C++ func from getting called (and pass return val back to the C++ func trying to call our hooked func)

Syntax: Select all

import memory
from entities import TakeDamageInfo
from players.entity import Player
from entities.hooks import EntityPreHook, EntityCondition,
from memory.hooks import PreHookReturn

@EntityPreHook(EntityCondition.is_player, 'on_take_damage')
def pre_on_take_damage(args):
return PreHookReturn.Block(False)# or True or any value not None and PreHookReturn.Block() would be void (or None == void. So void would be calling PreHookReturn.Block(None)) (could do a check with rtn specified (e.g. Argument.BOOL) here)

This is actually a good idea. I also thought about this, but I would rather just call it "Override". E. g.

Syntax: Select all

# For void functions:
return Override()

# For non-void functions:
return Override(<my_value>)

# This won't override it, but call the original function instead
return None
We could even add this without breaking compatibility with existing hooks.

I'm down, but how about making the None in return None be a constant to make it more clear what it does?

Ayuto wrote:
Doldol wrote:And maybe it could be beneficial to call a function at any point in the hook (ASAP) instead of returning a value, so the engine doesn't have to wait for the Py function(our hook) to execute, more of a nice-to-have though, possible downside is losing clarity because function call would be working as using return implicitly.

I'm not sure what you mean.


Something like:

Syntax: Select all

import memory
from entities import TakeDamageInfo
from players.entity import Player
from entities.hooks import EntityPreHook, EntityCondition,
from memory.hooks import PreHookReturn

@EntityPreHook(EntityCondition.is_player, 'on_take_damage')
def pre_on_take_damage(args):
#Do stuff that changes my <my_value>
Override(<my_value>)# return this value to the caller NOW
#Do some cleanup here maybe you need to calculate a bunch of primes here or something, in which it's important to know this function arguments, but we don't need to return any results of this part to the caller


Right now you could do this with a Delay or a queue etc. Like:

Syntax: Select all

@EntityPreHook(EntityCondition.is_player, 'on_take_damage')
def pre_on_take_damage(args):
victim = memory.make_object(Player, args[0])
info = memory.make_object(TakeDamageInfo, args[1])
Delay(0, someother_func, (victim, info))
info.damage = 500


I've done this when I had to do some math that in a prehook but which't have to happen before returning to caller/in the same frame. Not doing that lagged the game.
User avatar
Ayuto
Project Leader
Posts: 2193
Joined: Sat Jul 07, 2012 8:17 am
Location: Germany

Re: Changing returning hooks works + better documentation?

Postby Ayuto » Mon Feb 05, 2018 7:31 pm

Doldol wrote:Ah that makes sense, what would you do atm if you don't want to modify the return value, only read it in a post hook? Or does setting the return value in the hook immediately update what will be returned to the caller, if so what do different values for the return statement do in a post hook atm?

It works the same way like a pre-hook. If you want to modify the return value, you simply use make_object on the second argument and then you can modify it. If you want to replace it, you simply return the value that should be returned to the caller.

Doldol wrote:Oops I meant that in my suggestion setting info.damage = 500 actually only changes the value locally (only in the hook, in python), calling return PreHookReturn.Modify(victim,info) would actually update the arguments the C++ func gets called with, it's just a suggestion, but this seems more like what I would normally expect to happen in python, but I understand that this might not be worth implementing. And this is what I was referring to as possibly costing some performance, I didn't mean to say that this implementation could make it faster.

That's only making things complicated, which are currently quite easy. I also don't really understand why you would expect it to work like this. If you work with a simple Python class, you also wouldn't expect something like this:

Syntax: Select all

a = A()
a.some_attribute = 10
a.apply_attributes('some_attribute')


Doldol wrote:I'm down, but how about making the None in return None be a constant to make it more clear what it does?

A return statement in Python is completely optional. If it's ommited, it has the same effect like "return None". If people know that plus the fact that they don't need a return statement in a hook to let the hooked function continue as usual, it's also clear how to use a hook.

Doldol wrote:Something like:

Syntax: Select all

import memory
from entities import TakeDamageInfo
from players.entity import Player
from entities.hooks import EntityPreHook, EntityCondition,
from memory.hooks import PreHookReturn

@EntityPreHook(EntityCondition.is_player, 'on_take_damage')
def pre_on_take_damage(args):
#Do stuff that changes my <my_value>
Override(<my_value>)# return this value to the caller NOW
#Do some cleanup here maybe you need to calculate a bunch of primes here or something, in which it's important to know this function arguments, but we don't need to return any results of this part to the caller


Right now you could do this with a Delay or a queue etc. Like:

Syntax: Select all

@EntityPreHook(EntityCondition.is_player, 'on_take_damage')
def pre_on_take_damage(args):
victim = memory.make_object(Player, args[0])
info = memory.make_object(TakeDamageInfo, args[1])
Delay(0, someother_func, (victim, info))
info.damage = 500


I've done this when I had to do some math that in a prehook but which't have to happen before returning to caller/in the same frame. Not doing that lagged the game.

That looks really confusing to me, tbh. Creating a thread is exactly the correct way to handle this situation.

I'm really happy that you are taking a look at this topic, but your suggestions seem to complicate stuff.
User avatar
Doldol
Senior Member
Posts: 200
Joined: Sat Jul 07, 2012 7:09 pm
Location: Belgium

Re: Changing returning hooks works + better documentation?

Postby Doldol » Mon Feb 05, 2018 11:49 pm

It's really all just a suggestion (but I do really think it'd be cool to have documentation on how this all precisely works).

I admit that I'm adding some boilerplate, and I really understand where you're coming from. Thinking about it more, you're probably right that it'd be unnecessary/detrimental and that I'm just adding complexity. Except for the idea of what you suggest as being called "Override" and I do think that it's not obvious enough that returning not (as in anything but) None implies blocking the execution of the hooked function/that None implies continuing. Especially considering SP uses things like CommandReturn. I do understand where the current behavior comes from, None as in not typing the return statement or as in I have nothing to override with and None would be the closest to void. Although I do think only implementing Override would help a lot.
User avatar
iPlayer
Developer
Posts: 590
Joined: Sat Nov 14, 2015 8:37 am
Location: Moscow
Contact:

Re: Changing returning hooks works + better documentation?

Postby iPlayer » Sat Feb 10, 2018 3:13 am

You can actually look at

Syntax: Select all

return None
as of just

Syntax: Select all

return
If you don't provide any operands to return, the effect will be the same as of returning None.

Now, it's just a way of interpreting things, but for me it seems that saying "don't return anything to let the original function execute" is less confusing than saying "return None to let the original function execute, return anything else to block it".
Image /id/its_iPlayer
My plugins: Map Cycle • Killstreaker • DeadChat • Infinite Jumping • TripMines • AdPurge • Bot Damage • PLRBots • Entity AntiSpam

Hail, Companion. [...] Hands to yourself, sneak thief. Image

Return to “API Design”

Who is online

Users browsing this forum: No registered users and 10 guests