From 7969565ccaf4ebb76d726bcb597e6011c5460517 Mon Sep 17 00:00:00 2001 From: OCbwoy3 Date: Tue, 6 Jan 2026 09:40:14 +0200 Subject: [PATCH] codex: stuff --- .codex/AGENTS.md | 19 +- .../Remotes/InventoryRequest.rbxmx | 7 + .../Remotes/InventorySync.rbxmx | 7 + src/ReplicatedStorage/Shared/Catppuccin.lua | 32 ++++ src/ReplicatedStorage/Shared/Inventory.lua | 106 +++++++++++ .../Actor/ServerChunkManager/init.server.lua | 103 +++++++++++ src/StarterGui/Game_UI/LocalScript.client.lua | 164 ++++++++++++++++++ .../Actor/BlockInteraction.client.lua | 69 ++++---- 8 files changed, 466 insertions(+), 41 deletions(-) create mode 100644 src/ReplicatedStorage/Remotes/InventoryRequest.rbxmx create mode 100644 src/ReplicatedStorage/Remotes/InventorySync.rbxmx create mode 100644 src/ReplicatedStorage/Shared/Catppuccin.lua create mode 100644 src/ReplicatedStorage/Shared/Inventory.lua diff --git a/.codex/AGENTS.md b/.codex/AGENTS.md index 09f5df7..2a6d294 100644 --- a/.codex/AGENTS.md +++ b/.codex/AGENTS.md @@ -1,8 +1,13 @@ -This project is a Minecraft-like voxel system for Roblox using Rojo. The server -is authoritative over chunks and blocks. Clients only handle input, UI, and -requests. Clients must never create or destroy blocks directly. The server must -validate distance, block type, and target. Shared modules must not reference -Roblox services. +# Agents -Keep in mind that to replicate anything across the client-server model or to -write to any instance you must use serial luau +- This project is a Minecraft-like voxel system for Roblox using Rojo. +- The server is authoritative over chunks and blocks. +- Clients only handle input, UI, and requests. +- Clients must never create or destroy blocks directly. +- The server must validate distance, block type, and target. +- Shared modules must not reference Roblox services. + +Note: To replicate anything across the client-server model or to write to any +instance you must use serial Luau. + +The main AGENTS.md file is in the `.codex` folder! diff --git a/src/ReplicatedStorage/Remotes/InventoryRequest.rbxmx b/src/ReplicatedStorage/Remotes/InventoryRequest.rbxmx new file mode 100644 index 0000000..623396b --- /dev/null +++ b/src/ReplicatedStorage/Remotes/InventoryRequest.rbxmx @@ -0,0 +1,7 @@ + + + + InventoryRequest + + + diff --git a/src/ReplicatedStorage/Remotes/InventorySync.rbxmx b/src/ReplicatedStorage/Remotes/InventorySync.rbxmx new file mode 100644 index 0000000..5ca08e8 --- /dev/null +++ b/src/ReplicatedStorage/Remotes/InventorySync.rbxmx @@ -0,0 +1,7 @@ + + + + InventorySync + + + diff --git a/src/ReplicatedStorage/Shared/Catppuccin.lua b/src/ReplicatedStorage/Shared/Catppuccin.lua new file mode 100644 index 0000000..89f3a50 --- /dev/null +++ b/src/ReplicatedStorage/Shared/Catppuccin.lua @@ -0,0 +1,32 @@ +local Catppuccin = {} + +Catppuccin.mocha = { + rosewater = Color3.fromRGB(245, 224, 220), + flamingo = Color3.fromRGB(242, 205, 205), + pink = Color3.fromRGB(245, 194, 231), + base = Color3.fromRGB(30, 30, 46), + mantle = Color3.fromRGB(24, 24, 37), + crust = Color3.fromRGB(17, 17, 27), + surface0 = Color3.fromRGB(49, 50, 68), + surface1 = Color3.fromRGB(69, 71, 90), + surface2 = Color3.fromRGB(88, 91, 112), + overlay0 = Color3.fromRGB(108, 112, 134), + overlay1 = Color3.fromRGB(127, 132, 156), + overlay2 = Color3.fromRGB(147, 153, 178), + text = Color3.fromRGB(205, 214, 244), + subtext0 = Color3.fromRGB(166, 173, 200), + subtext1 = Color3.fromRGB(186, 194, 222), + mauve = Color3.fromRGB(203, 166, 247), + red = Color3.fromRGB(243, 139, 168), + maroon = Color3.fromRGB(235, 160, 172), + peach = Color3.fromRGB(250, 179, 135), + yellow = Color3.fromRGB(249, 226, 175), + green = Color3.fromRGB(166, 227, 161), + teal = Color3.fromRGB(148, 226, 213), + sky = Color3.fromRGB(137, 220, 235), + sapphire = Color3.fromRGB(116, 199, 236), + blue = Color3.fromRGB(137, 180, 250), + lavender = Color3.fromRGB(180, 190, 254), +} + +return Catppuccin diff --git a/src/ReplicatedStorage/Shared/Inventory.lua b/src/ReplicatedStorage/Shared/Inventory.lua new file mode 100644 index 0000000..3e62dc6 --- /dev/null +++ b/src/ReplicatedStorage/Shared/Inventory.lua @@ -0,0 +1,106 @@ +local Inventory = {} + +local HOTBAR_SIZE = 9 +local slots = table.create(HOTBAR_SIZE) +local selectedIndex = 1 + +local listeners = { + changed = {}, + selected = {}, +} + +local function addListener(list, fn) + table.insert(list, fn) + local active = true + return { + Disconnect = function() + if not active then + return + end + active = false + for i, cb in ipairs(list) do + if cb == fn then + table.remove(list, i) + break + end + end + end, + } +end + +local function notify(list, ...) + for _, cb in ipairs(list) do + cb(...) + end +end + +function Inventory.GetHotbarSize(): number + return HOTBAR_SIZE +end + +function Inventory.GetSlots(): {string?} + return slots +end + +function Inventory.GetSlot(index: number): string? + return slots[index] +end + +function Inventory.SetSlots(ids: {string?}) + for i = 1, HOTBAR_SIZE do + local value = ids[i] + if value == "" then + value = nil + end + slots[i] = value + end + selectedIndex = math.clamp(selectedIndex, 1, HOTBAR_SIZE) + if slots[selectedIndex] == nil then + for i = 1, HOTBAR_SIZE do + if slots[i] ~= nil then + selectedIndex = i + break + end + end + end + notify(listeners.changed, slots) + notify(listeners.selected, selectedIndex, Inventory.GetSelectedId()) +end + +function Inventory.SetSlot(index: number, id: string?) + if index < 1 or index > HOTBAR_SIZE then + return + end + slots[index] = id + notify(listeners.changed, slots) +end + +function Inventory.GetSelectedIndex(): number + return selectedIndex +end + +function Inventory.SetSelectedIndex(index: number) + if index < 1 or index > HOTBAR_SIZE then + return + end + if selectedIndex == index then + return + end + selectedIndex = index + notify(listeners.selected, selectedIndex, Inventory.GetSelectedId()) +end + +function Inventory.GetSelectedId(): string? + local id = slots[selectedIndex] + return id +end + +function Inventory.OnChanged(callback) + return addListener(listeners.changed, callback) +end + +function Inventory.OnSelected(callback) + return addListener(listeners.selected, callback) +end + +return Inventory diff --git a/src/ServerScriptService/Actor/ServerChunkManager/init.server.lua b/src/ServerScriptService/Actor/ServerChunkManager/init.server.lua index 18f291e..7d404ea 100644 --- a/src/ServerScriptService/Actor/ServerChunkManager/init.server.lua +++ b/src/ServerScriptService/Actor/ServerChunkManager/init.server.lua @@ -67,6 +67,8 @@ local tickRemote = ReplicatedStorage.Tick local remotes = ReplicatedStorage:WaitForChild("Remotes") local placeRemote = remotes:WaitForChild("PlaceBlock") local breakRemote = remotes:WaitForChild("BreakBlock") +local inventorySync = remotes:WaitForChild("InventorySync") +local inventoryRequest = remotes:WaitForChild("InventoryRequest") local blocksFolder = ReplicatedStorage:WaitForChild("Blocks") local function propogate(a, cx, cy, cz, x, y, z, bd) task.synchronize() @@ -75,7 +77,9 @@ local function propogate(a, cx, cy, cz, x, y, z, bd) end local MAX_REACH = 24 +local HOTBAR_SIZE = 9 local blockIdMap = {} +local playerInventories = {} local function rebuildBlockIdMap() table.clear(blockIdMap) @@ -92,6 +96,94 @@ rebuildBlockIdMap() blocksFolder.ChildAdded:Connect(rebuildBlockIdMap) blocksFolder.ChildRemoved:Connect(rebuildBlockIdMap) +local function buildDefaultSlots(): {string} + 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, function(a, b) + return a < b + end) + local slots = table.create(HOTBAR_SIZE, "") + for i = 1, HOTBAR_SIZE do + slots[i] = ids[i] + end + return slots +end + +local function syncInventory(player: Player) + local data = playerInventories[player.UserId] + if not data then + return + end + task.synchronize() + inventorySync:FireClient(player, data.slots) +end + +local function rebuildAllInventories() + for _, player in ipairs(game:GetService("Players"):GetPlayers()) do + local slots = buildDefaultSlots() + local allowed = {} + for i = 1, HOTBAR_SIZE do + local id = slots[i] + if id ~= "" then + allowed[id] = true + end + end + playerInventories[player.UserId] = { + slots = slots, + allowed = allowed, + } + syncInventory(player) + end +end + +blocksFolder.ChildAdded:Connect(rebuildAllInventories) +blocksFolder.ChildRemoved:Connect(rebuildAllInventories) + +game:GetService("Players").PlayerAdded:Connect(function(player: Player) + local slots = buildDefaultSlots() + local allowed = {} + for i = 1, HOTBAR_SIZE do + local id = slots[i] + if id ~= "" then + allowed[id] = true + end + end + playerInventories[player.UserId] = { + slots = slots, + allowed = allowed, + } + syncInventory(player) +end) + +game:GetService("Players").PlayerRemoving:Connect(function(player: Player) + playerInventories[player.UserId] = nil +end) + +for _, player in ipairs(game:GetService("Players"):GetPlayers()) do + local slots = buildDefaultSlots() + local allowed = {} + for i = 1, HOTBAR_SIZE do + local id = slots[i] + if id ~= "" then + allowed[id] = true + end + end + playerInventories[player.UserId] = { + slots = slots, + allowed = allowed, + } + syncInventory(player) +end + +inventoryRequest.OnServerEvent:Connect(function(player: Player) + syncInventory(player) +end) + local function getPlayerPosition(player: Player): Vector3? local character = player.Character if not character then @@ -117,6 +209,14 @@ local function resolveBlockId(blockId: any): string | number | nil return blockIdMap[blockId] end +local function playerHasBlockId(player: Player, blockId: string | number): boolean + local data = playerInventories[player.UserId] + if not data then + return false + end + return data.allowed[tostring(blockId)] == true +end + local function getServerChunk(cx: number, cy: number, cz: number) task.desynchronize() local chunk = TG:GetChunk(cx, cy, cz) @@ -146,6 +246,9 @@ placeRemote.OnServerEvent:Connect(function(player, cx, cy, cz, x, y, z, blockId) if not resolvedId then return end + if not playerHasBlockId(player, resolvedId) then + return + end local chunk = getServerChunk(cx, cy, cz) if chunk:GetBlockAt(x, y, z) then diff --git a/src/StarterGui/Game_UI/LocalScript.client.lua b/src/StarterGui/Game_UI/LocalScript.client.lua index 0b92109..48532ef 100644 --- a/src/StarterGui/Game_UI/LocalScript.client.lua +++ b/src/StarterGui/Game_UI/LocalScript.client.lua @@ -5,6 +5,11 @@ end local ui = script.Parent local ReplicatedStorage = game:GetService("ReplicatedStorage") +local Inventory = require(ReplicatedStorage.Shared.Inventory) +local Catppuccin = require(ReplicatedStorage.Shared.Catppuccin) +local blocksFolder = ReplicatedStorage:WaitForChild("Blocks") + +local mocha = Catppuccin.mocha ReplicatedStorage:WaitForChild("Objects"):WaitForChild("MLLoaded") @@ -17,6 +22,165 @@ cd.Parent = game:GetService("Workspace"):FindFirstChildOfClass("Terrain") sky.Parent = game:GetService("Workspace"):FindFirstChildOfClass("Terrain") base.Parent = game:GetService("Workspace"):FindFirstChildOfClass("Terrain") +local hotbarRoot = ui:WaitForChild("Hotbar") +local hotbarSlotsRoot = hotbarRoot:WaitForChild("Frame") +local hotbarSelectLabel = ui:WaitForChild("HotbarItemSelectLabel") +local hotbarSelectText = hotbarSelectLabel:WaitForChild("TextLabel") +local hotbarDebug = ui:WaitForChild("HotbarDebug") +local hotbarDebugText = hotbarDebug:WaitForChild("TextLabel") +local debugUpperText = ui:WaitForChild("DebugUpperText") +local hotbarStroke = hotbarRoot:FindFirstChild("UIStroke") +local hotbarSelectStroke = hotbarSelectLabel:FindFirstChild("UIStroke") +local hotbarDebugStroke = hotbarDebug:FindFirstChild("UIStroke") + +hotbarRoot.BackgroundColor3 = mocha.base +if hotbarStroke then + hotbarStroke.Color = mocha.blue +end +hotbarSelectLabel.BackgroundColor3 = mocha.base +if hotbarSelectStroke then + hotbarSelectStroke.Color = mocha.blue +end +hotbarDebug.BackgroundColor3 = mocha.base +if hotbarDebugStroke then + hotbarDebugStroke.Color = mocha.surface2 +end +hotbarSelectText.TextColor3 = mocha.text +hotbarDebugText.TextColor3 = mocha.text +debugUpperText.TextColor3 = mocha.text + +local slotFrames = {} +local slotStrokes = {} +local slotLabels = {} +local renderHotbar = nil + +local blockDisplayNames = {} +local blockIcons = {} + +local function rebuildBlockMappings() + table.clear(blockDisplayNames) + table.clear(blockIcons) + for _, block in ipairs(blocksFolder:GetChildren()) do + local id = block:GetAttribute("n") + if id ~= nil then + blockDisplayNames[tostring(id)] = block.Name + local icon = block:GetAttribute("icon") + if typeof(icon) == "string" and icon ~= "" then + blockIcons[tostring(id)] = icon + end + end + end + if renderHotbar then + renderHotbar() + end +end + +rebuildBlockMappings() +blocksFolder.ChildAdded:Connect(rebuildBlockMappings) +blocksFolder.ChildRemoved:Connect(rebuildBlockMappings) + +for i = 1, Inventory.GetHotbarSize() do + local slotName = `HotbarSlot{i - 1}` + local slot = hotbarSlotsRoot:WaitForChild(slotName) + slotFrames[i] = slot + local stroke = slot:FindFirstChild("UIStroke") + if not stroke then + stroke = Instance.new("UIStroke") + stroke.Parent = slot + end + slotStrokes[i] = stroke + + local imageLabel = slot:FindFirstChild("ImageLabel") + if not imageLabel then + imageLabel = Instance.new("ImageLabel") + imageLabel.Name = "ImageLabel" + imageLabel.BackgroundTransparency = 1 + imageLabel.Size = UDim2.fromScale(1, 1) + imageLabel.ScaleType = Enum.ScaleType.Fit + imageLabel.Parent = slot + end + + local label = slot:FindFirstChild("TextLabel") + if not label then + label = Instance.new("TextLabel") + label.Name = "TextLabel" + label.BackgroundTransparency = 1 + label.Size = UDim2.fromScale(1, 1) + label.TextScaled = true + label.Font = Enum.Font.Gotham + label.TextColor3 = mocha.text + label.TextWrapped = true + label.ZIndex = 3 + label.Parent = slot + end + slotLabels[i] = label +end + +renderHotbar = function() + for i = 1, Inventory.GetHotbarSize() do + local slot = slotFrames[i] + if slot then + slot.BackgroundColor3 = mocha.surface0 + local stroke = slotStrokes[i] + if stroke then + if i == Inventory.GetSelectedIndex() then + stroke.Color = mocha.blue + stroke.Thickness = 2 + else + stroke.Color = mocha.surface2 + stroke.Thickness = 1 + end + end + + local imageLabel = slot:FindFirstChild("ImageLabel") + local id = Inventory.GetSlot(i) + local displayName = id and (blockDisplayNames[tostring(id)] or tostring(id)) or "" + if imageLabel then + local icon = id and blockIcons[tostring(id)] or nil + imageLabel.Visible = icon ~= nil + imageLabel.Image = icon or "" + imageLabel.BackgroundTransparency = 1 + imageLabel.ZIndex = 2 + end + + local textLabel = slotLabels[i] + if textLabel then + textLabel.Text = displayName + textLabel.TextColor3 = mocha.text + textLabel.Visible = displayName ~= "" + end + end + end +end + +local function updateSelectedLabel() + local id = Inventory.GetSelectedId() + local displayName = id and (blockDisplayNames[tostring(id)] or tostring(id)) or nil + if hotbarSelectText then + hotbarSelectText.Text = displayName and `Selected: {displayName}` or "Selected: (empty)" + end + if hotbarDebugText then + hotbarDebugText.Text = `Slot {Inventory.GetSelectedIndex()} Id {displayName or "empty"}` + end +end + +local extraSlot = hotbarSlotsRoot and hotbarSlotsRoot:FindFirstChild("HotbarSlot9") +if extraSlot then + extraSlot.Visible = false +end + +renderHotbar() +updateSelectedLabel() + +Inventory.OnChanged(function() + renderHotbar() + updateSelectedLabel() +end) + +Inventory.OnSelected(function() + renderHotbar() + updateSelectedLabel() +end) game:GetService("RunService").RenderStepped:Connect(function(dt) local fps = math.round(1/dt) diff --git a/src/StarterPlayer/StarterPlayerScripts/Actor/BlockInteraction.client.lua b/src/StarterPlayer/StarterPlayerScripts/Actor/BlockInteraction.client.lua index 2af7785..90ba90e 100644 --- a/src/StarterPlayer/StarterPlayerScripts/Actor/BlockInteraction.client.lua +++ b/src/StarterPlayer/StarterPlayerScripts/Actor/BlockInteraction.client.lua @@ -7,12 +7,11 @@ 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 Inventory = require(ReplicatedStorage.Shared.Inventory) +local remotes = ReplicatedStorage:WaitForChild("Remotes") +local inventorySync = remotes:WaitForChild("InventorySync") +local inventoryRequest = remotes:WaitForChild("InventoryRequest") local keyToSlot = { [Enum.KeyCode.One] = 1, @@ -26,40 +25,25 @@ local keyToSlot = { [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)) +local function findNextFilledIndex(startIndex: number, direction: number): number + local size = Inventory.GetHotbarSize() + for step = 1, size do + local idx = ((startIndex - 1 + (direction * step)) % size) + 1 + if Inventory.GetSlot(idx) ~= nil then + return idx end end - - table.sort(ids) - for i = 1, HOTBAR_SIZE do - hotbar[i] = ids[i] or "" - end - selectedSlot = math.clamp(selectedSlot, 1, HOTBAR_SIZE) + return startIndex 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 +inventorySync.OnClientEvent:Connect(function(slots) + if typeof(slots) ~= "table" then return end - selectedSlot = slot -end + Inventory.SetSlots(slots) +end) -rebuildHotbar() -blocksFolder.ChildAdded:Connect(rebuildHotbar) -blocksFolder.ChildRemoved:Connect(rebuildHotbar) +inventoryRequest:FireServer() UIS.InputBegan:Connect(function(input: InputObject, gameProcessedEvent: boolean) if gameProcessedEvent then @@ -68,7 +52,9 @@ UIS.InputBegan:Connect(function(input: InputObject, gameProcessedEvent: boolean) local slot = keyToSlot[input.KeyCode] if slot then - setSelectedSlot(slot) + if Inventory.GetSlot(slot) ~= nil then + Inventory.SetSelectedIndex(slot) + end return end @@ -83,10 +69,25 @@ UIS.InputBegan:Connect(function(input: InputObject, gameProcessedEvent: boolean) if not mouseBlock then return end - local blockId = getSelectedBlockId() + local blockId = Inventory.GetSelectedId() 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) + +UIS.InputChanged:Connect(function(input: InputObject, gameProcessedEvent: boolean) + if gameProcessedEvent then + return + end + if input.UserInputType == Enum.UserInputType.MouseWheel then + local delta = input.Position.Z + if delta == 0 then + return + end + local direction = delta > 0 and -1 or 1 + local nextIndex = findNextFilledIndex(Inventory.GetSelectedIndex(), direction) + Inventory.SetSelectedIndex(nextIndex) + end +end)