Chickynoid, server authoritative character replacement

@MrChickenRocket I need some advice on this.

So I have my game’s mechanics coded on 2 independent simulations (Server,Client) such as cooldowns, stamina, and states. How exactly do I synchronize them?

Do I use a network event to constantly send the client the current server values of the server simulation?, How do I check or assume they are synced if the result is already late?. Im a bit confused on this, For now I will look into the chickynoid weapon prediction module you mentioned since maybe it has info related to this.

As far as I understand it:
When chickynoid resimulates, it just sets the state of your character to what the server thinks it is, then plays out whatever commands the server hasn’t confirmed yet. This puts you back roughly to where you just were.

The weapons are synchronized in a slightly different way;

  1. Client makes commands. Executes weapons based on said commands. Sends commands to server for it to follow through with.
  2. Server plays out command (whether they’re fake, predicted commands or real commands). This leads to the gun being shot on both the client and the server.
  3. The server sees that there’s been a change to the weapon record. It then sends a list of the changes over to the client.
  4. Client sets it’s weapon record to match the server.

The main difference comes in that your inputs are not re-simulated, your weapon is just being forcefully set to reflect the state of the weapon on the server. Now this could lead to some issues, i.e firing a machine gun making your ammo count seemingly increase right after firing a bullet, before lowering again. The way chickynoid solves that is by preventing the weapon’s state from being overwritten for a few seconds. (“SetPredictedState” function in WeaponsClient).

Basically, yeah. You should check out how the weapons module does it- it employs a delta table util to only send relevant data when needed.

The client holds on to the states it receives from the server and updates it as new info comes through. The server state is never cleared, you shouldn’t have to worry about this scenario.

Here’s how you could handle the things you’ve listed, based on how the weapon modules are structured:

  • When applying a cooldown, note down your record’s totalTime. Only allow the command for using said ability to pass through if the weaponrecord’s totalTime exceeds the noted cooldownTime by X seconds.

  • Stamina can be treated similarly to ammo.

  • States are auto-replicated based on the model provided by chickynoid.

Finally you may want to be mindful of splitting things up in ways that make sense. Since a machine gun ignores server states until it is done firing, it wouldn’t make sense to couple it with an ability system since a possible cooldown mispredict won’t get corrected until you stop firing your machine gun.

5 Likes

WONDERFUL writeup! Thankyou for that, I think I’ll add it to the documentation…

To add to it, the weapon system is what I call “predict forward only”. The reason it’s handled differently than movement is because it doesn’t make any sense to resimulate a big visual/audio event like a bullet being fired on the client - the player has already seen the effect and animation so there’s no real way to fix that even if it mispredicted.

About the original question - if you’re doing skills like stamina and sprinting, don’t use the weapon system for that. Those should be done as moveTypes and handled through the player simulation.

3 Likes

This Module is very nice! I’ve been using it for my combat game It really had major improvement the Hitbox is more accurate since all the player is server authoritative.

@MrChickenRocket Is there a way to cleanly teleport a player on the server without having the client treat it as a misprediction and it interpolating to the teleport destination?
I could probably call the function on both the client and the server but it’d be great if it were possible to just flag a state update for not being interpolated for a single frame.

EDIT: It seems like the “step up” and “step down” simulation functions are broken in the current github repository & example place. I am unable to walk over the smallest of steps, only getting pushed up by slopes from my horizontal momentum. Letting you know as this seems like a pretty big issue.

Thanks for the report and patch, taken.

Ok thank you so much for your reply, it holds valuable information but I still have a few questions.

So what I understood from what you said and other articles is this:

function Melee:ProcessCommand(command)
	local playerRecord = ClientPlayerRecord:GetPlayerRecord(LocalPlayer)
	
	if command.CTID and command.MAI then
		
		local EquippedMelee = Melee.ParentClass.CToolRecords[command.CTID]
		
		if command.MAI == Enums.MeleeData.Block then
			EquippedMelee:Block()
		elseif command.MAI == Enums.MeleeData.Unblock then
			EquippedMelee:Unblock()
		elseif command.MAI == Enums.MeleeData.Shove and command.SC then
			if command.SC > playerRecord.Commands.SC then
				EquippedMelee:Shove()
				playerRecord.Commands.SC = command.SC
			end
		elseif command.MAI == Enums.MeleeData.Charge then
			EquippedMelee:Charge()
		elseif command.MAI == Enums.MeleeData.Hit and command.HC then
			if command.HC > playerRecord.Commands.HC then
				EquippedMelee:Hit()
				playerRecord.Commands.HC = command.HC
			end
		end
	end
end
  • Server gets the commands eventually and then executes them there
function Melee:ProcessCommand(command,playerRecord,CTool)
	
	-- Sanitize data
	if command.MAI and CTool.IsEquipped == true then
		if typeof(command.MAI) ~= "number" then
			Firewall:Kick(playerRecord, Enums.FirewallKickType.InvalidDataType)
			return
		end
	else
		return
	end
	
	if command.MAI == Enums.MeleeData.Shove then
		-- Sanitize data
		if command.SC then
			if typeof(command.SC) ~= "number" then
				Firewall:Kick(playerRecord, Enums.FirewallKickType.InvalidDataType)
				return
			end
		else
			return
		end

		if command.SC > playerRecord.Commands.SC then
			CTool:Shove()
			playerRecord.Commands.SC = command.SC
		end
	elseif command.MAI == Enums.MeleeData.Hit then
		-- Sanitize data
		if command.HC then
			if typeof(command.HC) ~= "number" then
				Firewall:Kick(playerRecord, Enums.FirewallKickType.InvalidDataType)
				return
			end
		else
			return
		end
		
		if command.HC > playerRecord.Commands.HC then
			CTool:Hit()
			playerRecord.Commands.HC = command.HC
		end
	elseif command.MAI == Enums.MeleeData.Charge then
		CTool:Charge()
	elseif command.MAI == Enums.MeleeData.Block then
		CTool:Block()
	elseif command.MAI == Enums.MeleeData.Unblock then
		CTool:Unblock()
	end
end
  • We set up the server to send data related to the weapon’s simulation running on the server to the client every tick using a mod’s :Step() function, right?.

So thats what I got right but here are the things I did not understand:

  • Lets say a client wants to swing his melee, So he sets a command that says, I wanna hit.
  • Then this command gets executed on the client and it gets to the server, Now as the client we are constantly recieving data containing the cooldown value of the server, while also running our own cooldown value on our client simulation

Heres the problem, am I supposed to rewind my client sided cooldown to the server cooldown? and then keep simulating back to normal every time I recieve a server state? but I still dont get how to tell if my state is out of date, Am I supposed to use the server time provided by chickynoid to then somehow compare the last server time to the current state recieved and then check somehow if my values are technically synced?, If so whats the best way to do it.

Also I totally get the part where i am not supposed to rewind actions that have already happened or actions that are not crutial such as bullet tracers, Sound effects, particles and those sort of things.

How do I detect other players? is there any documentation to this?

@MrChickenRocket Is there a wally for this?

You’re writing your cooldowns directly to commands. Your commands should only include info on what the player’s inputs are. Your mods/weapons/movetypes can then infer what the player is trying to do based on the inputs.

For instance:

  • You write to commands that you are holding LMB down.

  • Your melee (your currently equipped weapon) processes the commands, sees that it has LMB set to true, checks the last time you’ve attacked (using the command’s serverTime variable).

  • If it’s been over 0.5 seconds since the noted time and the command’s serverTime, note down the new time to the weapons record’s state and perform an attack.
    You’d then do the exact same on the server.

If you take a look at the example weapons built into chickynoid, this is what they do. They do use the command’s deltaTime instead of serverTime, but I used serverTime in this example since it’d be easier to explain it that way.

Also everything you write to your weapon record’s state gets auto-replicated to the client and gets corrected as need-be. This is where you’d put stuff like a weapon’s ammo, the last time they were fired, etc.

Yeah but it’s out of date. You’ll want to directly download it from github

I don’t think im using the right words to explain what I actually wan’t to know, All of the things you mention are already done.

I only process inputs and nothing else, a separate function sees these inputs and does whatever they are meant to do, You can see this here on the client and server

Client:

function Melee:ProcessCommand(command)
	local playerRecord = ClientPlayerRecord:GetPlayerRecord(LocalPlayer)
	
	if command.CTID and command.MAI then
		
		local EquippedMelee = Melee.ParentClass.CToolRecords[command.CTID]
		
		if command.MAI == Enums.MeleeData.Block then
			EquippedMelee:Block()
		elseif command.MAI == Enums.MeleeData.Unblock then
			EquippedMelee:Unblock()
		elseif command.MAI == Enums.MeleeData.Shove and command.SC then
			if command.SC > playerRecord.Commands.SC then
				EquippedMelee:Shove()
				playerRecord.Commands.SC = command.SC
			end
		elseif command.MAI == Enums.MeleeData.Charge then
			EquippedMelee:Charge()
		elseif command.MAI == Enums.MeleeData.Hit and command.HC then
			if command.HC > playerRecord.Commands.HC then
				EquippedMelee:Hit()
				playerRecord.Commands.HC = command.HC
			end
		end
	end
end

Server:

function Melee:ProcessCommand(command,playerRecord,CTool)
	
	-- Sanitize data
	if command.MAI and CTool.IsEquipped == true then
		if typeof(command.MAI) ~= "number" then
			Firewall:Kick(playerRecord, Enums.FirewallKickType.InvalidDataType)
			return
		end
	else
		return
	end
	
	if command.MAI == Enums.MeleeData.Shove then
		-- Sanitize data
		if command.SC then
			if typeof(command.SC) ~= "number" then
				Firewall:Kick(playerRecord, Enums.FirewallKickType.InvalidDataType)
				return
			end
		else
			return
		end

		if command.SC > playerRecord.Commands.SC then
			CTool:Shove()
			playerRecord.Commands.SC = command.SC
		end
	elseif command.MAI == Enums.MeleeData.Hit then
		-- Sanitize data
		if command.HC then
			if typeof(command.HC) ~= "number" then
				Firewall:Kick(playerRecord, Enums.FirewallKickType.InvalidDataType)
				return
			end
		else
			return
		end
		
		if command.HC > playerRecord.Commands.HC then
			CTool:Hit()
			playerRecord.Commands.HC = command.HC
		end
	elseif command.MAI == Enums.MeleeData.Charge then
		CTool:Charge()
	elseif command.MAI == Enums.MeleeData.Block then
		CTool:Block()
	elseif command.MAI == Enums.MeleeData.Unblock then
		CTool:Unblock()
	end
end

I just use a counter to let the :ProcessCommand() function know when an user presses the same key again, so that way it doesnt thinks we are just holding it, since packets are sent every tick.

Also cooldowns are checked like you said from both sides:
Client:

This code just steps all cooldowns using delta time so they are actively simulated and can be rewinded or fowarded with no problem, same for the charge mechanic.

function Melee:Step(client,_deltaTime)
	local playerRecord = ClientPlayerRecord:GetPlayerRecord(LocalPlayer)
	local States = playerRecord.States
	-- Step the charge state
	if States.IsCharging == true then
		-- step it according to equipped melee stats
		local EquippedMelee = playerRecord.Inventory.Hand
		if EquippedMelee then
			local CalculatedCharge = States.Charge
			local ChargeSpeed = EquippedMelee.MaxDamage / EquippedMelee.ChargeLength

			CalculatedCharge += ChargeSpeed * _deltaTime

			if CalculatedCharge >= EquippedMelee.MaxDamage then
				States.Charge = EquippedMelee.MaxDamage
				playerRecord.GUI.HUD.ChargeBarOutline.Charge.Size = UDim2.new(1,0,1,0)
			else
				States.Charge = CalculatedCharge
				playerRecord.GUI.HUD.ChargeBarOutline.Charge.Size = UDim2.new((1/EquippedMelee.MaxDamage) * States.Charge,0,1,0)
			end
		end
	else
		States.Charge = 0
	end
	-- Step cooldowns
	if States.SwingRecovery > 0 then
		States.IsSwingRecovering = true
		local CalculatedSwingRecovery = States.SwingRecovery
		CalculatedSwingRecovery -= _deltaTime
		if CalculatedSwingRecovery <= 0 then
			States.SwingRecovery = 0
			States.IsSwingRecovering = false
		else
			States.SwingRecovery = CalculatedSwingRecovery
		end
	end
	if States.BlockRecovery > 0 then
		States.IsBlockRecovering = true
		local CalculatedBlockRecovery = States.BlockRecovery
		CalculatedBlockRecovery -= _deltaTime
		if CalculatedBlockRecovery <= 0 then
			States.BlockRecovery = 0
			States.IsBlockRecovering = false
		else
			States.BlockRecovery = CalculatedBlockRecovery
		end
	end
	-- Step LMB charge delay thing
	if playerRecord.Input.IsHoldingLMB == true then
		local CalculatedLMBChargeDelay = playerRecord.Input.LMBChargeDelay
		CalculatedLMBChargeDelay -= _deltaTime
		if CalculatedLMBChargeDelay <= 0 then
			playerRecord.Input.LMBChargeDelay = 0
			ClientCommands:SetCommand("MAI",Enums.MeleeData.Charge)
		else
			playerRecord.Input.LMBChargeDelay = CalculatedLMBChargeDelay
		end
	else
		playerRecord.Input.LMBChargeDelay = playerRecord.Input.LMBChargeDelayDuration
	end
end

Server:
Same for server side as client side.

function Melee:Step(server, _deltaTime)
	for UserId,playerRecord in pairs(server.playerRecords) do
		local States = playerRecord.States
		-- Step the charge state
		if States.IsCharging == true then
			-- step it according to equipped melee stats
			local EquippedMelee = playerRecord.Inventory.Hand
			if EquippedMelee then
				local CalculatedCharge = States.Charge
				local ChargeSpeed = EquippedMelee.MaxDamage / EquippedMelee.ChargeLength

				CalculatedCharge += ChargeSpeed * _deltaTime

				if CalculatedCharge >= EquippedMelee.MaxDamage then
					States.Charge = EquippedMelee.MaxDamage
				else
					States.Charge = CalculatedCharge
				end
			end
		else
			States.Charge = 0
		end
		-- Step cooldowns
		if States.SwingRecovery > 0 then
			States.IsSwingRecovering = true
			local CalculatedSwingRecovery = States.SwingRecovery
			CalculatedSwingRecovery -= _deltaTime
			if CalculatedSwingRecovery <= 0 then
				States.SwingRecovery = 0
				States.IsSwingRecovering = false
			else
				States.SwingRecovery = CalculatedSwingRecovery
			end
		end
		if States.BlockRecovery > 0 then
			States.IsBlockRecovering = true
			local CalculatedBlockRecovery = States.BlockRecovery
			CalculatedBlockRecovery -= _deltaTime
			if CalculatedBlockRecovery <= 0 then
				States.BlockRecovery = 0
				States.IsBlockRecovering = false
			else
				States.BlockRecovery = CalculatedBlockRecovery
			end
		end
	end
end

What I actually wan’t to understand is how to deal with these going out of sync with the server, since from what I understood. Clients and server can get out of sync due to ping fluctuation, if ping was always the same for a client we wouldn’t have to worry about this.

Please I just want to know this, its the last piece of knowledge im missing on how to properly do server authority.

Sorry, I read “HC” as “Hit Cooldown”. Either way, I’m not entirely sure what it’s doing inside of commands? It’s not an input, it’s a stat being kept track of. That means an exploiter can just spoof it as much as they want. Why not just do this inside of a record instead?

You don’t have to worry about this. As long as you are writing things to states- which you are, the server will automatically restore states should they desynchronize for any reason at all. As for the location of the code responsible for these things, ctrl+F “numChanges” in WeaponsServer, and ctrl+F “WeaponState” in WeaponsClient.

If there is any sensitive function that needs to be interrupted if a desync occurs (Only a problem in niche cases), you can either add your own functions that you run when the client receives a state update or compare states with a local copy of states in :ClientThink().

Do note that simulation related things can run multiple times, so if you, for instance, make a sound play when you start sprinting, rewind could make it play again. This can, again, be avoided by making a local copy of states to compare your new state to.

Did you come up with that weapon prediction solution yourself yielding the overwrite? Pretty neat way to avoid worrying about resimulation.

Seems like Valorant uses the same thing, can test it out by using a lag simulator. Wish I was smart enough to come up with these techniques, as I found no documentation for them in other games.

I did! I don’t remember seeing anyone else doing it, honestly I think it’s a bit janky :smiley: But it does the job and its pretty easy to reason about.

1 Like

HC is just hit counter, If I were to send the command with the action we wanna perform it would only perform it once, thats why the counter is used, so the system can look that the number changed and the user probably wants to hit again.

I am not writing these into states I do not know what those are, all my values are stored into a table on the server and a table for the client. Can you explain to me what those are on chickynoid?

You’re writing to states when you’re doing things like “States.Charge = 0”.
You can just think of states like two distinct tables on the client and the server that, if a desync occurs between the two, the server corrects.

Again, you don’t need to put your hit counter inside of commands. Your commands are always gonna be authoritative on what happens on the server, and, if a lag spike occurs and the server is forced to generate commands you never sent through, it will correct the state in your weapon to become accurate (e.g. will increase hit count despite you never pressing lmb, bringing everything back to sync.). It will NOT however correct your commands! Those will stay out of sync as it only restores the state of your weapons and the state of your simulation.

1 Like

You are not getting the purpose of hc, inputs need to be sent every frame, if i just sent the input as hit the server would just continue to hit infinetely instead of once, and sending the command once is not possible with my system and also packet loss can always happens, the counter is just used so the server can know we actually want to hit again. The number sent doesnt really matters. In fact the animation replication system for chickynoid uses this same tactic, I copied it from there.

Also the states table is a table i made myself so i dont think chickynoid is picking up that data and automatically syncing it, how do i exactly tell chickynoid what to sync, what to replicate and what not, i actually dont know to do these yet.

Could we try and keep things a bit more on topic in here please? This seems to be devolving into general tech support now.

1 Like