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

core: fix hotbar

+426 -51
+2 -1
.gitignore
··· 1 - Packages/ 1 + Packages/ 2 + ServerPackages/
+128
ReplicatedStorage/Shared/ClientState.lua
··· 1 + --!native 2 + --!optimize 2 3 + 4 + local RunService = game:GetService("RunService") 5 + 6 + if RunService:IsServer() then 7 + error("ClientState can only be required on the client") 8 + end 9 + 10 + local Players = game:GetService("Players") 11 + local ReplicatedStorage = game:GetService("ReplicatedStorage") 12 + 13 + local Replica = require(ReplicatedStorage.Packages.replica) 14 + 15 + local ClientState = {} 16 + 17 + local HOTBAR_SIZE = 10 18 + 19 + local localPlayer = Players.LocalPlayer 20 + local replicaForPlayer = nil 21 + local changed = Instance.new("BindableEvent") 22 + 23 + local function fireChanged() 24 + changed:Fire() 25 + end 26 + 27 + local function onReplicaNew(replica) 28 + local tags = replica.Tags or {} 29 + if tags.UserId ~= localPlayer.UserId and tags.Player ~= localPlayer then 30 + return 31 + end 32 + 33 + replicaForPlayer = replica 34 + replica:OnChange(fireChanged) 35 + fireChanged() 36 + end 37 + 38 + Replica.OnNew("ClientState", onReplicaNew) 39 + Replica.RequestData() 40 + 41 + function ClientState:IsReady(): boolean 42 + return replicaForPlayer ~= nil 43 + end 44 + 45 + function ClientState:GetReplica() 46 + return replicaForPlayer 47 + end 48 + 49 + function ClientState:GetSelectedSlot(): number? 50 + if not replicaForPlayer then 51 + return nil 52 + end 53 + return replicaForPlayer.Data.selectedSlot 54 + end 55 + 56 + local function getInventory() 57 + return replicaForPlayer and replicaForPlayer.Data.inventory or nil 58 + end 59 + 60 + function ClientState:GetItemInfo(blockId: any) 61 + if not replicaForPlayer or not blockId then 62 + return nil 63 + end 64 + local inv = getInventory() 65 + local entry = inv and inv[tostring(blockId)] 66 + if not entry then 67 + return nil 68 + end 69 + 70 + return { 71 + id = tostring(blockId), 72 + name = entry.name or tostring(blockId), 73 + count = entry.count, 74 + } 75 + end 76 + 77 + function ClientState:GetHotbarSlots(): {string} 78 + if not replicaForPlayer then 79 + local slots = table.create(HOTBAR_SIZE) 80 + for i = 1, HOTBAR_SIZE do 81 + slots[i] = "" 82 + end 83 + return slots 84 + end 85 + 86 + return replicaForPlayer.Data.hotbar or {} 87 + end 88 + 89 + function ClientState:GetSlotInfo(slot: number) 90 + if not replicaForPlayer then 91 + return nil 92 + end 93 + local hotbar = replicaForPlayer.Data.hotbar 94 + if not hotbar then 95 + return nil 96 + end 97 + local id = hotbar[slot] 98 + if not id then 99 + return nil 100 + end 101 + return ClientState:GetItemInfo(id) 102 + end 103 + 104 + function ClientState:GetSelectedBlock() 105 + if not replicaForPlayer then 106 + return nil 107 + end 108 + local slot = ClientState:GetSelectedSlot() 109 + if not slot then 110 + return nil 111 + end 112 + return ClientState:GetSlotInfo(slot) 113 + end 114 + 115 + function ClientState:SetSelectedSlot(slot: number) 116 + if not replicaForPlayer then 117 + return 118 + end 119 + local hotbar = replicaForPlayer.Data.hotbar 120 + if not hotbar or not hotbar[slot] then 121 + return 122 + end 123 + replicaForPlayer:FireServer("SelectHotbarSlot", slot) 124 + end 125 + 126 + ClientState.Changed = changed.Event 127 + 128 + return ClientState
+198
ServerScriptService/Actor/ClientState.lua
··· 1 + --!native 2 + --!optimize 2 3 + 4 + local Players = game:GetService("Players") 5 + local ReplicatedStorage = game:GetService("ReplicatedStorage") 6 + 7 + local Replica = require(ReplicatedStorage.Packages.replica) 8 + 9 + local ClientStateService = {} 10 + 11 + local HOTBAR_SIZE = 10 12 + 13 + local token = Replica.Token("ClientState") 14 + 15 + local blockCatalog = {} 16 + local playerReplicas = {} :: {[Player]: any} 17 + local blocksFolder: Folder? = nil 18 + local readyConnections = {} :: {[Player]: RBXScriptConnection} 19 + 20 + local function sortBlocks() 21 + table.sort(blockCatalog, function(a, b) 22 + local na = tonumber(a.id) 23 + local nb = tonumber(b.id) 24 + if na and nb then 25 + return na < nb 26 + end 27 + if na then 28 + return true 29 + end 30 + if nb then 31 + return false 32 + end 33 + return a.id < b.id 34 + end) 35 + end 36 + 37 + local function rebuildBlockCatalog() 38 + table.clear(blockCatalog) 39 + if not blocksFolder then 40 + return 41 + end 42 + 43 + for _, block in ipairs(blocksFolder:GetChildren()) do 44 + local id = block:GetAttribute("n") 45 + if id ~= nil then 46 + table.insert(blockCatalog, { 47 + id = tostring(id), 48 + name = block:GetAttribute("displayName") or block:GetAttribute("dn") or block.Name, 49 + }) 50 + end 51 + end 52 + sortBlocks() 53 + end 54 + 55 + local function makeBaseState() 56 + local inventory = {} 57 + local hotbar = {} 58 + 59 + for _, entry in ipairs(blockCatalog) do 60 + inventory[entry.id] = { 61 + name = entry.name, 62 + count = 999999, 63 + } 64 + if #hotbar < HOTBAR_SIZE then 65 + table.insert(hotbar, entry.id) 66 + end 67 + end 68 + 69 + return { 70 + inventory = inventory, 71 + hotbar = hotbar, 72 + selectedSlot = #hotbar > 0 and 1 or 0, 73 + } 74 + end 75 + 76 + local function sanitizeSelection(hotbar, selectedSlot) 77 + if type(selectedSlot) ~= "number" then 78 + return (#hotbar > 0) and 1 or 0 79 + end 80 + if selectedSlot < 1 or selectedSlot > HOTBAR_SIZE then 81 + return (#hotbar > 0) and 1 or 0 82 + end 83 + if not hotbar[selectedSlot] then 84 + return (#hotbar > 0) and 1 or 0 85 + end 86 + return selectedSlot 87 + end 88 + 89 + local function refreshReplica(replica) 90 + local state = makeBaseState() 91 + replica:Set({"inventory"}, state.inventory) 92 + replica:Set({"hotbar"}, state.hotbar) 93 + replica:Set({"selectedSlot"}, sanitizeSelection(state.hotbar, replica.Data.selectedSlot)) 94 + end 95 + 96 + function ClientStateService:SetBlocksFolder(folder: Folder?) 97 + blocksFolder = folder 98 + rebuildBlockCatalog() 99 + for _, replica in pairs(playerReplicas) do 100 + refreshReplica(replica) 101 + end 102 + end 103 + 104 + function ClientStateService:GetReplica(player: Player) 105 + return playerReplicas[player] 106 + end 107 + 108 + function ClientStateService:GetSelectedBlockId(player: Player) 109 + local replica = playerReplicas[player] 110 + if not replica then 111 + return nil 112 + end 113 + local data = replica.Data 114 + local hotbar = data.hotbar or {} 115 + local selectedSlot = sanitizeSelection(hotbar, data.selectedSlot) 116 + return hotbar[selectedSlot] 117 + end 118 + 119 + function ClientStateService:HasInInventory(player: Player, blockId: any): boolean 120 + local replica = playerReplicas[player] 121 + if not replica or not blockId then 122 + return false 123 + end 124 + local inv = replica.Data.inventory 125 + return inv and inv[tostring(blockId)] ~= nil or false 126 + end 127 + 128 + local function handleReplicaEvents(player: Player, replica) 129 + replica.OnServerEvent:Connect(function(plr, action, payload) 130 + if plr ~= player then 131 + return 132 + end 133 + 134 + if action == "SelectHotbarSlot" then 135 + local slot = tonumber(payload) 136 + local hotbar = replica.Data.hotbar 137 + if not hotbar then 138 + return 139 + end 140 + if slot and slot >= 1 and slot <= HOTBAR_SIZE and hotbar[slot] then 141 + replica:Set({"selectedSlot"}, slot) 142 + end 143 + end 144 + end) 145 + end 146 + 147 + local function onPlayerAdded(player: Player) 148 + local replica = Replica.New({ 149 + Token = token, 150 + Tags = { 151 + UserId = player.UserId, 152 + Player = player, 153 + }, 154 + Data = makeBaseState(), 155 + }) 156 + 157 + if Replica.ReadyPlayers[player] then 158 + replica:Subscribe(player) 159 + else 160 + readyConnections[player] = Replica.NewReadyPlayer:Connect(function(newPlayer) 161 + if newPlayer ~= player then 162 + return 163 + end 164 + if readyConnections[player] then 165 + readyConnections[player]:Disconnect() 166 + readyConnections[player] = nil 167 + end 168 + replica:Subscribe(player) 169 + end) 170 + end 171 + 172 + handleReplicaEvents(player, replica) 173 + playerReplicas[player] = replica 174 + end 175 + 176 + local function onPlayerRemoving(player: Player) 177 + local replica = playerReplicas[player] 178 + if replica then 179 + replica:Destroy() 180 + playerReplicas[player] = nil 181 + end 182 + if readyConnections[player] then 183 + readyConnections[player]:Disconnect() 184 + readyConnections[player] = nil 185 + end 186 + end 187 + 188 + function ClientStateService:Init() 189 + rebuildBlockCatalog() 190 + 191 + for _, player in ipairs(Players:GetPlayers()) do 192 + onPlayerAdded(player) 193 + end 194 + Players.PlayerAdded:Connect(onPlayerAdded) 195 + Players.PlayerRemoving:Connect(onPlayerRemoving) 196 + end 197 + 198 + return ClientStateService
+44 -5
ServerScriptService/Actor/ServerChunkManager/init.server.lua
··· 2 2 --!optimize 2 3 3 4 4 local ReplicatedStorage = game:GetService("ReplicatedStorage") 5 - 5 + local ServerStorage = game:GetService("ServerStorage") 6 + local ClientStateService = require(script.Parent.ClientState) 6 7 7 8 local Shared = ReplicatedStorage:WaitForChild("Shared") 8 9 local ModsFolder = ReplicatedStorage:WaitForChild("Mods") 10 + local BlocksFolderRS = ReplicatedStorage:FindFirstChild("Blocks") or Instance.new("Folder") 11 + BlocksFolderRS.Name = "Blocks" 12 + BlocksFolderRS.Parent = ReplicatedStorage 13 + local BlocksFolderSS = ServerStorage:FindFirstChild("Blocks") or Instance.new("Folder") 14 + BlocksFolderSS.Name = "Blocks" 15 + BlocksFolderSS.Parent = ServerStorage 9 16 10 17 local Util = require(Shared.Util) 11 - local TG = require("./ServerChunkManager/TerrainGen") 18 + local TG = require(script.TerrainGen) 12 19 local Players = game:GetService("Players") 13 20 21 + local blockIdMap = {} 22 + local rebuildBlockIdMap 23 + 24 + local function syncBlocksToServerStorage() 25 + BlocksFolderSS:ClearAllChildren() 26 + for _, child in ipairs(BlocksFolderRS:GetChildren()) do 27 + child:Clone().Parent = BlocksFolderSS 28 + end 29 + ClientStateService:SetBlocksFolder(BlocksFolderSS) 30 + if rebuildBlockIdMap then 31 + rebuildBlockIdMap() 32 + end 33 + end 34 + 35 + BlocksFolderRS.ChildAdded:Connect(syncBlocksToServerStorage) 36 + BlocksFolderRS.ChildRemoved:Connect(syncBlocksToServerStorage) 37 + 14 38 do 15 39 local workspaceModFolder = game:GetService("Workspace"):WaitForChild("mods") 16 40 ··· 22 46 23 47 local ML = require(Shared.ModLoader) 24 48 ML.loadModsS() 49 + syncBlocksToServerStorage() 50 + ClientStateService:Init() 25 51 26 52 do 27 53 local bv = Instance.new("BoolValue") ··· 67 93 local remotes = ReplicatedStorage:WaitForChild("Remotes") 68 94 local placeRemote = remotes:WaitForChild("PlaceBlock") 69 95 local breakRemote = remotes:WaitForChild("BreakBlock") 70 - local blocksFolder = ReplicatedStorage:WaitForChild("Blocks") 96 + local blocksFolder = BlocksFolderSS 71 97 local function propogate(a, cx, cy, cz, x, y, z, bd) 72 98 task.synchronize() 73 99 tickRemote:FireAllClients(a, cx, cy, cz, x, y, z, bd) ··· 75 101 end 76 102 77 103 local MAX_REACH = 512 78 - local blockIdMap = {} 79 104 80 - local function rebuildBlockIdMap() 105 + rebuildBlockIdMap = function() 81 106 table.clear(blockIdMap) 82 107 for _, block in ipairs(blocksFolder:GetChildren()) do 83 108 local id = block:GetAttribute("n") ··· 113 138 return blockIdMap[blockId] 114 139 end 115 140 141 + local function playerCanUseBlock(player: Player, resolvedId: any): boolean 142 + if not ClientStateService:HasInInventory(player, resolvedId) then 143 + return false 144 + end 145 + local selected = ClientStateService:GetSelectedBlockId(player) 146 + if not selected then 147 + return false 148 + end 149 + return tostring(selected) == tostring(resolvedId) 150 + end 151 + 116 152 local function getServerChunk(cx: number, cy: number, cz: number) 117 153 task.desynchronize() 118 154 local chunk = TG:GetChunk(cx, cy, cz) ··· 175 211 local resolvedId = resolveBlockId(blockId) 176 212 if not resolvedId then 177 213 return reject("invalid id") 214 + end 215 + if not playerCanUseBlock(player, resolvedId) then 216 + return reject("not in inventory/hotbar") 178 217 end 179 218 180 219 local blockPos = Util.ChunkPosToCFrame(Vector3.new(cx, cy, cz), Vector3.new(x, y, z)).Position
+47 -44
StarterGui/Hotbar/LocalScript.client.lua
··· 18 18 local BlockManager = require(ReplicatedStorage.Shared.ChunkManager.BlockManager) 19 19 local PlacementState = require(ReplicatedStorage.Shared.PlacementState) 20 20 local Util = require(ReplicatedStorage.Shared.Util) 21 - 22 - local blocksFolder = ReplicatedStorage:WaitForChild("Blocks") 21 + local ClientState = require(ReplicatedStorage.Shared.ClientState) 23 22 24 23 local HOTBAR_SIZE = 10 25 24 ··· 53 52 return config ~= nil and config.IsFocused 54 53 end 55 54 56 - local function buildHotbarIds(): {string} 57 - local ids = {} 58 - local names = {} 59 - for _, block in ipairs(blocksFolder:GetChildren()) do 60 - local id = block:GetAttribute("n") 61 - if id ~= nil then 62 - local n = tonumber(id) 63 - if n and n > 0 then 64 - local idStr = tostring(n) 65 - table.insert(ids, idStr) 66 - names[idStr] = block:GetAttribute("displayName") or block:GetAttribute("dn") or block.Name 67 - end 55 + local function resolveSelectedSlot(slots, desired) 56 + if desired and desired >= 1 and desired <= HOTBAR_SIZE and slots[desired] ~= "" then 57 + return desired 58 + end 59 + for i = 1, HOTBAR_SIZE do 60 + if slots[i] and slots[i] ~= "" then 61 + return i 68 62 end 69 63 end 70 - table.sort(ids, function(a, b) 71 - local na = tonumber(a) 72 - local nb = tonumber(b) 73 - if na and nb then 74 - return na < nb 75 - end 76 - return a < b 77 - end) 64 + return desired or 1 65 + end 66 + 67 + local function buildHotbarFromState() 78 68 local slots = table.create(HOTBAR_SIZE) 69 + local names = {} 70 + 79 71 for i = 1, HOTBAR_SIZE do 80 - slots[i] = ids[i] or "" 72 + local info = ClientState:GetSlotInfo(i) 73 + if info then 74 + slots[i] = tostring(info.id) 75 + names[slots[i]] = info.name or slots[i] 76 + else 77 + slots[i] = "" 78 + end 81 79 end 82 - return slots, names 80 + 81 + local selected = resolveSelectedSlot(slots, ClientState:GetSelectedSlot()) 82 + return slots, names, selected 83 83 end 84 84 85 85 local function ensurePreviewRig(part: Instance) ··· 131 131 local Hotbar = Roact.Component:extend("Hotbar") 132 132 133 133 function Hotbar:init() 134 + local slots, names, selected = buildHotbarFromState() 134 135 self.state = { 135 - slots = nil, 136 - names = nil, 137 - selected = 1, 136 + slots = slots, 137 + names = names, 138 + selected = selected, 138 139 } 139 - local slots, names = buildHotbarIds() 140 - self.state.slots = slots 141 - self.state.names = names 142 - local initialId = slots and slots[1] or "" 143 - if initialId and initialId ~= "" then 144 - local initialName = names and (names[initialId] or initialId) or initialId 145 - PlacementState:SetSelected(initialId, initialName) 146 - end 147 140 148 - self._updateSlots = function() 149 - local nextSlots, nextNames = buildHotbarIds() 141 + self._syncFromClientState = function() 142 + local nextSlots, nextNames, nextSelected = buildHotbarFromState() 143 + nextSelected = resolveSelectedSlot(nextSlots, nextSelected or self.state.selected) 150 144 self:setState({ 151 145 slots = nextSlots, 152 146 names = nextNames, 147 + selected = nextSelected, 153 148 }) 149 + local id = nextSlots[nextSelected] or "" 150 + local name = "" 151 + if id ~= "" then 152 + name = nextNames[id] or id 153 + end 154 + PlacementState:SetSelected(id, name) 154 155 end 155 156 156 157 self._setSelected = function(slot: number) 157 158 if slot < 1 or slot > HOTBAR_SIZE then 158 159 return 159 160 end 161 + local info = ClientState:GetSlotInfo(slot) 162 + if not info then 163 + return 164 + end 165 + ClientState:SetSelectedSlot(slot) 160 166 self:setState({ 161 167 selected = slot, 162 168 }) 163 - local id = self.state.slots and self.state.slots[slot] or "" 164 - local name = "" 165 - if id ~= "" and self.state.names then 166 - name = self.state.names[id] or id 167 - end 169 + local id = tostring(info.id) 170 + local name = info.name or id 168 171 Util.StudioLog("[PLACE][CLIENT][SELECT]", "slot", slot, "id", id, "name", name) 169 172 PlacementState:SetSelected(id, name) 170 173 end ··· 258 261 259 262 function Hotbar:didMount() 260 263 self._connections = { 261 - blocksFolder.ChildAdded:Connect(self._updateSlots), 262 - blocksFolder.ChildRemoved:Connect(self._updateSlots), 264 + ClientState.Changed:Connect(self._syncFromClientState), 263 265 UIS.InputBegan:Connect(self._handleInput), 264 266 UIS.InputChanged:Connect(self._handleScroll), 265 267 } 268 + self._syncFromClientState() 266 269 self:_refreshViewports() 267 270 -- initialize selection broadcast 268 271 local id = self.state.slots and self.state.slots[self.state.selected] or ""
+6 -1
wally.lock
··· 8 8 dependencies = [] 9 9 10 10 [[package]] 11 + name = "ivasmigins/replica" 12 + version = "0.1.0" 13 + dependencies = [] 14 + 15 + [[package]] 11 16 name = "ocbwoy3-development-studios/minecraft-roblox" 12 17 version = "0.1.0" 13 - dependencies = [["cmdr", "evaera/cmdr@1.12.0"], ["roact", "roblox/roact@1.4.4"]] 18 + dependencies = [["cmdr", "evaera/cmdr@1.12.0"], ["replica", "ivasmigins/replica@0.1.0"], ["roact", "roblox/roact@1.4.4"]] 14 19 15 20 [[package]] 16 21 name = "roblox/roact"
+1
wally.toml
··· 7 7 [dependencies] 8 8 cmdr = "evaera/cmdr@1.12.0" 9 9 roact = "roblox/roact@1.4.4" 10 + replica = "ivasmigins/replica@0.1.0"