Skip to content

Commit fbd9fba

Browse files
committed
fix(keys): Refactor to fix continuous use handling
We refactored the "use key handling" to correctly work with entities that use the continuous use functionality, like the health station. Previous efforts to consolidate "use key handling" led to a regression where continuous use did not work as intended anymore when directly looking at such an entity. Though, it still worked when using the use key in the proximity of the entity without directly looking at it or running into it with the use key held down, because the server then handled the continuous use by itself. This is behavior that the client cannot "block", so we needed to refactor this code to use the server-side continuous use handling as its only source of truth again. We mainly refactored the server-side "use key handling" to fix this issue and simplify the code. Also, the continous use execution frequency is now every tick again, instead of every 0.1 seconds. The actual continuous execution is now handled by solely by the server again. The `TTT2PlayerUseEntity` net message code was refactored to only handle its special cases like spectator use, remote use and weapon pickup. We send this message from within the bind handler on the client if applicable. This code is still necessary, as these are cases where the normal use handling is not triggered by the server due to the items being out of reach or may not be correctly prioritized e.g. the wrong weapon being picked up instead of the intended looked at entity. The corpse search and old `UseOverride` handling was moved back to the original TTT way of handling it, by utilizing the `KeyRelease` hook. We did not modify the code here and merely copied over the original code again. Removed `ENT.CanUseKey` from placeable entities as it is not needed anymore with the new handling. Removed now obsolete `IsSpecialUsableEntity` and `IsUsableEntity` functions. Refactored `ClientUse` function to be client only and always block further use handling. This function is generally useful for client-side only use key handling, e.g., for UI interactions. E.g. the C4 uses this to open the C4 menu when the use key is pressed while looking at it.
1 parent 5d9a353 commit fbd9fba

File tree

9 files changed

+122
-199
lines changed

9 files changed

+122
-199
lines changed

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,12 @@ All notable changes to TTT2 will be documented here. Inspired by [keep a changel
2020
- Fixed loading screen tips translation (by @mexikoedi)
2121
- Fixed missing `limit_left` translation (by @mexikoedi)
2222
- Fixed `GetHeadshotMultiplier` nil value error (by @mexikoedi)
23+
- Fixed continuous use not working correctly with entities like health stations (by @saibotk)
24+
25+
### Removed / Breaking Changes
26+
27+
- Removed `IsSpecialUsableEntity` & `IsUsableEntity` functions on entities (by @saibotk)
28+
- Changed `ENT:ClientUse()`: Removed return value, now always blocks the default use handling (by @saibotk)
2329

2430
## [v0.14.5b](https://github.com/TTT-2/TTT2/tree/v0.14.5b) (2025-08-18)
2531

gamemodes/terrortown/entities/entities/ttt_base_placeable.lua

Lines changed: 0 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,6 @@ ENT.isDestructible = true
1414

1515
ENT.pickupWeaponClass = nil
1616

17-
ENT.CanUseKey = true
18-
1917
local soundDeny = Sound("HL2Player.UseDeny")
2018

2119
---
@@ -55,29 +53,6 @@ function ENT:PlayerCanPickupWeapon(activator)
5553
return true
5654
end
5755

58-
if CLIENT then
59-
---
60-
-- Hook that is called if a player uses their use key while focusing on the entity.
61-
-- Implement this to predict early if entity can be picked up
62-
-- @return bool True to prevent pickup
63-
-- @realm client
64-
function ENT:ClientUse()
65-
local client = LocalPlayer()
66-
67-
if not IsValid(client) or not client:IsTerror() or not self.pickupWeaponClass then
68-
return true
69-
end
70-
71-
if not self:PlayerCanPickupWeapon(client) then
72-
LANG.Msg("pickup_fail", nil, MSG_MSTACK_WARN)
73-
74-
self:EmitSound(soundDeny)
75-
76-
return true
77-
end
78-
end
79-
end -- CLIENT
80-
8156
if SERVER then
8257
local soundRumble = {
8358
Sound("physics/concrete/concrete_break2.wav"),

gamemodes/terrortown/entities/entities/ttt_c4/shared.lua

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -721,9 +721,7 @@ else -- CLIENT
721721
})
722722

723723
---
724-
-- Hook that is called if a player uses their use key while focusing on the entity.
725-
-- Shows C4 UI
726-
-- @return bool True to prevent pickup
724+
-- This is called if a player uses their use key while in proximity of the entity.
727725
-- @realm client
728726
function ENT:ClientUse()
729727
if IsValid(self) then
@@ -733,8 +731,6 @@ else -- CLIENT
733731
ShowC4Disarm(self)
734732
end
735733
end
736-
737-
return true
738734
end
739735

740736
---

gamemodes/terrortown/entities/entities/ttt_hat_deerstalker.lua

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,6 @@ ENT.PrintName = "hat_deerstalker_name"
1111
ENT.Model = Model("models/ttt/deerstalker.mdl")
1212
ENT.CanHavePrints = false
1313

14-
ENT.CanUseKey = true
15-
1614
---
1715
-- @realm shared
1816
function ENT:SetupDataTables()

gamemodes/terrortown/entities/entities/ttt_health_station.lua

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -198,19 +198,6 @@ else
198198
walkkey = Key("+walk", "WALK"),
199199
}
200200

201-
---
202-
-- Hook that is called if a player uses their use key while focusing on the entity.
203-
-- Early check if client can use the health station
204-
-- @return bool True to prevent pickup
205-
-- @realm client
206-
function ENT:ClientUse()
207-
local client = LocalPlayer()
208-
209-
if not IsValid(client) or not client:IsPlayer() or not client:IsActive() then
210-
return true
211-
end
212-
end
213-
214201
-- handle looking at healthstation
215202
hook.Add("TTTRenderEntityInfo", "HUDDrawTargetIDHealthStation", function(tData)
216203
local client = LocalPlayer()

gamemodes/terrortown/gamemode/client/cl_keys.lua

Lines changed: 50 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,8 @@ function GM:OnSpawnMenuOpen()
2626
end
2727

2828
---
29-
-- Runs when a bind has been pressed. Allows to block commands.
29+
-- Runs when a bind has been pressed. Allows blocking commands.
30+
-- @note This does not block continuous use activation on the server when running into the entity with +use already being pressed.
3031
-- @note By using the "alias" console command, this hook can be effectively circumvented
3132
-- @note To stop the user from using +attack, +left and any other movement commands
3233
-- of the sort, please look into using @{GM:StartCommand} instead
@@ -64,6 +65,17 @@ function GM:PlayerBindPress(ply, bindName, pressed)
6465
return true
6566
end
6667
elseif bindName == "+use" and pressed then
68+
-- Handle special spectator use override
69+
if ply:IsSpec() then
70+
net.Start("TTT2PlayerUseEntity")
71+
net.WriteEntity(nil)
72+
net.WriteString("spectator_use")
73+
net.SendToServer()
74+
75+
-- Suppress the +use bind
76+
return true
77+
end
78+
6779
-- Do old traitor button check
6880
if TBHUD:PlayerIsFocused() then
6981
if ply:KeyDown(IN_WALK) then
@@ -76,47 +88,56 @@ function GM:PlayerBindPress(ply, bindName, pressed)
7688
end
7789

7890
-- Find out if a marker is focused otherwise check normal use
79-
local isClientOnly = false
80-
local useEnt = markerVision.GetFocusedEntity()
81-
local isRemote = IsValid(useEnt)
82-
if not isRemote then
83-
local shootPos = ply:GetShootPos()
84-
85-
local tr = util.TraceLine({
86-
start = shootPos,
87-
endpos = shootPos + ply:GetAimVector() * 100,
88-
filter = ply,
89-
mask = bit.bor(MASK_SOLID, CONTENTS_DEBRIS),
90-
})
91-
92-
useEnt = tr.Entity
93-
94-
if not tr.Hit or not IsValid(useEnt) then
95-
useEnt = nil
96-
end
91+
local markerVisionFocusedEnt = markerVision.GetFocusedEntity()
9792

98-
if useEnt and isfunction(useEnt.ClientUse) then
99-
isClientOnly = useEnt:ClientUse()
100-
end
101-
elseif isfunction(useEnt.RemoteUse) then
93+
if IsValid(markerVisionFocusedEnt) and isfunction(markerVisionFocusedEnt.RemoteUse) then
10294
sound.ConditionalPlay(soundUse, SOUND_TYPE_INTERACT)
10395

104-
isClientOnly = useEnt:RemoteUse(ply)
96+
local clientSideOnly = markerVisionFocusedEnt:RemoteUse()
97+
98+
if not clientSideOnly then
99+
-- Call this on the server too.
100+
-- This cannot be done on the server's Entity:Use hook etc., because we suppress the +use bind here to not trigger it
101+
-- on other close by entities.
102+
103+
net.Start("TTT2PlayerUseEntity")
104+
net.WriteEntity(markerVisionFocusedEnt)
105+
net.WriteString("remote_use")
106+
net.SendToServer()
107+
end
108+
109+
-- Suppress the +use bind here, so it does not trigger the Entity:Use hook on other close by entities.
110+
-- Suppressing means, that the server will not see this key as being pressed anymore, even if the client is still pressing it.
111+
return true
105112
end
106113

107-
-- If returned true by ClientUse or RemoteUse, then dont call Use and UseOverride or RemoteUse serverside
108-
if isClientOnly then
114+
-- This can sometimes deviate from the entity that the server actually uses.
115+
-- But this is fine for now.
116+
-- See https://github.com/Facepunch/garrysmod-issues/issues/5027
117+
local clientUseEnt = ply:GetUseEntity()
118+
119+
-- Handle client side use methods
120+
-- E.g. for showing client side UIs on interaction
121+
if IsValid(clientUseEnt) and isfunction(clientUseEnt.ClientUse) then
122+
clientUseEnt:ClientUse()
123+
109124
return true
110125
end
111126

112-
if IsValid(useEnt) and (ply:IsSpec() or useEnt:IsSpecialUsableEntity()) then
127+
-- Handle special weapon pickup use override
128+
local trace = util.QuickTrace(ply:GetShootPos(), ply:GetAimVector() * 100, ply)
129+
130+
if IsValid(trace.Entity) and trace.Entity:IsWeapon() then
113131
net.Start("TTT2PlayerUseEntity")
114-
net.WriteEntity(useEnt)
115-
net.WriteBool(isRemote)
132+
net.WriteEntity(trace.Entity)
133+
net.WriteString("weapon_pickup")
116134
net.SendToServer()
117135

136+
-- Suppress the +use bind
118137
return true
119138
end
139+
140+
-- Otherwise the +use is executed as usual, triggering the server-side Entity:Use function.
120141
elseif string.sub(bindName, 1, 4) == "slot" and pressed then
121142
local idx = tonumber(string.sub(bindName, 5, -1)) or 1
122143

gamemodes/terrortown/gamemode/server/sv_entity.lua

Lines changed: 0 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,6 @@
22
-- @ref https://wiki.facepunch.com/gmod/Entity
33
-- @class Entity
44

5-
-- Caps taken from here: https://github.com/ValveSoftware/source-sdk-2013/blob/55ed12f8d1eb6887d348be03aee5573d44177ffb/mp/src/game/shared/baseentity_shared.h#L21-L38
6-
FCAP_IMPULSE_USE = 16
7-
FCAP_CONTINUOUS_USE = 32
8-
FCAP_ONOFF_USE = 64
9-
FCAP_DIRECTIONAL_USE = 128
10-
FCAP_USE_ONGROUND = 256
11-
FCAP_USE_IN_RADIUS = 512
12-
135
local safeCollisionGroups = {
146
[COLLISION_GROUP_WEAPON] = true,
157
}
@@ -78,38 +70,6 @@ function entmeta:Spawn()
7870
oldSpawn(self)
7971
end
8072

81-
---
82-
-- Checks if the entity has any use functionality attached. This can be attached in the engine/via hammer or by
83-
-- setting `.CanUseKey` to true. Player ragdolls and weapons always have use functionality attached.
84-
-- @param[default=0] number requiredCaps Use caps that are required for this entity
85-
-- @return boolean Returns true if the entity is usable by the player
86-
-- @ref https://github.com/ValveSoftware/source-sdk-2013/blob/0d8dceea4310fde5706b3ce1c70609d72a38efdf/sp/src/game/server/player.cpp#L2766C71-L2781
87-
-- @realm server
88-
function entmeta:IsUsableEntity(requiredCaps)
89-
requiredCaps = requiredCaps or 0
90-
91-
-- special case: TTT specific lua based use interactions
92-
-- when we're looking for specifically the lua use
93-
if self:IsSpecialUsableEntity() then
94-
return true
95-
end
96-
97-
local caps = self:ObjectCaps()
98-
99-
if
100-
bit.band(
101-
caps,
102-
bit.bor(FCAP_IMPULSE_USE, FCAP_CONTINUOUS_USE, FCAP_ONOFF_USE, FCAP_DIRECTIONAL_USE)
103-
)
104-
> 0
105-
and bit.band(caps, requiredCaps) == requiredCaps
106-
then
107-
return true
108-
end
109-
110-
return false
111-
end
112-
11373
---
11474
-- Some sounds are important enough that they shouldn't be affected by CPASAttenuationFilter
11575
-- @param string snd The name of the sound to be played

0 commit comments

Comments
 (0)