Terrain randomly teleports down to the bottom of the game

My map is made mostly out of Terrain.
I placed the Terrain high up from the baseplate (like 500 studs), so I can use the baseplate as a testing area.

Occasionally, in a live server, the Terrain just teleports to the bottom (right on top of the baseplate) without warning.
This is really annoying, and I don’t know what is causing this.

My code only uses Terrain:FillBall(), Terrain:ReadVoxels(), Terrain:GetMaterialColor(), Terrain:SetMaterialColor().
(GetMaterial and SetMaterial is rarely used)

As you can see, only the Terrain moved to the bottom and the map Parts did not




This is what the map is meant to look like:

Expected behavior

I expect my Terrain to stay where I placed it, not randomly teleport to the bottom (on top of the baseplate).

A private message is associated with this bug report

1 Like

Does workspace.Terrain.CFrame / workspace.Terrain.Position change when the terrain suddenly moves?
Are there any scripts that modify idk

I’lll add a game.Terrain.Changed connection that detects changes, and then see if anything changes when it happens

changing the cframe or position of Terrain does nothing…
(at least when I tested it with the command bar in studios)

Hey @Ryan_MAN100 , I want to take a closer look at this. Help me rule out a couple of things if you can:

Is this visual only?

  • i.e if a player were to spawn, do they fall down, or is terrain collision still happening at the appropriate location.

Does it persist?

  • i.e it doesn’t magically snap back after some time or in response to a change in the world

Confirm you got a repro in a Roblox Studio play session (your latest reply suggests you did). That should make things easier to reproduce on my end. If the place file is something you’re comfortable sharing, let me know.

Thanks!

I’m sorry for the late reply, things have been busy and I haven’t checked too often :pensive_face:

Yes, the collision does update and the players fall down too.

I don’t believe that it magically snap back after some time, but I can’t verify this since everyone eventually leaves after a few minutes because the game is basically unplayable.

Do you want the game files of my game?
I would willingly give it to you if it will help with figuring out the problem!

Oh wait, a player sent me a video. (right after I sent my previous reply)

It appears as though the terrain’s collision stays until the player dies. And afterwards, the collision updates. The raycasts from the player grappling on the terrain is working, so raycasts are hitting the updated terrain collision correctly.

Terrain that I had placed under the map also seems to have moved down (or deleted) too.

Players get teleported outside of the shelter (the safe where everyone are at the end of the day/cycle) BEFORE the rain started too. The players did not teleport to the same location though (you can see another player right before the recording player dies; they seem to also be standing on thin air).
(the player started recording after they got teleported out of the shelter; they are also taking damage because they left the shelter safe zone)

Terrain appears to also not be directly on top of the baseplate, but also below it

(this happens pretty often, see how the player reacts “Not again”)

Other player:

Standing in air:

Falling after death (look at the trail):

Recorder player and other player (white; pretty far) falling down after respawning:

Raycast hitting terrain correctly (red dot is where the raycast landed):

Player looking up at where the terrain is supposed to be (inside of that box in the air):

Some of the terrain actually went under the baseplate… (there is supposed to be a cave there)

This should make you download the video I think (if not, let me know):

this can happen to servers as young as 1 hours old to 20 hours old
I just had one happen back to back, so basically all of my active players got deleted from shutting down these 2 servers

more info:

The red circled parts are terrain (it looks like half of the terrain is above the baseplate while other half is below):

apparently, it can happen either at the start of the rain or at the end of the rain (rain is when the game is clearing out creatures and items and spawning new items, so it is generally when it is super laggy)
Here is our conversation

Some players did not get teleported out and stayed inside the safe zone (this was when it happened after the rain ended; the video from my previous replay was from when the rain started; not sure if this means anything):

btw, I don’t know how to reproduce the problem at all. I definitely did not write any code to do that either.

I am pretty confident that it did not come from a malicious script from the toolbox because I always delete scripts from the toolbox.

:3c

Could you check whether this place was ever saved or published under Terrain Early Access, including by Team Create collaborators? Recent EAP reports mention unexpected terrain loss, though the symptoms differ

just guess
I don’t know, can it remade?

okay, slightly different problem..

this time the terrain is just straight up deleted (Nothing below baseplate from what I see)

what is that?

also, this problem has been a thing for a while…

Sorry, I missed out on your replies too. The finding with collision is a useful hint.

In the meantime do double check on the Early Access @NaNesser pointed out. That may have changed aspects of how terrain is serialized, which is an immediate red flag with issues like yours.

You mentioned you are using the Terrain APIs to create this terrain, maybe you can share those scripts instead? e.g is the terrain created only once at startup, do you read/write to it as the game evolves, what is the rain script doing, and which scripts are running on the Server vs Client. If it is easier for you, go ahead and share the place file and I’ll take a look.

P.S: The attached video is no longer available, try resharing it if it is still around.

Thanks!

no, I don’t think I am using the Terrain Early Access thing. I never heard of it before. And this problem existed before that even existed. (I don’t remember when it started)

I built the map (Terrain) in Studios using the Terrain Editor, not with a script.

Here is the video again (Youtube):

Here are the lines where I did stuff with Terrain:

-- Destroy Terrain
if destroyTerrain and not TerrainHelper.IsInWater(position, radius * 2) then
	local hitSize = math.min(radius / 5, 100)
	workspace.Terrain:FillBall(position, hitSize, Enum.Material.Air)
end
local function isInWater(position : Vector3, size : Vector3?)
	if not size then size = Vector3.new(2, 2, 2) end
	local min = position - size / 2
	local max = position + size / 2
	local region = Region3.new(min, max):ExpandToGrid(4)
	local material = workspace.Terrain:ReadVoxels(region, 4)[1][1][1]
	return material == Enum.Material.Water
end
local function waterMultiplier(position : Vector3, size : Vector3)
	if not size then size = Vector3.new(2, 2, 2) end
	local min = position - size / 2
	local max = position + size / 2
	local region = Region3.new(min, max):ExpandToGrid(4)
	
	local material = workspace.Terrain:ReadVoxels(region, 4)[1][1][1]
	if material == Enum.Material.Water then
		return 0.8
	end
	return 0.99
end
workspace.Terrain:FillBall(hitPosition, hitBox.Size.X/10, Enum.Material.Air)
VisualsSetting.RealisticGrassChanged = function(enabled : boolean)
	if enabled then
		if workspace.Terrain:GetMaterialColor(Enum.Material.Grass) == Color3.fromRGB(116, 174, 47) then
			workspace.Terrain:SetMaterialColor(Enum.Material.Grass, Color3.fromRGB(111, 126, 62))
		end
	else
		if workspace.Terrain:GetMaterialColor(Enum.Material.Grass) == Color3.fromRGB(111, 126, 62) then
			workspace.Terrain:SetMaterialColor(Enum.Material.Grass, Color3.fromRGB(116, 174, 47))
		end
	end
end
ApplyStyling.OnClientEvent:Connect(function(region : string)
	local module : ModuleScript = RegionData:FindFirstChild(region)
	if not module then
		warn("No region styling for " .. region)
		return
	end
	local Style = require(module)
	
	-- Apply Terrain Color
	for material : Enum.Material, color : Color3 in Style.TerrainColor do
		if material == Enum.Material.Water then
			Terrain.WaterColor = color
			continue
		end
		Terrain:SetMaterialColor(material, color)
	end
	
	-- Apply Atmosphere Properties
	TweenService:Create(Lighting.Atmosphere, tweenInfo, Style.Atmosphere):Play()
end)

I am not sure if this is worth noting, but I used this plugin to import terrain from another place:


It copies terrain from my building experience to the actual experience. It works pretty well.
https://create.roblox.com/store/asset/148042198/Terrain-Save-and-Load
However, I think this problem existed well before I heard of this plugin…

The client doesn’t do anything with Terrain, only the Server does.

Here is the Time module that handles rain and stuff:

-- Services
local Lighting = game:GetService("Lighting")
local Players = game:GetService("Players")
local RunService = game:GetService("RunService")
local ServerStorage = game:GetService("ServerStorage")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local ServerScriptService = game:GetService("ServerScriptService")
-- Modules
local Helper = require(ReplicatedStorage.Modules.Helper)
local WebhookHandler = require(ServerStorage.Modules.WebhookHandler)
local PlayerHandler = require(ServerStorage.Modules.PlayerHandler)
local CharacterHandler = require(ServerStorage.Modules.CharacterHandler)
local DataManager = require(ServerScriptService.Data.DataManager)
local KicksHandler = require(ServerScriptService.Modules.KicksHandler)
-- Remotes
local HighlightShelter = ReplicatedStorage.Remotes.Visuals.HighlightShelter
local SetClock = ReplicatedStorage.Remotes.Time.SetClock
local SetClockParams = require(SetClock.Params)
local SetRain = ReplicatedStorage.Remotes.Time.SetRain
local SetRainParams = require(SetRain.Params)
local Received = ReplicatedStorage.Remotes.Referral.Received
-- Bindables
local NewCycle = ServerStorage.Bindables.Time.NewCycle
local RainStarted = ServerStorage.Bindables.Time.RainStarted
local RespawnItems = ServerStorage.Bindables.ItemSpawner.Respawn
local ShelterGate = ServerStorage.Bindables.ShelterGate
local ShelterGateEnum = require(ShelterGate.Enum)
local StartCreatureSpawning = ServerStorage.Bindables.CreatureSpawner.Start
local StopCreatureSpawning = ServerStorage.Bindables.CreatureSpawner.Stop
-- Fun Panel
local Transition = ServerStorage.Bindables.Fun.Time.Transition
local SetDayLengthRange = ServerStorage.Bindables.Fun.Time.SetDayLengthRange
local SetNightLengthRange = ServerStorage.Bindables.Fun.Time.SetNightLengthRange
-- Data
local TimeData = require(ReplicatedStorage.Modules.Data.TimeData)
local TimeEnum = require(ReplicatedStorage.Modules.Data.TimeData.Enum)
local Assets = require(ReplicatedStorage.Modules.Assets)

local Time = {}

Time.TimeEvents = {}
Time.CurrentTimeEvents = {}

-- Data
Time.DayLengthRange = TimeData.DayLengthRange
Time.NightLengthRange = TimeData.NightLengthRange

-- Variables
Time.CurrentCycle = 0
Time.CurrentTime = TimeData.DaytimeRange.Y
Time.CurrentState = TimeEnum.State.Night
Time.StateDuration = 3
Time.TimeRate = TimeData.NighttimeLength / Time.StateDuration

-- Fun Panel
Time.FunPanel = {
	Transition = false,
}

-- Debounce
Time.LastClockTimeUpdate = time()

--[[
	Initialize the time system
	- Setup tick
	- Setup fun panel
	- Setup player joining
]]
Time.Init = function()
	-- Holds Functions that run at specific times
	local timeEvents = {
		StartRain = {
			RemainingTime = TimeData.RainStartOffset,
			State = TimeEnum.State.Day,
			Event = Time.StartRain,
		},
		StopRain = {
			State = TimeEnum.State.Day,
			Event = Time.StopRain,
		},
		StartDangerousRain = {
			State = TimeEnum.State.Night,
			Event = Time.StartDangerousRain,
		},
		RespawnItems = {
			Time = 0,
			Event = Time.RespawnItems,
			LastFired = -1, -- so items respawn at the start of the round
		},
		BatflyClearing = {
			Time = 0,
			Event = Time.BatflyClearing,
		},
		LizardClearing = {
			Time = 0,
			Event = Time.LizardClearing,
		},
		StartCreatureSpawning = {
			State = TimeEnum.State.Day,
			Event = Time.StartCreatureSpawning,
		},
		StopCreatureSpawning = {
			RemainingTime = 10,
			State = TimeEnum.State.Day,
			Event = Time.StopCreatureSpawning,
		},
		OpenShelterGates = {
			State = TimeEnum.State.Day,
			Event = Time.OpenShelterGates,
		},
		CloseShelterGates = {
			State = TimeEnum.State.Night,
			Event = Time.CloseShelterGates,
		}
	}
	
	-- Format Time Events
	for eventName,event : TimeEvent in timeEvents do
		if not event.Event then
			warn("Event is missing from time event: ", event)
			timeEvents[eventName] = nil
			continue
		end
		if not event.LastFired then
			event.LastFired = 0
		end
	end
	
	-- Set Time Events
	Time.TimeEvents = timeEvents
	Time.CurrentTimeEvents = table.clone(timeEvents)
	
	-- Setup workspace attributes
	workspace:SetAttribute("ServerCycleCount", 0)
	workspace:SetAttribute("TimeState", Time.CurrentState)
	workspace:SetAttribute("StateDuration", Time.StateDuration)
	workspace:SetAttribute("DayLengthRange", Time.DayLengthRange)
	workspace:SetAttribute("NightLengthRange", Time.NightLengthRange)
	
	-- Setup Fun Panel
	Transition.Event:Connect(function()
		Time.FunPanel.Transition = true
	end)
	SetDayLengthRange.Event:Connect(function(min : number, max : number)
		Time.DayLengthRange = Vector2.new(min, max)
		workspace:SetAttribute("DayLengthRange", Time.DayLengthRange)
		timeEvents.StartRain.RemainingTime = math.min(TimeData.RainStartOffset, Time.DayLengthRange.X - 1)
	end)
	SetNightLengthRange.Event:Connect(function(min : number, max : number)
		Time.NightLengthRange = Vector2.new(min, max)
		workspace:SetAttribute("NightLengthRange", Time.NightLengthRange)
	end)
	
	-- Setup Tick
	RunService.Heartbeat:Connect(function(deltaTime)
		Time.Tick(deltaTime)
	end)
	
	-- Player joining handling
	Players.PlayerAdded:Connect(function(player)
		-- Set Clock on join (handle late joiners)
		local clockParams = SetClockParams.new()
		clockParams.CurrentState = Time.CurrentState
		clockParams.StateDuration = Time.StateDuration
		clockParams.Progress = Time.GetTimeProgressInCurrentState()
		SetClock:FireClient(player, clockParams)
		
		local rainParams = SetRainParams.new()
		rainParams.Enabled = workspace:GetAttribute("Raining")
		if Time.CurrentState == TimeEnum.State.Night then
			-- if night, instantly enable
			rainParams.Dangerous = true
			-- Note: No need to set duration because it defaults to 0 and the thing ignores it anyways because it is dangerous rain
		else
			-- if day, set duration to the remaining time left
			rainParams.Duration = Time.GetRemainingTimeInCurrentState()
		end
		SetRain:FireClient(player, rainParams)
	end)
	
	-- Exploit prevention
	SetClock.OnServerEvent:Connect(function(player : Player)
		KicksHandler.Kick(player, KicksHandler.Enum.KickCode.SetClockFiredFromClient, KicksHandler.Enum.Reason.TSC)
	end)
	SetRain.OnServerEvent:Connect(function(player : Player)
		KicksHandler.Kick(player, KicksHandler.Enum.KickCode.StartRainFiredFromClient, KicksHandler.Enum.Reason.TSR)
	end)
	HighlightShelter.OnServerEvent:Connect(function(player)
		KicksHandler.Kick(player, "HSFFC", "Highlight shelter fired from client")
	end)
	Received.OnServerEvent:Connect(function(player)
		KicksHandler.Kick(player, "RCE", "Received Referral fired from client")
	end)
end

--[[
	Transition to the next state
]]
Time.TransitionToNextState = function(setCurrentTime : boolean?)
	-- Get new state and the state's settings
	local newState = Time.CurrentState == TimeEnum.State.Day and TimeEnum.State.Night or TimeEnum.State.Day
	local stateDuration = Time.GetNewStateDuration(newState)
	local newTimeRate = Time.GetTimeRate(newState, stateDuration)
	
	-- Update state
	Time.CurrentState = newState
	Time.StateDuration = stateDuration
	Time.TimeRate = newTimeRate
	
	-- Set Workspace Attributes
	workspace:SetAttribute("TimeState", newState)
	workspace:SetAttribute("StateDuration", stateDuration)
	
	print("Transitioned to", newState, "state. State duration:", stateDuration, "seconds. Time rate:", newTimeRate, "hours per second.")
	
	-- Set Current Time to be the start of the next state
	if setCurrentTime then
		if newState == TimeEnum.State.Day then
			Time.CurrentTime = TimeData.DaytimeRange.X
		else
			Time.CurrentTime = TimeData.DaytimeRange.Y
		end
	end
	
	-- Set Clock on Clients
	local params = SetClockParams.new()
	params.CurrentState = newState
	params.StateDuration = stateDuration
	params.Progress = 0
	SetClock:FireAllClients(params)
	
	-- Handle New Cycle
	if newState == TimeEnum.State.Day then
		-- Increment Server Cycle Count
		Time.CurrentCycle += 1
		workspace:SetAttribute("ServerCycleCount", Time.CurrentCycle)
		WebhookHandler.AppendEventMessage(`Cycle {Time.CurrentCycle} started! Length: \`{stateDuration}\` seconds`)
		
		-- Fire Events
		NewCycle:Fire()
		
		-- Unhighlight Shelters
		HighlightShelter:FireAllClients(false)
		
		-- Update Player Stats
		Time.UpdatePlayers()
		
		workspace:SetAttribute("Raining", false)
	end
	
	-- Handle rain
	if newState == TimeEnum.State.Night then
		-- Fire Events
		RainStarted:Fire()
	end
end

--[[
	Update the time based on the current state (called every frame)
	> deltaTime - the time since the last update
]]
Time.Tick = function(deltaTime : number)
	-- Store previous time
	local previousTime = Time.CurrentTime
	
	-- Calculate current time
	local currentTime = (previousTime + (Time.TimeRate * deltaTime) )
	
	-- Check if we've looped back to the start (why not %?, because I need to know when it loop)
	local looped = false
	if currentTime > 24 then
		currentTime -= 24
		looped = true
	end
	
	-- Skip current state
	if Time.FunPanel.Transition then
		if Time.CurrentState == TimeEnum.State.Day then
			-- ez, set to start of day
			currentTime = TimeData.DaytimeRange.Y
		elseif Time.CurrentState == TimeEnum.State.Night then
			-- go to the end of the Time Events list
			currentTime = 24
			-- fire necessary time events (pretty much all of them)
			Time.HandleTimeEvents(previousTime, currentTime)
			-- reset current time events
			Time.CurrentTimeEvents = table.clone(Time.TimeEvents)
			-- set time to start of day
			currentTime = TimeData.DaytimeRange.X
			
			-- prevent resetting (just in case)
			looped = false
		end
		Time.FunPanel.Transition = false
	end
	
	-- Make sure time is capped within their state range (so the Time Events fire accurately)
	if Time.CurrentState == TimeEnum.State.Day then
		currentTime = math.clamp(currentTime, TimeData.DaytimeRange.X, TimeData.DaytimeRange.Y)
	elseif Time.CurrentState == TimeEnum.State.Night then
		if currentTime > TimeData.DaytimeRange.X and currentTime < TimeData.DaytimeRange.Y then
			currentTime = TimeData.DaytimeRange.X
		end
	end
	
	-- Set current time
	Time.CurrentTime = currentTime
	
	-- Update Clock Time
	if time() - Time.LastClockTimeUpdate > 1 then
		Lighting.ClockTime = currentTime
		Time.LastClockTimeUpdate = time()
	end
	
	--------------------------------------------------
	-- Handle Time Events
	
	Time.HandleTimeEvents(previousTime, currentTime)
	
	--------------------------------------------------
	-- Reset Time Events
	
	if looped then
		Time.CurrentTimeEvents = table.clone(Time.TimeEvents)
	end
	
	--------------------------------------------------
	-- Transition to next state if needed
	if Time.GetStateFromCurrentTime(Time.CurrentTime) ~= Time.CurrentState then
		Time.TransitionToNextState()
	end
	
end

Time.HandleTimeEvents = function(previousTime : number, currentTime : number)
	-- Cache values
	local currentState = Time.CurrentState

	-- Store Remaining Time (better to calculate once at the start because it won't change)
	local remainingTime = Time.GetRemainingTimeInCurrentState()

	--print("Remaining Time:", remainingTime, "Progress:", Time.GetProgressOfCurrentState() * 100, "%")

	-- Fire Events
	for eventName, event : TimeEvent in Time.CurrentTimeEvents do
		-- Check if already fired
		if event.LastFired >= Time.CurrentCycle then continue end

		-- Check if correct state
		if event.State and event.State ~= currentState then continue end

		-- Check if Remaining Time is passed
		if event.RemainingTime and remainingTime > event.RemainingTime then continue end

		-- Check if Time is passed (previous time is before event time)
		if event.Time then
			local passedEvent = false

			if previousTime <= currentTime then
				-- Normal day progression (e.g., 14.5 -> 15.2)
				passedEvent = (previousTime < event.Time and currentTime >= event.Time)
			else
				-- Midnight wrap-around (e.g., 23.8 -> 0.1)
				passedEvent = (previousTime < event.Time or currentTime >= event.Time)
			end

			if not passedEvent then 
				continue 
			end
		end

		-- Fire event
		event.Event()
		event.LastFired = Time.CurrentCycle

		-- Remove event
		Time.CurrentTimeEvents[eventName] = nil
	end
end

----------------------------------------------------------------------------------------------------
-- Time Event Functions

--[[
	Add a time event to the TimeEvents table
	> eventName - name of the event (must be unique; if not, it will override the slot)
	> eventData - the event data
	> ignoreCurrentCycle (optional) - if true, the event will ignore the current cycle
]]
Time.AddTimeEvent = function(eventName : string, eventData : TimeEvent, ignoreCurrentCycle : boolean?)
	if ignoreCurrentCycle ~= nil then
		if ignoreCurrentCycle then
			eventData.LastFired = Time.CurrentCycle
		else
			eventData.LastFired = 0
		end
	end
	
	if not eventData.LastFired then
		eventData.LastFired = 0
	end
	
	-- formatting
	if eventData.Time then
		eventData.Time = eventData.Time % 24
	end
	if eventData.RemainingTime then
		eventData.RemainingTime = math.max(eventData.RemainingTime, 0)
	end
	if eventData.State then
		if not table.find(TimeEnum.State, eventData.State) then
			warn("Invalid State:", eventData.State)
			return
		end
	end
	
	Time.TimeEvents[eventName] = eventData
	Time.CurrentTimeEvents[eventName] = eventData
end

--[[
	Remove a time event from the TimeEvents table
]]
Time.RemoveTimeEvent = function(eventName : string)
	Time.TimeEvents[eventName] = nil
	Time.CurrentTimeEvents[eventName] = nil
end

Time.StartRain = function()
	print("Start Rain", Time.CurrentTime, Time.CurrentCycle)
	
	workspace:SetAttribute("Raining", true)
	
	-- Enable Rain
	local params = SetRainParams.new()
	params.Enabled = true
	params.Duration = TimeData.RainStartOffset
	SetRain:FireAllClients(params)
	
	-- Highlight Shelters
	HighlightShelter:FireAllClients(true)
end

Time.StartDangerousRain = function()
	print("Start Dangerous Rain", Time.CurrentTime, Time.CurrentCycle)
	
	-- Start Dangerous Rain
	local params = SetRainParams.new()
	params.Enabled = true
	params.Dangerous = true
	SetRain:FireAllClients(params)
	
	-- Start Damaging & Starving
	for _,player in Players:GetPlayers() do
		CharacterHandler.RainCheck(player, Time.StateDuration)
		CharacterHandler.FoodCheck(player, Time.StateDuration)
	end
end

Time.StopRain = function()
	print("Stop Rain", Time.CurrentTime, Time.CurrentCycle)
	
	workspace:SetAttribute("Raining", false)
	
	-- Disable Rain
	local params = SetRainParams.new()
	params.Enabled = false
	params.Duration = TimeData.RainEndOffset
	SetRain:FireAllClients(params)
end

Time.RespawnItems = function()
	print("Respawn Items", Time.CurrentTime, Time.CurrentCycle)
	RespawnItems:Fire()
end

-- TODO: I should get rid of this soon
Time.BatflyClearing = function()
	print("Batfly Clearing", Time.CurrentTime, Time.CurrentCycle)
	workspace.Creatures.Batflies:ClearAllChildren()
end

Time.LizardClearing = function()
	print("Lizard Clearing", Time.CurrentTime, Time.CurrentCycle)
	workspace.Creatures.Lizards:ClearAllChildren()
end

Time.StartCreatureSpawning = function()
	print("Start Creature Spawning", Time.CurrentTime, Time.CurrentCycle)
	StartCreatureSpawning:Fire()
end

Time.StopCreatureSpawning = function()
	print("Stop Creature Spawning", Time.CurrentTime, Time.CurrentCycle)
	StopCreatureSpawning:Fire()
end

Time.OpenShelterGates = function()
	print("Open Shelter Gates", Time.CurrentTime, Time.CurrentCycle)
	ShelterGate:Fire(ShelterGateEnum.State.Opened)
end

Time.CloseShelterGates = function()
	print("Close Shelter Gates", Time.CurrentTime, Time.CurrentCycle)
	ShelterGate:Fire(ShelterGateEnum.State.Closed)
end

----------------------------------------------------------------------------------------------------
-- Helper Functions

--[[
	Get the progress of the current state (kinda like percentage but in decimal)
	< Returns progress (0 - 1)
]]
Time.GetProgressOfCurrentState = function()
	local state = Time.CurrentState
	local currentTime = Time.CurrentTime
	local stateDuration = Time.StateDuration
	
	if state == TimeEnum.State.Day then
		return ( (currentTime - TimeData.DaytimeRange.X) / TimeData.DaytimeLength)
	elseif state == TimeEnum.State.Night then
		if currentTime >= TimeData.DaytimeRange.Y then
			return ( (currentTime - TimeData.DaytimeRange.Y) / TimeData.NighttimeLength )
		elseif currentTime >= 0 then
			local lastSectionLength = 24 - TimeData.DaytimeRange.Y -- time from TimeData.DaytimeRange.Y to 24
			return ( (currentTime + lastSectionLength ) / TimeData.NighttimeLength )
		end
	end
end

--[[
	Get the progress of the current state in seconds
	< Returns progress in seconds
]]
Time.GetTimeProgressInCurrentState = function()
	return Time.StateDuration * Time.GetProgressOfCurrentState()
end

--[[
	Get the remaining time in the current state
	< Returns remaining time in seconds
]]
Time.GetRemainingTimeInCurrentState = function()
	return Time.StateDuration * (1 - Time.GetProgressOfCurrentState())
end

--[[
	Get the state of time from the current time
	> currentTime - the current time in hours
	< Returns the state of time (Day or Night)
]]
Time.GetStateFromCurrentTime = function(currentTime : number)
	if currentTime >= TimeData.DaytimeRange.X and currentTime < TimeData.DaytimeRange.Y then
		return TimeEnum.State.Day
	else
		return TimeEnum.State.Night
	end
end

--[[
	Get the new time rate for the given state and duration
	> newState - the new state to get the time rate for
	> stateDuration - the duration of the new state
	< Returns the time rate (in hours elapsed per second)
]]
Time.GetTimeRate = function(newState : string, stateDuration : number)
	if newState == TimeEnum.State.Day then
		return TimeData.DaytimeLength / stateDuration
	elseif newState == TimeEnum.State.Night then
		return TimeData.NighttimeLength / stateDuration
	else
		warn("Invalid state given to GetNewTimeRate:", newState)
		return 0
	end
end

Time.UpdatePlayers = function()
	for _, player in Players:GetPlayers() do
		local character = player.Character
		if not character then continue end

		local humanoid : Humanoid = character:FindFirstChild("Humanoid")
		if not humanoid then continue end
		if humanoid.Health <= 0 then continue end

		local playerData = player:FindFirstChild("PlayerData")
		if not playerData then continue end

		-- Increase Cycle Count
		playerData.Cycles.Value += 1
		player:SetAttribute("CyclesSurvived", player:GetAttribute("CyclesSurvived") + 1)
		
		-- Give Referral Rewards
		if DateTime.now().UnixTimestamp <= 1793440800 then
			if playerData.Cycles.Value >= 2 then
				local referringPlayerId = player:GetJoinData().ReferredByPlayerId
				if referringPlayerId then
					local referringPlayer = Players:GetPlayerByUserId(referringPlayerId)
					if referringPlayer then
						DataManager.AddPearls(player, 100, Enum.AnalyticsEconomyTransactionType.Onboarding, "100PearlsReferral")
						DataManager.AddPearls(referringPlayer, 100, Enum.AnalyticsEconomyTransactionType.Onboarding, "100PearlsReferral")
						Received:FireClient(player, true)
						Received:FireClient(referringPlayer, false)
					end
				end
			end
		end

		-- Add Sleep Actions
		PlayerHandler.AddAction(player, "Sleep", "")
		PlayerHandler.AddAction(player, "Cycle", playerData.Cycles.Value + 1)

		-- Increment Player's Karma
		DataManager.IncrementKarma(player)

		-- Grant Badges
		if not workspace:GetAttribute("IsPrivateServer") and playerData.Cycles.Value >= 50 then
			PlayerHandler.AwardBadge(player, 4446430336868065, true)
		end
		if not workspace:GetAttribute("IsPrivateServer") and player:GetAttribute("CyclesSurvived") >= 1000 then
			PlayerHandler.AwardBadge(player, 3402815993852149, true)
		end
	end
end

--[[
	Get a random new state duration for the given state
	> newState - the new state to get the duration for
	< Returns the duration of the state
]]
Time.GetNewStateDuration = function(newState : string)
	if newState == TimeEnum.State.Day then
		return math.random(Time.DayLengthRange.X, Time.DayLengthRange.Y)
	elseif newState == TimeEnum.State.Night then
		return math.random(Time.NightLengthRange.X, Time.NightLengthRange.Y)
	else
		warn("Invalid state given to GetNewStateDuration:", newState)
		return 0
	end
end

export type TimeEvent = {
	Time : number?, -- in hours [0 - 24)
	RemainingTime : number?, -- in seconds
	State : string?,
	Event : () -> (),
	LastFired : number, -- last server cycle that this event was fired
}

--[[ Time Events (autofill did not work so I had to combine them)
{ -- Fires when the given time is reached
	Time : number,
	Event : () -> (),
	LastFired : number
}
{ -- Fires when the given value is <= the remaining time left in the current state (can be combined with State)
	RemainingTime : number,
	State : string?,
	Event : () -> (),
	LastFired : number
}
{ -- Fires every time the state transitions to the given state
	State : string,
	Event : () -> (),
	LastFired : number
}
{ -- Fires every new cycle
	Event : () -> (),
	LastFired : number
}
]]

return Time

Also, how do I privately share the place file?

New videos:

Before it happens (happens at the very end of the video)

Right after it happens (after recorder dies and is respawning)

Aftermath (near the end it shows how much the terrain extended below the baseplate; exactly like how I shown you in my previous replies)