Compare commits

..

12 Commits

Author SHA1 Message Date
bc62f8bd70 hotbar: wrap 2026-01-09 18:10:16 +02:00
4785795640 core: some fixes 2026-01-09 17:02:35 +02:00
2a0dd51659 core: fix hotbar 2026-01-08 23:52:32 +02:00
2c41f40151 chore: refractor 2026-01-08 22:58:58 +02:00
da7de7b08a core: loading screen, realism 2026-01-08 22:49:48 +02:00
a3d316c673 plr: decrease scale to 1.3 2026-01-08 20:54:52 +02:00
1fc4d501da chore: update default.project.json 2026-01-08 18:09:31 +00:00
8bedef61f5 placement: handle existing block edge case 2026-01-08 18:09:31 +00:00
d4992b3095 placement: fix selection box 2026-01-08 18:09:31 +00:00
b34830a493 core: fix building 2026-01-08 18:09:31 +00:00
165913ca51 core: broken 2026-01-07 22:51:11 +02:00
a9da63e90e core: impovements 2026-01-07 20:28:34 +02:00
56 changed files with 2481 additions and 616 deletions

1
.gitignore vendored
View File

@@ -1 +1,2 @@
Packages/ Packages/
ServerPackages/

View File

@@ -0,0 +1,13 @@
--!native
--!optimize 2
local ReplicatedStorage = game:GetService("ReplicatedStorage")
if not game:IsLoaded() then
game.Loaded:Wait()
end
local Bootstrap = require(ReplicatedStorage:WaitForChild("Client"):WaitForChild("Bootstrap"))
local objects = ReplicatedStorage:WaitForChild("Objects", 9e9)
objects:WaitForChild("MLLoaded", 9e9)
Bootstrap.start()

View File

@@ -0,0 +1,83 @@
--!native
--!optimize 2
local Players = game:GetService("Players")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local LoadingScreen = require(ReplicatedStorage.Client.LoadingScreen)
local ModLoader = require(ReplicatedStorage.Shared.ModLoader)
local ChunkManager = require(ReplicatedStorage.Shared.ChunkManager)
local PlacementManager = require(ReplicatedStorage.Shared.PlacementManager)
local Bootstrap = {}
local started = false
local function ensureFlag(name: string)
local objects = ReplicatedStorage:WaitForChild("Objects")
local existing = objects:FindFirstChild(name)
if existing and existing:IsA("BoolValue") then
return existing
end
local ready = Instance.new("BoolValue")
ready.Name = name
ready.Value = true
ready.Parent = objects
return ready
end
local contentProvider = game:GetService("ContentProvider")
local function waitForGameLoaded()
if game:IsLoaded() then
return
end
game.Loaded:Wait()
end
function Bootstrap.start()
if started then
return
end
started = true
contentProvider:PreloadAsync(game:GetDescendants())
ensureFlag("ClientReady")
local screen = LoadingScreen.mount()
screen.setStatus("Connecting...")
screen.setDetail("Waiting for server")
screen.setProgress(0.2)
task.wait(1)
waitForGameLoaded()
local modsFolder = ReplicatedStorage:WaitForChild("Mods")
local totalMods = #modsFolder:GetChildren()
local modProgressWeight = 0.6
ModLoader.loadModsC(function(index, total, modInstance, success)
total = total > 0 and total or math.max(totalMods, 1)
local ratio = index / total
screen.setStatus(`Modloading progress: {index}/{total}`)
screen.setDetail(`Loading {modInstance.Name}`)
screen.setProgress(0.05 + ratio * modProgressWeight)
if not success then
screen.setDetail(`Failed loading {modInstance.Name}; continuing`)
end
end)
ensureFlag("CSMLLoaded") -- needed
screen.setStatus("Joining world...")
screen.setDetail("Syncing with server")
screen.setProgress(0.7)
task.wait(0.5)
screen.close()
end
return Bootstrap

View File

@@ -0,0 +1,70 @@
--!native
--!optimize 2
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Roact = require(ReplicatedStorage.Packages.roact)
local ProgressBar = require(script.Parent.Components.ProgressBar)
local theme = {
background = Color3.fromRGB(17, 17, 27),
text = Color3.fromRGB(255, 255, 255),
subtext = Color3.fromRGB(205, 214, 244),
}
local function LoadingScreen(props)
local status = props.status or "Loading game..."
local detail = props.detail or "Preloading..."
local progress = props.progress or 0
local visible = props.visible
return Roact.createElement("ScreenGui", {
Name = "LoadingScreen",
DisplayOrder = 9999,
IgnoreGuiInset = true,
ResetOnSpawn = false,
ZIndexBehavior = Enum.ZIndexBehavior.Sibling,
Enabled = visible ~= false,
}, {
Root = Roact.createElement("Frame", {
Size = UDim2.fromScale(1, 1),
BackgroundColor3 = theme.background,
BorderSizePixel = 0,
}, {
Title = Roact.createElement("TextLabel", {
AnchorPoint = Vector2.new(0.5, 0.5),
Position = UDim2.new(0.5, 0, 0.5, 0),
BackgroundTransparency = 1,
Font = Enum.Font.Code,
Text = status,
TextColor3 = theme.text,
TextSize = 32,
TextWrapped = true,
AutomaticSize = Enum.AutomaticSize.XY,
TextXAlignment = Enum.TextXAlignment.Center,
TextYAlignment = Enum.TextYAlignment.Center,
}, {
Gradient = Roact.createElement("UIGradient", {
Color = ColorSequence.new({
ColorSequenceKeypoint.new(0, Color3.fromRGB(245, 194, 231)),
ColorSequenceKeypoint.new(0.5, Color3.fromRGB(203, 166, 247)),
ColorSequenceKeypoint.new(1, Color3.fromRGB(137, 180, 250)),
}),
Rotation = 30,
Transparency = NumberSequence.new({
NumberSequenceKeypoint.new(0, 0),
NumberSequenceKeypoint.new(1, 0),
}),
}),
}),
Progress = Roact.createElement(ProgressBar, {
AnchorPoint = Vector2.new(0.5, 1),
Position = UDim2.new(0.5, 0, 1, -32),
progress = progress,
detail = detail,
}),
}),
})
end
return LoadingScreen

View File

@@ -0,0 +1,101 @@
--!native
--!optimize 2
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Roact = require(ReplicatedStorage.Packages.roact)
local theme = {
background = Color3.fromRGB(30, 30, 46),
text = Color3.fromHex("#cdd6f4"),
holder = Color3.fromRGB(17, 17, 27),
fill = Color3.fromRGB(255, 255, 255),
stroke = Color3.fromRGB(49, 50, 68),
holderStroke = Color3.fromRGB(108, 112, 134),
}
local function ProgressBar(props)
local function progressToTransparency(v: number)
local p = math.clamp(v or 0, 0, 1)
return NumberSequence.new({
NumberSequenceKeypoint.new(0, 0),
NumberSequenceKeypoint.new(p, 0),
NumberSequenceKeypoint.new(p, 1),
NumberSequenceKeypoint.new(1, 1),
})
end
local gradientTransparency: NumberSequence
if typeof(props.progress) == "table" and props.progress.map then
gradientTransparency = props.progress:map(progressToTransparency)
else
gradientTransparency = progressToTransparency(props.progress or 0)
end
return Roact.createElement("Frame", {
Name = "ProgressBar",
AnchorPoint = props.AnchorPoint or Vector2.new(0.5, 1),
Position = props.Position or UDim2.new(0.5, 0, 1, -32),
Size = props.Size or UDim2.new(0, 500, 0, 32),
BackgroundColor3 = theme.background,
BorderSizePixel = 0,
}, {
Label = Roact.createElement("TextLabel", {
AnchorPoint = Vector2.new(0, 0),
Position = UDim2.new(0, 16, 0, -32),
Size = UDim2.new(1, -32, 0, 18),
BackgroundTransparency = 1,
Font = Enum.Font.Code,
Text = props.detail,
TextColor3 = theme.text,
TextSize = 18,
TextXAlignment = Enum.TextXAlignment.Left,
TextYAlignment = Enum.TextYAlignment.Top,
}),
Corner = Roact.createElement("UICorner", {
CornerRadius = UDim.new(0, 16),
}),
Stroke = Roact.createElement("UIStroke", {
Color = theme.stroke,
ApplyStrokeMode = Enum.ApplyStrokeMode.Border,
Thickness = 1,
}),
FillHolder = Roact.createElement("Frame", {
Name = "FillHolder",
AnchorPoint = Vector2.new(0, 1),
Position = UDim2.new(0, 8, 1, -8),
Size = UDim2.new(1, -16, 0, 16),
BackgroundColor3 = theme.holder,
BorderSizePixel = 0,
}, {
Corner = Roact.createElement("UICorner", {
CornerRadius = UDim.new(0, 8),
}),
Stroke = Roact.createElement("UIStroke", {
Color = theme.holderStroke,
ApplyStrokeMode = Enum.ApplyStrokeMode.Border,
Thickness = 1,
}),
Fill = Roact.createElement("Frame", {
AnchorPoint = Vector2.new(0, 0),
BackgroundColor3 = theme.fill,
BorderSizePixel = 0,
Size = UDim2.new(1, 0, 1, 0),
}, {
Corner = Roact.createElement("UICorner", {
CornerRadius = UDim.new(0, 8),
}),
Gradient = Roact.createElement("UIGradient", {
Color = ColorSequence.new({
ColorSequenceKeypoint.new(0, Color3.fromHex("#f5c2e7")),
ColorSequenceKeypoint.new(0.5, Color3.fromHex("#cba6f7")),
ColorSequenceKeypoint.new(1, Color3.fromHex("#89b4fa")),
}),
Transparency = gradientTransparency,
Rotation = 0,
}),
}),
}),
})
end
return ProgressBar

View File

@@ -0,0 +1,55 @@
--!native
--!optimize 2
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Roact = require(ReplicatedStorage.Packages.roact)
local Players = game:GetService("Players")
local App = require(script.App)
local LoadingScreen = {}
function LoadingScreen.mount(target: Instance?)
local playerGui = target or Players.LocalPlayer:WaitForChild("PlayerGui")
local statusBinding, setStatus = Roact.createBinding("Loading...")
local detailBinding, setDetail = Roact.createBinding("Preparing client")
local progressBinding, setProgress = Roact.createBinding(0)
local visibleBinding, setVisible = Roact.createBinding(true)
local handle = Roact.mount(Roact.createElement(App, {
status = statusBinding,
detail = detailBinding,
progress = progressBinding,
visible = visibleBinding,
}), playerGui, "RoactLoadingScreen")
local closed = false
local function close()
if closed then
return
end
closed = true
setVisible(false)
Roact.unmount(handle)
handle = nil
end
return {
setStatus = function(text: string)
setStatus(text)
end,
setDetail = function(text: string)
setDetail(text)
end,
setProgress = function(value: number)
setProgress(math.clamp(value or 0, 0, 1))
end,
show = function()
setVisible(true)
end,
hide = close,
close = close,
}
end
return LoadingScreen

View File

@@ -23,9 +23,6 @@ end
local warnedBlockIds = {} local warnedBlockIds = {}
function BlockManager:GetBlock(blockId: number, attr: {[typeof("")]: any}?) function BlockManager:GetBlock(blockId: number, attr: {[typeof("")]: any}?)
task.synchronize()
if not BlockManager.BlockIdMappings[blockId] then if not BlockManager.BlockIdMappings[blockId] then
if not warnedBlockIds[blockId] then if not warnedBlockIds[blockId] then
warnedBlockIds[blockId] = true warnedBlockIds[blockId] = true
@@ -59,8 +56,6 @@ function BlockManager:GetBlockRotated(blockId: number, face: Enum.NormalId, attr
local block = BlockManager:GetBlock(blockId, attr) local block = BlockManager:GetBlock(blockId, attr)
local rot = CFrame.new() local rot = CFrame.new()
task.synchronize()
if face == Enum.NormalId.Front then if face == Enum.NormalId.Front then
rot = CFrame.Angles(0, 0, 0) -- no rot rot = CFrame.Angles(0, 0, 0) -- no rot
elseif face == Enum.NormalId.Back then elseif face == Enum.NormalId.Back then

View File

@@ -75,7 +75,6 @@ function Chunk.from(x,y,z,data)
end end
function Chunk:DoesNeighboringBlockExist(rx, ry, rz, gx, gy, gz, offsetX, offsetY, offsetZ) function Chunk:DoesNeighboringBlockExist(rx, ry, rz, gx, gy, gz, offsetX, offsetY, offsetZ)
task.desynchronize()
-- Calculate the local position of the neighboring block -- Calculate the local position of the neighboring block
local neighborRX, neighborRY, neighborRZ = rx + offsetX, ry + offsetY, rz + offsetZ local neighborRX, neighborRY, neighborRZ = rx + offsetX, ry + offsetY, rz + offsetZ
local neighborGX, neighborGY, neighborGZ = gx, gy, gz local neighborGX, neighborGY, neighborGZ = gx, gy, gz
@@ -120,7 +119,6 @@ function Chunk:DoesNeighboringBlockExist(rx, ry, rz, gx, gy, gz, offsetX, offset
end end
function Chunk:IsBlockRenderable(rx, ry, rz) function Chunk:IsBlockRenderable(rx, ry, rz)
task.desynchronize()
local gx, gy, gz = self.pos.X, self.pos.Y, self.pos.Z local gx, gy, gz = self.pos.X, self.pos.Y, self.pos.Z
-- Check all six neighboring blocks -- Check all six neighboring blocks
local d = not ( local d = not (
@@ -144,7 +142,6 @@ function Chunk:PropogateChanges(x: number,y: number,z: number,d:BlockData)
end end
function Chunk:GetBlockAt(x,y,z) function Chunk:GetBlockAt(x,y,z)
task.desynchronize()
if not self.data[keyFromCoords(x, y, z)] then if not self.data[keyFromCoords(x, y, z)] then
return nil return nil
end end
@@ -158,27 +155,13 @@ function Chunk:CreateBlock(x: number,y: number,z: number,d:BlockData)
end end
function Chunk:RemoveBlock(x, y, z) function Chunk:RemoveBlock(x, y, z)
print("[DEBUG] Chunk:RemoveBlock called - Chunk:", self.pos, "Block coords:", x, y, z)
local blockKey = keyFromCoords(x, y, z) local blockKey = keyFromCoords(x, y, z)
local existingBlock = self.data[blockKey]
if existingBlock then
print("[DEBUG] Removing existing block with ID:", existingBlock.id)
else
print("[DEBUG] No block found at coords", x, y, z)
end
self.data[blockKey] = nil self.data[blockKey] = nil
self:PropogateChanges(x,y,z,0) self:PropogateChanges(x,y,z,0)
end end
function Chunk:RemoveBlockSmooth(x, y, z) function Chunk:RemoveBlockSmooth(x, y, z)
print("[DEBUG] Chunk:RemoveBlockSmooth called - Chunk:", self.pos, "Block coords:", x, y, z)
local blockKey = keyFromCoords(x, y, z) local blockKey = keyFromCoords(x, y, z)
local existingBlock = self.data[blockKey]
if existingBlock then
print("[DEBUG] Smooth removing existing block with ID:", existingBlock.id)
else
print("[DEBUG] Smooth remove: no block found at coords", x, y, z)
end
self.data[blockKey] = nil self.data[blockKey] = nil
self.delayedRemoval[blockKey] = true self.delayedRemoval[blockKey] = true
self:PropogateChanges(x,y,z,0) self:PropogateChanges(x,y,z,0)
@@ -190,7 +173,6 @@ end
function Chunk:Unload() function Chunk:Unload()
task.synchronize()
self.loaded = false self.loaded = false
-- SLOWCLEAR -- SLOWCLEAR
@@ -208,11 +190,22 @@ function Chunk:Unload()
end end
end end
task.synchronize()
self.instance.Parent = nil self.instance.Parent = nil
self.instance:Destroy() self.instance:Destroy()
self.unloadChunkHook() self.unloadChunkHook()
task.desynchronize() end)
end
function Chunk:UnloadImmediate()
self.loaded = false
pcall(function()
self.unloadChunkHook()
end)
pcall(function()
if self.instance then
self.instance.Parent = nil
self.instance:Destroy()
end
end) end)
end end

View File

@@ -43,8 +43,6 @@ local function Swait(l)
end end
local function propogateNeighboringBlockChanges(cx, cy, cz, x, y, z) local function propogateNeighboringBlockChanges(cx, cy, cz, x, y, z)
--warn("propogateNeighboringBlockChanges",cx,cy,cz,x,y,z)
-- updates block in another chunk
local c = Chunk.AllChunks[`{cx},{cy},{cz}`] local c = Chunk.AllChunks[`{cx},{cy},{cz}`]
if not c then return end if not c then return end
@@ -53,18 +51,14 @@ local function propogateNeighboringBlockChanges(cx, cy, cz, x, y, z)
if c:IsBlockRenderable(x, y, z) then if c:IsBlockRenderable(x, y, z) then
if c.instance:FindFirstChild(`{x},{y},{z}`) then return end 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) local block = BlockManager:GetBlockRotated(d.id, util.RotationStringToNormalId(d.state["r"] or "f"), d.state)
block.Name = `{x},{y},{z}` block.Name = `{x},{y},{z}`
block:PivotTo(util.ChunkPosToCFrame(c.pos, Vector3.new(x, y, z))) block:PivotTo(util.ChunkPosToCFrame(c.pos, Vector3.new(x, y, z)))
block.Parent = c.instance block.Parent = c.instance
task.desynchronize()
else else
local existing = c.instance:FindFirstChild(`{x},{y},{z}`) local existing = c.instance:FindFirstChild(`{x},{y},{z}`)
if existing then if existing then
task.synchronize()
existing:Destroy() existing:Destroy()
task.desynchronize()
end end
end end
end end
@@ -80,33 +74,27 @@ function ChunkBuilder:BuildChunk(c: typeof(Chunk.new(0,0,0)),parent: Instance?)
local finished = false local finished = false
local ch = Instance.new("Folder") local ch = Instance.new("Folder")
ch.Parent = parent ch.Parent = parent
ch.Name = `{c.pos.X},{c.pos.Y},{c.pos.Z}` ch.Name = `{c.pos.X},{c.pos.Y},{c.pos.Z}`
local conn = c.UpdateBlockBindableL.Event:Connect(function(x: number, y: number, z: number, d: BlockData) local conn = c.UpdateBlockBindableL.Event:Connect(function(x: number, y: number, z: number, d: BlockData)
task.desynchronize()
if finished == false then if finished == false then
newcache[`{x},{y},{z}`] = d newcache[`{x},{y},{z}`] = d
return return
end end
task.synchronize()
for _, o in pairs(NEIGHBOR_OFFSETS) do for _, o in pairs(NEIGHBOR_OFFSETS) do
--warn("propogate",o[1],o[2],o[3])
-- Adjust for chunk boundaries
local b = {x = x + o[1], y = y + o[2], z = z + o[3]} local b = {x = x + o[1], y = y + o[2], z = z + o[3]}
local ch = {x = c.pos.X, y = c.pos.Y, z = c.pos.Z} local chPos = {x = c.pos.X, y = c.pos.Y, z = c.pos.Z}
if b.x < 1 then ch.x = c.pos.X - 1 b.x = 8 end if b.x < 1 then chPos.x = c.pos.X - 1 b.x = 8 end
if b.x > 8 then ch.x = c.pos.X + 1 b.x = 1 end if b.x > 8 then chPos.x = c.pos.X + 1 b.x = 1 end
if b.y < 1 then ch.y = c.pos.Y - 1 b.y = 8 end if b.y < 1 then chPos.y = c.pos.Y - 1 b.y = 8 end
if b.y > 8 then ch.y = c.pos.Y + 1 b.y = 1 end if b.y > 8 then chPos.y = c.pos.Y + 1 b.y = 1 end
if b.z < 1 then ch.z = c.pos.Z - 1 b.z = 8 end if b.z < 1 then chPos.z = c.pos.Z - 1 b.z = 8 end
if b.z > 8 then ch.z = c.pos.Z + 1 b.z = 1 end if b.z > 8 then chPos.z = c.pos.Z + 1 b.z = 1 end
propogateNeighboringBlockChanges(ch.x, ch.y, ch.z, b.x, b.y, b.z) propogateNeighboringBlockChanges(chPos.x, chPos.y, chPos.z, b.x, b.y, b.z)
--BlockManager:GetBlock(ch.x)
end end
local blockName = `{x},{y},{z}` local blockName = `{x},{y},{z}`
@@ -116,12 +104,10 @@ function ChunkBuilder:BuildChunk(c: typeof(Chunk.new(0,0,0)),parent: Instance?)
c.delayedRemoval[blockName] = nil c.delayedRemoval[blockName] = nil
if existing then if existing then
task.defer(function() task.defer(function()
task.synchronize()
RunService.RenderStepped:Wait() RunService.RenderStepped:Wait()
if existing.Parent then if existing.Parent then
existing:Destroy() existing:Destroy()
end end
task.desynchronize()
end) end)
elseif DEBUG_GHOST then elseif DEBUG_GHOST then
print("[CHUNKBUILDER][GHOST] Delayed remove missing instance", c.pos, blockName) print("[CHUNKBUILDER][GHOST] Delayed remove missing instance", c.pos, blockName)
@@ -129,9 +115,7 @@ function ChunkBuilder:BuildChunk(c: typeof(Chunk.new(0,0,0)),parent: Instance?)
return return
end end
if existing then if existing then
task.synchronize()
existing:Destroy() existing:Destroy()
task.desynchronize()
elseif DEBUG_GHOST then elseif DEBUG_GHOST then
print("[CHUNKBUILDER][GHOST] Remove missing instance", c.pos, blockName) print("[CHUNKBUILDER][GHOST] Remove missing instance", c.pos, blockName)
end end
@@ -139,26 +123,20 @@ function ChunkBuilder:BuildChunk(c: typeof(Chunk.new(0,0,0)),parent: Instance?)
end end
if not c:IsBlockRenderable(x, y, z) then if not c:IsBlockRenderable(x, y, z) then
if existing then if existing then
task.synchronize()
existing:Destroy() existing:Destroy()
task.desynchronize()
end end
return return
end end
if existing then if existing then
task.synchronize()
existing:Destroy() existing:Destroy()
task.desynchronize()
end end
if not d then return end if not d then return end
if d.id == 0 then return end if d.id == 0 then return end
local N = util.RotationStringToNormalId(d.state["r"] or "f") local N = util.RotationStringToNormalId(d.state["r"] or "f")
task.synchronize()
local block = BlockManager:GetBlockRotated(d.id, N, d.state) local block = BlockManager:GetBlockRotated(d.id, N, d.state)
block.Name = blockName block.Name = blockName
block:PivotTo(util.ChunkPosToCFrame(c.pos, Vector3.new(x, y, z))) block:PivotTo(util.ChunkPosToCFrame(c.pos, Vector3.new(x, y, z)))
block.Parent = ch block.Parent = ch
task.desynchronize()
end) end)
c.unloadChunkHook = function() c.unloadChunkHook = function()
@@ -171,12 +149,10 @@ function ChunkBuilder:BuildChunk(c: typeof(Chunk.new(0,0,0)),parent: Instance?)
local p = 0 local p = 0
task.synchronize()
local hb = false local hb = false
for _,b in pairs(blocks) do
for a,b in pairs(blocks) do
hb = true hb = true
break
end end
local border = Instance.new("Part") local border = Instance.new("Part")
@@ -186,22 +162,18 @@ function ChunkBuilder:BuildChunk(c: typeof(Chunk.new(0,0,0)),parent: Instance?)
end end
for a,b in pairs(blocks) do for a,b in pairs(blocks) do
task.desynchronize()
local coords = util.BlockPosStringToCoords(a) local coords = util.BlockPosStringToCoords(a)
if not c:IsBlockRenderable(coords.X, coords.Y, coords.Z) then if not c or not c.IsBlockRenderable or not c:IsBlockRenderable(coords.X, coords.Y, coords.Z) then
if ch:FindFirstChild(a) then if ch:FindFirstChild(a) then
task.synchronize()
ch:FindFirstChild(a):Destroy() ch:FindFirstChild(a):Destroy()
task.desynchronize()
end end
continue continue
end end
task.desynchronize()
local N = util.RotationStringToNormalId(b.state["r"] or "f") local N = util.RotationStringToNormalId(b.state["r"] or "f")
task.synchronize()
local block = BlockManager:GetBlockRotated(b.id, N, b.state) local block = BlockManager:GetBlockRotated(b.id, N, b.state)
if ch:FindFirstChild(a) then local existing = ch:FindFirstChild(a)
ch:FindFirstChild(a):Destroy() if existing then
existing:Destroy()
end end
block.Name = a block.Name = a
block:PivotTo(util.ChunkPosToCFrame(c.pos, coords)) block:PivotTo(util.ChunkPosToCFrame(c.pos, coords))
@@ -215,16 +187,12 @@ function ChunkBuilder:BuildChunk(c: typeof(Chunk.new(0,0,0)),parent: Instance?)
finished = true finished = true
task.synchronize()
border:Destroy() border:Destroy()
task.desynchronize()
task.defer(function() task.defer(function()
task.synchronize()
for key, data in pairs(newcache) do for key, data in pairs(newcache) do
local coords = util.BlockPosStringToCoords(key) local coords = util.BlockPosStringToCoords(key)
for _, o in pairs(NEIGHBOR_OFFSETS) do for _, o in pairs(NEIGHBOR_OFFSETS) do
-- chunks are 8x8x8
local nb = {x = coords.X + o[1], y = coords.Y + o[2], z = coords.Z + o[3]} local nb = {x = coords.X + o[1], y = coords.Y + o[2], z = coords.Z + o[3]}
local chCoords = {x = c.pos.X, y = c.pos.Y, z = c.pos.Z} local chCoords = {x = c.pos.X, y = c.pos.Y, z = c.pos.Z}
if nb.x == 0 then chCoords.x = c.pos.X - 1 nb.x = 8 end if nb.x == 0 then chCoords.x = c.pos.X - 1 nb.x = 8 end
@@ -246,7 +214,7 @@ function ChunkBuilder:BuildChunk(c: typeof(Chunk.new(0,0,0)),parent: Instance?)
end end
continue continue
end end
if not c:IsBlockRenderable(coords.X, coords.Y, coords.Z) then if not c or not c.IsBlockRenderable or not c:IsBlockRenderable(coords.X, coords.Y, coords.Z) then
if existing then if existing then
existing:Destroy() existing:Destroy()
end end
@@ -267,7 +235,6 @@ function ChunkBuilder:BuildChunk(c: typeof(Chunk.new(0,0,0)),parent: Instance?)
newcache = nil newcache = nil
blocks = nil blocks = nil
end) end)
task.desynchronize()
end) end)
c.loaded = true c.loaded = true

View File

@@ -13,6 +13,7 @@ local Globals = require(script.Parent:WaitForChild("Globals"))
local remote = game:GetService("ReplicatedStorage"):WaitForChild("RecieveChunkPacket") local remote = game:GetService("ReplicatedStorage"):WaitForChild("RecieveChunkPacket")
local tickremote = game:GetService("ReplicatedStorage"):WaitForChild("Tick") local tickremote = game:GetService("ReplicatedStorage"):WaitForChild("Tick")
local Players = game:GetService("Players")
local ChunkFolder = Instance.new("Folder") local ChunkFolder = Instance.new("Folder")
ChunkFolder.Name = "$blockscraft_client" ChunkFolder.Name = "$blockscraft_client"
@@ -23,7 +24,7 @@ local CHUNK_RADIUS = Globals.RenderDistance or 5
local LOAD_BATCH = Globals.LoadBatch or 8 local LOAD_BATCH = Globals.LoadBatch or 8
local RESYNC_INTERVAL = Globals.ResyncInterval or 5 local RESYNC_INTERVAL = Globals.ResyncInterval or 5
local RESYNC_RADIUS = Globals.ResyncRadius or 2 local RESYNC_RADIUS = Globals.ResyncRadius or 2
local DEBUG_RESYNC = true local DEBUG_RESYNC = false
local FORCELOAD_CHUNKS = { local FORCELOAD_CHUNKS = {
{0, 1, 0} {0, 1, 0}
} }
@@ -31,6 +32,20 @@ local FORCELOAD_CHUNKS = {
local unloadingChunks = {} local unloadingChunks = {}
local pendingChunkRequests = {} local pendingChunkRequests = {}
local lastChunkKey: string? = nil
local lastHeavyTick = 0
local HEAVY_TICK_INTERVAL = 1.5
local lastUnloadSweep = 0
local UNLOAD_SWEEP_INTERVAL = 3 -- slower sweep cadence
local MAX_LOADED_CHUNKS = 0
local SPAWN_CHUNK_KEY: string? = nil
local playerFrozen = false
local storedMovementState = nil
local function worldToChunkCoord(v: number): number
return math.floor((v + 16) / 32)
end
local CHUNK_OFFSETS = {} local CHUNK_OFFSETS = {}
do do
for y = -CHUNK_RADIUS, CHUNK_RADIUS do for y = -CHUNK_RADIUS, CHUNK_RADIUS do
@@ -43,15 +58,155 @@ do
table.sort(CHUNK_OFFSETS, function(a, b) table.sort(CHUNK_OFFSETS, function(a, b)
return a[4] < b[4] return a[4] < b[4]
end) end)
MAX_LOADED_CHUNKS = math.max(1, math.floor(#CHUNK_OFFSETS * 2)) -- tighter cap than full render cube
if FORCELOAD_CHUNKS[1] then
local forced = FORCELOAD_CHUNKS[1]
SPAWN_CHUNK_KEY = `{forced[1]},{forced[2]},{forced[3]}`
end
end
function ChunkManager:UnloadAllNow()
for key, chunk in pairs(Chunk.AllChunks) do
unloadingChunks[key] = true
pcall(function()
if chunk.loaded then
chunk:UnloadImmediate()
end
end)
pcall(function()
chunk:Destroy()
end)
Chunk.AllChunks[key] = nil
unloadingChunks[key] = nil
end
end end
local function Swait(l) local function Swait(l)
task.synchronize()
for _ = 1, l do for _ = 1, l do
RunService.Stepped:Wait() RunService.Stepped:Wait()
end end
end end
local function setCharacterFrozen(shouldFreeze: boolean)
local player = Players.LocalPlayer
if not player then
return
end
local character = player.Character
if not character then
return
end
local humanoid = character:FindFirstChildOfClass("Humanoid")
local root = character:FindFirstChild("HumanoidRootPart")
if not humanoid or not root then
return
end
if shouldFreeze == playerFrozen then
return
end
if shouldFreeze then
if not storedMovementState then
storedMovementState = {
walkSpeed = humanoid.WalkSpeed,
autoRotate = humanoid.AutoRotate,
}
if humanoid.UseJumpPower then
storedMovementState.jumpPower = humanoid.JumpPower
else
storedMovementState.jumpHeight = humanoid.JumpHeight
end
end
humanoid.AutoRotate = false
humanoid.WalkSpeed = 0
if humanoid.UseJumpPower then
humanoid.JumpPower = 0
else
humanoid.JumpHeight = 0
end
root.Anchored = true
else
root.Anchored = false
if storedMovementState then
humanoid.AutoRotate = storedMovementState.autoRotate
humanoid.WalkSpeed = storedMovementState.walkSpeed
if humanoid.UseJumpPower and storedMovementState.jumpPower then
humanoid.JumpPower = storedMovementState.jumpPower
elseif storedMovementState.jumpHeight then
humanoid.JumpHeight = storedMovementState.jumpHeight
end
end
storedMovementState = nil
end
playerFrozen = shouldFreeze
end
local function getLocalPlayerChunkPos()
local player = Players.LocalPlayer
if not player then
return nil
end
local character = player.Character
if not character then
return nil
end
local root = character:FindFirstChild("HumanoidRootPart")
if not root then
return nil
end
local pos = root.Position
return {
x = worldToChunkCoord(pos.X),
y = worldToChunkCoord(pos.Y),
z = worldToChunkCoord(pos.Z)
}
end
local function isWithinRenderDistance(chunkPos: Vector3, centerChunkPos): boolean
if not centerChunkPos then
return false
end
return math.abs(chunkPos.X - centerChunkPos.x) <= CHUNK_RADIUS
and math.abs(chunkPos.Y - centerChunkPos.y) <= CHUNK_RADIUS
and math.abs(chunkPos.Z - centerChunkPos.z) <= CHUNK_RADIUS
end
local function shouldSkipUnload(key: string): boolean
return SPAWN_CHUNK_KEY ~= nil and key == SPAWN_CHUNK_KEY
end
local function scheduleChunkUnload(key: string, chunk)
if not chunk or unloadingChunks[key] then
return
end
unloadingChunks[key] = true
task.defer(function()
chunk:Unload()
chunk:Destroy()
Chunk.AllChunks[key] = nil
unloadingChunks[key] = nil
end)
end
local function evictOutOfRangeChunks(centerChunkPos)
if not centerChunkPos then
return
end
local loadedCount = 0
for key, loadedChunk in pairs(Chunk.AllChunks) do
if loadedChunk.loaded and not shouldSkipUnload(key) then
local inRange = isWithinRenderDistance(loadedChunk.pos, centerChunkPos)
if inRange then
loadedCount += 1
if loadedCount > MAX_LOADED_CHUNKS then
scheduleChunkUnload(key, loadedChunk)
end
else
scheduleChunkUnload(key, loadedChunk)
end
end
end
end
function ChunkManager:GetChunk(x, y, z) function ChunkManager:GetChunk(x, y, z)
local key = `{x},{y},{z}` local key = `{x},{y},{z}`
if Chunk.AllChunks[key] then if Chunk.AllChunks[key] then
@@ -59,14 +214,12 @@ function ChunkManager:GetChunk(x, y, z)
end end
if pendingChunkRequests[key] then if pendingChunkRequests[key] then
task.synchronize()
while pendingChunkRequests[key] do while pendingChunkRequests[key] do
task.wait() task.wait()
end end
return Chunk.AllChunks[key] return Chunk.AllChunks[key]
end end
task.synchronize()
pendingChunkRequests[key] = true pendingChunkRequests[key] = true
local ok, data = pcall(function() local ok, data = pcall(function()
return remote:InvokeServer(x, y, z) return remote:InvokeServer(x, y, z)
@@ -74,7 +227,6 @@ function ChunkManager:GetChunk(x, y, z)
if not ok then if not ok then
data = {} data = {}
end end
task.synchronize()
local ch = Chunk.from(x, y, z, data) local ch = Chunk.from(x, y, z, data)
Chunk.AllChunks[key] = ch Chunk.AllChunks[key] = ch
pendingChunkRequests[key] = nil pendingChunkRequests[key] = nil
@@ -90,7 +242,10 @@ local function ensureNeighboringChunksLoaded(x, y, z)
for _, offset in ipairs(offsets) do for _, offset in ipairs(offsets) do
local nx, ny, nz = x + offset[1], y + offset[2], z + offset[3] local nx, ny, nz = x + offset[1], y + offset[2], z + offset[3]
ChunkManager:GetChunk(nx, ny, nz):Tick() local neighbor = ChunkManager:GetChunk(nx, ny, nz)
if neighbor then
neighbor:Tick()
end
end end
end end
@@ -102,7 +257,6 @@ function ChunkManager:LoadChunk(x, y, z)
unloadingChunks[key] = true unloadingChunks[key] = true
task.defer(function() task.defer(function()
task.desynchronize()
ensureNeighboringChunksLoaded(x, y, z) ensureNeighboringChunksLoaded(x, y, z)
local chunk = Chunk.AllChunks[key] local chunk = Chunk.AllChunks[key]
@@ -111,11 +265,11 @@ function ChunkManager:LoadChunk(x, y, z)
Chunk.AllChunks[key] = chunk Chunk.AllChunks[key] = chunk
end end
task.synchronize()
local instance = ChunkBuilder:BuildChunk(chunk, ChunkFolder) local instance = ChunkBuilder:BuildChunk(chunk, ChunkFolder)
chunk.instance = instance chunk.instance = instance
chunk.loaded = true chunk.loaded = true
unloadingChunks[key] = nil unloadingChunks[key] = nil
evictOutOfRangeChunks(getLocalPlayerChunkPos())
end) end)
end end
@@ -129,7 +283,6 @@ function ChunkManager:RefreshChunk(x, y, z)
return return
end end
task.synchronize()
local ok, newData = pcall(function() local ok, newData = pcall(function()
return remote:InvokeServer(x, y, z) return remote:InvokeServer(x, y, z)
end) end)
@@ -198,9 +351,7 @@ function ChunkManager:RefreshChunk(x, y, z)
for _, child in ipairs(chunk.instance:GetChildren()) do for _, child in ipairs(chunk.instance:GetChildren()) do
if not newData[child.Name] then if not newData[child.Name] then
pruned += 1 pruned += 1
task.synchronize()
child:Destroy() child:Destroy()
task.desynchronize()
end end
end end
if DEBUG_RESYNC and pruned > 0 then if DEBUG_RESYNC and pruned > 0 then
@@ -210,7 +361,6 @@ function ChunkManager:RefreshChunk(x, y, z)
if DEBUG_RESYNC and (changed > 0 or removed > 0) then if DEBUG_RESYNC and (changed > 0 or removed > 0) then
print("[CHUNKMANAGER][RESYNC] Applied diff", key, "changed", changed, "removed", removed) print("[CHUNKMANAGER][RESYNC] Applied diff", key, "changed", changed, "removed", removed)
end end
task.desynchronize()
end end
function ChunkManager:ForceTick() function ChunkManager:ForceTick()
@@ -235,24 +385,36 @@ end
function ChunkManager:Tick() function ChunkManager:Tick()
ChunkManager:ForceTick() ChunkManager:ForceTick()
local player = game:GetService("Players").LocalPlayer local player = Players.LocalPlayer
if not player.Character then if not player.Character then
return return
end end
local pos = player.Character:GetPivot().Position local pos = player.Character:GetPivot().Position
local chunkPos = { local chunkPos = {
x = math.round(pos.X / 32), x = worldToChunkCoord(pos.X),
y = math.round(pos.Y / 32), y = worldToChunkCoord(pos.Y),
z = math.round(pos.Z / 32) z = worldToChunkCoord(pos.Z)
} }
local ck = `{chunkPos.x},{chunkPos.y},{chunkPos.z}`
local currentChunk = Chunk.AllChunks[ck]
local now = tick()
local shouldHeavyTick = (ck ~= lastChunkKey) or (now - lastHeavyTick >= HEAVY_TICK_INTERVAL)
lastChunkKey = ck
if shouldHeavyTick then
lastHeavyTick = now
end
setCharacterFrozen(not (currentChunk and currentChunk.loaded))
if shouldHeavyTick then
task.defer(function() task.defer(function()
local processed = 0 local processed = 0
for _, offset in ipairs(CHUNK_OFFSETS) do for _, offset in ipairs(CHUNK_OFFSETS) do
local cx, cy, cz = chunkPos.x + offset[1], chunkPos.y + offset[2], chunkPos.z + offset[3] local cx, cy, cz = chunkPos.x + offset[1], chunkPos.y + offset[2], chunkPos.z + offset[3]
local chunk = ChunkManager:GetChunk(cx, cy, cz) local chunk = ChunkManager:GetChunk(cx, cy, cz)
chunk.inhabitedTime = tick() if chunk then
chunk.inhabitedTime = now
if not chunk.loaded then if not chunk.loaded then
ChunkManager:LoadChunk(cx, cy, cz) ChunkManager:LoadChunk(cx, cy, cz)
processed += 1 processed += 1
@@ -261,14 +423,19 @@ function ChunkManager:Tick()
end end
end end
end end
end
end) end)
else
if currentChunk then
currentChunk.inhabitedTime = now
end
end
--[[ --[[
task.defer(function() task.defer(function()
for y = 0, 2 do for y = 0, 2 do
task.defer(function() task.defer(function()
for x = -CHUNK_RADIUS, CHUNK_RADIUS do for x = -CHUNK_RADIUS, CHUNK_RADIUS do
task.desynchronize()
for z = -CHUNK_RADIUS, CHUNK_RADIUS do for z = -CHUNK_RADIUS, CHUNK_RADIUS do
local cx, cy, cz = chunkPos.x + x, y, chunkPos.z + z local cx, cy, cz = chunkPos.x + x, y, chunkPos.z + z
local key = `{cx},{cy},{cz}` local key = `{cx},{cy},{cz}`
@@ -279,7 +446,6 @@ function ChunkManager:Tick()
Swait(2) Swait(2)
end end
end end
task.synchronize()
end end
end) end)
Swait(10) Swait(10)
@@ -287,16 +453,12 @@ function ChunkManager:Tick()
end) end)
--]] --]]
if now - lastUnloadSweep >= UNLOAD_SWEEP_INTERVAL then
lastUnloadSweep = now
for key, loadedChunk in pairs(Chunk.AllChunks) do for key, loadedChunk in pairs(Chunk.AllChunks) do
if tick() - loadedChunk.inhabitedTime > 15 and not unloadingChunks[key] then if now - loadedChunk.inhabitedTime > 30 and not unloadingChunks[key] and not shouldSkipUnload(key) then -- keep chunks around longer before unloading
unloadingChunks[key] = true scheduleChunkUnload(key, loadedChunk)
task.defer(function() end
task.synchronize()
loadedChunk:Unload()
loadedChunk:Destroy()
Chunk.AllChunks[key] = nil
unloadingChunks[key] = nil
end)
end end
end end
end end
@@ -308,9 +470,9 @@ function ChunkManager:ResyncAroundPlayer(radius: number)
end end
local pos = player.Character:GetPivot().Position local pos = player.Character:GetPivot().Position
local chunkPos = { local chunkPos = {
x = math.round(pos.X / 32), x = worldToChunkCoord(pos.X),
y = math.round(pos.Y / 32), y = worldToChunkCoord(pos.Y),
z = math.round(pos.Z / 32) z = worldToChunkCoord(pos.Z)
} }
for y = -radius, radius do for y = -radius, radius do
for x = -radius, radius do for x = -radius, radius do
@@ -340,6 +502,12 @@ function ChunkManager:Init()
ChunkFolder.Parent = game:GetService("Workspace") ChunkFolder.Parent = game:GetService("Workspace")
ChunkManager:ForceTick() ChunkManager:ForceTick()
tickremote.OnClientEvent:Connect(function(m)
if m == "U_ALL" then
ChunkManager:UnloadAllNow()
end
end)
task.defer(function() task.defer(function()
while true do while true do
wait(2) wait(2)

View File

@@ -0,0 +1,130 @@
--!native
--!optimize 2
local RunService = game:GetService("RunService")
if RunService:IsServer() then
error("ClientState can only be required on the client")
end
local Players = game:GetService("Players")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Replica = require(ReplicatedStorage.Packages.replica)
local ClientState = {}
local HOTBAR_SIZE = 10
local localPlayer = Players.LocalPlayer
local replicaForPlayer = nil
local changed = Instance.new("BindableEvent")
local function fireChanged()
changed:Fire()
end
local function onReplicaNew(replica)
local tags = replica.Tags or {}
if tags.UserId ~= localPlayer.UserId and tags.Player ~= localPlayer then
return
end
replicaForPlayer = replica
replica:OnChange(fireChanged)
fireChanged()
end
Replica.OnNew("ClientState", onReplicaNew)
Replica.RequestData()
function ClientState:IsReady(): boolean
return replicaForPlayer ~= nil
end
function ClientState:GetReplica()
return replicaForPlayer
end
function ClientState:GetSelectedSlot(): number?
if not replicaForPlayer then
return nil
end
return replicaForPlayer.Data.selectedSlot
end
local function getInventory()
return replicaForPlayer and replicaForPlayer.Data.inventory or nil
end
function ClientState:GetItemInfo(blockId: any)
if not replicaForPlayer or not blockId then
return nil
end
local inv = getInventory()
local entry = inv and inv[tostring(blockId)]
if not entry then
return nil
end
return {
id = tostring(blockId),
name = entry.name or tostring(blockId),
count = entry.count,
}
end
function ClientState:GetHotbarSlots(): {string}
if not replicaForPlayer then
local slots = table.create(HOTBAR_SIZE)
for i = 1, HOTBAR_SIZE do
slots[i] = ""
end
return slots
end
return replicaForPlayer.Data.hotbar or {}
end
function ClientState:GetSlotInfo(slot: number)
if not replicaForPlayer then
return nil
end
local hotbar = replicaForPlayer.Data.hotbar
if not hotbar then
return nil
end
local id = hotbar[slot]
if not id then
return nil
end
return ClientState:GetItemInfo(id)
end
function ClientState:GetSelectedBlock()
if not replicaForPlayer then
return nil
end
local slot = ClientState:GetSelectedSlot()
if not slot then
return nil
end
return ClientState:GetSlotInfo(slot)
end
function ClientState:SetSelectedSlot(slot: number)
if not replicaForPlayer then
return
end
local hotbar = replicaForPlayer.Data.hotbar
if not hotbar then
return
end
if slot and slot >= 1 and slot <= HOTBAR_SIZE then
replicaForPlayer:FireServer("SelectHotbarSlot", slot)
end
end
ClientState.Changed = changed.Event
return ClientState

View File

@@ -31,10 +31,13 @@ function ML.loadModsS()
end end
end end
function ML.loadModsC() function ML.loadModsC(onProgress: ((number, number, Instance, boolean) -> ())?)
print("[CSModLoader] Loading Mods") print("[CSModLoader] Loading Mods")
for _, m in pairs(ModsFolder:GetChildren()) do local mods = ModsFolder:GetChildren()
local total = #mods
for i, m in ipairs(mods) do
local success, reason = pcall(function() local success, reason = pcall(function()
-- ignore type err -- ignore type err
local mod: modContext = require(m) local mod: modContext = require(m)
@@ -44,6 +47,11 @@ function ML.loadModsC()
if not success then if not success then
warn(`[CSModLoader] Error loading {m.Name}: {reason}`) warn(`[CSModLoader] Error loading {m.Name}: {reason}`)
end end
if onProgress then
pcall(function()
onProgress(i, total, m, success)
end)
end
end end
end end

View File

@@ -0,0 +1,711 @@
--!native
--!optimize 2
local PlacementManager = {}
local ChunkManager = require("./ChunkManager")
local Util = require("./Util")
local DEBUG_PLACEMENT = false
local function debugPlacementLog(...: any)
if DEBUG_PLACEMENT then
Util.StudioLog(...)
end
end
local function debugPlacementWarn(...: any)
if DEBUG_PLACEMENT then
Util.StudioWarn(...)
end
end
PlacementManager.ChunkFolder = ChunkManager.ChunkFolder
local raycastParams = RaycastParams.new()
raycastParams.FilterDescendantsInstances = {PlacementManager.ChunkFolder}
raycastParams.FilterType = Enum.RaycastFilterType.Include
raycastParams.IgnoreWater = true
local Mouse: Mouse = nil
local lastNormalId: Enum.NormalId? = nil
local lastRaycastFailure: string? = nil
local lastSelectedChunkKey: string? = nil
local lastSelectedBlockKey: string? = nil
local duplicateResyncCooldown: {[string]: number} = {}
local BREAK_ROLLBACK_TIMEOUT = 0.6
local pendingBreaks = {}
local clearSelection
if _G.__BLOCKSCRAFT_PLACEMENT_MANAGER then
return _G.__BLOCKSCRAFT_PLACEMENT_MANAGER
end
_G.__BLOCKSCRAFT_PLACEMENT_MANAGER = PlacementManager
PlacementManager.SelectionBox = script.SelectionBox:Clone()
PlacementManager.SelectionBox.Name = "$SelectionBox"..(game:GetService("RunService"):IsServer() and "_SERVER" or "")
PlacementManager.SelectionBox.Parent = game:GetService("Workspace"):FindFirstChildOfClass("Terrain")
PlacementManager.SelectionBox.Adornee = nil
PlacementManager.SelectionBox:GetPropertyChangedSignal("Adornee"):Connect(function()
local adornee = PlacementManager.SelectionBox.Adornee
if not adornee then
return
end
adornee.AncestryChanged:Connect(function(_, parent)
if not parent then
clearSelection("adornee destroyed")
end
end)
end)
-- Trash method TODO: Fix this
local function findChunkFolderFromDescendant(inst: Instance): Instance?
local current = inst
while current and current.Parent do
if current.Parent == PlacementManager.ChunkFolder then
return current
end
-- Fallback: match by name in case the ChunkFolder reference differs (e.g. recreated/parented later)
if current.Parent:IsA("Folder") and current.Parent.Name == (PlacementManager.ChunkFolder and PlacementManager.ChunkFolder.Name) then
return current
end
current = current.Parent
end
return nil
end
local function findBlockRoot(inst: Instance, chunkFolder: Instance): Instance?
local current = inst
while current and current ~= chunkFolder do
if current:IsA("BasePart") then
return current
end
current = current.Parent
end
return nil
end
local function resolveBlockInstance(chunkFolder: Instance, chunkName: string, blockName: string): Instance?
local chunkInst = chunkFolder:FindFirstChild(chunkName)
if not chunkInst then
return nil
end
return chunkInst:FindFirstChild(blockName)
end
clearSelection = function(reason: string?)
PlacementManager.SelectionBox.Adornee = nil
PlacementManager.SelectionBox.Parent = nil
lastNormalId = nil
lastSelectedChunkKey = nil
lastSelectedBlockKey = nil
if reason then
lastRaycastFailure = reason
end
end
local function setSelection(target: Instance, parent: Instance)
PlacementManager.SelectionBox.Parent = parent
PlacementManager.SelectionBox.Adornee = target
end
local function findChunkAndBlock(inst: Instance): (string?, string?)
local root = PlacementManager.ChunkFolder
if not root then
return nil, nil
end
local current = inst
while current and current.Parent do
-- case: current parent is the chunk folder root; then current is the chunk itself (no block name yet)
if current.Parent == root then
return current.Name, inst.Name
end
-- case: grandparent is chunk folder root; parent is chunk, current is block/model
if current.Parent.Parent == root then
return current.Parent.Name, current.Name
end
current = current.Parent
end
return nil, nil
end
local function vectorToNormalId(normal: Vector3): Enum.NormalId
local ax, ay, az = math.abs(normal.X), math.abs(normal.Y), math.abs(normal.Z)
if ax >= ay and ax >= az then
return normal.X >= 0 and Enum.NormalId.Right or Enum.NormalId.Left
elseif ay >= ax and ay >= az then
return normal.Y >= 0 and Enum.NormalId.Top or Enum.NormalId.Bottom
else
return normal.Z >= 0 and Enum.NormalId.Back or Enum.NormalId.Front
end
end
local function makeChunkKey(cx: number, cy: number, cz: number): string
return `{cx},{cy},{cz}`
end
local function makeBlockKey(x: number, y: number, z: number): string
return `{x},{y},{z}`
end
local function getPendingBreak(chunkKey: string, blockKey: string)
local chunkMap = pendingBreaks[chunkKey]
if not chunkMap then
return nil
end
return chunkMap[blockKey]
end
local function clearPendingBreak(chunkKey: string, blockKey: string)
local chunkMap = pendingBreaks[chunkKey]
if not chunkMap then
return
end
chunkMap[blockKey] = nil
if not next(chunkMap) then
pendingBreaks[chunkKey] = nil
end
end
local function clearPendingBreaksForChunk(chunkKey: string)
pendingBreaks[chunkKey] = nil
end
local function scheduleBreakRollback(cx: number, cy: number, cz: number, x: number, y: number, z: number)
task.delay(BREAK_ROLLBACK_TIMEOUT, function()
local chunkKey = makeChunkKey(cx, cy, cz)
local blockKey = makeBlockKey(x, y, z)
local pending = getPendingBreak(chunkKey, blockKey)
if not pending then
return
end
clearPendingBreak(chunkKey, blockKey)
local chunk = ChunkManager:GetChunk(cx, cy, cz)
if pending.data and chunk then
chunk:CreateBlock(x, y, z, pending.data)
end
ChunkManager:RefreshChunk(cx, cy, cz)
end)
end
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
local function getPlayerPosition(): Vector3?
local player = game:GetService("Players").LocalPlayer
local character = player and player.Character
if not character then
return nil
end
local root = character:FindFirstChild("HumanoidRootPart")
return root and root.Position or nil
end
local MAX_REACH = 512
local function isWithinReach(cx: number, cy: number, cz: number, x: number, y: number, z: number): boolean
-- Client-side reach loosened; rely on server authority
return true
end
local function ensureChunkFolder(): Instance?
if PlacementManager.ChunkFolder and PlacementManager.ChunkFolder.Parent then
return PlacementManager.ChunkFolder
end
local found = workspace:FindFirstChild("$blockscraft_client")
if found then
PlacementManager.ChunkFolder = found
return found
end
return nil
end
-- Gets the block and normalid of the block (and surface) the player is looking at
function PlacementManager:Raycast(skipSelection: boolean?)
if not Mouse then
Mouse = game:GetService("Players").LocalPlayer:GetMouse()
end
local chunkFolder = ensureChunkFolder()
if not chunkFolder then
if not skipSelection then
clearSelection("chunk folder missing")
end
script.RaycastResult.Value = nil
return
end
raycastParams.FilterDescendantsInstances = {chunkFolder}
local cam = workspace.CurrentCamera
if not cam then
lastRaycastFailure = "no camera"
return
end
local ray = Mouse.UnitRay
local result = workspace:Raycast(ray.Origin, ray.Direction * MAX_REACH, raycastParams)
if not result then
if not skipSelection then
clearSelection("raycast miss")
end
script.RaycastResult.Value = nil
debugPlacementLog("[PLACE][CLIENT][RAYCAST]", "miss")
return
end
local objLookingAt = result.Instance
if not objLookingAt then
if not skipSelection then
clearSelection("raycast nil instance")
end
script.RaycastResult.Value = nil
debugPlacementWarn("[PLACE][CLIENT][RAYCAST]", "nil instance in result")
return
end
local hitChunkFolder = findChunkFolderFromDescendant(objLookingAt)
if not hitChunkFolder then
debugPlacementWarn(
"[PLACE][CLIENT][REJECT]",
"target not in chunk folder",
objLookingAt:GetFullName(),
"parent",
objLookingAt.Parent and objLookingAt.Parent:GetFullName() or "nil"
)
if not skipSelection then
clearSelection("target not in chunk folder")
end
script.RaycastResult.Value = nil
return
end
if hitChunkFolder:GetAttribute("ns") == true then
debugPlacementWarn(
"[PLACE][CLIENT][REJECT]",
"chunk flagged ns",
hitChunkFolder:GetFullName()
)
if not skipSelection then
clearSelection("target chunk marked ns")
end
script.RaycastResult.Value = nil
return
end
PlacementManager.ChunkFolder = chunkFolder
local blockRoot = findBlockRoot(objLookingAt, chunkFolder) or objLookingAt
local chunkName, blockName = findChunkAndBlock(blockRoot)
if not chunkName or not blockName then
if not skipSelection then
clearSelection("failed to resolve chunk/block")
end
script.RaycastResult.Value = nil
return
end
local okChunk, chunkCoords = pcall(function()
return Util.BlockPosStringToCoords(chunkName)
end)
local okBlock, blockCoords = pcall(function()
return Util.BlockPosStringToCoords(blockName)
end)
if not okChunk or not okBlock then
if not skipSelection then
clearSelection("failed to parse chunk/block names")
end
script.RaycastResult.Value = nil
return
end
local chunkKey = makeChunkKey(chunkCoords.X, chunkCoords.Y, chunkCoords.Z)
local blockKey = makeBlockKey(blockCoords.X, blockCoords.Y, blockCoords.Z)
-- block is being optimistically broken, do not highlight it
if getPendingBreak(chunkKey, blockKey) then
if not skipSelection then
clearSelection("block pending break")
end
script.RaycastResult.Value = nil
return
end
-- hide selection if block no longer exists (air/removed)
local chunk = ChunkManager:GetChunk(chunkCoords.X, chunkCoords.Y, chunkCoords.Z)
local blockData = chunk and chunk:GetBlockAt(blockCoords.X, blockCoords.Y, blockCoords.Z)
if not blockData or blockData == 0 or blockData.id == 0 then
if not skipSelection then
clearSelection("block missing/air")
end
script.RaycastResult.Value = nil
return
end
local blockInstance = resolveBlockInstance(chunkFolder, chunkName, blockName) or blockRoot
if not blockInstance then
if not skipSelection then
clearSelection("missing block instance")
end
script.RaycastResult.Value = nil
return
end
lastRaycastFailure = nil
if not skipSelection then
if lastSelectedChunkKey ~= chunkKey or lastSelectedBlockKey ~= blockKey then
setSelection(blockInstance, PlacementManager.ChunkFolder)
lastSelectedChunkKey = chunkKey
lastSelectedBlockKey = blockKey
end
end
script.RaycastResult.Value = objLookingAt
lastNormalId = vectorToNormalId(result.Normal)
debugPlacementLog(
"[PLACE][CLIENT][RAYCAST][HIT]",
blockInstance:GetFullName(),
"chunkFolder",
hitChunkFolder:GetFullName(),
"blockName",
blockInstance.Name,
"normal",
lastNormalId.Name
)
return objLookingAt, lastNormalId
end
function PlacementManager:RaycastGetResult()
return script.RaycastResult.Value
end
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, blockId: string)
debugPlacementLog("[PLACE][CLIENT][PLACE_CALL]", "chunk", cx, cy, cz, "block", x, y, z, "blockId", blockId)
if blockId == "hand" then
debugPlacementWarn("[PLACE][CLIENT][REJECT]", "hand cannot place")
return
end
if typeof(cx) ~= "number" or typeof(cy) ~= "number" or typeof(cz) ~= "number" then
debugPlacementWarn("[PLACE][CLIENT][REJECT]", "chunk type", cx, cy, cz, x, y, z, blockId)
return
end
if typeof(x) ~= "number" or typeof(y) ~= "number" or typeof(z) ~= "number" then
debugPlacementWarn("[PLACE][CLIENT][REJECT]", "block type", cx, cy, cz, x, y, z, blockId)
return
end
if x < 1 or x > 8 or y < 1 or y > 8 or z < 1 or z > 8 then
debugPlacementWarn("[PLACE][CLIENT][REJECT]", "block bounds", cx, cy, cz, x, y, z, blockId)
return
end
if not isWithinReach(cx, cy, cz, x, y, z) then
debugPlacementWarn("[PLACE][CLIENT][REJECT]", "reach", cx, cy, cz, x, y, z, blockId)
return
end
-- ensure chunk is present/rendered client-side
local chunk = ChunkManager:GetChunk(cx, cy, cz)
if chunk and not chunk.loaded then
ChunkManager:LoadChunk(cx, cy, cz)
end
if not chunk then
debugPlacementWarn("[PLACE][CLIENT][REJECT]", "missing chunk", cx, cy, cz, x, y, z, blockId)
return
end
-- allow sending even if the client thinks the id matches; server truth wins
if chunk then
local existing = chunk:GetBlockAt(x, y, z)
local existingId = existing and existing.id
if existingId and tostring(existingId) == tostring(blockId) then
debugPlacementLog(
"[PLACE][CLIENT][DUPLICATE]",
"still sending",
"chunk",
cx,
cy,
cz,
"block",
x,
y,
z,
"existingId",
existingId,
"blockId",
blockId
)
local ck = makeChunkKey(cx, cy, cz)
local last = duplicateResyncCooldown[ck]
if not last or (tick() - last) > 0.5 then
duplicateResyncCooldown[ck] = tick()
task.defer(function()
ChunkManager:RefreshChunk(cx, cy, cz)
end)
end
else
debugPlacementLog(
"[PLACE][CLIENT][EXISTING]",
"chunk",
cx,
cy,
cz,
"block",
x,
y,
z,
"existingId",
existingId
)
end
end
-- optimistic local apply; server will correct on tick
if chunk then
chunk:CreateBlock(x, y, z, {
id = tonumber(blockId) or blockId,
state = {},
})
end
debugPlacementLog("[PLACE][CLIENT][SEND]", cx, cy, cz, x, y, z, blockId)
placeRemote:FireServer(cx, cy, cz, x, y, z, blockId)
end
-- FIRES REMOTE
function PlacementManager:BreakBlock(cx, cy, cz, x, y, z)
if typeof(cx) ~= "number" or typeof(cy) ~= "number" or typeof(cz) ~= "number" then
return
end
if typeof(x) ~= "number" or typeof(y) ~= "number" or typeof(z) ~= "number" then
debugPlacementWarn("[BREAK][CLIENT][REJECT]", "block type", cx, cy, cz, x, y, z)
return
end
if x < 1 or x > 8 or y < 1 or y > 8 or z < 1 or z > 8 then
debugPlacementWarn("[BREAK][CLIENT][REJECT]", "block bounds", cx, cy, cz, x, y, z)
return
end
if not isWithinReach(cx, cy, cz, x, y, z) then
debugPlacementWarn("[BREAK][CLIENT][REJECT]", "reach", cx, cy, cz, x, y, z)
return
end
local chunk = ChunkManager:GetChunk(cx, cy, cz)
local blockData = chunk and chunk:GetBlockAt(x, y, z) or nil
local chunkKey = makeChunkKey(cx, cy, cz)
local blockKey = makeBlockKey(x, y, z)
if getPendingBreak(chunkKey, blockKey) then
debugPlacementLog("[BREAK][CLIENT][SKIP]", "pending rollback", cx, cy, cz, x, y, z)
return
end
pendingBreaks[chunkKey] = pendingBreaks[chunkKey] or {}
pendingBreaks[chunkKey][blockKey] = {
data = blockData,
time = tick(),
}
if blockData then
chunk:RemoveBlock(x, y, z)
end
scheduleBreakRollback(cx, cy, cz, x, y, z)
debugPlacementLog("[BREAK][CLIENT][SEND]", cx, cy, cz, x, y, z)
breakRemote:FireServer(cx, cy, cz, x, y, z)
end
-- CLIENTSIDED: only apply server-validated changes
local function applyPlaceBlockLocal(cx, cy, cz, x, y, z, blockData)
local chunk = ChunkManager:GetChunk(cx, cy, cz)
if chunk and not chunk.loaded then
ChunkManager:LoadChunk(cx, cy, cz)
end
chunk:CreateBlock(x, y, z, blockData)
end
-- CLIENTSIDED: only apply server-validated changes
local function applyBreakBlockLocal(cx, cy, cz, x, y, z)
local chunk = ChunkManager:GetChunk(cx, cy, cz)
if chunk and not chunk.loaded then
ChunkManager:LoadChunk(cx, cy, cz)
end
if not chunk then
return
end
local chunkKey = makeChunkKey(cx, cy, cz)
local blockKey = makeBlockKey(x, y, z)
if getPendingBreak(chunkKey, blockKey) then
clearPendingBreak(chunkKey, blockKey)
return
end
chunk:RemoveBlock(x, y, z)
end
function PlacementManager:GetBlockAtMouse(skipSelection: boolean?): nil | {chunk:Vector3, block: Vector3}
pcall(function()
PlacementManager:Raycast(skipSelection)
end)
local selectedPart = PlacementManager:RaycastGetResult()
--print(selectedPart and selectedPart:GetFullName() or nil)
if selectedPart == nil then
if not skipSelection then
clearSelection()
end
script.RaycastResult.Value = nil
debugPlacementLog("[PLACE][CLIENT][TARGET]", "no selectedPart after raycast", lastRaycastFailure)
return nil
end
local chunkName, blockName = findChunkAndBlock(selectedPart)
if not chunkName or not blockName then
debugPlacementWarn(
"[PLACE][CLIENT][TARGET]",
"failed to find chunk/block from selection",
selectedPart:GetFullName()
)
return nil
end
local okChunk, chunkCoords = pcall(function()
return Util.BlockPosStringToCoords(chunkName :: string)
end)
local okBlock, blockCoords = pcall(function()
return Util.BlockPosStringToCoords(blockName :: string)
end)
if not okChunk or not okBlock then
debugPlacementWarn(
"[PLACE][CLIENT][TARGET]",
"failed to parse names",
"chunkName",
chunkName,
"blockName",
blockName
)
return nil
end
debugPlacementLog(
"[PLACE][CLIENT][TARGET]",
"chunk",
chunkName,
"block",
blockName,
"normal",
(lastNormalId and lastNormalId.Name) or "nil"
)
return {
chunk = chunkCoords,
block = blockCoords
}
end
function PlacementManager:GetTargetAtMouse(skipSelection: boolean?): nil | {chunk:Vector3, block: Vector3, normal: Enum.NormalId}
local hit = PlacementManager:GetBlockAtMouse(skipSelection)
if not hit then
return nil
end
local normal = lastNormalId or Enum.NormalId.Top
return {
chunk = hit.chunk,
block = hit.block,
normal = normal
}
end
function PlacementManager:GetPlacementAtMouse(skipSelection: boolean?): nil | {chunk:Vector3, block: Vector3}
local hit = PlacementManager:GetTargetAtMouse(skipSelection)
if not hit then
return nil
end
local offset = normalIdToOffset(hit.normal)
local placeChunk, placeBlock = offsetChunkBlock(hit.chunk, hit.block, offset)
debugPlacementLog(
"[PLACE][CLIENT][PLACE_TARGET]",
"target chunk",
hit.chunk,
"target block",
hit.block,
"normal",
hit.normal.Name,
"place chunk",
placeChunk,
"place block",
placeBlock
)
return {
chunk = placeChunk,
block = placeBlock
}
end
function PlacementManager:DebugGetPlacementOrWarn(skipSelection: boolean?)
local placement = PlacementManager:GetPlacementAtMouse(skipSelection)
if not placement then
debugPlacementWarn("[PLACE][CLIENT][REJECT]", "no placement target under mouse", lastRaycastFailure)
end
return placement
end
function PlacementManager:Init()
game:GetService("RunService").RenderStepped:Connect(function()
local a,b = pcall(function()
PlacementManager:Raycast()
end)
if not a then
clearSelection("raycast error")
script.RaycastResult.Value = nil
end
end)
tickRemote.OnClientEvent:Connect(function(m, cx, cy, cz, x, y, z, d)
--warn("PROPOGATED TICK", m, cx, cy, cz, x, y, z, d)
if m == "B_C" then
applyPlaceBlockLocal(cx, cy, cz, x, y ,z, d)
end
if m == "B_D" then
applyBreakBlockLocal(cx, cy, cz, x, y ,z)
end
if m == "C_R" then
clearPendingBreaksForChunk(makeChunkKey(cx, cy, cz))
ChunkManager:RefreshChunk(cx, cy, cz)
end
end)
end
return PlacementManager

View File

@@ -0,0 +1,35 @@
--!native
--!optimize 2
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local PlacementState = {}
local selectedId: string = ""
local selectedName: string = ""
local changed = Instance.new("BindableEvent")
PlacementState.Changed = changed.Event
local valueObject = ReplicatedStorage:FindFirstChild("HotbarSelectedBlock")
if not valueObject then
valueObject = Instance.new("StringValue")
valueObject.Name = "HotbarSelectedBlock"
valueObject.Parent = ReplicatedStorage
end
PlacementState.ValueObject = valueObject
function PlacementState:SetSelected(id: string?, name: string?)
selectedId = id or ""
selectedName = name or selectedId
valueObject.Value = selectedName or ""
local Util = require(ReplicatedStorage:WaitForChild("Shared"):WaitForChild("Util"))
Util.StudioLog("[PLACE][CLIENT][SELECT]", "id", selectedId, "name", selectedName)
changed:Fire(selectedId, selectedName)
end
function PlacementState:GetSelected()
return selectedId, selectedName
end
return PlacementState

View File

@@ -1,5 +1,24 @@
local RunService = game:GetService("RunService")
local IS_STUDIO = RunService:IsStudio()
local ENABLE_STUDIO_LOG = false
local module = {} local module = {}
-- Prints only when running in Studio (avoids noisy live logs)
function module.StudioLog(...: any)
if not IS_STUDIO or not ENABLE_STUDIO_LOG then
return
end
print(...)
end
function module.StudioWarn(...: any)
if not IS_STUDIO or not ENABLE_STUDIO_LOG then
return
end
warn(...)
end
function module.isNaN(n: number): boolean function module.isNaN(n: number): boolean
-- NaN is never equal to itself -- NaN is never equal to itself
return n ~= n return n ~= n

View File

@@ -0,0 +1,196 @@
--!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

View File

@@ -0,0 +1,102 @@
--!native
--!optimize 2
local TerrainGen = {}
local ChunkManager = require(game:GetService("ReplicatedStorage"):WaitForChild("Shared").ChunkManager)
local Chunk = require(game:GetService("ReplicatedStorage"):WaitForChild("Shared").ChunkManager.Chunk)
TerrainGen.ServerChunkCache = {} :: {[typeof("")]: typeof(Chunk.new(0,0,0))}
local function chunkKeyFromCoords(x: number, y: number, z: number): string
return `{x},{y},{z}`
end
function TerrainGen:UnloadAllChunks(): number
local count = 0
for key in pairs(TerrainGen.ServerChunkCache) do
TerrainGen.ServerChunkCache[key] = nil
count += 1
end
return count
end
local function worldToChunkCoord(v: number): number
return math.floor((v + 16) / 32)
end
function TerrainGen:PreloadNearPlayers(radius: number, yRadius: number?): number
local Players = game:GetService("Players")
local r = radius or 5
local ry = yRadius or 1
local loaded = 0
for _, player in ipairs(Players:GetPlayers()) do
local character = player.Character
local root = character and character:FindFirstChild("HumanoidRootPart")
if root then
local pos = root.Position
local cx = worldToChunkCoord(pos.X)
local cy = worldToChunkCoord(pos.Y)
local cz = worldToChunkCoord(pos.Z)
for y = -ry, ry do
for x = -r, r do
for z = -r, r do
TerrainGen:GetChunk(cx + x, cy + y, cz + z)
loaded += 1
end
end
end
end
end
return loaded
end
-- Load a chunk from the DataStore or generate it if not found
function TerrainGen:GetChunk(x, y, z)
local key = chunkKeyFromCoords(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)
if y == 1 then
for cx = 1, 8 do
for cz = 1, 8 do
chunk:CreateBlock(cx, 1, cz, { id = "mc:grass_block", state = {} })
end
end
end
if y == 0 then
for cx = 1, 8 do
for cy = 1, 8 do
for cz = 1, 8 do
chunk:CreateBlock(cx, cy, cz, { id = "mc:dirt_block", state = {} })
end
end
end
end
TerrainGen.ServerChunkCache[key] = chunk
return chunk
end
-- Fake Chunk
function TerrainGen:GetFakeChunk(x, y, z)
-- Generate a new chunk if it doesn't exist
local chunk = Chunk.new(x, y, z)
for cy = 1,8 do
for cx = 1, 8 do
for cz = 1, 8 do
chunk:CreateBlock(cx, cy, cz, { id = "invalid", state = {} })
end
end
end
return chunk
end
TerrainGen.CM = ChunkManager
return TerrainGen

View File

@@ -1,18 +1,39 @@
--!native --!native
--!optimize 2 --!optimize 2
print("Hello world!")
task.synchronize()
local ReplicatedStorage = game:GetService("ReplicatedStorage") local ReplicatedStorage = game:GetService("ReplicatedStorage")
local ServerStorage = game:GetService("ServerStorage")
local ClientStateService = require(script.Parent.ClientState)
local Shared = ReplicatedStorage:WaitForChild("Shared") local Shared = ReplicatedStorage:WaitForChild("Shared")
local ModsFolder = ReplicatedStorage:WaitForChild("Mods") local ModsFolder = ReplicatedStorage:WaitForChild("Mods")
local BlocksFolderRS = ReplicatedStorage:FindFirstChild("Blocks") or Instance.new("Folder")
BlocksFolderRS.Name = "Blocks"
BlocksFolderRS.Parent = ReplicatedStorage
local BlocksFolderSS = ServerStorage:FindFirstChild("Blocks") or Instance.new("Folder")
BlocksFolderSS.Name = "Blocks"
BlocksFolderSS.Parent = ServerStorage
local Util = require(Shared.Util) local Util = require(Shared.Util)
local TG = require("./ServerChunkManager/TerrainGen") local TG = require(script.TerrainGen)
local Players = game:GetService("Players")
local blockIdMap = {}
local rebuildBlockIdMap
local function syncBlocksToServerStorage()
BlocksFolderSS:ClearAllChildren()
for _, child in ipairs(BlocksFolderRS:GetChildren()) do
child:Clone().Parent = BlocksFolderSS
end
ClientStateService:SetBlocksFolder(BlocksFolderSS)
if rebuildBlockIdMap then
rebuildBlockIdMap()
end
end
BlocksFolderRS.ChildAdded:Connect(syncBlocksToServerStorage)
BlocksFolderRS.ChildRemoved:Connect(syncBlocksToServerStorage)
do do
local workspaceModFolder = game:GetService("Workspace"):WaitForChild("mods") local workspaceModFolder = game:GetService("Workspace"):WaitForChild("mods")
@@ -25,6 +46,8 @@ end
local ML = require(Shared.ModLoader) local ML = require(Shared.ModLoader)
ML.loadModsS() ML.loadModsS()
syncBlocksToServerStorage()
ClientStateService:Init()
do do
local bv = Instance.new("BoolValue") local bv = Instance.new("BoolValue")
@@ -70,17 +93,16 @@ local tickRemote = ReplicatedStorage.Tick
local remotes = ReplicatedStorage:WaitForChild("Remotes") local remotes = ReplicatedStorage:WaitForChild("Remotes")
local placeRemote = remotes:WaitForChild("PlaceBlock") local placeRemote = remotes:WaitForChild("PlaceBlock")
local breakRemote = remotes:WaitForChild("BreakBlock") local breakRemote = remotes:WaitForChild("BreakBlock")
local blocksFolder = ReplicatedStorage:WaitForChild("Blocks") local blocksFolder = BlocksFolderSS
local function propogate(a, cx, cy, cz, x, y, z, bd) local function propogate(a, cx, cy, cz, x, y, z, bd)
task.synchronize() task.synchronize()
tickRemote:FireAllClients(a, cx, cy, cz, x, y, z, bd) tickRemote:FireAllClients(a, cx, cy, cz, x, y, z, bd)
task.desynchronize() task.desynchronize()
end end
local MAX_REACH = 24 local MAX_REACH = 512
local blockIdMap = {}
local function rebuildBlockIdMap() rebuildBlockIdMap = function()
table.clear(blockIdMap) table.clear(blockIdMap)
for _, block in ipairs(blocksFolder:GetChildren()) do for _, block in ipairs(blocksFolder:GetChildren()) do
local id = block:GetAttribute("n") local id = block:GetAttribute("n")
@@ -108,18 +130,25 @@ local function getPlayerPosition(player: Player): Vector3?
end end
local function isWithinReach(player: Player, cx: number, cy: number, cz: number, x: number, y: number, z: number): boolean local function isWithinReach(player: Player, cx: number, cy: number, cz: number, x: number, y: number, z: number): boolean
local playerPos = getPlayerPosition(player) -- Relaxed reach; always true unless you want to re-enable limits
if not playerPos then return true
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 end
local function resolveBlockId(blockId: any): string | number | nil local function resolveBlockId(blockId: any): string | number | nil
return blockIdMap[blockId] return blockIdMap[blockId]
end end
local function playerCanUseBlock(player: Player, resolvedId: any): boolean
if not ClientStateService:HasInInventory(player, resolvedId) then
return false
end
local selected = ClientStateService:GetSelectedBlockId(player)
if not selected then
return false
end
return tostring(selected) == tostring(resolvedId)
end
local function getServerChunk(cx: number, cy: number, cz: number) local function getServerChunk(cx: number, cy: number, cz: number)
task.desynchronize() task.desynchronize()
local chunk = TG:GetChunk(cx, cy, cz) local chunk = TG:GetChunk(cx, cy, cz)
@@ -127,77 +156,119 @@ local function getServerChunk(cx: number, cy: number, cz: number)
return chunk return chunk
end end
-- local PLAYER_BOX_SIZE = Vector3.new(3, 6, 3)
local function isBlockInsidePlayer(blockPos: Vector3): boolean
for _, player in ipairs(Players:GetPlayers()) do
local character = player.Character
if character then
local cf, size = character:GetBoundingBox()
local localPos = cf:PointToObjectSpace(blockPos)
if math.abs(localPos.X) <= size.X * 0.5
and math.abs(localPos.Y) <= size.Y * 0.5
and math.abs(localPos.Z) <= size.Z * 0.5 then
return true
end
end
end
return false
end
local DEBUG_PLACEMENT = false
local function debugPlacementLog(...: any)
if DEBUG_PLACEMENT then
Util.StudioLog(...)
end
end
local function debugPlacementWarn(...: any)
if DEBUG_PLACEMENT then
Util.StudioWarn(...)
end
end
placeRemote.OnServerEvent:Connect(function(player, cx, cy, cz, x, y, z, blockId) placeRemote.OnServerEvent:Connect(function(player, cx, cy, cz, x, y, z, blockId)
--print("place",player, cx, cy, cz, x, y, z, blockData) local function reject(reason: string)
debugPlacementWarn("[PLACE][REJECT]", player.Name, reason, "chunk", cx, cy, cz, "block", x, y, z, "id", blockId)
return
end
if typeof(cx) ~= "number" or typeof(cy) ~= "number" or typeof(cz) ~= "number" then if typeof(cx) ~= "number" or typeof(cy) ~= "number" or typeof(cz) ~= "number" then
return return reject("chunk types")
end end
if typeof(x) ~= "number" or typeof(y) ~= "number" or typeof(z) ~= "number" then if typeof(x) ~= "number" or typeof(y) ~= "number" or typeof(z) ~= "number" then
return return reject("block types")
end end
if x < 1 or x > 8 or y < 1 or y > 8 or z < 1 or z > 8 then if x < 1 or x > 8 or y < 1 or y > 8 or z < 1 or z > 8 then
return return reject("block bounds")
end end
if math.abs(cx) > MAX_CHUNK_DIST or math.abs(cy) > MAX_CHUNK_DIST or math.abs(cz) > MAX_CHUNK_DIST then if math.abs(cx) > MAX_CHUNK_DIST or math.abs(cy) > MAX_CHUNK_DIST or math.abs(cz) > MAX_CHUNK_DIST then
--return return reject("chunk bounds")
end end
if not isWithinReach(player, cx, cy, cz, x, y, z) then if not isWithinReach(player, cx, cy, cz, x, y, z) then
return return reject("out of reach")
end end
local resolvedId = resolveBlockId(blockId) local resolvedId = resolveBlockId(blockId)
if not resolvedId then if not resolvedId then
return return reject("invalid id")
end
if not playerCanUseBlock(player, resolvedId) then
return reject("not in inventory/hotbar")
end
local blockPos = Util.ChunkPosToCFrame(Vector3.new(cx, cy, cz), Vector3.new(x, y, z)).Position
if isBlockInsidePlayer(blockPos) then
return reject("inside player")
end end
local chunk = getServerChunk(cx, cy, cz) local chunk = getServerChunk(cx, cy, cz)
if chunk:GetBlockAt(x, y, z) then local existing = chunk:GetBlockAt(x, y, z)
if existing and existing.id and existing.id ~= 0 then
if existing.id == resolvedId then
-- same block already there; treat as success without changes
debugPlacementLog("[PLACE][OK][NOOP]", player.Name, "chunk", cx, cy, cz, "block", x, y, z, "id", resolvedId)
return return
end end
-- allow replacement when different id: remove then place
chunk:RemoveBlock(x, y, z)
end
local data = { local data = {
id = resolvedId, id = resolvedId,
state = {} state = {}
} }
chunk:CreateBlock(x, y, z, data) chunk:CreateBlock(x, y, z, data)
propogate("B_C", cx, cy, cz, x, y, z, data) propogate("B_C", cx, cy, cz, x, y, z, data)
debugPlacementLog("[PLACE][OK]", player.Name, "chunk", cx, cy, cz, "block", x, y, z, "id", resolvedId)
end) end)
breakRemote.OnServerEvent:Connect(function(player, cx, cy, cz, x, y, z) breakRemote.OnServerEvent:Connect(function(player, cx, cy, cz, x, y, z)
print("[DEBUG] Server breakRemote received - Player:", player.Name, "Chunk:", cx, cy, cz, "Block:", x, y, z)
if typeof(cx) ~= "number" or typeof(cy) ~= "number" or typeof(cz) ~= "number" then if typeof(cx) ~= "number" or typeof(cy) ~= "number" or typeof(cz) ~= "number" then
print("[DEBUG] Invalid chunk coordinate types")
return return
end end
if typeof(x) ~= "number" or typeof(y) ~= "number" or typeof(z) ~= "number" then if typeof(x) ~= "number" or typeof(y) ~= "number" or typeof(z) ~= "number" then
print("[DEBUG] Invalid block coordinate types")
return return
end end
if x < 1 or x > 8 or y < 1 or y > 8 or z < 1 or z > 8 then if x < 1 or x > 8 or y < 1 or y > 8 or z < 1 or z > 8 then
print("[DEBUG] Block coordinates out of range:", x, y, z)
return return
end end
if math.abs(cx) > MAX_CHUNK_DIST or math.abs(cy) > MAX_CHUNK_DIST or math.abs(cz) > MAX_CHUNK_DIST then if math.abs(cx) > MAX_CHUNK_DIST or math.abs(cy) > MAX_CHUNK_DIST or math.abs(cz) > MAX_CHUNK_DIST then
print("[DEBUG] Chunk coordinates out of range:", cx, cy, cz)
return return
end end
if not isWithinReach(player, cx, cy, cz, x, y, z) then if not isWithinReach(player, cx, cy, cz, x, y, z) then
print("[DEBUG] Block not within player reach")
return return
end end
local chunk = getServerChunk(cx, cy, cz) local chunk = getServerChunk(cx, cy, cz)
if not chunk:GetBlockAt(x, y, z) then if not chunk:GetBlockAt(x, y, z) then
print("[DEBUG] No block found at specified location")
task.synchronize() task.synchronize()
tickRemote:FireClient(player, "C_R", cx, cy, cz, 0, 0, 0, 0) tickRemote:FireClient(player, "C_R", cx, cy, cz, 0, 0, 0, 0)
task.desynchronize() task.desynchronize()
debugPlacementLog("[BREAK][NOOP]", player.Name, "chunk", cx, cy, cz, "block", x, y, z)
return return
end end
print("[DEBUG] All validations passed, removing block")
chunk:RemoveBlock(x, y, z) chunk:RemoveBlock(x, y, z)
propogate("B_D", cx, cy, cz, x, y, z, 0) propogate("B_D", cx, cy, cz, x, y, z, 0)
print("[DEBUG] Block removal propagated to clients") debugPlacementLog("[BREAK][OK]", player.Name, "chunk", cx, cy, cz, "block", x, y, z)
end) end)
task.desynchronize() task.desynchronize()

View File

@@ -0,0 +1,79 @@
return {
Name = "chunkcull",
Aliases = {"cullchunks", "resetchunks"},
Description = "Unload all server chunk cache instantly, then preload only chunks near players (and force clients to unload/resync).",
Group = "Admin",
Args = {
{
Type = "integer",
Name = "radius",
Description = "Horizontal chunk radius around each player to preload",
Optional = true,
Default = 5,
},
{
Type = "integer",
Name = "yRadius",
Description = "Vertical chunk radius around each player to preload",
Optional = true,
Default = 1,
},
},
Run = function(context, radius, yRadius)
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Players = game:GetService("Players")
local terrainGen = require(
game:GetService("ServerScriptService")
:WaitForChild("Actor")
:WaitForChild("ServerChunkManager")
:WaitForChild("TerrainGen")
)
local tickRemote = ReplicatedStorage:WaitForChild("Tick")
local r = radius or 5
local ry = yRadius or 1
local unloaded = 0
pcall(function()
unloaded = terrainGen:UnloadAllChunks()
end)
-- Tell all clients to immediately drop their local chunk instances
pcall(function()
tickRemote:FireAllClients("U_ALL", 0, 0, 0, 0, 0, 0, 0)
end)
-- Preload server chunks around players (reduces initial lag spikes after cull)
local preloaded = 0
pcall(function()
preloaded = terrainGen:PreloadNearPlayers(r, ry)
end)
-- Force clients to resync around themselves
local resyncCount = 0
for _, player in ipairs(Players:GetPlayers()) do
local character = player.Character
local root = character and character:FindFirstChild("HumanoidRootPart")
if root then
local pos = root.Position
local cx = math.floor((pos.X + 16) / 32)
local cy = math.floor((pos.Y + 16) / 32)
local cz = math.floor((pos.Z + 16) / 32)
for y = -ry, ry do
for x = -r, r do
for z = -r, r do
tickRemote:FireClient(player, "C_R", cx + x, cy + y, cz + z, 0, 0, 0, 0)
resyncCount += 1
end
end
end
end
end
return (
"chunkcull done | unloaded=%d | preloaded=%d | resyncPackets=%d | radius=%d yRadius=%d"
):format(unloaded, preloaded, resyncCount, r, ry)
end,
}

View File

@@ -10,7 +10,9 @@ local UIS = game:GetService("UserInputService")
local TXTS = game:GetService("TextChatService") local TXTS = game:GetService("TextChatService")
local TXTS_CIF = TXTS:FindFirstChildOfClass("ChatInputBarConfiguration") local TXTS_CIF = TXTS:FindFirstChildOfClass("ChatInputBarConfiguration")
ReplicatedStorage:WaitForChild("Objects"):WaitForChild("MLLoaded") local objects = ReplicatedStorage:WaitForChild("Objects", 9e9)
objects:WaitForChild("MLLoaded", 9e9)
objects:WaitForChild("CSMLLoaded", 9e9)
game:GetService("Players").LocalPlayer.CameraMode = Enum.CameraMode.LockFirstPerson game:GetService("Players").LocalPlayer.CameraMode = Enum.CameraMode.LockFirstPerson
UIS.MouseIconEnabled = false UIS.MouseIconEnabled = false

View File

@@ -8,8 +8,15 @@ end
local ui = script.Parent local ui = script.Parent
local ReplicatedStorage = game:GetService("ReplicatedStorage") local ReplicatedStorage = game:GetService("ReplicatedStorage")
local PlacementState = require(ReplicatedStorage.Shared.PlacementState)
ReplicatedStorage:WaitForChild("Objects"):WaitForChild("MLLoaded") local objects = ReplicatedStorage:WaitForChild("Objects", 9e9)
objects:WaitForChild("MLLoaded", 9e9)
objects:WaitForChild("CSMLLoaded", 9e9)
local clientReady = ReplicatedStorage.Objects:WaitForChild("ClientReady", 5)
if clientReady and not clientReady.Value then
clientReady:GetPropertyChangedSignal("Value"):Wait()
end
local cd = ReplicatedStorage.Objects.ChunkDebug:Clone() local cd = ReplicatedStorage.Objects.ChunkDebug:Clone()
local sky = ReplicatedStorage.Objects.Sky:Clone() local sky = ReplicatedStorage.Objects.Sky:Clone()
@@ -47,7 +54,8 @@ game:GetService("RunService").RenderStepped:Connect(function(dt)
if math.abs(bpos.z) == 0 then bpos.z = 0 end if math.abs(bpos.z) == 0 then bpos.z = 0 end
sky.CFrame = pos sky.CFrame = pos
ui.DebugUpperText.Text = `Chunk {chunk.x} {chunk.y} {chunk.z}\nPos {bpos.x} {bpos.y} {bpos.z}\n<b>{fps} FPS</b>` local selected = PlacementState:GetSelected()
ui.DebugUpperText.Text = `Chunk {chunk.x} {chunk.y} {chunk.z}\nPos {bpos.x} {bpos.y} {bpos.z}\nSel {selected}\n<b>{fps} FPS</b>`
cd:PivotTo(CFrame.new( cd:PivotTo(CFrame.new(
chunk.x*32, chunk.x*32,

View File

@@ -0,0 +1,460 @@
--!native
--!optimize 2
if not game:IsLoaded() then
game.Loaded:Wait()
end
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local UIS = game:GetService("UserInputService")
local TextChatService = game:GetService("TextChatService")
local objects = ReplicatedStorage:WaitForChild("Objects", 9e9)
objects:WaitForChild("MLLoaded", 9e9)
objects:WaitForChild("CSMLLoaded", 9e9)
local Roact = require(ReplicatedStorage.Packages.roact)
local PM = require(ReplicatedStorage.Shared.PlacementManager)
local BlockManager = require(ReplicatedStorage.Shared.ChunkManager.BlockManager)
local PlacementState = require(ReplicatedStorage.Shared.PlacementState)
local Util = require(ReplicatedStorage.Shared.Util)
local ClientState = require(ReplicatedStorage.Shared.ClientState)
local HOTBAR_SIZE = 10
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,
[Enum.KeyCode.Zero] = 10,
}
local colors = {
base = Color3.fromRGB(30, 30, 46),
slot = Color3.fromRGB(17, 17, 27),
stroke = Color3.fromRGB(88, 91, 112),
selectedStroke = Color3.fromRGB(137, 180, 250),
text = Color3.fromRGB(205, 214, 244),
subtext = Color3.fromRGB(166, 173, 200),
}
local function isTextInputFocused(): boolean
if UIS:GetFocusedTextBox() then
return true
end
local config = TextChatService:FindFirstChildOfClass("ChatInputBarConfiguration")
return config ~= nil and config.IsFocused
end
local function resolveSelectedSlot(slots, desired)
if desired and desired >= 1 and desired <= HOTBAR_SIZE then
return desired
end
for i = 1, HOTBAR_SIZE do
if slots[i] and slots[i] ~= "" then
return i
end
end
return desired or 1
end
local function buildHotbarFromState()
local slots = table.create(HOTBAR_SIZE)
local names = {}
for i = 1, HOTBAR_SIZE do
local info = ClientState:GetSlotInfo(i)
if info then
slots[i] = tostring(info.id)
names[slots[i]] = info.name or slots[i]
else
slots[i] = ""
end
end
local selected = resolveSelectedSlot(slots, ClientState:GetSelectedSlot())
return slots, names, selected
end
local function ensurePreviewRig(part: Instance)
for _, descendant in ipairs(part:GetDescendants()) do
if descendant:IsA("BasePart") then
descendant.Anchored = true
descendant.CanCollide = false
end
end
if part:IsA("BasePart") then
part.Anchored = true
part.CanCollide = false
end
end
local function updateViewport(viewport: ViewportFrame, blockId: string)
viewport:ClearAllChildren()
if blockId == "" then
return
end
local camera = Instance.new("Camera")
camera.Parent = viewport
viewport.CurrentCamera = camera
local world = Instance.new("WorldModel")
world.Parent = viewport
local resolvedId = tonumber(blockId) or blockId
local preview = BlockManager:GetBlock(resolvedId)
preview.Parent = world
ensurePreviewRig(preview)
local cf, size
if preview:IsA("BasePart") then
cf = preview.CFrame
size = preview.Size
else
cf, size = preview:GetBoundingBox()
end
local maxSize = math.max(size.X, size.Y, size.Z)
local distance = maxSize * 1.8
local target = cf.Position
camera.CFrame = CFrame.new(target + Vector3.new(distance, distance, distance), target)
preview:PivotTo(CFrame.new())
end
local Hotbar = Roact.Component:extend("Hotbar")
function Hotbar:init()
local slots, names, selected = buildHotbarFromState()
self.state = {
slots = slots,
names = names,
selected = selected,
}
self._syncFromClientState = function()
local nextSlots, nextNames, nextSelected = buildHotbarFromState()
nextSelected = resolveSelectedSlot(nextSlots, nextSelected or self.state.selected)
self:setState({
slots = nextSlots,
names = nextNames,
selected = nextSelected,
})
local rawId = nextSlots[nextSelected] or ""
local effectiveId = rawId ~= "" and rawId or "hand"
local name = ""
if rawId ~= "" then
name = nextNames[rawId] or rawId
end
PlacementState:SetSelected(effectiveId, name)
end
self._setSelected = function(slot: number)
if slot < 1 or slot > HOTBAR_SIZE then
return
end
ClientState:SetSelectedSlot(slot)
self:setState({
selected = slot,
})
local rawId = self.state.slots[slot] or ""
local effectiveId = rawId ~= "" and rawId or "hand"
local name = ""
if rawId ~= "" then
name = self.state.names[rawId] or rawId
end
Util.StudioLog("[PLACE][CLIENT][SELECT]", "slot", slot, "id", effectiveId, "name", name)
PlacementState:SetSelected(effectiveId, name)
end
self._handleInput = function(input: InputObject, gameProcessedEvent: boolean)
if isTextInputFocused() then
return
end
local slot = keyToSlot[input.KeyCode]
if slot then
if gameProcessedEvent then
return
end
self._setSelected(slot)
return
end
if input.UserInputType == Enum.UserInputType.MouseButton1 then
Util.StudioLog("[INPUT][CLIENT]", "MouseButton1", "processed", gameProcessedEvent)
-- Allow click even if gameProcessedEvent (UI can set this), but only if we're actually pointing at a block
if not PM:GetBlockAtMouse() then
return
end
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
Util.StudioLog("[INPUT][CLIENT]", "MouseButton2", "processed", gameProcessedEvent)
-- Allow click even if gameProcessedEvent (UI can set this), but only if we're actually pointing at a block
local mouseBlock = PM:DebugGetPlacementOrWarn(true) -- skip selection outline on right click
if not mouseBlock then
return
end
local id = PlacementState:GetSelected()
if not id or id == "" then
Util.StudioWarn("[PLACE][CLIENT][REJECT]", "no selected id")
return
end
Util.StudioLog(
"[PLACE][CLIENT][SEND][CLICK]",
"chunk",
mouseBlock.chunk,
"block",
mouseBlock.block,
"id",
id
)
PM:PlaceBlock(
mouseBlock.chunk.X,
mouseBlock.chunk.Y,
mouseBlock.chunk.Z,
mouseBlock.block.X,
mouseBlock.block.Y,
mouseBlock.block.Z,
id
)
end
end
self._handleScroll = function(input: InputObject, gameProcessedEvent: boolean)
if gameProcessedEvent or isTextInputFocused() then
return
end
if input.UserInputType ~= Enum.UserInputType.MouseWheel then
return
end
local direction = input.Position.Z
if direction == 0 then
return
end
local delta = direction > 0 and -1 or 1
local nextSlot = ((self.state.selected - 1 + delta) % HOTBAR_SIZE) + 1
if nextSlot ~= self.state.selected then
self._setSelected(nextSlot)
end
end
self._viewportRefs = {}
self._viewportState = {}
end
function Hotbar:didMount()
self._connections = {
ClientState.Changed:Connect(self._syncFromClientState),
UIS.InputBegan:Connect(self._handleInput),
UIS.InputChanged:Connect(self._handleScroll),
}
self._syncFromClientState()
self:_refreshViewports()
-- initialize selection broadcast
local rawId = self.state.slots and self.state.slots[self.state.selected] or ""
local effectiveId = rawId ~= "" and rawId or "hand"
local name = ""
if rawId ~= "" and self.state.names then
name = self.state.names[rawId] or rawId
end
PlacementState:SetSelected(effectiveId, name)
end
function Hotbar:willUnmount()
for _, conn in ipairs(self._connections or {}) do
conn:Disconnect()
end
self._connections = nil
end
function Hotbar:didUpdate(prevProps, prevState)
if prevState.slots ~= self.state.slots then
self:_refreshViewports()
end
end
function Hotbar:_refreshViewports()
for i = 1, HOTBAR_SIZE do
local viewport = self._viewportRefs[i]
if viewport then
local id = self.state.slots[i] or ""
if self._viewportState[i] ~= id then
self._viewportState[i] = id
updateViewport(viewport, id)
end
end
end
end
function Hotbar:render()
local slotElements = {}
local selectedId = self.state.slots[self.state.selected] or ""
local selectedName = ""
if selectedId ~= "" and self.state.names then
selectedName = self.state.names[selectedId] or selectedId
end
for i = 1, HOTBAR_SIZE do
local id = self.state.slots[i] or ""
local isSelected = i == self.state.selected
local displayName = id ~= "" and (self.state.names and self.state.names[id] or id) or ""
slotElements[`Slot{i-1}`] = Roact.createElement("TextButton", {
Size = UDim2.fromOffset(50, 50),
BackgroundColor3 = colors.slot,
BorderSizePixel = 0,
AutoButtonColor = false,
ClipsDescendants = true,
Text = "",
LayoutOrder = i,
[Roact.Event.Activated] = function()
self._setSelected(i)
end,
}, {
Corner = Roact.createElement("UICorner", {
CornerRadius = UDim.new(0, 13),
}),
Stroke = Roact.createElement("UIStroke", {
Color = isSelected and colors.selectedStroke or colors.stroke,
Thickness = isSelected and 2 or 1,
ApplyStrokeMode = Enum.ApplyStrokeMode.Border
}),
Preview = Roact.createElement("ViewportFrame", {
BackgroundTransparency = 1,
Size = UDim2.new(1, 0, 1, 0),
BorderSizePixel = 0,
[Roact.Ref] = function(r)
self._viewportRefs[i] = r
end,
}),
IndexLabel = Roact.createElement("TextLabel", {
BackgroundTransparency = 1,
Position = UDim2.fromOffset(8, 4),
Size = UDim2.fromOffset(18, 14),
Font = Enum.Font.Gotham,
Text = i == 10 and "0" or tostring(i),
TextColor3 = colors.subtext,
TextSize = 12,
TextXAlignment = Enum.TextXAlignment.Left,
TextYAlignment = Enum.TextYAlignment.Top,
}),
IdLabel = Roact.createElement("TextLabel", {
BackgroundTransparency = 1,
Position = UDim2.fromOffset(4, 26),
Size = UDim2.new(1, -8, 0, 18),
Font = Enum.Font.GothamBold,
Text = displayName,
TextColor3 = colors.text,
TextSize = 15,
TextWrapped = true,
TextXAlignment = Enum.TextXAlignment.Left,
TextYAlignment = Enum.TextYAlignment.Bottom,
}),
})
end
local hotbarFrame = Roact.createElement("Frame", {
AnchorPoint = Vector2.new(0.5, 1),
AutomaticSize = Enum.AutomaticSize.X,
BackgroundColor3 = colors.base,
BorderSizePixel = 0,
Position = UDim2.new(0.5, 0, 1, -20),
Size = UDim2.fromOffset(0, 58),
}, {
Corner = Roact.createElement("UICorner", {
CornerRadius = UDim.new(0, 16),
}),
Stroke = Roact.createElement("UIStroke", {
Color = colors.selectedStroke,
Thickness = 2,
ApplyStrokeMode = Enum.ApplyStrokeMode.Border
}),
Padding = Roact.createElement("UIPadding", {
PaddingLeft = UDim.new(0, 5),
PaddingRight = UDim.new(0, 5),
PaddingTop = UDim.new(0, 5),
PaddingBottom = UDim.new(0, 5),
}),
Slots = Roact.createElement("Frame", {
AutomaticSize = Enum.AutomaticSize.X,
BackgroundTransparency = 1,
Size = UDim2.new(0, 0, 1, 0),
}, {
Layout = Roact.createElement("UIListLayout", {
FillDirection = Enum.FillDirection.Horizontal,
HorizontalAlignment = Enum.HorizontalAlignment.Center,
VerticalAlignment = Enum.VerticalAlignment.Center,
Padding = UDim.new(0, 5),
}),
Slots = Roact.createFragment(slotElements),
}),
})
local selectedNameFrame = Roact.createElement("Frame", {
AnchorPoint = Vector2.new(0.5, 1),
AutomaticSize = Enum.AutomaticSize.X,
BackgroundColor3 = colors.base,
BorderSizePixel = 0,
Position = UDim2.new(0.5, 0, 1, -80-10),
Size = UDim2.fromOffset(0, 25),
Visible = selectedName ~= "",
}, {
Corner = Roact.createElement("UICorner", {
CornerRadius = UDim.new(0, 8),
}),
Stroke = Roact.createElement("UIStroke", {
Color = colors.selectedStroke,
Thickness = 2,
ApplyStrokeMode = Enum.ApplyStrokeMode.Border
}),
Padding = Roact.createElement("UIPadding", {
PaddingLeft = UDim.new(0, 18),
PaddingRight = UDim.new(0, 18),
PaddingTop = UDim.new(0, 2),
PaddingBottom = UDim.new(0, 2),
}),
Label = Roact.createElement("TextLabel", {
BackgroundTransparency = 1,
Size = UDim2.new(1, 0, 1, 0),
Font = Enum.Font.JosefinSans,
RichText = true,
Text = selectedName ~= "" and selectedName or " ",
TextColor3 = colors.text,
TextSize = 19,
TextWrapped = true,
AutomaticSize = Enum.AutomaticSize.X,
TextXAlignment = Enum.TextXAlignment.Center,
TextYAlignment = Enum.TextYAlignment.Center
}),
})
return Roact.createFragment({
Hotbar = hotbarFrame,
SelectedName = selectedNameFrame,
})
end
local handle = Roact.mount(Roact.createElement(Hotbar), script.Parent, "RoactHotbar")
script.AncestryChanged:Connect(function(_, parent)
if parent == nil then
Roact.unmount(handle)
end
end)

View File

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

View File

@@ -7,16 +7,16 @@ end
pcall(function() pcall(function()
task.synchronize() task.synchronize()
game:GetService("Workspace"):WaitForChild("$blockscraft_server",5):Destroy() task.defer(function()
game:GetService("Workspace"):WaitForChild("$blockscraft_server",9e9):Destroy()
end)
end) end)
local ReplicatedStorage = game:GetService("ReplicatedStorage") local ReplicatedStorage = game:GetService("ReplicatedStorage")
ReplicatedStorage:WaitForChild("Objects"):WaitForChild("MLLoaded") local objects = ReplicatedStorage:WaitForChild("Objects", 9e9)
objects:WaitForChild("MLLoaded", 9e9)
local ML = require(ReplicatedStorage:WaitForChild("Shared"):WaitForChild("ModLoader")) objects:WaitForChild("CSMLLoaded", 9e9)
ML.loadModsC()
do do
local PM = require(ReplicatedStorage:WaitForChild("Shared"):WaitForChild("PlacementManager")) local PM = require(ReplicatedStorage:WaitForChild("Shared"):WaitForChild("PlacementManager"))

View File

@@ -0,0 +1,9 @@
--!native
--!optimize 2
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local objects = ReplicatedStorage:WaitForChild("Objects", 9e9)
objects:WaitForChild("MLLoaded", 9e9)
objects:WaitForChild("CSMLLoaded", 9e9)
return

View File

@@ -4,5 +4,9 @@ until game:IsLoaded() == true
local ReplicatedStorage = game:GetService("ReplicatedStorage") local ReplicatedStorage = game:GetService("ReplicatedStorage")
local objects = ReplicatedStorage:WaitForChild("Objects", 9e9)
objects:WaitForChild("MLLoaded", 9e9)
objects:WaitForChild("CSMLLoaded", 9e9)
local Cmdr = require(ReplicatedStorage:WaitForChild("CmdrClient")) local Cmdr = require(ReplicatedStorage:WaitForChild("CmdrClient"))
Cmdr:SetActivationKeys({ Enum.KeyCode.F2 }) Cmdr:SetActivationKeys({ Enum.KeyCode.F2 })

View File

@@ -1,44 +1,52 @@
{ {
"name": "project", "name": "minecraft-roblox",
"tree": { "tree": {
"$className": "DataModel", "$className": "DataModel",
"ReplicatedStorage": { "ReplicatedStorage": {
"$className": "ReplicatedStorage", "$className": "ReplicatedStorage",
"$ignoreUnknownInstances": true, "$ignoreUnknownInstances": true,
"$path": "src/ReplicatedStorage", "$path": "ReplicatedStorage",
"Packages": { "Packages": {
"$className": "Folder", "$className": "Folder",
"$path": "Packages" "$path": "Packages"
} }
}, },
"ReplicatedFirst": { "ReplicatedFirst": {
"$className": "ReplicatedFirst", "$className": "ReplicatedFirst",
"$ignoreUnknownInstances": true, "$ignoreUnknownInstances": true,
"$path": "src/ReplicatedFirst" "$path": "ReplicatedFirst"
}, },
"ServerScriptService": { "ServerScriptService": {
"$className": "ServerScriptService", "$className": "ServerScriptService",
"$ignoreUnknownInstances": true, "$ignoreUnknownInstances": true,
"$path": "src/ServerScriptService" "$path": "ServerScriptService"
}, },
"StarterGui": { "StarterGui": {
"$className": "StarterGui", "$className": "StarterGui",
"$ignoreUnknownInstances": true, "$ignoreUnknownInstances": true,
"$path": "src/StarterGui" "$path": "StarterGui"
}, },
"StarterPlayer": { "StarterPlayer": {
"$className": "StarterPlayer", "$className": "StarterPlayer",
"$ignoreUnknownInstances": true,
"StarterPlayerScripts": { "StarterPlayerScripts": {
"$className": "StarterPlayerScripts", "$className": "StarterPlayerScripts",
"$ignoreUnknownInstances": true, "$ignoreUnknownInstances": true,
"$path": "src/StarterPlayer/StarterPlayerScripts" "$path": "StarterPlayer/StarterPlayerScripts"
}, }
"$ignoreUnknownInstances": true
}, },
"Workspace": { "Workspace": {
"$className": "Workspace", "$className": "Workspace",
"$ignoreUnknownInstances": true, "$ignoreUnknownInstances": true,
"$path": "src/Workspace" "$path": "Workspace"
} }
} }
} }

View File

@@ -1,271 +0,0 @@
--!native
--!optimize 2
local PlacementManager = {}
local ChunkManager = require("./ChunkManager")
local Util = require("./Util")
local RunService = game:GetService("RunService")
PlacementManager.ChunkFolder = ChunkManager.ChunkFolder
local raycastParams = RaycastParams.new()
raycastParams.FilterDescendantsInstances = {PlacementManager.ChunkFolder}
raycastParams.FilterType = Enum.RaycastFilterType.Include
raycastParams.IgnoreWater = true
if _G.SB then return nil end
_G.SB = true
PlacementManager.SelectionBox = script.SelectionBox:Clone()
PlacementManager.SelectionBox.Name = "$SelectionBox"..(game:GetService("RunService"):IsServer() and "_SERVER" or "")
PlacementManager.SelectionBox.Parent = game:GetService("Workspace"):FindFirstChildOfClass("Terrain")
-- Trash method TODO: Fix this
local function findParent(i: Instance): Instance
local f = i:FindFirstAncestorOfClass("Folder")
local d = i
repeat
d = d.Parent
until d.Parent == f
return d
end
local Mouse: Mouse = nil
local lastNormalId: Enum.NormalId? = nil
local pendingBreakResync = {}
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()
if not Mouse then
Mouse = game:GetService("Players").LocalPlayer:GetMouse()
end
task.synchronize()
local objLookingAt = Mouse.Target
local dir = Mouse.TargetSurface
if not objLookingAt then
PlacementManager.SelectionBox.Adornee = nil
script.RaycastResult.Value = nil
lastNormalId = nil
return
end
--if not objLookingAt:IsDescendantOf(ChunkManager.ChunkFolder) then return end
local parent = findParent(objLookingAt)
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
function PlacementManager:RaycastGetResult()
return script.RaycastResult.Value
end
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, blockId: string)
--print("placeblock")
--local chunk = ChunkManager:GetChunk(cx, cy, cz)
--chunk:CreateBlock(x, y, z, blockData)
task.synchronize()
placeRemote:FireServer(cx, cy, cz, x, y, z, blockId)
task.desynchronize()
end
-- FIRES REMOTE
function PlacementManager:BreakBlock(cx, cy, cz, x, y, z)
print("[DEBUG] PlacementManager:BreakBlock called - Chunk:", cx, cy, cz, "Block:", x, y, z)
local chunk = ChunkManager:GetChunk(cx, cy, cz)
if chunk and not chunk:GetBlockAt(x, y, z) then
print("[DEBUG] Client missing block; resyncing nearby chunks")
ChunkManager:ResyncAroundChunk(cx, cy, cz, 1)
task.defer(function()
task.synchronize()
RunService.RenderStepped:Wait()
task.desynchronize()
local refreshed = ChunkManager:GetChunk(cx, cy, cz)
if refreshed and refreshed:GetBlockAt(x, y, z) then
task.synchronize()
breakRemote:FireServer(cx, cy, cz, x, y, z)
task.desynchronize()
print("[DEBUG] BreakBlock remote fired to server after resync")
end
end)
return
end
task.synchronize()
breakRemote:FireServer(cx, cy, cz, x, y, z)
task.desynchronize()
print("[DEBUG] BreakBlock remote fired to server")
end
-- CLIENTSIDED: only apply server-validated changes
local function applyPlaceBlockLocal(cx, cy, cz, x, y, z, blockData)
local chunk = ChunkManager:GetChunk(cx, cy, cz)
chunk:CreateBlock(x, y, z, blockData)
end
-- CLIENTSIDED: only apply server-validated changes
local function applyBreakBlockLocal(cx, cy, cz, x, y, z)
print("[DEBUG] PlacementManager:BreakBlockLocal called - Chunk:", cx, cy, cz, "Block:", x, y, z)
local chunk = ChunkManager:GetChunk(cx, cy, cz)
if chunk then
print("[DEBUG] Found chunk, calling RemoveBlock")
if chunk.RemoveBlockSmooth then
chunk:RemoveBlockSmooth(x, y, z)
else
chunk:RemoveBlock(x, y, z)
end
else
print("[DEBUG] Chunk not found at coords:", cx, cy, cz)
end
end
function PlacementManager:GetBlockAtMouse(): nil | {chunk:Vector3, block: Vector3}
local selectedPart = PlacementManager:RaycastGetResult()
--print(selectedPart and selectedPart:GetFullName() or nil)
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)
local blockCoords = Util.BlockPosStringToCoords(selectedPart.Name)
return {
chunk = chunkCoords,
block = blockCoords
}
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").RenderStepped:Connect(function()
local a,b = pcall(function()
PlacementManager:Raycast()
end)
if not a then
task.synchronize()
PlacementManager.SelectionBox.Adornee = nil
script.RaycastResult.Value = nil
task.desynchronize()
end
end)
tickRemote.OnClientEvent:Connect(function(m, cx, cy, cz, x, y, z, d)
--warn("PROPOGATED TICK", m, cx, cy, cz, x, y, z, d)
if m == "B_C" then
applyPlaceBlockLocal(cx, cy, cz, x, y ,z, d)
end
if m == "B_D" then
applyBreakBlockLocal(cx, cy, cz, x, y ,z)
local key = `{cx},{cy},{cz}`
if not pendingBreakResync[key] then
pendingBreakResync[key] = true
task.defer(function()
task.synchronize()
RunService.RenderStepped:Wait()
task.desynchronize()
pendingBreakResync[key] = nil
ChunkManager:ResyncAroundChunk(cx, cy, cz, 1)
end)
end
end
if m == "C_R" then
ChunkManager:RefreshChunk(cx, cy, cz)
end
end)
end
return PlacementManager

View File

@@ -1,73 +0,0 @@
--!native
--!optimize 2
local TerrainGen = {}
local deflate = require("./TerrainGen/Deflate")
local DSS = game:GetService("DataStoreService")
local WORLDNAME = "DEFAULT"
local WORLDID = "b73bb5a6-297d-4352-b637-daec7e8c8f3e"
local Store = DSS:GetDataStore("BlockscraftWorldV1", WORLDID)
local ChunkManager = require(game:GetService("ReplicatedStorage"):WaitForChild("Shared").ChunkManager)
local Chunk = require(game:GetService("ReplicatedStorage"):WaitForChild("Shared").ChunkManager.Chunk)
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)
if y == 1 then
for cx = 1, 8 do
for cz = 1, 8 do
--local perlin = math.noise(((x*8)+cx)/100,((z*8)+cz)/100)
chunk:CreateBlock(cx, 1, cz, { id = 1, state = {} })
--chunk:CreateBlock(x, 2, z, { id = 1, state = {} })
end
end
end
if y == 0 then
for cx = 1, 8 do
for cy = 1, 8 do
for cz = 1, 8 do
--local perlin = math.noise(((x*8)+cx)/100,((z*8)+cz)/100)
chunk:CreateBlock(cx, cy, cz, { id = 2, state = {} })
--chunk:CreateBlock(x, 2, z, { id = 1, state = {} })
end
end
end
end
TerrainGen.ServerChunkCache[key] = chunk
return chunk
end
-- Fake Chunk
function TerrainGen:GetFakeChunk(x, y, z)
-- Generate a new chunk if it doesn't exist
local chunk = Chunk.new(x, y, z)
for cy = 1,8 do
for cx = 1, 8 do
for cz = 1, 8 do
--local perlin = math.noise(((x*8)+cx)/100,((z*8)+cz)/100)
chunk:CreateBlock(cx, cy, cz, { id = -2, state = {} })
--chunk:CreateBlock(x, 2, z, { id = 1, state = {} })
end
end
end
return chunk
end
TerrainGen.CM = ChunkManager
return TerrainGen

View File

@@ -1,95 +0,0 @@
--!native
--!optimize 2
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

@@ -7,7 +7,17 @@ name = "evaera/cmdr"
version = "1.12.0" version = "1.12.0"
dependencies = [] dependencies = []
[[package]]
name = "ivasmigins/replica"
version = "0.1.0"
dependencies = []
[[package]] [[package]]
name = "ocbwoy3-development-studios/minecraft-roblox" name = "ocbwoy3-development-studios/minecraft-roblox"
version = "0.1.0" version = "0.1.0"
dependencies = [["cmdr", "evaera/cmdr@1.12.0"]] dependencies = [["cmdr", "evaera/cmdr@1.12.0"], ["replica", "ivasmigins/replica@0.1.0"], ["roact", "roblox/roact@1.4.4"]]
[[package]]
name = "roblox/roact"
version = "1.4.4"
dependencies = []

View File

@@ -6,3 +6,5 @@ realm = "shared"
[dependencies] [dependencies]
cmdr = "evaera/cmdr@1.12.0" cmdr = "evaera/cmdr@1.12.0"
roact = "roblox/roact@1.4.4"
replica = "ivasmigins/replica@0.1.0"