How do Validate a weapon's firerate in the server?

I been developing a weapon system using fastcast that fires a cast on the client and server in order to validate the shot. it currently works fine but im having trouble finding a reliable method of validating the weapon’s firerate.

Currently what im doing is using a custom “cooldown” module i made that increases and decreases between 0 and 1 over time and hvae it be created on both server and client, when the player fires, the client’s cooldown starts and when the server recieves the remote event, it also fires.

My issue is that the server will always fire slightly later than the client due to latency, and so with small fire rates the server will sometimes invalidate a shot because the server timer is still running despite the client timer finishing. i tried using Player:GetNetworkPing() but the issue still persist.

Is there a way to sync these timers in order to more reliably validate the weapon’s firerate/
or do i avoid validating it in the first place?

here’s the weapon script:

function weapon.New(tool : Tool)
	local self = setmetatable({}, weapon)
	--Basic Params
	self.Tool = tool
	self.Config = require(tool.Config)
	self.FireEvent = tool.Fire
	self.HitEvent = tool.Hit
	self.MuzzleExit = tool.Barrel.MuzzleExit
	
	--FastCast Params
	self.Caster = fastcast.new()
	self.Behavior = fastcast.newBehavior()
	self.Params = RaycastParams.new()
	
	self.Behavior.Acceleration = self.Config.Gravity
	self.Behavior.RaycastParams = self.Params
	
	self.Params.FilterType = Enum.RaycastFilterType.Exclude
	
	self.FirerateCooldown = Cooldown.new()
	self.fired = false
	
	--Functions for both client and server
	self.Connections = {
		--Controls cast behavior.
		BulletBehavior = self.Caster.LengthChanged:Connect(function(cast, lastPoint, direction, length, velocity, bullet)
			--Sets the velocity back to normal after the first step
			functions.InstantTravelReset(cast, self.Config.MuzzleVelocity, direction)
		end),

		--Fires when tool is equipped.
		OnEquipped = self.Tool.Equipped:Connect(function()
			--Makes cast ignore the tool and the character holding it
			self.Params.FilterDescendantsInstances = {tool, tool.Parent}
			self.Player = game:GetService("Players"):GetPlayerFromCharacter(tool.Parent)
		end),
		
		OnFirerateWarmUp = self.FirerateCooldown.WarmedUp:Connect(function()
			self.fired = false
			self.FirerateCooldown:Skip(0)
			print("Firerate Reset")
		end)
	}
	
	--Server Only Functions
	if runService:IsServer() then
		--Fires when the client fires a shot.
		self.Connections.OnFire = self.FireEvent.OnServerEvent:Connect(function(plr, pos, dir, dt)
			--print("Fired From " .. plr.Name .. "'s Client")
			
			--Caculates velocity and fires the server cast
			if not self.fired then
				local initialVelocity = (self.Config.MuzzleVelocity * self.Config.InstantTravelTime) / dt
				self.Caster:Fire(pos, dir, initialVelocity, self.Behavior)
				self.fired = true
				self.FirerateCooldown:WarmUp(self.Config.Firerate - self.Player:GetNetworkPing() * 2)
			else
				warn("Server cast not fired, firerate")
			end
			
		end)
			
		--Fires when the server cast has hit something.
		self.Connections.OnHit = self.Caster.RayHit:Connect(function(cast, result, velocity, bullet)
			--print("Server Cast Hit")
				
			--Creates temporary table containing the hit results
			self.HitResults  = {
				Cast = cast,
				Result = result,
				Velocity = velocity,
				Bullet = bullet
			}
		end)
			
		--fires when the client cast has hit something.
		self.Connections.OnClientHit = self.HitEvent.OnServerEvent:Connect(function(plr, pos, velocity)
			--print("Recieved Client Result, Waiting On Server...")
			local hitData = {}
				
			if self.HitResults then
				--If server cast has already hit before the client
				hitData = self.HitResults
			else
				--if server cast hasn't hit yet
				self.Caster.RayHit:Wait()
				hitData = self.HitResults
			end
				
			local diff = pos - hitData.Result.Position
			--Validates hit based on difference in hit positions
			if diff.Magnitude < 5 then
				print("Validated")
			else
				warn("Position Invalid, Checking Velocity")
				
				local velDiff = velocity - self.HitResults.Velocity
				
				if velDiff.Magnitude < 3 then
					print("Validated by Velocity")
				else
					warn("Velocity Invalid")
				end
			end
	
			--Erases result table
			self.HitResults = nil
		end)
		
	--Client Only Functions
	elseif runService:IsClient() then
		--Fires when client cast hits something.
		self.Connections.OnHit = self.Caster.RayHit:Connect(function(cast, result, velocity, bullet)
			--Sends hit position to the server for validation purposes.
			--print("Client Cast Hit")
			self.HitEvent:FireServer(result.Position, velocity)
		end)
		
		
	end
		
	return self
end

function weapon:Fire()
	if runService:IsClient() and not self.fired then
		--print("Client Fire")
		--caculates needed velocity to travel the same distance as it would normally in 0.2 seconds, in one frame
		local initialVelocity = (self.Config.MuzzleVelocity * self.Config.InstantTravelTime) / deltaTime
		
		self.FirerateCooldown:WarmUp(self.Config.Firerate)
		self.fired = true
		self.FireEvent:FireServer(workspace.CurrentCamera.CFrame.Position, workspace.CurrentCamera.CFrame.LookVector, deltaTime)
		self.Caster:Fire(workspace.CurrentCamera.CFrame.Position, workspace.CurrentCamera.CFrame.LookVector, initialVelocity, self.Behavior)
	end
end

return weapon

everytime the server is triggered save the time the shot was at, next when someone shoots again check to make sure it was after X amount of time. Boom validated

2 Likes

ultimately you can’t perfectly validate the firerate. The internet will take a variable amount of time to send the packets across so any timings will be off. You should just be checking that on average the firerate roughly matches that of the expected value with a leniency so as to reduce the amount of false positives due to network conditions. You should keep a rolling value of shot timings and compare that on average they don’t come faster than the expected value and a small leniency number. It won’t be perfect, but it should do the job reliably enough to only consistently impact cheaters. You could also attempt to just add leniency to each timer, but a grouping will probably help you get more stable results (though adds enough complexity depending on implementation it might not be worth it)

1 Like

Since a player’s ping/latency is going to remain roughly the same, the primary concern should be benchmarking. Use os.clock() to get the current time when the gun is fired, and store it as a variable. Once the client tries to fire again, make a new os.clock() and subtract it by the stored variable to get the time passed. Like this:

Benchmark = os.clock()
task.wait(1)
TimePassed = os.clock() - Benchmark --// ~1s

You can add some leniency, say 0.05s, to adjust for the un-evenness of latency.

OK, After trying your method and using Workspace:GetServerTimeNow() i was able to modify my cooldown module to take the start time in Epoch time and count from there. when i fire the weapon the client starts its timer and sends the start time to the server, where it uses that to start the timer a little later instead of from 0.

now the timers finish within 2-0ms from each other consistently:
image_2024-07-01_191131575

1 Like

This topic was automatically closed 14 days after the last reply. New replies are no longer allowed.