Open PeturDarri opened 2 years ago
This is somewhat expected SyncVars are "magic" in the sense that the weaver replaces code after compilation with a property and only detects changes via setting to that property for your example that might look something like this
public int Networkhealth
{
get
{
return health;
}
[param: In]
set
{
if (!NetworkBehaviour.SyncVarEqual(value, ref health))
{
int oldValue = health;
SetSyncVar(value, ref health, 1uL);
if (NetworkServer.localClientActive && !GetSyncVarHookGuard(1uL))
{
SetSyncVarHookGuard(1uL, value: true);
OnChanged(oldValue, value);
SetSyncVarHookGuard(1uL, value: false);
}
}
}
}
I'm not sure it is possible to support the ref usage in this case
There should probably be an error raised from the weaver if you attempt to ref a syncvar, but I'm not sure how easy that would be to implement
I expected this as well. I actually don't use Mirror myself but I was aware Mirror used IL weaving and noticed SyncVars are just regular fields so I suspected this would be problematic. I expected an error because I've seen people complain about weaver errors in Mirror, but when I didn't see one, I submitted this issue.
I don't think it's necessary to make this work, it should just raise an error. It should be pretty trivial to spot whenever a SyncVar field is being passed by reference. The IL weaver is already looking for ldflda
instructions related to SyncVar fields for another purpose.
in
and ref readonly
are safe in this case though, the IL weaver should permit those.
I'd think that'd work but spamming values into it in Update won't sync every change because it's too fast...it sends nothing at all?
I'd think that'd work but spamming values into it in Update won't sync every change because it's too fast...it sends nothing at all?
This is a made up example, kept simple to keep the focus on the true cause of the problem. I've confirmed the issue by decompiling the assembly after Mirror's IL weaver has modified it. It clearly bypasses the generated syncing code:
public class Enemy : NetworkBehaviour
{
[SyncVar]
public int Health = 100;
[Server]
public void RandomizeHealth()
{
Health = Random.Range(0, 100);
}
[Server]
public void RandomizeHealthRef()
{
ref var healthRef = ref Health;
healthRef = Random.Range(0, 100);
}
}
decompiled becomes:
public class Enemy : NetworkBehaviour
{
[SyncVar]
public int Health = 100;
[Server]
public void RandomizeHealth()
{
if (!NetworkServer.active)
{
Debug.LogWarning("[Server] function 'System.Void Enemy::RandomizeHealth()' called when server was not active");
return;
}
// Health was replaced with NetworkHealth, which is correct.
this.NetworkHealth = Random.Range(0, 100);
}
[Server]
public void RandomizeHealthRef()
{
if (!NetworkServer.active)
{
Debug.LogWarning("[Server] function 'System.Void Enemy::RandomizeHealthRef()' called when server was not active");
return;
}
// NetworkHealth is nowhere to be seen here, which means GeneratedSyncVarSetter is never called.
ref int healthRef = ref this.Health;
healthRef = Random.Range(0, 100);
}
private void MirrorProcessed()
{
}
public int NetworkHealth
{
get
{
return this.Health;
}
[param: In]
set
{
base.GeneratedSyncVarSetter<int>(value, ref this.Health, 1UL, null);
}
}
public override bool SerializeSyncVars(NetworkWriter writer, bool forceAll)
{
bool result = base.SerializeSyncVars(writer, forceAll);
if (forceAll)
{
writer.WriteInt(this.Health);
return true;
}
writer.WriteULong(base.syncVarDirtyBits);
if ((base.syncVarDirtyBits & 1UL) != 0UL)
{
writer.WriteInt(this.Health);
result = true;
}
return result;
}
public override void DeserializeSyncVars(NetworkReader reader, bool initialState)
{
base.DeserializeSyncVars(reader, initialState);
if (initialState)
{
base.GeneratedSyncVarDeserialize<int>(ref this.Health, null, reader.ReadInt());
return;
}
long num = (long)reader.ReadULong();
if ((num & 1L) != 0L)
{
base.GeneratedSyncVarDeserialize<int>(ref this.Health, null, reader.ReadInt());
}
}
}
Obviously no one is using ref
locals like this, but it highlights the problem in a simple way. The same underlying issue applies to ref
and out
parameters and ref
returns.
I'll leave this open for @vis2k to look at to determine level of effort to solve.
nice find. weaver replaces all access to the variable. we never check for ref there, need to see which IL this generates and add it. should be doable, soon ish after more urgent bug fixes
first step: added a test to make sure it never happens again after fix.
[SyncVar]s are actually properties internally, generated by weaver. passing a property as ref is not supported by c#:
so we should probably detect that case with weaver and generate a compilation error instead of silently failing
IL for passing a value as ref:
// Setter(out serverComponent.value);
IL_0016: ldloc.0
IL_0017: ldflda int32 Mirror.Tests.SyncVarAttributeTests.SyncVarSetByRef::'value'
IL_001c: call void Mirror.Tests.SyncVarAttributeTests.SyncVarAttributeTest::'<SyncsIfSetByRef>g__Setter|14_0'(int32&)
forbidding ldflda for syncvars may not work. for example:
// syncproportions is a [SyncVar] struct
ldflda_Array = _syncProportions.Array;
IL:
IL_0008: ldarg.0
IL_0009: ldarg.0
IL_000a: ldflda valuetype Mirror.Tests.SyncVarAttributeTests.Proportions Mirror.Tests.SyncVarAttributeTests.ImerHook_Ldflda::_syncProportions
IL_000f: ldfld uint8[] Mirror.Tests.SyncVarAttributeTests.Proportions::Array
IL_0014: stfld uint8[] Mirror.Tests.SyncVarAttributeTests.ImerHook_Ldflda::ldflda_Array
this may not be detectable with weaver.
ref / out uses ldflda (load field address). but that's not used only for refs. it's also used for the code above.
=> disallowing ldflda for syncvars would not work. => scanning method parameters for 'out' does not work because we don't know if a syncvar is passed or not
only way to solve this seems to be to scan actual code to see if a syncvar is actually passed as 'ref' or 'out'. weaver can not do this. perhaps using roslyn source generators could solve this in the future. ... unless anyone has any other idea.
another option would be to force [SyncVar]s as properties. another option is to fully serialize all syncvars all the time some day. then we don't need the properties. another option is to enforce blittable so we can memcmp instead of dirty bits. which C# would then disallow from passing as refs.
WIP for ldflda detection is in fix_3129 branch btw
=> disallowing ldflda for syncvars would not work.
In my attempt to fix the problem, I tried disallowing all ldflda
usages for SyncVars. That was problematic because Mirror's own generated IL is using ldflda
. So I added a HashSet<MethodDefinition>
to SyncVarAccessLists
of methods that are allowed to use ldflda
for SyncVars and added all the relevant generated methods.
That fixed most of the errors, but there were some tests that were still causing errors, like this one: https://github.com/vis2k/Mirror/blob/0b4787d3d933f804885bf68ee7306109a218d199/Assets/Mirror/Tests/Editor/SyncVarAttributeHookTest.cs#L143-L157
Currently this seems to just silently fail, like ref
SyncVars. Shouldn't this cause an error as well?
The ImerHook_Ldflda
is a tricky case, and yeah I can't see any way to fix it without doing more detailed analysis of exactly what happens to the SyncVar address in the stack. That's a rabbit hole that goes very deep. This kind of analysis would also be needed to allow readonly references, because it's all just ldflda
in IL.
another option is to fully serialize all syncvars all the time some day.
I did consider this as well. One solution could be to just set a SyncVar as dirty whenever it's used with ldflda
. I assume it's serialized sometime later after being dirtied, so the value should already be set by that time. This solution would also automatically make the DavidHook
test work instead of silently failing.
The downside is it would cause unintentional serialization in cases where it's only being read (but that's still better than serializing always).
Describe the bug If you pass a SyncVar field by reference (either with
ref
orout
) and modify the value, the value will change locally but won't be synced to other clients.[IMPORTANT] How can we reproduce the issue, step by step:
out
orref
).Expected behavior I expect the value of the field to be synced or for there to be an error when I attempt to pass a SyncVar by reference.
Desktop (please complete the following information):
Additional context This is also problematic for
ref
returns: