core: broken

This commit is contained in:
2026-01-07 22:51:11 +02:00
parent a9da63e90e
commit 165913ca51
13 changed files with 582 additions and 217 deletions

View File

@@ -23,9 +23,6 @@ end
local warnedBlockIds = {} local warnedBlockIds = {}
function BlockManager:GetBlock(blockId: number, attr: {[typeof("")]: any}?) function BlockManager:GetBlock(blockId: number, attr: {[typeof("")]: any}?)
task.synchronize()
if not BlockManager.BlockIdMappings[blockId] then if not BlockManager.BlockIdMappings[blockId] then
if not warnedBlockIds[blockId] then if not warnedBlockIds[blockId] then
warnedBlockIds[blockId] = true warnedBlockIds[blockId] = true
@@ -58,8 +55,6 @@ function BlockManager:GetBlockRotated(blockId: number, face: Enum.NormalId, attr
-- Returns block with id blockId, rotated so the given face (NormalId) points north (+X). -- Returns block with id blockId, rotated so the given face (NormalId) points north (+X).
local block = BlockManager:GetBlock(blockId, attr) local block = BlockManager:GetBlock(blockId, attr)
local rot = CFrame.new() local rot = CFrame.new()
task.synchronize()
if face == Enum.NormalId.Front then if face == Enum.NormalId.Front then
rot = CFrame.Angles(0, 0, 0) -- no rot rot = CFrame.Angles(0, 0, 0) -- no rot

View File

@@ -75,7 +75,6 @@ function Chunk.from(x,y,z,data)
end end
function Chunk:DoesNeighboringBlockExist(rx, ry, rz, gx, gy, gz, offsetX, offsetY, offsetZ) function Chunk:DoesNeighboringBlockExist(rx, ry, rz, gx, gy, gz, offsetX, offsetY, offsetZ)
task.desynchronize()
-- Calculate the local position of the neighboring block -- Calculate the local position of the neighboring block
local neighborRX, neighborRY, neighborRZ = rx + offsetX, ry + offsetY, rz + offsetZ local neighborRX, neighborRY, neighborRZ = rx + offsetX, ry + offsetY, rz + offsetZ
local neighborGX, neighborGY, neighborGZ = gx, gy, gz local neighborGX, neighborGY, neighborGZ = gx, gy, gz
@@ -120,7 +119,6 @@ function Chunk:DoesNeighboringBlockExist(rx, ry, rz, gx, gy, gz, offsetX, offset
end end
function Chunk:IsBlockRenderable(rx, ry, rz) function Chunk:IsBlockRenderable(rx, ry, rz)
task.desynchronize()
local gx, gy, gz = self.pos.X, self.pos.Y, self.pos.Z local gx, gy, gz = self.pos.X, self.pos.Y, self.pos.Z
-- Check all six neighboring blocks -- Check all six neighboring blocks
local d = not ( local d = not (
@@ -144,7 +142,6 @@ function Chunk:PropogateChanges(x: number,y: number,z: number,d:BlockData)
end end
function Chunk:GetBlockAt(x,y,z) function Chunk:GetBlockAt(x,y,z)
task.desynchronize()
if not self.data[keyFromCoords(x, y, z)] then if not self.data[keyFromCoords(x, y, z)] then
return nil return nil
end end
@@ -176,7 +173,6 @@ end
function Chunk:Unload() function Chunk:Unload()
task.synchronize()
self.loaded = false self.loaded = false
-- SLOWCLEAR -- SLOWCLEAR
@@ -194,11 +190,9 @@ function Chunk:Unload()
end end
end end
task.synchronize()
self.instance.Parent = nil self.instance.Parent = nil
self.instance:Destroy() self.instance:Destroy()
self.unloadChunkHook() self.unloadChunkHook()
task.desynchronize()
end) end)
end end

View File

@@ -43,8 +43,6 @@ local function Swait(l)
end end
local function propogateNeighboringBlockChanges(cx, cy, cz, x, y, z) local function propogateNeighboringBlockChanges(cx, cy, cz, x, y, z)
--warn("propogateNeighboringBlockChanges",cx,cy,cz,x,y,z)
-- updates block in another chunk
local c = Chunk.AllChunks[`{cx},{cy},{cz}`] local c = Chunk.AllChunks[`{cx},{cy},{cz}`]
if not c then return end if not c then return end
@@ -53,18 +51,14 @@ local function propogateNeighboringBlockChanges(cx, cy, cz, x, y, z)
if c:IsBlockRenderable(x, y, z) then if c:IsBlockRenderable(x, y, z) then
if c.instance:FindFirstChild(`{x},{y},{z}`) then return end if c.instance:FindFirstChild(`{x},{y},{z}`) then return end
task.synchronize()
local block = BlockManager:GetBlockRotated(d.id, util.RotationStringToNormalId(d.state["r"] or "f"), d.state) local block = BlockManager:GetBlockRotated(d.id, util.RotationStringToNormalId(d.state["r"] or "f"), d.state)
block.Name = `{x},{y},{z}` block.Name = `{x},{y},{z}`
block:PivotTo(util.ChunkPosToCFrame(c.pos, Vector3.new(x, y, z))) block:PivotTo(util.ChunkPosToCFrame(c.pos, Vector3.new(x, y, z)))
block.Parent = c.instance block.Parent = c.instance
task.desynchronize()
else else
local existing = c.instance:FindFirstChild(`{x},{y},{z}`) local existing = c.instance:FindFirstChild(`{x},{y},{z}`)
if existing then if existing then
task.synchronize()
existing:Destroy() existing:Destroy()
task.desynchronize()
end end
end end
end end
@@ -80,33 +74,27 @@ function ChunkBuilder:BuildChunk(c: typeof(Chunk.new(0,0,0)),parent: Instance?)
local finished = false local finished = false
local ch = Instance.new("Folder") local ch = Instance.new("Folder")
ch.Parent = parent ch.Parent = parent
ch.Name = `{c.pos.X},{c.pos.Y},{c.pos.Z}` ch.Name = `{c.pos.X},{c.pos.Y},{c.pos.Z}`
local conn = c.UpdateBlockBindableL.Event:Connect(function(x: number, y: number, z: number, d: BlockData) local conn = c.UpdateBlockBindableL.Event:Connect(function(x: number, y: number, z: number, d: BlockData)
task.desynchronize()
if finished == false then if finished == false then
newcache[`{x},{y},{z}`] = d newcache[`{x},{y},{z}`] = d
return return
end end
task.synchronize()
for _, o in pairs(NEIGHBOR_OFFSETS) do for _, o in pairs(NEIGHBOR_OFFSETS) do
--warn("propogate",o[1],o[2],o[3])
-- Adjust for chunk boundaries
local b = {x = x + o[1], y = y + o[2], z = z + o[3]} local b = {x = x + o[1], y = y + o[2], z = z + o[3]}
local ch = {x = c.pos.X, y = c.pos.Y, z = c.pos.Z} local chPos = {x = c.pos.X, y = c.pos.Y, z = c.pos.Z}
if b.x < 1 then ch.x = c.pos.X - 1 b.x = 8 end if b.x < 1 then chPos.x = c.pos.X - 1 b.x = 8 end
if b.x > 8 then ch.x = c.pos.X + 1 b.x = 1 end if b.x > 8 then chPos.x = c.pos.X + 1 b.x = 1 end
if b.y < 1 then ch.y = c.pos.Y - 1 b.y = 8 end if b.y < 1 then chPos.y = c.pos.Y - 1 b.y = 8 end
if b.y > 8 then ch.y = c.pos.Y + 1 b.y = 1 end if b.y > 8 then chPos.y = c.pos.Y + 1 b.y = 1 end
if b.z < 1 then ch.z = c.pos.Z - 1 b.z = 8 end if b.z < 1 then chPos.z = c.pos.Z - 1 b.z = 8 end
if b.z > 8 then ch.z = c.pos.Z + 1 b.z = 1 end if b.z > 8 then chPos.z = c.pos.Z + 1 b.z = 1 end
propogateNeighboringBlockChanges(ch.x, ch.y, ch.z, b.x, b.y, b.z) propogateNeighboringBlockChanges(chPos.x, chPos.y, chPos.z, b.x, b.y, b.z)
--BlockManager:GetBlock(ch.x)
end end
local blockName = `{x},{y},{z}` local blockName = `{x},{y},{z}`
@@ -116,12 +104,10 @@ function ChunkBuilder:BuildChunk(c: typeof(Chunk.new(0,0,0)),parent: Instance?)
c.delayedRemoval[blockName] = nil c.delayedRemoval[blockName] = nil
if existing then if existing then
task.defer(function() task.defer(function()
task.synchronize()
RunService.RenderStepped:Wait() RunService.RenderStepped:Wait()
if existing.Parent then if existing.Parent then
existing:Destroy() existing:Destroy()
end end
task.desynchronize()
end) end)
elseif DEBUG_GHOST then elseif DEBUG_GHOST then
print("[CHUNKBUILDER][GHOST] Delayed remove missing instance", c.pos, blockName) print("[CHUNKBUILDER][GHOST] Delayed remove missing instance", c.pos, blockName)
@@ -129,9 +115,7 @@ function ChunkBuilder:BuildChunk(c: typeof(Chunk.new(0,0,0)),parent: Instance?)
return return
end end
if existing then if existing then
task.synchronize()
existing:Destroy() existing:Destroy()
task.desynchronize()
elseif DEBUG_GHOST then elseif DEBUG_GHOST then
print("[CHUNKBUILDER][GHOST] Remove missing instance", c.pos, blockName) print("[CHUNKBUILDER][GHOST] Remove missing instance", c.pos, blockName)
end end
@@ -139,26 +123,20 @@ function ChunkBuilder:BuildChunk(c: typeof(Chunk.new(0,0,0)),parent: Instance?)
end end
if not c:IsBlockRenderable(x, y, z) then if not c:IsBlockRenderable(x, y, z) then
if existing then if existing then
task.synchronize()
existing:Destroy() existing:Destroy()
task.desynchronize()
end end
return return
end end
if existing then if existing then
task.synchronize()
existing:Destroy() existing:Destroy()
task.desynchronize()
end end
if not d then return end if not d then return end
if d.id == 0 then return end if d.id == 0 then return end
local N = util.RotationStringToNormalId(d.state["r"] or "f") local N = util.RotationStringToNormalId(d.state["r"] or "f")
task.synchronize()
local block = BlockManager:GetBlockRotated(d.id, N, d.state) local block = BlockManager:GetBlockRotated(d.id, N, d.state)
block.Name = blockName block.Name = blockName
block:PivotTo(util.ChunkPosToCFrame(c.pos, Vector3.new(x, y, z))) block:PivotTo(util.ChunkPosToCFrame(c.pos, Vector3.new(x, y, z)))
block.Parent = ch block.Parent = ch
task.desynchronize()
end) end)
c.unloadChunkHook = function() c.unloadChunkHook = function()
@@ -171,12 +149,10 @@ function ChunkBuilder:BuildChunk(c: typeof(Chunk.new(0,0,0)),parent: Instance?)
local p = 0 local p = 0
task.synchronize()
local hb = false local hb = false
for _,b in pairs(blocks) do
for a,b in pairs(blocks) do
hb = true hb = true
break
end end
local border = Instance.new("Part") local border = Instance.new("Part")
@@ -186,22 +162,18 @@ function ChunkBuilder:BuildChunk(c: typeof(Chunk.new(0,0,0)),parent: Instance?)
end end
for a,b in pairs(blocks) do for a,b in pairs(blocks) do
task.desynchronize()
local coords = util.BlockPosStringToCoords(a) local coords = util.BlockPosStringToCoords(a)
if not c:IsBlockRenderable(coords.X, coords.Y, coords.Z) then if not c or not c.IsBlockRenderable or not c:IsBlockRenderable(coords.X, coords.Y, coords.Z) then
if ch:FindFirstChild(a) then if ch:FindFirstChild(a) then
task.synchronize()
ch:FindFirstChild(a):Destroy() ch:FindFirstChild(a):Destroy()
task.desynchronize()
end end
continue continue
end end
task.desynchronize()
local N = util.RotationStringToNormalId(b.state["r"] or "f") local N = util.RotationStringToNormalId(b.state["r"] or "f")
task.synchronize()
local block = BlockManager:GetBlockRotated(b.id, N, b.state) local block = BlockManager:GetBlockRotated(b.id, N, b.state)
if ch:FindFirstChild(a) then local existing = ch:FindFirstChild(a)
ch:FindFirstChild(a):Destroy() if existing then
existing:Destroy()
end end
block.Name = a block.Name = a
block:PivotTo(util.ChunkPosToCFrame(c.pos, coords)) block:PivotTo(util.ChunkPosToCFrame(c.pos, coords))
@@ -215,16 +187,12 @@ function ChunkBuilder:BuildChunk(c: typeof(Chunk.new(0,0,0)),parent: Instance?)
finished = true finished = true
task.synchronize()
border:Destroy() border:Destroy()
task.desynchronize()
task.defer(function() task.defer(function()
task.synchronize()
for key, data in pairs(newcache) do for key, data in pairs(newcache) do
local coords = util.BlockPosStringToCoords(key) local coords = util.BlockPosStringToCoords(key)
for _, o in pairs(NEIGHBOR_OFFSETS) do for _, o in pairs(NEIGHBOR_OFFSETS) do
-- chunks are 8x8x8
local nb = {x = coords.X + o[1], y = coords.Y + o[2], z = coords.Z + o[3]} local nb = {x = coords.X + o[1], y = coords.Y + o[2], z = coords.Z + o[3]}
local chCoords = {x = c.pos.X, y = c.pos.Y, z = c.pos.Z} local chCoords = {x = c.pos.X, y = c.pos.Y, z = c.pos.Z}
if nb.x == 0 then chCoords.x = c.pos.X - 1 nb.x = 8 end if nb.x == 0 then chCoords.x = c.pos.X - 1 nb.x = 8 end
@@ -246,7 +214,7 @@ function ChunkBuilder:BuildChunk(c: typeof(Chunk.new(0,0,0)),parent: Instance?)
end end
continue continue
end end
if not c:IsBlockRenderable(coords.X, coords.Y, coords.Z) then if not c or not c.IsBlockRenderable or not c:IsBlockRenderable(coords.X, coords.Y, coords.Z) then
if existing then if existing then
existing:Destroy() existing:Destroy()
end end
@@ -267,7 +235,6 @@ function ChunkBuilder:BuildChunk(c: typeof(Chunk.new(0,0,0)),parent: Instance?)
newcache = nil newcache = nil
blocks = nil blocks = nil
end) end)
task.desynchronize()
end) end)
c.loaded = true c.loaded = true

View File

@@ -46,7 +46,6 @@ do
end end
local function Swait(l) local function Swait(l)
task.synchronize()
for _ = 1, l do for _ = 1, l do
RunService.Stepped:Wait() RunService.Stepped:Wait()
end end
@@ -59,14 +58,12 @@ function ChunkManager:GetChunk(x, y, z)
end end
if pendingChunkRequests[key] then if pendingChunkRequests[key] then
task.synchronize()
while pendingChunkRequests[key] do while pendingChunkRequests[key] do
task.wait() task.wait()
end end
return Chunk.AllChunks[key] return Chunk.AllChunks[key]
end end
task.synchronize()
pendingChunkRequests[key] = true pendingChunkRequests[key] = true
local ok, data = pcall(function() local ok, data = pcall(function()
return remote:InvokeServer(x, y, z) return remote:InvokeServer(x, y, z)
@@ -74,7 +71,6 @@ function ChunkManager:GetChunk(x, y, z)
if not ok then if not ok then
data = {} data = {}
end end
task.synchronize()
local ch = Chunk.from(x, y, z, data) local ch = Chunk.from(x, y, z, data)
Chunk.AllChunks[key] = ch Chunk.AllChunks[key] = ch
pendingChunkRequests[key] = nil pendingChunkRequests[key] = nil
@@ -102,7 +98,6 @@ function ChunkManager:LoadChunk(x, y, z)
unloadingChunks[key] = true unloadingChunks[key] = true
task.defer(function() task.defer(function()
task.desynchronize()
ensureNeighboringChunksLoaded(x, y, z) ensureNeighboringChunksLoaded(x, y, z)
local chunk = Chunk.AllChunks[key] local chunk = Chunk.AllChunks[key]
@@ -111,7 +106,6 @@ function ChunkManager:LoadChunk(x, y, z)
Chunk.AllChunks[key] = chunk Chunk.AllChunks[key] = chunk
end end
task.synchronize()
local instance = ChunkBuilder:BuildChunk(chunk, ChunkFolder) local instance = ChunkBuilder:BuildChunk(chunk, ChunkFolder)
chunk.instance = instance chunk.instance = instance
chunk.loaded = true chunk.loaded = true
@@ -129,7 +123,6 @@ function ChunkManager:RefreshChunk(x, y, z)
return return
end end
task.synchronize()
local ok, newData = pcall(function() local ok, newData = pcall(function()
return remote:InvokeServer(x, y, z) return remote:InvokeServer(x, y, z)
end) end)
@@ -198,9 +191,7 @@ function ChunkManager:RefreshChunk(x, y, z)
for _, child in ipairs(chunk.instance:GetChildren()) do for _, child in ipairs(chunk.instance:GetChildren()) do
if not newData[child.Name] then if not newData[child.Name] then
pruned += 1 pruned += 1
task.synchronize()
child:Destroy() child:Destroy()
task.desynchronize()
end end
end end
if DEBUG_RESYNC and pruned > 0 then if DEBUG_RESYNC and pruned > 0 then
@@ -210,7 +201,6 @@ function ChunkManager:RefreshChunk(x, y, z)
if DEBUG_RESYNC and (changed > 0 or removed > 0) then if DEBUG_RESYNC and (changed > 0 or removed > 0) then
print("[CHUNKMANAGER][RESYNC] Applied diff", key, "changed", changed, "removed", removed) print("[CHUNKMANAGER][RESYNC] Applied diff", key, "changed", changed, "removed", removed)
end end
task.desynchronize()
end end
function ChunkManager:ForceTick() function ChunkManager:ForceTick()
@@ -268,7 +258,6 @@ function ChunkManager:Tick()
for y = 0, 2 do for y = 0, 2 do
task.defer(function() task.defer(function()
for x = -CHUNK_RADIUS, CHUNK_RADIUS do for x = -CHUNK_RADIUS, CHUNK_RADIUS do
task.desynchronize()
for z = -CHUNK_RADIUS, CHUNK_RADIUS do for z = -CHUNK_RADIUS, CHUNK_RADIUS do
local cx, cy, cz = chunkPos.x + x, y, chunkPos.z + z local cx, cy, cz = chunkPos.x + x, y, chunkPos.z + z
local key = `{cx},{cy},{cz}` local key = `{cx},{cy},{cz}`
@@ -279,7 +268,6 @@ function ChunkManager:Tick()
Swait(2) Swait(2)
end end
end end
task.synchronize()
end end
end) end)
Swait(10) Swait(10)
@@ -291,7 +279,6 @@ function ChunkManager:Tick()
if tick() - loadedChunk.inhabitedTime > 15 and not unloadingChunks[key] then if tick() - loadedChunk.inhabitedTime > 15 and not unloadingChunks[key] then
unloadingChunks[key] = true unloadingChunks[key] = true
task.defer(function() task.defer(function()
task.synchronize()
loadedChunk:Unload() loadedChunk:Unload()
loadedChunk:Destroy() loadedChunk:Destroy()
Chunk.AllChunks[key] = nil Chunk.AllChunks[key] = nil

View File

@@ -21,13 +21,15 @@ PlacementManager.SelectionBox.Name = "$SelectionBox"..(game:GetService("RunServi
PlacementManager.SelectionBox.Parent = game:GetService("Workspace"):FindFirstChildOfClass("Terrain") PlacementManager.SelectionBox.Parent = game:GetService("Workspace"):FindFirstChildOfClass("Terrain")
-- Trash method TODO: Fix this -- Trash method TODO: Fix this
local function findParent(i: Instance): Instance local function findChunkFolderFromDescendant(inst: Instance): Instance?
local f = i:FindFirstAncestorOfClass("Folder") local current = inst
local d = i while current and current.Parent do
repeat if current.Parent == PlacementManager.ChunkFolder then
d = d.Parent return current
until d.Parent == f end
return d current = current.Parent
end
return nil
end end
local Mouse: Mouse = nil local Mouse: Mouse = nil
@@ -141,13 +143,11 @@ local function getPlayerPosition(): Vector3?
return root and root.Position or nil return root and root.Position or nil
end end
local MAX_REACH = 512
local function isWithinReach(cx: number, cy: number, cz: number, x: number, y: number, z: number): boolean local function isWithinReach(cx: number, cy: number, cz: number, x: number, y: number, z: number): boolean
local playerPos = getPlayerPosition() -- Client-side reach loosened; rely on server authority
if not playerPos then return true
return false
end
local blockPos = Util.ChunkPosToCFrame(Vector3.new(cx, cy, cz), Vector3.new(x, y, z)).Position
return (blockPos - playerPos).Magnitude <= 24
end end
-- Gets the block and normalid of the block (and surface) the player is looking at -- Gets the block and normalid of the block (and surface) the player is looking at
@@ -155,9 +155,8 @@ function PlacementManager:Raycast()
if not Mouse then if not Mouse then
Mouse = game:GetService("Players").LocalPlayer:GetMouse() Mouse = game:GetService("Players").LocalPlayer:GetMouse()
end end
task.synchronize()
local objLookingAt = Mouse.Target local objLookingAt = Mouse.Target
local dir = Mouse.TargetSurface local dir = Mouse.TargetSurface or Enum.NormalId.Top
if not objLookingAt then if not objLookingAt then
PlacementManager.SelectionBox.Adornee = nil PlacementManager.SelectionBox.Adornee = nil
script.RaycastResult.Value = nil script.RaycastResult.Value = nil
@@ -166,17 +165,23 @@ function PlacementManager:Raycast()
end end
--if not objLookingAt:IsDescendantOf(ChunkManager.ChunkFolder) then return end --if not objLookingAt:IsDescendantOf(ChunkManager.ChunkFolder) then return end
local parent = findParent(objLookingAt) local chunkFolder = findChunkFolderFromDescendant(objLookingAt)
if parent:GetAttribute("ns") == true then if not chunkFolder then
PlacementManager.SelectionBox.Adornee = nil PlacementManager.SelectionBox.Adornee = nil
script.RaycastResult.Value = nil script.RaycastResult.Value = nil
lastNormalId = nil lastNormalId = nil
return return
end end
PlacementManager.SelectionBox.Adornee = parent if chunkFolder:GetAttribute("ns") == true then
script.RaycastResult.Value = parent PlacementManager.SelectionBox.Adornee = nil
script.RaycastResult.Value = nil
lastNormalId = nil
return
end
PlacementManager.SelectionBox.Adornee = objLookingAt
script.RaycastResult.Value = objLookingAt
lastNormalId = dir lastNormalId = dir
return parent, dir return objLookingAt, dir
end end
function PlacementManager:RaycastGetResult() function PlacementManager:RaycastGetResult()
@@ -190,12 +195,30 @@ local tickRemote = game:GetService("ReplicatedStorage").Tick
-- FIRES REMOTE -- FIRES REMOTE
function PlacementManager:PlaceBlock(cx, cy, cz, x, y, z, blockId: string) function PlacementManager:PlaceBlock(cx, cy, cz, x, y, z, blockId: string)
--print("placeblock") -- ensure chunk is present/rendered client-side
--local chunk = ChunkManager:GetChunk(cx, cy, cz) local chunk = ChunkManager:GetChunk(cx, cy, cz)
--chunk:CreateBlock(x, y, z, blockData) if chunk and not chunk.loaded then
task.synchronize() ChunkManager:LoadChunk(cx, cy, cz)
end
-- if the client already thinks this block is the same id, skip sending
if chunk then
local existing = chunk:GetBlockAt(x, y, z)
local existingId = existing and existing.id
if existingId and tostring(existingId) == tostring(blockId) then
return
end
end
-- optimistic local apply; server will correct on tick
if chunk then
chunk:CreateBlock(x, y, z, {
id = tonumber(blockId) or blockId,
state = {},
})
end
placeRemote:FireServer(cx, cy, cz, x, y, z, blockId) placeRemote:FireServer(cx, cy, cz, x, y, z, blockId)
task.desynchronize()
end end
-- FIRES REMOTE -- FIRES REMOTE
@@ -229,20 +252,24 @@ function PlacementManager:BreakBlock(cx, cy, cz, x, y, z)
chunk:RemoveBlock(x, y, z) chunk:RemoveBlock(x, y, z)
end end
scheduleBreakRollback(cx, cy, cz, x, y, z) scheduleBreakRollback(cx, cy, cz, x, y, z)
task.synchronize()
breakRemote:FireServer(cx, cy, cz, x, y, z) breakRemote:FireServer(cx, cy, cz, x, y, z)
task.desynchronize()
end end
-- CLIENTSIDED: only apply server-validated changes -- CLIENTSIDED: only apply server-validated changes
local function applyPlaceBlockLocal(cx, cy, cz, x, y, z, blockData) local function applyPlaceBlockLocal(cx, cy, cz, x, y, z, blockData)
local chunk = ChunkManager:GetChunk(cx, cy, cz) local chunk = ChunkManager:GetChunk(cx, cy, cz)
if chunk and not chunk.loaded then
ChunkManager:LoadChunk(cx, cy, cz)
end
chunk:CreateBlock(x, y, z, blockData) chunk:CreateBlock(x, y, z, blockData)
end end
-- CLIENTSIDED: only apply server-validated changes -- CLIENTSIDED: only apply server-validated changes
local function applyBreakBlockLocal(cx, cy, cz, x, y, z) local function applyBreakBlockLocal(cx, cy, cz, x, y, z)
local chunk = ChunkManager:GetChunk(cx, cy, cz) local chunk = ChunkManager:GetChunk(cx, cy, cz)
if chunk and not chunk.loaded then
ChunkManager:LoadChunk(cx, cy, cz)
end
if not chunk then if not chunk then
return return
end end
@@ -270,6 +297,12 @@ function PlacementManager:GetBlockAtMouse(): nil | {chunk:Vector3, block: Vector
lastNormalId = nil lastNormalId = nil
return nil return nil
end end
if not selectedPart.Parent then
PlacementManager.SelectionBox.Adornee = nil
script.RaycastResult.Value = nil
lastNormalId = nil
return nil
end
local chunkCoords = Util.BlockPosStringToCoords(selectedPart.Parent.Name) local chunkCoords = Util.BlockPosStringToCoords(selectedPart.Parent.Name)
local blockCoords = Util.BlockPosStringToCoords(selectedPart.Name) local blockCoords = Util.BlockPosStringToCoords(selectedPart.Name)
@@ -282,14 +315,15 @@ end
function PlacementManager:GetTargetAtMouse(): nil | {chunk:Vector3, block: Vector3, normal: Enum.NormalId} function PlacementManager:GetTargetAtMouse(): nil | {chunk:Vector3, block: Vector3, normal: Enum.NormalId}
local hit = PlacementManager:GetBlockAtMouse() local hit = PlacementManager:GetBlockAtMouse()
if not hit or not lastNormalId then if not hit then
return nil return nil
end end
local normal = lastNormalId or Enum.NormalId.Top
return { return {
chunk = hit.chunk, chunk = hit.chunk,
block = hit.block, block = hit.block,
normal = lastNormalId normal = normal
} }
end end
@@ -312,10 +346,8 @@ function PlacementManager:Init()
PlacementManager:Raycast() PlacementManager:Raycast()
end) end)
if not a then if not a then
task.synchronize()
PlacementManager.SelectionBox.Adornee = nil PlacementManager.SelectionBox.Adornee = nil
script.RaycastResult.Value = nil script.RaycastResult.Value = nil
task.desynchronize()
end end
end) end)
tickRemote.OnClientEvent:Connect(function(m, cx, cy, cz, x, y, z, d) tickRemote.OnClientEvent:Connect(function(m, cx, cy, cz, x, y, z, d)

View File

@@ -0,0 +1,33 @@
--!native
--!optimize 2
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local PlacementState = {}
local selectedId: string = ""
local selectedName: string = ""
local changed = Instance.new("BindableEvent")
PlacementState.Changed = changed.Event
local valueObject = ReplicatedStorage:FindFirstChild("HotbarSelectedBlock")
if not valueObject then
valueObject = Instance.new("StringValue")
valueObject.Name = "HotbarSelectedBlock"
valueObject.Parent = ReplicatedStorage
end
PlacementState.ValueObject = valueObject
function PlacementState:SetSelected(id: string?, name: string?)
selectedId = id or ""
selectedName = name or selectedId
valueObject.Value = selectedName or ""
changed:Fire(selectedId, selectedName)
end
function PlacementState:GetSelected()
return selectedId, selectedName
end
return PlacementState

View File

@@ -1,8 +1,6 @@
--!native --!native
--!optimize 2 --!optimize 2
task.synchronize()
local ReplicatedStorage = game:GetService("ReplicatedStorage") local ReplicatedStorage = game:GetService("ReplicatedStorage")
@@ -76,7 +74,7 @@ local function propogate(a, cx, cy, cz, x, y, z, bd)
task.desynchronize() task.desynchronize()
end end
local MAX_REACH = 24 local MAX_REACH = 512
local blockIdMap = {} local blockIdMap = {}
local function rebuildBlockIdMap() local function rebuildBlockIdMap()
@@ -107,12 +105,8 @@ local function getPlayerPosition(player: Player): Vector3?
end end
local function isWithinReach(player: Player, cx: number, cy: number, cz: number, x: number, y: number, z: number): boolean local function isWithinReach(player: Player, cx: number, cy: number, cz: number, x: number, y: number, z: number): boolean
local playerPos = getPlayerPosition(player) -- Relaxed reach; always true unless you want to re-enable limits
if not playerPos then return true
return false
end
local blockPos = Util.ChunkPosToCFrame(Vector3.new(cx, cy, cz), Vector3.new(x, y, z)).Position
return (blockPos - playerPos).Magnitude <= MAX_REACH
end end
local function resolveBlockId(blockId: any): string | number | nil local function resolveBlockId(blockId: any): string | number | nil
@@ -126,6 +120,8 @@ local function getServerChunk(cx: number, cy: number, cz: number)
return chunk return chunk
end end
-- local PLAYER_BOX_SIZE = Vector3.new(3, 6, 3)
local function isBlockInsidePlayer(blockPos: Vector3): boolean local function isBlockInsidePlayer(blockPos: Vector3): boolean
for _, player in ipairs(Players:GetPlayers()) do for _, player in ipairs(Players:GetPlayers()) do
local character = player.Character local character = player.Character
@@ -142,37 +138,53 @@ local function isBlockInsidePlayer(blockPos: Vector3): boolean
return false return false
end end
local DEBUG_PLACEMENT = true
placeRemote.OnServerEvent:Connect(function(player, cx, cy, cz, x, y, z, blockId) placeRemote.OnServerEvent:Connect(function(player, cx, cy, cz, x, y, z, blockId)
--print("place",player, cx, cy, cz, x, y, z, blockData) local function reject(reason: string)
if DEBUG_PLACEMENT then
warn("[PLACE][REJECT]", player.Name, reason, "chunk", cx, cy, cz, "block", x, y, z, "id", blockId)
end
return
end
if typeof(cx) ~= "number" or typeof(cy) ~= "number" or typeof(cz) ~= "number" then if typeof(cx) ~= "number" or typeof(cy) ~= "number" or typeof(cz) ~= "number" then
return return reject("chunk types")
end end
if typeof(x) ~= "number" or typeof(y) ~= "number" or typeof(z) ~= "number" then if typeof(x) ~= "number" or typeof(y) ~= "number" or typeof(z) ~= "number" then
return return reject("block types")
end end
if x < 1 or x > 8 or y < 1 or y > 8 or z < 1 or z > 8 then if x < 1 or x > 8 or y < 1 or y > 8 or z < 1 or z > 8 then
return return reject("block bounds")
end end
if math.abs(cx) > MAX_CHUNK_DIST or math.abs(cy) > MAX_CHUNK_DIST or math.abs(cz) > MAX_CHUNK_DIST then if math.abs(cx) > MAX_CHUNK_DIST or math.abs(cy) > MAX_CHUNK_DIST or math.abs(cz) > MAX_CHUNK_DIST then
--return return reject("chunk bounds")
end end
if not isWithinReach(player, cx, cy, cz, x, y, z) then if not isWithinReach(player, cx, cy, cz, x, y, z) then
return return reject("out of reach")
end end
local resolvedId = resolveBlockId(blockId) local resolvedId = resolveBlockId(blockId)
if not resolvedId then if not resolvedId then
return return reject("invalid id")
end end
local blockPos = Util.ChunkPosToCFrame(Vector3.new(cx, cy, cz), Vector3.new(x, y, z)).Position local blockPos = Util.ChunkPosToCFrame(Vector3.new(cx, cy, cz), Vector3.new(x, y, z)).Position
if isBlockInsidePlayer(blockPos) then if isBlockInsidePlayer(blockPos) then
return return reject("inside player")
end end
local chunk = getServerChunk(cx, cy, cz) local chunk = getServerChunk(cx, cy, cz)
if chunk:GetBlockAt(x, y, z) then local existing = chunk:GetBlockAt(x, y, z)
return if existing and existing.id and existing.id ~= 0 then
if existing.id == resolvedId then
-- same block already there; treat as success without changes
if DEBUG_PLACEMENT then
print("[PLACE][OK][NOOP]", player.Name, "chunk", cx, cy, cz, "block", x, y, z, "id", resolvedId)
end
return
end
-- allow replacement when different id: remove then place
chunk:RemoveBlock(x, y, z)
end end
local data = { local data = {
id = resolvedId, id = resolvedId,
@@ -180,6 +192,9 @@ placeRemote.OnServerEvent:Connect(function(player, cx, cy, cz, x, y, z, blockId)
} }
chunk:CreateBlock(x, y, z, data) chunk:CreateBlock(x, y, z, data)
propogate("B_C", cx, cy, cz, x, y, z, data) propogate("B_C", cx, cy, cz, x, y, z, data)
if DEBUG_PLACEMENT then
print("[PLACE][OK]", player.Name, "chunk", cx, cy, cz, "block", x, y, z, "id", resolvedId)
end
end) end)
breakRemote.OnServerEvent:Connect(function(player, cx, cy, cz, x, y, z) breakRemote.OnServerEvent:Connect(function(player, cx, cy, cz, x, y, z)

View File

@@ -8,6 +8,7 @@ end
local ui = script.Parent local ui = script.Parent
local ReplicatedStorage = game:GetService("ReplicatedStorage") local ReplicatedStorage = game:GetService("ReplicatedStorage")
local PlacementState = require(ReplicatedStorage.Shared.PlacementState)
ReplicatedStorage:WaitForChild("Objects"):WaitForChild("MLLoaded") ReplicatedStorage:WaitForChild("Objects"):WaitForChild("MLLoaded")
@@ -47,7 +48,8 @@ game:GetService("RunService").RenderStepped:Connect(function(dt)
if math.abs(bpos.z) == 0 then bpos.z = 0 end if math.abs(bpos.z) == 0 then bpos.z = 0 end
sky.CFrame = pos sky.CFrame = pos
ui.DebugUpperText.Text = `Chunk {chunk.x} {chunk.y} {chunk.z}\nPos {bpos.x} {bpos.y} {bpos.z}\n<b>{fps} FPS</b>` local selected = PlacementState:GetSelected()
ui.DebugUpperText.Text = `Chunk {chunk.x} {chunk.y} {chunk.z}\nPos {bpos.x} {bpos.y} {bpos.z}\nSel {selected}\n<b>{fps} FPS</b>`
cd:PivotTo(CFrame.new( cd:PivotTo(CFrame.new(
chunk.x*32, chunk.x*32,

View File

@@ -0,0 +1,421 @@
--!native
--!optimize 2
if not game:IsLoaded() then
game.Loaded:Wait()
end
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local UIS = game:GetService("UserInputService")
local TextChatService = game:GetService("TextChatService")
ReplicatedStorage:WaitForChild("Objects"):WaitForChild("MLLoaded")
local Roact = require(ReplicatedStorage.Packages.roact)
local PM = require(ReplicatedStorage.Shared.PlacementManager)
local BlockManager = require(ReplicatedStorage.Shared.ChunkManager.BlockManager)
local PlacementState = require(ReplicatedStorage.Shared.PlacementState)
local blocksFolder = ReplicatedStorage:WaitForChild("Blocks")
local HOTBAR_SIZE = 10
local keyToSlot = {
[Enum.KeyCode.One] = 1,
[Enum.KeyCode.Two] = 2,
[Enum.KeyCode.Three] = 3,
[Enum.KeyCode.Four] = 4,
[Enum.KeyCode.Five] = 5,
[Enum.KeyCode.Six] = 6,
[Enum.KeyCode.Seven] = 7,
[Enum.KeyCode.Eight] = 8,
[Enum.KeyCode.Nine] = 9,
[Enum.KeyCode.Zero] = 10,
}
local colors = {
base = Color3.fromRGB(30, 30, 46),
slot = Color3.fromRGB(17, 17, 27),
stroke = Color3.fromRGB(88, 91, 112),
selectedStroke = Color3.fromRGB(137, 180, 250),
text = Color3.fromRGB(205, 214, 244),
subtext = Color3.fromRGB(166, 173, 200),
}
local function isTextInputFocused(): boolean
if UIS:GetFocusedTextBox() then
return true
end
local config = TextChatService:FindFirstChildOfClass("ChatInputBarConfiguration")
return config ~= nil and config.IsFocused
end
local function buildHotbarIds(): {string}
local ids = {}
local names = {}
for _, block in ipairs(blocksFolder:GetChildren()) do
local id = block:GetAttribute("n")
if id ~= nil then
local idStr = tostring(id)
table.insert(ids, idStr)
names[idStr] = block:GetAttribute("displayName") or block:GetAttribute("dn") or block.Name
end
end
table.sort(ids, function(a, b)
local na = tonumber(a)
local nb = tonumber(b)
if na and nb then
return na < nb
end
return a < b
end)
local slots = table.create(HOTBAR_SIZE)
for i = 1, HOTBAR_SIZE do
slots[i] = ids[i] or ""
end
return slots, names
end
local function ensurePreviewRig(part: Instance)
for _, descendant in ipairs(part:GetDescendants()) do
if descendant:IsA("BasePart") then
descendant.Anchored = true
descendant.CanCollide = false
end
end
if part:IsA("BasePart") then
part.Anchored = true
part.CanCollide = false
end
end
local function updateViewport(viewport: ViewportFrame, blockId: string)
viewport:ClearAllChildren()
if blockId == "" then
return
end
local camera = Instance.new("Camera")
camera.Parent = viewport
viewport.CurrentCamera = camera
local world = Instance.new("WorldModel")
world.Parent = viewport
local resolvedId = tonumber(blockId) or blockId
local preview = BlockManager:GetBlock(resolvedId)
preview.Parent = world
ensurePreviewRig(preview)
local cf, size
if preview:IsA("BasePart") then
cf = preview.CFrame
size = preview.Size
else
cf, size = preview:GetBoundingBox()
end
local maxSize = math.max(size.X, size.Y, size.Z)
local distance = maxSize * 1.8
local target = cf.Position
camera.CFrame = CFrame.new(target + Vector3.new(distance, distance, distance), target)
preview:PivotTo(CFrame.new())
end
local Hotbar = Roact.Component:extend("Hotbar")
function Hotbar:init()
self.state = {
slots = nil,
names = nil,
selected = 1,
}
local slots, names = buildHotbarIds()
self.state.slots = slots
self.state.names = names
self._updateSlots = function()
local nextSlots, nextNames = buildHotbarIds()
self:setState({
slots = nextSlots,
names = nextNames,
})
end
self._setSelected = function(slot: number)
if slot < 1 or slot > HOTBAR_SIZE then
return
end
self:setState({
selected = slot,
})
local id = self.state.slots and self.state.slots[slot] or ""
local name = ""
if id ~= "" and self.state.names then
name = self.state.names[id] or id
end
PlacementState:SetSelected(id, name)
end
self._handleInput = function(input: InputObject, gameProcessedEvent: boolean)
if gameProcessedEvent or isTextInputFocused() then
return
end
local slot = keyToSlot[input.KeyCode]
if slot then
self._setSelected(slot)
return
end
if input.UserInputType == Enum.UserInputType.MouseButton1 then
local mouseBlock = PM:GetBlockAtMouse()
if not mouseBlock then
return
end
PM:BreakBlock(
mouseBlock.chunk.X,
mouseBlock.chunk.Y,
mouseBlock.chunk.Z,
mouseBlock.block.X,
mouseBlock.block.Y,
mouseBlock.block.Z
)
elseif input.UserInputType == Enum.UserInputType.MouseButton2 then
local mouseBlock = PM:GetPlacementAtMouse()
if not mouseBlock then
return
end
local id = PlacementState:GetSelected()
if not id or id == "" then
return
end
PM:PlaceBlock(
mouseBlock.chunk.X,
mouseBlock.chunk.Y,
mouseBlock.chunk.Z,
mouseBlock.block.X,
mouseBlock.block.Y,
mouseBlock.block.Z,
id
)
end
end
self._handleScroll = function(input: InputObject, gameProcessedEvent: boolean)
if gameProcessedEvent or isTextInputFocused() then
return
end
if input.UserInputType ~= Enum.UserInputType.MouseWheel then
return
end
local direction = input.Position.Z
if direction == 0 then
return
end
local delta = direction > 0 and -1 or 1
local nextSlot = math.clamp(self.state.selected + delta, 1, HOTBAR_SIZE)
if nextSlot ~= self.state.selected then
self._setSelected(nextSlot)
end
end
self._viewportRefs = {}
self._viewportState = {}
end
function Hotbar:didMount()
self._connections = {
blocksFolder.ChildAdded:Connect(self._updateSlots),
blocksFolder.ChildRemoved:Connect(self._updateSlots),
UIS.InputBegan:Connect(self._handleInput),
UIS.InputChanged:Connect(self._handleScroll),
}
self:_refreshViewports()
-- initialize selection broadcast
local id = self.state.slots and self.state.slots[self.state.selected] or ""
local name = ""
if id ~= "" and self.state.names then
name = self.state.names[id] or id
end
PlacementState:SetSelected(id, name)
end
function Hotbar:willUnmount()
for _, conn in ipairs(self._connections or {}) do
conn:Disconnect()
end
self._connections = nil
end
function Hotbar:didUpdate(prevProps, prevState)
if prevState.slots ~= self.state.slots then
self:_refreshViewports()
end
end
function Hotbar:_refreshViewports()
for i = 1, HOTBAR_SIZE do
local viewport = self._viewportRefs[i]
if viewport then
local id = self.state.slots[i] or ""
if self._viewportState[i] ~= id then
self._viewportState[i] = id
updateViewport(viewport, id)
end
end
end
end
function Hotbar:render()
local slotElements = {}
local selectedId = self.state.slots[self.state.selected] or ""
local selectedName = ""
if selectedId ~= "" and self.state.names then
selectedName = self.state.names[selectedId] or selectedId
end
for i = 1, HOTBAR_SIZE do
local id = self.state.slots[i] or ""
local isSelected = i == self.state.selected
slotElements[`Slot{i-1}`] = Roact.createElement("TextButton", {
Size = UDim2.fromOffset(50, 50),
BackgroundColor3 = colors.slot,
BorderSizePixel = 0,
AutoButtonColor = false,
ClipsDescendants = true,
Text = "",
LayoutOrder = i,
[Roact.Event.Activated] = function()
self._setSelected(i)
end,
}, {
Corner = Roact.createElement("UICorner", {
CornerRadius = UDim.new(0, 13),
}),
Stroke = Roact.createElement("UIStroke", {
Color = isSelected and colors.selectedStroke or colors.stroke,
Thickness = isSelected and 2 or 1,
ApplyStrokeMode = Enum.ApplyStrokeMode.Border
}),
Preview = Roact.createElement("ViewportFrame", {
BackgroundTransparency = 1,
Size = UDim2.new(1, 0, 1, 0),
BorderSizePixel = 0,
[Roact.Ref] = function(r)
self._viewportRefs[i] = r
end,
}),
IndexLabel = Roact.createElement("TextLabel", {
BackgroundTransparency = 1,
Position = UDim2.fromOffset(4, 2),
Size = UDim2.fromOffset(18, 14),
Font = Enum.Font.Gotham,
Text = i == 10 and "0" or tostring(i),
TextColor3 = colors.subtext,
TextSize = 12,
TextXAlignment = Enum.TextXAlignment.Left,
TextYAlignment = Enum.TextYAlignment.Top,
}),
IdLabel = Roact.createElement("TextLabel", {
BackgroundTransparency = 1,
Position = UDim2.fromOffset(4, 26),
Size = UDim2.new(1, -8, 0, 18),
Font = Enum.Font.GothamBold,
Text = id,
TextColor3 = colors.text,
TextSize = 15,
TextWrapped = true,
TextXAlignment = Enum.TextXAlignment.Left,
TextYAlignment = Enum.TextYAlignment.Bottom,
}),
})
end
local hotbarFrame = Roact.createElement("Frame", {
AnchorPoint = Vector2.new(0.5, 1),
AutomaticSize = Enum.AutomaticSize.X,
BackgroundColor3 = colors.base,
BorderSizePixel = 0,
Position = UDim2.new(0.5, 0, 1, -20),
Size = UDim2.fromOffset(0, 58),
}, {
Corner = Roact.createElement("UICorner", {
CornerRadius = UDim.new(0, 16),
}),
Stroke = Roact.createElement("UIStroke", {
Color = colors.selectedStroke,
Thickness = 2,
ApplyStrokeMode = Enum.ApplyStrokeMode.Border
}),
Padding = Roact.createElement("UIPadding", {
PaddingLeft = UDim.new(0, 5),
PaddingRight = UDim.new(0, 5),
PaddingTop = UDim.new(0, 5),
PaddingBottom = UDim.new(0, 5),
}),
Slots = Roact.createElement("Frame", {
AutomaticSize = Enum.AutomaticSize.X,
BackgroundTransparency = 1,
Size = UDim2.new(0, 0, 1, 0),
}, {
Layout = Roact.createElement("UIListLayout", {
FillDirection = Enum.FillDirection.Horizontal,
HorizontalAlignment = Enum.HorizontalAlignment.Center,
VerticalAlignment = Enum.VerticalAlignment.Center,
Padding = UDim.new(0, 5),
}),
Slots = Roact.createFragment(slotElements),
}),
})
local selectedNameFrame = Roact.createElement("Frame", {
AnchorPoint = Vector2.new(0.5, 1),
AutomaticSize = Enum.AutomaticSize.X,
BackgroundColor3 = colors.base,
BorderSizePixel = 0,
Position = UDim2.new(0.5, 0, 1, -80-10),
Size = UDim2.fromOffset(0, 25),
}, {
Corner = Roact.createElement("UICorner", {
CornerRadius = UDim.new(0, 8),
}),
Stroke = Roact.createElement("UIStroke", {
Color = colors.selectedStroke,
Thickness = 2,
ApplyStrokeMode = Enum.ApplyStrokeMode.Border
}),
Padding = Roact.createElement("UIPadding", {
PaddingLeft = UDim.new(0, 18),
PaddingRight = UDim.new(0, 18),
PaddingTop = UDim.new(0, 2),
PaddingBottom = UDim.new(0, 2),
}),
Label = Roact.createElement("TextLabel", {
BackgroundTransparency = 1,
Size = UDim2.new(1, 0, 1, 0),
Font = Enum.Font.JosefinSans,
RichText = true,
Text = selectedName ~= "" and selectedName or " ",
TextColor3 = colors.text,
TextSize = 19,
TextWrapped = true,
AutomaticSize = Enum.AutomaticSize.X,
TextXAlignment = Enum.TextXAlignment.Center,
TextYAlignment = Enum.TextYAlignment.Center
}),
})
return Roact.createFragment({
Hotbar = hotbarFrame,
SelectedName = selectedNameFrame,
})
end
local handle = Roact.mount(Roact.createElement(Hotbar), script.Parent, "RoactHotbar")
script.AncestryChanged:Connect(function(_, parent)
if parent == nil then
Roact.unmount(handle)
end
end)

View File

@@ -0,0 +1,4 @@
{
"className": "ScreenGui",
"ignoreUnknownInstances": true
}

View File

@@ -1,95 +1,4 @@
--!native --!native
--!optimize 2 --!optimize 2
if not game:IsLoaded() then return
game.Loaded:Wait()
end
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local UIS = game:GetService("UserInputService")
ReplicatedStorage:WaitForChild("Objects"):WaitForChild("MLLoaded")
local blocksFolder = ReplicatedStorage:WaitForChild("Blocks")
local PM = require(ReplicatedStorage.Shared.PlacementManager)
local HOTBAR_SIZE = 9
local hotbar = table.create(HOTBAR_SIZE)
local selectedSlot = 1
local keyToSlot = {
[Enum.KeyCode.One] = 1,
[Enum.KeyCode.Two] = 2,
[Enum.KeyCode.Three] = 3,
[Enum.KeyCode.Four] = 4,
[Enum.KeyCode.Five] = 5,
[Enum.KeyCode.Six] = 6,
[Enum.KeyCode.Seven] = 7,
[Enum.KeyCode.Eight] = 8,
[Enum.KeyCode.Nine] = 9,
}
local function rebuildHotbar()
local ids = {}
for _, block in ipairs(blocksFolder:GetChildren()) do
local id = block:GetAttribute("n")
if id ~= nil then
table.insert(ids, tostring(id))
end
end
table.sort(ids)
for i = 1, HOTBAR_SIZE do
hotbar[i] = ids[i] or ""
end
selectedSlot = math.clamp(selectedSlot, 1, HOTBAR_SIZE)
end
local function getSelectedBlockId(): string?
local id = hotbar[selectedSlot]
if id == "" then
return nil
end
return id
end
local function setSelectedSlot(slot: number)
if slot < 1 or slot > HOTBAR_SIZE then
return
end
selectedSlot = slot
end
rebuildHotbar()
blocksFolder.ChildAdded:Connect(rebuildHotbar)
blocksFolder.ChildRemoved:Connect(rebuildHotbar)
UIS.InputBegan:Connect(function(input: InputObject, gameProcessedEvent: boolean)
if gameProcessedEvent then
return
end
local slot = keyToSlot[input.KeyCode]
if slot then
setSelectedSlot(slot)
return
end
if input.UserInputType == Enum.UserInputType.MouseButton1 then
local mouseBlock = PM:GetBlockAtMouse()
if not mouseBlock then
return
end
PM:BreakBlock(mouseBlock.chunk.X, mouseBlock.chunk.Y, mouseBlock.chunk.Z, mouseBlock.block.X, mouseBlock.block.Y, mouseBlock.block.Z)
elseif input.UserInputType == Enum.UserInputType.MouseButton2 then
local mouseBlock = PM:GetPlacementAtMouse()
if not mouseBlock then
return
end
local blockId = getSelectedBlockId()
if not blockId then
return
end
PM:PlaceBlock(mouseBlock.chunk.X, mouseBlock.chunk.Y, mouseBlock.chunk.Z, mouseBlock.block.X, mouseBlock.block.Y, mouseBlock.block.Z, blockId)
end
end)

View File

@@ -10,4 +10,9 @@ dependencies = []
[[package]] [[package]]
name = "ocbwoy3-development-studios/minecraft-roblox" name = "ocbwoy3-development-studios/minecraft-roblox"
version = "0.1.0" version = "0.1.0"
dependencies = [["cmdr", "evaera/cmdr@1.12.0"]] dependencies = [["cmdr", "evaera/cmdr@1.12.0"], ["roact", "roblox/roact@1.4.4"]]
[[package]]
name = "roblox/roact"
version = "1.4.4"
dependencies = []

View File

@@ -5,4 +5,5 @@ registry = "https://github.com/UpliftGames/wally-index"
realm = "shared" realm = "shared"
[dependencies] [dependencies]
cmdr = "evaera/cmdr@1.12.0" cmdr = "evaera/cmdr@1.12.0"
roact = "roblox/roact@1.4.4"