This is the modules code I think. Zone.
-- LOCAL
local players = game:GetService("Players")
local runService = game:GetService("RunService")
local heartbeat = runService.Heartbeat
local localPlayer = runService:IsClient() and players.LocalPlayer
local replicatedStorage = game:GetService("ReplicatedStorage")
local httpService = game:GetService("HttpService")
local Enum_ = require(script.Enum)
local enum = Enum_.enums
local Janitor = require(script.Janitor)
local Signal = require(script.Signal)
local ZonePlusReference = require(script.ZonePlusReference)
local referenceObject = ZonePlusReference.getObject()
local zoneControllerModule = script.ZoneController
local trackerModule = zoneControllerModule.Tracker
local collectiveWorldModelModule = zoneControllerModule.CollectiveWorldModel
local ZoneController = require(zoneControllerModule)
local referenceLocation = (game:GetService("RunService"):IsClient() and "Client") or "Server"
local referencePresent = referenceObject and referenceObject:FindFirstChild(referenceLocation)
if referencePresent then
return require(referenceObject.Value)
end
local Zone = {}
Zone.__index = Zone
if not referencePresent then
ZonePlusReference.addToReplicatedStorage()
end
Zone.enum = enum
-- CONSTRUCTORS
function Zone.new(container)
local self = {}
setmetatable(self, Zone)
-- Validate container
local INVALID_TYPE_WARNING = "The zone container must be a model, folder, basepart or table!"
local containerType = typeof(container)
if not(containerType == "table" or containerType == "Instance") then
error(INVALID_TYPE_WARNING)
end
-- Configurable
self.accuracy = enum.Accuracy.High
self.autoUpdate = true
self.respectUpdateQueue = true
--self.maxPartsAddition = 20
--self.ignoreRecommendedMaxParts = false
-- Variable
local janitor = Janitor.new()
self.janitor = janitor
self._updateConnections = janitor:add(Janitor.new(), "destroy")
self.container = container
self.zoneParts = {}
self.overlapParams = {}
self.region = nil
self.volume = nil
self.boundMin = nil
self.boundMax = nil
self.recommendedMaxParts = nil
self.zoneId = httpService:GenerateGUID()
self.activeTriggers = {}
self.occupants = {}
self.trackingTouchedTriggers = {}
self.enterDetection = enum.Detection.Centre
self.exitDetection = enum.Detection.Centre
self._currentEnterDetection = nil -- This will update automatically internally
self._currentExitDetection = nil -- This will also update automatically internally
self.totalPartVolume = 0
self.allZonePartsAreBlocks = true
self.trackedItems = {}
self.settingsGroupName = nil
self.worldModel = workspace
self.onItemDetails = {}
self.itemsToUntrack = {}
-- This updates _currentEnterDetection and _currentExitDetection right away to prevent nil comparisons
ZoneController.updateDetection(self)
-- Signals
self.updated = janitor:add(Signal.new(), "destroy")
local triggerTypes = {
"player",
"part",
"localPlayer",
"item"
}
local triggerEvents = {
"entered",
"exited",
}
for _, triggerType in pairs(triggerTypes) do
local activeConnections = 0
local previousActiveConnections = 0
for i, triggerEvent in pairs(triggerEvents) do
-- this enables us to determine when a developer connects to an event
-- so that we can act accoridngly (i.e. begin or end a checker loop)
local signal = janitor:add(Signal.new(true), "destroy")
local triggerEventUpper = triggerEvent:sub(1,1):upper()..triggerEvent:sub(2)
local signalName = triggerType..triggerEventUpper
self[signalName] = signal
signal.connectionsChanged:Connect(function(increment)
if triggerType == "localPlayer" and not localPlayer and increment == 1 then
error(("Can only connect to 'localPlayer%s' on the client!"):format(triggerEventUpper))
end
previousActiveConnections = activeConnections
activeConnections += increment
if previousActiveConnections == 0 and activeConnections > 0 then
-- At least 1 connection active, begin loop
ZoneController._registerConnection(self, triggerType, triggerEventUpper)
elseif previousActiveConnections > 0 and activeConnections == 0 then
-- All connections have disconnected, end loop
ZoneController._deregisterConnection(self, triggerType)
end
end)
end
end
-- Setup touched receiver functions where applicable
Zone.touchedConnectionActions = {}
for _, triggerType in pairs(triggerTypes) do
local methodName = ("_%sTouchedZone"):format(triggerType)
local correspondingMethod = self[methodName]
if correspondingMethod then
self.trackingTouchedTriggers[triggerType] = {}
Zone.touchedConnectionActions[triggerType] = function(touchedItem)
correspondingMethod(self, touchedItem)
end
end
end
-- This constructs the zones boundaries, region, etc
self:_update()
-- Register/deregister zone
ZoneController._registerZone(self)
janitor:add(function()
ZoneController._deregisterZone(self)
end, true)
return self
end
function Zone.fromRegion(cframe, size)
local MAX_PART_SIZE = 2024
local container = Instance.new("Model")
local function createCube(cubeCFrame, cubeSize)
if cubeSize.X > MAX_PART_SIZE or cubeSize.Y > MAX_PART_SIZE or cubeSize.Z > MAX_PART_SIZE then
local quarterSize = cubeSize * 0.25
local halfSize = cubeSize * 0.5
createCube(cubeCFrame * CFrame.new(-quarterSize.X, -quarterSize.Y, -quarterSize.Z), halfSize)
createCube(cubeCFrame * CFrame.new(-quarterSize.X, -quarterSize.Y, quarterSize.Z), halfSize)
createCube(cubeCFrame * CFrame.new(-quarterSize.X, quarterSize.Y, -quarterSize.Z), halfSize)
createCube(cubeCFrame * CFrame.new(-quarterSize.X, quarterSize.Y, quarterSize.Z), halfSize)
createCube(cubeCFrame * CFrame.new(quarterSize.X, -quarterSize.Y, -quarterSize.Z), halfSize)
createCube(cubeCFrame * CFrame.new(quarterSize.X, -quarterSize.Y, quarterSize.Z), halfSize)
createCube(cubeCFrame * CFrame.new(quarterSize.X, quarterSize.Y, -quarterSize.Z), halfSize)
createCube(cubeCFrame * CFrame.new(quarterSize.X, quarterSize.Y, quarterSize.Z), halfSize)
else
local part = Instance.new("Part")
part.CFrame = cubeCFrame
part.Size = cubeSize
part.Anchored = true
part.Parent = container
end
end
createCube(cframe, size)
local zone = Zone.new(container)
zone:relocate()
return zone
end
-- PRIVATE METHODS
function Zone:_calculateRegion(tableOfParts, dontRound)
local bounds = {["Min"] = {}, ["Max"] = {}}
for boundType, details in pairs(bounds) do
details.Values = {}
function details.parseCheck(v, currentValue)
if boundType == "Min" then
return (v <= currentValue)
elseif boundType == "Max" then
return (v >= currentValue)
end
end
function details:parse(valuesToParse)
for i,v in pairs(valuesToParse) do
local currentValue = self.Values[i] or v
if self.parseCheck(v, currentValue) then
self.Values[i] = v
end
end
end
end
for _, part in pairs(tableOfParts) do
local sizeHalf = part.Size * 0.5
local corners = {
part.CFrame * CFrame.new(-sizeHalf.X, -sizeHalf.Y, -sizeHalf.Z),
part.CFrame * CFrame.new(-sizeHalf.X, -sizeHalf.Y, sizeHalf.Z),
part.CFrame * CFrame.new(-sizeHalf.X, sizeHalf.Y, -sizeHalf.Z),
part.CFrame * CFrame.new(-sizeHalf.X, sizeHalf.Y, sizeHalf.Z),
part.CFrame * CFrame.new(sizeHalf.X, -sizeHalf.Y, -sizeHalf.Z),
part.CFrame * CFrame.new(sizeHalf.X, -sizeHalf.Y, sizeHalf.Z),
part.CFrame * CFrame.new(sizeHalf.X, sizeHalf.Y, -sizeHalf.Z),
part.CFrame * CFrame.new(sizeHalf.X, sizeHalf.Y, sizeHalf.Z),
}
for _, cornerCFrame in pairs(corners) do
local x, y, z = cornerCFrame:GetComponents()
local values = {x, y, z}
bounds.Min:parse(values)
bounds.Max:parse(values)
end
end
local minBound = {}
local maxBound = {}
-- Rounding a regions coordinates to multiples of 4 ensures the region optimises the region
-- by ensuring it aligns on the voxel grid
local function roundToFour(to_round)
local ROUND_TO = 4
local divided = (to_round+ROUND_TO/2) / ROUND_TO
local rounded = ROUND_TO * math.floor(divided)
return rounded
end
for boundName, boundDetail in pairs(bounds) do
for _, v in pairs(boundDetail.Values) do
local newTable = (boundName == "Min" and minBound) or maxBound
local newV = v
if not dontRound then
local roundOffset = (boundName == "Min" and -2) or 2
newV = roundToFour(v+roundOffset) -- +-2 to ensures the zones region is not rounded down/up
end
table.insert(newTable, newV)
end
end
local boundMin = Vector3.new(unpack(minBound))
local boundMax = Vector3.new(unpack(maxBound))
local region = Region3.new(boundMin, boundMax)
return region, boundMin, boundMax
end
function Zone:_displayBounds()
if not self.displayBoundParts then
self.displayBoundParts = true
local boundParts = {BoundMin = self.boundMin, BoundMax = self.boundMax}
for boundName, boundCFrame in pairs(boundParts) do
local part = Instance.new("Part")
part.Anchored = true
part.CanCollide = false
part.Transparency = 0.5
part.Size = Vector3.new(1,1,1)
part.Color = Color3.fromRGB(255,0,0)
part.CFrame = CFrame.new(boundCFrame)
part.Name = boundName
part.Parent = workspace
self.janitor:add(part, "Destroy")
end
end
end
function Zone:_update()
local container = self.container
local zoneParts = {}
local updateQueue = 0
self._updateConnections:clean()
local containerType = typeof(container)
local holders = {}
local INVALID_TYPE_WARNING = "The zone container must be a model, folder, basepart or table!"
if containerType == "table" then
for _, part in pairs(container) do
if part:IsA("BasePart") then
table.insert(zoneParts, part)
end
end
elseif containerType == "Instance" then
if container:IsA("BasePart") then
table.insert(zoneParts, container)
else
table.insert(holders, container)
for _, part in pairs(container:GetDescendants()) do
if part:IsA("BasePart") then
table.insert(zoneParts, part)
else
table.insert(holders, part)
end
end
end
end
self.zoneParts = zoneParts
self.overlapParams = {}
local allZonePartsAreBlocksNew = true
for _, zonePart in pairs(zoneParts) do
local success, shapeName = pcall(function() return zonePart.Shape.Name end)
if shapeName ~= "Block" then
allZonePartsAreBlocksNew = false
end
end
self.allZonePartsAreBlocks = allZonePartsAreBlocksNew
local zonePartsWhitelist = OverlapParams.new()
zonePartsWhitelist.FilterType = Enum.RaycastFilterType.Whitelist
zonePartsWhitelist.MaxParts = #zoneParts
zonePartsWhitelist.FilterDescendantsInstances = zoneParts
self.overlapParams.zonePartsWhitelist = zonePartsWhitelist
local zonePartsIgnorelist = OverlapParams.new()
zonePartsIgnorelist.FilterType = Enum.RaycastFilterType.Blacklist
zonePartsIgnorelist.FilterDescendantsInstances = zoneParts
self.overlapParams.zonePartsIgnorelist = zonePartsIgnorelist
-- this will call update on the zone when the container parts size or position changes, and when a
-- child is removed or added from a holder (anything which isn't a basepart)
local function update()
if self.autoUpdate then
local executeTime = os.clock()
if self.respectUpdateQueue then
updateQueue += 1
executeTime += 0.1
end
local updateConnection
updateConnection = runService.Heartbeat:Connect(function()
if os.clock() >= executeTime then
updateConnection:Disconnect()
if self.respectUpdateQueue then
updateQueue -= 1
end
if updateQueue == 0 and self.zoneId then
self:_update()
end
end
end)
end
end
local partProperties = {"Size", "Position"}
local function verifyDefaultCollision(instance)
if instance.CollisionGroupId ~= 0 then
error("Zone parts must belong to the 'Default' (0) CollisionGroup! Consider using zone:relocate() if you wish to move zones outside of workspace to prevent them interacting with other parts.")
end
end
for _, part in pairs(zoneParts) do
for _, prop in pairs(partProperties) do
self._updateConnections:add(part:GetPropertyChangedSignal(prop):Connect(update), "Disconnect")
end
verifyDefaultCollision(part)
self._updateConnections:add(part:GetPropertyChangedSignal("CollisionGroupId"):Connect(function()
verifyDefaultCollision(part)
end), "Disconnect")
end
local containerEvents = {"ChildAdded", "ChildRemoved"}
for _, holder in pairs(holders) do
for _, event in pairs(containerEvents) do
self._updateConnections:add(self.container[event]:Connect(function(child)
if child:IsA("BasePart") then
update()
end
end), "Disconnect")
end
end
local region, boundMin, boundMax = self:_calculateRegion(zoneParts)
local exactRegion, _, _ = self:_calculateRegion(zoneParts, true)
self.region = region
self.exactRegion = exactRegion
self.boundMin = boundMin
self.boundMax = boundMax
local rSize = region.Size
self.volume = rSize.X*rSize.Y*rSize.Z
-- Update: I was going to use this for the old part detection until the CanTouch property was released
-- everything below is now irrelevant however I'll keep just in case I use again for future
-------------------------------------------------------------------------------------------------
-- When a zones region is determined, we also check for parts already existing within the zone
-- these parts are likely never to move or interact with the zone, so we set the number of these
-- to the baseline MaxParts value. 'recommendMaxParts' is then determined through the sum of this
-- and maxPartsAddition. This ultimately optimises region checks as they can be generated with
-- minimal MaxParts (i.e. recommendedMaxParts can be used instead of math.huge every time)
--[[
local result = self.worldModel:FindPartsInRegion3(region, nil, math.huge)
local maxPartsBaseline = #result
self.recommendedMaxParts = maxPartsBaseline + self.maxPartsAddition
--]]
self:_updateTouchedConnections()
self.updated:Fire()
end
function Zone:_updateOccupants(trackerName, newOccupants)
local previousOccupants = self.occupants[trackerName]
if not previousOccupants then
previousOccupants = {}
self.occupants[trackerName] = previousOccupants
end
local signalsToFire = {}
for occupant, prevItem in pairs(previousOccupants) do
local newItem = newOccupants[occupant]
if newItem == nil or newItem ~= prevItem then
previousOccupants[occupant] = nil
if not signalsToFire.exited then
signalsToFire.exited = {}
end
table.insert(signalsToFire.exited, occupant)
end
end
for occupant, _ in pairs(newOccupants) do
if previousOccupants[occupant] == nil then
local isAPlayer = occupant:IsA("Player")
previousOccupants[occupant] = (isAPlayer and occupant.Character) or true
if not signalsToFire.entered then
signalsToFire.entered = {}
end
table.insert(signalsToFire.entered, occupant)
end
end
return signalsToFire
end
function Zone:_formTouchedConnection(triggerType)
local touchedJanitorName = "_touchedJanitor"..triggerType
local touchedJanitor = self[touchedJanitorName]
if touchedJanitor then
touchedJanitor:clean()
else
touchedJanitor = self.janitor:add(Janitor.new(), "destroy")
self[touchedJanitorName] = touchedJanitor
end
self:_updateTouchedConnection(triggerType)
end
function Zone:_updateTouchedConnection(triggerType)
local touchedJanitorName = "_touchedJanitor"..triggerType
local touchedJanitor = self[touchedJanitorName]
if not touchedJanitor then return end
for _, basePart in pairs(self.zoneParts) do
touchedJanitor:add(basePart.Touched:Connect(self.touchedConnectionActions[triggerType], self), "Disconnect")
end
end
function Zone:_updateTouchedConnections()
for triggerType, _ in pairs(self.touchedConnectionActions) do
local touchedJanitorName = "_touchedJanitor"..triggerType
local touchedJanitor = self[touchedJanitorName]
if touchedJanitor then
touchedJanitor:cleanup()
self:_updateTouchedConnection(triggerType)
end
end
end
function Zone:_disconnectTouchedConnection(triggerType)
local touchedJanitorName = "_touchedJanitor"..triggerType
local touchedJanitor = self[touchedJanitorName]
if touchedJanitor then
touchedJanitor:cleanup()
self[touchedJanitorName] = nil
end
end
local function round(number, decimalPlaces)
return math.round(number * 10^decimalPlaces) * 10^-decimalPlaces
end
function Zone:_partTouchedZone(part)
local trackingDict = self.trackingTouchedTriggers["part"]
if trackingDict[part] then return end
local nextCheck = 0
local verifiedEntrance = false
local enterPosition = part.Position
local enterTime = os.clock()
local partJanitor = self.janitor:add(Janitor.new(), "destroy")
trackingDict[part] = partJanitor
local instanceClassesToIgnore = {Seat = true, VehicleSeat = true}
local instanceNamesToIgnore = {HumanoidRootPart = true}
if not (instanceClassesToIgnore[part.ClassName] or not instanceNamesToIgnore[part.Name]) then
part.CanTouch = false
end
--
local partVolume = round((part.Size.X * part.Size.Y * part.Size.Z), 5)
self.totalPartVolume += partVolume
--
partJanitor:add(heartbeat:Connect(function()
local clockTime = os.clock()
if clockTime >= nextCheck then
----
local cooldown = enum.Accuracy.getProperty(self.accuracy)
nextCheck = clockTime + cooldown
----
-- We initially perform a singular point check as this is vastly more lightweight than a large part check
-- If the former returns false, perform a whole part check in case the part is on the outer bounds.
local withinZone = self:findPoint(part.CFrame)
if not withinZone then
withinZone = self:findPart(part)
end
if not verifiedEntrance then
if withinZone then
verifiedEntrance = true
self.partEntered:Fire(part)
elseif (part.Position - enterPosition).Magnitude > 1.5 and clockTime - enterTime >= cooldown then
-- Even after the part has exited the zone, we track it for a brief period of time based upon the criteria
-- in the line above to ensure the .touched behaviours are not abused
partJanitor:cleanup()
end
elseif not withinZone then
verifiedEntrance = false
enterPosition = part.Position
enterTime = os.clock()
self.partExited:Fire(part)
end
end
end), "Disconnect")
partJanitor:add(function()
trackingDict[part] = nil
part.CanTouch = true
self.totalPartVolume = round((self.totalPartVolume - partVolume), 5)
end, true)
end
local partShapeActions = {
["Ball"] = function(part)
return "GetPartBoundsInRadius", {part.Position, part.Size.X}
end,
["Block"] = function(part)
return "GetPartBoundsInBox", {part.CFrame, part.Size}
end,
["Other"] = function(part)
return "GetPartsInPart", {part}
end,
}
function Zone:_getRegionConstructor(part, overlapParams)
local success, shapeName = pcall(function() return part.Shape.Name end)
local methodName, args
if success and self.allZonePartsAreBlocks then
local action = partShapeActions[shapeName]
if action then
methodName, args = action(part)
end
end
if not methodName then
methodName, args = partShapeActions.Other(part)
end
if overlapParams then
table.insert(args, overlapParams)
end
return methodName, args
end
-- PUBLIC METHODS
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
This is the Zone code.