feat: Allow generic NetworkBehaviour<T> subclasses (#3073)

* feat: Allow generic NetworkBehaviour subclasses

It's only generic SyncVars (via attribute) and rpcs/cmds we don't want to deal with and that aren't supported.
Even generic SyncVar<T> works

* Generate IL2CPP compatible base calls

see cf91e1d547

* Make SyncVar field/hook references generic too

Fixes bad IL

* Update Extensions.cs

* Update Assets/Mirror/Editor/Weaver/Extensions.cs

Co-authored-by: vis2k <info@noobtuts.com>
This commit is contained in:
Robin Rolf 2022-02-23 05:50:15 +01:00 committed by GitHub
parent 7670271bf1
commit d67dc74bbd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 249 additions and 33 deletions

View File

@ -140,6 +140,18 @@ public static MethodReference MakeHostInstanceGeneric(this MethodReference self,
return module.ImportReference(reference); return module.ImportReference(reference);
} }
// needed for NetworkBehaviour<T> support
// https://github.com/vis2k/Mirror/pull/3073/
public static FieldReference MakeHostInstanceGeneric(this FieldReference self)
{
var declaringType = new GenericInstanceType(self.DeclaringType);
foreach (var parameter in self.DeclaringType.GenericParameters)
{
declaringType.GenericArguments.Add(parameter);
}
return new FieldReference(self.Name, self.FieldType, declaringType);
}
// Given a field of a generic class such as Writer<T>.write, // Given a field of a generic class such as Writer<T>.write,
// and a generic instance such as ArraySegment`int // and a generic instance such as ArraySegment`int
// Creates a reference to the specialized method ArraySegment`int`.get_Count // Creates a reference to the specialized method ArraySegment`int`.get_Count
@ -251,5 +263,75 @@ public static AssemblyNameReference FindReference(this ModuleDefinition module,
} }
return null; return null;
} }
// Takes generic arguments from child class and applies them to parent reference, if possible
// eg makes `Base<T>` in Child<int> : Base<int> have `int` instead of `T`
// Originally by James-Frowen under MIT
// https://github.com/MirageNet/Mirage/commit/cf91e1d54796866d2cf87f8e919bb5c681977e45
public static TypeReference ApplyGenericParameters(this TypeReference parentReference,
TypeReference childReference)
{
// If the parent is not generic, we got nothing to apply
if (!parentReference.IsGenericInstance)
return parentReference;
GenericInstanceType parentGeneric = (GenericInstanceType)parentReference;
// make new type so we can replace the args on it
// resolve it so we have non-generic instance (eg just instance with <T> instead of <int>)
// if we don't cecil will make it double generic (eg INVALID IL)
GenericInstanceType generic = new GenericInstanceType(parentReference.Resolve());
foreach (TypeReference arg in parentGeneric.GenericArguments)
generic.GenericArguments.Add(arg);
for (int i = 0; i < generic.GenericArguments.Count; i++)
{
// if arg is not generic
// eg List<int> would be int so not generic.
// But List<T> would be T so is generic
if (!generic.GenericArguments[i].IsGenericParameter)
continue;
// get the generic name, eg T
string name = generic.GenericArguments[i].Name;
// find what type T is, eg turn it into `int` if `List<int>`
TypeReference arg = FindMatchingGenericArgument(childReference, name);
// import just to be safe
TypeReference imported = parentReference.Module.ImportReference(arg);
// set arg on generic, parent ref will be Base<int> instead of just Base<T>
generic.GenericArguments[i] = imported;
}
return generic;
}
// Finds the type reference for a generic parameter with the provided name in the child reference
// Originally by James-Frowen under MIT
// https://github.com/MirageNet/Mirage/commit/cf91e1d54796866d2cf87f8e919bb5c681977e45
static TypeReference FindMatchingGenericArgument(TypeReference childReference, string paramName)
{
TypeDefinition def = childReference.Resolve();
// child class must be generic if we are in this part of the code
// eg Child<T> : Base<T> <--- child must have generic if Base has T
// vs Child : Base<int> <--- wont be here if Base has int (we check if T exists before calling this)
if (!def.HasGenericParameters)
throw new InvalidOperationException(
"Base class had generic parameters, but could not find them in child class");
// go through parameters in child class, and find the generic that matches the name
for (int i = 0; i < def.GenericParameters.Count; i++)
{
GenericParameter param = def.GenericParameters[i];
if (param.Name == paramName)
{
GenericInstanceType generic = (GenericInstanceType)childReference;
// return generic arg with same index
return generic.GenericArguments[i];
}
}
// this should never happen, if it does it means that this code is bugged
throw new InvalidOperationException("Did not find matching generic");
}
} }
} }

View File

@ -68,14 +68,6 @@ public bool Process(ref bool WeavingFailed)
return false; return false;
} }
if (netBehaviourSubclass.HasGenericParameters)
{
Log.Error($"{netBehaviourSubclass.Name} cannot have generic parameters", netBehaviourSubclass);
WeavingFailed = true;
// originally Process returned true in every case, except if already processed.
// maybe return false here in the future.
return true;
}
MarkAsProcessed(netBehaviourSubclass); MarkAsProcessed(netBehaviourSubclass);
// deconstruct tuple and set fields // deconstruct tuple and set fields
@ -437,8 +429,13 @@ void GenerateSerialization(ref bool WeavingFailed)
worker.Emit(OpCodes.Ldarg_2); worker.Emit(OpCodes.Ldarg_2);
worker.Emit(OpCodes.Brfalse, initialStateLabel); worker.Emit(OpCodes.Brfalse, initialStateLabel);
foreach (FieldDefinition syncVar in syncVars) foreach (FieldDefinition syncVarDef in syncVars)
{ {
FieldReference syncVar = syncVarDef;
if (netBehaviourSubclass.HasGenericParameters)
{
syncVar = syncVarDef.MakeHostInstanceGeneric();
}
// Generates a writer call for each sync variable // Generates a writer call for each sync variable
// writer // writer
worker.Emit(OpCodes.Ldarg_1); worker.Emit(OpCodes.Ldarg_1);
@ -481,8 +478,14 @@ void GenerateSerialization(ref bool WeavingFailed)
// start at number of syncvars in parent // start at number of syncvars in parent
int dirtyBit = syncVarAccessLists.GetSyncVarStart(netBehaviourSubclass.BaseType.FullName); int dirtyBit = syncVarAccessLists.GetSyncVarStart(netBehaviourSubclass.BaseType.FullName);
foreach (FieldDefinition syncVar in syncVars) foreach (FieldDefinition syncVarDef in syncVars)
{ {
FieldReference syncVar = syncVarDef;
if (netBehaviourSubclass.HasGenericParameters)
{
syncVar = syncVarDef.MakeHostInstanceGeneric();
}
Instruction varLabel = worker.Create(OpCodes.Nop); Instruction varLabel = worker.Create(OpCodes.Nop);
// Generates: if ((base.get_syncVarDirtyBits() & 1uL) != 0uL) // Generates: if ((base.get_syncVarDirtyBits() & 1uL) != 0uL)
@ -539,7 +542,15 @@ void DeserializeField(FieldDefinition syncVar, ILProcessor worker, ref bool Weav
// push 'ref T this.field' // push 'ref T this.field'
worker.Emit(OpCodes.Ldarg_0); worker.Emit(OpCodes.Ldarg_0);
worker.Emit(OpCodes.Ldflda, syncVar); // if the netbehaviour class is generic, we need to make the field reference generic as well for correct IL
if (netBehaviourSubclass.HasGenericParameters)
{
worker.Emit(OpCodes.Ldflda, syncVar.MakeHostInstanceGeneric());
}
else
{
worker.Emit(OpCodes.Ldflda, syncVar);
}
// hook? then push 'new Action<T,T>(Hook)' onto stack // hook? then push 'new Action<T,T>(Hook)' onto stack
MethodDefinition hookMethod = syncVarAttributeProcessor.GetHookMethod(netBehaviourSubclass, syncVar, ref WeavingFailed); MethodDefinition hookMethod = syncVarAttributeProcessor.GetHookMethod(netBehaviourSubclass, syncVar, ref WeavingFailed);
@ -821,6 +832,14 @@ bool ValidateParameters(MethodReference method, RemoteCallType callType, ref boo
// validate parameters for a remote function call like Rpc/Cmd // validate parameters for a remote function call like Rpc/Cmd
bool ValidateParameter(MethodReference method, ParameterDefinition param, RemoteCallType callType, bool firstParam, ref bool WeavingFailed) bool ValidateParameter(MethodReference method, ParameterDefinition param, RemoteCallType callType, bool firstParam, ref bool WeavingFailed)
{ {
// need to check this before any type lookups since those will fail since generic types don't resolve
if (param.ParameterType.IsGenericParameter)
{
Log.Error($"{method.Name} cannot have generic parameters", method);
WeavingFailed = true;
return false;
}
bool isNetworkConnection = param.ParameterType.Is<NetworkConnection>(); bool isNetworkConnection = param.ParameterType.Is<NetworkConnection>();
bool isSenderConnection = IsSenderConnection(param, callType); bool isSenderConnection = IsSenderConnection(param, callType);

View File

@ -16,6 +16,12 @@ public static List<FieldDefinition> FindSyncObjectsFields(Writers writers, Reade
foreach (FieldDefinition fd in td.Fields) foreach (FieldDefinition fd in td.Fields)
{ {
if (fd.FieldType.IsGenericParameter)
{
// can't call .Resolve on generic ones
continue;
}
if (fd.FieldType.Resolve().IsDerivedFrom<SyncObject>()) if (fd.FieldType.Resolve().IsDerivedFrom<SyncObject>())
{ {
if (fd.IsStatic) if (fd.IsStatic)

View File

@ -69,17 +69,28 @@ public void GenerateNewActionFromHookMethod(FieldDefinition syncVar, ILProcessor
worker.Emit(OpCodes.Ldarg_0); worker.Emit(OpCodes.Ldarg_0);
} }
MethodReference hookMethodReference;
// if the network behaviour class is generic, we need to make the method reference generic for correct IL
if (hookMethod.DeclaringType.HasGenericParameters)
{
hookMethodReference = hookMethod.MakeHostInstanceGeneric(hookMethod.Module, hookMethod.DeclaringType.MakeGenericInstanceType(hookMethod.DeclaringType.GenericParameters.ToArray()));
}
else
{
hookMethodReference = hookMethod;
}
// we support regular and virtual hook functions. // we support regular and virtual hook functions.
if (hookMethod.IsVirtual) if (hookMethod.IsVirtual)
{ {
// for virtual / overwritten hooks, we need different IL. // for virtual / overwritten hooks, we need different IL.
// this is from simply testing Action = VirtualHook; in C#. // this is from simply testing Action = VirtualHook; in C#.
worker.Emit(OpCodes.Dup); worker.Emit(OpCodes.Dup);
worker.Emit(OpCodes.Ldvirtftn, hookMethod); worker.Emit(OpCodes.Ldvirtftn, hookMethodReference);
} }
else else
{ {
worker.Emit(OpCodes.Ldftn, hookMethod); worker.Emit(OpCodes.Ldftn, hookMethodReference);
} }
// call 'new Action<T,T>()' constructor to convert the function to an action // call 'new Action<T,T>()' constructor to convert the function to an action
@ -143,6 +154,29 @@ public MethodDefinition GenerateSyncVarGetter(FieldDefinition fd, string origina
ILProcessor worker = get.Body.GetILProcessor(); ILProcessor worker = get.Body.GetILProcessor();
FieldReference fr;
if (fd.DeclaringType.HasGenericParameters)
{
fr = fd.MakeHostInstanceGeneric();
}
else
{
fr = fd;
}
FieldReference netIdFieldReference = null;
if (netFieldId != null)
{
if (netFieldId.DeclaringType.HasGenericParameters)
{
netIdFieldReference = netFieldId.MakeHostInstanceGeneric();
}
else
{
netIdFieldReference = netFieldId;
}
}
// [SyncVar] GameObject? // [SyncVar] GameObject?
if (fd.FieldType.Is<UnityEngine.GameObject>()) if (fd.FieldType.Is<UnityEngine.GameObject>())
{ {
@ -150,9 +184,9 @@ public MethodDefinition GenerateSyncVarGetter(FieldDefinition fd, string origina
// this. // this.
worker.Emit(OpCodes.Ldarg_0); worker.Emit(OpCodes.Ldarg_0);
worker.Emit(OpCodes.Ldarg_0); worker.Emit(OpCodes.Ldarg_0);
worker.Emit(OpCodes.Ldfld, netFieldId); worker.Emit(OpCodes.Ldfld, netIdFieldReference);
worker.Emit(OpCodes.Ldarg_0); worker.Emit(OpCodes.Ldarg_0);
worker.Emit(OpCodes.Ldflda, fd); worker.Emit(OpCodes.Ldflda, fr);
worker.Emit(OpCodes.Call, weaverTypes.getSyncVarGameObjectReference); worker.Emit(OpCodes.Call, weaverTypes.getSyncVarGameObjectReference);
worker.Emit(OpCodes.Ret); worker.Emit(OpCodes.Ret);
} }
@ -163,9 +197,9 @@ public MethodDefinition GenerateSyncVarGetter(FieldDefinition fd, string origina
// this. // this.
worker.Emit(OpCodes.Ldarg_0); worker.Emit(OpCodes.Ldarg_0);
worker.Emit(OpCodes.Ldarg_0); worker.Emit(OpCodes.Ldarg_0);
worker.Emit(OpCodes.Ldfld, netFieldId); worker.Emit(OpCodes.Ldfld, netIdFieldReference);
worker.Emit(OpCodes.Ldarg_0); worker.Emit(OpCodes.Ldarg_0);
worker.Emit(OpCodes.Ldflda, fd); worker.Emit(OpCodes.Ldflda, fr);
worker.Emit(OpCodes.Call, weaverTypes.getSyncVarNetworkIdentityReference); worker.Emit(OpCodes.Call, weaverTypes.getSyncVarNetworkIdentityReference);
worker.Emit(OpCodes.Ret); worker.Emit(OpCodes.Ret);
} }
@ -175,9 +209,9 @@ public MethodDefinition GenerateSyncVarGetter(FieldDefinition fd, string origina
// this. // this.
worker.Emit(OpCodes.Ldarg_0); worker.Emit(OpCodes.Ldarg_0);
worker.Emit(OpCodes.Ldarg_0); worker.Emit(OpCodes.Ldarg_0);
worker.Emit(OpCodes.Ldfld, netFieldId); worker.Emit(OpCodes.Ldfld, netIdFieldReference);
worker.Emit(OpCodes.Ldarg_0); worker.Emit(OpCodes.Ldarg_0);
worker.Emit(OpCodes.Ldflda, fd); worker.Emit(OpCodes.Ldflda, fr);
MethodReference getFunc = weaverTypes.getSyncVarNetworkBehaviourReference.MakeGeneric(assembly.MainModule, fd.FieldType); MethodReference getFunc = weaverTypes.getSyncVarNetworkBehaviourReference.MakeGeneric(assembly.MainModule, fd.FieldType);
worker.Emit(OpCodes.Call, getFunc); worker.Emit(OpCodes.Call, getFunc);
worker.Emit(OpCodes.Ret); worker.Emit(OpCodes.Ret);
@ -186,7 +220,7 @@ public MethodDefinition GenerateSyncVarGetter(FieldDefinition fd, string origina
else else
{ {
worker.Emit(OpCodes.Ldarg_0); worker.Emit(OpCodes.Ldarg_0);
worker.Emit(OpCodes.Ldfld, fd); worker.Emit(OpCodes.Ldfld, fr);
worker.Emit(OpCodes.Ret); worker.Emit(OpCodes.Ret);
} }
@ -215,6 +249,28 @@ public MethodDefinition GenerateSyncVarSetter(TypeDefinition td, FieldDefinition
weaverTypes.Import(typeof(void))); weaverTypes.Import(typeof(void)));
ILProcessor worker = set.Body.GetILProcessor(); ILProcessor worker = set.Body.GetILProcessor();
FieldReference fr;
if (fd.DeclaringType.HasGenericParameters)
{
fr = fd.MakeHostInstanceGeneric();
}
else
{
fr = fd;
}
FieldReference netIdFieldReference = null;
if (netFieldId != null)
{
if (netFieldId.DeclaringType.HasGenericParameters)
{
netIdFieldReference = netFieldId.MakeHostInstanceGeneric();
}
else
{
netIdFieldReference = netFieldId;
}
}
// if (!SyncVarEqual(value, ref playerData)) // if (!SyncVarEqual(value, ref playerData))
Instruction endOfMethod = worker.Create(OpCodes.Nop); Instruction endOfMethod = worker.Create(OpCodes.Nop);
@ -241,7 +297,7 @@ public MethodDefinition GenerateSyncVarSetter(TypeDefinition td, FieldDefinition
// push 'ref T this.field' // push 'ref T this.field'
worker.Emit(OpCodes.Ldarg_0); worker.Emit(OpCodes.Ldarg_0);
worker.Emit(OpCodes.Ldflda, fd); worker.Emit(OpCodes.Ldflda, fr);
// push the dirty bit for this SyncVar // push the dirty bit for this SyncVar
worker.Emit(OpCodes.Ldc_I8, dirtyBit); worker.Emit(OpCodes.Ldc_I8, dirtyBit);
@ -265,14 +321,14 @@ public MethodDefinition GenerateSyncVarSetter(TypeDefinition td, FieldDefinition
{ {
// GameObject setter needs one more parameter: netId field ref // GameObject setter needs one more parameter: netId field ref
worker.Emit(OpCodes.Ldarg_0); worker.Emit(OpCodes.Ldarg_0);
worker.Emit(OpCodes.Ldflda, netFieldId); worker.Emit(OpCodes.Ldflda, netIdFieldReference);
worker.Emit(OpCodes.Call, weaverTypes.generatedSyncVarSetter_GameObject); worker.Emit(OpCodes.Call, weaverTypes.generatedSyncVarSetter_GameObject);
} }
else if (fd.FieldType.Is<NetworkIdentity>()) else if (fd.FieldType.Is<NetworkIdentity>())
{ {
// NetworkIdentity setter needs one more parameter: netId field ref // NetworkIdentity setter needs one more parameter: netId field ref
worker.Emit(OpCodes.Ldarg_0); worker.Emit(OpCodes.Ldarg_0);
worker.Emit(OpCodes.Ldflda, netFieldId); worker.Emit(OpCodes.Ldflda, netIdFieldReference);
worker.Emit(OpCodes.Call, weaverTypes.generatedSyncVarSetter_NetworkIdentity); worker.Emit(OpCodes.Call, weaverTypes.generatedSyncVarSetter_NetworkIdentity);
} }
// TODO this only uses the persistent netId for types DERIVED FROM NB. // TODO this only uses the persistent netId for types DERIVED FROM NB.
@ -283,7 +339,7 @@ public MethodDefinition GenerateSyncVarSetter(TypeDefinition td, FieldDefinition
// NetworkIdentity setter needs one more parameter: netId field ref // NetworkIdentity setter needs one more parameter: netId field ref
// (actually its a NetworkBehaviourSyncVar type) // (actually its a NetworkBehaviourSyncVar type)
worker.Emit(OpCodes.Ldarg_0); worker.Emit(OpCodes.Ldarg_0);
worker.Emit(OpCodes.Ldflda, netFieldId); worker.Emit(OpCodes.Ldflda, netIdFieldReference);
// make generic version of GeneratedSyncVarSetter_NetworkBehaviour<T> // make generic version of GeneratedSyncVarSetter_NetworkBehaviour<T>
MethodReference getFunc = weaverTypes.generatedSyncVarSetter_NetworkBehaviour_T.MakeGeneric(assembly.MainModule, fd.FieldType); MethodReference getFunc = weaverTypes.generatedSyncVarSetter_NetworkBehaviour_T.MakeGeneric(assembly.MainModule, fd.FieldType);
worker.Emit(OpCodes.Call, getFunc); worker.Emit(OpCodes.Call, getFunc);
@ -315,16 +371,18 @@ public void ProcessSyncVar(TypeDefinition td, FieldDefinition fd, Dictionary<Fie
if (fd.FieldType.IsDerivedFrom<NetworkBehaviour>()) if (fd.FieldType.IsDerivedFrom<NetworkBehaviour>())
{ {
netIdField = new FieldDefinition($"___{fd.Name}NetId", netIdField = new FieldDefinition($"___{fd.Name}NetId",
FieldAttributes.Private, FieldAttributes.Family, // needs to be protected for generic classes, otherwise access isn't allowed
weaverTypes.Import<NetworkBehaviour.NetworkBehaviourSyncVar>()); weaverTypes.Import<NetworkBehaviour.NetworkBehaviourSyncVar>());
netIdField.DeclaringType = td;
syncVarNetIds[fd] = netIdField; syncVarNetIds[fd] = netIdField;
} }
else if (fd.FieldType.IsNetworkIdentityField()) else if (fd.FieldType.IsNetworkIdentityField())
{ {
netIdField = new FieldDefinition($"___{fd.Name}NetId", netIdField = new FieldDefinition($"___{fd.Name}NetId",
FieldAttributes.Private, FieldAttributes.Family, // needs to be protected for generic classes, otherwise access isn't allowed
weaverTypes.Import<uint>()); weaverTypes.Import<uint>());
netIdField.DeclaringType = td;
syncVarNetIds[fd] = netIdField; syncVarNetIds[fd] = netIdField;
} }
@ -377,6 +435,13 @@ public void ProcessSyncVar(TypeDefinition td, FieldDefinition fd, Dictionary<Fie
continue; continue;
} }
if (fd.FieldType.IsGenericParameter)
{
Log.Error($"{fd.Name} has generic type. Generic SyncVars are not supported", fd);
WeavingFailed = true;
continue;
}
if (fd.FieldType.IsArray) if (fd.FieldType.IsArray)
{ {
Log.Error($"{fd.Name} has invalid type. Use SyncLists instead of arrays", fd); Log.Error($"{fd.Name} has invalid type. Use SyncLists instead of arrays", fd);

View File

@ -48,16 +48,21 @@ public static MethodReference TryResolveMethodInParents(TypeReference tr, Assemb
{ {
return null; return null;
} }
foreach (MethodDefinition methodRef in tr.Resolve().Methods) foreach (MethodDefinition methodDef in tr.Resolve().Methods)
{ {
if (methodRef.Name == name) if (methodDef.Name == name)
{ {
MethodReference methodRef = methodDef;
if (tr.IsGenericInstance)
{
methodRef = methodRef.MakeHostInstanceGeneric(tr.Module, (GenericInstanceType)tr);
}
return assembly.MainModule.ImportReference(methodRef); return assembly.MainModule.ImportReference(methodRef);
} }
} }
// Could not find the method in this class, try the parent // Could not find the method in this class, try the parent
return TryResolveMethodInParents(tr.Resolve().BaseType, assembly, name); return TryResolveMethodInParents(tr.Resolve().BaseType.ApplyGenericParameters(tr), assembly, name);
} }
public static MethodDefinition ResolveDefaultPublicCtor(TypeReference variable) public static MethodDefinition ResolveDefaultPublicCtor(TypeReference variable)

View File

@ -5,10 +5,17 @@ namespace Mirror.Weaver.Tests
public class WeaverNetworkBehaviourTests : WeaverTestsBuildFromTestName public class WeaverNetworkBehaviourTests : WeaverTestsBuildFromTestName
{ {
[Test] [Test]
public void NetworkBehaviourGeneric() public void NetworkBehaviourGenericSyncVar()
{ {
HasError("NetworkBehaviourGeneric`1 cannot have generic parameters", HasError("genericSyncVarNotAllowed has generic type. Generic SyncVars are not supported",
"WeaverNetworkBehaviourTests.NetworkBehaviourGeneric.NetworkBehaviourGeneric`1"); "T WeaverNetworkBehaviourTests.NetworkBehaviourGeneric.NetworkBehaviourGeneric`1::genericSyncVarNotAllowed");
}
[Test]
public void NetworkBehaviourGenericRpc()
{
HasError("RpcGeneric cannot have generic parameters",
"System.Void WeaverNetworkBehaviourTests.NetworkBehaviourGeneric.NetworkBehaviourGeneric`1::RpcGeneric(T)");
} }
[Test] [Test]

View File

@ -0,0 +1,16 @@
using Mirror;
namespace WeaverNetworkBehaviourTests.NetworkBehaviourGeneric
{
class NetworkBehaviourGeneric<T> : NetworkBehaviour
{
public T param;
public readonly SyncVar<T> syncVar = new SyncVar<T>(default);
public readonly SyncList<T> syncList = new SyncList<T>();
}
class GenericImplInt : NetworkBehaviourGeneric<int>
{
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: b279c2f37eaa4de09c1f15a5b80029b4
timeCreated: 1643560588

View File

@ -4,6 +4,7 @@ namespace WeaverNetworkBehaviourTests.NetworkBehaviourGeneric
{ {
class NetworkBehaviourGeneric<T> : NetworkBehaviour class NetworkBehaviourGeneric<T> : NetworkBehaviour
{ {
T genericsNotAllowed; [ClientRpc]
void RpcGeneric(T param) {}
} }
} }

View File

@ -0,0 +1,12 @@
using Mirror;
namespace WeaverNetworkBehaviourTests.NetworkBehaviourGeneric
{
class NetworkBehaviourGeneric<T> : NetworkBehaviour
{
[SyncVar]
T genericSyncVarNotAllowed;
T genericTypeIsFine;
}
}