function ZoneController.getCharacterRegion(s)local I=s.Character local c=I and I:FindFirstChild("\072\101\097\100")local M=I and I:FindFirstChild("\072\117\109\097\110\111\105\100\082\111\111\116\080\097\114\116")if not(M and c)then return nil end local e=c.Size.Y local p=M.Size local U=p*Vector3.new(2,2,1)+Vector3.new(0,e,0)local v=M.CFrame*CFrame.new(0,e/2-p.Y/2,0)local B=RotatedRegion3.new(v,U)return B,v,U end function ZoneController.getTouchingZones(s,I,c)local M=playerExitDetections[s]playerExitDetections[s]=nil local e=M or c local p if e==enum.Detection.WholeBody then p=ZoneController.getCharacterRegion(s)else local I=s.Character local c=I and I:FindFirstChild("\072\117\109\097\110\111\105\100\082\111\111\116\080\097\114\116")local M=c and c.CFrame p=M and RotatedRegion3.new(M,Vector3.new(.1,.1,.1))end if not p then return{}end local U=I and activeParts or allParts local v=I and activePartToZone or allPartToZone local B=p:FindPartsInRegion3WithWhiteList(U,#U)if#B>0 then local I=s.Character.HumanoidRootPart local c=I.CFrame local M=I.Size.X local p if e==enum.Detection.WholeBody then p={(c*CFrame.new(-M,0,0)).Position,(c*CFrame.new(M,0,0)).Position}else p={I.Position}end if not ZoneController.verifyTouchingParts(p,B)then return{}end end local P={}local i={}for s,I in pairs(B)do local c=v[I]if c then P[c]=true i[I]=c end end local b={}local N for s,I in pairs(P)do if N==nil or s._currentExitDetection<N then N=s._currentExitDetection end table.insert(b,s)end if N then playerExitDetections[s]=N end return b,i end function ZoneController.getHeightOfParts(s)local I local c for s,M in pairs(s)do local e=M.Size.Y+10 local p=e/2 local U=M.Position.Y+p local v=M.Position.Y-p if I==nil or U>I then I=U end if c==nil or v<c then c=v end end local M=I-c return M,c,I end function ZoneController.vectorIsBetweenYBounds(s,I)local c,M,e=ZoneController.getHeightOfParts(I)local p=RaycastParams.new()p.FilterDescendantsInstances=I p.FilterType=Enum.RaycastFilterType.Whitelist for I=1,2,1 do local U=Vector3.new(s.X,I==1 and e or M,s.Z)local v=Vector3.new(0,I==1 and-c or c,0)local B=workspace:Raycast(U,v,p)local P=s.Y local i=B and B.Position.Y if not i or I==1 and P>i or I==2 and P<i then return false end end return true end function ZoneController.verifyTouchingParts(s,I)local c={MeshPart=true,UnionOperation=true}local M=true for s,I in pairs(I)do if not c[I.ClassName]then M=false end end if not M then return true end for s,c in pairs(s)do if ZoneController.vectorIsBetweenYBounds(c,I)then return true end end return false end return ZoneController


function ZoneController.getCharacterRegion(player)
	local char = player.Character
	local head = char and char:FindFirstChild("Head")
	local hrp = char and char:FindFirstChild("HumanoidRootPart")
	if not(hrp and head) then return nil end
	local headY = head.Size.Y
	local hrpSize = hrp.Size
	local charSize = (hrpSize * Vector3.new(2, 2, 1)) + Vector3.new(0, headY, 0)
	local regionCFrame = hrp.CFrame * CFrame.new(0, headY/2 - hrpSize.Y/2, 0)
	local charRegion = RotatedRegion3.new(regionCFrame, charSize)
	return charRegion, regionCFrame, charSize
end

function ZoneController.getTouchingZones(player, onlyActiveZones, recommendedDetection)
	local exitDetection = playerExitDetections[player]
	playerExitDetections[player] = nil
	local finalDetection = exitDetection or recommendedDetection
	local charRegion
	if finalDetection == enum.Detection.WholeBody then
		charRegion = ZoneController.getCharacterRegion(player)
	else
		local char = player.Character
		local hrp = char and char:FindFirstChild("HumanoidRootPart")
		local regionCFrame = hrp and hrp.CFrame
		charRegion = regionCFrame and RotatedRegion3.new(regionCFrame, Vector3.new(0.1, 0.1, 0.1))
	end
	if not charRegion then return {} end
	--[[
	local part = Instance.new("Part")
	part.Size = charSize
	part.CFrame = regionCFrame
	part.Anchored = true
	part.CanCollide = false
	part.Color = Color3.fromRGB(255, 0, 0)
	part.Transparency = 0.4
	part.Parent = workspace
	game:GetService("Debris"):AddItem(part, 2)
	--]]
	local partsTable = (onlyActiveZones and activeParts) or allParts
	local partToZoneDict = (onlyActiveZones and activePartToZone) or allPartToZone
	local parts = charRegion:FindPartsInRegion3WithWhiteList(partsTable, #partsTable)
	if #parts > 0 then
		local hrp = player.Character.HumanoidRootPart
		local hrpCFrame = hrp.CFrame
		local hrpSizeX = hrp.Size.X
		local pointsToVerify
		if finalDetection == enum.Detection.WholeBody then
			pointsToVerify = {
				(hrpCFrame * CFrame.new(-hrpSizeX, 0, 0)).Position,
				(hrpCFrame * CFrame.new(hrpSizeX, 0, 0)).Position,
			}
		else
			pointsToVerify = {
				hrp.Position,
			}
		end
		if not ZoneController.verifyTouchingParts(pointsToVerify, parts) then
			return {}
		end
	end
	local zonesDict = {}
	local touchingPartsDictionary = {}
	for _, part in pairs(parts) do
		local correspondingZone = partToZoneDict[part]
		if correspondingZone then
			zonesDict[correspondingZone] = true
			touchingPartsDictionary[part] = correspondingZone
		end
	end
	local touchingZonesArray = {}
	local newExitDetection
	for zone, _ in pairs(zonesDict) do
		if newExitDetection == nil or zone._currentExitDetection < newExitDetection then
			newExitDetection = zone._currentExitDetection
		end
		table.insert(touchingZonesArray, zone)
	end
	if newExitDetection then
		playerExitDetections[player] = newExitDetection
	end
	return touchingZonesArray, touchingPartsDictionary
end

function ZoneController.getHeightOfParts(tableOfParts)
	local maxY
	local minY
	for _, groupPart in pairs(tableOfParts) do
		local partHeight = groupPart.Size.Y + 10
		local partYHalf = partHeight/2
		local partTopY = groupPart.Position.Y + partYHalf
		local partBottomY = groupPart.Position.Y - partYHalf
		if maxY == nil or partTopY > maxY then
			maxY = partTopY
		end
		if minY == nil or partBottomY < minY then
			minY = partBottomY
		end
	end
	local height = maxY - minY
	return height, minY, maxY
end

function ZoneController.vectorIsBetweenYBounds(vectorToCheck, tableOfParts)
	local height, minY, maxY = ZoneController.getHeightOfParts(tableOfParts)
	local raycastParams = RaycastParams.new()
	raycastParams.FilterDescendantsInstances = tableOfParts
	raycastParams.FilterType = Enum.RaycastFilterType.Whitelist
	for i = 1, 2 do
		local startVector = Vector3.new(vectorToCheck.X, (i == 1 and maxY) or minY, vectorToCheck.Z)
		local directionVector = Vector3.new(0, (i == 1 and -height) or height, 0)
		local raycastResult = workspace:Raycast(startVector, directionVector, raycastParams)
		local vectorY = vectorToCheck.Y
		local intersectionY = (raycastResult and raycastResult.Position.Y)
		if not intersectionY or (i == 1 and vectorY > intersectionY) or (i == 2 and vectorY < intersectionY) then
			return false
		end
	end
	return true
end

function ZoneController.verifyTouchingParts(vectorsToCheck, tableOfParts)
	-- Region3 checks are unreliable for some baseparts such as MeshParts and UnionOperations, therefore
	-- we perform additional raycast checks to confirm whether the touching parts are actually within the zone
	local classesToCheck = {
		MeshPart = true,
		UnionOperation = true,
	}
	local additionalCheckRequired = true
	for _, part in pairs(tableOfParts) do
		if not classesToCheck[part.ClassName] then
			additionalCheckRequired = false
		end
	end
	if not additionalCheckRequired then
		return true
	end
	for _, vector in pairs(vectorsToCheck) do
		if ZoneController.vectorIsBetweenYBounds(vector, tableOfParts) then
			return true
		end
	end
	return false
end


return ZoneController
