codex: stuff

This commit is contained in:
2026-01-06 09:40:14 +02:00
parent 8ad642239f
commit 7969565cca
8 changed files with 466 additions and 41 deletions

View File

@@ -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!

View File

@@ -0,0 +1,7 @@
<roblox version="4">
<Item class="RemoteEvent" referent="RBX5B7E5C8D4E2348B78F8C1A2E9E6D4F90">
<Properties>
<string name="Name">InventoryRequest</string>
</Properties>
</Item>
</roblox>

View File

@@ -0,0 +1,7 @@
<roblox version="4">
<Item class="RemoteEvent" referent="RBX7A1C0A5FA0E0412F9B2F5A43B9C4B3A1">
<Properties>
<string name="Name">InventorySync</string>
</Properties>
</Item>
</roblox>

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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)

View File

@@ -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)