Unknown bug causing multiple core systems to break in my game

Hello, everyone! I’ve tried absolutely everything in power to solve this problem, but I’m left at a complete loss at what could be causing multiple unrelated systems to break in my game “Ultimate Camp Tycoon: RE-CAMPED!” Game Link

These are the features that break:

  • The GUI “Open Backpack” image button will stop working, preventing the player from opening or closing it again. (Including the shortcut button)
  • The Tools that allow you to collect resources from the world will stop working, preventing players from earning resources. (Core gameplay)
  • The Proximity Prompt buttons for purchasing tycoon objects will stop showing what the button requires. (Gui Frame that Tweens in and out from top left of the screen)

These are some facts that I have gathered regarding what is happening:

  • When one of these things breaks, all 3 of them do. This leads me to believe that they are connected and caused by a single problem.
  • Some players never come into contact with this bug. They are able to play the game completely normally, as intended. (This is also my experience when I test it)
  • There are no thrown errors or warnings regarding these scripts. Nothing script-wise seems broken, even when I check the developer console in live servers with players who have the bug. (Client and server)
  • These 3 things are abstracted from each other, and the only thing connecting them is potentially just the replicated values for the player’s saved resources. However, it can still happen even when the saved data is perfectly fine and loaded, which makes me think that is not the problem.
  • There is no particular cause or player action that triggers the bug to appear. I’ve received reports from a whole spectrum of players about what they were doing beforehand.
  • There is no particular time this occurs. Some players load into the server with the bug, and others can get it after playing normally for over an hour.

Tools Client side:

resourceCollider.Touched:Connect(function(hit)

	if hit.Name == "Stone" or hit.Name == "Wood" or hit.Name == "Metal" or hit.Name == "Gem" or hit.Name == "Cloth" then
	
		--if we haven't collected with this activation
		if toolMadeContact == false then
			toolMadeContact = true
			
			ToolHandler.PlayerResourceHit(player, hit, tool)		
		end
	end

end)


tool.Activated:Connect(function()
	
	if activateDebounce == false then
		activateDebounce = true

		toolCollecting = true

		while toolCollecting do
			tool.ManualActivationOnly = true
			toolMadeContact = false
			AnimationHandler.PlayAnimation(player, "Swing")
			task.wait(RepGlobalConstants.ToolCooldownTime)
			toolMadeContact = true
			tool.ManualActivationOnly = false
		end

		activateDebounce = false
	end
	
end)
ToolHandler.PlayerResourceHit = function(player, hit, tool)

	local resourceCollider = tool:FindFirstChild("ResourceCollider")
	if resourceCollider then

		local damage = resourceCollider:GetAttribute(hit.Name .. "Damage")
		if damage ~= nil then

			--correct tool and damage
			if damage > 0 then
				
				local material = hit.Parent
				if material ~= nil then
					
					AudioHandler.PlaySFX(hit.Name .. "Hit")
					
					--Check the material health
					local materialFullHealth = material:GetAttribute("FullHealth")
					local materialHealth = material:GetAttribute("Health")

					--do damage to the material
					if materialHealth then
						materialHealth = materialHealth - damage
						local reward = damage

						local overrides = RepGlobalConstants.VFXoverrides
						overrides.Time = 0.5
						PlayClientVFXEvent:FireServer(hit.Name .. "Hit", hit, overrides)

						--check if the material is destroyed
						if materialHealth <= 0 then

							local destroyed = material:GetAttribute("Destroyed")
							if not destroyed then

								--set destroyed
								material:SetAttribute("Destroyed", true)

								--the material is destroyed, get the bonus reward
								local bonusMaterial = material:GetAttribute("Reward")
								--collect and only reward the amount of health left
								reward = (bonusMaterial + (damage + materialHealth))

								--create a parent part
								local particleParent = Instance.new("Part")
								particleParent.Anchored = true
								particleParent.Position = hit.Position
								particleParent.Transparency = 1
								particleParent.CanCollide = false
								particleParent.Parent = material

								task.wait()

								--send VFX to client
								local overrides2 = RepGlobalConstants.VFXoverrides
								overrides2.Time = 2
								overrides2.Texture = RepGlobalConstants.VFXTextures[hit.Name]

								PlayClientVFXEvent:FireServer("ResourceCompleted", particleParent, overrides2)	
							else
								return 0
							end
						else
							--set the new tree health
							material:SetAttribute("Health", materialHealth)
						end

						--send this data to the remote event for health bar display
						activatedHealthBarEvent:FireServer(material.Name, materialFullHealth, materialHealth)
						
						toolHitRemoteEvent:FireServer(hit.Name, reward, material, material:GetAttribute("Destroyed"))
					end

				else
					warn("Material is nil in Reward Handler")
				end

				--upgrade tool
			elseif damage == 0 then
				errorFeedbackEvent:FireServer(RepGlobalConstants.ErrorFeedbackMessages.UpgradeTool)
				tooltipFeedbackEvent:FireServer(RepGlobalConstants.ToolTipFeedbackMessages.UpgradeTool)

			elseif damage == -1 then
				errorFeedbackEvent:FireServer(RepGlobalConstants.ErrorFeedbackMessages.WrongTool)

			end

		else
			warn("Damage value not found in Tool Handler")
		end

	else
		warn("Resource Collider not found in Tool Handler")
	end
end

Tools Server side (after the remote event):

RewardHandler.HarvestResourceAmount = function(player, resourceName, reward)
	
	local playerData = ProfileDataHandler:GetPlayerData(player)
	if playerData ~= nil then

		local resources = playerData["Resources"]
		local playerStats = playerData["AllStats"]
		local playerUpgrades = playerData["UpgradesPurchased"]

		--get rep Folder
		local repPlayerFolder = repPlayerData:WaitForChild(player.Name)
		local passesFolder = repPlayerFolder:WaitForChild("GamePasses")
		local pass = passesFolder:WaitForChild("2XResources")

		--check for token increase
		local newReward = reward + playerUpgrades["ResourcesEarned"]

		--this is harvested, check 2X resources.
		if pass.Value == true then
			newReward = newReward * 2
		end

		--set new resources
		if resourceName ~= nil then
			if resources[resourceName] ~= nil then

				resources[resourceName] = resources[resourceName] + newReward
				ProfileDataHandler:SetPlayerData(player, "Resources", resources)

				--track the resources in the players stats
				playerStats["ResourcesCollected"] = playerStats["ResourcesCollected"] + newReward
				--playerStats["ResourcesCollected"] = 999900

				--check for the badge
				if playerStats["ResourcesCollected"] > 1000000 then
					BadgeHandler.AwardBadge(player, BadgeHandler.badgeIDs.HarvestHero)
				end

				ProfileDataHandler:SetPlayerData(player, "AllStats", playerStats)

				--track the resource quests
				if resourceName == "Wood" then
					CheckQuestProgress(player, "WoodCollected", newReward)
					CheckQuestProgress(player, "WoodCollected2", newReward)

				elseif resourceName == "Stone" then
					CheckQuestProgress(player, "StoneCollected", newReward)
					CheckQuestProgress(player, "StoneCollected2", newReward)

				elseif resourceName == "Metal" then
					CheckQuestProgress(player, "MetalCollected", newReward)
					CheckQuestProgress(player, "MetalCollected2", newReward)

				elseif resourceName == "Cloth" then
					--check if this reward is a bonus reward
					if reward >= 60 then
						CheckQuestProgress(player, "TentsDestroyed", 1)
					end
				end

			else
				warn("RESOURCES TABLE WAS NIL FOR " .. resourceName)
			end

		else
			warn("RESOURCE NAME WAS NIL!")
		end

	else
		warn("PLAYER DATA WAS NIL IN REWARD HANDLER")
	end
end

Backpack:

--BACKPACK BUTTON
local function ActivateBackpackButton()
	if backpackGui.Enabled == true then
		OvertakeGUI(mainGui)
	else
		OvertakeGUI(backpackGui, mainGui)
	end
end
backpackButton.Activated:Connect(function()
	ActivateBackpackButton()
end)
--function that overrides all current GUIs to be disabled in favor of new one
local function OvertakeGUI(guiToEnable, extraGUI)
	
	--check for backpack SFX and visuals
	if guiToEnable == backpackGui or extraGUI == backpackGui then
		
		AudioHandler.PlaySFX("BackpackOpened")
		backpackButton.Image = "rbxassetid://11437829560"
		backpackButton.PressedImage = "rbxassetid://11437829681"
		backpackButton.HoverImage = "rbxassetid://11437829681"
		
	elseif backpackGui.Enabled == true then
		AudioHandler.PlaySFX("BackpackClosed")
		backpackButton.Image = "rbxassetid://11437829681"
		backpackButton.PressedImage = "rbxassetid://11437829560"
		backpackButton.HoverImage = "rbxassetid://11437829560"
	end
	
	
	for i, v in pairs(containedScreenGuis) do
		v.Enabled = false
	end
	
	guiToEnable.Enabled = true
	if extraGUI then
		extraGUI.Enabled = true
	end
	
	if UserInputService.GamepadEnabled then
		if extraGUI == nil and guiToEnable == mainGui then
			GamePadService:DisableGamepadCursor()
		else
			local mainFrame = guiToEnable:FindFirstChildWhichIsA("Frame")
			if mainFrame then
				GamePadService:EnableGamepadCursor(mainFrame)
			else
				warn("No frame available for gamepad cursor")
			end
		end
	end
	
	if guiToEnable == mainGui then
		blur.Enabled = false
	else
		blur.Enabled = true
	end
	
end

Proximity Prompts:

--when a prompt is displayed to this user
ProximityPromptService.PromptShown:Connect(function(prompt)
	
	AudioHandler.PlaySFX("ButtonShrink")
	
	if prompt.Parent ~= nil then
		
		local buttonName = prompt.Parent.Name
		if buttonName == "Button" then

			TweenButton(prompt, true)
			ToggleGui(prompt)
			promptingButton = true
		end
		
	else
		warn("PROXIMITY PROMPT PARENT WAS NIL, MEANING IT WAS DESTROYED ")
	end

end)

--when a prompt is no longer being displayed to this user
ProximityPromptService.PromptHidden:Connect(function(prompt)
	
	if prompt.Parent ~= nil then
		local buttonName = prompt.Parent.Name

		AudioHandler.PlaySFX("ButtonGrow")

		--if this was a regular button
		if buttonName == "Button" then
			TweenButton(prompt, false)
			promptingButton = false
		end
	end
	
end)
local function ToggleGui(prompt)
	
	--look for a billboard Gui to display
	local buttonHead = prompt.Parent
	local buttonActivated = buttonHead.Parent

	--for each cost frame
	for i, v in pairs(costFrames) do
		--check that this is a frame
		if v:IsA("Frame") then
			--check if this frame is needed 
			local thisAttribute = buttonActivated:GetAttribute(v.Name)
			if thisAttribute ~= nil then
				
				local amount = v:WaitForChild("Amount")
				
				--change amount value
				amount.Text = RepGlobalConstants.ShortNumber(thisAttribute)

				--check color
				local playerResourceVal = playerCostTracking[v.Name].Value
				local amount = v:WaitForChild("Amount")
				
				if playerResourceVal >= thisAttribute then
					amount.TextColor3 = goodResourcesColor
				else
					amount.TextColor3 = badResourceColor
					--check for tooltip
					if v.Name == "Gem" then
		tooltipFeedbackEvent:FireServer(RepGlobalConstants.ToolTipFeedbackMessages.NeedGems)
					end
				end

				--tween in the cost for this resource
				tweenCostsIn[v.Name]:Play()

				--tween in the display for this resource
				if v.Name ~= "Price" then
					tweenIns[v.Name]:Play()
					tweenTimers[v.Name] = RepGlobalConstants.ResourceFrameFadeTime
				end
				
			else
				--tween out the cost frame
				tweenCostsOut[v.Name]:Play()
			end
		end
	end
end
--set up a fading display loop
while true do
	
	if not promptingButton then
		
		--tween out all the cost frames
		for i, v in pairs(tweenCostsOut) do
			v:Play()
		end
		
		--for each frame, tick down the tween timers
		for i, v in pairs(resourceTrackingFrames) do

			--if we are still displaying, tick
			if tweenTimers[v.Name] > 0 then
				tweenTimers[v.Name] = tweenTimers[v.Name] - 1

				--we are done displaying, turn off and tween
			else
				tweenOuts[v.Name]:Play()
			end
		end
	end
	
	task.wait(0.5)
end

I’ve tried finding forum posts about similar problems, fixing random Roblox errors in the error-report, rewriting the tools to use “GetPartsInPart()” rather than touched events, changing the resource nodes (trees, boulders) to be collected client-side, adding a backpack shortcut with ContextActionService, investigating players with the bug in live servers, and everything in-between.

If you have any ideas, suggestions, thoughts, or theories, I’m open to them. I can provide additional details, code, information, and explanations on anything in the codebase. (I wrote all of it except some modules like ProfileService and Weather systems.)

Thank you for reading!