Can anyone help me fix my placement inventory system?

I am making a building game but I have been stuck on trying to get the placement inventory calculations correct.

The main problem is the scaled blocks to add up with the placed blocks.
Placing and deleting the blocks normally works well.

I tried to add it by the total inventory amount minus the total scaled amount but that isnt working right.

local placement = {}

local ReplicatedStorage = game:GetService("ReplicatedStorage")
local RunService = game:GetService("RunService")
local CollectionService = game:GetService("CollectionService")

local Debris = game:GetService("Debris")

local DataStore2 = require(game.ServerScriptService.ServerModules.DataStore2)
DataStore2.Combine("DATA", "inventory")

local InventoryModule = require(game.ServerScriptService.ServerModules.InventoryServer)
local pooling = require(ReplicatedStorage.Utils.Pooling)
local getBlockInBounds = require(ReplicatedStorage.Utils.BlockInBounds)

local PoolInstance = pooling.new(ReplicatedStorage.Blocks)

local Weld = require(game.ServerScriptService.ServerModules.Utils.SurfaceWeld)

local placedBlocksFolder
local plotsFolder = workspace.Plots

placement.deleteDebounce = {}
placement.PlacementInventory = {}
placement.CachedBlockProperties = {}

local function getAllScaledAmount(model, userFolder)
	print("Past 1")
	if not model then return 1 end
	print("past 2")
	if not userFolder then return 1 end
	print("past 3")
	local BlockId = model:GetAttribute("BLOCKID")
	if not BlockId then return 1 end
	print("past 4")
	
	local amount = 0
	for _, block in pairs(userFolder:GetChildren()) do
		print(block)
		if block:GetAttribute("BLOCKID") == BlockId then
			local part = block:FindFirstChildWhichIsA("BasePart")
			
			local size = part.Size
			local blocksX = math.floor(size.X / 2)
			local blocksY = math.floor(size.Y / 2)
			local blocksZ = math.floor(size.Z / 2)
			amount = amount + (blocksX * blocksY * blocksZ)
		end
	end

	return amount
end

local function validateBounds(partInPaidBounds, partInBounds, gamepassValue)
	if partInPaidBounds and not partInBounds then
		if gamepassValue.Value then
			return true
		else
			return false
		end
	elseif partInBounds and partInPaidBounds then
		return true
	end
end

local function getPlacedBlocks(player, targetBLOCKID)
	local matchedBlocks = {}
	local placedFolder = workspace:WaitForChild("PlacedBlocks"):FindFirstChild(player.UserId)
	if not placedFolder then return matchedBlocks end

	for _, blockModel in pairs(placedFolder:GetChildren()) do
		if blockModel:IsA("Model") and blockModel:GetAttribute("BLOCKID") == targetBLOCKID and blockModel.Name ~= "GhostModel" then
			table.insert(matchedBlocks, blockModel)
		end
	end
	return matchedBlocks
end

local function DestroyWeldsReferencingParts(parts, root)
	root = root or workspace
	local partsSet = {}
	for _, part in pairs(parts) do
		partsSet[part] = true
	end

	for _, weld in ipairs(root:GetDescendants()) do
		if weld:IsA("WeldConstraint") or weld:IsA("Weld") or weld:IsA("Motor6D") then
			if partsSet[weld.Part0] or partsSet[weld.Part1] then
				weld:Destroy()
			end
		end
	end
end

local function findItemIndex(inventoryTable, BLOCKID)
	for i, item in ipairs(inventoryTable) do
		if item.BLOCKID == BLOCKID then
			return i
		end
	end
	return nil
end

function placement:Init()
	local folder = Instance.new("Folder")
	folder.Name = "PlacedBlocks"
	placedBlocksFolder = folder
	folder.Parent = workspace

	game.Players.PlayerAdded:Connect(function(player)
		local userId = player.UserId

		local playerFolder = Instance.new("Folder")
		playerFolder.Name = userId
		playerFolder.Parent = folder
		
		local blockConnections = {}

		playerFolder.ChildAdded:Connect(function(block)
			if not block or not block:IsA("Instance") then return end
			local blockId = block:GetAttribute("BLOCKID")
			if not blockId then return end
			
			local inventory = InventoryModule.getPlayerInventory(player)
			local index = findItemIndex(inventory, blockId)
			
			if blockConnections[block] then
				blockConnections[block]:Disconnect()
				blockConnections[block] = nil
			end
			
			placement.CachedBlockProperties[block] = placement.CachedBlockProperties[block] or {}
			local basePart = block:FindFirstChildWhichIsA("BasePart")
			if not basePart then return end
			placement.CachedBlockProperties[block].Size = basePart.Size
			
			blockConnections[block] = block.AncestryChanged:Connect(function(_, parent)
				if parent ~= nil then return end

				if blockConnections[block] then
					blockConnections[block]:Disconnect()
					blockConnections[block] = nil
				end

				local blockId = block:GetAttribute("BLOCKID")
				if not blockId then return end
				
				local amount = 0
				if placement.CachedBlockProperties[block] then
					amount = getAllScaledAmount(block, playerFolder)
					placement.CachedBlockProperties[block] = nil
				end
				
				local currentAmount = 0
				if index then
					currentAmount = inventory[index].AMOUNT or 0
				end
				
				placement.PlacementInventory[player.UserId] = placement.PlacementInventory[player.UserId] or {}
				placement.PlacementInventory[player.UserId][blockId] = currentAmount + amount

				if ReplicatedStorage.Remotes:FindFirstChild("UpdateInventorySlots") then
					ReplicatedStorage.Remotes.UpdateInventorySlots:FireClient(
						player,
						blockId,
						placement.PlacementInventory[player.UserId][blockId]
					)
				end
			end)
		end)
	end)

	game.Players.PlayerRemoving:Connect(function(player)
		local userId = player.UserId
		placement.PlacementInventory[userId] = nil
		local playerFolder = folder:FindFirstChild(userId)
		if playerFolder then
			playerFolder:Destroy()
		end
	end)
	
	-- Fires when adding new blocks to inventory
	ReplicatedStorage.Remotes.UpdatePlacementInventory.Event:Connect(function(player, amount, blockId)
		local placement = require(game.ServerScriptService.ServerModules.PlacementServer)
		if not placement.PlacementInventory[player.UserId] then
			placement.PlacementInventory[player.UserId] = placement.PlacementInventory[player.UserId] or {}
		end
		placement.PlacementInventory[player.UserId][blockId] = (placement.PlacementInventory[player.UserId][blockId] or 0) + amount
		ReplicatedStorage.Remotes.UpdateInventorySlots:FireClient(player, blockId, placement.PlacementInventory[player.UserId][blockId])
	end)
end

local function GetBlockById(targetBlockId)
	local block = nil
	
	for _, block in pairs(game.ReplicatedStorage.Blocks:GetChildren()) do
		if block:GetAttribute("BLOCKID") == targetBlockId then
			block = block
			return block
		end
	end
	
	return block
end

function placement.Place(player, pivot, blockId, anchor)
	local inventory = InventoryModule.getPlayerInventory(player)
	local index = findItemIndex(inventory, blockId)

	local validateAmount
	if placement.PlacementInventory[player.UserId] then
		validateAmount = InventoryModule.ValidatePlayerBlockAmount(player, blockId, placement.PlacementInventory[player.UserId][blockId])
	else
		validateAmount = InventoryModule.ValidatePlayerBlockAmount(player, blockId, inventory[index] and inventory[index].AMOUNT or 0)
	end
	if not validateAmount then
		return {Result = "Rejected", placedBlock = nil}
	end

	local block = GetBlockById(blockId)
	if not block then 
		return {Result = "Rejected", placedBlock = nil}
	end

	local gamePassFolder = player:FindFirstChild("Gamepasses")
	if not gamePassFolder then return {Result = "Rejected"} end
	local gamepassValue = gamePassFolder:FindFirstChild("Extended_Plot")
	if not gamepassValue then return {Result = "Rejected"} end
	local playerTeam = player.Team
	if not playerTeam then return {Result = "Rejected"} end
	local playerPlot = plotsFolder:FindFirstChild(playerTeam.Name)
	if not playerPlot then return {Result = "Rejected"} end
	local launched = playerPlot:FindFirstChild("Launched")
	if not launched or launched.Value then
		return {Result = "Rejected"}
	end

	local normalBounds = playerPlot:FindFirstChild("FreeBuildBounds")
	local paidBounds = playerPlot:FindFirstChild("PaidBuildBounds")
	if not normalBounds or not paidBounds then return {Result = "Rejected"} end
	local partsInBounds = getBlockInBounds.IsPivotInBounds(pivot, normalBounds)
	local partsInPaidBounds = getBlockInBounds.IsPivotInBounds(pivot, paidBounds)
	local validatedBounds = validateBounds(partsInPaidBounds, partsInBounds, gamepassValue)
	if not validatedBounds then
		return {Result = "Rejected"}
	end

	local PlacedBlock = PoolInstance:Get(block.Name)
	local blockChildren = PlacedBlock:GetChildren()
	PlacedBlock.PrimaryPart = blockChildren[1]
	PlacedBlock:SetAttribute("Owner", player.UserId)
	PlacedBlock:SetAttribute("Plot", playerTeam.Name)
	PlacedBlock:PivotTo(pivot)
	local playerFolder = placedBlocksFolder:FindFirstChild(player.UserId)
	if not playerFolder then
		return {Result = "Rejected"}
	end
	PlacedBlock.Parent = playerFolder

	placement.PlacementInventory[player.UserId] = placement.PlacementInventory[player.UserId] or {}

	local scaledAmount = getAllScaledAmount(PlacedBlock, playerFolder)

	local currentAmount = 0
	if index then
		currentAmount = inventory[index].AMOUNT or 0
	end
	placement.PlacementInventory[player.UserId][blockId] = currentAmount - scaledAmount

	ReplicatedStorage.Remotes.UpdateInventorySlots:FireClient(player, blockId, placement.PlacementInventory[player.UserId][blockId])

	Weld.WeldModel(PlacedBlock)
	if not anchor then
		for _, part in pairs(PlacedBlock:GetDescendants()) do
			if part:IsA("BasePart") then
				part.Anchored = false
			end
		end
	end

	return {Result = "Success", placedBlock = PlacedBlock}
end

function placement.DeleteBlock(player, block)
	coroutine.wrap(function()
		if not block or not block:IsA("Model") then return end

		local PlacedBlocks = workspace:FindFirstChild("PlacedBlocks")
		if not PlacedBlocks then return end

		local userFolder = PlacedBlocks:FindFirstChild(player.UserId)
		if not userFolder then return end

		local owner = block:GetAttribute("Owner")

		if not block:IsDescendantOf(userFolder) then return end
		if (not owner) == player.UserId then return end

		local partsToDelete = {}
		for _, part in ipairs(block:GetDescendants()) do
			if part:IsA("BasePart") then
				part.Anchored = true
				part.CanCollide = false
				table.insert(partsToDelete, part)
			end
		end

		DestroyWeldsReferencingParts(partsToDelete, userFolder)

		local playerInventory = InventoryModule.getPlayerInventory(player)
		if not playerInventory then return end

		local blockId = block:GetAttribute("BLOCKID")
		if not blockId then return end

		local index = findItemIndex(playerInventory, blockId)
		if not index then return end

		local inventoryEntry = playerInventory[index]
		if not inventoryEntry then return end

		RunService.Heartbeat:Wait()

		block:Destroy()
		Weld.PruneWeldCache()

		local currentAmount = 0

		if index then
			currentAmount = playerInventory[index].AMOUNT or 0
		end
	end)()
end

function placement.Scale(player, part, cframe, size)
	local userFolder = placedBlocksFolder:FindFirstChild(player.UserId)
	if not userFolder then
		return false
	end
	
	if not part then
		return false
	end
	
	local block = part.Parent
	if not block then
		return false	
	end
	
	if not CollectionService:HasTag(block, "Scalable") then
		return false
	end
	
	local blockId = block:GetAttribute("BLOCKID")
	if not blockId then
		return false
	end
	
	local inventory = InventoryModule.getPlayerInventory(player)
	local index = findItemIndex(inventory, blockId)

	local validateAmount
	if placement.PlacementInventory[player.UserId] then
		validateAmount = InventoryModule.ValidatePlayerBlockAmount(player, blockId, placement.PlacementInventory[player.UserId][blockId])
	else
		validateAmount = InventoryModule.ValidatePlayerBlockAmount(player, blockId, inventory[index] and inventory[index].AMOUNT or 0)
	end
	if not validateAmount then
		return false
	end
	
	local prevSize = part.Size
	local prevCframe = part.CFrame
	
	part.Size = size
	part.CFrame = cframe
	
	local totalAmount = getAllScaledAmount(block, userFolder)
	print(totalAmount)
	
	local currentAmount = 0
	if index then
		currentAmount = inventory[index].AMOUNT or 0
	end
	
	if currentAmount - totalAmount <= 0 then
		part.Size = prevSize
		part.CFrame = prevCframe
		return false
	end
	
	Weld.ClearWelds(block)
	Weld.PruneWeldCache(block)
	Weld.WeldModel(block)
	
	placement.PlacementInventory[player.UserId][blockId] = currentAmount - totalAmount

	ReplicatedStorage.Remotes.UpdateInventorySlots:FireClient(player, blockId, placement.PlacementInventory[player.UserId][blockId])
	
	placement.CachedBlockProperties[block] = placement.CachedBlockProperties[block] or {}
	placement.CachedBlockProperties[block].Size = part.Size
	
	return true
end

function placement.ClearAll(folder, player)
	coroutine.wrap(function()
		for i, block in ipairs(folder:GetChildren()) do
			if not folder then return end
			placement.DeleteBlock(player, block)
		end
	end)()
end

ReplicatedStorage.Remotes.InventoryUpdateFinish.OnServerEvent:Connect(function(player)
	if placement.deleteDebounce[player.UserId] then
		placement.deleteDebounce[player.UserId] = nil
	end
end)


return placement

I’d appreciate any help since this is the biggest pain in this game to work on and I’ve been stuck on this for over 3 days.

1 Like

Please send a shortened version of the script with the exact area(s) where you think the error is occuring. Keep the full script in your post though.

Also, explain how your script works. What are scaled blocks?

Sorry that it took a while to respond, I took a break for a few days.

Scaled blocks are placed blocks that had their sizes changed by the scale tool I made. (kinda like build a boat’s scale tool)

This script is the server sided module that validates placed/scaled/deleted blocks before actually changing them. These actions are done first on the client (for looks) and validates it on the server.

Heres the function that handles scaling, the issue happens where the part’s size and cframe gets set.
I need all of the welded parts to not move when it is scaled.

function placement.Scale(player, part, newCFrame, newSize)
	local playerTeam = player.Team
	if not playerTeam then return false end
	local playerPlot = plotsFolder:FindFirstChild(playerTeam.Name)
	if not playerPlot then return false end

	local userFolder = placedBlocksFolder:FindFirstChild(player.UserId)
	if not userFolder then return false end
	if not part then return false end

	local block = part.Parent
	if not block or block.Parent ~= userFolder then return false end
	if not CollectionService:HasTag(block, "Scalable") then return false end

	local blockId = block:GetAttribute("BLOCKID")
	if not blockId then return false end

	local inventory = InventoryModule.getPlayerInventory(player)
	local index = findItemIndex(inventory, blockId)

	local validateAmount
	if placement.PlacementInventory[player.UserId] then
		validateAmount = InventoryModule.ValidatePlayerBlockAmount(player, blockId, placement.PlacementInventory[player.UserId][blockId])
	else
		validateAmount = InventoryModule.ValidatePlayerBlockAmount(player, blockId, inventory[index] and inventory[index].AMOUNT or 0)
	end
	if not validateAmount then return false end

	local gamePassFolder = player:FindFirstChild("Gamepasses")
	if not gamePassFolder then return false end
	local gamepassValue = gamePassFolder:FindFirstChild("Extended_Plot")
	if not gamepassValue then return false end

	local normalBounds = playerPlot:FindFirstChild("FreeBuildBounds")
	local paidBounds = playerPlot:FindFirstChild("PaidBuildBounds")
	if not normalBounds or not paidBounds then return false end

	local partsInBounds = getBlockInBounds.IsPivotInBounds(newCFrame, normalBounds)
	local partsInPaidBounds = getBlockInBounds.IsPivotInBounds(newCFrame, paidBounds)
	local validatedBounds = validateBounds(partsInPaidBounds, partsInBounds, gamepassValue)
	if not validatedBounds then return false end

	local prevSize = part.Size
	local prevCFrame = part.CFrame
	
	part.Size = newSize
	part.CFrame = newCFrame

	local totalAmount = getAllScaledAmount(block, userFolder)
	local currentAmount = index and inventory[index].AMOUNT or 0
	if currentAmount - totalAmount <= 0 then
		part.Size = prevSize
		part.CFrame = prevCFrame
		return false
	end
	
	placement.PlacementInventory[player.UserId][blockId] = currentAmount - totalAmount
	ReplicatedStorage.Remotes.UpdateInventorySlots:FireClient(player, blockId, placement.PlacementInventory[player.UserId][blockId])
	placement.CachedBlockProperties[block] = placement.CachedBlockProperties[block] or {}
	placement.CachedBlockProperties[block].Size = part.Size

	return true
end

Sorry, not sure if I still understand, but is the issue:

  1. Other parts (not the scaled part) moving when the part is scaled?
  2. Or, the amount of unit parts in the scaled part is incorrect (Ex: 2x2x2 scaled part should be made from 8 unit parts)?

Could you provide a screenshot (if useful)?

If it’s the first problem then you can probably just set the CFrame before adding the weld and that should fix it.

For the second problem it looks the problem is this part of code here?

if currentAmount - totalAmount <= 0 then
	part.Size = prevSize
	part.CFrame = prevCframe
	return false
end

What is currentAmount? Is that the new total amount of blocks? Is totalAmount the old total amount of blocks?