···117117 return
118118 end
119119 local hotbar = replicaForPlayer.Data.hotbar
120120- if not hotbar or not hotbar[slot] then
120120+ if not hotbar then
121121 return
122122 end
123123- replicaForPlayer:FireServer("SelectHotbarSlot", slot)
123123+ if slot and slot >= 1 and slot <= HOTBAR_SIZE then
124124+ replicaForPlayer:FireServer("SelectHotbarSlot", slot)
125125+ end
124126end
125127126128ClientState.Changed = changed.Event
+53-25
ReplicatedStorage/Shared/PlacementManager.lua
···7979 if current:IsA("BasePart") then
8080 return current
8181 end
8282- current = current.Parent
8282+ current = current.Parent
8383 end
8484 return nil
8585end
···265265end
266266267267-- Gets the block and normalid of the block (and surface) the player is looking at
268268-function PlacementManager:Raycast()
268268+function PlacementManager:Raycast(skipSelection: boolean?)
269269 if not Mouse then
270270 Mouse = game:GetService("Players").LocalPlayer:GetMouse()
271271 end
272272 local chunkFolder = ensureChunkFolder()
273273 if not chunkFolder then
274274- clearSelection("chunk folder missing")
274274+ if not skipSelection then
275275+ clearSelection("chunk folder missing")
276276+ end
275277 script.RaycastResult.Value = nil
276278 return
277279 end
···285287 local ray = Mouse.UnitRay
286288 local result = workspace:Raycast(ray.Origin, ray.Direction * MAX_REACH, raycastParams)
287289 if not result then
288288- clearSelection("raycast miss")
290290+ if not skipSelection then
291291+ clearSelection("raycast miss")
292292+ end
289293 script.RaycastResult.Value = nil
290294 debugPlacementLog("[PLACE][CLIENT][RAYCAST]", "miss")
291295 return
···293297294298 local objLookingAt = result.Instance
295299 if not objLookingAt then
296296- clearSelection("raycast nil instance")
300300+ if not skipSelection then
301301+ clearSelection("raycast nil instance")
302302+ end
297303 script.RaycastResult.Value = nil
298304 debugPlacementWarn("[PLACE][CLIENT][RAYCAST]", "nil instance in result")
299305 return
···308314 "parent",
309315 objLookingAt.Parent and objLookingAt.Parent:GetFullName() or "nil"
310316 )
311311- clearSelection("target not in chunk folder")
317317+ if not skipSelection then
318318+ clearSelection("target not in chunk folder")
319319+ end
312320 script.RaycastResult.Value = nil
313321 return
314322 end
···318326 "chunk flagged ns",
319327 hitChunkFolder:GetFullName()
320328 )
321321- clearSelection("target chunk marked ns")
329329+ if not skipSelection then
330330+ clearSelection("target chunk marked ns")
331331+ end
322332 script.RaycastResult.Value = nil
323333 return
324334 end
···327337 local blockRoot = findBlockRoot(objLookingAt, chunkFolder) or objLookingAt
328338 local chunkName, blockName = findChunkAndBlock(blockRoot)
329339 if not chunkName or not blockName then
330330- clearSelection("failed to resolve chunk/block")
340340+ if not skipSelection then
341341+ clearSelection("failed to resolve chunk/block")
342342+ end
331343 script.RaycastResult.Value = nil
332344 return
333345 end
···338350 return Util.BlockPosStringToCoords(blockName)
339351 end)
340352 if not okChunk or not okBlock then
341341- clearSelection("failed to parse chunk/block names")
353353+ if not skipSelection then
354354+ clearSelection("failed to parse chunk/block names")
355355+ end
342356 script.RaycastResult.Value = nil
343357 return
344358 end
···347361348362 -- block is being optimistically broken, do not highlight it
349363 if getPendingBreak(chunkKey, blockKey) then
350350- clearSelection("block pending break")
364364+ if not skipSelection then
365365+ clearSelection("block pending break")
366366+ end
351367 script.RaycastResult.Value = nil
352368 return
353369 end
···356372 local chunk = ChunkManager:GetChunk(chunkCoords.X, chunkCoords.Y, chunkCoords.Z)
357373 local blockData = chunk and chunk:GetBlockAt(blockCoords.X, blockCoords.Y, blockCoords.Z)
358374 if not blockData or blockData == 0 or blockData.id == 0 then
359359- clearSelection("block missing/air")
375375+ if not skipSelection then
376376+ clearSelection("block missing/air")
377377+ end
360378 script.RaycastResult.Value = nil
361379 return
362380 end
363381 local blockInstance = resolveBlockInstance(chunkFolder, chunkName, blockName) or blockRoot
364382 if not blockInstance then
365365- clearSelection("missing block instance")
383383+ if not skipSelection then
384384+ clearSelection("missing block instance")
385385+ end
366386 script.RaycastResult.Value = nil
367387 return
368388 end
369389370390 lastRaycastFailure = nil
371371- if lastSelectedChunkKey ~= chunkKey or lastSelectedBlockKey ~= blockKey then
372372- setSelection(blockInstance, PlacementManager.ChunkFolder)
373373- lastSelectedChunkKey = chunkKey
374374- lastSelectedBlockKey = blockKey
391391+ if not skipSelection then
392392+ if lastSelectedChunkKey ~= chunkKey or lastSelectedBlockKey ~= blockKey then
393393+ setSelection(blockInstance, PlacementManager.ChunkFolder)
394394+ lastSelectedChunkKey = chunkKey
395395+ lastSelectedBlockKey = blockKey
396396+ end
375397 end
376398 script.RaycastResult.Value = objLookingAt
377399 lastNormalId = vectorToNormalId(result.Normal)
···400422-- FIRES REMOTE
401423function PlacementManager:PlaceBlock(cx, cy, cz, x, y, z, blockId: string)
402424 debugPlacementLog("[PLACE][CLIENT][PLACE_CALL]", "chunk", cx, cy, cz, "block", x, y, z, "blockId", blockId)
425425+ if blockId == "hand" then
426426+ debugPlacementWarn("[PLACE][CLIENT][REJECT]", "hand cannot place")
427427+ return
428428+ end
403429 if typeof(cx) ~= "number" or typeof(cy) ~= "number" or typeof(cz) ~= "number" then
404430 debugPlacementWarn("[PLACE][CLIENT][REJECT]", "chunk type", cx, cy, cz, x, y, z, blockId)
405431 return
···551577 chunk:RemoveBlock(x, y, z)
552578end
553579554554-function PlacementManager:GetBlockAtMouse(): nil | {chunk:Vector3, block: Vector3}
580580+function PlacementManager:GetBlockAtMouse(skipSelection: boolean?): nil | {chunk:Vector3, block: Vector3}
555581 pcall(function()
556556- PlacementManager:Raycast()
582582+ PlacementManager:Raycast(skipSelection)
557583 end)
558584 local selectedPart = PlacementManager:RaycastGetResult()
559585 --print(selectedPart and selectedPart:GetFullName() or nil)
560586 if selectedPart == nil then
561561- clearSelection()
587587+ if not skipSelection then
588588+ clearSelection()
589589+ end
562590 script.RaycastResult.Value = nil
563591 debugPlacementLog("[PLACE][CLIENT][TARGET]", "no selectedPart after raycast", lastRaycastFailure)
564592 return nil
···607635608636end
609637610610-function PlacementManager:GetTargetAtMouse(): nil | {chunk:Vector3, block: Vector3, normal: Enum.NormalId}
611611- local hit = PlacementManager:GetBlockAtMouse()
638638+function PlacementManager:GetTargetAtMouse(skipSelection: boolean?): nil | {chunk:Vector3, block: Vector3, normal: Enum.NormalId}
639639+ local hit = PlacementManager:GetBlockAtMouse(skipSelection)
612640 if not hit then
613641 return nil
614642 end
···621649 }
622650end
623651624624-function PlacementManager:GetPlacementAtMouse(): nil | {chunk:Vector3, block: Vector3}
625625- local hit = PlacementManager:GetTargetAtMouse()
652652+function PlacementManager:GetPlacementAtMouse(skipSelection: boolean?): nil | {chunk:Vector3, block: Vector3}
653653+ local hit = PlacementManager:GetTargetAtMouse(skipSelection)
626654 if not hit then
627655 return nil
628656 end
···647675 }
648676end
649677650650-function PlacementManager:DebugGetPlacementOrWarn()
651651- local placement = PlacementManager:GetPlacementAtMouse()
678678+function PlacementManager:DebugGetPlacementOrWarn(skipSelection: boolean?)
679679+ local placement = PlacementManager:GetPlacementAtMouse(skipSelection)
652680 if not placement then
653681 debugPlacementWarn("[PLACE][CLIENT][REJECT]", "no placement target under mouse", lastRaycastFailure)
654682 end
+1-4
ServerScriptService/Actor/ClientState.lua
···8181 if selectedSlot < 1 or selectedSlot > HOTBAR_SIZE then
8282 return (#hotbar > 0) and 1 or 0
8383 end
8484- if not hotbar[selectedSlot] then
8585- return (#hotbar > 0) and 1 or 0
8686- end
8784 return selectedSlot
8885end
8986···138135 if not hotbar then
139136 return
140137 end
141141- if slot and slot >= 1 and slot <= HOTBAR_SIZE and hotbar[slot] then
138138+ if slot and slot >= 1 and slot <= HOTBAR_SIZE then
142139 replica:Set({"selectedSlot"}, slot)
143140 end
144141 end
+23-20
StarterGui/Hotbar/LocalScript.client.lua
···5353end
54545555local function resolveSelectedSlot(slots, desired)
5656- if desired and desired >= 1 and desired <= HOTBAR_SIZE and slots[desired] ~= "" then
5656+ if desired and desired >= 1 and desired <= HOTBAR_SIZE then
5757 return desired
5858 end
5959 for i = 1, HOTBAR_SIZE do
···146146 names = nextNames,
147147 selected = nextSelected,
148148 })
149149- local id = nextSlots[nextSelected] or ""
149149+ local rawId = nextSlots[nextSelected] or ""
150150+ local effectiveId = rawId ~= "" and rawId or "hand"
150151 local name = ""
151151- if id ~= "" then
152152- name = nextNames[id] or id
152152+ if rawId ~= "" then
153153+ name = nextNames[rawId] or rawId
153154 end
154154- PlacementState:SetSelected(id, name)
155155+ PlacementState:SetSelected(effectiveId, name)
155156 end
156157157158 self._setSelected = function(slot: number)
158159 if slot < 1 or slot > HOTBAR_SIZE then
159160 return
160161 end
161161- local info = ClientState:GetSlotInfo(slot)
162162- if not info then
163163- return
164164- end
165162 ClientState:SetSelectedSlot(slot)
166163 self:setState({
167164 selected = slot,
168165 })
169169- local id = tostring(info.id)
170170- local name = info.name or id
171171- Util.StudioLog("[PLACE][CLIENT][SELECT]", "slot", slot, "id", id, "name", name)
172172- PlacementState:SetSelected(id, name)
166166+ local rawId = self.state.slots[slot] or ""
167167+ local effectiveId = rawId ~= "" and rawId or "hand"
168168+ local name = ""
169169+ if rawId ~= "" then
170170+ name = self.state.names[rawId] or rawId
171171+ end
172172+ Util.StudioLog("[PLACE][CLIENT][SELECT]", "slot", slot, "id", effectiveId, "name", name)
173173+ PlacementState:SetSelected(effectiveId, name)
173174 end
174175175176 self._handleInput = function(input: InputObject, gameProcessedEvent: boolean)
···207208 elseif input.UserInputType == Enum.UserInputType.MouseButton2 then
208209 Util.StudioLog("[INPUT][CLIENT]", "MouseButton2", "processed", gameProcessedEvent)
209210 -- Allow click even if gameProcessedEvent (UI can set this), but only if we're actually pointing at a block
210210- local mouseBlock = PM:DebugGetPlacementOrWarn()
211211+ local mouseBlock = PM:DebugGetPlacementOrWarn(true) -- skip selection outline on right click
211212 if not mouseBlock then
212213 return
213214 end
···249250 return
250251 end
251252 local delta = direction > 0 and -1 or 1
252252- local nextSlot = math.clamp(self.state.selected + delta, 1, HOTBAR_SIZE)
253253+ local nextSlot = ((self.state.selected - 1 + delta) % HOTBAR_SIZE) + 1
253254 if nextSlot ~= self.state.selected then
254255 self._setSelected(nextSlot)
255256 end
···268269 self._syncFromClientState()
269270 self:_refreshViewports()
270271 -- initialize selection broadcast
271271- local id = self.state.slots and self.state.slots[self.state.selected] or ""
272272+ local rawId = self.state.slots and self.state.slots[self.state.selected] or ""
273273+ local effectiveId = rawId ~= "" and rawId or "hand"
272274 local name = ""
273273- if id ~= "" and self.state.names then
274274- name = self.state.names[id] or id
275275+ if rawId ~= "" and self.state.names then
276276+ name = self.state.names[rawId] or rawId
275277 end
276276- PlacementState:SetSelected(id, name)
278278+ PlacementState:SetSelected(effectiveId, name)
277279end
278280279281function Hotbar:willUnmount()
···345347 }),
346348 IndexLabel = Roact.createElement("TextLabel", {
347349 BackgroundTransparency = 1,
348348- Position = UDim2.fromOffset(4, 2),
350350+ Position = UDim2.fromOffset(8, 4),
349351 Size = UDim2.fromOffset(18, 14),
350352 Font = Enum.Font.Gotham,
351353 Text = i == 10 and "0" or tostring(i),
···412414 BorderSizePixel = 0,
413415 Position = UDim2.new(0.5, 0, 1, -80-10),
414416 Size = UDim2.fromOffset(0, 25),
417417+ Visible = selectedName ~= "",
415418 }, {
416419 Corner = Roact.createElement("UICorner", {
417420 CornerRadius = UDim.new(0, 8),