dobrograd-13-06-2022/garrysmod/addons/util-dlib/lua/dlib/modules/predictedvars.lua
Jonny_Bro (Nikita) e4d5311906 first commit
2023-11-16 15:01:19 +05:00

577 lines
15 KiB
Lua

-- Copyright (C) 2017-2020 DBotThePony
-- Permission is hereby granted, free of charge, to any person obtaining a copy
-- of this software and associated documentation files (the "Software"), to deal
-- in the Software without restriction, including without limitation the rights
-- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
-- of the Software, and to permit persons to whom the Software is furnished to do so,
-- subject to the following conditions:
-- The above copyright notice and this permission notice shall be included in all copies
-- or substantial portions of the Software.
-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
-- INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
-- PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
-- FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
-- OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
-- DEALINGS IN THE SOFTWARE.
-- Those are based on source engine entities
-- Willox (C)
-- it's less of a limitation and more of proper usage
-- a real source mod is going to add dtvars to the player class, they are a compile-time thing so addons can't really do that
-- although tf2 does have certain controller entities that parent to players and have dtvars, so i guess it's not too different to that
-- i didn't think the jetpack would be so many lines of code... not a great example
-- despite everything
-- this module does not work (because entities don't work they way i want to use them for this)
-- consider using DLib.PredictedVarList
-- or NW2Vars when their proper version get merged with stable/x64/chromium branch
local net = net
local DLib = DLib
local string = string
local table = table
local pairs = pairs
local math = math
if SERVER then
net.pool('dlib_pred_ack')
end
DLib.pred = DLib.pred or {}
local pred = DLib.pred
local plyMeta = FindMetaTable('Player')
pred._ack = {}
local lockRebuild = false
pred.Vars = pred.Vars or {}
pred._known = pred._known or {}
pred._Vars = pred._Vars or {}
pred.MaxEnt = pred.MaxEnt or 0
pred.SlotCounters = pred.SlotCounters or {
String = 0,
Bool = 0,
Float = 0,
Int = 0,
Vector = 0,
Angle = 0,
Entity = 0,
}
pred.Slots = pred.Slots or {
String = {},
Bool = {},
Float = {},
Int = {},
Vector = {},
Angle = {},
Entity = {},
}
--[[
@doc
@fname DLib.pred.Reload
@internal
@desc
Forcefully rebuilds all entity definition for mimicking. Does nothing when gmod version is fresh enough.
@enddesc
]]
function pred.Reload()
local vars = pred.Vars
pred.Vars = {}
pred.MaxEnt = 0
pred._known = {}
pred._Vars = {}
pred.SlotCounters = {
String = 0,
Bool = 0,
Float = 0,
Int = 0,
Vector = 0,
Angle = 0,
Entity = 0,
}
lockRebuild = true
for key, value in pairs(vars) do
pred.Define(key, value.type, value.default)
end
lockRebuild = false
for i = 0, pred.MaxEnt do
pred.RebuildEntityDefinition(i)
end
end
--[[
@doc
@fname DLib.pred.Define
@args string identify, string type, any default
@deprecated
@desc
If gmod version is good enough, this function just defines NW2Vars methods to player metatable based
on identify you provided (e.g.)
`plyMeta['Get' .. identify]`
`plyMeta['Set' .. identify]`
`plyMeta['Add' .. identify]`
`plyMeta['Reset' .. identify]`
if gmod version is old, it tries to mimic NW2Vars behavior by creating predicted network entities
(which of course sometimes do and sometimes do not work properly, thanks to gmod)
Also, in such case, you have to call `Player:DLibInvalidatePrediction(true)` and `Player:DLibInvalidatePrediction(false)`
where you would normally call !g:Entity:SetPredictable
`Player:DLibInvalidatePrediction` is safe to be called serverside and does nothing there
Types for `type` argument you can find on [Data Tables](https://wiki.garrysmod.com/page/Networking_Entities) gmod wiki page
This function is marked as deprecated since it is somewhat *very* dirty hack over gmod,
but it probably won't be removed
@enddesc
]]
function pred.Define(identify, mtype, default)
if VERSION >= 0x0002e8fb then
plyMeta['Get' .. identify] = function(self)
return self['GetNW2' .. mtype](self, identify, default)
end
plyMeta['Set' .. identify] = function(self, val)
return self['SetNW2' .. mtype](self, identify, val)
end
plyMeta['Add' .. identify] = function(self, val, min, max)
if min and not max then
if val < 0 then
return self['SetNW2' .. mtype](self, identify, math.max(self['GetNW2' .. mtype](self, identify, default) + val, min))
else
return self['SetNW2' .. mtype](self, identify, math.min(self['GetNW2' .. mtype](self, identify, default) + val, min))
end
elseif min and max then
return self['SetNW2' .. mtype](self, identify, math.clamp(self['GetNW2' .. mtype](self, identify, default) + val, min, max))
end
return self['SetNW2' .. mtype](self, identify, self['GetNW2' .. mtype](self, identify, default) + val)
end
plyMeta['Reset' .. identify] = function(self)
return self['SetNW2' .. mtype](self, identify, default)
end
return
end
if pred.Vars[identify] then
-- assert(pred.Vars[identify].type == mtype, 'Can not change type of variable at runtime')
if pred.Vars[identify].type ~= mtype then
pred.Vars[identify].default = default
pred.Vars[identify].type = mtype
pred.Reload()
return
end
pred.Vars[identify].default = default
return
end
if not pred.SlotCounters[mtype] then
error('Invalid variable type provided: ' .. mtype)
end
local crc = util.CRC(identify):tonumber()
if crc < 0 then
crc = 0xFFFFFFFF + crc
end
pred.Vars[identify] = {
identify = identify,
type = mtype,
--slot = slot,
crc = crc,
default = default
}
local mData = pred.Vars[identify]
local sorted = {}
for key, data in pairs(pred.Vars) do
sorted[data.type] = sorted[data.type] or {}
table.insert(sorted[data.type], data)
end
local function sorter(a, b)
return a.crc > b.crc
end
for itype, list in pairs(sorted) do
table.sort(list, sorter)
for i, data in ipairs(list) do
data.slot = i - 1
end
end
pred._Vars = {}
pred.MaxEnt = 0
for key, data in pairs(pred.Vars) do
local _, entId, realSlot = pred.GetEntityAndSlot(key)
data.realSlot = realSlot
data.entId = entId
pred._Vars[entId] = pred._Vars[entId] or {}
pred._Vars[entId][key] = data
pred.MaxEnt = pred.MaxEnt:max(entId)
end
if not lockRebuild then
for i = 0, pred.MaxEnt do
timer.Create('DLib.pred.DeferReload' .. i, 0, 1, function()
pred.RebuildEntityDefinition(i)
end)
end
end
plyMeta['Get' .. identify] = function(self)
local ent = self.__dlib_pred_ent and self.__dlib_pred_ent[mData.entId + 1]
if not IsValid(ent) then return mData.default end
if not ent['Get' .. identify] then return mData.default end
return ent['Get' .. identify](ent)
end
plyMeta['Set' .. identify] = function(self, newValue)
local ent = self.__dlib_pred_ent and self.__dlib_pred_ent[mData.entId + 1]
if not IsValid(ent) then return end
if not ent['Set' .. identify] then return end
return ent['Set' .. identify](ent, newValue)
end
plyMeta['Add' .. identify] = function(self, val, min, max)
if min and not max then
if val < 0 then
return self['SetNW2' .. mtype](self, identify, math.max(self['Get' .. identify](self) + val, min))
else
return self['SetNW2' .. mtype](self, identify, math.min(self['Get' .. identify](self) + val, min))
end
elseif min and max then
return self['SetNW2' .. mtype](self, identify, math.clamp(self['Get' .. identify](self) + val, min, max))
end
return self['SetNW2' .. mtype](self, identify, self['Get' .. identify](self) + val)
end
plyMeta['Reset' .. identify] = function(self)
return self['Set' .. identify](self, pred.Vars[identify].default)
end
end
--[[
@doc
@fname plyMeta:DLibInvalidatePrediction
@args boolean status = false
@desc
Sets !g:Entity:SetPredictable on all parented DLib vars entities to desired state
Does nothing when gmod version is fresh enough
@enddesc
]]
function plyMeta:DLibInvalidatePrediction(status)
if not self.__dlib_pred_ent or SERVER then return end
for i, ent in ipairs(self.__dlib_pred_ent) do
if IsValid(ent) then
ent:SetPredictable(status or false)
end
end
end
--[[
@doc
@fname DLib.pred.Fingerprint
@args number entitySlotID = nil
@returns
number: the fingerpring
]]
function pred.Fingerprint(entId)
local fingerprint = 0
if not entId then
for key, data in pairs(pred.Vars) do
fingerprint = fingerprint + ((data.crc % 4634661) / 12):floor()
fingerprint = fingerprint + ((util.CRC(data.type):tonumber():rshift(4) % 612348) / 5):floor()
fingerprint = fingerprint + (data.slot:rshift(6) + 23):band(65535)
fingerprint = fingerprint % 5839581561
end
elseif pred._Vars[entId] then
for key, data in pairs(pred._Vars[entId]) do
fingerprint = fingerprint + ((data.crc % 4634661) / 12):floor()
fingerprint = fingerprint + ((util.CRC(data.type):tonumber():rshift(4) % 612348) / 5):floor()
fingerprint = fingerprint + (data.slot:rshift(6) + 23):band(65535)
fingerprint = fingerprint % 5839581561
end
end
return fingerprint
end
--[[
@doc
@fname DLib.pred.RebuildEntityDefinition
@args number entitySlotID, boolean _now = false
@internal
]]
function pred.RebuildEntityDefinition(entId, _now)
if SERVER and not _now then
pred._ack[entId] = player.GetHumans()
if #pred._ack[entId] == 0 then
pred.RebuildEntityDefinition(entId, true)
return
end
net.Start('dlib_pred_ack')
net.WriteUInt8(entId)
net.WriteUInt64(pred.Fingerprint())
net.Broadcast()
timer.Start('DLib.pred.RebuildAck', 10, 1, function()
pred.RebuildEntityDefinition(entId, true)
end)
return
end
pred._ack[entId] = nil
local ENT = {}
ENT.Type = 'anim'
ENT.Spawnable = false
ENT.AdminSpawnable = false
ENT.Author = 'DBotThePony'
ENT.PrintName = 'DLib Predicted player variables bundle'
function ENT:Initialize()
self:SetSolid(SOLID_NONE)
self:SetMoveType(MOVETYPE_NONE)
self:SetNoDraw(true)
self:SetTransmitWithParent(true)
end
function ENT:Draw()
end
function ENT:SetupDataTables()
if not pred._Vars[entId] then return end
for k, v in pairs(pred._Vars[entId]) do
self:NetworkVar(v.type, v.realSlot, k)
end
for k, v in pairs(pred._Vars[entId]) do
self['Set' .. k](self, v.default)
end
end
function ENT:DumpVariables()
local output = {}
if not pred._Vars[entId] then return output end
for k, v in pairs(pred._Vars[entId]) do
if self['Get' .. k] then
output[k] = self['Get' .. k](self)
end
end
return output
end
function ENT:LoadVariables(input)
for k, v in pairs(input) do
if self['Set' .. k] then
self['Set' .. k](self, v)
end
end
end
function ENT:Think()
if not IsValid(self:GetParent()) then
if CLIENT then return end
if not IsValid(self.__dlib_parent) then
SafeRemoveEntity(self)
return
else
self:SetParent(self.__dlib_parent)
self:SetPos(self.__dlib_parent:GetPos())
end
end
local ply = self:GetParent():GetTable()
if not ply.__dlib_pred_ent then
ply.__dlib_pred_ent = {}
end
if SERVER and IsValid(ply.__dlib_pred_ent[entId + 1]) and ply.__dlib_pred_ent[entId + 1] ~= self then
SafeRemoveEntity(ply.__dlib_pred_ent[entId + 1])
end
ply.__dlib_pred_ent[entId + 1] = self
end
scripted_ents.Register(ENT, 'dlib_predictednw' .. entId)
if CLIENT then return end
for i, ent in ipairs(ents.FindByClass('dlib_predictednw' .. entId)) do
local dump = ent:DumpVariables()
local ply = ent:GetParent()
SafeRemoveEntity(ent)
if IsValid(ply) then
ent = ents.Create('dlib_predictednw' .. entId)
ent:SetParent(ply)
ent:SetPos(ply:GetPos())
ent:LoadVariables(dump)
ent:Spawn()
ent:LoadVariables(dump)
ent.__dlib_parent = ply
ent:Activate()
ent:Think()
end
end
for i, ply in ipairs(player.GetAll()) do
if not ply.__dlib_pred_ent then
for entId = 0, pred.MaxEnt do
local ent = ents.Create('dlib_predictednw' .. entId)
ent:SetParent(ply)
ent:SetPos(ply:GetPos())
ent:Spawn()
ent.__dlib_parent = ply
ent:Activate()
ent:Think()
end
else
for entId = 0, pred.MaxEnt do
if not ply.__dlib_pred_ent[entId + 1] then
local ent = ents.Create('dlib_predictednw' .. entId)
ent:SetParent(ply)
ent:SetPos(ply:GetPos())
ent:Spawn()
ent.__dlib_parent = ply
ent:Activate()
ent:Think()
end
end
end
end
end
for entId = 0, pred.MaxEnt do
pred.RebuildEntityDefinition(entId)
end
function pred.GetEntityAndSlot(identify)
local data = assert(pred.Vars[identify], 'Unknown variable name provided')
if data.type == 'String' then
return data.slot, (data.slot - data.slot % 4) / 4, data.slot % 4
end
return data.slot, (data.slot - data.slot % 32) / 32, data.slot % 32
end
if CLIENT then
net.receive('dlib_pred_ack', function()
local entId = net.ReadUInt8()
local fingerprint = net.ReadUInt64()
pred.RebuildEntityDefinition(entId)
local mfinger = pred.Fingerprint(entId)
if mfinger ~= fingerprint then
DLib.MessageError('Integrity check failed in prediction module. Server expected ', fingerprint, ' fingerprint but i got ', mfinger, '. Expect desyncs with server!')
end
net.Start('dlib_pred_ack')
net.WriteUInt8(entId)
net.WriteUInt64(mfinger)
net.SendToServer()
end)
return
end
net.receive('dlib_pred_ack', function(len, ply)
local entId = net.ReadUInt8()
if not pred._ack[entId] then return end
local hit = false
for i, ply2 in ipairs(pred._ack[entId]) do
if ply2 == ply then
hit = true
table.remove(pred._ack[entId], i)
break
end
end
if not hit then return end
local fingerprint = net.ReadUInt64()
local mfinger = pred.Fingerprint(entId)
if mfinger ~= fingerprint then
DLib.MessageError('Integrity check failed in prediction module over client. Client expected ', fingerprint, ' fingerprint, but i got ', mfinger, '. Expect desyncs with that client!')
end
if #pred._ack[entId] == 0 then
pred.RebuildEntityDefinition(entId, true)
end
end)
hook.Add('PlayerAuthed', 'DLib.pred', function(ply)
for entId = 0, pred.MaxEnt do
local ent = ents.Create('dlib_predictednw' .. entId)
ent:SetParent(ply)
ent:SetPos(ply:GetPos())
ent:Spawn()
ent.__dlib_parent = ply
ent:Activate()
ent:Think()
end
end, -3)
hook.Add('PostCleanupMap', 'DLib.pred', function()
for i, ply in ipairs(player.GetAll()) do
if ply.__dlib_pred_ent then
for entId = 0, pred.MaxEnt do
if not IsValid(ply.__dlib_pred_ent[entId + 1]) then
local ent = ents.Create('dlib_predictednw' .. entId)
ent:SetParent(ply)
ent:SetPos(ply:GetPos())
ent:Spawn()
ent.__dlib_parent = ply
ent:Activate()
ent:Think()
end
end
end
end
end, -3)