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

Fix the game #1

merged opened by kris.darkworld.download targeting main from fix
Labels

None yet.

assignee

None yet.

Participants 1
AT URI
at://did:plc:s7cesz7cr6ybltaryy4meb6y/sh.tangled.repo.pull/3mbwlaaj3a222
+638 -90
Diff #0
+1 -1
default.project.json
··· 1 1 { 2 - "name": "project", 2 + "name": "minecraft-roblox", 3 3 "tree": { 4 4 "$className": "DataModel", 5 5 "ReplicatedStorage": {
+13
src/ReplicatedStorage/Shared/ChunkManager/Chunk.lua
··· 196 196 end) 197 197 end 198 198 199 + function Chunk:UnloadImmediate() 200 + self.loaded = false 201 + pcall(function() 202 + self.unloadChunkHook() 203 + end) 204 + pcall(function() 205 + if self.instance then 206 + self.instance.Parent = nil 207 + self.instance:Destroy() 208 + end 209 + end) 210 + end 211 + 199 212 -- DO NOT INTERACT WITH CHUNK AFTER CALLING THIS 200 213 function Chunk:Destroy() 201 214 self.data = {}
+76 -27
src/ReplicatedStorage/Shared/ChunkManager/init.lua
··· 31 31 local unloadingChunks = {} 32 32 local pendingChunkRequests = {} 33 33 34 + local lastChunkKey: string? = nil 35 + local lastHeavyTick = 0 36 + local HEAVY_TICK_INTERVAL = 1.5 37 + local lastUnloadSweep = 0 38 + local UNLOAD_SWEEP_INTERVAL = 1.5 39 + 40 + local function worldToChunkCoord(v: number): number 41 + return math.floor((v + 16) / 32) 42 + end 43 + 34 44 local CHUNK_OFFSETS = {} 35 45 do 36 46 for y = -CHUNK_RADIUS, CHUNK_RADIUS do ··· 43 53 table.sort(CHUNK_OFFSETS, function(a, b) 44 54 return a[4] < b[4] 45 55 end) 56 + end 57 + 58 + function ChunkManager:UnloadAllNow() 59 + for key, chunk in pairs(Chunk.AllChunks) do 60 + unloadingChunks[key] = true 61 + pcall(function() 62 + if chunk.loaded then 63 + chunk:UnloadImmediate() 64 + end 65 + end) 66 + pcall(function() 67 + chunk:Destroy() 68 + end) 69 + Chunk.AllChunks[key] = nil 70 + unloadingChunks[key] = nil 71 + end 46 72 end 47 73 48 74 local function Swait(l) ··· 232 258 233 259 local pos = player.Character:GetPivot().Position 234 260 local chunkPos = { 235 - x = math.round(pos.X / 32), 236 - y = math.round(pos.Y / 32), 237 - z = math.round(pos.Z / 32) 261 + x = worldToChunkCoord(pos.X), 262 + y = worldToChunkCoord(pos.Y), 263 + z = worldToChunkCoord(pos.Z) 238 264 } 265 + local ck = `{chunkPos.x},{chunkPos.y},{chunkPos.z}` 266 + local now = tick() 267 + local shouldHeavyTick = (ck ~= lastChunkKey) or (now - lastHeavyTick >= HEAVY_TICK_INTERVAL) 268 + lastChunkKey = ck 269 + if shouldHeavyTick then 270 + lastHeavyTick = now 271 + end 239 272 240 - task.defer(function() 241 - local processed = 0 242 - for _, offset in ipairs(CHUNK_OFFSETS) do 243 - local cx, cy, cz = chunkPos.x + offset[1], chunkPos.y + offset[2], chunkPos.z + offset[3] 244 - local chunk = ChunkManager:GetChunk(cx, cy, cz) 245 - chunk.inhabitedTime = tick() 246 - if not chunk.loaded then 247 - ChunkManager:LoadChunk(cx, cy, cz) 248 - processed += 1 249 - if processed % LOAD_BATCH == 0 then 250 - Swait(1) 273 + if shouldHeavyTick then 274 + task.defer(function() 275 + local processed = 0 276 + for _, offset in ipairs(CHUNK_OFFSETS) do 277 + local cx, cy, cz = chunkPos.x + offset[1], chunkPos.y + offset[2], chunkPos.z + offset[3] 278 + local chunk = ChunkManager:GetChunk(cx, cy, cz) 279 + chunk.inhabitedTime = now 280 + if not chunk.loaded then 281 + ChunkManager:LoadChunk(cx, cy, cz) 282 + processed += 1 283 + if processed % LOAD_BATCH == 0 then 284 + Swait(1) 285 + end 251 286 end 252 287 end 288 + end) 289 + else 290 + local current = Chunk.AllChunks[ck] 291 + if current then 292 + current.inhabitedTime = now 253 293 end 254 - end) 294 + end 255 295 256 296 --[[ 257 297 task.defer(function() ··· 275 315 end) 276 316 --]] 277 317 278 - for key, loadedChunk in pairs(Chunk.AllChunks) do 279 - if tick() - loadedChunk.inhabitedTime > 15 and not unloadingChunks[key] then 280 - unloadingChunks[key] = true 281 - task.defer(function() 282 - loadedChunk:Unload() 283 - loadedChunk:Destroy() 284 - Chunk.AllChunks[key] = nil 285 - unloadingChunks[key] = nil 286 - end) 318 + if now - lastUnloadSweep >= UNLOAD_SWEEP_INTERVAL then 319 + lastUnloadSweep = now 320 + for key, loadedChunk in pairs(Chunk.AllChunks) do 321 + if now - loadedChunk.inhabitedTime > 15 and not unloadingChunks[key] then 322 + unloadingChunks[key] = true 323 + task.defer(function() 324 + loadedChunk:Unload() 325 + loadedChunk:Destroy() 326 + Chunk.AllChunks[key] = nil 327 + unloadingChunks[key] = nil 328 + end) 329 + end 287 330 end 288 331 end 289 332 end ··· 295 338 end 296 339 local pos = player.Character:GetPivot().Position 297 340 local chunkPos = { 298 - x = math.round(pos.X / 32), 299 - y = math.round(pos.Y / 32), 300 - z = math.round(pos.Z / 32) 341 + x = worldToChunkCoord(pos.X), 342 + y = worldToChunkCoord(pos.Y), 343 + z = worldToChunkCoord(pos.Z) 301 344 } 302 345 for y = -radius, radius do 303 346 for x = -radius, radius do ··· 326 369 327 370 ChunkFolder.Parent = game:GetService("Workspace") 328 371 ChunkManager:ForceTick() 372 + 373 + tickremote.OnClientEvent:Connect(function(m) 374 + if m == "U_ALL" then 375 + ChunkManager:UnloadAllNow() 376 + end 377 + end) 329 378 330 379 task.defer(function() 331 380 while true do
+352 -37
src/ReplicatedStorage/Shared/PlacementManager.lua
··· 6 6 local ChunkManager = require("./ChunkManager") 7 7 local Util = require("./Util") 8 8 9 + local DEBUG_PLACEMENT = true 10 + local function debugPlacementLog(...: any) 11 + if DEBUG_PLACEMENT then 12 + Util.StudioLog(...) 13 + end 14 + end 15 + 16 + local function debugPlacementWarn(...: any) 17 + if DEBUG_PLACEMENT then 18 + Util.StudioWarn(...) 19 + end 20 + end 21 + 9 22 PlacementManager.ChunkFolder = ChunkManager.ChunkFolder 10 23 11 24 local raycastParams = RaycastParams.new() ··· 13 26 raycastParams.FilterType = Enum.RaycastFilterType.Include 14 27 raycastParams.IgnoreWater = true 15 28 16 - if _G.SB then return nil end 17 - _G.SB = true 29 + local Mouse: Mouse = nil 30 + local lastNormalId: Enum.NormalId? = nil 31 + local lastRaycastFailure: string? = nil 32 + local lastSelectedChunkKey: string? = nil 33 + local lastSelectedBlockKey: string? = nil 34 + local duplicateResyncCooldown: {[string]: number} = {} 35 + local BREAK_ROLLBACK_TIMEOUT = 0.6 36 + local pendingBreaks = {} 37 + local clearSelection 38 + 39 + if _G.__BLOCKSCRAFT_PLACEMENT_MANAGER then 40 + return _G.__BLOCKSCRAFT_PLACEMENT_MANAGER 41 + end 42 + _G.__BLOCKSCRAFT_PLACEMENT_MANAGER = PlacementManager 18 43 19 44 PlacementManager.SelectionBox = script.SelectionBox:Clone() 20 45 PlacementManager.SelectionBox.Name = "$SelectionBox"..(game:GetService("RunService"):IsServer() and "_SERVER" or "") 21 46 PlacementManager.SelectionBox.Parent = game:GetService("Workspace"):FindFirstChildOfClass("Terrain") 47 + PlacementManager.SelectionBox.Adornee = nil 48 + PlacementManager.SelectionBox:GetPropertyChangedSignal("Adornee"):Connect(function() 49 + local adornee = PlacementManager.SelectionBox.Adornee 50 + if not adornee then 51 + return 52 + end 53 + adornee.AncestryChanged:Connect(function(_, parent) 54 + if not parent then 55 + clearSelection("adornee destroyed") 56 + end 57 + end) 58 + end) 22 59 23 60 -- Trash method TODO: Fix this 24 61 local function findChunkFolderFromDescendant(inst: Instance): Instance? ··· 27 64 if current.Parent == PlacementManager.ChunkFolder then 28 65 return current 29 66 end 67 + -- Fallback: match by name in case the ChunkFolder reference differs (e.g. recreated/parented later) 68 + if current.Parent:IsA("Folder") and current.Parent.Name == (PlacementManager.ChunkFolder and PlacementManager.ChunkFolder.Name) then 69 + return current 70 + end 30 71 current = current.Parent 31 72 end 32 73 return nil 33 74 end 34 75 35 - local Mouse: Mouse = nil 36 - local lastNormalId: Enum.NormalId? = nil 37 - local BREAK_ROLLBACK_TIMEOUT = 0.6 38 - local pendingBreaks = {} 76 + local function findBlockRoot(inst: Instance, chunkFolder: Instance): Instance? 77 + local current = inst 78 + while current and current ~= chunkFolder do 79 + if current:IsA("BasePart") then 80 + return current 81 + end 82 + current = current.Parent 83 + end 84 + return nil 85 + end 86 + 87 + local function resolveBlockInstance(chunkFolder: Instance, chunkName: string, blockName: string): Instance? 88 + local chunkInst = chunkFolder:FindFirstChild(chunkName) 89 + if not chunkInst then 90 + return nil 91 + end 92 + return chunkInst:FindFirstChild(blockName) 93 + end 94 + 95 + clearSelection = function(reason: string?) 96 + PlacementManager.SelectionBox.Adornee = nil 97 + PlacementManager.SelectionBox.Parent = nil 98 + lastNormalId = nil 99 + lastSelectedChunkKey = nil 100 + lastSelectedBlockKey = nil 101 + if reason then 102 + lastRaycastFailure = reason 103 + end 104 + end 105 + 106 + local function setSelection(target: Instance, parent: Instance) 107 + PlacementManager.SelectionBox.Parent = parent 108 + PlacementManager.SelectionBox.Adornee = target 109 + end 110 + 111 + local function findChunkAndBlock(inst: Instance): (string?, string?) 112 + local root = PlacementManager.ChunkFolder 113 + if not root then 114 + return nil, nil 115 + end 116 + local current = inst 117 + while current and current.Parent do 118 + -- case: current parent is the chunk folder root; then current is the chunk itself (no block name yet) 119 + if current.Parent == root then 120 + return current.Name, inst.Name 121 + end 122 + -- case: grandparent is chunk folder root; parent is chunk, current is block/model 123 + if current.Parent.Parent == root then 124 + return current.Parent.Name, current.Name 125 + end 126 + current = current.Parent 127 + end 128 + return nil, nil 129 + end 130 + 131 + local function vectorToNormalId(normal: Vector3): Enum.NormalId 132 + local ax, ay, az = math.abs(normal.X), math.abs(normal.Y), math.abs(normal.Z) 133 + if ax >= ay and ax >= az then 134 + return normal.X >= 0 and Enum.NormalId.Right or Enum.NormalId.Left 135 + elseif ay >= ax and ay >= az then 136 + return normal.Y >= 0 and Enum.NormalId.Top or Enum.NormalId.Bottom 137 + else 138 + return normal.Z >= 0 and Enum.NormalId.Back or Enum.NormalId.Front 139 + end 140 + end 39 141 40 142 local function makeChunkKey(cx: number, cy: number, cz: number): string 41 143 return `{cx},{cy},{cz}` ··· 150 252 return true 151 253 end 152 254 255 + local function ensureChunkFolder(): Instance? 256 + if PlacementManager.ChunkFolder and PlacementManager.ChunkFolder.Parent then 257 + return PlacementManager.ChunkFolder 258 + end 259 + local found = workspace:FindFirstChild("$blockscraft_client") 260 + if found then 261 + PlacementManager.ChunkFolder = found 262 + return found 263 + end 264 + return nil 265 + end 266 + 153 267 -- Gets the block and normalid of the block (and surface) the player is looking at 154 268 function PlacementManager:Raycast() 155 269 if not Mouse then 156 270 Mouse = game:GetService("Players").LocalPlayer:GetMouse() 157 271 end 158 - local objLookingAt = Mouse.Target 159 - local dir = Mouse.TargetSurface or Enum.NormalId.Top 272 + local chunkFolder = ensureChunkFolder() 273 + if not chunkFolder then 274 + clearSelection("chunk folder missing") 275 + script.RaycastResult.Value = nil 276 + return 277 + end 278 + 279 + raycastParams.FilterDescendantsInstances = {chunkFolder} 280 + local cam = workspace.CurrentCamera 281 + if not cam then 282 + lastRaycastFailure = "no camera" 283 + return 284 + end 285 + local ray = Mouse.UnitRay 286 + local result = workspace:Raycast(ray.Origin, ray.Direction * MAX_REACH, raycastParams) 287 + if not result then 288 + clearSelection("raycast miss") 289 + script.RaycastResult.Value = nil 290 + debugPlacementLog("[PLACE][CLIENT][RAYCAST]", "miss") 291 + return 292 + end 293 + 294 + local objLookingAt = result.Instance 160 295 if not objLookingAt then 161 - PlacementManager.SelectionBox.Adornee = nil 296 + clearSelection("raycast nil instance") 297 + script.RaycastResult.Value = nil 298 + debugPlacementWarn("[PLACE][CLIENT][RAYCAST]", "nil instance in result") 299 + return 300 + end 301 + 302 + local hitChunkFolder = findChunkFolderFromDescendant(objLookingAt) 303 + if not hitChunkFolder then 304 + debugPlacementWarn( 305 + "[PLACE][CLIENT][REJECT]", 306 + "target not in chunk folder", 307 + objLookingAt:GetFullName(), 308 + "parent", 309 + objLookingAt.Parent and objLookingAt.Parent:GetFullName() or "nil" 310 + ) 311 + clearSelection("target not in chunk folder") 312 + script.RaycastResult.Value = nil 313 + return 314 + end 315 + if hitChunkFolder:GetAttribute("ns") == true then 316 + debugPlacementWarn( 317 + "[PLACE][CLIENT][REJECT]", 318 + "chunk flagged ns", 319 + hitChunkFolder:GetFullName() 320 + ) 321 + clearSelection("target chunk marked ns") 322 + script.RaycastResult.Value = nil 323 + return 324 + end 325 + PlacementManager.ChunkFolder = chunkFolder 326 + 327 + local blockRoot = findBlockRoot(objLookingAt, chunkFolder) or objLookingAt 328 + local chunkName, blockName = findChunkAndBlock(blockRoot) 329 + if not chunkName or not blockName then 330 + clearSelection("failed to resolve chunk/block") 331 + script.RaycastResult.Value = nil 332 + return 333 + end 334 + local okChunk, chunkCoords = pcall(function() 335 + return Util.BlockPosStringToCoords(chunkName) 336 + end) 337 + local okBlock, blockCoords = pcall(function() 338 + return Util.BlockPosStringToCoords(blockName) 339 + end) 340 + if not okChunk or not okBlock then 341 + clearSelection("failed to parse chunk/block names") 342 + script.RaycastResult.Value = nil 343 + return 344 + end 345 + local chunkKey = makeChunkKey(chunkCoords.X, chunkCoords.Y, chunkCoords.Z) 346 + local blockKey = makeBlockKey(blockCoords.X, blockCoords.Y, blockCoords.Z) 347 + 348 + -- block is being optimistically broken, do not highlight it 349 + if getPendingBreak(chunkKey, blockKey) then 350 + clearSelection("block pending break") 162 351 script.RaycastResult.Value = nil 163 - lastNormalId = nil 164 352 return 165 353 end 166 - 167 - --if not objLookingAt:IsDescendantOf(ChunkManager.ChunkFolder) then return end 168 - local chunkFolder = findChunkFolderFromDescendant(objLookingAt) 169 - if not chunkFolder then 170 - PlacementManager.SelectionBox.Adornee = nil 354 + 355 + -- hide selection if block no longer exists (air/removed) 356 + local chunk = ChunkManager:GetChunk(chunkCoords.X, chunkCoords.Y, chunkCoords.Z) 357 + local blockData = chunk and chunk:GetBlockAt(blockCoords.X, blockCoords.Y, blockCoords.Z) 358 + if not blockData or blockData == 0 or blockData.id == 0 then 359 + clearSelection("block missing/air") 171 360 script.RaycastResult.Value = nil 172 - lastNormalId = nil 173 361 return 174 362 end 175 - if chunkFolder:GetAttribute("ns") == true then 176 - PlacementManager.SelectionBox.Adornee = nil 363 + local blockInstance = resolveBlockInstance(chunkFolder, chunkName, blockName) or blockRoot 364 + if not blockInstance then 365 + clearSelection("missing block instance") 177 366 script.RaycastResult.Value = nil 178 - lastNormalId = nil 179 367 return 180 368 end 181 - PlacementManager.SelectionBox.Adornee = objLookingAt 369 + 370 + lastRaycastFailure = nil 371 + if lastSelectedChunkKey ~= chunkKey or lastSelectedBlockKey ~= blockKey then 372 + setSelection(blockInstance, PlacementManager.ChunkFolder) 373 + lastSelectedChunkKey = chunkKey 374 + lastSelectedBlockKey = blockKey 375 + end 182 376 script.RaycastResult.Value = objLookingAt 183 - lastNormalId = dir 184 - return objLookingAt, dir 377 + lastNormalId = vectorToNormalId(result.Normal) 378 + debugPlacementLog( 379 + "[PLACE][CLIENT][RAYCAST][HIT]", 380 + blockInstance:GetFullName(), 381 + "chunkFolder", 382 + hitChunkFolder:GetFullName(), 383 + "blockName", 384 + blockInstance.Name, 385 + "normal", 386 + lastNormalId.Name 387 + ) 388 + return objLookingAt, lastNormalId 185 389 end 186 390 187 391 function PlacementManager:RaycastGetResult() ··· 195 399 196 400 -- FIRES REMOTE 197 401 function PlacementManager:PlaceBlock(cx, cy, cz, x, y, z, blockId: string) 402 + debugPlacementLog("[PLACE][CLIENT][PLACE_CALL]", "chunk", cx, cy, cz, "block", x, y, z, "blockId", blockId) 403 + if typeof(cx) ~= "number" or typeof(cy) ~= "number" or typeof(cz) ~= "number" then 404 + debugPlacementWarn("[PLACE][CLIENT][REJECT]", "chunk type", cx, cy, cz, x, y, z, blockId) 405 + return 406 + end 407 + if typeof(x) ~= "number" or typeof(y) ~= "number" or typeof(z) ~= "number" then 408 + debugPlacementWarn("[PLACE][CLIENT][REJECT]", "block type", cx, cy, cz, x, y, z, blockId) 409 + return 410 + end 411 + if x < 1 or x > 8 or y < 1 or y > 8 or z < 1 or z > 8 then 412 + debugPlacementWarn("[PLACE][CLIENT][REJECT]", "block bounds", cx, cy, cz, x, y, z, blockId) 413 + return 414 + end 415 + if not isWithinReach(cx, cy, cz, x, y, z) then 416 + debugPlacementWarn("[PLACE][CLIENT][REJECT]", "reach", cx, cy, cz, x, y, z, blockId) 417 + return 418 + end 419 + 198 420 -- ensure chunk is present/rendered client-side 199 421 local chunk = ChunkManager:GetChunk(cx, cy, cz) 200 422 if chunk and not chunk.loaded then 201 423 ChunkManager:LoadChunk(cx, cy, cz) 202 424 end 425 + if not chunk then 426 + debugPlacementWarn("[PLACE][CLIENT][REJECT]", "missing chunk", cx, cy, cz, x, y, z, blockId) 427 + return 428 + end 203 429 204 - -- if the client already thinks this block is the same id, skip sending 430 + -- allow sending even if the client thinks the id matches; server truth wins 205 431 if chunk then 206 432 local existing = chunk:GetBlockAt(x, y, z) 207 433 local existingId = existing and existing.id 208 434 if existingId and tostring(existingId) == tostring(blockId) then 209 - return 435 + debugPlacementLog( 436 + "[PLACE][CLIENT][DUPLICATE]", 437 + "still sending", 438 + "chunk", 439 + cx, 440 + cy, 441 + cz, 442 + "block", 443 + x, 444 + y, 445 + z, 446 + "existingId", 447 + existingId, 448 + "blockId", 449 + blockId 450 + ) 451 + local ck = makeChunkKey(cx, cy, cz) 452 + local last = duplicateResyncCooldown[ck] 453 + if not last or (tick() - last) > 0.5 then 454 + duplicateResyncCooldown[ck] = tick() 455 + task.defer(function() 456 + ChunkManager:RefreshChunk(cx, cy, cz) 457 + end) 458 + end 459 + else 460 + debugPlacementLog( 461 + "[PLACE][CLIENT][EXISTING]", 462 + "chunk", 463 + cx, 464 + cy, 465 + cz, 466 + "block", 467 + x, 468 + y, 469 + z, 470 + "existingId", 471 + existingId 472 + ) 210 473 end 211 474 end 212 475 ··· 218 481 }) 219 482 end 220 483 484 + debugPlacementLog("[PLACE][CLIENT][SEND]", cx, cy, cz, x, y, z, blockId) 221 485 placeRemote:FireServer(cx, cy, cz, x, y, z, blockId) 222 486 end 223 487 ··· 227 491 return 228 492 end 229 493 if typeof(x) ~= "number" or typeof(y) ~= "number" or typeof(z) ~= "number" then 494 + debugPlacementWarn("[BREAK][CLIENT][REJECT]", "block type", cx, cy, cz, x, y, z) 230 495 return 231 496 end 232 497 if x < 1 or x > 8 or y < 1 or y > 8 or z < 1 or z > 8 then 498 + debugPlacementWarn("[BREAK][CLIENT][REJECT]", "block bounds", cx, cy, cz, x, y, z) 233 499 return 234 500 end 235 501 if not isWithinReach(cx, cy, cz, x, y, z) then 502 + debugPlacementWarn("[BREAK][CLIENT][REJECT]", "reach", cx, cy, cz, x, y, z) 236 503 return 237 504 end 238 505 ··· 241 508 local chunkKey = makeChunkKey(cx, cy, cz) 242 509 local blockKey = makeBlockKey(x, y, z) 243 510 if getPendingBreak(chunkKey, blockKey) then 511 + debugPlacementLog("[BREAK][CLIENT][SKIP]", "pending rollback", cx, cy, cz, x, y, z) 244 512 return 245 513 end 246 514 pendingBreaks[chunkKey] = pendingBreaks[chunkKey] or {} ··· 252 520 chunk:RemoveBlock(x, y, z) 253 521 end 254 522 scheduleBreakRollback(cx, cy, cz, x, y, z) 523 + debugPlacementLog("[BREAK][CLIENT][SEND]", cx, cy, cz, x, y, z) 255 524 breakRemote:FireServer(cx, cy, cz, x, y, z) 256 525 end 257 526 ··· 283 552 end 284 553 285 554 function PlacementManager:GetBlockAtMouse(): nil | {chunk:Vector3, block: Vector3} 555 + pcall(function() 556 + PlacementManager:Raycast() 557 + end) 286 558 local selectedPart = PlacementManager:RaycastGetResult() 287 559 --print(selectedPart and selectedPart:GetFullName() or nil) 288 560 if selectedPart == nil then 289 - PlacementManager.SelectionBox.Adornee = nil 561 + clearSelection() 290 562 script.RaycastResult.Value = nil 291 - lastNormalId = nil 563 + debugPlacementLog("[PLACE][CLIENT][TARGET]", "no selectedPart after raycast", lastRaycastFailure) 292 564 return nil 293 565 end 294 - if not selectedPart.Parent then 295 - PlacementManager.SelectionBox.Adornee = nil 296 - script.RaycastResult.Value = nil 297 - lastNormalId = nil 566 + local chunkName, blockName = findChunkAndBlock(selectedPart) 567 + if not chunkName or not blockName then 568 + debugPlacementWarn( 569 + "[PLACE][CLIENT][TARGET]", 570 + "failed to find chunk/block from selection", 571 + selectedPart:GetFullName() 572 + ) 298 573 return nil 299 574 end 300 - if not selectedPart.Parent then 301 - PlacementManager.SelectionBox.Adornee = nil 302 - script.RaycastResult.Value = nil 303 - lastNormalId = nil 575 + 576 + local okChunk, chunkCoords = pcall(function() 577 + return Util.BlockPosStringToCoords(chunkName :: string) 578 + end) 579 + local okBlock, blockCoords = pcall(function() 580 + return Util.BlockPosStringToCoords(blockName :: string) 581 + end) 582 + if not okChunk or not okBlock then 583 + debugPlacementWarn( 584 + "[PLACE][CLIENT][TARGET]", 585 + "failed to parse names", 586 + "chunkName", 587 + chunkName, 588 + "blockName", 589 + blockName 590 + ) 304 591 return nil 305 592 end 306 - local chunkCoords = Util.BlockPosStringToCoords(selectedPart.Parent.Name) 307 - local blockCoords = Util.BlockPosStringToCoords(selectedPart.Name) 593 + debugPlacementLog( 594 + "[PLACE][CLIENT][TARGET]", 595 + "chunk", 596 + chunkName, 597 + "block", 598 + blockName, 599 + "normal", 600 + (lastNormalId and lastNormalId.Name) or "nil" 601 + ) 308 602 309 603 return { 310 604 chunk = chunkCoords, ··· 334 628 end 335 629 local offset = normalIdToOffset(hit.normal) 336 630 local placeChunk, placeBlock = offsetChunkBlock(hit.chunk, hit.block, offset) 631 + debugPlacementLog( 632 + "[PLACE][CLIENT][PLACE_TARGET]", 633 + "target chunk", 634 + hit.chunk, 635 + "target block", 636 + hit.block, 637 + "normal", 638 + hit.normal.Name, 639 + "place chunk", 640 + placeChunk, 641 + "place block", 642 + placeBlock 643 + ) 337 644 return { 338 645 chunk = placeChunk, 339 646 block = placeBlock 340 647 } 341 648 end 342 649 650 + function PlacementManager:DebugGetPlacementOrWarn() 651 + local placement = PlacementManager:GetPlacementAtMouse() 652 + if not placement then 653 + debugPlacementWarn("[PLACE][CLIENT][REJECT]", "no placement target under mouse", lastRaycastFailure) 654 + end 655 + return placement 656 + end 657 + 343 658 function PlacementManager:Init() 344 659 game:GetService("RunService").RenderStepped:Connect(function() 345 660 local a,b = pcall(function() 346 661 PlacementManager:Raycast() 347 662 end) 348 663 if not a then 349 - PlacementManager.SelectionBox.Adornee = nil 664 + clearSelection("raycast error") 350 665 script.RaycastResult.Value = nil 351 666 end 352 667 end)
+2
src/ReplicatedStorage/Shared/PlacementState.lua
··· 23 23 selectedId = id or "" 24 24 selectedName = name or selectedId 25 25 valueObject.Value = selectedName or "" 26 + local Util = require(ReplicatedStorage:WaitForChild("Shared"):WaitForChild("Util")) 27 + Util.StudioLog("[PLACE][CLIENT][SELECT]", "id", selectedId, "name", selectedName) 26 28 changed:Fire(selectedId, selectedName) 27 29 end 28 30
+18
src/ReplicatedStorage/Shared/Util.lua
··· 1 + local RunService = game:GetService("RunService") 2 + local IS_STUDIO = RunService:IsStudio() 3 + 1 4 local module = {} 5 + 6 + -- Prints only when running in Studio (avoids noisy live logs) 7 + function module.StudioLog(...: any) 8 + if not IS_STUDIO then 9 + return 10 + end 11 + print(...) 12 + end 13 + 14 + function module.StudioWarn(...: any) 15 + if not IS_STUDIO then 16 + return 17 + end 18 + warn(...) 19 + end 2 20 3 21 function module.isNaN(n: number): boolean 4 22 -- NaN is never equal to itself
+45 -10
src/ServerScriptService/Actor/ServerChunkManager/TerrainGen/init.lua
··· 3 3 4 4 local TerrainGen = {} 5 5 6 - local deflate = require("./TerrainGen/Deflate") 7 - 8 - local DSS = game:GetService("DataStoreService") 9 - local WORLDNAME = "DEFAULT" 10 - local WORLDID = "b73bb5a6-297d-4352-b637-daec7e8c8f3e" 11 - local Store = DSS:GetDataStore("BlockscraftWorldV1", WORLDID) 12 - 13 6 local ChunkManager = require(game:GetService("ReplicatedStorage"):WaitForChild("Shared").ChunkManager) 14 7 local Chunk = require(game:GetService("ReplicatedStorage"):WaitForChild("Shared").ChunkManager.Chunk) 15 8 16 9 TerrainGen.ServerChunkCache = {} :: {[typeof("")]: typeof(Chunk.new(0,0,0))} 17 10 11 + local function chunkKeyFromCoords(x: number, y: number, z: number): string 12 + return `{x},{y},{z}` 13 + end 14 + 15 + function TerrainGen:UnloadAllChunks(): number 16 + local count = 0 17 + for key in pairs(TerrainGen.ServerChunkCache) do 18 + TerrainGen.ServerChunkCache[key] = nil 19 + count += 1 20 + end 21 + return count 22 + end 23 + 24 + local function worldToChunkCoord(v: number): number 25 + return math.floor((v + 16) / 32) 26 + end 27 + 28 + function TerrainGen:PreloadNearPlayers(radius: number, yRadius: number?): number 29 + local Players = game:GetService("Players") 30 + local r = radius or 5 31 + local ry = yRadius or 1 32 + local loaded = 0 33 + for _, player in ipairs(Players:GetPlayers()) do 34 + local character = player.Character 35 + local root = character and character:FindFirstChild("HumanoidRootPart") 36 + if root then 37 + local pos = root.Position 38 + local cx = worldToChunkCoord(pos.X) 39 + local cy = worldToChunkCoord(pos.Y) 40 + local cz = worldToChunkCoord(pos.Z) 41 + for y = -ry, ry do 42 + for x = -r, r do 43 + for z = -r, r do 44 + TerrainGen:GetChunk(cx + x, cy + y, cz + z) 45 + loaded += 1 46 + end 47 + end 48 + end 49 + end 50 + end 51 + return loaded 52 + end 53 + 18 54 -- Load a chunk from the DataStore or generate it if not found 19 55 function TerrainGen:GetChunk(x, y, z) 20 - local key = `{x},{y},{z}` 56 + local key = chunkKeyFromCoords(x, y, z) 21 57 if TerrainGen.ServerChunkCache[key] then 22 58 return TerrainGen.ServerChunkCache[key] 23 59 end 24 - 60 + 25 61 -- Generate a new chunk if it doesn't exist 26 62 local chunk = Chunk.new(x, y, z) 27 63 if y == 1 then ··· 66 102 67 103 return chunk 68 104 end 69 - 70 105 71 106 TerrainGen.CM = ChunkManager 72 107
+17 -10
src/ServerScriptService/Actor/ServerChunkManager/init.server.lua
··· 138 138 return false 139 139 end 140 140 141 - local DEBUG_PLACEMENT = true 141 + local DEBUG_PLACEMENT = false 142 + local function debugPlacementLog(...: any) 143 + if DEBUG_PLACEMENT then 144 + Util.StudioLog(...) 145 + end 146 + end 147 + 148 + local function debugPlacementWarn(...: any) 149 + if DEBUG_PLACEMENT then 150 + Util.StudioWarn(...) 151 + end 152 + end 142 153 143 154 placeRemote.OnServerEvent:Connect(function(player, cx, cy, cz, x, y, z, blockId) 144 155 local function reject(reason: string) 145 - if DEBUG_PLACEMENT then 146 - warn("[PLACE][REJECT]", player.Name, reason, "chunk", cx, cy, cz, "block", x, y, z, "id", blockId) 147 - end 156 + debugPlacementWarn("[PLACE][REJECT]", player.Name, reason, "chunk", cx, cy, cz, "block", x, y, z, "id", blockId) 148 157 return 149 158 end 150 159 ··· 178 187 if existing and existing.id and existing.id ~= 0 then 179 188 if existing.id == resolvedId then 180 189 -- same block already there; treat as success without changes 181 - if DEBUG_PLACEMENT then 182 - print("[PLACE][OK][NOOP]", player.Name, "chunk", cx, cy, cz, "block", x, y, z, "id", resolvedId) 183 - end 190 + debugPlacementLog("[PLACE][OK][NOOP]", player.Name, "chunk", cx, cy, cz, "block", x, y, z, "id", resolvedId) 184 191 return 185 192 end 186 193 -- allow replacement when different id: remove then place ··· 192 199 } 193 200 chunk:CreateBlock(x, y, z, data) 194 201 propogate("B_C", cx, cy, cz, x, y, z, data) 195 - if DEBUG_PLACEMENT then 196 - print("[PLACE][OK]", player.Name, "chunk", cx, cy, cz, "block", x, y, z, "id", resolvedId) 197 - end 202 + debugPlacementLog("[PLACE][OK]", player.Name, "chunk", cx, cy, cz, "block", x, y, z, "id", resolvedId) 198 203 end) 199 204 200 205 breakRemote.OnServerEvent:Connect(function(player, cx, cy, cz, x, y, z) ··· 219 224 task.synchronize() 220 225 tickRemote:FireClient(player, "C_R", cx, cy, cz, 0, 0, 0, 0) 221 226 task.desynchronize() 227 + debugPlacementLog("[BREAK][NOOP]", player.Name, "chunk", cx, cy, cz, "block", x, y, z) 222 228 return 223 229 end 224 230 chunk:RemoveBlock(x, y, z) 225 231 propogate("B_D", cx, cy, cz, x, y, z, 0) 232 + debugPlacementLog("[BREAK][OK]", player.Name, "chunk", cx, cy, cz, "block", x, y, z) 226 233 end) 227 234 228 235 task.desynchronize()
+79
src/ServerScriptService/CmdrCommands/ChunkCullWorld.lua
··· 1 + return { 2 + Name = "chunkcull", 3 + Aliases = {"cullchunks", "resetchunks"}, 4 + Description = "Unload all server chunk cache instantly, then preload only chunks near players (and force clients to unload/resync).", 5 + Group = "Admin", 6 + Args = { 7 + { 8 + Type = "integer", 9 + Name = "radius", 10 + Description = "Horizontal chunk radius around each player to preload", 11 + Optional = true, 12 + Default = 5, 13 + }, 14 + { 15 + Type = "integer", 16 + Name = "yRadius", 17 + Description = "Vertical chunk radius around each player to preload", 18 + Optional = true, 19 + Default = 1, 20 + }, 21 + }, 22 + Run = function(context, radius, yRadius) 23 + local ReplicatedStorage = game:GetService("ReplicatedStorage") 24 + local Players = game:GetService("Players") 25 + 26 + local terrainGen = require( 27 + game:GetService("ServerScriptService") 28 + :WaitForChild("Actor") 29 + :WaitForChild("ServerChunkManager") 30 + :WaitForChild("TerrainGen") 31 + ) 32 + 33 + local tickRemote = ReplicatedStorage:WaitForChild("Tick") 34 + 35 + local r = radius or 5 36 + local ry = yRadius or 1 37 + 38 + local unloaded = 0 39 + pcall(function() 40 + unloaded = terrainGen:UnloadAllChunks() 41 + end) 42 + 43 + -- Tell all clients to immediately drop their local chunk instances 44 + pcall(function() 45 + tickRemote:FireAllClients("U_ALL", 0, 0, 0, 0, 0, 0, 0) 46 + end) 47 + 48 + -- Preload server chunks around players (reduces initial lag spikes after cull) 49 + local preloaded = 0 50 + pcall(function() 51 + preloaded = terrainGen:PreloadNearPlayers(r, ry) 52 + end) 53 + 54 + -- Force clients to resync around themselves 55 + local resyncCount = 0 56 + for _, player in ipairs(Players:GetPlayers()) do 57 + local character = player.Character 58 + local root = character and character:FindFirstChild("HumanoidRootPart") 59 + if root then 60 + local pos = root.Position 61 + local cx = math.floor((pos.X + 16) / 32) 62 + local cy = math.floor((pos.Y + 16) / 32) 63 + local cz = math.floor((pos.Z + 16) / 32) 64 + for y = -ry, ry do 65 + for x = -r, r do 66 + for z = -r, r do 67 + tickRemote:FireClient(player, "C_R", cx + x, cy + y, cz + z, 0, 0, 0, 0) 68 + resyncCount += 1 69 + end 70 + end 71 + end 72 + end 73 + end 74 + 75 + return ( 76 + "chunkcull done | unloaded=%d | preloaded=%d | resyncPackets=%d | radius=%d yRadius=%d" 77 + ):format(unloaded, preloaded, resyncCount, r, ry) 78 + end, 79 + }
+35 -5
src/StarterGui/Hotbar/LocalScript.client.lua
··· 15 15 local PM = require(ReplicatedStorage.Shared.PlacementManager) 16 16 local BlockManager = require(ReplicatedStorage.Shared.ChunkManager.BlockManager) 17 17 local PlacementState = require(ReplicatedStorage.Shared.PlacementState) 18 + local Util = require(ReplicatedStorage.Shared.Util) 18 19 19 20 local blocksFolder = ReplicatedStorage:WaitForChild("Blocks") 20 21 ··· 56 57 for _, block in ipairs(blocksFolder:GetChildren()) do 57 58 local id = block:GetAttribute("n") 58 59 if id ~= nil then 59 - local idStr = tostring(id) 60 - table.insert(ids, idStr) 61 - names[idStr] = block:GetAttribute("displayName") or block:GetAttribute("dn") or block.Name 60 + local n = tonumber(id) 61 + if n and n > 0 then 62 + local idStr = tostring(n) 63 + table.insert(ids, idStr) 64 + names[idStr] = block:GetAttribute("displayName") or block:GetAttribute("dn") or block.Name 65 + end 62 66 end 63 67 end 64 68 table.sort(ids, function(a, b) ··· 133 137 local slots, names = buildHotbarIds() 134 138 self.state.slots = slots 135 139 self.state.names = names 140 + local initialId = slots and slots[1] or "" 141 + if initialId and initialId ~= "" then 142 + local initialName = names and (names[initialId] or initialId) or initialId 143 + PlacementState:SetSelected(initialId, initialName) 144 + end 136 145 137 146 self._updateSlots = function() 138 147 local nextSlots, nextNames = buildHotbarIds() ··· 154 163 if id ~= "" and self.state.names then 155 164 name = self.state.names[id] or id 156 165 end 166 + Util.StudioLog("[PLACE][CLIENT][SELECT]", "slot", slot, "id", id, "name", name) 157 167 PlacementState:SetSelected(id, name) 158 168 end 159 169 160 170 self._handleInput = function(input: InputObject, gameProcessedEvent: boolean) 161 - if gameProcessedEvent or isTextInputFocused() then 171 + if isTextInputFocused() then 162 172 return 163 173 end 164 174 165 175 local slot = keyToSlot[input.KeyCode] 166 176 if slot then 177 + if gameProcessedEvent then 178 + return 179 + end 167 180 self._setSelected(slot) 168 181 return 169 182 end 170 183 171 184 if input.UserInputType == Enum.UserInputType.MouseButton1 then 185 + Util.StudioLog("[INPUT][CLIENT]", "MouseButton1", "processed", gameProcessedEvent) 186 + -- Allow click even if gameProcessedEvent (UI can set this), but only if we're actually pointing at a block 187 + if not PM:GetBlockAtMouse() then 188 + return 189 + end 172 190 local mouseBlock = PM:GetBlockAtMouse() 173 191 if not mouseBlock then 174 192 return ··· 182 200 mouseBlock.block.Z 183 201 ) 184 202 elseif input.UserInputType == Enum.UserInputType.MouseButton2 then 185 - local mouseBlock = PM:GetPlacementAtMouse() 203 + Util.StudioLog("[INPUT][CLIENT]", "MouseButton2", "processed", gameProcessedEvent) 204 + -- Allow click even if gameProcessedEvent (UI can set this), but only if we're actually pointing at a block 205 + local mouseBlock = PM:DebugGetPlacementOrWarn() 186 206 if not mouseBlock then 187 207 return 188 208 end 189 209 local id = PlacementState:GetSelected() 190 210 if not id or id == "" then 211 + Util.StudioWarn("[PLACE][CLIENT][REJECT]", "no selected id") 191 212 return 192 213 end 214 + Util.StudioLog( 215 + "[PLACE][CLIENT][SEND][CLICK]", 216 + "chunk", 217 + mouseBlock.chunk, 218 + "block", 219 + mouseBlock.block, 220 + "id", 221 + id 222 + ) 193 223 PM:PlaceBlock( 194 224 mouseBlock.chunk.X, 195 225 mouseBlock.chunk.Y,

History

1 round 0 comments
sign up or login to add to the discussion
4 commits
expand
core: fix building
placement: fix selection box
placement: handle existing block edge case
chore: update default.project.json
expand 0 comments
pull request successfully merged