414 lines
9.4 KiB
Lua
414 lines
9.4 KiB
Lua
--!native
|
|
--!optimize 2
|
|
|
|
local ChunkManager = {}
|
|
|
|
local RunService = game:GetService("RunService")
|
|
|
|
local Chunk = require("./ChunkManager/Chunk")
|
|
local BlockManager = require("./ChunkManager/BlockManager")
|
|
local ChunkBuilder = require("./ChunkManager/ChunkBuilder")
|
|
local Util = require("./Util")
|
|
local Globals = require(script.Parent:WaitForChild("Globals"))
|
|
|
|
local remote = game:GetService("ReplicatedStorage"):WaitForChild("RecieveChunkPacket")
|
|
local tickremote = game:GetService("ReplicatedStorage"):WaitForChild("Tick")
|
|
|
|
local ChunkFolder = Instance.new("Folder")
|
|
ChunkFolder.Name = "$blockscraft_client"
|
|
|
|
ChunkManager.ChunkFolder = ChunkFolder
|
|
|
|
local CHUNK_RADIUS = Globals.RenderDistance or 5
|
|
local LOAD_BATCH = Globals.LoadBatch or 8
|
|
local RESYNC_INTERVAL = Globals.ResyncInterval or 5
|
|
local RESYNC_RADIUS = Globals.ResyncRadius or 2
|
|
local DEBUG_RESYNC = true
|
|
local FORCELOAD_CHUNKS = {
|
|
{0, 1, 0}
|
|
}
|
|
|
|
local unloadingChunks = {}
|
|
local pendingChunkRequests = {}
|
|
|
|
local lastChunkKey: string? = nil
|
|
local lastHeavyTick = 0
|
|
local HEAVY_TICK_INTERVAL = 1.5
|
|
local lastUnloadSweep = 0
|
|
local UNLOAD_SWEEP_INTERVAL = 1.5
|
|
|
|
local function worldToChunkCoord(v: number): number
|
|
return math.floor((v + 16) / 32)
|
|
end
|
|
|
|
local CHUNK_OFFSETS = {}
|
|
do
|
|
for y = -CHUNK_RADIUS, CHUNK_RADIUS do
|
|
for x = -CHUNK_RADIUS, CHUNK_RADIUS do
|
|
for z = -CHUNK_RADIUS, CHUNK_RADIUS do
|
|
table.insert(CHUNK_OFFSETS, {x, y, z, (x * x) + (y * y) + (z * z)})
|
|
end
|
|
end
|
|
end
|
|
table.sort(CHUNK_OFFSETS, function(a, b)
|
|
return a[4] < b[4]
|
|
end)
|
|
end
|
|
|
|
function ChunkManager:UnloadAllNow()
|
|
for key, chunk in pairs(Chunk.AllChunks) do
|
|
unloadingChunks[key] = true
|
|
pcall(function()
|
|
if chunk.loaded then
|
|
chunk:UnloadImmediate()
|
|
end
|
|
end)
|
|
pcall(function()
|
|
chunk:Destroy()
|
|
end)
|
|
Chunk.AllChunks[key] = nil
|
|
unloadingChunks[key] = nil
|
|
end
|
|
end
|
|
|
|
local function Swait(l)
|
|
for _ = 1, l do
|
|
RunService.Stepped:Wait()
|
|
end
|
|
end
|
|
|
|
function ChunkManager:GetChunk(x, y, z)
|
|
local key = `{x},{y},{z}`
|
|
if Chunk.AllChunks[key] then
|
|
return Chunk.AllChunks[key]
|
|
end
|
|
|
|
if pendingChunkRequests[key] then
|
|
while pendingChunkRequests[key] do
|
|
task.wait()
|
|
end
|
|
return Chunk.AllChunks[key]
|
|
end
|
|
|
|
pendingChunkRequests[key] = true
|
|
local ok, data = pcall(function()
|
|
return remote:InvokeServer(x, y, z)
|
|
end)
|
|
if not ok then
|
|
data = {}
|
|
end
|
|
local ch = Chunk.from(x, y, z, data)
|
|
Chunk.AllChunks[key] = ch
|
|
pendingChunkRequests[key] = nil
|
|
return ch
|
|
end
|
|
|
|
local function ensureNeighboringChunksLoaded(x, y, z)
|
|
local offsets = {
|
|
{1, 0, 0}, {-1, 0, 0},
|
|
{0, 1, 0}, {0, -1, 0},
|
|
{0, 0, 1}, {0, 0, -1}
|
|
}
|
|
|
|
for _, offset in ipairs(offsets) do
|
|
local nx, ny, nz = x + offset[1], y + offset[2], z + offset[3]
|
|
ChunkManager:GetChunk(nx, ny, nz):Tick()
|
|
end
|
|
end
|
|
|
|
function ChunkManager:LoadChunk(x, y, z)
|
|
local key = `{x},{y},{z}`
|
|
if unloadingChunks[key] or not Chunk.AllChunks[key] or Chunk.AllChunks[key].loaded then
|
|
return
|
|
end
|
|
|
|
unloadingChunks[key] = true
|
|
task.defer(function()
|
|
ensureNeighboringChunksLoaded(x, y, z)
|
|
|
|
local chunk = Chunk.AllChunks[key]
|
|
if not chunk then
|
|
chunk = ChunkManager:GetChunk(x, y, z)
|
|
Chunk.AllChunks[key] = chunk
|
|
end
|
|
|
|
local instance = ChunkBuilder:BuildChunk(chunk, ChunkFolder)
|
|
chunk.instance = instance
|
|
chunk.loaded = true
|
|
unloadingChunks[key] = nil
|
|
end)
|
|
end
|
|
|
|
function ChunkManager:RefreshChunk(x, y, z)
|
|
local key = `{x},{y},{z}`
|
|
local chunk = Chunk.AllChunks[key]
|
|
if not chunk then
|
|
pendingChunkRequests[key] = nil
|
|
ChunkManager:GetChunk(x, y, z)
|
|
ChunkManager:LoadChunk(x, y, z)
|
|
return
|
|
end
|
|
|
|
local ok, newData = pcall(function()
|
|
return remote:InvokeServer(x, y, z)
|
|
end)
|
|
if not ok or typeof(newData) ~= "table" then
|
|
if DEBUG_RESYNC then
|
|
warn("[CHUNKMANAGER][RESYNC] Failed to fetch chunk data", key, ok, typeof(newData))
|
|
end
|
|
return
|
|
end
|
|
|
|
local function sameState(a, b)
|
|
if a == b then
|
|
return true
|
|
end
|
|
if not a or not b then
|
|
return false
|
|
end
|
|
local count = 0
|
|
for k, v in pairs(a) do
|
|
count += 1
|
|
if b[k] ~= v then
|
|
return false
|
|
end
|
|
end
|
|
for _ in pairs(b) do
|
|
count -= 1
|
|
end
|
|
return count == 0
|
|
end
|
|
|
|
local function sameBlockData(a, b)
|
|
if a == b then
|
|
return true
|
|
end
|
|
if not a or not b then
|
|
return false
|
|
end
|
|
if a.id ~= b.id then
|
|
return false
|
|
end
|
|
return sameState(a.state, b.state)
|
|
end
|
|
|
|
local changed = 0
|
|
local removed = 0
|
|
for keyStr, newBlock in pairs(newData) do
|
|
local oldBlock = chunk.data[keyStr]
|
|
if not sameBlockData(oldBlock, newBlock) then
|
|
chunk.data[keyStr] = newBlock
|
|
local coords = Util.BlockPosStringToCoords(keyStr)
|
|
chunk:PropogateChanges(coords.X, coords.Y, coords.Z, newBlock)
|
|
changed += 1
|
|
end
|
|
end
|
|
|
|
for keyStr in pairs(chunk.data) do
|
|
if not newData[keyStr] then
|
|
chunk.data[keyStr] = nil
|
|
local coords = Util.BlockPosStringToCoords(keyStr)
|
|
chunk:PropogateChanges(coords.X, coords.Y, coords.Z, 0)
|
|
removed += 1
|
|
end
|
|
end
|
|
if chunk.loaded and chunk.instance then
|
|
local pruned = 0
|
|
for _, child in ipairs(chunk.instance:GetChildren()) do
|
|
if not newData[child.Name] then
|
|
pruned += 1
|
|
child:Destroy()
|
|
end
|
|
end
|
|
if DEBUG_RESYNC and pruned > 0 then
|
|
print("[CHUNKMANAGER][RESYNC] Pruned ghost instances", key, "count", pruned)
|
|
end
|
|
end
|
|
if DEBUG_RESYNC and (changed > 0 or removed > 0) then
|
|
print("[CHUNKMANAGER][RESYNC] Applied diff", key, "changed", changed, "removed", removed)
|
|
end
|
|
end
|
|
|
|
function ChunkManager:ForceTick()
|
|
for _, coords in ipairs(FORCELOAD_CHUNKS) do
|
|
local key = `{coords[1]},{coords[2]},{coords[3]}`
|
|
local chunk = Chunk.AllChunks[key]
|
|
if not chunk then
|
|
ChunkManager:LoadChunk(coords[1], coords[2], coords[3])
|
|
else
|
|
chunk:Tick()
|
|
end
|
|
end
|
|
end
|
|
|
|
function ChunkManager:TickI()
|
|
for key, chunk in pairs(Chunk.AllChunks) do
|
|
if tick() - chunk.inhabitedTime <= 5 then
|
|
tickremote:FireServer(key)
|
|
end
|
|
end
|
|
end
|
|
|
|
function ChunkManager:Tick()
|
|
ChunkManager:ForceTick()
|
|
local player = game:GetService("Players").LocalPlayer
|
|
if not player.Character then
|
|
return
|
|
end
|
|
|
|
local pos = player.Character:GetPivot().Position
|
|
local chunkPos = {
|
|
x = worldToChunkCoord(pos.X),
|
|
y = worldToChunkCoord(pos.Y),
|
|
z = worldToChunkCoord(pos.Z)
|
|
}
|
|
local ck = `{chunkPos.x},{chunkPos.y},{chunkPos.z}`
|
|
local now = tick()
|
|
local shouldHeavyTick = (ck ~= lastChunkKey) or (now - lastHeavyTick >= HEAVY_TICK_INTERVAL)
|
|
lastChunkKey = ck
|
|
if shouldHeavyTick then
|
|
lastHeavyTick = now
|
|
end
|
|
|
|
if shouldHeavyTick then
|
|
task.defer(function()
|
|
local processed = 0
|
|
for _, offset in ipairs(CHUNK_OFFSETS) do
|
|
local cx, cy, cz = chunkPos.x + offset[1], chunkPos.y + offset[2], chunkPos.z + offset[3]
|
|
local chunk = ChunkManager:GetChunk(cx, cy, cz)
|
|
chunk.inhabitedTime = now
|
|
if not chunk.loaded then
|
|
ChunkManager:LoadChunk(cx, cy, cz)
|
|
processed += 1
|
|
if processed % LOAD_BATCH == 0 then
|
|
Swait(1)
|
|
end
|
|
end
|
|
end
|
|
end)
|
|
else
|
|
local current = Chunk.AllChunks[ck]
|
|
if current then
|
|
current.inhabitedTime = now
|
|
end
|
|
end
|
|
|
|
--[[
|
|
task.defer(function()
|
|
for y = 0, 2 do
|
|
task.defer(function()
|
|
for x = -CHUNK_RADIUS, CHUNK_RADIUS do
|
|
for z = -CHUNK_RADIUS, CHUNK_RADIUS do
|
|
local cx, cy, cz = chunkPos.x + x, y, chunkPos.z + z
|
|
local key = `{cx},{cy},{cz}`
|
|
local chunk = ChunkManager:GetChunk(cx, cy, cz)
|
|
chunk.inhabitedTime = tick()
|
|
if not chunk.loaded then
|
|
ChunkManager:LoadChunk(cx, cy, cz)
|
|
Swait(2)
|
|
end
|
|
end
|
|
end
|
|
end)
|
|
Swait(10)
|
|
end
|
|
end)
|
|
--]]
|
|
|
|
if now - lastUnloadSweep >= UNLOAD_SWEEP_INTERVAL then
|
|
lastUnloadSweep = now
|
|
for key, loadedChunk in pairs(Chunk.AllChunks) do
|
|
if now - loadedChunk.inhabitedTime > 15 and not unloadingChunks[key] then
|
|
unloadingChunks[key] = true
|
|
task.defer(function()
|
|
loadedChunk:Unload()
|
|
loadedChunk:Destroy()
|
|
Chunk.AllChunks[key] = nil
|
|
unloadingChunks[key] = nil
|
|
end)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
function ChunkManager:ResyncAroundPlayer(radius: number)
|
|
local player = game:GetService("Players").LocalPlayer
|
|
if not player.Character then
|
|
return
|
|
end
|
|
local pos = player.Character:GetPivot().Position
|
|
local chunkPos = {
|
|
x = worldToChunkCoord(pos.X),
|
|
y = worldToChunkCoord(pos.Y),
|
|
z = worldToChunkCoord(pos.Z)
|
|
}
|
|
for y = -radius, radius do
|
|
for x = -radius, radius do
|
|
for z = -radius, radius do
|
|
local cx, cy, cz = chunkPos.x + x, chunkPos.y + y, chunkPos.z + z
|
|
ChunkManager:RefreshChunk(cx, cy, cz)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
function ChunkManager:ResyncAroundChunk(cx: number, cy: number, cz: number, radius: number)
|
|
for y = -radius, radius do
|
|
for x = -radius, radius do
|
|
for z = -radius, radius do
|
|
ChunkManager:RefreshChunk(cx + x, cy + y, cz + z)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
function ChunkManager:Init()
|
|
if not RunService:IsClient() then
|
|
error("ChunkManager:Init can only be called on the client")
|
|
end
|
|
|
|
ChunkFolder.Parent = game:GetService("Workspace")
|
|
ChunkManager:ForceTick()
|
|
|
|
tickremote.OnClientEvent:Connect(function(m)
|
|
if m == "U_ALL" then
|
|
ChunkManager:UnloadAllNow()
|
|
end
|
|
end)
|
|
|
|
task.defer(function()
|
|
while true do
|
|
wait(2)
|
|
ChunkManager:TickI()
|
|
end
|
|
end)
|
|
|
|
task.defer(function()
|
|
while true do
|
|
task.defer(function()
|
|
local success, err = pcall(function()
|
|
ChunkManager:Tick()
|
|
end)
|
|
if not success then
|
|
warn("[CHUNKMANAGER]", err)
|
|
end
|
|
end)
|
|
Swait(20)
|
|
end
|
|
end)
|
|
|
|
task.defer(function()
|
|
while true do
|
|
wait(RESYNC_INTERVAL)
|
|
local ok, err = pcall(function()
|
|
ChunkManager:ResyncAroundPlayer(RESYNC_RADIUS)
|
|
end)
|
|
if not ok then
|
|
warn("[CHUNKMANAGER][RESYNC]", err)
|
|
end
|
|
end
|
|
end)
|
|
end
|
|
|
|
return ChunkManager
|