diff --git a/lua/stargate/shared/tracelines.lua b/lua/stargate/shared/tracelines.lua index 6b517ed1..6c69a46f 100644 --- a/lua/stargate/shared/tracelines.lua +++ b/lua/stargate/shared/tracelines.lua @@ -1,5 +1,5 @@ -/* - Stargate Lib for GarrysMod10 +--[[ + Stargate Lib for Garry's Mod 10 Copyright (C) 2007 aVoN This program is free software: you can redistribute it and/or modify @@ -14,17 +14,29 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . -*/ +]] --######################################### --- Traclines - To stop them on lua drawn physboxes +-- Traclines - To stop them on lua drawn physboxes --######################################### StarGate.Trace = StarGate.Trace or {}; StarGate.Trace.Entities = StarGate.Trace.Entities or {}; StarGate.Trace.Classes = StarGate.Trace.Classes or {}; +StarGate.Trace.Code = util.TraceLine; -- Trace code to be executed +StarGate.Trace.Data = { + start = Vector(), -- The start position of the trace + endpos = Vector(), -- The end position of the trace + mins = Vector(), -- The lowest corner of the trace + maxs = Vector(), -- The highest corner of the trace + filter = nil, -- Things the trace should not hit + mask = MASK_SOLID, -- This determines what the trace should hit + collisiongroup = COLLISION_GROUP_NONE, -- What the trace should hit collision group regards + ignoreworld = false, -- Should the trace ignore world or not + output = nil -- Result will be written here instead of returning a new table +}; ---################# Deephook to ents.Create serverside @aVoN +-- ################# Deephook to ents.Create serverside @aVoN if SERVER then hook.Add("OnEntityCreated","StarGate.OnEntityCreated",function(e) if(not IsValid(e) or not StarGate.Trace) then return end; @@ -34,25 +46,25 @@ if SERVER then end) end ---################# Add a class to check to the tracesline calculation @aVoN +-- ################# Add a class to check to the tracesline calculation @aVoN function StarGate.Trace:Add(c,condition) - self.Classes[c] = {Condition=condition}; - for _,v in pairs(ents.FindByClass(c)) do + self.Classes[c] = {Condition = condition}; + for _, v in pairs(ents.FindByClass(c)) do self:GetEntityData(v); end end ---################# Adds a condition-function to the specific entity. This function decides if the trace goes through the BoundingBox or not @aVoN +-- ################# Adds a condition-function to the specific entity. This function decides if the trace goes through the BoundingBox or not @aVoN function StarGate.Trace:AddCondition(c,condition) if(self.Classes[c]) then - self.Classes[c] = {Condition=condition}; + self.Classes[c] = {Condition = condition}; end end ---################# Remoces such a class @aVoN +-- ################# Remoces such a class @aVoN function StarGate.Trace:Remove(c) self.Classes[c] = nil; - for k,_ in pairs(self.Entities) do + for k, _ in pairs(self.Entities) do if(k:IsValid()) then if(k:GetClass() == c) then self.Entities[k] = nil; @@ -63,7 +75,7 @@ function StarGate.Trace:Remove(c) end end ---################# Is the vector given inside the box or outside of it? @aVoN +-- ################# Is the vector given inside the box or outside of it? @aVoN function StarGate.Trace:InBox(pos,Min,Max) if( (pos.x >= Min.x and pos.x <= Max.x) and @@ -77,17 +89,18 @@ end -- Because this might also be usefull for other scripts, were defininig this global StarGate.InBox = function(a,b,c) return StarGate.Trace:InBox(a,b,c) end; ---################# Updates the entities OBB Datas @aVoN +-- ################# Updates the entities OBB Datas @aVoN function StarGate.Trace:GetEntityData(e) if(IsValid(e)) then - local time = CurTime(); - if(not self.Entities[e] or self.Entities[e].Last + 1 < time) then - local offset = e:OBBCenter(); -- We need the OBB's relatively to the Entiti's position, not to the OBBCenter + local now = CurTime(); + if(not self.Entities[e] or self.Entities[e].Last + 1 < now) then + -- We need the OBB relatively to the Entity's position, not to the OBBCenter + local offset = e:OBBCenter(); self.Entities[e] = { - Min = e:OBBMins()+offset, - Max = e:OBBMaxs()+offset, - --Radius = e:BoudingRadius(), - Last = time, + Min = e:OBBMins() + offset, + Max = e:OBBMaxs() + offset, + -- Radius = e:BoudingRadius(), + Last = now, } end return true; @@ -97,80 +110,168 @@ function StarGate.Trace:GetEntityData(e) end end ---################# Helper Function: Makes the direction vector longer and checks if the hitpos is within a specific range (== hit wall) @aVoN +function StarGate.Trace:InSphere(rorg, spos, srad) + local rorg = Vector(rorg); rorg:Sub(spos) + return (rorg:LengthSqr() <= srad^2) +end + +function StarGate.Trace:AmongRay(bpos, rorg, rdir, full) + local sray = Vector(bpos); sray:Sub(rorg); + if(sray:Cross(rdir):LengthSqr() < 0.01) then + local eray = Vector(rorg); eray:Add(rdir); eray:Sub(); + eray.x, eray.y, eray.z = -eray.x, -eray.y, -eray.z; + sdot, edot = sray:Dot(rdir), eray:Dot(rdir); + if(sdot < 0 and edot < 0) then return false; end -- Behind + if(not full and sdot > 0 and edot > 0) then return false; end + return true; -- Position is on the ray in non-full format + end; return false; +end + +--[[ + * Checks whenever ray hits sphere @dvdvideo1234 + * This can be used to check collisions for shields and spheres in general + * I've explained this here: https://math.stackexchange.com/a/2633290/266012 + * Returns the nearest and furthest circle intersection point ( when available ) + * rorg > Ray start origin position. Where are we tracing from. + * rdir > Ray direction vector. Trace direction being checked + * rlen > Ray length forced value overdrives direction ( not mandatory ) + * spos > Sphere position vector. The sphere location in 3D space + * srad > Sphere radius value. The actual sphere size in 3D space + * blen > When enabled considers the ray dot for intersections + * This forces the function to produce actual intersections + * that check whenever the points belong on the ray or not +]] +function StarGate.Trace:HitSphere(rorg, rdir, rlen, spos, srad, blen) + local eque = (rlen and tonumber(rlen) or rdir:Length()); + if(eque <= 0) then return nil end -- No intersection + local rdir = rdir:GetNormalized(); rdir:Mul(equa); -- Read length + local equr, equa = Vector(rorg), eque^2; equr:Sub(spos); -- Sphere norm + local equb, equc = 2 * rdir:Dot(equr), (equr:LengthSqr() - srad^2); + local equd = (equb ^ 2 - 4 * equa * equc) -- Check imaginary roots + if(equd < 0) then return nil end -- No intersection discriminant + local mqua = (1 / (2 * equa)); equd, equb = mqua*math.sqrt(equd), -equb*mqua; + local mpos = Vector(rdir); mpos:Mul(equb - equd); mpos:Add(rorg) + local ppos = Vector(rdir); ppos:Mul(equb + equd); ppos:Add(rorg) + local isin = self:InSphere(rorg, spos, srad) -- Inside check + local xpos, ounr = (isin and ppos or mpos), (isin and -1 or 1) + -- The position that a bullet will actually hit the circle when fired + if(not self:AmongRay(xpos, rorg, rdir, true)) then return nil else + local norm = Vector(xpos); norm:Sub(spos); norm:Normalize() + local frac = xpos:Distance(rorg) / eque; norm:Mul(ounr) + return {HitPos = xpos, Fraction = frac, HitNormal = norm}; + end +end + +-- ################# Helper Function: Makes the direction vector longer and checks if the hitpos is within a specific range (== hit wall) @aVoN function StarGate.Trace:HitWall(coordinate,pos,norm,mul,Min,Max,len,hit_normal) - local old = norm; - local norm = norm*mul; -- Make the normal hit the wall + local norm = norm * mul; -- Make the normal hit the wall local length = norm:Length(); -- The new normal's length! - if(length <= len) then -- The necessary normal length is shorter than the trace's length (we haven't hit anything before we hit the actual object!) + if(length <= len) then -- The necessary normal length is shorter than the trace's length. + -- We haven't hit anything before we hit the actual object! -- Check, if the remaining two coordinates are within the Min/Max range! local hit = pos + norm; - if( - -- The coordinate == "x,y,z" is because of rounding issues. The checked variable has to get skipped - It is on the wall! Sometimes we have 1.999999999 ~= 2 + if( -- The coordinate == "x,y,z" is because of rounding issues. + -- The checked variable has to get skipped - It is on the wall! + -- Sometimes we have 1.999999999 ~= 2 (coordinate == "x" or hit.x >= Min.x and hit.x <= Max.x) and (coordinate == "y" or hit.y >= Min.y and hit.y <= Max.y) and (coordinate == "z" or hit.z >= Min.z and hit.z <= Max.z) - ) then - return {HitPos=hit,Fraction=length/len,HitNormal=hit_normal}; -- The hitpos and fraction! + ) then -- The hitpos and fraction! + return {HitPos = hit, Fraction = length / len, HitNormal = hit_normal}; end end end ---################# This checks one coordinate of the trace's normal @aVoN +-- ################# This checks one coordinate of the trace's normal @aVoN function StarGate.Trace:CheckCoordinate(coordinate,pos,norm,Min,Max,len,in_box) -- I will not check if the trace start position is exactly on a wall, neither I will check, if the start pos is exactly in the center of this entity. - -- Doing this would need me to add some more special exeptions where the probability for these cases are < 0.1% (except you are forcing it) + -- Doing this would need me to add some more special exceptions where the probability for these cases are < 0.1% (except you are forcing it) local hit_normal = Vector(0,0,0); hit_normal[coordinate] = 1; - --################# We are inside the bounding box - Trace to one wall! - if(in_box) then + if(in_box) then -- We are inside the bounding box - Trace to one wall! local mul = 0; if(norm[coordinate] > 0) then -- Norm == 0 has been avoided in StarGate.Trace:New -- The multiplier so the coordinate we're checking is exact on the wall's surface - mul = math.abs((pos[coordinate] - Max[coordinate])/norm[coordinate]); - hit_normal = -1*hit_normal; + mul = math.abs((pos[coordinate] - Max[coordinate]) / norm[coordinate]); + hit_normal = -1 * hit_normal; else - mul = math.abs((pos[coordinate] - Min[coordinate])/norm[coordinate]); + mul = math.abs((pos[coordinate] - Min[coordinate]) / norm[coordinate]); end return self:HitWall(coordinate,pos,norm,mul,Min,Max,len,hit_normal); - else - --################# We are outside the bounding box. + else -- We are outside the bounding box. if(pos[coordinate] < Min[coordinate] and norm[coordinate] > 0) then -- We are below the Minimum and the normal goes up => We can hit - local mul = math.abs((pos[coordinate] - Min[coordinate])/norm[coordinate]); -- The multiplier so the coordinate we're checking is exact on the wall's surface - return self:HitWall(coordinate,pos,norm,mul,Min,Max,len,-1*hit_normal); + local mul = math.abs((pos[coordinate] - Min[coordinate]) / norm[coordinate]); -- The multiplier so the coordinate we're checking is exact on the wall's surface + return self:HitWall(coordinate,pos,norm,mul,Min,Max,len,-1 * hit_normal); elseif(pos[coordinate] > Max[coordinate] and norm[coordinate] < 0) then --Above Max, norm down - local mul = math.abs((pos[coordinate] - Max[coordinate])/norm[coordinate]); + local mul = math.abs((pos[coordinate] - Max[coordinate]) / norm[coordinate]); return self:HitWall(coordinate,pos,norm,mul,Min,Max,len,hit_normal); end end end ---################# Start a traceline which can hit Lua Drawn BoundingBoxes @aVoN -function StarGate.Trace:New(start,dir,ignore) - -- Clients need to add new entities inside this function (Server uses "HookBased" with ents.Create which uses less reouces!) +--[[ + * Converts trace ignore argument to quick indexing @dvdvideo1234 + * Replace this with a dedicated class method in the routine + * Function filters are processed by regular traces and CAP specifics are skipped +]] +function StarGate.Trace:QuickIgnore(ignore) + local quick = {}; + if(type(ignore) == "table") then + for _, v in pairs(ignore) do + quick[v] = true; + end + elseif(ignore) then + quick[ignore] = true; + end + return quick +end + +-- ################# Start a trace line which can hit Lua Drawn BoundingBoxes @aVoN +function StarGate.Trace:New(start,dir,ignore,mask,cogrp,iworld,width) + -- Clients need to add new entities inside this function (Server uses "HookBased" with ents.Create which uses less resources!) if CLIENT then - for k,_ in pairs(self.Classes) do - for _,v in pairs(ents.FindByClass(k)) do + for k, _ in pairs(self.Classes) do + for _, v in pairs(ents.FindByClass(k)) do self:GetEntityData(v); end end end - local trace = util.QuickTrace(start,dir,ignore); - --This is better and faster than using table.HasValue(ignore,e) (nested for loops) - local quick_ignore = {}; - if(type(ignore) == "table") then - for _,v in pairs(ignore) do - quick_ignore[v] = true; - end - elseif(ignore) then - quick_ignore[ignore] = true; + + -- Setup trace parameters and routine code + self.Data.filter = ignore + self.Data.start:Set(start) + self.Data.ignoreworld = tobool(iworld) + self.Data.mask = (tonumber(mask) or MASK_SOLID) + self.Data.endpos:Set(dir); Data.endpos:Add(start) + self.Data.collisiongroup = (tonumber(cogrp) or COLLISION_GROUP_NONE) + + -- Setup trace width and routine @dvdvideo1234 + if(width) then -- Trace cube hull with side of width + local m = (tonumber(width) or 0) / 2 + if(m > 0) then -- Width is a valid non-zero number + self.Data.mins:SetUnpacked(-m, -m, -m) + self.Data.maxs:SetUnpacked( m, m, m) + self.Code = util.TraceHull -- Use New trace + else -- Margin must be a valid non-zero number + self.Code = util.TraceLine -- Use the old trace + end -- Otherwise falls back to using the old trace method + else -- No width so work as before. Fall back to zero width trace + self.Code = util.TraceLine end - local len = dir:Length()*trace.Fraction; -- First of all: The length of the trace. - local norm_world = dir:GetNormal(); -- Get Normal of the dir vector (world coordinates!) - -- We need to sort all entities first according to their distance to the trace-start, or we hit a prop behind a prop instead of the one infront + + -- Run the trace when setup is ready and code is picked + local trace = self.Code(self.Data) + + -- This is better and faster than using table.HasValue(ignore,e) (nested for loops) + local quick_ignore = self:QuickIgnore(self.Data.filter) + + local len = dir:Length() * trace.Fraction; -- First of all: The length of the trace. + local norm_world = dir:GetNormal(); -- Get Normal of the direction vector (world coordinates!) + -- We need to sort all entities first according to their distance to the + -- trace-start or we hit a prop behind a prop instead of the one in front -- Problem noticed by Lynix here: http://img140.imageshack.us/img140/7589/gmflatgrass0017bj9.jpg - local traced_entities = {} -- Lynix modification + local trace_array = {} -- Lynix modification - for e,_ in pairs(self.Entities) do + for e, _ in pairs(self.Entities) do if(not quick_ignore[e]) then if(self:GetEntityData(e)) then -- Update dimension data and check, if the ent is valid! local class = e:GetClass(); @@ -178,37 +279,37 @@ function StarGate.Trace:New(start,dir,ignore) local pos = e:WorldToLocal(start); local in_box = false; - if (class == "shield_core_buble") then + if(class == "shield_core_buble") then in_box = StarGate.IsInShieldCore(e, start); - elseif (class == "shield") then - in_box = (e:GetPos():Distance(start) 0) then - local min = 10000000000; -- Random startvalue - for _,v in pairs(traced_entities) do - local dist = trace.StartPos:Distance(v.HitPos); - if(dist < min) then - min = dist; - trace = v; - end + + --[[ @Lynix @dvdvideo1234 + * First entry is considered the first minimum + * Use for-integer loop as it is way faster than pairs + * Store true trace data reference to a local and compare + * If margin is not defined will be considered as minimum + ]] + local anc, mar = self.Data.start; + for i = 1, #trace_array do + local v = trace_array[i]; + local m = anc:DistToSqr(v.HitPos); + if(not mar or m < mar) then + mar, trace = m, v; end end + -- END OF Lynix modification return trace; end