public class ProceduralAnimeController : MonoBehaviour
{
    [SerializeField] private Transform _target;

    [Header("Head")] [SerializeField] private Transform _headBone;
    [SerializeField] private float _headMaxTurnAngle;
    [SerializeField] private float _headTrackingSpeed;

    [Header("Eyes")] [SerializeField] private Transform _leftEyeBone;
    [SerializeField] private Transform _rightEyeBone;
    [SerializeField] private float _eyeTrackingSpeed;
    [SerializeField] private float _leftEyeMaxYRotation;
    [SerializeField] private float _leftEyeMinYRotation;
    [SerializeField] private float _rightEyeMaxYRotation;
    [SerializeField] private float _rightEyeMinYRotation;

    [Header("Legs")]
    [SerializeField] private LegIKStepper _frontLeftLegStepper;
    [SerializeField] private LegIKStepper _frontRightLegStepper;
    [SerializeField] private LegIKStepper _backLeftLegStepper;
    [SerializeField] private LegIKStepper _backRightLegStepper;

    [Header("Movement")]
    [SerializeField] private float _turnSpeed;
    [SerializeField] private float _moveSpeed;
    [SerializeField] private float _turnAcceleration;
    [SerializeField] private float _moveAcceleration;
    [SerializeField] private float _minDistToTarget;
    [SerializeField] private float _maxDistToTarget;
    [SerializeField] private float _maxAngelToTarget;

    public Vector3 _currentVelocity;
    public Vector3 FlockAdditon;
    private float _currentAngularVelocity;

    private void Start()
    {    //Set our Stepper to world position. Did this so we can save the whole object as a prefab.
        _frontLeftLegStepper.gameObject.transform.parent = null;
        _frontRightLegStepper.gameObject.transform.parent = null;
        _backLeftLegStepper.gameObject.transform.parent = null;
        _backRightLegStepper.gameObject.transform.parent = null;
        StartCoroutine(LegUpdate());
    }


    void RootMotionUpdate(float dt)
    {
        //Rotation
        Vector3 towardTarget = _target.position - transform.position;
        Vector3 towardTargetProjected = Vector3.ProjectOnPlane(towardTarget, transform.up);
        float angelToTarget = Vector3.SignedAngle(transform.forward, towardTargetProjected, transform.up);
        float targetAngularVelocity = 0;

        if (Mathf.Abs(angelToTarget) > _maxAngelToTarget)
        {
            if (angelToTarget > 0)
                targetAngularVelocity = _turnSpeed;
            else
                targetAngularVelocity = -_turnSpeed;
        }

        _currentAngularVelocity = Mathf.Lerp(_currentAngularVelocity, targetAngularVelocity,
            1 - Mathf.Exp(-_turnAcceleration * Time.deltaTime));

        transform.Rotate(0, _currentAngularVelocity * dt, 0, Space.World); //Y in worldspace

        //Position
        Vector3 targetVelocity = Vector3.zero;

        if (Mathf.Abs(angelToTarget) < 90)
        {
            float distToTarget = Vector3.Distance(transform.position, _target.position);

            if (distToTarget > _maxDistToTarget)
                targetVelocity = _moveSpeed * towardTargetProjected.normalized;

            else if (distToTarget < _minDistToTarget)
                targetVelocity = (_moveSpeed / 2f) * -towardTargetProjected.normalized;

        }

        _currentVelocity = Vector3.Lerp(_currentVelocity, targetVelocity, 1 - Mathf.Exp(-_moveAcceleration * dt));
        transform.position += _currentVelocity * dt;

        if((transform.position - _target.position).magnitude > 5f)
            transform.position += FlockAdditon * dt;

    }

    private void LateUpdate()
    {
        float dt = Time.deltaTime;

        RootMotionUpdate(dt);
        HeadTrackingUpdate(dt);
        EyeTrackingUpdate(dt);
    }

    IEnumerator LegUpdate()
    {
        while (true)
        {
            do
            {
                _frontLeftLegStepper.TryMove();
                _backRightLegStepper.TryMove();
                yield return null;

            } while (_frontLeftLegStepper.Moving || _backRightLegStepper.Moving);

            do
            {
                _frontRightLegStepper.TryMove();
                _backLeftLegStepper.TryMove();
                yield return null;
            } while (_frontRightLegStepper.Moving || _backLeftLegStepper.Moving);
        }
    }
}
                                        
                                    


Npc ID System

To keep Client and Server communication clean. I made the Npc system and most of the other systems ID based. An ID is created by both the client/server when a new npc is spawned into the world. I then loop through all the npc on the server to update their position. If a position is changed... the ID and the new position is sent to the client.
Same thing is going on Client side more or less. The Client loops through a list of the IDs checking for changes and updates their visualisation.

                                        
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Netkraft;
using Netkraft.Messaging;
using UnityEngine;

[System.Serializable]
public class ServerNpcManager
{
    [SerializeField] private Server _server;
    [SerializeField] private Grid _grid;
    [SerializeField] private Ship _ship;

    private readonly List _npcs = new List();
    private int _idCounter = 0;



    public void Update()
    {
        float dt = Time.deltaTime;
        foreach (ServerNpc npc in _npcs)
        {
            if (npc.Path.Count > 1)
            {
                npc.timer += dt;

                if (npc.timer < 1f)
                    continue;

                //Remove completed Targetcell
                _grid.OccupyCell(npc.TargetCell, false);
                npc.Path.RemoveAt(npc.Path.Count-1);
                npc.timer = 0;

                //If we have more Targetcells dont skip
                if (npc.Path.Count < 1)
                    continue;

                //Set new TargetCell
                npc.TargetCell = npc.Path[npc.Path.Count-1];
                _grid.OccupyCell(npc.TargetCell, true);
            }
        }

        Server.Send(new NpcUpdateMessage {ServerNpcs = _npcs.ToArray()});
    }

    public void AddNpc(Vector2Int gridPosition)
    {
        _idCounter++;
        _npcs.Add(new ServerNpc {Id = _idCounter, TargetCell = gridPosition});
        Debug.Log("Added Npc with ID: " + _idCounter);
    }

    public bool AddPathToNpc(int npcId, Vector2Int goal)
    {
        foreach (ServerNpc npc in _npcs)
        {
            if (npc.Id != npcId)
                continue;

            npc.Path = ServerAstar.GetPath(npc.TargetCell, goal).ToList();
            if (npc.Path.Count != 0)
            {
                npc.TargetCell = npc.Path[npc.Path.Count - 1];
                Debug.Log("Server added path!");
                return true;
            }
        }

        Debug.Log("Server could not find Npc ID! Or Path was not reachable!");
        return false;
    }

    [Writable]
    public class ServerNpc
    {
        public int Id;
        public Vector2Int TargetCell;
        [SkipIndex] public float timer;
        [SkipIndex] public List Path = new List();
    }
}
                                        
                                    

Pathfinding A*

When a path is requested, the server creates a path with an A* algorithm. Said path is only stored on the server, and the path is evaluated by the server with a timer for each node in the path array.
The Clients only receive an update object position to lerp towards. This works for all object in the game.

                                        
using System;
using System.Collections.Generic;
using System.Linq;
using Netkraft.Messaging;
using UnityEngine;

public static class ServerAstar
{
    private static Grid _grid;
    private static List _openList = new List();
    private static List _closedList = new List();
    private static Node _current;
    private static List _adjacencies;


    public static void Init(Grid grid)
    {
        _grid = grid;
    }

    public static Vector2Int[] GetPath(Vector2Int gridStart, Vector2Int gridEnd)
    {
        _openList.Clear();
        _closedList.Clear();
        List path = new List();
        Node start = new Node(gridStart);
        _openList.Add(start);

        while (_openList.Count > 0 && !_closedList.Exists(x => x.Position == gridEnd))
        {
            _current = _openList[0];
            _openList.Remove(_current);
            _closedList.Add(_current);
            _adjacencies = GetAdjacentNodes(_current);

            foreach (Node n in _adjacencies)
            {
                if(_closedList.Contains(n) || _openList.Contains(n))
                    continue;;

                n.Parent = _current;
                n.DistanceToGoal = ManhattanDistance(n.Position, gridEnd);
                n.Cost = 1 + n.Parent.Cost;
                _openList.Add(n);
                _openList = _openList.OrderBy(node => node.F).ToList();
            }
        }

        if (!_closedList.Exists(x => x.Position == gridEnd))
        {
            Debug.Log("Could not find a Path!");
            return null;
        }

        Node currentNode = _closedList[_closedList.IndexOf(_current)];

        while (currentNode.Parent != start && currentNode != null)
        {
            path.Insert(0, currentNode);
            currentNode = currentNode.Parent;
        }

        List temp = new List();

        foreach (Node x in path)
        {
            temp.Add(x.Position);
        }

        return temp.ToArray();
    }

    private static List GetAdjacentNodes(Node n)
    {
        List temp = new List();

        if(n.Position.y + 1 <= _grid.Height)
        {
            if(_grid.ValidMove(_grid.GetCell(n.Position), new Vector2Int(0, 1)))
                temp.Add(new Node(new Vector2Int(n.Position.x, n.Position.y + 1)));
        }
        if(n.Position.y - 1 >= 0)
        {
            if(_grid.ValidMove(_grid.GetCell(n.Position), new Vector2Int(0, -1)))
                temp.Add(new Node(new Vector2Int(n.Position.x, n.Position.y - 1)));
        }
        if(n.Position.x - 1 >= 0)
        {
            if(_grid.ValidMove(_grid.GetCell(n.Position), new Vector2Int(-1, 0)))
                temp.Add(new Node(new Vector2Int(n.Position.x - 1, n.Position.y)));
        }
        if(n.Position.x + 1 <= _grid.Width)
        {
            if(_grid.ValidMove(_grid.GetCell(n.Position), new Vector2Int(1, 0)))
                temp.Add(new Node(new Vector2Int(n.Position.x + 1, n.Position.y)));
        }


        return temp;
    }
    private static int ManhattanDistance(Vector2Int start, Vector2Int end)
    {
        checked {
            return Mathf.Abs(start.x - end.x) + Mathf.Abs(start.y - end.y);
        }
    }

}

[Writable, Serializable]
public class Node
{
    public Vector2Int Position;
    public int DistanceToGoal;
    public int Cost;
    public Node Parent;
    public float F
    {
        get
        {
            if (DistanceToGoal != -1 && Cost != -1)
                return DistanceToGoal + Cost;
            else
                return -1;
        }
    }

    public Node(Vector2Int start)
    {
        Position = start;
        Cost = 1;
        DistanceToGoal = -1;
        Parent = null;
    }

}
}
                                        
                                    

Pushables

We are going for a Faster Than Light ripoff mixed with Overcooked. So our inventory system should be object-based in our ship. To create chaos for the players.
The server detects when a player input is being blocked by a pushable, if that pushable has space in the same direction as the input, it will be pushed. You can also drag pushables.
As with the pathfinding, the clients only the position to lerp to foreach moveable object in the scene. All checks for pushables are done on the server from the movement input provided by the Client player.

                                        
using UnityEngine;
using System.Collections.Generic;

[System.Serializable]
public class ServerInteractorManager
{
    [SerializeField] private Grid _grid;
    public Dictionary _interactables = new Dictionary();

    public bool LookForInteractable(Vector2Int checkLocation)
    {
        if (_interactables.ContainsKey(checkLocation))
            return true;
        return false;
    }

    public void AddItem(Vector2Int position, InteractData interactData)
    {
        if (LookForInteractable(position))
            return;

        _interactables.Add(position, interactData);
        _grid.OccupyCell(position, true);
        Server.Send(new InteractablesMessage{MoveTo = position, InteractData = interactData});
    }

    public bool RemoveItem(Vector2Int checkLocation)
    {
        return _interactables.Remove(checkLocation);
    }

    public void MoveInteractable(Vector2Int checkLocation, Vector2Int input)
    {
        _grid.OccupyCell(checkLocation, false);
        _grid.OccupyCell(checkLocation + input, true);

        InteractData temp = _interactables[checkLocation];
        temp.Position = checkLocation + input;
        _interactables.Add(temp.Position,  temp);
        _interactables.Remove(checkLocation);

        Server.Send(new InteractablesMessage{MoveTo = input, InteractData = temp, CheckLocation = checkLocation});
    }
}