diff --git a/src/ReplicatedStorage/Shared/ChunkManager/Chunk.lua b/src/ReplicatedStorage/Shared/ChunkManager/Chunk.lua index 3a45620..bd57cd8 100644 --- a/src/ReplicatedStorage/Shared/ChunkManager/Chunk.lua +++ b/src/ReplicatedStorage/Shared/ChunkManager/Chunk.lua @@ -7,6 +7,23 @@ Chunk.AllChunks = {} :: {[typeof("")]: typeof(Chunk.new(0,0,0))} local RunService = game:GetService("RunService") +local function normalizeCoord(n) + if typeof(n) ~= "number" then + return n + end + if n >= 0 then + return math.floor(n + 0.5) + end + return math.ceil(n - 0.5) +end + +local function keyFromCoords(x, y, z) + x = normalizeCoord(x) + y = normalizeCoord(y) + z = normalizeCoord(z) + return `{tostring(x)},{tostring(y)},{tostring(z)}` +end + local function Swait(l) for i = 1,l do RunService.Stepped:Wait() @@ -33,6 +50,7 @@ function Chunk.new(x,y,z) self.loaded = false self.loading = false + self.delayedRemoval = {} self.data = {} :: {[typeof("")]: BlockData} -- "X,Y,Z": BlockData ("-1,-1,1": BlockData) return self @@ -49,6 +67,7 @@ function Chunk.from(x,y,z,data) self.UpdateBlockBindableL = Instance.new("BindableEvent") :: BindableEvent self.data = data :: {[typeof("")]: BlockData} -- "X,Y,Z": BlockData ("-1,-1,1": BlockData) + self.delayedRemoval = {} return self end @@ -123,20 +142,42 @@ end function Chunk:GetBlockAt(x,y,z) task.desynchronize() - if not self.data[`{tostring(x)},{tostring(y)},{tostring(z)}`] then + if not self.data[keyFromCoords(x, y, z)] then return nil end - return self.data[`{tostring(x)},{tostring(y)},{tostring(z)}`] + return self.data[keyFromCoords(x, y, z)] end function Chunk:CreateBlock(x: number,y: number,z: number,d:BlockData) - self.data[`{tostring(x)},{tostring(y)},{tostring(z)}`] = d + self.data[keyFromCoords(x, y, z)] = d self:PropogateChanges(x,y,z,d) return self:GetBlockAt(x,y,z) end function Chunk:RemoveBlock(x, y, z) - self.data[x .. "," .. y .. "," .. z] = nil + print("[DEBUG] Chunk:RemoveBlock called - Chunk:", self.pos, "Block coords:", x, y, z) + local blockKey = keyFromCoords(x, y, z) + local existingBlock = self.data[blockKey] + if existingBlock then + print("[DEBUG] Removing existing block with ID:", existingBlock.id) + else + print("[DEBUG] No block found at coords", x, y, z) + end + self.data[blockKey] = nil + self:PropogateChanges(x,y,z,0) +end + +function Chunk:RemoveBlockSmooth(x, y, z) + print("[DEBUG] Chunk:RemoveBlockSmooth called - Chunk:", self.pos, "Block coords:", x, y, z) + local blockKey = keyFromCoords(x, y, z) + local existingBlock = self.data[blockKey] + if existingBlock then + print("[DEBUG] Smooth removing existing block with ID:", existingBlock.id) + else + print("[DEBUG] Smooth remove: no block found at coords", x, y, z) + end + self.data[blockKey] = nil + self.delayedRemoval[blockKey] = true self:PropogateChanges(x,y,z,0) end diff --git a/src/ReplicatedStorage/Shared/ChunkManager/ChunkBuilder.lua b/src/ReplicatedStorage/Shared/ChunkManager/ChunkBuilder.lua index a207873..a920b50 100644 --- a/src/ReplicatedStorage/Shared/ChunkManager/ChunkBuilder.lua +++ b/src/ReplicatedStorage/Shared/ChunkManager/ChunkBuilder.lua @@ -9,6 +9,7 @@ local objects = script.Parent.Parent.Parent.Objects local RunService = game:GetService("RunService") local ChunkBorderFolder = game:GetService("Workspace"):FindFirstChildOfClass("Terrain") +local DEBUG_GHOST = true local NEIGHBOR_OFFSETS = { {-1, 0, 0}, {1, 0, 0}, @@ -108,10 +109,28 @@ function ChunkBuilder:BuildChunk(c: typeof(Chunk.new(0,0,0)),parent: Instance?) local blockName = `{x},{y},{z}` local existing = ch:FindFirstChild(blockName) if d == 0 then + if c.delayedRemoval and c.delayedRemoval[blockName] then + c.delayedRemoval[blockName] = nil + if existing then + task.defer(function() + task.synchronize() + RunService.RenderStepped:Wait() + if existing.Parent then + existing:Destroy() + end + task.desynchronize() + end) + elseif DEBUG_GHOST then + print("[CHUNKBUILDER][GHOST] Delayed remove missing instance", c.pos, blockName) + end + return + end if existing then task.synchronize() existing:Destroy() task.desynchronize() + elseif DEBUG_GHOST then + print("[CHUNKBUILDER][GHOST] Remove missing instance", c.pos, blockName) end return end diff --git a/src/ReplicatedStorage/Shared/ChunkManager/init.lua b/src/ReplicatedStorage/Shared/ChunkManager/init.lua index aaa60d0..70b522f 100644 --- a/src/ReplicatedStorage/Shared/ChunkManager/init.lua +++ b/src/ReplicatedStorage/Shared/ChunkManager/init.lua @@ -5,6 +5,8 @@ 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") @@ -14,8 +16,11 @@ ChunkFolder.Name = "$blockscraft_client" ChunkManager.ChunkFolder = ChunkFolder -local CHUNK_RADIUS = 5 -local LOAD_BATCH = 8 +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} } @@ -111,6 +116,100 @@ function ChunkManager:LoadChunk(x, y, z) 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 + + task.synchronize() + 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 + task.synchronize() + child:Destroy() + task.desynchronize() + 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 + task.desynchronize() +end + function ChunkManager:ForceTick() for _, coords in ipairs(FORCELOAD_CHUNKS) do local key = `{coords[1]},{coords[2]},{coords[3]}` @@ -199,6 +298,37 @@ function ChunkManager:Tick() 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 = math.round(pos.X / 32), + y = math.round(pos.Y / 32), + z = math.round(pos.Z / 32) + } + 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") @@ -227,6 +357,18 @@ function ChunkManager:Init() 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 diff --git a/src/ReplicatedStorage/Shared/Globals.lua b/src/ReplicatedStorage/Shared/Globals.lua new file mode 100644 index 0000000..72926c3 --- /dev/null +++ b/src/ReplicatedStorage/Shared/Globals.lua @@ -0,0 +1,8 @@ +local Globals = {} + +Globals.RenderDistance = 6 +Globals.LoadBatch = 8 +Globals.ResyncInterval = 5 +Globals.ResyncRadius = 2 + +return Globals diff --git a/src/ReplicatedStorage/Shared/PlacementManager.lua b/src/ReplicatedStorage/Shared/PlacementManager.lua index 96c316f..91dd084 100644 --- a/src/ReplicatedStorage/Shared/PlacementManager.lua +++ b/src/ReplicatedStorage/Shared/PlacementManager.lua @@ -2,6 +2,7 @@ local PlacementManager = {} local ChunkManager = require("./ChunkManager") local Util = require("./Util") +local RunService = game:GetService("RunService") PlacementManager.ChunkFolder = ChunkManager.ChunkFolder @@ -29,6 +30,7 @@ end local Mouse: Mouse = nil local lastNormalId: Enum.NormalId? = nil +local pendingBreakResync = {} local function normalIdToOffset(normal: Enum.NormalId): Vector3 if normal == Enum.NormalId.Top then @@ -121,27 +123,58 @@ function PlacementManager:PlaceBlock(cx, cy, cz, x, y, z, blockId: string) --print("placeblock") --local chunk = ChunkManager:GetChunk(cx, cy, cz) --chunk:CreateBlock(x, y, z, blockData) + task.synchronize() placeRemote:FireServer(cx, cy, cz, x, y, z, blockId) + task.desynchronize() end -- FIRES REMOTE function PlacementManager:BreakBlock(cx, cy, cz, x, y, z) - --print("breakblock") - --local chunk = ChunkManager:GetChunk(cx, cy, cz) - --chunk:RemoveBlock(x, y, z) + print("[DEBUG] PlacementManager:BreakBlock called - Chunk:", cx, cy, cz, "Block:", x, y, z) + local chunk = ChunkManager:GetChunk(cx, cy, cz) + if chunk and not chunk:GetBlockAt(x, y, z) then + print("[DEBUG] Client missing block; resyncing nearby chunks") + ChunkManager:ResyncAroundChunk(cx, cy, cz, 1) + task.defer(function() + task.synchronize() + RunService.RenderStepped:Wait() + task.desynchronize() + local refreshed = ChunkManager:GetChunk(cx, cy, cz) + if refreshed and refreshed:GetBlockAt(x, y, z) then + task.synchronize() + breakRemote:FireServer(cx, cy, cz, x, y, z) + task.desynchronize() + print("[DEBUG] BreakBlock remote fired to server after resync") + end + end) + return + end + task.synchronize() breakRemote:FireServer(cx, cy, cz, x, y, z) + task.desynchronize() + print("[DEBUG] BreakBlock remote fired to server") end --- CLIENTSIDED -function PlacementManager:PlaceBlockLocal(cx, cy, cz, x, y, z, blockData) +-- CLIENTSIDED: only apply server-validated changes +local function applyPlaceBlockLocal(cx, cy, cz, x, y, z, blockData) local chunk = ChunkManager:GetChunk(cx, cy, cz) chunk:CreateBlock(x, y, z, blockData) end --- CLIENTSIDED -function PlacementManager:BreakBlockLocal(cx, cy, cz, x, y, z) +-- CLIENTSIDED: only apply server-validated changes +local function applyBreakBlockLocal(cx, cy, cz, x, y, z) + print("[DEBUG] PlacementManager:BreakBlockLocal called - Chunk:", cx, cy, cz, "Block:", x, y, z) local chunk = ChunkManager:GetChunk(cx, cy, cz) - chunk:RemoveBlock(x, y, z) + if chunk then + print("[DEBUG] Found chunk, calling RemoveBlock") + if chunk.RemoveBlockSmooth then + chunk:RemoveBlockSmooth(x, y, z) + else + chunk:RemoveBlock(x, y, z) + end + else + print("[DEBUG] Chunk not found at coords:", cx, cy, cz) + end end function PlacementManager:GetBlockAtMouse(): nil | {chunk:Vector3, block: Vector3} @@ -196,7 +229,7 @@ function PlacementManager:GetPlacementAtMouse(): nil | {chunk:Vector3, block: Ve end function PlacementManager:Init() - game:GetService("RunService").Heartbeat:Connect(function() + game:GetService("RunService").RenderStepped:Connect(function() local a,b = pcall(function() PlacementManager:Raycast() end) @@ -210,10 +243,24 @@ function PlacementManager:Init() tickRemote.OnClientEvent:Connect(function(m, cx, cy, cz, x, y, z, d) --warn("PROPOGATED TICK", m, cx, cy, cz, x, y, z, d) if m == "B_C" then - PlacementManager:PlaceBlockLocal(cx, cy, cz, x, y ,z, d) + applyPlaceBlockLocal(cx, cy, cz, x, y ,z, d) end if m == "B_D" then - PlacementManager:BreakBlockLocal(cx, cy, cz, x, y ,z) + applyBreakBlockLocal(cx, cy, cz, x, y ,z) + local key = `{cx},{cy},{cz}` + if not pendingBreakResync[key] then + pendingBreakResync[key] = true + task.defer(function() + task.synchronize() + RunService.RenderStepped:Wait() + task.desynchronize() + pendingBreakResync[key] = nil + ChunkManager:ResyncAroundChunk(cx, cy, cz, 1) + end) + end + end + if m == "C_R" then + ChunkManager:RefreshChunk(cx, cy, cz) end end) end diff --git a/src/ServerScriptService/Actor/ServerChunkManager/init.server.lua b/src/ServerScriptService/Actor/ServerChunkManager/init.server.lua index 18f291e..acdb3ae 100644 --- a/src/ServerScriptService/Actor/ServerChunkManager/init.server.lua +++ b/src/ServerScriptService/Actor/ServerChunkManager/init.server.lua @@ -160,30 +160,41 @@ placeRemote.OnServerEvent:Connect(function(player, cx, cy, cz, x, y, z, blockId) end) breakRemote.OnServerEvent:Connect(function(player, cx, cy, cz, x, y, z) - --print("del",player, cx, cy, cz, x, y, z) + print("[DEBUG] Server breakRemote received - Player:", player.Name, "Chunk:", cx, cy, cz, "Block:", x, y, z) if typeof(cx) ~= "number" or typeof(cy) ~= "number" or typeof(cz) ~= "number" then + print("[DEBUG] Invalid chunk coordinate types") return end if typeof(x) ~= "number" or typeof(y) ~= "number" or typeof(z) ~= "number" then + print("[DEBUG] Invalid block coordinate types") return end if x < 1 or x > 8 or y < 1 or y > 8 or z < 1 or z > 8 then + print("[DEBUG] Block coordinates out of range:", x, y, z) return end if math.abs(cx) > MAX_CHUNK_DIST or math.abs(cy) > MAX_CHUNK_DIST or math.abs(cz) > MAX_CHUNK_DIST then + print("[DEBUG] Chunk coordinates out of range:", cx, cy, cz) return end if not isWithinReach(player, cx, cy, cz, x, y, z) then + print("[DEBUG] Block not within player reach") return end local chunk = getServerChunk(cx, cy, cz) if not chunk:GetBlockAt(x, y, z) then + print("[DEBUG] No block found at specified location") + task.synchronize() + tickRemote:FireClient(player, "C_R", cx, cy, cz, 0, 0, 0, 0) + task.desynchronize() return end + print("[DEBUG] All validations passed, removing block") chunk:RemoveBlock(x, y, z) propogate("B_D", cx, cy, cz, x, y, z, 0) + print("[DEBUG] Block removal propagated to clients") end) task.desynchronize()