local P=game:GetService("\080\108\097\121\101\114\115")local z=require(script.Parent.Parent.Signal)local g=require(script.Parent.Parent.Janitor)local q={}q.__index=q local e={}q.trackers=e q.itemAdded=z.new()q.itemRemoved=z.new()q.bodyPartsToIgnore={UpperTorso=true;LowerTorso=true;Torso=true,LeftHand=true;RightHand=true;LeftFoot=true;RightFoot=true}function q.getCombinedTotalVolumes()local P=0 for z,g in pairs(e)do P+=z.totalVolume end return P end function q.getCharacterSize(P)local z=P and P:FindFirstChild("\072\101\097\100")local g=P and P:FindFirstChild("\072\117\109\097\110\111\105\100\082\111\111\116\080\097\114\116")if not(g and z)then return nil end if not z:IsA("\066\097\115\101\080\097\114\116")then z=g end local q=z.Size.Y local e=g.Size local k=e*Vector3.new(2,2,1)+Vector3.new(0,q,0)local A=g.CFrame*CFrame.new(0,q/2-e.Y/2,0)return k,A end function q.new(z)local k={}setmetatable(k,q)k.name=z k.totalVolume=0 k.parts={}k.partToItem={}k.items={}k.whitelistParams=nil k.characters={}k.baseParts={}k.exitDetections={}k.janitor=g.new()if z=="\112\108\097\121\101\114"then local function z()local z={}for P,g in pairs(P:GetPlayers())do local q=g.Character if q then z[q]=true end end k.characters=z end local function g(P)local function g(P)local g=P:WaitForChild("\072\117\109\097\110\111\105\100")z()k:update()for P,z in pairs(g:GetChildren())do if z:IsA("\078\117\109\098\101\114\086\097\108\117\101")then z.Changed:Connect(function()k:update()end)end end end if P.Character then task.spawn(g,P.Character)end P.CharacterAdded:Connect(g)P.CharacterRemoving:Connect(function(P)k.exitDetections[P]=nil end)end P.PlayerAdded:Connect(g)for P,z in pairs(P:GetPlayers())do task.spawn(g,z)end P.PlayerRemoving:Connect(function(P)z()k:update()end)elseif z=="\105\116\101\109"then local function P(P,z)if P.isCharacter then k.characters[P.item]=z elseif P.isBasePart then k.baseParts[P.item]=z end k:update()end q.itemAdded:Connect(function(z)P(z,true)end)q.itemRemoved:Connect(function(z)k.exitDetections[z.item]=nil P(z,nil)end)end e[k]=true task.defer(k.update,k)return k end function q._preventMultiFrameUpdates(z,P,...)z._preventMultiDetails=z._preventMultiDetails or{}local g=z._preventMultiDetails[P]if not g then g={calling=false;callsThisFrame=0,updatedThisFrame=false}z._preventMultiDetails[P]=g end g.callsThisFrame+=1 if g.callsThisFrame==1 then local q=table.pack(...)task.defer(function()local e=g.callsThisFrame g.callsThisFrame=0 if e>1 then z[P](z,unpack(q))end end)return false end return true end function q.update(P)if P:_preventMultiFrameUpdates("\117\112\100\097\116\101")then return end P.totalVolume=0 P.parts={}P.partToItem={}P.items={}for z,e in pairs(P.characters)do local k=q.getCharacterSize(z)if not k then continue end local A=k local t=(A.X*A.Y)*A.Z P.totalVolume+=t local E=P.janitor:add(g.new(),"\100\101\115\116\114\111\121","\116\114\097\099\107\067\104\097\114\097\099\116\101\114\080\097\114\116\115-"..P.name)local function F(z)E:add(z.AncestryChanged:Connect(function()if not z:IsDescendantOf(game)then if z.Parent==nil and E~=nil then E:destroy()E=nil P:update()end end end),"\068\105\115\099\111\110\110\101\099\116")end for g,e in pairs(z:GetChildren())do if e:IsA("\066\097\115\101\080\097\114\116")and not q.bodyPartsToIgnore[e.Name]then P.partToItem[e]=z table.insert(P.parts,e)F(e)end end F(z)table.insert(P.items,z)end for z,g in pairs(P.baseParts)do local q=z.Size local e=(q.X*q.Y)*q.Z P.totalVolume+=e P.partToItem[z]=z table.insert(P.parts,z)table.insert(P.items,z)end P.whitelistParams=OverlapParams.new()P.whitelistParams.FilterType=Enum.RaycastFilterType.Include P.whitelistParams.MaxParts=#P.parts P.whitelistParams.FilterDescendantsInstances=P.parts end return q
-- #selene: allow(unused_variable)
-- This enables data on volumes, HumanoidRootParts, etc to be handled on an event-basis, instead of being retrieved every interval

-- LOCAL
local players = game:GetService("Players")

local Signal = require(script.Parent.Parent.Signal)
local Janitor = require(script.Parent.Parent.Janitor)

-- PUBLIC
local Tracker = {}
Tracker.__index = Tracker
local trackers = {}
Tracker.trackers = trackers
Tracker.itemAdded = Signal.new()
Tracker.itemRemoved = Signal.new()
Tracker.bodyPartsToIgnore = {
	-- We ignore these due to their insignificance (e.g. we ignore the lower and
	-- upper torso because the HumanoidRootPart also covers these areas)
	-- This ultimately reduces the burden on the player region checks
	UpperTorso = true,
	LowerTorso = true,
	Torso = true,
	LeftHand = true,
	RightHand = true,
	LeftFoot = true,
	RightFoot = true,
}

-- FUNCTIONS
function Tracker.getCombinedTotalVolumes()
	local combinedVolume = 0
	for tracker, _ in pairs(trackers) do
		combinedVolume += tracker.totalVolume
	end
	return combinedVolume
end

function Tracker.getCharacterSize(character)
	local head = character and character:FindFirstChild("Head")
	local hrp = character and character:FindFirstChild("HumanoidRootPart")
	if not (hrp and head) then
		return nil
	end
	if not head:IsA("BasePart") then
		head = hrp
	end
	local headY = head.Size.Y
	local hrpSize = hrp.Size
	local charSize = (hrpSize * Vector3.new(2, 2, 1)) + Vector3.new(0, headY, 0)
	local charCFrame = hrp.CFrame * CFrame.new(0, headY / 2 - hrpSize.Y / 2, 0)
	return charSize, charCFrame
end

-- CONSTRUCTOR
function Tracker.new(name)
	local self = {}
	setmetatable(self, Tracker)

	self.name = name
	self.totalVolume = 0
	self.parts = {}
	self.partToItem = {}
	self.items = {}
	self.whitelistParams = nil
	self.characters = {}
	self.baseParts = {}
	self.exitDetections = {}
	self.janitor = Janitor.new()

	if name == "player" then
		local function updatePlayerCharacters()
			local characters = {}
			for _, player in pairs(players:GetPlayers()) do
				local character = player.Character
				if character then
					characters[character] = true
				end
			end
			self.characters = characters
		end

		local function playerAdded(player)
			local function charAdded(character)
				local humanoid = character:WaitForChild("Humanoid")

				updatePlayerCharacters()
				self:update()
				for _, valueInstance in pairs(humanoid:GetChildren()) do
					if valueInstance:IsA("NumberValue") then
						valueInstance.Changed:Connect(function()
							self:update()
						end)
					end
				end
			end
			if player.Character then
				task.spawn(charAdded, player.Character)
			end
			player.CharacterAdded:Connect(charAdded)
			player.CharacterRemoving:Connect(function(removingCharacter)
				self.exitDetections[removingCharacter] = nil
			end)
		end

		players.PlayerAdded:Connect(playerAdded)
		for _, player in pairs(players:GetPlayers()) do
			task.spawn(playerAdded, player)
		end

		players.PlayerRemoving:Connect(function(player)
			updatePlayerCharacters()
			self:update()
		end)
	elseif name == "item" then
		local function updateItem(itemDetail, newValue)
			if itemDetail.isCharacter then
				self.characters[itemDetail.item] = newValue
			elseif itemDetail.isBasePart then
				self.baseParts[itemDetail.item] = newValue
			end
			self:update()
		end
		Tracker.itemAdded:Connect(function(itemDetail)
			updateItem(itemDetail, true)
		end)
		Tracker.itemRemoved:Connect(function(itemDetail)
			self.exitDetections[itemDetail.item] = nil
			updateItem(itemDetail, nil)
		end)
	end

	trackers[self] = true
	task.defer(self.update, self)
	return self
end

-- METHODS
function Tracker:_preventMultiFrameUpdates(methodName, ...)
	-- This prevents the funtion being called twice within a single frame
	-- If called more than once, the function will initally be delayed again until the next frame, then all others cancelled
	self._preventMultiDetails = self._preventMultiDetails or {}
	local detail = self._preventMultiDetails[methodName]
	if not detail then
		detail = {
			calling = false,
			callsThisFrame = 0,
			updatedThisFrame = false,
		}
		self._preventMultiDetails[methodName] = detail
	end

	detail.callsThisFrame += 1
	if detail.callsThisFrame == 1 then
		local args = table.pack(...)
		task.defer(function()
			local newCallsThisFrame = detail.callsThisFrame
			detail.callsThisFrame = 0
			if newCallsThisFrame > 1 then
				self[methodName](self, unpack(args))
			end
		end)
		return false
	end
	return true
end

function Tracker:update()
	if self:_preventMultiFrameUpdates("update") then
		return
	end

	self.totalVolume = 0
	self.parts = {}
	self.partToItem = {}
	self.items = {}

	-- This tracks the bodyparts of a character
	for character, _ in pairs(self.characters) do
		local charSize = Tracker.getCharacterSize(character)
		if not charSize then
			continue
		end
		local rSize = charSize
		local charVolume = rSize.X * rSize.Y * rSize.Z
		self.totalVolume += charVolume

		local characterJanitor =
			self.janitor:add(Janitor.new(), "destroy", "trackCharacterParts-" .. self.name)
		local function updateTrackerOnParentChanged(instance)
			characterJanitor:add(
				instance.AncestryChanged:Connect(function()
					if not instance:IsDescendantOf(game) then
						if instance.Parent == nil and characterJanitor ~= nil then
							characterJanitor:destroy()
							characterJanitor = nil
							self:update()
						end
					end
				end),
				"Disconnect"
			)
		end

		for _, part in pairs(character:GetChildren()) do
			if part:IsA("BasePart") and not Tracker.bodyPartsToIgnore[part.Name] then
				self.partToItem[part] = character
				table.insert(self.parts, part)
				updateTrackerOnParentChanged(part)
			end
		end
		updateTrackerOnParentChanged(character)
		table.insert(self.items, character)
	end

	-- This tracks any additional baseParts
	for additionalPart, _ in pairs(self.baseParts) do
		local rSize = additionalPart.Size
		local partVolume = rSize.X * rSize.Y * rSize.Z
		self.totalVolume += partVolume
		self.partToItem[additionalPart] = additionalPart
		table.insert(self.parts, additionalPart)
		table.insert(self.items, additionalPart)
	end

	self.whitelistParams = OverlapParams.new()
	self.whitelistParams.FilterType = Enum.RaycastFilterType.Include
	self.whitelistParams.MaxParts = #self.parts
	self.whitelistParams.FilterDescendantsInstances = self.parts
end

return Tracker
