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

core: broken

+582 -217
-5
src/ReplicatedStorage/Shared/ChunkManager/BlockManager.lua
··· 23 23 local warnedBlockIds = {} 24 24 25 25 function BlockManager:GetBlock(blockId: number, attr: {[typeof("")]: any}?) 26 - 27 - task.synchronize() 28 - 29 26 if not BlockManager.BlockIdMappings[blockId] then 30 27 if not warnedBlockIds[blockId] then 31 28 warnedBlockIds[blockId] = true ··· 58 55 -- Returns block with id blockId, rotated so the given face (NormalId) points north (+X). 59 56 local block = BlockManager:GetBlock(blockId, attr) 60 57 local rot = CFrame.new() 61 - 62 - task.synchronize() 63 58 64 59 if face == Enum.NormalId.Front then 65 60 rot = CFrame.Angles(0, 0, 0) -- no rot
-6
src/ReplicatedStorage/Shared/ChunkManager/Chunk.lua
··· 75 75 end 76 76 77 77 function Chunk:DoesNeighboringBlockExist(rx, ry, rz, gx, gy, gz, offsetX, offsetY, offsetZ) 78 - task.desynchronize() 79 78 -- Calculate the local position of the neighboring block 80 79 local neighborRX, neighborRY, neighborRZ = rx + offsetX, ry + offsetY, rz + offsetZ 81 80 local neighborGX, neighborGY, neighborGZ = gx, gy, gz ··· 120 119 end 121 120 122 121 function Chunk:IsBlockRenderable(rx, ry, rz) 123 - task.desynchronize() 124 122 local gx, gy, gz = self.pos.X, self.pos.Y, self.pos.Z 125 123 -- Check all six neighboring blocks 126 124 local d = not ( ··· 144 142 end 145 143 146 144 function Chunk:GetBlockAt(x,y,z) 147 - task.desynchronize() 148 145 if not self.data[keyFromCoords(x, y, z)] then 149 146 return nil 150 147 end ··· 176 173 177 174 function Chunk:Unload() 178 175 179 - task.synchronize() 180 176 self.loaded = false 181 177 182 178 -- SLOWCLEAR ··· 194 190 end 195 191 end 196 192 197 - task.synchronize() 198 193 self.instance.Parent = nil 199 194 self.instance:Destroy() 200 195 self.unloadChunkHook() 201 - task.desynchronize() 202 196 end) 203 197 end 204 198
+15 -48
src/ReplicatedStorage/Shared/ChunkManager/ChunkBuilder.lua
··· 43 43 end 44 44 45 45 local function propogateNeighboringBlockChanges(cx, cy, cz, x, y, z) 46 - --warn("propogateNeighboringBlockChanges",cx,cy,cz,x,y,z) 47 - -- updates block in another chunk 48 46 local c = Chunk.AllChunks[`{cx},{cy},{cz}`] 49 47 if not c then return end 50 48 ··· 53 51 54 52 if c:IsBlockRenderable(x, y, z) then 55 53 if c.instance:FindFirstChild(`{x},{y},{z}`) then return end 56 - task.synchronize() 57 54 local block = BlockManager:GetBlockRotated(d.id, util.RotationStringToNormalId(d.state["r"] or "f"), d.state) 58 55 block.Name = `{x},{y},{z}` 59 56 block:PivotTo(util.ChunkPosToCFrame(c.pos, Vector3.new(x, y, z))) 60 57 block.Parent = c.instance 61 - task.desynchronize() 62 58 else 63 59 local existing = c.instance:FindFirstChild(`{x},{y},{z}`) 64 60 if existing then 65 - task.synchronize() 66 61 existing:Destroy() 67 - task.desynchronize() 68 62 end 69 63 end 70 64 end ··· 80 74 81 75 local finished = false 82 76 83 - 84 77 local ch = Instance.new("Folder") 85 78 ch.Parent = parent 86 79 ch.Name = `{c.pos.X},{c.pos.Y},{c.pos.Z}` 87 80 88 81 local conn = c.UpdateBlockBindableL.Event:Connect(function(x: number, y: number, z: number, d: BlockData) 89 - task.desynchronize() 90 82 if finished == false then 91 83 newcache[`{x},{y},{z}`] = d 92 84 return 93 85 end 94 - task.synchronize() 95 86 for _, o in pairs(NEIGHBOR_OFFSETS) do 96 - --warn("propogate",o[1],o[2],o[3]) 97 - -- Adjust for chunk boundaries 98 87 local b = {x = x + o[1], y = y + o[2], z = z + o[3]} 99 - local ch = {x = c.pos.X, y = c.pos.Y, z = c.pos.Z} 88 + local chPos = {x = c.pos.X, y = c.pos.Y, z = c.pos.Z} 100 89 101 - if b.x < 1 then ch.x = c.pos.X - 1 b.x = 8 end 102 - if b.x > 8 then ch.x = c.pos.X + 1 b.x = 1 end 103 - if b.y < 1 then ch.y = c.pos.Y - 1 b.y = 8 end 104 - if b.y > 8 then ch.y = c.pos.Y + 1 b.y = 1 end 105 - if b.z < 1 then ch.z = c.pos.Z - 1 b.z = 8 end 106 - if b.z > 8 then ch.z = c.pos.Z + 1 b.z = 1 end 90 + if b.x < 1 then chPos.x = c.pos.X - 1 b.x = 8 end 91 + if b.x > 8 then chPos.x = c.pos.X + 1 b.x = 1 end 92 + if b.y < 1 then chPos.y = c.pos.Y - 1 b.y = 8 end 93 + if b.y > 8 then chPos.y = c.pos.Y + 1 b.y = 1 end 94 + if b.z < 1 then chPos.z = c.pos.Z - 1 b.z = 8 end 95 + if b.z > 8 then chPos.z = c.pos.Z + 1 b.z = 1 end 107 96 108 - propogateNeighboringBlockChanges(ch.x, ch.y, ch.z, b.x, b.y, b.z) 109 - --BlockManager:GetBlock(ch.x) 97 + propogateNeighboringBlockChanges(chPos.x, chPos.y, chPos.z, b.x, b.y, b.z) 110 98 end 111 99 112 100 local blockName = `{x},{y},{z}` ··· 116 104 c.delayedRemoval[blockName] = nil 117 105 if existing then 118 106 task.defer(function() 119 - task.synchronize() 120 107 RunService.RenderStepped:Wait() 121 108 if existing.Parent then 122 109 existing:Destroy() 123 110 end 124 - task.desynchronize() 125 111 end) 126 112 elseif DEBUG_GHOST then 127 113 print("[CHUNKBUILDER][GHOST] Delayed remove missing instance", c.pos, blockName) ··· 129 115 return 130 116 end 131 117 if existing then 132 - task.synchronize() 133 118 existing:Destroy() 134 - task.desynchronize() 135 119 elseif DEBUG_GHOST then 136 120 print("[CHUNKBUILDER][GHOST] Remove missing instance", c.pos, blockName) 137 121 end ··· 139 123 end 140 124 if not c:IsBlockRenderable(x, y, z) then 141 125 if existing then 142 - task.synchronize() 143 126 existing:Destroy() 144 - task.desynchronize() 145 127 end 146 128 return 147 129 end 148 130 if existing then 149 - task.synchronize() 150 131 existing:Destroy() 151 - task.desynchronize() 152 132 end 153 133 if not d then return end 154 134 if d.id == 0 then return end 155 135 local N = util.RotationStringToNormalId(d.state["r"] or "f") 156 - task.synchronize() 157 136 local block = BlockManager:GetBlockRotated(d.id, N, d.state) 158 137 block.Name = blockName 159 138 block:PivotTo(util.ChunkPosToCFrame(c.pos, Vector3.new(x, y, z))) 160 139 block.Parent = ch 161 - task.desynchronize() 162 140 end) 163 141 164 142 c.unloadChunkHook = function() ··· 170 148 task.defer(function() 171 149 172 150 local p = 0 173 - 174 - task.synchronize() 175 151 176 152 local hb = false 177 - 178 - for a,b in pairs(blocks) do 153 + for _,b in pairs(blocks) do 179 154 hb = true 155 + break 180 156 end 181 157 182 158 local border = Instance.new("Part") ··· 186 162 end 187 163 188 164 for a,b in pairs(blocks) do 189 - task.desynchronize() 190 165 local coords = util.BlockPosStringToCoords(a) 191 - if not c:IsBlockRenderable(coords.X, coords.Y, coords.Z) then 166 + if not c or not c.IsBlockRenderable or not c:IsBlockRenderable(coords.X, coords.Y, coords.Z) then 192 167 if ch:FindFirstChild(a) then 193 - task.synchronize() 194 168 ch:FindFirstChild(a):Destroy() 195 - task.desynchronize() 196 169 end 197 170 continue 198 171 end 199 - task.desynchronize() 200 172 local N = util.RotationStringToNormalId(b.state["r"] or "f") 201 - task.synchronize() 202 173 local block = BlockManager:GetBlockRotated(b.id, N, b.state) 203 - if ch:FindFirstChild(a) then 204 - ch:FindFirstChild(a):Destroy() 174 + local existing = ch:FindFirstChild(a) 175 + if existing then 176 + existing:Destroy() 205 177 end 206 178 block.Name = a 207 179 block:PivotTo(util.ChunkPosToCFrame(c.pos, coords)) ··· 215 187 216 188 finished = true 217 189 218 - task.synchronize() 219 190 border:Destroy() 220 - task.desynchronize() 221 191 222 192 task.defer(function() 223 - task.synchronize() 224 193 for key, data in pairs(newcache) do 225 194 local coords = util.BlockPosStringToCoords(key) 226 195 for _, o in pairs(NEIGHBOR_OFFSETS) do 227 - -- chunks are 8x8x8 228 196 local nb = {x = coords.X + o[1], y = coords.Y + o[2], z = coords.Z + o[3]} 229 197 local chCoords = {x = c.pos.X, y = c.pos.Y, z = c.pos.Z} 230 198 if nb.x == 0 then chCoords.x = c.pos.X - 1 nb.x = 8 end ··· 246 214 end 247 215 continue 248 216 end 249 - if not c:IsBlockRenderable(coords.X, coords.Y, coords.Z) then 217 + if not c or not c.IsBlockRenderable or not c:IsBlockRenderable(coords.X, coords.Y, coords.Z) then 250 218 if existing then 251 219 existing:Destroy() 252 220 end ··· 267 235 newcache = nil 268 236 blocks = nil 269 237 end) 270 - task.desynchronize() 271 238 end) 272 239 273 240 c.loaded = true
-13
src/ReplicatedStorage/Shared/ChunkManager/init.lua
··· 46 46 end 47 47 48 48 local function Swait(l) 49 - task.synchronize() 50 49 for _ = 1, l do 51 50 RunService.Stepped:Wait() 52 51 end ··· 59 58 end 60 59 61 60 if pendingChunkRequests[key] then 62 - task.synchronize() 63 61 while pendingChunkRequests[key] do 64 62 task.wait() 65 63 end 66 64 return Chunk.AllChunks[key] 67 65 end 68 66 69 - task.synchronize() 70 67 pendingChunkRequests[key] = true 71 68 local ok, data = pcall(function() 72 69 return remote:InvokeServer(x, y, z) ··· 74 71 if not ok then 75 72 data = {} 76 73 end 77 - task.synchronize() 78 74 local ch = Chunk.from(x, y, z, data) 79 75 Chunk.AllChunks[key] = ch 80 76 pendingChunkRequests[key] = nil ··· 102 98 103 99 unloadingChunks[key] = true 104 100 task.defer(function() 105 - task.desynchronize() 106 101 ensureNeighboringChunksLoaded(x, y, z) 107 102 108 103 local chunk = Chunk.AllChunks[key] ··· 111 106 Chunk.AllChunks[key] = chunk 112 107 end 113 108 114 - task.synchronize() 115 109 local instance = ChunkBuilder:BuildChunk(chunk, ChunkFolder) 116 110 chunk.instance = instance 117 111 chunk.loaded = true ··· 129 123 return 130 124 end 131 125 132 - task.synchronize() 133 126 local ok, newData = pcall(function() 134 127 return remote:InvokeServer(x, y, z) 135 128 end) ··· 198 191 for _, child in ipairs(chunk.instance:GetChildren()) do 199 192 if not newData[child.Name] then 200 193 pruned += 1 201 - task.synchronize() 202 194 child:Destroy() 203 - task.desynchronize() 204 195 end 205 196 end 206 197 if DEBUG_RESYNC and pruned > 0 then ··· 210 201 if DEBUG_RESYNC and (changed > 0 or removed > 0) then 211 202 print("[CHUNKMANAGER][RESYNC] Applied diff", key, "changed", changed, "removed", removed) 212 203 end 213 - task.desynchronize() 214 204 end 215 205 216 206 function ChunkManager:ForceTick() ··· 268 258 for y = 0, 2 do 269 259 task.defer(function() 270 260 for x = -CHUNK_RADIUS, CHUNK_RADIUS do 271 - task.desynchronize() 272 261 for z = -CHUNK_RADIUS, CHUNK_RADIUS do 273 262 local cx, cy, cz = chunkPos.x + x, y, chunkPos.z + z 274 263 local key = `{cx},{cy},{cz}` ··· 279 268 Swait(2) 280 269 end 281 270 end 282 - task.synchronize() 283 271 end 284 272 end) 285 273 Swait(10) ··· 291 279 if tick() - loadedChunk.inhabitedTime > 15 and not unloadingChunks[key] then 292 280 unloadingChunks[key] = true 293 281 task.defer(function() 294 - task.synchronize() 295 282 loadedChunk:Unload() 296 283 loadedChunk:Destroy() 297 284 Chunk.AllChunks[key] = nil
+63 -31
src/ReplicatedStorage/Shared/PlacementManager.lua
··· 21 21 PlacementManager.SelectionBox.Parent = game:GetService("Workspace"):FindFirstChildOfClass("Terrain") 22 22 23 23 -- Trash method TODO: Fix this 24 - local function findParent(i: Instance): Instance 25 - local f = i:FindFirstAncestorOfClass("Folder") 26 - local d = i 27 - repeat 28 - d = d.Parent 29 - until d.Parent == f 30 - return d 24 + local function findChunkFolderFromDescendant(inst: Instance): Instance? 25 + local current = inst 26 + while current and current.Parent do 27 + if current.Parent == PlacementManager.ChunkFolder then 28 + return current 29 + end 30 + current = current.Parent 31 + end 32 + return nil 31 33 end 32 34 33 35 local Mouse: Mouse = nil ··· 141 143 return root and root.Position or nil 142 144 end 143 145 146 + local MAX_REACH = 512 147 + 144 148 local function isWithinReach(cx: number, cy: number, cz: number, x: number, y: number, z: number): boolean 145 - local playerPos = getPlayerPosition() 146 - if not playerPos then 147 - return false 148 - end 149 - local blockPos = Util.ChunkPosToCFrame(Vector3.new(cx, cy, cz), Vector3.new(x, y, z)).Position 150 - return (blockPos - playerPos).Magnitude <= 24 149 + -- Client-side reach loosened; rely on server authority 150 + return true 151 151 end 152 152 153 153 -- Gets the block and normalid of the block (and surface) the player is looking at ··· 155 155 if not Mouse then 156 156 Mouse = game:GetService("Players").LocalPlayer:GetMouse() 157 157 end 158 - task.synchronize() 159 158 local objLookingAt = Mouse.Target 160 - local dir = Mouse.TargetSurface 159 + local dir = Mouse.TargetSurface or Enum.NormalId.Top 161 160 if not objLookingAt then 162 161 PlacementManager.SelectionBox.Adornee = nil 163 162 script.RaycastResult.Value = nil ··· 166 165 end 167 166 168 167 --if not objLookingAt:IsDescendantOf(ChunkManager.ChunkFolder) then return end 169 - local parent = findParent(objLookingAt) 170 - if parent:GetAttribute("ns") == true then 168 + local chunkFolder = findChunkFolderFromDescendant(objLookingAt) 169 + if not chunkFolder then 170 + PlacementManager.SelectionBox.Adornee = nil 171 + script.RaycastResult.Value = nil 172 + lastNormalId = nil 173 + return 174 + end 175 + if chunkFolder:GetAttribute("ns") == true then 171 176 PlacementManager.SelectionBox.Adornee = nil 172 177 script.RaycastResult.Value = nil 173 178 lastNormalId = nil 174 179 return 175 180 end 176 - PlacementManager.SelectionBox.Adornee = parent 177 - script.RaycastResult.Value = parent 181 + PlacementManager.SelectionBox.Adornee = objLookingAt 182 + script.RaycastResult.Value = objLookingAt 178 183 lastNormalId = dir 179 - return parent, dir 184 + return objLookingAt, dir 180 185 end 181 186 182 187 function PlacementManager:RaycastGetResult() ··· 190 195 191 196 -- FIRES REMOTE 192 197 function PlacementManager:PlaceBlock(cx, cy, cz, x, y, z, blockId: string) 193 - --print("placeblock") 194 - --local chunk = ChunkManager:GetChunk(cx, cy, cz) 195 - --chunk:CreateBlock(x, y, z, blockData) 196 - task.synchronize() 198 + -- ensure chunk is present/rendered client-side 199 + local chunk = ChunkManager:GetChunk(cx, cy, cz) 200 + if chunk and not chunk.loaded then 201 + ChunkManager:LoadChunk(cx, cy, cz) 202 + end 203 + 204 + -- if the client already thinks this block is the same id, skip sending 205 + if chunk then 206 + local existing = chunk:GetBlockAt(x, y, z) 207 + local existingId = existing and existing.id 208 + if existingId and tostring(existingId) == tostring(blockId) then 209 + return 210 + end 211 + end 212 + 213 + -- optimistic local apply; server will correct on tick 214 + if chunk then 215 + chunk:CreateBlock(x, y, z, { 216 + id = tonumber(blockId) or blockId, 217 + state = {}, 218 + }) 219 + end 220 + 197 221 placeRemote:FireServer(cx, cy, cz, x, y, z, blockId) 198 - task.desynchronize() 199 222 end 200 223 201 224 -- FIRES REMOTE ··· 229 252 chunk:RemoveBlock(x, y, z) 230 253 end 231 254 scheduleBreakRollback(cx, cy, cz, x, y, z) 232 - task.synchronize() 233 255 breakRemote:FireServer(cx, cy, cz, x, y, z) 234 - task.desynchronize() 235 256 end 236 257 237 258 -- CLIENTSIDED: only apply server-validated changes 238 259 local function applyPlaceBlockLocal(cx, cy, cz, x, y, z, blockData) 239 260 local chunk = ChunkManager:GetChunk(cx, cy, cz) 261 + if chunk and not chunk.loaded then 262 + ChunkManager:LoadChunk(cx, cy, cz) 263 + end 240 264 chunk:CreateBlock(x, y, z, blockData) 241 265 end 242 266 243 267 -- CLIENTSIDED: only apply server-validated changes 244 268 local function applyBreakBlockLocal(cx, cy, cz, x, y, z) 245 269 local chunk = ChunkManager:GetChunk(cx, cy, cz) 270 + if chunk and not chunk.loaded then 271 + ChunkManager:LoadChunk(cx, cy, cz) 272 + end 246 273 if not chunk then 247 274 return 248 275 end ··· 270 297 lastNormalId = nil 271 298 return nil 272 299 end 300 + if not selectedPart.Parent then 301 + PlacementManager.SelectionBox.Adornee = nil 302 + script.RaycastResult.Value = nil 303 + lastNormalId = nil 304 + return nil 305 + end 273 306 local chunkCoords = Util.BlockPosStringToCoords(selectedPart.Parent.Name) 274 307 local blockCoords = Util.BlockPosStringToCoords(selectedPart.Name) 275 308 ··· 282 315 283 316 function PlacementManager:GetTargetAtMouse(): nil | {chunk:Vector3, block: Vector3, normal: Enum.NormalId} 284 317 local hit = PlacementManager:GetBlockAtMouse() 285 - if not hit or not lastNormalId then 318 + if not hit then 286 319 return nil 287 320 end 321 + local normal = lastNormalId or Enum.NormalId.Top 288 322 289 323 return { 290 324 chunk = hit.chunk, 291 325 block = hit.block, 292 - normal = lastNormalId 326 + normal = normal 293 327 } 294 328 end 295 329 ··· 312 346 PlacementManager:Raycast() 313 347 end) 314 348 if not a then 315 - task.synchronize() 316 349 PlacementManager.SelectionBox.Adornee = nil 317 350 script.RaycastResult.Value = nil 318 - task.desynchronize() 319 351 end 320 352 end) 321 353 tickRemote.OnClientEvent:Connect(function(m, cx, cy, cz, x, y, z, d)
+33
src/ReplicatedStorage/Shared/PlacementState.lua
··· 1 + --!native 2 + --!optimize 2 3 + 4 + local ReplicatedStorage = game:GetService("ReplicatedStorage") 5 + 6 + local PlacementState = {} 7 + 8 + local selectedId: string = "" 9 + local selectedName: string = "" 10 + 11 + local changed = Instance.new("BindableEvent") 12 + PlacementState.Changed = changed.Event 13 + 14 + local valueObject = ReplicatedStorage:FindFirstChild("HotbarSelectedBlock") 15 + if not valueObject then 16 + valueObject = Instance.new("StringValue") 17 + valueObject.Name = "HotbarSelectedBlock" 18 + valueObject.Parent = ReplicatedStorage 19 + end 20 + PlacementState.ValueObject = valueObject 21 + 22 + function PlacementState:SetSelected(id: string?, name: string?) 23 + selectedId = id or "" 24 + selectedName = name or selectedId 25 + valueObject.Value = selectedName or "" 26 + changed:Fire(selectedId, selectedName) 27 + end 28 + 29 + function PlacementState:GetSelected() 30 + return selectedId, selectedName 31 + end 32 + 33 + return PlacementState
+34 -19
src/ServerScriptService/Actor/ServerChunkManager/init.server.lua
··· 1 1 --!native 2 2 --!optimize 2 3 3 4 - task.synchronize() 5 - 6 4 local ReplicatedStorage = game:GetService("ReplicatedStorage") 7 5 8 6 ··· 76 74 task.desynchronize() 77 75 end 78 76 79 - local MAX_REACH = 24 77 + local MAX_REACH = 512 80 78 local blockIdMap = {} 81 79 82 80 local function rebuildBlockIdMap() ··· 107 105 end 108 106 109 107 local function isWithinReach(player: Player, cx: number, cy: number, cz: number, x: number, y: number, z: number): boolean 110 - local playerPos = getPlayerPosition(player) 111 - if not playerPos then 112 - return false 113 - end 114 - local blockPos = Util.ChunkPosToCFrame(Vector3.new(cx, cy, cz), Vector3.new(x, y, z)).Position 115 - return (blockPos - playerPos).Magnitude <= MAX_REACH 108 + -- Relaxed reach; always true unless you want to re-enable limits 109 + return true 116 110 end 117 111 118 112 local function resolveBlockId(blockId: any): string | number | nil ··· 125 119 task.synchronize() 126 120 return chunk 127 121 end 122 + 123 + -- local PLAYER_BOX_SIZE = Vector3.new(3, 6, 3) 128 124 129 125 local function isBlockInsidePlayer(blockPos: Vector3): boolean 130 126 for _, player in ipairs(Players:GetPlayers()) do ··· 142 138 return false 143 139 end 144 140 141 + local DEBUG_PLACEMENT = true 142 + 145 143 placeRemote.OnServerEvent:Connect(function(player, cx, cy, cz, x, y, z, blockId) 146 - --print("place",player, cx, cy, cz, x, y, z, blockData) 144 + 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 148 + return 149 + end 147 150 148 151 if typeof(cx) ~= "number" or typeof(cy) ~= "number" or typeof(cz) ~= "number" then 149 - return 152 + return reject("chunk types") 150 153 end 151 154 if typeof(x) ~= "number" or typeof(y) ~= "number" or typeof(z) ~= "number" then 152 - return 155 + return reject("block types") 153 156 end 154 157 if x < 1 or x > 8 or y < 1 or y > 8 or z < 1 or z > 8 then 155 - return 158 + return reject("block bounds") 156 159 end 157 160 if math.abs(cx) > MAX_CHUNK_DIST or math.abs(cy) > MAX_CHUNK_DIST or math.abs(cz) > MAX_CHUNK_DIST then 158 - --return 161 + return reject("chunk bounds") 159 162 end 160 163 if not isWithinReach(player, cx, cy, cz, x, y, z) then 161 - return 164 + return reject("out of reach") 162 165 end 163 166 local resolvedId = resolveBlockId(blockId) 164 167 if not resolvedId then 165 - return 168 + return reject("invalid id") 166 169 end 167 170 168 171 local blockPos = Util.ChunkPosToCFrame(Vector3.new(cx, cy, cz), Vector3.new(x, y, z)).Position 169 172 if isBlockInsidePlayer(blockPos) then 170 - return 173 + return reject("inside player") 171 174 end 172 175 173 176 local chunk = getServerChunk(cx, cy, cz) 174 - if chunk:GetBlockAt(x, y, z) then 175 - return 177 + local existing = chunk:GetBlockAt(x, y, z) 178 + if existing and existing.id and existing.id ~= 0 then 179 + if existing.id == resolvedId then 180 + -- 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 184 + return 185 + end 186 + -- allow replacement when different id: remove then place 187 + chunk:RemoveBlock(x, y, z) 176 188 end 177 189 local data = { 178 190 id = resolvedId, ··· 180 192 } 181 193 chunk:CreateBlock(x, y, z, data) 182 194 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 183 198 end) 184 199 185 200 breakRemote.OnServerEvent:Connect(function(player, cx, cy, cz, x, y, z)
+3 -1
src/StarterGui/Game_UI/LocalScript.client.lua
··· 8 8 local ui = script.Parent 9 9 10 10 local ReplicatedStorage = game:GetService("ReplicatedStorage") 11 + local PlacementState = require(ReplicatedStorage.Shared.PlacementState) 11 12 12 13 ReplicatedStorage:WaitForChild("Objects"):WaitForChild("MLLoaded") 13 14 ··· 47 48 if math.abs(bpos.z) == 0 then bpos.z = 0 end 48 49 49 50 sky.CFrame = pos 50 - ui.DebugUpperText.Text = `Chunk {chunk.x} {chunk.y} {chunk.z}\nPos {bpos.x} {bpos.y} {bpos.z}\n<b>{fps} FPS</b>` 51 + local selected = PlacementState:GetSelected() 52 + ui.DebugUpperText.Text = `Chunk {chunk.x} {chunk.y} {chunk.z}\nPos {bpos.x} {bpos.y} {bpos.z}\nSel {selected}\n<b>{fps} FPS</b>` 51 53 52 54 cd:PivotTo(CFrame.new( 53 55 chunk.x*32,
+421
src/StarterGui/Hotbar/LocalScript.client.lua
··· 1 + --!native 2 + --!optimize 2 3 + 4 + if not game:IsLoaded() then 5 + game.Loaded:Wait() 6 + end 7 + 8 + local ReplicatedStorage = game:GetService("ReplicatedStorage") 9 + local UIS = game:GetService("UserInputService") 10 + local TextChatService = game:GetService("TextChatService") 11 + 12 + ReplicatedStorage:WaitForChild("Objects"):WaitForChild("MLLoaded") 13 + 14 + local Roact = require(ReplicatedStorage.Packages.roact) 15 + local PM = require(ReplicatedStorage.Shared.PlacementManager) 16 + local BlockManager = require(ReplicatedStorage.Shared.ChunkManager.BlockManager) 17 + local PlacementState = require(ReplicatedStorage.Shared.PlacementState) 18 + 19 + local blocksFolder = ReplicatedStorage:WaitForChild("Blocks") 20 + 21 + local HOTBAR_SIZE = 10 22 + 23 + local keyToSlot = { 24 + [Enum.KeyCode.One] = 1, 25 + [Enum.KeyCode.Two] = 2, 26 + [Enum.KeyCode.Three] = 3, 27 + [Enum.KeyCode.Four] = 4, 28 + [Enum.KeyCode.Five] = 5, 29 + [Enum.KeyCode.Six] = 6, 30 + [Enum.KeyCode.Seven] = 7, 31 + [Enum.KeyCode.Eight] = 8, 32 + [Enum.KeyCode.Nine] = 9, 33 + [Enum.KeyCode.Zero] = 10, 34 + } 35 + 36 + local colors = { 37 + base = Color3.fromRGB(30, 30, 46), 38 + slot = Color3.fromRGB(17, 17, 27), 39 + stroke = Color3.fromRGB(88, 91, 112), 40 + selectedStroke = Color3.fromRGB(137, 180, 250), 41 + text = Color3.fromRGB(205, 214, 244), 42 + subtext = Color3.fromRGB(166, 173, 200), 43 + } 44 + 45 + local function isTextInputFocused(): boolean 46 + if UIS:GetFocusedTextBox() then 47 + return true 48 + end 49 + local config = TextChatService:FindFirstChildOfClass("ChatInputBarConfiguration") 50 + return config ~= nil and config.IsFocused 51 + end 52 + 53 + local function buildHotbarIds(): {string} 54 + local ids = {} 55 + local names = {} 56 + for _, block in ipairs(blocksFolder:GetChildren()) do 57 + local id = block:GetAttribute("n") 58 + 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 62 + end 63 + end 64 + table.sort(ids, function(a, b) 65 + local na = tonumber(a) 66 + local nb = tonumber(b) 67 + if na and nb then 68 + return na < nb 69 + end 70 + return a < b 71 + end) 72 + local slots = table.create(HOTBAR_SIZE) 73 + for i = 1, HOTBAR_SIZE do 74 + slots[i] = ids[i] or "" 75 + end 76 + return slots, names 77 + end 78 + 79 + local function ensurePreviewRig(part: Instance) 80 + for _, descendant in ipairs(part:GetDescendants()) do 81 + if descendant:IsA("BasePart") then 82 + descendant.Anchored = true 83 + descendant.CanCollide = false 84 + end 85 + end 86 + if part:IsA("BasePart") then 87 + part.Anchored = true 88 + part.CanCollide = false 89 + end 90 + end 91 + 92 + local function updateViewport(viewport: ViewportFrame, blockId: string) 93 + viewport:ClearAllChildren() 94 + if blockId == "" then 95 + return 96 + end 97 + 98 + local camera = Instance.new("Camera") 99 + camera.Parent = viewport 100 + viewport.CurrentCamera = camera 101 + 102 + local world = Instance.new("WorldModel") 103 + world.Parent = viewport 104 + 105 + local resolvedId = tonumber(blockId) or blockId 106 + local preview = BlockManager:GetBlock(resolvedId) 107 + preview.Parent = world 108 + ensurePreviewRig(preview) 109 + 110 + local cf, size 111 + if preview:IsA("BasePart") then 112 + cf = preview.CFrame 113 + size = preview.Size 114 + else 115 + cf, size = preview:GetBoundingBox() 116 + end 117 + 118 + local maxSize = math.max(size.X, size.Y, size.Z) 119 + local distance = maxSize * 1.8 120 + local target = cf.Position 121 + camera.CFrame = CFrame.new(target + Vector3.new(distance, distance, distance), target) 122 + preview:PivotTo(CFrame.new()) 123 + end 124 + 125 + local Hotbar = Roact.Component:extend("Hotbar") 126 + 127 + function Hotbar:init() 128 + self.state = { 129 + slots = nil, 130 + names = nil, 131 + selected = 1, 132 + } 133 + local slots, names = buildHotbarIds() 134 + self.state.slots = slots 135 + self.state.names = names 136 + 137 + self._updateSlots = function() 138 + local nextSlots, nextNames = buildHotbarIds() 139 + self:setState({ 140 + slots = nextSlots, 141 + names = nextNames, 142 + }) 143 + end 144 + 145 + self._setSelected = function(slot: number) 146 + if slot < 1 or slot > HOTBAR_SIZE then 147 + return 148 + end 149 + self:setState({ 150 + selected = slot, 151 + }) 152 + local id = self.state.slots and self.state.slots[slot] or "" 153 + local name = "" 154 + if id ~= "" and self.state.names then 155 + name = self.state.names[id] or id 156 + end 157 + PlacementState:SetSelected(id, name) 158 + end 159 + 160 + self._handleInput = function(input: InputObject, gameProcessedEvent: boolean) 161 + if gameProcessedEvent or isTextInputFocused() then 162 + return 163 + end 164 + 165 + local slot = keyToSlot[input.KeyCode] 166 + if slot then 167 + self._setSelected(slot) 168 + return 169 + end 170 + 171 + if input.UserInputType == Enum.UserInputType.MouseButton1 then 172 + local mouseBlock = PM:GetBlockAtMouse() 173 + if not mouseBlock then 174 + return 175 + end 176 + PM:BreakBlock( 177 + mouseBlock.chunk.X, 178 + mouseBlock.chunk.Y, 179 + mouseBlock.chunk.Z, 180 + mouseBlock.block.X, 181 + mouseBlock.block.Y, 182 + mouseBlock.block.Z 183 + ) 184 + elseif input.UserInputType == Enum.UserInputType.MouseButton2 then 185 + local mouseBlock = PM:GetPlacementAtMouse() 186 + if not mouseBlock then 187 + return 188 + end 189 + local id = PlacementState:GetSelected() 190 + if not id or id == "" then 191 + return 192 + end 193 + PM:PlaceBlock( 194 + mouseBlock.chunk.X, 195 + mouseBlock.chunk.Y, 196 + mouseBlock.chunk.Z, 197 + mouseBlock.block.X, 198 + mouseBlock.block.Y, 199 + mouseBlock.block.Z, 200 + id 201 + ) 202 + end 203 + end 204 + 205 + self._handleScroll = function(input: InputObject, gameProcessedEvent: boolean) 206 + if gameProcessedEvent or isTextInputFocused() then 207 + return 208 + end 209 + if input.UserInputType ~= Enum.UserInputType.MouseWheel then 210 + return 211 + end 212 + local direction = input.Position.Z 213 + if direction == 0 then 214 + return 215 + end 216 + local delta = direction > 0 and -1 or 1 217 + local nextSlot = math.clamp(self.state.selected + delta, 1, HOTBAR_SIZE) 218 + if nextSlot ~= self.state.selected then 219 + self._setSelected(nextSlot) 220 + end 221 + end 222 + 223 + self._viewportRefs = {} 224 + self._viewportState = {} 225 + end 226 + 227 + function Hotbar:didMount() 228 + self._connections = { 229 + blocksFolder.ChildAdded:Connect(self._updateSlots), 230 + blocksFolder.ChildRemoved:Connect(self._updateSlots), 231 + UIS.InputBegan:Connect(self._handleInput), 232 + UIS.InputChanged:Connect(self._handleScroll), 233 + } 234 + self:_refreshViewports() 235 + -- initialize selection broadcast 236 + local id = self.state.slots and self.state.slots[self.state.selected] or "" 237 + local name = "" 238 + if id ~= "" and self.state.names then 239 + name = self.state.names[id] or id 240 + end 241 + PlacementState:SetSelected(id, name) 242 + end 243 + 244 + function Hotbar:willUnmount() 245 + for _, conn in ipairs(self._connections or {}) do 246 + conn:Disconnect() 247 + end 248 + self._connections = nil 249 + end 250 + 251 + function Hotbar:didUpdate(prevProps, prevState) 252 + if prevState.slots ~= self.state.slots then 253 + self:_refreshViewports() 254 + end 255 + end 256 + 257 + function Hotbar:_refreshViewports() 258 + for i = 1, HOTBAR_SIZE do 259 + local viewport = self._viewportRefs[i] 260 + if viewport then 261 + local id = self.state.slots[i] or "" 262 + if self._viewportState[i] ~= id then 263 + self._viewportState[i] = id 264 + updateViewport(viewport, id) 265 + end 266 + end 267 + end 268 + end 269 + 270 + function Hotbar:render() 271 + local slotElements = {} 272 + local selectedId = self.state.slots[self.state.selected] or "" 273 + local selectedName = "" 274 + if selectedId ~= "" and self.state.names then 275 + selectedName = self.state.names[selectedId] or selectedId 276 + end 277 + 278 + for i = 1, HOTBAR_SIZE do 279 + local id = self.state.slots[i] or "" 280 + local isSelected = i == self.state.selected 281 + 282 + slotElements[`Slot{i-1}`] = Roact.createElement("TextButton", { 283 + Size = UDim2.fromOffset(50, 50), 284 + BackgroundColor3 = colors.slot, 285 + BorderSizePixel = 0, 286 + AutoButtonColor = false, 287 + ClipsDescendants = true, 288 + Text = "", 289 + LayoutOrder = i, 290 + [Roact.Event.Activated] = function() 291 + self._setSelected(i) 292 + end, 293 + }, { 294 + Corner = Roact.createElement("UICorner", { 295 + CornerRadius = UDim.new(0, 13), 296 + }), 297 + Stroke = Roact.createElement("UIStroke", { 298 + Color = isSelected and colors.selectedStroke or colors.stroke, 299 + Thickness = isSelected and 2 or 1, 300 + ApplyStrokeMode = Enum.ApplyStrokeMode.Border 301 + }), 302 + Preview = Roact.createElement("ViewportFrame", { 303 + BackgroundTransparency = 1, 304 + Size = UDim2.new(1, 0, 1, 0), 305 + BorderSizePixel = 0, 306 + [Roact.Ref] = function(r) 307 + self._viewportRefs[i] = r 308 + end, 309 + }), 310 + IndexLabel = Roact.createElement("TextLabel", { 311 + BackgroundTransparency = 1, 312 + Position = UDim2.fromOffset(4, 2), 313 + Size = UDim2.fromOffset(18, 14), 314 + Font = Enum.Font.Gotham, 315 + Text = i == 10 and "0" or tostring(i), 316 + TextColor3 = colors.subtext, 317 + TextSize = 12, 318 + TextXAlignment = Enum.TextXAlignment.Left, 319 + TextYAlignment = Enum.TextYAlignment.Top, 320 + }), 321 + IdLabel = Roact.createElement("TextLabel", { 322 + BackgroundTransparency = 1, 323 + Position = UDim2.fromOffset(4, 26), 324 + Size = UDim2.new(1, -8, 0, 18), 325 + Font = Enum.Font.GothamBold, 326 + Text = id, 327 + TextColor3 = colors.text, 328 + TextSize = 15, 329 + TextWrapped = true, 330 + TextXAlignment = Enum.TextXAlignment.Left, 331 + TextYAlignment = Enum.TextYAlignment.Bottom, 332 + }), 333 + }) 334 + end 335 + 336 + local hotbarFrame = Roact.createElement("Frame", { 337 + AnchorPoint = Vector2.new(0.5, 1), 338 + AutomaticSize = Enum.AutomaticSize.X, 339 + BackgroundColor3 = colors.base, 340 + BorderSizePixel = 0, 341 + Position = UDim2.new(0.5, 0, 1, -20), 342 + Size = UDim2.fromOffset(0, 58), 343 + }, { 344 + Corner = Roact.createElement("UICorner", { 345 + CornerRadius = UDim.new(0, 16), 346 + }), 347 + Stroke = Roact.createElement("UIStroke", { 348 + Color = colors.selectedStroke, 349 + Thickness = 2, 350 + ApplyStrokeMode = Enum.ApplyStrokeMode.Border 351 + }), 352 + Padding = Roact.createElement("UIPadding", { 353 + PaddingLeft = UDim.new(0, 5), 354 + PaddingRight = UDim.new(0, 5), 355 + PaddingTop = UDim.new(0, 5), 356 + PaddingBottom = UDim.new(0, 5), 357 + }), 358 + Slots = Roact.createElement("Frame", { 359 + AutomaticSize = Enum.AutomaticSize.X, 360 + BackgroundTransparency = 1, 361 + Size = UDim2.new(0, 0, 1, 0), 362 + }, { 363 + Layout = Roact.createElement("UIListLayout", { 364 + FillDirection = Enum.FillDirection.Horizontal, 365 + HorizontalAlignment = Enum.HorizontalAlignment.Center, 366 + VerticalAlignment = Enum.VerticalAlignment.Center, 367 + Padding = UDim.new(0, 5), 368 + }), 369 + Slots = Roact.createFragment(slotElements), 370 + }), 371 + }) 372 + local selectedNameFrame = Roact.createElement("Frame", { 373 + AnchorPoint = Vector2.new(0.5, 1), 374 + AutomaticSize = Enum.AutomaticSize.X, 375 + BackgroundColor3 = colors.base, 376 + BorderSizePixel = 0, 377 + Position = UDim2.new(0.5, 0, 1, -80-10), 378 + Size = UDim2.fromOffset(0, 25), 379 + }, { 380 + Corner = Roact.createElement("UICorner", { 381 + CornerRadius = UDim.new(0, 8), 382 + }), 383 + Stroke = Roact.createElement("UIStroke", { 384 + Color = colors.selectedStroke, 385 + Thickness = 2, 386 + ApplyStrokeMode = Enum.ApplyStrokeMode.Border 387 + }), 388 + Padding = Roact.createElement("UIPadding", { 389 + PaddingLeft = UDim.new(0, 18), 390 + PaddingRight = UDim.new(0, 18), 391 + PaddingTop = UDim.new(0, 2), 392 + PaddingBottom = UDim.new(0, 2), 393 + }), 394 + Label = Roact.createElement("TextLabel", { 395 + BackgroundTransparency = 1, 396 + Size = UDim2.new(1, 0, 1, 0), 397 + Font = Enum.Font.JosefinSans, 398 + RichText = true, 399 + Text = selectedName ~= "" and selectedName or " ", 400 + TextColor3 = colors.text, 401 + TextSize = 19, 402 + TextWrapped = true, 403 + AutomaticSize = Enum.AutomaticSize.X, 404 + TextXAlignment = Enum.TextXAlignment.Center, 405 + TextYAlignment = Enum.TextYAlignment.Center 406 + }), 407 + }) 408 + 409 + return Roact.createFragment({ 410 + Hotbar = hotbarFrame, 411 + SelectedName = selectedNameFrame, 412 + }) 413 + end 414 + 415 + local handle = Roact.mount(Roact.createElement(Hotbar), script.Parent, "RoactHotbar") 416 + 417 + script.AncestryChanged:Connect(function(_, parent) 418 + if parent == nil then 419 + Roact.unmount(handle) 420 + end 421 + end)
+4
src/StarterGui/Hotbar/init.meta.json
··· 1 + { 2 + "className": "ScreenGui", 3 + "ignoreUnknownInstances": true 4 + }
+1 -92
src/StarterPlayer/StarterPlayerScripts/Actor/BlockInteraction.client.lua
··· 1 1 --!native 2 2 --!optimize 2 3 3 4 - if not game:IsLoaded() then 5 - game.Loaded:Wait() 6 - end 7 - 8 - local ReplicatedStorage = game:GetService("ReplicatedStorage") 9 - local UIS = game:GetService("UserInputService") 10 - 11 - ReplicatedStorage:WaitForChild("Objects"):WaitForChild("MLLoaded") 12 - 13 - local blocksFolder = ReplicatedStorage:WaitForChild("Blocks") 14 - local PM = require(ReplicatedStorage.Shared.PlacementManager) 15 - 16 - local HOTBAR_SIZE = 9 17 - local hotbar = table.create(HOTBAR_SIZE) 18 - local selectedSlot = 1 19 - 20 - local keyToSlot = { 21 - [Enum.KeyCode.One] = 1, 22 - [Enum.KeyCode.Two] = 2, 23 - [Enum.KeyCode.Three] = 3, 24 - [Enum.KeyCode.Four] = 4, 25 - [Enum.KeyCode.Five] = 5, 26 - [Enum.KeyCode.Six] = 6, 27 - [Enum.KeyCode.Seven] = 7, 28 - [Enum.KeyCode.Eight] = 8, 29 - [Enum.KeyCode.Nine] = 9, 30 - } 31 - 32 - local function rebuildHotbar() 33 - local ids = {} 34 - for _, block in ipairs(blocksFolder:GetChildren()) do 35 - local id = block:GetAttribute("n") 36 - if id ~= nil then 37 - table.insert(ids, tostring(id)) 38 - end 39 - end 40 - 41 - table.sort(ids) 42 - for i = 1, HOTBAR_SIZE do 43 - hotbar[i] = ids[i] or "" 44 - end 45 - selectedSlot = math.clamp(selectedSlot, 1, HOTBAR_SIZE) 46 - end 47 - 48 - local function getSelectedBlockId(): string? 49 - local id = hotbar[selectedSlot] 50 - if id == "" then 51 - return nil 52 - end 53 - return id 54 - end 55 - 56 - local function setSelectedSlot(slot: number) 57 - if slot < 1 or slot > HOTBAR_SIZE then 58 - return 59 - end 60 - selectedSlot = slot 61 - end 62 - 63 - rebuildHotbar() 64 - blocksFolder.ChildAdded:Connect(rebuildHotbar) 65 - blocksFolder.ChildRemoved:Connect(rebuildHotbar) 66 - 67 - UIS.InputBegan:Connect(function(input: InputObject, gameProcessedEvent: boolean) 68 - if gameProcessedEvent then 69 - return 70 - end 71 - 72 - local slot = keyToSlot[input.KeyCode] 73 - if slot then 74 - setSelectedSlot(slot) 75 - return 76 - end 77 - 78 - if input.UserInputType == Enum.UserInputType.MouseButton1 then 79 - local mouseBlock = PM:GetBlockAtMouse() 80 - if not mouseBlock then 81 - return 82 - end 83 - PM:BreakBlock(mouseBlock.chunk.X, mouseBlock.chunk.Y, mouseBlock.chunk.Z, mouseBlock.block.X, mouseBlock.block.Y, mouseBlock.block.Z) 84 - elseif input.UserInputType == Enum.UserInputType.MouseButton2 then 85 - local mouseBlock = PM:GetPlacementAtMouse() 86 - if not mouseBlock then 87 - return 88 - end 89 - local blockId = getSelectedBlockId() 90 - if not blockId then 91 - return 92 - end 93 - PM:PlaceBlock(mouseBlock.chunk.X, mouseBlock.chunk.Y, mouseBlock.chunk.Z, mouseBlock.block.X, mouseBlock.block.Y, mouseBlock.block.Z, blockId) 94 - end 95 - end) 4 + return
+6 -1
wally.lock
··· 10 10 [[package]] 11 11 name = "ocbwoy3-development-studios/minecraft-roblox" 12 12 version = "0.1.0" 13 - dependencies = [["cmdr", "evaera/cmdr@1.12.0"]] 13 + dependencies = [["cmdr", "evaera/cmdr@1.12.0"], ["roact", "roblox/roact@1.4.4"]] 14 + 15 + [[package]] 16 + name = "roblox/roact" 17 + version = "1.4.4" 18 + dependencies = []
+2 -1
wally.toml
··· 5 5 realm = "shared" 6 6 7 7 [dependencies] 8 - cmdr = "evaera/cmdr@1.12.0" 8 + cmdr = "evaera/cmdr@1.12.0" 9 + roact = "roblox/roact@1.4.4"