diff --git a/.gitignore b/.gitignore
index e816e79..eee4ea7 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1 +1,3 @@
-place.rbxl.lock
\ No newline at end of file
+place.rbxl.lock
+Packages/
+ServerPackages/
diff --git a/default.project.json b/default.project.json
index bddf56a..5a247f5 100644
--- a/default.project.json
+++ b/default.project.json
@@ -5,11 +5,19 @@
"ReplicatedStorage": {
"$className": "ReplicatedStorage",
"$ignoreUnknownInstances": true,
+ "Packages": {
+ "$className": "Folder",
+ "$path": "Packages"
+ },
"$path": "src/ReplicatedStorage"
},
"ServerScriptService": {
"$className": "ServerScriptService",
"$ignoreUnknownInstances": true,
+ "ServerPackages": {
+ "$className": "Folder",
+ "$path": "ServerPackages"
+ },
"$path": "src/ServerScriptService"
},
"StarterGui": {
@@ -32,4 +40,4 @@
"$path": "src/Workspace"
}
}
-}
\ No newline at end of file
+}
diff --git a/src/ReplicatedStorage/Shared/ChunkManager/init.lua b/src/ReplicatedStorage/Shared/ChunkManager/init.lua
index aaa60d0..dacf85c 100644
--- a/src/ReplicatedStorage/Shared/ChunkManager/init.lua
+++ b/src/ReplicatedStorage/Shared/ChunkManager/init.lua
@@ -1,6 +1,7 @@
local ChunkManager = {}
local RunService = game:GetService("RunService")
+local Players = game:GetService("Players")
local Chunk = require("./ChunkManager/Chunk")
local BlockManager = require("./ChunkManager/BlockManager")
@@ -15,10 +16,10 @@ ChunkFolder.Name = "$blockscraft_client"
ChunkManager.ChunkFolder = ChunkFolder
local CHUNK_RADIUS = 5
+local INITIAL_SYNC_RADIUS = 2
local LOAD_BATCH = 8
-local FORCELOAD_CHUNKS = {
- {0, 1, 0}
-}
+local CHUNK_WORLD_SIZE = 32 -- 8 blocks * 4 studs
+local FORCELOAD_CHUNKS = {}
local unloadingChunks = {}
local pendingChunkRequests = {}
@@ -37,6 +38,15 @@ do
end)
end
+local function worldToChunkCoords(pos: Vector3): { x: number, y: number, z: number }
+ -- Align chunk boundaries so chunk 0 spans roughly [-16,16] with block centers every 4 studs.
+ return {
+ x = math.floor((pos.X + (CHUNK_WORLD_SIZE / 2)) / CHUNK_WORLD_SIZE),
+ y = math.floor((pos.Y + (CHUNK_WORLD_SIZE / 2)) / CHUNK_WORLD_SIZE),
+ z = math.floor((pos.Z + (CHUNK_WORLD_SIZE / 2)) / CHUNK_WORLD_SIZE),
+ }
+end
+
local function Swait(l)
task.synchronize()
for _ = 1, l do
@@ -139,11 +149,7 @@ function ChunkManager:Tick()
end
local pos = player.Character:GetPivot().Position
- local chunkPos = {
- x = math.round(pos.X / 32),
- y = math.round(pos.Y / 32),
- z = math.round(pos.Z / 32)
- }
+ local chunkPos = worldToChunkCoords(pos)
task.defer(function()
local processed = 0
@@ -207,6 +213,32 @@ function ChunkManager:Init()
ChunkFolder.Parent = game:GetService("Workspace")
ChunkManager:ForceTick()
+ -- Synchronously warm a small area around the spawn chunk to ensure visible terrain on join.
+ local player = Players.LocalPlayer
+ local function warmInitial(character)
+ if not character then
+ return
+ end
+ local chunkPos = worldToChunkCoords(character:GetPivot().Position)
+ for _, offset in ipairs(CHUNK_OFFSETS) do
+ if offset[4] <= (INITIAL_SYNC_RADIUS * INITIAL_SYNC_RADIUS) then
+ local cx, cy, cz = chunkPos.x + offset[1], chunkPos.y + offset[2], chunkPos.z + offset[3]
+ local chunk = ChunkManager:GetChunk(cx, cy, cz)
+ chunk:Tick()
+ if not chunk.loaded then
+ ChunkManager:LoadChunk(cx, cy, cz)
+ end
+ end
+ end
+ end
+
+ if player.Character then
+ warmInitial(player.Character)
+ else
+ player.CharacterAdded:Wait()
+ warmInitial(player.Character)
+ end
+
task.defer(function()
while true do
wait(2)
diff --git a/src/ReplicatedStorage/Shared/PlacementManager.lua b/src/ReplicatedStorage/Shared/PlacementManager.lua
index 96c316f..2cf022c 100644
--- a/src/ReplicatedStorage/Shared/PlacementManager.lua
+++ b/src/ReplicatedStorage/Shared/PlacementManager.lua
@@ -1,76 +1,137 @@
local PlacementManager = {}
+local Players = game:GetService("Players")
+local Workspace = game:GetService("Workspace")
+local ReplicatedStorage = game:GetService("ReplicatedStorage")
+local RunService = game:GetService("RunService")
+
local ChunkManager = require("./ChunkManager")
local Util = require("./Util")
-PlacementManager.ChunkFolder = ChunkManager.ChunkFolder
+local remotes = ReplicatedStorage:WaitForChild("Remotes")
+local placeRemote = remotes:WaitForChild("PlaceBlock")
+local breakRemote = remotes:WaitForChild("BreakBlock")
+local tickRemote = ReplicatedStorage:WaitForChild("Tick")
+
+local LOCAL_PLAYER = Players.LocalPlayer
+local CAMERA = Workspace.CurrentCamera
+
+local CHUNK_SIZE = 8
+local BLOCK_SIZE = 4
+local CHUNK_WORLD_SIZE = CHUNK_SIZE * BLOCK_SIZE
+local MAX_RAY_DISTANCE = 1024
local raycastParams = RaycastParams.new()
-raycastParams.FilterDescendantsInstances = {PlacementManager.ChunkFolder}
+raycastParams.FilterDescendantsInstances = { ChunkManager.ChunkFolder }
raycastParams.FilterType = Enum.RaycastFilterType.Include
raycastParams.IgnoreWater = true
-if _G.SB then return nil end
-_G.SB = true
+local selectionBox = Instance.new("SelectionBox")
+selectionBox.Name = "$SelectionBox"
+selectionBox.LineThickness = 0.03
+selectionBox.Color3 = Color3.new(1, 1, 0)
+selectionBox.SurfaceTransparency = 0.85
+selectionBox.Transparency = 0.25
+selectionBox.Adornee = nil
+selectionBox.Parent = Workspace:FindFirstChildOfClass("Terrain") or Workspace
-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 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)
+local function normalToId(normal: Vector3): Enum.NormalId
+ local absX, absY, absZ = math.abs(normal.X), math.abs(normal.Y), math.abs(normal.Z)
+ if absX > absY and absX > absZ then
+ return normal.X > 0 and Enum.NormalId.Right or Enum.NormalId.Left
+ elseif absY > absX and absY > absZ 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
- 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
+local function findChunkAndBlock(hitInstance: Instance)
+ -- Find chunk container (child of ChunkFolder)
+ local inst = hitInstance
+ local chunkInstance = nil
+ while inst and inst.Parent do
+ if inst.Parent == ChunkManager.ChunkFolder then
+ chunkInstance = inst
+ break
+ end
+ if inst.Parent.Parent == ChunkManager.ChunkFolder then
+ chunkInstance = inst.Parent
+ break
+ end
+ inst = inst.Parent
+ end
+ if not chunkInstance or chunkInstance.Parent ~= ChunkManager.ChunkFolder then
+ return nil
+ end
+ -- Find block container (direct child of chunk)
+ local blockInstance = nil
+ inst = hitInstance
+ while inst and inst.Parent do
+ if inst.Parent == chunkInstance then
+ blockInstance = inst
+ break
+ end
+ inst = inst.Parent
+ end
+ if not blockInstance then
+ return nil
+ end
+
+ local chunkCoords = Util.BlockPosStringToCoords(chunkInstance.Name)
+ local blockCoords = nil
+
+ if blockInstance:IsA("BasePart") or blockInstance:IsA("Model") then
+ blockCoords = Util.BlockPosStringToCoords(blockInstance.Name)
+ end
+
+ if not blockCoords then
+ return nil
+ end
+
+ return chunkCoords, blockCoords, blockInstance
+end
+
+local function offsetForPlacement(chunk: Vector3, block: Vector3, normalId: Enum.NormalId)
+ local cx, cy, cz = chunk.X, chunk.Y, chunk.Z
+ local bx, by, bz = block.X, block.Y, block.Z
+
+ if normalId == Enum.NormalId.Top then
+ by += 1
+ elseif normalId == Enum.NormalId.Bottom then
+ by -= 1
+ elseif normalId == Enum.NormalId.Left then
+ bx -= 1
+ elseif normalId == Enum.NormalId.Right then
+ bx += 1
+ elseif normalId == Enum.NormalId.Front then
+ bz -= 1
+ elseif normalId == Enum.NormalId.Back then
+ bz += 1
+ end
+
+ -- Wrap across chunk boundaries (chunks are 1-indexed blocks 1..8)
if bx < 1 then
- bx = 8
+ bx = CHUNK_SIZE
cx -= 1
- elseif bx > 8 then
+ elseif bx > CHUNK_SIZE then
bx = 1
cx += 1
end
if by < 1 then
- by = 8
+ by = CHUNK_SIZE
cy -= 1
- elseif by > 8 then
+ elseif by > CHUNK_SIZE then
by = 1
cy += 1
end
if bz < 1 then
- bz = 8
+ bz = CHUNK_SIZE
cz -= 1
- elseif bz > 8 then
+ elseif bz > CHUNK_SIZE then
bz = 1
cz += 1
end
@@ -78,142 +139,107 @@ local function offsetChunkBlock(chunk: Vector3, block: Vector3, offset: Vector3)
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()
+local lastHit = nil
+
+local function castFromCamera()
+ if not LOCAL_PLAYER or not CAMERA then
+ return nil
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
+
+ local mouse = LOCAL_PLAYER:GetMouse()
+ if not mouse then
+ return nil
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
+
+ local unitRay = mouse.UnitRay
+ local result = Workspace:Raycast(unitRay.Origin, unitRay.Direction * MAX_RAY_DISTANCE, raycastParams)
+ if not result then
+ selectionBox.Adornee = nil
+ lastHit = nil
+ return nil
end
- PlacementManager.SelectionBox.Adornee = parent
- script.RaycastResult.Value = parent
- lastNormalId = dir
- return parent, dir
+
+ local chunkCoords, blockCoords, blockInstance = findChunkAndBlock(result.Instance)
+ if not chunkCoords or not blockCoords then
+ selectionBox.Adornee = nil
+ lastHit = nil
+ return nil
+ end
+
+ local hitNormalId = normalToId(result.Normal)
+ local adornTarget = blockInstance
+ if adornTarget and adornTarget:IsA("Model") then
+ adornTarget = adornTarget.PrimaryPart or adornTarget:FindFirstChildWhichIsA("BasePart")
+ end
+ selectionBox.Adornee = adornTarget or result.Instance
+
+ lastHit = {
+ chunk = chunkCoords,
+ block = blockCoords,
+ normal = hitNormalId,
+ instance = adornTarget or result.Instance,
+ }
+
+ return lastHit
end
-function PlacementManager:RaycastGetResult()
- return script.RaycastResult.Value
+-- Public API
+
+function PlacementManager:GetBlockAtMouse()
+ local hit = castFromCamera()
+ if not hit then
+ return nil
+ end
+ return {
+ chunk = hit.chunk,
+ block = hit.block,
+ }
end
-local remotes = game:GetService("ReplicatedStorage"):WaitForChild("Remotes")
-local placeRemote = remotes:WaitForChild("PlaceBlock")
-local breakRemote = remotes:WaitForChild("BreakBlock")
-local tickRemote = game:GetService("ReplicatedStorage").Tick
+function PlacementManager:GetPlacementAtMouse()
+ local hit = castFromCamera()
+ if not hit then
+ return nil
+ end
+ local placeChunk, placeBlock = offsetForPlacement(hit.chunk, hit.block, hit.normal)
+ return {
+ chunk = placeChunk,
+ block = placeBlock,
+ }
+end
--- 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)
placeRemote:FireServer(cx, cy, cz, x, y, z, blockId)
end
--- FIRES REMOTE
function PlacementManager:BreakBlock(cx, cy, cz, x, y, z)
- --print("breakblock")
- --local chunk = ChunkManager:GetChunk(cx, cy, cz)
- --chunk:RemoveBlock(x, y, z)
breakRemote:FireServer(cx, cy, cz, x, y, z)
end
--- CLIENTSIDED
function PlacementManager:PlaceBlockLocal(cx, cy, cz, x, y, z, blockData)
local chunk = ChunkManager:GetChunk(cx, cy, cz)
chunk:CreateBlock(x, y, z, blockData)
end
--- CLIENTSIDED
function PlacementManager:BreakBlockLocal(cx, cy, cz, x, y, z)
local chunk = ChunkManager:GetChunk(cx, cy, cz)
chunk:RemoveBlock(x, y, z)
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").Heartbeat: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()
+ RunService.Heartbeat:Connect(function()
+ local ok = pcall(castFromCamera)
+ if not ok then
+ selectionBox.Adornee = nil
+ lastHit = 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
- PlacementManager:PlaceBlockLocal(cx, cy, cz, x, y ,z, d)
- end
- if m == "B_D" then
- PlacementManager:BreakBlockLocal(cx, cy, cz, x, y ,z)
+ PlacementManager:PlaceBlockLocal(cx, cy, cz, x, y, z, d)
+ elseif m == "B_D" then
+ PlacementManager:BreakBlockLocal(cx, cy, cz, x, y, z)
end
end)
end
diff --git a/src/ServerScriptService/Actor/ServerChunkManager/init.server.lua b/src/ServerScriptService/Actor/ServerChunkManager/init.server.lua
index 7d404ea..3b22468 100644
--- a/src/ServerScriptService/Actor/ServerChunkManager/init.server.lua
+++ b/src/ServerScriptService/Actor/ServerChunkManager/init.server.lua
@@ -3,16 +3,62 @@ print("Hello world!")
task.synchronize()
local ReplicatedStorage = game:GetService("ReplicatedStorage")
+local Workspace = game:GetService("Workspace")
+local ServerScriptService = game:GetService("ServerScriptService")
+local function getOrCreateFolder(parent: Instance, name: string): Folder
+ local existing = parent:FindFirstChild(name)
+ if existing then
+ return existing
+ end
+ local folder = Instance.new("Folder")
+ folder.Name = name
+ folder.Parent = parent
+ return folder
+end
local Shared = ReplicatedStorage:WaitForChild("Shared")
-local ModsFolder = ReplicatedStorage:WaitForChild("Mods")
+local ModsFolder = getOrCreateFolder(ReplicatedStorage, "Mods")
+local ObjectsFolder = getOrCreateFolder(ReplicatedStorage, "Objects")
+local BlocksFolder = getOrCreateFolder(ReplicatedStorage, "Blocks")
+local BlockUpdateOpsFolder = getOrCreateFolder(ReplicatedStorage, "BlockUpdateOperations")
+
+local function ensureBlock(id: number, name: string, color: Color3)
+ for _, child in ipairs(BlocksFolder:GetChildren()) do
+ if child:GetAttribute("n") == id then
+ return
+ end
+ end
+ local part = Instance.new("Part")
+ part.Name = name
+ part.Anchored = true
+ part.Size = Vector3.new(4, 4, 4)
+ part.Material = Enum.Material.SmoothPlastic
+ part.Color = color
+ part:SetAttribute("n", id)
+ part.Parent = BlocksFolder
+end
+
+-- Seed minimal blocks so generation/placement never produce invalid ids.
+ensureBlock(1, "mc:grass_block", Color3.fromRGB(117, 201, 112))
+ensureBlock(2, "mc:dirt", Color3.fromRGB(134, 96, 67))
+
+local ReplicaService = require(ServerScriptService:WaitForChild("ServerPackages"):WaitForChild("ReplicaService"))
+local InventoryClassToken = ReplicaService.NewClassToken("Inventory")
local Util = require(Shared.Util)
local TG = require("./ServerChunkManager/TerrainGen")
+local mlLoadedFlag = ObjectsFolder:FindFirstChild("MLLoaded")
+if not mlLoadedFlag then
+ mlLoadedFlag = Instance.new("BoolValue")
+ mlLoadedFlag.Name = "MLLoaded"
+ mlLoadedFlag.Parent = ObjectsFolder
+end
+mlLoadedFlag.Value = false
+
do
- local workspaceModFolder = game:GetService("Workspace"):WaitForChild("mods")
+ local workspaceModFolder = Workspace:FindFirstChild("mods") or getOrCreateFolder(Workspace, "mods")
for _,v in pairs(workspaceModFolder:GetChildren()) do
v.Parent = ModsFolder
@@ -23,12 +69,7 @@ end
local ML = require(Shared.ModLoader)
ML.loadModsS()
-do
- local bv = Instance.new("BoolValue")
- bv.Name = "MLLoaded"
- bv.Value = true
- bv.Parent = ReplicatedStorage:WaitForChild("Objects")
-end
+mlLoadedFlag.Value = true
local MAX_CHUNK_DIST = 200
@@ -67,8 +108,6 @@ local tickRemote = ReplicatedStorage.Tick
local remotes = ReplicatedStorage:WaitForChild("Remotes")
local placeRemote = remotes:WaitForChild("PlaceBlock")
local breakRemote = remotes:WaitForChild("BreakBlock")
-local inventorySync = remotes:WaitForChild("InventorySync")
-local inventoryRequest = remotes:WaitForChild("InventoryRequest")
local blocksFolder = ReplicatedStorage:WaitForChild("Blocks")
local function propogate(a, cx, cy, cz, x, y, z, bd)
task.synchronize()
@@ -76,10 +115,11 @@ local function propogate(a, cx, cy, cz, x, y, z, bd)
task.desynchronize()
end
-local MAX_REACH = 24
+local MAX_REACH = math.huge
local HOTBAR_SIZE = 9
local blockIdMap = {}
local playerInventories = {}
+local playerReplicas = {}
local function rebuildBlockIdMap()
table.clear(blockIdMap)
@@ -109,7 +149,11 @@ local function buildDefaultSlots(): {string}
end)
local slots = table.create(HOTBAR_SIZE, "")
for i = 1, HOTBAR_SIZE do
- slots[i] = ids[i]
+ local val = ids[i]
+ if val == nil or val == "" then
+ val = ""
+ end
+ slots[i] = val
end
return slots
end
@@ -119,8 +163,37 @@ local function syncInventory(player: Player)
if not data then
return
end
- task.synchronize()
- inventorySync:FireClient(player, data.slots)
+ if type(data.slots) ~= "table" then
+ data.slots = buildDefaultSlots()
+ end
+ if type(data.allowed) ~= "table" then
+ data.allowed = {}
+ for i = 1, HOTBAR_SIZE do
+ local id = data.slots[i] or ""
+ if id ~= "" then
+ data.allowed[id] = true
+ end
+ end
+ playerInventories[player.UserId] = data
+ end
+ local replica = playerReplicas[player]
+ if not replica then
+ replica = ReplicaService.NewReplica({
+ ClassToken = InventoryClassToken,
+ Tags = {
+ Player = player,
+ },
+ Data = {
+ Slots = data.slots,
+ Allowed = data.allowed,
+ },
+ Replication = player,
+ })
+ playerReplicas[player] = replica
+ else
+ replica:SetValue("Slots", data.slots)
+ replica:SetValue("Allowed", data.allowed)
+ end
end
local function rebuildAllInventories()
@@ -128,7 +201,7 @@ local function rebuildAllInventories()
local slots = buildDefaultSlots()
local allowed = {}
for i = 1, HOTBAR_SIZE do
- local id = slots[i]
+ local id = slots[i] or ""
if id ~= "" then
allowed[id] = true
end
@@ -147,12 +220,12 @@ blocksFolder.ChildRemoved:Connect(rebuildAllInventories)
game:GetService("Players").PlayerAdded:Connect(function(player: Player)
local slots = buildDefaultSlots()
local allowed = {}
- for i = 1, HOTBAR_SIZE do
- local id = slots[i]
- if id ~= "" then
- allowed[id] = true
- end
+ for i = 1, HOTBAR_SIZE do
+ local id = slots[i] or ""
+ if id ~= "" then
+ allowed[id] = true
end
+ end
playerInventories[player.UserId] = {
slots = slots,
allowed = allowed,
@@ -162,17 +235,22 @@ end)
game:GetService("Players").PlayerRemoving:Connect(function(player: Player)
playerInventories[player.UserId] = nil
+ local replica = playerReplicas[player]
+ if replica then
+ replica:Destroy()
+ playerReplicas[player] = nil
+ end
end)
for _, player in ipairs(game:GetService("Players"):GetPlayers()) do
local slots = buildDefaultSlots()
local allowed = {}
- for i = 1, HOTBAR_SIZE do
- local id = slots[i]
- if id ~= "" then
- allowed[id] = true
- end
+ for i = 1, HOTBAR_SIZE do
+ local id = slots[i] or ""
+ if id ~= "" then
+ allowed[id] = true
end
+ end
playerInventories[player.UserId] = {
slots = slots,
allowed = allowed,
@@ -180,10 +258,6 @@ for _, player in ipairs(game:GetService("Players"):GetPlayers()) do
syncInventory(player)
end
-inventoryRequest.OnServerEvent:Connect(function(player: Player)
- syncInventory(player)
-end)
-
local function getPlayerPosition(player: Player): Vector3?
local character = player.Character
if not character then
@@ -197,12 +271,8 @@ local function getPlayerPosition(player: Player): Vector3?
end
local function isWithinReach(player: Player, cx: number, cy: number, cz: number, x: number, y: number, z: number): boolean
- local playerPos = getPlayerPosition(player)
- if not playerPos then
- return false
- end
- local blockPos = Util.ChunkPosToCFrame(Vector3.new(cx, cy, cz), Vector3.new(x, y, z)).Position
- return (blockPos - playerPos).Magnitude <= MAX_REACH
+ -- Creative: disable reach limits.
+ return true
end
local function resolveBlockId(blockId: any): string | number | nil
@@ -210,11 +280,8 @@ local function resolveBlockId(blockId: any): string | number | nil
end
local function playerHasBlockId(player: Player, blockId: string | number): boolean
- local data = playerInventories[player.UserId]
- if not data then
- return false
- end
- return data.allowed[tostring(blockId)] == true
+ -- Creative mode: allow all block ids.
+ return true
end
local function getServerChunk(cx: number, cy: number, cz: number)
diff --git a/src/StarterGui/Crosshair/LocalScript.client.lua b/src/StarterGui/Crosshair/LocalScript.client.lua
index 92b726b..0dd9e03 100644
--- a/src/StarterGui/Crosshair/LocalScript.client.lua
+++ b/src/StarterGui/Crosshair/LocalScript.client.lua
@@ -7,7 +7,14 @@ end
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local UIS = game:GetService("UserInputService")
-ReplicatedStorage:WaitForChild("Objects"):WaitForChild("MLLoaded")
+local function waitForModLoader()
+ local marker = ReplicatedStorage:WaitForChild("Objects"):WaitForChild("MLLoaded")
+ if marker:IsA("BoolValue") and marker.Value ~= true then
+ marker:GetPropertyChangedSignal("Value"):Wait()
+ end
+end
+
+waitForModLoader()
game:GetService("Players").LocalPlayer.CameraMode = Enum.CameraMode.LockFirstPerson
UIS.MouseIconEnabled = false
@@ -19,4 +26,4 @@ UIS.InputEnded:Connect(function(k)
script.Parent.CrosshairLabel.Visible = not v
script.Parent.DummyButton.Modal = v
end
-end)
\ No newline at end of file
+end)
diff --git a/src/StarterGui/Game_UI/LocalScript.client.lua b/src/StarterGui/Game_UI/LocalScript.client.lua
index 48532ef..3b2a3a2 100644
--- a/src/StarterGui/Game_UI/LocalScript.client.lua
+++ b/src/StarterGui/Game_UI/LocalScript.client.lua
@@ -5,54 +5,48 @@ end
local ui = script.Parent
local ReplicatedStorage = game:GetService("ReplicatedStorage")
+local Players = game:GetService("Players")
+local RunService = game:GetService("RunService")
+local Roact = require(ReplicatedStorage:WaitForChild("Packages"):WaitForChild("Roact"))
+local ReplicaController = require(ReplicatedStorage:WaitForChild("Packages"):WaitForChild("ReplicaController"))
local Inventory = require(ReplicatedStorage.Shared.Inventory)
local Catppuccin = require(ReplicatedStorage.Shared.Catppuccin)
local blocksFolder = ReplicatedStorage:WaitForChild("Blocks")
local mocha = Catppuccin.mocha
-ReplicatedStorage:WaitForChild("Objects"):WaitForChild("MLLoaded")
+local function waitForModLoader()
+ local marker = ReplicatedStorage:WaitForChild("Objects"):WaitForChild("MLLoaded")
+ if marker:IsA("BoolValue") and marker.Value ~= true then
+ marker:GetPropertyChangedSignal("Value"):Wait()
+ end
+end
+
+waitForModLoader()
local cd = ReplicatedStorage.Objects.ChunkDebug:Clone()
local sky = ReplicatedStorage.Objects.Sky:Clone()
local base = ReplicatedStorage.Objects.FakeBaseplate:Clone()
-
cd.Parent = game:GetService("Workspace"):FindFirstChildOfClass("Terrain")
sky.Parent = game:GetService("Workspace"):FindFirstChildOfClass("Terrain")
base.Parent = game:GetService("Workspace"):FindFirstChildOfClass("Terrain")
-local hotbarRoot = ui:WaitForChild("Hotbar")
-local hotbarSlotsRoot = hotbarRoot:WaitForChild("Frame")
-local hotbarSelectLabel = ui:WaitForChild("HotbarItemSelectLabel")
-local hotbarSelectText = hotbarSelectLabel:WaitForChild("TextLabel")
-local hotbarDebug = ui:WaitForChild("HotbarDebug")
-local hotbarDebugText = hotbarDebug:WaitForChild("TextLabel")
-local debugUpperText = ui:WaitForChild("DebugUpperText")
-local hotbarStroke = hotbarRoot:FindFirstChild("UIStroke")
-local hotbarSelectStroke = hotbarSelectLabel:FindFirstChild("UIStroke")
-local hotbarDebugStroke = hotbarDebug:FindFirstChild("UIStroke")
+local debugUpperText = ui:FindFirstChild("DebugUpperText")
+if debugUpperText then
+ debugUpperText:Destroy()
+end
-hotbarRoot.BackgroundColor3 = mocha.base
-if hotbarStroke then
- hotbarStroke.Color = mocha.blue
+local function destroyLegacyUi(name)
+ local inst = ui:FindFirstChild(name)
+ if inst then
+ inst:Destroy()
+ end
end
-hotbarSelectLabel.BackgroundColor3 = mocha.base
-if hotbarSelectStroke then
- hotbarSelectStroke.Color = mocha.blue
-end
-hotbarDebug.BackgroundColor3 = mocha.base
-if hotbarDebugStroke then
- hotbarDebugStroke.Color = mocha.surface2
-end
-hotbarSelectText.TextColor3 = mocha.text
-hotbarDebugText.TextColor3 = mocha.text
-debugUpperText.TextColor3 = mocha.text
-local slotFrames = {}
-local slotStrokes = {}
-local slotLabels = {}
-local renderHotbar = nil
+destroyLegacyUi("Hotbar")
+destroyLegacyUi("HotbarItemSelectLabel")
+destroyLegacyUi("HotbarDebug")
local blockDisplayNames = {}
local blockIcons = {}
@@ -63,164 +57,364 @@ local function rebuildBlockMappings()
for _, block in ipairs(blocksFolder:GetChildren()) do
local id = block:GetAttribute("n")
if id ~= nil then
- blockDisplayNames[tostring(id)] = block.Name
+ local idKey = tostring(id)
+ blockDisplayNames[idKey] = block.Name
local icon = block:GetAttribute("icon")
if typeof(icon) == "string" and icon ~= "" then
- blockIcons[tostring(id)] = icon
- end
- end
- end
- if renderHotbar then
- renderHotbar()
- end
-end
-
-rebuildBlockMappings()
-blocksFolder.ChildAdded:Connect(rebuildBlockMappings)
-blocksFolder.ChildRemoved:Connect(rebuildBlockMappings)
-
-for i = 1, Inventory.GetHotbarSize() do
- local slotName = `HotbarSlot{i - 1}`
- local slot = hotbarSlotsRoot:WaitForChild(slotName)
- slotFrames[i] = slot
- local stroke = slot:FindFirstChild("UIStroke")
- if not stroke then
- stroke = Instance.new("UIStroke")
- stroke.Parent = slot
- end
- slotStrokes[i] = stroke
-
- local imageLabel = slot:FindFirstChild("ImageLabel")
- if not imageLabel then
- imageLabel = Instance.new("ImageLabel")
- imageLabel.Name = "ImageLabel"
- imageLabel.BackgroundTransparency = 1
- imageLabel.Size = UDim2.fromScale(1, 1)
- imageLabel.ScaleType = Enum.ScaleType.Fit
- imageLabel.Parent = slot
- end
-
- local label = slot:FindFirstChild("TextLabel")
- if not label then
- label = Instance.new("TextLabel")
- label.Name = "TextLabel"
- label.BackgroundTransparency = 1
- label.Size = UDim2.fromScale(1, 1)
- label.TextScaled = true
- label.Font = Enum.Font.Gotham
- label.TextColor3 = mocha.text
- label.TextWrapped = true
- label.ZIndex = 3
- label.Parent = slot
- end
- slotLabels[i] = label
-end
-
-renderHotbar = function()
- for i = 1, Inventory.GetHotbarSize() do
- local slot = slotFrames[i]
- if slot then
- slot.BackgroundColor3 = mocha.surface0
- local stroke = slotStrokes[i]
- if stroke then
- if i == Inventory.GetSelectedIndex() then
- stroke.Color = mocha.blue
- stroke.Thickness = 2
- else
- stroke.Color = mocha.surface2
- stroke.Thickness = 1
- end
- end
-
- local imageLabel = slot:FindFirstChild("ImageLabel")
- local id = Inventory.GetSlot(i)
- local displayName = id and (blockDisplayNames[tostring(id)] or tostring(id)) or ""
- if imageLabel then
- local icon = id and blockIcons[tostring(id)] or nil
- imageLabel.Visible = icon ~= nil
- imageLabel.Image = icon or ""
- imageLabel.BackgroundTransparency = 1
- imageLabel.ZIndex = 2
- end
-
- local textLabel = slotLabels[i]
- if textLabel then
- textLabel.Text = displayName
- textLabel.TextColor3 = mocha.text
- textLabel.Visible = displayName ~= ""
+ blockIcons[idKey] = icon
end
end
end
end
-local function updateSelectedLabel()
- local id = Inventory.GetSelectedId()
- local displayName = id and (blockDisplayNames[tostring(id)] or tostring(id)) or nil
- if hotbarSelectText then
- hotbarSelectText.Text = displayName and `Selected: {displayName}` or "Selected: (empty)"
- end
- if hotbarDebugText then
- hotbarDebugText.Text = `Slot {Inventory.GetSelectedIndex()} Id {displayName or "empty"}`
+local function getDisplayName(id)
+ if id == nil then
+ return nil
end
+ return blockDisplayNames[tostring(id)] or tostring(id)
end
-local extraSlot = hotbarSlotsRoot and hotbarSlotsRoot:FindFirstChild("HotbarSlot9")
-if extraSlot then
- extraSlot.Visible = false
+local HOTBAR_SLOTS = Inventory.GetHotbarSize()
+local SLOT_SIZE = 40
+local SLOT_PADDING = 6
+local HOTBAR_PADDING = 6
+local HOTBAR_WIDTH = (SLOT_SIZE * HOTBAR_SLOTS) + (SLOT_PADDING * (HOTBAR_SLOTS - 1)) + (HOTBAR_PADDING * 2)
+local HOTBAR_HEIGHT = SLOT_SIZE + (HOTBAR_PADDING * 2)
+local LABEL_WIDTH = 150
+local LABEL_HEIGHT = 26
+local HOTBAR_BOTTOM_OFFSET = 20
+local LABEL_GAP = 8
+
+local function isDevPlayer()
+ if RunService:IsStudio() then
+ return true
+ end
+ local player = Players.LocalPlayer
+ if game.CreatorType == Enum.CreatorType.User then
+ return player.UserId == game.CreatorId
+ end
+ if game.CreatorType == Enum.CreatorType.Group then
+ return player:IsInGroup(game.CreatorId)
+ end
+ return false
end
-renderHotbar()
-updateSelectedLabel()
+local HotbarApp = Roact.Component:extend("HotbarApp")
-Inventory.OnChanged(function()
- renderHotbar()
- updateSelectedLabel()
-end)
+function HotbarApp:init()
+ self.state = {
+ slots = table.clone(Inventory.GetSlots()),
+ selectedIndex = Inventory.GetSelectedIndex(),
+ selectedName = getDisplayName(Inventory.GetSelectedId()) or "empty",
+ positionDebug = "",
+ inventoryDebug = "",
+ }
+end
-Inventory.OnSelected(function()
- renderHotbar()
- updateSelectedLabel()
-end)
+function HotbarApp:didMount()
+ local function update()
+ local selectedId = Inventory.GetSelectedId()
+ local selectedDisplay = getDisplayName(selectedId) or "empty"
+ local filled = 0
+ for i = 1, HOTBAR_SLOTS do
+ if Inventory.GetSlot(i) ~= nil then
+ filled += 1
+ end
+ end
+ self:setState({
+ slots = table.clone(Inventory.GetSlots()),
+ selectedIndex = Inventory.GetSelectedIndex(),
+ selectedName = selectedDisplay,
+ inventoryDebug = `Slot {Inventory.GetSelectedIndex()} Id {selectedDisplay} Filled {filled}/{HOTBAR_SLOTS}`,
+ })
+ end
-game:GetService("RunService").RenderStepped:Connect(function(dt)
- local fps = math.round(1/dt)
- pcall(function()
- -- pos in chunks of 32 studs of char
- local pos = game:GetService("Players").LocalPlayer.Character:GetPivot()
- local chunk = {
- x = math.round(pos.X/32),
- y = math.round(pos.Y/32),
- z = math.round(pos.Z/32)
- }
-
- if math.abs(chunk.x) == 0 then chunk.x = 0 end
- if math.abs(chunk.y) == 0 then chunk.y = 0 end
- if math.abs(chunk.z) == 0 then chunk.z = 0 end
-
- local bpos = {
- x = math.round(pos.X/4),
- y = math.round(pos.Y/4),
- z = math.round(pos.Z/4)
- }
+ rebuildBlockMappings()
+ update()
- if math.abs(bpos.x) == 0 then bpos.x = 0 end
- if math.abs(bpos.y) == 0 then bpos.y = 0 end
- if math.abs(bpos.z) == 0 then bpos.z = 0 end
-
- sky.CFrame = pos
- ui.DebugUpperText.Text = `Chunk {chunk.x} {chunk.y} {chunk.z}\nPos {bpos.x} {bpos.y} {bpos.z}\n{fps} FPS`
-
- cd:PivotTo(CFrame.new(
- chunk.x*32,
- chunk.y*32,
- chunk.z*32
- ))
-
- base.CFrame = CFrame.new(
- chunk.x*32,
- -24,
- chunk.z*32
- )
-
+ self._changedConn = Inventory.OnChanged(update)
+ self._selectedConn = Inventory.OnSelected(update)
+ self._blockAdded = blocksFolder.ChildAdded:Connect(function()
+ rebuildBlockMappings()
+ update()
end)
-end)
+ self._blockRemoved = blocksFolder.ChildRemoved:Connect(function()
+ rebuildBlockMappings()
+ update()
+ end)
+
+ self._renderConn = RunService.RenderStepped:Connect(function(dt)
+ local fps = math.round(1 / dt)
+ pcall(function()
+ local pos = Players.LocalPlayer.Character:GetPivot()
+ local chunk = {
+ x = math.round(pos.X / 32),
+ y = math.round(pos.Y / 32),
+ z = math.round(pos.Z / 32)
+ }
+
+ if math.abs(chunk.x) == 0 then chunk.x = 0 end
+ if math.abs(chunk.y) == 0 then chunk.y = 0 end
+ if math.abs(chunk.z) == 0 then chunk.z = 0 end
+
+ local bpos = {
+ x = math.round(pos.X / 4),
+ y = math.round(pos.Y / 4),
+ z = math.round(pos.Z / 4)
+ }
+
+ if math.abs(bpos.x) == 0 then bpos.x = 0 end
+ if math.abs(bpos.y) == 0 then bpos.y = 0 end
+ if math.abs(bpos.z) == 0 then bpos.z = 0 end
+
+ sky.CFrame = pos
+ cd:PivotTo(CFrame.new(
+ chunk.x * 32,
+ chunk.y * 32,
+ chunk.z * 32
+ ))
+
+ base.CFrame = CFrame.new(
+ chunk.x * 32,
+ -24,
+ chunk.z * 32
+ )
+
+ self:setState({
+ positionDebug = `Chunk {chunk.x} {chunk.y} {chunk.z}\nPos {bpos.x} {bpos.y} {bpos.z}\n{fps} FPS`,
+ })
+ end)
+ end)
+end
+
+function HotbarApp:willUnmount()
+ if self._changedConn then
+ self._changedConn:Disconnect()
+ self._changedConn = nil
+ end
+ if self._selectedConn then
+ self._selectedConn:Disconnect()
+ self._selectedConn = nil
+ end
+ if self._blockAdded then
+ self._blockAdded:Disconnect()
+ self._blockAdded = nil
+ end
+ if self._blockRemoved then
+ self._blockRemoved:Disconnect()
+ self._blockRemoved = nil
+ end
+ if self._renderConn then
+ self._renderConn:Disconnect()
+ self._renderConn = nil
+ end
+end
+
+function HotbarApp:render()
+ local slots = self.state.slots or {}
+ local selectedIndex = self.state.selectedIndex or 1
+ local selectedName = self.state.selectedName or "empty"
+ local positionDebug = self.state.positionDebug or ""
+ local inventoryDebug = self.state.inventoryDebug or ""
+ local debugText = positionDebug
+ if inventoryDebug ~= "" then
+ debugText ..= "\n" .. inventoryDebug
+ end
+
+ local slotChildren = {
+ Layout = Roact.createElement("UIListLayout", {
+ FillDirection = Enum.FillDirection.Horizontal,
+ HorizontalAlignment = Enum.HorizontalAlignment.Center,
+ VerticalAlignment = Enum.VerticalAlignment.Center,
+ SortOrder = Enum.SortOrder.LayoutOrder,
+ Padding = UDim.new(0, SLOT_PADDING),
+ }),
+ Padding = Roact.createElement("UIPadding", {
+ PaddingLeft = UDim.new(0, HOTBAR_PADDING),
+ PaddingRight = UDim.new(0, HOTBAR_PADDING),
+ PaddingTop = UDim.new(0, HOTBAR_PADDING),
+ PaddingBottom = UDim.new(0, HOTBAR_PADDING),
+ }),
+ }
+
+ for i = 1, HOTBAR_SLOTS do
+ local id = slots[i]
+ local displayName = getDisplayName(id) or ""
+ local icon = id and blockIcons[tostring(id)] or nil
+ local isSelected = i == selectedIndex
+
+ slotChildren["Slot" .. i] = Roact.createElement("Frame", {
+ Name = "Slot" .. i,
+ Size = UDim2.fromOffset(SLOT_SIZE, SLOT_SIZE),
+ BackgroundColor3 = mocha.surface0,
+ LayoutOrder = i,
+ }, {
+ Corner = Roact.createElement("UICorner", {
+ CornerRadius = UDim.new(0, 8),
+ }),
+ Stroke = Roact.createElement("UIStroke", {
+ Color = isSelected and mocha.blue or mocha.surface2,
+ Thickness = isSelected and 2 or 1,
+ }),
+ Icon = Roact.createElement("ImageLabel", {
+ BackgroundTransparency = 1,
+ Size = UDim2.fromScale(1, 1),
+ Image = icon or "",
+ Visible = icon ~= nil,
+ ScaleType = Enum.ScaleType.Fit,
+ ZIndex = 2,
+ }),
+ Label = Roact.createElement("TextLabel", {
+ BackgroundTransparency = 1,
+ Size = UDim2.fromScale(1, 1),
+ Text = displayName,
+ TextColor3 = mocha.text,
+ Font = Enum.Font.GothamMedium,
+ TextSize = 12,
+ TextWrapped = true,
+ Visible = icon == nil and displayName ~= "",
+ ZIndex = 3,
+ }),
+ })
+ end
+
+ return Roact.createElement("Folder", nil, {
+ DebugUpperText = Roact.createElement("TextLabel", {
+ Name = "DebugUpperText",
+ AnchorPoint = Vector2.new(0, 0),
+ Position = UDim2.new(0, 12, 0, 12),
+ Size = UDim2.fromOffset(300, 90),
+ BackgroundTransparency = 1,
+ Text = debugText,
+ TextColor3 = mocha.red,
+ Font = Enum.Font.GothamMedium,
+ TextSize = 14,
+ RichText = true,
+ TextXAlignment = Enum.TextXAlignment.Left,
+ TextYAlignment = Enum.TextYAlignment.Top,
+ }),
+ SelectedLabel = Roact.createElement("TextLabel", {
+ Name = "HotbarItemLabel",
+ AnchorPoint = Vector2.new(0.5, 1),
+ Position = UDim2.new(0.5, 0, 1, -(HOTBAR_BOTTOM_OFFSET + HOTBAR_HEIGHT + LABEL_GAP)),
+ Size = UDim2.fromOffset(LABEL_WIDTH, LABEL_HEIGHT),
+ BackgroundColor3 = mocha.base,
+ Text = selectedName,
+ TextColor3 = mocha.text,
+ Font = Enum.Font.JosefinSans,
+ TextSize = 14,
+ TextTruncate = Enum.TextTruncate.AtEnd,
+ }, {
+ Corner = Roact.createElement("UICorner", {
+ CornerRadius = UDim.new(0, 8),
+ }),
+ Stroke = Roact.createElement("UIStroke", {
+ Color = mocha.blue,
+ Thickness = 2,
+ ApplyStrokeMode = Enum.ApplyStrokeMode.Border,
+ }),
+ Padding = Roact.createElement("UIPadding", {
+ PaddingLeft = UDim.new(0, 10),
+ PaddingRight = UDim.new(0, 10),
+ }),
+ }),
+ Hotbar = Roact.createElement("Frame", {
+ Name = "Hotbar",
+ AnchorPoint = Vector2.new(0.5, 1),
+ Position = UDim2.new(0.5, 0, 1, -HOTBAR_BOTTOM_OFFSET),
+ Size = UDim2.fromOffset(HOTBAR_WIDTH, HOTBAR_HEIGHT),
+ BackgroundColor3 = mocha.base,
+ }, {
+ Corner = Roact.createElement("UICorner", {
+ CornerRadius = UDim.new(0, 12),
+ }),
+ Stroke = Roact.createElement("UIStroke", {
+ Color = mocha.blue,
+ Thickness = 2,
+ ApplyStrokeMode = Enum.ApplyStrokeMode.Border,
+ }),
+ Slots = Roact.createElement("Frame", {
+ BackgroundTransparency = 1,
+ Size = UDim2.fromScale(1, 1),
+ }, slotChildren),
+ }),
+ })
+end
+
+local ReplicaDebugger = Roact.Component:extend("ReplicaDebugger")
+
+function ReplicaDebugger:init()
+ self._replicaCount = 0
+ self.state = {
+ replicaCount = 0,
+ initialData = ReplicaController.InitialDataReceived == true,
+ }
+end
+
+function ReplicaDebugger:didMount()
+ self._newReplicaConn = ReplicaController.NewReplicaSignal:Connect(function()
+ self._replicaCount += 1
+ self:setState({
+ replicaCount = self._replicaCount,
+ })
+ end)
+ self._initialConn = ReplicaController.InitialDataReceivedSignal:Connect(function()
+ self:setState({
+ initialData = true,
+ })
+ end)
+end
+
+function ReplicaDebugger:willUnmount()
+ if self._newReplicaConn then
+ self._newReplicaConn:Disconnect()
+ self._newReplicaConn = nil
+ end
+ if self._initialConn then
+ self._initialConn:Disconnect()
+ self._initialConn = nil
+ end
+end
+
+function ReplicaDebugger:render()
+ return Roact.createElement("TextLabel", {
+ Name = "ReplicaDebugger",
+ AnchorPoint = Vector2.new(1, 0),
+ Position = UDim2.new(1, -12, 0, 12),
+ Size = UDim2.fromOffset(220, 60),
+ BackgroundColor3 = mocha.mantle,
+ BackgroundTransparency = 0.2,
+ Text = `Replica (client)\nInitial: {tostring(self.state.initialData)}\nReplicas: {self.state.replicaCount}`,
+ TextColor3 = mocha.text,
+ Font = Enum.Font.GothamMedium,
+ TextSize = 12,
+ TextXAlignment = Enum.TextXAlignment.Left,
+ TextYAlignment = Enum.TextYAlignment.Top,
+ }, {
+ Corner = Roact.createElement("UICorner", {
+ CornerRadius = UDim.new(0, 8),
+ }),
+ Padding = Roact.createElement("UIPadding", {
+ PaddingLeft = UDim.new(0, 8),
+ PaddingRight = UDim.new(0, 8),
+ PaddingTop = UDim.new(0, 6),
+ PaddingBottom = UDim.new(0, 6),
+ }),
+ Stroke = Roact.createElement("UIStroke", {
+ Color = mocha.surface2,
+ Thickness = 1,
+ }),
+ })
+end
+
+Roact.mount(Roact.createElement(HotbarApp), ui, "HotbarRoact")
+
+if isDevPlayer() then
+ local playerGui = Players.LocalPlayer:WaitForChild("PlayerGui")
+ local debugGui = Instance.new("ScreenGui")
+ debugGui.Name = "ReplicaDebugger"
+ debugGui.ResetOnSpawn = false
+ debugGui.IgnoreGuiInset = true
+ debugGui.Parent = playerGui
+ Roact.mount(Roact.createElement(ReplicaDebugger), debugGui, "ReplicaDebugger")
+end
diff --git a/src/StarterPlayer/StarterPlayerScripts/Actor/Init.client.lua b/src/StarterPlayer/StarterPlayerScripts/Actor/ActorInit.client.lua
similarity index 69%
rename from src/StarterPlayer/StarterPlayerScripts/Actor/Init.client.lua
rename to src/StarterPlayer/StarterPlayerScripts/Actor/ActorInit.client.lua
index deccdd2..e8cbcd1 100644
--- a/src/StarterPlayer/StarterPlayerScripts/Actor/Init.client.lua
+++ b/src/StarterPlayer/StarterPlayerScripts/Actor/ActorInit.client.lua
@@ -9,7 +9,14 @@ end)
local ReplicatedStorage = game:GetService("ReplicatedStorage")
-ReplicatedStorage:WaitForChild("Objects"):WaitForChild("MLLoaded")
+local function waitForModLoader()
+ local marker = ReplicatedStorage:WaitForChild("Objects"):WaitForChild("MLLoaded")
+ if marker:IsA("BoolValue") and marker.Value ~= true then
+ marker:GetPropertyChangedSignal("Value"):Wait()
+ end
+end
+
+waitForModLoader()
local ML = require(ReplicatedStorage:WaitForChild("Shared"):WaitForChild("ModLoader"))
diff --git a/src/StarterPlayer/StarterPlayerScripts/Actor/BlockInteraction.client.lua b/src/StarterPlayer/StarterPlayerScripts/Actor/BlockInteraction.client.lua
index 90ba90e..bd9bf2a 100644
--- a/src/StarterPlayer/StarterPlayerScripts/Actor/BlockInteraction.client.lua
+++ b/src/StarterPlayer/StarterPlayerScripts/Actor/BlockInteraction.client.lua
@@ -5,13 +5,18 @@ end
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local UIS = game:GetService("UserInputService")
-ReplicatedStorage:WaitForChild("Objects"):WaitForChild("MLLoaded")
+local function waitForModLoader()
+ local marker = ReplicatedStorage:WaitForChild("Objects"):WaitForChild("MLLoaded")
+ if marker:IsA("BoolValue") and marker.Value ~= true then
+ marker:GetPropertyChangedSignal("Value"):Wait()
+ end
+end
+
+waitForModLoader()
local PM = require(ReplicatedStorage.Shared.PlacementManager)
local Inventory = require(ReplicatedStorage.Shared.Inventory)
-local remotes = ReplicatedStorage:WaitForChild("Remotes")
-local inventorySync = remotes:WaitForChild("InventorySync")
-local inventoryRequest = remotes:WaitForChild("InventoryRequest")
+local ReplicaController = require(ReplicatedStorage:WaitForChild("Packages"):WaitForChild("ReplicaController"))
local keyToSlot = {
[Enum.KeyCode.One] = 1,
@@ -36,14 +41,22 @@ local function findNextFilledIndex(startIndex: number, direction: number): numbe
return startIndex
end
-inventorySync.OnClientEvent:Connect(function(slots)
+local function applySlots(slots)
if typeof(slots) ~= "table" then
return
end
Inventory.SetSlots(slots)
+end
+
+ReplicaController.ReplicaOfClassCreated("Inventory", function(replica)
+ applySlots(replica.Data.Slots)
+ local changeConn = replica:ListenToChange({"Slots"}, function(newSlots)
+ applySlots(newSlots)
+ end)
+ replica:AddCleanupTask(changeConn)
end)
-inventoryRequest:FireServer()
+ReplicaController.RequestData()
UIS.InputBegan:Connect(function(input: InputObject, gameProcessedEvent: boolean)
if gameProcessedEvent then
@@ -52,9 +65,7 @@ UIS.InputBegan:Connect(function(input: InputObject, gameProcessedEvent: boolean)
local slot = keyToSlot[input.KeyCode]
if slot then
- if Inventory.GetSlot(slot) ~= nil then
- Inventory.SetSelectedIndex(slot)
- end
+ Inventory.SetSelectedIndex(slot)
return
end
@@ -87,7 +98,6 @@ UIS.InputChanged:Connect(function(input: InputObject, gameProcessedEvent: boolea
return
end
local direction = delta > 0 and -1 or 1
- local nextIndex = findNextFilledIndex(Inventory.GetSelectedIndex(), direction)
- Inventory.SetSelectedIndex(nextIndex)
+ Inventory.SetSelectedIndex(((Inventory.GetSelectedIndex() - 1 + direction) % Inventory.GetHotbarSize()) + 1)
end
end)
diff --git a/wally.lock b/wally.lock
new file mode 100644
index 0000000..9347d21
--- /dev/null
+++ b/wally.lock
@@ -0,0 +1,23 @@
+# This file is automatically @generated by Wally.
+# It is not intended for manual editing.
+registry = "test"
+
+[[package]]
+name = "etheroit/replicacontroller"
+version = "1.0.0"
+dependencies = []
+
+[[package]]
+name = "etheroit/replicaservice"
+version = "1.0.2"
+dependencies = []
+
+[[package]]
+name = "ocbwoy3/minecraft-roblox"
+version = "0.1.0"
+dependencies = [["ReplicaController", "etheroit/replicacontroller@1.0.0"], ["Roact", "roblox/roact@1.4.4"], ["ReplicaService", "etheroit/replicaservice@1.0.2"]]
+
+[[package]]
+name = "roblox/roact"
+version = "1.4.4"
+dependencies = []
diff --git a/wally.toml b/wally.toml
new file mode 100644
index 0000000..27b756e
--- /dev/null
+++ b/wally.toml
@@ -0,0 +1,15 @@
+[package]
+name = "ocbwoy3-development-studios/minecraft-roblox"
+description = "A Roblox game inspired by Minecraft's world building system"
+version = "0.1.0"
+registry = "https://github.com/UpliftGames/wally-index"
+realm = "shared"
+authors = ["ocbwoy3 "]
+private = true
+
+[dependencies]
+Roact = "roblox/roact@1.4.4"
+ReplicaController = "etheroit/replicacontroller@1.0.0"
+
+[server-dependencies]
+ReplicaService = "etheroit/replicaservice@1.0.2"