Possible Engine Bug? Numeric variables nil inside ContextActionService callback functions

Here’s a weird one. I’m working on the client script for a weapon that incorporates a variable zoom function. When I pickup the weapon, the zoom works. When I try to adjust the zoom, the variable that holds the current zoom is nil as the error message says. When I print the offending variable, and a few others, the numeric variables show as nil while the ones holding boolean values show their values as true or false.

local useZoom = false
local zoomToggle = false
local zoomFoV = 40
local zoomRD = 30
local zoomCurrent = 40
local zoomMin = 70
local zoomMax = 10
local zoomDebounce = false

Further down in the script…

-- Zooms the scope in and out.
local function zoomScope(actionName, inputState, inputObject)
	print(zoomCurrent, zoomToggle, zoomFoV, zoomDebounce, equipped)
	if actionName == "ScopeZoomIn" then
		if inputState == Enum.UserInputState.Begin then
			zoomCurrent -= 1
			if zoomCurrent <= zoomMax then
				zoomCurrent = zoomMax
			end
			workspace.CurrentCamera.FieldOfView = zoomCurrent
		elseif inputState == Enum.UserInputState.Change then
			if not zoomDebounce then
				zoomDebounce = true
				delay(0.25, function()
					zoomCurrent -= 1
					if zoomCurrent <= zoomMax then
						zoomCurrent = zoomMax
					end
					workspace.CurrentCamera.FieldOfView = zoomCurrent
				end)
			end
		end
	elseif actionName == "ScopeZoomOut" then
		if inputState == Enum.UserInputState.Begin then
			zoomCurrent += 1
			if zoomCurrent >= zoomMin then
				zoomCurrent = zoomMin
			end
			workspace.CurrentCamera.FieldOfView = zoomCurrent
		elseif inputState == Enum.UserInputState.Change then
			if not zoomDebounce then
				zoomDebounce = true
				delay(0.25, function()
					zoomCurrent += 1
					if zoomCurrent >= zoomMin then
						zoomCurrent = zoomMin
					end
					workspace.CurrentCamera.FieldOfView = zoomCurrent
					zoomDebounce = false
				end)
			end
		end
	end
end

I’m beginning to think this is an engine bug, as this is affecting ALL numeric variables, but Roblox says that I do not yet have the authority to report a bug. What do you guys think?

There’s a very small chance this is an engine bug. Do you change the values any time before calling the function?

In assignment, and when the client queries the server for the parameters of the weapon when it’s equipped. Here’s the entire script.

--
-- Local Weapon Script
--

-- ******** Requirements

-- Required Game Services and Facilities
local CAS = game:GetService("ContextActionService")
local playerService = game:GetService("Players")
local replicatedStorage = game:GetService("ReplicatedStorage")
local screenGui = game:GetService("GuiService")

-- Script Dependancies



-- ******** Local data

-- Housekeeping
local waitTimeout = 10
local localPlayer = playerService.LocalPlayer
local toolInstance = script.Parent
local mouseCursor
local equipped = false
local mouseButton1Down = false;

-- Ammo
local ammoMax = 0

-- Clip
local useClip = false
local clipReload = true
local clipSize = 0

-- AutoFire
local useAuto = false
local autoRate = 0

-- Zoom
local useZoom = false
local zoomToggle = false
local zoomFoV = 40
local zoomRD = 30
local zoomCurrent = 40
local zoomMin = 70
local zoomMax = 10
local zoomDebounce = false


-- ******** Functions/Methods

-- Debugging Tool
printTableRecursive = function(td, tab)
	local tableData = td
	if td == nil then
		warn("utilityMod.printTableRecursive: Parameter is nil.")
		return
	elseif type(td) ~= "table" then
		warn("utilityMod.printTableRecursive: Parameter is not a table:",
			type(tableData), " --> ", tableData)
		return
	end

	if tab == nil then
		tab = ""
	end
	for key, value in pairs(tableData) do
		if type(value) == "table" then
			print(string.format(tab) .. ">> Subtable: ", key)
			printTableRecursive(value, tab .. "\t")
		else
			print(string.format(tab), key, "=", value)
		end
	end
end

-- Enables the zoom GUI
local function enableZoomGui()
	local PlayerGUI = localPlayer:WaitForChild("PlayerGui", waitTimeout)
	if PlayerGUI then
		local WeaponsHUD = PlayerGUI:WaitForChild("WeaponsHud",waitTimeout)
		if WeaponsHUD then
			local ZoomScope = WeaponsHUD:WaitForChild("ZoomScope", waitTimeout)
			if ZoomScope then
				ZoomScope.Visible = true
			end
		end
	end
end

-- Disables the zoom GUI
local function disableZoomGui()
	local PlayerGUI = localPlayer:WaitForChild("PlayerGui", waitTimeout)
	if PlayerGUI then
		local WeaponsHUD = PlayerGUI:WaitForChild("WeaponsHud",waitTimeout)
		if WeaponsHUD then
			local ZoomScope = WeaponsHUD:WaitForChild("ZoomScope", waitTimeout)
			if ZoomScope then
				ZoomScope.Visible = false
			end
		end
	end
end

-- Zooms the scope in and out.
local function zoomScope(actionName, inputState, inputObject)
	print(zoomCurrent, zoomToggle, zoomFoV, zoomDebounce, equipped)
	if actionName == "ScopeZoomIn" then
		if inputState == Enum.UserInputState.Begin then
			zoomCurrent -= 1
			if zoomCurrent <= zoomMax then
				zoomCurrent = zoomMax
			end
			workspace.CurrentCamera.FieldOfView = zoomCurrent
		elseif inputState == Enum.UserInputState.Change then
			if not zoomDebounce then
				zoomDebounce = true
				delay(0.25, function()
					zoomCurrent -= 1
					if zoomCurrent <= zoomMax then
						zoomCurrent = zoomMax
					end
					workspace.CurrentCamera.FieldOfView = zoomCurrent
				end)
			end
		end
	elseif actionName == "ScopeZoomOut" then
		if inputState == Enum.UserInputState.Begin then
			zoomCurrent += 1
			if zoomCurrent >= zoomMin then
				zoomCurrent = zoomMin
			end
			workspace.CurrentCamera.FieldOfView = zoomCurrent
		elseif inputState == Enum.UserInputState.Change then
			if not zoomDebounce then
				zoomDebounce = true
				delay(0.25, function()
					zoomCurrent += 1
					if zoomCurrent >= zoomMin then
						zoomCurrent = zoomMin
					end
					workspace.CurrentCamera.FieldOfView = zoomCurrent
					zoomDebounce = false
				end)
			end
		end
	end
end

-- Zoom in the gun camera
local function zoomIn()
	localPlayer.CameraMode = Enum.CameraMode.LockFirstPerson
	workspace.CurrentCamera.FieldOfView = zoomFoV
	localPlayer.Character.Humanoid.CameraOffset = Vector3.new(0, 0, 4)
	enableZoomGui()
	zoomCurrent = zoomFoV
	
	-- Set hotkeys for variable zoom
	CAS:BindAction("ScopeZoomIn", zoomScope, true, Enum.KeyCode.RightBracket,
		Enum.KeyCode.ButtonL1)
	CAS:BindAction("ScopeZoomOut", zoomScope, true, Enum.KeyCode.LeftBracket,
		Enum.KeyCode.ButtonR1)
	print(zoomFoV, zoomCurrent)
end

-- Zoom out the gun camera
local function zoomOut()
	localPlayer.CameraMode = Enum.CameraMode.Classic
	workspace.CurrentCamera.FieldOfView = 70
	localPlayer.Character.Humanoid.CameraOffset = Vector3.new(0, 0, 0)
	localPlayer.CameraMinZoomDistance = zoomRD;
	disableZoomGui()
	wait()
	localPlayer.CameraMinZoomDistance = 0.5;
	CAS:UnbindAction("ScopeZoomIn")
	CAS:UnbindAction("ScopeZoomOut")
end

-- Called when the gun is equipped/unequipped. This helps to mitigate a problem
-- with the zoom getting stuck.
local function equipZoomOut()
	zoomOut()
	zoomToggle = false
end

-- Toggle zooms the gun camera in or out.
local function zoomCamera(actionName, inputState, inputObject)
	if not equipped then
		equipZoomOut()
		return
	end
	if actionName == "ZoomCameraScope" then
		if inputState == Enum.UserInputState.Begin then
			if zoomToggle then
				zoomToggle = false
				zoomOut(localPlayer, workspace.CurrentCamera)
			else
				zoomToggle = true
				zoomIn(localPlayer, workspace.CurrentCamera)
			end
		elseif inputState == Enum.UserInputState.End then
		end
	end
end

-- Initiates a manual clip reload.
local function reloadClipManual(actionName, inputState, inputObject)
	if not equipped then
		return
	end
	if actionName == "ManualClipReload" then
		if inputState == Enum.UserInputState.Begin then
			local folderEvents = toolInstance:WaitForChild("Events", waitTimeout)
			local eventReload = folderEvents:WaitForChild("Reload", waitTimeout)
			eventReload:FireServer()
		end
	end
end

-- Enables the Ammo Clip weapons HUD.
local function enableHUDClip()
	local PlayerGUI = localPlayer:WaitForChild("PlayerGui", waitTimeout)
	if PlayerGUI then
		local WeaponsHUD = PlayerGUI:WaitForChild("WeaponsHud",waitTimeout)
		if WeaponsHUD then
			local ClipHUD = WeaponsHUD:WaitForChild("AmmoDisplayClip", waitTimeout)
			if ClipHUD then
				WeaponsHUD.Enabled = true
				ClipHUD.Visible = true
			end
		end
	end
end

-- Enables the No Ammo Clip weapons HUD.
local function enableHUDNoClip()
	local PlayerGUI = localPlayer:WaitForChild("PlayerGui", waitTimeout)
	if PlayerGUI then
		local WeaponsHUD = PlayerGUI:WaitForChild("WeaponsHud",waitTimeout)
		if WeaponsHUD then
			local NoClipHUD = WeaponsHUD:WaitForChild("AmmoDisplayNoClip", waitTimeout)
			if NoClipHUD then
				WeaponsHUD.Enabled = true
				NoClipHUD.Visible = true
			end
		end
	end
end

-- Disables all HUDs
local function disableHUD()
	local PlayerGUI = localPlayer:WaitForChild("PlayerGui", waitTimeout)
	if PlayerGUI then
		local WeaponsHUD = PlayerGUI:WaitForChild("WeaponsHud",waitTimeout)
		if WeaponsHUD then
			local FrameList = WeaponsHUD:GetChildren()
			if FrameList and FrameList ~= nil and #FrameList > 0 then
				for _, frame in pairs(FrameList) do
					frame.Visible = false
				end
			end
			WeaponsHUD.Enabled = false
		end
	end
end

-- Sets text colors based on a percentage
local function textColors(value, str)
	local text
	if value > 0.75 then
		text = str
	elseif value > 0.50 then
		-- Blue
		text = "<font color=\"#4482B4\">" .. str .. "</font>"
	elseif value > 0.33 then
		-- Green
		text = "<font color=\"#00DD00\">" .. str .. "</font>"
	elseif value > 0.25 then
		-- Gold/Yellow
		text = "<font color=\"#FFD700\">" .. str .. "</font>"
	elseif value > 0.10 then
		-- Orange
		text = "<font color=\"#FFA500\">" .. str .. "</font>"
	elseif value <= 0.10 then
		-- Red
		text = "<font color=\"#FF0000\">" .. str .. "</font>"
	end
	return text
end

-- Write to the Clip version of the weapons HUD.
local function writeHudClip(clip, spare)
	local fracClip = clip / clipSize
	local fracSpare = spare / ammoMax
	local textClip = "<b>" .. tostring(clip) .. "</b>"
	local textSpare = "<b>" .. tostring(spare) .. "</b>"
	local PlayerGUI = localPlayer:WaitForChild("PlayerGui", waitTimeout)
	if PlayerGUI then
		local WeaponsHUD = PlayerGUI:WaitForChild("WeaponsHud",waitTimeout)
		if WeaponsHUD then
			local ClipHUD = WeaponsHUD:WaitForChild("AmmoDisplayClip", waitTimeout)
			if ClipHUD then
				WeaponsHUD.Enabled = true
				ClipHUD.Visible = true
				local displayClip = ClipHUD:WaitForChild("Clip", waitTimeout)
				local displaySpare = ClipHUD:WaitForChild("Spare", waitTimeout)
				displayClip.Text = textColors(fracClip, textClip)
				displaySpare.Text = textColors(fracSpare, textSpare)
			end
		end
	end
end

-- Write to the No Clip version of the weapons HUD.
local function writeHudNoClip(ammo)
	local frac = ammo / ammoMax
	local text = "<b>" .. ammo .. "</b>"
	local PlayerGUI = localPlayer:WaitForChild("PlayerGui", waitTimeout)
	if PlayerGUI then
		local WeaponsHUD = PlayerGUI:WaitForChild("WeaponsHud",waitTimeout)
		if WeaponsHUD then
			local NoClipHUD = WeaponsHUD:WaitForChild("AmmoDisplayNoClip", waitTimeout)
			if NoClipHUD then
				WeaponsHUD.Enabled = true
				NoClipHUD.Visible = true
				local displayAmmo = NoClipHUD:WaitForChild("Ammo", waitTimeout)
				displayAmmo.Text = textColors(frac, text)
			end
		end
	end
end

-- Called when the weapon is equipped.
script.Parent.Equipped:connect(function(mouse)
	equipped = true
	mouseCursor = replicatedStorage.Images.Normal.Texture
	local folderEvents = toolInstance:WaitForChild("Events", waitTimeout)
	local eventClick = folderEvents:WaitForChild("Click", waitTimeout)
	local eventAmmo = folderEvents:WaitForChild("Ammo", waitTimeout)
	local eventParam = folderEvents:WaitForChild("Parameter", waitTimeout)
	local eventCursor = folderEvents:WaitForChild("Cursor", waitTimeout)

	-- Targeting Cursor
	mouse.Icon = mouseCursor

	-- Fire
	mouse.Button1Down:connect(function()
		mouseButton1Down = true
		eventClick:FireServer(mouse.Hit.p)
		mouse.Icon = mouseCursor
		if useAuto then
			-- Autofire for automatic weapons.
			-- We delay 0.2 seconds before beginning so
			-- the player has enough time to release the
			-- button if they wanted only one shot.
			-- The fire rate comes from server parameters
			-- and cannot be changed on the server side by
			-- the player.  So the server will never accept
			-- an input faster than the firing rate.
			task.wait(0.2)
			while mouseButton1Down do
				eventClick:FireServer(mouse.Hit.p)
				mouse.Icon = mouseCursor
				task.wait(autoRate)
			end
		end
	end)

	-- Resets the mouse button 1 down flag to false when
	-- the player releases the mouse button.	
	mouse.Button1Up:Connect(function()
		mouseButton1Down = false
	end)

	-- Cursor change based on weapon state
	eventCursor.OnClientEvent:Connect(function(cursor)
		if cursor == 0 then
			mouseCursor = replicatedStorage.Images.Normal.Texture
		elseif cursor == 1 then
			mouseCursor = replicatedStorage.Images.Reload.Texture
		elseif cursor == 2 then
			mouseCursor = replicatedStorage.Images.NoAmmo.Texture
		else
			mouseCursor = replicatedStorage.Images.NeedReload.Texture
		end
		mouse.Icon = mouseCursor
	end)

	-- When the tool is equipped, the server sends a few parameters.
	eventParam.OnClientEvent:Connect(function(params)
		-- Set Parameters
		ammoMax = params.ammoMax

		-- Camera Zoom Function
		if params.useZoom then
			useZoom = true
			zoomToggle = false
			zoomFoV = params.zoomFoV
			zoomRD = params.zoomRD
			zoomCurrent = params.zoomFoV
			equipZoomOut()
			CAS:BindAction("ZoomCameraScope", zoomCamera, true,
				Enum.KeyCode.Z, Enum.KeyCode.ButtonB)
		end

		-- Auto-Fire Function
		if params.useAuto then
			useAuto = true
			autoRate = params.rate
		end

		-- Ammo Clip Function
		if params.useClip then
			useClip = true
			clipSize = params.clipSize
			clipReload = params.clipLoad
			CAS:BindAction("ManualClipReload", reloadClipManual,
				true, Enum.KeyCode.R, Enum.KeyCode.ButtonY)
			writeHudClip(params.clipAmmo, params.ammo)
			enableHUDClip()
		else
			useClip = false
			writeHudNoClip(params.ammo)
			enableHUDNoClip()
		end
	end)

	-- Updates the ammunition count display
	eventAmmo.OnClientEvent:Connect(function(ammo1, ammo2)
		if useClip then
			writeHudClip(ammo1, ammo2)
		else
			writeHudNoClip(ammo2)
		end
	end)

	-- Request the parameters
	eventParam:FireServer()

end)

-- Called when the weapon is unequipped.
toolInstance.Unequipped:Connect(function(mouse)
	equipped = false
	if useZoom then
		zoomToggle = false
		equipZoomOut()
		CAS:UnbindAction("ZoomCameraScope")
	end
	if useClip then
		CAS:UnbindAction("ManualClipReload")
	end
	disableHUD()
end)

This is an engine bug. But, I was able to work around it. It seems that tables are also seen inside of ContextActionService callback functions, so I put the values in a table and it’s working correctly now.