--!native --!optimize 2 local Players = game:GetService("Players") local ReplicatedStorage = game:GetService("ReplicatedStorage") local Replica = require(ReplicatedStorage.Packages.replica) local ClientStateService = {} local HOTBAR_SIZE = 10 local token = Replica.Token("ClientState") local blockCatalog = {} local playerReplicas = {} :: {[Player]: any} local blocksFolder: Folder? = nil local readyConnections = {} :: {[Player]: RBXScriptConnection} local function sortBlocks() table.sort(blockCatalog, function(a, b) local na = tonumber(a.id) local nb = tonumber(b.id) if na and nb then return na < nb end if na then return true end if nb then return false end return a.id < b.id end) end local function rebuildBlockCatalog() table.clear(blockCatalog) if not blocksFolder then return end for _, block in ipairs(blocksFolder:GetChildren()) do local id = block:GetAttribute("n") if id ~= nil then local displayName = block:GetAttribute("name") or block:GetAttribute("displayName") or block:GetAttribute("dn") or block.Name table.insert(blockCatalog, { id = tostring(id), name = displayName, }) end end sortBlocks() end local function makeBaseState() local inventory = {} local hotbar = {} for _, entry in ipairs(blockCatalog) do inventory[entry.id] = { name = entry.name, count = 999999, } if #hotbar < HOTBAR_SIZE then table.insert(hotbar, entry.id) end end return { inventory = inventory, hotbar = hotbar, selectedSlot = #hotbar > 0 and 1 or 0, } end local function sanitizeSelection(hotbar, selectedSlot) if type(selectedSlot) ~= "number" then return (#hotbar > 0) and 1 or 0 end if selectedSlot < 1 or selectedSlot > HOTBAR_SIZE then return (#hotbar > 0) and 1 or 0 end return selectedSlot end local function refreshReplica(replica) local state = makeBaseState() replica:Set({"inventory"}, state.inventory) replica:Set({"hotbar"}, state.hotbar) replica:Set({"selectedSlot"}, sanitizeSelection(state.hotbar, replica.Data.selectedSlot)) end function ClientStateService:SetBlocksFolder(folder: Folder?) blocksFolder = folder rebuildBlockCatalog() for _, replica in pairs(playerReplicas) do refreshReplica(replica) end end function ClientStateService:GetReplica(player: Player) return playerReplicas[player] end function ClientStateService:GetSelectedBlockId(player: Player) local replica = playerReplicas[player] if not replica then return nil end local data = replica.Data local hotbar = data.hotbar or {} local selectedSlot = sanitizeSelection(hotbar, data.selectedSlot) return hotbar[selectedSlot] end function ClientStateService:HasInInventory(player: Player, blockId: any): boolean local replica = playerReplicas[player] if not replica or not blockId then return false end local inv = replica.Data.inventory return inv and inv[tostring(blockId)] ~= nil or false end local function handleReplicaEvents(player: Player, replica) replica.OnServerEvent:Connect(function(plr, action, payload) if plr ~= player then return end if action == "SelectHotbarSlot" then local slot = tonumber(payload) local hotbar = replica.Data.hotbar if not hotbar then return end if slot and slot >= 1 and slot <= HOTBAR_SIZE then replica:Set({"selectedSlot"}, slot) end end end) end local function onPlayerAdded(player: Player) local replica = Replica.New({ Token = token, Tags = { UserId = player.UserId, Player = player, }, Data = makeBaseState(), }) if Replica.ReadyPlayers[player] then replica:Subscribe(player) else readyConnections[player] = Replica.NewReadyPlayer:Connect(function(newPlayer) if newPlayer ~= player then return end if readyConnections[player] then readyConnections[player]:Disconnect() readyConnections[player] = nil end replica:Subscribe(player) end) end handleReplicaEvents(player, replica) playerReplicas[player] = replica end local function onPlayerRemoving(player: Player) local replica = playerReplicas[player] if replica then replica:Destroy() playerReplicas[player] = nil end if readyConnections[player] then readyConnections[player]:Disconnect() readyConnections[player] = nil end end function ClientStateService:Init() rebuildBlockCatalog() for _, player in ipairs(Players:GetPlayers()) do onPlayerAdded(player) end Players.PlayerAdded:Connect(onPlayerAdded) Players.PlayerRemoving:Connect(onPlayerRemoving) end return ClientStateService