function Zone.findLocalPlayer(E)if not localPlayer then error("\067\097\110 \111\110\108\121 \099\097\108\108 \'\102\105\110\100\076\111\099\097\108\080\108\097\121\101\114\' \111\110 \116\104\101 \099\108\105\101\110\116\033")end return E:findPlayer(localPlayer)end function Zone._find(i,E,l)ZoneController.updateDetection(i)local h=ZoneController.trackers[E]local q=ZoneController.getTouchingZones(l,false,i._currentEnterDetection,h)for E,l in pairs(q)do if l==i then return true end end return false end function Zone.findPlayer(l,E)local i=E.Character local h=i and i:FindFirstChildOfClass("\072\117\109\097\110\111\105\100")if not h then return false end return l:_find("\112\108\097\121\101\114",E.Character)end function Zone.findItem(l,E)return l:_find("\105\116\101\109",E)end function Zone.findPart(l,E)local i,h=l:_getRegionConstructor(E,l.overlapParams.zonePartsWhitelist)local q=l.worldModel[i](l.worldModel,unpack(h))if#q>0 then return true,q end return false end function Zone.getCheckerPart(E)local l=E.checkerPart if not l then l=E.janitor:add(Instance.new("\080\097\114\116"),"\068\101\115\116\114\111\121")l.Size=Vector3.new(.1,.1,.1)l.Name="\090\111\110\101\080\108\117\115\067\104\101\099\107\101\114\080\097\114\116"l.Anchored=true l.Transparency=1 l.CanCollide=false E.checkerPart=l end local i=E.worldModel if i==workspace then i=ZoneController.getWorkspaceContainer()end if l.Parent~=i then l.Parent=i end return l end function Zone.findPoint(l,E)local i=E if typeof(E)=="\086\101\099\116\111\114\051"then i=CFrame.new(E)end local h=l:getCheckerPart()h.CFrame=i local q,z=l:_getRegionConstructor(h,l.overlapParams.zonePartsWhitelist)local y=l.worldModel[q](l.worldModel,unpack(z))if#y>0 then return true,y end return false end function Zone._getAll(l,E)ZoneController.updateDetection(l)local i={}local h=ZoneController._getZonesAndItems(E,{self=true},l.volume,false,l._currentEnterDetection)local q=h[l]if q then for E,l in pairs(q)do table.insert(i,E)end end return i end function Zone.getPlayers(E)return E:_getAll("\112\108\097\121\101\114")end function Zone.getItems(E)return E:_getAll("\105\116\101\109")end function Zone.getParts(E)local l={}if E.activeTriggers.part then local i=E.trackingTouchedTriggers.part for E,i in pairs(i)do table.insert(l,E)end return l end local i=E.worldModel:GetPartBoundsInBox(E.region.CFrame,E.region.Size,E.overlapParams.zonePartsIgnorelist)for i,h in pairs(i)do if E:findPart(h)then table.insert(l,h)end end return l end function Zone.getRandomPoint(E)local l=E.exactRegion local i=l.Size local h=l.CFrame local q=Random.new()local z local y,Q local Z repeat z=h*CFrame.new(q:NextNumber(-i.X/2,i.X/2),q:NextNumber(-i.Y/2,i.Y/2),q:NextNumber(-i.Z/2,i.Z/2))y,Q=E:findPoint(z)if y then Z=true end until Z local P=z.Position return P,Q end function Zone.setAccuracy(l,E)local i=tonumber(E)if not i then i=enum.Accuracy[E]if not i then error(("\'\037\115\' \105\115 \097\110 \105\110\118\097\108\105\100 \101\110\117\109\078\097\109\101\033"):format(E))end else local E=enum.Accuracy.getName(i)if not E then error(("\037\115 \105\115 \097\110 \105\110\118\097\108\105\100 \101\110\117\109\073\100\033"):format(i))end end l.accuracy=i end function Zone.setDetection(l,E)local i=tonumber(E)if not i then i=enum.Detection[E]if not i then error(("\'\037\115\' \105\115 \097\110 \105\110\118\097\108\105\100 \101\110\117\109\078\097\109\101\033"):format(E))end else local E=enum.Detection.getName(i)if not E then error(("\037\115 \105\115 \097\110 \105\110\118\097\108\105\100 \101\110\117\109\073\100\033"):format(i))end end l.enterDetection=i l.exitDetection=i end function Zone.trackItem(l,E)local i=E:IsA("\066\097\115\101\080\097\114\116")local h=false if not i then h=E:FindFirstChildOfClass("\072\117\109\097\110\111\105\100")and E:FindFirstChild("\072\117\109\097\110\111\105\100\082\111\111\116\080\097\114\116")end assert(i or h,"\079\110\108\121 \066\097\115\101\080\097\114\116\115 \111\114 \067\104\097\114\097\099\116\101\114\115\047\078\080\067\115 \099\097\110 \098\101 \116\114\097\099\107\101\100\033")if l.trackedItems[E]then return end if l.itemsToUntrack[E]then l.itemsToUntrack[E]=nil end local q=l.janitor:add(Janitor.new(),"\100\101\115\116\114\111\121")local z={janitor=q;item=E;isBasePart=i,isCharacter=h}l.trackedItems[E]=z q:add(E.AncestryChanged:Connect(function()if not E:IsDescendantOf(game)then l:untrackItem(E)end end),"\068\105\115\099\111\110\110\101\099\116")local y=require(trackerModule)y.itemAdded:Fire(z)end function Zone.untrackItem(l,E)local i=l.trackedItems[E]if i then i.janitor:destroy()end l.trackedItems[E]=nil local h=require(trackerModule)h.itemRemoved:Fire(i)end function Zone.bindToGroup(l,E)l:unbindFromGroup()local i=ZoneController.getGroup(E)or ZoneController.setGroup(E)i._memberZones[l.zoneId]=l l.settingsGroupName=E end function Zone.unbindFromGroup(E)if E.settingsGroupName then local l=ZoneController.getGroup(E.settingsGroupName)if l then l._memberZones[E.zoneId]=nil end E.settingsGroupName=nil end end function Zone.relocate(E)if E.hasRelocated then return end local l=require(collectiveWorldModelModule)local i=l.setupWorldModel(E)E.worldModel=i E.hasRelocated=true local h=E.container if typeof(h)=="\116\097\098\108\101"then h=Instance.new("\070\111\108\100\101\114")for E,l in pairs(E.zoneParts)do l.Parent=h end end E.relocationContainer=E.janitor:add(h,"\068\101\115\116\114\111\121","\082\101\108\111\099\097\116\105\111\110\067\111\110\116\097\105\110\101\114")h.Parent=i end function Zone._onItemCallback(q,E,l,i,h)local z=q.onItemDetails[i]if not z then z={}q.onItemDetails[i]=z end if#z==0 then q.itemsToUntrack[i]=true end table.insert(z,i)q:trackItem(i)local function y()h()if q.itemsToUntrack[i]then q.itemsToUntrack[i]=nil q:untrackItem(i)end end local Q=q:findItem(i)if Q==l then y()else local l l=q[E]:Connect(function(E)if l and E==i then l:Disconnect()l=nil y()end end)end end function Zone.onItemEnter(E,...)E:_onItemCallback("\105\116\101\109\069\110\116\101\114\101\100",true,...)end function Zone.onItemExit(E,...)E:_onItemCallback("\105\116\101\109\069\120\105\116\101\100",false,...)end function Zone.destroy(E)E:unbindFromGroup()E.janitor:destroy()end Zone.Destroy=Zone.destroy return Zone

function Zone:findLocalPlayer()
	if not localPlayer then
		error("Can only call 'findLocalPlayer' on the client!")
	end
	return self:findPlayer(localPlayer)
end

function Zone:_find(trackerName, item)
	ZoneController.updateDetection(self)
	local tracker = ZoneController.trackers[trackerName]
	local touchingZones = ZoneController.getTouchingZones(item, false, self._currentEnterDetection, tracker)
	for _, zone in pairs(touchingZones) do
		if zone == self then
			return true
		end
	end
	return false
end

function Zone:findPlayer(player)
	local character = player.Character
	local humanoid = character and character:FindFirstChildOfClass("Humanoid")
	if not humanoid then
		return false
	end
	return self:_find("player", player.Character)
end

function Zone:findItem(item)
	return self:_find("item", item)
end

function Zone:findPart(part)
	local methodName, args = self:_getRegionConstructor(part, self.overlapParams.zonePartsWhitelist)
	local touchingZoneParts = self.worldModel[methodName](self.worldModel, unpack(args))
	--local touchingZoneParts = self.worldModel:GetPartsInPart(part, self.overlapParams.zonePartsWhitelist)
	if #touchingZoneParts > 0 then
		return true, touchingZoneParts
	end
	return false
end

function Zone:getCheckerPart()
	local checkerPart = self.checkerPart
	if not checkerPart then
		checkerPart = self.janitor:add(Instance.new("Part"), "Destroy")
		checkerPart.Size = Vector3.new(0.1, 0.1, 0.1)
		checkerPart.Name = "ZonePlusCheckerPart"
		checkerPart.Anchored = true
		checkerPart.Transparency = 1
		checkerPart.CanCollide = false
		self.checkerPart = checkerPart
	end
	local checkerParent = self.worldModel
	if checkerParent == workspace then
		checkerParent = ZoneController.getWorkspaceContainer()
	end
	if checkerPart.Parent ~= checkerParent then
		checkerPart.Parent = checkerParent
	end
	return checkerPart
end

function Zone:findPoint(positionOrCFrame)
	local cframe = positionOrCFrame
	if typeof(positionOrCFrame) == "Vector3" then
		cframe = CFrame.new(positionOrCFrame)
	end
	local checkerPart = self:getCheckerPart()
	checkerPart.CFrame = cframe
	--checkerPart.Parent = self.worldModel
	local methodName, args = self:_getRegionConstructor(checkerPart, self.overlapParams.zonePartsWhitelist)
	local touchingZoneParts = self.worldModel[methodName](self.worldModel, unpack(args))
	--local touchingZoneParts = self.worldModel:GetPartsInPart(self.checkerPart, self.overlapParams.zonePartsWhitelist)
	if #touchingZoneParts > 0 then
		return true, touchingZoneParts
	end
	return false
end

function Zone:_getAll(trackerName)
	ZoneController.updateDetection(self)
	local itemsArray = {}
	local zonesAndOccupants = ZoneController._getZonesAndItems(trackerName, {self = true}, self.volume, false, self._currentEnterDetection)
	local occupantsDict = zonesAndOccupants[self]
	if occupantsDict then
		for item, _ in pairs(occupantsDict) do
			table.insert(itemsArray, item)
		end
	end
	return itemsArray
end

function Zone:getPlayers()
	return self:_getAll("player")
end

function Zone:getItems()
	return self:_getAll("item")
end

function Zone:getParts()
	-- This is designed for infrequent 'one off' use
	-- If you plan on checking for parts within a zone frequently, it's recommended you
	-- use the .partEntered and .partExited events instead.
	local partsArray = {}
	if self.activeTriggers["part"] then
		local trackingDict = self.trackingTouchedTriggers["part"]
		for part, _ in pairs(trackingDict) do
			table.insert(partsArray, part)
		end
		return partsArray
	end
	local partsInRegion = self.worldModel:GetPartBoundsInBox(self.region.CFrame, self.region.Size, self.overlapParams.zonePartsIgnorelist)
	for _, part in pairs(partsInRegion) do
		if self:findPart(part) then
			table.insert(partsArray, part)
		end
	end
	return partsArray
end

function Zone:getRandomPoint()
	local region = self.exactRegion
	local size = region.Size
	local cframe = region.CFrame
	local random = Random.new()
	local randomCFrame
	local success, touchingZoneParts
	local pointIsWithinZone
	repeat
		randomCFrame = cframe * CFrame.new(random:NextNumber(-size.X/2,size.X/2), random:NextNumber(-size.Y/2,size.Y/2), random:NextNumber(-size.Z/2,size.Z/2))
		success, touchingZoneParts = self:findPoint(randomCFrame)
		if success then
			pointIsWithinZone = true
		end
	until pointIsWithinZone
	local randomVector = randomCFrame.Position
	return randomVector, touchingZoneParts
end

function Zone:setAccuracy(enumIdOrName)
	local enumId = tonumber(enumIdOrName)
	if not enumId then
		enumId = enum.Accuracy[enumIdOrName]
		if not enumId then
			error(("'%s' is an invalid enumName!"):format(enumIdOrName))
		end
	else
		local enumName = enum.Accuracy.getName(enumId)
		if not enumName then
			error(("%s is an invalid enumId!"):format(enumId))
		end
	end
	self.accuracy = enumId
end

function Zone:setDetection(enumIdOrName)
	local enumId = tonumber(enumIdOrName)
	if not enumId then
		enumId = enum.Detection[enumIdOrName]
		if not enumId then
			error(("'%s' is an invalid enumName!"):format(enumIdOrName))
		end
	else
		local enumName = enum.Detection.getName(enumId)
		if not enumName then
			error(("%s is an invalid enumId!"):format(enumId))
		end
	end
	self.enterDetection = enumId
	self.exitDetection = enumId
end

function Zone:trackItem(instance)
	local isBasePart = instance:IsA("BasePart")
	local isCharacter = false
	if not isBasePart then
		isCharacter = instance:FindFirstChildOfClass("Humanoid") and instance:FindFirstChild("HumanoidRootPart")
	end

	assert(isBasePart or isCharacter, "Only BaseParts or Characters/NPCs can be tracked!")

	if self.trackedItems[instance] then
		return
	end
	if self.itemsToUntrack[instance] then
		self.itemsToUntrack[instance] = nil
	end

	local itemJanitor = self.janitor:add(Janitor.new(), "destroy")
	local itemDetail = {
		janitor = itemJanitor,
		item = instance,
		isBasePart = isBasePart,
		isCharacter = isCharacter,
	}
	self.trackedItems[instance] = itemDetail

	itemJanitor:add(instance.AncestryChanged:Connect(function()
		if not instance:IsDescendantOf(game) then
			self:untrackItem(instance)
		end
	end), "Disconnect")

	local Tracker = require(trackerModule)
	Tracker.itemAdded:Fire(itemDetail)
end

function Zone:untrackItem(instance)
	local itemDetail = self.trackedItems[instance]
	if itemDetail then
		itemDetail.janitor:destroy()
	end
	self.trackedItems[instance] = nil

	local Tracker = require(trackerModule)
	Tracker.itemRemoved:Fire(itemDetail)
end

function Zone:bindToGroup(settingsGroupName)
	self:unbindFromGroup()
	local group = ZoneController.getGroup(settingsGroupName) or ZoneController.setGroup(settingsGroupName)
	group._memberZones[self.zoneId] = self
	self.settingsGroupName = settingsGroupName
end

function Zone:unbindFromGroup()
	if self.settingsGroupName then
		local group = ZoneController.getGroup(self.settingsGroupName)
		if group then
			group._memberZones[self.zoneId] = nil
		end
		self.settingsGroupName = nil
	end
end

function Zone:relocate()
	if self.hasRelocated then
		return
	end

	local CollectiveWorldModel = require(collectiveWorldModelModule)
	local worldModel = CollectiveWorldModel.setupWorldModel(self)
	self.worldModel = worldModel
	self.hasRelocated = true
	
	local relocationContainer = self.container
	if typeof(relocationContainer) == "table" then
		relocationContainer = Instance.new("Folder")
		for _, zonePart in pairs(self.zoneParts) do
			zonePart.Parent = relocationContainer
		end
	end
	self.relocationContainer = self.janitor:add(relocationContainer, "Destroy", "RelocationContainer")
	relocationContainer.Parent = worldModel
end

function Zone:_onItemCallback(eventName, desiredValue, instance, callbackFunction)
	local detail = self.onItemDetails[instance]
	if not detail then
		detail = {}
		self.onItemDetails[instance] = detail
	end
	if #detail == 0 then
		self.itemsToUntrack[instance] = true
	end
	table.insert(detail, instance)
	self:trackItem(instance)

	local function triggerCallback()
		callbackFunction()
		if self.itemsToUntrack[instance] then
			self.itemsToUntrack[instance] = nil
			self:untrackItem(instance)
		end
	end

	local inZoneAlready = self:findItem(instance)
	if inZoneAlready == desiredValue then
		triggerCallback()
	else
		local connection
		connection = self[eventName]:Connect(function(item)
			if connection and item == instance then
				connection:Disconnect()
				connection = nil
				triggerCallback()
			end
		end)
		--[[
		if typeof(expireAfterSeconds) == "number" then
			task.delay(expireAfterSeconds, function()
				if connection ~= nil then
					print("EXPIRE!")
					connection:Disconnect()
					connection = nil
					triggerCallback()
				end
			end)
		end
		--]]
	end
end

function Zone:onItemEnter(...)
	self:_onItemCallback("itemEntered", true, ...)
end

function Zone:onItemExit(...)
	self:_onItemCallback("itemExited", false, ...)
end

function Zone:destroy()
	self:unbindFromGroup()
	self.janitor:destroy()
end
Zone.Destroy = Zone.destroy



return Zone
