codex: stuff

This commit is contained in:
2026-01-06 08:31:53 +02:00
parent 15e4fc4a3a
commit 8ad642239f
11 changed files with 309 additions and 36 deletions

8
.codex/AGENTS.md Normal file
View File

@@ -0,0 +1,8 @@
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.
Keep in mind that to replicate anything across the client-server model or to
write to any instance you must use serial luau

View File

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

View File

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

View File

@@ -0,0 +1,4 @@
{
"className": "Folder",
"ignoreUnknownInstances": true
}

View File

@@ -39,17 +39,20 @@ local function propogateNeighboringBlockChanges(cx, cy, cz, x, y, z)
local d = c.data[`{x},{y},{z}`]
if not d then return end
task.synchronize()
if c:IsBlockRenderable(x, y, z) then
if c.instance:FindFirstChild(`{x},{y},{z}`) then return end
task.synchronize()
local block = BlockManager:GetBlockRotated(d.id, util.RotationStringToNormalId(d.state["r"] or "f"), d.state)
block.Name = `{x},{y},{z}`
block:PivotTo(util.ChunkPosToCFrame(c.pos, Vector3.new(x, y, z)))
block.Parent = c.instance
task.desynchronize()
else
if c.instance:FindFirstChild(`{x},{y},{z}`) then
c.instance:FindFirstChild(`{x},{y},{z}`):Destroy()
local existing = c.instance:FindFirstChild(`{x},{y},{z}`)
if existing then
task.synchronize()
existing:Destroy()
task.desynchronize()
end
end
end

View File

@@ -28,6 +28,55 @@ local function findParent(i: Instance): Instance
end
local Mouse: Mouse = nil
local lastNormalId: Enum.NormalId? = nil
local function normalIdToOffset(normal: Enum.NormalId): Vector3
if normal == Enum.NormalId.Top then
return Vector3.new(0, 1, 0)
elseif normal == Enum.NormalId.Bottom then
return Vector3.new(0, -1, 0)
elseif normal == Enum.NormalId.Left then
return Vector3.new(-1, 0, 0)
elseif normal == Enum.NormalId.Right then
return Vector3.new(1, 0, 0)
elseif normal == Enum.NormalId.Back then
return Vector3.new(0, 0, 1)
elseif normal == Enum.NormalId.Front then
return Vector3.new(0, 0, -1)
end
return Vector3.new(0, 0, 0)
end
local function offsetChunkBlock(chunk: Vector3, block: Vector3, offset: Vector3)
local cx, cy, cz = chunk.X, chunk.Y, chunk.Z
local bx, by, bz = block.X + offset.X, block.Y + offset.Y, block.Z + offset.Z
if bx < 1 then
bx = 8
cx -= 1
elseif bx > 8 then
bx = 1
cx += 1
end
if by < 1 then
by = 8
cy -= 1
elseif by > 8 then
by = 1
cy += 1
end
if bz < 1 then
bz = 8
cz -= 1
elseif bz > 8 then
bz = 1
cz += 1
end
return Vector3.new(cx, cy, cz), Vector3.new(bx, by, bz)
end
-- Gets the block and normalid of the block (and surface) the player is looking at
function PlacementManager:Raycast()
@@ -40,6 +89,7 @@ function PlacementManager:Raycast()
if not objLookingAt then
PlacementManager.SelectionBox.Adornee = nil
script.RaycastResult.Value = nil
lastNormalId = nil
return
end
@@ -48,10 +98,12 @@ function PlacementManager:Raycast()
if parent:GetAttribute("ns") == true then
PlacementManager.SelectionBox.Adornee = nil
script.RaycastResult.Value = nil
lastNormalId = nil
return
end
PlacementManager.SelectionBox.Adornee = parent
script.RaycastResult.Value = parent
lastNormalId = dir
return parent, dir
end
@@ -59,16 +111,17 @@ function PlacementManager:RaycastGetResult()
return script.RaycastResult.Value
end
local placeRemote = game:GetService("ReplicatedStorage").PlaceBlock
local breakRemote = game:GetService("ReplicatedStorage").BreakBlock
local remotes = game:GetService("ReplicatedStorage"):WaitForChild("Remotes")
local placeRemote = remotes:WaitForChild("PlaceBlock")
local breakRemote = remotes:WaitForChild("BreakBlock")
local tickRemote = game:GetService("ReplicatedStorage").Tick
-- FIRES REMOTE
function PlacementManager:PlaceBlock(cx, cy, cz, x, y, z, blockData)
function PlacementManager:PlaceBlock(cx, cy, cz, x, y, z, blockId: string)
--print("placeblock")
--local chunk = ChunkManager:GetChunk(cx, cy, cz)
--chunk:CreateBlock(x, y, z, blockData)
placeRemote:FireServer(cx, cy, cz, x, y, z, blockData)
placeRemote:FireServer(cx, cy, cz, x, y, z, blockId)
end
-- FIRES REMOTE
@@ -97,11 +150,13 @@ function PlacementManager:GetBlockAtMouse(): nil | {chunk:Vector3, block: Vector
if selectedPart == nil then
PlacementManager.SelectionBox.Adornee = nil
script.RaycastResult.Value = nil
lastNormalId = nil
return nil
end
if not selectedPart.Parent then
PlacementManager.SelectionBox.Adornee = nil
script.RaycastResult.Value = nil
lastNormalId = nil
return nil
end
local chunkCoords = Util.BlockPosStringToCoords(selectedPart.Parent.Name)
@@ -114,6 +169,32 @@ function PlacementManager:GetBlockAtMouse(): nil | {chunk:Vector3, block: Vector
end
function PlacementManager:GetTargetAtMouse(): nil | {chunk:Vector3, block: Vector3, normal: Enum.NormalId}
local hit = PlacementManager:GetBlockAtMouse()
if not hit or not lastNormalId then
return nil
end
return {
chunk = hit.chunk,
block = hit.block,
normal = lastNormalId
}
end
function PlacementManager:GetPlacementAtMouse(): nil | {chunk:Vector3, block: Vector3}
local hit = PlacementManager:GetTargetAtMouse()
if not hit then
return nil
end
local offset = normalIdToOffset(hit.normal)
local placeChunk, placeBlock = offsetChunkBlock(hit.chunk, hit.block, offset)
return {
chunk = placeChunk,
block = placeBlock
}
end
function PlacementManager:Init()
game:GetService("RunService").Heartbeat:Connect(function()
local a,b = pcall(function()

View File

@@ -14,6 +14,10 @@ TerrainGen.ServerChunkCache = {} :: {[typeof("")]: typeof(Chunk.new(0,0,0))}
-- Load a chunk from the DataStore or generate it if not found
function TerrainGen:GetChunk(x, y, z)
local key = `{x},{y},{z}`
if TerrainGen.ServerChunkCache[key] then
return TerrainGen.ServerChunkCache[key]
end
-- Generate a new chunk if it doesn't exist
local chunk = Chunk.new(x, y, z)
@@ -38,6 +42,7 @@ function TerrainGen:GetChunk(x, y, z)
end
end
TerrainGen.ServerChunkCache[key] = chunk
return chunk
end

View File

@@ -8,7 +8,7 @@ local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Shared = ReplicatedStorage:WaitForChild("Shared")
local ModsFolder = ReplicatedStorage:WaitForChild("Mods")
local PlacementManager = require(Shared.PlacementManager)
local Util = require(Shared.Util)
local TG = require("./ServerChunkManager/TerrainGen")
do
@@ -64,11 +64,67 @@ ReplicatedStorage.RecieveChunkPacket.OnServerInvoke = function(plr: Player, x: n
end
local tickRemote = ReplicatedStorage.Tick
local remotes = ReplicatedStorage:WaitForChild("Remotes")
local placeRemote = remotes:WaitForChild("PlaceBlock")
local breakRemote = remotes:WaitForChild("BreakBlock")
local blocksFolder = ReplicatedStorage:WaitForChild("Blocks")
local function propogate(a, cx, cy, cz, x, y, z, bd)
task.synchronize()
tickRemote:FireAllClients(a, cx, cy, cz, x, y, z, bd)
task.desynchronize()
end
ReplicatedStorage.PlaceBlock.OnServerEvent:Connect(function(player, cx, cy, cz, x, y, z, blockData)
local MAX_REACH = 24
local blockIdMap = {}
local function rebuildBlockIdMap()
table.clear(blockIdMap)
for _, block in ipairs(blocksFolder:GetChildren()) do
local id = block:GetAttribute("n")
if id ~= nil then
blockIdMap[id] = id
blockIdMap[tostring(id)] = id
end
end
end
rebuildBlockIdMap()
blocksFolder.ChildAdded:Connect(rebuildBlockIdMap)
blocksFolder.ChildRemoved:Connect(rebuildBlockIdMap)
local function getPlayerPosition(player: Player): Vector3?
local character = player.Character
if not character then
return nil
end
local root = character:FindFirstChild("HumanoidRootPart")
if not root then
return nil
end
return root.Position
end
local function isWithinReach(player: Player, cx: number, cy: number, cz: number, x: number, y: number, z: number): boolean
local playerPos = getPlayerPosition(player)
if not playerPos then
return false
end
local blockPos = Util.ChunkPosToCFrame(Vector3.new(cx, cy, cz), Vector3.new(x, y, z)).Position
return (blockPos - playerPos).Magnitude <= MAX_REACH
end
local function resolveBlockId(blockId: any): string | number | nil
return blockIdMap[blockId]
end
local function getServerChunk(cx: number, cy: number, cz: number)
task.desynchronize()
local chunk = TG:GetChunk(cx, cy, cz)
task.synchronize()
return chunk
end
placeRemote.OnServerEvent:Connect(function(player, cx, cy, cz, x, y, z, blockId)
--print("place",player, cx, cy, cz, x, y, z, blockData)
if typeof(cx) ~= "number" or typeof(cy) ~= "number" or typeof(cz) ~= "number" then
@@ -77,16 +133,33 @@ ReplicatedStorage.PlaceBlock.OnServerEvent:Connect(function(player, cx, cy, cz,
if typeof(x) ~= "number" or typeof(y) ~= "number" or typeof(z) ~= "number" then
return
end
if x < 1 or x > 8 or y < 1 or y > 8 or z < 1 or z > 8 then
return
end
if math.abs(cx) > MAX_CHUNK_DIST or math.abs(cy) > MAX_CHUNK_DIST or math.abs(cz) > MAX_CHUNK_DIST then
--return
end
if not isWithinReach(player, cx, cy, cz, x, y, z) then
return
end
local resolvedId = resolveBlockId(blockId)
if not resolvedId then
return
end
--local chunk = TG:GetChunk(cx, cy, cz)
--PlacementManager:PlaceBlockLocal(cx, cy, cz, x, y, z, blockData)
propogate("B_C", cx, cy, cz, x, y, z, blockData)
local chunk = getServerChunk(cx, cy, cz)
if chunk:GetBlockAt(x, y, z) then
return
end
local data = {
id = resolvedId,
state = {}
}
chunk:CreateBlock(x, y, z, data)
propogate("B_C", cx, cy, cz, x, y, z, data)
end)
ReplicatedStorage.BreakBlock.OnServerEvent:Connect(function(player, cx, cy, cz, x, y, z)
breakRemote.OnServerEvent:Connect(function(player, cx, cy, cz, x, y, z)
--print("del",player, cx, cy, cz, x, y, z)
if typeof(cx) ~= "number" or typeof(cy) ~= "number" or typeof(cz) ~= "number" then
@@ -95,13 +168,22 @@ ReplicatedStorage.BreakBlock.OnServerEvent:Connect(function(player, cx, cy, cz,
if typeof(x) ~= "number" or typeof(y) ~= "number" or typeof(z) ~= "number" then
return
end
if x < 1 or x > 8 or y < 1 or y > 8 or z < 1 or z > 8 then
return
end
if math.abs(cx) > MAX_CHUNK_DIST or math.abs(cy) > MAX_CHUNK_DIST or math.abs(cz) > MAX_CHUNK_DIST then
return
end
if not isWithinReach(player, cx, cy, cz, x, y, z) then
return
end
--local chunk = TG:GetChunk(cx, cy, cz)
--PlacementManager:BreakBlockLocal(cx, cy, cz, x, y, z)
local chunk = getServerChunk(cx, cy, cz)
if not chunk:GetBlockAt(x, y, z) then
return
end
chunk:RemoveBlock(x, y, z)
propogate("B_D", cx, cy, cz, x, y, z, 0)
end)
task.desynchronize()
task.desynchronize()

View File

@@ -5,7 +5,6 @@ end
local ui = script.Parent
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local UIS = game:GetService("UserInputService")
ReplicatedStorage:WaitForChild("Objects"):WaitForChild("MLLoaded")
@@ -18,22 +17,6 @@ cd.Parent = game:GetService("Workspace"):FindFirstChildOfClass("Terrain")
sky.Parent = game:GetService("Workspace"):FindFirstChildOfClass("Terrain")
base.Parent = game:GetService("Workspace"):FindFirstChildOfClass("Terrain")
local PM = require(ReplicatedStorage.Shared.PlacementManager)
UIS.InputEnded:Connect(function(input: InputObject, gameProcessedEvent: boolean)
if input.UserInputType == Enum.UserInputType.MouseButton1 then
local mouseBlock = PM:GetBlockAtMouse()
if not mouseBlock then return end
PM:BreakBlock(mouseBlock.chunk.X, mouseBlock.chunk.Y, mouseBlock.chunk.Z, mouseBlock.block.X, mouseBlock.block.Y, mouseBlock.block.Z)
elseif input.UserInputType == Enum.UserInputType.MouseButton2 then
local mouseBlock = PM:GetBlockAtMouse()
if not mouseBlock then return end
PM:PlaceBlock(mouseBlock.chunk.X, mouseBlock.chunk.Y, mouseBlock.chunk.Z, mouseBlock.block.X, mouseBlock.block.Y, mouseBlock.block.Z, {
id = 2,
state = {}
})
end
end)
game:GetService("RunService").RenderStepped:Connect(function(dt)
local fps = math.round(1/dt)
@@ -76,4 +59,4 @@ game:GetService("RunService").RenderStepped:Connect(function(dt)
)
end)
end)
end)

View File

@@ -0,0 +1,92 @@
if not game:IsLoaded() then
game.Loaded:Wait()
end
local ReplicatedStorage = game:GetService("ReplicatedStorage")
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 keyToSlot = {
[Enum.KeyCode.One] = 1,
[Enum.KeyCode.Two] = 2,
[Enum.KeyCode.Three] = 3,
[Enum.KeyCode.Four] = 4,
[Enum.KeyCode.Five] = 5,
[Enum.KeyCode.Six] = 6,
[Enum.KeyCode.Seven] = 7,
[Enum.KeyCode.Eight] = 8,
[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))
end
end
table.sort(ids)
for i = 1, HOTBAR_SIZE do
hotbar[i] = ids[i] or ""
end
selectedSlot = math.clamp(selectedSlot, 1, HOTBAR_SIZE)
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
return
end
selectedSlot = slot
end
rebuildHotbar()
blocksFolder.ChildAdded:Connect(rebuildHotbar)
blocksFolder.ChildRemoved:Connect(rebuildHotbar)
UIS.InputBegan:Connect(function(input: InputObject, gameProcessedEvent: boolean)
if gameProcessedEvent then
return
end
local slot = keyToSlot[input.KeyCode]
if slot then
setSelectedSlot(slot)
return
end
if input.UserInputType == Enum.UserInputType.MouseButton1 then
local mouseBlock = PM:GetBlockAtMouse()
if not mouseBlock then
return
end
PM:BreakBlock(mouseBlock.chunk.X, mouseBlock.chunk.Y, mouseBlock.chunk.Z, mouseBlock.block.X, mouseBlock.block.Y, mouseBlock.block.Z)
elseif input.UserInputType == Enum.UserInputType.MouseButton2 then
local mouseBlock = PM:GetPlacementAtMouse()
if not mouseBlock then
return
end
local blockId = getSelectedBlockId()
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)

View File

@@ -3,6 +3,7 @@ if not game:IsLoaded() then
end
pcall(function()
task.synchronize()
game:GetService("Workspace"):WaitForChild("$blockscraft_server",5):Destroy()
end)
@@ -22,4 +23,4 @@ end
do
local CM = require(ReplicatedStorage:WaitForChild("Shared"):WaitForChild("ChunkManager"))
CM:Init()
end
end