--[[ | This file was obtained through the combined efforts | of Madbluntz & Plymouth Antiquarian Society. | | Credits: lifestorm, Gregory Wayne Rossel JR., | Maloy, DrPepper10 @ RIP, Atle! | | Visit for more: https://plymouth.thetwilightzone.ru/ --]] -- Copyright © 2022-2072, Nak, https://steamcommunity.com/id/Nak2/ -- All Rights Reserved. Not allowed to be reuploaded. local NikNaks = NikNaks local util_TraceLine, Vector, table_insert, table_Count, table_SortByKey = util.TraceLine, Vector, table.insert, table.Count, table.SortByKey local floor, band = math.floor, bit.band local FindEntityHull = util.FindEntityHull NikNaks.NodeGraph = {} --- @class NodeGraph local n_meta = {} n_meta.__index = n_meta n_meta.__tostring = function( self ) return "NodeGraph: " .. ( self._file or "New" ) end NikNaks.__metatables["NodeGraph"] = n_meta --- @class ain_node local ain_node = {} ain_node.__tostring = function( self ) return "Ain Node: " .. self:GetID() end ain_node.__index = ain_node --- @class ain_link local ain_link = {} ain_link.__index = ain_link -- Load AIN file. local error_detected = {} do -- Reads and returns the node from AIN file --- @param b BitBuffer local function readNode( b ) --- @class ain_node local t = {} t.pos = b:ReadVector() t.yaw = b:ReadFloat() t.flOffsets = {} for i = 0, ( NikNaks.NUM_HULLS or 10 ) - 1 do t.flOffsets[i] = b:ReadFloat() -- Float end t.nodeType = b:ReadByte() -- Byte t.nodeInfo = b:ReadUShort() -- UShort t.zone = b:ReadShort() -- Clamp Invalid if t.nodeType < NikNaks.NODE_TYPE_INVALID or t.nodeType > ( NikNaks.NODE_TYPE_WATER or 5 ) then t.nodeType = NikNaks.NODE_TYPE_INVALID end -- Trade down ( Cause Source-nodes fly in the air ) if t.nodeType == NikNaks.NODE_TYPE_GROUND then local trace = util_TraceLine( { start = t.pos + Vector( 0, 0, 50 ), endpos = t.pos - Vector( 0, 0, 128 ), mask = MASK_SOLID_BRUSHONLY } ) -- Sometimes trace-line fails for some reason. if trace then t.pos = trace.Hit and trace.HitPos or t.pos end end return setmetatable( t, ain_node ) end --- Read and returns the link from AIN file. --- @param b BitBuffer local function readLink( b ) --- @class ain_link --- @field moves number[] local l = {} l.srcId = b:ReadShort() + 1 -- Short l.destId = b:ReadShort() + 1 -- Short l.moves = {} for i = 0, ( NikNaks.NUM_HULLS or 10 ) - 1 do l.moves[i] = b:ReadByte() -- Byte end return setmetatable( l, ain_link ) end --- Parses the link data. --- @param self NodeGraph --- @param link ain_link local function parseLink( self, link ) local from = self._nodes[link.srcId] local to = self._nodes[link.destId] from._connect[#from._connect + 1] = { to, link } to._connect[#to._connect + 1] = { from, link } if to.zone ~= from.zone then error_detected[to] = true error_detected[from] = true end for i = 0, ( NikNaks.NUM_HULLS or 10 ) - 1 do local _type = link.moves[i] if _type ~= 0 then if not from._connect_hull[i] then from._connect_hull[i] = {} end if not to._connect_hull[i] then to._connect_hull[i] = {} end table_insert( from._connect_hull[i], { to, _type } ) table_insert( to._connect_hull[i], { from, _type } ) end end end -- Easy Add function local function add( tab, x, y, node ) if not tab[x] then tab[x] = {} end local c = tab[x][y] if not c then c = {} tab[x][y] = c end c[#c + 1] = node end --- Function to add the node to a nodegraph. --- @param self NodeGraph --- @param node ain_node local function addNodeToGraph( self, node ) local ng = self._nodegraph local p = node.pos local xf, yf = p.x / 1000, p.y / 1000 local x, y = floor( xf ), floor( yf ) add( ng, x, y, node ) local L = xf % 1 < 0.5 local B = yf % 1 < 0.5 if L then add( ng, x - 1, y, node ) if B then add( ng, x, y - 1, node ) add( ng, x - 1, y - 1, node ) else add( ng, x, y + 1, node ) add( ng, x - 1, y + 1, node ) end else add( ng, x + 1, y, node ) if B then add( ng, x, y - 1, node ) add( ng, x + 1, y - 1, node ) else add( ng, x, y + 1, node ) add( ng, x + 1, y + 1, node ) end end end -- Tries to patch the table error_detected local function scan( node, tab, zone ) for _, t in pairs( node:GetConnections() ) do local n = t[1] if not tab[n:GetID()] then tab[n:GetID()] = n local z = n:GetZone() zone[z] = ( zone[z] or 0 ) + 1 scan( t[1], tab, zone ) end end end --- @return number local function patchZones() local fixes = 0 for _ = 1, table_Count( error_detected ) do local node = next( error_detected ) if not node then break end local tab, zone = {}, {} scan( node, tab, zone ) -- Take the most commen zone zone = table_SortByKey( zone )[1] for _, n in pairs( tab ) do if n.zone ~= zone then fixes = fixes + 1 n.zone = zone end error_detected[n] = nil end end return fixes end -- Load map data and add it to the nodegraph. local l = { -- ["info_hint"] = true, ["info_node_hint"] = NikNaks.NODE_TYPE_GROUND, ["info_node_air_hint"] = NikNaks.NODE_TYPE_AIR, ["info_node_climb"] = NikNaks.NODE_TYPE_CLIMB } local function parseEntity( self, ent ) if not ent.classname then return end local _type = l[ent.classname] if not _type then return end -- local node = self:GetNode(v.nodeid) -- For some reason, node ID won't match the entities. I guess there are some internal magic to correct this, -- but our only way is to use the position-data instead. Note that Z position might change cause of trace. local node = self:FindNode( ent.origin, _type ) if not node then ErrorNoHaltWithStack( "Unable to locate " .. _type .. "'s node!" ) else node.hint = ent end end local function parseMap( self ) for _, v in pairs( NikNaks.CurrentMap:GetEntities() ) do parseEntity( self, v ) end end local thisMap = "maps/graphs/" .. game.GetMap() .. ".ain" --- Loads the NodeGraph from a file. Note: Can output "AIN_ERROR_ZONEPATCH", if the file had invalid zones that got patched. --- @param fileName string --- @return NodeGraph|nil --- @return number AIN_ERROR_* local function loadAin( fileName ) -- Make sure you can't create a nodegraph if entities hasn't been initialised assert( NikNaks.PostInit, "Can't use AIN before InitPostEntity!" ) if not fileName and _nodeG then return _nodeG end if not fileName then fileName = "maps/graphs/" .. game.GetMap() .. ".ain" end if not string.match( fileName, "%.ain$" ) and not string.match( fileName, "%.dat$" ) then -- Add file type fileName = fileName .. ".ain" end --if thisMapObject and fileName == thisMap then return thisMapObject end if not file.Exists( fileName, "GAME" ) then return end --- @type BitBuffer local b = NikNaks.BitBuffer.OpenFile( fileName, "GAME" ) -- Create new NG object --- @class NodeGraph local n = {} n._version = b:ReadLong() n._map_version = b:ReadLong() if n._version ~= 37 then -- This is an old / newer AIN file. local s = n._version > 37 and "newer" or "older" print( "[NodeGraph]: This .AIN version is " .. s .. ", not the supported 37!" ) return nil, NikNaks.AIN_ERROR_VERSIONNUM end n._file = fileName --- @type ain_node[] n._nodes = {} --- @type ain_link[] n._links = {} n._lookup = {} n._nodegraph = {} -- This is a custom table to speed up finding nearby notes. Locating notes is one of the costly things. setmetatable( n, n_meta ) -- Load nodes local num_nodes = b:ReadLong() error_detected = {} for i = 1, num_nodes do -- No limitsh ere --- @class ain_node local a = readNode( b ) a._id = i a._connect = {} -- A list of nodes connect to this. a._connect_hull = {} -- A list of nodes connect to this, by hull. n._nodes[i] = a addNodeToGraph( n, a ) end -- Read Links local num_links = b:ReadLong() for i = 1, num_links do -- No limits here --- @class ain_link local link = readLink( b ) link._id = i n._links[i] = link parseLink( n, link ) end -- Read lookup for i = 1, num_nodes do n._lookup[i] = b:ReadLong() end local err = table.Count( error_detected ) if err > 0 then print( "NodeGraph: Detected zone errors in " .. fileName .. "!" ) print( "NodeGraph: Patching zones .." ) local s = SysTime() local fixed = patchZones() or 0 print( string.format( "NodeGraph: Took %fms to fix " .. fixed .. " nodes.", SysTime() - s ) ) end if fileName == thisMap then parseMap( n ) -- thisMapObject = n end --local leftOver = b:Read() --print("LEFTOVER: ", string.byte(leftOver), #leftOver) return n, err > 0 and NikNaks.AIN_ERROR_PATCHEDDATA end NikNaks.NodeGraph.LoadAin = loadAin local varNG --- Returns the nodegraph for the current map and caches it --- @return NodeGraph|boolean --- @return number? AIN_ERROR_* function NikNaks.NodeGraph.GetMap() assert( NikNaks.PostInit, "Can't use AIN before InitPostEntity!" ) if varNG ~= nil then return varNG end local a, err = loadAin() varNG = a or false return varNG, err end end -- Node Meta do function ain_node:__tostring() return "ain_node [" .. self:GetID() .. "]" end --- Returns the node ID. function ain_node:GetID() return self._id or -1 end --- Returns true if the node is valid. function ain_node:IsValid() return self._id >= 0 and self:GetType() > NikNaks.NODE_TYPE_DELETED end --- Returns the node position. --- @return Vector function ain_node:GetPos( hull ) if not hull then hull = 0 end return self.pos + Vector( 0, 0, self.flOffsets[hull] ) end --- Returns the node YAW. function ain_node:GetYaw() return self.yaw end --- Returns the node type. --- @return number NODE_TYPE function ain_node:GetType() return self.nodeType end --- Returns the node info. function ain_node:GetInfo() return self.nodeInfo end --- Returns the node zone. It should match any notes connected to this one. function ain_node:GetZone() return self.zone end end -- Link Meta do function ain_link:__tostring() return "ain_link [" .. self:GetID() .. "]" end --- Returns the link ID. function ain_link:GetID() return self._id or -1 end --- Returns the move bitflags. --- @param HULL number function ain_link:GetMove( HULL ) return self.moves[HULL or 0] end --- Checks to see if it has any of said move flag --- @param HULL number --- @param flag number function ain_link:HasMoveFlag( HULL, flag ) return band( self.moves[HULL or 0], flag ) ~= 0 end --- Returns the node ID of the source node. function ain_link:GetSrcID() return self.srcId end --- Returns the node IF of the distination node. function ain_link:GetDestID() return self.destId end end -- Special Node Meta do --- Returns all the connections from this node. --- @return table function ain_node:GetConnections() return self._connect end local e_t = {} --- Returns all the connections from this node, with said hull that aren't invalid. --- @param hull number --- @return table function ain_node:GetConnectionsByHull( hull ) return self._connect_hull[hull] or e_t end --- Returns true if the node_type match. --- @param NODE_TYPE number --- @return boolean function ain_node:IsNodeType( NODE_TYPE ) if NODE_TYPE == NikNaks.NODE_TYPE_ANY then return true end if self.nodeType == NODE_TYPE then return true end return false end end -- Get Grid Nodes do local function scan( node, tab ) for _, t in pairs( node:GetConnections() ) do if not tab[t[1]:GetID()] then tab[t[1]:GetID()] = t[1] scan( t[1], tab ) end end end --- A function returning all nodes connected to this one. A bit costly. --- @return table function ain_node:GetAllGridNodes() local p = {} local tab = {} scan( self, tab ) for _, node in pairs( tab ) do p[#p + 1] = node end return p end end -- NodeGraph do -- Adds a dis-cost if node isn't visible. 1 = clear, 10 = blocked local function traceCheck( from, to ) local trace = util_TraceLine( { start = from, endpos = to, mask = MASK_SOLID_BRUSHONLY } ) if not trace then return false end return not trace.Hit and trace.Fraction < 1 end --- Returns the AIN version. Should be 37. function n_meta:GetVersion() return self._version end --- Returns the AIN map-version. function n_meta:GetMapVersion() return self._map_version end --- Returns the given ain_node at said ID. --- @param id number function n_meta:GetNode( id ) return self._nodes[id] end --- Returns a list of all nodes. With the ID as keys. function n_meta:GetAllNodes() return self._nodes end --- Returns the nearest node --- @param position Vector --- @param NODE_TYPE? number --- @param Zone? number --- @return ain_node function n_meta:FindNode( position, NODE_TYPE, Zone, HULL ) NODE_TYPE = NODE_TYPE or NikNaks.NODE_TYPE_GROUND local x, y = floor( position.x / 1000 ), floor( position.y / 1000 ) local c, v -- Use the nodegraph to search, if none at position, scan all nodes ( slow ) local ng = self._nodegraph[x] and self._nodegraph[x][y] if ng then for _, node in pairs( ng ) do if Zone and node.zone ~= Zone then continue end if not node:IsNodeType( NODE_TYPE ) then continue end local vis = traceCheck( position, node:GetPos( HULL ) + Vector( 0, 0, 60 ) ) if not vis then continue end local d = position:DistToSqr( node:GetPos( HULL ) + Vector( 0, 0, 60 ) ) if not c or d < c then c = d v = node end end if v then return v end end -- We didn't find a node within the chunk, or a chunk with no matching note type. We need to scan everything. for _, node in pairs( self._nodes ) do if Zone and node.zone ~= Zone then continue end if not node:IsNodeType( NODE_TYPE ) then continue end local vis = traceCheck( position, node:GetPos( HULL ) + Vector( 0, 0, 60 ) ) local d = position:DistToSqr( node:GetPos() ) if not vis then d = d * 100 end if not c or d < c then c = d v = node end end return v end --- Returns the nearest node with a connection matching the hull. --- @param position Vector --- @param NODE_TYPE? number --- @param HULL number --- @param Zone? number --- @return ain_node function n_meta:FindNodeWithHull( position, NODE_TYPE, Zone, HULL ) NODE_TYPE = NODE_TYPE or NikNaks.NODE_TYPE_GROUND local x, y = floor( position.x / 1000 ), floor( position.y / 1000 ) local c, v -- Use the nodegraph to search, if none at position, scan all nodes ( slow ) local ng = self._nodegraph[x] and self._nodegraph[x][y] if ng then for _, node in pairs( ng ) do if Zone and node.zone ~= Zone then continue end if not node:IsNodeType( NODE_TYPE ) then continue end if #node._connect_hull[HULL] < 1 then continue end local d = position:DistToSqr( node:GetPos( HULL ) + Vector( 0, 0, 60 ) ) if not c or d < c then c = d v = node end end if v then return v end end -- We didn't find a node within the chunk, or a chunk with no matching note type. We need to scan everything. for _, node in pairs( self._nodes ) do if Zone and node.zone ~= Zone then continue end if not node:IsNodeType( NODE_TYPE ) then continue end if #node._connect_hull[HULL] < 1 then continue end local d = position:DistToSqr( node:GetPos( HULL ) + Vector( 0, 0, 60 ) ) if not c or d < c then c = d v = node end end return v end --- Returns the nearest node with said HintType. --- @param position Vector --- @param NODE_TYPE? number --- @param HintType number --- @param HintGroup? number --- @param Zone? number --- @return ain_node function n_meta:FindHintNode( position, NODE_TYPE, HintType, HintGroup, Zone, HULL ) NODE_TYPE = NODE_TYPE or NikNaks.NODE_TYPE_GROUND local x, y = floor( position.x / 1000 ), floor( position.y / 1000 ) local c, v -- Use the nodegraph to search, if none at position, scan all nodes ( slow ) local ng = self._nodegraph[x] and self._nodegraph[x][y] if ng then for _, node in pairs( ng ) do if not node.hint or node.hint.hinttype ~= HintType then continue end if HintGroup and node.hint.group ~= HintGroup then continue end if Zone and node.zone ~= Zone then continue end if not node:IsNodeType( NODE_TYPE ) then continue end local d = position:DistToSqr( node:GetPos( HULL ) ) if not c or d < c then c = d v = node end end if v then return v end end -- We didn't find a node within the chunk, or a chunk with no matching note type. We need to scan everything. for _, node in pairs( self._nodes ) do if not node.hint or node.hint.hinttype ~= HintType then continue end if HintGroup and node.hint.group ~= HintGroup then continue end if Zone and node.zone ~= Zone then continue end if not node:IsNodeType( NODE_TYPE ) then continue end local d = position:DistToSqr( node:GetPos( HULL ) ) if not c or d < c then c = d v = node end end return v end local MAX_NODES = MAX_NODES or 4096 -- This is the limit for Source, not NikNaks. --- Returns the nodegraph as a BitBuffer. --- @return BitBuffer function n_meta:SaveToBuf() local b = NikNaks.BitBuffer() b:WriteLong( self:GetVersion() ) -- Should be 37 b:WriteLong( self._map_version ) -- Write node num local note_num = #self._nodes if note_num > MAX_NODES then print( "[NodeGrpah]: Warning! Reached over the default MAX_NODES limits. " .. fileName .. " will only work with NikNak's pathfinding." ) end b:WriteLong( note_num ) for i = 1, note_num do local node = self._nodes[i] b:WriteVector( node.pos ) b:WriteFloat( node.yaw ) for j = 0, ( NikNaks.NUM_HULLS or 10 ) - 1 do b:WriteFloat( node.flOffsets[j] ) end b:WriteByte( node.nodeType ) b:WriteUShort( node.nodeInfo ) b:WriteShort( node.zone ) end -- Write links. No limits I know of. local num = #self._links b:WriteLong( num ) for i = 1, num do local l = self._links[i] b:WriteShort( l.srcId - 1 ) b:WriteShort( l.destId - 1 ) for ii = 0, ( NikNaks.NUM_HULLS or 10 ) - 1 do b:WriteByte( l.moves[ii] ) end end -- Write lookup for i = 1, note_num do b:WriteLong( self._lookup[i] ) end return b end --- Saves the nodegraph to a file. --- @param filePath string function n_meta:SaveAin( filePath ) filePath = filePath or self._file or "nodegraph" if not string.match( filePath, ".dat$" ) then filePath = filePath .. ".dat" end -- Add file type. We use .dat to stop users from opening it in notepad. self:SaveToBuf():SaveToFile( filePath ) print( "[NodeGraph]: Saved to " .. tostring( "data/" .. filePath ) ) end --- Overrides and generates all the zones in the nodegraph. This can fix zone-errors. function n_meta:GenerateZones() -- Load all nodes to list. local s = SysTime() local all_nodes = {} local nodes = self._nodes for i = 1, #nodes do local node = nodes[i] all_nodes[node:GetID()] = node end -- For each node .. local zone = 0 for _ = 1, #nodes do local id, node = next( all_nodes ) if not id then break end -- No more nodes left. all_nodes[id] = nil -- Remove this node node.zone = zone -- For each node connected to this one, set the zone to match. for _, con in pairs( node:GetAllConnections() ) do con.zone = zone all_nodes[con:GetID()] = nil -- Remove said node from the lookup table. end zone = zone + 1 end print( string.format( "[NodeGraph] Generated zones within: %f", SysTime() - s ) ) end end -- A* PathFinder Node functions do local cost_l, t_cost, open_list, closed_list, move_list = {}, {}, {}, {}, {} function ain_node:ClearSearchLists() cost_l, t_cost, open_list, closed_list, move_list = {}, {}, {}, {}, {} end --- Sets the cost for pathfinding. --- @param cost number function ain_node:SetCostSoFar( cost ) cost_l[self:GetID()] = cost end --- Returns the cost so far. --- @return number function ain_node:GetCostSoFar() return cost_l[self:GetID()] or -1 end --- Sets the total cost. --- @param cost number function ain_node:SetTotalCost( cost ) t_cost[self:GetID()] = cost end --- Returns the total cost. --- @return number function ain_node:GetTotalCost() return t_cost[self:GetID()] or -1 end ---Adds the node to the open list. function ain_node:AddToOpenList() open_list[#open_list + 1] = self end --- Returns true if the node is on the open list. --- @return boolean function ain_node:IsOpen() for i = 1, #open_list do if open_list[i]:GetID() == self:GetID() then return true end end return false end --- Returns true if the open list is empty. --- @return boolean function ain_node:IsOpenListEmpty() if next( open_list ) then return false end return true end local function sorter( a, b ) return a:GetTotalCost() < b:GetTotalCost() end --- Updates the open list. function ain_node:UpdateOnOpenList() table.sort( open_list, sorter ) end --- Pops the open list and returns the kiwest total cost node. function ain_node:PopOpenList() return table.remove( open_list, 1 ) end --- Adds the node to the closed list. function ain_node:AddToClosedList() closed_list[self:GetID()] = true end --- Returns true if the node is within the closed list. --- @return boolean function ain_node:IsClosed() return closed_list[self:GetID()] or false end --- Removes the node from the closed list. function ain_node:RemoveFromClosedList() closed_list[self:GetID()] = nil end --- Sets the move-type to this node. --- @param CAP_MOVE number function ain_node:SetMoveType( CAP_MOVE ) move_list[self:GetID()] = CAP_MOVE end --- Returns the move-type to this node. --- @return number function ain_node:GetMoveType() return move_list[self:GetID()] end end -- A* PathFinder local LPFMeta = NikNaks.__metatables["LPathFollower"] do local function heuristic_cost_estimate( start, goal, HULL ) -- Perhaps play with some calculations on which corner is closest/farthest or whatever return start:GetPos( HULL ):Distance( goal:GetPos( HULL ) ) end local function reconstruct_path( cameFrom, current ) local total_path = { current } while ( cameFrom[current] ) do current = cameFrom[current] table.insert( total_path, current ) end return total_path end -- Finds the best move option local function getMultiplier( moveoptions, canWalk, canJump, canClimb, canFly, JumpMultiplier, ClimbMultiplier ) if canFly and band( moveoptions, NikNaks.CAP_MOVE_FLY ) ~= 0 then -- Flying seems to always be the best option return NikNaks.CAP_MOVE_FLY, 1 end -- We like to jump more than walking if canJump and JumpMultiplier < 1 and band( moveoptions, NikNaks.CAP_MOVE_JUMP ) ~= 0 then return NikNaks.CAP_MOVE_JUMP, JumpMultiplier end if canWalk and band( moveoptions, NikNaks.CAP_MOVE_GROUND ) ~= 0 then return NikNaks.CAP_MOVE_GROUND, 1 elseif canClimb and band( moveoptions, NikNaks.CAP_MOVE_CLIMB ) ~= 0 then -- %20 climb cost return NikNaks.CAP_MOVE_CLIMB, ClimbMultiplier elseif canJump and band( moveoptions, NikNaks.CAP_MOVE_JUMP ) ~= 0 then return NikNaks.CAP_MOVE_JUMP, JumpMultiplier end end --[[ Tries to A* pathfind to the location. true = Same Nodes false = Unable to pathfind at all table = List of nodes from goal towards the start ]] local function AStart( node_start, node_goal, HULL, BitCapability, JumpMultiplier, ClimbMultiplier, generator, MaxDistance ) if not node_start or not node_goal then return false end if node_start == node_goal then return true end local band = band node_start:ClearSearchLists() node_start:AddToOpenList() local cameFrom = {} node_start:SetCostSoFar( 0 ) node_start:SetTotalCost( heuristic_cost_estimate( node_start, node_goal, HULL ) ) node_start:UpdateOnOpenList() local canWalk = band( BitCapability, NikNaks.CAP_MOVE_GROUND ) ~= 0 local canFly = band( BitCapability, NikNaks.CAP_MOVE_FLY ) ~= 0 local canClimb = band( BitCapability, NikNaks.CAP_MOVE_CLIMB ) ~= 0 local canJump = band( BitCapability, NikNaks.CAP_MOVE_JUMP ) ~= 0 while not node_start:IsOpenListEmpty() do local current = node_start:PopOpenList() if ( current == node_goal ) then return reconstruct_path( cameFrom, current ) end current:AddToClosedList() for _, tab in pairs( current:GetConnectionsByHull( HULL ) ) do local neighbor = tab[1] if not neighbor then continue end local moveoptions = band( BitCapability, tab[2] or 0 ) if moveoptions == 0 then continue end -- Unable to use this link. No options local CAP_MOVE, Multi = getMultiplier( moveoptions, canWalk, canJump, canClimb, canFly, JumpMultiplier, ClimbMultiplier ) if not CAP_MOVE then continue end -- Cost calculator local newCostSoFar if not generator then -- Custom generator newCostSoFar = current:GetCostSoFar() + heuristic_cost_estimate( current, neighbor, HULL ) * Multi else -- Default generator -- TODO: Elevator? Check L4D elevator maps and what they are. newCostSoFar = current:GetCostSoFar() + generator( current, neighbor, CAP_MOVE, BitCapability, heuristic_cost_estimate( current, neighbor, HULL ) * Multi ) end if newCostSoFar < 0 or MaxDistance and newCostSoFar > MaxDistance then -- Check if we went over max-distance continue end if ( neighbor:IsOpen() or neighbor:IsClosed() ) and neighbor:GetCostSoFar() <= newCostSoFar then -- This node is already open/close and the cost is shorter continue else neighbor:SetCostSoFar( newCostSoFar ); neighbor:SetTotalCost( newCostSoFar + heuristic_cost_estimate( neighbor, node_goal, HULL ) ) neighbor:SetMoveType( CAP_MOVE ) if ( neighbor:IsClosed() ) then neighbor:RemoveFromClosedList() end if ( neighbor:IsOpen() ) then -- This area is already on the open list, update its position in the list to keep costs sorted neighbor:UpdateOnOpenList() else neighbor:AddToOpenList() end cameFrom[neighbor] = current end end end return false end --- A* pathfinding using the NodeGraph. --- @param start_pos Vector|Entity --- @param end_pos Vector|Entity --- @param NODE_TYPE? number --- @param options? table --- @param HULL_SIZE? number --- @param generator? function -- A funtion that allows you to calculate your own cost: func( node, fromNode, CAP_MOVE, elevator, length ) --- @return LPathFollower|boolean function n_meta:PathFind( start_pos, end_pos, NODE_TYPE, options, HULL_SIZE, generator ) if UseZone == nil then UseZone = true end if not options then options = {} end local MaxDistance = options.MaxDistance or 100000 local BitCapability = options.BitCapability or ( NODE_TYPE == NikNaks.NODE_TYPE_AIR and NikNaks.CAP_MOVE_FLY or NikNaks.CAP_MOVE_GROUND ) -- Make default walk, unless NODE_TYPE is fly ) local JumpMultiplier = options.JumpMultiplier or 1.4 local ClimbMultiplier = options.ClimbMultiplier or 1.2 if not JumpMultiplier then JumpMultiplier = 1.4 -- Default, make it kinda hate jumping around elseif JumpMultiplier < 0.3 then JumpMultiplier = 0.3 end -- Make sure it can't go below 0.3. Can inf loop if so. if not NODE_TYPE then NODE_TYPE = NikNaks.NODE_TYPE_GROUND end -- Default node: Ground. -- Entity checks local ent = start_pos.OBBMins and start_pos local ent_e = end_pos.OBBMins and end_pos if ent then start_pos = ent:GetPos() if not HULL_SIZE then if ent.GetHullType then HULL_SIZE = ent:GetHullType() -- Get the hulltype from the hulltype function, if that is the starting entity. else HULL_SIZE = FindEntityHull( ent ) or 0 -- Get the hull size from the OBB, use Hull Human as fallback end end elseif not HULL_SIZE then HULL_SIZE = 0 -- Hull Human end if ent_e then end_pos = ent_e:GetPos() + ent_e:OBBCenter() end -- Find the start and end node. local start_node = self:FindNode( start_pos, NODE_TYPE ) if not start_node then return false end local offset = start_pos - start_node:GetPos( HULL_SIZE ) -- Sway the position a bit for the node to be located local end_node = self:FindNode( end_pos + offset, NODE_TYPE, start_node:GetZone() ) -- Find an end-node. matching the starting node's zone. if not end_node then return false end -- Path find to location local t = AStart( start_node, end_node, HULL_SIZE, BitCapability, JumpMultiplier, ClimbMultiplier, generator, MaxDistance ) local def_cap = NODE_TYPE == NikNaks.NODE_TYPE_AIR and NikNaks.CAP_MOVE_FLY or NikNaks.CAP_MOVE_GROUND if t == false then -- Unable to pathfind to location return false elseif t == true then -- Same location, return an "empty" path object. local p = LPFMeta.CreatePathFollower( start_pos ) local s = p:AddSegment( start_pos, end_pos, 0, def_cap ) s.target_ent = ent_e -- The last position of the pathfollower, is an entity. p._generator = generator p._MaxDistance = MaxDistance return p else -- A table of locations local p = LPFMeta.CreatePathFollower( start_pos ) local lP = start_pos -- Add end pos for i = #t, 1, -1 do local node = t[i] local s = p:AddSegment( lP, node:GetPos(), 0, node:GetMoveType() or def_cap ) s.node = node lP = t[i]:GetPos() end local s = p:AddSegment( lP, end_pos, 0, def_cap ) s.target_ent = ent_e p._generator = generator p._MaxDistance = MaxDistance return p end end --- A cheap lookup function. Checks to see if we can reach the position using nearby nodes. --- Note that this use zones and might have false positives on maps with a broken NodeGraph. --- @param start_pos Vector --- @param end_pos Vector --- @param NODE_TYPE? number --- @param HULL_SIZE? number --- @param max_dis? number -- Distance to nearest node --- @return boolean function n_meta:CanMaybeReach( start_pos, end_pos, NODE_TYPE, HULL_SIZE, max_dis ) if not HULL_SIZE then HULL_SIZE = 0 end if not NODE_TYPE then NODE_TYPE = NikNaks.NODE_TYPE_GROUND end local a = self:FindNode( start_pos, NODE_TYPE ) if not a then return false elseif max_dis and a:GetPos():Distance( start_pos ) > max_dis then return false end local b = self:FindNode( end_pos, NODE_TYPE, a:GetZone() ) if not b then return false elseif max_dis and b:GetPos():Distance( start_pos ) > max_dis then return false end return true end end -- ASync. Calculates 10 paths pr tick. do local async, run = {}, false local function remove_hook() if not run then return end run = false hook.Remove( "Think","ain_apath" ) end local function add_hook() if run then return end run = true hook.Add( "Think", "ain_apath", function() local n = #async if n < 1 then return remove_hook() -- None left to calculate end for i = n, 1, -1 do local ok, message = coroutine.resume( async[i] ) if ( ok == false ) then ErrorNoHalt( " Error: ", message, "\n" ) table.remove( async, 1 ) elseif message then table.remove( async, 1 ) end end end ) end --- A* pathfinding using the NodeGraph. Returns the result in the callback. Calculates 20 paths pr tick. --- @param start_pos Vector|Entity --- @param end_pos Vector|Entity --- @param callback function -- Returns the result. LPathFollower or false --- @param NODE_TYPE? number --- @param options? table --- @param HULL_SIZE? number --- @param generator? function -- A funtion that allows you to calculate your own cost: func( node, fromNode, CAP_MOVE, elevator, length ) function n_meta:PathFindASync( start_pos, end_pos, callback, NODE_TYPE, options, HULL_SIZE, generator ) add_hook() -- Make sure the async runs local count = 0 local function awaitGen( current, neighbor, CAP_MOVE, elevator, h_cost_estimate ) count = count + 1 if count > 20 then count = 0 coroutine.yield() end if generator then return generator( current, neighbor, CAP_MOVE, elevator, h_cost_estimate ) end return current:GetCostSoFar() + h_cost_estimate end table.insert( async, coroutine.create( function() callback( self:PathFind( start_pos, end_pos, NODE_TYPE, HULL_SIZE, BitCapability, JumpMultiplier, awaitGen, MaxDistance, UseZone ) ) return true end ) ) end end if SERVER then return end do local c = { [NikNaks.NODE_TYPE_AIR ] = Color( 155, 155, 255 ), [NikNaks.NODE_TYPE_GROUND ] = Color( 155, 255, 155 ), --[NODE_TYPE_WATER ]= Color(0,0,255) , [NikNaks.NODE_TYPE_CLIMB ] = Color( 155, 155, 155 ) } local l = {} for i = 0, 9 do l[i] = Material( "sprites/key_" .. i ) end local hType = { [0] = "None", [2] = "Window", [12] = "Act Busy", [13] = "Visually Interesting", [14] = "Visually Interesting(Dont aim)", [15] = "Inhibit Combine Mines", [16] = "Visually Interesting (Stealth mode)", [100] = "Crouch Cover Medium", -- Angles + FOV is important [101] = "Crouch Cover Low", -- Angles + FOV is important [102] = "Waste Scanner Spawn", [103] = "Entrance / Exit Pinch (Cut content from Antlion guard)", [104] = "Guard Point", [105] = "Enemy Disadvantage Point", [106] = "Health Kit (Cut content from npc_assassin)", [400] = "Antlion: Burrow Point", [401] = "Antlion: Thumper Flee Point", [450] = "Headcrab: Burrow Point", [451] = "Headcrab: Exit Pod Point", [500] = "Roller: Patrol Point", [501] = "Roller: Cleanup Spot", [700] = "Crow: Fly to point", [701] = "Crow: Perch point", [900] = "Follower: Wait point", [901] = "Override jump permission", [902] = "Player squad transition point", [903] = "NPC exit point", [904] = "Strider mnode", [950] = "Player Ally: Push away destination", [951] = "Player Ally: Fear withdrawal destination", [1000] = "HL1 World: Machinery", [1001] = "HL1 World: Blinking Light", [1002] = "HL1 World: Human Blood", [1003] = "HL1 World: Alien Blood", } local function getH( num ) if not num then return "?" end if hType[num] then return hType[num] .. "[" .. num .. "]" end return num end function ain_node:DebugRender( size ) --if self.zone ~= 4 then return end size = size or 32 if self.hint then render.SetMaterial( l[self.zone % 10] ) render.DrawSprite( self:GetPos(), size, size, HSVToColor( ( CurTime() * 420 ) % 360, 0.5, 0.5 ) ) if LocalPlayer():GetPos():DistToSqr( self:GetPos() ) < 40000 then local angle = EyeAngles() angle:RotateAroundAxis( angle:Up(), -90 ) angle:RotateAroundAxis( angle:Forward(), 90 ) cam.Start3D2D( self:GetPos() + Vector( 0, 0, 30 ), angle, 0.1 ) draw.DrawText( "HintType: " .. getH( self.hint.hinttype ), "DermaLarge", 0, 0, color_white, TEXT_ALIGN_CENTER ) if self.hint.targetnode and self.hint.targetnode > -1 then draw.DrawText( "Target node: " .. self.hint.targetnode, "DermaLarge", 0, 30, color_white, TEXT_ALIGN_CENTER ) elseif ( self.hint.hinttype or 0 ) > 0 or not self.hint.group then draw.DrawText( "ID node: " .. self.hint.nodeid, "DermaLarge", 0, 30, color_white, TEXT_ALIGN_CENTER ) else draw.DrawText( "Group node: " .. self.hint.group, "DermaLarge", 0, 30, color_white, TEXT_ALIGN_CENTER ) end cam.End3D2D() end elseif self.zone <= 9 then render.SetMaterial( l[self.zone % 10] ) render.DrawSprite( self:GetPos() + Vector( 0, 0, 15 ), size, size, c[self:GetType()] ) else local angle = EyeAngles() angle:RotateAroundAxis( angle:Up(), -90 ) angle:RotateAroundAxis( angle:Forward(), 90 ) cam.Start3D2D( self:GetPos() + Vector( 0, 0, 30 ), angle, 0.5 ) draw.DrawText( "" .. self.zone, "DermaLarge", 0, 0, color_white, TEXT_ALIGN_CENTER ) cam.End3D2D() end local h = 0 for _, v in pairs( self._connect ) do if v[2]:HasMoveFlag( h, NikNaks.CAP_MOVE_GROUND ) then -- or v[2]:HasMoveFlag( h, CAP_MOVE_FLY ) then render.DrawLine( self:GetPos(), v[1]:GetPos(), c[self:GetType()] ) end end end end function n_meta:DebugRender() local lp = LocalPlayer() if not lp then return end local x, y = math.floor( lp:GetPos().x / 1000 ), math.floor( lp:GetPos().y / 1000 ) -- Use the nodegraph to search, if none at position, scan all nodes ( slow ) local ng = self._nodegraph[x] and self._nodegraph[x][y] if not ng then return end for _, v in pairs( ng ) do v:DebugRender() end end