function ZoneController.getTouchingZones(P,l,r,O)local v,S if O then v=O.exitDetections[P]O.exitDetections[P]=nil end S=v or r local z,x local A=P:IsA("\066\097\115\101\080\097\114\116")local U=not A local k={}if A then z,x=P.Size,P.CFrame table.insert(k,P)elseif S==enum.Detection.WholeBody then z,x=Tracker.getCharacterSize(P)k=P:GetChildren()else local l=P:FindFirstChild("\072\117\109\097\110\111\105\100\082\111\111\116\080\097\114\116")if l then z,x=l.Size,l.CFrame table.insert(k,l)end end if not z or not x then return{}end local B=l and activeParts or allParts local s=l and activePartToZone or allPartToZone local R=OverlapParams.new()R.FilterType=Enum.RaycastFilterType.Whitelist R.MaxParts=#B R.FilterDescendantsInstances=B local I={}local y={}local E=CollectiveWorldModel:GetPartBoundsInBox(x,z,R)local Z={}for P,l in pairs(E)do local r=s[l]if r and r.allZonePartsAreBlocks then y[r]=true I[l]=r else table.insert(Z,l)end end local L=#Z local e=0 if L>0 then local l=OverlapParams.new()l.FilterType=Enum.RaycastFilterType.Whitelist l.MaxParts=L l.FilterDescendantsInstances=Z local r=P for P,r in pairs(k)do local O=false if not r:IsA("\066\097\115\101\080\097\114\116")or U and Tracker.bodyPartsToIgnore[r.Name]then continue end local v=CollectiveWorldModel:GetPartsInPart(r,l)for P,l in pairs(v)do if not I[l]then local P=s[l]if P then y[P]=true I[l]=P e+=1 end if e==L then O=true break end end end if O then break end end end local V={}local h for P,l in pairs(y)do if h==nil or P._currentExitDetection<h then h=P._currentExitDetection end table.insert(V,P)end if h and O then O.exitDetections[P]=h end return V,I end local P={}function ZoneController.setGroup(l,r)local O=P[l]if not O then O={}P[l]=O end O.onlyEnterOnceExitedAll=true O._name=l O._memberZones={}if typeof(r)=="\116\097\098\108\101"then for P,l in pairs(r)do O[P]=l end end return O end function ZoneController.getGroup(l)return P[l]end local l local r=string.format("\090\111\110\101\080\108\117\115\037\115\067\111\110\116\097\105\110\101\114",runService:IsClient()and"\067\108\105\101\110\116"or"\083\101\114\118\101\114")function ZoneController.getWorkspaceContainer()local P=l or workspace:FindFirstChild(r)if not P then P=Instance.new("\070\111\108\100\101\114")P.Name=r P.Parent=workspace l=P end return P end return ZoneController
--[[
-- the player touched events which utilise active zones at the moment may change to the new CanTouch method for parts in the future
-- hence im disabling this as it may be depreciated quite soon
function ZoneController.getActiveZones()
	local zonesArray = {}
	for zone, _ in pairs(activeZones) do
		table.insert(zonesArray, zone)
	end
	return zonesArray
end
--]]

function ZoneController.getTouchingZones(item, onlyActiveZones, recommendedDetection, tracker)
	local exitDetection, finalDetection
	if tracker then
		exitDetection = tracker.exitDetections[item]
		tracker.exitDetections[item] = nil
	end
	finalDetection = exitDetection or recommendedDetection

	local itemSize, itemCFrame
	local itemIsBasePart = item:IsA("BasePart")
	local itemIsCharacter = not itemIsBasePart
	local bodyPartsToCheck = {}
	if itemIsBasePart then
		itemSize, itemCFrame = item.Size, item.CFrame
		table.insert(bodyPartsToCheck, item)
	elseif finalDetection == enum.Detection.WholeBody then
		itemSize, itemCFrame = Tracker.getCharacterSize(item)
		bodyPartsToCheck = item:GetChildren()
	else
		local hrp = item:FindFirstChild("HumanoidRootPart")
		if hrp then
			itemSize, itemCFrame = hrp.Size, hrp.CFrame
			table.insert(bodyPartsToCheck, hrp)
		end
	end
	if not itemSize or not itemCFrame then
		return {}
	end

	--[[
	local part = Instance.new("Part")
	part.Size = itemSize
	part.CFrame = itemCFrame
	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 boundParams = OverlapParams.new()
	boundParams.FilterType = Enum.RaycastFilterType.Whitelist
	boundParams.MaxParts = #partsTable
	boundParams.FilterDescendantsInstances = partsTable

	-- This retrieves the bounds (the rough shape) of all parts touching the item/character
	-- If the corresponding zone is made up of *entirely* blocks then the bound will
	-- be the actual shape of the part.
	local touchingPartsDictionary = {}
	local zonesDict = {}
	local boundParts = CollectiveWorldModel:GetPartBoundsInBox(itemCFrame, itemSize, boundParams)
	local boundPartsThatRequirePreciseChecks = {}
	for _, boundPart in pairs(boundParts) do
		local correspondingZone = partToZoneDict[boundPart]
		if correspondingZone and correspondingZone.allZonePartsAreBlocks then
			zonesDict[correspondingZone] = true
			touchingPartsDictionary[boundPart] = correspondingZone
		else
			table.insert(boundPartsThatRequirePreciseChecks, boundPart)
		end
	end

	-- If the bound parts belong to a zone that isn't entirely made up of blocks, then
	-- we peform additional checks using GetPartsInPart which enables shape
	-- geometries to be precisely determined for non-block baseparts.
	local totalRemainingBoundParts = #boundPartsThatRequirePreciseChecks
	local precisePartsCount = 0
	if totalRemainingBoundParts > 0 then
		local preciseParams = OverlapParams.new()
		preciseParams.FilterType = Enum.RaycastFilterType.Whitelist
		preciseParams.MaxParts = totalRemainingBoundParts
		preciseParams.FilterDescendantsInstances = boundPartsThatRequirePreciseChecks

		local character = item
		for _, bodyPart in pairs(bodyPartsToCheck) do
			local endCheck = false
			if not bodyPart:IsA("BasePart") or (itemIsCharacter and Tracker.bodyPartsToIgnore[bodyPart.Name]) then
				continue
			end
			local preciseParts = CollectiveWorldModel:GetPartsInPart(bodyPart, preciseParams)
			for _, precisePart in pairs(preciseParts) do
				if not touchingPartsDictionary[precisePart] then
					local correspondingZone = partToZoneDict[precisePart]
					if correspondingZone then
						zonesDict[correspondingZone] = true
						touchingPartsDictionary[precisePart] = correspondingZone
						precisePartsCount += 1
					end
					if precisePartsCount == totalRemainingBoundParts then
						endCheck = true
						break
					end
				end
			end
			if endCheck then
				break
			end
		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 and tracker then
		tracker.exitDetections[item] = newExitDetection
	end
	return touchingZonesArray, touchingPartsDictionary
end

local settingsGroups = {}
function ZoneController.setGroup(settingsGroupName, properties)
	local group = settingsGroups[settingsGroupName]
	if not group then
		group = {}
		settingsGroups[settingsGroupName] = group
	end

	-- PUBLIC PROPERTIES --
	group.onlyEnterOnceExitedAll = true

	-- PRIVATE PROPERTIES --
	group._name = settingsGroupName
	group._memberZones = {}

	if typeof(properties) == "table" then
		for k, v in pairs(properties) do
			group[k] = v
		end
	end
	return group
end

function ZoneController.getGroup(settingsGroupName)
	return settingsGroups[settingsGroupName]
end

local workspaceContainer
local workspaceContainerName = string.format("ZonePlus%sContainer", (runService:IsClient() and "Client") or "Server")
function ZoneController.getWorkspaceContainer()
	local container = workspaceContainer or workspace:FindFirstChild(workspaceContainerName)
	if not container then
		container = Instance.new("Folder")
		container.Name = workspaceContainerName
		container.Parent = workspace
		workspaceContainer = container
	end
	return container
end

return ZoneController
