Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement Listen-Only Hooks #2119

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open

Implement Listen-Only Hooks #2119

wants to merge 2 commits into from

Conversation

Zaurzo
Copy link
Contributor

@Zaurzo Zaurzo commented Sep 20, 2024

This pull request wants to add these 3 functions:

hook.Listen( string eventName, any name, bool isPostHook, function func )
hook.Forget( string eventName, any name, bool isPostHook )
hook.GetListenTable()

There is also a few micro-optimizations done to hook.Add and hook.Remove with their type checking.

Listen-only hooks are hooks that will call regardless if one had returned a value. These hooks will allow us to hook onto events without having to worry about a hook being ran before ours, returning a value, and completely skipping our logic, or about weird addons returning values when they shouldn't.

Example:

hook.Add('PlayerFootstep', 'SmallPuffOnStep', function(ply, pos)
	local ef = EffectData()

	ef:SetOrigin(pos)

	util.Effect('ElectricSpark', ef)
end)

Let's say I wanted to have an effect happen every time I take a step. We use the PlayerFootstep event, and for this example, I made it create small puff balls of smoke at the position of every footstep you take. However, addons can return true to suppress the step sound, and unfortunately for us, the order in which hooks are called in is random. That means such hooks by those addons could run before ours, completely blocking our fancy little effect. Instead, we can use hook.Listen like so:

hook.Listen('PlayerFootstep', 'SmallPuffOnStep', false, function(ply, pos)
	local ef = EffectData()

	ef:SetOrigin(pos)

	util.Effect('ElectricSpark', ef)
end)

That way, even if an addon returns a value in their hook, we still have our fancy smoke effect on each step.

Setting the third argument to true will make the listen-only hook be called after all of the normal hooks are called. This can be used to make sure your logic is only called if a hook return didn't happen. For example, let's say I wanted to track how many times the player toggled their flashlight. I can do this like so

hook.Add('PlayerSwitchFlashlight', 'PlayerFlashlightTracker9000', function(ply)
	ply.FlashlightToggleAmount = (ply.FlashlightToggleAmount or 0) + 1
end)

Addons can block us from toggling our flashlight by returning false. That mean's there is a chance our logic is completely skipped. So, we use hook.Listen. However, listen-only hooks are called before normal hooks, so there's a chance we add onto that variable, even if the toggle was blocked or not. So, by setting the third argument to true, we can make that listen-only hook be called after the rest, that way we can make sure we are counting unblocked flashlight changes, with no worry of our logic being randomly skipped.

hook.Listen('PlayerSwitchFlashlight', 'PlayerFlashlightTracker9000', true, function(ply)
	ply.FlashlightToggleAmount = (ply.FlashlightToggleAmount or 0) + 1
end)

Listen and post listen hooks are put in two separate tables. Listen only hooks can also have objects as identifiers, just like regular hooks.

You can remove listen-only hooks like so:

hook.Forget("EventName", "Identifier")
hook.Forget("EventName", "Identifier", true) -- removes post listen-hook

You can get the listen-only hook tables like so:

local listeners, listenersPost = hook.GetListenTable()

PrintTable(listeners)
PrintTable(listenersPost)

I am awaiting feedback.

@Zvbhrf
Copy link

Zvbhrf commented Oct 2, 2024

Thats a really great addition, but I guess it may conflict with some libraries with priorities, like ULX' hooks or others and break some functionality of some addons.

@robotboy655 robotboy655 added the Addition The pull request adds new functionality. label Oct 2, 2024
@Zaurzo
Copy link
Contributor Author

Zaurzo commented Oct 4, 2024

Thats a really great addition, but I guess it may conflict with some libraries with priorities, like ULX' hooks or others and break some functionality of some addons.

The only problem I see is it conflicting with addons that completely override hook.Call

@Zaurzo
Copy link
Contributor Author

Zaurzo commented Oct 21, 2024

Hi everyone. I want to express a problem here, and a potential solution to said problem. The problem has to do with post listen-only hooks. I added this feature so that, for example, someone could hook onto EntityEmitSound without having to worry about a hook returning true, but wouldn't want their hook to be called if false was returned.

The problem here is that post-listen hooks are not called if a normal hook returned a value. So taking the same example there, it wouldn't matter what was returned (true or false) the hook wouldn't be called. This is not ideal. So I propose a solution that I want your feedback on. I hope these "examples" explain it well.

-- The "rets" argument is a table that contains all of the values returned from the normal hook that returned a value (or values).
-- It is added as the first argument to every listen-only hook function. If the identifier for the hook is an object, "rets" will be the second argument instead.
-- The values in the table are reset right before each listen-only hook call, in-case that one of them messes with the table.
hook.Listen('EntityEmitSound', 'test', function(rets, data)
    if rets[1] == false then return end -- Normal hook returned false prior, don't do anything

    -- Do something with the sound. Sound alert system perhaps?
end)
-- Outside entity
hook.Listen('EntityEmitSound', ent, function(self, rets, data)
    if rets[1] == false then return end

    -- do stuff
end)

----------------------------------

-- Inside entity
function ENT:Initialize()
    hook.Listen('EntityEmitSound', self, self.OnEmitSound)
end

function ENT:OnEmitSound(rets, data)
    if rets[1] == false then return end

    -- do stuff
end

Not only would this solve the problem, it would remove the need to have pre and post listen hooks. We can just have them all called after the normal hooks.

I am looking forward to feedback on this, and whether or not I should update the PR with this. To me, this looks really weird, lol. Let me know what you think.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Addition The pull request adds new functionality.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants