From 356d72882069a2d8d0a45258bd5c645d95961307 Mon Sep 17 00:00:00 2001 From: Saibotk Date: Fri, 20 Jun 2025 01:40:45 +0200 Subject: [PATCH] wip: refactor +use handling This commit will be split up later --- .../entities/entities/ttt_base_placeable.lua | 6 +- .../entities/entities/ttt_c4/shared.lua | 2 +- .../entities/entities/ttt_hat_deerstalker.lua | 2 - .../entities/entities/ttt_health_station.lua | 21 +-- .../terrortown/gamemode/client/cl_keys.lua | 97 ++++++++---- .../terrortown/gamemode/server/sv_entity.lua | 40 ----- .../terrortown/gamemode/server/sv_player.lua | 138 ++++++++---------- .../terrortown/gamemode/shared/sh_entity.lua | 11 -- 8 files changed, 146 insertions(+), 171 deletions(-) diff --git a/gamemodes/terrortown/entities/entities/ttt_base_placeable.lua b/gamemodes/terrortown/entities/entities/ttt_base_placeable.lua index 2bfaea4a8d..43ab34629c 100644 --- a/gamemodes/terrortown/entities/entities/ttt_base_placeable.lua +++ b/gamemodes/terrortown/entities/entities/ttt_base_placeable.lua @@ -14,8 +14,6 @@ ENT.isDestructible = true ENT.pickupWeaponClass = nil -ENT.CanUseKey = true - local soundDeny = Sound("HL2Player.UseDeny") --- @@ -56,10 +54,12 @@ function ENT:PlayerCanPickupWeapon(activator) end if CLIENT then + -- TODO: Remove? This is not really that efficient anyway --- -- Hook that is called if a player uses their use key while focusing on the entity. -- Implement this to predict early if entity can be picked up - -- @return bool True to prevent pickup + -- @note This is only called once. Continuous use is not supported here. + -- @return bool True to suppress the actual +use operation of the entity on the Server. -- @realm client function ENT:ClientUse() local client = LocalPlayer() diff --git a/gamemodes/terrortown/entities/entities/ttt_c4/shared.lua b/gamemodes/terrortown/entities/entities/ttt_c4/shared.lua index deb1dad9bc..89c025c273 100644 --- a/gamemodes/terrortown/entities/entities/ttt_c4/shared.lua +++ b/gamemodes/terrortown/entities/entities/ttt_c4/shared.lua @@ -723,7 +723,7 @@ else -- CLIENT --- -- Hook that is called if a player uses their use key while focusing on the entity. -- Shows C4 UI - -- @return bool True to prevent pickup + -- @return bool True to only run on the clientside -- @realm client function ENT:ClientUse() if IsValid(self) then diff --git a/gamemodes/terrortown/entities/entities/ttt_hat_deerstalker.lua b/gamemodes/terrortown/entities/entities/ttt_hat_deerstalker.lua index 1bf705ac99..183eba6011 100644 --- a/gamemodes/terrortown/entities/entities/ttt_hat_deerstalker.lua +++ b/gamemodes/terrortown/entities/entities/ttt_hat_deerstalker.lua @@ -11,8 +11,6 @@ ENT.PrintName = "hat_deerstalker_name" ENT.Model = Model("models/ttt/deerstalker.mdl") ENT.CanHavePrints = false -ENT.CanUseKey = true - --- -- @realm shared function ENT:SetupDataTables() diff --git a/gamemodes/terrortown/entities/entities/ttt_health_station.lua b/gamemodes/terrortown/entities/entities/ttt_health_station.lua index 6e20b10a40..ffd6fb2904 100644 --- a/gamemodes/terrortown/entities/entities/ttt_health_station.lua +++ b/gamemodes/terrortown/entities/entities/ttt_health_station.lua @@ -168,6 +168,14 @@ if SERVER then return end + Dev( + 1, + "[TTT2][HealthStation] Player " + .. ply:Nick() + .. " used health station " + .. self:EntIndex() + ) + local t = CurTime() if t < self.NextHeal then return @@ -198,19 +206,6 @@ else walkkey = Key("+walk", "WALK"), } - --- - -- Hook that is called if a player uses their use key while focusing on the entity. - -- Early check if client can use the health station - -- @return bool True to prevent pickup - -- @realm client - function ENT:ClientUse() - local client = LocalPlayer() - - if not IsValid(client) or not client:IsPlayer() or not client:IsActive() then - return true - end - end - -- handle looking at healthstation hook.Add("TTTRenderEntityInfo", "HUDDrawTargetIDHealthStation", function(tData) local client = LocalPlayer() diff --git a/gamemodes/terrortown/gamemode/client/cl_keys.lua b/gamemodes/terrortown/gamemode/client/cl_keys.lua index 3c6bbe0f35..ad1e2aa8a5 100644 --- a/gamemodes/terrortown/gamemode/client/cl_keys.lua +++ b/gamemodes/terrortown/gamemode/client/cl_keys.lua @@ -43,6 +43,8 @@ function GM:PlayerBindPress(ply, bindName, pressed) return end + Dev(2, "[TTT2][Keys] PlayerBindPress: " .. bindName .. " pressed: " .. tostring(pressed)) + if bindName == "invnext" and pressed then if not ply:IsSpec() then WSWITCH:SelectNext() @@ -64,6 +66,19 @@ function GM:PlayerBindPress(ply, bindName, pressed) return true end elseif bindName == "+use" and pressed then + -- Handle special spectator use override + if ply:IsSpec() then + net.Start("TTT2PlayerUseEntity") + net.WriteEntity(nil) + net.WriteString("spectator_use") + net.SendToServer() + + Dev(1, "specuse use triggered") + + -- Suppress the +use bind + return true + end + -- Do old traitor button check if TBHUD:PlayerIsFocused() then if ply:KeyDown(IN_WALK) then @@ -76,45 +91,73 @@ function GM:PlayerBindPress(ply, bindName, pressed) end -- Find out if a marker is focused otherwise check normal use - local isClientOnly = false - local useEnt = markerVision.GetFocusedEntity() - local isRemote = IsValid(useEnt) - if not isRemote then - local tr = util.TraceLine({ - start = ply:GetShootPos(), - endpos = ply:GetShootPos() + ply:GetAimVector() * 100, - filter = ply, - mask = MASK_ALL, - }) - - useEnt = tr.Entity - - if not tr.Hit or not IsValid(useEnt) then - useEnt = nil - end + local markerVisionFocusedEnt = markerVision.GetFocusedEntity() - if useEnt and isfunction(useEnt.ClientUse) then - isClientOnly = useEnt:ClientUse() - end - elseif isfunction(useEnt.RemoteUse) then + if IsValid(markerVisionFocusedEnt) and isfunction(markerVisionFocusedEnt.RemoteUse) then sound.ConditionalPlay(soundUse, SOUND_TYPE_INTERACT) - isClientOnly = useEnt:RemoteUse(ply) - end + local clientsideOnly = markerVisionFocusedEnt:RemoteUse() - -- If returned true by ClientUse or RemoteUse, then dont call Use and UseOverride or RemoteUse serverside - if isClientOnly then + if not clientsideOnly then + -- Call this on the server too. + -- This cannot be done on the server's Entity:Use hook etc., because we suppress the +use bind here to not trigger it + -- on other close by entities. + + net.Start("TTT2PlayerUseEntity") + net.WriteEntity(markerVisionFocusedEnt) + net.WriteString("remote_use") + net.SendToServer() + end + + Dev(1, "markervision use triggered") + + -- Suppress the +use bind here, so it does not trigger the Entity:Use hook on other close by entities. + -- Suppressing means, that the server will not see this key as being pressed anymore, even if the client is still pressing it. return true end - if IsValid(useEnt) and (ply:IsSpec() or useEnt:IsSpecialUsableEntity()) then + -- This can sometimes deviate from the entity that the server actually uses. + -- But this is fine for now. + -- See https://github.com/Facepunch/garrysmod-issues/issues/5027 + local clientUseEnt = ply:GetUseEntity() + + if IsValid(clientUseEnt) and isfunction(clientUseEnt.ClientUse) then + -- This does not reliably block +use on entities. You should not rely on this to block +use on entities. + -- E.g. when pressing +use and running into an enity, the server will still call Entity:Use on that entity. + -- Because this function is not reevaluated, when the player is still pressing +use. + -- TODO: Maybe remove the clientsideOnly? + local clientsideOnly = clientUseEnt:ClientUse() + + Dev( + 2, + "client use triggered on " + .. clientUseEnt:GetClass() + .. " with clientsideOnly: " + .. tostring(clientsideOnly) + ) + + if clientsideOnly then + -- Suppress the +use bind + return true + end + end + + -- Handle special weapon pickup use override + local trace = util.QuickTrace(ply:GetShootPos(), ply:GetAimVector() * 100, ply) + + if IsValid(trace.Entity) and trace.Entity:IsWeapon() then net.Start("TTT2PlayerUseEntity") - net.WriteEntity(useEnt) - net.WriteBool(isRemote) + net.WriteEntity(trace.Entity) + net.WriteString("weapon_pickup") net.SendToServer() + Dev(1, "weapon use triggered") + + -- Suppress the +use bind return true end + + -- Otherwise the +use is executed as usual, triggering the server-side Entity:Use function. elseif string.sub(bindName, 1, 4) == "slot" and pressed then local idx = tonumber(string.sub(bindName, 5, -1)) or 1 diff --git a/gamemodes/terrortown/gamemode/server/sv_entity.lua b/gamemodes/terrortown/gamemode/server/sv_entity.lua index 9618ada46c..e665d618f3 100644 --- a/gamemodes/terrortown/gamemode/server/sv_entity.lua +++ b/gamemodes/terrortown/gamemode/server/sv_entity.lua @@ -2,14 +2,6 @@ -- @ref https://wiki.facepunch.com/gmod/Entity -- @class Entity --- Caps taken from here: https://github.com/ValveSoftware/source-sdk-2013/blob/55ed12f8d1eb6887d348be03aee5573d44177ffb/mp/src/game/shared/baseentity_shared.h#L21-L38 -FCAP_IMPULSE_USE = 16 -FCAP_CONTINUOUS_USE = 32 -FCAP_ONOFF_USE = 64 -FCAP_DIRECTIONAL_USE = 128 -FCAP_USE_ONGROUND = 256 -FCAP_USE_IN_RADIUS = 512 - local safeCollisionGroups = { [COLLISION_GROUP_WEAPON] = true, } @@ -78,38 +70,6 @@ function entmeta:Spawn() oldSpawn(self) end ---- --- Checks if the entity has any use functionality attached. This can be attached in the engine/via hammer or by --- setting `.CanUseKey` to true. Player ragdolls and weapons always have use functionality attached. --- @param[default=0] number requiredCaps Use caps that are required for this entity --- @return boolean Returns true if the entity is usable by the player --- @ref https://github.com/ValveSoftware/source-sdk-2013/blob/0d8dceea4310fde5706b3ce1c70609d72a38efdf/sp/src/game/server/player.cpp#L2766C71-L2781 --- @realm server -function entmeta:IsUsableEntity(requiredCaps) - requiredCaps = requiredCaps or 0 - - -- special case: TTT specific lua based use interactions - -- when we're looking for specifically the lua use - if self:IsSpecialUsableEntity() then - return true - end - - local caps = self:ObjectCaps() - - if - bit.band( - caps, - bit.bor(FCAP_IMPULSE_USE, FCAP_CONTINUOUS_USE, FCAP_ONOFF_USE, FCAP_DIRECTIONAL_USE) - ) - > 0 - and bit.band(caps, requiredCaps) == requiredCaps - then - return true - end - - return false -end - --- -- Some sounds are important enough that they shouldn't be affected by CPASAttenuationFilter -- @param string snd The name of the sound to be played diff --git a/gamemodes/terrortown/gamemode/server/sv_player.lua b/gamemodes/terrortown/gamemode/server/sv_player.lua index b9316931df..f7bcca538c 100644 --- a/gamemodes/terrortown/gamemode/server/sv_player.lua +++ b/gamemodes/terrortown/gamemode/server/sv_player.lua @@ -524,101 +524,91 @@ local function SpecUseKey(ply, ent) end end -local function EntityContinuousUse(ent, ply) - --- - -- @realm server - if hook.Run("PlayerUse", ply, ent) then - ent:Use(ply, ply) - end - - if ply:IsSpec() then - SpecUseKey(ply, ent) - - return - end - - if not ply:IsTerror() then - return - end +--- +-- This is called by a client when using the "+use"-key for special actions +-- that need to suppress the default +use functionality. +-- E.g. spectator's trying to access corpse info etc. +-- +-- This might check the same conditions again, because we need to verify this +-- on the server. Clients need to do the same checks to determine the usage type. +-- +-- @param number len Length of the message +-- @param Player ply Player that sent the message +-- @realm server +-- @internal +net.Receive("TTT2PlayerUseEntity", function(len, ply) + local ent = net.ReadEntity() + local usageType = net.ReadString() - if ent.CanUseKey and ent.UseOverride then - local phys = ent:GetPhysicsObject() + Dev(1, "TTT2PlayerUseEntity: " .. usageType .. " for " .. tostring(ent) .. " by " .. ply:Nick()) - if not IsValid(phys) or phys:HasGameFlag(FVPHYSICS_PLAYER_HELD) then + if usageType == "remote_use" then + if IsValid(ent) and isfunction(ent.RemoteUse) then + Dev(2, "RemoteUse called") + ent:RemoteUse(ply) + end + elseif usageType == "spectator_use" then + if not ply:IsSpec() then return end - ent:UseOverride(ply) - elseif ent.player_ragdoll then - CORPSE.ShowSearch(ply, ent, ply:KeyDown(IN_WALK) or ply:KeyDownLast(IN_WALK)) - - return - elseif ent:IsWeapon() then - ply:SafePickupWeapon(ent, false, true, true, nil) - - return - end - - -- if it is a SENT, this will always return true - -- if it is a map entity, the flag will be checked - if not ent:IsUsableEntity(FCAP_CONTINUOUS_USE) then - return - end + -- Use a custom trace + local trace = util.QuickTrace(ply:GetShootPos(), ply:GetAimVector() * 100, ply) - -- make sure it is called 10 times per second - timer.Simple(0.1, function() - if not IsValid(ent) or not IsValid(ply) then - return + if trace.Hit and IsValid(trace.Entity) then + Dev(2, "Specuse actually called on " .. trace.Entity:GetClass()) + SpecUseKey(ply, trace.Entity) end - - -- make sure the use key is still pressed - if not ply:KeyDown(IN_USE) then + elseif usageType == "weapon_pickup" then + if not ply:IsTerror() or not IsValid(ent) or not ent:IsWeapon() then return end - -- make sure the entity is still in a good position - local distance = ply:GetShootPos():Distance(ent:WorldSpaceCenter()) + -- Double check if the use interaction is valid (serverside) + local trace = util.QuickTrace(ply:GetShootPos(), ply:GetAimVector() * 100, ply) - if distance > 100 + ent:BoundingRadius() then + -- The entity needs to be the same entity as the one being requested + if not trace.Hit or trace.Entity ~= ent then + Dev(1, "Weapon to pickup is not equal to the client side") return end - EntityContinuousUse(ent, ply) - end) -end - ---- --- This is called by a client when using the "+use"-key --- and contains the entity which was detected --- @param number len Length of the message --- @param Player ply Player that sent the message --- @realm server --- @internal -net.Receive("TTT2PlayerUseEntity", function(len, ply) - local ent = net.ReadEntity() - local isRemote = net.ReadBool() + Dev(2, "Picking up weapon") - if not (IsValid(ent) and (ply:IsSpec() or ent:IsSpecialUsableEntity())) then - return + ply:SafePickupWeapon(ent, false, true, true, nil) end +end) - if isRemote then - if isfunction(ent.RemoteUse) then - ent:RemoteUse(ply) - end - +function GM:KeyRelease(ply, key) + if not (key == IN_USE and IsValid(ply) and ply:IsTerror()) then return end - -- Check if the use interaction is possible - -- Add the bounding radius to compensate for center position - local distance = ply:GetShootPos():Distance(ent:WorldSpaceCenter()) - if distance > 100 + ent:BoundingRadius() then - return - end + -- see if we need to do some custom usekey overriding + local tr = util.TraceLine({ + start = ply:GetShootPos(), + endpos = ply:GetShootPos() + ply:GetAimVector() * 84, + filter = ply, + mask = MASK_SHOT, + }) + + if tr.Hit and IsValid(tr.Entity) then + if tr.Entity.CanUseKey and tr.Entity.UseOverride then + local phys = tr.Entity:GetPhysicsObject() + if IsValid(phys) and not phys:HasGameFlag(FVPHYSICS_PLAYER_HELD) then + tr.Entity:UseOverride(ply) + return true + else + -- do nothing, can't +use held objects + return true + end + elseif tr.Entity.player_ragdoll then + CORPSE.ShowSearch(ply, tr.Entity, ply:KeyDown(IN_WALK) or ply:KeyDownLast(IN_WALK)) - EntityContinuousUse(ent, ply) -end) + return true + end + end +end --- -- A hook that is called before a button is pressed. Can be used to cancel the event by returning false. diff --git a/gamemodes/terrortown/gamemode/shared/sh_entity.lua b/gamemodes/terrortown/gamemode/shared/sh_entity.lua index 328d9e24a4..6b95d16594 100644 --- a/gamemodes/terrortown/gamemode/shared/sh_entity.lua +++ b/gamemodes/terrortown/gamemode/shared/sh_entity.lua @@ -40,14 +40,3 @@ end function entmeta:IsButton() return self:IsDefaultButton() or self:IsRotatingButton() end - ---- --- @return boolean --- @realm server -function entmeta:IsSpecialUsableEntity() - return self:IsWeapon() - or self.player_ragdoll - or self:IsPlayerRagdoll() - or self.CanUseKey - or self.RemoteUse -end