Initial commit

This commit is contained in:
pavetr
2026-03-15 14:54:49 +03:00
commit 64f8029c06
4027 changed files with 254888 additions and 0 deletions

View File

@@ -0,0 +1,587 @@
FPP = FPP or {}
local plyMeta = FindMetaTable("Player")
local entMeta = FindMetaTable("Entity")
local bit_bor = bit.bor
local bit_band = bit.band
--[[-------------------------------------------------------------------------
Entity data explanation.
Every ent has a field FPPCanTouch. This is a table with one entry per player.
Every bit in FPPCanTouch represents a type.
The first bit says whether the player can physgun the ent
The second bit whether the player can use gravun on the ent
etc.
Then there is the FPPCanTouchWhy var This var follows the same idea as FPPCanTouch
except there are five bits for each type. These 4 bits represent a number that shows the reason
why a player can or cannot touch a prop.
---------------------------------------------------------------------------]]
local touchTypes = {
Physgun = 1,
Gravgun = 2,
Toolgun = 4,
PlayerUse = 8,
EntityDamage = 16
}
local touchTypeNumbers = {
[1] = "Physgun",
[2] = "Gravgun",
[4] = "Toolgun",
[8] = "PlayerUse",
[16] = "EntityDamage"
}
local reasonSize = 4 -- bits
local reasonNumbers = {
["owner"] = 1,
["world"] = 2,
["disconnected"] = 3,
["blocked"] = 4,
["constrained"] = 5,
["buddy"] = 6,
["shared"] = 7,
["player"] = 8,
}
--[[-------------------------------------------------------------------------
Utility functions
---------------------------------------------------------------------------]]
local function getPlySetting(ply, settingName)
return (ply[settingName] or ply:GetInfo(settingName)) == "1"
end
local function getSetting(touchType)
return touchType .. "1", string.upper("FPP_" .. touchType .. "1")
end
local constraints = { -- These little buggers think they're not constraints, but they are
["phys_spring"] = true,
}
local function isConstraint(ent)
return ent:IsConstraint() or constraints[ent:GetClass()] or false
end
--[[-------------------------------------------------------------------------
Touch calculations
---------------------------------------------------------------------------]]
local hardWhiteListed = { -- things that mess up when not allowed
["worldspawn"] = true, -- constraints with the world
["gmod_anchor"] = true, -- used in slider constraints with world
["gmod_wire_hologram"] = true, -- created by Expression2, has no physics, but can be constrained to other entities
["phys_spring"] = true, -- https://developer.valvesoftware.com/wiki/Phys_spring
["env_projectedtexture"] = true, -- https://developer.valvesoftware.com/wiki/Env_projectedtexture
["keyframe_rope"] = true, -- https://developer.valvesoftware.com/wiki/Keyframe_rope
["env_skypaint"] = true, -- https://developer.valvesoftware.com/wiki/Env_skypaint
["env_fog_controller"] = true -- https://developer.valvesoftware.com/wiki/Env_fog_controller
}
local function calculateCanTouchForType(ply, ent, touchType)
if not IsValid(ent) then return false, 0 end
ply.FPP_Privileges = ply.FPP_Privileges or {}
local class = ent:GetClass()
local setting, tablename = getSetting(touchType)
local FPPSettings = FPP.Settings[tablename]
-- hard white list
if hardWhiteListed[class] then return true, reasonNumbers.world end
-- picking up players
if touchType == "Physgun" and ent:IsPlayer() and not getPlySetting(ply, "cl_pickupplayers") then
return false, reasonNumbers.player
end
-- blocked entity
local whitelist = FPPSettings.iswhitelist ~= 0
local isInList = FPP.Blocked[setting][string.lower(class)] or false
local isAdmin = ply.FPP_Privileges.FPP_TouchOtherPlayersProps
local isBlocked = whitelist ~= isInList -- XOR
if isBlocked then
local adminsCanTouchBlocked = FPPSettings.admincanblocked ~= 0
local playersCanBlocked = FPPSettings.canblocked ~= 0
local canPhysgunBlocked = (playersCanBlocked or isAdmin and adminsCanTouchBlocked) and not getPlySetting(ply, "FPP_PrivateSettings_BlockedProps")
if not canPhysgunBlocked then
return canPhysgunBlocked, reasonNumbers.blocked
end
end
-- touch own props
local owner = ent.FPPOwner -- Circumvent CPPI for micro-optimisation
if owner == ply then
return not getPlySetting(ply, "FPP_PrivateSettings_OwnProps"),
reasonNumbers.owner
end
local noTouchOtherPlayerProps = getPlySetting(ply, "FPP_PrivateSettings_OtherPlayerProps")
-- Shared entity
if ent.AllowedPlayers and table.HasValue(ent.AllowedPlayers, ply) then
return not noTouchOtherPlayerProps, reasonNumbers.shared
end
if ent["Share" .. setting] then return not noTouchOtherPlayerProps, reasonNumbers.shared end
if IsValid(owner) then
-- Player is buddies with the owner of the entity
if owner.Buddies and owner.Buddies[ply] and owner.Buddies[ply][touchType] then return not noTouchOtherPlayerProps, reasonNumbers.buddy end
-- Someone else's prop
local adminProps = FPPSettings.adminall ~= 0
return isAdmin and adminProps and not noTouchOtherPlayerProps, reasonNumbers.owner
end
-- World props and disconnected players' props
local adminWorldProps = FPPSettings.adminworldprops ~= 0
local peopleWorldProps = FPPSettings.worldprops ~= 0
local restrictWorld = getPlySetting(ply, "FPP_PrivateSettings_WorldProps")
return not restrictWorld and (peopleWorldProps or (isAdmin and adminWorldProps)),
owner == nil and reasonNumbers.world or reasonNumbers.disconnected
end
local blockedEnts = {
["ai_network"] = true,
["network"] = true, -- alternative name for ai_network
["ambient_generic"] = true,
["beam"] = true,
["bodyque"] = true,
["env_soundscape"] = true,
["env_sprite"] = true,
["env_sun"] = true,
["env_tonemap_controller"] = true,
["func_useableladder"] = true,
["gmod_hands"] = true,
["info_ladder_dismount"] = true,
["info_player_start"] = true,
["info_player_terrorist"] = true,
["light_environment"] = true,
["light_spot"] = true,
["physgun_beam"] = true,
["player_manager"] = true,
["point_spotlight"] = true,
["predicted_viewmodel"] = true,
["scene_manager"] = true,
["shadow_control"] = true,
["soundent"] = true,
["spotlight_end"] = true,
["water_lod_control"] = true,
["gmod_gamerules"] = true,
["bodyqueue"] = true,
["phys_bone_follower"] = true,
}
function FPP.calculateCanTouch(ply, ent)
local canTouch = 0
local reasons = 0
local i = 0
for Bit, touchType in pairs(touchTypeNumbers) do
local canTouchType, why = calculateCanTouchForType(ply, ent, touchType)
if canTouchType then
canTouch = bit.bor(canTouch, Bit)
end
reasons = bit.bor(reasons, bit.lshift(why, i * reasonSize))
i = i + 1
end
ent.FPPCanTouch = ent.FPPCanTouch or {}
ent.FPPCanTouchWhy = ent.FPPCanTouchWhy or {}
local changed = ent.FPPCanTouch[ply] ~= canTouch or ent.FPPCanTouchWhy[ply] ~= reasons or ent.FPPOwnerChanged
ent.FPPCanTouch[ply] = canTouch
ent.FPPCanTouchWhy[ply] = reasons
return changed
end
-- try not to call this with both player.GetAll() and ents.GetAll()
local function recalculateCanTouch(players, entities)
for k, v in pairs(entities) do
if not IsValid(v) then entities[k] = nil continue end
if v:IsEFlagSet(EFL_SERVER_ONLY) then entities[k] = nil continue end
if blockedEnts[v:GetClass()] then entities[k] = nil continue end
if v:IsWeapon() and IsValid(v:GetOwner()) then entities[k] = nil continue end
end
for _, ply in pairs(players) do
if not IsValid(ply) then continue end
-- optimisations
ply.FPPIsAdmin = ply.FPP_Privileges.FPP_TouchOtherPlayersProps
ply.FPP_PrivateSettings_OtherPlayerProps = ply:GetInfo("FPP_PrivateSettings_OtherPlayerProps")
ply.cl_pickupplayers = ply:GetInfo("cl_pickupplayers")
ply.FPP_PrivateSettings_BlockedProps = ply:GetInfo("FPP_PrivateSettings_BlockedProps")
ply.FPP_PrivateSettings_OwnProps = ply:GetInfo("FPP_PrivateSettings_OwnProps")
ply.FPP_PrivateSettings_WorldProps = ply:GetInfo("FPP_PrivateSettings_WorldProps")
local changed = {}
for _, ent in pairs(entities) do
local hasChanged = FPP.calculateCanTouch(ply, ent)
if hasChanged then table.insert(changed, ent) end
end
FPP.plySendTouchData(ply, changed)
-- end optimisations
ply.FPP_PrivateSettings_OtherPlayerProps = nil
ply.cl_pickupplayers = nil
ply.FPP_PrivateSettings_BlockedProps = nil
ply.FPP_PrivateSettings_OwnProps = nil
ply.FPP_PrivateSettings_WorldProps = nil
ply.FPPIsAdmin = nil
end
end
function FPP.recalculateCanTouch(plys, ens)
FPP.calculatePlayerPrivilege("FPP_TouchOtherPlayersProps", function() recalculateCanTouch(plys, ens) end)
end
--[[-------------------------------------------------------------------------
Touch interface
---------------------------------------------------------------------------]]
function FPP.plyCanTouchEnt(ply, ent, touchType)
local entTable = ent:GetTable()
local entCanTouch = entTable.FPPCanTouch
if not entCanTouch then
entCanTouch = {}
entTable.FPPCanTouch = entCanTouch
end
entCanTouch[ply] = entCanTouch[ply] or 0
local allowedPlayers = entTable.AllowedPlayers
if not allowedPlayers then
entTable.AllowedPlayers = {}
end
local canTouch = entCanTouch[ply]
-- if an entity is constrained, return the least of the rights
local entRestrictConstraint = entTable.FPPRestrictConstraint
if entRestrictConstraint and entRestrictConstraint[ply] then
canTouch = bit_band(entRestrictConstraint[ply], entCanTouch[ply])
end
-- return the answer for every touch type if parameter is empty
if not touchType then
return canTouch
end
return bit_bor(canTouch, touchTypes[touchType]) == canTouch
end
local GetTable = entMeta.GetTable
function FPP.entGetOwner(ent)
return GetTable(ent).FPPOwner
end
--[[-------------------------------------------------------------------------
Networking
---------------------------------------------------------------------------]]
util.AddNetworkString("FPP_TouchabilityData")
-- Sends 13 + 8 + 5 + 20 = 46 bits of ownership data per entity
local function netWriteEntData(ply, ent)
-- EntIndex for when it's out of the PVS of the player
net.WriteUInt(ent:EntIndex(), 13)
local owner = ent:CPPIGetOwner()
net.WriteUInt(IsValid(owner) and owner:EntIndex() or 255, 8)
local entTable = ent:GetTable()
net.WriteUInt(entTable.FPPRestrictConstraint and entTable.FPPRestrictConstraint[ply] or entTable.FPPCanTouch[ply], 5) -- touchability information
net.WriteUInt(entTable.FPPConstraintReasons and entTable.FPPConstraintReasons[ply] or entTable.FPPCanTouchWhy[ply], 20) -- reasons
end
-- The net message gets too big with 5826 or more entities. In that case,
-- break up the input into chunks and recurse per chunk.
--
-- Note: There is still a limit on the entity count! If there are too many
-- entities, the client will disconnect with "Disconnect: Client 0
-- overflowed reliable channel..". That limit is above the entity limit of
-- 16384, though, so no further work is done to lift that limit.
local count_limit = 5825
function FPP.plySendTouchData(ply, ents)
local count = #ents
if count == 0 then return end
if count > count_limit then
local accumulator = {}
for i = 1, count do
table.insert(accumulator, ents[i])
if i % count_limit == 0 then
FPP.plySendTouchData(ply, accumulator)
accumulator = {}
end
end
if #accumulator > 0 then
FPP.plySendTouchData(ply, accumulator)
end
return
end
net.Start("FPP_TouchabilityData")
for i = 1, count do
netWriteEntData(ply, ents[i])
net.WriteBit(i == count)
end
net.Send(ply)
end
--[[-------------------------------------------------------------------------
Events that trigger recalculation
---------------------------------------------------------------------------]]
local function handleConstraintCreation(ent)
local ent1, ent2 = ent:GetConstrainedEntities()
ent1, ent2 = ent1 or ent.Ent1, ent2 or ent.Ent2
if not ent1 or not ent2 or not ent1.FPPCanTouch or not ent2.FPPCanTouch then return end
local reason = 0
local i = 0
for Bit, touchType in pairs(touchTypeNumbers) do
reason = bit.bor(reason, bit.lshift(reasonNumbers.constrained, i * reasonSize))
i = i + 1
end
for _, ply in ipairs(player.GetAll()) do
local touch1, touch2 = FPP.plyCanTouchEnt(ply, ent1), FPP.plyCanTouchEnt(ply, ent2)
-- The constrained entities have the same touching rights.
if touch1 == touch2 then continue end
local restrictedAccess = bit.band(touch1, touch2)
local send = {}
for _, e in pairs(constraint.GetAllConstrainedEntities(ent1) or {}) do
if not IsValid(e) then continue end
if FPP.plyCanTouchEnt(ply, e) == restrictedAccess then continue end
e.FPPRestrictConstraint = e.FPPRestrictConstraint or {}
e.FPPConstraintReasons = e.FPPConstraintReasons or {}
e.FPPRestrictConstraint[ply] = restrictedAccess
e.FPPConstraintReasons[ply] = reason
table.insert(send, e)
end
FPP.plySendTouchData(ply, send)
end
end
--[[-------------------------------------------------------------------------
On entity created
---------------------------------------------------------------------------]]
local function onEntitiesCreated(ents)
-- Table from player to list of entities that need to be networked
local sendToPlayers = {}
for _, ent in ipairs(ents) do
if not ent:IsValid() then continue end
if isConstraint(ent) then
handleConstraintCreation(ent)
continue
end
-- Don't send information about server only entities to the clients
if ent:GetSolid() == 0 or ent:IsEFlagSet(EFL_SERVER_ONLY) then
continue
end
if blockedEnts[ent:GetClass()] then continue end
for _, ply in ipairs(player.GetAll()) do
local changed = FPP.calculateCanTouch(ply, ent)
-- Only send data that has been changed
if not changed then continue end
sendToPlayers[ply] = sendToPlayers[ply] or {}
table.insert(sendToPlayers[ply], ent)
end
end
for ply, entsToSend in pairs(sendToPlayers) do
FPP.plySendTouchData(ply, entsToSend)
end
end
-- Make a queue of entities created per frame, so the server will send out a maximum-
-- of one message per player per frame
local entQueue = {}
local timerFunc = function()
onEntitiesCreated(entQueue)
entQueue = {}
timer.Remove("FPP_OnEntityCreatedTimer")
end
hook.Add("OnEntityCreated", "FPP_EntityCreated", function(ent)
table.insert(entQueue, ent)
if timer.Exists("FPP_OnEntityCreatedTimer") then return end
timer.Create("FPP_OnEntityCreatedTimer", 0, 1, timerFunc)
end)
--[[-------------------------------------------------------------------------
On entity removed
---------------------------------------------------------------------------]]
-- Recalculates touchability information for constrained entities
-- Note: Assumes normal touchability information is up to date!
-- Update constraints, O(players * (entities + constraints))
function FPP.RecalculateConstrainedEntities(players, entities)
for i, ent in pairs(entities) do
if not IsValid(ent) then entities[i] = nil continue end
if ent:IsEFlagSet(EFL_SERVER_ONLY) then entities[i] = nil continue end
if blockedEnts[ent:GetClass()] then entities[i] = nil continue end
ent.FPPRestrictConstraint = ent.FPPRestrictConstraint or {}
ent.FPPConstraintReasons = ent.FPPConstraintReasons or {}
end
-- constrained entities form a graph.
-- and graphs are things you can traverse with BFS
for _, ply in pairs(players) do
local discovered = {}
-- BFS vars
local BFSQueue = {}
local black, gray = {}, {} -- black = fully discovered, gray = seen, but discovery from this point is needed
local value -- used as key and value of the BFSQueue
for _, ent in pairs(entities) do
if discovered[ent] then continue end -- We've seen this ent in a graph
ent.FPPCanTouch = ent.FPPCanTouch or {}
ent.FPPCanTouch[ply] = ent.FPPCanTouch[ply] or 0
local left, right = 1, 2
BFSQueue[left] = ent
local FPP_CanTouch = ent.FPPCanTouch[ply] -- Find out the canTouch state
while BFSQueue[left] do
value = BFSQueue[left]
BFSQueue[left] = nil
left = left + 1
for _, constr in pairs(value.Constraints or {}) do
local otherEnt = constr.Ent1 == value and constr.Ent2 or constr.Ent1
if not IsValid(otherEnt) or gray[otherEnt] or black[otherEnt] then continue end
gray[otherEnt] = true
BFSQueue[right] = otherEnt
right = right + 1
end
black[value] = true
discovered[value] = true
-- The entity doesn't necessarily have CanTouch data at this point
value.FPPCanTouch = value.FPPCanTouch or {}
value.FPPCanTouch[ply] = value.FPPCanTouch[ply] or 0
FPP_CanTouch = bit.band(FPP_CanTouch or 0, value.FPPCanTouch[ply])
end
-- now update the ents to the client
local updated = {}
for e in pairs(black) do
if FPP.plyCanTouchEnt(ply, e) ~= FPP_CanTouch then
e.FPPRestrictConstraint = e.FPPRestrictConstraint or {}
e.FPPRestrictConstraint[ply] = e.FPPCanTouch[ply] ~= FPP_CanTouch and FPP_CanTouch or nil
table.insert(updated, e)
end
end
FPP.plySendTouchData(ply, updated)
-- reset BFS information for next BFS round
black = {}
gray = {}
end
end
end
local entMem = {}
local function constraintRemovedTimer(ent1, ent2)
if not IsValid(ent1) and not IsValid(ent2) then return end
entMem = {}
local allConstrainedEnts = {}
table.Add(allConstrainedEnts, constraint.GetAllConstrainedEntities(ent1))
table.Add(allConstrainedEnts, constraint.GetAllConstrainedEntities(ent2))
FPP.RecalculateConstrainedEntities(player.GetAll(), allConstrainedEnts)
end
local function handleConstraintRemoved(ent)
local ent1, ent2 = ent:GetConstrainedEntities()
ent1, ent2 = ent1 or ent.Ent1, ent2 or ent.Ent2
if not IsValid(ent1) or not IsValid(ent2) then return end
-- prevent the function from being called too often when many constraints are removed at once
if entMem[ent1] or entMem[ent2] then return end
entMem[ent1] = true
entMem[ent2] = true
timer.Create("FPP_ConstraintRemovedTimer", 0, 1, function() constraintRemovedTimer(ent1, ent2) end)
end
local function onEntityRemoved(ent)
if isConstraint(ent) then handleConstraintRemoved(ent) end
end
hook.Add("EntityRemoved", "FPP_OnEntityRemoved", onEntityRemoved)
--[[-------------------------------------------------------------------------
Player disconnected
---------------------------------------------------------------------------]]
local function playerDisconnected(ply)
local ownedEnts = {}
for _, ent in ipairs(ents.GetAll()) do
if ent:CPPIGetOwner() == ply then
table.insert(ownedEnts, ent)
end
end
timer.Simple(0, function() FPP.recalculateCanTouch(player.GetAll(), ownedEnts) end)
end
hook.Add("PlayerDisconnected", "FPP_PlayerDisconnected", playerDisconnected)
--[[-------------------------------------------------------------------------
Usergroup changed
---------------------------------------------------------------------------]]
local function userGroupRecalculate(ply)
if not IsValid(ply) or not ply:IsPlayer() then return end
timer.Create("FPP_recalculate_cantouch_" .. ply:UserID(), 0, 1, function()
FPP.recalculateCanTouch({ply}, ents.GetAll())
end)
end
FPP.oldSetUserGroup = FPP.oldSetUserGroup or plyMeta.SetUserGroup
function plyMeta:SetUserGroup(group)
userGroupRecalculate(self)
return FPP.oldSetUserGroup(self, group)
end
FPP.oldSetNWString = FPP.oldSetNWString or entMeta.SetNWString
function entMeta:SetNWString(str, val)
if str ~= "usergroup" then return FPP.oldSetNWString(self, str, val) end
userGroupRecalculate(self)
return FPP.oldSetNWString(self, str, val)
end
FPP.oldSetNetworkedString = FPP.oldSetNetworkedString or entMeta.SetNetworkedString
function entMeta:SetNetworkedString(str, val)
if str ~= "usergroup" then return FPP.oldSetNetworkedString(self, str, val) end
userGroupRecalculate(self)
return FPP.oldSetNetworkedString(self, str, val)
end