The easiest way to adjust the clock is for the server to tell the client how early/late it is.
Each time the server decodes a packet, it finds the earliest tick command in the packet (may be only one tick if you don't batch them.) This is easy to do when you're figuring out which tick to queue the incoming commands for in the receive loop.
It then compares the current server tick to the command tick, and compares to the desired input window. For example, let's say that you want messages to arrive in the window between 1..4 ticks ahead of time. If it arrives more than 4 ticks ahead of time, then the server calculates the offset as "ahead by (client_tick - server_tick - 3)" to aim for the "3 ahead" point. If it arrives less than 1 tick ahead of time (just in time, or too late) then it calculates the offset as "behind by (server_tick + 2 - client_tick)" to aim for the "2 ahead" point. If the timestamp is within the desired 1..4 ticks ahead window, then the server simply sets the offset to 0.
Then, the server sends back the timing offset to the client. When the client receives a timing offset value, it simply adjusts its clock offset by that value for any future packets.
Note that there will be multiple packets with a time adjustment in the pipeline/in flight, so you may also want to have an "adjustment generation" value inside the packet, set by the client, and returned by the server, and the client only actually adjusts its clock offset if the generation in the return packet is the same as its current generation.
Here's some example code illustration for client logic:
struct client_state {
int tickOffset;
uint8_t tickGeneration;
} * clientState;
int clientClockTick() {
return clockMicroseconds() / FRAME_LENGTH_US + clientState->tickOffset;
}
send_to_server() {
...
packet->targetTick = clientClockTick();
packet->tickGeneration = clienState->tickGeneration;
...
}
receive_from_server() {
...
if (packet->timeOffset != 0 && packet->tickGeneration == clientState->tickGeneration) {
clientState->tickOffset -= packet->timeOffset;
clientState->tickGeneration++;
}
...
}
Here's some example code illustration for server logic:
receive_on_server() {
...
int st = serverCockTick();
int clientOffset = 0;
if (packet->targetTick < st + 1) {
clientOffset = (st + 2) - packet->targetTick;
} else if (packet->targetTick > st + 4) {
clientOffset = packet->targetTick - (st + 3);
}
returnPacket->timeOffset = clientOffset;
returnPacket->tickGeneration = packet->tickGeneration;
...
}