Compiler bug when inheriting from a generic networkbheaviour class

I’ve been trying to make a generic class for handling serverAuth, that includes client prediction and reconciliation, but when i try to test it, unity throws a huge error. I’m using the latest netcode for gameobjects version and almost the latest unity version.

error:

Unity.Netcode.Editor.CodeGen.NetworkBehaviourILPP: (0,0): error  - System.NullReferenceException: Object reference not set to an instance of an object.||   at 
Mono.Cecil.ImportGenericContext.TypeParameter(String type, Int32 position)||   at 
Mono.Cecil.DefaultMetadataImporter.ImportTypeSpecification(TypeReference type, ImportGenericContext context)||   at 
Mono.Cecil.DefaultMetadataImporter.ImportType(TypeReference type, ImportGenericContext context)||   at
Mono.Cecil.DefaultMetadataImporter.ImportTypeSpecification(TypeReference type, ImportGenericContext context)||   at 
Mono.Cecil.DefaultMetadataImporter.ImportType(TypeReference type, ImportGenericContext context)||   at 
Mono.Cecil.DefaultMetadataImporter.ImportMethodSpecification(MethodReference method, ImportGenericContext context)||   at 
Mono.Cecil.DefaultMetadataImporter.ImportMethod(MethodReference method, ImportGenericContext context)||   at 
Mono.Cecil.DefaultMetadataImporter.ImportReference(MethodReference method, IGenericParameterProvider context)||   at 
Unity.Netcode.Editor.CodeGen.NetworkBehaviourILPP.GetWriteMethodForParameter(TypeReference paramType, MethodReference& methodRef)||   at 
Unity.Netcode.Editor.CodeGen.NetworkBehaviourILPP.InjectWriteAndCallBlocks(MethodDefinition methodDefinition, CustomAttribute rpcAttribute, UInt32 rpcMethodId)||   at 
Unity.Netcode.Editor.CodeGen.NetworkBehaviourILPP.ProcessNetworkBehaviour(TypeDefinition typeDefinition, String[] assemblyDefines)||   at 
Unity.Netcode.Editor.CodeGen.NetworkBehaviourILPP.ProcessNetworkBehaviour(TypeDefinition typeDefinition, String[] assemblyDefines)||   at 
System.Collections.Generic.List`1.ForEach(Action`1 action)||   at 
Unity.Netcode.Editor.CodeGen.NetworkBehaviourILPP.Process(ICompiledAssembly compiledAssembly)   at 
Mono.Cecil.ImportGenericContext.TypeParameter(String type, Int32 position)||   at 
Mono.Cecil.DefaultMetadataImporter.ImportTypeSpecification(TypeReference type, ImportGenericContext context)||   at 
Mono.Cecil.DefaultMetadataImporter.ImportType(TypeReference type, ImportGenericContext context)||   at 
Mono.Cecil.DefaultMetadataImporter.ImportTypeSpecification(TypeReference type, ImportGenericContext context)||   at 
Mono.Cecil.DefaultMetadataImporter.ImportType(TypeReference type, ImportGenericContext context)||   at 
Mono.Cecil.DefaultMetadataImporter.ImportMethodSpecification(MethodReference method, ImportGenericContext context)||   at 
Mono.Cecil.DefaultMetadataImporter.ImportMethod(MethodReference method, ImportGenericContext context)||   at 
Mono.Cecil.DefaultMetadataImporter.ImportReference(MethodReference method, IGenericParameterProvider context)||   at 
Unity.Netcode.Editor.CodeGen.NetworkBehaviourILPP.GetWriteMethodForParameter(TypeReference paramType, MethodReference& methodRef)||   at 
Unity.Netcode.Editor.CodeGen.NetworkBehaviourILPP.InjectWriteAndCallBlocks(MethodDefinition methodDefinition, CustomAttribute rpcAttribute, UInt32 rpcMethodId)||   at 
Unity.Netcode.Editor.CodeGen.NetworkBehaviourILPP.ProcessNetworkBehaviour(TypeDefinition typeDefinition, String[] assemblyDefines)||   at 
Unity.Netcode.Editor.CodeGen.NetworkBehaviourILPP.ProcessNetworkBehaviour(TypeDefinition typeDefinition, String[] assemblyDefines)||   at 
System.Collections.Generic.List`1.ForEach(Action`1 action)||   at 
Unity.Netcode.Editor.CodeGen.NetworkBehaviourILPP.Process(ICompiledAssembly compiledAssembly)

Both scripts have a lot of commented code because i’ve tried a lot of things

ServerAuthHandler:

using System;
using System.Collections.Generic;
using Unity.Netcode;
using Unity.VisualScripting;
using UnityEngine;

/*
///<summary>
///A function to be called when an input is polled to be sent to the server.
///</summary>
///<returns>
///A class implementing IInputData, containing the input information to be sent to the server.
///</returns>
public delegate TInputData GetInputDataDelegate<TInputData>() where TInputData : IInputData;
///<summary>
///The function needs to process the input and apply it to the scene, while also returning the new state.
///</summary>
///<returns>
///A class implementing IStateData, containing the new state after the changes.
///</returns>
public delegate TStateData ProcessInputDelegate<TInputData, TStateData>(TInputData input) where TInputData : IInputData where TStateData : IStateData;
///<summary>
///The function needs to calculate the error between the two states and determine if a reconciliation is necessary.
///</summary>
///<returns>
///A bool that is true if a reconciliation is needed, and false if it isn't.
///</returns>
public delegate bool CalculateErrorDelegate<TStateData>(TStateData serverState, TStateData clientState) where TStateData : IStateData;
///<summary>
///The function needs to load the state into the scene.
///</summary>
public delegate void LoadStateDelegate<TStateData>(TStateData state) where TStateData : IStateData;
*/

/// <summary>
/// A class for handling serverauth, it includes client prediction and reconciliation
/// </summary>
/// <typeparam name="InputData">A struct that inherits INetworkSerializable and contains all the information needed to determine the next state</typeparam>
/// <typeparam name="StateData">A struct that inherits INetworkSerializable and contains all the information about the current state</typeparam>
public abstract class ServerAuthHandler<InputData, StateData> : NetworkBehaviour where InputData : struct, INetworkSerializable where StateData : struct, INetworkSerializable
{
    float timer;
    int currentTick;
    public float timePerTick;
    public bool logReconciliationWarning;
    const float tickRate = 60f;
    const int bufferSize = 1024;
    /*
    public GetInputDataDelegate<IInputData> FGetInputData;
    public ProcessInputDelegate<IInputData, IStateData> FProcessInput;
    public CalculateErrorDelegate<IStateData> FCalculateError;
    public LoadStateDelegate<IStateData> FLoadState;
    */

    //Client
    InputPayload[] inputBuffer;
    StatePayload[] cStateBuffer;
    StatePayload lastServerState;
    StatePayload lastProcessedState;

    //Server
    Queue<InputPayload> inputQueue;
    StatePayload[] sStateBuffer;

    /*
    public static ServerAuthHandler CreateInstance<TInputData, TStateData>(
    GetInputDataDelegate<TInputData> GetInputData,
    ProcessInputDelegate<TInputData, TStateData> GetStateData,
    CalculateErrorDelegate<TStateData> CalculateError,
    LoadStateDelegate<TStateData> LoadState,
    bool isServer) 
    where TInputData : IInputData 
    where TStateData : IStateData
    {
        if (GetInputData == null || GetStateData == null || CalculateError == null || LoadState == null)
        {
            Debug.LogError("One or more delegates are null!");
            return null;
        }
        ServerAuthHandler instance = MultiplayerManager.instance.serverAuthHandlers.AddComponent<ServerAuthHandler>();
        instance.FGetInputData = GetInputData as GetInputDataDelegate<IInputData>;
        instance.FProcessInput = GetStateData as ProcessInputDelegate<IInputData, IStateData>;
        instance.FCalculateError = CalculateError as CalculateErrorDelegate<IStateData>;
        instance.FLoadState = LoadState as LoadStateDelegate<IStateData>;
        instance.isServer = isServer;
        return instance;
    }
    */

    // Start is called once before the first execution of Update after the MonoBehaviour is created
    public void Start()
    {
        timePerTick = 1 / tickRate;
        inputBuffer = new InputPayload[bufferSize];
        cStateBuffer = new StatePayload[bufferSize];
        inputQueue = new Queue<InputPayload>();
        sStateBuffer = new StatePayload[bufferSize];
    }

    // Update is called once per frame
    public void Update()
    {
        timer += Time.deltaTime;
        while (timer >= timePerTick)
        {
            if (MultiplayerManager.isServer) SHandleTick(); else CHandleTick();
            timer -= timePerTick;
            currentTick++;
        }
    }

    void CHandleTick()
    {
        //Reconciliation
        if (!lastServerState.Equals(default(StatePayload)) && (lastProcessedState.Equals(default(StatePayload)) || !lastServerState.Equals(lastProcessedState)))
        {



            //ARREGLAR !lastServerState.Equals(lastProcessedState) SIEMPRE == TRUE




            //Debug.Log(!lastServerState.Equals(default(StatePayload)) + "---" + lastProcessedState.Equals(default(StatePayload)) + "---" + !lastServerState.Equals(lastProcessedState));
            HandleServerReconciliation();
        }

        //Prediction and sending
        int bufferIndex = currentTick % bufferSize;
        InputData input = GetInputData();
        InputPayload payload = new()
        {
            tick = currentTick,
            data = input
        };
        inputBuffer[bufferIndex] = payload;
        cStateBuffer[bufferIndex] = GetStatePayload(payload);
        OnClientInputServerRPC(payload);
    }

    [ServerRpc(RequireOwnership = false)]
    void OnClientInputServerRPC(InputPayload payload)
    {
        inputQueue.Enqueue(payload);
    }

    void HandleServerReconciliation()
    {
        lastProcessedState = lastServerState;
        int serverStateBufferIndex = lastServerState.tick % bufferSize;
        if (CalculateError(lastServerState.data, cStateBuffer[serverStateBufferIndex].data))
        {
            if (logReconciliationWarning) Debug.LogWarning("Reconciliating");
            LoadState(lastServerState.data);
            cStateBuffer[serverStateBufferIndex] = lastServerState;
            int tickToProcess = lastServerState.tick + 1;
            while (tickToProcess < currentTick)
            {
                cStateBuffer[tickToProcess % bufferSize] = GetStatePayload(inputBuffer[tickToProcess % bufferSize]);
                tickToProcess++;
            }
        }
    }

    void SHandleTick()
    {
        int bufferIndex = -1;
        while (inputQueue.Count > 0)
        {
            InputPayload payload = inputQueue.Dequeue();
            bufferIndex = payload.tick % bufferSize;
            sStateBuffer[bufferIndex] = GetStatePayload(payload);
        }
        if (bufferIndex != -1)
        {
            OnServerStateClientRPC(sStateBuffer[bufferIndex]);
        }
    }

    [ClientRpc]
    void OnServerStateClientRPC(StatePayload payload)
    {
        lastServerState = payload;
    }

    StatePayload GetStatePayload(InputPayload inputPayload)
    {
        StatePayload statePayload = new()
        {
            tick = inputPayload.tick,
            data = ProcessInput(inputPayload.data)
        };
        return statePayload;
    }

    [Serializable]
    public struct InputPayload : INetworkSerializable
    {
        public int tick;
        public InputData data;

        public void NetworkSerialize<T>(BufferSerializer<T> serializer) where T : IReaderWriter
        {
            serializer.SerializeValue(ref tick);
            serializer.SerializeValue(ref data);
        }
    }

    [Serializable]
    public struct StatePayload : INetworkSerializable
    {
        public int tick;
        public StateData data;
        
        public void NetworkSerialize<T>(BufferSerializer<T> serializer) where T : IReaderWriter
        {
            serializer.SerializeValue(ref tick);
            serializer.SerializeValue(ref data);
        }
    }

    public abstract bool CalculateError(StateData serverState, StateData clientState);

    public abstract void LoadState(StateData state);

    public abstract StateData ProcessInput(InputData input);

    public abstract InputData GetInputData();
}

/*public struct InputData : INetworkSerializable
{
    public Vector2 movement;
    public bool jump;
    public bool sprint;
    public void NetworkSerialize<T>(BufferSerializer<T> serializer) where T : IReaderWriter 
    {
        serializer.SerializeValue(ref movement);
        serializer.SerializeValue(ref jump);
        serializer.SerializeValue(ref sprint);
    }
}

public struct StateData : INetworkSerializable
{
    public Vector3 position;
    public Vector3 velocity;
    public void NetworkSerialize<T>(BufferSerializer<T> serializer) where T : IReaderWriter 
    {
        serializer.SerializeValue(ref position);
        serializer.SerializeValue(ref velocity);
    }
}*/

///<summary>
///The class needs to be serializable, which means having all variables be public and overriding the NetworkSerialize function to provide serialization for each variable.
///</summary>
//public class IInputData : INetworkSerializable {public virtual void NetworkSerialize<T>(BufferSerializer<T> serializer) where T : IReaderWriter {}}

///<summary>
///The class needs to be serializable, which means having all variables be public and overriding the NetworkSerialize function to provide serialization for each variable.
///</summary>
//public class IStateData : INetworkSerializable {public virtual void NetworkSerialize<T>(BufferSerializer<T> serializer) where T : IReaderWriter {}}

PlayerController:

using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using Unity.VisualScripting;
using UnityEngine;
using System.IO;
using Unity.Netcode;

public class PlayerController : ServerAuthHandler<InputData, StateData>
{
    public float sensitivity = 20f;
    public Transform cameraTransform;
    public int speed;
    public float jumpForce;
    public float maxPosError;
    public float maxVelError;

    new void Start()
    {
        base.Start();
        Cursor.visible = false;
        Cursor.lockState = CursorLockMode.Locked;
        if (!IsOwner) Destroy(cameraTransform.gameObject);
        /*
        serverAuthHandler = ServerAuthHandler.CreateInstance<InputData, StateData>(GetInputData, GetStateData, CalculateError, LoadStateData, MultiplayerManager.isServer);
        serverAuthHandler.FGetInputData = GetInputData;
        serverAuthHandler.FProcessInput = GetStateData;
        serverAuthHandler.FCalculateError = CalculateError;
        serverAuthHandler.FLoadState = LoadStateData;
        serverAuthHandler.isServer = MultiplayerManager.isServer;
        */
    }

    new void Update()
    {
        base.Update();
        if (!IsOwner) return;
        MoveCamera();
    }

    void MoveCamera()
    {
        Vector2 deltaMouse;
        deltaMouse = new Vector2(Input.GetAxis("Mouse X"), -Input.GetAxis("Mouse Y"));
        float treatedCameraX;
        if (cameraTransform.eulerAngles.x < 180)
        {
            treatedCameraX = cameraTransform.eulerAngles.x;
        }
        else
        {
            treatedCameraX = -180 + (cameraTransform.eulerAngles.x -180);
        }
        transform.Rotate(deltaMouse.x * sensitivity * Time.deltaTime * Vector3.up);
        cameraTransform.eulerAngles = new Vector3(Mathf.Clamp(treatedCameraX + deltaMouse.y * sensitivity * Time.deltaTime, -90, 90), cameraTransform.eulerAngles.y, cameraTransform.eulerAngles.z);
    }


    

    public override InputData GetInputData()
    {
        Vector2 movement = Vector2.zero;
        if (Input.GetKey(KeyCode.W))
        {
            movement += new Vector2(transform.forward.x, transform.forward.z);
        }
        if (Input.GetKey(KeyCode.A))
        {
            movement -= new Vector2(transform.right.x, transform.right.z);
        }
        if (Input.GetKey(KeyCode.S))
        {
            movement -= new Vector2(transform.forward.x, transform.forward.z);
        }
        if (Input.GetKey(KeyCode.D))
        {
            movement += new Vector2(transform.right.x, transform.right.z);
        }
        InputData pMovement = new()
        {
            jump = Input.GetKeyDown(KeyCode.Space),
            sprint = Input.GetKey(KeyCode.LeftShift) && Input.GetKey(KeyCode.W),
            movement = movement
        };
        return pMovement;
        
    }

    public override StateData ProcessInput(InputData inputData)
    {
        if (inputData.jump && Physics.Raycast(transform.position, Vector3.down, 1f)) GetComponent<Rigidbody>().AddForce(Vector3.up * jumpForce, ForceMode.Impulse);
        transform.Translate((inputData.sprint ? 1f : 1.5f) * base.timePerTick * speed * new Vector3(inputData.movement.x, 0, inputData.movement.y).normalized, Space.World);
        StateData pPos = new()
        {
            position = transform.position,
            velocity = GetComponent<Rigidbody>().linearVelocity
        };
        return pPos;
    }

    public override bool CalculateError(StateData serverState, StateData clientState)
    {
        return Vector3.Distance(serverState.position, clientState.position) > maxPosError || Vector3.Distance(serverState.velocity, clientState.velocity) > maxVelError;
    }

    public override void LoadState(StateData stateData)
    {
        transform.position = stateData.position;
        GetComponent<Rigidbody>().linearVelocity = stateData.velocity;
    }

}
    public struct InputData : INetworkSerializable
    {
        public Vector2 movement;
        public bool jump;
        public bool sprint;
        public void NetworkSerialize<T>(BufferSerializer<T> serializer) where T : IReaderWriter 
        {
            serializer.SerializeValue(ref movement);
            serializer.SerializeValue(ref jump);
            serializer.SerializeValue(ref sprint);
        }
    }

    public struct StateData : INetworkSerializable
    {
        public Vector3 position;
        public Vector3 velocity;
        public void NetworkSerialize<T>(BufferSerializer<T> serializer) where T : IReaderWriter 
        {
            serializer.SerializeValue(ref position);
            serializer.SerializeValue(ref velocity);
        }
    }

InputData and StateData are structs that inherit from INetworkSerializable

Edit:
I tried replacing TInputData and TStateData with InputData and StateData and it worked perfectly, so the issue is only with generic types.

TInputData and TStateData are also used inside 2 structs, InputPayload and StatePayload, and those are sent back and forth between server and client through RPCs.

There is also another error, but i thought it was a result of the first one:

Assembly 'Library/ScriptAssemblies/Assembly-CSharp-Editor.dll' will not be loaded due to errors:
Unable to resolve reference 'Assembly-CSharp'. Is the assembly missing or incompatible with the current platform?
Reference validation can be disabled in the Plugin Inspector.

A generic class that handles two completely unrelated tasks?

Authentication has nothing to do with prediction/reconciliation.

The ILPP seems to indicate the issue is when you’re testing a build. What happens when testing in the editor with Multiplayer Playmode?

You could try simplifying your code to narrow down the issue. But we’d need to see more of your code to make suggestions as to what could possibly be going on here.

Oh no, sorry, i can see how that might be confused. I probably should write better names. It’s a script that gives you functionality related to a serverauth infrastructure, more specifically, client prediction and reconciliation.
As for providing more code, both scripts are very long and i didn’t know how to make them collapsable, so you didn’t need to scroll for ages to get to the comment section.

Also, that error appears when the code gets compiled, before building or pressing play, and i alredy tried replacing the generics with concrete types and it worked, i added some new information at the end of the post and put both scripts entirely.