Minecraft-like Roblox block game rblx.games/135624152691584
roblox roblox-game rojo

core: improved building system

+286 -18
+45 -4
src/ReplicatedStorage/Shared/ChunkManager/Chunk.lua
··· 7 7 8 8 local RunService = game:GetService("RunService") 9 9 10 + local function normalizeCoord(n) 11 + if typeof(n) ~= "number" then 12 + return n 13 + end 14 + if n >= 0 then 15 + return math.floor(n + 0.5) 16 + end 17 + return math.ceil(n - 0.5) 18 + end 19 + 20 + local function keyFromCoords(x, y, z) 21 + x = normalizeCoord(x) 22 + y = normalizeCoord(y) 23 + z = normalizeCoord(z) 24 + return `{tostring(x)},{tostring(y)},{tostring(z)}` 25 + end 26 + 10 27 local function Swait(l) 11 28 for i = 1,l do 12 29 RunService.Stepped:Wait() ··· 33 50 34 51 self.loaded = false 35 52 self.loading = false 53 + self.delayedRemoval = {} 36 54 37 55 self.data = {} :: {[typeof("")]: BlockData} -- "X,Y,Z": BlockData ("-1,-1,1": BlockData) 38 56 return self ··· 49 67 self.UpdateBlockBindableL = Instance.new("BindableEvent") :: BindableEvent 50 68 51 69 self.data = data :: {[typeof("")]: BlockData} -- "X,Y,Z": BlockData ("-1,-1,1": BlockData) 70 + self.delayedRemoval = {} 52 71 return self 53 72 end 54 73 ··· 123 142 124 143 function Chunk:GetBlockAt(x,y,z) 125 144 task.desynchronize() 126 - if not self.data[`{tostring(x)},{tostring(y)},{tostring(z)}`] then 145 + if not self.data[keyFromCoords(x, y, z)] then 127 146 return nil 128 147 end 129 - return self.data[`{tostring(x)},{tostring(y)},{tostring(z)}`] 148 + return self.data[keyFromCoords(x, y, z)] 130 149 end 131 150 132 151 function Chunk:CreateBlock(x: number,y: number,z: number,d:BlockData) 133 - self.data[`{tostring(x)},{tostring(y)},{tostring(z)}`] = d 152 + self.data[keyFromCoords(x, y, z)] = d 134 153 self:PropogateChanges(x,y,z,d) 135 154 return self:GetBlockAt(x,y,z) 136 155 end 137 156 138 157 function Chunk:RemoveBlock(x, y, z) 139 - self.data[x .. "," .. y .. "," .. z] = nil 158 + print("[DEBUG] Chunk:RemoveBlock called - Chunk:", self.pos, "Block coords:", x, y, z) 159 + local blockKey = keyFromCoords(x, y, z) 160 + local existingBlock = self.data[blockKey] 161 + if existingBlock then 162 + print("[DEBUG] Removing existing block with ID:", existingBlock.id) 163 + else 164 + print("[DEBUG] No block found at coords", x, y, z) 165 + end 166 + self.data[blockKey] = nil 167 + self:PropogateChanges(x,y,z,0) 168 + end 169 + 170 + function Chunk:RemoveBlockSmooth(x, y, z) 171 + print("[DEBUG] Chunk:RemoveBlockSmooth called - Chunk:", self.pos, "Block coords:", x, y, z) 172 + local blockKey = keyFromCoords(x, y, z) 173 + local existingBlock = self.data[blockKey] 174 + if existingBlock then 175 + print("[DEBUG] Smooth removing existing block with ID:", existingBlock.id) 176 + else 177 + print("[DEBUG] Smooth remove: no block found at coords", x, y, z) 178 + end 179 + self.data[blockKey] = nil 180 + self.delayedRemoval[blockKey] = true 140 181 self:PropogateChanges(x,y,z,0) 141 182 end 142 183
+19
src/ReplicatedStorage/Shared/ChunkManager/ChunkBuilder.lua
··· 9 9 local RunService = game:GetService("RunService") 10 10 11 11 local ChunkBorderFolder = game:GetService("Workspace"):FindFirstChildOfClass("Terrain") 12 + local DEBUG_GHOST = true 12 13 13 14 local NEIGHBOR_OFFSETS = { 14 15 {-1, 0, 0}, {1, 0, 0}, ··· 108 109 local blockName = `{x},{y},{z}` 109 110 local existing = ch:FindFirstChild(blockName) 110 111 if d == 0 then 112 + if c.delayedRemoval and c.delayedRemoval[blockName] then 113 + c.delayedRemoval[blockName] = nil 114 + if existing then 115 + task.defer(function() 116 + task.synchronize() 117 + RunService.RenderStepped:Wait() 118 + if existing.Parent then 119 + existing:Destroy() 120 + end 121 + task.desynchronize() 122 + end) 123 + elseif DEBUG_GHOST then 124 + print("[CHUNKBUILDER][GHOST] Delayed remove missing instance", c.pos, blockName) 125 + end 126 + return 127 + end 111 128 if existing then 112 129 task.synchronize() 113 130 existing:Destroy() 114 131 task.desynchronize() 132 + elseif DEBUG_GHOST then 133 + print("[CHUNKBUILDER][GHOST] Remove missing instance", c.pos, blockName) 115 134 end 116 135 return 117 136 end
+144 -2
src/ReplicatedStorage/Shared/ChunkManager/init.lua
··· 5 5 local Chunk = require("./ChunkManager/Chunk") 6 6 local BlockManager = require("./ChunkManager/BlockManager") 7 7 local ChunkBuilder = require("./ChunkManager/ChunkBuilder") 8 + local Util = require("./Util") 9 + local Globals = require(script.Parent:WaitForChild("Globals")) 8 10 9 11 local remote = game:GetService("ReplicatedStorage"):WaitForChild("RecieveChunkPacket") 10 12 local tickremote = game:GetService("ReplicatedStorage"):WaitForChild("Tick") ··· 14 16 15 17 ChunkManager.ChunkFolder = ChunkFolder 16 18 17 - local CHUNK_RADIUS = 5 18 - local LOAD_BATCH = 8 19 + local CHUNK_RADIUS = Globals.RenderDistance or 5 20 + local LOAD_BATCH = Globals.LoadBatch or 8 21 + local RESYNC_INTERVAL = Globals.ResyncInterval or 5 22 + local RESYNC_RADIUS = Globals.ResyncRadius or 2 23 + local DEBUG_RESYNC = true 19 24 local FORCELOAD_CHUNKS = { 20 25 {0, 1, 0} 21 26 } ··· 111 116 end) 112 117 end 113 118 119 + function ChunkManager:RefreshChunk(x, y, z) 120 + local key = `{x},{y},{z}` 121 + local chunk = Chunk.AllChunks[key] 122 + if not chunk then 123 + pendingChunkRequests[key] = nil 124 + ChunkManager:GetChunk(x, y, z) 125 + ChunkManager:LoadChunk(x, y, z) 126 + return 127 + end 128 + 129 + task.synchronize() 130 + local ok, newData = pcall(function() 131 + return remote:InvokeServer(x, y, z) 132 + end) 133 + if not ok or typeof(newData) ~= "table" then 134 + if DEBUG_RESYNC then 135 + warn("[CHUNKMANAGER][RESYNC] Failed to fetch chunk data", key, ok, typeof(newData)) 136 + end 137 + return 138 + end 139 + 140 + local function sameState(a, b) 141 + if a == b then 142 + return true 143 + end 144 + if not a or not b then 145 + return false 146 + end 147 + local count = 0 148 + for k, v in pairs(a) do 149 + count += 1 150 + if b[k] ~= v then 151 + return false 152 + end 153 + end 154 + for _ in pairs(b) do 155 + count -= 1 156 + end 157 + return count == 0 158 + end 159 + 160 + local function sameBlockData(a, b) 161 + if a == b then 162 + return true 163 + end 164 + if not a or not b then 165 + return false 166 + end 167 + if a.id ~= b.id then 168 + return false 169 + end 170 + return sameState(a.state, b.state) 171 + end 172 + 173 + local changed = 0 174 + local removed = 0 175 + for keyStr, newBlock in pairs(newData) do 176 + local oldBlock = chunk.data[keyStr] 177 + if not sameBlockData(oldBlock, newBlock) then 178 + chunk.data[keyStr] = newBlock 179 + local coords = Util.BlockPosStringToCoords(keyStr) 180 + chunk:PropogateChanges(coords.X, coords.Y, coords.Z, newBlock) 181 + changed += 1 182 + end 183 + end 184 + 185 + for keyStr in pairs(chunk.data) do 186 + if not newData[keyStr] then 187 + chunk.data[keyStr] = nil 188 + local coords = Util.BlockPosStringToCoords(keyStr) 189 + chunk:PropogateChanges(coords.X, coords.Y, coords.Z, 0) 190 + removed += 1 191 + end 192 + end 193 + if chunk.loaded and chunk.instance then 194 + local pruned = 0 195 + for _, child in ipairs(chunk.instance:GetChildren()) do 196 + if not newData[child.Name] then 197 + pruned += 1 198 + task.synchronize() 199 + child:Destroy() 200 + task.desynchronize() 201 + end 202 + end 203 + if DEBUG_RESYNC and pruned > 0 then 204 + print("[CHUNKMANAGER][RESYNC] Pruned ghost instances", key, "count", pruned) 205 + end 206 + end 207 + if DEBUG_RESYNC and (changed > 0 or removed > 0) then 208 + print("[CHUNKMANAGER][RESYNC] Applied diff", key, "changed", changed, "removed", removed) 209 + end 210 + task.desynchronize() 211 + end 212 + 114 213 function ChunkManager:ForceTick() 115 214 for _, coords in ipairs(FORCELOAD_CHUNKS) do 116 215 local key = `{coords[1]},{coords[2]},{coords[3]}` ··· 199 298 end 200 299 end 201 300 301 + function ChunkManager:ResyncAroundPlayer(radius: number) 302 + local player = game:GetService("Players").LocalPlayer 303 + if not player.Character then 304 + return 305 + end 306 + local pos = player.Character:GetPivot().Position 307 + local chunkPos = { 308 + x = math.round(pos.X / 32), 309 + y = math.round(pos.Y / 32), 310 + z = math.round(pos.Z / 32) 311 + } 312 + for y = -radius, radius do 313 + for x = -radius, radius do 314 + for z = -radius, radius do 315 + local cx, cy, cz = chunkPos.x + x, chunkPos.y + y, chunkPos.z + z 316 + ChunkManager:RefreshChunk(cx, cy, cz) 317 + end 318 + end 319 + end 320 + end 321 + 322 + function ChunkManager:ResyncAroundChunk(cx: number, cy: number, cz: number, radius: number) 323 + for y = -radius, radius do 324 + for x = -radius, radius do 325 + for z = -radius, radius do 326 + ChunkManager:RefreshChunk(cx + x, cy + y, cz + z) 327 + end 328 + end 329 + end 330 + end 331 + 202 332 function ChunkManager:Init() 203 333 if not RunService:IsClient() then 204 334 error("ChunkManager:Init can only be called on the client") ··· 225 355 end 226 356 end) 227 357 Swait(20) 358 + end 359 + end) 360 + 361 + task.defer(function() 362 + while true do 363 + wait(RESYNC_INTERVAL) 364 + local ok, err = pcall(function() 365 + ChunkManager:ResyncAroundPlayer(RESYNC_RADIUS) 366 + end) 367 + if not ok then 368 + warn("[CHUNKMANAGER][RESYNC]", err) 369 + end 228 370 end 229 371 end) 230 372 end
+8
src/ReplicatedStorage/Shared/Globals.lua
··· 1 + local Globals = {} 2 + 3 + Globals.RenderDistance = 6 4 + Globals.LoadBatch = 8 5 + Globals.ResyncInterval = 5 6 + Globals.ResyncRadius = 2 7 + 8 + return Globals
+58 -11
src/ReplicatedStorage/Shared/PlacementManager.lua
··· 2 2 3 3 local ChunkManager = require("./ChunkManager") 4 4 local Util = require("./Util") 5 + local RunService = game:GetService("RunService") 5 6 6 7 PlacementManager.ChunkFolder = ChunkManager.ChunkFolder 7 8 ··· 29 30 30 31 local Mouse: Mouse = nil 31 32 local lastNormalId: Enum.NormalId? = nil 33 + local pendingBreakResync = {} 32 34 33 35 local function normalIdToOffset(normal: Enum.NormalId): Vector3 34 36 if normal == Enum.NormalId.Top then ··· 121 123 --print("placeblock") 122 124 --local chunk = ChunkManager:GetChunk(cx, cy, cz) 123 125 --chunk:CreateBlock(x, y, z, blockData) 126 + task.synchronize() 124 127 placeRemote:FireServer(cx, cy, cz, x, y, z, blockId) 128 + task.desynchronize() 125 129 end 126 130 127 131 -- FIRES REMOTE 128 132 function PlacementManager:BreakBlock(cx, cy, cz, x, y, z) 129 - --print("breakblock") 130 - --local chunk = ChunkManager:GetChunk(cx, cy, cz) 131 - --chunk:RemoveBlock(x, y, z) 133 + print("[DEBUG] PlacementManager:BreakBlock called - Chunk:", cx, cy, cz, "Block:", x, y, z) 134 + local chunk = ChunkManager:GetChunk(cx, cy, cz) 135 + if chunk and not chunk:GetBlockAt(x, y, z) then 136 + print("[DEBUG] Client missing block; resyncing nearby chunks") 137 + ChunkManager:ResyncAroundChunk(cx, cy, cz, 1) 138 + task.defer(function() 139 + task.synchronize() 140 + RunService.RenderStepped:Wait() 141 + task.desynchronize() 142 + local refreshed = ChunkManager:GetChunk(cx, cy, cz) 143 + if refreshed and refreshed:GetBlockAt(x, y, z) then 144 + task.synchronize() 145 + breakRemote:FireServer(cx, cy, cz, x, y, z) 146 + task.desynchronize() 147 + print("[DEBUG] BreakBlock remote fired to server after resync") 148 + end 149 + end) 150 + return 151 + end 152 + task.synchronize() 132 153 breakRemote:FireServer(cx, cy, cz, x, y, z) 154 + task.desynchronize() 155 + print("[DEBUG] BreakBlock remote fired to server") 133 156 end 134 157 135 - -- CLIENTSIDED 136 - function PlacementManager:PlaceBlockLocal(cx, cy, cz, x, y, z, blockData) 158 + -- CLIENTSIDED: only apply server-validated changes 159 + local function applyPlaceBlockLocal(cx, cy, cz, x, y, z, blockData) 137 160 local chunk = ChunkManager:GetChunk(cx, cy, cz) 138 161 chunk:CreateBlock(x, y, z, blockData) 139 162 end 140 163 141 - -- CLIENTSIDED 142 - function PlacementManager:BreakBlockLocal(cx, cy, cz, x, y, z) 164 + -- CLIENTSIDED: only apply server-validated changes 165 + local function applyBreakBlockLocal(cx, cy, cz, x, y, z) 166 + print("[DEBUG] PlacementManager:BreakBlockLocal called - Chunk:", cx, cy, cz, "Block:", x, y, z) 143 167 local chunk = ChunkManager:GetChunk(cx, cy, cz) 144 - chunk:RemoveBlock(x, y, z) 168 + if chunk then 169 + print("[DEBUG] Found chunk, calling RemoveBlock") 170 + if chunk.RemoveBlockSmooth then 171 + chunk:RemoveBlockSmooth(x, y, z) 172 + else 173 + chunk:RemoveBlock(x, y, z) 174 + end 175 + else 176 + print("[DEBUG] Chunk not found at coords:", cx, cy, cz) 177 + end 145 178 end 146 179 147 180 function PlacementManager:GetBlockAtMouse(): nil | {chunk:Vector3, block: Vector3} ··· 196 229 end 197 230 198 231 function PlacementManager:Init() 199 - game:GetService("RunService").Heartbeat:Connect(function() 232 + game:GetService("RunService").RenderStepped:Connect(function() 200 233 local a,b = pcall(function() 201 234 PlacementManager:Raycast() 202 235 end) ··· 210 243 tickRemote.OnClientEvent:Connect(function(m, cx, cy, cz, x, y, z, d) 211 244 --warn("PROPOGATED TICK", m, cx, cy, cz, x, y, z, d) 212 245 if m == "B_C" then 213 - PlacementManager:PlaceBlockLocal(cx, cy, cz, x, y ,z, d) 246 + applyPlaceBlockLocal(cx, cy, cz, x, y ,z, d) 214 247 end 215 248 if m == "B_D" then 216 - PlacementManager:BreakBlockLocal(cx, cy, cz, x, y ,z) 249 + applyBreakBlockLocal(cx, cy, cz, x, y ,z) 250 + local key = `{cx},{cy},{cz}` 251 + if not pendingBreakResync[key] then 252 + pendingBreakResync[key] = true 253 + task.defer(function() 254 + task.synchronize() 255 + RunService.RenderStepped:Wait() 256 + task.desynchronize() 257 + pendingBreakResync[key] = nil 258 + ChunkManager:ResyncAroundChunk(cx, cy, cz, 1) 259 + end) 260 + end 261 + end 262 + if m == "C_R" then 263 + ChunkManager:RefreshChunk(cx, cy, cz) 217 264 end 218 265 end) 219 266 end
+12 -1
src/ServerScriptService/Actor/ServerChunkManager/init.server.lua
··· 160 160 end) 161 161 162 162 breakRemote.OnServerEvent:Connect(function(player, cx, cy, cz, x, y, z) 163 - --print("del",player, cx, cy, cz, x, y, z) 163 + print("[DEBUG] Server breakRemote received - Player:", player.Name, "Chunk:", cx, cy, cz, "Block:", x, y, z) 164 164 165 165 if typeof(cx) ~= "number" or typeof(cy) ~= "number" or typeof(cz) ~= "number" then 166 + print("[DEBUG] Invalid chunk coordinate types") 166 167 return 167 168 end 168 169 if typeof(x) ~= "number" or typeof(y) ~= "number" or typeof(z) ~= "number" then 170 + print("[DEBUG] Invalid block coordinate types") 169 171 return 170 172 end 171 173 if x < 1 or x > 8 or y < 1 or y > 8 or z < 1 or z > 8 then 174 + print("[DEBUG] Block coordinates out of range:", x, y, z) 172 175 return 173 176 end 174 177 if math.abs(cx) > MAX_CHUNK_DIST or math.abs(cy) > MAX_CHUNK_DIST or math.abs(cz) > MAX_CHUNK_DIST then 178 + print("[DEBUG] Chunk coordinates out of range:", cx, cy, cz) 175 179 return 176 180 end 177 181 if not isWithinReach(player, cx, cy, cz, x, y, z) then 182 + print("[DEBUG] Block not within player reach") 178 183 return 179 184 end 180 185 181 186 local chunk = getServerChunk(cx, cy, cz) 182 187 if not chunk:GetBlockAt(x, y, z) then 188 + print("[DEBUG] No block found at specified location") 189 + task.synchronize() 190 + tickRemote:FireClient(player, "C_R", cx, cy, cz, 0, 0, 0, 0) 191 + task.desynchronize() 183 192 return 184 193 end 194 + print("[DEBUG] All validations passed, removing block") 185 195 chunk:RemoveBlock(x, y, z) 186 196 propogate("B_D", cx, cy, cz, x, y, z, 0) 197 + print("[DEBUG] Block removal propagated to clients") 187 198 end) 188 199 189 200 task.desynchronize()