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
