How do I cancel my sentry's placement?

I want to code my cancel placement system (Enum.Keycode.Q) so that it stops the placement of sentries and resets the connections of ActivatedButton

However, when I cancel my sentry’s placement and then place a new one, it places a duplicate. This duplicate does NOT happen again unless I cancel another sentry again.

This duplicate problem does not happen when I unequip the weapon, then equip it again so I suspect there may be something wrong with my cancelling system.

Client Script to inspect

-- Services
local Players = game:GetService("Players")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local UserInputService = game:GetService("UserInputService")
local RunService = game:GetService("RunService")

-- Events
local Events = ReplicatedStorage.Events
local ActivateAbility = Events:FindFirstChild("ActivateAbility")
local SpawnSentry = Events:FindFirstChild("SpawnSentry")

-- Other Variables
local player = Players.LocalPlayer
local gui = player.PlayerGui:WaitForChild("CharacterGui")
local Backpack = player:WaitForChild("Backpack")
local camera = workspace.CurrentCamera

local sentryToSpawn = nil
local selectedSentry = nil
local hoveredInstance = nil
local activatedRepair = nil
local activatedButton = nil
local destroyingConnection = nil
local canPlace = false
local rotation = 0
local lastTouch = tick()

local activatedButton = {}

local function MouseRaycast(excludedInstances)
	local mousePosition = UserInputService:GetMouseLocation() -- gets mouse position on the screen
	local mouseRay = camera:ViewportPointToRay(mousePosition.X, mousePosition.Y) -- gets mouse raycast

	local raycastParams = RaycastParams.new()
	raycastParams.FilterDescendantsInstances = {player.Character, excludedInstances}
	raycastParams.FilterType = Enum.RaycastFilterType.Exclude

	local raycastResult = workspace:Raycast(mouseRay.Origin, mouseRay.Direction * 1000, raycastParams)

	return raycastResult
end

local function RemovePlaceholderSentry()
	if not sentryToSpawn then
		return
	end

	sentryToSpawn:Destroy()
	sentryToSpawn = nil
	rotation = 0
end

local function AddPlaceholderSentry(attack)
	RemovePlaceholderSentry()

	sentryToSpawn = attack.Sentry.Value:Clone()
	sentryToSpawn.Parent = workspace.Camera

	for i, object in ipairs(sentryToSpawn:GetDescendants()) do
		if object:IsA("BasePart") then
			object.CollisionGroup = "SentryToSpawn"
			object.Material = Enum.Material.ForceField
			object.Massless = true
		end
	end
end

local function ColourPlaceholderSentry(colour)
	for i, object in ipairs(sentryToSpawn:GetDescendants()) do
		if object:IsA("BasePart") then
			object.Color = colour
		end
	end
end

-- Function to handle the removal of weapons
local function handleWeaponDeletion()
	-- Clear the ability buttons when a weapon is removed
	for _, button in pairs(gui.Abilities:GetChildren()) do
		if button:IsA("UIListLayout") or button.Name == "Template" then
			continue
		end
		button:Destroy()
	end
	gui.Abilities.Visible = false
	gui.ManageTools.Visible = false -- Reset ManageTools UI when the weapon is unequipped

	for i, connection in activatedButton do
		connection:Disconnect()
	end
	activatedButton = {}

	--	while activatedButton do
	--		activatedButton:Disconnect()
	--		task.wait()
	--	end
end

local function handleWeaponRemoval()
	gui.Abilities.Visible = false
	gui.ManageTools.Visible = false -- Reset ManageTools UI when the weapon is unequipped

	for i, connection in activatedButton do
		connection:Disconnect()
	end
	activatedButton = {}

	--	while activatedButton do
	--		activatedButton:Disconnect()
	--		task.wait()
	--	end
end

-- Function to handle the addition of weapons
local function handleWeaponAddition(weapon)
	gui.Abilities.Visible = true

	for _, attack in ipairs(weapon.Abilities:GetChildren()) do
		local attackConfig = attack:GetAttributes()
		local button = gui.Abilities:FindFirstChild(attack.Name)

		-- Create a new button if it doesn't exist
		if not button then
			button = gui.Abilities.Template:Clone()
			button.Name = attack.Name
			button.Image = attack.Image.Texture
			button.Ability.Text = attack.Name
			button.Visible = true
			button.Parent = gui.Abilities
		end

		--		for i, connection in activatedButton do
		--			connection:Disconnect()
		--		end

		-- Function to run when the ability button is activated
		local function onButtonActivated()
			print(activatedButton)
			-- Prevent multiple activations
			if button.Active == false then
				return
			end

			-- Handle ability cooldowns
			local function handleCooldown(ability, cooldownDuration)
				if cooldownDuration > 0 then
					for _, manageTools in gui.ManageTools.List:GetChildren() do
						if not manageTools:IsA("TextButton") or manageTools.Active == false then
							continue
						end

						manageTools.Active = false
						manageTools.Shadow.Visible = true
					end

					ability.Shadow.Cooldown.Visible = true
					ability.GuiCooldown.Value = cooldownDuration

					for remainingTime = cooldownDuration, 0, -0.1 do
						ability.GuiCooldown.Value = remainingTime
						ability.Shadow.Cooldown.Text = string.format("%.1f", remainingTime)
						task.wait(0.1)

						if ability.GuiCooldown.Value ~= remainingTime then
							return
						end
					end
				end

				ability.Active = true
				ability.Shadow.Visible = false
				ability.Shadow.Cooldown.Visible = false

				for _, manageTools in gui.ManageTools.List:GetChildren() do
					if not manageTools:IsA("TextButton")then
						continue
					end

					manageTools.Active = true
					manageTools.Shadow.Visible = false
				end

			end

			-- Process each ability button
			for _, ability in pairs(button.Parent:GetChildren()) do
				if not ability:IsA("ImageButton") then
					continue
				end

				ability.Active = false
				ability.Shadow.Visible = true

				local holdCooldown = attack:GetAttribute("HoldCooldown")
				if not holdCooldown then
					coroutine.wrap(handleCooldown)(ability, attackConfig["Cooldown"])
				else
					coroutine.wrap(function()
						local result = MouseRaycast(sentryToSpawn)
						while not result or not result.Instance or not workspace.Maps:IsAncestorOf(result.Instance) do
							result = MouseRaycast(sentryToSpawn)
							RunService.RenderStepped:Wait()
						end

						if ability == button then
							AddPlaceholderSentry(attack)
						end

						-- Wait for the hold cooldown to change
						attack:GetAttributeChangedSignal("HoldCooldown"):Wait()
						handleCooldown(ability, attackConfig["Cooldown"])
					end)()
				end
			end

			-- Fire the server event to activate the ability
			ActivateAbility:FireServer(attack, weapon)
		end
		activatedButton[attack] = button.Activated:Connect(onButtonActivated)
		--		table.insert(activatedButton, attack == button.Activated:Connect(onButtonActivated))
		--		activatedButton = button.Activated:Connect(onButtonActivated)
	end
end

-- Main function that runs when the player's weapon changes its parent
local function AncestryChanged(weapon)
	if not weapon then
		handleWeaponDeletion()
		return
	end

	local weaponEquipped = Players:GetPlayerFromCharacter(weapon.Parent)
	if not weaponEquipped or not weapon:FindFirstChild("Abilities") then
		handleWeaponRemoval(weapon)
		return
	end

	handleWeaponAddition(weapon)
end

-- Connect the functions to the appropriate events
Backpack.ChildAdded:Connect(AncestryChanged)
Backpack.ChildRemoved:Connect(AncestryChanged)
player.Character.ChildRemoved:Connect(AncestryChanged)

local function SpawnNewSentry()
	if canPlace == true then
		SpawnSentry:FireServer(sentryToSpawn.Name, sentryToSpawn.PrimaryPart.CFrame)
		RemovePlaceholderSentry()
	end
end

UserInputService.InputBegan:Connect(function(input, processed)
	if processed then
		return
	end

	if sentryToSpawn then
		if input.UserInputType == Enum.UserInputType.MouseButton1 then
			SpawnNewSentry()
		elseif input.UserInputType == Enum.UserInputType.Touch then
			local timeSinceLastTouch = tick() - lastTouch

			if timeSinceLastTouch < 0.25 then
				SpawnNewSentry()
			end
			lastTouch = tick()
		elseif input.KeyCode == Enum.KeyCode.R then -- Rotating Keycode
			rotation += 45
		elseif input.KeyCode == Enum.KeyCode.Q then
			-- Cancelling Sentry Placement
			for i, connection in activatedButton do
				connection:Disconnect()
			end
			activatedButton = {}
			print(activatedButton)
			RemovePlaceholderSentry()

			for _, ability in pairs(gui.Abilities:GetChildren()) do
				if not ability:IsA("ImageButton") then
					continue
				end

				ability.Shadow.Visible = false
				ability.Active = true
			end

			local weapon = player.Character:FindFirstChild("Mechanic") or player.Backpack:FindFirstChild("Mechanic")
			handleWeaponAddition(weapon)
		end
	elseif hoveredInstance and gui.ManageTools.List.Repair.Active == false and input.UserInputType == Enum.UserInputType.MouseButton1 then
		local model = hoveredInstance:FindFirstAncestorOfClass("Model")

		if model and model.Parent == workspace.Sentries and model.Config:GetAttribute("Owner") == player.Name then
			selectedSentry = model
			activatedRepair = nil
		else
			selectedSentry = nil
		end

	end
end)

If you’ve reached the end and know what’s going on, please leave a reply down below

2 Likes

video showing the problem in better detail

anyone here to help???
this forum kinda inactive

1 Like

If I’m understanding this correctly, the problem is that two sentries are being created instead of one in the exact same spot?
I cannot spot anything immediately wrong in the code, but my best guess is that the input is being received/processed twice in some manner (possibly a second instance of the script, but that would have other, more noticeable, effects).
Could you add a print to the SpawnNewSentry() function (have it print script and see if when clicked they lead to the same script too, to prove or disprove my guess above) and repeat the bug to see if it’s that function being called twice?

If I’m understanding this correctly, the problem is that two sentries are being created instead of one in the exact same spot?

Yes

Could you add a print to the SpawnNewSentry() function (have it print script and see if when clicked they lead to the same script too, to prove or disprove my guess above) and repeat the bug to see if it’s that function being called twice?

SpawnNewSentry() is called once, however, it is only used to disable HoldCooldown which is used to enable sentry placing.

The real remote event that is used to enable sentry placing is ActivateAbility:FireServer(attack, weapon) which is located inside the onButtonActivated() function

so the SpawnSentry remote does not spawn the physical sentry, rather the ActivateAbility remote does?
In that case could you attach a print like I mentioned to it instead? Or if I am misunderstanding what you mean, could you clarify what part of the client code spawns the sentry further please?

so the SpawnSentry remote does not spawn the physical sentry, rather the ActivateAbility remote does?

Yes, sorry for the confusion as this is the product of lazily trying to fit a placement tutorial into my existing code

deleted below

In that case could you attach a print like I mentioned to it instead?

the onButtonActivated function runs once every time I click on it (even after I cancel the previous placement, however the function doesnt seem to disconnect properly after some tests with printing around the below section, where it prints twice more than what its supposed to print

-- Wait for the hold cooldown to change
attack:GetAttributeChangedSignal("HoldCooldown"):Wait()
handleCooldown(ability, attackConfig["Cooldown"])

I think I’ve figured out the problem

The ability is sent the server using ActivateAbility:FireServer(attack, weapon) RemoteEvent meaning that if I try to disconnect onButtonActivated(), it wouldn’t cover the RemoteEvent that was already fired.

So when I place another sentry (confirmed using SpawnSentry() ) after cancelling the previous one with Enum.Keycode.Q, the uncalled ActivateAbility:FireServer(attack, weapon) goes active again and then the current one fires at the same time, leading to placing two sentries.

If you don’t understand, I’ll give you a look at the server script which shows how SpawnSentry() works, since I still don’t know how to cancel the ActivateAbility remote

nvm solved the problem myself anyways