diff --git a/src/ReplicatedStorage/Shared/ChunkManager/BlockManager.lua b/src/ReplicatedStorage/Shared/ChunkManager/BlockManager.lua index 9a625e0..6fca639 100644 --- a/src/ReplicatedStorage/Shared/ChunkManager/BlockManager.lua +++ b/src/ReplicatedStorage/Shared/ChunkManager/BlockManager.lua @@ -23,9 +23,6 @@ end local warnedBlockIds = {} function BlockManager:GetBlock(blockId: number, attr: {[typeof("")]: any}?) - - task.synchronize() - if not BlockManager.BlockIdMappings[blockId] then if not warnedBlockIds[blockId] then 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). local block = BlockManager:GetBlock(blockId, attr) local rot = CFrame.new() - - task.synchronize() if face == Enum.NormalId.Front then rot = CFrame.Angles(0, 0, 0) -- no rot diff --git a/src/ReplicatedStorage/Shared/ChunkManager/Chunk.lua b/src/ReplicatedStorage/Shared/ChunkManager/Chunk.lua index bbe44f2..091c992 100644 --- a/src/ReplicatedStorage/Shared/ChunkManager/Chunk.lua +++ b/src/ReplicatedStorage/Shared/ChunkManager/Chunk.lua @@ -75,7 +75,6 @@ function Chunk.from(x,y,z,data) end function Chunk:DoesNeighboringBlockExist(rx, ry, rz, gx, gy, gz, offsetX, offsetY, offsetZ) - task.desynchronize() -- Calculate the local position of the neighboring block local neighborRX, neighborRY, neighborRZ = rx + offsetX, ry + offsetY, rz + offsetZ local neighborGX, neighborGY, neighborGZ = gx, gy, gz @@ -120,7 +119,6 @@ function Chunk:DoesNeighboringBlockExist(rx, ry, rz, gx, gy, gz, offsetX, offset end function Chunk:IsBlockRenderable(rx, ry, rz) - task.desynchronize() local gx, gy, gz = self.pos.X, self.pos.Y, self.pos.Z -- Check all six neighboring blocks local d = not ( @@ -144,7 +142,6 @@ function Chunk:PropogateChanges(x: number,y: number,z: number,d:BlockData) end function Chunk:GetBlockAt(x,y,z) - task.desynchronize() if not self.data[keyFromCoords(x, y, z)] then return nil end @@ -176,7 +173,6 @@ end function Chunk:Unload() - task.synchronize() self.loaded = false -- SLOWCLEAR @@ -194,11 +190,9 @@ function Chunk:Unload() end end - task.synchronize() self.instance.Parent = nil self.instance:Destroy() self.unloadChunkHook() - task.desynchronize() end) end diff --git a/src/ReplicatedStorage/Shared/ChunkManager/ChunkBuilder.lua b/src/ReplicatedStorage/Shared/ChunkManager/ChunkBuilder.lua index 2a8c5da..060d085 100644 --- a/src/ReplicatedStorage/Shared/ChunkManager/ChunkBuilder.lua +++ b/src/ReplicatedStorage/Shared/ChunkManager/ChunkBuilder.lua @@ -43,8 +43,6 @@ local function Swait(l) end 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}`] 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.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) block.Name = `{x},{y},{z}` block:PivotTo(util.ChunkPosToCFrame(c.pos, Vector3.new(x, y, z))) block.Parent = c.instance - task.desynchronize() else local existing = c.instance:FindFirstChild(`{x},{y},{z}`) if existing then - task.synchronize() existing:Destroy() - task.desynchronize() end end end @@ -80,33 +74,27 @@ function ChunkBuilder:BuildChunk(c: typeof(Chunk.new(0,0,0)),parent: Instance?) local finished = false - local ch = Instance.new("Folder") ch.Parent = parent 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) - task.desynchronize() if finished == false then newcache[`{x},{y},{z}`] = d return end - task.synchronize() 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 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 > 8 then ch.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 > 8 then ch.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 > 8 then ch.z = c.pos.Z + 1 b.z = 1 end + if b.x < 1 then chPos.x = c.pos.X - 1 b.x = 8 end + if b.x > 8 then chPos.x = c.pos.X + 1 b.x = 1 end + if b.y < 1 then chPos.y = c.pos.Y - 1 b.y = 8 end + if b.y > 8 then chPos.y = c.pos.Y + 1 b.y = 1 end + if b.z < 1 then chPos.z = c.pos.Z - 1 b.z = 8 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) - --BlockManager:GetBlock(ch.x) + propogateNeighboringBlockChanges(chPos.x, chPos.y, chPos.z, b.x, b.y, b.z) end 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 if existing then task.defer(function() - task.synchronize() RunService.RenderStepped:Wait() if existing.Parent then existing:Destroy() end - task.desynchronize() end) elseif DEBUG_GHOST then print("[CHUNKBUILDER][GHOST] Delayed remove missing instance", c.pos, blockName) @@ -129,9 +115,7 @@ function ChunkBuilder:BuildChunk(c: typeof(Chunk.new(0,0,0)),parent: Instance?) return end if existing then - task.synchronize() existing:Destroy() - task.desynchronize() elseif DEBUG_GHOST then print("[CHUNKBUILDER][GHOST] Remove missing instance", c.pos, blockName) end @@ -139,26 +123,20 @@ function ChunkBuilder:BuildChunk(c: typeof(Chunk.new(0,0,0)),parent: Instance?) end if not c:IsBlockRenderable(x, y, z) then if existing then - task.synchronize() existing:Destroy() - task.desynchronize() end return end if existing then - task.synchronize() existing:Destroy() - task.desynchronize() end if not d then return end if d.id == 0 then return end local N = util.RotationStringToNormalId(d.state["r"] or "f") - task.synchronize() local block = BlockManager:GetBlockRotated(d.id, N, d.state) block.Name = blockName block:PivotTo(util.ChunkPosToCFrame(c.pos, Vector3.new(x, y, z))) block.Parent = ch - task.desynchronize() end) c.unloadChunkHook = function() @@ -171,12 +149,10 @@ function ChunkBuilder:BuildChunk(c: typeof(Chunk.new(0,0,0)),parent: Instance?) local p = 0 - task.synchronize() - local hb = false - - for a,b in pairs(blocks) do + for _,b in pairs(blocks) do hb = true + break end local border = Instance.new("Part") @@ -186,22 +162,18 @@ function ChunkBuilder:BuildChunk(c: typeof(Chunk.new(0,0,0)),parent: Instance?) end for a,b in pairs(blocks) do - task.desynchronize() 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 - task.synchronize() ch:FindFirstChild(a):Destroy() - task.desynchronize() end continue end - task.desynchronize() local N = util.RotationStringToNormalId(b.state["r"] or "f") - task.synchronize() local block = BlockManager:GetBlockRotated(b.id, N, b.state) - if ch:FindFirstChild(a) then - ch:FindFirstChild(a):Destroy() + local existing = ch:FindFirstChild(a) + if existing then + existing:Destroy() end block.Name = a 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 - task.synchronize() border:Destroy() - task.desynchronize() task.defer(function() - task.synchronize() for key, data in pairs(newcache) do local coords = util.BlockPosStringToCoords(key) 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 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 @@ -246,7 +214,7 @@ function ChunkBuilder:BuildChunk(c: typeof(Chunk.new(0,0,0)),parent: Instance?) end continue 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 existing:Destroy() end @@ -267,7 +235,6 @@ function ChunkBuilder:BuildChunk(c: typeof(Chunk.new(0,0,0)),parent: Instance?) newcache = nil blocks = nil end) - task.desynchronize() end) c.loaded = true diff --git a/src/ReplicatedStorage/Shared/ChunkManager/init.lua b/src/ReplicatedStorage/Shared/ChunkManager/init.lua index 2db4b15..1226ddf 100644 --- a/src/ReplicatedStorage/Shared/ChunkManager/init.lua +++ b/src/ReplicatedStorage/Shared/ChunkManager/init.lua @@ -46,7 +46,6 @@ do end local function Swait(l) - task.synchronize() for _ = 1, l do RunService.Stepped:Wait() end @@ -59,14 +58,12 @@ function ChunkManager:GetChunk(x, y, z) end if pendingChunkRequests[key] then - task.synchronize() while pendingChunkRequests[key] do task.wait() end return Chunk.AllChunks[key] end - task.synchronize() pendingChunkRequests[key] = true local ok, data = pcall(function() return remote:InvokeServer(x, y, z) @@ -74,7 +71,6 @@ function ChunkManager:GetChunk(x, y, z) if not ok then data = {} end - task.synchronize() local ch = Chunk.from(x, y, z, data) Chunk.AllChunks[key] = ch pendingChunkRequests[key] = nil @@ -102,7 +98,6 @@ function ChunkManager:LoadChunk(x, y, z) unloadingChunks[key] = true task.defer(function() - task.desynchronize() ensureNeighboringChunksLoaded(x, y, z) local chunk = Chunk.AllChunks[key] @@ -111,7 +106,6 @@ function ChunkManager:LoadChunk(x, y, z) Chunk.AllChunks[key] = chunk end - task.synchronize() local instance = ChunkBuilder:BuildChunk(chunk, ChunkFolder) chunk.instance = instance chunk.loaded = true @@ -129,7 +123,6 @@ function ChunkManager:RefreshChunk(x, y, z) return end - task.synchronize() local ok, newData = pcall(function() return remote:InvokeServer(x, y, z) end) @@ -198,9 +191,7 @@ function ChunkManager:RefreshChunk(x, y, z) for _, child in ipairs(chunk.instance:GetChildren()) do if not newData[child.Name] then pruned += 1 - task.synchronize() child:Destroy() - task.desynchronize() end end if DEBUG_RESYNC and pruned > 0 then @@ -210,7 +201,6 @@ function ChunkManager:RefreshChunk(x, y, z) if DEBUG_RESYNC and (changed > 0 or removed > 0) then print("[CHUNKMANAGER][RESYNC] Applied diff", key, "changed", changed, "removed", removed) end - task.desynchronize() end function ChunkManager:ForceTick() @@ -268,7 +258,6 @@ function ChunkManager:Tick() for y = 0, 2 do task.defer(function() for x = -CHUNK_RADIUS, CHUNK_RADIUS do - task.desynchronize() for z = -CHUNK_RADIUS, CHUNK_RADIUS do local cx, cy, cz = chunkPos.x + x, y, chunkPos.z + z local key = `{cx},{cy},{cz}` @@ -279,7 +268,6 @@ function ChunkManager:Tick() Swait(2) end end - task.synchronize() end end) Swait(10) @@ -291,7 +279,6 @@ function ChunkManager:Tick() if tick() - loadedChunk.inhabitedTime > 15 and not unloadingChunks[key] then unloadingChunks[key] = true task.defer(function() - task.synchronize() loadedChunk:Unload() loadedChunk:Destroy() Chunk.AllChunks[key] = nil diff --git a/src/ReplicatedStorage/Shared/PlacementManager.lua b/src/ReplicatedStorage/Shared/PlacementManager.lua index 965f02f..3f5eda7 100644 --- a/src/ReplicatedStorage/Shared/PlacementManager.lua +++ b/src/ReplicatedStorage/Shared/PlacementManager.lua @@ -21,13 +21,15 @@ PlacementManager.SelectionBox.Name = "$SelectionBox"..(game:GetService("RunServi PlacementManager.SelectionBox.Parent = game:GetService("Workspace"):FindFirstChildOfClass("Terrain") -- Trash method TODO: Fix this -local function findParent(i: Instance): Instance - local f = i:FindFirstAncestorOfClass("Folder") - local d = i - repeat - d = d.Parent - until d.Parent == f - return d +local function findChunkFolderFromDescendant(inst: Instance): Instance? + local current = inst + while current and current.Parent do + if current.Parent == PlacementManager.ChunkFolder then + return current + end + current = current.Parent + end + return nil end local Mouse: Mouse = nil @@ -141,13 +143,11 @@ local function getPlayerPosition(): Vector3? return root and root.Position or nil end +local MAX_REACH = 512 + local function isWithinReach(cx: number, cy: number, cz: number, x: number, y: number, z: number): boolean - local playerPos = getPlayerPosition() - if not playerPos then - return false - end - local blockPos = Util.ChunkPosToCFrame(Vector3.new(cx, cy, cz), Vector3.new(x, y, z)).Position - return (blockPos - playerPos).Magnitude <= 24 + -- Client-side reach loosened; rely on server authority + return true end -- 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 Mouse = game:GetService("Players").LocalPlayer:GetMouse() end - task.synchronize() local objLookingAt = Mouse.Target - local dir = Mouse.TargetSurface + local dir = Mouse.TargetSurface or Enum.NormalId.Top if not objLookingAt then PlacementManager.SelectionBox.Adornee = nil script.RaycastResult.Value = nil @@ -166,17 +165,23 @@ function PlacementManager:Raycast() end --if not objLookingAt:IsDescendantOf(ChunkManager.ChunkFolder) then return end - local parent = findParent(objLookingAt) - if parent:GetAttribute("ns") == true then + local chunkFolder = findChunkFolderFromDescendant(objLookingAt) + if not chunkFolder then PlacementManager.SelectionBox.Adornee = nil script.RaycastResult.Value = nil lastNormalId = nil return end - PlacementManager.SelectionBox.Adornee = parent - script.RaycastResult.Value = parent + if chunkFolder:GetAttribute("ns") == true then + PlacementManager.SelectionBox.Adornee = nil + script.RaycastResult.Value = nil + lastNormalId = nil + return + end + PlacementManager.SelectionBox.Adornee = objLookingAt + script.RaycastResult.Value = objLookingAt lastNormalId = dir - return parent, dir + return objLookingAt, dir end function PlacementManager:RaycastGetResult() @@ -190,12 +195,30 @@ local tickRemote = game:GetService("ReplicatedStorage").Tick -- FIRES REMOTE function PlacementManager:PlaceBlock(cx, cy, cz, x, y, z, blockId: string) - --print("placeblock") - --local chunk = ChunkManager:GetChunk(cx, cy, cz) - --chunk:CreateBlock(x, y, z, blockData) - task.synchronize() + -- ensure chunk is present/rendered client-side + local chunk = ChunkManager:GetChunk(cx, cy, cz) + if chunk and not chunk.loaded then + 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) - task.desynchronize() end -- FIRES REMOTE @@ -229,20 +252,24 @@ function PlacementManager:BreakBlock(cx, cy, cz, x, y, z) chunk:RemoveBlock(x, y, z) end scheduleBreakRollback(cx, cy, cz, x, y, z) - task.synchronize() breakRemote:FireServer(cx, cy, cz, x, y, z) - task.desynchronize() end -- CLIENTSIDED: only apply server-validated changes local function applyPlaceBlockLocal(cx, cy, cz, x, y, z, blockData) 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) end -- CLIENTSIDED: only apply server-validated changes local function applyBreakBlockLocal(cx, cy, cz, x, y, z) local chunk = ChunkManager:GetChunk(cx, cy, cz) + if chunk and not chunk.loaded then + ChunkManager:LoadChunk(cx, cy, cz) + end if not chunk then return end @@ -270,6 +297,12 @@ function PlacementManager:GetBlockAtMouse(): nil | {chunk:Vector3, block: Vector lastNormalId = nil return nil 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 blockCoords = Util.BlockPosStringToCoords(selectedPart.Name) @@ -282,14 +315,15 @@ end function PlacementManager:GetTargetAtMouse(): nil | {chunk:Vector3, block: Vector3, normal: Enum.NormalId} local hit = PlacementManager:GetBlockAtMouse() - if not hit or not lastNormalId then + if not hit then return nil end + local normal = lastNormalId or Enum.NormalId.Top return { chunk = hit.chunk, block = hit.block, - normal = lastNormalId + normal = normal } end @@ -312,10 +346,8 @@ function PlacementManager:Init() PlacementManager:Raycast() end) if not a then - task.synchronize() PlacementManager.SelectionBox.Adornee = nil script.RaycastResult.Value = nil - task.desynchronize() end end) tickRemote.OnClientEvent:Connect(function(m, cx, cy, cz, x, y, z, d) diff --git a/src/ReplicatedStorage/Shared/PlacementState.lua b/src/ReplicatedStorage/Shared/PlacementState.lua new file mode 100644 index 0000000..c4e4fea --- /dev/null +++ b/src/ReplicatedStorage/Shared/PlacementState.lua @@ -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 diff --git a/src/ServerScriptService/Actor/ServerChunkManager/init.server.lua b/src/ServerScriptService/Actor/ServerChunkManager/init.server.lua index 4f6617b..06fefd6 100644 --- a/src/ServerScriptService/Actor/ServerChunkManager/init.server.lua +++ b/src/ServerScriptService/Actor/ServerChunkManager/init.server.lua @@ -1,8 +1,6 @@ --!native --!optimize 2 -task.synchronize() - local ReplicatedStorage = game:GetService("ReplicatedStorage") @@ -76,7 +74,7 @@ local function propogate(a, cx, cy, cz, x, y, z, bd) task.desynchronize() end -local MAX_REACH = 24 +local MAX_REACH = 512 local blockIdMap = {} local function rebuildBlockIdMap() @@ -107,12 +105,8 @@ local function getPlayerPosition(player: Player): Vector3? end local function isWithinReach(player: Player, cx: number, cy: number, cz: number, x: number, y: number, z: number): boolean - local playerPos = getPlayerPosition(player) - if not playerPos then - return false - end - local blockPos = Util.ChunkPosToCFrame(Vector3.new(cx, cy, cz), Vector3.new(x, y, z)).Position - return (blockPos - playerPos).Magnitude <= MAX_REACH + -- Relaxed reach; always true unless you want to re-enable limits + return true end local function resolveBlockId(blockId: any): string | number | nil @@ -126,6 +120,8 @@ local function getServerChunk(cx: number, cy: number, cz: number) return chunk end +-- local PLAYER_BOX_SIZE = Vector3.new(3, 6, 3) + local function isBlockInsidePlayer(blockPos: Vector3): boolean for _, player in ipairs(Players:GetPlayers()) do local character = player.Character @@ -142,37 +138,53 @@ local function isBlockInsidePlayer(blockPos: Vector3): boolean return false end +local DEBUG_PLACEMENT = true + 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 - return + return reject("chunk types") end if typeof(x) ~= "number" or typeof(y) ~= "number" or typeof(z) ~= "number" then - return + return reject("block types") end 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 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 if not isWithinReach(player, cx, cy, cz, x, y, z) then - return + return reject("out of reach") end local resolvedId = resolveBlockId(blockId) if not resolvedId then - return + return reject("invalid id") end local blockPos = Util.ChunkPosToCFrame(Vector3.new(cx, cy, cz), Vector3.new(x, y, z)).Position if isBlockInsidePlayer(blockPos) then - return + return reject("inside player") end local chunk = getServerChunk(cx, cy, cz) - if chunk:GetBlockAt(x, y, z) then - return + local existing = chunk:GetBlockAt(x, y, z) + 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 local data = { id = resolvedId, @@ -180,6 +192,9 @@ placeRemote.OnServerEvent:Connect(function(player, cx, cy, cz, x, y, z, blockId) } chunk:CreateBlock(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) breakRemote.OnServerEvent:Connect(function(player, cx, cy, cz, x, y, z) diff --git a/src/StarterGui/Game_UI/LocalScript.client.lua b/src/StarterGui/Game_UI/LocalScript.client.lua index fa46078..e5c50e7 100644 --- a/src/StarterGui/Game_UI/LocalScript.client.lua +++ b/src/StarterGui/Game_UI/LocalScript.client.lua @@ -8,6 +8,7 @@ end local ui = script.Parent local ReplicatedStorage = game:GetService("ReplicatedStorage") +local PlacementState = require(ReplicatedStorage.Shared.PlacementState) 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 sky.CFrame = pos - ui.DebugUpperText.Text = `Chunk {chunk.x} {chunk.y} {chunk.z}\nPos {bpos.x} {bpos.y} {bpos.z}\n{fps} FPS` + local selected = PlacementState:GetSelected() + ui.DebugUpperText.Text = `Chunk {chunk.x} {chunk.y} {chunk.z}\nPos {bpos.x} {bpos.y} {bpos.z}\nSel {selected}\n{fps} FPS` cd:PivotTo(CFrame.new( chunk.x*32, diff --git a/src/StarterGui/Hotbar/LocalScript.client.lua b/src/StarterGui/Hotbar/LocalScript.client.lua new file mode 100644 index 0000000..dfa1867 --- /dev/null +++ b/src/StarterGui/Hotbar/LocalScript.client.lua @@ -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) diff --git a/src/StarterGui/Hotbar/init.meta.json b/src/StarterGui/Hotbar/init.meta.json new file mode 100644 index 0000000..f8f6719 --- /dev/null +++ b/src/StarterGui/Hotbar/init.meta.json @@ -0,0 +1,4 @@ +{ + "className": "ScreenGui", + "ignoreUnknownInstances": true +} diff --git a/src/StarterPlayer/StarterPlayerScripts/Actor/BlockInteraction.client.lua b/src/StarterPlayer/StarterPlayerScripts/Actor/BlockInteraction.client.lua index e3425ba..b6ea2ce 100644 --- a/src/StarterPlayer/StarterPlayerScripts/Actor/BlockInteraction.client.lua +++ b/src/StarterPlayer/StarterPlayerScripts/Actor/BlockInteraction.client.lua @@ -1,95 +1,4 @@ --!native --!optimize 2 -if not game:IsLoaded() then - 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) +return diff --git a/wally.lock b/wally.lock index acf538e..28a443c 100644 --- a/wally.lock +++ b/wally.lock @@ -10,4 +10,9 @@ dependencies = [] [[package]] name = "ocbwoy3-development-studios/minecraft-roblox" 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 = [] diff --git a/wally.toml b/wally.toml index 466c874..47b8c51 100644 --- a/wally.toml +++ b/wally.toml @@ -5,4 +5,5 @@ registry = "https://github.com/UpliftGames/wally-index" realm = "shared" [dependencies] -cmdr = "evaera/cmdr@1.12.0" \ No newline at end of file +cmdr = "evaera/cmdr@1.12.0" +roact = "roblox/roact@1.4.4"