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"