Skip to content

Commit b980ea4

Browse files
authored
fix: #4046 [Command]s are now immediately invoked in host mode in order to fix edge cases & race conditions (#4048)
1 parent babb0ac commit b980ea4

File tree

4 files changed

+91
-9
lines changed

4 files changed

+91
-9
lines changed

Assets/Mirror/Editor/Weaver/Processors/CommandProcessor.cs

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,35 @@ public static class CommandProcessor
1010
// generates code like:
1111
public void CmdThrust(float thrusting, int spin)
1212
{
13+
// no host, invoke the original function immediately.
14+
// -> Mirror's Host mode is just a server, we can't simulate an independent client in Unity
15+
// -> delaying this for later introduces cooldown & prediction issues in games.
16+
// for example, assume CmdFireWeapon function with 100ms cooldown between shots.
17+
//
18+
// client-only mode:
19+
// simply calling CmdFireWeapon and waiting for [SyncVar] cooldown would be too irregular (i.e. 150ms)
20+
// client has to predict the cooldown locally in order to call CmdFireWeapon every 100ms
21+
// this works fine, and that's how games need to predict weapon firing / skill usage / etc.
22+
//
23+
// host mode:
24+
// Cmds usued to be queued up for network message processing to 'simulate' a client
25+
// this introduces a massive headache:
26+
// firing 3x would queue up CmdFireWeapon 3 times, without ever setting the cooldown yet
27+
// eventually messages are processed:
28+
// CmdFireWeapon first call goes through, sets cooldown
29+
// CmdFireWeapon second/third call would be rejected: "user attempted to fire on cooldown"
30+
// in other words, we would need to predict cooldowns on host too, which is super weird since host is the server
31+
//
32+
// common sense: on host, calling a Cmd should happen immediately, anything else is too much magic
33+
// and causes edge cases until Unity supports true server/client separation on host!
34+
//
35+
if (isServer && isClient) // isHost
36+
{
37+
UserCode_CmdThrust(value);
38+
return;
39+
}
40+
41+
// otherwise send a command message over the network
1342
NetworkWriterPooled writer = NetworkWriterPool.Get();
1443
writer.Write(thrusting);
1544
writer.WritePackedUInt32((uint)spin);
@@ -38,6 +67,32 @@ public static MethodDefinition ProcessCommandCall(WeaverTypes weaverTypes, Write
3867

3968
NetworkBehaviourProcessor.WriteSetupLocals(worker, weaverTypes);
4069

70+
Instruction skipIfNotHost = worker.Create(OpCodes.Nop);
71+
72+
// Check if isServer && isClient
73+
// note that we don't use NetworkServer/Client.active here,
74+
// otherwise [Command] tests which simulate server/client separation would fail.
75+
worker.Emit(OpCodes.Ldarg_0); // loads this. for isServer check later
76+
worker.Emit(OpCodes.Call, weaverTypes.NetworkBehaviourIsServerReference);
77+
worker.Emit(OpCodes.Brfalse, skipIfNotHost);
78+
79+
worker.Emit(OpCodes.Ldarg_0); // loads this. for isClient check later
80+
worker.Emit(OpCodes.Call, weaverTypes.NetworkBehaviourIsClientReference);
81+
worker.Emit(OpCodes.Brfalse, skipIfNotHost);
82+
83+
// Load 'this' reference (Ldarg_0)
84+
worker.Emit(OpCodes.Ldarg_0);
85+
86+
// Load all the remaining arguments (Ldarg_1, Ldarg_2, ...)
87+
for (int i = 1; i < md.Parameters.Count + 1; i++)
88+
worker.Emit(OpCodes.Ldarg, i);
89+
90+
// Call the original function directly (UserCode_CmdTest__Int32)
91+
worker.Emit(OpCodes.Call, cmd);
92+
worker.Emit(OpCodes.Ret);
93+
94+
worker.Append(skipIfNotHost);
95+
4196
// NetworkWriter writer = new NetworkWriter();
4297
NetworkBehaviourProcessor.WriteGetWriter(worker, weaverTypes);
4398

Assets/Mirror/Editor/Weaver/WeaverTypes.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ public class WeaverTypes
1010
{
1111
public MethodReference ScriptableObjectCreateInstanceMethod;
1212

13+
public MethodReference NetworkBehaviourIsClientReference;
14+
public MethodReference NetworkBehaviourIsServerReference;
1315
public FieldReference NetworkBehaviourDirtyBitsReference;
1416
public MethodReference GetWriterReference;
1517
public MethodReference ReturnWriterReference;
@@ -90,6 +92,9 @@ public WeaverTypes(AssemblyDefinition assembly, Logger Log, ref bool WeavingFail
9092

9193
TypeReference NetworkBehaviourType = Import<NetworkBehaviour>();
9294

95+
NetworkBehaviourIsClientReference = Resolvers.ResolveMethod(NetworkBehaviourType, assembly, Log, "get_isClient", ref WeavingFailed);
96+
NetworkBehaviourIsServerReference = Resolvers.ResolveMethod(NetworkBehaviourType, assembly, Log, "get_isServer", ref WeavingFailed);
97+
9398
NetworkBehaviourDirtyBitsReference = Resolvers.ResolveField(NetworkBehaviourType, assembly, Log, "syncVarDirtyBits", ref WeavingFailed);
9499

95100
generatedSyncVarSetter = Resolvers.ResolveMethod(NetworkBehaviourType, assembly, Log, "GeneratedSyncVarSetter", ref WeavingFailed);

Assets/Mirror/Tests/Editor/NetworkServer/NetworkServerTest.cs

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -800,20 +800,22 @@ public void SendCommand_OnlyAllowedOnOwnedObjects()
800800
{
801801
// listen & connect
802802
NetworkServer.Listen(1);
803-
ConnectHostClientBlockingAuthenticatedAndReady();
803+
ConnectClientBlockingAuthenticatedAndReady(out _);
804804

805805
// add an identity with two networkbehaviour components
806806
// spawned, otherwise command handler won't find it in .spawned.
807807
// WITH OWNER = WITH AUTHORITY
808-
CreateNetworkedAndSpawn(out GameObject _, out NetworkIdentity identity, out CommandTestNetworkBehaviour comp, NetworkServer.localConnection);
808+
CreateNetworkedAndSpawn(out _, out NetworkIdentity serverIdentity, out CommandTestNetworkBehaviour serverComponent,
809+
out _, out NetworkIdentity clientIdentity, out CommandTestNetworkBehaviour clientComponent,
810+
NetworkServer.localConnection);
809811

810812
// change identity's owner connection so we can't call [Commands] on it
811-
identity.connectionToClient = new LocalConnectionToClient();
813+
serverIdentity.connectionToClient = new LocalConnectionToClient();
812814

813815
// call the command
814-
comp.TestCommand();
816+
clientComponent.TestCommand();
815817
ProcessMessages();
816-
Assert.That(comp.called, Is.EqualTo(0));
818+
Assert.That(serverComponent.called, Is.EqualTo(0));
817819
}
818820

819821
[Test]
@@ -826,13 +828,13 @@ public void SendCommand_RequiresAuthority()
826828
// add an identity with two networkbehaviour components
827829
// spawned, otherwise command handler won't find it in .spawned.
828830
// WITHOUT OWNER = WITHOUT AUTHORITY for this test
829-
CreateNetworkedAndSpawn(out _, out _, out CommandTestNetworkBehaviour comp,
830-
out _, out _, out _);
831+
CreateNetworkedAndSpawn(out _, out _, out CommandTestNetworkBehaviour serverComponent,
832+
out _, out _, out CommandTestNetworkBehaviour clientComponent);
831833

832834
// call the command
833-
comp.TestCommand();
835+
clientComponent.TestCommand();
834836
ProcessMessages();
835-
Assert.That(comp.called, Is.EqualTo(0));
837+
Assert.That(serverComponent.called, Is.EqualTo(0));
836838
}
837839

838840
[Test]

Assets/Mirror/Tests/Editor/Rpcs/CommandTest.cs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -277,5 +277,25 @@ public void Command_RequiresAuthorityFalse_ForOtherObjectWithoutConnectionToServ
277277
ProcessMessages();
278278
Assert.That(called, Is.EqualTo(1));
279279
}
280+
281+
// cmds must be invoked immediately on host, without processing.
282+
// (see Weaver CommandProcessor comments)
283+
[Test]
284+
public void Command_IsInvokeImmediately()
285+
{
286+
// spawn without owner (= without connectionToClient)
287+
CreateNetworkedAndSpawn(out _, out _, out IgnoreAuthorityBehaviour comp);
288+
289+
// setup callback
290+
int called = 0;
291+
comp.onSendInt += _ => { ++called; };
292+
293+
// call command. don't require authority.
294+
// the object doesn't have a .connectionToServer (like a scene object)
295+
Assert.That(comp.connectionToServer, Is.Null);
296+
comp.CmdSendInt(0);
297+
// ProcessMessages(); <- should be invoked IMMEDIATELY without processing
298+
Assert.That(called, Is.EqualTo(1));
299+
}
280300
}
281301
}

0 commit comments

Comments
 (0)