Making sure placement code is secure and properly done (client)

I have this basic placement script that runs on the client. Basic idea, when you click a button, it will place the specific piece of furniture into your plot.

local Players = game:GetService('Players')
local ReplicatedStorage = game:GetService('ReplicatedStorage')

local Player = Players.LocalPlayer
local Mouse = Player:GetMouse()

local Furniture = ReplicatedStorage:WaitForChild('Furniture')
local Remotes = ReplicatedStorage:WaitForChild('Remotes')

local Events = Remotes:WaitForChild('Events')
local Place = Events:WaitForChild('Place')

local Camera = workspace.CurrentCamera

local Plots = workspace:WaitForChild('Plots')
local Interior = Plots:WaitForChild('Interior')
local PlayersInterior = Interior:WaitForChild(Player.Name)

local function Begin(item)
	local Target = Mouse.Target
	if not Target then return end
	
	-- Make sure player can only place within the plot
	if not Mouse.Target:IsDescendantOf(PlayersInterior) then return end
	
	-- Wait for the players mouse to be over the floor
	repeat wait() until string.sub(Mouse.Target.Name, 1, 5) == 'Floor'
	
	local ClonedItem = item:Clone()
	
	local UnitRay = Camera:ScreenPointToRay(Mouse.X, Mouse.Y, 1)
	local NewRay = Ray.new(UnitRay.Origin, UnitRay.Direction * 1000)
	local Hit, Pos, Normal = workspace:FindPartOnRayWithIgnoreList(NewRay, {ClonedItem, Player.Character})
	
	-- Snap to a 0.5 grid
	local PosX = math.floor((Pos.X / 0.5) + 0.5) * 0.5
	local PosY = math.floor((Pos.Y / 0.5) + 0.5) * 0.5
	local PosZ = math.floor((Pos.Z / 0.5) + 0.5) * 0.5
	
	local NewPos = Vector3.new(PosX, PosY, PosZ)
	
	-- Place object on client (visual aid, so there's no 2-3 second wait time to reach the server)
	ClonedItem:SetPrimaryPartCFrame(
		CFrame.new(NewPos) *
		CFrame.Angles(0, math.rad(90), 0) *
		CFrame.new(0, ClonedItem.PrimaryPart.Size.Y / 2, 0)
	)

    ClonedItem.Parent = PlayersInterior.Furniture
	
	-- Place on the server
	Place:FireServer(item.Name, ClonedItem.PrimaryPart.CFrame)
end

script.Parent.Activated:Connect(function()
	local FurnitureItem = Furniture:FindFirstChild(script.Parent.Name)
	if not FurnitureItem then return end
	
	Begin(FurnitureItem)
end)

Please note this is just bare bones for now. I just wanna make sure I’m on the right path :grimacing: The thing I’m mostly wanting to check is the idea of placing it within their lot. This does not (and will not handle) removing the object. This purely just places the object into their plot, and how I had to do it was basically wait until their mouse left the button and place it on whether the mouse was located. I tried placing the model like a few studs infront of the players torso, but then if a player was up against a wall they could place it through the wall, and then not be able to access it

Also worth noting, while I haven’t code the server side yet, I will be doing checks on the server to make it’s placed inside the players own plot (not sure how I’m gonna do that yet, but I’ll figure it out) and all the other checks to make sure the right item is being placed in the right spots

If you’re going for secure, as well as having other players being able to see the parts, I’m pretty sure you will need to call the server to make sure that the player can place the part and then to clone it.

If you read the script you’d see that I have a Place event that’s gonna place it on the server

This is the right way to go about it, just make sure that verify everything on the server, i.e.

  • Can the player actually place that item? (e.g. can they afford to buy it, do they have it in their inventory, w/e)
  • Is the passed CFrame valid? (e.g. is it on the grid, is that position unoccupied, is it within the bounds)

Which is pretty much what you said you’ll be doing. This way if a cheater tries to place items by firing the remote event, they’ll only make it look all messed up on their screen which doesn’t ruin the game for anyone else.

But you’re actually placing it in the workspace before even checking to see if the player was even allowed to place that object. This isn’t even about securing your code now, it is about what do normal users do. What will happen is the object will be placed in the workspace for the user, then the server script is called to check and see if that was even allowed. Then if it isn’t allowed, now you have to perform some logic back on the client to remove that part. This can be avoided by not even cloning the object in the first place if the player isn’t allowed to.

The better solution is to do the cloning and placement of the object on the server side after checking if the player was allowed to perform that action.

I place it on the client purely for latency reasons. Remote events take several seconds to communicate with the server. So if it was purely server, you’d click, and it’d take 3-4 seconds before the item is placed. The reason why it gets placed on the client as well is purely to bridge that gap. I don’t care if exploiters can place it without doing server side checks, as it won’t replciate to the server, and so they only ruin the game for them.

Or I could simply use a RemoteFunction. Something like

-- Clone the item to workspace

if Place:InvokeServer(item) then
    -- Keep the item on client
else
    -- Destroy the item on client (can't afford it, etc.)
end

3-4 seconds? Are you exaggerating? You’ve probably got something else going on.

A remote function would work for this logic, but the user experience will be item is placed then disappears and will probably not be pleasant.

How else can I fix it then? And maybe not ‘3-4’ seconds but it’s a noticeable time, and causes really bad user experience. I know in other games when you buy something and place it it’s practically instant. So they have to be doing some sort of client placement as well

Well I suppose the best option for the normal user is to validate on the client-side before the furniture is placed that the user can actually place the furniture. Then you use a stand-in model like a silhouette or a transparent version of the model, or even the actual model for positioning. Only after clicking does the real part get added to the world by calling the server-side. And it also gives the user a visual aid to see how things will look before actually clicking and having funds deducted and sort of gives the user the feeling that it happens faster than it actually does.

When the user clicks, the server-side logic is called to actually perform the action, like check to make sure there is enough money and deduct the money and then add the part to the workspace so that all clients can then see the furniture in their views.

This approach also prevents duplicate parts, because if you put the part on the local client, then tell the server about the part and the server then needs to add this part to the workspace so that all clients can see it, the client that added the part will have two parts of the same kind in the same position in its workspace.

Doing a silhouette or whatever on the client would still cause issues of the actual model taking forever to load in from the server. What I was gonna do to prevent 2 of the same model being created, it’d create the client model and fire the position from the client to server, and then create the item on the server. Once the server item has been made I destroy the client model. So it’s fully seamless
Server

local function PlaceObject(player, item, position)
	local PlayersInterior = Interiors:WaitForChild(player.Name)
	if not PlayersInterior then return end
	
	local StoredFurniture = Furniture:FindFirstChild(item)
	if not StoredFurniture then return end
	
	local PlayerData = player:FindFirstChild('PlayerData')
	if not PlayerData then return end
	
	local Processed = false
	
	-- Check if player actually owns the item
	for i, v in pairs(PlayerData.Furniture:GetChildren()) do
		if v.Name == item then
			if v.Value > 1 then
				v.Value = v.Value - 1
			else
				v:Destroy()
			end

			Processed = true
						
			break
		end
	end
	
	if not Processed then return end
	
	-- Do checks to make it's being placed within their base
	
	-- Item found
	local ItemClone = StoredFurniture:Clone()
	
	ItemClone:SetPrimaryPartCFrame(position)
	ItemClone.Parent = PlayersInterior.Build.Furniture
end

Alright. Your game, your call.